The conversational surface, in the editor rather than on its own page. The reason is the feedback loop: watching the canvas change as the agent works IS the confirmation, which is exactly what makes "act freely without asking first" tolerable. It also means the agent never has to guess which garden you mean. Tool calls surface as they happen, in the app's own vocabulary — "Clearing a bed", "Looking up a plant" — not raw tool names or JSON. That is the difference between the panel feeling like it's doing something and feeling like it's hung, which matters because a replant makes a dozen calls over tens of seconds. An unknown tool degrades to readable words rather than showing snake_case at the user, so the client can lag the server by a tool without looking broken. The canvas refreshes as each step lands, not just at the end. Refreshing only on completion would put the whole point of siting the chat here — watching it work — behind the same wait that streaming exists to remove. Undo sits on the turn itself, so the common case never involves opening the History panel. It uses #49's useUndo, not a second implementation, which meant giving that hook an UndoTarget: the chat knows a turn's change set id but not its tally, and fabricating counts to satisfy the type would have produced a confidently wrong "1 of 1 changes undone". describeUndo now says "Partly undone" when it has no denominator rather than inventing one. The panel is only offered when the instance actually has the assistant configured, via a new /capabilities read. The routes 404 without a key, so without this the client would have to probe for a 404 to find out — and a tab that opens onto an apology is worse than no tab. Streaming is hand-rolled over fetch rather than EventSource, which can only issue GETs and this needs a POST body. The wire format is still SSE, so a proxy that understands it doesn't buffer and the server wouldn't change if EventSource became viable. Partial frames are buffered across chunks and a malformed frame is skipped rather than killing a working stream. Errors read as sentences, and as different sentences: a permission refusal, a timeout and a model failure want different reactions. Every failure path refreshes, because something may have landed before it failed — and says where to look for it. Closes #57 Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01H3zbym8Doka2d7D48maSgZ