Skip to content

Add chat-scoped Generative UI HTML artifacts - #61

Merged
sambitcreate merged 26 commits into
mainfrom
cursor/generative-ui-artifacts-plan-9cf3
Aug 28, 2026
Merged

Add chat-scoped Generative UI HTML artifacts#61
sambitcreate merged 26 commits into
mainfrom
cursor/generative-ui-artifacts-plan-9cf3

Conversation

@sambitcreate

@sambitcreate sambitcreate commented Aug 26, 2026

Copy link
Copy Markdown
Owner

Summary

Implements in-chat Generative UI artifacts as a chat-scoped Pi-runtime extension, following the existing display_image path rather than Pi’s coding-agent loader.

Canonical plan: docs/plans/generative-ui-artifacts-plan.md. Runtime notes: docs/pi-gui-artifacts.md.

What shipped

  • aiden.gui.generative-ui with tool render_artifact({ title, html } | { title, path }). Replay is never. Tool results are text only. Same enablement gate as display_image (attended desktop workspace chat only).
  • App-owned store (generative-ui-artifacts.json, 0600) plus message metadata/mediaId. Pending stages share the existing visual-artifact recovery composer gate. Staged rows already present on the chat are reconciled so a post-append commit failure does not brick the composer. Recovery failures are isolated per chat. Chat copy remaps mediaId but keeps the content-digest id.
  • Sandboxed iframe: sandbox="allow-scripts" only. Main wraps HTML and serves it from aiden-genui://preview/<64-hex-token> with guest CSP as a response header. The iframe uses src, not srcDoc (srcdoc inherits the privileged renderer CSP). Parent frame-src is 'self' aiden-genui:; parent script-src is not widened. Guest script-src allowlists only exact host-library URLs. Same-generation replaces keep mediaId and change a content-digest id so the iframe reloads even when byte length is unchanged.
  • Path mode walks every lexical component with lstat and refuses intermediate symlinks, then opens the leaf with O_NOFOLLOW.
  • UI: inline frame, expand dialog, export (document-owned, pending-gated). /visualize composer instruction (clears attachments/skills like a normal send). iOS/Android: “Can't view on this device. View in Aiden Agent.” No onboarding tile.

Tests

npm run test:generative-ui now includes Chromium Playwright containment (playwright.generative-ui.config.ts): same-origin unique-origin sandbox denies window.parent.document, guest fetch throws with a connect-src CSP violation, privileged-parent srcdoc scripts do not run, and parent frame-src 'self' aiden-genui: does not load https:// frames. CI installs Playwright Chromium before npm test. npm run type-check is green. Host libraries are vendored at postinstall via scripts/vendor-generative-ui-libs.mjs (not committed).

Presentation hardening (flicker + live activity)

  • No flicker on replace: artifact cards are keyed by mediaId (stable across same-title replaces). The preview effect no longer clears src before fetching — the old iframe stays mounted and interactive until the replacement URL resolves, then navigates in place. Frame and list are React.memo'd so transcript streaming storms skip the iframe cards.
  • Atomic handoff: while the streaming row is mounted, its live cards win and the persisted copies stay hidden (hiddenHtmlMediaIds); the persisted card appears in the same commit that unmounts the streaming row. Exactly one card per mediaId at every frame (onDone, onError-with-partial, crash-without-done, chat switch, detached projection).
  • Visualizing shimmer: while a render_artifact call is pending/running, a ReasoningBlock reuse (label="Visualizing") rides directly above the artifact list, and the bottom activity row shows a shimmering Visualizing… phase when the block isn't rendering (e.g. detached projection revisits).
  • Provider-neutral live activity: llm-client now consumes toolcall_start and opens a pending timeline step while the model is still writing tool-call arguments — the longest phase of a GenUI turn was previously dark for every provider (Aiden dropped all toolcall_* events). Execution events upgrade the step with the full descriptor; empty/backfilled ids from OpenAI-compatible endpoints are skipped. The activity row keys Responding… off recent text deltas (TEXT_STREAMING_IDLE_MS), so stale prose can no longer strand a silent static row — the Thinking shimmer survives for providers that stream no reasoning summaries (Codex was the worst case). The ReasoningBlock's active state now derives from an open thinking step, so the shimmer returns for later reasoning stretches after prose.

Review

Dual-subagent review loops (edge-case + adversarial) after each phase, including this containment fixture. Latest pair (post presentation-hardening) returned GO after fixes: test-file type errors, a duplicate-pending-step bug for empty tool-call ids on OpenAI-compatible endpoints, a detached-drain case where the Visualizing row was suppressed while its block was also hidden, and a transient double “Thinking…” indicator. Residuals: no live Electron aiden-genui: protocol paint in CI; golden chart/control fixtures are admission/wrap tests, not clicked in Chromium; optional expanded work-surface tab was not added. html remains a live tool argument (results stay text-only; the plan allows the html param).

Integration decisions (do not reverse)

Do not use the Pi coding-agent extension loader, buildAgentTools for GUI, <webview>, or Designer Mode Vite preview. Do not put HTML in Pi history as tool results. Guest HTML is untrusted. Do not add allow-same-origin to the iframe sandbox. Do not widen privileged renderer script-src.

Open in Web Open in Cursor 

cursoragent and others added 2 commits August 26, 2026 23:25
Document the Antigravity GenUI mapping: reuse display_image's chat-scoped
extension + chat:artifact path, sandboxed iframes, and bundled viz libs
instead of Pi's extension loader or Designer Mode.

Co-authored-by: Sambit Biswas <sambitcreate@users.noreply.github.com>
Ship render_artifact as a Pi-runtime extension with an app-owned store, sandboxed iframe preview, vendored Chart.js/Plotly/KaTeX, /visualize, and mobile placeholder copy.

Co-authored-by: Sambit Biswas <sambitcreate@users.noreply.github.com>
@cursor cursor Bot changed the title Plan Generative UI artifacts as an inbuilt Pi-runtime extension Add chat-scoped Generative UI HTML artifacts Aug 27, 2026
cursoragent and others added 5 commits August 27, 2026 00:38
Privileged-renderer srcdoc inherits parent CSP, so guest scripts and host
libraries never ran. Preview documents now use a token URL with CSP as a
response header. Export is document-owned and pending-gated, /visualize
clears attachments and skills like a normal send, and image stores keep
the ChatImageArtifactV1 type after the html union landed.

Co-authored-by: Sambit Biswas <sambitcreate@users.noreply.github.com>
Reject intermediate directory symlinks in render_artifact path mode,
reload same-size replacements via a content digest id, isolate
per-chat recovery failures, and commit staged HTML that already
landed on the chat so a failed commit cannot brick send/copy/export.

Co-authored-by: Sambit Biswas <sambitcreate@users.noreply.github.com>
Copied chats remap mediaId for store isolation but preserve the
content-digest id so iframe reload identity stays tied to HTML bytes.

Co-authored-by: Sambit Biswas <sambitcreate@users.noreply.github.com>
Phase 0 required a live fixture that guest fetch and parent.document fail
under unique-origin sandbox plus guest CSP. Add Playwright Chromium tests
wired into test:generative-ui and CI, and cover expand/export keyboard chrome.

Co-authored-by: Sambit Biswas <sambitcreate@users.noreply.github.com>
Artifact cards now key by mediaId so same-title replaces swap the iframe
src in place instead of remounting through the placeholder, the
streaming-to-persisted handoff keeps exactly one live card until the
streaming row unmounts, and the frame/list are memoized against
transcript re-render storms.

A render_artifact call in flight shows a shimmering Visualizing state
reusing the ReasoningBlock plus a Visualizing activity phase. Tool-call
argument streaming now opens a pending timeline step at toolcall_start
for every provider, the ReasoningBlock active state derives from open
thinking steps, and Responding keys off recent text deltas so stale
prose can no longer strand a silent activity row (worst on Codex,
which often streams no reasoning summaries).
@sambitcreate

Copy link
Copy Markdown
Owner Author

Presentation hardening: flicker-free rendering + Visualizing shimmer (c2804a6)

Two investigation subagents traced the reported issues to root causes, and two review subagents verified the fixes (GO after addressing their findings).

Jitter/flicker while a visualization is created — three compounding causes, all fixed:

  1. Artifact cards were keyed by content digest, so every same-title replace remounted the whole card through the “Loading visualization…” placeholder. Now keyed by the stable mediaId, with the iframe src swapped in place once the new preview resolves.
  2. The load effect cleared src before the async fetch, tearing down the working preview first. Removed — the old preview stays visible until the replacement is ready.
  3. The streaming→persisted handoff unmounted the card from the streaming row and cold-mounted a copy in the persisted row (double scroll + flash). The live card now wins until the streaming row unmounts; the persisted copy appears in the same commit (single atomic swap). HtmlArtifactFrame/HtmlArtifactList are memoized so streaming re-render storms skip the iframes.

“Thinking” shimmer dying while the sidebar spinner kept going — the in-chat indicators keyed off content shapes (!content, streamingText.length) while Aiden dropped every toolcall_* stream event, so the model's longest phase — writing the tool-call arguments, i.e. the entire HTML document — was invisible for all providers. Codex was hit hardest because it often streams no reasoning summaries. Fixes, provider-neutral:

  • toolcall_start now opens a pending timeline step during argument streaming; execution events upgrade it with the full descriptor (render_artifact detail stays the sanitized title only — HTML never crosses the timeline boundary).
  • New shimmering Visualizing state (reusing the existing Thinking/ReasoningBlock element) while render_artifact is in flight, plus a Visualizing… activity phase with shimmer.
  • The ReasoningBlock shimmer now derives from an open thinking step, so it returns for later reasoning stretches after prose.
  • Responding… requires recent text deltas (2s window), so stale prose can no longer pin a static row while the model works — and the suppression rule only hides the bottom row while the block covering it is actually shimmering.

Edge cases covered by the reviewers: Codex id continuity across toolcall_starttool_execution_start (verified in the Responses adapter, incl. parallel calls), empty/backfilled ids on OpenAI-compatible endpoints (skipped, no duplicate step), orphaned pending steps on abort/error/truncation (settled honestly by finish()), approval interplay (Waiting for approval wins; awaiting steps don't shimmer), detached projection revisits (row keeps narrating), and reduced-motion gating.

…artifacts-plan-9cf3

# Conflicts:
#	.papercuts/troubleshooting.md
#	package-lock.json
…cript Node globals.

The postinstall vendoring step materializes minified Chart.js, Plotly, and
KaTeX under resources/generative-ui (gitignored) and the tracked vendor
script uses Buffer/process; eslint . ran neither-clean in CI because the
branch's earlier commits had no CI runs yet.
@sambitcreate
sambitcreate marked this pull request as ready for review August 28, 2026 00:20
@pullfrog

pullfrog Bot commented Aug 28, 2026

Copy link
Copy Markdown

This run was cancelled 🛑

The workflow was cancelled before completion. Please check the link below for details.

Pullfrog  | View workflow run | via Pullfrog𝕏

@sambitcreate
sambitcreate merged commit 10dcc86 into main Aug 28, 2026
5 of 6 checks passed
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.

2 participants