Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,15 @@ The management API exposes `GET`/`PUT /api/v2`, `/api/injection-model`, `/api/ef
`/api/subagent-models`, and `/api/subagent-model-fallback`. Injection-model updates are partial;
the custom prompt is the `prompt` field on that API.

The Codex Auth page can also toggle Codex's own `default_mode_request_user_input`
feature flag (`GET`/`PUT /api/codex-auth/features/default-mode-request-user-input`). Enabling it
adds `[features] default_mode_request_user_input = true` to Codex's
`$CODEX_HOME/config.toml` through the official `codex features enable|disable` CLI
(format-preserving edit, removed again when disabled), which lets Codex pause a
Default-mode session and ask you questions with the `request_user_input` tool. The
flag is under development upstream and only applies to new sessions; the toggle fails
loudly when the installed Codex build does not know the flag yet.

## Roster and guidance

The effective v2 roster is the configured, picker-visible, priority-sorted first five models that
Expand Down
138 changes: 138 additions & 0 deletions gui/src/components/DefaultModeRequestUserInputSetting.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { useT } from "../i18n/shared";
import { readJsonOrThrow } from "../fetch-json";

const FEATURE_ENDPOINT = "/api/codex-auth/features/default-mode-request-user-input";

type Feedback = { tone: "ok" | "err"; message: string } | null;

/**
* Codex Auth page toggle for Codex's own `default_mode_request_user_input`
* feature flag. Reads/writes $CODEX_HOME/config.toml through the management
* API, which flips the flag via the official `codex features` CLI.
*/
export default function DefaultModeRequestUserInputSetting({ apiBase }: { apiBase: string }) {
const t = useT();
const [enabled, setEnabled] = useState(false);
const [hydrated, setHydrated] = useState(false);
const [saving, setSaving] = useState(false);
const [loadError, setLoadError] = useState(false);
const [feedback, setFeedback] = useState<Feedback>(null);
const savingRef = useRef(false);
const enabledRef = useRef(false);
const loadGenerationRef = useRef(0);

const load = useCallback(async () => {
// A poll landing between the optimistic flip and the PUT response must not
// revert the UI to the server's pre-save value. The generation also drops
// GETs that were already in flight when a save started.
if (savingRef.current) return;
const generation = ++loadGenerationRef.current;
try {
const res = await fetch(`${apiBase}${FEATURE_ENDPOINT}`);
if (!res.ok) throw new Error("load");
const payload = await res.json() as { enabled?: unknown };
if (savingRef.current || generation !== loadGenerationRef.current) return;
enabledRef.current = payload.enabled === true;
setEnabled(enabledRef.current);
Comment on lines +36 to +37

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Ignore poll results that predate a toggle

If the 30-second GET starts just before the user toggles and its stale response resolves during or after the PUT, these assignments overwrite enabledRef and the rendered state with the pre-toggle value. The next click can then submit the opposite of the persisted state; track a request revision or defer/ignore reads that overlap a save, following the existing auto-switch controller's reconciliation pattern.

AGENTS.md reference: gui/AGENTS.md:L9-L10

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[shipping-github] Fixed in 3a324231c: load() now tracks a generation ref; GETs that were in flight when a save started are ignored after await, so a stale poll can no longer revert the optimistic state. New GUI test covers the overlap window (poll resolves mid-PUT).

setHydrated(true);
setLoadError(false);
} catch {
if (!savingRef.current && generation === loadGenerationRef.current) setLoadError(true);
}
}, [apiBase]);

useEffect(() => {
const timeout = window.setTimeout(() => { void load(); }, 0);
const interval = window.setInterval(() => { void load(); }, 30_000);
return () => {
window.clearTimeout(timeout);
window.clearInterval(interval);
};
}, [load]);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const toggle = useCallback(async () => {
if (savingRef.current || !hydrated || loadError) return;
const next = !enabledRef.current;
const previous = enabledRef.current;
enabledRef.current = next;
setEnabled(next);
savingRef.current = true;
setSaving(true);
setFeedback(null);
loadGenerationRef.current++;
try {
const res = await fetch(`${apiBase}${FEATURE_ENDPOINT}`, {
method: "PUT",
headers: { "content-type": "application/json" },
body: JSON.stringify({ enabled: next }),
});
const payload = (await readJsonOrThrow<{ ok?: boolean; enabled?: unknown; changed?: unknown }>(res)) ?? {};
if (payload.ok !== true) throw new Error(String(res.status));
enabledRef.current = payload.enabled === true;
setEnabled(enabledRef.current);
setHydrated(true);
setFeedback({
tone: "ok",
message: t(payload.changed === true ? "codexAuth.requestUserInputUpdatedRestart" : "codexAuth.requestUserInputUpdated"),
});
} catch (error) {
enabledRef.current = previous;
setEnabled(previous);
const reason = error instanceof Error && error.message && !/^HTTP \d{3}$/.test(error.message)
? error.message
: t("codexAuth.requestUserInputUpdateFailed");
setFeedback({ tone: "err", message: reason });
} finally {
savingRef.current = false;
setSaving(false);
}
}, [apiBase, hydrated, loadError, t]);

const controlsDisabled = saving || !hydrated || loadError;

return (
<div
className="card card-row codex-request-user-input-card"
style={{ marginTop: 16 }}
aria-busy={saving || (!hydrated && !loadError) || undefined}
>
<div className="codex-request-user-input-copy">
<strong>{t("codexAuth.requestUserInput")}</strong>
<div className="card-sub" role={loadError ? "alert" : undefined}>
{loadError ? t("codexAuth.requestUserInputLoadFailed") : t("codexAuth.requestUserInputDesc")}
</div>
<code className="mono codex-request-user-input-config">
{`[features]\ndefault_mode_request_user_input = true`}
</code>
</div>
<div className="codex-request-user-input-controls">
{loadError && (
<button type="button" className="btn btn-ghost btn-sm" onClick={() => { void load(); }}>
{t("common.retry")}
</button>
)}
<button
type="button"
className={`toggle ${enabled ? "on" : ""}`}
onClick={() => { void toggle(); }}
disabled={controlsDisabled}
aria-pressed={enabled}
aria-label={t("codexAuth.requestUserInput")}
title={t("codexAuth.requestUserInput")}
>
<span className="toggle-knob" />
</button>
</div>
{feedback && (
<div
className={`codex-request-user-input-feedback${feedback.tone === "err" ? " is-error" : ""}`}
role={feedback.tone === "err" ? "alert" : "status"}
aria-atomic="true"
>
{feedback.message}
</div>
)}
</div>
);
}
6 changes: 6 additions & 0 deletions gui/src/i18n/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -818,6 +818,12 @@ export const de: Record<TKey, string> = {
"codexAuth.autoSwitchThresholdInvalid": "Gib eine ganze Zahl von 1 bis 100 ein",
"codexAuth.autoSwitchUpdated": "Der proaktive Wechsel nach Nutzung wurde aktualisiert",
"codexAuth.autoSwitchUpdateFailed": "Die Aktualisierung des nutzungsbasierten Wechsels konnte nicht bestätigt werden. Der zuletzt bestätigte Wert wird angezeigt.",
"codexAuth.requestUserInput": "Im Default-Modus nachfragen",
"codexAuth.requestUserInputDesc": "Erlaubt Codex, eine Session im Default-Modus zu pausieren und dir über das request_user_input-Tool Fragen zu stellen.",
"codexAuth.requestUserInputUpdated": "Feature-Flag aktualisiert - gilt für neue Sessions.",
"codexAuth.requestUserInputUpdatedRestart": "Feature-Flag aktualisiert - gilt für neue Sessions. Starte die Codex-App neu, damit es wirksam wird.",
"codexAuth.requestUserInputUpdateFailed": "Feature-Flag konnte nicht aktualisiert werden. Es wurde nichts geändert.",
"codexAuth.requestUserInputLoadFailed": "Feature-Flag konnte nicht aus config.toml gelesen werden.",
"anthropicPool.title": "Claude-Kontenpool (experimentell)",
"anthropicPool.enabledDesc": "Bei 429 wird das Konto gekühlt und umgeschaltet. Neue Sitzungen bevorzugen Nutzung unter {threshold}% (5-Stunden-Balken).",
"anthropicPool.disabledDesc": "Nutzt nur das aktive Claude-Konto. Nur aktivieren, wenn experimentelles Routing akzeptabel ist.",
Expand Down
6 changes: 6 additions & 0 deletions gui/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1260,6 +1260,12 @@ export const en = {
"codexAuth.autoSwitchThresholdInvalid": "Enter a whole number from 1 to 100",
"codexAuth.autoSwitchUpdated": "Usage-based proactive switching updated",
"codexAuth.autoSwitchUpdateFailed": "The usage-based switching update could not be confirmed. The last confirmed value is shown.",
"codexAuth.requestUserInput": "Ask for input in Default mode",
"codexAuth.requestUserInputDesc": "Lets Codex pause a Default-mode session and ask you questions with the request_user_input tool.",
"codexAuth.requestUserInputUpdated": "Feature flag updated - applies to new sessions.",
"codexAuth.requestUserInputUpdatedRestart": "Feature flag updated - applies to new sessions. Restart the Codex app to pick it up.",
"codexAuth.requestUserInputUpdateFailed": "Could not update the feature flag. Nothing was changed.",
"codexAuth.requestUserInputLoadFailed": "Could not read the feature flag from config.toml.",

"anthropicPool.title": "Claude account pool (experimental)",
"anthropicPool.enabledDesc": "On 429, cools the account and fails over. New sessions prefer usage under {threshold}% (5-hour bar).",
Expand Down
6 changes: 6 additions & 0 deletions gui/src/i18n/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1210,6 +1210,12 @@ export const ja: Record<TKey, string> = {
"codexAuth.autoSwitchThresholdInvalid": "1 から 100 までの整数を入力してください",
"codexAuth.autoSwitchUpdated": "使用量ベースのプロアクティブ切り替え設定を更新しました",
"codexAuth.autoSwitchUpdateFailed": "使用量ベースの切り替え更新を確認できませんでした。最後に確認された値を表示しています。",
"codexAuth.requestUserInput": "Default モードで入力を求める",
"codexAuth.requestUserInputDesc": "Default モードのセッションで Codex が一時停止し、request_user_input ツールで質問できるようにします。",
"codexAuth.requestUserInputUpdated": "機能フラグを更新しました - 新しいセッションから適用されます。",
"codexAuth.requestUserInputUpdatedRestart": "機能フラグを更新しました - 新しいセッションから適用されます。Codex アプリを再起動してください。",
"codexAuth.requestUserInputUpdateFailed": "機能フラグを更新できませんでした。変更はありません。",
"codexAuth.requestUserInputLoadFailed": "config.toml から機能フラグを読み込めませんでした。",
"anthropicPool.title": "Claude アカウントプール(実験的)",
"anthropicPool.enabledDesc": "429 時にアカウントをクールダウンしてフェイルオーバーします。新規セッションは 5 時間使用率が {threshold}% 未満のアカウントを優先します。",
"anthropicPool.disabledDesc": "アクティブな Claude アカウントのみを使用します。実験的ルーティングを受け入れる場合のみ有効にしてください。",
Expand Down
6 changes: 6 additions & 0 deletions gui/src/i18n/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -842,6 +842,12 @@ export const ko: Record<TKey, string> = {
"codexAuth.autoSwitchThresholdInvalid": "1~100 사이의 정수를 입력하세요",
"codexAuth.autoSwitchUpdated": "사용량 기반 선제 전환 설정을 저장했습니다",
"codexAuth.autoSwitchUpdateFailed": "사용량 기반 전환 설정 변경을 확인하지 못했습니다. 마지막으로 확인된 값을 표시합니다.",
"codexAuth.requestUserInput": "Default 모드에서 입력 요청",
"codexAuth.requestUserInputDesc": "Default 모드 세션에서 Codex가 일시 중지하고 request_user_input 도구로 질문할 수 있게 합니다.",
"codexAuth.requestUserInputUpdated": "기능 플래그가 업데이트되었습니다 - 새 세션부터 적용됩니다.",
"codexAuth.requestUserInputUpdatedRestart": "기능 플래그가 업데이트되었습니다 - 새 세션부터 적용됩니다. Codex 앱을 다시 시작하세요.",
"codexAuth.requestUserInputUpdateFailed": "기능 플래그를 업데이트하지 못했습니다. 변경된 내용이 없습니다.",
"codexAuth.requestUserInputLoadFailed": "config.toml에서 기능 플래그를 읽지 못했습니다.",
"anthropicPool.title": "Claude 계정 풀(실험적)",
"anthropicPool.enabledDesc": "429 시 계정을 쿨다운하고 장애 조치합니다. 새 세션은 5시간 사용량이 {threshold}% 미만인 계정을 우선합니다.",
"anthropicPool.disabledDesc": "활성 Claude 계정만 사용합니다. 실험적 라우팅을 감수할 때만 켜세요.",
Expand Down
6 changes: 6 additions & 0 deletions gui/src/i18n/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1252,6 +1252,12 @@ export const ru: Record<TKey, string> = {
"codexAuth.autoSwitchThresholdInvalid": "Введите целое число от 1 до 100",
"codexAuth.autoSwitchUpdated": "Проактивное переключение по использованию обновлено",
"codexAuth.autoSwitchUpdateFailed": "Не удалось подтвердить обновление переключения по использованию. Показано последнее подтверждённое значение.",
"codexAuth.requestUserInput": "Запрашивать ввод в режиме Default",
"codexAuth.requestUserInputDesc": "Позволяет Codex ставить сеанс Default на паузу и задавать вопросы через инструмент request_user_input.",
"codexAuth.requestUserInputUpdated": "Флаг обновлён - применяется к новым сеансам.",
"codexAuth.requestUserInputUpdatedRestart": "Флаг обновлён - применяется к новым сеансам. Перезапустите приложение Codex.",
"codexAuth.requestUserInputUpdateFailed": "Не удалось обновить флаг. Ничего не изменено.",
"codexAuth.requestUserInputLoadFailed": "Не удалось прочитать флаг из config.toml.",
"anthropicPool.title": "Пул аккаунтов Claude (экспериментально)",
"anthropicPool.enabledDesc": "При 429 аккаунт охлаждается и выполняется переключение. Новые сессии предпочитают использование ниже {threshold}% (полоса 5 часов).",
"anthropicPool.disabledDesc": "Используется только активный аккаунт Claude. Включайте только если принимаете экспериментальную маршрутизацию.",
Expand Down
6 changes: 6 additions & 0 deletions gui/src/i18n/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -835,6 +835,12 @@ export const zh: Record<TKey, string> = {
"codexAuth.autoSwitchThresholdInvalid": "请输入 1 到 100 之间的整数",
"codexAuth.autoSwitchUpdated": "基于用量的主动切换设置已更新",
"codexAuth.autoSwitchUpdateFailed": "无法确认基于用量的切换更新。当前显示最后一次确认的值。",
"codexAuth.requestUserInput": "在 Default 模式下请求输入",
"codexAuth.requestUserInputDesc": "允许 Codex 在 Default 模式会话中暂停,并通过 request_user_input 工具向你提问。",
"codexAuth.requestUserInputUpdated": "功能标志已更新 - 适用于新会话。",
"codexAuth.requestUserInputUpdatedRestart": "功能标志已更新 - 适用于新会话。请重启 Codex 应用。",
"codexAuth.requestUserInputUpdateFailed": "无法更新功能标志。未做任何更改。",
"codexAuth.requestUserInputLoadFailed": "无法从 config.toml 读取功能标志。",
"anthropicPool.title": "Claude 账户池(实验性)",
"anthropicPool.enabledDesc": "遇到 429 时冷却该账户并故障转移。新会话优先使用 5 小时用量低于 {threshold}% 的账户。",
"anthropicPool.disabledDesc": "仅使用当前活跃的 Claude 账户。仅在接受实验性路由时启用。",
Expand Down
8 changes: 7 additions & 1 deletion gui/src/pages/CodexAuth.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { useT } from "../i18n/shared";
import CodexAccountPool from "../components/CodexAccountPool";
import DefaultModeRequestUserInputSetting from "../components/DefaultModeRequestUserInputSetting";
import { codexAccountModeState, type CodexAccountModeState } from "../codex-multi-state";
import { ensureOpenAiProvider, openAiAccountProviderState, OpenAiEnableError } from "../provider-payload";
import { readSessionListCache, writeSessionListCache } from "../session-list-cache";
Expand Down Expand Up @@ -174,5 +175,10 @@ export default function CodexAuth({ apiBase }: { apiBase: string }) {
{enableError && <div className="notice notice-err" role="alert">{enableError}</div>}
</>;

return <CodexAccountPool apiBase={apiBase} accountModeState={accountModeState} banner={banner} />;
return (
<>
<CodexAccountPool apiBase={apiBase} accountModeState={accountModeState} banner={banner} />
<DefaultModeRequestUserInputSetting apiBase={apiBase} />
</>
);
}
8 changes: 8 additions & 0 deletions gui/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -1357,6 +1357,14 @@ dialog.modal-overlay::backdrop {
.codex-auto-switch-copy { flex: 1 1 auto; min-width: 0; }
.codex-auto-switch-copy .card-sub { padding: 2px 0 0; }
.codex-auto-switch-controls { display: flex; align-items: flex-end; gap: 12px; flex: 0 0 auto; margin-left: auto; }
.codex-request-user-input-card { gap: 16px; flex-wrap: wrap; }
.codex-request-user-input-copy { flex: 1 1 auto; min-width: 0; }
.codex-request-user-input-copy .card-sub { padding: 2px 0 0; }
.codex-request-user-input-config { display: block; margin-top: 6px; overflow-wrap: anywhere; font-size: var(--text-label); color: var(--muted); }
.codex-request-user-input-controls { display: flex; align-items: flex-end; gap: 12px; flex: 0 0 auto; margin-left: auto; }
.codex-request-user-input-controls > .toggle { margin-left: auto; }
.codex-request-user-input-feedback { flex: 1 0 100%; margin-top: -8px; color: var(--muted); font-size: var(--text-label); line-height: var(--leading-body); text-align: right; }
.codex-request-user-input-feedback.is-error { color: var(--red); }
/* The toggle now lives in the slot below, which carries the auto margin itself. */
/*
Toggle slot: matches the height of the threshold compound so the 20px toggle centres against
Expand Down
Loading
Loading