From 1d36104ef0506dabe98965c28e42c87ad954365a Mon Sep 17 00:00:00 2001 From: tsouth89 Date: Fri, 17 Jul 2026 20:40:58 -0400 Subject: [PATCH 1/4] SOU-178 foundation: floatBarInformationMode setting + Calm logic Adds the Exact/Calm information-mode setting (separate from density) with safe migration, and the pure Calm presentation logic. No rendering yet; Exact stays the default so existing bars are unchanged. - settings: float_bar_information_mode ("exact"|"calm"), default exact, normalized (unknown/older -> exact, never silently calm), threaded through raw loader, floatbar SettingsPatch, bridge snapshot/update, and TS types. Settings default/normalization/round-trip tests. - capacityPresentation.calmPresentation(): trustworthy pace state only when the snapshot is live and pace is actually supported (never invents "on pace"); falls back to next reset, then to exact %. Unit tests cover every branch (steady / running-low-with-eta / no-eta-silent / missing / stale / errored / reset-vs-exact fallback). --- .../src-tauri/src/commands/bridge.rs | 2 + .../src-tauri/src/commands/settings.rs | 2 + .../src-tauri/src/floatbar/mod.rs | 6 ++ .../src/lib/capacityPresentation.test.ts | 73 ++++++++++++++++++- .../src/lib/capacityPresentation.ts | 55 ++++++++++++++ apps/desktop-tauri/src/types/bridge.ts | 4 + rust/src/settings.rs | 21 ++++++ rust/src/settings/raw.rs | 6 ++ rust/src/settings/tests.rs | 10 +++ 9 files changed, 178 insertions(+), 1 deletion(-) diff --git a/apps/desktop-tauri/src-tauri/src/commands/bridge.rs b/apps/desktop-tauri/src-tauri/src/commands/bridge.rs index 4fd637b4..dbc36e73 100644 --- a/apps/desktop-tauri/src-tauri/src/commands/bridge.rs +++ b/apps/desktop-tauri/src-tauri/src/commands/bridge.rs @@ -561,6 +561,7 @@ pub struct SettingsSnapshot { float_bar_style: String, taskbar_widget_open_on_hover: bool, float_bar_density: String, + float_bar_information_mode: String, float_bar_contrast: String, float_bar_click_through: bool, float_bar_provider_ids: Vec, @@ -661,6 +662,7 @@ impl From for SettingsSnapshot { float_bar_style: settings.float_bar_style, taskbar_widget_open_on_hover: settings.taskbar_widget_open_on_hover, float_bar_density: settings.float_bar_density, + float_bar_information_mode: settings.float_bar_information_mode, float_bar_contrast, float_bar_click_through: settings.float_bar_click_through, float_bar_provider_ids: settings.float_bar_provider_ids, diff --git a/apps/desktop-tauri/src-tauri/src/commands/settings.rs b/apps/desktop-tauri/src-tauri/src/commands/settings.rs index eea204fe..0dc68718 100644 --- a/apps/desktop-tauri/src-tauri/src/commands/settings.rs +++ b/apps/desktop-tauri/src-tauri/src/commands/settings.rs @@ -58,6 +58,7 @@ pub struct SettingsUpdate { pub float_bar_style: Option, pub taskbar_widget_open_on_hover: Option, pub float_bar_density: Option, + pub float_bar_information_mode: Option, pub float_bar_contrast: Option, pub float_bar_click_through: Option, pub float_bar_provider_ids: Option>, @@ -283,6 +284,7 @@ impl SettingsUpdate { style: self.float_bar_style.clone(), open_on_hover: self.taskbar_widget_open_on_hover, density: self.float_bar_density.clone(), + information_mode: self.float_bar_information_mode.clone(), contrast: self.float_bar_contrast.clone(), click_through: self.float_bar_click_through, provider_ids: self.float_bar_provider_ids.clone(), diff --git a/apps/desktop-tauri/src-tauri/src/floatbar/mod.rs b/apps/desktop-tauri/src-tauri/src/floatbar/mod.rs index c1ba6ab1..88f4987c 100644 --- a/apps/desktop-tauri/src-tauri/src/floatbar/mod.rs +++ b/apps/desktop-tauri/src-tauri/src/floatbar/mod.rs @@ -107,6 +107,7 @@ pub struct SettingsPatch { pub style: Option, pub open_on_hover: Option, pub density: Option, + pub information_mode: Option, pub contrast: Option, pub click_through: Option, pub provider_ids: Option>, @@ -126,6 +127,7 @@ impl SettingsPatch { && self.style.is_none() && self.open_on_hover.is_none() && self.density.is_none() + && self.information_mode.is_none() && self.contrast.is_none() && self.click_through.is_none() && self.provider_ids.is_none() @@ -164,6 +166,10 @@ impl SettingsPatch { if let Some(v) = &self.density { settings.float_bar_density = codexbar::settings::normalize_float_bar_density(v); } + if let Some(v) = &self.information_mode { + settings.float_bar_information_mode = + codexbar::settings::normalize_float_bar_information_mode(v); + } if let Some(v) = &self.contrast { settings.float_bar_contrast = Some(codexbar::settings::normalize_float_bar_contrast(v)); } diff --git a/apps/desktop-tauri/src/lib/capacityPresentation.test.ts b/apps/desktop-tauri/src/lib/capacityPresentation.test.ts index 0c65b48c..509947d0 100644 --- a/apps/desktop-tauri/src/lib/capacityPresentation.test.ts +++ b/apps/desktop-tauri/src/lib/capacityPresentation.test.ts @@ -8,8 +8,13 @@ import { providerGlanceStatus, resetCreditsAvailable, codexResetCredits, + calmPresentation, } from "./capacityPresentation"; -import type { ProviderUsageSnapshot, RateWindowSnapshot } from "../types/bridge"; +import type { + PaceSnapshot, + ProviderUsageSnapshot, + RateWindowSnapshot, +} from "../types/bridge"; function window(usedPercent: number): RateWindowSnapshot { return { @@ -231,4 +236,70 @@ describe("capacityPresentation", () => { codexResetCredits(provider({ providerId: "cursor", resetCreditsAvailable: 2 })), ).toBeNull(); }); + + describe("calmPresentation", () => { + const pace = (over: Partial = {}): PaceSnapshot => ({ + windowLabel: "Weekly", + stage: "on_track", + deltaPercent: 0, + willLastToReset: true, + etaSeconds: null, + expectedUsedPercent: 40, + actualUsedPercent: 40, + ...over, + }); + const withReset = (usedPercent: number): RateWindowSnapshot => ({ + ...window(usedPercent), + resetsAt: new Date(Date.now() + 3_600_000).toISOString(), + }); + + it("shows a steady pace state when fresh pace lasts to reset", () => { + const snap = provider({ pace: pace({ willLastToReset: true }) }); + const result = calmPresentation(snap, constrainingWindow(snap)); + expect(result.pace).toEqual({ label: "On pace", tone: "steady" }); + expect(result.showExactFallback).toBe(false); + }); + + it("warns only when there is a real finite ETA to run out", () => { + const risky = provider({ + pace: pace({ willLastToReset: false, etaSeconds: 3600 }), + }); + expect(calmPresentation(risky, constrainingWindow(risky)).pace).toEqual({ + label: "Running low", + tone: "watch", + }); + + // No usable ETA: stay silent rather than invent a state. + const vague = provider({ + pace: pace({ willLastToReset: false, etaSeconds: null }), + }); + expect(calmPresentation(vague, constrainingWindow(vague)).pace).toBeNull(); + }); + + it("never invents a pace state when pace is missing, stale, or errored", () => { + const noPace = provider({ pace: null }); + expect(calmPresentation(noPace, constrainingWindow(noPace)).pace).toBeNull(); + + const stale = provider({ + pace: pace({ willLastToReset: true }), + updatedAt: new Date(Date.now() - 60 * 60 * 1000).toISOString(), + }); + expect(calmPresentation(stale, constrainingWindow(stale)).pace).toBeNull(); + + const errored = provider({ pace: pace({ willLastToReset: true }), error: "boom" }); + expect(calmPresentation(errored, constrainingWindow(errored)).pace).toBeNull(); + }); + + it("falls back to exact percentage only when neither pace nor a reset exists", () => { + const noReset = provider({ pace: null, primary: window(30) }); + const bare = calmPresentation(noReset, constrainingWindow(noReset)); + expect(bare.hasReset).toBe(false); + expect(bare.showExactFallback).toBe(true); + + const withResetSnap = provider({ pace: null, primary: withReset(30) }); + const reset = calmPresentation(withResetSnap, constrainingWindow(withResetSnap)); + expect(reset.hasReset).toBe(true); + expect(reset.showExactFallback).toBe(false); + }); + }); }); diff --git a/apps/desktop-tauri/src/lib/capacityPresentation.ts b/apps/desktop-tauri/src/lib/capacityPresentation.ts index 8f5448c9..ad78d7a2 100644 --- a/apps/desktop-tauri/src/lib/capacityPresentation.ts +++ b/apps/desktop-tauri/src/lib/capacityPresentation.ts @@ -1,4 +1,5 @@ import type { + PaceSnapshot, ProviderUsageSnapshot, RateWindowSnapshot, } from "../types/bridge"; @@ -214,6 +215,60 @@ export function capacityFreshness( return "live"; } +export type CalmPaceTone = "steady" | "watch"; +export type CalmPaceState = { label: string; tone: CalmPaceTone }; +export type CalmPresentation = { + /** Trustworthy pace state, or null when pace isn't fresh + supported. */ + pace: CalmPaceState | null; + /** The window whose reset + label headline the calm pill. */ + window: ConstrainingWindow; + /** Whether that window exposes a reset (a time or a description). */ + hasReset: boolean; + /** No pace and no reset — callers fall back to the exact percentage. */ + showExactFallback: boolean; +}; + +/** + * Trustworthy pace state for Calm mode, or null when there isn't enough signal. + * Never invents an "on pace" state (SOU-178): it only speaks when the provider + * actually reports usable pace data. + */ +function calmPaceState(pace: PaceSnapshot | null): CalmPaceState | null { + if (!pace) return null; + // Data-backed and reassuring: the current pace lasts to the reset. + if (pace.willLastToReset) return { label: "On pace", tone: "steady" }; + // Only warn when there is a real, finite estimate of running out; otherwise + // stay silent rather than fabricate a state. + if ( + typeof pace.etaSeconds === "number" && + Number.isFinite(pace.etaSeconds) && + pace.etaSeconds > 0 + ) { + return { label: "Running low", tone: "watch" }; + } + return null; +} + +/** + * What Calm mode surfaces for a provider: a trustworthy pace state plus the + * next reset of the displayed window, with exact percentages left to expand. + * Pace is claimed only when the snapshot is live and the provider reports + * usable pace data. When pace is unavailable we fall back to the next reset; + * when that is missing too, callers show the exact percentage instead. + */ +export function calmPresentation( + provider: ProviderUsageSnapshot, + window: ConstrainingWindow, + nowMs: number = Date.now(), +): CalmPresentation { + const fresh = capacityFreshness(provider, nowMs) === "live"; + const pace = fresh && !provider.error ? calmPaceState(provider.pace) : null; + const hasReset = Boolean( + window.window.resetsAt || window.window.resetDescription, + ); + return { pace, window, hasReset, showExactFallback: !pace && !hasReset }; +} + /** Boost promos that affect glance surfaces (strip / overview hero). */ export function activePromoBoosts( provider: ProviderUsageSnapshot, diff --git a/apps/desktop-tauri/src/types/bridge.ts b/apps/desktop-tauri/src/types/bridge.ts index 209f004b..a9e3d2a6 100644 --- a/apps/desktop-tauri/src/types/bridge.ts +++ b/apps/desktop-tauri/src/types/bridge.ts @@ -47,6 +47,7 @@ export type MenuBarDisplayMode = "minimal" | "compact" | "detailed"; export type FloatBarOrientation = "horizontal" | "vertical"; export type FloatBarStyle = "floating" | "taskbar"; export type FloatBarDensity = "compact" | "standard" | "detailed"; +export type FloatBarInformationMode = "exact" | "calm"; export type FloatBarContrast = "auto" | "light-text" | "dark-text"; export type ProofProviderId = | "codex" @@ -264,6 +265,8 @@ export interface SettingsSnapshot { /** Open the taskbar glance panel after a short pointer dwell. */ taskbarWidgetOpenOnHover: boolean; floatBarDensity: FloatBarDensity; + /** "exact" (icon + %) or "calm" (pace state + next reset). Separate from density. */ + floatBarInformationMode: FloatBarInformationMode; floatBarContrast: FloatBarContrast; floatBarClickThrough: boolean; /** Empty array = show all enabled providers. */ @@ -328,6 +331,7 @@ export interface SettingsUpdate { floatBarStyle?: FloatBarStyle; taskbarWidgetOpenOnHover?: boolean; floatBarDensity?: FloatBarDensity; + floatBarInformationMode?: FloatBarInformationMode; floatBarContrast?: FloatBarContrast; floatBarClickThrough?: boolean; floatBarProviderIds?: string[]; diff --git a/rust/src/settings.rs b/rust/src/settings.rs index 134a52ec..27d5cf5e 100755 --- a/rust/src/settings.rs +++ b/rust/src/settings.rs @@ -245,6 +245,13 @@ pub struct Settings { #[serde(default = "default_float_bar_density")] pub float_bar_density: String, + /// Floating-bar information mode: "exact" (provider icon + exact percentage + /// and label) or "calm" (a trustworthy pace state plus the next reset, with + /// exact percentages on expand). Separate from density, which is geometry. + /// Exact is the migration default so existing bars are unchanged. + #[serde(default = "default_float_bar_information_mode")] + pub float_bar_information_mode: String, + /// Floating-bar contrast mode. `None` means a pre-density settings file; /// resolve it through the legacy `float_bar_dark_text` preference so /// upgrades preserve their appearance. New installs default to auto. @@ -310,6 +317,10 @@ fn default_float_bar_density() -> String { "standard".to_string() } +fn default_float_bar_information_mode() -> String { + "exact".to_string() +} + /// Clamp the floating-bar opacity to the supported range. /// /// Opacity values below 30% would make the bar effectively invisible, so we @@ -353,6 +364,15 @@ pub fn normalize_float_bar_density(value: &str) -> String { } } +/// Normalize a floating-bar information mode. Unknown or older values fall back +/// to "exact" so an upgrade never silently switches a user into calm mode. +pub fn normalize_float_bar_information_mode(value: &str) -> String { + match value { + "calm" => "calm".to_string(), + _ => "exact".to_string(), + } +} + /// Normalize the resolved contrast mode used by the desktop bridge. pub fn normalize_float_bar_contrast(value: &str) -> String { match value { @@ -497,6 +517,7 @@ impl Default for Settings { float_bar_style: "floating".to_string(), taskbar_widget_open_on_hover: true, float_bar_density: default_float_bar_density(), + float_bar_information_mode: default_float_bar_information_mode(), float_bar_contrast: Some("auto".to_string()), float_bar_click_through: false, float_bar_provider_ids: Vec::new(), diff --git a/rust/src/settings/raw.rs b/rust/src/settings/raw.rs index 5228f581..d28c0d34 100644 --- a/rust/src/settings/raw.rs +++ b/rust/src/settings/raw.rs @@ -143,6 +143,8 @@ pub(super) struct RawSettings { taskbar_widget_open_on_hover: bool, #[serde(default = "default_float_bar_density")] float_bar_density: String, + #[serde(default = "default_float_bar_information_mode")] + float_bar_information_mode: String, #[serde(default)] float_bar_contrast: Option, #[serde(default)] @@ -240,6 +242,7 @@ impl Default for RawSettings { float_bar_style: Some(s.float_bar_style), taskbar_widget_open_on_hover: s.taskbar_widget_open_on_hover, float_bar_density: s.float_bar_density, + float_bar_information_mode: s.float_bar_information_mode, float_bar_contrast: s.float_bar_contrast, float_bar_click_through: s.float_bar_click_through, float_bar_provider_ids: s.float_bar_provider_ids, @@ -541,6 +544,9 @@ impl From for Settings { float_bar_style: "floating".to_string(), taskbar_widget_open_on_hover: raw.taskbar_widget_open_on_hover, float_bar_density: normalize_float_bar_density(&raw.float_bar_density), + float_bar_information_mode: normalize_float_bar_information_mode( + &raw.float_bar_information_mode, + ), float_bar_contrast: raw .float_bar_contrast .map(|value| normalize_float_bar_contrast(&value)), diff --git a/rust/src/settings/tests.rs b/rust/src/settings/tests.rs index 50cfd930..183ff8ca 100644 --- a/rust/src/settings/tests.rs +++ b/rust/src/settings/tests.rs @@ -134,6 +134,7 @@ fn float_bar_defaults_are_safe() { assert_eq!(settings.float_bar_style, "floating"); assert!(settings.taskbar_widget_open_on_hover); assert_eq!(settings.float_bar_density, "standard"); + assert_eq!(settings.float_bar_information_mode, "exact"); assert_eq!(resolved_float_bar_contrast(&settings), "auto"); assert!(!settings.float_bar_click_through); assert!(settings.float_bar_provider_ids.is_empty()); @@ -247,6 +248,13 @@ fn float_bar_density_and_contrast_normalization_reject_unknown_values() { assert_eq!(normalize_float_bar_density("detailed"), "detailed"); assert_eq!(normalize_float_bar_density("dense"), "standard"); + assert_eq!(normalize_float_bar_information_mode("exact"), "exact"); + assert_eq!(normalize_float_bar_information_mode("calm"), "calm"); + // Unknown/older values migrate to exact, never silently into calm. + assert_eq!(normalize_float_bar_information_mode(""), "exact"); + assert_eq!(normalize_float_bar_information_mode("Calm"), "exact"); + assert_eq!(normalize_float_bar_information_mode("minimal"), "exact"); + assert_eq!(normalize_float_bar_contrast("auto"), "auto"); assert_eq!(normalize_float_bar_contrast("light-text"), "light-text"); assert_eq!(normalize_float_bar_contrast("dark-text"), "dark-text"); @@ -281,6 +289,7 @@ fn float_bar_settings_round_trip_through_raw() { float_bar_style: "floating".to_string(), taskbar_widget_open_on_hover: false, float_bar_density: "compact".to_string(), + float_bar_information_mode: "calm".to_string(), float_bar_contrast: Some("dark-text".to_string()), float_bar_click_through: true, float_bar_provider_ids: vec!["claude".into(), "codex".into()], @@ -301,6 +310,7 @@ fn float_bar_settings_round_trip_through_raw() { assert_eq!(back.float_bar_style, "floating"); assert!(!back.taskbar_widget_open_on_hover); assert_eq!(back.float_bar_density, "compact"); + assert_eq!(back.float_bar_information_mode, "calm"); assert_eq!(resolved_float_bar_contrast(&back), "dark-text"); assert!(back.float_bar_click_through); assert_eq!(back.float_bar_provider_ids, vec!["claude", "codex"]); From 804748e804a05b6ccd704c41035004acfbc96fdf Mon Sep 17 00:00:00 2001 From: tsouth89 Date: Fri, 17 Jul 2026 20:51:11 -0400 Subject: [PATCH 2/4] SOU-178: render Calm mode in the floating bar + settings control Wires the Exact/Calm information mode into the float bar and settings. - FloatBar: Calm pills lead with the trustworthy pace state ("On pace" / "Running low") and the next reset of the displayed window; the exact % is revealed on click/Enter (keyboard-accessible, not hover-only). The Calm pill is a focusable button that opts out of the native drag region (drag the bar by its handle) and stops propagation so a click never starts a move. Usage-pressure tone (border) still shows, so a critical provider isn't hidden; no new motion, so reduced-motion is unaffected. - Settings: an "Information" select (Exact / Calm) in the float-bar section, separate from Density (geometry). - CSS: calm pace states (steady/watch) with light-bg overrides and a focus ring. - Tests: FloatBar renders the pace state, hides the exact % until expand, and expands on click; every fixture carries the new setting. --- apps/desktop-tauri/src/App.test.tsx | 1 + apps/desktop-tauri/src/floatbar/FloatBar.css | 45 ++++++++ .../src/floatbar/FloatBar.test.tsx | 43 ++++++++ apps/desktop-tauri/src/floatbar/FloatBar.tsx | 104 ++++++++++++++++-- .../src/floatbar/SettingsSection.test.tsx | 1 + .../src/floatbar/SettingsSection.tsx | 17 +++ .../src/surfaces/PopOutPanel.test.tsx | 1 + .../src/surfaces/TrayPanel.test.tsx | 1 + .../surfaces/settings/tabs/AboutTab.test.tsx | 1 + .../settings/tabs/GeneralTab.test.tsx | 1 + apps/desktop-tauri/src/types/bridge.test.ts | 1 + 11 files changed, 206 insertions(+), 10 deletions(-) diff --git a/apps/desktop-tauri/src/App.test.tsx b/apps/desktop-tauri/src/App.test.tsx index 0d433617..d191d671 100644 --- a/apps/desktop-tauri/src/App.test.tsx +++ b/apps/desktop-tauri/src/App.test.tsx @@ -112,6 +112,7 @@ function settings(overrides: Partial = {}): SettingsSnapshot { floatBarStyle: "floating", taskbarWidgetOpenOnHover: true, floatBarDensity: "standard", + floatBarInformationMode: "exact", floatBarContrast: "auto", floatBarClickThrough: false, floatBarProviderIds: [], diff --git a/apps/desktop-tauri/src/floatbar/FloatBar.css b/apps/desktop-tauri/src/floatbar/FloatBar.css index 08371526..2b015d0b 100644 --- a/apps/desktop-tauri/src/floatbar/FloatBar.css +++ b/apps/desktop-tauri/src/floatbar/FloatBar.css @@ -287,6 +287,51 @@ body.floatbar-window #root { .floatbar__reset--emphasis .floatbar__reset-time { font-weight: 650; } +/* ── Calm information mode ───────────────────────────────────────────── + The pill leads with a trustworthy pace state + next reset and becomes an + expandable button. No new motion, so reduced-motion is unaffected. */ +.floatbar__pill--calm { + cursor: pointer; +} +.floatbar__pill--calm:focus-visible { + outline: 2px solid color-mix(in srgb, var(--ceiling-accent, #26b5ce) 70%, white); + outline-offset: 1px; +} +.floatbar__pace { + font-size: 0.86em; + font-weight: 650; + line-height: 1; + white-space: nowrap; +} +.floatbar__pace--steady { + color: color-mix(in srgb, #34d399 62%, white); +} +.floatbar__pace--watch { + color: color-mix(in srgb, #f59e0b 68%, white); +} +.floatbar__calm-reset { + display: inline-flex; + align-items: center; + gap: calc(3px * var(--floatbar-scale, 1)); + opacity: 0.66; + line-height: 1; +} +.floatbar__calm-sep { + opacity: 0.5; +} +.floatbar__pct--calm { + padding-left: calc(6px * var(--floatbar-scale, 1)); + margin-left: calc(2px * var(--floatbar-scale, 1)); + border-left: 1px solid color-mix(in srgb, currentColor 18%, transparent); + font-size: 0.98em; +} +.floatbar--light-bg .floatbar__pace--steady { + color: color-mix(in srgb, #059669 78%, black); +} +.floatbar--light-bg .floatbar__pace--watch { + color: color-mix(in srgb, #b45309 82%, black); +} + .floatbar--vertical .floatbar__text { flex-direction: column; align-items: center; diff --git a/apps/desktop-tauri/src/floatbar/FloatBar.test.tsx b/apps/desktop-tauri/src/floatbar/FloatBar.test.tsx index 97f93921..18f745b6 100644 --- a/apps/desktop-tauri/src/floatbar/FloatBar.test.tsx +++ b/apps/desktop-tauri/src/floatbar/FloatBar.test.tsx @@ -137,6 +137,7 @@ function settings(overrides: Partial = {}): SettingsSnapshot { floatBarStyle: "floating", taskbarWidgetOpenOnHover: true, floatBarDensity: "standard", + floatBarInformationMode: "exact", floatBarContrast: "light-text", floatBarClickThrough: false, floatBarProviderIds: [], @@ -221,6 +222,48 @@ describe("FloatBar", () => { expect(titles[1]).toMatch(/Claude: 20% used/); }); + it("calm mode leads with a pace state and expands to the exact % on click", async () => { + const calmProvider: ProviderUsageSnapshot = { + ...snapshot("codex", "Codex", 60, { + resetsAt: new Date(Date.now() + 2 * 3600_000).toISOString(), + }), + updatedAt: new Date().toISOString(), + pace: { + windowLabel: "Weekly", + stage: "on_track", + deltaPercent: 0, + willLastToReset: true, + etaSeconds: null, + expectedUsedPercent: 60, + actualUsedPercent: 60, + }, + }; + tauriMocks.getCachedProviders.mockResolvedValue([calmProvider]); + tauriMocks.getSettingsSnapshot.mockResolvedValue( + settings({ floatBarInformationMode: "calm" }), + ); + + const { container } = renderFloatBar( + bootstrap({ floatBarInformationMode: "calm" }), + ); + await waitFor(() => { + expect(container.querySelector(".floatbar__pill--calm")).toBeInTheDocument(); + }); + + // Calm leads with the trustworthy pace state; the exact % is hidden until + // the user expands, and the pill is a keyboard-accessible button. + expect(container.querySelector(".floatbar__pace")).toHaveTextContent("On pace"); + expect(container.querySelector(".floatbar__pct--calm")).toBeNull(); + const pill = container.querySelector(".floatbar__pill--calm")!; + expect(pill.getAttribute("role")).toBe("button"); + expect(pill.getAttribute("tabindex")).toBe("0"); + + act(() => { + pill.click(); + }); + expect(container.querySelector(".floatbar__pct--calm")).toHaveTextContent("60%"); + }); + it("does not render hypothetical local costs from the legacy setting", async () => { tauriMocks.getCachedProviders.mockResolvedValue([ snapshot("codex", "Codex", 75), diff --git a/apps/desktop-tauri/src/floatbar/FloatBar.tsx b/apps/desktop-tauri/src/floatbar/FloatBar.tsx index bcd04bbf..409a6f3c 100644 --- a/apps/desktop-tauri/src/floatbar/FloatBar.tsx +++ b/apps/desktop-tauri/src/floatbar/FloatBar.tsx @@ -5,6 +5,7 @@ import { useRef, useState, type CSSProperties, + type KeyboardEvent as ReactKeyboardEvent, type MouseEvent, } from "react"; import { listen } from "@tauri-apps/api/event"; @@ -36,9 +37,11 @@ import { capacityFreshness, constrainingWindow, activePromoBoosts, + calmPresentation, type CapacityFreshness, type ConstrainingWindow, } from "../lib/capacityPresentation"; +import type { FloatBarInformationMode } from "../types/bridge"; import "./FloatBar.css"; function ResetIcon({ size }: { size: number }) { @@ -117,6 +120,9 @@ function ProviderPill({ usedSuffix, remainingSuffix, capacityEventKind, + informationMode, + expanded, + onToggleExpand, }: { provider: ProviderUsageSnapshot; highRemaining: number; @@ -128,6 +134,9 @@ function ProviderPill({ usedSuffix: string; remainingSuffix: string; capacityEventKind?: CapacityEventPayload["kind"]; + informationMode: FloatBarInformationMode; + expanded: boolean; + onToggleExpand: () => void; }) { // Keep the number, label, and tone tied to the same displayed window. const hero = floatBarWindow(provider); @@ -171,17 +180,75 @@ function ProviderPill({ .filter(Boolean) .join("\n"); + const pillClass = [ + "floatbar__pill", + `floatbar__pill--${tone}`, + freshness !== "live" ? `floatbar__pill--${freshness}` : null, + capacityEventKind ? "floatbar__pill--capacity-event" : null, + capacityEventKind ? `floatbar__pill--${capacityEventKind}` : null, + ] + .filter(Boolean) + .join(" "); + + // Calm mode: lead with a trustworthy pace state and the next reset; exact % + // stays one keyboard/click away. The pill becomes an expandable button, so it + // opts out of the native drag region (drag the bar by its handle instead). + if (informationMode === "calm") { + const calm = calmPresentation(provider, hero); + const showExact = calm.showExactFallback || expanded; + const onKeyToggle = (event: ReactKeyboardEvent) => { + if (event.key === "Enter" || event.key === " ") { + event.preventDefault(); + onToggleExpand(); + } + }; + return ( +
event.stopPropagation()} + style={{ "--brand": brand } as CSSProperties} + > + + + + + {calm.pace ? ( + + {calm.pace.label} + + ) : null} + {calm.hasReset ? ( + + {hero.label} + {inlineReset ? ( + <> + + · + + + {inlineReset} + + ) : null} + + ) : null} + {showExact ? ( + {label} + ) : null} + +
+ ); + } + return (
>(new Set()); + const toggleExpanded = useCallback((key: string) => { + setExpandedKeys((prev) => { + const next = new Set(prev); + if (next.has(key)) next.delete(key); + else next.add(key); + return next; + }); + }, []); const startDrag = useCallback((event: MouseEvent) => { if (event.button !== 0 || lockedRef.current || menuOpenRef.current) return; @@ -405,6 +482,8 @@ export default function FloatBar({ state }: { state: BootstrapState }) { ? settings.floatBarDensity : "standard"; const contrast = settings.floatBarContrast ?? "light-text"; + const informationMode: FloatBarInformationMode = + settings.floatBarInformationMode === "calm" ? "calm" : "exact"; const darkText = contrast === "dark-text" || (contrast === "auto" && systemPrefersLight); const filterIds = settings.floatBarProviderIds; @@ -459,6 +538,8 @@ export default function FloatBar({ state }: { state: BootstrapState }) { scale, showResetInline, settings.resetTimeRelative, + informationMode, + expandedKeys, menuOpen, ]); @@ -503,7 +584,7 @@ export default function FloatBar({ state }: { state: BootstrapState }) { return (
toggleExpanded(providerKey(p))} /> ))}
diff --git a/apps/desktop-tauri/src/floatbar/SettingsSection.test.tsx b/apps/desktop-tauri/src/floatbar/SettingsSection.test.tsx index 2aa23927..a67aa3c9 100644 --- a/apps/desktop-tauri/src/floatbar/SettingsSection.test.tsx +++ b/apps/desktop-tauri/src/floatbar/SettingsSection.test.tsx @@ -17,6 +17,7 @@ const settings = { floatBarStyle: "floating", taskbarWidgetOpenOnHover: true, floatBarDensity: "standard", + floatBarInformationMode: "exact", floatBarContrast: "auto", floatBarShowCost: false, floatBarShowResetInline: false, diff --git a/apps/desktop-tauri/src/floatbar/SettingsSection.tsx b/apps/desktop-tauri/src/floatbar/SettingsSection.tsx index fd5695aa..20a298f1 100644 --- a/apps/desktop-tauri/src/floatbar/SettingsSection.tsx +++ b/apps/desktop-tauri/src/floatbar/SettingsSection.tsx @@ -4,6 +4,7 @@ import type { FloatBarOrientation, FloatBarContrast, FloatBarDensity, + FloatBarInformationMode, SettingsSnapshot, SettingsUpdate, } from "../types/bridge"; @@ -150,6 +151,22 @@ export default function FloatBarSettingsSection({ settings, saving, set }: Props onChange={(v) => set({ floatBarDensity: v as FloatBarDensity })} /> + +