Skip to content

feat: show HTML visuals in the Thread timeline - #2285

Merged
chuks-qua merged 4 commits into
mainfrom
feat/issue-2211
Oct 23, 2026
Merged

chuks-qua merged 4 commits into
mainfrom
feat/issue-2211

Conversation

@chuks-qua

@chuks-qua chuks-qua commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Closes #2211

What

A Thread can now show an HTML visual above the agent's reply. The visual is a self-contained page in a sandboxed frame, as wide as the reply column, with no box around it. Its height follows the content up to the cap the visual requests. Before this change Mcode had no way to show a visual.

MessageSchema.visual          { title, html (≤ 512 KiB UTF-8), maxHeight (80..2000) }
MessageStore.createVisual     system message, `visual` JSON column, text fallback "Visual: <title>"
MessageBubble
  SystemMessageContent
    providerSwitch → marker
    visual         → <HtmlVisualFrame>
                       iframe sandbox="allow-scripts", srcdoc = CSP meta + theme bootstrap + page
                       posts design tokens on load and on light/dark change
                       accepts height only from its own frame, clamped to [80, maxHeight]
    otherwise      → notice

Nothing produces visuals yet. The agent tool arrives in #2216, and html_preview in #2221. For this PR I injected a visual by seeding a fixture-repo Thread with a prompt, a visual and a reply.

The page is untrusted:

  • The sandbox has no allow-same-origin, so the page gets an opaque origin and cannot reach app storage, cookies or the parent document.
  • With no allow-top-navigation or allow-popups, it cannot navigate the app or open windows.
  • A Content-Security-Policy in the frame head blocks fetch, beacons and remote images, fonts and scripts. Pages must inline everything.

Verification:

  • Contracts: schema tests cover the byte limit, the title and the height bounds.
  • Server: a persistence round trip and a replay test check that the visual survives a conversation reload, in order between prompt and reply. Migration lint passes.
  • Web: frame tests cover the sandbox, the theme repost on dark mode, height clamping, foreign-source messages, doctype placement, token escaping and the policy's position in the head.
  • Live, in Electron and in the web client in Chromium: the probe in the demo page reports localStorage, document.cookie, parent.document and fetch blocked. window.origin is "null". On a light/dark switch the in-frame --ink changes from oklch(19% …) to oklch(95.5% …) while a marker set inside the page survives, so the page did not reload. Chromium logged the fetch refusal as a CSP violation.

Why

Agents can show results as a chart, table or diagram instead of prose (epic #2207). This PR builds the timeline item that the later tool writes to.

UI Changes

Electron live-testing captures follow. Before (main) is the same seeded Thread, which shows only the fallback note. After shows light and dark, plus the Thread after an app reload.

Config Changes

None.

Review Notes

  • Migration: 0099 adds a nullable visual TEXT column to messages. It is additive.
  • Security: a page can still navigate its own frame to an external URL, and CSP cannot block that. The sandbox keeps the navigation inside the frame.
  • Strict parsing: a stored visual that fails the schema throws on load, like the sibling JSON columns (providerSwitch, systemNotice).
  • Theme scope: the app has one theme axis, light or dark, and the frame follows it. If named themes arrive, useAppearance needs to cover them.
  • Flaky capture harness: Playwright reload loops in Electron sometimes hit a renderer crash or a click timeout. This happened once in 13 reloads of the visual Thread, and once in the plain control Thread with no visual, so I have not tied it to this change.

Before (main): the seeded visual shows only its fallback note

After, dark: prompt, visual, reply; probes blocked

After, light: same page, tokens updated without reload

After an app reload: the visual is still there


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

A system message can carry a self-contained HTML page with a title and a
requested height. The page is capped at 512 KiB of UTF-8 so one visual
always fits a conversation history page.
Store the visual as JSON on a system message, return it on conversation
pages and the tail, and add MessageStore.createVisual for the upcoming
html_render tool. Its text names the visual for readers without the frame.
The frame allows scripts only, so the page runs in an opaque origin with
no access to the app document, storage, or cookies, and cannot navigate
the window. Mcode's design tokens are injected before the page and
posted again when light or dark mode changes, so colours follow the app
without a reload. The frame grows with the page up to its requested
height.
The sandbox isolates a visual from the app, but its page could still
fetch or beacon to any host. A meta Content-Security-Policy in the
frame head keeps the page self-contained.
@chuks-qua
chuks-qua merged commit 7568ec8 into main Oct 23, 2026
14 checks passed
@chuks-qua
chuks-qua deleted the feat/issue-2211 branch October 23, 2026 14:14
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.

feat: render an HTML visual item in the thread timeline

1 participant