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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
6 changes: 3 additions & 3 deletions apps/desktop-tauri/src-tauri/src/commands/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,15 @@ fn validate_surface_target_accepts_matching_target() {
let target = validate_surface_target(
SurfaceMode::Settings,
SurfaceTarget::Settings {
tab: "apiKeys".into(),
tab: "accounts".into(),
},
)
.unwrap();

assert_eq!(
target,
SurfaceTarget::Settings {
tab: "apiKeys".into()
tab: "accounts".into()
}
);
}
Expand All @@ -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();
Expand Down
76 changes: 70 additions & 6 deletions apps/desktop-tauri/src-tauri/src/proof_harness.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -82,7 +85,7 @@ static PROOF_SYNC_CONTROL: LazyLock<Mutex<ProofSyncControl>> =
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<String>,
/// Optional target payload for richer proof routing, such as
/// `"provider:codex"` for pop-out provider views.
Expand Down Expand Up @@ -695,15 +698,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()
}
);
});
Expand Down Expand Up @@ -799,6 +802,67 @@ 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"] {
Comment thread
tsouth89 marked this conversation as resolved.
assert!(
ProofCommand::parse(&format!("open-settings:{retired}")).is_none(),
"{retired} must not be a proof-mode settings tab"
);
}
}

/// Every backtick-quoted bullet example from this module's header comment.
fn header_examples() -> Vec<String> {
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();
Expand Down
8 changes: 4 additions & 4 deletions apps/desktop-tauri/src-tauri/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -372,15 +372,15 @@ mod tests {
let transition = state.transition_surface(
SurfaceMode::Settings,
SurfaceTarget::Settings {
tab: "apiKeys".into(),
tab: "accounts".into(),
},
);

assert!(transition.is_some());
assert_eq!(
state.current_target,
SurfaceTarget::Settings {
tab: "apiKeys".into()
tab: "accounts".into()
}
);
}
Expand Down Expand Up @@ -427,7 +427,7 @@ mod tests {
state.transition_surface(
SurfaceMode::Settings,
SurfaceTarget::Settings {
tab: "apiKeys".into(),
tab: "accounts".into(),
},
);

Expand Down Expand Up @@ -474,7 +474,7 @@ mod tests {
state.transition_surface(
SurfaceMode::Settings,
SurfaceTarget::Settings {
tab: "apiKeys".into(),
tab: "accounts".into(),
},
);

Expand Down
41 changes: 34 additions & 7 deletions apps/desktop-tauri/src-tauri/src/surface_target.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
];
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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"
);
}
}
}
25 changes: 24 additions & 1 deletion apps/desktop-tauri/src/floatbar/FloatBar.test.tsx
Original file line number Diff line number Diff line change
@@ -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(() => ({
Expand All @@ -12,6 +12,7 @@ const tauriMocks = vi.hoisted(() => ({
updateSettings: vi.fn(),
getLocaleStrings: vi.fn(),
setUiLanguage: vi.fn(),
openSettingsWindow: vi.fn(),
}));

const eventMocks = vi.hoisted(() => ({
Expand Down Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop-tauri/src/floatbar/FloatBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand Down
3 changes: 2 additions & 1 deletion apps/desktop-tauri/src/lib/tauri.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import type {
ProviderTokenAccountsBridge,
TokenAccountSupportBridge,
SettingsSnapshot,
SettingsTabId,
SettingsUpdate,
SurfaceMode,
SurfaceTargetForMode,
Expand Down Expand Up @@ -116,7 +117,7 @@ export function endFlyoutGesture(): Promise<void> {
return invoke<void>("end_flyout_gesture");
}

export function openSettingsWindow(tab: string): Promise<void> {
export function openSettingsWindow(tab: SettingsTabId): Promise<void> {
return invoke<void>("open_settings_window", { tab });
}

Expand Down
20 changes: 19 additions & 1 deletion apps/desktop-tauri/src/surfaces/Settings.test.ts
Original file line number Diff line number Diff line change
@@ -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]);
});
});
6 changes: 0 additions & 6 deletions apps/desktop-tauri/src/surfaces/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,6 @@ const TabIcons: Record<SettingsTab, ReactElement> = {
<path d="M6.5 13a1.7 1.7 0 0 0 3 0" />
</Svg>
),
menuBar: (
<Svg>
<path d="M1.5 8c1.6-3 4-4.5 6.5-4.5S13 5 14.5 8c-1.5 3-4 4.5-6.5 4.5S3.1 11 1.5 8Z" />
<circle cx="8" cy="8" r="2" />
</Svg>
),
menu: (
<Svg>
<rect x="2" y="2" width="5" height="5" rx="1" />
Expand Down
4 changes: 4 additions & 0 deletions apps/desktop-tauri/src/test/raw.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
declare module "*?raw" {
const content: string;
export default content;
}
Loading
Loading