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:
@@ -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})");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user