返回 CodeWhale
SUBAGENT_FOCUS.md
根目录 / docs / design / SUBAGENT_FOCUS.md
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
112 lines MARKDOWN