Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 31 additions & 2 deletions website/src/content/docs/reference/websocket.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Server→client **chat events** carry a common **`Routing`** block in `payload`:
| `projectSlug` | `string` | Workspace key — a project slug, or `""` for the root workspace. |
| `sessionId` | `string \| null` | Null until a brand-new chat's id first streams back. |
| `jobId` | `string \| null` | The cancellable job id, when known. |
| `seq` | `number?` | Per-turn monotonic sequence for reconnect/gap-replay. Absent on frames not stamped by the hub's `emit` — `chat:error`, `chat:resync`, `chat:active`, `chat:queued_flushed`, `chat:queued_state`, `chat:queued_returned`, `chat:killed_task`, `pong`. Those last four go out via `hub.broadcast`, which reaches the origin socket **and** every subscriber without seq-stamping or buffering, precisely so an out-of-band signal still reaches a client that reconnected on a new socket. |
| `seq` | `number?` | Per-turn monotonic sequence for reconnect/gap-replay. Absent on frames not stamped by the hub's `emit` — `chat:error`, `chat:resync`, `chat:active`, `chat:queued_flushed`, `chat:queued_state`, `chat:queued_returned`, `chat:killed_task`, `chat:background`, `pong`. Those last five (`chat:queued_flushed` through `chat:background`) go out via `hub.broadcast`, which reaches the origin socket **and** every subscriber without seq-stamping or buffering, precisely so an out-of-band signal still reaches a client that reconnected on a new socket. |

Client→server payloads carry `projectSlug`. Invalid JSON / unknown kinds get a `chat:error` reply.

Expand Down Expand Up @@ -65,7 +65,7 @@ any client ([#736](https://github.com/edspencer/paddock/issues/736)). Send `qid`

| Kind | When it fires | Payload (beyond `Routing`) |
|------|---------------|-----------------------------|
| `chat:active` | A session's live-turn status changed (start/stop); broadcast to all clients, and sent as a snapshot to a newly-connected or subscribing socket. | `sessionId: string`, `jobId: string \| null`, `running: boolean`, `startedAt?: number` (epoch-ms the turn began, from the hub — the only thing that knows, since a job record is written when a turn *ends* and the transcript's timestamps are the model's; present on the stop frame too, where it describes the turn that just ended. This frame carries its own `projectSlug`/`sessionId`, no `seq`) |
| `chat:active` | A session's live-turn status changed (start/stop); broadcast to all clients, and sent as a snapshot to a newly-connected or subscribing socket. Since [#604](https://github.com/edspencer/paddock/issues/604) it also fires when the session's background-task set flips it between busy and idle — but only when the answer actually changes, so a task starting or stopping mid-turn does not re-broadcast. | `sessionId: string`, `jobId: string \| null`, `running: boolean`, `turnRunning?: boolean`, `startedAt?: number` (epoch-ms the turn began, from the hub — the only thing that knows, since a job record is written when a turn *ends* and the transcript's timestamps are the model's; present on the stop frame too, where it describes the turn that just ended. This frame carries its own `projectSlug`/`sessionId`, no `seq`) |
| `chat:response` | A streamed assistant text delta. Also surfaces a `/compact` boundary as a synthetic note. | `chunk: string` |
| `chat:tool_start` | A tool_use begins (before it runs) — renders a pending "running…" row. | `toolName: string`, `inputSummary?: string`, `toolUseId?: string`, `parentToolUseId: string \| null`, `subagentType?: string`, `description?: string`, `hasSubagent?: boolean` |
| `chat:tool_call` | A tool completes (paired tool_use→tool_result); reconciles the pending row. | `toolName: string`, `inputSummary?: string`, `output: string`, `isError: boolean`, `durationMs?: number`, `toolUseId?: string`, `subagentType?: string`, `description?: string`, `hasSubagent?: boolean` |
Expand All @@ -78,6 +78,7 @@ any client ([#736](https://github.com/edspencer/paddock/issues/736)). Send `qid`
| `chat:queued_state` | The chat's queue slot was written. **Broadcast to every socket attached to the session**, so the queue is shared chat state that all clients render identically. | `projectSlug: string`, `sessionId: string`, `text: string \| null` (null ⇒ the slot is now empty), `attachments?: AttachmentRef[]`, `qid?: string` (adopt it, so your next edit updates this slot in place rather than appending beside it), `reason?: "returned"` |
| `chat:queued_returned` | A user pressed Stop, so the message queued behind that turn is handed **back** to them. Sent **only to the socket that issued `chat:cancel`**; the other clients get a `chat:queued_state` with `reason: "returned"` instead. | `projectSlug: string`, `sessionId: string`, `text: string`, `attachments?: AttachmentRef[]` |
| `chat:killed_task` | A background task the chat was waiting on was killed. Broadcast **live**, the moment the recovery engine detects it — otherwise the notification sits in the SDK input queue until some later turn flushes it, and the "Claude is idle / Continue" affordance only appears after a manual refresh. Rendered as the amber killed-task notice. Gated on `recovery.surfaceKilledTask`, which is **on by default**. | `projectSlug: string`, `sessionId: string`, `summary: string` (the killed `<task-notification>`'s `<summary>`, or a generic fallback), `timestamp: string` (ISO, used client-side to dedup replays) |
| `chat:background` | A session's set of live background tasks changed ([#604](https://github.com/edspencer/paddock/issues/604)). **A LEVEL frame with REPLACE semantics** — `tasks` is the complete set, and an empty array means "nothing is running". A client swaps its whole set rather than pairing start/stop edges, so a dropped frame cannot wedge a stale indicator. Broadcast on every membership change, and replayed to a newly-connected socket, so a remount or a reload learns what is in flight without polling. | `projectSlug: string`, `sessionId: string`, `tasks: LiveBackgroundTaskWire[]` |
| `chat:notice` | A turn dead-ended without a normal reply — a usage/subscription limit, the max-turns cap, or an error (network, API 5xx-overloaded, auth, crash). Emitted **inline during the turn** and session-routed like the other turn frames, so the chat says *why* it stopped instead of looking dead. | `notice: TurnNotice` (carries the reset time for a usage limit, and `retryable` for the Retry/Continue affordance) |
| `pong` | Reply to a client `ping`. | *(none)* |

Expand All @@ -86,6 +87,34 @@ any client ([#736](https://github.com/edspencer/paddock/issues/736)). Send `qid`
cacheCreation), `contextLimit` (= the model's context limit). Stale-by-one-turn by
design.

**`LiveBackgroundTaskWire`** (on `chat:background`): `id`, `type` (`shell` |
`subagent` | `monitor` | `workflow`, or a raw discriminant the server does not
recognise — treat an unknown value as a task, not as an error), `description`,
`startedAt` (epoch-ms), plus the optional `toolUseId`, `agentType`, `command`,
`workflowName`, `server`, `tool`, `lastToolName`, `toolUses` and `skipTranscript`.
Which of the optionals are populated depends on the kind of work: a shell carries
`command`, a workflow `workflowName`, a sub-agent `agentType`.

Nothing here is reconstructed from disk — the signal is per-process. After a
server restart the set is empty until the next change, which is correct: Paddock
stops the fleet with `waitForJobs: false`, so those tasks are dead.

:::note[`running` and `turnRunning` answer different questions]
Since `chat:background` exists, `chat:active.running` is true whenever a **model
turn** is in flight **or** the session is holding live background work. That is
what status readouts want — the sidebar dot, the [fleet
readout](/using/working-in-chats/#what-the-whole-fleet-is-doing), the Home
in-flight badge, the running-only filter — because a chat with a `Monitor` in it
genuinely is busy.

`turnRunning` is the narrower signal: a model turn, and nothing else. The web
client gates the composer lock and the working indicator on it, because a
background task can run for an hour and locking the composer for that hour would
misdescribe what is happening. It is **optional on the wire**, so a client built
against an older server still parses the frame; such a client should fall back to
`running`, which is what it read before.
:::

:::note[This page documents what the server sends, not what the types say]
Two places in the code currently understate the protocol, so neither is a safe
source to "correct" this page against:
Expand Down
120 changes: 90 additions & 30 deletions website/src/content/docs/using/reading-claudes-work.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Reading Claude's work
description: How to review what Claude actually did — richly rendered tool calls (diffs with line numbers, images, exit codes, search counts, background jobs), live nested sub-agent cards, Paddock's own tools as first-class UI, the context + cost meter, pinned file tabs, and the Files and Changes tabs for browsing and committing what the agent produced.
description: How to review what Claude actually did — richly rendered tool calls (diffs with line numbers, images, exit codes, search counts, background jobs), live nested sub-agent cards, a pinned bar of everything still running, Paddock's own tools as first-class UI, the context + cost meter, pinned file tabs, and the Files and Changes tabs for browsing and committing what the agent produced.
---

import DemoVideo from '../../../components/DemoVideo.astro';
Expand Down Expand Up @@ -101,40 +101,74 @@ spawned by another sub-agent) is still running it shows as a generic row; it
picks up its real type, steps, duration and cost within a poll of finishing.
:::

### Running sub-agents stay in view

A sub-agent can work for minutes behind a collapsed card, and by the time it does
the transcript has usually scrolled it off the top of the window. So while any
sub-agent is running, Paddock pins a **live bar just above the message box**
listing each one — headed `2 sub-agents running` — and it stays there no matter
how far the transcript has moved on.

Each row names the sub-agent's **type** (e.g. `general-purpose`), its
**description**, and — the useful part — **what it is doing right now**, refreshed
from that sub-agent's own transcript every couple of seconds, with a running
**step count** on the right. A row that hasn't reported yet reads `starting…`.

**Tap a row** and Paddock scrolls the transcript to that sub-agent's card, expands
it, and pulses it briefly so you can see which one it landed on. The bar
disappears by itself when the last sub-agent finishes, so an ordinary turn looks
exactly as it always did.

The same live step doubles as the **collapsed card's subtitle** while the
sub-agent works — so a card scrolled past in the transcript reads as *what it's
doing*, not a description written minutes ago. It reverts to the description once
the sub-agent is done.
### What is still running stays in view

Work can outlive the turn that started it, and by then the card that launched it
has usually scrolled off the top of the window — so there is nowhere to look to
find out whether anything is still happening. Paddock's answer is a **live bar
pinned just above the message box**, listing every piece of running work: not
only sub-agents, but background shells, `Monitor`s and workflows too. It stays
there no matter how far the transcript has moved on, and it renders nothing at
all when nothing is running, so an ordinary turn looks exactly as it always did.

The heading stays specific when it can: `2 sub-agents running` while sub-agents
are the only kind of work in the bar, and `3 things running` only on a genuinely
mixed one.

Each row carries a short **bold label** — the sub-agent's type (e.g.
`general-purpose`), the workflow's name, the monitored tool, or just the kind of
work — then a wide middle column saying **what it is doing right now**, then a
step count. For a background shell that middle column is the command it is
running; for anything else it's the last tool it used, falling back to its
description. A row that hasn't reported yet reads `starting…`. Rows sourced from
the server also carry their own **elapsed clock**.

**Tap a row** and Paddock scrolls the transcript to that piece of work's card,
expands it, and pulses it briefly so you can see which one it landed on.

The bar merges **two sources**, and the difference is worth knowing because it
shows:

- **Sub-agents** come from the transcript, which is where the richer detail is:
the live current step, refreshed from that sub-agent's own transcript every
couple of seconds, and a climbing step count. That same live step doubles as
the **collapsed card's subtitle** while the sub-agent works — so a card
scrolled past in the transcript reads as *what it's doing*, not a description
written minutes ago. It reverts to the description once the sub-agent is done.
- **Everything else** — background shells, monitors, workflows — comes from the
server, which now tracks live background work per session and pushes it to the
browser as it changes. That is the first time any of them has had **liveness**
at all. Before it, a background `Bash` or a `Monitor` rendered a static
`running` chip that meant only *no completion notification was found in the
transcript* — not "we checked, and it is still going". A killed task kept that
chip forever.

A sub-agent the transcript has already found is never drawn twice; one it hasn't
found yet still appears, which is what makes reloading mid-run honest. Work the
runtime marks as ambient housekeeping is deliberately left out — the bar is for
work you asked for.

Two limits worth stating so they don't read as bugs. **Not every row is
tappable**: Paddock can only jump to a card it can identify, and a background
shell launched several turns ago may carry nothing tying it to one — those rows
render as plain rows rather than buttons. And the bar is **per chat**, showing
this chat's work; for what the rest of the instance is doing, the [fleet
readout](/using/working-in-chats/#what-the-whole-fleet-is-doing) sits across the
top of every route.

<DemoVideo
src="/demo/subagent-bar.mp4"
poster="/demo/subagent-bar-poster.jpg"
alt="A Paddock chat spawns two research sub-agents. A bar above the message box lists both, each showing its live current step and a climbing step count. Clicking a row scrolls to that sub-agent's card and expands it, revealing its nested steps arriving in real time. The keeper writes a haiku while the two agents keep working, and when they finish the bar disappears and the cards show their real durations of 2m 7s and 2m 32s."
caption="Two sub-agents researching in parallel. Sped up 3x — the real turn took about three and a half minutes."
caption="Two sub-agents researching in parallel. Sped up 3x — the real turn took about three and a half minutes. Recorded before the bar generalised beyond sub-agents, so it shows the sub-agent rows only."
/>

:::note[Only the ones it can see]
The bar tracks sub-agents spawned by **the turn you are watching**. A sub-agent
one level deeper — one that another sub-agent spawned — shows up inside its
parent's expanded card rather than as its own row.
:::note[Only the sub-agents it can see]
The *sub-agent* rows track sub-agents spawned by **the turn you are watching**. A
sub-agent one level deeper — one that another sub-agent spawned — shows up inside
its parent's expanded card rather than as its own row. That limit is specific to
those rows: a background shell or monitor is reported by the server whether or
not it belongs to the turn on screen, including one that outlived it.
:::

### Background work outlives the turn
Expand All @@ -143,8 +177,34 @@ Since **v0.43**, work Claude kicks off in the background on a session-mode chat
— a `run_in_background` shell, a background sub-agent, a long build — keeps
running when the turn that started it ends, including on a brand-new chat's very
first turn. When it finishes, the follow-up turn **streams into the open chat
live**, with no refresh. So a card that says `running` when the turn ends is
genuinely still working, and you'll see the result land in place.
live**, with no refresh.

The bar above is the live answer to "is it still going?", and the chat is counted
as **busy** while it holds background work — the sidebar dot stays lit, the chat
keeps its channel in the fleet readout, and it still matches the running-only
filter. What *doesn't* stay locked is the composer: a `Monitor` can run for an
hour, and locking the message box for that hour while animating a "working"
indicator would both make the chat unusable and misdescribe what is happening.

:::caution[Sending during a background phase is not fixed yet]
The composer is unlocked, but the send path behind it is not yet ready for this
case. Paddock resumes the chat's session to run your message, and it can't do
that while the background work is still holding that session open — so the turn
starts, the working indicator animates, and nothing streams, until the background
work finishes or an internal five-minute ceiling expires (after which the resume
goes ahead and can interrupt itself). **Stop** doesn't help in that window
either: it waits on a job id that hasn't been minted yet.

There is also no way to stop *only* the background work. Stop appears while a
model turn is in flight, and it ends the session — taking the background work
with it; once the turn has returned, the background work has no control of its
own and runs until it finishes.

Both are tracked in
[#806](https://github.com/edspencer/paddock/issues/806). Until it lands, the
practical advice is to let a chat's background work finish before writing into
it.
:::

:::caution[Not every background death is fixed]
A separate, upstream failure mode still exists — the underlying Claude Code
Expand Down
Loading