Skip to content

Commit cf277c7

Browse files
authored
fix: improve file preview fallback handling (#472)
1 parent db783d9 commit cf277c7

7 files changed

Lines changed: 79 additions & 19 deletions

File tree

src/handlers/filesystem-handlers.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,10 @@ export async function handleReadFile(args: unknown): Promise<ServerResult> {
137137
fileName: path.basename(resolvedFilePath),
138138
filePath: resolvedFilePath,
139139
fileType: 'unsupported' as const,
140+
content: pdfContent
141+
.filter((item): item is { type: "text"; text: string } => item.type === "text")
142+
.map((item) => item.text)
143+
.join("\n"),
140144
},
141145
};
142146
}
@@ -160,6 +164,7 @@ export async function handleReadFile(args: unknown): Promise<ServerResult> {
160164
fileName: path.basename(resolvedFilePath),
161165
filePath: resolvedFilePath,
162166
fileType: 'image',
167+
content: imageData,
163168
imageData,
164169
mimeType: fileResult.mimeType
165170
}
@@ -178,6 +183,7 @@ export async function handleReadFile(args: unknown): Promise<ServerResult> {
178183
fileName: path.basename(resolvedFilePath),
179184
filePath: resolvedFilePath,
180185
fileType,
186+
content: textContent,
181187
},
182188
};
183189
}

src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ export interface FilePreviewStructuredContent {
7676
fileName: string;
7777
filePath: string;
7878
fileType: PreviewFileType;
79+
content?: string;
7980
imageData?: string;
8081
mimeType?: string;
8182
}

src/ui/file-preview/src/app.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -626,15 +626,6 @@ export function bootstrapApp(): void {
626626
onConnected: () => {
627627
currentHostContext = app.getHostContext() as Record<string, unknown> | undefined;
628628
pendingCachedPayload = widgetState.read() ?? undefined;
629-
630-
window.setTimeout(() => {
631-
if (!initialStateResolved) {
632-
resolveInitialState(
633-
undefined,
634-
'Preview unavailable after page refresh. Switch threads or re-run the tool.'
635-
);
636-
}
637-
}, 8000);
638629
},
639630
}).catch(() => {
640631
renderStatusState(container, 'Failed to connect to host.');

src/ui/file-preview/src/file-type-handlers.ts

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -92,15 +92,26 @@ const handlerRegistry: Partial<Record<RenderPayload['fileType'], FileTypeHandler
9292
},
9393
},
9494
unsupported: {
95-
getCapabilities: () => ({
96-
supportsPreview: false,
97-
canCopy: false,
98-
canOpenInFolder: true,
99-
}),
100-
renderBody: () => ({
101-
notice: 'Preview is not available for this file type.',
102-
html: '<div class="panel-content source-content"></div>',
103-
}),
95+
getCapabilities: (payload) => {
96+
const hasRawContent = stripReadStatusLine(payload.content).trim().length > 0;
97+
return {
98+
supportsPreview: hasRawContent,
99+
canCopy: hasRawContent,
100+
canOpenInFolder: !isLikelyUrl(payload.filePath),
101+
};
102+
},
103+
renderBody: ({ payload }) => {
104+
const rawContent = stripReadStatusLine(payload.content);
105+
if (rawContent.trim().length === 0) {
106+
return {
107+
notice: 'Preview is not available for this file type.',
108+
html: '<div class="panel-content source-content"></div>',
109+
};
110+
}
111+
return {
112+
html: `<div class="panel-content source-content">${renderRawFallback(rawContent)}</div>`,
113+
};
114+
},
104115
},
105116
};
106117

src/ui/file-preview/src/payload-utils.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,13 @@ export function extractToolText(value: unknown): string | undefined {
5555
return undefined;
5656
}
5757

58+
function extractStructuredContentText(value: unknown): string | undefined {
59+
if (!isObjectRecord(value)) {
60+
return undefined;
61+
}
62+
return typeof value.content === 'string' ? value.content : undefined;
63+
}
64+
5865
export function extractRenderPayload(value: unknown): RenderPayload | undefined {
5966
if (!isObjectRecord(value)) {
6067
return undefined;
@@ -65,7 +72,10 @@ export function extractRenderPayload(value: unknown): RenderPayload | undefined
6572
? value
6673
: null;
6774
if (!meta) return undefined;
68-
const text = extractToolText(value) ?? extractToolText(value.structuredContent) ?? '';
75+
const text = extractStructuredContentText(value.structuredContent)
76+
?? extractToolText(value)
77+
?? extractToolText(value.structuredContent)
78+
?? '';
6979
return buildRenderPayload(meta, text);
7080
}
7181

test/test-file-handlers.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,18 +306,21 @@ async function testReadFilePreviewMetadata() {
306306
assert.ok(markdownResult.structuredContent, 'Markdown should include structuredContent');
307307
assert.strictEqual(markdownResult.structuredContent.fileType, 'markdown', 'Markdown fileType should be markdown');
308308
assert.strictEqual(markdownResult.structuredContent.filePath, MD_FILE, 'Markdown file path should be present');
309+
assert.strictEqual(markdownResult.structuredContent.content, markdownResult.content[0].text, 'Markdown structuredContent should include returned content');
309310

310311
const textResult = await handleReadFile({ path: TEXT_FILE });
311312
assert.ok(Array.isArray(textResult.content), 'Result should include content array');
312313
assert.ok(textResult.content[0].text.includes(textContent), 'Legacy content should still include text body');
313314
assert.ok(textResult.structuredContent, 'Text should include structuredContent');
314315
assert.strictEqual(textResult.structuredContent.fileType, 'text', 'Text fileType should be text');
316+
assert.strictEqual(textResult.structuredContent.content, textResult.content[0].text, 'Text structuredContent should include returned content');
315317

316318
const htmlResult = await handleReadFile({ path: HTML_FILE });
317319
assert.ok(Array.isArray(htmlResult.content), 'Result should include content array');
318320
assert.ok(htmlResult.content[0].text.includes('<h1>Preview</h1>'), 'Legacy content should still include html body');
319321
assert.ok(htmlResult.structuredContent, 'HTML should include structuredContent');
320322
assert.strictEqual(htmlResult.structuredContent.fileType, 'html', 'HTML fileType should be html');
323+
assert.strictEqual(htmlResult.structuredContent.content, htmlResult.content[0].text, 'HTML structuredContent should include returned content');
321324

322325
const imageResult = await handleReadFile({ path: IMAGE_FILE });
323326
assert.ok(Array.isArray(imageResult.content), 'Image result should include content array');
@@ -326,6 +329,7 @@ async function testReadFilePreviewMetadata() {
326329
assert.strictEqual(imageResult.structuredContent.fileType, 'image', 'Image fileType should map to image preview state');
327330
assert.strictEqual(typeof imageResult.structuredContent.imageData, 'string', 'Image structured payload should include imageData');
328331
assert.ok(imageResult.structuredContent.imageData.length > 0, 'Image structured payload should include non-empty imageData');
332+
assert.strictEqual(imageResult.structuredContent.content, imageResult.structuredContent.imageData, 'Image structuredContent should include file content');
329333
assert.strictEqual(imageResult.structuredContent.mimeType, 'image/png', 'Image structured payload should include mimeType');
330334
assert.strictEqual(imageResult.structuredContent.filePath, IMAGE_FILE, 'Image file path should be present');
331335

test/test-markdown-preview.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import { renderMarkdownEditorShell } from '../dist/ui/file-preview/src/markdown/
99
import { createMarkdownController } from '../dist/ui/file-preview/src/markdown/controller.js';
1010
import { createSlugTracker, slugifyMarkdownHeading } from '../dist/ui/file-preview/src/markdown/slugify.js';
1111
import { getDocumentFullscreenAvailability, shouldAutoLoadDocumentOnEnterFullscreen } from '../dist/ui/file-preview/src/document-workspace.js';
12+
import { renderPayloadBody, getFileTypeCapabilities } from '../dist/ui/file-preview/src/file-type-handlers.js';
13+
import { extractRenderPayload } from '../dist/ui/file-preview/src/payload-utils.js';
1214

1315
async function testSlugGeneration() {
1416
console.log('\n--- Test 1: heading slug generation ---');
@@ -520,6 +522,40 @@ async function testRefreshDoesNotMisclassifyMarkdownContentAsDeletion() {
520522
console.log('✓ refresh only treats actual tool errors as missing files');
521523
}
522524

525+
async function testUnsupportedRawContentPreview() {
526+
console.log('\n--- Test 11: unsupported files render raw structured content ---');
527+
528+
const payload = extractRenderPayload({
529+
content: [{ type: 'text', text: 'PDF file: report.pdf (1 pages)\n' }],
530+
structuredContent: {
531+
fileName: 'report.pdf',
532+
filePath: '/tmp/report.pdf',
533+
fileType: 'unsupported',
534+
content: '<!-- Page: 1 -->\nRaw PDF text',
535+
},
536+
});
537+
538+
assert.ok(payload, 'Unsupported payload should be extracted');
539+
assert.strictEqual(payload.content, '<!-- Page: 1 -->\nRaw PDF text', 'Structured content text should be used as raw source');
540+
541+
const capabilities = getFileTypeCapabilities(payload);
542+
assert.strictEqual(capabilities.supportsPreview, true, 'Unsupported payload with raw content should be displayable');
543+
assert.strictEqual(capabilities.canCopy, true, 'Unsupported raw source should be copyable');
544+
545+
const body = renderPayloadBody({
546+
payload,
547+
htmlMode: 'rendered',
548+
startLine: 1,
549+
markdownController: {},
550+
});
551+
552+
assert.strictEqual(body.notice, undefined, 'Raw source display should not show unavailable notice');
553+
assert.ok(body.html.includes('Raw PDF text'), 'Raw content should be rendered');
554+
assert.ok(body.html.includes('&lt;!-- Page: 1 --&gt;'), 'Raw content should be escaped');
555+
556+
console.log('✓ unsupported raw structured content renders as source');
557+
}
558+
523559
export default async function runTests() {
524560
try {
525561
await testSlugGeneration();
@@ -530,6 +566,7 @@ export default async function runTests() {
530566
await testCopyFormatsAndEditorShell();
531567
await testPartialDocumentBecomesNewEditBaseline();
532568
await testRefreshDoesNotMisclassifyMarkdownContentAsDeletion();
569+
await testUnsupportedRawContentPreview();
533570
await testFailedSaveResyncsEditBaseline();
534571
await testSuccessfulSaveResetsUndoBaseline();
535572
console.log('\n✅ Markdown preview tests passed!');

0 commit comments

Comments
 (0)