From 77877ca0a8b53102e58701e57c390d9cf67a70b4 Mon Sep 17 00:00:00 2001 From: xuelongmu Date: Tue, 8 Sep 2026 00:12:42 -0400 Subject: [PATCH] Keep account switching visible and simplify usage cards --- .../src/components/CodexAccountsMenu.tsx | 8 +- .../src/components/MenuCard.test.tsx | 36 +++- .../desktop-tauri/src/components/MenuCard.tsx | 20 +- .../src/components/MenuCardDetails.tsx | 175 +++++++++--------- apps/desktop-tauri/src/i18n/keys.ts | 1 + .../settings/providers/ProviderDetailPane.tsx | 7 +- rust/src/locale.rs | 1 + rust/src/locale/en-US.ftl | 3 +- 8 files changed, 144 insertions(+), 107 deletions(-) diff --git a/apps/desktop-tauri/src/components/CodexAccountsMenu.tsx b/apps/desktop-tauri/src/components/CodexAccountsMenu.tsx index 643d69ee00..868d5727a3 100644 --- a/apps/desktop-tauri/src/components/CodexAccountsMenu.tsx +++ b/apps/desktop-tauri/src/components/CodexAccountsMenu.tsx @@ -26,9 +26,11 @@ import { export default function CodexAccountsMenu({ hideEmail, resetTimeRelative, + onLayoutChange, }: { hideEmail: boolean; resetTimeRelative: boolean; + onLayoutChange?: () => void; }) { const { t } = useLocale(); const [accounts, setAccounts] = useState([]); @@ -56,6 +58,10 @@ export default function CodexAccountsMenu({ void load(); }, [load]); + useEffect(() => { + onLayoutChange?.(); + }, [accounts.length, error, onLayoutChange]); + useEffect(() => { let cancelled = false; const unlistenPromise = listen("codex-accounts-updated", () => { @@ -87,7 +93,7 @@ export default function CodexAccountsMenu({ } return ( -
+
{t("CodexAccountsTitle")} {accounts.length} diff --git a/apps/desktop-tauri/src/components/MenuCard.test.tsx b/apps/desktop-tauri/src/components/MenuCard.test.tsx index c7838cf8e6..e199df15d0 100644 --- a/apps/desktop-tauri/src/components/MenuCard.test.tsx +++ b/apps/desktop-tauri/src/components/MenuCard.test.tsx @@ -6,6 +6,7 @@ const tauriMocks = vi.hoisted(() => ({ getDeepSeekPricingStatus: vi.fn(), getLocaleStrings: vi.fn(), setUiLanguage: vi.fn(), + claudeAccountsList: vi.fn(), })); const eventMocks = vi.hoisted(() => ({ @@ -108,6 +109,7 @@ function renderCard( describe("MenuCard", () => { beforeEach(() => { vi.clearAllMocks(); + tauriMocks.claudeAccountsList.mockResolvedValue([]); tauriMocks.getLocaleStrings.mockResolvedValue( buildBundle({ ActionCopyError: "Copy error", @@ -465,6 +467,23 @@ describe("MenuCard", () => { expect(screen.getByText("30d tokens")).toBeInTheDocument(); expect(screen.getByText("584K")).toBeInTheDocument(); expect(screen.getByText("Estimated from local logs")).toBeInTheDocument(); + const details = container.querySelector(".menu-card__more")!; + expect(details.open).toBe(false); + fireEvent.click(details.querySelector("summary")!); + expect(details.open).toBe(true); + }); + + it("places Claude accounts above metrics and the collapsed usage details", async () => { + tauriMocks.claudeAccountsList.mockResolvedValue([ + { id: "a", email: "a@example.com", organization: "Personal", isActive: true, isSaved: true }, + { id: "b", email: "b@example.com", organization: "Work", isActive: false, isSaved: true }, + ]); + const { container } = renderCard(provider(null)); + await screen.findByText("ClaudeAccountsTitle"); + const accounts = container.querySelector(".codex-menu-accounts")!; + const metrics = container.querySelector(".menu-card__metrics")!; + expect(accounts.compareDocumentPosition(metrics) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy(); + expect(container.querySelector(".menu-card__more")?.open).toBe(false); }); it("shows on-pace budgets and expands projection details", async () => { @@ -483,12 +502,13 @@ describe("MenuCard", () => { renderCard(snapshot, { onLayoutChange }); const toggle = await screen.findByRole("button", { name: /On-pace budget/ }); - expect(screen.getByText("now 20%")).toBeInTheDocument(); - expect(screen.getByText("1h 21%")).toBeInTheDocument(); + expect(screen.queryByText("now 20%")).not.toBeInTheDocument(); expect(screen.queryByRole("img", { name: /PaceChartAriaLabel/i })).not.toBeInTheDocument(); fireEvent.click(toggle); + expect(screen.getByText("now 20%")).toBeInTheDocument(); + expect(screen.getByText("1h 21%")).toBeInTheDocument(); expect(toggle).toHaveAttribute("aria-expanded", "true"); expect(screen.getByRole("img", { name: /PaceChartAriaLabel/i })).toBeInTheDocument(); await waitFor(() => { @@ -506,12 +526,12 @@ describe("MenuCard", () => { renderCard(snapshot); - expect( - await screen.findByRole("button", { name: /On-pace budget/ }), - ).toBeInTheDocument(); - expect(screen.getByText("now 0%")).toBeInTheDocument(); - expect(screen.queryByText(/in reserve/)).not.toBeInTheDocument(); - expect(screen.queryByText("Lasts until reset")).not.toBeInTheDocument(); + const toggle = await screen.findByRole("button", { name: /On-pace budget/ }); + expect(screen.queryByText("now 0%")).not.toBeInTheDocument(); + fireEvent.click(toggle); + expect(screen.getByText("now 0%")).toBeInTheDocument(); + expect(screen.queryByText(/in reserve/)).not.toBeInTheDocument(); + expect(screen.queryByText("Lasts until reset")).not.toBeInTheDocument(); }); it("does not show pace budgets for a five-hour session window", async () => { diff --git a/apps/desktop-tauri/src/components/MenuCard.tsx b/apps/desktop-tauri/src/components/MenuCard.tsx index 77cfe62044..5bd43ae71d 100644 --- a/apps/desktop-tauri/src/components/MenuCard.tsx +++ b/apps/desktop-tauri/src/components/MenuCard.tsx @@ -275,6 +275,17 @@ export default function MenuCard({ )} + {provider.providerId === "codex" && ( + + )} + {provider.providerId === "claude" && ( + + )} + {hasDetails &&
} {hasDetails && ( @@ -321,15 +332,6 @@ export default function MenuCard({ )} - {provider.providerId === "codex" && ( - - )} - {provider.providerId === "claude" && ( - - )} ); } diff --git a/apps/desktop-tauri/src/components/MenuCardDetails.tsx b/apps/desktop-tauri/src/components/MenuCardDetails.tsx index 4ef58e3d1c..a707de0806 100644 --- a/apps/desktop-tauri/src/components/MenuCardDetails.tsx +++ b/apps/desktop-tauri/src/components/MenuCardDetails.tsx @@ -376,7 +376,7 @@ function MetricRow({ {t("PanelOnPaceBudget")} {reserveDescription && {reserveDescription}} -
+ {expanded &&
{[ [t("PanelNow"), paceView.budget.now], [t("PanelOneHour"), paceView.budget.nextHour], @@ -387,7 +387,7 @@ function MetricRow({ {label} {formatBudget(Number(value))}% ))} -
+
} {expanded && }
)} @@ -530,14 +530,6 @@ export default function MenuCardDetails({ {wayfinderUsage && } - {localUsage && ( - - )} - {hasMetrics && hasCost && costStyle !== "hidden" &&
} {provider.cost && costStyle !== "hidden" && ( @@ -612,83 +604,96 @@ export default function MenuCardDetails({ )} - {(hasMetrics || hasCost) && hasPace &&
} - - {hasPace && provider.pace && ( -
-
- {t("DetailPaceTitle")} - - {t(paceStageKey(provider.pace.stage))} ( - {provider.pace.deltaPercent >= 0 ? "+" : ""} - {provider.pace.deltaPercent.toFixed(1)}%) - -
-
-
-
-
-
-
+ {t("PanelUsageDetails")} +
+ {localUsage && ( + -
+ )} + + {hasPace && provider.pace && ( +
+
+ {t("DetailPaceTitle")} + + {t(paceStageKey(provider.pace.stage))} ( + {provider.pace.deltaPercent >= 0 ? "+" : ""} + {provider.pace.deltaPercent.toFixed(1)}%) + +
+
+
+
+
+
+
+
+
+ {provider.pace.etaSeconds != null && !provider.pace.willLastToReset && ( +
+ ⚠{" "} + {t("DetailPaceRunsOutIn")} {formatEta(provider.pace.etaSeconds)} +
+ )} + {provider.pace.willLastToReset && ( +
+ ✓ {t("DetailPaceWillLastToReset")} +
+ )} +
+ )} + + {(hasMetrics || hasCost || hasPace) && hasCharts && ( +
+ )} + + {hasCharts && ( +
+ {hasCostHistory && ( + `$${v.toFixed(2)}`} + t={t} + /> + )} + {hasCreditsHistory && ( + v.toFixed(1)} + t={t} + /> + )} + {hasUsageBreakdown && ( + + )} +
+ )}
- {provider.pace.etaSeconds != null && !provider.pace.willLastToReset && ( -
- ⚠{" "} - {t("DetailPaceRunsOutIn")} {formatEta(provider.pace.etaSeconds)} -
- )} - {provider.pace.willLastToReset && ( -
- ✓ {t("DetailPaceWillLastToReset")} -
- )} -
- )} - - {(hasMetrics || hasCost || hasPace) && hasCharts && ( -
- )} - - {hasCharts && ( -
- {hasCostHistory && ( - `$${v.toFixed(2)}`} - t={t} - /> - )} - {hasCreditsHistory && ( - v.toFixed(1)} - t={t} - /> - )} - {hasUsageBreakdown && ( - - )} -
+
)} ); diff --git a/apps/desktop-tauri/src/i18n/keys.ts b/apps/desktop-tauri/src/i18n/keys.ts index 83e3a22d2a..3d83892bf5 100644 --- a/apps/desktop-tauri/src/i18n/keys.ts +++ b/apps/desktop-tauri/src/i18n/keys.ts @@ -76,6 +76,7 @@ export const ALL_LOCALE_KEYS = [ "ProviderDisabled", "ProviderInfo", "ProviderUsage", + "PanelUsageDetails", "AuthType", "DataSource", "ProviderNotDetected", diff --git a/apps/desktop-tauri/src/surfaces/settings/providers/ProviderDetailPane.tsx b/apps/desktop-tauri/src/surfaces/settings/providers/ProviderDetailPane.tsx index 2893cb15ac..86b33ce438 100644 --- a/apps/desktop-tauri/src/surfaces/settings/providers/ProviderDetailPane.tsx +++ b/apps/desktop-tauri/src/surfaces/settings/providers/ProviderDetailPane.tsx @@ -31,8 +31,8 @@ import { CookieSourceSection } from "./sections/CookieSourceSection"; import { GrokUsageSourceSection } from "./sections/GrokUsageSourceSection"; import { RegionSection } from "./sections/RegionSection"; import { CodexUsageOptions } from "./sections/credentials/CodexUsageOptions"; -import { ClaudeAccountsSection } from "./sections/credentials/ClaudeAccountsSection"; import { CodexAccountsSection } from "./sections/credentials/CodexAccountsSection"; +import { ClaudeAccountsSection } from "./sections/credentials/ClaudeAccountsSection"; import { TokenAccountsPanel } from "../tokens/TokenAccountsPanel"; import { ApiKeySection } from "./ApiKeySection"; import { CookieSection } from "./CookieSection"; @@ -265,6 +265,9 @@ export function ProviderDetailPane({
+ {detail.id === "codex" && } + {detail.id === "claude" && } + {detail.lastError && ( )} @@ -325,8 +328,6 @@ export function ProviderDetailPane({ /> {detail.id === "codex" && } - {detail.id === "codex" && } - {detail.id === "claude" && }