diff --git a/gui/src/components/CodexAccountPool.tsx b/gui/src/components/CodexAccountPool.tsx index e41dcc4912..cfedd15013 100644 --- a/gui/src/components/CodexAccountPool.tsx +++ b/gui/src/components/CodexAccountPool.tsx @@ -72,6 +72,10 @@ export default function CodexAccountPool({ apiBase, accountModeState = null, ban const [actionFeedbackTone, setActionFeedbackTone] = useState(null); const feedbackTimerRef = useRef | null>(null); const [refreshingQuota, setRefreshingQuota] = useState(false); + // undefined until /api/settings answers: the switch must not render a guessed position and + // then visibly correct itself a moment later. + const [sparkVisible, setSparkVisible] = useState(undefined); + const [sparkBusy, setSparkBusy] = useState(false); const [resetPopup, setResetPopup] = useState(null); const [resetConfirm, setResetConfirm] = useState(false); const [redeeming, setRedeeming] = useState(false); @@ -228,6 +232,49 @@ export default function CodexAccountPool({ apiBase, accountModeState = null, ban } }; + useEffect(() => { + // AbortController rather than a `cancelled` flag: the in-flight request is actually torn + // down on unmount, and the state update lands in a .then() the linter can see is guarded. + const abort = new AbortController(); + fetch(`${apiBase}/api/settings`, { signal: abort.signal }) + .then(response => (response.ok ? response.json() : null)) + .then((payload: { showCodexSparkQuota?: unknown } | null) => { + if (abort.signal.aborted || typeof payload?.showCodexSparkQuota !== "boolean") return; + setSparkVisible(payload.showCodexSparkQuota); + }) + // A settings read failure leaves the switch unrendered rather than guessing a position. + .catch(() => {}); + return () => { abort.abort(); }; + }, [apiBase]); + + const toggleSpark = async () => { + if (sparkBusy || sparkVisible === undefined) return; + const requested = !sparkVisible; + setSparkBusy(true); + // Optimistic, then reconciled against what the server confirms — the same shape the account + // picker toggle uses, so a rejected write visibly snaps back instead of lying. + setSparkVisible(requested); + try { + const response = await fetch(`${apiBase}/api/settings`, { + method: "PUT", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ showCodexSparkQuota: requested }), + }); + if (!response.ok) throw new Error("save"); + const payload = await response.json() as { showCodexSparkQuota?: unknown }; + const confirmed = typeof payload.showCodexSparkQuota === "boolean" ? payload.showCodexSparkQuota : requested; + setSparkVisible(confirmed); + showActionFeedback(t(confirmed ? "codexAuth.sparkQuotaShown" : "codexAuth.sparkQuotaHidden"), "ok"); + await load(true); + } catch { + setSparkVisible(!requested); + showActionFeedback(t("codexAuth.sparkQuotaFailed"), "err"); + } finally { + setSparkBusy(false); + } + }; + + const pauseExhausted = async () => { const result = await controller.pauseExhaustedAccounts(); if (!result.ok && result.reason === "busy") return; @@ -294,6 +341,9 @@ export default function CodexAccountPool({ apiBase, accountModeState = null, ban pauseBusy={pauseBusy} onRefresh={() => { void refreshQuotas(); }} onPauseExhausted={() => { void pauseExhausted(); }} + sparkVisible={sparkVisible} + sparkBusy={sparkBusy} + onToggleSpark={() => { void toggleSpark(); }} /> {banner} diff --git a/gui/src/components/codex-account-pool-main-card.tsx b/gui/src/components/codex-account-pool-main-card.tsx index 70a75023d9..7f53e122dc 100644 --- a/gui/src/components/codex-account-pool-main-card.tsx +++ b/gui/src/components/codex-account-pool-main-card.tsx @@ -183,6 +183,9 @@ export function CodexAccountPoolPageHead({ actionFeedbackTone, onRefresh, onPauseExhausted, + sparkVisible, + sparkBusy, + onToggleSpark, }: { t: TFn; embedded: boolean; @@ -193,6 +196,10 @@ export function CodexAccountPoolPageHead({ actionFeedbackTone?: NoticeTone | null; onRefresh: () => void; onPauseExhausted: () => void; + /** undefined until the preference has loaded, so the switch never renders a guessed state. */ + sparkVisible?: boolean; + sparkBusy?: boolean; + onToggleSpark?: () => void; }) { return (
{actionFeedback ?? ""} + {sparkVisible !== undefined && onToggleSpark && ( + + {t("codexAuth.sparkQuota")} + + + )}