Skip to main content
ClaudeWave
Install in Claude Code
Copy
git clone --depth 1 https://github.com/anthropics/claude-quickstarts /tmp/assistant-ui && cp -r /tmp/assistant-ui/managed-agents/assistant-ui ~/.claude/skills/assistant-ui
Then start a new Claude Code session; the skill loads automatically.

skill.md

# Setup tips and tricks: assistant-ui × Claude Managed Agents

Things that aren't obvious from the docs and tend to cost debugging time. If you're Claude driving setup for a user, walk the **Setup checklist** and use the rest for reference when something looks off.

---

## Mental model

### The session is the state
There is no database, no message store, no in-memory transcript. The sidebar is `sessions.list()`, an old chat is `sessions.events.list()` replayed, and the live chat is the same reducer folding the tail. If you catch yourself adding a table "just to remember the conversations," you're re-implementing what the session already is. Restart the dev server mid-conversation and reload: the transcript comes back, because it never lived here.

### One pure function joins the two products
`lib/managed-agents/reducer.ts` maps events to assistant-ui messages and knows nothing about React or fetch. Every rendering question ("why is the gate not showing", "why did the text duplicate") is answered by which branch of `applyEvent` handled which event. The unit test (`npm test`) folds the golden turn from the wire protocol; when the mapping changes, change the test first.

### Stream first, then send
A Managed Agents stream only delivers events emitted after it attaches. The browser opens `/api/sessions/[id]/stream` before it posts a message, and keeps that one tail open across turns and across the pause while an approval waits. `session-controller.ts` enforces this order; don't move the send in front of the attach.

### The approval gate is a stopped session, not a UI state
When bash needs approval the session emits `session.status_idle` with `stop_reason: { type: "requires_action", event_ids: [...] }` and *waits*. The buttons render because the reducer stamped `approval: { id }` onto the tool part from that event, not because a component set a flag. That's why reloading with the gate open works: the pending request is in the event log.

### Never trust a conversation ID
The flip side of "the browser sends a session ID" is that the browser sends a session ID. It becomes an API path parameter, and the server's Anthropic credentials can see every session in the workspace, including other agents' sessions. `ownedSession()` in `lib/owned-session.ts` is the gate every session-touching route goes through: the ID must look like an ID, resolve, belong to this quickstart's agent, carry the metadata tag, and not be archived. The file download route adds a second check: the file must belong to *that* session. If you fork this, keep both checks in the path of anything that takes an ID from a client.

---

## Gotchas

### Token previews are gated per org
`event_deltas` streaming is part of the 2026-07-01 Managed Agents update and rolls out per organization. Without the gate, the parameter is silently ignored: no `event_start` arrives and replies land whole. That is not a bug; the same code path handles it. Confirm which behavior you have before "fixing" streaming.

### `agent.thinking` has no text
It's a start-of-work signal, not the reasoning content. The reducer renders an empty reasoning part on purpose. Don't wire a component that displays `thinking.text`; there isn't one.

### Batch confirmations, and the id is the *event* id
`tool_use_id` in `user.tool_confirmation` is the `sevt_...` id of the `agent.tool_use` event, taken from `stop_reason.event_ids`. If several tools block at once, answer them in **one** `events.send`; the controller debounces clicks into a batch for exactly this reason, because a second racing send can be rejected with `400 no non-archived thread is waiting on tool_use_id`. If you hit that 400, refetch the latest `requires_action` and answer only what's still listed.

### Custom tools take results, not confirmations
`show_chart` is answered with `user.custom_tool_result` keyed by `custom_tool_use_id`. Sending `user.tool_confirmation` for a custom tool id is a 400. The controller answers `show_chart` automatically the moment it renders; a tab closed mid-turn just means the answer goes out when the session is next opened.

### A long-parked approval can resume into a cold sandbox
The gate waits forever, but the sandbox underneath doesn't stay hot forever. Leave a `bash` approval sitting for several minutes and the first execution after Allow can come back as `Tool execution was interrupted by a crash. Please retry.` This is expected: the tool result carries the error, the agent says "let me retry that" and re-issues the command (a second, fresh gate), and the warm sandbox runs it. The UI renders the failed attempt in red above the retry, which is the honest transcript. Don't paper over it; do point demo audiences at it as the recovery path working.

### Stop abandons the response, not the turn
Closing the tab or navigating away only drops the SSE response; the agent keeps working server-side and the reply is waiting in history when you come back. The composer's stop button sends a real `user.interrupt`. Don't "fix" a slow turn by resending the message; you'll queue a duplicate.

### Uploads mount under `/mnt/session/uploads/`
`mount_path` on `sessions.resources.add` is the absolute in-container path (its default is `/mnt/session/uploads/<file_id>`). The composer mounts an attachment at `/mnt/session/uploads/<file_id>/<name>` — the file id keeps repeat uploads of the same filename from colliding, the basename keeps it readable — and the message tells the agent that exact path; the mounted path and the announced path must always be the same string. Deliverables belong in `/mnt/session/outputs/` to show up as download chips.

### `files.list({ scope_id })` may not be enabled everywhere
Listing a session's produced files by `scope_id` is newer than the rest of the API. `lib/managed-agents/session-files.ts` catches the failure and still returns the mounted uploads, and downloads of anything not in the session's file set stay refused. If download chips never appear after the agent writ