Render watchdog countdown in subagent panel

This commit is contained in:
Friel
2026-03-29 21:11:31 +00:00
parent a2f37d5548
commit 45bf65d007
5 changed files with 77 additions and 6 deletions
+3
View File
@@ -1121,6 +1121,9 @@ impl SubagentRegistry {
name: info.name.clone(),
status: info.status.clone(),
is_watchdog: info.is_watchdog(),
watchdog_countdown_started_at: info
.is_watchdog()
.then_some(info.running_started_at()),
preview: running_preview(info),
latest_update_at: info.latest_update_at,
})
@@ -1,7 +1,9 @@
---
source: tui/src/chatwidget/tests.rs
assertion_line: 2568
expression: terminal.backend().vt100().screen().contents()
---
╭───────────────────────────────────────╮
│ >_ OpenAI Codex (v0.0.0) │
│ │
@@ -10,7 +12,7 @@ expression: terminal.backend().vt100().screen().contents()
╰───────────────────────────────────────╯
• Subagents (0s • no subagents running • esc to interrupt)
• [#1] [watchdog] watchdog-agent idle — watchdog idle
• [#1] [watchdog] watchdog-agent idle (59s) — watchdog idle
› show current subagent state
@@ -0,0 +1,6 @@
---
source: tui/src/chatwidget/tests.rs
assertion_line: 464
expression: rendered
---
• Agent message: Please review the latest diff from 019cbff7-558b-77d3-8653-8238ab5361ec
+2
View File
@@ -2483,6 +2483,7 @@ async fn subagent_panel_is_not_flushed_into_transcript_history() {
name: "user-request-derisk-implement".to_string(),
status: AgentStatus::PendingInit,
is_watchdog: true,
watchdog_countdown_started_at: Some(Instant::now()),
preview: "watchdog idle".to_string(),
latest_update_at: Instant::now(),
}],
@@ -2532,6 +2533,7 @@ async fn subagent_panel_mounts_while_placeholder_active_cell_exists_snapshot() {
name: "watchdog-agent".to_string(),
status: AgentStatus::PendingInit,
is_watchdog: true,
watchdog_countdown_started_at: Some(Instant::now()),
preview: "watchdog idle".to_string(),
latest_update_at: Instant::now(),
}],
+63 -5
View File
@@ -507,6 +507,7 @@ pub(crate) struct SubagentPanelAgent {
pub(crate) name: String,
pub(crate) status: AgentStatus,
pub(crate) is_watchdog: bool,
pub(crate) watchdog_countdown_started_at: Option<Instant>,
pub(crate) preview: String,
pub(crate) latest_update_at: Instant,
}
@@ -530,7 +531,7 @@ impl SubagentPanelState {
pub(crate) fn has_animating_agents(&self, now: Instant) -> bool {
self.running_agents
.iter()
.any(|agent| should_shimmer(agent, now))
.any(|agent| should_shimmer(agent, now) || has_watchdog_countdown(agent, now))
}
}
@@ -604,7 +605,7 @@ impl HistoryCell for SubagentStatusCell {
}
spans.push(Span::from(agent.name.clone()));
spans.push(" ".into());
spans.push(status_span_for_panel(&agent));
spans.push(status_span_for_panel(&agent, now));
spans.push(" — ".dim());
if self.animations_enabled && should_shimmer(&agent, now) {
spans.extend(shimmer_spans(&preview));
@@ -708,9 +709,15 @@ fn is_running_status(status: &AgentStatus) -> bool {
}
#[cfg_attr(not(test), allow(dead_code))]
fn status_span_for_panel(agent: &SubagentPanelAgent) -> Span<'static> {
fn status_span_for_panel(agent: &SubagentPanelAgent, now: Instant) -> Span<'static> {
match &agent.status {
AgentStatus::PendingInit if agent.is_watchdog => "idle".dim(),
AgentStatus::PendingInit if agent.is_watchdog => {
if let Some(countdown) = watchdog_countdown_remaining(agent, now) {
format!("idle ({})", fmt_elapsed_compact(countdown.as_secs())).dim()
} else {
"idle".dim()
}
}
AgentStatus::PendingInit | AgentStatus::Running => "running".cyan().bold(),
AgentStatus::Interrupted => "interrupted".magenta(),
AgentStatus::Completed(_) => "completed".green(),
@@ -722,6 +729,7 @@ fn status_span_for_panel(agent: &SubagentPanelAgent) -> Span<'static> {
#[cfg_attr(not(test), allow(dead_code))]
const SUBAGENT_SHIMMER_WINDOW: Duration = Duration::from_secs(1);
const WATCHDOG_COUNTDOWN: Duration = Duration::from_secs(60);
#[cfg_attr(not(test), allow(dead_code))]
fn should_shimmer(agent: &SubagentPanelAgent, now: Instant) -> bool {
@@ -732,6 +740,26 @@ fn should_shimmer(agent: &SubagentPanelAgent, now: Instant) -> bool {
&& now.saturating_duration_since(agent.latest_update_at) <= SUBAGENT_SHIMMER_WINDOW
}
#[cfg_attr(not(test), allow(dead_code))]
fn has_watchdog_countdown(agent: &SubagentPanelAgent, now: Instant) -> bool {
watchdog_countdown_remaining(agent, now).is_some_and(|remaining| remaining > Duration::ZERO)
}
#[cfg_attr(not(test), allow(dead_code))]
fn watchdog_countdown_remaining(agent: &SubagentPanelAgent, now: Instant) -> Option<Duration> {
if !agent.is_watchdog {
return None;
}
if !matches!(agent.status, AgentStatus::PendingInit) {
return None;
}
let Some(started_at) = agent.watchdog_countdown_started_at else {
return None;
};
let elapsed = now.saturating_duration_since(started_at);
Some(WATCHDOG_COUNTDOWN.saturating_sub(elapsed))
}
#[allow(dead_code)]
fn status_label_span(status: &AgentStatus) -> Span<'static> {
match status {
@@ -3638,6 +3666,7 @@ mod tests {
name: "watchdog-agent".to_string(),
status: AgentStatus::PendingInit,
is_watchdog: true,
watchdog_countdown_started_at: Some(Instant::now()),
preview: "monitor parent progress".to_string(),
latest_update_at: Instant::now(),
}],
@@ -3647,10 +3676,11 @@ mod tests {
assert!(lines[0].contains("no subagents running"));
assert!(lines[1].contains("[watchdog] watchdog-agent idle"));
assert!(lines[1].contains("idle ("));
}
#[test]
fn subagent_panel_animation_tick_ignores_idle_watchdogs() {
fn subagent_panel_animation_tick_ticks_idle_watchdogs() {
let state = Arc::new(Mutex::new(SubagentPanelState {
started_at: Instant::now(),
total_agents: 1,
@@ -3660,6 +3690,32 @@ mod tests {
name: "watchdog-agent".to_string(),
status: AgentStatus::PendingInit,
is_watchdog: true,
watchdog_countdown_started_at: Some(Instant::now()),
preview: "monitor parent progress".to_string(),
latest_update_at: Instant::now(),
}],
}));
let cell = SubagentStatusCell::new(state, /*animations_enabled*/ true);
assert!(cell.transcript_animation_tick().is_some());
}
#[test]
fn subagent_panel_animation_tick_stops_after_countdown_expires() {
let state = Arc::new(Mutex::new(SubagentPanelState {
started_at: Instant::now(),
total_agents: 1,
running_count: 0,
running_agents: vec![SubagentPanelAgent {
ordinal: 1,
name: "watchdog-agent".to_string(),
status: AgentStatus::PendingInit,
is_watchdog: true,
watchdog_countdown_started_at: Some(
Instant::now()
.checked_sub(Duration::from_secs(61))
.unwrap_or_else(Instant::now),
),
preview: "monitor parent progress".to_string(),
latest_update_at: Instant::now(),
}],
@@ -3680,6 +3736,7 @@ mod tests {
name: "worker-agent".to_string(),
status: AgentStatus::Running,
is_watchdog: false,
watchdog_countdown_started_at: None,
preview: "working".to_string(),
latest_update_at: Instant::now(),
}],
@@ -3706,6 +3763,7 @@ mod tests {
name: "worker-agent".to_string(),
status: AgentStatus::Running,
is_watchdog: false,
watchdog_countdown_started_at: None,
preview: "working".to_string(),
latest_update_at: stale_update,
}],