Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/handlers/filesystem-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,10 @@ export async function handleReadFile(args: unknown): Promise<ServerResult> {
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"),
},
};
}
Expand All @@ -160,6 +164,7 @@ export async function handleReadFile(args: unknown): Promise<ServerResult> {
fileName: path.basename(resolvedFilePath),
filePath: resolvedFilePath,
fileType: 'image',
content: imageData,
imageData,
mimeType: fileResult.mimeType
}
Expand All @@ -178,6 +183,7 @@ export async function handleReadFile(args: unknown): Promise<ServerResult> {
fileName: path.basename(resolvedFilePath),
filePath: resolvedFilePath,
fileType,
content: textContent,
},
};
}
Expand Down
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export interface FilePreviewStructuredContent {
fileName: string;
filePath: string;
fileType: PreviewFileType;
content?: string;
imageData?: string;
mimeType?: string;
}
Expand Down
9 changes: 0 additions & 9 deletions src/ui/file-preview/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -626,15 +626,6 @@ export function bootstrapApp(): void {
onConnected: () => {
currentHostContext = app.getHostContext() as Record<string, unknown> | 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.');
Expand Down
29 changes: 20 additions & 9 deletions src/ui/file-preview/src/file-type-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,15 +92,26 @@ const handlerRegistry: Partial<Record<RenderPayload['fileType'], FileTypeHandler
},
},
unsupported: {
getCapabilities: () => ({
supportsPreview: false,
canCopy: false,
canOpenInFolder: true,
}),
renderBody: () => ({
notice: 'Preview is not available for this file type.',
html: '<div class="panel-content source-content"></div>',
}),
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: '<div class="panel-content source-content"></div>',
};
}
return {
html: `<div class="panel-content source-content">${renderRawFallback(rawContent)}</div>`,
};
},
},
};

Expand Down
12 changes: 11 additions & 1 deletion src/ui/file-preview/src/payload-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
}

Expand Down
4 changes: 4 additions & 0 deletions test/test-file-handlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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('<h1>Preview</h1>'), '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');
Expand All @@ -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');

Expand Down
37 changes: 37 additions & 0 deletions test/test-markdown-preview.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 ---');
Expand Down Expand Up @@ -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: '<!-- Page: 1 -->\nRaw PDF text',
},
});

assert.ok(payload, 'Unsupported payload should be extracted');
assert.strictEqual(payload.content, '<!-- Page: 1 -->\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('&lt;!-- Page: 1 --&gt;'), 'Raw content should be escaped');

console.log('✓ unsupported raw structured content renders as source');
}

export default async function runTests() {
try {
await testSlugGeneration();
Expand All @@ -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!');
Expand Down
Loading