fix(swarm): label each member with its own item

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 "<description> (N members)" and `is_task_variant` already knows
`AgentSwarm`, so the blocking-wait spinner registers.
This commit is contained in:
2026-07-27 03:01:42 -04:00
parent 16328e55f9
commit a07d889ad9
@@ -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)]