From ab9cb0b39cab2f6f6dc6ee4eaa0ef288a518a5fd Mon Sep 17 00:00:00 2001 From: Tyler Date: Sun, 16 Aug 2026 05:37:13 +0000 Subject: [PATCH 1/2] Open Display from the floating-bar Settings action (SBS-872) The three settings-tab lists had drifted, so FloatBar sent menuBar and the Settings shell fell back to General. --- CHANGELOG.md | 1 + .../src-tauri/src/commands/tests.rs | 6 +- .../src-tauri/src/proof_harness.rs | 35 ++++++++++-- apps/desktop-tauri/src-tauri/src/state.rs | 8 +-- .../src-tauri/src/surface_target.rs | 41 ++++++++++--- .../src/floatbar/FloatBar.test.tsx | 25 +++++++- apps/desktop-tauri/src/floatbar/FloatBar.tsx | 2 +- apps/desktop-tauri/src/lib/tauri.ts | 3 +- .../src/surfaces/Settings.test.ts | 20 ++++++- apps/desktop-tauri/src/surfaces/Settings.tsx | 6 -- apps/desktop-tauri/src/test/raw.d.ts | 4 ++ .../src/test/settingsTabs.test.ts | 57 +++++++++++++++++++ apps/desktop-tauri/src/types/bridge.ts | 1 - 13 files changed, 180 insertions(+), 29 deletions(-) create mode 100644 apps/desktop-tauri/src/test/raw.d.ts create mode 100644 apps/desktop-tauri/src/test/settingsTabs.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index b193f88e..e37141b4 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - **The floating bar can follow the app you are in.** Pinned providers stay the default. Active shows the provider for the focused supported app or terminal agent. Active plus critical also keeps providers at or above the warning threshold. An unrelated window keeps the last active provider. Detection is local, cached, and does not call provider APIs. You can turn watching off. ### Fixed +- **Opening Settings from the floating bar no longer lands on General.** The bar asked for a `menuBar` tab the Settings window does not have, so the window fell back to General instead of Display. It now opens Display, the same tab the dashboard already uses for that action. The lists that name a Settings tab on each side of the bridge are compared in CI so a renamed tab cannot silently send you to General again. - **Cursor no longer treats missing usage as zero.** An empty `individualUsage` object used to paint a 0% monthly bar and hide a real team pool sitting next to it. Monthly is now marked unavailable when Cursor reports no reading, and an empty individual object falls through to team usage. On-demand stays billed spend; plan and included dollars are labeled **Included** so they are not read as an invoice. A missing Composer tracking database is shown as unavailable, not as no activity. - **Leftover English on glance surfaces now goes through locale keys.** Floatbar settings, freshness chips, account-status labels, Charts tab names, and About update copy used hardcoded English. They now use `en-US.ftl` (and zh-CN) so chips no longer show raw `stale` / `error` tokens. - **Refreshing model prices no longer blanks out the prices you already had.** The models.dev price cache was emptied before the new copy was written, so a second Ceiling process reading during that moment saw nothing, and a crash mid-write threw the cache away for good. Either way token costs quietly disappeared until a network refresh worked. The new copy is now written beside the old one and swapped in, so there is never a moment with no prices on disk. diff --git a/apps/desktop-tauri/src-tauri/src/commands/tests.rs b/apps/desktop-tauri/src-tauri/src/commands/tests.rs index 4764234e..290ba7d0 100644 --- a/apps/desktop-tauri/src-tauri/src/commands/tests.rs +++ b/apps/desktop-tauri/src-tauri/src/commands/tests.rs @@ -18,7 +18,7 @@ fn validate_surface_target_accepts_matching_target() { let target = validate_surface_target( SurfaceMode::Settings, SurfaceTarget::Settings { - tab: "apiKeys".into(), + tab: "accounts".into(), }, ) .unwrap(); @@ -26,7 +26,7 @@ fn validate_surface_target_accepts_matching_target() { assert_eq!( target, SurfaceTarget::Settings { - tab: "apiKeys".into() + tab: "accounts".into() } ); } @@ -36,7 +36,7 @@ fn validate_surface_target_rejects_mismatched_target() { let error = validate_surface_target( SurfaceMode::TrayPanel, SurfaceTarget::Settings { - tab: "apiKeys".into(), + tab: "accounts".into(), }, ) .unwrap_err(); diff --git a/apps/desktop-tauri/src-tauri/src/proof_harness.rs b/apps/desktop-tauri/src-tauri/src/proof_harness.rs index fcecb341..6445f012 100644 --- a/apps/desktop-tauri/src-tauri/src/proof_harness.rs +++ b/apps/desktop-tauri/src-tauri/src/proof_harness.rs @@ -82,7 +82,7 @@ static PROOF_SYNC_CONTROL: LazyLock> = pub struct ProofConfig { /// The surface to show on startup (serialized as the camelCase id). pub target_surface: String, - /// Optional settings tab id (e.g. `"apiKeys"`, `"cookies"`). + /// Optional settings tab id (e.g. `"accounts"`, `"menu"`). pub settings_tab: Option, /// Optional target payload for richer proof routing, such as /// `"provider:codex"` for pop-out provider views. @@ -695,15 +695,15 @@ mod tests { #[test] fn parse_settings_with_tab() { - with_proof_mode_env(Some("settings:apiKeys"), || { + with_proof_mode_env(Some("settings:accounts"), || { let cfg = ProofConfig::from_env().unwrap(); assert_eq!(cfg.target_surface, "settings"); - assert_eq!(cfg.settings_tab.as_deref(), Some("apiKeys")); + assert_eq!(cfg.settings_tab.as_deref(), Some("accounts")); assert_eq!(cfg.surface_mode(), SurfaceMode::Settings); assert_eq!( cfg.surface_target(), SurfaceTarget::Settings { - tab: "apiKeys".into() + tab: "accounts".into() } ); }); @@ -799,6 +799,33 @@ mod tests { assert!(ProofCommand::parse("open-settings:security").is_none()); } + #[test] + fn parse_proof_command_accepts_live_settings_tabs() { + // SBS-872: `accounts` is a live shell tab that the old allowlist dropped. + assert_eq!( + ProofCommand::parse("open-settings:accounts"), + Some(ProofCommand::OpenSettings { + tab: "accounts".into() + }) + ); + assert_eq!( + ProofCommand::parse("open-settings:menu"), + Some(ProofCommand::OpenSettings { tab: "menu".into() }) + ); + } + + #[test] + fn parse_proof_command_rejects_retired_settings_tabs() { + // SBS-872: these ids used to pass the Rust allowlist (or the TS union) + // after the Settings shell stopped rendering them. + for retired in ["menuBar", "display", "apiKeys", "cookies"] { + assert!( + ProofCommand::parse(&format!("open-settings:{retired}")).is_none(), + "{retired} must not be a proof-mode settings tab" + ); + } + } + #[test] fn about_path_snapshot_persists_only_after_success() { let _guard = MENU_LOCK.lock().unwrap(); diff --git a/apps/desktop-tauri/src-tauri/src/state.rs b/apps/desktop-tauri/src-tauri/src/state.rs index a133f7f4..a26d8187 100644 --- a/apps/desktop-tauri/src-tauri/src/state.rs +++ b/apps/desktop-tauri/src-tauri/src/state.rs @@ -372,7 +372,7 @@ mod tests { let transition = state.transition_surface( SurfaceMode::Settings, SurfaceTarget::Settings { - tab: "apiKeys".into(), + tab: "accounts".into(), }, ); @@ -380,7 +380,7 @@ mod tests { assert_eq!( state.current_target, SurfaceTarget::Settings { - tab: "apiKeys".into() + tab: "accounts".into() } ); } @@ -427,7 +427,7 @@ mod tests { state.transition_surface( SurfaceMode::Settings, SurfaceTarget::Settings { - tab: "apiKeys".into(), + tab: "accounts".into(), }, ); @@ -474,7 +474,7 @@ mod tests { state.transition_surface( SurfaceMode::Settings, SurfaceTarget::Settings { - tab: "apiKeys".into(), + tab: "accounts".into(), }, ); diff --git a/apps/desktop-tauri/src-tauri/src/surface_target.rs b/apps/desktop-tauri/src-tauri/src/surface_target.rs index b82850cf..93543f61 100644 --- a/apps/desktop-tauri/src-tauri/src/surface_target.rs +++ b/apps/desktop-tauri/src-tauri/src/surface_target.rs @@ -4,12 +4,17 @@ use codexbar::core::ProviderId; use crate::surface::SurfaceMode; +/// Live Settings shell tabs, in the order `TAB_META` renders them. +/// +/// Keep this list identical to `SettingsTabId` / `TAB_META` in the frontend. +/// `settingsTabs.test.ts` compares the three; a silent extra or missing id +/// is how SBS-872 shipped (`menuBar` / `apiKeys` vs `menu` / `accounts`). const SETTINGS_TAB_IDS: &[&str] = &[ "general", "providers", - "display", - "apiKeys", - "cookies", + "accounts", + "notifications", + "menu", "advanced", "about", ]; @@ -88,7 +93,9 @@ pub fn is_supported_settings_tab(tab: &str) -> bool { #[cfg(test)] mod tests { - use super::{SurfaceTarget, is_supported_provider_id, is_supported_settings_tab}; + use super::{ + SETTINGS_TAB_IDS, SurfaceTarget, is_supported_provider_id, is_supported_settings_tab, + }; use serde_json::json; #[test] @@ -171,8 +178,28 @@ mod tests { #[test] fn supported_settings_tabs_match_shell_tabs() { - assert!(is_supported_settings_tab("apiKeys")); - assert!(is_supported_settings_tab("about")); - assert!(!is_supported_settings_tab("security")); + // Pins the failure mode in SBS-872: the Rust allowlist sampled its own + // constant and never compared it to the Settings shell, so `apiKeys` + // stayed allowed after that tab was removed and `accounts` / `menu` + // were rejected. + const LIVE: &[&str] = &[ + "general", + "providers", + "accounts", + "notifications", + "menu", + "advanced", + "about", + ]; + assert_eq!(SETTINGS_TAB_IDS, LIVE); + for tab in LIVE { + assert!(is_supported_settings_tab(tab), "{tab} must be a live tab"); + } + for retired in ["menuBar", "display", "apiKeys", "cookies", "security"] { + assert!( + !is_supported_settings_tab(retired), + "{retired} is not a live Settings tab" + ); + } } } diff --git a/apps/desktop-tauri/src/floatbar/FloatBar.test.tsx b/apps/desktop-tauri/src/floatbar/FloatBar.test.tsx index 357a8941..0050fc2d 100644 --- a/apps/desktop-tauri/src/floatbar/FloatBar.test.tsx +++ b/apps/desktop-tauri/src/floatbar/FloatBar.test.tsx @@ -1,4 +1,4 @@ -import { act, render, waitFor } from "@testing-library/react"; +import { act, fireEvent, render, waitFor } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; const tauriMocks = vi.hoisted(() => ({ @@ -12,6 +12,7 @@ const tauriMocks = vi.hoisted(() => ({ updateSettings: vi.fn(), getLocaleStrings: vi.fn(), setUiLanguage: vi.fn(), + openSettingsWindow: vi.fn(), })); const eventMocks = vi.hoisted(() => ({ @@ -758,6 +759,28 @@ describe("FloatBar", () => { ); }); + it("opens the live Display settings tab from the menu, not menuBar (SBS-872)", async () => { + // Failure mode: FloatBar sent menuBar, Settings.isSettingsTab rejected + // it, and the detached window landed on General. PopOutPanel already + // uses the live menu id for the same Display tab. + tauriMocks.getCachedProviders.mockResolvedValue([ + snapshot("codex", "Codex", 40), + ]); + tauriMocks.getSettingsSnapshot.mockResolvedValue(settings()); + tauriMocks.openSettingsWindow.mockResolvedValue(undefined); + + const { container, getByText } = renderFloatBar(bootstrap()); + await waitFor(() => { + expect(container.querySelector(".floatbar")).not.toBeNull(); + }); + + fireEvent.contextMenu(container.querySelector(".floatbar")!); + fireEvent.click(getByText("FloatBarOpenSettings")); + + expect(tauriMocks.openSettingsWindow).toHaveBeenCalledWith("menu"); + expect(tauriMocks.openSettingsWindow).not.toHaveBeenCalledWith("menuBar"); + }); + it("polls refreshProvidersIfStale on the configured interval", async () => { vi.useFakeTimers(); try { diff --git a/apps/desktop-tauri/src/floatbar/FloatBar.tsx b/apps/desktop-tauri/src/floatbar/FloatBar.tsx index 76ae1151..d2c544af 100644 --- a/apps/desktop-tauri/src/floatbar/FloatBar.tsx +++ b/apps/desktop-tauri/src/floatbar/FloatBar.tsx @@ -702,7 +702,7 @@ export default function FloatBar({ state }: { state: BootstrapState }) { setMenuOpen(false); }, [settings.floatBarClickThrough]); const handleOpenSettings = useCallback(() => { - void openSettingsWindow("menuBar").catch(() => {}); + void openSettingsWindow("menu").catch(() => {}); setMenuOpen(false); }, []); const handleHide = useCallback(() => { diff --git a/apps/desktop-tauri/src/lib/tauri.ts b/apps/desktop-tauri/src/lib/tauri.ts index 4041e109..66efa1e5 100644 --- a/apps/desktop-tauri/src/lib/tauri.ts +++ b/apps/desktop-tauri/src/lib/tauri.ts @@ -26,6 +26,7 @@ import type { ProviderTokenAccountsBridge, TokenAccountSupportBridge, SettingsSnapshot, + SettingsTabId, SettingsUpdate, SurfaceMode, SurfaceTargetForMode, @@ -116,7 +117,7 @@ export function endFlyoutGesture(): Promise { return invoke("end_flyout_gesture"); } -export function openSettingsWindow(tab: string): Promise { +export function openSettingsWindow(tab: SettingsTabId): Promise { return invoke("open_settings_window", { tab }); } diff --git a/apps/desktop-tauri/src/surfaces/Settings.test.ts b/apps/desktop-tauri/src/surfaces/Settings.test.ts index 0dadca6c..dec41d93 100644 --- a/apps/desktop-tauri/src/surfaces/Settings.test.ts +++ b/apps/desktop-tauri/src/surfaces/Settings.test.ts @@ -1,12 +1,30 @@ import { describe, expect, it } from "vitest"; import { TAB_META } from "./Settings"; +/** Live Settings shell tabs, in render order. SBS-872. */ +const LIVE_SETTINGS_TABS = [ + "general", + "providers", + "accounts", + "notifications", + "menu", + "advanced", + "about", +] as const; + describe("Settings navigation", () => { it("lists providers separately after general", () => { expect(TAB_META.slice(0, 2)).toEqual([ { id: "general", labelKey: "TabGeneral" }, { id: "providers", labelKey: "TabProviders" }, ]); - expect(TAB_META.some((tab) => tab.id === "menuBar")).toBe(false); + }); + + it("exposes exactly the live shell tabs", () => { + // Pins SBS-872: TAB_META is the only list isSettingsTab consults. A + // caller that still sends a retired id (menuBar, apiKeys, display) + // falls through to General. settingsTabs.test.ts compares this list + // to the Rust allowlist and the SettingsTabId union. + expect(TAB_META.map((tab) => tab.id)).toEqual([...LIVE_SETTINGS_TABS]); }); }); diff --git a/apps/desktop-tauri/src/surfaces/Settings.tsx b/apps/desktop-tauri/src/surfaces/Settings.tsx index e8a6261b..5974ec77 100644 --- a/apps/desktop-tauri/src/surfaces/Settings.tsx +++ b/apps/desktop-tauri/src/surfaces/Settings.tsx @@ -76,12 +76,6 @@ const TabIcons: Record = { ), - menuBar: ( - - - - - ), menu: ( diff --git a/apps/desktop-tauri/src/test/raw.d.ts b/apps/desktop-tauri/src/test/raw.d.ts new file mode 100644 index 00000000..ce1e43f5 --- /dev/null +++ b/apps/desktop-tauri/src/test/raw.d.ts @@ -0,0 +1,4 @@ +declare module "*?raw" { + const content: string; + export default content; +} diff --git a/apps/desktop-tauri/src/test/settingsTabs.test.ts b/apps/desktop-tauri/src/test/settingsTabs.test.ts new file mode 100644 index 00000000..f18bbd54 --- /dev/null +++ b/apps/desktop-tauri/src/test/settingsTabs.test.ts @@ -0,0 +1,57 @@ +import { describe, expect, it } from "vitest"; +import { TAB_META } from "../surfaces/Settings"; +import rustSource from "../../src-tauri/src/surface_target.rs?raw"; +import bridgeSource from "../types/bridge.ts?raw"; +import floatBarSource from "../floatbar/FloatBar.tsx?raw"; +import popOutSource from "../surfaces/PopOutPanel.tsx?raw"; +import traySource from "../surfaces/TrayPanel.tsx?raw"; + +function quoted(block: string): string[] { + return [...block.matchAll(/"([^"]+)"/g)].map((match) => match[1]); +} + +function rustTabs(src: string): string[] { + const block = src.match(/const SETTINGS_TAB_IDS: &\[&str\] = &\[([\s\S]*?)\];/); + if (!block) throw new Error("SETTINGS_TAB_IDS not found"); + return quoted(block[1]); +} + +function unionTabs(src: string): string[] { + const block = src.match(/export type SettingsTabId =\s*([\s\S]*?);/); + if (!block) throw new Error("SettingsTabId not found"); + return quoted(block[1]); +} + +function callTabs(src: string): string[] { + return [...src.matchAll(/openSettingsWindow\(\s*["]([^"]+)["]\s*\)/g)].map( + (match) => match[1], + ); +} + +describe("settings tab contract (SBS-872)", () => { + const live = TAB_META.map((tab) => tab.id); + + it("keeps the Rust allowlist identical to TAB_META", () => { + expect(rustTabs(rustSource)).toEqual(live); + }); + + it("keeps SettingsTabId identical to TAB_META", () => { + expect(unionTabs(bridgeSource)).toEqual(live); + }); + + it("makes every openSettingsWindow caller send a live tab", () => { + const calls = [ + ...callTabs(floatBarSource).map((tab) => ({ file: "FloatBar.tsx", tab })), + ...callTabs(popOutSource).map((tab) => ({ file: "PopOutPanel.tsx", tab })), + ...callTabs(traySource).map((tab) => ({ file: "TrayPanel.tsx", tab })), + ]; + expect(calls.length).toBeGreaterThan(0); + for (const call of calls) { + expect(live, call.file + " sent " + call.tab).toContain(call.tab); + } + }); + + it("opens Display from the float bar via the live menu id", () => { + expect(callTabs(floatBarSource)).toEqual(["menu"]); + }); +}); diff --git a/apps/desktop-tauri/src/types/bridge.ts b/apps/desktop-tauri/src/types/bridge.ts index 31e8a428..093b91a9 100644 --- a/apps/desktop-tauri/src/types/bridge.ts +++ b/apps/desktop-tauri/src/types/bridge.ts @@ -5,7 +5,6 @@ export type SettingsTabId = | "providers" | "accounts" | "notifications" - | "menuBar" | "menu" | "advanced" | "about"; From fb69eb0733b8406fbeebccbffa7938371894aa5d Mon Sep 17 00:00:00 2001 From: tsouth89 Date: Sun, 16 Aug 2026 06:24:13 -0400 Subject: [PATCH 2/2] Point the proof-mode header at live settings tabs (SBS-872) The crate header still advertised `settings:apiKeys` and `settings:cookies`. Both were dropped from `SETTINGS_TAB_IDS`, so `ProofConfig::from_env` logs an unsupported target and returns None: anyone following the header never enters proof mode and never gets a Settings window at all. Replaces them with `settings:accounts` and `settings:menu`, and adds `header_examples_are_live_proof_mode_values`, which reads the bullet examples back out of this file's own source and asserts each one still parses. A retired id cannot survive in the header again. --- .../src-tauri/src/proof_harness.rs | 41 ++++++++++++++++++- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/apps/desktop-tauri/src-tauri/src/proof_harness.rs b/apps/desktop-tauri/src-tauri/src/proof_harness.rs index 6445f012..21b9e24c 100644 --- a/apps/desktop-tauri/src-tauri/src/proof_harness.rs +++ b/apps/desktop-tauri/src-tauri/src/proof_harness.rs @@ -8,10 +8,13 @@ //! - `popOut` — show the pop-out dashboard //! - `popOut:provider:codex` — show a provider pop-out //! - `settings` — show settings (General tab) -//! - `settings:apiKeys` — show settings on the API Keys tab -//! - `settings:cookies` — show settings on the Cookies tab +//! - `settings:accounts` — show settings on the Accounts tab +//! - `settings:menu` — show settings on the Display tab //! - `settings:about` — show settings on the About tab //! +//! Every example above is pinned by `header_examples_are_live_proof_mode_values` +//! so a retired tab id cannot survive here (SBS-872). +//! //! In proof mode the shell immediately transitions to the requested surface //! and suppresses blur-dismiss so the window stays visible for automated //! screenshot capture. @@ -826,6 +829,40 @@ mod tests { } } + /// Every backtick-quoted bullet example from this module's header comment. + fn header_examples() -> Vec { + include_str!("proof_harness.rs") + .lines() + .take_while(|line| line.starts_with("//!")) + .filter_map(|line| { + let rest = line.trim_start_matches("//!").trim_start(); + let (example, _) = rest.strip_prefix("- `")?.split_once('`')?; + Some(example.to_string()) + }) + .collect() + } + + #[test] + fn header_examples_are_live_proof_mode_values() { + // SBS-872: the header advertised `settings:apiKeys` / `settings:cookies` + // after the allowlist dropped them, so anyone following it silently got + // no proof mode at all. + let examples = header_examples(); + assert_eq!( + examples.len(), + 7, + "header bullet list changed shape: {examples:?}" + ); + for example in examples { + with_proof_mode_env(Some(&example), || { + assert!( + ProofConfig::from_env().is_some(), + "header example `{example}` no longer enters proof mode" + ); + }); + } + } + #[test] fn about_path_snapshot_persists_only_after_success() { let _guard = MENU_LOCK.lock().unwrap();