feat(agent-ui): size views by their content, and stop re-teaching the state rules - #149
Merged
Merged
Conversation
Every view iframe was a fixed 15rem box. A one-line answer wasted it and a long table was trapped inside it. The MCP Apps protocol already carries the fix. `ui/notifications/size-changed` is part of the spec, and `@modelcontextprotocol/ext-apps` sends it on its own from a `ResizeObserver` on the document — so no view needs code for this, and no custom message has to be invented beside the protocol. Both ends of the runtime ignored it; the host end now acts on it. `autoResize` is the SDK's own default, set explicitly here: it is the whole reason a view's height is not a number somebody picked, and a default that decides that much is worth being able to read in the bridge rather than inferring it from the SDK. Verified end to end in a browser against two built bundles: a one-line view asks for 72px and the example clip view for 243px, both clamped and applied. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Seven changes from a fork of this client that had been running against a real toolset, kept where they earn their room. The frame of a `ui://` view now follows the view's own `size-changed`, clamped between a line of text and a screenful. Views are drawn where an answer is drawn — a sibling of the messages — rather than folded inside the receipt that announced them, since a view *is* the tool's answer. They keep the hover link to the call that made them. Raw tool results and receipts move behind a `debug` switch, off by default: they answer "is the view lying?", which is not a question a normal turn raises. A `clear` switch beside it starts a new thread, which is how a conversation gets empty session state; nothing is deleted and the old thread keeps its `?thread=` URL. The state panel starts collapsed to a spine, because it is a reference rather than the output. While a run is in flight a pill above the composer names the call, rotating when several are in flight at once, so the gap between a question and its answer is not silent. What it announces is not what it draws: a live region carrying a name that changes every 1.8s would be read aloud every 1.8s, so the count is what is spoken and the rotation is for the eye. A call's arguments are drawn with nested stringified JSON parsed back, since a provider that encodes one structured argument as text does it at any depth and the escaping is not what the model wrote. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A toolset ships views for some tools and not others, and with the raw payloads behind `debug` the ones without had nothing on screen at all — only the model's account of what the tool said, which is the one reading a reader cannot check. So a result whose call drew no view is now drawn in the transcript, in the column and the position a view takes, indented as JSON where it is JSON and left alone where it is prose. Nested stringified JSON is parsed back, for the reason it is in a call's arguments. Open, because it is the answer; foldable and capped, because a tool that returned a hundred rows should not cost the reader the scrollback to get past it. A call that *did* draw a view is unchanged — its answer is on screen in the form the toolset built for it, and the text of it stays `debug`'s business rather than a second copy underneath. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The card led with a badge and a line of metadata wrapped around the key, and then said "written by the model" once per argument. Three changes, one idea: say each thing once, in the place a reader is already looking. The key is now the whole of the card's first line. What was beside it becomes a row of chips under it — new, the size, and how many turns hold a value — because those are facts about the key rather than part of it. The tool is not among them: a key is `<toolset>/<tool>/<field>`, so naming it there printed it twice. The repeated sentence goes, and the colour it was spelling out stays: amber for a value the model wrote, blue for one that came from another key. A legend at the top of the panel is where the words are now spent, once, and it is written in those two colours rather than beside swatches of them — the sample and the legend are the same object, so there is nothing to carry across. That leaves amber and blue meaning exactly one thing each in this panel, so the chips take none of it and are told apart by weight instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two changes that pull the same way. `debug` is gone, and the state panel does what it did. They were one question asked twice: the panel and the receipts describe the same thing from two ends — what the tools exchanged without the model reading it — and nobody wants one without the other. Closed, the page is a chat; open, it is the wire. And a tool's result is now drawn for every call, not only for the calls whose toolset shipped no view. Gating it on that said a result matters less when someone wrote a view for it, which is backwards: a view is a nicer reading of a result, not a more important one, and a toolset that wrote views for three tools and not the fourth has not thereby ranked them. Where there is a view, both are on screen — the drawing and the thing it was drawn from. The block is capped at the 18rem the folded blocks elsewhere use, since every call now draws one, and it still folds and still drags open. The summary says "result" whether or not the content parsed as JSON. Which of the two it is was never worth a word: it is the first thing visible in the block underneath. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The two colour swatches go. They named what the colours mean — blue for what AG-UI gives any client, amber for what this runtime adds — which is a fact about the wire rather than anything a reader of a conversation is deciding between, and it sat in the one row where the controls are. What is left is sorted by what it is. The thread id joins the name, because it says what the page is showing rather than offering something to do about it, and it keeps its own element so a long title ellipsises without taking the id with it. The right end of the row is now buttons and nothing else, which is what `clear` needed to be read as one. Results are folded by default. Every call draws one, so opening them all put the answer a screen below the question that earned it; the model's reply already says what the result was, and this is where you go to check that reply against the thing it is about. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The breadcrumb carried two things: which keys a tool just wrote, and how to
use a key. Only the first is news about the call. The second — a read takes
the bare key, a handle goes only on a parameter whose schema accepts one — is
a standing rule that SESSION_STATE_PROMPT already states, and a capture is the
message a long run accumulates most of, so the note paid for that rule again
every time a tool published anything.
[state updated: sketch-ops/sketch_area/area_km2,
sketch-ops/sketch_area/boundary — pass the bare key to inspect_state to
read one; pass @State:<key> only to a tool parameter whose schema accepts
it]
becomes
[state updated: sketch-ops/sketch_area/area_km2,
sketch-ops/sketch_area/boundary]
The overwrite clause stays: that a write displaced what an earlier turn left
is news about this call, and it reaches the model nowhere else.
The test that asserted the wording is replaced by two: one pinning the note to
the keys alone, and one pinning the rules to the prompt — which is the other
half of the trade, and would otherwise be the thing nobody notices going
missing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The note said the write displaced an earlier value *and* how to go and read
it. The first is news about this call and reaches the model nowhere else; the
second is the `turn` argument's own contract, repeated into every capture that
overwrote anything.
[state updated: gazet/get_aoi/bbox. This replaces what gazet/get_aoi/bbox
held at turn 2, which is still readable with inspect_state(key, turn=2)]
becomes
[state updated: gazet/get_aoi/bbox. This replaces what gazet/get_aoi/bbox
held at turn 2]
The turn number is the whole of what the note has to hand over, so the `turn`
argument now documents what to do with one. It names all three surfaces that
produce a number — the capture note's "replaces what <key> held at turn <n>",
a read reporting that several turns wrote the key, and a listing marked
"written in N turns" — since with the tail gone, a model holding a turn number
from any of them has this description and the prompt and nothing else. It also
says what to do when a turn is no longer retained, which is the answer a model
gets wrong by default: it answers from the current value instead.
Two tests pin the trade. One holds the note to the fact alone; the other holds
the description to naming all three sources, so trimming it cannot quietly
take the last statement of the rule with it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
yellowcap
force-pushed
the
feat/port-agent-ui-improvements
branch
from
September 18, 2026 09:47
2793ede to
80181fd
Compare
ciaransweet
requested changes
Sep 18, 2026
…he composer The pill above the composer is gone. While a run waits, three dots show where the wait is: under a tool call that has no result yet, or at the end of the log while the model has the turn. They hide while the answer streams, because the caret already shows that. With reduced motion, the dots fade in place instead of hopping. The state panel legend now reads "a tool produced it" instead of "another key", from review. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ciaransweet
approved these changes
Sep 18, 2026
yellowcap
pushed a commit
that referenced
this pull request
Sep 18, 2026
🤖 I have created a release *beep* *boop* --- ## [0.10.0](mcp-toolsets-runtime-v0.9.1...mcp-toolsets-runtime-v0.10.0) (2026-09-18) ### ⚠ BREAKING CHANGES * the `[web]` extra, the `mcp_agent.web` module, the `mcp-agent-web` entry point, the packaged `McpView.jsx` element and the `mcp-agent install-elements` command are removed. A deployment that served the Chainlit chat should serve `mcp_agent_api.app` instead, which ships its own web client; a host that wants Chainlit specifically can build one against the HTTP API. `AgentSettings` also loses `chainlit_port`. ### Features * **agent-ui:** size views by their content, and stop re-teaching the state rules ([#149](#149)) ([454eb10](454eb10)) * **agent:** let the agent ask the user with an interrupt tool and AG-UI interrupts ([#147](#147)) ([09e8c14](09e8c14)) * remove the bundled Chainlit host ([#140](#140)) ([b0c0922](b0c0922)) ### Documentation * plain English, and fix what the docs get wrong ([#139](#139)) ([289a6fb](289a6fb)) * **toolset:** ship a toolset-authoring skill with the package ([#138](#138)) ([1e8be98](1e8be98)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: ds-release-bot[bot] <116609932+ds-release-bot[bot]@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary. Views now size themselves to their content instead of sitting in a
fixed 15rem box. Every tool result is in the transcript, folded, drawn the same
way whether or not the tool ships a view. The
debugtoggle is gone — openingthe session-state panel shows the receipts. And session-state notes stop
repeating rules that the system prompt already gives.
View sizing: there was no protocol gap
MCP Apps defines
ui/notifications/size-changed, andext-appsalready sendsit from a
ResizeObserver. Both of this runtime's hosts were discarding anotification they already received. So this is a host-side fix. No view needs
code, and no custom message is added beside the protocol.
Every published view bundle gets this on upgrade — the example bundle here was
built last August and already emits the notification.
Verified in a browser: a one-line view asks for 72px, the example clip view for
243px. Both are clamped to 44–1000 and applied.
Client
result yet, or at the end of the log while the model thinks. They hide
while the answer streams, because the caret shows that. With reduced motion,
the dots fade and do not move.
clearstarts a new thread. Nothing is deleted; the old thread keeps its URL.means a tool produced it, and a legend says so once.
One bug fixed rather than ported: a call left unsettled by a failed run was
shown as running in every later run. It now reads from the running turn.
Session-state notes
The capture note said which keys were written and how to use a key. Only the
first is news. A capture is the message a long run accumulates most of.
The overwrite clause keeps its fact (
This replaces what <key> held at turn 2)and drops its instruction.
inspect_state'sturnargument now documentswhere a turn number comes from, and what to do when a turn has been pruned.
Tests pin both halves: the notes carry facts only, and the prompt and tool
description still state the rules.
Checks
scripts/build-js, 543 Python tests, ruff, mypy. Each client state was checkedagainst a real browser render.
Merge order
ext-appsto 2.0.0. View sizing rests on that package'sautoResizedefault. Whichever PR lands second must re-check it.docs/CONSUMING.mdlineabout
McpView.jsx, which will conflict.🤖 Generated with Claude Code