fix: improve user feedback during long operations and on turn completion (#2166)

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.
This commit is contained in:
dongchao
2026-05-26 23:28:58 +08:00
committed by GitHub
parent cd357de0c8
commit 87f7f05fee
4 changed files with 41 additions and 4 deletions
+21 -2
View File
@@ -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})");
+1 -1
View File
@@ -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;
+6 -1
View File
@@ -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
+13
View File
@@ -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.