| 1 | # Subagent focus — one child, one conversation, one composer target |
| 2 | |
| 3 | Status: implemented on `work/v098-agent-focus-20260815` (TUI + runtime |
| 4 | contract), on by default, no settings key. Its acceptance surface is the actual |
| 5 | terminal: focus a worker, send a follow-up, confirm that the composer target and |
| 6 | transcript agree, and return to the main conversation. Historical automated PTY |
| 7 | captures were removed because they froze layout geometry instead of protecting |
| 8 | the runtime contract. |
| 9 | |
| 10 | ## What Claude Code does (the reference behaviour) |
| 11 | |
| 12 | Observed on Claude Code's terminal UI while it ran several forked subagents: |
| 13 | |
| 14 | - Below the composer, an **agent list**: one row per agent — `main` first, |
| 15 | then each child — shaped `<status dot> <kind> <live activity summary> |
| 16 | <elapsed> · ↓ <tokens>` (e.g. `○ fork Running fleet_setup unit tests |
| 17 | 25m 23s · ↓ 377.0k tokens`). The selected row carries a `❯` at the far left |
| 18 | and a filled `●` dot; the others show `○`. |
| 19 | - The footer hint chain reads `esc to interrupt · ← for agents · ↓ to manage` |
| 20 | in the same ` · ` dot-chain style as the rest of the shell. |
| 21 | - Selecting an agent switches the visible transcript to **that agent's full |
| 22 | conversation**, scrollable like the main one. |
| 23 | - The composer grows a **chip on its top-right border** naming the targeted |
| 24 | agent (its short description). Sending continues that agent with its context |
| 25 | intact (`SendMessage`), whether it is still running or already stopped — |
| 26 | a stopped agent is resumed on its own fork. |
| 27 | - Sending to a busy agent adds a trailing ` · 1 queued` counter (accent |
| 28 | colour) on its row until the agent takes the message at its next turn |
| 29 | boundary; the transcript keeps a small receipt line under the user's message |
| 30 | (`Message queued for delivery to <agent> at its next tool round.`), and, when |
| 31 | the permission classifier allowed something, a second line names it. |
| 32 | - `↓ to manage` opens a manager for the agents (stop, inspect, return). |
| 33 | |
| 34 | ## What Codewhale does now (default, no configuration) |
| 35 | |
| 36 | Codewhale already had the row grammar (`crates/tui/src/tui/work_surface/`, |
| 37 | Agents panel: `<mark> <role> <status> <objective> … <elapsed> · ↓ <tokens>`) |
| 38 | and a bounded per-agent transcript pager. This lane replaces the pager with a |
| 39 | **focus** model and adds the composer target and the follow-up contract. |
| 40 | |
| 41 | ### Surfaces |
| 42 | |
| 43 | | Surface | Behaviour | |
| 44 | | --- | --- | |
| 45 | | Agent list | The rail's Agents panel (`Alt+W`, `Alt+2`/`Alt+@`, or `←` on an empty composer). While a worker is focused every row gets a two-cell gutter and the focused row shows `❯` there. A running worker with follow-ups it has not yet taken shows ` · N queued` in the accent colour. | |
| 46 | | Focus | Enter on a row / click / Enter in `/agents` **focuses** the worker: its full transcript (durable artifact first, resident tail otherwise) is rendered with the ordinary history cells in the conversation area, headed by one line `● <name> · <status word>`. PageUp/PageDown/wheel scroll it exactly like the main transcript. `⌥V`/`Alt+V` opens that worker's Agent Details. | |
| 47 | | Composer | Chip `→ <name>` on the top-right border; empty hint `Message <name> · Esc returns to main`. Esc on an empty composer returns to the main conversation (rail selection is kept). | |
| 48 | | Footer | While workers exist the hint chain gains `← for agents · ↓ to manage` (ASCII-safe: `<- for agents · v to manage`), also on the settled `✓ done` strip. Words are `MessageId::FooterHintForAgents` / `FooterHintToManage`. | |
| 49 | | Manage | `↓` on an empty composer (or `/agents`) opens the register: `↑/↓ select · Enter focus · X stop · R refresh · F roster/setup · Esc close`. | |
| 50 | | Receipts | Sending while focused writes one system line in the **main** transcript — `Queued for <name>` — and echoes the message in the focused view until the child's own transcript carries it. The delivery outcome arrives as one line in the focused view: `Queued for <name>: it reads the message at its next round.`, `<name> had finished; continued on a new fork (<target>) …`, or `Could not deliver to <name>: <reason>`. Approval decisions keep Codewhale's existing approval receipts (Ask / Auto-Review / Full Access wording); no separate "classifier" line is invented. | |
| 51 | |
| 52 | ### Runtime contract (real work, not UI illusion) |
| 53 | |
| 54 | - `Op::FollowUpSubAgent { agent_id, text }` (TUI → engine) → |
| 55 | `SubAgentManager::continue_child_from_user`: |
| 56 | - **Running** child: text goes to its live input channel and is folded into |
| 57 | its next model round (`followup_child`). The manager counts it in |
| 58 | `queued_follow_up_counts()` until the loop takes it (`SubAgentInput:: |
| 59 | mark_taken`), which is what the rail's ` · N queued` shows via |
| 60 | `Event::AgentList { queued_follow_ups }`. |
| 61 | - **Interrupted or Completed** child with a continuable checkpoint: resumed |
| 62 | on a new agent id from the checkpoint plus the follow-up |
| 63 | (`resume_from_checkpoint_with_policy(InterruptedOrCompleted)`); the |
| 64 | terminal record stays an immutable receipt and `resume_targets` links the |
| 65 | fork. Focus follows the fork. The model-facing `agents/followup` keeps its |
| 66 | interrupted-only contract. |
| 67 | - **Failed / Cancelled / BudgetExhausted**: refused with the exact reason. |
| 68 | - `Event::SubAgentFollowUp { agent_id, outcome }` carries the receipt back. |
| 69 | - The engine builds the resume runtime from the installed session route |
| 70 | (`Engine::off_turn_subagent_runtime`), so a continued fork inherits the |
| 71 | session's provider, model, permissions posture, and denied tools. |
| 72 | |
| 73 | ### Keys |
| 74 | |
| 75 | | Key | Where | Effect | |
| 76 | | --- | --- | --- | |
| 77 | | `←` | empty composer, workers exist | enter the agent list (rail Agents panel; `/agents` register when the rail is off) | |
| 78 | | `↓` | empty composer, workers exist | open the manage register | |
| 79 | | `↑`/`↓`, Enter | rail (focused) or register | select / focus a worker | |
| 80 | | `X` | register | stop the selected worker | |
| 81 | | Esc | empty composer while focused | back to the main conversation | |
| 82 | | `⌥V` / `Alt+V` | while focused | that worker's Agent Details | |
| 83 | | PageUp/PageDown, wheel | while focused | scroll the worker's transcript | |
| 84 | |
| 85 | Tab is untouched: it never changes the message target. |
| 86 | |
| 87 | ### How this is the default |
| 88 | |
| 89 | Nothing to enable. Any session with children gets the hints, the list, focus, |
| 90 | and follow-ups. The one-agent-one-destination rule from v0.9.7 still holds: |
| 91 | every activation of an agent row lands on the same place — now the in-place |
| 92 | focus rather than a modal pager. Rail placement/panel settings are unchanged |
| 93 | (`rail_panel`, `work_surface_*`); a rail set to `off` still reaches everything |
| 94 | through `←`/`↓` and `/agents`. |
| 95 | |
| 96 | ### Hook points left for parallel lanes |
| 97 | |
| 98 | - Whale role badges: rail rows and the focus banner render the worker name |
| 99 | through `agent_focus::agent_display_label`; a badge can be prefixed there |
| 100 | without touching the focus logic. |
| 101 | - Compact tier (< 60 cols): the strip collapses; keep the focus banner and chip |
| 102 | to one line each and inspect the current product when this surface changes. |
| 103 | |
| 104 | ### Not done / follow-ups |
| 105 | |
| 106 | - No token counter for continued forks beyond what the runtime already |
| 107 | reports per worker (no invented numbers). |
| 108 | - The focused view re-reads the child's transcript at ~400 ms; a push-based |
| 109 | refresh from `SubAgentMailbox` events would be cheaper on very long chats. |
| 110 | - Re-focusing a worker after a session restart works from the durable |
| 111 | artifact, but focus itself is not persisted in the session snapshot. |
| 112 |