Skip to content

feat(web): render HTML and PDF files in the file viewer - #9143

Merged
juliusmarminge merged 2 commits into
t3code/open-outside-workspace-filesfrom
t3code/html-preview-in-panel
Sep 2, 2026
Merged

feat(web): render HTML and PDF files in the file viewer#9143
juliusmarminge merged 2 commits into
t3code/open-outside-workspace-filesfrom
t3code/html-preview-in-panel

Conversation

@juliusmarminge

@juliusmarminge juliusmarminge commented Sep 2, 2026

Copy link
Copy Markdown
Member

Stacked on #9140.

On web there was no way to see an HTML file as a page: the integrated browser is desktop-only, so the file panel always showed markup. Mobile already renders browser files in a WebView; this brings web (and the desktop panel) in line.

How

FilePreviewPanel gains a WorkspaceBrowserPreview that renders HTML and PDF from the signed asset URL in an <iframe>:

  • HTML defaults to the rendered page with a source toggle in the panel header, using the same control as the rendered-Markdown toggle (persisted separately, and a line reveal still forces source). PDF is always rendered and skips the text read, like images do.
  • HTML frames use sandbox="allow-scripts allow-forms allow-popups allow-modals" (no allow-same-origin), so the page gets an opaque origin and cannot reach the app's storage or bearer session. feat(files): open markdown, HTML, and PDF files outside the workspace #9140 now also sends the same policy as a Content-Security-Policy: sandbox response header, so the frame attribute is defence in depth rather than the only barrier. PDFs load unsandboxed because the built-in viewer needs it and a PDF runs no script.
  • Files inside the workspace use the directory-scoped workspace-file URL so sibling CSS/images load; host files outside it use the exact-file media-file URL from feat(files): open markdown, HTML, and PDF files outside the workspace #9140.
  • Workspace mutations bump the frame's workspace-revision query, matching the image preview.

Demo

A /tmp HTML chip opening as a rendered page, toggling to source and back, then a workspace report.html from the explorer that pulls in its sibling report.css and badge.svg:

https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/c36b5d260d5fa6ec/html-preview-in-panel.webm

Verification

  • Web typecheck and lint on the changed file; flow exercised end to end in the recording above (headless Chromium against vp run dev).
  • Not verified: PDF rendering. Headless Chromium has no PDF viewer, so the PDF branch needs a look in a real browser.

Written by Claude Fable 5 in Claude Code.

🤖 Generated with Claude Code


Note

Medium Risk
HTML previews run user-controlled scripts inside a sandboxed iframe (no app session access), but popups/modals are allowed; PDFs use an unsandboxed iframe by design.

Overview
The file viewer can show HTML and PDF as rendered pages on web and desktop, not only raw markup—closing the gap where the integrated browser was desktop-only.

FilePreviewPanel adds WorkspaceBrowserPreview, which loads signed asset URLs in an iframe and appends workspace-revision on workspace mutations (same pattern as images). Workspace HTML uses workspace-file URLs so sibling assets resolve; host paths outside the workspace use media-file. HTML defaults to rendered view with a persisted header toggle (shared UX with rendered Markdown); line-reveal links still force source. PDFs are always rendered and skip the text file query, truncation banner, and workspace text refresh—like other binary previews.

HTML iframes use sandbox="allow-scripts allow-forms allow-popups allow-modals" (no same-origin); PDFs load without sandbox for the built-in viewer. User docs in composer.md describe the toggle, isolation, and desktop “open in browser” behavior.

Reviewed by Cursor Bugbot for commit 4f990b7. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Add HTML and PDF rendering to web file viewer

  • Renders HTML and PDF files in-place using WorkspaceBrowserPreview in FilePreviewPanel.tsx.
  • HTML files load in a sandboxed iframe and can toggle between source and rendered page using a persisted local-storage preference. PDFs load in an unsandboxed iframe and always show the rendered page without a source toggle.
  • Line links force HTML and Markdown files into source mode until the reveal is handled. Truncation warnings are hidden for browser-rendered content.
  • Risk: WorkspaceBrowserPreview renders PDFs in an unsandboxed iframe, exposing the viewer to potential untrusted PDF execution.

Macroscope summarized 4f990b7.

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Sep 2, 2026
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ℹ️ No successful main baseline artifact is available yet. This run establishes the initial measurement.

Provider Metric Main baseline This PR Impact PR ceiling
Codex Total thread wire 13.4 KiB 15.1 KiB
Codex Thread snapshot wire 6.9 KiB 7.3 KiB
Codex Live turn WebSocket wire 6.5 KiB 7.8 KiB
Codex Live turn WebSocket decoded 57.0 KiB 66.4 KiB
Codex Live turn messages 10 21
Claude Total thread wire 13.5 KiB 15.1 KiB
Claude Thread snapshot wire 6.9 KiB 7.3 KiB
Claude Live turn WebSocket wire 6.6 KiB 7.8 KiB
Claude Live turn WebSocket decoded 57.8 KiB 66.4 KiB
Claude Live turn messages 10 21

Baseline: unavailable · PR result: 4f990b7 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 109.4 KiB
  • Claude decoded thread snapshot: 110.1 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Critical files/FilePreviewPanel.tsx:258

Untrusted workspace HTML can execute JavaScript and exfiltrate its location.href, which contains the bearer-signed workspace-file URL; anyone receiving that URL can fetch the selected file and permitted sibling assets until expiry. The opaque sandbox origin does not hide the frame URL, so remove allow-scripts or otherwise avoid exposing the bearer credential to executable page content.

Suggested change
sandbox="allow-scripts allow-forms allow-popups allow-modals"
sandbox="allow-forms allow-popups allow-modals"
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/components/files/FilePreviewPanel.tsx around line 258:

Untrusted workspace HTML can execute JavaScript and exfiltrate its `location.href`, which contains the bearer-signed `workspace-file` URL; anyone receiving that URL can fetch the selected file and permitted sibling assets until expiry. The opaque sandbox origin does not hide the frame URL, so remove `allow-scripts` or otherwise avoid exposing the bearer credential to executable page content.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Considered and accepted. The desktop integrated browser and the mobile WebView already load this exact signed URL with scripts enabled, so this is not a new exposure, and docs/internals/environment-auth.md already treats signed asset URLs as bearer capabilities. What the page can exfiltrate is a one-hour, single-file (or, inside the workspace, preview-extension-only sibling) read of content the author already controls, to an attacker who can also reach the environment. Removing allow-scripts would break most agent-generated reports (charts, tabs), which is the point of the feature. The frame has an opaque origin, so it cannot reach the app's session or storage. Dismissing.

@juliusmarminge
juliusmarminge force-pushed the t3code/html-preview-in-panel branch from 9549690 to 4028a2d Compare September 2, 2026 00:47
Comment thread apps/web/src/components/files/FilePreviewPanel.tsx Outdated
Web had no way to see an HTML file as a page: the integrated browser is
desktop-only, so the file panel always showed markup. Mobile already renders
browser files in a WebView.

The panel now shows HTML and PDF as a rendered page from the signed asset
URL, with a source toggle for HTML that works like the Markdown one. HTML
runs in a sandboxed frame with an opaque origin so a page cannot reach the
app's session; workspace files keep loading sibling assets, host files are
served on their own.

Co-Authored-By: Claude Code <noreply@anthropic.com>
@juliusmarminge
juliusmarminge force-pushed the t3code/html-preview-in-panel branch from 4028a2d to 779ad56 Compare September 2, 2026 00:57
Co-Authored-By: Claude Code <noreply@anthropic.com>
Comment thread apps/web/src/components/files/FilePreviewPanel.tsx

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Fix All in Cursor

Bugbot Autofix is ON, but a cloud agent failed to start.

Reviewed by Cursor Bugbot for commit 4f990b7. Configure here.

sandbox="allow-scripts allow-forms allow-popups allow-modals"
/>
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rendered HTML ignores in-panel edits

Medium Severity

Toggling an HTML file from source back to the rendered page loads the signed asset URL in an iframe, not the in-panel contents. Asset responses are cached for an hour, and the frame only cache-busts when workspaceMutationId changes, so a source edit can still show the old page after the toggle.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 4f990b7. Configure here.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same mechanism the image preview relies on: an in-panel save goes through projects.writeFile, the workspace watcher bumps workspaceMutationId, and the frame is keyed on the URL plus that revision, so it remounts with a fresh query string and misses the cache. There is no window where the toggle can show a stale page without the watcher having also missed the write. Dismissing.

const revisionSuffix =
props.workspaceMutationId === null
? ""
: `${assetUrl._tag === "Success" && assetUrl.url.includes("?") ? "&" : "?"}workspace-revision=${encodeURIComponent(props.workspaceMutationId)}`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Host previews skip exact-file remint

Medium Severity

Host HTML and PDF previews mint a media-file URL, which is bound to device and inode, but only append workspace-revision on change. Video previews remint that exact-file capability. After an outside-workspace file is replaced, the iframe can keep a dead or cached URL instead of a new capability.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 4f990b7. Configure here.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Documented media-file semantics (environment-auth.md: replacing a file atomically requires a freshly signed URL). Nothing watches files outside the workspace, so there is no mutation signal to remint on; WorkspaceVideoPreview has the same limitation for host paths since its refresh also keys off workspaceMutationId. Closing and reopening the tab remints. A refresh affordance for host files is a reasonable follow-up but out of scope here. Dismissing.

@macroscopeapp

macroscopeapp Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — Adds a new default-on production path for rendering HTML and PDF files through signed asset URLs, including executable HTML frames and a line-level sandbox lint suppression. The unresolved security and preview-freshness concerns make the runtime impact significant enough for human review.

Not approved because:

  • 1 blocking correctness issue found at or above your repo's Minimum Blocking Severity

Adjust the Minimum Blocking Severity for this repo — including turning it Off — in Settings. You can add or adjust custom eligibility rules. Learn more.

@juliusmarminge
juliusmarminge merged commit d937e30 into main Sep 2, 2026
27 checks passed
@juliusmarminge
juliusmarminge deleted the t3code/html-preview-in-panel branch September 2, 2026 02:32
github-actions Bot added a commit to omarcresp/t3code-flake that referenced this pull request Sep 2, 2026
## What's Changed
* perf(client-runtime): keep turn and checkpoint refs stable while streaming by @t3dotgg in pingdotgg/t3code#9145
* perf(clients): lease sidebar status by visibility by @StiensWout in pingdotgg/t3code#9052
* fix(desktop): show newest changes in nightly previews by @t3dotgg in pingdotgg/t3code#9138
* fix(settings): sync auto-settle and other shared preferences across environments by @t3dotgg in pingdotgg/t3code#9147
* fix(server): prevent accidental service downgrades by @t3dotgg in pingdotgg/t3code#5302
* fix(server): keep attachments until the command commits by @t3dotgg in pingdotgg/t3code#7941
* fix(claude): preview images read from the workspace by @t3dotgg in pingdotgg/t3code#9119
* fix(web): keep generated muted foreground dimmer than entered text by @flamboh in pingdotgg/t3code#9113
* fix(clients): stop repeating expanded commands by @t3dotgg in pingdotgg/t3code#9120
* fix(grok): health check, model selection, and stop all work against the real CLI by @t3dotgg in pingdotgg/t3code#9154
* perf(web): halve the cold-start bundle by splitting Clerk and cold routes by @StiensWout in pingdotgg/t3code#9058
* feat(desktop): update the desktop app on remote Macs from the Update button by @t3dotgg in pingdotgg/t3code#6554
* test(server): measure shell, second client, and reconnect transfer by @t3dotgg in pingdotgg/t3code#9157
* fix(web): project default model works on the hosted app by @juliusmarminge in pingdotgg/t3code#9142
* fix(web): darken neutral control surfaces by @maria-rcks in pingdotgg/t3code#9064
* fix(web): preserve panel state across workspace refreshes by @maria-rcks in pingdotgg/t3code#8968
* feat(files): open markdown, HTML, and PDF files outside the workspace by @juliusmarminge in pingdotgg/t3code#9140
* feat(web): render HTML and PDF files in the file viewer by @juliusmarminge in pingdotgg/t3code#9143
* fix(web): compact project settings actions by @maria-rcks in pingdotgg/t3code#9160
* fix(web): browse folders from file breadcrumbs by @404khai in pingdotgg/t3code#8910

## New Contributors
* @404khai made their first contribution in pingdotgg/t3code#8910

**Full Changelog**: pingdotgg/t3code@v0.0.39-nightly.20260902.1252...v0.0.39-nightly.20260902.1253

Upstream release: https://github.com/pingdotgg/t3code/releases/tag/v0.0.39-nightly.20260902.1253
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L 100-499 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant