fix(tui): throttle AgentProgress redraws to prevent freeze under subagent load (#3033)

When 4+ sub-agents run concurrently, each AgentProgress event triggers
a full terminal redraw via received_engine_event → needs_redraw.  The
render loop saturates, sidebar recomputation dominates the frame budget,
and terminal input events (including Ctrl+C) are starved.

Limit progress-driven redraws to at most one per 100ms per agent.  The
status-animation timer (80ms cadence) still guarantees sidebar updates.
Agent state is recorded immediately; the sidebar picks it up on the next
permitted redraw.

Adds last_agent_progress_redraw field to App to track throttle state.
This commit is contained in:
Hunter Bown
2026-06-10 15:47:35 -07:00
parent b23067bacd
commit 7b1446f7b0
2 changed files with 22 additions and 0 deletions
+4
View File
@@ -1445,6 +1445,9 @@ pub struct App {
pub pending_subagent_dispatch: Option<String>,
/// Animation anchor for status-strip active sub-agent spinner.
pub agent_activity_started_at: Option<Instant>,
/// Last time a sub-agent progress event triggered a redraw.
/// Used to throttle redraws under high sub-agent concurrency (#3033).
pub last_agent_progress_redraw: Option<Instant>,
pub ui_theme: UiTheme,
/// Active named theme. Drives the cell-level color remap in
/// `tui::color_compat::ColorCompatBackend` so community presets
@@ -2174,6 +2177,7 @@ impl App {
last_fanout_card_index: None,
pending_subagent_dispatch: None,
agent_activity_started_at: None,
last_agent_progress_redraw: None,
ui_theme,
theme_id,
onboarding,
+18
View File
@@ -2318,6 +2318,24 @@ async fn run_event_loop(
app.agent_activity_started_at = Some(Instant::now());
}
app.status_message = Some(format!("Sub-agent {id}: {display}"));
// #3033: Throttle redraws from rapid AgentProgress events.
// When 4+ sub-agents are running concurrently, each firing
// progress events, the per-event `needs_redraw = true` saturates
// the render loop and starves terminal input. Limit
// progress-driven repaints to at most one per 100ms; the
// status-animation timer (80ms cadence) provides a guaranteed
// floor for sidebar updates. Data is still recorded immediately;
// the sidebar picks it up on the next permitted redraw.
let now = Instant::now();
if let Some(last) = app.last_agent_progress_redraw {
if now.duration_since(last) < Duration::from_millis(100) {
received_engine_event = false;
} else {
app.last_agent_progress_redraw = Some(now);
}
} else {
app.last_agent_progress_redraw = Some(now);
}
}
EngineEvent::AgentComplete { id, result } => {
execute_subagent_observer_hook(