diff --git a/crates/tui/src/tui/footer_ui.rs b/crates/tui/src/tui/footer_ui.rs index 1ced179b..52cf066b 100644 --- a/crates/tui/src/tui/footer_ui.rs +++ b/crates/tui/src/tui/footer_ui.rs @@ -78,10 +78,29 @@ pub(crate) fn render_footer(f: &mut Frame, area: Rect, app: &mut App) { let dot_frame = footer_working_label_frame(now_ms, app.fancy_animations); // Surface one compact live status row in the footer whenever a turn // is live. Tool turns get the current action plus active/done counts; - // non-tool work falls back to the existing dot-pulse label. + // non-tool work falls back to a descriptive label with elapsed time. + let elapsed_secs = app + .turn_started_at + .map(|t| t.elapsed().as_secs()) + .unwrap_or(0); let mut label = active_subagent_status_label(app) .or_else(|| active_tool_status_label(app)) - .unwrap_or_else(|| crate::tui::widgets::footer_working_label(dot_frame, app.ui_locale)); + .unwrap_or_else(|| { + // Show a more specific label when the model is still loading + // or compacting, not just a generic "working…". + let base = if app.is_loading { + crate::tui::widgets::footer_working_label(dot_frame, app.ui_locale) + } else if app.is_compacting { + "compacting" + } else { + crate::tui::widgets::footer_working_label(dot_frame, app.ui_locale) + }; + if elapsed_secs > 0 { + format!("{base} ({elapsed_secs}s)") + } else { + base.to_string() + } + }); // Append stall reason when the turn has been running > 30 s. if let Some(reason) = stall_reason(app) { label = format!("{label} ({reason})"); diff --git a/crates/tui/src/tui/history.rs b/crates/tui/src/tui/history.rs index 477eafa0..90a07b9e 100644 --- a/crates/tui/src/tui/history.rs +++ b/crates/tui/src/tui/history.rs @@ -21,7 +21,7 @@ use crate::tui::markdown_render; use std::process::Command; const TOOL_COMMAND_LINE_LIMIT: usize = 3; const TOOL_OUTPUT_LINE_LIMIT: usize = 6; -const TOOL_TEXT_LIMIT: usize = 180; +const TOOL_TEXT_LIMIT: usize = 300; const TOOL_HEADER_SUMMARY_LIMIT: usize = 56; const TOOL_OUTPUT_HEAD_LINES: usize = 2; const TOOL_OUTPUT_TAIL_LINES: usize = 2; diff --git a/crates/tui/src/tui/notifications.rs b/crates/tui/src/tui/notifications.rs index 1087c4ac..d2b068e8 100644 --- a/crates/tui/src/tui/notifications.rs +++ b/crates/tui/src/tui/notifications.rs @@ -87,8 +87,13 @@ fn resolve_method() -> Method { _ => {} } + // Windows: use BEL so `windows_bell()` (MessageBeep) fires on turn + // completion. Previous behavior returned `Off` to avoid the error chime + // (#583), but `MessageBeep(MB_OK)` plays the *default system sound* — + // distinct from the error sound — so BEL is safe and gives Windows users + // audible feedback when a long turn finishes. if cfg!(target_os = "windows") { - return Method::Off; + return Method::Bel; } // Ghostty-based terminals (cmux, etc.) may not set their own diff --git a/crates/tui/src/tui/ui.rs b/crates/tui/src/tui/ui.rs index 73c86c63..758f84a9 100644 --- a/crates/tui/src/tui/ui.rs +++ b/crates/tui/src/tui/ui.rs @@ -1546,6 +1546,10 @@ async fn run_event_loop( } // Generate post-turn receipt for completed turns. + // Also push a persistent status toast so users always + // see the outcome in the footer (not just the 8-second + // composer receipt), regardless of notification method + // or platform. if status == crate::core::events::TurnOutcomeStatus::Completed { let tool_count = app.tool_evidence.len(); let mut receipt = "✓ turn completed".to_string(); @@ -1561,6 +1565,15 @@ async fn run_event_loop( } } app.set_receipt_text(receipt); + // Mirror as a persistent status toast (10s TTL). + // The footer bar visibly shows status toasts, + // which is more glanceable than the composer + // border receipt alone. + app.push_status_toast( + receipt, + crate::tui::app::StatusToastLevel::Info, + Some(10_000), + ); } // Auto-save completed turn and clear crash checkpoint.