@@ -9,6 +9,7 @@ use std::sync::atomic::AtomicBool;
99use std::sync::atomic::AtomicU64;
1010use std::sync::atomic::Ordering;
1111use std::time::Duration;
12+ use std::time::Instant;
1213
1314use ratatui::style::Modifier;
1415use ratatui::style::Style;
@@ -218,6 +219,8 @@ pub(crate) struct ChatWidget<'a> {
218219 agent_task: Option<String>,
219220 overall_task_status: String,
220221 active_plan_title: Option<String>,
222+ /// Runtime timing per-agent (by id) to improve visibility in the HUD
223+ agent_runtime: HashMap<String, AgentRuntime>,
221224 // Sparkline data for showing agent activity (using RefCell for interior mutability)
222225 // Each tuple is (value, is_completed) where is_completed indicates if any agent was complete at that time
223226 sparkline_data: std::cell::RefCell<Vec<(u64, bool)>>,
@@ -341,6 +344,16 @@ struct PendingJumpBack {
341344 removed_cells: Vec<Box<dyn HistoryCell>>, // cells removed from the end (from selected user message onward)
342345}
343346
347+ #[derive(Debug, Clone, Default)]
348+ struct AgentRuntime {
349+ /// First time this agent entered Running
350+ started_at: Option<Instant>,
351+ /// Time of the latest status update we observed
352+ last_update: Option<Instant>,
353+ /// Time the agent reached a terminal state (Completed/Failed)
354+ completed_at: Option<Instant>,
355+ }
356+
344357// ---------- Stable ordering & routing helpers ----------
345358#[derive(Clone, Copy, Debug, PartialEq, Eq)]
346359struct OrderKey {
@@ -385,10 +398,19 @@ use self::perf::PerfStats;
385398
386399#[derive(Debug, Clone)]
387400struct AgentInfo {
401+ // Stable id to correlate updates
402+ id: String,
403+ // Display name
388404 name: String,
405+ // Current status
389406 status: AgentStatus,
407+ // Optional model name
408+ model: Option<String>,
409+ // Final success message when completed
390410 result: Option<String>,
411+ // Final error message when failed
391412 error: Option<String>,
413+ // Most recent progress line from core
392414 last_progress: Option<String>,
393415}
394416
@@ -486,6 +508,19 @@ enum SystemPlacement {
486508}
487509
488510impl ChatWidget<'_> {
511+ fn fmt_short_duration(&self, d: Duration) -> String {
512+ let s = d.as_secs();
513+ let h = s / 3600;
514+ let m = (s % 3600) / 60;
515+ let sec = s % 60;
516+ if h > 0 {
517+ format!("{}h{}m", h, m)
518+ } else if m > 0 {
519+ format!("{}m{}s", m, sec)
520+ } else {
521+ format!("{}s", sec)
522+ }
523+ }
489524 fn is_branch_worktree_path(path: &std::path::Path) -> bool {
490525 for ancestor in path.ancestors() {
491526 if ancestor
@@ -1895,6 +1930,7 @@ impl ChatWidget<'_> {
18951930 agent_task: None,
18961931 overall_task_status: "preparing".to_string(),
18971932 active_plan_title: None,
1933+ agent_runtime: HashMap::new(),
18981934 sparkline_data: std::cell::RefCell::new(Vec::new()),
18991935 last_sparkline_update: std::cell::RefCell::new(std::time::Instant::now()),
19001936 stream: crate::streaming::controller::StreamController::new(config.clone()),
@@ -2081,6 +2117,7 @@ impl ChatWidget<'_> {
20812117 agent_task: None,
20822118 overall_task_status: "preparing".to_string(),
20832119 active_plan_title: None,
2120+ agent_runtime: HashMap::new(),
20842121 sparkline_data: std::cell::RefCell::new(Vec::new()),
20852122 last_sparkline_update: std::cell::RefCell::new(std::time::Instant::now()),
20862123 stream: crate::streaming::controller::StreamController::new(config.clone()),
@@ -4340,15 +4377,34 @@ impl ChatWidget<'_> {
43404377 .update_status_text("using browser (CDP)".to_string());
43414378 }
43424379 }
4343- EventMsg::AgentStatusUpdate(AgentStatusUpdateEvent {
4344- agents,
4345- context,
4346- task,
4347- }) => {
4348- // Update the active agents list from the event
4380+ EventMsg::AgentStatusUpdate(AgentStatusUpdateEvent { agents, context, task }) => {
4381+ // Update the active agents list from the event and track timing
43494382 self.active_agents.clear();
4383+ let now = Instant::now();
43504384 for agent in agents {
4385+ // Update runtime map
4386+ let entry = self
4387+ .agent_runtime
4388+ .entry(agent.id.clone())
4389+ .or_insert_with(AgentRuntime::default);
4390+ entry.last_update = Some(now);
4391+ match agent.status.as_str() {
4392+ "running" => {
4393+ if entry.started_at.is_none() {
4394+ entry.started_at = Some(now);
4395+ }
4396+ }
4397+ "completed" | "failed" => {
4398+ if entry.completed_at.is_none() {
4399+ entry.completed_at = entry.completed_at.or(Some(now));
4400+ }
4401+ }
4402+ _ => {}
4403+ }
4404+
4405+ // Mirror agent list for rendering
43514406 self.active_agents.push(AgentInfo {
4407+ id: agent.id.clone(),
43524408 name: agent.name.clone(),
43534409 status: match agent.status.as_str() {
43544410 "pending" => AgentStatus::Pending,
@@ -4357,6 +4413,7 @@ impl ChatWidget<'_> {
43574413 "failed" => AgentStatus::Failed,
43584414 _ => AgentStatus::Pending,
43594415 },
4416+ model: agent.model,
43604417 result: agent.result,
43614418 error: agent.error,
43624419 last_progress: agent.last_progress,
@@ -10890,13 +10947,26 @@ impl ChatWidget<'_> {
1089010947 } else {
1089110948 let mut parts: Vec<String> = Vec::new();
1089210949 for a in self.active_agents.iter().take(3) {
10893- let s = match a.status {
10894- AgentStatus::Pending => "pending",
10895- AgentStatus::Running => "running",
10896- AgentStatus::Completed => "done",
10897- AgentStatus::Failed => "failed",
10950+ let state = match a.status {
10951+ AgentStatus::Pending => "pending".to_string(),
10952+ AgentStatus::Running => {
10953+ // Show elapsed running time when available
10954+ if let Some(rt) = self.agent_runtime.get(&a.id) {
10955+ if let Some(start) = rt.started_at {
10956+ let now = Instant::now();
10957+ let elapsed = now.saturating_duration_since(start);
10958+ format!("running {}", self.fmt_short_duration(elapsed))
10959+ } else {
10960+ "running".to_string()
10961+ }
10962+ } else {
10963+ "running".to_string()
10964+ }
10965+ }
10966+ AgentStatus::Completed => "done".to_string(),
10967+ AgentStatus::Failed => "failed".to_string(),
1089810968 };
10899- let mut label = format!("{} ({})", a.name, s );
10969+ let mut label = format!("{} ({})", a.name, state );
1090010970 if matches!(a.status, AgentStatus::Running) {
1090110971 if let Some(lp) = &a.last_progress {
1090210972 let mut lp_trim = lp.trim().to_string();
@@ -11190,23 +11260,88 @@ impl ChatWidget<'_> {
1119011260 AgentStatus::Failed => crate::colors::error(),
1119111261 };
1119211262
11263+ // Build status + timing suffix where available
1119311264 let status_text = match agent.status {
11194- AgentStatus::Pending => "pending",
11195- AgentStatus::Running => "running",
11196- AgentStatus::Completed => "completed",
11197- AgentStatus::Failed => "failed",
11265+ AgentStatus::Pending => "pending".to_string(),
11266+ AgentStatus::Running => {
11267+ if let Some(rt) = self.agent_runtime.get(&agent.id) {
11268+ if let Some(start) = rt.started_at {
11269+ let now = Instant::now();
11270+ let elapsed = now.saturating_duration_since(start);
11271+ format!("running {}", self.fmt_short_duration(elapsed))
11272+ } else {
11273+ "running".to_string()
11274+ }
11275+ } else {
11276+ "running".to_string()
11277+ }
11278+ }
11279+ AgentStatus::Completed | AgentStatus::Failed => {
11280+ if let Some(rt) = self.agent_runtime.get(&agent.id) {
11281+ if let (Some(start), Some(done)) = (rt.started_at, rt.completed_at) {
11282+ let dur = done.saturating_duration_since(start);
11283+ let base = if matches!(agent.status, AgentStatus::Completed) {
11284+ "completed"
11285+ } else {
11286+ "failed"
11287+ };
11288+ format!("{} {}", base, self.fmt_short_duration(dur))
11289+ } else {
11290+ match agent.status {
11291+ AgentStatus::Completed => "completed".to_string(),
11292+ AgentStatus::Failed => "failed".to_string(),
11293+ _ => unreachable!(),
11294+ }
11295+ }
11296+ } else {
11297+ match agent.status {
11298+ AgentStatus::Completed => "completed".to_string(),
11299+ AgentStatus::Failed => "failed".to_string(),
11300+ _ => unreachable!(),
11301+ }
11302+ }
11303+ }
1119811304 };
1119911305
11200- text_content.push(RLine::from(vec![
11201- Span::from(" "),
11306+ let mut line_spans: Vec<Span> = Vec::new();
11307+ line_spans.push(Span::from(" "));
11308+ line_spans.push(
1120211309 Span::styled(
11203- format!("{}: ", agent.name),
11310+ format!("{}", agent.name),
1120411311 Style::default()
1120511312 .fg(crate::colors::text())
1120611313 .add_modifier(Modifier::BOLD),
1120711314 ),
11208- Span::styled(status_text, Style::default().fg(status_color)),
11209- ]));
11315+ );
11316+ if let Some(ref model) = agent.model {
11317+ if !model.is_empty() {
11318+ line_spans.push(Span::styled(
11319+ format!(" ({})", model),
11320+ Style::default().fg(crate::colors::text_dim()),
11321+ ));
11322+ }
11323+ }
11324+ line_spans.push(Span::from(": "));
11325+ line_spans.push(Span::styled(status_text, Style::default().fg(status_color)));
11326+ text_content.push(RLine::from(line_spans));
11327+
11328+ // For running agents, show latest progress hint if available
11329+ if matches!(agent.status, AgentStatus::Running) {
11330+ if let Some(ref lp) = agent.last_progress {
11331+ let mut lp_trim = lp.trim().to_string();
11332+ if lp_trim.len() > 120 {
11333+ lp_trim.truncate(120);
11334+ lp_trim.push('…');
11335+ }
11336+ text_content.push(RLine::from(vec![
11337+ Span::from(" "),
11338+ Span::styled(
11339+ lp_trim,
11340+ Style::default().fg(crate::colors::text_dim()),
11341+ ),
11342+ ]));
11343+ }
11344+ }
1121011345
1121111346 // For completed/failed agents, show their final message or error
1121211347 match agent.status {
0 commit comments