Add chat-scoped Generative UI HTML artifacts - #61
Conversation
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>
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).
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:
“Thinking” shimmer dying while the sidebar spinner kept going — the in-chat indicators keyed off content shapes (
Edge cases covered by the reviewers: Codex id continuity across |
…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.
|
This run was cancelled 🛑 The workflow was cancelled before completion. Please check the link below for details. |

Summary
Implements in-chat Generative UI artifacts as a chat-scoped Pi-runtime extension, following the existing
display_imagepath 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-uiwith toolrender_artifact({ title, html } | { title, path }). Replay isnever. Tool results are text only. Same enablement gate asdisplay_image(attended desktop workspace chat only).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 remapsmediaIdbut keeps the content-digestid.sandbox="allow-scripts"only. Main wraps HTML and serves it fromaiden-genui://preview/<64-hex-token>with guest CSP as a response header. The iframe usessrc, notsrcDoc(srcdoc inherits the privileged renderer CSP). Parentframe-srcis'self' aiden-genui:; parentscript-srcis not widened. Guestscript-srcallowlists only exact host-library URLs. Same-generation replaces keepmediaIdand change a content-digestidso the iframe reloads even when byte length is unchanged.lstatand refuses intermediate symlinks, then opens the leaf withO_NOFOLLOW./visualizecomposer 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-uinow includes Chromium Playwright containment (playwright.generative-ui.config.ts): same-origin unique-origin sandbox denieswindow.parent.document, guestfetchthrows with aconnect-srcCSP violation, privileged-parentsrcdocscripts do not run, and parentframe-src 'self' aiden-genui:does not loadhttps://frames. CI installs Playwright Chromium beforenpm test.npm run type-checkis green. Host libraries are vendored atpostinstallviascripts/vendor-generative-ui-libs.mjs(not committed).Presentation hardening (flicker + live activity)
mediaId(stable across same-title replaces). The preview effect no longer clearssrcbefore fetching — the old iframe stays mounted and interactive until the replacement URL resolves, then navigates in place. Frame and list areReact.memo'd so transcript streaming storms skip the iframe cards.hiddenHtmlMediaIds); the persisted card appears in the same commit that unmounts the streaming row. Exactly one card permediaIdat every frame (onDone, onError-with-partial, crash-without-done, chat switch, detached projection).render_artifactcall is pending/running, aReasoningBlockreuse (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).llm-clientnow consumestoolcall_startand 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 alltoolcall_*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.htmlremains 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,
buildAgentToolsfor GUI,<webview>, or Designer Mode Vite preview. Do not put HTML in Pi history as tool results. Guest HTML is untrusted. Do not addallow-same-originto the iframe sandbox. Do not widen privileged rendererscript-src.