feat(web): expand ContentViewer to render PDF, CSV, HTML, XML, JSON, CSS (closes #1329)#1551
Conversation
…CSS (closes #1329) Turn ContentViewer into a per-MIME dispatch surface for resource content. It now accepts raw TextResourceContents | BlobResourceContents (in addition to the existing ContentBlock path), so ResourcePreviewPanel.toContentBlock collapses to a passthrough and blob-typed resources render natively instead of falling back to opaque "[Binary content …]" text. New renderers (clients/web/src/components/elements/): - CodeHighlight: lazy react-syntax-highlighter prism-light element. Runtime, tomorrow theme, and each grammar dynamic-import on first use (one chunk per grammar). Module-level registeredLanguages / failedLoads / loadingPromises caches; plain Code block until a grammar resolves. - PdfFrame: iframe over a Blob URL (#view=FitH), revoked on unmount. - CsvTable: papaparse -> Mantine Table (first 100 rows), plain-Code fallback. - HtmlFrame: sandbox="" iframe over a Blob URL with an injected CSP meta (no script-src, falls through to default-src 'none'). - contentViewerUtils: getMimeKind, base64 decode helpers, formatXml, wrapHtmlWithCsp, SAFE_HREF markdown-anchor allowlist (unsafe href -> span). ResourcePreviewPanel: extend the URI-suffix MIME inference table (.csv/.json/.xml/.html/.htm/.css/.pdf alongside .md). Deps: papaparse + react-syntax-highlighter (+ @types). tsconfig paths stubs redirect the deep RSH imports for typecheck (the @types ambient declarations are shadowed by the real .js under bundler resolution) — same pattern the repo uses for react/pino. Tests cover PDF blob-URL creation + cleanup, CSV table render + fallback, HTML sandbox attributes + CSP injection, JSON/XML/CSS highlighted output, the lazy-load/alias/failed-load/shared-runtime paths, and the URI-suffix MIME-inference paths. validate, test:coverage (per-file gate), test:storybook, and integration all pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
@claude review |
|
Claude finished @cliffhall's task in 2m 33s —— View job Code review — ContentViewer MIME dispatchRead through the new dispatch surface, the lazy highlighter, the three blob frames, and the panel wiring. This is a clean, well-factored change — pure helpers split out for isolated testing, layered HTML hardening, a genuinely lazy highlighter, and thorough tests/stories. A few things worth addressing, one of which I think is a real (dev-only) bug. 🔴
|
Address review feedback on the ContentViewer MIME-dispatch PR: - useObjectUrl: fix a dev-only StrictMode bug where the committed object URL was revoked but never recreated (setup→cleanup→setup with no re-render), blanking PDF/HTML previews. Revocation is now deferred to a microtask and guarded by a "live URL" ref so a StrictMode re-setup cancels it, while a real unmount / blob change still releases the URL. Avoids set-state-in-effect; mirrors AppRenderer's deferred-disposal. - atob: guard base64 decoding with tryDecodeBase64ToUtf8 / tryDecodeBase64ToBytes so a malformed blob from an external server degrades to the binary-content notice instead of throwing during render and taking down the whole preview panel. - CsvTable: surface the 100-row display cap with a "Showing first N of M rows" caption (no silent truncation). - Extract BinaryNotice into its own module so PdfFrame can reuse it without an import cycle. Tests added for the StrictMode path, malformed-base64 fallbacks (PDF + textual blob), the safe-decode helpers, and the truncation caption. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thanks for the thorough review — pushed 🔴 🟡 Unguarded 🟡 CSV silent truncation — Fixed. 🟢 Minor:
Also extracted @claude review |
|
Claude finished @cliffhall's task in 1m 17s —— View job Re-review — ContentViewer fixes (
|
Closes #1329.
Turns
ContentViewerinto a per-MIME dispatch surface for resource content. It now accepts rawTextResourceContents | BlobResourceContents(in addition to the existingContentBlockpath), soResourcePreviewPanel.toContentBlockcollapses to a passthrough and blob-typed resources render natively instead of falling back to opaque[Binary content …]text.Type matrix
application/pdf<iframe>over aBlobURL (#view=FitH), revoked on unmounttext/csv(or.csv)Table(first 100 rows), plain-Codefallbackapplication/json(or.json)text/xml,application/xml(or.xml)text/html(or.html/.htm)sandbox=""iframe over aBlobURL + injected CSPtext/css(or.css)text/markdown(or.md)hrefanchor allowlistCross-cutting
elements/CodeHighlight): the prism-light runtime,tomorrowtheme, and each grammar dynamic-import on first use — one emitted chunk per grammar (prism-light,json,markup,css,tomorrow). Module-levelregisteredLanguages/failedLoads/loadingPromisescaches; renders plainCodeuntil a grammar resolves.wrapHtmlWithCspinjectsdefault-src 'none'; …with noscript-src(so it stays load-bearing even ifsandboxis loosened); served from aBlobURL, revoked on unmount.SAFE_HREFallowlist (^(https?:|mailto:|#|/(?!/))); non-matching anchors render as inert<span>.ResourcePreviewPanelextends its URI-suffix table to cover the new renderers.Dependencies
papaparse+react-syntax-highlighter(+@types/*), both named in the issue. Added tsconfigpathsstubs undersrc/types/because@types/react-syntax-highlighter's ambient deep-module declarations are shadowed by the real.jsunder bundler resolution — the same redirect pattern the repo already uses forreact/pino.Notes / deviations
Screenshots
Smoke-tested against a stdio MCP server exposing one resource per MIME type. Every renderer below was verified live in the web client. Drag each
NN-*.pnginto its placeholder to embed it.PDF —
application/pdfCSV —
text/csvJSON —
application/jsonXML —
application/xmlHTML (sandboxed iframe) —
text/htmlCSS —
text/cssMarkdown —
text/markdownPNG —
image/pngJPEG —
image/jpegWEBP —
image/webpPlain text —
text/plainTesting
npm run validate(all 4 clients),npm run test:coverage(per-file gate — every new file ≥90 on all four dimensions),npm run test:storybook, and the integration suite all pass. New tests cover PDF blob-URL creation + cleanup, CSV table render + fallback, HTML sandbox attributes + CSP injection, JSON/XML/CSS highlighted output, the lazy-load / alias / failed-load / shared-runtime paths, and the URI-suffix MIME-inference paths.🤖 Generated with Claude Code