サブエージェントが「動いてる/終わった」を当てる

"🤖 evaluator" と表示して、終わったら消す。それだけのことを、正確にやるための小さな工夫。

ccmux のペインのタイトルには、こんな感じでサブエージェントが出ます:

● claude [1] 🤖 evaluator, reviewer 🔧 Bash
  • evaluatorreviewer の 2 つのサブエージェントが走ってる
  • そのうち片方が Bash を実行中

どちらかが終わると、その名前が消えます。両方終われば 🤖 自体が消える。

どうやってこの “動いている感” を追跡してるか の話です。

見えているもの

前回 で書いた通り、ccmux は Claude Code のログファイル(JSONL)を読んでいます。その中に、こんなペアが現れる:

起動時:

{
"type": "assistant",
"message": {
"content": [{
"type": "tool_use",
"id": "toolu_01ABC",
"name": "Agent",
"input": { "subagent_type": "evaluator" }
}]
}
}

完了時:

{
"type": "user",
"message": {
"content": [{
"type": "tool_result",
"tool_use_id": "toolu_01ABC",
"content": "..."
}]
}
}

ポイントは idtool_use_id が同じ値でペアになっている こと。この 2 つが見つかれば、開始と終了を紐づけられます。

HashMap で「今動いてるやつ」を持つ

データ構造は、ほとんど集合演算です:

src/claude_monitor.rs
pub struct PaneMonitor {
// tool_use_id → サブエージェント種別
active_task_ids: HashMap<String, String>,
// ...
}

起動を見たら入れる。完了を見たら消す。それだけ。

// tool_use が来た時
if name == "Agent" {
if let Some(kind) = input.get("subagent_type").and_then(|v| v.as_str()) {
active_task_ids.insert(id.to_string(), kind.to_string());
}
}
// tool_result が来た時
active_task_ids.remove(tool_use_id);

表示用のリストは、今持っている HashMap の値を並べるだけ:

let mut types: Vec<String> = active_task_ids.values().cloned().collect();
types.sort();
types.dedup();

これで「同じ種類のサブエージェントが 2 つ動いていても 1 つに見える」まで含めて実装終わり。

ハマりどころ 1: 名前が違う

最初は「サブエージェントのツール名は Task だろう」と決めつけてました。でも実ファイルを見たら Agent でした。Task だけマッチさせてた初期実装は、何も表示されないという静かな失敗。

ドキュメントの記述と、実際に流れてるバイトが食い違うことはある。一次情報が一番強い

ハマりどころ 2: 間に他のイベントが混ざる

起動イベントと完了イベントは、連続して現れるとは限りません。間には:

  • ユーザーのメッセージ
  • 他の tool_use
  • アシスタントのテキスト応答

などが入り乱れます。だから「次に来るのが対応する完了」とは仮定しない。必ず tool_use_id で紐づける。

ハマりどころ 3: 親がクラッシュしたらどうなる

Claude Code のセッションを強制終了すると、完了イベントだけ書かれずに終わるケースがあります。すると ccmux 側の HashMap に「永遠に走ってることになっているサブエージェント」が残ってしまう。

対策は愚直で、別セッションのログファイルが現れたら、全部リセット:

if new_path != self.jsonl_path {
self.active_task_ids.clear();
self.state = ClaudeState::default();
self.jsonl_path = new_path;
}

ccmux 自体を再起動してもリセットされます。状態は全部 JSONL 由来なので、起動時に再スキャンするだけで復元されます。

全体像

結局やっていることは:

  1. 起動 → HashMap に入れる
  2. 完了 → HashMap から消す
  3. 表示 → HashMap の中身を並べる

JSON の流れから状態を持つ」のは、こういう時に書き味がシンプルになります。イベントストリームに対して HashMap が 1 つある、という形は覚えておくと他でも使えます。


次回は Claude Code を「起動してますよ」と判定する別のトリック(OSC ウィンドウタイトル)について書きます。