From eec6d470cc3723d098e8dedb1087c7686fc7ac27 Mon Sep 17 00:00:00 2001 From: donglovejava <211940267+donglovejava@users.noreply.github.com> Date: Tue, 26 May 2026 14:05:13 +0800 Subject: [PATCH] fix: improve user feedback during long operations and on turn completion Three interrelated feedback issues resolved: 1. Windows desktop notifications (notifications.rs) - resolve_method() now returns Method::Bel on Windows instead of Method::Off, so windows_bell() (MessageBeep) fires a system notification sound when a long turn completes. 2. Tool output summary truncation (history.rs) - TOOL_TEXT_LIMIT increased from 180 to 300 characters, reducing the chance that meaningful tool output is cut short in the one-line summary shown in tool cards. 3. Turn completion status visibility (ui.rs) - Added a push_status_toast(Info, 10s TTL) call alongside the existing set_receipt_text() in the TurnComplete handler. The receipt text is now visible in both the composer border (8s) and the footer status bar (10s). 4. Footer working-time feedback (footer_ui.rs) - Footer state label now includes elapsed seconds from turn_started_at immediately, instead of waiting 30 seconds for the stall_reason classification to kick in. - When app.is_loading is true, the label shows elapsed time so users see ongoing progress during model-loading phases. --- crates/tui/src/tui/footer_ui.rs | 23 +++++++++++++++++++++-- crates/tui/src/tui/history.rs | 2 +- crates/tui/src/tui/notifications.rs | 7 ++++++- crates/tui/src/tui/ui.rs | 13 +++++++++++++ 4 files changed, 41 insertions(+), 4 deletions(-) diff --git a/crates/tui/src/tui/footer_ui.rs b/crates/tui/src/tui/footer_ui.rs index 0269c8dea..eca227a3a 100644 --- a/crates/tui/src/tui/footer_ui.rs +++ b/crates/tui/src/tui/footer_ui.rs @@ -71,10 +71,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 477eafa01..90a07b9e2 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 670f73f9b..6088368e3 100644 --- a/crates/tui/src/tui/notifications.rs +++ b/crates/tui/src/tui/notifications.rs @@ -85,8 +85,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 61cb195c7..637e783d7 100644 --- a/crates/tui/src/tui/ui.rs +++ b/crates/tui/src/tui/ui.rs @@ -1490,6 +1490,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(); @@ -1505,6 +1509,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.