Skip to content

Commit 458bdf3

Browse files
authored
Merge pull request #192 from just-every/issue-191 Close #191
[agent-progress-ui] feat(tui/agents): show per-agent elapsed time and progress in HUD
2 parents 600e4cd + 8add2c4 commit 458bdf3

1 file changed

Lines changed: 156 additions & 21 deletions

File tree

codex-rs/tui/src/chatwidget.rs

Lines changed: 156 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use std::sync::atomic::AtomicBool;
99
use std::sync::atomic::AtomicU64;
1010
use std::sync::atomic::Ordering;
1111
use std::time::Duration;
12+
use std::time::Instant;
1213

1314
use ratatui::style::Modifier;
1415
use 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)]
346359
struct OrderKey {
@@ -385,10 +398,19 @@ use self::perf::PerfStats;
385398

386399
#[derive(Debug, Clone)]
387400
struct 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

488510
impl 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

Comments
 (0)