perf(tui): refresh status chrome once a second while a decision prompt waits on you

Refs #6728 (CW-6993).

Measured on macOS against a local mock provider. A session sitting on an
approval prompt used 3.1-3.2% CPU with the installed 0.10.2 release build
(91dbd4f785), against 0.41% on the launch screen and 0.35% idle after a
finished turn. With NO_ANIMATIONS=1 the same approval wait used 0.53%, so
the extra cost was motion. A `sample` of the debug build put the main
thread's active time in draw_app_frame_inner: ApprovalView and
DecisionBand re-planning a full frame on every status tick.

Cause: while a turn waits on approval, `is_loading` and the pending tool
card keep should_tick_status_animation true, so the 5 Hz braille status
clock (UI_STATUS_ANIMATION_MS, 200 ms) redraws the whole frame. Ambient
ocean motion already stops whenever a view is on the stack
(decorative_shell_motion_enabled), so the status clock was the only driver.
Behind a prompt waiting on the person the only thing that needs to stay
current is the second-resolution elapsed label ("running (19s)").

Fix: status_animation_interval_ms returns 1,000 ms while an Approval,
Elevation or UserInput prompt is on top. Low-motion still wins with its own
2.4 s cadence. Input, engine events and sub-agent progress still set
needs_redraw directly, so they are not slowed.

Evidence (same tree, my change reverted vs applied, debug builds, 60 s
idle on an approval prompt, two runs each, CPU summed over codewhale and
its tmux host): before 14.68% / 14.73%, after 3.91% / 3.50%. tmux pane
captures 2 s apart still show the elapsed label advancing (19s -> 21s).
Artifacts: CW/artifacts/cw6993-idle-20261018 (measure.sh, results.txt,
samples, screens).

Tests: scripts/dev-test.sh tui motion 56 passed; cadence 13 passed;
status_animation 5 passed; approval 405 passed. clippy -p codewhale-tui
--all-targets --locked with CI flags: clean. cargo fmt --check clean.
sync-changelog --check up to date.

Not addressed: the card for a command waiting on approval still says
"running (Ns)" with a spinner although nothing has started. That label is
a UX decision, left for the founder. Not measured on FreeBSD (the reporter's
platform) or in a release build of this tree.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NBKbhx19BRbZtvWK8rVKjz
This commit is contained in:
CodeWhale BotandClaude Opus 5.5 committed 2026-10-18 05:13:13 -07:00
1 parent 9a34d80a83
commit cebea8cd49
4 files changed
+12

No files matched your search

+3
View File
@@ -16,6 +16,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
failure mark.
- The keybinding docs now say what Alt-V opens with nothing selected, and
that clicking a card keeps Alt-V on it while newer cards arrive.
- The TUI uses less CPU while it waits on you. With an approval, elevation
or question prompt open, the status spinner and elapsed-time labels behind
it refresh once a second instead of five times a second.
- On Windows, Codewhale processes that start at the same moment no longer
lose the built-in plugins. The rename that publishes the built-in plugin
snapshot now retries a brief sharing or access refusal, as the other atomic
+3
View File
@@ -16,6 +16,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
failure mark.
- The keybinding docs now say what Alt-V opens with nothing selected, and
that clicking a card keeps Alt-V on it while newer cards arrive.
- The TUI uses less CPU while it waits on you. With an approval, elevation
or question prompt open, the status spinner and elapsed-time labels behind
it refresh once a second instead of five times a second.
- On Windows, Codewhale processes that start at the same moment no longer
lose the built-in plugins. The rename that publishes the built-in plugin
snapshot now retries a brief sharing or access refusal, as the other atomic
+1
View File
@@ -213,6 +213,7 @@ const TOOL_HANG_WATCHDOG_TIMEOUT: Duration = Duration::from_secs(600);
// the per-tool spinner pulse — keep this fast enough that the whale-spout
// braille pattern reads as continuous motion instead of teleport-frames.
const UI_STATUS_ANIMATION_MS: u64 = crate::tui::spinner::BRAILLE_SPINNER_FRAME_MS;
const UI_DECISION_PROMPT_STATUS_ANIMATION_MS: u64 = 1_000;
/// Ambient fish, the idle-mark caustic, and the completion wake use a modest
/// ~12.5fps clock by default. On measured high-Hz displays the adaptive probe
/// may raise this (still bounded); low_motion always freezes the cadence.
+5
View File
@@ -158,6 +158,11 @@ pub(crate) fn rail_min_chat_width(idle_empty: bool) -> u16 {
pub(crate) fn status_animation_interval_ms(app: &App) -> u64 {
if app.effective_low_motion_for_status() {
crate::tui::display_refresh::adaptive_animation_interval_ms(true)
} else if matches!(
app.view_stack.top_kind(),
Some(ModalKind::Approval | ModalKind::Elevation | ModalKind::UserInput)
) {
UI_DECISION_PROMPT_STATUS_ANIMATION_MS
} else {
// Keep the braille marker on its fixed 5 Hz table for width stability;
// only atmosphere uses the measured display cadence.