diff --git a/website/src/content/docs/reference/websocket.md b/website/src/content/docs/reference/websocket.md index ab4826d2..ce6c6101 100644 --- a/website/src/content/docs/reference/websocket.md +++ b/website/src/content/docs/reference/websocket.md @@ -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. @@ -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` | @@ -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 ``'s ``, 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)* | @@ -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: diff --git a/website/src/content/docs/using/reading-claudes-work.mdx b/website/src/content/docs/using/reading-claudes-work.mdx index 9df83f0a..6afb1863 100644 --- a/website/src/content/docs/using/reading-claudes-work.mdx +++ b/website/src/content/docs/using/reading-claudes-work.mdx @@ -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'; @@ -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. -:::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 @@ -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 diff --git a/website/src/content/docs/using/working-in-chats.md b/website/src/content/docs/using/working-in-chats.md index 034bfdad..562bd4c4 100644 --- a/website/src/content/docs/using/working-in-chats.md +++ b/website/src/content/docs/using/working-in-chats.md @@ -1,6 +1,6 @@ --- title: Working in chats -description: A hands-on walkthrough — start a chat, adopt your existing terminal claude history, understand project chats vs root chats, resume from anywhere, use the composer and message queue, Stop a turn, rewind or fork from any message, and keep a growing chat list legible with unread dots, stars, search, and archive. +description: A hands-on walkthrough — read the fleet readout, start a chat, adopt your existing terminal claude history, understand project chats vs root chats, resume from anywhere, use the composer and message queue, Stop a turn, rewind or fork from any message, and keep a growing chat list legible with unread dots, stars, search, and archive. --- A **chat** is where you actually work in Paddock — one conversation with an @@ -13,7 +13,56 @@ By the end you'll know how to start a chat, tell **project** chats from **root** ones, pick a conversation back up from any device, drive the composer and its message **queue**, **Stop** a running turn, **fork or rewind** from any message, and keep a growing sidebar legible with **unread** dots, **stars**, the -per-chat **context + cost** meter, **search**, and **archive**. +per-chat **context + cost** meter, **search**, and **archive**. First, though, +the strip that is on screen the whole time. + +## What the whole fleet is doing + +Before any of that, one thing is worth knowing because it is on screen the whole +time: the **fleet readout**, a thin strip pinned across the top of *every* route. +It is not a Home feature — it is there in a maximised chat, in Files, in Config — +because "what is running, and for how long?" is a question you ask from wherever +you happen to be. + +On the left are two counts, both fleet-wide and both links to Home: + +- **running** — turns in flight across every project. +- **unread** — chats holding a reply you haven't read. + +They are the same numbers as the sidebar badges, computed once and shared, so the +strip and the badges two inches to its left cannot disagree. + +To the right of them is one **channel per running turn**: the project's name, an +**elapsed clock**, and a small six-segment gauge of that chat's **context fill** +(amber past 75%, red past 90%; absent entirely on a chat that has never completed +a turn, because an empty gauge would claim a measurement that doesn't exist). +Hovering a channel names the chat; clicking one takes you straight to it. Two of +those readings existed nowhere else in the UI before: how long a turn has been +going — a forty-minute turn and an eight-second one looked identical — and how +much context it is burning, which you previously had to open the chat to see. + +- **It is bounded, and honest about it.** Only as many channels as fit get a + strip — three on a wide window, two at medium, one on a phone — and the rest + collapse into a `+N` link to Home. The longest-running turn keeps its channel, + on the theory that it is the one most likely to be wedged. The counts on the + left are always exact whatever fits. +- **It keeps its height when the fleet is idle** rather than collapsing, and says + `Idle · last turn 20m ago` — or, on an instance that has never run a turn, + `No turns yet — start a chat →`. A readout that disappears cannot be told from + a readout that broke. +- **The only thing that animates is the clocks**, deliberately. The counters + advancing *are* the running indicator; a pulsing lamp on top of them would say + the same thing twice. +- **It costs nothing at rest.** Which turns are running comes from the live + socket updates Paddock already broadcasts, so the strip paints on a first load + with no chat open and survives a reload mid-turn. The one request it makes — + for chat names and context fill — is armed only while a turn is actually in + flight, so an idle instance fetches nothing and schedules no timers. + +A chat counts as **running** here while it holds live [background +work](/using/reading-claudes-work/#what-is-still-running-stays-in-view) too, not +only while the model is mid-reply — a `Monitor` or a background dev server keeps +its channel lit after the turn that started it has returned. ## Start a chat