diff --git a/crates/tui/src/tui/ui.rs b/crates/tui/src/tui/ui.rs index e73ec537..c53039e7 100644 --- a/crates/tui/src/tui/ui.rs +++ b/crates/tui/src/tui/ui.rs @@ -3930,6 +3930,8 @@ fn reconcile_turn_liveness(app: &mut App, now: Instant, has_running_agents: bool app.runtime_turn_status = None; app.runtime_turn_id = None; app.dispatch_started_at = None; + // Per-turn scroll lock — clear so the next turn auto-scrolls. + app.user_scrolled_during_stream = false; app.push_status_toast( "Turn stalled — no completion signal received. Please try again.", StatusToastLevel::Error, diff --git a/crates/tui/src/tui/ui/tests.rs b/crates/tui/src/tui/ui/tests.rs index d41cf68b..5bc6fc8a 100644 --- a/crates/tui/src/tui/ui/tests.rs +++ b/crates/tui/src/tui/ui/tests.rs @@ -2040,6 +2040,7 @@ fn turn_liveness_recovers_stalled_in_progress_turn() { app.turn_started_at = Some(Instant::now() - TURN_STALL_WATCHDOG_TIMEOUT - Duration::from_millis(1)); app.streaming_message_index = Some(0); + app.user_scrolled_during_stream = true; let recovered = reconcile_turn_liveness(&mut app, Instant::now(), false); @@ -2051,6 +2052,7 @@ fn turn_liveness_recovers_stalled_in_progress_turn() { assert!(app.dispatch_started_at.is_none()); assert!(app.streaming_message_index.is_none()); assert!(app.streaming_thinking_active_entry.is_none()); + assert!(!app.user_scrolled_during_stream); let toast = app.status_toasts.back().expect("stall toast"); assert_eq!(toast.level, StatusToastLevel::Error); assert!(toast.text.contains("Turn stalled"));