diff --git a/gui/src/clamp-draft.ts b/gui/src/clamp-draft.ts new file mode 100644 index 000000000..f1cb8fc15 --- /dev/null +++ b/gui/src/clamp-draft.ts @@ -0,0 +1,15 @@ +/** Clamp a numeric draft string into [min, max] after applying delta (for NumberStepper). */ +export function clampNumberDraft( + raw: string, + delta: number, + min: number, + max: number, + step = 1, +): string { + const trimmed = raw.trim(); + const parsed = trimmed === "" ? NaN : Number(trimmed); + const base = Number.isFinite(parsed) ? parsed : min; + const next = Math.min(max, Math.max(min, base + delta)); + // Keep one decimal for GiB-style steps; integers otherwise. + return step < 1 ? String(Math.round(next * 10) / 10) : String(Math.round(next)); +} diff --git a/gui/src/codex-auto-switch.ts b/gui/src/codex-auto-switch.ts index 288d2f5cf..ed7d7168d 100644 --- a/gui/src/codex-auto-switch.ts +++ b/gui/src/codex-auto-switch.ts @@ -39,6 +39,14 @@ export function autoSwitchThresholdReadDisposition( return editing || saving ? "defer" : "apply"; } +/** Accept bare threshold numbers or a full /active payload. */ +export function extractAutoSwitchThresholdPayload(value: unknown): unknown { + if (value && typeof value === "object" && value !== null && "autoSwitchThreshold" in value) { + return (value as { autoSwitchThreshold: unknown }).autoSwitchThreshold; + } + return value; +} + export interface AutoSwitchTogglePlan { threshold: number; lastEnabled: number; diff --git a/gui/src/components/AccountPoolStrategyControls.tsx b/gui/src/components/AccountPoolStrategyControls.tsx index 8db64a0df..3eca97cb1 100644 --- a/gui/src/components/AccountPoolStrategyControls.tsx +++ b/gui/src/components/AccountPoolStrategyControls.tsx @@ -3,6 +3,9 @@ import { ACCOUNT_POOL_STRATEGIES, type AccountPoolStrategy, } from "../account-pool-strategy"; +import { clampNumberDraft } from "../clamp-draft"; +import { NumberStepper } from "./NumberStepper"; +import { Select } from "../ui"; const STRATEGY_LABEL_KEYS = { quota: "accountPool.strategyQuota", @@ -24,7 +27,8 @@ export interface AccountPoolStrategyControlsProps { strategyLabelHidden?: boolean; onStrategyChange(strategy: AccountPoolStrategy): void; onStickyDraftChange(value: string): void; - onStickyCommit(): void; + /** Optional draft overrides React state when steppers commit in the same tick as a draft change. */ + onStickyCommit(nextDraft?: string): void; } /** @@ -42,57 +46,73 @@ export default function AccountPoolStrategyControls({ onStickyCommit, }: AccountPoolStrategyControlsProps) { const t = useT(); + const strategyOptions = ACCOUNT_POOL_STRATEGIES.map((value) => ({ + value, + label: t(STRATEGY_LABEL_KEYS[value]), + })); + return ( -
-