Skip to content

fix(web): render Codex visualizations inline - #7220

Closed
marijnbent wants to merge 1 commit into
pingdotgg:mainfrom
marijnbent:t3code/render-visualize-output
Closed

marijnbent wants to merge 1 commit into
pingdotgg:mainfrom
marijnbent:t3code/render-visualize-output

Conversation

@marijnbent

@marijnbent marijnbent commented Aug 16, 2026 •

Copy link
Copy Markdown

Fixes #7191

Problem

Codex visualize references appeared as raw marker text, so users had to find and open the generated HTML file themselves.

Fix

  • Parse canonical and replacement-character visualize references while leaving fenced examples as Markdown.
  • Issue signed URLs only for bounded temporary HTML artifacts referenced by an assistant message.
  • Bind each URL to the artifact content, serve it as inert text, and execute it only in a sandboxed iframe with no network access.
  • Keep normal messages aligned while allowing only wide visualizations to extend beyond the chat column.

Proof

Before:

Raw visualize marker before this fix

After: see the proof comment below for a real Codex-generated visualization rendered inline.

Verification

  • 45 focused tests passed across asset access and timeline rendering.
  • Shared, contracts, web, and server type checks passed.
  • Focused lint, formatting, and git diff --check passed.
  • The web production build passed.
  • An integrated Chrome test verified wide-mode panel containment and light/dark theme bridging on a real Codex visualize result.

Implemented with GPT-5.6-Sol through Codex in T3 Code.

Note

Render Codex visualizations inline in assistant messages

  • Assistant messages containing inline visualization markers are parsed by splitInlineVisualizations (new shared module) and rendered as sandboxed iframes via the new InlineVisualization component, replacing raw marker text.
  • The server validates and signs visualization HTML fragments (temp-dir only, single hardlink, ≤1 MiB, HTML/XHTML extension, SHA-256 revision-locked) before issuing asset URLs, and serves resolved assets as inert text with a restrictive CSP.
  • ProjectionSnapshotQuery.hasAssistantVisualizationReference is added to confirm a path is referenced by an assistant message before a signed URL is issued, preventing unauthorized access.
  • The desktop app's CSP is updated to permit blob: in frame-src so blob-URL iframes can load in Electron.
  • Risk: visualization files must reside under the OS temp directory; files outside that root are rejected at issuance time.

Macroscope summarized 3164bfe.


Note

Medium Risk
Untrusted HTML is executed in the browser inside sandboxed iframes with layered CSP and server-side path gating, but any sandbox or CSP gap would be client-side code execution.

Overview
Assistant messages can now show Codex visualize markers as embedded previews instead of raw marker text. Shared parsing splits message text into markdown and visualization parts (standalone markers only; fenced code stays markdown).

The web InlineVisualization component requests a signed visualization asset URL, fetches the HTML fragment, and runs it in a nested sandboxed blob: iframe with strict CSP, auto height, and host theme tokens. Wide mode can extend past the normal chat column; timeline layout uses container queries and overflow tweaks.

On the server, asset access adds a visualization resource: bounded temp .html files (size cap, canonical path, no links outside allowed tmp roots), SHA-256 revision on the token, and resolution as inert text with sandbox response headers—not as a direct file download. assetsCreateUrl only issues visualization URLs when hasAssistantVisualizationReference finds that path in an assistant message for the thread (batched projection scan). Desktop CSP frame-src now allows blob: for the nested iframe host.

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

@coderabbitai

coderabbitai Bot commented Aug 16, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 37812b3d-77d0-428d-9174-311e7a23bcea

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Aug 16, 2026
Comment thread apps/server/src/ws.ts Outdated
Comment thread packages/shared/src/inlineVisualization.ts Outdated
Comment thread apps/server/src/assets/AssetAccess.ts Outdated
Comment thread apps/server/src/assets/AssetAccess.ts Outdated

@macroscopeapp macroscopeapp 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.

UI consistency review found three issues in the new inline-visualization UI: the frame's presentation depends on remote CDN assets that the desktop renderer's CSP blocks, the loading placeholder does not follow the repo's loading-state pattern, and the wide-mode width decision is split across two owners in a way that widens the message text column.

Posted via Macroscope — UI Consistency

Comment thread apps/web/src/components/chat/MessagesTimeline.tsx
Comment on lines +1 to +2
const VISUALIZATION_STYLES_URL =
"https://cdn.jsdelivr.net/gh/openai/codex@9ded177ce7c1c0bd2047f902936c177612ab3434/codex-rs/tui/src/inline_visualization/assets/visualize.css";

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.

All of the frame's styling comes from remote CDNs (cdn.jsdelivr.net here, unpkg.com for lucide on line 55), but a srcdoc iframe inherits the embedder's CSP in addition to its own <meta> policy. In the desktop renderer every app-scheme response gets default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval' … (apps/desktop/src/electron/ElectronProtocol.ts, makeDesktopContentSecurityPolicy), so the stylesheet and the lucide script are blocked and the visualization renders unstyled/iconless on desktop — while docs/user/providers-codex.md in this PR states desktop shows these visualizations.

Suggested smallest fix: ship visualize.css (and lucide, if it is required) as a local asset served by the app instead of a pinned third-party CDN URL, so the frame's baseline presentation does not depend on origins the desktop CSP forbids. If remote hosts must stay, the desktop policy has to allow them for frames and the docs' desktop claim needs to be verified in a packaged build.

Posted via Macroscope — UI Consistency

Comment thread apps/web/src/components/chat/MessagesTimeline.tsx Outdated
Comment thread apps/web/src/components/chat/InlineVisualization.tsx Outdated
Comment thread apps/server/src/assets/AssetAccess.ts Outdated
@macroscopeapp

macroscopeapp Bot commented Aug 16, 2026 •

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR introduces a new feature (inline Codex visualization rendering) with substantial complexity across web, server, and desktop. Multiple unresolved comments identify potential CSP issues on desktop and UI/copy bugs that warrant human review before merging.

You can customize Macroscope's approvability policy. Learn more.

@marijnbent
marijnbent force-pushed the t3code/render-visualize-output branch from 1c35642 to 88295b3 Compare August 16, 2026 15:13

@macroscopeapp macroscopeapp 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.

Effect service conventions: two spots drop the underlying failure when wrapping real errors into AssetVisualizationUnavailableError. Sibling branches in the same code paths keep cause (e.g. AssetWorkspaceAssetInspectionError, AssetWorkspaceContextResolutionError), so the visualization path should do the same.

Posted via Macroscope — Effect Service Conventions

Comment thread apps/server/src/ws.ts Outdated
Comment thread apps/server/src/assets/AssetAccess.ts Outdated
Comment thread apps/web/src/components/chat/InlineVisualization.tsx Outdated

@macroscopeapp macroscopeapp 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.

Two findings in apps/web/src/components/chat/InlineVisualization.tsx. The previously flagged desktop CSP issue is re-anchored because inlineVisualizationDocument.ts was folded back into this component, and the new fixed-height frame regresses row geometry relative to the removed height bridge. The earlier notes on the max-w-5xl row wrapper and the loading placeholder treatment still apply as posted.

Posted via Macroscope — UI Consistency

Comment thread apps/web/src/components/chat/InlineVisualization.tsx Outdated
Comment thread apps/web/src/components/chat/InlineVisualization.tsx Outdated
Comment thread apps/server/src/assets/AssetAccess.ts
@marijnbent
marijnbent force-pushed the t3code/render-visualize-output branch from 88295b3 to 660f676 Compare August 16, 2026 15:30

@macroscopeapp macroscopeapp 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.

One minor convention nit on the new visualization asset branch in apps/server/src/ws.ts. The rest of the change (optional cause on AssetVisualizationUnavailableError, the reused inspectError mapper in AssetAccess.ts, subpath namespace Effect imports, no new runtimes or service-instance injection) matches the repository's Effect conventions.

Posted via Macroscope — Effect Service Conventions

Comment thread apps/server/src/ws.ts Outdated
@marijnbent

marijnbent commented Aug 16, 2026 •

Copy link
Copy Markdown
Author

After

Real Codex visualize output rendered inline after this fix.

Inline Codex visualization after the fix

Latest verification on 3164bfedb: 24 timeline tests, web type checking, targeted lint, and the React Compiler production build passed. Earlier review verification also passed 14 AssetAccess tests, 6 desktop protocol tests, and server and desktop type checks. The integrated Chrome attack check confirmed that a self-navigation attempt stayed on the thread and sent no request.

Comment thread packages/shared/src/inlineVisualization.ts Outdated

@macroscopeapp macroscopeapp 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.

Reviewed the web UI changes for layout/theme ownership. Three findings on the new inline visualization surface and the timeline row wrapper it changed.

Posted via Macroscope — UI Consistency

Comment thread apps/web/src/components/chat/InlineVisualization.tsx Outdated
Comment thread apps/web/src/components/chat/InlineVisualization.tsx Outdated
Comment thread apps/web/src/components/chat/MessagesTimeline.tsx Outdated
Comment thread apps/web/src/components/chat/InlineVisualization.tsx
Comment thread apps/server/src/ws.ts
@marijnbent
marijnbent force-pushed the t3code/render-visualize-output branch from 660f676 to aa5d1d5 Compare August 16, 2026 15:56
Comment thread apps/server/src/assets/AssetAccess.ts Outdated
Comment thread apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts Outdated

@macroscopeapp macroscopeapp 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.

Two geometry/theming issues in InlineVisualization.tsx. Details inline.

Posted via Macroscope — UI Consistency

Comment thread apps/web/src/components/chat/InlineVisualization.tsx Outdated
Comment thread apps/web/src/components/chat/InlineVisualization.tsx Outdated
Comment thread apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts
@marijnbent
marijnbent force-pushed the t3code/render-visualize-output branch from aa5d1d5 to 7eef150 Compare August 16, 2026 17:00
Comment thread apps/web/src/components/chat/InlineVisualization.tsx

@macroscopeapp macroscopeapp 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.

One finding: the new host-token bridge in InlineVisualization only reacts to light/dark, so switching between palettes of the same appearance leaves the frame's interior on the previous theme while its wrapper repaints immediately.

Posted via Macroscope — UI Consistency

Comment thread apps/web/src/components/chat/InlineVisualization.tsx Outdated
Comment thread packages/shared/src/inlineVisualization.ts
@marijnbent
marijnbent force-pushed the t3code/render-visualize-output branch from 7eef150 to 711cc30 Compare August 16, 2026 17:30
@github-actions github-actions Bot added size:XL 500-999 changed lines (additions + deletions). and removed size:L 100-499 changed lines (additions + deletions). labels Aug 16, 2026
Comment thread apps/server/src/assets/AssetAccess.ts

@macroscopeapp macroscopeapp 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.

One finding: the new nested blob: frame in InlineVisualization cannot load under the desktop app's inherited CSP, so inline visualizations render as an empty card there.

Posted via Macroscope — UI Consistency

Comment on lines +150 to +154
return `<!doctype html><html><head>
<meta charset="utf-8"><meta http-equiv="Content-Security-Policy" content="${FRAME_HOST_CSP}">
<style>html,body,iframe{width:100%;height:100%;margin:0;border:0}</style></head><body>
<iframe title="${escapeAttribute(title)}" sandbox="allow-scripts"></iframe>
<script>(()=>{const frame=document.querySelector("iframe");const url=URL.createObjectURL(new Blob([${serializeScriptString(visualizationDocument)}],{type:"text/html"}));frame.addEventListener("load",()=>URL.revokeObjectURL(url),{once:true});addEventListener("message",event=>{if(event.source===frame.contentWindow&&event.data?.type==="${HEIGHT_MESSAGE}"&&typeof event.data.height==="number")parent.postMessage(event.data,"*")});frame.src=url})()</script>

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.

The nested blob: frame can't load in the desktop app. A srcdoc document inherits the embedder's CSP in addition to its own <meta> policy, and the Electron protocol handler stamps frame-src 'self' https://challenges.cloudflare.com onto every app-scheme response (apps/desktop/src/electron/ElectronProtocol.ts:93), so the host frame's frame-src blob: cannot widen the inherited policy — frame.src = url is refused and the card paints as an empty 384px bg-background block: the outer frame itself loads fine, so neither failed nor the asset._tag === "Failure" branch shows the "Visualization unavailable." state. This is the same inherited-policy mechanism as the earlier CDN issue, now hit by the host/blob hop rather than by remote assets.

Smallest fix: allow blob: in the desktop frame-src directive (alongside the existing worker-src 'self' blob:) while keeping the frame's own restrictive <meta> CSP, or serve the wrapper document from the asset route so it carries its own response CSP instead of inheriting the app's. Also worth confirming in the real app that the object URL loads at all: the host frame is sandbox="allow-scripts" without allow-same-origin, so its blob URL is minted on an opaque origin (blob:null/<uuid>), which browsers can also refuse to navigate to.

Posted via Macroscope — UI Consistency

@marijnbent
marijnbent force-pushed the t3code/render-visualize-output branch from 711cc30 to c7f9015 Compare August 16, 2026 17:45

@macroscopeapp macroscopeapp 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.

One convention finding on the new visualization branch in AssetAccess.ts. Everything else (error definitions with structured attributes and preserved cause, environment-based dependency acquisition in readVisualizationFile/visualizationRevision, namespace subpath imports, and the inline error mapping in ws.ts) matches the conventions.

Posted via Macroscope — Effect Service Conventions

Comment thread apps/server/src/assets/AssetAccess.ts Outdated

@macroscopeapp macroscopeapp 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.

One new finding on the timeline wrapper; see the inline comment.

Note: the nested blob: frame CSP problem in the desktop renderer flagged on an earlier commit still stands (apps/desktop/src/electron/ElectronProtocol.ts:93 still sends frame-src 'self' https://challenges.cloudflare.com, which a srcdoc host frame inherits and cannot widen). Not re-posted to avoid duplication.

Posted via Macroscope — UI Consistency

Comment thread apps/web/src/components/chat/MessagesTimeline.tsx Outdated
@marijnbent
marijnbent force-pushed the t3code/render-visualize-output branch 2 times, most recently from 073a9c3 to 5d68216 Compare August 16, 2026 18:08

@macroscopeapp macroscopeapp 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.

Two findings on the changed web UI code. (The [container-type:inline-size] stacking-context regression on MessagesTimeline.tsx:582 flagged in a previous review is still present and unaddressed — not re-posted here.)

Posted via Macroscope — UI Consistency

Comment thread apps/web/src/components/chat/InlineVisualization.tsx Outdated
Comment thread apps/web/src/components/chat/MessagesTimeline.tsx Outdated
Comment thread apps/web/src/components/chat/InlineVisualization.tsx Outdated
@marijnbent
marijnbent force-pushed the t3code/render-visualize-output branch from 5d68216 to 77b33ee Compare August 16, 2026 19:20
Comment thread apps/web/src/components/chat/InlineVisualization.tsx Outdated

@macroscopeapp macroscopeapp 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.

One finding on the new wide visualization geometry: it expands into the side gutter the timeline minimap explicitly reserves. Details inline.

Posted via Macroscope — UI Consistency

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.

wide mode expands to the full container minus the scroller padding + scrollbar lanes (3.25rem), which is exactly the side gutter the timeline minimap relies on being empty: resolveTimelineMinimapHitStripWidth caps the strip to (viewportWidth - 48rem)/2 precisely so it "never extends past the gutter into the content column" (MessagesTimeline.logic.ts:102-122), and the rail is absolute inset-y-0 left-0 z-40 w-18 (MessagesTimeline.tsx:790) painting above the list.

For container widths in roughly 864–1128px the persistent-gutter rail is always visible and the wide frame's left edge lands at ~26px: the dots (left-3, up to w-6 → 12–36px) paint on top of the frame's border/rounded corner, and the pointer-events-auto hit strip (12–52px) swallows hover/click over the chart's left edge — hovering pops the w-80 message preview over the visualization, clicking jumps to a message instead of reaching the iframe.

Suggest keeping the wide frame out of the reserved rail, e.g. by also subtracting the strip width per side (or reusing the same measured gutter the minimap computes):

Suggested change
? "relative left-1/2 w-[min(64rem,calc(100cqw-3.25rem))] -translate-x-1/2"
? "relative left-1/2 w-[min(64rem,calc(100cqw-9.75rem))] -translate-x-1/2"

Posted via Macroscope — UI Consistency

@marijnbent

Copy link
Copy Markdown
Author

just keeps on going

@marijnbent
marijnbent force-pushed the t3code/render-visualize-output branch from 77b33ee to 3164bfe Compare August 17, 2026 06:07

@macroscopeapp macroscopeapp 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.

One finding on the wide visualization mode's width calculation. Everything else in the changed web UI looks consistent with the shared primitives and Tailwind ownership rules.

Posted via Macroscope — UI Consistency

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.

wide mode can render narrower than the default mode. The default branch is w-full max-w-3xl, i.e. the full content column (100cqw minus the scroller's px-3/sm:px-5 and the two scrollbar lanes ≈ 100cqw - 3.25rem, capped at 48rem), while wide subtracts 9.75rem for the minimap gutters on both sides. Whenever the timeline container is under ~51rem — a split layout with the diff panel open, a narrow window, or the sidebar expanded — 100cqw - 9.75rem is ~104px smaller than the column a non-wide visualization gets, so a chart the model marked wide shrinks below its neighbours and pulls in from the column edges instead of bleeding out.

Suggest flooring the wide width at the normal column width so it can only ever grow:

Suggested change
? "relative left-1/2 w-[min(64rem,calc(100cqw-9.75rem))] -translate-x-1/2"
? "relative left-1/2 w-[min(64rem,max(100%,calc(100cqw-9.75rem)))] -translate-x-1/2"

Posted via Macroscope — UI Consistency

@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 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 3164bfedb45b6a6d65fbeed37a77ba765a5ca4b1. Configure here.

threadRef={ctx.threadRef}
/>
) : null,
)}

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.

Copy keeps hidden visualize markers

Medium Severity

Assistant rendering now splits Codex visualize markers into inline previews, but copy still uses raw row.message.text, so clipboard output includes marker payloads users no longer see. The timeline minimap preview builds from the same raw text, so private-use or replacement-character marker junk can also appear there.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 3164bfedb45b6a6d65fbeed37a77ba765a5ca4b1. Configure here.

@marijnbent
marijnbent force-pushed the t3code/render-visualize-output branch from 3164bfe to b97103d Compare August 17, 2026 15:15
@marijnbent
marijnbent force-pushed the t3code/render-visualize-output branch from b97103d to 6a0738b Compare August 17, 2026 15:15
@t3-code

t3-code Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

thanks for working on inline visualizations. we are closing this implementation because the feature needs a durable artifact model rather than continued dependence on provider-generated temporary files.

  • historical visualizations depend on the original temporary HTML path remaining available. temporary-file cleanup can leave old messages showing “Visualization unavailable.”
  • the artifact is not permanently bound to the message. a content hash protects an issued URL, but issuing another URL hashes whatever currently occupies the path, so reusing a filename can change what an old message displays.
  • theme changes rebuild the iframe’s srcDoc, restarting scripts and losing interactive state.
  • mobile has no corresponding rendering or fallback.
  • asset authorization repeatedly scans assistant message text for a matching visualization path rather than using a stored message-to-artifact relationship.

this introduces executable HTML hosting, a new asset type, nested iframes, theme bridging, and wider chat layouts. those pieces serve the feature; file count alone is not the objection. the persistence, identity, and lifecycle behavior need to be designed together before this becomes a supported artifact type.

the sandboxing and file checks show care. this decision is not a claim of a demonstrated sandbox escape or careless security work.

a new pr is welcome with durable message-bound artifacts, preserved interactive state, and a clear cross-client behavior. if you disagree with this assessment, please link back to this pr and explain how those concerns are addressed.

closed at the request of @StiensWout.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL 500-999 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: visualize output is shown as raw tool text instead of rendering the visualization

1 participant