fix(tui): retain update notice in the metrics line

Show a quiet available-version notice on the normal and empty-session metrics line, without replacing permission or remote-control status. A staged unsigned update no longer claims it is ready. Existing hidden/mini/pet layouts remain unchanged and the notice is display-only; /update remains the action.

Refs #7136. Verified on the frozen Linux source export: the existing empty-session render and status-item coverage checks passed (2 passed, 0 failed; 13,924 filtered in each). rustfmt and diff checks passed. First VM test attempt was killed at its memory limit; the one-job retry with swap passed. No installed binary, real update check, clickable control, or hosted CI was verified. Read-only design review by Claude.
This commit is contained in:
CodeWhale Bot committed 2026-10-28 20:37:35 -07:00
1 parent 8f131fbdd7
commit 71bc853fe6
6 files changed
+42 -22

No files matched your search

+2 -2
View File
@@ -1669,9 +1669,9 @@ pub struct App {
pub status_message: Option<String>,
/// Recent status toasts (ephemeral, newest at back).
pub status_toasts: VecDeque<StatusToast>,
/// Header chip label (e.g. `↑ v0.9.5`) set once by the fire-and-forget
/// Metrics-line label (e.g. `↑ v0.9.5`) set once by the fire-and-forget
/// startup version check when a newer stable release exists. Drives the
/// small persistent update chip in the header so the affordance survives
/// persistent update notice so the affordance survives
/// the transient toast without nagging (FINISH-0.9.4 #14).
pub update_available: Option<String>,
/// Sticky status toast used for important warnings/errors.
+4 -4
View File
@@ -27,13 +27,13 @@
//! and at every fullness, not only from 50% up (#5950).
//!
//! Shed order as width drops: cache, output count and billing tier, the help
//! hint, then rate and TTFT, then cost and balance
//! hint, then rate and TTFT, then cost and balance, followed by goal and update
//! ([`InfoSegmentId::shed_priority`]). The model and `ctx NN%` never shed; below that floor the row clips at its
//! right edge.
//!
//! Which segments exist at all is the user's call: `/statusline` and
//! `tui.status_items` compose the row, and [`crate::tui::ui::frame::info_segments`]
//! builds only the ones that are on. Shedding decides what survives the
//! `/statusline` and `tui.status_items` select the session metrics;
//! [`crate::tui::ui::frame::info_segments`] also includes an active goal or
//! an available-update notice. Shedding decides what survives the
//! width that is left. `tui.metrics_line` sizes the row (#5950): `hidden`
//! gives the line back to the transcript, and `compact` starts the shed
//! pass with secondary counts and the help hint already gone; TTFT and rate
+2 -2
View File
@@ -1445,11 +1445,11 @@ impl UpdateNotice {
)
}
/// Compact header chip label shown once the check has landed. Quiet by
/// Compact metrics-line label shown once the check has landed. Quiet by
/// design: no action verb, no repetition — the toast and transcript
/// notice carry the update instructions (#14).
fn chip_label(&self) -> String {
if self.staged.is_some() {
if self.staged == Some(true) {
format!("↑ v{latest} ready", latest = self.latest)
} else {
format!("↑ v{latest}", latest = self.latest)
+1 -1
View File
@@ -2239,7 +2239,7 @@ pub(crate) async fn run_event_loop(
// Transient toast for immediate visibility, plus a durable
// in-transcript notice so the prompt survives the toast TTL and
// stays actionable during a busy session (#3961). The persistent
// header chip keeps a quiet affordance after both (#14).
// metrics-line notice keeps a quiet affordance after both (#14).
// Which command to advertise depends on who owns this binary on
// disk, so resolve that here rather than hardcoding our own
// updater into the wording.
+31 -12
View File
@@ -72,10 +72,9 @@ fn output_tokens(app: &App) -> Option<u64> {
/// the git bottom view own them. Fleet, whale and automation counts left too —
/// the posture bar's live counts own activity.
///
/// Composition is the user's (#5950): every segment here is gated on the
/// matching [`StatusItem`] in `app.status_items`, which is what `/statusline`
/// edits and `tui.status_items` persists. Between 0.9.12 and this change the
/// row ignored that list entirely and the picker's toggles did nothing.
/// Session metrics follow the matching [`StatusItem`] in `app.status_items`,
/// which `/statusline` edits and `tui.status_items` persists. The active goal
/// and available-update notice follow their own live state.
pub(crate) fn info_segments(app: &App, width: u16) -> Vec<InfoSegment> {
use codewhale_localization::MessageId;
use codewhale_palette::ChromeInk;
@@ -325,6 +324,15 @@ pub(crate) fn info_segments(app: &App, width: u16) -> Vec<InfoSegment> {
}
}
if let Some(label) = app.update_available.as_deref() {
segments.push(InfoSegment::new(
InfoSegmentId::Update,
"",
label,
ChromeInk::Info,
));
}
segments
}
@@ -418,10 +426,9 @@ fn split_route_hitbox(
}
/// Render the info line into its one row and record its segment
/// hitboxes (spec §5b `Constraint::Length(1)`). The ONE header on every
/// screen: the session shell and the launch screen both call this, so the
/// brand lockup, contextual segments, and the pinned meter + clock never
/// change identity between pre- and post-session states. Segment rects are
/// hitboxes (spec §5b `Constraint::Length(1)`). The session shell and launch
/// screen both use this row, keeping the visible identity and notice facts
/// consistent between pre- and post-session states. Segment rects are
/// recorded for hover (this frame's highlight resolves against the previous
/// frame's rects, the standard one-frame-lag registry pattern) and for typed
/// click routing.
@@ -446,7 +453,10 @@ fn render_info_row(
segments.retain(|segment| {
matches!(
segment.id,
InfoSegmentId::Model | InfoSegmentId::Workspace | InfoSegmentId::GitBranch
InfoSegmentId::Model
| InfoSegmentId::Workspace
| InfoSegmentId::GitBranch
| InfoSegmentId::Update
)
});
}
@@ -3019,9 +3029,8 @@ mod tests {
}
}
/// Every remaining status item owns a segment, and an empty list leaves
/// the row with nothing but the help hint — no toggle in `/statusline`
/// paints something no toggle can remove.
/// Every selected status item owns its segment; without live notices an
/// empty list leaves the metrics line empty.
#[test]
fn every_metrics_segment_answers_to_a_status_item() {
let mut app = app_with_context_percent(60);
@@ -3070,6 +3079,7 @@ mod tests {
app.workspace = std::path::PathBuf::from("/fixture/checkout");
app.workspace_context = Some("feature-6112 | clean".to_string());
app.status_items = vec![StatusItem::Workspace, StatusItem::GitBranch];
app.update_available = Some("↑ v9.9.9".to_string());
app.metrics_line = codewhale_runtime::config::ChromeRowPreset::Compact;
let backend = ratatui::backend::TestBackend::new(100, 1);
let mut terminal = ratatui::Terminal::new(backend).unwrap();
@@ -3088,6 +3098,15 @@ mod tests {
.collect();
assert!(rendered.contains("checkout"), "{rendered}");
assert!(rendered.contains("feature-6112"), "{rendered}");
assert!(rendered.contains("v9.9.9"), "{rendered}");
let mut notice = super::super::UpdateNotice {
current: "0.10.1".to_string(),
latest: "0.10.2".to_string(),
staged: Some(false),
};
assert_eq!(notice.chip_label(), "↑ v0.10.2");
notice.staged = Some(true);
assert_eq!(notice.chip_label(), "↑ v0.10.2 ready");
}
/// #6112: the opt-in workspace and branch chips read cached state only —
+2 -1
View File
@@ -469,6 +469,7 @@ pub enum MetricKind {
Goal,
Workspace,
GitBranch,
Update,
}
impl MetricKind {
@@ -482,7 +483,7 @@ impl MetricKind {
Self::Rate | Self::Ttft | Self::Cost => 5,
Self::OutputTokens | Self::BillingTier => 7,
Self::Balance | Self::Workspace | Self::GitBranch => 4,
Self::Goal => 3,
Self::Goal | Self::Update => 3,
Self::Model | Self::Context => 0,
}
}