From e5149c40ee7d176834adf8b8e7ff86ca8f564dcf Mon Sep 17 00:00:00 2001 From: NessZerra Date: Wed, 8 Jul 2026 09:35:12 +0700 Subject: [PATCH 1/9] Add tray masonry columns Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- apps/desktop-tauri/src/styles.css | 18 +++-- .../src/surfaces/TrayPanel.test.tsx | 37 +++++++++- apps/desktop-tauri/src/surfaces/TrayPanel.tsx | 67 +++++++++++++------ 3 files changed, 90 insertions(+), 32 deletions(-) diff --git a/apps/desktop-tauri/src/styles.css b/apps/desktop-tauri/src/styles.css index 1f28c2873e..8572f9ff08 100644 --- a/apps/desktop-tauri/src/styles.css +++ b/apps/desktop-tauri/src/styles.css @@ -1097,28 +1097,26 @@ body:has(.tray-panel-reveal) { } /* Side-by-side provider cards once the user has widened the flyout enough to - fit two columns — mirrors the PopOut window's `.menu-surface--popout - .menu-stack` row layout (styles.css ~3223), but scoped to the tray's - usersized state only so the auto-fit (content-hugging) path never sees it. - Unlike PopOut, tray cards KEEP their visual chrome (background/border on - `.menu-stack__item--selected`, card padding, etc.) — only the separators - between stacked cards are hidden, since they read as noise once cards sit - side by side instead of one on top of the other. */ + fit two columns. The columns are independent so a tall card in one column + doesn't force empty space under the shorter card beside it. */ @media (min-width: 640px) { .tray-panel-reveal--usersized .menu-surface--tray .menu-stack { display: flex; flex-direction: row; - flex-wrap: wrap; align-content: flex-start; align-items: flex-start; gap: 12px 16px; width: 100%; } - .tray-panel-reveal--usersized .menu-surface--tray .menu-stack__item { + + .tray-panel-reveal--usersized .menu-surface--tray .menu-stack__column { flex: 1 1 0; min-width: min(300px, 100%); - width: auto; + display: flex; + flex-direction: column; + gap: 12px; } + .tray-panel-reveal--usersized .menu-surface--tray .menu-stack__sep { display: none; } diff --git a/apps/desktop-tauri/src/surfaces/TrayPanel.test.tsx b/apps/desktop-tauri/src/surfaces/TrayPanel.test.tsx index 50b7caf24b..8797a26c87 100644 --- a/apps/desktop-tauri/src/surfaces/TrayPanel.test.tsx +++ b/apps/desktop-tauri/src/surfaces/TrayPanel.test.tsx @@ -1,5 +1,5 @@ import { act, fireEvent, render, screen, waitFor } from "@testing-library/react"; -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; const tauriMocks = vi.hoisted(() => ({ getCachedProviders: vi.fn(), @@ -181,6 +181,7 @@ describe("TrayPanel provider grid", () => { beforeEach(() => { vi.clearAllMocks(); eventMocks.listeners.clear(); + tauriMocks.flyoutStoredSize.mockResolvedValue(null); tauriMocks.refreshProviders.mockResolvedValue(undefined); tauriMocks.refreshProvidersIfStale.mockResolvedValue(undefined); tauriMocks.dismissTrayPanel.mockResolvedValue(undefined); @@ -238,6 +239,9 @@ describe("TrayPanel provider grid", () => { }, ); }); + afterEach(() => { + vi.restoreAllMocks(); + }); it("reveals regardless of the shared surface-mode snapshot (TrayPanel now runs in its own dedicated window)", async () => { // TrayPanel is now hosted exclusively in the dedicated `flyout` OS @@ -486,6 +490,37 @@ describe("TrayPanel provider grid", () => { ).toEqual(["Codex", "Claude", "Cursor", "Factory", "Gemini"]); }); + it("uses independent columns for a wide user-sized overview", async () => { + vi.spyOn(window, "innerWidth", "get").mockReturnValue(700); + tauriMocks.flyoutStoredSize.mockResolvedValue([700, 700]); + const providers = [ + provider("codex", "Codex"), + provider("claude", "Claude"), + provider("antigravity", "Antigravity"), + provider("copilot", "GitHub Copilot"), + ]; + + const { container } = renderTrayPanel(providers, { + enabledProviders: providers.map((snapshot) => snapshot.providerId), + }); + + await waitFor(() => { + expect(container.querySelector(".tray-panel-reveal--usersized")).not.toBeNull(); + }); + + expect( + Array.from(container.querySelectorAll(".menu-stack__column")).map((column) => + Array.from(column.querySelectorAll(".menu-stack__item")).map( + (item) => item.id, + ), + ), + ).toEqual([ + ["card-codex", "card-antigravity"], + ["card-claude", "card-copilot"], + ]); + expect(container.querySelector(".menu-stack__sep")).toBeNull(); + }); + it("collapses and expands the full provider catalog in the dense tray grid", async () => { const providers = TEST_PROVIDER_CATALOG.map(([id, displayName], index) => provider(id, displayName, (index * 7) % 100), diff --git a/apps/desktop-tauri/src/surfaces/TrayPanel.tsx b/apps/desktop-tauri/src/surfaces/TrayPanel.tsx index a272a29747..398a65ca03 100644 --- a/apps/desktop-tauri/src/surfaces/TrayPanel.tsx +++ b/apps/desktop-tauri/src/surfaces/TrayPanel.tsx @@ -309,6 +309,21 @@ export default function TrayPanel({ state }: { state: BootstrapState }) { // reveal itself. Hardcoded true: being mounted IS "the flyout is open". const isFlyoutOpen = true; const fixedFlyoutSize = Array.isArray(flyoutSize) ? flyoutSize : null; + const [viewportWidth, setViewportWidth] = useState(() => window.innerWidth); + useEffect(() => { + const updateViewportWidth = () => setViewportWidth(window.innerWidth); + window.addEventListener("resize", updateViewportWidth); + return () => window.removeEventListener("resize", updateViewportWidth); + }, []); + const useWideColumns = + selectedProviderId === null && fixedFlyoutSize !== null && viewportWidth >= 640; + const wideColumns = useMemo(() => { + const columns: ProviderUsageSnapshot[][] = [[], []]; + visibleProviders.forEach((provider, index) => { + columns[index % 2].push(provider); + }); + return columns; + }, [visibleProviders]); const { layoutReady, requestLayout } = useTrayPanelLayout({ canMeasure: hasLoadedCache || sorted.length > 0, denseOverview: expectsDenseOverview, @@ -408,6 +423,26 @@ export default function TrayPanel({ state }: { state: BootstrapState }) { /> ); const revealClassName = `tray-panel-reveal${layoutReady ? " tray-panel-reveal--ready" : ""}${expectsDenseOverview ? " tray-panel-reveal--dense" : ""}${fixedFlyoutSize ? " tray-panel-reveal--usersized" : ""}`; + const renderProviderCard = (p: ProviderUsageSnapshot) => { + const isSelected = + selectedProviderId !== null && p.providerId === selectedProviderId; + return ( +
+ +
+ ); + }; if (sorted.length === 0) { return ( @@ -458,28 +493,18 @@ export default function TrayPanel({ state }: { state: BootstrapState }) { />
- {visibleProviders.map((p, idx) => { - const isSelected = - selectedProviderId !== null && p.providerId === selectedProviderId; - return ( - - {idx > 0 &&
} -
- + {useWideColumns + ? wideColumns.map((column, index) => ( +
+ {column.map(renderProviderCard)}
- - ); - })} + )) + : visibleProviders.map((p, idx) => ( + + {idx > 0 &&
} + {renderProviderCard(p)} + + ))}
{/* Context actions — detail mode only, matches macOS actionsSection */} {selectedProviderId && (HAS_DASHBOARD.has(selectedProviderId) || HAS_STATUS_PAGE.has(selectedProviderId)) && ( From 33c0392b4748bef0b8f118a50526715d6830df0f Mon Sep 17 00:00:00 2001 From: NessZerra Date: Wed, 8 Jul 2026 17:43:23 +0700 Subject: [PATCH 2/9] Use saved flyout width for tray masonry Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../src/surfaces/TrayPanel.test.tsx | 23 ++++++++++++++++++- apps/desktop-tauri/src/surfaces/TrayPanel.tsx | 10 +++----- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/apps/desktop-tauri/src/surfaces/TrayPanel.test.tsx b/apps/desktop-tauri/src/surfaces/TrayPanel.test.tsx index 8797a26c87..ab19b4b3ef 100644 --- a/apps/desktop-tauri/src/surfaces/TrayPanel.test.tsx +++ b/apps/desktop-tauri/src/surfaces/TrayPanel.test.tsx @@ -491,7 +491,6 @@ describe("TrayPanel provider grid", () => { }); it("uses independent columns for a wide user-sized overview", async () => { - vi.spyOn(window, "innerWidth", "get").mockReturnValue(700); tauriMocks.flyoutStoredSize.mockResolvedValue([700, 700]); const providers = [ provider("codex", "Codex"), @@ -521,6 +520,28 @@ describe("TrayPanel provider grid", () => { expect(container.querySelector(".menu-stack__sep")).toBeNull(); }); + it("keeps the stacked layout when the saved flyout width is narrow", async () => { + vi.spyOn(window, "innerWidth", "get").mockReturnValue(700); + tauriMocks.flyoutStoredSize.mockResolvedValue([500, 700]); + const providers = [ + provider("codex", "Codex"), + provider("claude", "Claude"), + provider("antigravity", "Antigravity"), + provider("copilot", "GitHub Copilot"), + ]; + + const { container } = renderTrayPanel(providers, { + enabledProviders: providers.map((snapshot) => snapshot.providerId), + }); + + await waitFor(() => { + expect(container.querySelector(".tray-panel-reveal--usersized")).not.toBeNull(); + }); + + expect(container.querySelector(".menu-stack__column")).toBeNull(); + expect(container.querySelectorAll(".menu-stack__sep")).toHaveLength(3); + }); + it("collapses and expands the full provider catalog in the dense tray grid", async () => { const providers = TEST_PROVIDER_CATALOG.map(([id, displayName], index) => provider(id, displayName, (index * 7) % 100), diff --git a/apps/desktop-tauri/src/surfaces/TrayPanel.tsx b/apps/desktop-tauri/src/surfaces/TrayPanel.tsx index 398a65ca03..dd4a2dc8d5 100644 --- a/apps/desktop-tauri/src/surfaces/TrayPanel.tsx +++ b/apps/desktop-tauri/src/surfaces/TrayPanel.tsx @@ -309,14 +309,10 @@ export default function TrayPanel({ state }: { state: BootstrapState }) { // reveal itself. Hardcoded true: being mounted IS "the flyout is open". const isFlyoutOpen = true; const fixedFlyoutSize = Array.isArray(flyoutSize) ? flyoutSize : null; - const [viewportWidth, setViewportWidth] = useState(() => window.innerWidth); - useEffect(() => { - const updateViewportWidth = () => setViewportWidth(window.innerWidth); - window.addEventListener("resize", updateViewportWidth); - return () => window.removeEventListener("resize", updateViewportWidth); - }, []); const useWideColumns = - selectedProviderId === null && fixedFlyoutSize !== null && viewportWidth >= 640; + selectedProviderId === null && + fixedFlyoutSize !== null && + fixedFlyoutSize[0] >= 640; const wideColumns = useMemo(() => { const columns: ProviderUsageSnapshot[][] = [[], []]; visibleProviders.forEach((provider, index) => { From cdcef9e5eec7f31946c7d94429c4eeb2e83fca0b Mon Sep 17 00:00:00 2001 From: NessZerra Date: Sun, 12 Jul 2026 11:10:18 +0700 Subject: [PATCH 3/9] Fix Claude quotas and simplify shared code Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- Cargo.lock | 1 - .../src-tauri/src/commands/bridge.rs | 2 + .../src-tauri/src/commands/settings.rs | 2 + .../src-tauri/src/floatbar/mod.rs | 5 ++ .../src/floatbar/FloatBar.test.tsx | 17 +++- apps/desktop-tauri/src/floatbar/FloatBar.tsx | 15 ++-- .../src/floatbar/SettingsSection.tsx | 11 +++ apps/desktop-tauri/src/types/bridge.ts | 78 +------------------ rust/Cargo.toml | 1 - rust/src/core/mod.rs | 2 - rust/src/lib.rs | 1 - rust/src/providers/claude/cli_reset.rs | 2 +- rust/src/providers/claude/mod.rs | 71 ++++++++++++++++- rust/src/providers/claude/oauth/mod.rs | 24 +++++- rust/src/providers/claude/web_api.rs | 45 +++++++++++ rust/src/settings.rs | 5 ++ rust/src/settings/raw.rs | 4 + rust/src/settings/tests.rs | 3 + rust/src/status/mod.rs | 11 --- 19 files changed, 195 insertions(+), 105 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9a13634079..dd323d5a5b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -667,7 +667,6 @@ dependencies = [ "dirs", "fluent-templates", "futures", - "global-hotkey", "iana-time-zone", "image", "keyring", diff --git a/apps/desktop-tauri/src-tauri/src/commands/bridge.rs b/apps/desktop-tauri/src-tauri/src/commands/bridge.rs index f80945326c..0540b98c44 100644 --- a/apps/desktop-tauri/src-tauri/src/commands/bridge.rs +++ b/apps/desktop-tauri/src-tauri/src/commands/bridge.rs @@ -443,6 +443,7 @@ pub struct SettingsSnapshot { float_bar_provider_ids: Vec, float_bar_dark_text: bool, float_bar_show_reset_inline: bool, + float_bar_show_cost: bool, } #[tauri::command] @@ -528,6 +529,7 @@ impl From for SettingsSnapshot { float_bar_provider_ids: settings.float_bar_provider_ids, float_bar_dark_text: settings.float_bar_dark_text, float_bar_show_reset_inline: settings.float_bar_show_reset_inline, + float_bar_show_cost: settings.float_bar_show_cost, } } } diff --git a/apps/desktop-tauri/src-tauri/src/commands/settings.rs b/apps/desktop-tauri/src-tauri/src/commands/settings.rs index 115a796297..d3472618f8 100644 --- a/apps/desktop-tauri/src-tauri/src/commands/settings.rs +++ b/apps/desktop-tauri/src-tauri/src/commands/settings.rs @@ -51,6 +51,7 @@ pub struct SettingsUpdate { pub float_bar_provider_ids: Option>, pub float_bar_dark_text: Option, pub float_bar_show_reset_inline: Option, + pub float_bar_show_cost: Option, } impl SettingsUpdate { @@ -244,6 +245,7 @@ impl SettingsUpdate { provider_ids: self.float_bar_provider_ids.clone(), dark_text: self.float_bar_dark_text, show_reset_inline: self.float_bar_show_reset_inline, + show_cost: self.float_bar_show_cost, } } diff --git a/apps/desktop-tauri/src-tauri/src/floatbar/mod.rs b/apps/desktop-tauri/src-tauri/src/floatbar/mod.rs index d8288a9029..ea38db93f2 100644 --- a/apps/desktop-tauri/src-tauri/src/floatbar/mod.rs +++ b/apps/desktop-tauri/src-tauri/src/floatbar/mod.rs @@ -105,6 +105,7 @@ pub struct SettingsPatch { pub provider_ids: Option>, pub dark_text: Option, pub show_reset_inline: Option, + pub show_cost: Option, } impl SettingsPatch { @@ -118,6 +119,7 @@ impl SettingsPatch { && self.provider_ids.is_none() && self.dark_text.is_none() && self.show_reset_inline.is_none() + && self.show_cost.is_none() } /// Apply this patch to a mutable `Settings`. Values are clamped and @@ -150,6 +152,9 @@ impl SettingsPatch { if let Some(v) = self.show_reset_inline { settings.float_bar_show_reset_inline = v; } + if let Some(v) = self.show_cost { + settings.float_bar_show_cost = v; + } } } diff --git a/apps/desktop-tauri/src/floatbar/FloatBar.test.tsx b/apps/desktop-tauri/src/floatbar/FloatBar.test.tsx index cfed699510..0d4158a3d4 100644 --- a/apps/desktop-tauri/src/floatbar/FloatBar.test.tsx +++ b/apps/desktop-tauri/src/floatbar/FloatBar.test.tsx @@ -133,6 +133,7 @@ function settings(overrides: Partial = {}): SettingsSnapshot { floatBarProviderIds: [], floatBarDarkText: false, floatBarShowResetInline: false, + floatBarShowCost: false, ...overrides, }; } @@ -199,7 +200,7 @@ describe("FloatBar", () => { tauriMocks.getCachedProviders.mockResolvedValue([ snapshot("codex", "Codex", 75), ]); - tauriMocks.getSettingsSnapshot.mockResolvedValue(settings()); + tauriMocks.getSettingsSnapshot.mockResolvedValue(settings({ floatBarShowCost: true })); tauriMocks.getProviderLocalUsageSummary.mockResolvedValue({ todayCost: 1.25, thirtyDayCost: 12.5, @@ -209,7 +210,7 @@ describe("FloatBar", () => { estimateNote: "Estimated from local logs", }); - renderFloatBar(bootstrap()); + renderFloatBar(bootstrap({ floatBarShowCost: true })); await waitFor(() => { expect(tauriMocks.getProviderLocalUsageSummary).toHaveBeenCalledWith("codex"); @@ -217,6 +218,18 @@ describe("FloatBar", () => { expect(tauriMocks.getProviderChartData).not.toHaveBeenCalled(); }); + it("does not scan local costs by default", async () => { + tauriMocks.getCachedProviders.mockResolvedValue([snapshot("codex", "Codex", 75)]); + tauriMocks.getSettingsSnapshot.mockResolvedValue(settings()); + + renderFloatBar(bootstrap()); + + await waitFor(() => { + expect(tauriMocks.getCachedProviders).toHaveBeenCalled(); + }); + expect(tauriMocks.getProviderLocalUsageSummary).not.toHaveBeenCalled(); + }); + it("can show remaining percentages when configured", async () => { tauriMocks.getCachedProviders.mockResolvedValue([ snapshot("claude", "Claude", 20), diff --git a/apps/desktop-tauri/src/floatbar/FloatBar.tsx b/apps/desktop-tauri/src/floatbar/FloatBar.tsx index 6af1d8c9a8..0ec15c46f3 100644 --- a/apps/desktop-tauri/src/floatbar/FloatBar.tsx +++ b/apps/desktop-tauri/src/floatbar/FloatBar.tsx @@ -292,6 +292,7 @@ export default function FloatBar({ state }: { state: BootstrapState }) { const filterIds = settings.floatBarProviderIds; const scale = Math.max(0.75, Math.min(2, settings.floatBarScale / 100)); const showResetInline = settings.floatBarShowResetInline; + const showCost = settings.floatBarShowCost === true; const visible = useMemo(() => { const enabled = new Set(settings.enabledProviders); let list = providers.filter((p) => enabled.has(p.providerId)); @@ -307,12 +308,14 @@ export default function FloatBar({ state }: { state: BootstrapState }) { .join("|"); const visibleCostTargets = useMemo( () => - visible.map((provider) => ({ - key: providerCostKey(provider), - providerId: provider.providerId, - displayName: provider.displayName, - })), - [visibleCostTargetKey], + showCost + ? visible.map((provider) => ({ + key: providerCostKey(provider), + providerId: provider.providerId, + displayName: provider.displayName, + })) + : [], + [showCost, visibleCostTargetKey], ); useEffect(() => { diff --git a/apps/desktop-tauri/src/floatbar/SettingsSection.tsx b/apps/desktop-tauri/src/floatbar/SettingsSection.tsx index 97e2e46308..57bf65d19e 100644 --- a/apps/desktop-tauri/src/floatbar/SettingsSection.tsx +++ b/apps/desktop-tauri/src/floatbar/SettingsSection.tsx @@ -143,6 +143,17 @@ export default function FloatBarSettingsSection({ settings, saving, set }: Props onChange={(v) => set({ floatBarShowResetInline: v })} /> + + set({ floatBarShowCost: v })} + /> + ; - floatBarEnabled?: boolean; - floatBarOpacity?: number; - floatBarScale?: number; - floatBarOrientation?: FloatBarOrientation; - floatBarStyle?: FloatBarStyle; - floatBarClickThrough?: boolean; - floatBarProviderIds?: string[]; - floatBarDarkText?: boolean; - floatBarShowResetInline?: boolean; -} +export type SettingsUpdate = Partial>; export interface BootstrapState { contractVersion: string; @@ -479,37 +436,8 @@ export interface ProviderTokenAccountsBridge { // ── Phase 4 — provider ordering / cookie source / region ───────────── -export interface ProviderSummary { - id: string; - displayName: string; - enabled: boolean; - order: number; -} - // ── Phase 4 — credential detection ─────────────────────────────────── -export interface GeminiCliStatus { - signedIn: boolean; - credentialsPath: string | null; -} - -export interface VertexAiStatus { - hasCredentials: boolean; - credentialsPath: string | null; -} - -export interface JetbrainsIde { - id: string; - displayName: string; - path: string; - detected: boolean; -} - -export interface KiroStatus { - available: boolean; - hint: string | null; -} - // ── Phase 4 — session / environment ────────────────────────────────── export interface WorkAreaRect { diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 596e075ee0..8f83bb9eef 100755 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -48,7 +48,6 @@ async-trait = "0.1" futures = "0.3" image = { version = "0.25", default-features = false, features = ["png"] } -global-hotkey = "0.7.0" # SQLite for reading browser cookies rusqlite = { version = "0.32", features = ["bundled"] } diff --git a/rust/src/core/mod.rs b/rust/src/core/mod.rs index 41546a5473..eb5f15c907 100755 --- a/rust/src/core/mod.rs +++ b/rust/src/core/mod.rs @@ -2,7 +2,6 @@ mod cost_pricing; mod credential_migration; -mod credentials; mod http; mod jsonl_scanner; mod openai_dashboard; @@ -18,7 +17,6 @@ mod widget_snapshot; pub use cost_pricing::*; pub use credential_migration::*; -pub use credentials::*; pub use http::*; pub use jsonl_scanner::*; pub use openai_dashboard::*; diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 67afd07fcf..1966ae1578 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -15,7 +15,6 @@ pub mod notifications; pub mod providers; pub mod secure_file; pub mod settings; -pub mod shortcuts; pub mod sound; pub mod status; diff --git a/rust/src/providers/claude/cli_reset.rs b/rust/src/providers/claude/cli_reset.rs index 4026265434..68f4bc99b9 100644 --- a/rust/src/providers/claude/cli_reset.rs +++ b/rust/src/providers/claude/cli_reset.rs @@ -96,7 +96,7 @@ pub(super) fn extract_cli_scoped_weekly_limits( limits } -fn slug_claude_model(label: &str) -> String { +pub(super) fn slug_claude_model(label: &str) -> String { let mut slug = String::new(); let mut previous_dash = false; for character in label.chars() { diff --git a/rust/src/providers/claude/mod.rs b/rust/src/providers/claude/mod.rs index 83e25f0a03..6ca06b4083 100755 --- a/rust/src/providers/claude/mod.rs +++ b/rust/src/providers/claude/mod.rs @@ -6,8 +6,9 @@ mod oauth; mod web_api; use async_trait::async_trait; -use chrono::Utc; +use chrono::{DateTime, Utc}; use regex_lite::Regex; +use serde::Deserialize; #[cfg(windows)] use std::os::windows::process::CommandExt; #[cfg(windows)] @@ -15,8 +16,8 @@ use std::process::{Command as StdCommand, Stdio}; use crate::cli::tty_runner::{TtyCommandOptions, TtyCommandRunner}; use crate::core::{ - FetchContext, Provider, ProviderError, ProviderFetchResult, ProviderId, ProviderMetadata, - RateWindow, SourceMode, UsageSnapshot, + FetchContext, NamedRateWindow, Provider, ProviderError, ProviderFetchResult, ProviderId, + ProviderMetadata, RateWindow, SourceMode, UsageSnapshot, }; use admin_api::ClaudeAdminApiFetcher; @@ -24,11 +25,73 @@ use admin_api::ClaudeAdminApiFetcher; use cli_reset::parse_claude_reset_date_in_system_zone; use cli_reset::{ extract_cli_scoped_weekly_limits, normalized_for_label_search, parse_claude_reset_date, - parse_percent_line, starts_next_usage_section, + parse_percent_line, slug_claude_model, starts_next_usage_section, }; pub use oauth::ClaudeOAuthFetcher; pub use web_api::ClaudeWebApiFetcher; +#[derive(Debug, Deserialize)] +struct ScopedWeeklyLimit { + kind: Option, + group: Option, + percent: Option, + #[serde(alias = "resetsAt")] + resets_at: Option, + scope: Option, +} + +#[derive(Debug, Deserialize)] +struct ScopedWeeklyScope { + model: Option, +} + +#[derive(Debug, Deserialize)] +struct ScopedWeeklyModel { + id: Option, + #[serde(alias = "displayName")] + display_name: Option, +} + +fn scoped_weekly_windows(limits: &[ScopedWeeklyLimit]) -> Vec { + let mut seen = std::collections::HashSet::new(); + limits + .iter() + .filter_map(|limit| { + if limit.kind.as_deref() != Some("weekly_scoped") + || limit.group.as_deref() != Some("weekly") + { + return None; + } + let percent = limit.percent.filter(|value| value.is_finite())?; + let model = limit.scope.as_ref()?.model.as_ref()?; + let title = model.display_name.as_deref()?.trim(); + if title.is_empty() { + return None; + } + let identity = model + .id + .as_deref() + .map(str::trim) + .filter(|value| !value.is_empty()) + .unwrap_or(title); + let slug = slug_claude_model(identity); + if slug.is_empty() || !seen.insert(slug.clone()) { + return None; + } + let resets_at = limit + .resets_at + .as_deref() + .and_then(|value| DateTime::parse_from_rfc3339(value).ok()) + .map(|value| value.with_timezone(&Utc)); + Some(NamedRateWindow::new( + format!("claude-weekly-scoped-{slug}"), + format!("{title} only"), + RateWindow::with_details(percent, Some(10080), resets_at, None), + )) + }) + .collect() +} + /// Claude provider implementation pub struct ClaudeProvider { metadata: ProviderMetadata, diff --git a/rust/src/providers/claude/oauth/mod.rs b/rust/src/providers/claude/oauth/mod.rs index 3bb62dec92..f113274fd8 100644 --- a/rust/src/providers/claude/oauth/mod.rs +++ b/rust/src/providers/claude/oauth/mod.rs @@ -73,6 +73,9 @@ pub struct OAuthUsageResponse { #[serde(rename = "extraUsage", alias = "extra_usage")] pub extra_usage: Option, + + #[serde(default)] + limits: Vec, } /// A usage window from the OAuth API @@ -398,6 +401,9 @@ impl ClaudeOAuthFetcher { .push(NamedRateWindow::new(id, title, window)); } } + usage + .extra_rate_windows + .extend(super::scoped_weekly_windows(&response.limits)); // Login method from rate limit tier or default if let Some(ref tier) = credentials.rate_limit_tier { @@ -499,6 +505,14 @@ mod tests { "five_hour": {"utilization": 1.0, "resets_at": "2026-05-22T22:10:00Z"}, "seven_day": {"utilization": 0.14, "resets_at": "2026-05-29T10:00:00Z"}, "seven_day_oauth_apps": {"utilization": 0.0}, + "limits": [{ + "kind": "weekly_scoped", + "group": "weekly", + "percent": 7, + "resets_at": "2026-05-29T10:00:00Z", + "scope": {"model": {"id": null, "display_name": "Fable"}}, + "is_active": true + }], "extra_usage": {"is_enabled": true, "used_credits": 0, "monthly_limit": 1000, "currency": "USD"} }"#, ) @@ -515,7 +529,15 @@ mod tests { assert_eq!(usage.primary.used_percent, 100.0); assert!((usage.secondary.expect("weekly").used_percent - 14.0).abs() < 0.001); - assert!(usage.extra_rate_windows.is_empty()); + let scoped = usage + .extra_rate_windows + .iter() + .find(|window| window.id == "claude-weekly-scoped-fable") + .expect("Fable scoped weekly limit"); + assert_eq!(scoped.title, "Fable only"); + assert_eq!(scoped.window.used_percent, 7.0); + assert_eq!(scoped.window.window_minutes, Some(10080)); + assert!(scoped.window.resets_at.is_some()); } #[test] diff --git a/rust/src/providers/claude/web_api.rs b/rust/src/providers/claude/web_api.rs index d5261169dd..056de129dd 100755 --- a/rust/src/providers/claude/web_api.rs +++ b/rust/src/providers/claude/web_api.rs @@ -95,6 +95,13 @@ struct UsageResponse { seven_day_design: Option, seven_day_routines: Option, extra_usage: Option, + limits: Vec, +} + +impl UsageResponse { + fn scoped_weekly_windows(&self) -> Vec { + super::scoped_weekly_windows(&self.limits) + } } impl<'de> Deserialize<'de> for UsageResponse { @@ -156,6 +163,14 @@ impl<'de> Deserialize<'de> for UsageResponse { "cowork", ], )?, + limits: map + .get("limits") + .filter(|value| !value.is_null()) + .cloned() + .map(serde_json::from_value) + .transpose() + .map_err(serde::de::Error::custom)? + .unwrap_or_default(), extra_usage: map .remove("extra_usage") .filter(|value| !value.is_null()) @@ -352,6 +367,9 @@ impl ClaudeWebApiFetcher { .push(NamedRateWindow::new(id, title, window)); } } + snapshot + .extra_rate_windows + .extend(usage.scoped_weekly_windows()); if let Some(ref acc) = account { if let Some(ref email) = acc.email_address { @@ -805,6 +823,33 @@ mod tests { assert!((routines.used_percent - 11.0).abs() < f64::EPSILON); } + #[test] + fn maps_scoped_weekly_limits_even_when_inactive() { + let usage: super::UsageResponse = serde_json::from_str( + r#"{ + "limits": [ + { + "kind": "weekly_scoped", + "group": "weekly", + "percent": 7, + "resets_at": "2026-07-16T10:00:00Z", + "scope": {"model": {"id": null, "display_name": "Fable"}}, + "is_active": false + } + ] + }"#, + ) + .unwrap(); + + let windows = usage.scoped_weekly_windows(); + + assert_eq!(windows.len(), 1); + assert_eq!(windows[0].id, "claude-weekly-scoped-fable"); + assert_eq!(windows[0].title, "Fable only"); + assert_eq!(windows[0].window.used_percent, 7.0); + assert!(windows[0].window.resets_at.is_some()); + } + #[test] fn parses_duplicate_design_and_routines_aliases_with_preferred_key() { let usage: super::UsageResponse = serde_json::from_str( diff --git a/rust/src/settings.rs b/rust/src/settings.rs index 95e70000d4..902d77400b 100755 --- a/rust/src/settings.rs +++ b/rust/src/settings.rs @@ -205,6 +205,10 @@ pub struct Settings { /// When true, show the primary window's next reset inline in each pill. #[serde(default)] pub float_bar_show_reset_inline: bool, + + /// When true, show local cost summaries in the floating bar. + #[serde(default)] + pub float_bar_show_cost: bool, } fn default_window_scale_percent() -> u16 { @@ -383,6 +387,7 @@ impl Default for Settings { float_bar_provider_ids: Vec::new(), float_bar_dark_text: false, float_bar_show_reset_inline: false, + float_bar_show_cost: false, } } } diff --git a/rust/src/settings/raw.rs b/rust/src/settings/raw.rs index 844c7eae8c..40ede59308 100644 --- a/rust/src/settings/raw.rs +++ b/rust/src/settings/raw.rs @@ -134,6 +134,8 @@ pub(super) struct RawSettings { float_bar_dark_text: bool, #[serde(default)] float_bar_show_reset_inline: bool, + #[serde(default)] + float_bar_show_cost: bool, } impl Default for RawSettings { @@ -212,6 +214,7 @@ impl Default for RawSettings { float_bar_provider_ids: s.float_bar_provider_ids, float_bar_dark_text: s.float_bar_dark_text, float_bar_show_reset_inline: s.float_bar_show_reset_inline, + float_bar_show_cost: s.float_bar_show_cost, } } } @@ -471,6 +474,7 @@ impl From for Settings { float_bar_provider_ids: raw.float_bar_provider_ids, float_bar_dark_text: raw.float_bar_dark_text, float_bar_show_reset_inline: raw.float_bar_show_reset_inline, + float_bar_show_cost: raw.float_bar_show_cost, } } } diff --git a/rust/src/settings/tests.rs b/rust/src/settings/tests.rs index e9ff1f0768..8e7ec58382 100644 --- a/rust/src/settings/tests.rs +++ b/rust/src/settings/tests.rs @@ -23,6 +23,7 @@ fn float_bar_defaults_are_safe() { assert!(settings.float_bar_provider_ids.is_empty()); assert!(!settings.float_bar_dark_text); assert!(!settings.float_bar_show_reset_inline); + assert!(!settings.float_bar_show_cost); } #[test] @@ -138,6 +139,7 @@ fn float_bar_settings_round_trip_through_raw() { float_bar_provider_ids: vec!["claude".into(), "codex".into()], float_bar_dark_text: true, float_bar_show_reset_inline: true, + float_bar_show_cost: true, ..Settings::default() }; @@ -152,6 +154,7 @@ fn float_bar_settings_round_trip_through_raw() { assert_eq!(back.float_bar_provider_ids, vec!["claude", "codex"]); assert!(back.float_bar_dark_text); assert!(back.float_bar_show_reset_inline); + assert!(back.float_bar_show_cost); } #[test] diff --git a/rust/src/status/mod.rs b/rust/src/status/mod.rs index 2d98374471..d0a248568e 100755 --- a/rust/src/status/mod.rs +++ b/rust/src/status/mod.rs @@ -3,20 +3,9 @@ //! Fetches operational status from provider status pages #![allow(dead_code)] -#![allow(unused_imports)] - -pub mod indicators; - use serde::{Deserialize, Serialize}; use std::collections::HashMap; -// Re-export indicator types for convenience -pub use indicators::{ - OverlayPosition, ProviderStatus as IndicatorProviderStatus, - StatusLevel as IndicatorStatusLevel, StatusOverlayConfig, StatuspageIncident, - StatuspageResponse, StatuspageStatus, -}; - /// Status level for a provider #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] #[serde(rename_all = "lowercase")] From 667b6cd60b0e13d48e9afb4264283875dddd08d6 Mon Sep 17 00:00:00 2001 From: NessZerra Date: Sun, 12 Jul 2026 11:15:23 +0700 Subject: [PATCH 4/9] Simplify scoped quota mapping Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- apps/desktop-tauri/src/types/bridge.ts | 4 ---- rust/src/providers/claude/web_api.rs | 10 ++-------- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/apps/desktop-tauri/src/types/bridge.ts b/apps/desktop-tauri/src/types/bridge.ts index eaeafe53a5..e7d6b6acb8 100644 --- a/apps/desktop-tauri/src/types/bridge.ts +++ b/apps/desktop-tauri/src/types/bridge.ts @@ -434,10 +434,6 @@ export interface ProviderTokenAccountsBridge { activeIndex: number; } -// ── Phase 4 — provider ordering / cookie source / region ───────────── - -// ── Phase 4 — credential detection ─────────────────────────────────── - // ── Phase 4 — session / environment ────────────────────────────────── export interface WorkAreaRect { diff --git a/rust/src/providers/claude/web_api.rs b/rust/src/providers/claude/web_api.rs index 056de129dd..6cb3578532 100755 --- a/rust/src/providers/claude/web_api.rs +++ b/rust/src/providers/claude/web_api.rs @@ -98,12 +98,6 @@ struct UsageResponse { limits: Vec, } -impl UsageResponse { - fn scoped_weekly_windows(&self) -> Vec { - super::scoped_weekly_windows(&self.limits) - } -} - impl<'de> Deserialize<'de> for UsageResponse { fn deserialize>(deserializer: D) -> Result { let mut map: std::collections::HashMap = @@ -369,7 +363,7 @@ impl ClaudeWebApiFetcher { } snapshot .extra_rate_windows - .extend(usage.scoped_weekly_windows()); + .extend(super::scoped_weekly_windows(&usage.limits)); if let Some(ref acc) = account { if let Some(ref email) = acc.email_address { @@ -841,7 +835,7 @@ mod tests { ) .unwrap(); - let windows = usage.scoped_weekly_windows(); + let windows = super::super::scoped_weekly_windows(&usage.limits); assert_eq!(windows.len(), 1); assert_eq!(windows[0].id, "claude-weekly-scoped-fable"); From 2bdb4ae9f2331aae84248557932ef9c8637d2712 Mon Sep 17 00:00:00 2001 From: NessZerra Date: Sun, 12 Jul 2026 11:24:38 +0700 Subject: [PATCH 5/9] Clarify localized reset handling Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- rust/src/providers/windsurf/mod.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/rust/src/providers/windsurf/mod.rs b/rust/src/providers/windsurf/mod.rs index 8fbbf4a89c..88c9f5d5fb 100644 --- a/rust/src/providers/windsurf/mod.rs +++ b/rust/src/providers/windsurf/mod.rs @@ -272,8 +272,7 @@ fn valid_json_text(text: &str) -> Option { fn window_from_remaining_percent(remaining: f64, reset_unix: Option) -> RateWindow { let resets_at = reset_unix.and_then(|ts| DateTime::::from_timestamp(ts, 0)); - // ponytail: reset countdown is localized at render time from `resets_at`; - // do not bake a language-specific description into the cached snapshot. + // Reset countdowns are localized at render time; keep cached snapshots language-neutral. RateWindow::with_details((100.0 - remaining).clamp(0.0, 100.0), None, resets_at, None) } From 0ff7224e1a0eee0004a12cceef8dd4e613cef5c0 Mon Sep 17 00:00:00 2001 From: NessZerra Date: Sun, 12 Jul 2026 11:37:05 +0700 Subject: [PATCH 6/9] Remove duplicate FloatBar cost setting Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../src/floatbar/SettingsSection.test.ts | 8 ++++++++ apps/desktop-tauri/src/floatbar/SettingsSection.tsx | 11 ----------- 2 files changed, 8 insertions(+), 11 deletions(-) create mode 100644 apps/desktop-tauri/src/floatbar/SettingsSection.test.ts diff --git a/apps/desktop-tauri/src/floatbar/SettingsSection.test.ts b/apps/desktop-tauri/src/floatbar/SettingsSection.test.ts new file mode 100644 index 0000000000..9b85803c4c --- /dev/null +++ b/apps/desktop-tauri/src/floatbar/SettingsSection.test.ts @@ -0,0 +1,8 @@ +import { describe, expect, it } from "vitest"; +import source from "./SettingsSection.tsx?raw"; + +describe("FloatBar settings", () => { + it("renders one cost toggle", () => { + expect(source.match(/floatBarShowCost: v/g)).toHaveLength(1); + }); +}); diff --git a/apps/desktop-tauri/src/floatbar/SettingsSection.tsx b/apps/desktop-tauri/src/floatbar/SettingsSection.tsx index 1d3bcec982..f4b04d86df 100644 --- a/apps/desktop-tauri/src/floatbar/SettingsSection.tsx +++ b/apps/desktop-tauri/src/floatbar/SettingsSection.tsx @@ -156,17 +156,6 @@ export default function FloatBarSettingsSection({ settings, saving, set }: Props onChange={(v) => set({ floatBarShowResetInline: v })} /> - - set({ floatBarShowCost: v })} - /> - Date: Sun, 12 Jul 2026 11:49:51 +0700 Subject: [PATCH 7/9] Document Providers tab restoration Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- ...2026-07-12-restore-providers-tab-design.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-12-restore-providers-tab-design.md diff --git a/docs/superpowers/specs/2026-07-12-restore-providers-tab-design.md b/docs/superpowers/specs/2026-07-12-restore-providers-tab-design.md new file mode 100644 index 0000000000..b699c790de --- /dev/null +++ b/docs/superpowers/specs/2026-07-12-restore-providers-tab-design.md @@ -0,0 +1,19 @@ +# Restore Providers Tab + +## Goal + +Restore provider management as a top-level Settings tab instead of embedding it below General settings. + +## Design + +- Add **Providers** immediately after **General** in the Settings tab bar. +- General contains only language, system, and automation settings and uses the normal settings-page scroll behavior. +- Providers retains the existing 600px split-pane layout, searchable provider sidebar, provider detail pane, and independent pane scrolling. +- Opening Settings with the `providers` route selects Providers directly. +- Other tabs, persisted settings, and provider behavior remain unchanged. + +## Verification + +- Add a focused Settings test proving General and Providers render as separate tab panels. +- Run frontend tests, type-check, locale parity, and the desktop build. +- Verify General, Providers, and provider-pane scrolling in the rebuilt app with CUA Driver. From 67e2a62a6d3c06036bb228e3937e95447fb8da13 Mon Sep 17 00:00:00 2001 From: NessZerra Date: Sun, 12 Jul 2026 11:55:41 +0700 Subject: [PATCH 8/9] Restore standalone Providers settings tab Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- apps/desktop-tauri/src/i18n/keys.ts | 1 + .../src/surfaces/Settings.test.ts | 9 +++++ apps/desktop-tauri/src/surfaces/Settings.tsx | 33 ++++++++++++------- apps/desktop-tauri/src/types/bridge.ts | 1 + rust/src/locale.rs | 1 + rust/src/locale/en-US.ftl | 1 + rust/src/locale/es-MX.ftl | 1 + rust/src/locale/ja-JP.ftl | 1 + rust/src/locale/ko-KR.ftl | 1 + rust/src/locale/zh-CN.ftl | 1 + rust/src/locale/zh-TW.ftl | 1 + 11 files changed, 39 insertions(+), 12 deletions(-) create mode 100644 apps/desktop-tauri/src/surfaces/Settings.test.ts diff --git a/apps/desktop-tauri/src/i18n/keys.ts b/apps/desktop-tauri/src/i18n/keys.ts index 51e029cf5b..fcea2e03e9 100644 --- a/apps/desktop-tauri/src/i18n/keys.ts +++ b/apps/desktop-tauri/src/i18n/keys.ts @@ -6,6 +6,7 @@ export const ALL_LOCALE_KEYS = [ "TabGeneral", + "TabProviders", "TabNotifications", "TabMenuBar", "TabMenu", diff --git a/apps/desktop-tauri/src/surfaces/Settings.test.ts b/apps/desktop-tauri/src/surfaces/Settings.test.ts new file mode 100644 index 0000000000..f3e575b7d9 --- /dev/null +++ b/apps/desktop-tauri/src/surfaces/Settings.test.ts @@ -0,0 +1,9 @@ +import { describe, expect, it } from "vitest"; +import source from "./Settings.tsx?raw"; + +describe("Settings navigation", () => { + it("routes providers separately from general", () => { + expect(source).toContain('{ id: "providers", labelKey: "TabProviders" }'); + expect(source).toMatch(/activeTab === "general"[\s\S]*? = { ), + providers: ( + + + + + + + ), notifications: ( @@ -91,6 +99,7 @@ const TabIcons: Record = { const TAB_META: { id: SettingsTab; labelKey: LocaleKey }[] = [ { id: "general", labelKey: "TabGeneral" }, + { id: "providers", labelKey: "TabProviders" }, { id: "notifications", labelKey: "TabNotifications" }, { id: "menuBar", labelKey: "TabMenuBar" }, { id: "menu", labelKey: "TabMenu" }, @@ -108,7 +117,7 @@ const SETTINGS_WINDOW_PROVIDERS_WIDTH = 600; async function applySettingsWindowSize(tab: SettingsTab) { const requestedWidth = - tab === "general" + tab === "providers" ? SETTINGS_WINDOW_PROVIDERS_WIDTH : SETTINGS_WINDOW_DEFAULT_WIDTH; const workArea = await getWorkAreaRect().catch(() => null); @@ -197,7 +206,7 @@ export default function Settings({ state, initialTab: propTab }: { state: Bootst return (
{/* custom title bar (decorations disabled for guaranteed dark theme) */}
@@ -248,17 +257,17 @@ export default function Settings({ state, initialTab: propTab }: { state: Bootst )} {/* tab panels */} -
+
{activeTab === "general" && ( - <> - - - + + )} + {activeTab === "providers" && ( + )} {activeTab === "notifications" && ( diff --git a/apps/desktop-tauri/src/types/bridge.ts b/apps/desktop-tauri/src/types/bridge.ts index ef7efba335..67e0258109 100644 --- a/apps/desktop-tauri/src/types/bridge.ts +++ b/apps/desktop-tauri/src/types/bridge.ts @@ -2,6 +2,7 @@ export type SurfaceMode = "hidden" | "trayPanel" | "popOut" | "settings"; export type VisibleSurfaceMode = Exclude; export type SettingsTabId = | "general" + | "providers" | "notifications" | "menuBar" | "menu" diff --git a/rust/src/locale.rs b/rust/src/locale.rs index 179330e8e4..4a696e876d 100644 --- a/rust/src/locale.rs +++ b/rust/src/locale.rs @@ -158,6 +158,7 @@ locale_keys! { // Tab names (Preferences) TabGeneral, + TabProviders, TabNotifications, TabMenuBar, TabMenu, diff --git a/rust/src/locale/en-US.ftl b/rust/src/locale/en-US.ftl index 54cc41cb3b..ebf9f7a8ac 100644 --- a/rust/src/locale/en-US.ftl +++ b/rust/src/locale/en-US.ftl @@ -1,4 +1,5 @@ TabGeneral = General +TabProviders = Providers TabNotifications = Notifications TabMenuBar = Menu Bar TabMenu = Menu diff --git a/rust/src/locale/es-MX.ftl b/rust/src/locale/es-MX.ftl index 8b062ab342..5cc203a715 100644 --- a/rust/src/locale/es-MX.ftl +++ b/rust/src/locale/es-MX.ftl @@ -1,4 +1,5 @@ TabGeneral = General +TabProviders = Proveedores TabNotifications = Notifications TabMenuBar = Menu Bar TabMenu = Menu diff --git a/rust/src/locale/ja-JP.ftl b/rust/src/locale/ja-JP.ftl index 47e0551aa1..26c7f1e7f4 100644 --- a/rust/src/locale/ja-JP.ftl +++ b/rust/src/locale/ja-JP.ftl @@ -1,4 +1,5 @@ TabGeneral = 一般 +TabProviders = プロバイダー TabNotifications = Notifications TabMenuBar = Menu Bar TabMenu = Menu diff --git a/rust/src/locale/ko-KR.ftl b/rust/src/locale/ko-KR.ftl index 42223154a3..1518a56011 100644 --- a/rust/src/locale/ko-KR.ftl +++ b/rust/src/locale/ko-KR.ftl @@ -1,4 +1,5 @@ TabGeneral = 일반 +TabProviders = 제공업체 TabNotifications = Notifications TabMenuBar = Menu Bar TabMenu = Menu diff --git a/rust/src/locale/zh-CN.ftl b/rust/src/locale/zh-CN.ftl index bfcf2e70c1..09a651f859 100644 --- a/rust/src/locale/zh-CN.ftl +++ b/rust/src/locale/zh-CN.ftl @@ -1,4 +1,5 @@ TabGeneral = 通用 +TabProviders = 服务商 TabNotifications = Notifications TabMenuBar = Menu Bar TabMenu = Menu diff --git a/rust/src/locale/zh-TW.ftl b/rust/src/locale/zh-TW.ftl index 5f4dbeb1ca..4656e39e7c 100644 --- a/rust/src/locale/zh-TW.ftl +++ b/rust/src/locale/zh-TW.ftl @@ -1,4 +1,5 @@ TabGeneral = 一般 +TabProviders = 提供者 TabNotifications = Notifications TabMenuBar = Menu Bar TabMenu = Menu From 025f9d9d0bed039a69d7119f5fd4d96833bfa772 Mon Sep 17 00:00:00 2001 From: NessZerra Date: Sun, 12 Jul 2026 12:04:58 +0700 Subject: [PATCH 9/9] Simplify final UI cleanup Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../src/floatbar/SettingsSection.test.ts | 8 --- .../src/floatbar/SettingsSection.test.tsx | 30 ++++++++ .../src/surfaces/Settings.test.ts | 10 +-- apps/desktop-tauri/src/surfaces/Settings.tsx | 2 +- rust/src/providers/claude/mod.rs | 71 ++----------------- 5 files changed, 41 insertions(+), 80 deletions(-) delete mode 100644 apps/desktop-tauri/src/floatbar/SettingsSection.test.ts create mode 100644 apps/desktop-tauri/src/floatbar/SettingsSection.test.tsx diff --git a/apps/desktop-tauri/src/floatbar/SettingsSection.test.ts b/apps/desktop-tauri/src/floatbar/SettingsSection.test.ts deleted file mode 100644 index 9b85803c4c..0000000000 --- a/apps/desktop-tauri/src/floatbar/SettingsSection.test.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { describe, expect, it } from "vitest"; -import source from "./SettingsSection.tsx?raw"; - -describe("FloatBar settings", () => { - it("renders one cost toggle", () => { - expect(source.match(/floatBarShowCost: v/g)).toHaveLength(1); - }); -}); diff --git a/apps/desktop-tauri/src/floatbar/SettingsSection.test.tsx b/apps/desktop-tauri/src/floatbar/SettingsSection.test.tsx new file mode 100644 index 0000000000..73b5bb842c --- /dev/null +++ b/apps/desktop-tauri/src/floatbar/SettingsSection.test.tsx @@ -0,0 +1,30 @@ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import type { SettingsSnapshot } from "../types/bridge"; +import FloatBarSettingsSection from "./SettingsSection"; + +vi.mock("../hooks/useLocale", () => ({ + useLocale: () => ({ t: (key: string) => key }), +})); + +const settings = { + floatBarEnabled: true, + floatBarOpacity: 90, + floatBarScale: 100, + floatBarOrientation: "horizontal", + floatBarStyle: "floating", + floatBarShowCost: false, + floatBarShowResetInline: false, + floatBarDarkText: false, + floatBarClickThrough: false, +} as unknown as SettingsSnapshot; + +describe("FloatBar settings", () => { + it("renders one cost toggle", () => { + render( + , + ); + + expect(screen.getAllByText("FloatBarShowCost")).toHaveLength(1); + }); +}); diff --git a/apps/desktop-tauri/src/surfaces/Settings.test.ts b/apps/desktop-tauri/src/surfaces/Settings.test.ts index f3e575b7d9..4a06728369 100644 --- a/apps/desktop-tauri/src/surfaces/Settings.test.ts +++ b/apps/desktop-tauri/src/surfaces/Settings.test.ts @@ -1,9 +1,11 @@ import { describe, expect, it } from "vitest"; -import source from "./Settings.tsx?raw"; +import { TAB_META } from "./Settings"; describe("Settings navigation", () => { - it("routes providers separately from general", () => { - expect(source).toContain('{ id: "providers", labelKey: "TabProviders" }'); - expect(source).toMatch(/activeTab === "general"[\s\S]*? { + expect(TAB_META.slice(0, 2)).toEqual([ + { id: "general", labelKey: "TabGeneral" }, + { id: "providers", labelKey: "TabProviders" }, + ]); }); }); diff --git a/apps/desktop-tauri/src/surfaces/Settings.tsx b/apps/desktop-tauri/src/surfaces/Settings.tsx index 90b3d4a61b..daf9001cb4 100644 --- a/apps/desktop-tauri/src/surfaces/Settings.tsx +++ b/apps/desktop-tauri/src/surfaces/Settings.tsx @@ -97,7 +97,7 @@ const TabIcons: Record = { ), }; -const TAB_META: { id: SettingsTab; labelKey: LocaleKey }[] = [ +export const TAB_META: { id: SettingsTab; labelKey: LocaleKey }[] = [ { id: "general", labelKey: "TabGeneral" }, { id: "providers", labelKey: "TabProviders" }, { id: "notifications", labelKey: "TabNotifications" }, diff --git a/rust/src/providers/claude/mod.rs b/rust/src/providers/claude/mod.rs index 6fae657cf2..62ed245965 100755 --- a/rust/src/providers/claude/mod.rs +++ b/rust/src/providers/claude/mod.rs @@ -7,9 +7,8 @@ mod scoped_weekly; mod web_api; use async_trait::async_trait; -use chrono::{DateTime, Utc}; +use chrono::Utc; use regex_lite::Regex; -use serde::Deserialize; #[cfg(windows)] use std::os::windows::process::CommandExt; #[cfg(windows)] @@ -17,8 +16,8 @@ use std::process::{Command as StdCommand, Stdio}; use crate::cli::tty_runner::{TtyCommandOptions, TtyCommandRunner}; use crate::core::{ - FetchContext, NamedRateWindow, Provider, ProviderError, ProviderFetchResult, ProviderId, - ProviderMetadata, RateWindow, SourceMode, UsageSnapshot, + FetchContext, Provider, ProviderError, ProviderFetchResult, ProviderId, ProviderMetadata, + RateWindow, SourceMode, UsageSnapshot, }; use admin_api::ClaudeAdminApiFetcher; @@ -26,73 +25,11 @@ use admin_api::ClaudeAdminApiFetcher; use cli_reset::parse_claude_reset_date_in_system_zone; use cli_reset::{ extract_cli_scoped_weekly_limits, normalized_for_label_search, parse_claude_reset_date, - parse_percent_line, slug_claude_model, starts_next_usage_section, + parse_percent_line, starts_next_usage_section, }; pub use oauth::ClaudeOAuthFetcher; pub use web_api::ClaudeWebApiFetcher; -#[derive(Debug, Deserialize)] -struct ScopedWeeklyLimit { - kind: Option, - group: Option, - percent: Option, - #[serde(alias = "resetsAt")] - resets_at: Option, - scope: Option, -} - -#[derive(Debug, Deserialize)] -struct ScopedWeeklyScope { - model: Option, -} - -#[derive(Debug, Deserialize)] -struct ScopedWeeklyModel { - id: Option, - #[serde(alias = "displayName")] - display_name: Option, -} - -fn scoped_weekly_windows(limits: &[ScopedWeeklyLimit]) -> Vec { - let mut seen = std::collections::HashSet::new(); - limits - .iter() - .filter_map(|limit| { - if limit.kind.as_deref() != Some("weekly_scoped") - || limit.group.as_deref() != Some("weekly") - { - return None; - } - let percent = limit.percent.filter(|value| value.is_finite())?; - let model = limit.scope.as_ref()?.model.as_ref()?; - let title = model.display_name.as_deref()?.trim(); - if title.is_empty() { - return None; - } - let identity = model - .id - .as_deref() - .map(str::trim) - .filter(|value| !value.is_empty()) - .unwrap_or(title); - let slug = slug_claude_model(identity); - if slug.is_empty() || !seen.insert(slug.clone()) { - return None; - } - let resets_at = limit - .resets_at - .as_deref() - .and_then(|value| DateTime::parse_from_rfc3339(value).ok()) - .map(|value| value.with_timezone(&Utc)); - Some(NamedRateWindow::new( - format!("claude-weekly-scoped-{slug}"), - format!("{title} only"), - RateWindow::with_details(percent, Some(10080), resets_at, None), - )) - }) - .collect() -} - /// Claude provider implementation pub struct ClaudeProvider { metadata: ProviderMetadata,