Skip to content

Commit fa443c0

Browse files
committed
fix(gui): address Codex review on pool hydration stack
1 parent c4c98c0 commit fa443c0

18 files changed

Lines changed: 154 additions & 151 deletions

gui/src/components/AccountPoolStrategyControls.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ export interface AccountPoolStrategyControlsProps {
2727
strategyLabelHidden?: boolean;
2828
onStrategyChange(strategy: AccountPoolStrategy): void;
2929
onStickyDraftChange(value: string): void;
30-
onStickyCommit(): void;
30+
/** Optional draft overrides React state when steppers commit in the same tick as a draft change. */
31+
onStickyCommit(nextDraft?: string): void;
3132
}
3233

3334
/**
@@ -99,8 +100,16 @@ export default function AccountPoolStrategyControls({
99100
disabled={disabled}
100101
incrementLabel={t("accountPool.stickyLimitInc")}
101102
decrementLabel={t("accountPool.stickyLimitDec")}
102-
onIncrement={() => onStickyDraftChange(clampNumberDraft(stickyDraft, 1, 1, 100))}
103-
onDecrement={() => onStickyDraftChange(clampNumberDraft(stickyDraft, -1, 1, 100))}
103+
onIncrement={() => {
104+
const next = clampNumberDraft(stickyDraft, 1, 1, 100);
105+
onStickyDraftChange(next);
106+
onStickyCommit(next);
107+
}}
108+
onDecrement={() => {
109+
const next = clampNumberDraft(stickyDraft, -1, 1, 100);
110+
onStickyDraftChange(next);
111+
onStickyCommit(next);
112+
}}
104113
/>
105114
</span>
106115
<div className="card-sub">{t("accountPool.stickyLimitHelp")}</div>

gui/src/components/CodexPoolStrategySetting.tsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ export default function CodexPoolStrategySetting({
9191

9292
// Shared /active observer (preferred): same payload the pool already fetched.
9393
// Ignore stale polls that started before a PUT bumped revision, and never apply while saving.
94+
// Replay readLastActive on subscribe so late mounts hydrate without waiting a poll.
9495
useEffect(() => {
9596
if (!subscribeLoadObserver) return;
9697
const observer: CodexAccountLoadObserver = {
@@ -104,14 +105,16 @@ export default function CodexPoolStrategySetting({
104105
if (!hydratedRef.current) setLoadError(true);
105106
},
106107
};
107-
return subscribeLoadObserver(observer);
108-
}, [subscribeLoadObserver, applyActivePayload]);
108+
const unsubscribe = subscribeLoadObserver(observer);
109+
if (!savingRef.current && readLastActive) applyActivePayload(readLastActive());
110+
return unsubscribe;
111+
}, [subscribeLoadObserver, applyActivePayload, readLastActive]);
109112

110113
useEffect(() => {
111-
if (!readLastActive) return;
114+
if (!readLastActive || subscribeLoadObserver) return;
112115
if (savingRef.current) return;
113116
applyActivePayload(readLastActive());
114-
}, [readLastActive, applyActivePayload]);
117+
}, [readLastActive, applyActivePayload, subscribeLoadObserver]);
115118

116119
// Standalone fallback when no shared controller observer is wired (keeps tests without observer green).
117120
useEffect(() => {
@@ -182,9 +185,9 @@ export default function CodexPoolStrategySetting({
182185
void save({ strategy: next });
183186
}}
184187
onStickyDraftChange={setStickyDraft}
185-
onStickyCommit={() => {
188+
onStickyCommit={(nextDraft) => {
186189
if (controlsDisabled) return;
187-
const parsed = parseAccountPoolStickyLimitDraft(stickyDraft);
190+
const parsed = parseAccountPoolStickyLimitDraft(nextDraft ?? stickyDraft);
188191
if (parsed === null) {
189192
setStickyDraft(String(stickyLimit));
190193
setError(t("accountPool.stickyLimitInvalid"));

gui/src/components/NumberStepper.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export function NumberStepper({
2525
className="ocx-stepper__btn"
2626
disabled={disabled}
2727
aria-label={incrementLabel}
28+
onMouseDown={(event) => event.preventDefault()}
2829
onClick={onIncrement}
2930
>
3031
<IconArrowUp width={10} height={10} aria-hidden="true" />
@@ -34,6 +35,7 @@ export function NumberStepper({
3435
className="ocx-stepper__btn"
3536
disabled={disabled}
3637
aria-label={decrementLabel}
38+
onMouseDown={(event) => event.preventDefault()}
3739
onClick={onDecrement}
3840
>
3941
<IconArrowDown width={10} height={10} aria-hidden="true" />

gui/src/components/codex-account-pool-helpers.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,15 @@ export function CodexCreditItem({ index, grantedAt, expiresAt, isNext, locale, t
2828
export function CodexTicketBadge({ account, onClick, t }: { account: CodexAccountEntry; onClick: () => void; t: TFn }) {
2929
const credits = account.quota?.resetCredits;
3030
// Reserve badge width while WHAM quota is still null so the card-head does not grow
31-
// when resetCredits arrives (0 or N).
32-
if (credits === undefined) {
31+
// when resetCredits arrives (0 or N). Quota loaded without resetCredits → no badge.
32+
if (account.quota == null) {
3333
return (
3434
<span className="badge badge-muted codex-ticket-badge-slot" aria-hidden="true">
3535
<IconTicket width={12} />0
3636
</span>
3737
);
3838
}
39+
if (credits === undefined) return null;
3940
const hasCredits = typeof credits === "number" && credits > 0;
4041
return (
4142
<button type="button"

gui/src/components/codex-account-pool-main-card.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ export function CodexAccountPoolMainCard({
130130
plan={main?.plan}
131131
threshold={threshold}
132132
t={t}
133-
pending={main?.quota == null}
133+
pending={main != null && main.quota == null}
134134
/>
135135
)}
136136
</div>

gui/src/components/provider-workspace/AnthropicAccountPoolSettings.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -242,8 +242,8 @@ export default function AnthropicAccountPoolSettings({
242242
});
243243
}}
244244
onStickyDraftChange={setStickyDraft}
245-
onStickyCommit={() => {
246-
const parsed = parseAccountPoolStickyLimitDraft(stickyDraft);
245+
onStickyCommit={(nextDraft) => {
246+
const parsed = parseAccountPoolStickyLimitDraft(nextDraft ?? stickyDraft);
247247
if (parsed === null) {
248248
setStickyDraft(String(stickyLimit));
249249
setError(t("accountPool.stickyLimitInvalid"));

gui/src/hooks/useCodexAccountPool.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ export function useCodexAccountPool(apiBase: string, enabled = true): CodexAccou
8888
const seed = lastGoodByBase.get(apiBase);
8989
const [accounts, setAccounts] = useState<CodexAccountEntry[]>(() => seed?.accounts ?? []);
9090
const [activeId, setActiveId] = useState<string | null>(() => seed?.activeId ?? null);
91-
const [loadState, setLoadState] = useState<CodexAccountLoadState>(() => (seed?.accounts.length ? "ready" : "loading"));
91+
const [loadState, setLoadState] = useState<CodexAccountLoadState>(() => (seed != null ? "ready" : "loading"));
9292
const [switchingId, setSwitchingId] = useState<string | null>(null);
9393
const [pauseUpdatingId, setPauseUpdatingId] = useState<string | null>(null);
9494
const [pausingExhausted, setPausingExhausted] = useState(false);
@@ -106,10 +106,19 @@ export function useCodexAccountPool(apiBase: string, enabled = true): CodexAccou
106106
const lastActiveRef = useRef<{ value: unknown } | null>(null);
107107
const switchingRef = useRef<string | null>(null);
108108
const hasAccountsRef = useRef(Boolean(seed?.accounts.length));
109+
// Distinct from hasAccountsRef: empty successful loads still count as loaded so soft
110+
// polls do not flip the UI back to the cold skeleton.
111+
const hasLoadedRef = useRef(seed != null);
109112
const pauseMutationRef = useRef<"bulk" | { accountId: string } | null>(null);
110113

111114
const subscribeLoadObserver = useCallback((observer: CodexAccountLoadObserver) => {
112115
observersRef.current.add(observer);
116+
// Replay last /active for late subscribers that mount after a load finished.
117+
const last = lastActiveRef.current?.value;
118+
if (last !== undefined) {
119+
const revision = observer.beginActiveRead();
120+
observer.acceptActiveRead(last, revision);
121+
}
113122
return () => { observersRef.current.delete(observer); };
114123
}, []);
115124

@@ -132,7 +141,7 @@ export function useCodexAccountPool(apiBase: string, enabled = true): CodexAccou
132141
const revisions = new Map<CodexAccountLoadObserver, number>();
133142
for (const observer of observers) revisions.set(observer, observer.beginActiveRead());
134143
// Soft refresh when boxes are already on screen — avoid full-page loading flash.
135-
if (!refreshQuota && !hasAccountsRef.current) setLoadState("loading");
144+
if (!refreshQuota && !hasLoadedRef.current) setLoadState("loading");
136145

137146
let nextAccounts: CodexAccountEntry[] | null = null;
138147
let nextActiveId: string | null | undefined;
@@ -146,6 +155,7 @@ export function useCodexAccountPool(apiBase: string, enabled = true): CodexAccou
146155
nextAccounts = (payload.accounts ?? []) as CodexAccountEntry[];
147156
setAccounts(nextAccounts);
148157
hasAccountsRef.current = nextAccounts.length > 0;
158+
hasLoadedRef.current = true;
149159
// Progressive: paint account/quota boxes as soon as /accounts returns.
150160
setLoadState("ready");
151161
}
@@ -189,14 +199,17 @@ export function useCodexAccountPool(apiBase: string, enabled = true): CodexAccou
189199
if (loadGenerationRef.current !== generation) return false;
190200
if (accountsOk) {
191201
setLoadState("ready");
202+
hasLoadedRef.current = true;
192203
const prior = lastGoodByBase.get(apiBase);
193204
lastGoodByBase.set(apiBase, {
194205
accounts: nextAccounts ?? prior?.accounts ?? [],
195206
activeId: nextActiveId !== undefined ? nextActiveId : (prior?.activeId ?? null),
196207
});
197208
return activeOk;
198209
}
199-
if (!hasAccountsRef.current) setLoadState("error");
210+
// Cold failure only: after a successful load (including empty), keep rows and stay ready
211+
// so a soft poll miss does not flash the skeleton / wipe the pool.
212+
if (!hasLoadedRef.current) setLoadState("error");
200213
return false;
201214
}, [apiBase]);
202215

gui/src/i18n/de.ts

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -455,16 +455,6 @@ export const de: Record<TKey, string> = {
455455
"sub.moveUp": "{m} nach oben",
456456
"sub.moveDown": "{m} nach unten",
457457
"sub.removeAria": "{m} entfernen",
458-
"sub.workspace.allModels": "Alle Modelle",
459-
"sub.workspace.selector": "Öffentlicher Selektor",
460-
"sub.workspace.priority": "Priorität",
461-
"sub.workspace.notFeatured": "Nicht hervorgehoben",
462-
"sub.workspace.addToFeatured": "{m} zu Hervorgehobenen hinzufügen",
463-
"sub.workspace.removeFromFeatured": "{m} aus Hervorgehobenen entfernen",
464-
"sub.workspace.featuredFull": "Hervorgehobene Liste ist voll (max. 5)",
465-
"sub.workspace.selectModel": "Modell auswählen",
466-
"sub.workspace.selectModelDesc": "Wählen Sie ein Modell aus der Liste, um Details anzuzeigen und es für spawn_agent hervorzuheben.",
467-
"sub.workspace.mainAria": "Subagent-Modelldetails",
468458
"logs.title": "Anfrage-Protokolle",
469459
"logs.tabLogs": "Protokolle",
470460
"logs.tabDebug": "Diagnose",
@@ -602,8 +592,6 @@ export const de: Record<TKey, string> = {
602592
"usage.section.models": "Modelle",
603593
"usage.section.providers": "Anbieter",
604594
"usage.section.coverage": "Abdeckungs-Aufschlüsselung",
605-
"usage.workspace.sections": "Nutzungsabschnitte",
606-
"usage.workspace.report": "Nutzungsbericht",
607595
"usage.coverage.measured": "Gemessen",
608596
"usage.coverage.reported": "Anbieter gemeldet",
609597
"usage.coverage.estimated": "Geschätzt",
@@ -846,15 +834,6 @@ export const de: Record<TKey, string> = {
846834
"api.activeKeys": "Aktive Schlüssel ({count})",
847835
"api.activeKeysLoading": "Aktive Schlüssel",
848836
"api.noKeys": "Noch keine API-Schlüssel. Erstelle oben einen.",
849-
"api.workspace.overview": "Übersicht",
850-
"api.workspace.details": "API-Schlüsseldetails",
851-
"api.workspace.keyDetails": "Schlüsseldetails",
852-
"api.workspace.keyPrefix": "Schlüssel-Präfix",
853-
"api.workspace.deleteKey": "Schlüssel löschen",
854-
"api.workspace.deleteConfirm": "Diesen Schlüssel wirklich löschen? Das lässt sich nicht rückgängig machen.",
855-
"api.workspace.noKeysHint": "Noch keine API-Schlüssel. Erstelle einen, um zu starten.",
856-
"api.workspace.selectKeyHint": "Wähle einen Schlüssel aus der Liste, um Details zu sehen.",
857-
"api.workspace.usageExamples": "Nutzungsbeispiele",
858837
"api.copyUrlHint": "Klick um URL zu kopieren",
859838
"api.urlCopied": "URL kopiert",
860839
"api.copyExampleHint": "Klick um Beispiel zu kopieren",
@@ -1355,7 +1334,6 @@ export const de: Record<TKey, string> = {
13551334
"codexAuth.addIdPlaceholder": "codex-work, codex-alt, team…",
13561335
"codexAuth.resetCreditsAria": "{count} Reset-Guthaben",
13571336
"claude.pageTitle": "Claude Code",
1358-
"claude.workspace.settings": "Einstellungen",
13591337

13601338
// Combos workspace
13611339
"cws.loading": "Combos werden geladen…",

gui/src/i18n/en.ts

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -471,16 +471,6 @@ export const en = {
471471
"sub.moveUp": "Move {m} up",
472472
"sub.moveDown": "Move {m} down",
473473
"sub.removeAria": "Remove {m}",
474-
"sub.workspace.allModels": "All models",
475-
"sub.workspace.selector": "Public selector",
476-
"sub.workspace.priority": "Priority",
477-
"sub.workspace.notFeatured": "Not featured",
478-
"sub.workspace.addToFeatured": "Add {m} to featured",
479-
"sub.workspace.removeFromFeatured": "Remove {m} from featured",
480-
"sub.workspace.featuredFull": "Featured list is full (max 5)",
481-
"sub.workspace.selectModel": "Select a model",
482-
"sub.workspace.selectModelDesc": "Pick a model from the list to see details and feature it for spawn_agent.",
483-
"sub.workspace.mainAria": "Subagent model details",
484474

485475
// logs
486476
"logs.title": "Request Logs",
@@ -624,8 +614,6 @@ export const en = {
624614
"usage.section.models": "Models",
625615
"usage.section.providers": "Providers",
626616
"usage.section.coverage": "Coverage breakdown",
627-
"usage.workspace.sections": "Usage sections",
628-
"usage.workspace.report": "Usage report",
629617
"usage.coverage.measured": "Measured",
630618
"usage.coverage.reported": "Provider reported",
631619
"usage.coverage.estimated": "Estimated",
@@ -1261,15 +1249,6 @@ export const en = {
12611249
"api.activeKeys": "Active keys ({count})",
12621250
"api.activeKeysLoading": "Active keys",
12631251
"api.noKeys": "No API keys yet. Generate one above.",
1264-
"api.workspace.overview": "Overview",
1265-
"api.workspace.details": "API key details",
1266-
"api.workspace.keyDetails": "Key details",
1267-
"api.workspace.keyPrefix": "Key prefix",
1268-
"api.workspace.deleteKey": "Delete key",
1269-
"api.workspace.deleteConfirm": "Are you sure you want to delete this key? This cannot be undone.",
1270-
"api.workspace.noKeysHint": "No API keys yet. Generate one to get started.",
1271-
"api.workspace.selectKeyHint": "Select a key from the list to view its details.",
1272-
"api.workspace.usageExamples": "Usage examples",
12731252
"api.copyUrlHint": "Click to copy URL",
12741253
"api.urlCopied": "URL copied",
12751254
"api.copyExampleHint": "Click to copy example",
@@ -1312,7 +1291,6 @@ export const en = {
13121291
"nav.claude": "Claude",
13131292
"claude.subtitle": "Use GPT, Gemini, and other models inside Claude Code.",
13141293
"claude.pageTitle": "Claude Code",
1315-
"claude.workspace.settings": "Settings",
13161294
"claude.enabledLabel": "Claude connection",
13171295
"claude.enabledHint": "When off, Claude Code cannot use this proxy.",
13181296
"claude.authMode": "Auth Mode",

gui/src/i18n/ja.ts

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -438,16 +438,6 @@ export const ja: Record<TKey, string> = {
438438
"sub.moveUp": "{m} を上へ移動",
439439
"sub.moveDown": "{m} を下へ移動",
440440
"sub.removeAria": "{m} を削除",
441-
"sub.workspace.allModels": "すべてのモデル",
442-
"sub.workspace.selector": "公開セレクター",
443-
"sub.workspace.priority": "優先度",
444-
"sub.workspace.notFeatured": "おすすめ未設定",
445-
"sub.workspace.addToFeatured": "{m} をおすすめに追加",
446-
"sub.workspace.removeFromFeatured": "{m} をおすすめから削除",
447-
"sub.workspace.featuredFull": "おすすめリストがいっぱいです(最大 5)",
448-
"sub.workspace.selectModel": "モデルを選択",
449-
"sub.workspace.selectModelDesc": "一覧からモデルを選んで詳細を確認し、spawn_agent のおすすめに設定します。",
450-
"sub.workspace.mainAria": "サブエージェントのモデル詳細",
451441

452442
// logs
453443
"logs.title": "リクエストログ",
@@ -591,8 +581,6 @@ export const ja: Record<TKey, string> = {
591581
"usage.section.models": "モデル",
592582
"usage.section.providers": "プロバイダー",
593583
"usage.section.coverage": "カバレッジ内訳",
594-
"usage.workspace.sections": "使用量セクション",
595-
"usage.workspace.report": "使用量レポート",
596584
"usage.coverage.measured": "計測",
597585
"usage.coverage.reported": "プロバイダー報告",
598586
"usage.coverage.estimated": "推定",
@@ -1241,15 +1229,6 @@ export const ja: Record<TKey, string> = {
12411229
"api.activeKeys": "アクティブなキー ({count})",
12421230
"api.activeKeysLoading": "有効なキー",
12431231
"api.noKeys": "まだ API キーがありません。上で生成してください。",
1244-
"api.workspace.overview": "概要",
1245-
"api.workspace.details": "APIキーの詳細",
1246-
"api.workspace.keyDetails": "キーの詳細",
1247-
"api.workspace.keyPrefix": "キーのプレフィックス",
1248-
"api.workspace.deleteKey": "キーを削除",
1249-
"api.workspace.deleteConfirm": "このキーを削除しますか?この操作は元に戻せません。",
1250-
"api.workspace.noKeysHint": "API キーがまだありません。作成して始めましょう。",
1251-
"api.workspace.selectKeyHint": "一覧からキーを選択すると、詳細が表示されます。",
1252-
"api.workspace.usageExamples": "使用例",
12531232
"api.copyUrlHint": "クリックして URL をコピー",
12541233
"api.urlCopied": "URL をコピーしました",
12551234
"api.copyExampleHint": "クリックして例をコピー",
@@ -1267,7 +1246,6 @@ export const ja: Record<TKey, string> = {
12671246
"nav.claude": "Claude",
12681247
"claude.subtitle": "Claude Code 内で GPT、Gemini などのモデルを使用します。",
12691248
"claude.pageTitle": "Claude Code",
1270-
"claude.workspace.settings": "設定",
12711249
"claude.enabledLabel": "Claude 接続",
12721250
"claude.enabledHint": "オフにすると Claude Code はこのプロキシを使用できません。",
12731251
"claude.authMode": "認証モード",

0 commit comments

Comments
 (0)