diff --git a/crates/mcp-config/src/codex/activity.rs b/crates/mcp-config/src/codex/activity.rs index 1e1cb18..0b74c83 100644 --- a/crates/mcp-config/src/codex/activity.rs +++ b/crates/mcp-config/src/codex/activity.rs @@ -6,7 +6,8 @@ //! thread is doing, and this turns that into the one word the participant //! panel's row has room for, and the longer form the line under the room says. //! Nothing here reads the terminal, a command line, a tool's arguments or any -//! message body: only an item's kind and, for a tool, its server and name. +//! message body: only an item's kind and, for a tool, its server and name, +//! and for a sub-agent call, which operation it is (#406). //! //! **Method names and shapes are the generated schema's** (`codex app-server //! generate-json-schema`, Codex CLI 0.160.1, with and without @@ -16,7 +17,7 @@ //! |---|---| //! | `thread/status/changed` | `threadId`, `status.type` (`active` / `idle` / `systemError` / `notLoaded`), `status.activeFlags` (`waitingOnApproval` / `waitingOnUserInput`) | //! | `turn/started`, `turn/completed` | `threadId` | -//! | `item/started`, `item/completed` | `threadId`, `item.id`, `item.type`; `item.server` / `item.tool` (`mcpToolCall`), `item.namespace` / `item.tool` (`dynamicToolCall`) | +//! | `item/started`, `item/completed` | `threadId`, `item.id`, `item.type`; `item.server` / `item.tool` (`mcpToolCall`), `item.namespace` / `item.tool` (`dynamicToolCall`), `item.tool` (`collabAgentToolCall`, Codex CLI 0.162.1, #406) | //! | `thread/closed` | `threadId` | //! //! The start and resume answers carry `thread.status` in the same shape @@ -89,12 +90,17 @@ enum Kind { Compaction, ImageGeneration, ImageView, + /// A sub-agent call of the seat's own thread (`collabAgentToolCall`, + /// #406), by its operation (`spawnAgent`, `wait`, ...) when named. + Delegate(Option), } impl Kind { - /// The kinds shown. Every other item — a message, reasoning, a plan, a - /// sub-agent — is not a state this says: the first three are what 考え中… - /// and 出力中 already cover, and a sub-agent's activity was not measured (#324). + /// The kinds shown. Every other item — a message, reasoning, a plan — is + /// not a state this says: those are what 考え中… and 出力中 already cover. + /// A sub-agent is said by the parent's own call to it (`collabAgentToolCall`, + /// #406); what the sub-agent's thread does is not this seat's and is dropped + /// with every other thread's notifications. fn of(item: &Value) -> Option { let name = |first: &str| { let tool = text(&item["tool"])?; @@ -112,6 +118,7 @@ impl Kind { "contextCompaction" => Kind::Compaction, "imageGeneration" => Kind::ImageGeneration, "imageView" => Kind::ImageView, + "collabAgentToolCall" => Kind::Delegate(text(&item["tool"])), _ => return None, }) } @@ -127,6 +134,8 @@ impl Kind { Kind::Compaction => "要約中", Kind::ImageGeneration => "画像生成", Kind::ImageView => "画像参照", + // The Claude seat's word for the same state (#406). + Kind::Delegate(_) => crate::hook_activity::SUBAGENT_WORD, } } @@ -141,6 +150,8 @@ impl Kind { Kind::Compaction => "要約中".into(), Kind::ImageGeneration => "画像生成中".into(), Kind::ImageView => "画像参照中".into(), + Kind::Delegate(Some(tool)) => format!("サブエージェント実行中({tool})"), + Kind::Delegate(None) => "サブエージェント実行中".into(), } } } @@ -542,6 +553,42 @@ mod tests { assert_eq!(a.display(), None); } + fn collab(tool: &str) -> Value { + json!({"type": "collabAgentToolCall", "tool": tool, "senderThreadId": T, "receiverThreadIds": ["sub-1"], "prompt": "secret task", "status": "inProgress"}) + } + + #[test] + fn a_sub_agent_call_says_delegating_until_it_completes() { + let mut a = seat(); + a.feed(&started("g1", collab("spawnAgent"))); + let d = a.display().unwrap(); + assert_eq!((d.word, d.line.as_str(), d.waiting), ("委任中", "サブエージェント実行中(spawnAgent)", false)); + assert!(!d.line.contains("secret")); + a.feed(&completed("g1")); + assert_eq!(a.display(), None); + a.feed(&started("g2", json!({"type": "collabAgentToolCall", "status": "inProgress"}))); + assert_eq!(words(&a), Some(("委任中", "サブエージェント実行中".into()))); + } + + #[test] + fn approval_outranks_a_sub_agent_call() { + let mut a = seat(); + a.feed(&started("g1", collab("wait"))); + a.feed(&status(json!({"type": "active", "activeFlags": ["waitingOnApproval"]}))); + assert_eq!(words(&a), Some(("許可待ち", "許可待ち".into()))); + a.feed(&status(json!({"type": "active", "activeFlags": []}))); + assert_eq!(words(&a), Some(("委任中", "サブエージェント実行中(wait)".into()))); + } + + #[test] + fn a_child_threads_sub_agent_call_is_not_this_seats() { + let mut a = seat(); + let mut item = collab("spawnAgent"); + item["id"] = json!("g9"); + a.feed(¬e("item/started", json!({"threadId": "sub-1", "turnId": "x", "startedAtMs": 1, "item": item}))); + assert_eq!(a.display(), None); + } + #[test] fn a_tool_name_is_cleaned_and_cut() { let mut a = seat(); diff --git a/docs/2-screen.md b/docs/2-screen.md index 995ff5e..f5353ea 100644 --- a/docs/2-screen.md +++ b/docs/2-screen.md @@ -459,13 +459,14 @@ Master「コメント書き込み欄の上にある下までスクロールボ | 要約中 | 要約中 | `contextCompaction` | | 画像生成 | 画像生成中 | `imageGeneration` | | 画像参照 | 画像参照中 | `imageView` | +| 委任中 | サブエージェント実行中(tool) | `collabAgentToolCall` の `item/started` から `item/completed` まで(`tool` は `spawnAgent`・`wait` などの操作名、#406) | | 様子不明 | 様子不明(app-server との接続が切れた) | 席の server との接続が切れた(下記) | - **この席の知らせが先に立つ。** 優先は、上限で止まったと分かっている席の「制限中」(#342、#372、下記)>「許可待ち」「答え待ち」>仕事の種類>スレッドが idle と分かっているときの「制限中」「待機」(#329、下記)>「考え中…」「出力中」>「制限中」>「様子不明」>端末の沈黙による「待機」である。許可を待つ TUI は画面を描き直すので「出力中」と出、黙れば「待機」と出る——この節が言い分けられなかった二つの場合であり、知らせはそれを言える。長いコマンドの間に端末が黙っても、「待機」ではなく「実行中」が立つ。宛先にされていても仕事の種類のほうを出す——どちらも「まだ終わっていない」ことを言い、種類のほうが多くを言う。 - **スレッドが idle と分かっていれば、端末の合図を見ずに「待機」を出す(#329、Master「Lunaがずっと出力中表示になってるな」2026-10-08)。** TUI が画面を描き直し続けると端末は黙らず、何もしていない席が「出力中」のまま残った。そこで事象は語とは別に、スレッドの様子(`thread_status`:`idle`/`active`/まだ分からない)を持つ。接続中でスレッドが idle なら、端末に文字が届いていても「待機」とし、利用上限なら「制限中」とする。語が無いことは idle ではない——推論中や答えを書いている active のターンにも語は無い。その間と、様子がまだ分からない間は、今までどおり端末の合図(「考え中…」「出力中」、黙れば「待機」)を出す。切断では様子を忘れ、「様子不明」は変わらない。様子は `thread/status/changed` と `thread/start`/`thread/resume` の答えの `thread.status`(`idle`/`active`。`systemError`・`notLoaded` は「分からない」に戻す)、`turn/started`(active)、`turn/completed`(idle)から読み、`thread/closed` で「分からない」に戻す。**起動・再開の答えの idle も、ターンの前の idle の知らせも、受け取ったときにそのまま送る(#368、Master「Lunaが出力中で10分以上たってる?」2026-10-10)。** 端末が始めたターンの知らせはアプリの接続にも届く——2026-10-10 に記録で確かめた([実装状況](4-status.md#codex-の-app-server-の席の待機3292026-10-08))。それまでは届くかが分からないとして、ターンがこの接続に届くと示す知らせを一度受け取るまで様子を「分からない」として送っていたため、起動し直した席は最初のターンまで「出力中」のまま残った。席の接続に届いた知らせの種類は一行ずつ記録する(同じ節)。 - **待ちは仕事より優先する。** `item/started` は承認の前にも届く(#324)。flag が立っている間は「実行中」と出さない。二つの flag が同時に立てば「許可待ち」を出す。 - **仕事は集合で持つ。** 並列の道具があるため、item の id ごとに持つ。札は最後に始まったものを言い、一行は残りの数を「ほか N 件」で足す。 -- **言うのは種類と最小限の名前までである。** コマンドの全文、道具の引数、本文は出さない。道具の名前は制御文字を落とし、48 文字で切る。メッセージ・推論・計画・サブエージェントの item は語にしない(前の三つは「考え中…」「出力中」の受け持ちであり、サブエージェントの活動は測っていない、#324)。 +- **言うのは種類と最小限の名前までである。** コマンドの全文、道具の引数、本文は出さない。道具の名前は制御文字を落とし、48 文字で切る。メッセージ・推論・計画の item は語にしない(「考え中…」「出力中」の受け持ちである)。サブエージェントは、この席のスレッドがそれを呼ぶ item(`collabAgentToolCall`)を Claude Code の席と同じ「委任中」として出す(#406、Master 観察 2026-10-12)。サブエージェント自身のスレッドの知らせは今どおりこの席のものとして読まない。 - **札の語は四文字までにする。** 下記「語の長さはこの場所の制約を受ける」のとおり、名前と note は一つの桁を分け合う。長い形(道具の名前を含む)は札の title と会話面の下の一行が持つ。この一行が札の言わない席を言うことは無いという線(#307)は変わらない——同じ一つの関数(`rowActivity`)から、札の語とその長い形を読む。 - **色と動き。** 仕事の語は「考え中…」「出力中」と同じ `--ok` の札で、同じく明滅する。「許可待ち」「答え待ち」も発話が終わっていない語として同じ色で出し、会話面の下の一行にも載るが、明滅はしない——動いているものが無いためである。新しい動きは足していない(動きは「動き(#307)」と同じく `prefers-reduced-motion` に従う)。 - **古い語を残さない。** ターンの始まりと終わり、スレッドが active でなくなったこと、スレッドが閉じたことで、仕事と flag を消す。server との接続が切れたら、仕事と flag とスレッドの様子は消え、札は「様子不明」になる。「待機」には戻さない——待機中の切断でも、仕事の途中の切断でも同じである。端末の沈黙は、席が承認の画面で止まっているのか長いコマンドを走らせているのかを言えず、それを言っていた知らせはもう届かないためである。端末に出力が届いていれば「出力中」「考え中…」が、利用上限なら「制限中」が先に立つ——どちらも別の観測から来る語である。「様子不明」は地の色で、明滅しない。同じ席を起動し直せば、新しい端末の席として接続中から始まる(前の起動の知らせは端末の id で弾く)。