From ddbe8fcab8ce10ce559e110ecce2b0afbd88e00b Mon Sep 17 00:00:00 2001 From: edgarssskore Date: Wed, 13 May 2026 17:05:03 +0300 Subject: [PATCH] fix: improve file preview fallback handling --- src/handlers/filesystem-handlers.ts | 6 +++ src/types.ts | 1 + src/ui/file-preview/src/app.ts | 9 ----- src/ui/file-preview/src/file-type-handlers.ts | 29 ++++++++++----- src/ui/file-preview/src/payload-utils.ts | 12 +++++- test/test-file-handlers.js | 4 ++ test/test-markdown-preview.js | 37 +++++++++++++++++++ 7 files changed, 79 insertions(+), 19 deletions(-) diff --git a/src/handlers/filesystem-handlers.ts b/src/handlers/filesystem-handlers.ts index ce002d97..83ce9aad 100644 --- a/src/handlers/filesystem-handlers.ts +++ b/src/handlers/filesystem-handlers.ts @@ -137,6 +137,10 @@ export async function handleReadFile(args: unknown): Promise { fileName: path.basename(resolvedFilePath), filePath: resolvedFilePath, fileType: 'unsupported' as const, + content: pdfContent + .filter((item): item is { type: "text"; text: string } => item.type === "text") + .map((item) => item.text) + .join("\n"), }, }; } @@ -160,6 +164,7 @@ export async function handleReadFile(args: unknown): Promise { fileName: path.basename(resolvedFilePath), filePath: resolvedFilePath, fileType: 'image', + content: imageData, imageData, mimeType: fileResult.mimeType } @@ -178,6 +183,7 @@ export async function handleReadFile(args: unknown): Promise { fileName: path.basename(resolvedFilePath), filePath: resolvedFilePath, fileType, + content: textContent, }, }; } diff --git a/src/types.ts b/src/types.ts index 06de3067..6d5bf5cd 100644 --- a/src/types.ts +++ b/src/types.ts @@ -76,6 +76,7 @@ export interface FilePreviewStructuredContent { fileName: string; filePath: string; fileType: PreviewFileType; + content?: string; imageData?: string; mimeType?: string; } diff --git a/src/ui/file-preview/src/app.ts b/src/ui/file-preview/src/app.ts index 825f68c0..89b98402 100644 --- a/src/ui/file-preview/src/app.ts +++ b/src/ui/file-preview/src/app.ts @@ -626,15 +626,6 @@ export function bootstrapApp(): void { onConnected: () => { currentHostContext = app.getHostContext() as Record | undefined; pendingCachedPayload = widgetState.read() ?? undefined; - - window.setTimeout(() => { - if (!initialStateResolved) { - resolveInitialState( - undefined, - 'Preview unavailable after page refresh. Switch threads or re-run the tool.' - ); - } - }, 8000); }, }).catch(() => { renderStatusState(container, 'Failed to connect to host.'); diff --git a/src/ui/file-preview/src/file-type-handlers.ts b/src/ui/file-preview/src/file-type-handlers.ts index 887c134f..4b8625f0 100644 --- a/src/ui/file-preview/src/file-type-handlers.ts +++ b/src/ui/file-preview/src/file-type-handlers.ts @@ -92,15 +92,26 @@ const handlerRegistry: Partial ({ - supportsPreview: false, - canCopy: false, - canOpenInFolder: true, - }), - renderBody: () => ({ - notice: 'Preview is not available for this file type.', - html: '
', - }), + getCapabilities: (payload) => { + const hasRawContent = stripReadStatusLine(payload.content).trim().length > 0; + return { + supportsPreview: hasRawContent, + canCopy: hasRawContent, + canOpenInFolder: !isLikelyUrl(payload.filePath), + }; + }, + renderBody: ({ payload }) => { + const rawContent = stripReadStatusLine(payload.content); + if (rawContent.trim().length === 0) { + return { + notice: 'Preview is not available for this file type.', + html: '
', + }; + } + return { + html: `
${renderRawFallback(rawContent)}
`, + }; + }, }, }; diff --git a/src/ui/file-preview/src/payload-utils.ts b/src/ui/file-preview/src/payload-utils.ts index f2dc2f1b..671478ec 100644 --- a/src/ui/file-preview/src/payload-utils.ts +++ b/src/ui/file-preview/src/payload-utils.ts @@ -55,6 +55,13 @@ export function extractToolText(value: unknown): string | undefined { return undefined; } +function extractStructuredContentText(value: unknown): string | undefined { + if (!isObjectRecord(value)) { + return undefined; + } + return typeof value.content === 'string' ? value.content : undefined; +} + export function extractRenderPayload(value: unknown): RenderPayload | undefined { if (!isObjectRecord(value)) { return undefined; @@ -65,7 +72,10 @@ export function extractRenderPayload(value: unknown): RenderPayload | undefined ? value : null; if (!meta) return undefined; - const text = extractToolText(value) ?? extractToolText(value.structuredContent) ?? ''; + const text = extractStructuredContentText(value.structuredContent) + ?? extractToolText(value) + ?? extractToolText(value.structuredContent) + ?? ''; return buildRenderPayload(meta, text); } diff --git a/test/test-file-handlers.js b/test/test-file-handlers.js index b1458dee..44070acc 100644 --- a/test/test-file-handlers.js +++ b/test/test-file-handlers.js @@ -306,18 +306,21 @@ async function testReadFilePreviewMetadata() { assert.ok(markdownResult.structuredContent, 'Markdown should include structuredContent'); assert.strictEqual(markdownResult.structuredContent.fileType, 'markdown', 'Markdown fileType should be markdown'); assert.strictEqual(markdownResult.structuredContent.filePath, MD_FILE, 'Markdown file path should be present'); + assert.strictEqual(markdownResult.structuredContent.content, markdownResult.content[0].text, 'Markdown structuredContent should include returned content'); const textResult = await handleReadFile({ path: TEXT_FILE }); assert.ok(Array.isArray(textResult.content), 'Result should include content array'); assert.ok(textResult.content[0].text.includes(textContent), 'Legacy content should still include text body'); assert.ok(textResult.structuredContent, 'Text should include structuredContent'); assert.strictEqual(textResult.structuredContent.fileType, 'text', 'Text fileType should be text'); + assert.strictEqual(textResult.structuredContent.content, textResult.content[0].text, 'Text structuredContent should include returned content'); const htmlResult = await handleReadFile({ path: HTML_FILE }); assert.ok(Array.isArray(htmlResult.content), 'Result should include content array'); assert.ok(htmlResult.content[0].text.includes('

Preview

'), 'Legacy content should still include html body'); assert.ok(htmlResult.structuredContent, 'HTML should include structuredContent'); assert.strictEqual(htmlResult.structuredContent.fileType, 'html', 'HTML fileType should be html'); + assert.strictEqual(htmlResult.structuredContent.content, htmlResult.content[0].text, 'HTML structuredContent should include returned content'); const imageResult = await handleReadFile({ path: IMAGE_FILE }); assert.ok(Array.isArray(imageResult.content), 'Image result should include content array'); @@ -326,6 +329,7 @@ async function testReadFilePreviewMetadata() { assert.strictEqual(imageResult.structuredContent.fileType, 'image', 'Image fileType should map to image preview state'); assert.strictEqual(typeof imageResult.structuredContent.imageData, 'string', 'Image structured payload should include imageData'); assert.ok(imageResult.structuredContent.imageData.length > 0, 'Image structured payload should include non-empty imageData'); + assert.strictEqual(imageResult.structuredContent.content, imageResult.structuredContent.imageData, 'Image structuredContent should include file content'); assert.strictEqual(imageResult.structuredContent.mimeType, 'image/png', 'Image structured payload should include mimeType'); assert.strictEqual(imageResult.structuredContent.filePath, IMAGE_FILE, 'Image file path should be present'); diff --git a/test/test-markdown-preview.js b/test/test-markdown-preview.js index 5f48ebb0..e11e914e 100644 --- a/test/test-markdown-preview.js +++ b/test/test-markdown-preview.js @@ -9,6 +9,8 @@ import { renderMarkdownEditorShell } from '../dist/ui/file-preview/src/markdown/ import { createMarkdownController } from '../dist/ui/file-preview/src/markdown/controller.js'; import { createSlugTracker, slugifyMarkdownHeading } from '../dist/ui/file-preview/src/markdown/slugify.js'; import { getDocumentFullscreenAvailability, shouldAutoLoadDocumentOnEnterFullscreen } from '../dist/ui/file-preview/src/document-workspace.js'; +import { renderPayloadBody, getFileTypeCapabilities } from '../dist/ui/file-preview/src/file-type-handlers.js'; +import { extractRenderPayload } from '../dist/ui/file-preview/src/payload-utils.js'; async function testSlugGeneration() { console.log('\n--- Test 1: heading slug generation ---'); @@ -520,6 +522,40 @@ async function testRefreshDoesNotMisclassifyMarkdownContentAsDeletion() { console.log('āœ“ refresh only treats actual tool errors as missing files'); } +async function testUnsupportedRawContentPreview() { + console.log('\n--- Test 11: unsupported files render raw structured content ---'); + + const payload = extractRenderPayload({ + content: [{ type: 'text', text: 'PDF file: report.pdf (1 pages)\n' }], + structuredContent: { + fileName: 'report.pdf', + filePath: '/tmp/report.pdf', + fileType: 'unsupported', + content: '\nRaw PDF text', + }, + }); + + assert.ok(payload, 'Unsupported payload should be extracted'); + assert.strictEqual(payload.content, '\nRaw PDF text', 'Structured content text should be used as raw source'); + + const capabilities = getFileTypeCapabilities(payload); + assert.strictEqual(capabilities.supportsPreview, true, 'Unsupported payload with raw content should be displayable'); + assert.strictEqual(capabilities.canCopy, true, 'Unsupported raw source should be copyable'); + + const body = renderPayloadBody({ + payload, + htmlMode: 'rendered', + startLine: 1, + markdownController: {}, + }); + + assert.strictEqual(body.notice, undefined, 'Raw source display should not show unavailable notice'); + assert.ok(body.html.includes('Raw PDF text'), 'Raw content should be rendered'); + assert.ok(body.html.includes('<!-- Page: 1 -->'), 'Raw content should be escaped'); + + console.log('āœ“ unsupported raw structured content renders as source'); +} + export default async function runTests() { try { await testSlugGeneration(); @@ -530,6 +566,7 @@ export default async function runTests() { await testCopyFormatsAndEditorShell(); await testPartialDocumentBecomesNewEditBaseline(); await testRefreshDoesNotMisclassifyMarkdownContentAsDeletion(); + await testUnsupportedRawContentPreview(); await testFailedSaveResyncsEditBaseline(); await testSuccessfulSaveResetsUndoBaseline(); console.log('\nāœ… Markdown preview tests passed!');