Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions crates/tui/src/tui/footer_ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
};
Comment on lines +84 to +90
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

This block will fail to compile due to a type mismatch. The if branch returns a String (from footer_working_label), but the else if branch returns a &'static str ("compacting"). In Rust, all branches of an if expression must evaluate to the same type.

Additionally, "compacting" is a hardcoded user-facing string, which violates internationalization (i18n) best practices. Consider localizing this string in the future.

Suggested change
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)
};
let base = if app.is_loading {
crate::tui::widgets::footer_working_label(dot_frame, app.ui_locale)
} else if app.is_compacting {
"compacting".to_string()
} else {
crate::tui::widgets::footer_working_label(dot_frame, app.ui_locale)
};

Comment on lines +84 to +90
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 The if app.is_loading branch and the else fallback branch produce identical output — both call footer_working_label(dot_frame, app.ui_locale). The is_loading condition is a dead branch.

Suggested change
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)
};
let base = if app.is_compacting {
"compacting"
} else {
crate::tui::widgets::footer_working_label(dot_frame, app.ui_locale)
};

Fix in Codex Fix in Claude Code Fix in Cursor

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})");
Expand Down
2 changes: 1 addition & 1 deletion crates/tui/src/tui/history.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
7 changes: 6 additions & 1 deletion crates/tui/src/tui/notifications.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 13 additions & 0 deletions crates/tui/src/tui/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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),
);
Comment on lines +1512 to +1520
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

This will cause a use of moved value compilation error. The receipt variable (which is a String) is moved into app.set_receipt_text(receipt) on line 1511, making it unavailable for the subsequent app.push_status_toast(receipt, ...) call.

To resolve this, you must clone receipt when passing it to set_receipt_text on line 1511:

app.set_receipt_text(receipt.clone());

Comment on lines 1511 to +1520
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P0 Use of moved value — compile error

app.set_receipt_text(receipt) takes receipt: String by value via impl Into<String>, moving it. The subsequent app.push_status_toast(receipt, ...) on line 1516 then references the already-moved binding, which Rust rejects with E0382 "use of moved value". The PR cannot compile as written.

The fix is to clone receipt before the first call, or pass a reference for the first call (since &String: Into<String>).

Fix in Codex Fix in Claude Code Fix in Cursor

}

// Auto-save completed turn and clear crash checkpoint.
Expand Down