diff --git a/apps/desktop-tauri/src/components/CodexAccountsMenu.tsx b/apps/desktop-tauri/src/components/CodexAccountsMenu.tsx index b216c8f325..fdfc1cdbec 100644 --- a/apps/desktop-tauri/src/components/CodexAccountsMenu.tsx +++ b/apps/desktop-tauri/src/components/CodexAccountsMenu.tsx @@ -27,9 +27,11 @@ import { export default function CodexAccountsMenu({ hideEmail, resetTimeRelative, + onLayoutChange, }: { hideEmail: boolean; resetTimeRelative: boolean; + onLayoutChange?: () => void; }) { const { t } = useLocale(); const [accounts, setAccounts] = useState([]); @@ -59,6 +61,10 @@ export default function CodexAccountsMenu({ void load(); }, [load]); + useEffect(() => { + onLayoutChange?.(); + }, [accounts.length, error, onLayoutChange]); + useEffect(() => { let cancelled = false; const unlistenPromise = listen("codex-accounts-updated", () => { @@ -95,7 +101,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 4a34f057a6..f300e70069 100644 --- a/apps/desktop-tauri/src/components/MenuCard.test.tsx +++ b/apps/desktop-tauri/src/components/MenuCard.test.tsx @@ -7,6 +7,7 @@ const tauriMocks = vi.hoisted(() => ({ getDeepSeekPricingStatus: vi.fn(), getLocaleStrings: vi.fn(), setUiLanguage: vi.fn(), + claudeAccountsList: vi.fn(), })); const eventMocks = vi.hoisted(() => ({ @@ -111,6 +112,7 @@ function renderCard( describe("MenuCard", () => { beforeEach(() => { vi.clearAllMocks(); + tauriMocks.claudeAccountsList.mockResolvedValue([]); tauriMocks.getLocaleStrings.mockResolvedValue( buildBundle({ ActionCopyError: "Copy error", @@ -558,6 +560,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 () => { @@ -576,12 +595,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(() => { @@ -599,12 +619,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 a2591e19d4..69ec4e0355 100644 --- a/apps/desktop-tauri/src/components/MenuCard.tsx +++ b/apps/desktop-tauri/src/components/MenuCard.tsx @@ -283,6 +283,17 @@ export default function MenuCard({ )} + {provider.providerId === "codex" && ( + + )} + {provider.providerId === "claude" && ( + + )} + {hasDetails &&
} {hasDetails && ( @@ -329,15 +340,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 e445991a8a..4d0b47d24d 100644 --- a/apps/desktop-tauri/src/components/MenuCardDetails.tsx +++ b/apps/desktop-tauri/src/components/MenuCardDetails.tsx @@ -379,7 +379,7 @@ function MetricRow({ {t("PanelOnPaceBudget")} {reserveDescription && {reserveDescription}} -
+ {expanded &&
{[ [t("PanelNow"), paceView.budget.now], [t("PanelOneHour"), paceView.budget.nextHour], @@ -390,7 +390,7 @@ function MetricRow({ {label} {formatBudget(Number(value))}% ))} -
+
} {expanded && }
)} @@ -542,14 +542,6 @@ export default function MenuCardDetails({ {wayfinderUsage && } - {localUsage && ( - - )} - {hasMetrics && hasCost &&
} {hasCost && provider.cost && ( @@ -626,83 +618,96 @@ export default function MenuCardDetails({ )} - {(hasMetrics || hasCost) && hasPace &&
} - - {paceEnabled && hasPace && provider.pace && ( -
-
- {t("DetailPaceTitle")} - - {t(paceStageKey(provider.pace.stage))} ( - {provider.pace.deltaPercent >= 0 ? "+" : ""} - {provider.pace.deltaPercent.toFixed(1)}%) - -
-
-
-
-
-
-
+ {t("PanelUsageDetails")} +
+ {localUsage && ( + -
+ )} + + {paceEnabled && 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 dfccba100c..602b7aaf35 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/main.tsx b/apps/desktop-tauri/src/main.tsx index 5b258d4f15..d5ac7f7057 100644 --- a/apps/desktop-tauri/src/main.tsx +++ b/apps/desktop-tauri/src/main.tsx @@ -3,6 +3,7 @@ import ReactDOM from "react-dom/client"; import App from "./App"; import "./styles.css"; import "./surfaces/settings/settings-layout.css"; +import "./surfaces/menu-surface-layout.css"; ReactDOM.createRoot(document.getElementById("root")!).render( diff --git a/apps/desktop-tauri/src/styles.css b/apps/desktop-tauri/src/styles.css index 6c4eb97406..84272845f1 100644 --- a/apps/desktop-tauri/src/styles.css +++ b/apps/desktop-tauri/src/styles.css @@ -3786,17 +3786,7 @@ html:has(.menu-surface--tray) { margin-top: 0; font-size: 10px; } -/* Hide pace section in tray — macOS doesn't show pace in the compact popover */ -.menu-surface--tray .menu-card__pace, -.menu-surface--tray .menu-card__pace + .menu-card__divider, -.menu-surface--tray .menu-card__content > .menu-card__divider:has(+ .menu-card__pace) { - display: none; -} -/* Hide large charts in tray; keep the compact local token/cost summary visible. */ -.menu-surface--tray .menu-card__charts, -.menu-surface--tray .menu-card__local-chart { - display: none; -} +/* Secondary charts and pace are available inside the Usage details disclosure. */ .menu-surface--tray .menu-card__local-grid { gap: 9px 20px; } @@ -6024,6 +6014,7 @@ button, input, select, textarea { font-family: var(--font-ui); } + .select, .number-input, .text-input { min-height: 28px; } diff --git a/apps/desktop-tauri/src/surfaces/menu-surface-layout.css b/apps/desktop-tauri/src/surfaces/menu-surface-layout.css new file mode 100644 index 0000000000..e79ac3293a --- /dev/null +++ b/apps/desktop-tauri/src/surfaces/menu-surface-layout.css @@ -0,0 +1,173 @@ +/* Tray/popout account and usage-card layout. */ +.menu-surface { + --surface-inset: 16px; + line-height: 1.4; +} + +:is(.menu-surface--tray, .menu-surface--popout) .menu-card { + --menu-card-x: var(--surface-inset); + --menu-card-header-only-y: 12px; + --menu-card-section-y: 12px; + gap: 12px; +} + +:is(.menu-surface--tray, .menu-surface--popout) .menu-card__name-group { + flex-direction: row; + align-items: baseline; + justify-content: space-between; + gap: 12px; +} + +:is(.menu-surface--tray, .menu-surface--popout) .menu-card__name { + font-size: 14px; + font-weight: 600; + letter-spacing: 0; +} + +:is(.menu-surface--tray, .menu-surface--popout) .menu-card__email { + max-width: 60%; + text-align: right; + font-size: 11px; +} + +:is(.menu-surface--tray, .menu-surface--popout) .menu-card__subtitle, +:is(.menu-surface--tray, .menu-surface--popout) .menu-card__plan-badge { + font-size: 11px; + font-weight: 400; + background: transparent; + padding: 0; +} + +:is(.menu-surface--tray, .menu-surface--popout) .menu-card__content, +.menu-card__metrics, +.menu-card__more-content { + display: flex; + flex-direction: column; + gap: 16px; + padding-bottom: 0; +} + +:is(.menu-surface--tray, .menu-surface--popout) .menu-card__divider { + margin: 0; +} + +:is(.menu-surface--tray, .menu-surface--popout) .menu-metric { + gap: 6px; +} + +:is(.menu-surface--tray, .menu-surface--popout) .menu-metric__title { + font-size: 13px; + font-weight: 600; +} + +:is(.menu-surface--tray, .menu-surface--popout) .menu-metric__pct, +:is(.menu-surface--tray, .menu-surface--popout) .menu-metric__reset { + font-size: 11px; + line-height: 1.4; +} + +.menu-metric__budget-header { + color: var(--text-secondary); + padding: 2px 0; + justify-content: flex-start; + gap: 6px; +} +.menu-metric__budget-header > span + span { + margin-left: auto; +} +.menu-metric__budget-header::before { + content: "›"; + margin-right: 4px; +} +.menu-metric__budget-header[aria-expanded="true"]::before { + transform: rotate(90deg); +} + +.codex-menu-accounts { + margin: 0; + padding: 0; + border: none; +} +.codex-menu-accounts__summary, +.menu-card__more > summary { + min-height: 28px; + font-size: 12px; + font-weight: 400; + text-transform: none; + letter-spacing: 0; + color: var(--text-secondary); + cursor: pointer; +} +.codex-menu-accounts__summary:hover, +.menu-card__more > summary:hover { + color: var(--text-primary); +} +.codex-menu-accounts__list { + margin-top: 4px; + gap: 4px; +} +.codex-menu-accounts__row { + min-height: 36px; + padding: 4px 0; +} +.codex-menu-accounts__email { + font-size: 12px; + flex-wrap: wrap; + overflow-wrap: anywhere; + white-space: normal; +} +.codex-menu-accounts__badge { + font-size: 10px; + font-weight: 500; + text-transform: none; + letter-spacing: 0; + color: var(--text-secondary); +} +.codex-menu-accounts__switch { + min-height: 28px; + padding: 3px 10px; + font-size: 11px; + font-weight: 500; +} +.menu-card__more { + border-top: 1px solid var(--panel-border); + padding-top: 8px; +} +.menu-card__more > summary { + line-height: 28px; +} +.menu-card__more-content { + padding-top: 12px; +} + +:is(.menu-surface--tray, .menu-surface--popout) .menu-stack__item, +:is(.menu-surface--tray, .menu-surface--popout) .menu-stack__item--selected { + background: transparent; + border: 0; + border-radius: 0; + padding: 0; +} +:is(.menu-surface--tray, .menu-surface--popout) .menu-stack__sep { + height: 1px; + margin: 4px var(--surface-inset); + background: var(--panel-border); +} +:is(.menu-surface--tray, .menu-surface--popout) .provider-grid { + padding: 8px var(--surface-inset); + gap: 8px; +} +:is(.menu-surface--tray, .menu-surface--popout) .provider-grid__item { + min-height: 32px; +} +:is(.menu-surface--tray, .menu-surface--popout) .menu-surface__footer-row { + min-height: 28px; + font-size: 12px; + padding-top: 5px; + padding-bottom: 5px; +} +.context-actions { + padding: 0 var(--surface-inset); +} +.context-actions__btn { + min-height: 28px; +} diff --git a/apps/desktop-tauri/src/surfaces/settings/providers/ProviderDetailPane.tsx b/apps/desktop-tauri/src/surfaces/settings/providers/ProviderDetailPane.tsx index fed2273fa2..bffe6217ff 100644 --- a/apps/desktop-tauri/src/surfaces/settings/providers/ProviderDetailPane.tsx +++ b/apps/desktop-tauri/src/surfaces/settings/providers/ProviderDetailPane.tsx @@ -33,8 +33,8 @@ import { UsageSourceSection } from "./sections/UsageSourceSection"; import { shouldShowCookieSource } from "./sections/usageSourcePolicy"; 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"; @@ -267,6 +267,9 @@ export function ProviderDetailPane({
+ {detail.id === "codex" && } + {detail.id === "claude" && } + {detail.lastError && ( )} @@ -336,8 +339,6 @@ export function ProviderDetailPane({ /> {detail.id === "codex" && } - {detail.id === "codex" && } - {detail.id === "claude" && }