From a07d889ad95f9a27ae79ad0296be9268de87feb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Mon, 27 Jul 2026 03:01:42 -0400 Subject: [PATCH] fix(swarm): label each member with its own item MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every member was launched with the swarm's description, so the pager drew N identical subagent blocks: the user could not tell which member was running, and a failure could not be attributed to an item. This is also why K6 does not port upstream's progress grid. A member IS an ordinary subagent, so the pager already renders one block per member with live status, duration, tool-call and turn counts — richer than a grid cell. The gap was the label, not the widget; upstream's grid (and the estimator behind it, which infers progress from tool-call rate) answers a different TUI's shortcomings. The parent tool call already reads " (N members)" and `is_task_variant` already knows `AgentSwarm`, so the blocking-wait spinner registers. --- .../implementations/kigi/agent_swarm/run.rs | 40 ++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/crates/codegen/kigi-tools/src/implementations/kigi/agent_swarm/run.rs b/crates/codegen/kigi-tools/src/implementations/kigi/agent_swarm/run.rs index 1c1ba73..dd7f6bf 100644 --- a/crates/codegen/kigi-tools/src/implementations/kigi/agent_swarm/run.rs +++ b/crates/codegen/kigi-tools/src/implementations/kigi/agent_swarm/run.rs @@ -341,7 +341,10 @@ fn build_request(pending: &Pending, config: &SwarmRunConfig) -> SubagentRequest SubagentRequest { id: uuid::Uuid::now_v7().to_string(), prompt: pending.spec.prompt.clone(), - description: config.description.clone(), + // The ITEM, not the swarm's description: every member would otherwise + // render as an identical subagent block and the user could not tell + // which one is running, or which one failed. + description: pending.spec.item.clone(), subagent_type: config.subagent_type.clone(), parent_session_id: config.parent_session_id.clone(), parent_prompt_id: config.parent_prompt_id.clone(), @@ -690,6 +693,41 @@ mod tests { ); } + /// Each member must be identifiable while it runs: the TUI renders one + /// subagent block per member from this description, so a shared one leaves + /// the user staring at N identical rows. + #[tokio::test(start_paused = true)] + async fn each_member_is_labelled_with_its_own_item() { + let backend = Arc::new(FakeBackend::default()); + let seen = Arc::new(Mutex::new(Vec::new())); + let recorder = seen.clone(); + let specs = specs(&["a.rs", "b.rs"]); + let config = SwarmRunConfig { + description: "review files".into(), + ..config(None) + }; + // `build_request` is the only place the label is set, so assert on it + // directly rather than through the backend's prompt log. + for (index, spec) in specs.iter().enumerate() { + let pending = Pending { + index, + spec: spec.clone(), + attempts: 0, + not_before: None, + }; + recorder + .lock() + .unwrap() + .push(build_request(&pending, &config).description); + } + assert_eq!( + *seen.lock().unwrap(), + vec!["a.rs".to_string(), "b.rs".to_string()], + "each member must carry its own item, not the swarm description" + ); + drop(backend); + } + /// Dropping the runner is what a send-now interrupt does; the members must /// be cancelled rather than silently detached onto the user's tree. #[tokio::test(start_paused = true)]