use super::{
ChangedFileEntry, ChatMessage, MAX_CHANGED_FILES, MAX_MESSAGES, MAX_TOOL_LOG, MessageRole,
PopupKind, PopupState, SwarmAgentStatus, SwarmAgentUiEntry, SwarmPhase, SwarmUiStatus,
ToolLogEntry, UiState, WorkerDetailState, WorkerToolEvent, push_bounded,
};
impl UiState {
pub(super) fn ev_token(&mut self, token: String) {
self.streaming_buffer.push_str(&token);
}
pub(super) fn ev_response(&mut self) {
if !self.streaming_buffer.is_empty() {
push_bounded(
&mut self.messages,
ChatMessage::text(
MessageRole::Assistant,
self.streaming_buffer.drain(..).collect::<String>(),
),
MAX_MESSAGES,
);
*self.streaming_render_cache.borrow_mut() = None;
}
}
pub(super) fn ev_tool_call(&mut self, name: String, args: String, call_id: Option<String>) {
if matches!(name.as_str(), "file_edit" | "file_write" | "git_patch")
&& let Ok(json) = serde_json::from_str::<serde_json::Value>(&args)
&& let Some(path) = json.get("path").and_then(|v| v.as_str())
{
let adds = json
.get("new_string")
.or_else(|| json.get("content"))
.and_then(|v| v.as_str())
.map_or(0, |t| t.lines().count());
let dels = json
.get("old_string")
.and_then(|v| v.as_str())
.map_or(0, |t| t.lines().count());
if let Some(entry) = self.changed_files.iter_mut().find(|e| e.path == path) {
entry.additions += adds;
entry.deletions += dels;
} else {
push_bounded(
&mut self.changed_files,
ChangedFileEntry {
path: path.to_string(),
additions: adds,
deletions: dels,
},
MAX_CHANGED_FILES,
);
}
}
let preview = if args.len() > 100 {
let boundary = args
.char_indices()
.map(|(i, _)| i)
.take_while(|&i| i <= 100)
.last()
.unwrap_or(0);
format!("{}...", &args[..boundary])
} else {
args.clone()
};
let args_full = if matches!(name.as_str(), "file_edit" | "file_write") {
Some(args.clone())
} else {
None
};
push_bounded(
&mut self.tool_log,
ToolLogEntry {
name: name.clone(),
args_preview: preview,
args_full,
result_preview: "running...".to_string(),
success: true,
call_id: call_id.clone(),
},
MAX_TOOL_LOG,
);
self.status_msg = format!("Running tool: {name}");
if name.starts_with("mcp__")
&& let Some(server) = name
.strip_prefix("mcp__")
.and_then(|s| s.split("__").next())
{
self.active_mcp_server = Some(server.to_string());
self.active_mcp_server_set_at = Some(std::time::Instant::now());
}
}
pub(super) fn ev_tool_result(
&mut self,
name: String,
result: String,
success: bool,
call_id: Option<String>,
) {
let preview = if result.len() > 200 {
let boundary = result
.char_indices()
.map(|(i, _)| i)
.take_while(|&i| i <= 200)
.last()
.unwrap_or(0);
format!("{}...", &result[..boundary])
} else {
result.clone()
};
let matched_idx = call_id
.as_deref()
.and_then(|cid| {
self.tool_log.iter().position(|e| {
e.call_id.as_deref() == Some(cid) && e.result_preview == "running..."
})
})
.or_else(|| {
self.tool_log
.iter()
.position(|e| e.name == name && e.result_preview == "running...")
})
.or_else(|| {
let idx = self.tool_log.iter().rposition(|e| e.name == name);
if idx.is_some() {
tracing::warn!(
tool = %name,
"ToolResult arrived without a matching pending ToolCall"
);
}
idx
});
if let Some(idx) = matched_idx {
self.tool_log[idx].result_preview = preview.clone();
self.tool_log[idx].success = success;
}
let is_mcp = name.starts_with("mcp__");
let args_summary = matched_idx
.and_then(|idx| self.tool_log.get(idx))
.map(|e| {
e.args_full
.as_deref()
.unwrap_or(&e.args_preview)
.to_string()
})
.unwrap_or_default();
push_bounded(
&mut self.messages,
ChatMessage::text(
MessageRole::Tool {
name,
success,
args_summary,
},
preview,
),
MAX_MESSAGES,
);
if is_mcp {
let min_display = self
.active_mcp_server_set_at
.map(|t| t.elapsed() >= std::time::Duration::from_secs(2))
.unwrap_or(true);
if min_display {
self.active_mcp_server = None;
self.active_mcp_server_set_at = None;
}
}
self.status_msg = "Thinking".to_string();
}
pub(super) fn ev_image_notice(&mut self, notice: String, install_hint: Option<String>) {
self.messages
.push(ChatMessage::text(MessageRole::System, notice));
if let Some(cmd) = install_hint {
self.popup = Some(PopupState {
title: " Image Processing ".to_string(),
content: String::new(),
scroll: 0,
kind: PopupKind::LspInstall {
language: "OCR (Tesseract)".to_string(),
server: "tesseract".to_string(),
install_cmd: cmd,
selected: 0,
},
saved_theme: None,
select_prefix: None,
search: String::new(),
});
}
}
pub(super) fn ev_soul_reflecting(&mut self, agent_name: String) {
const GLOBAL_MSGS: &[&str] = &[
"The day settles inward...",
"Tending to memory...",
"Weaving the day's threads...",
];
const AGENT_MSGS: &[&str] = &[
"inscribes the day...",
"lets the work take root...",
"turns dust into soul...",
];
let idx = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.subsec_nanos() as usize)
.unwrap_or(0);
let msg = if agent_name == crate::agent::soul::GLOBAL_SOUL {
GLOBAL_MSGS[idx % GLOBAL_MSGS.len()].to_string()
} else {
format!("{agent_name} {}", AGENT_MSGS[idx % AGENT_MSGS.len()])
};
self.soul_toast = Some((msg, std::time::Instant::now()));
}
pub(super) fn ev_swarm_agent_started(
&mut self,
agent_id: String,
agent_name: String,
task_preview: String,
) {
if self.swarm_status.is_none() {
tracing::warn!(
agent = %agent_name,
"SwarmAgentStarted received before swarm_status was initialized — \
auto-initializing with unknown mode"
);
self.swarm_status = Some(SwarmUiStatus {
mode_label: "HIVE".to_string(),
agents: Vec::new(),
phase: SwarmPhase::Executing,
pending_conflicts: Vec::new(),
});
}
self.worker_streams
.entry(agent_id.clone())
.or_insert_with(|| WorkerDetailState {
auto_scroll: true,
..Default::default()
});
if let Some(ref mut hive) = self.swarm_status {
hive.agents.push(SwarmAgentUiEntry {
agent_id,
name: agent_name.clone(),
task_preview,
status: SwarmAgentStatus::Pending,
iteration: 0,
modified_files: Vec::new(),
tool_calls: 0,
input_tokens: 0,
output_tokens: 0,
output: String::new(),
current_tool: None,
streaming_preview: String::new(),
last_activity: Some(std::time::Instant::now()),
});
hive.phase = SwarmPhase::Executing;
}
self.status_msg = format!("Hive agent '{agent_name}' started");
}
pub(super) fn ev_swarm_agent_progress(
&mut self,
agent_id: String,
iteration: u32,
status: String,
) {
if let Some(ref mut hive) = self.swarm_status
&& let Some(entry) = hive.agents.iter_mut().find(|a| a.agent_id == agent_id)
{
entry.iteration = iteration;
if entry.status == SwarmAgentStatus::Pending {
entry.status = SwarmAgentStatus::Running;
}
}
self.status_msg = status;
}
#[allow(clippy::too_many_arguments)]
pub(super) fn ev_swarm_agent_done(
&mut self,
agent_id: String,
success: bool,
modified_files: Vec<String>,
tool_calls: u32,
input_tokens: u64,
output_tokens: u64,
response: String,
) {
if let Some(ref mut hive) = self.swarm_status
&& let Some(entry) = hive.agents.iter_mut().find(|a| a.agent_id == agent_id)
{
entry.status = SwarmAgentStatus::Completed { success };
entry.modified_files = modified_files;
entry.tool_calls = tool_calls;
entry.input_tokens = input_tokens;
entry.output_tokens = output_tokens;
entry.output = response;
}
}
pub(super) fn ev_swarm_mode_switch(&mut self, label: String) {
if let Some(ref mut hive) = self.swarm_status {
hive.mode_label = label;
}
}
pub(super) fn ev_swarm_workers_dispatched(&mut self) {
self.agent_busy = false;
self.status_msg = "Workers running (background)".to_string();
}
pub(super) fn ev_swarm_worker_paused(&mut self, agent_id: &str) {
if let Some(ref mut hive) = self.swarm_status
&& let Some(entry) = hive.agents.iter_mut().find(|a| a.agent_id == agent_id)
{
entry.status = SwarmAgentStatus::Paused;
entry.last_activity = Some(std::time::Instant::now());
}
}
pub(super) fn ev_swarm_worker_resumed(&mut self, agent_id: &str) {
if let Some(ref mut hive) = self.swarm_status
&& let Some(entry) = hive.agents.iter_mut().find(|a| a.agent_id == agent_id)
{
entry.status = SwarmAgentStatus::Running;
entry.last_activity = Some(std::time::Instant::now());
}
}
pub(super) fn ev_swarm_resolved_to_single(&mut self, agent_label: String) {
self.swarm_status = None;
self.agent_mode = agent_label;
}
pub(super) fn ev_swarm_worker_approaching(
&mut self,
agent_id: String,
task_preview: String,
remaining: u32,
) {
let preview = if task_preview.is_empty() {
agent_id.clone()
} else {
task_preview
};
push_bounded(
&mut self.messages,
ChatMessage::text(
MessageRole::System,
format!(
"[swarm] worker {agent_id} approaching iteration limit ({remaining} remaining) — task: {preview}"
),
),
MAX_MESSAGES,
);
}
pub(super) fn ev_swarm_conflict(&mut self, conflicts: Vec<(String, Vec<String>)>) {
if let Some(ref mut hive) = self.swarm_status {
hive.pending_conflicts = conflicts;
hive.phase = SwarmPhase::ResolvingConflicts;
}
self.status_msg = "Resolving hive conflicts...".to_string();
}
pub(super) fn ev_error(&mut self, msg: String) {
if !self.streaming_buffer.is_empty() {
let text: String = self.streaming_buffer.drain(..).collect();
push_bounded(
&mut self.messages,
ChatMessage::text(MessageRole::Assistant, text),
MAX_MESSAGES,
);
}
push_bounded(
&mut self.messages,
ChatMessage::text(MessageRole::System, format!("[error] {msg}")),
MAX_MESSAGES,
);
self.agent_busy = false;
self.status_msg = "Error".to_string();
self.stream_retry = 0;
}
pub(super) fn ev_guard_stop(&mut self, msg: String) {
push_bounded(
&mut self.messages,
ChatMessage::text(MessageRole::System, msg),
MAX_MESSAGES,
);
self.agent_busy = false;
self.status_msg = "Guard stop".to_string();
}
pub(super) fn ev_stream_retry(&mut self, attempt: u32, max: u32, message: String) {
self.stream_retry = attempt;
self.stream_max_retries = max;
self.status_msg = message;
self.streaming_buffer.clear();
*self.streaming_render_cache.borrow_mut() = None;
}
pub(super) fn ev_phase_change(&mut self, label: String) {
if label.contains("compacted") {
self.spinner.set_compact();
self.compaction_count += 1;
} else {
push_bounded(
&mut self.messages,
ChatMessage::text(MessageRole::System, label.clone()),
MAX_MESSAGES,
);
}
if let Some(ref mut hive) = self.swarm_status {
if label.contains("plan") && (label.contains("review") || label.contains("Review")) {
hive.phase = SwarmPhase::PlanReview;
} else if label.contains("merging") || label.contains("Merging") {
hive.phase = SwarmPhase::MergingResults;
} else if label.contains("done") || label.contains("Done") || label.contains("complete")
{
hive.phase = SwarmPhase::Done;
}
}
self.status_msg = label;
}
pub(super) fn ev_status(
&mut self,
iteration: u32,
elapsed_secs: u64,
prompt_tokens: u32,
completion_tokens: u32,
cached_tokens: u32,
context_tokens: usize,
) {
self.iteration = iteration;
let _ = elapsed_secs;
if self.status_msg.starts_with("Approving:") {
self.status_msg = "Running".to_string();
}
if prompt_tokens > 0 || completion_tokens > 0 {
self.token_stats
.record(prompt_tokens as u64, completion_tokens as u64);
}
if prompt_tokens > 0 && cached_tokens > 0 {
self.cache_hit_rate = (cached_tokens as f64 / prompt_tokens as f64).min(1.0);
self.cache_tokens_saved = cached_tokens as usize;
}
if context_tokens > 0 {
self.context_used_tokens = context_tokens;
}
}
#[allow(clippy::too_many_arguments)]
pub(super) fn ev_performance_update(
&mut self,
tool_latency_avg_ms: f64,
tool_latency_max_ms: u64,
api_latency_avg_ms: f64,
api_latency_max_ms: u64,
tool_success_count: u32,
tool_failure_count: u32,
total_iterations: u32,
total_tokens_used: u64,
total_tool_calls_made: u32,
top_tools: Vec<(String, u32)>,
) {
let dm = &mut self.debug_monitor;
dm.tool_latency_avg_ms = tool_latency_avg_ms;
dm.tool_latency_max_ms = tool_latency_max_ms;
dm.api_latency_avg_ms = api_latency_avg_ms;
dm.api_latency_max_ms = api_latency_max_ms;
let total = tool_success_count + tool_failure_count;
dm.tool_success_rate = if total > 0 {
tool_success_count as f32 / total as f32 * 100.0
} else {
100.0
};
dm.tokens_per_iteration = if total_iterations > 0 {
total_tokens_used as f64 / total_iterations as f64
} else {
0.0
};
dm.tools_per_iteration = if total_iterations > 0 {
total_tool_calls_made as f64 / total_iterations as f64
} else {
0.0
};
dm.top_tools = top_tools;
}
pub(super) fn ev_swarm_agent_tool_call(&mut self, agent_id: &str, name: &str, args: &str) {
if let Some(ref mut hive) = self.swarm_status
&& let Some(entry) = hive.agents.iter_mut().find(|a| a.agent_id == agent_id)
{
entry.current_tool = Some(name.to_string());
entry.last_activity = Some(std::time::Instant::now());
}
let detail = self.worker_streams.entry(agent_id.to_string()).or_default();
let args_preview = if args.len() > 120 {
format!(
"{}...",
&args[..args
.char_indices()
.take_while(|&(i, _)| i <= 120)
.last()
.map(|(i, _)| i)
.unwrap_or(0)]
)
} else {
args.to_string()
};
detail.push_tool_event(WorkerToolEvent {
name: name.to_string(),
args_preview,
result_preview: "running...".to_string(),
success: true,
});
}
pub(super) fn ev_swarm_agent_tool_result(
&mut self,
agent_id: &str,
name: &str,
result: &str,
success: bool,
) {
if let Some(ref mut hive) = self.swarm_status
&& let Some(entry) = hive.agents.iter_mut().find(|a| a.agent_id == agent_id)
{
entry.tool_calls += 1;
entry.current_tool = None;
entry.last_activity = Some(std::time::Instant::now());
if !success {
tracing::debug!(agent = %agent_id, tool = %name, "Swarm agent tool failed");
}
}
if let Some(detail) = self.worker_streams.get_mut(agent_id) {
let result_preview = if result.len() > 200 {
format!(
"{}...",
&result[..result
.char_indices()
.take_while(|&(i, _)| i <= 200)
.last()
.map(|(i, _)| i)
.unwrap_or(0)]
)
} else {
result.to_string()
};
if let Some(evt) = detail
.tool_events
.iter_mut()
.rev()
.find(|e| e.name == name && e.result_preview == "running...")
{
evt.result_preview = result_preview;
evt.success = success;
}
}
}
pub(super) fn ev_swarm_agent_token(&mut self, agent_id: &str, text: &str) {
if let Some(ref mut hive) = self.swarm_status
&& let Some(entry) = hive.agents.iter_mut().find(|a| a.agent_id == agent_id)
{
const MAX_PREVIEW: usize = 200;
entry.streaming_preview.push_str(text);
if entry.streaming_preview.len() > MAX_PREVIEW {
let drain_to = entry.streaming_preview.len() - MAX_PREVIEW;
let safe = entry.streaming_preview.ceil_char_boundary(drain_to);
entry.streaming_preview.drain(..safe);
}
entry.last_activity = Some(std::time::Instant::now());
}
let detail = self.worker_streams.entry(agent_id.to_string()).or_default();
detail.push_stream(text);
}
pub(super) fn ev_swarm_agent_response(&mut self, agent_id: &str, text: &str) {
if let Some(ref mut hive) = self.swarm_status
&& let Some(entry) = hive.agents.iter_mut().find(|a| a.agent_id == agent_id)
{
entry.output = text.to_string();
entry.last_activity = Some(std::time::Instant::now());
}
}
}