Skip to content

Answer with components by default, draw code like Claude Code, and widen answers - #940

Merged
davidmckayv merged 17 commits into
mainfrom
david/answers-genui-code
Nov 2, 2026
Merged

davidmckayv merged 17 commits into
mainfrom
david/answers-genui-code

Conversation

@davidmckayv

@davidmckayv davidmckayv commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

Bots answer with components by default, code looks like Claude Code, and answers are wider.

Components by default. Every Bot already had the component library: charts, tables, cards, checklists, forms, choices, and UI built on the fly. Nothing told a Bot when to use it. A new standing instruction (COMPONENT_GUIDANCE in shared/bot-prompt.ts) goes to built-in Bots and in the standing role sent to remote harness Bots, in chat and in background turns. When an answer has structure, it uses a component:

  • numbers or a comparison: a chart or headline figures;
  • steps or a plan: a checklist or table;
  • one thing's details: a record card;
  • a choice: a choice card.

It adds a sentence or two around the component, and keeps plain text for short replies. It names kinds of answer, never tools, because the tools vary per Bot. Live with the real model:

Question Before After
A plan headed lists checklist
One thing's details bullets record card
A comparison chart chart
A greeting text text

A Claude Agent SDK coding Bot does not see it: ag-ui-claude-sdk 0.1.5 builds the prompt from the last message and drops system messages, the standing role included. Run live in the app and asked for a plan, then for a code change, a coding Bot planned in prose, worked with its own Bash and Write, and drew no gallery component.

Code that looks like Claude Code.

  • Syntax highlighting. Bot messages, tool results and group chat use Streamdown's official code plugin (@streamdown/code, Shiki) with OpenBot's light and dark themes. Shiki loads with the first code block, not with the conversation. In the build, Shiki's core is its own 79.4 kB chunk loaded on demand, not about 80 kB more in the chunk every chat route loads, and each grammar and theme is its own chunk too.

  • Coding harness tool cards. Built from OpenBot's own components:

    • Bash: the command, output folded after six lines, and the exit code.
    • Edit, MultiEdit and Write: a red and green diff with +/- counts, using diff. An edit that replaced every occurrence says so.
    • Read, Grep and Glob, and plain ls, cat or grep commands: a run of them folds into one line, as Claude Code folds them ("Read 1 file, searched for 1 pattern and listed 1 directory"), which opens to each step. One on its own keeps its own line. A command folds only when shell-quote reads it as one pipeline of a read, search or list program and filters.
    • TodoWrite: the checklist.
    • TaskCreate and TaskUpdate: task steps.

    The shapes come from recorded runs of the Claude Agent SDK harness. The browser cannot tell which harness a Bot runs, so a call is drawn as Claude Code's only when it carries the arguments Claude Code sends; another harness's tool with the same name is drawn as any other tool.

  • Bugs fixed, each with a regression test:

    • a failed command showed as successful after the turn ended, because the harness drops the failure flag;
    • a write or command that Claude Code's sandbox refused showed as done once the turn ended, because the harness's end-of-turn history drops the failure mark. The harness adapter now keeps it;
    • a command that printed JSON with "error": true showed "Didn't work". Only the exact shape the harness wraps a failure in counts now;
    • at phone width a folded run's label ran past the column and was cut off. A long tool line label now wraps.

Wider answers. The answer column is now 768px (max-w-3xl), the reading width ChatGPT and Claude.ai use, up from 672px. The input bar stays 672px, and both stay full width on phones. A Bot's components fill the answer column inside a conversation; previews elsewhere keep their reading width. Skins keep their own widths. A full-width mode for code and tables can come later.

Checked:

  • Format, lint, typecheck, test typecheck and the build pass. Every app test file passes, and the harness tests pass.
  • Each fix's regression test fails without the fix and passes with it.
  • In headless Chrome on a private stack, with the real model and a real Claude Agent SDK harness built from this branch:
    • the answer column measures 768px, a component 736px, the composer 672px;
    • no sideways scroll at 1440px or phone width, in light and dark;
    • code is highlighted;
    • the coding cards, live and after a reload, from a replay of the recorded harness calls (a replace-all edit, a failed command and a refused edit among them) and from the real harness (a refused write among them);
    • the folded reads and searches, closed and open, from both.
Every Bot is offered the gallery, but nothing said when to use it, so a
plan or the details of one thing came back as prose. One standing
instruction, beside the provenance rule on the same paths (built-in
prompt and the remote standing role, which background turns also use),
names kinds of answer rather than tools: figures as a chart or metrics,
steps as a checklist or table, one thing's details as a record card, a
choice as a choice card, with a sentence or two of text around it.
Plain text stays for short conversational replies.
…, and widen answers

Bash, Edit, MultiEdit, Write, Read, Grep, Glob, TodoWrite and Claude Code's task tools get their
own renderers: a command with its output folded after six lines, an edit or a new file as a red
and green diff, reads and searches as one line with a count, and a plan as a checklist. They are
built in the frame of a fenced code block, and read a failure both as the harness streams it and
as the history it sends at the end of a turn has it. Tests draw them through the real transcript
from calls recorded off the real harness.

Fenced code is highlighted through Streamdown's code plugin, in light and dark, in replies, tool
answers and group chats.

A Bot's answers run to max-w-4xl while the composer stays max-w-2xl; every skin keeps its
column as wide as its composer.
Gallery cards kept their 672px reading width inside the new 896px answer
column, so a chart sat narrower than the code and tool cards around it.
In a conversation they now fill the column; previews elsewhere keep the
reading width.

@guidovizoso guidovizoso left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nice work, the coding cards and the component guidance are both solid, and the tests draw from real harness recordings, which I like. A few things inline. The main one is the width: I'd go to 768px (max-w-3xl) instead of 896px for now, and we can add a full-width mode soon for people who want more room.

Comment thread app/src/lib/copilot/coding-tools.tsx Resolved
Comment thread app/src/lib/copilot/coding-tools.tsx Resolved
Comment thread app/src/lib/copilot/coding-tools.tsx Resolved
Comment thread shared/bot-prompt.ts Resolved
Comment thread app/src/lib/markdown.tsx Resolved
At 896px a line of prose runs long enough to be harder to read, and the width reaches every answer,
not only code. 768px (max-w-3xl) is the reading width ChatGPT and Claude.ai give an answer, and it
is still wider than the 672px column main draws, so answers still widen. Code blocks and diffs
scroll sideways, so they keep working at 768px.
replace_all was parsed but not shown, so an edit that changed every occurrence of its text read as
a single replacement. The diff can only show the one replacement, because the arguments carry the
text and not the file, so the card's header now says All occurrences.
…ailure in

ag-ui-claude-sdk wraps a failed result as {"error": true, "content": "..."}, but it also parses
any result that is JSON text and sends it on re-serialised. A command that printed an API's
{"error": true, "message": "..."} therefore read as "Didn't work". Only error plus a string
content, and nothing else, now counts; the ambiguity that remains is noted where it is read.
…ents

The coding cards are registered by bare name (Bash, Read, Edit) for every Bot, because the browser
cannot tell which harness a Bot runs: a Bot is an AG-UI address, and the harness picked in the New
Bot dialog is never stored. Another harness with a tool of the same name would have drawn mostly
empty cards. A call is now drawn as Claude Code's only when its arguments have the fields the Agent
SDK's tool inputs require; anything else under those names is drawn, and folded, like any tool the
app has no component for.
… does

Registering a renderer for every coding tool stopped groupToolSteps folding them, so a long coding
turn drew one row per call. Claude Code itself folds a run of reads and searches (Read, Grep, Glob,
and plain ls, cat or grep commands) into one line, "Read 3 files, searched for 2 patterns", and
keeps edits, writes, other commands and the plan on their own rows. The transcript now does the
same, with each step behind the line saying what it found.

A shell command folds only when it is one pipeline of a read, search or list program and filters,
read with shell-quote, so anything that could change something keeps its own card.
@streamdown/code imports Shiki's full entry and its JavaScript regex engine at its top, and
markdown.tsx imported it statically, so Shiki's core rode in the chunk every chat surface loads
(483.5 kB, against 403.8 kB on main). Its grammars and themes were already separate chunks.
markdownPlugins now stands in for the plugin with the same interface and imports @streamdown/code
the first time a code block asks to be highlighted; Streamdown draws the plain code until then, as
it does while a grammar loads. The chat chunk is back to 404.5 kB, and Shiki's core is a 79.4 kB
chunk fetched on demand.
…ted ones

The replace-all edit and the command that printed JSON with an error field were not made by the
recorded run, so they sit in their own fixture that says what each was built from.
ag-ui-claude-sdk marks a failed result in the TOOL_CALL_RESULT it streams, but builds the
MESSAGES_SNAPSHOT that ends the turn from the bare result, so the mark is lost from the history a
reloaded conversation draws. In a real run, a mkdir and two writes that Claude Code's sandbox
refused showed as done once the turn ended, the writes as 30 added lines that were never written.
The adapter now gives each failed call's snapshot entry the result it streamed.
bun test walks every file in one module graph, so once another file has highlighted code, Shiki is
already loaded and the check cannot ask whether a conversation loads it early. Run beside the
highlighting test it hung the run. The check now runs as a probe in a fresh bun test, started by
the test file, with its results read from a file rather than a pipe.
A folded run of reads and searches is labelled in full, "Read 1 file, searched for 2 patterns and
listed 1 directory", and the label could not shrink, so at phone width it ran 59px past the
column and was cut off mid-word. The label now wraps, and the detail beside it, already
truncated, gives way first.

@guidovizoso guidovizoso left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks, all seven are sorted, and the extra digging was worth it: the harness fix for refused writes, and catching that Shiki wasn't lazy. I checked the snapshot fix against ag-ui-claude-sdk 0.1.5. It builds each snapshot from the history the client sends plus the new turn, so a patched failure stays failed in later turns. The app tests and the harness tests pass locally too.

Approving. A few notes, none blocking:

  • Worth its own issue: you found that ag-ui-claude-sdk only sends the last message as the prompt, so Claude SDK Bots never get the standing role message. That means they're missing PROVENANCE_GUIDANCE and UNTRUSTED_GUIDANCE too, not just the component guidance. The second one matters for prompt injection.
  • Minor: while a Bash call's arguments stream in, a plain cat or ls shows as a full command card, then jumps into the reads-and-searches line once they arrive. It's brief, so fine to leave.
  • Cosmetic: in the groupToolSteps comment in tool-activity.tsx, a sentence got appended to an existing line, so it runs past the wrap width.

We'll do the full-width mode as a follow-up.

@davidmckayv
davidmckayv merged commit 3c15e79 into main Nov 2, 2026
19 checks passed
@davidmckayv
davidmckayv deleted the david/answers-genui-code branch November 2, 2026 21:36
davidmckayv added a commit that referenced this pull request Nov 2, 2026
Main is red: `proactive-connected-reads.integration.test.ts` fails on "a Bot on another framework checks in on OpenBot's own runner".

**Cause:** #929 and #940 each passed alone but don't compose. #940 put `COMPONENT_GUIDANCE` between the provenance and untrusted-content guidance in a Bot's standing role. #929's check-in runner (`onOpenBotRunner`) strips that guidance from a remote Bot's role before adding OpenBot's own, but only when the role ends with provenance then untrusted-content, which it no longer did. A harness Bot's check-in carried all three twice.

**Fix:** `STANDING_GUIDANCE` now matches what `standingRoleMessage` ends with. The test now checks that each of the three is said once.

**Checked:** the test fails on main and passes with the fix. The other server tests that use the standing role (130) pass, along with format, lint, server typecheck and test typecheck.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

2 participants