Skip to content

feat(tui): render agent-generated visualizations in chat #2088

Open

Description

@LHagfoss

Problem

RustCode renders assistant messages through the TUI's Markdown pipeline. Fenced blocks are displayed as source/code, and tool results have a separate text-oriented transcript renderer. There is no message representation or rendering path for an agent to present a useful visual interaction directly in the conversation. As a result, charts, diagrams, exploratory controls, and other task-specific views fall back to prose, ASCII art, or Markdown tables.

T3 Code recently shipped agent-created visualizations in its thread. This is a useful reference for the interaction we want—not a requirement to copy its web stack or execute arbitrary HTML in RustCode.

Reference implementation research

RustCode codebase observations

  • rustcode/tui/src/ui/assistant_render.rs owns assistant-message presentation and currently routes fenced blocks through code/plain-text rendering; it already has careful handling for partial streams, fence length, and copy behavior.
  • rustcode/tui/src/ui/markdown.rs is the structured Markdown renderer, with bounded caching and explicit table rendering.
  • rustcode/tui/src/ui/history_cell.rs caches committed assistant rendering by width/theme and keeps an incremental Markdown cache for streaming responses.
  • rustcode/tui/src/ui/scrollback.rs deliberately holds incomplete fences and unstable Markdown blocks until they are safe to commit. Any visual block must preserve this streaming/scrollback contract.
  • rustcode/tui/src/ui/conversation_render.rs dispatches finalized assistant and tool messages through separate paths. rustcode/tui/src/ui/tool_result.rs bounds tool-result transcript output; visualization output should not bypass its own size/resource limits or be smuggled in as a tool-result side effect.
  • The repository has a ratatui/crossterm terminal frontend and currently has no HTML, iframe, or WebView rendering path. T3's iframe implementation cannot be transplanted into the TUI as-is.

Proposal

Add a first-class, bounded visualization block to assistant conversation output so the agent can choose a fit-for-purpose view instead of always emitting a Markdown table. Keep the canonical assistant text/protocol backward compatible, and make the renderer capability explicit: the terminal should render supported visualizations using native terminal widgets; an HTML/WebView implementation, if desired for a future graphical frontend, must be separately designed and sandboxed rather than silently executing HTML in the terminal.

A practical first slice is a small allowlisted schema with a few high-value widgets (for example, chart, key/value or sortable data table, and diagram), rendered natively in ratatui with keyboard-operable controls and textual fallback. The wire format and block boundaries should be structured and versioned, not inferred from arbitrary prose. The agent should be able to provide labels, data, accessible text/summary, and a clear fallback. Unsupported, malformed, oversized, or incomplete blocks remain visible as source or a concise fallback instead of disappearing.

Do not make arbitrary HTML/JavaScript execution, general-purpose component code, or cross-provider artifact recovery a prerequisite for the MVP. Consider those as follow-up work after the protocol, UX, and threat model are agreed.

Acceptance criteria

  • Decide and document the first supported format and widget allowlist, versioning, maximum payload/data sizes, and fallback behavior.
  • Define how visual blocks are represented in assistant content/tool protocol without breaking existing providers, stored sessions, copy, or export behavior.
  • Parse only complete, recognized blocks; malformed and unsupported blocks render safely as ordinary source/fallback text. Normal code fences and Markdown tables are unchanged.
  • Render the initial supported widgets in the TUI using native ratatui primitives; provide keyboard navigation/selection where interactive and a useful non-interactive textual representation.
  • Integrate with live streaming and finalized transcript paths without duplicate rows, flicker, unbounded re-rendering, or committing a partial visual block to terminal scrollback.
  • Keep assistant-only rendering semantics explicit. Tool output or user-provided content cannot cause executable content to run.
  • Set hard limits for dimensions, item counts, text lengths, and render cost; truncate with an explicit indication rather than monopolizing the viewport.
  • Add tests for incremental/partial blocks, malformed payloads, oversized inputs, unknown schema versions/widgets, width changes, theme changes, and parity between live and committed rendering.
  • Document the format and add a small example prompt/result demonstrating when the agent should choose a visualization over Markdown.
  • If a future GUI renderer executes HTML, require a separate security review and implement strict origin/path authorization, size limits, CSP/sandbox isolation, no ambient filesystem/network access, safe bridge messages, and explicit navigation/focus handling. Do not treat sanitization alone as a security boundary.

Open design questions

  1. Should the first milestone be only native structured widgets (cross-platform and safe by construction), or should it include a desktop-only sandboxed HTML artifact viewer?
  2. Should visualizations arrive as a typed assistant-content block, a dedicated tool result/artifact referenced by an assistant message, or both? The rendering layer must be able to associate an artifact with the correct thread/message and reject unreferenced paths.
  3. Which initial widget types materially help coding tasks beyond the existing Markdown table, code, diff, and diagram conventions?
  4. What should copy/export do for an interactive block: copy its source/schema, its textual summary, or offer both?

Related RustCode areas

  • rustcode/tui/src/ui/assistant_render.rs
  • rustcode/tui/src/ui/markdown.rs
  • rustcode/tui/src/ui/history_cell.rs
  • rustcode/tui/src/ui/scrollback.rs
  • rustcode/tui/src/ui/conversation_render.rs
  • rustcode/tui/src/ui/tool_result.rs

Activity

  1. LHagfoss commented on Oct 16, 2026

    @LHagfoss
    OwnerAuthor

    Decisions for the first milestone (implemented in #2094, documented in docs/visualizations.md):

    1. Native only. The first milestone uses structured widgets drawn natively. There is no HTML or WebView. Any future GUI renderer that runs HTML needs its own design and security review: origin/path authorization, size limits, CSP/sandbox, no ambient filesystem or network access, safe bridge messages, and focus/navigation handling. Sanitization alone is not a security boundary.
    2. Representation. A visualization is a ```rustcode-viz fenced block in ordinary assistant text, holding one JSON object {"version":1,"type":...,"title"?,"summary"?,...}. No provider, session, ACP or export format changes. Clients without a renderer show a JSON code block. Typed content blocks and tool-result artifacts are deferred.
    3. Widget allowlist (v1). bar_chart (labels and values, horizontal bars), table (validated cells, optional sort_by, numeric columns right-aligned, laid out by the existing Markdown table renderer), and tree (module, call or dependency hierarchies). A box-and-arrow diagram was left out because it needs a layout engine to be legible in a terminal.
    4. Limits. 16 KiB payload; 30 bars; 50 rows and 8 columns; 120 nodes and depth 8; title 80, summary 300, label 40 and cell 60 characters. Anything over a limit is truncated with a visible "… N more … not shown" row, and every row is cut to the viewport width. Control characters are replaced with spaces.
    5. Fallback. Malformed JSON, an unknown version or type, missing fields, an oversized payload, or a finalized message whose fence never closed all render as the JSON source with a one-line note. A fence that is still open while streaming shows a one-row placeholder and is never parsed or committed to scrollback.
    6. Copy/export. The canonical text is unchanged. /copy and export keep the fence, and the widget's copy-registry entry is the full source fence. Selecting rendered rows copies their text.
    7. Assistant-only. Only the assistant message renderer recognizes the fence. Tool output and user content never render widgets, and nothing in a payload is executed.
    8. Prompt. About 120 tokens of guidance go into root system prompts for every frontend, since the JSON fence is an acceptable fallback elsewhere and one prompt keeps the prompt cache shared. Subagent and compact prompts leave it out.

    Still open after #2094: interactive widgets (keyboard re-sorting, collapsing tree nodes), which need a transcript focus model, and more widget types.

  2. LHagfoss commented on Oct 16, 2026

    @LHagfoss
    OwnerAuthor

    Update to decision 8: the visualization prompt hint is now gated by frontend capability (#2094). The TUI sets it at startup, and ACP clients opt in with clientCapabilities._meta.rustcode.visualizations >= 1 at initialize. It is decided once per session state, so the cached prompt prefix stays stable. Every other frontend keeps the unchanged base prompt. The macOS app now renders the same widgets natively (sortable table, collapsible tree, bar chart with hover values) and advertises the capability: #2097 / #2098.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions