diff --git a/apps/desktop-tauri/src/components/CodexAccountsMenu.test.tsx b/apps/desktop-tauri/src/components/CodexAccountsMenu.test.tsx
index 9b66d809d3..607acd0221 100644
--- a/apps/desktop-tauri/src/components/CodexAccountsMenu.test.tsx
+++ b/apps/desktop-tauri/src/components/CodexAccountsMenu.test.tsx
@@ -112,6 +112,35 @@ describe("CodexAccountsMenu", () => {
expect((fills[1] as HTMLElement).style.width).toBe("70%");
});
+ it("renders a usage bar from a weekly-only snapshot (primaryWindow: null)", async () => {
+ const weeklyOnly: CodexAccountUsageSnapshot = {
+ email: "weekly@example.com",
+ providerAccountId: null,
+ plan: "pro",
+ allowed: true,
+ limitReached: false,
+ primaryWindow: null,
+ secondaryWindow: {
+ usedPercent: 42,
+ resetAt: null,
+ limitWindowSeconds: 604800,
+ },
+ credits: null,
+ updatedAt: "2024-01-01T00:00:00Z",
+ };
+ const { container } = renderMenu(false, {
+ accounts: [account("1", { source: "ambient" }), account("2")],
+ snapshots: { "1": weeklyOnly },
+ });
+ await screen.findByText("user-1@example.com");
+
+ const fills = container.querySelectorAll(
+ ".codex-menu-accounts__bar-fill",
+ );
+ expect(fills.length).toBe(1);
+ expect((fills[0] as HTMLElement).style.width).toBe("42%");
+ });
+
it("switches an account and kicks a provider refresh", async () => {
renderMenu(false, {
accounts: [account("1", { source: "ambient" }), account("2")],
diff --git a/apps/desktop-tauri/src/components/CodexAccountsMenu.tsx b/apps/desktop-tauri/src/components/CodexAccountsMenu.tsx
index a79d8d6fa4..aa2b8f9ff9 100644
--- a/apps/desktop-tauri/src/components/CodexAccountsMenu.tsx
+++ b/apps/desktop-tauri/src/components/CodexAccountsMenu.tsx
@@ -93,8 +93,15 @@ export default function CodexAccountsMenu({ hideEmail }: { hideEmail: boolean })
{accounts.map((account) => {
const snapshot = snapshots[account.id];
- const pct = snapshot?.primaryWindow
- ? Math.round(snapshot.primaryWindow.usedPercent)
+ // Prefer the primary (session) window, but accounts whose backend
+ // only returns a weekly window have primaryWindow: null — fall back
+ // to the next filled window in canonical order (primary →
+ // secondary; the account-snapshot bridge carries no tertiary or
+ // extra rate windows) so the usage bar still renders.
+ const usageWindow =
+ snapshot?.primaryWindow ?? snapshot?.secondaryWindow ?? null;
+ const pct = usageWindow
+ ? Math.round(usageWindow.usedPercent)
: null;
const label =
account.nickname ??
diff --git a/apps/desktop-tauri/src/styles.css b/apps/desktop-tauri/src/styles.css
index 04a744c8ec..9b2b4096d2 100644
--- a/apps/desktop-tauri/src/styles.css
+++ b/apps/desktop-tauri/src/styles.css
@@ -2484,6 +2484,31 @@ body:has(.tray-panel-reveal) {
color: var(--provider-status-error);
}
+/* CodexAccountsSection cards sit in the fixed 720px settings window next to a
+ 3-button actions row (~242px). A long account email has no break
+ opportunities, so without containment it overflows the info column and
+ paints over the actions. Ellipsize the info column/title instead, and pin
+ the actions row in place with flex-shrink: 0 + nowrap. */
+.codex-accounts-card .credential-card__info {
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.codex-accounts-card .credential-card__info strong {
+ max-width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.codex-accounts-card .credential-card__actions {
+ flex-shrink: 0;
+ flex-wrap: nowrap;
+ white-space: nowrap;
+}
+
.provider-detail-pace__stage {
font-size: 0.86rem;
font-weight: 600;
diff --git a/apps/desktop-tauri/src/surfaces/settings/providers/sections/credentials/CodexAccountsSection.test.tsx b/apps/desktop-tauri/src/surfaces/settings/providers/sections/credentials/CodexAccountsSection.test.tsx
index 1b5396ba4d..38a2c39450 100644
--- a/apps/desktop-tauri/src/surfaces/settings/providers/sections/credentials/CodexAccountsSection.test.tsx
+++ b/apps/desktop-tauri/src/surfaces/settings/providers/sections/credentials/CodexAccountsSection.test.tsx
@@ -1,3 +1,4 @@
+import { readFileSync } from "node:fs";
import { act, render, screen, waitFor } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type {
@@ -139,4 +140,54 @@ describe("CodexAccountsSection", () => {
});
expect(tauriMocks.codexAccountRestartDesktop).toHaveBeenCalledTimes(1);
});
+});
+
+// Layout containment cannot be asserted via jsdom (vitest runs with
+// `css: false`, so styles.css is never applied and computed styles are
+// empty). Assert the stylesheet rules directly instead: these are the exact
+// properties that keep a long account email from painting over the actions
+// row at the fixed 720px settings window. import.meta.dirname (not .url)
+// survives vitest's jsdom transform as the real on-disk directory.
+if (!import.meta.dirname) {
+ throw new Error("import.meta.dirname unavailable to vitest runner");
+}
+const stylesSource = readFileSync(
+ `${import.meta.dirname}/../../../../../styles.css`,
+ "utf8",
+);
+
+function ruleBlock(source: string, selector: string): string {
+ const escaped = selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
+ const match = source.match(new RegExp(`${escaped}\\s*\\{([^}]*)\\}`));
+ expect(match).not.toBeNull();
+ return match![1];
+}
+
+describe("CodexAccountsSection containment styles", () => {
+ it("ellipsizes the info column and pins the actions row inside the card", () => {
+ const info = ruleBlock(
+ stylesSource,
+ ".codex-accounts-card .credential-card__info",
+ );
+ expect(info).toContain("min-width: 0");
+ expect(info).toContain("overflow: hidden");
+ expect(info).toContain("text-overflow: ellipsis");
+ expect(info).toContain("white-space: nowrap");
+
+ const title = ruleBlock(
+ stylesSource,
+ ".codex-accounts-card .credential-card__info strong",
+ );
+ expect(title).toContain("max-width: 100%");
+ expect(title).toContain("overflow: hidden");
+ expect(title).toContain("text-overflow: ellipsis");
+ expect(title).toContain("white-space: nowrap");
+
+ const actions = ruleBlock(
+ stylesSource,
+ ".codex-accounts-card .credential-card__actions",
+ );
+ expect(actions).toContain("flex-shrink: 0");
+ expect(actions).toContain("nowrap");
+ });
});
\ No newline at end of file
diff --git a/apps/desktop-tauri/src/test/node-builtins.d.ts b/apps/desktop-tauri/src/test/node-builtins.d.ts
new file mode 100644
index 0000000000..0f66cc2a63
--- /dev/null
+++ b/apps/desktop-tauri/src/test/node-builtins.d.ts
@@ -0,0 +1,16 @@
+// Minimal ambient declarations for the Node builtins used by test files.
+// This workspace does not depend on @types/node; declaring only the touched
+// signatures keeps tsc honest without pulling the full Node type surface.
+// Delete this file if @types/node is ever added (the real declarations
+// supersede these).
+declare module "node:fs" {
+ export function readFileSync(path: string, encoding: "utf8"): string;
+}
+
+// Node 20.11+ / vite-node inject import.meta.dirname|filename; those types
+// also come from @types/node, so declare the touched member here. Note:
+// import.meta.url is NOT reliable under vitest's jsdom transform (it can
+// resolve relative URLs against the dev-server origin) — use dirname instead.
+interface ImportMeta {
+ readonly dirname?: string;
+}