Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
8ab2c7e
fix(gui): keep account actions from reflowing card header
Vocllum Sep 8, 2026
822ac4d
fix(gui): move account disclosure layout into CSS
Vocllum Sep 8, 2026
beb5cdb
fix(gui): keep default priority inside actions panel
Vocllum Sep 8, 2026
f36d1a5
fix(gui): render priority only while disclosure is open
Vocllum Sep 8, 2026
de2399e
Merge upstream dev into fix/codex-account-actions-wrap
Vocllum Sep 8, 2026
f739194
test(gui): pin account priority disclosure placement
Vocllum Sep 8, 2026
4530273
test(gui): assert one priority control per account
Vocllum Sep 8, 2026
c73d644
fix(gui): align account actions with popover styling
Vocllum Sep 9, 2026
96f074c
test(gui): pin compact account actions popover
Vocllum Sep 9, 2026
91c7e9d
refactor(gui): own account popover layout in codex styles
Vocllum Sep 9, 2026
21cefec
test(gui): cover account popover layout ownership
Vocllum Sep 9, 2026
353a42b
Merge latest dev into account actions fix
Vocllum Sep 9, 2026
aa67109
Merge latest dev into account actions fix
Vocllum Sep 9, 2026
a58972c
Merge pull request #2 from Vocllum/sync/dev-4062-260909b
Vocllum Sep 9, 2026
65cf58b
test(gui): drop stale account popover assertion
Vocllum Sep 9, 2026
ccbd5c9
Merge latest dev into account actions fix
Vocllum Sep 9, 2026
f44c301
Merge pull request #8 from Vocllum/sync/dev-4062-260909c
Vocllum Sep 9, 2026
9c9a0c8
chore(gui): reconcile account card with current dev
Vocllum Sep 10, 2026
dc4cb69
Merge pull request #12 from Vocllum/sync/dev-4062-260910
Vocllum Sep 10, 2026
246bdd7
Merge upstream dev into account-actions fix
Vocllum Sep 11, 2026
ecb84c2
Merge 246bdd74dca8750341cbe590354a77e045e9f4df into b550d24e103778731…
Vocllum Sep 11, 2026
a776728
docs(pr): add synthetic account-actions preview
Vocllum Sep 11, 2026
35145c9
Merge a776728385df199e93a65bcb4dfe128867880ebf into 42fcf9a0a6de7930a…
Vocllum Sep 11, 2026
43684c6
Merge 35145c90d810e91a18e92ac2b074c2bcf55e5586 into dd9a2906b0a921519…
Vocllum Sep 11, 2026
fb02a70
Merge 43684c6864942ef94366aafbe0a7d0e9615ba587 into 16f18d6543234839b…
Vocllum Sep 11, 2026
7a48153
Merge upstream dev into fix/codex-account-actions-wrap
Vocllum Sep 12, 2026
844ba55
Merge upstream dev into fix/codex-account-actions-wrap
Vocllum Sep 12, 2026
ddc7320
Merge upstream dev into fix/codex-account-actions-wrap
Vocllum Sep 12, 2026
a3b6be5
Merge upstream dev into fix/codex-account-actions-wrap
Vocllum Sep 12, 2026
83415d4
Merge upstream dev into fix/codex-account-actions-wrap
Vocllum Sep 12, 2026
f53f934
Merge upstream dev into fix/codex-account-actions-wrap
Vocllum Sep 12, 2026
9206420
Merge upstream dev into fix/codex-account-actions-wrap
Vocllum Sep 12, 2026
bfe5ab4
Merge upstream dev into fix/codex-account-actions-wrap
Vocllum Sep 12, 2026
30e4d31
Merge upstream dev into fix/codex-account-actions-wrap
Vocllum Sep 12, 2026
c5ec7df
Merge upstream dev into fix/codex-account-actions-wrap
Vocllum Sep 12, 2026
0a18cce
Merge upstream dev into fix/codex-account-actions-wrap
Vocllum Sep 12, 2026
2951c99
Merge upstream dev into fix/codex-account-actions-wrap
Vocllum Sep 12, 2026
4107cdc
Merge upstream dev into fix/codex-account-actions-wrap
Vocllum Sep 12, 2026
8486983
Merge upstream dev aa0dd508 into fix/codex-account-actions-wrap
Vocllum Sep 12, 2026
07d8aad
Merge upstream dev f5b2a0d into fix/codex-account-actions-wrap
Vocllum Sep 12, 2026
53553af
Merge upstream dev 92e21bc8 into fix/codex-account-actions-wrap
Vocllum Sep 12, 2026
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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
29 changes: 16 additions & 13 deletions gui/src/components/codex-account-pool-cards.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,20 @@ export function CodexAccountPoolCards({
const validationPending = a.health?.reason === "validation_pending";
const healthLabel = formatOAuthHealthLabel(t, a.health);
const healthSummary = formatOAuthHealthSummary(t, "codex", a.id, a.health);
const hasCustomPriority = normalizeAccountPriority(a.priority) !== DEFAULT_ACCOUNT_PRIORITY;
const priorityControl = (
<AccountPriorityControl
value={a.priority}
selectId={`codex-account-priority-${a.id}`}
// Every row, not just the one being written: the controller serializes order
// writes behind one mutation ref, so a second row's pick would come back "busy"
// and be dropped with no toast. Same global lock the pause button uses.
// A pending switch counts too — it writes the same pin this clears, so the
// controller refuses to overlap them, and that refusal is equally silent.
disabled={priorityUpdatingId !== null || switchingId !== null}
onChange={(priority) => onPriorityChange(a, priority)}
/>
);
return (
<div key={a.id} className={`card ${isNext(a) ? "card-active" : ""}`} style={{ marginBottom: 8 }}>
<div className="card-head">
Expand Down Expand Up @@ -153,6 +167,7 @@ export function CodexAccountPoolCards({
>
<summary className="btn btn-ghost btn-sm" aria-label={`${t("codexAuth.moreActions")} — ${a.email}`} title={t("codexAuth.moreActions")}>⋯</summary>
<div className="codex-account-more-body">
{!hasCustomPriority && moreOpen.has(a.id) && priorityControl}
<span className="mono text-caption muted">{t("prov.accountId")}: {displayAccountId(a.id)}</span>
<button type="button" className="btn btn-ghost btn-sm" onClick={() => idCopy.copy(a.id, a.id)}>
{idCopy.outcomeFor(a.id) === "copied" ? t("startup.copied") : t("codexAuth.copyId")}
Expand All @@ -174,19 +189,7 @@ export function CodexAccountPoolCards({
</div>
<div className="codex-account-identity">
<div className="codex-account-identity-copy">{a.email}{a.plan ? ` · ${a.plan}` : ""}</div>
{(normalizeAccountPriority(a.priority) !== DEFAULT_ACCOUNT_PRIORITY || moreOpen.has(a.id)) && (
<AccountPriorityControl
value={a.priority}
selectId={`codex-account-priority-${a.id}`}
// Every row, not just the one being written: the controller serializes order
// writes behind one mutation ref, so a second row's pick would come back "busy"
// and be dropped with no toast. Same global lock the pause button uses.
// A pending switch counts too — it writes the same pin this clears, so the
// controller refuses to overlap them, and that refusal is equally silent.
disabled={priorityUpdatingId !== null || switchingId !== null}
onChange={(priority) => onPriorityChange(a, priority)}
/>
)}
{hasCustomPriority && priorityControl}
</div>
{healthSummary && (
<div className="card-sub faint">{healthSummary}</div>
Expand Down
34 changes: 34 additions & 0 deletions gui/src/styles-codex-set.css
Original file line number Diff line number Diff line change
Expand Up @@ -441,3 +441,37 @@
.codex-main-hard-lock-dialog .modal-desc { text-wrap: pretty; }
:lang(ko) .codex-main-hard-lock-copy .card-sub,
:lang(ko) .codex-main-hard-lock-dialog .modal-desc { word-break: keep-all; }

/* Account-card ⋯ actions share the dashboard's compact popover language instead of
reading as a wide floating toolbar. This selector intentionally outranks the later
single-class rule in styles.css without moving layout ownership back inline. */
.codex-account-more .codex-account-more-body {
box-sizing: border-box;
z-index: var(--z-popover);
min-width: min(16rem, calc(100vw - 2rem));
max-width: min(22rem, calc(100vw - 2rem));
padding: 10px 12px;
gap: 6px;
justify-content: flex-start;
background: var(--raised);
border-radius: var(--radius);
box-shadow: 0 4px 24px rgb(0 0 0 / 0.14);
}

/* Layout ownership lives here too. The type-qualified selectors outrank the legacy
single-class declarations later in styles.css, so the disclosure stays out of card flow
even after styles.css is refreshed from upstream. */
details.codex-account-more {
position: relative;
display: inline-block;
}

details.codex-account-more > .codex-account-more-body {
position: absolute;
top: calc(100% + 6px);
right: 0;
display: flex;
flex-wrap: wrap;
align-items: center;
flex-basis: auto;
}
28 changes: 28 additions & 0 deletions gui/tests/codex-account-more-popover-style.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { expect, test } from "bun:test";

function ruleBody(css: string, selector: string): string {
const escaped = selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
return new RegExp(`${escaped}\\s*\\{([^}]*)\\}`).exec(css)?.[1] ?? "";
}

test("account more-actions uses the compact dashboard popover language", async () => {
const css = await Bun.file(new URL("../src/styles-codex-set.css", import.meta.url)).text();
const wrapper = ruleBody(css, "details.codex-account-more");
const layout = ruleBody(css, "details.codex-account-more > .codex-account-more-body");
const visual = ruleBody(css, ".codex-account-more .codex-account-more-body");

expect(wrapper).toMatch(/position:\s*relative/);
expect(wrapper).toMatch(/display:\s*inline-block/);
expect(layout).toMatch(/position:\s*absolute/);
expect(layout).toMatch(/top:\s*calc\(100% \+ 6px\)/);
expect(layout).toMatch(/right:\s*0/);
expect(layout).toMatch(/flex-basis:\s*auto/);

expect(visual).toMatch(/z-index:\s*var\(--z-popover\)/);
expect(visual).toMatch(/min-width:\s*min\(16rem, calc\(100vw - 2rem\)\)/);
expect(visual).toMatch(/max-width:\s*min\(22rem, calc\(100vw - 2rem\)\)/);
expect(visual).toMatch(/background:\s*var\(--raised\)/);
expect(visual).toMatch(/border-radius:\s*var\(--radius\)/);
expect(visual).toMatch(/box-shadow:\s*0 4px 24px rgb\(0 0 0 \/ 0\.14\)/);
expect(visual).toMatch(/justify-content:\s*flex-start/);
});
118 changes: 118 additions & 0 deletions gui/tests/codex-account-more-priority-placement.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { afterEach, beforeEach, expect, test } from "bun:test";
import { Window } from "happy-dom";
import { act } from "react";
import { createRoot, type Root } from "react-dom/client";
import { CodexAccountPoolCards } from "../src/components/codex-account-pool-cards";
import type { CodexAccountEntry } from "../src/components/codex-account-pool-types";
import { LanguageProvider } from "../src/i18n/provider";

const globals = ["document", "window", "navigator", "localStorage", "IS_REACT_ACT_ENVIRONMENT"] as const;
type GlobalName = (typeof globals)[number];
const prioritySelector = "#codex-account-priority-pool-1";

let previous: Record<GlobalName, PropertyDescriptor | undefined>;
let testWindow: Window;
let root: Root | null = null;
let host: HTMLElement;

function restoreProperty(target: object, key: PropertyKey, descriptor: PropertyDescriptor | undefined): void {
if (descriptor) Object.defineProperty(target, key, descriptor);
else Reflect.deleteProperty(target, key);
}

beforeEach(() => {
previous = Object.fromEntries(
globals.map(key => [key, Object.getOwnPropertyDescriptor(globalThis, key)]),
) as typeof previous;
testWindow = new Window({ url: "http://localhost/" });
Object.defineProperties(globalThis, {
document: { configurable: true, value: testWindow.document },
window: { configurable: true, value: testWindow },
navigator: { configurable: true, value: testWindow.navigator },
localStorage: { configurable: true, value: testWindow.localStorage },
IS_REACT_ACT_ENVIRONMENT: { configurable: true, value: true },
});
host = testWindow.document.createElement("div") as never as HTMLElement;
testWindow.document.body.appendChild(host as never);
});

afterEach(async () => {
if (root) await act(async () => root?.unmount());
root = null;
for (const key of globals) restoreProperty(globalThis, key, previous[key]);
await testWindow.happyDOM?.close?.();
});

function account(priority: number): CodexAccountEntry {
return {
id: "pool-1",
email: "pool@example.test",
isMain: false,
paused: false,
priority,
hasCredential: true,
quota: null,
quotaAutoRefresh: {
fiveHourAvailable: false,
weeklyAvailable: false,
fiveHourEnabled: false,
weeklyEnabled: false,
},
};
}

async function mount(priority: number): Promise<void> {
await act(async () => {
root = createRoot(host);
root.render(
<LanguageProvider>
<CodexAccountPoolCards
pool={[account(priority)]}
activeId={null}
accountModeState={null}
switchActionLabel="Switch"
threshold={80}
onOpenReset={() => undefined}
onSwitch={() => undefined}
onTogglePause={() => undefined}
pauseUpdatingId={null}
pauseBusy={false}
onPriorityChange={() => undefined}
priorityUpdatingId={null}
switchingId={null}
pinnedId={null}
onReauth={() => undefined}
onEditAlias={() => undefined}
onRemove={() => undefined}
/>
</LanguageProvider>,
);
});
}

test("default priority selector is rendered once inside the open more-actions panel", async () => {
await mount(0);
const more = host.querySelector<HTMLDetailsElement>("details.codex-account-more");
expect(more).not.toBeNull();
expect(host.querySelectorAll(prioritySelector)).toHaveLength(0);

await act(async () => {
more!.querySelector<HTMLElement>("summary")!.click();
await new Promise<void>(resolve => testWindow.setTimeout(resolve, 0));
});

expect(more!.open).toBe(true);
expect(host.querySelectorAll(prioritySelector)).toHaveLength(1);
expect(more!.querySelector(prioritySelector)).not.toBeNull();
expect(host.querySelector(`.codex-account-identity ${prioritySelector}`)).toBeNull();
});

test("non-default priority selector is rendered once inline and out of the closed disclosure", async () => {
await mount(2);
const more = host.querySelector<HTMLDetailsElement>("details.codex-account-more");
expect(more).not.toBeNull();
expect(more!.open).toBe(false);
expect(host.querySelectorAll(prioritySelector)).toHaveLength(1);
expect(more!.querySelector(prioritySelector)).toBeNull();
expect(host.querySelector(`.codex-account-identity ${prioritySelector}`)).not.toBeNull();
});
Loading