fix(v0.7.8): reconcile swarm state and unicode search
This commit is contained in:
@@ -20,10 +20,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- **`exec_shell_interact`** poll loop now observes the turn cancel token so stalled interactive sessions don't block turn cancellation. (#248)
|
||||
- **Transcript running-tool hint** — executing shell cells now show "Ctrl+B opens shell controls" while running. (#248)
|
||||
- **Keybinding registry** now includes `Ctrl+B` (opens shell controls) next to `Ctrl+C` (cancel/exits). (#248)
|
||||
- **Deferred swarm card creation** — `agent_swarm` no longer pre-seeds an all-pending FanoutCard from `ToolCallStarted`; the card is created only when the first `SwarmProgress` event carries real worker state. Until then the sidebar uses the declared task count as a pending dispatch placeholder. (#236, #238)
|
||||
- **Swarm wording normalized** — fanout-family fallback labels now render as `swarm`, matching the canonical `agent_swarm` / `rlm` model and avoiding mixed `fanout` / `swarm` terminology in the transcript. (#236, #238)
|
||||
- **OPERATIONS_RUNBOOK** and **TOOL_SURFACE** updated with new shell control paths and `exec_shell_cancel` documentation.
|
||||
|
||||
### Fixed
|
||||
- **Nonblocking swarm state drift** — the sidebar no longer falls back to `0` or a contradictory seeded placeholder before the first progress event arrives, which removes the visible `pending` vs `running/done` mismatch during early `agent_swarm` dispatch. (#236, #238)
|
||||
- **Unicode-safe search globbing** — search wildcard matching now iterates on UTF-8 char boundaries instead of raw byte offsets, preventing panics on filenames like `dialogue_line__冰糖.mp3`. (#249)
|
||||
|
||||
### Tests
|
||||
- 7 new integration tests: foreground-to-background detach, wait-cancel-leaves-process, single-task cancel, bulk cancel (kill-all), foreground-cancel-kills, ShellControlView default/select states
|
||||
- Expanded swarm/sidebar regression coverage for deferred card creation and pending-count fallback before first `SwarmProgress`. (#236, #238)
|
||||
- Added a Unicode filename regression test for wildcard search matching. (#249)
|
||||
|
||||
## [0.7.7] - 2026-04-30
|
||||
|
||||
|
||||
Generated
+14
-14
@@ -1011,7 +1011,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deepseek-agent"
|
||||
version = "0.7.7"
|
||||
version = "0.7.8"
|
||||
dependencies = [
|
||||
"deepseek-config",
|
||||
"serde",
|
||||
@@ -1019,7 +1019,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deepseek-app-server"
|
||||
version = "0.7.7"
|
||||
version = "0.7.8"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -1042,7 +1042,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deepseek-config"
|
||||
version = "0.7.7"
|
||||
version = "0.7.8"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deepseek-secrets",
|
||||
@@ -1055,7 +1055,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deepseek-core"
|
||||
version = "0.7.7"
|
||||
version = "0.7.8"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
@@ -1074,7 +1074,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deepseek-execpolicy"
|
||||
version = "0.7.7"
|
||||
version = "0.7.8"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deepseek-protocol",
|
||||
@@ -1083,7 +1083,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deepseek-hooks"
|
||||
version = "0.7.7"
|
||||
version = "0.7.8"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -1097,7 +1097,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deepseek-mcp"
|
||||
version = "0.7.7"
|
||||
version = "0.7.8"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deepseek-protocol",
|
||||
@@ -1107,7 +1107,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deepseek-protocol"
|
||||
version = "0.7.7"
|
||||
version = "0.7.8"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -1115,7 +1115,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deepseek-secrets"
|
||||
version = "0.7.7"
|
||||
version = "0.7.8"
|
||||
dependencies = [
|
||||
"dirs",
|
||||
"keyring",
|
||||
@@ -1128,7 +1128,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deepseek-state"
|
||||
version = "0.7.7"
|
||||
version = "0.7.8"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
@@ -1140,7 +1140,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deepseek-tools"
|
||||
version = "0.7.7"
|
||||
version = "0.7.8"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -1153,7 +1153,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deepseek-tui"
|
||||
version = "0.7.7"
|
||||
version = "0.7.8"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arboard",
|
||||
@@ -1213,7 +1213,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deepseek-tui-cli"
|
||||
version = "0.7.7"
|
||||
version = "0.7.8"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
@@ -1236,7 +1236,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deepseek-tui-core"
|
||||
version = "0.7.7"
|
||||
version = "0.7.8"
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
|
||||
@@ -7,5 +7,5 @@ repository.workspace = true
|
||||
description = "Model/provider registry and fallback strategy for DeepSeek workspace architecture"
|
||||
|
||||
[dependencies]
|
||||
deepseek-config = { path = "../config", version = "0.7.7" }
|
||||
deepseek-config = { path = "../config", version = "0.7.8" }
|
||||
serde.workspace = true
|
||||
|
||||
@@ -10,15 +10,15 @@ description = "Codex-style app-server transport for DeepSeek workspace architect
|
||||
anyhow.workspace = true
|
||||
axum.workspace = true
|
||||
clap.workspace = true
|
||||
deepseek-agent = { path = "../agent", version = "0.7.7" }
|
||||
deepseek-config = { path = "../config", version = "0.7.7" }
|
||||
deepseek-core = { path = "../core", version = "0.7.7" }
|
||||
deepseek-execpolicy = { path = "../execpolicy", version = "0.7.7" }
|
||||
deepseek-hooks = { path = "../hooks", version = "0.7.7" }
|
||||
deepseek-mcp = { path = "../mcp", version = "0.7.7" }
|
||||
deepseek-protocol = { path = "../protocol", version = "0.7.7" }
|
||||
deepseek-state = { path = "../state", version = "0.7.7" }
|
||||
deepseek-tools = { path = "../tools", version = "0.7.7" }
|
||||
deepseek-agent = { path = "../agent", version = "0.7.8" }
|
||||
deepseek-config = { path = "../config", version = "0.7.8" }
|
||||
deepseek-core = { path = "../core", version = "0.7.8" }
|
||||
deepseek-execpolicy = { path = "../execpolicy", version = "0.7.8" }
|
||||
deepseek-hooks = { path = "../hooks", version = "0.7.8" }
|
||||
deepseek-mcp = { path = "../mcp", version = "0.7.8" }
|
||||
deepseek-protocol = { path = "../protocol", version = "0.7.8" }
|
||||
deepseek-state = { path = "../state", version = "0.7.8" }
|
||||
deepseek-tools = { path = "../tools", version = "0.7.8" }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
tokio.workspace = true
|
||||
|
||||
@@ -14,13 +14,13 @@ path = "src/main.rs"
|
||||
anyhow.workspace = true
|
||||
clap.workspace = true
|
||||
clap_complete.workspace = true
|
||||
deepseek-agent = { path = "../agent", version = "0.7.7" }
|
||||
deepseek-app-server = { path = "../app-server", version = "0.7.7" }
|
||||
deepseek-config = { path = "../config", version = "0.7.7" }
|
||||
deepseek-execpolicy = { path = "../execpolicy", version = "0.7.7" }
|
||||
deepseek-mcp = { path = "../mcp", version = "0.7.7" }
|
||||
deepseek-secrets = { path = "../secrets", version = "0.7.7" }
|
||||
deepseek-state = { path = "../state", version = "0.7.7" }
|
||||
deepseek-agent = { path = "../agent", version = "0.7.8" }
|
||||
deepseek-app-server = { path = "../app-server", version = "0.7.8" }
|
||||
deepseek-config = { path = "../config", version = "0.7.8" }
|
||||
deepseek-execpolicy = { path = "../execpolicy", version = "0.7.8" }
|
||||
deepseek-mcp = { path = "../mcp", version = "0.7.8" }
|
||||
deepseek-secrets = { path = "../secrets", version = "0.7.8" }
|
||||
deepseek-state = { path = "../state", version = "0.7.8" }
|
||||
chrono.workspace = true
|
||||
dirs.workspace = true
|
||||
serde.workspace = true
|
||||
|
||||
@@ -8,7 +8,7 @@ description = "Config schema and precedence model for DeepSeek workspace archite
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
deepseek-secrets = { path = "../secrets", version = "0.7.7" }
|
||||
deepseek-secrets = { path = "../secrets", version = "0.7.8" }
|
||||
dirs.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
||||
@@ -9,14 +9,14 @@ description = "Core runtime boundaries for DeepSeek workspace architecture"
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
chrono.workspace = true
|
||||
deepseek-agent = { path = "../agent", version = "0.7.7" }
|
||||
deepseek-config = { path = "../config", version = "0.7.7" }
|
||||
deepseek-execpolicy = { path = "../execpolicy", version = "0.7.7" }
|
||||
deepseek-hooks = { path = "../hooks", version = "0.7.7" }
|
||||
deepseek-mcp = { path = "../mcp", version = "0.7.7" }
|
||||
deepseek-protocol = { path = "../protocol", version = "0.7.7" }
|
||||
deepseek-state = { path = "../state", version = "0.7.7" }
|
||||
deepseek-tools = { path = "../tools", version = "0.7.7" }
|
||||
deepseek-agent = { path = "../agent", version = "0.7.8" }
|
||||
deepseek-config = { path = "../config", version = "0.7.8" }
|
||||
deepseek-execpolicy = { path = "../execpolicy", version = "0.7.8" }
|
||||
deepseek-hooks = { path = "../hooks", version = "0.7.8" }
|
||||
deepseek-mcp = { path = "../mcp", version = "0.7.8" }
|
||||
deepseek-protocol = { path = "../protocol", version = "0.7.8" }
|
||||
deepseek-state = { path = "../state", version = "0.7.8" }
|
||||
deepseek-tools = { path = "../tools", version = "0.7.8" }
|
||||
serde_json.workspace = true
|
||||
tokio.workspace = true
|
||||
uuid.workspace = true
|
||||
|
||||
@@ -8,5 +8,5 @@ description = "Execution policy and approval model parity for DeepSeek workspace
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
deepseek-protocol = { path = "../protocol", version = "0.7.7" }
|
||||
deepseek-protocol = { path = "../protocol", version = "0.7.8" }
|
||||
serde.workspace = true
|
||||
|
||||
@@ -10,7 +10,7 @@ description = "Hook dispatch and notifications parity for DeepSeek workspace arc
|
||||
anyhow.workspace = true
|
||||
async-trait.workspace = true
|
||||
chrono.workspace = true
|
||||
deepseek-protocol = { path = "../protocol", version = "0.7.7" }
|
||||
deepseek-protocol = { path = "../protocol", version = "0.7.8" }
|
||||
reqwest.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
||||
@@ -8,6 +8,6 @@ description = "MCP server lifecycle and tool proxy compatibility for DeepSeek wo
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
deepseek-protocol = { path = "../protocol", version = "0.7.7" }
|
||||
deepseek-protocol = { path = "../protocol", version = "0.7.8" }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
||||
@@ -9,7 +9,7 @@ description = "Tool invocation lifecycle, schema validation, and scheduler paral
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
async-trait.workspace = true
|
||||
deepseek-protocol = { path = "../protocol", version = "0.7.7" }
|
||||
deepseek-protocol = { path = "../protocol", version = "0.7.8" }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
tokio.workspace = true
|
||||
|
||||
@@ -13,8 +13,8 @@ path = "src/main.rs"
|
||||
[dependencies]
|
||||
anyhow = "1.0.100"
|
||||
arboard = "3.4"
|
||||
deepseek-secrets = { path = "../secrets", version = "0.7.7" }
|
||||
deepseek-tools = { path = "../tools", version = "0.7.7" }
|
||||
deepseek-secrets = { path = "../secrets", version = "0.7.8" }
|
||||
deepseek-tools = { path = "../tools", version = "0.7.8" }
|
||||
async-stream = "0.3.6"
|
||||
async-trait = "0.1"
|
||||
bytes = "1.11.0"
|
||||
|
||||
@@ -356,13 +356,19 @@ fn matches_simple_glob(text: &str, pattern: &str) -> bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Try matching at each position
|
||||
// Try matching at each position (use char-indices to stay on
|
||||
// UTF-8 boundaries — byte-index slicing panics on multi-byte
|
||||
// characters like 冰糖, see #249).
|
||||
let remaining: String = text_chars.collect();
|
||||
for i in 0..=remaining.len() {
|
||||
for (i, _) in remaining.char_indices() {
|
||||
if matches_simple_glob(&remaining[i..], &next_pattern) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Also try the empty suffix at end of string
|
||||
if matches_simple_glob("", &next_pattern) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
'?' => {
|
||||
@@ -423,6 +429,21 @@ mod tests {
|
||||
assert!(!matches_glob("lib/main.rs", "src/*.rs"));
|
||||
}
|
||||
|
||||
/// Regression for #249: byte-index slicing panics on multi-byte
|
||||
/// characters inside filenames like `dialogue_line__冰糖.mp3`.
|
||||
#[test]
|
||||
fn test_matches_glob_unicode_filename() {
|
||||
let filename = "dialogue_line__冰糖.mp3";
|
||||
// The filename should match *.mp3 without panicking.
|
||||
assert!(matches_glob(filename, "*.mp3"));
|
||||
// Asterisk matching against multi-byte characters must succeed.
|
||||
assert!(matches_glob(filename, "dialogue_line__*"));
|
||||
// Literal multi-byte characters inside the pattern must match.
|
||||
assert!(matches_glob(filename, "*冰糖*"));
|
||||
// Non-matching pattern must not panic either.
|
||||
assert!(!matches_glob(filename, "nonexistent*"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_grep_files_basic() {
|
||||
let tmp = tempdir().expect("tempdir");
|
||||
|
||||
@@ -497,6 +497,11 @@ pub struct App {
|
||||
/// spawned by the same `agent_swarm` / `rlm` invocation route into
|
||||
/// this card; reset when a fresh fanout-family tool call starts.
|
||||
pub last_fanout_card_index: Option<usize>,
|
||||
/// Number of tasks declared by a pending `agent_swarm` invocation that
|
||||
/// hasn't yet received its first SwarmProgress event. Used by the
|
||||
/// sidebar to show "dispatching N" before the FanoutCard exists (#236/#238).
|
||||
/// Cleared once sync_fanout_card_from_swarm_outcome creates the card.
|
||||
pub pending_swarm_task_count: Option<usize>,
|
||||
/// Canonical swarm/job snapshots by swarm id. Transcript cards, sidebar
|
||||
/// counts, and footer status read from this model instead of recomputing
|
||||
/// worker totals independently.
|
||||
@@ -942,6 +947,7 @@ impl App {
|
||||
agent_progress: HashMap::new(),
|
||||
subagent_card_index: HashMap::new(),
|
||||
last_fanout_card_index: None,
|
||||
pending_swarm_task_count: None,
|
||||
swarm_jobs: HashMap::new(),
|
||||
last_swarm_id: None,
|
||||
swarm_card_index: HashMap::new(),
|
||||
|
||||
@@ -27,22 +27,32 @@ pub(super) fn running_agent_count(app: &App) -> usize {
|
||||
}
|
||||
|
||||
pub(super) fn active_fanout_counts(app: &App) -> Option<(usize, usize)> {
|
||||
// Canonical source: the in-progress SwarmOutcome from swarm_jobs.
|
||||
if let Some(swarm_id) = app.last_swarm_id.as_ref()
|
||||
&& let Some(outcome) = app.swarm_jobs.get(swarm_id)
|
||||
{
|
||||
return Some((outcome.counts.running, outcome.counts.total));
|
||||
}
|
||||
|
||||
let idx = app.last_fanout_card_index?;
|
||||
let Some(HistoryCell::SubAgent(SubAgentCell::Fanout(card))) = app.history.get(idx) else {
|
||||
return None;
|
||||
};
|
||||
let running = card
|
||||
.workers
|
||||
.iter()
|
||||
.filter(|slot| matches!(slot.status, AgentLifecycle::Running))
|
||||
.count();
|
||||
Some((running, card.worker_count()))
|
||||
// Card exists — read running count from the canonical slot states.
|
||||
if let Some(idx) = app.last_fanout_card_index
|
||||
&& let Some(HistoryCell::SubAgent(SubAgentCell::Fanout(card))) = app.history.get(idx)
|
||||
{
|
||||
let running = card
|
||||
.workers
|
||||
.iter()
|
||||
.filter(|slot| matches!(slot.status, AgentLifecycle::Running))
|
||||
.count();
|
||||
return Some((running, card.worker_count()));
|
||||
}
|
||||
|
||||
// No card yet — swarm was just dispatched but no SwarmProgress has
|
||||
// arrived. Show the declared task count so the sidebar doesn't read zero.
|
||||
if let Some(total) = app.pending_swarm_task_count {
|
||||
return Some((0, total));
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub(super) fn seed_fanout_card_from_tool_call(
|
||||
@@ -61,32 +71,15 @@ pub(super) fn seed_fanout_card_from_tool_call(
|
||||
return false;
|
||||
}
|
||||
|
||||
let ids = tasks
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(idx, task)| {
|
||||
let task_id = task
|
||||
.get("id")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(ToOwned::to_owned)
|
||||
.unwrap_or_else(|| idx.to_string());
|
||||
format!("task:{task_id}")
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let history_threshold_before_push = app.history.len();
|
||||
let active_in_flight = app.active_cell.is_some();
|
||||
let card = FanoutCard::new(name.to_string()).with_workers(ids);
|
||||
app.add_message(HistoryCell::SubAgent(SubAgentCell::Fanout(card)));
|
||||
shift_active_virtual_indices_after_history_insert(
|
||||
app,
|
||||
active_in_flight,
|
||||
history_threshold_before_push,
|
||||
);
|
||||
app.last_fanout_card_index = Some(app.history.len().saturating_sub(1));
|
||||
app.mark_history_updated();
|
||||
// Codex pattern: don't pre-seed a FanoutCard with all-Pending workers.
|
||||
// The card gets created by sync_fanout_card_from_swarm_outcome when the
|
||||
// first SwarmProgress carries real worker states. This eliminates the
|
||||
// "0 done · 0 running · 0 failed · N pending" vs sidebar "N running"
|
||||
// contradiction (#236/#238).
|
||||
//
|
||||
// Store the pending dispatch info so the transcript tool card (running
|
||||
// state) serves as the visual placeholder until workers start.
|
||||
app.pending_swarm_task_count = Some(tasks.len());
|
||||
true
|
||||
}
|
||||
|
||||
@@ -220,6 +213,8 @@ pub(super) fn sync_fanout_card_from_swarm_outcome(app: &mut App, outcome: &Swarm
|
||||
};
|
||||
app.swarm_card_index.insert(outcome.swarm_id.clone(), idx);
|
||||
|
||||
app.pending_swarm_task_count = None;
|
||||
|
||||
let Some(HistoryCell::SubAgent(SubAgentCell::Fanout(card))) = app.history.get_mut(idx) else {
|
||||
return false;
|
||||
};
|
||||
@@ -284,26 +279,6 @@ fn swarm_task_status_to_lifecycle(status: &SwarmTaskStatus) -> AgentLifecycle {
|
||||
}
|
||||
}
|
||||
|
||||
fn shift_active_virtual_indices_after_history_insert(
|
||||
app: &mut App,
|
||||
active_in_flight: bool,
|
||||
threshold: usize,
|
||||
) {
|
||||
if !active_in_flight {
|
||||
return;
|
||||
}
|
||||
for idx in app.tool_cells.values_mut() {
|
||||
if *idx >= threshold {
|
||||
*idx = idx.wrapping_add(1);
|
||||
}
|
||||
}
|
||||
for (cell_idx, _) in app.exploring_entries.values_mut() {
|
||||
if *cell_idx >= threshold {
|
||||
*cell_idx = cell_idx.wrapping_add(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn reconcile_subagent_activity_state(app: &mut App) {
|
||||
let running_agents: Vec<(String, String)> = app
|
||||
.subagent_cache
|
||||
@@ -426,7 +401,7 @@ pub(super) fn handle_subagent_mailbox(app: &mut App, seq: u64, message: &Mailbox
|
||||
card.claim_pending_worker(&agent_id, AgentLifecycle::Running);
|
||||
app.subagent_card_index.insert(agent_id, idx);
|
||||
} else {
|
||||
let mut card = FanoutCard::new(dispatch_kind.unwrap_or("fanout").to_string());
|
||||
let mut card = FanoutCard::new(dispatch_kind.unwrap_or("swarm").to_string());
|
||||
card.upsert_worker(&agent_id, AgentLifecycle::Running);
|
||||
app.add_message(HistoryCell::SubAgent(SubAgentCell::Fanout(card)));
|
||||
let idx = app.history.len().saturating_sub(1);
|
||||
|
||||
@@ -2433,11 +2433,18 @@ fn agent_swarm_seeded_fanout_card_uses_declared_task_count() {
|
||||
}),
|
||||
));
|
||||
|
||||
let HistoryCell::SubAgent(SubAgentCell::Fanout(card)) = &app.history[0] else {
|
||||
panic!("expected seeded fanout card");
|
||||
};
|
||||
assert_eq!(card.worker_count(), 3);
|
||||
assert_eq!(active_fanout_counts(&app), Some((0, 3)));
|
||||
// Card is deferred until first SwarmProgress (#236/#238).
|
||||
// Before that, only the pending task count is stored.
|
||||
assert_eq!(app.pending_swarm_task_count, Some(3));
|
||||
assert!(
|
||||
app.history.is_empty(),
|
||||
"no card pre-seeded before SwarmProgress"
|
||||
);
|
||||
assert_eq!(
|
||||
active_fanout_counts(&app),
|
||||
Some((0, 3)),
|
||||
"sidebar reads pending count"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2462,10 +2469,11 @@ fn seeded_fanout_card_preserves_existing_active_tool_indices() {
|
||||
}),
|
||||
));
|
||||
|
||||
// No card created → no history insertion → tool_cells indices unchanged.
|
||||
assert_eq!(
|
||||
app.tool_cells.get("search-1").copied(),
|
||||
Some(1),
|
||||
"active tool virtual index should shift after history insertion"
|
||||
Some(0),
|
||||
"active tool virtual index unchanged when card is deferred"
|
||||
);
|
||||
|
||||
let result = crate::tools::spec::ToolResult::success("done");
|
||||
|
||||
@@ -219,6 +219,7 @@ impl FanoutCard {
|
||||
}
|
||||
|
||||
/// Pre-seed worker slots when the fanout size is known up front.
|
||||
#[allow(dead_code)]
|
||||
pub fn with_workers<I, S>(mut self, ids: I) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = S>,
|
||||
|
||||
@@ -37,7 +37,7 @@ pub enum ToolFamily {
|
||||
Find,
|
||||
/// Single sub-agent dispatch. `◐ delegate`.
|
||||
Delegate,
|
||||
/// Multi-agent fanout (swarm, csv). `⋮⋮ fanout`.
|
||||
/// Multi-agent swarm (agent_swarm, csv, rlm). `⋮⋮ swarm`.
|
||||
Fanout,
|
||||
/// Reasoning / chain-of-thought. `… think`. Reasoning has its own
|
||||
/// render path (`render_thinking` in `history.rs`); the family is
|
||||
@@ -153,7 +153,7 @@ pub fn family_label(family: ToolFamily) -> &'static str {
|
||||
ToolFamily::Run => "run",
|
||||
ToolFamily::Find => "find",
|
||||
ToolFamily::Delegate => "delegate",
|
||||
ToolFamily::Fanout => "fanout",
|
||||
ToolFamily::Fanout => "swarm",
|
||||
ToolFamily::Think => "think",
|
||||
ToolFamily::Generic => "tool",
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user