Skip to content

feat: agents can show HTML pages inline in threads - #278

Merged
coreybain merged 1 commit into
mainfrom
feat/inline-html-renders
Oct 6, 2026
Merged

coreybain merged 1 commit into
mainfrom
feat/inline-html-renders

Conversation

@coreybain

@coreybain coreybain commented Oct 6, 2026 •

Copy link
Copy Markdown
Collaborator

Agents can only answer in Markdown, so charts, tables, diagrams, image collages, and mockups are out of reach. T3 Code just shipped inline HTML renders (pingdotgg/t3code#15968). This ports it to Pathway, including the follow-ups for the MCP Apps bridge (#16196) and the height and scroll fix (#16283).

What changes for users

An agent builds one self-contained HTML page, checks it with html_preview (a screenshot), then publishes it with html_render. The page appears inline above the agent's reply on web, desktop, and iOS, and stays visible when the turn folds into Worked for…. Each page:

  • follows the app theme;
  • sizes itself to its content;
  • opens full size;
  • is deleted with its thread.

Pages are for the user, so subagents and orchestrator workers never get these tools. They aren't listed for those threads, and calls are refused with a message telling the agent to report back to its parent. Forks keep the tools.

The existing visualize{} cards are unchanged.

How it works

Server. Two new MCP tools.

  • Preview browser: a separate, short-lived, locked-down browser. It reuses Pathway's pinned Playwright Chromium; T3's Chrome-for-Testing downloader is not ported. Protections kept from upstream:
    • The page is served from memory, so file:// and other local files are refused.
    • Image files are inlined only after their bytes are checked to really be images.
    • All traffic goes through a SOCKS5 proxy that only connects to public addresses, which blocks loopback, LAN, and this machine's own addresses.
    • WebRTC is removed, and popups and navigation are locked down.
    • Chromium runs with its OS sandbox and never retries without it. If Chromium is missing, the preview fails with an actionable error, and publishing still works, just without measured heights.
  • Normalized output: every adapter (Codex, Claude, Cursor, OpenCode, ACP, Grok) runs HTML tool output through one compactor. Stored items never carry raw HTML or screenshots, and failures keep their error message.
  • Storage and serving: published pages are thread-owned attachments, served through the existing signed asset URLs with a new disposition: "inline". Inline .html responses carry Content-Security-Policy: sandbox allow-scripts allow-forms allow-popups, plus nosniff and no-referrer. URLs resolve against the owning environment, so remote and Pathway Connect work the same way.
  • Deletion: deleting a thread sweeps that thread's own render files. Archive, rollback, and superseded attempts keep them.

Web and desktop.

  • New HtmlRenderFrame and HtmlRenderDocument. The iframe sandbox is allow-scripts allow-forms.
  • Theme and height use the MCP Apps bridge messages. Links open externally only from this frame, while it has focus, right after the user interacted.
  • There's a full-size dialog. The desktop CSP gains frame-src http: https:.
  • The standard palettes moved to @spiritdevs/shared/themePalettes.

iOS (native Swift).

  • WKWebView with a non-persistent store and no file access. The page must be served as HTML with a sandbox CSP.
  • Height and link messages go through an isolated content world that page scripts can't reach. Links open in Safari only after a tap.
  • Full screen has a 16pt gutter, and the page hands vertical drags back to the feed.

Docs.

  • docs/user/html-renders.md
  • docs/internals/html-renders.md
  • glossary entries

Verification

  • Server: 647 tests across 25 suites pass. They cover every touched suite plus the other suites that issue MCP credentials.
  • Web: 166 tests pass. Desktop: 11 tests pass. Contracts and shared: 86 tests pass.
  • iOS: build-for-testing passes, and 28 focused tests pass, including WebKit tests that load real sandboxed pages.
  • Typecheck: clean except the existing OrchestratorMcpService.ts:810 diagnostic, which this PR doesn't touch.
  • Lint and format: clean on touched files.

Each package was reviewed by the other model, and the confirmed findings were fixed. The notable ones:

  • A page popup could crash the server through an unhandled rejection in Playwright's route handler.
  • On Windows, a /\host\share image path could make the server open an SMB connection.
  • iOS's sandbox-CSP check matched a substring instead of the directive.
  • An ACP payload could carry full HTML and screenshots for unverified calls.
  • Failed Codex and ACP calls lost their error message.

Not yet verified:

  • Real-Chromium security test: HtmlPreviewBrowser.integration.test.ts is written but gated behind PATHWAY_HTML_RENDER_BROWSER_TESTS=1 and hasn't been run. It covers local-file leaks, private-network access, popups, and WebRTC.
  • Real clients: no manual pass on web, desktop, or iOS yet, so there are no screenshots. I'll add them after that pass.
  • Unverified cases: mixed content with a plain-http LAN environment, iOS scroll handoff on a device, and macOS/Windows/Linux packaging of Chromium.

Known limits / follow-ups

  • Orchestrator chat: the Orchestrators conversation view doesn't show pages; work items don't carry tool output. Pages do show in the worker's own thread, but workers can't make them anyway.
  • Generic tool titles: ACP harnesses that title MCP calls generically, Grok included, show HTML calls as ordinary tool rows.
  • Upstream risks also accepted here:
    • A focused page can capture keyboard input.
    • Pages can load public resources from the user's browser.

Implemented and reviewed by Claude Opus 5.5 and GPT-6.1 Sol subagents, orchestrated by Claude Opus 5.5 in Claude Code via Pathway.

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

Port of T3 Code's inline HTML renders (pingdotgg/t3code#15968, with the
#16196 MCP Apps bridge and #16283 height fix). Agents get html_preview and
html_render MCP tools; published pages render in a sandboxed frame above the
agent's reply on web, desktop, and iOS.

Pages are for the user, so subagents and orchestrator workers do not get the
tools.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

@greptile-apps greptile-apps 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.

Your trial has ended. Reactivate Greptile to resume code reviews.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-06T08:30:18.442916Z d776810 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@coreybain
coreybain merged commit 3827440 into main Oct 6, 2026
1 check passed

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d7768104a8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

payload: object,
invocation: McpInvocationContext.McpInvocationScope,
runtimeContext: Context.Context<never>,
signal?: AbortSignal,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Pass the abort signal into the HTML tool effect

For HTML calls, cancellation notifications, request disconnects, and handler shutdown only abort the signal passed here, but Effect.runPromiseWith(runtimeContext) is invoked without { signal }. The tool effect therefore keeps running: html_preview retains its browser and proxy until its own deadline, the newly added Effect.never cancellation tests hang, and html_render can continue publishing after its MCP request was cancelled. Pass the signal to the Effect runner so interruption reaches the tool and its finalizers.

Useful? React with 👍 / 👎.

id: `effect:${command.commandId}:attachment.cleanup`,
commandId: command.commandId,
threadId: command.threadId,
request: { type: "attachment.cleanup", attachmentIds, htmlRenderThreadId: thread.id },

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve inherited renders when deleting a source thread

When a source conversation has a surviving fork, the fork's inherited visible items still contain the source's HTML attachment IDs, but this deletion effect sweeps every HTML file minted by the source thread. Deleting the original conversation therefore removes files that the fork still displays, turning all inherited visual replies into broken asset URLs. Cleanup needs to retain or copy pages referenced by live forks, or remove them only after the last referencing thread is deleted.

Useful? React with 👍 / 👎.

const files = yield* Effect.forEach(
[...new Set(references.map((reference) => reference.path))],
(path) =>
fileSystem.stat(filePathFor(path)).pipe(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Restrict local image reads to the provider's file scope

When a provider is running with read-only or workspace-scoped filesystem access, it can still pass any absolute image path to html_preview or html_render, and this code has the unrestricted server process stat and read it without checking the thread workspace or runtime permissions. Preview returns the rendered pixels immediately, while publish embeds the bytes into a script-capable page, so a known outside-workspace PNG/SVG—or a symlink to one from a writable workspace—bypasses the provider's filesystem isolation and can disclose the file. Resolve images through an authorized workspace root or reject paths outside the caller's permitted scope.

Useful? React with 👍 / 👎.

const LOCAL_IMAGE_PATTERN = new RegExp(
String.raw`(["'\x60])(${ABSOLUTE_PATH}(?:(?!\1)[^\r\n]){0,2048}?\.(?:${IMAGE_EXTENSIONS}))\1` +
String.raw`|url\(\s*(${ABSOLUTE_PATH}[^\s"'\x60()]{0,2048}?\.(?:${IMAGE_EXTENSIONS}))\s*\)`,
"gid",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Match uppercase Windows paths and image extensions

On Windows, the usual absolute path such as C:\Users\me\chart.PNG does not match this case-sensitive pattern: the drive expression only accepts [a-z], and the generated extension alternatives are lowercase. The reference is consequently omitted from both inlining and missingImages, so preview and publish report success while the client receives a broken image URL. Make this pattern case-insensitive, which also handles valid uppercase CSS URL(...) spellings.

Useful? React with 👍 / 👎.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant