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
8 changes: 7 additions & 1 deletion apps/desktop-tauri/src/components/CodexAccountsMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<CodexAccount[]>([]);
Expand Down Expand Up @@ -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", () => {
Expand Down Expand Up @@ -95,7 +101,7 @@ export default function CodexAccountsMenu({
);

return (
<details className="codex-menu-accounts">
<details className="codex-menu-accounts" onToggle={onLayoutChange}>
<summary className="codex-menu-accounts__summary">
<span className="codex-menu-accounts__title">{t("CodexAccountsTitle")}</span>
<span className="codex-menu-accounts__count">{accounts.length}</span>
Expand Down
36 changes: 28 additions & 8 deletions apps/desktop-tauri/src/components/MenuCard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const tauriMocks = vi.hoisted(() => ({
getDeepSeekPricingStatus: vi.fn(),
getLocaleStrings: vi.fn(),
setUiLanguage: vi.fn(),
claudeAccountsList: vi.fn(),
}));

const eventMocks = vi.hoisted(() => ({
Expand Down Expand Up @@ -111,6 +112,7 @@ function renderCard(
describe("MenuCard", () => {
beforeEach(() => {
vi.clearAllMocks();
tauriMocks.claudeAccountsList.mockResolvedValue([]);
tauriMocks.getLocaleStrings.mockResolvedValue(
buildBundle({
ActionCopyError: "Copy error",
Expand Down Expand Up @@ -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<HTMLDetailsElement>(".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<HTMLDetailsElement>(".menu-card__more")?.open).toBe(false);
});

it("shows on-pace budgets and expands projection details", async () => {
Expand All @@ -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(() => {
Expand All @@ -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 () => {
Expand Down
20 changes: 11 additions & 9 deletions apps/desktop-tauri/src/components/MenuCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,17 @@ export default function MenuCard({
)}
</header>

{provider.providerId === "codex" && (
<CodexAccountsMenu
hideEmail={hideEmail}
resetTimeRelative={resetTimeRelative}
onLayoutChange={onLayoutChange}
/>
)}
{provider.providerId === "claude" && (
<ClaudeAccountsMenu hideEmail={hideEmail} onLayoutChange={onLayoutChange} />
)}

{hasDetails && <div className="menu-card__divider" />}

{hasDetails && (
Expand Down Expand Up @@ -329,15 +340,6 @@ export default function MenuCard({
</section>
)}

{provider.providerId === "codex" && (
<CodexAccountsMenu
hideEmail={hideEmail}
resetTimeRelative={resetTimeRelative}
/>
)}
{provider.providerId === "claude" && (
<ClaudeAccountsMenu hideEmail={hideEmail} />
)}
</article>
);
}
175 changes: 90 additions & 85 deletions apps/desktop-tauri/src/components/MenuCardDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,7 @@ function MetricRow({
<span>{t("PanelOnPaceBudget")}</span>
{reserveDescription && <span>{reserveDescription}</span>}
</button>
<div className="menu-metric__budget-pills">
{expanded && <div className="menu-metric__budget-pills">
{[
[t("PanelNow"), paceView.budget.now],
[t("PanelOneHour"), paceView.budget.nextHour],
Expand All @@ -390,7 +390,7 @@ function MetricRow({
{label} {formatBudget(Number(value))}%
</span>
))}
</div>
</div>}
{expanded && <PaceDetailsChart snap={snap} t={t} />}
</div>
)}
Expand Down Expand Up @@ -542,14 +542,6 @@ export default function MenuCardDetails({

{wayfinderUsage && <WayfinderUsageBlock usage={wayfinderUsage} />}

{localUsage && (
<LocalUsageBlock
providerId={provider.providerId}
summary={localUsage}
costHistory={localCostHistory}
/>
)}

{hasMetrics && hasCost && <div className="menu-card__divider" />}

{hasCost && provider.cost && (
Expand Down Expand Up @@ -626,83 +618,96 @@ export default function MenuCardDetails({
</section>
)}

{(hasMetrics || hasCost) && hasPace && <div className="menu-card__divider" />}

{paceEnabled && hasPace && provider.pace && (
<section className="menu-card__group menu-card__pace">
<div className="menu-card__pace-header">
<span className="menu-card__group-title">{t("DetailPaceTitle")}</span>
<span
className="menu-card__pace-label"
data-pace={paceCategory(provider.pace.stage)}
>
{t(paceStageKey(provider.pace.stage))} (
{provider.pace.deltaPercent >= 0 ? "+" : ""}
{provider.pace.deltaPercent.toFixed(1)}%)
</span>
</div>
<div className="menu-card__pace-bars">
<div className="menu-card__pace-track" title={t("PanelExpected")}>
<div
className="menu-card__pace-fill menu-card__pace-fill--expected"
style={{ width: `${provider.pace.expectedUsedPercent.toFixed(1)}%` }}
/>
</div>
<div className="menu-card__pace-track" title={t("PanelActual")}>
<div
className="menu-card__pace-fill"
data-pace={paceCategory(provider.pace.stage)}
style={{ width: `${provider.pace.actualUsedPercent.toFixed(1)}%` }}
{(localUsage || hasPace || hasCharts) && (
<details className="menu-card__more" onToggle={onLayoutChange}>
<summary>{t("PanelUsageDetails")}</summary>
<div className="menu-card__more-content">
{localUsage && (
<LocalUsageBlock
providerId={provider.providerId}
summary={localUsage}
costHistory={localCostHistory}
/>
</div>
)}

{paceEnabled && hasPace && provider.pace && (
<section className="menu-card__group menu-card__pace">
<div className="menu-card__pace-header">
<span className="menu-card__group-title">{t("DetailPaceTitle")}</span>
<span
className="menu-card__pace-label"
data-pace={paceCategory(provider.pace.stage)}
>
{t(paceStageKey(provider.pace.stage))} (
{provider.pace.deltaPercent >= 0 ? "+" : ""}
{provider.pace.deltaPercent.toFixed(1)}%)
</span>
</div>
<div className="menu-card__pace-bars">
<div className="menu-card__pace-track" title={t("PanelExpected")}>
<div
className="menu-card__pace-fill menu-card__pace-fill--expected"
style={{ width: `${provider.pace.expectedUsedPercent.toFixed(1)}%` }}
/>
</div>
<div className="menu-card__pace-track" title={t("PanelActual")}>
<div
className="menu-card__pace-fill"
data-pace={paceCategory(provider.pace.stage)}
style={{ width: `${provider.pace.actualUsedPercent.toFixed(1)}%` }}
/>
</div>
</div>
{provider.pace.etaSeconds != null && !provider.pace.willLastToReset && (
<div className="menu-card__pace-eta">
⚠{" "}
{t("DetailPaceRunsOutIn")} {formatEta(provider.pace.etaSeconds)}
</div>
)}
{provider.pace.willLastToReset && (
<div className="menu-card__pace-ok">
✓ {t("DetailPaceWillLastToReset")}
</div>
)}
</section>
)}

{(hasMetrics || hasCost || hasPace) && hasCharts && (
<div className="menu-card__divider" />
)}

{hasCharts && (
<section className="menu-card__group menu-card__charts">
{hasCostHistory && (
<SimpleBarChart
points={chartData!.costHistory}
label={t("DetailChartCost")}
color="var(--provider-accent, var(--accent))"
formatValue={(v) => `$${v.toFixed(2)}`}
t={t}
/>
)}
{hasCreditsHistory && (
<SimpleBarChart
points={chartData!.creditsHistory}
label={t("DetailChartCredits")}
color="var(--provider-status-ok)"
formatValue={(v) => v.toFixed(1)}
t={t}
/>
)}
{hasUsageBreakdown && (
<StackedBarChart
points={chartData!.usageBreakdown}
label={t("DetailChartUsageBreakdown")}
height={56}
t={t}
/>
)}
</section>
)}
</div>
{provider.pace.etaSeconds != null && !provider.pace.willLastToReset && (
<div className="menu-card__pace-eta">
⚠{" "}
{t("DetailPaceRunsOutIn")} {formatEta(provider.pace.etaSeconds)}
</div>
)}
{provider.pace.willLastToReset && (
<div className="menu-card__pace-ok">
✓ {t("DetailPaceWillLastToReset")}
</div>
)}
</section>
)}

{(hasMetrics || hasCost || hasPace) && hasCharts && (
<div className="menu-card__divider" />
)}

{hasCharts && (
<section className="menu-card__group menu-card__charts">
{hasCostHistory && (
<SimpleBarChart
points={chartData!.costHistory}
label={t("DetailChartCost")}
color="var(--provider-accent, var(--accent))"
formatValue={(v) => `$${v.toFixed(2)}`}
t={t}
/>
)}
{hasCreditsHistory && (
<SimpleBarChart
points={chartData!.creditsHistory}
label={t("DetailChartCredits")}
color="var(--provider-status-ok)"
formatValue={(v) => v.toFixed(1)}
t={t}
/>
)}
{hasUsageBreakdown && (
<StackedBarChart
points={chartData!.usageBreakdown}
label={t("DetailChartUsageBreakdown")}
height={56}
t={t}
/>
)}
</section>
</details>
)}
</div>
);
Expand Down
1 change: 1 addition & 0 deletions apps/desktop-tauri/src/i18n/keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export const ALL_LOCALE_KEYS = [
"ProviderDisabled",
"ProviderInfo",
"ProviderUsage",
"PanelUsageDetails",
"AuthType",
"DataSource",
"ProviderNotDetected",
Expand Down
1 change: 1 addition & 0 deletions apps/desktop-tauri/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<React.StrictMode>
Expand Down
13 changes: 2 additions & 11 deletions apps/desktop-tauri/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -6024,6 +6014,7 @@ button, input, select, textarea {
font-family: var(--font-ui);
}


.select, .number-input, .text-input {
min-height: 28px;
}
Expand Down
Loading