Skip to content

Commit a1cea88

Browse files
committed
merge: current dev into ClinePass provider branch
2 parents 20ea424 + c72acb3 commit a1cea88

16 files changed

Lines changed: 723 additions & 29 deletions

File tree

docs-site/src/content/docs/reference/configuration/agents.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,15 @@ The management API exposes `GET`/`PUT /api/v2`, `/api/injection-model`, `/api/ef
3131
`/api/subagent-models`, and `/api/subagent-model-fallback`. Injection-model updates are partial;
3232
the custom prompt is the `prompt` field on that API.
3333

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

3645
The effective v2 roster is the configured, picker-visible, priority-sorted first five models that
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import { useCallback, useEffect, useRef, useState } from "react";
2+
import { useT } from "../i18n/shared";
3+
import { readJsonOrThrow } from "../fetch-json";
4+
5+
const FEATURE_ENDPOINT = "/api/codex-auth/features/default-mode-request-user-input";
6+
7+
type Feedback = { tone: "ok" | "err"; message: string } | null;
8+
9+
/**
10+
* Codex Auth page toggle for Codex's own `default_mode_request_user_input`
11+
* feature flag. Reads/writes $CODEX_HOME/config.toml through the management
12+
* API, which flips the flag via the official `codex features` CLI.
13+
*/
14+
export default function DefaultModeRequestUserInputSetting({ apiBase }: { apiBase: string }) {
15+
const t = useT();
16+
const [enabled, setEnabled] = useState(false);
17+
const [hydrated, setHydrated] = useState(false);
18+
const [saving, setSaving] = useState(false);
19+
const [loadError, setLoadError] = useState(false);
20+
const [feedback, setFeedback] = useState<Feedback>(null);
21+
const savingRef = useRef(false);
22+
const enabledRef = useRef(false);
23+
const loadGenerationRef = useRef(0);
24+
25+
const load = useCallback(async () => {
26+
// A poll landing between the optimistic flip and the PUT response must not
27+
// revert the UI to the server's pre-save value. The generation also drops
28+
// GETs that were already in flight when a save started.
29+
if (savingRef.current) return;
30+
const generation = ++loadGenerationRef.current;
31+
try {
32+
const res = await fetch(`${apiBase}${FEATURE_ENDPOINT}`);
33+
if (!res.ok) throw new Error("load");
34+
const payload = await res.json() as { enabled?: unknown };
35+
if (savingRef.current || generation !== loadGenerationRef.current) return;
36+
enabledRef.current = payload.enabled === true;
37+
setEnabled(enabledRef.current);
38+
setHydrated(true);
39+
setLoadError(false);
40+
} catch {
41+
if (!savingRef.current && generation === loadGenerationRef.current) setLoadError(true);
42+
}
43+
}, [apiBase]);
44+
45+
useEffect(() => {
46+
const timeout = window.setTimeout(() => { void load(); }, 0);
47+
const interval = window.setInterval(() => { void load(); }, 30_000);
48+
return () => {
49+
window.clearTimeout(timeout);
50+
window.clearInterval(interval);
51+
};
52+
}, [load]);
53+
54+
const toggle = useCallback(async () => {
55+
if (savingRef.current || !hydrated || loadError) return;
56+
const next = !enabledRef.current;
57+
const previous = enabledRef.current;
58+
enabledRef.current = next;
59+
setEnabled(next);
60+
savingRef.current = true;
61+
setSaving(true);
62+
setFeedback(null);
63+
loadGenerationRef.current++;
64+
try {
65+
const res = await fetch(`${apiBase}${FEATURE_ENDPOINT}`, {
66+
method: "PUT",
67+
headers: { "content-type": "application/json" },
68+
body: JSON.stringify({ enabled: next }),
69+
});
70+
const payload = (await readJsonOrThrow<{ ok?: boolean; enabled?: unknown; changed?: unknown }>(res)) ?? {};
71+
if (payload.ok !== true) throw new Error(String(res.status));
72+
enabledRef.current = payload.enabled === true;
73+
setEnabled(enabledRef.current);
74+
setHydrated(true);
75+
setFeedback({
76+
tone: "ok",
77+
message: t(payload.changed === true ? "codexAuth.requestUserInputUpdatedRestart" : "codexAuth.requestUserInputUpdated"),
78+
});
79+
} catch (error) {
80+
enabledRef.current = previous;
81+
setEnabled(previous);
82+
const reason = error instanceof Error && error.message && !/^HTTP \d{3}$/.test(error.message)
83+
? error.message
84+
: t("codexAuth.requestUserInputUpdateFailed");
85+
setFeedback({ tone: "err", message: reason });
86+
} finally {
87+
savingRef.current = false;
88+
setSaving(false);
89+
}
90+
}, [apiBase, hydrated, loadError, t]);
91+
92+
const controlsDisabled = saving || !hydrated || loadError;
93+
94+
return (
95+
<div
96+
className="card card-row codex-request-user-input-card"
97+
style={{ marginTop: 16 }}
98+
aria-busy={saving || (!hydrated && !loadError) || undefined}
99+
>
100+
<div className="codex-request-user-input-copy">
101+
<strong>{t("codexAuth.requestUserInput")}</strong>
102+
<div className="card-sub" role={loadError ? "alert" : undefined}>
103+
{loadError ? t("codexAuth.requestUserInputLoadFailed") : t("codexAuth.requestUserInputDesc")}
104+
</div>
105+
<code className="mono codex-request-user-input-config">
106+
{`[features]\ndefault_mode_request_user_input = true`}
107+
</code>
108+
</div>
109+
<div className="codex-request-user-input-controls">
110+
{loadError && (
111+
<button type="button" className="btn btn-ghost btn-sm" onClick={() => { void load(); }}>
112+
{t("common.retry")}
113+
</button>
114+
)}
115+
<button
116+
type="button"
117+
className={`toggle ${enabled ? "on" : ""}`}
118+
onClick={() => { void toggle(); }}
119+
disabled={controlsDisabled}
120+
aria-pressed={enabled}
121+
aria-label={t("codexAuth.requestUserInput")}
122+
title={t("codexAuth.requestUserInput")}
123+
>
124+
<span className="toggle-knob" />
125+
</button>
126+
</div>
127+
{feedback && (
128+
<div
129+
className={`codex-request-user-input-feedback${feedback.tone === "err" ? " is-error" : ""}`}
130+
role={feedback.tone === "err" ? "alert" : "status"}
131+
aria-atomic="true"
132+
>
133+
{feedback.message}
134+
</div>
135+
)}
136+
</div>
137+
);
138+
}

gui/src/i18n/de.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -818,6 +818,12 @@ export const de: Record<TKey, string> = {
818818
"codexAuth.autoSwitchThresholdInvalid": "Gib eine ganze Zahl von 1 bis 100 ein",
819819
"codexAuth.autoSwitchUpdated": "Der proaktive Wechsel nach Nutzung wurde aktualisiert",
820820
"codexAuth.autoSwitchUpdateFailed": "Die Aktualisierung des nutzungsbasierten Wechsels konnte nicht bestätigt werden. Der zuletzt bestätigte Wert wird angezeigt.",
821+
"codexAuth.requestUserInput": "Im Default-Modus nachfragen",
822+
"codexAuth.requestUserInputDesc": "Erlaubt Codex, eine Session im Default-Modus zu pausieren und dir über das request_user_input-Tool Fragen zu stellen.",
823+
"codexAuth.requestUserInputUpdated": "Feature-Flag aktualisiert - gilt für neue Sessions.",
824+
"codexAuth.requestUserInputUpdatedRestart": "Feature-Flag aktualisiert - gilt für neue Sessions. Starte die Codex-App neu, damit es wirksam wird.",
825+
"codexAuth.requestUserInputUpdateFailed": "Feature-Flag konnte nicht aktualisiert werden. Es wurde nichts geändert.",
826+
"codexAuth.requestUserInputLoadFailed": "Feature-Flag konnte nicht aus config.toml gelesen werden.",
821827
"anthropicPool.title": "Claude-Kontenpool (experimentell)",
822828
"anthropicPool.enabledDesc": "Bei 429 wird das Konto gekühlt und umgeschaltet. Neue Sitzungen bevorzugen Nutzung unter {threshold}% (5-Stunden-Balken).",
823829
"anthropicPool.disabledDesc": "Nutzt nur das aktive Claude-Konto. Nur aktivieren, wenn experimentelles Routing akzeptabel ist.",

gui/src/i18n/en.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1260,6 +1260,12 @@ export const en = {
12601260
"codexAuth.autoSwitchThresholdInvalid": "Enter a whole number from 1 to 100",
12611261
"codexAuth.autoSwitchUpdated": "Usage-based proactive switching updated",
12621262
"codexAuth.autoSwitchUpdateFailed": "The usage-based switching update could not be confirmed. The last confirmed value is shown.",
1263+
"codexAuth.requestUserInput": "Ask for input in Default mode",
1264+
"codexAuth.requestUserInputDesc": "Lets Codex pause a Default-mode session and ask you questions with the request_user_input tool.",
1265+
"codexAuth.requestUserInputUpdated": "Feature flag updated - applies to new sessions.",
1266+
"codexAuth.requestUserInputUpdatedRestart": "Feature flag updated - applies to new sessions. Restart the Codex app to pick it up.",
1267+
"codexAuth.requestUserInputUpdateFailed": "Could not update the feature flag. Nothing was changed.",
1268+
"codexAuth.requestUserInputLoadFailed": "Could not read the feature flag from config.toml.",
12631269

12641270
"anthropicPool.title": "Claude account pool (experimental)",
12651271
"anthropicPool.enabledDesc": "On 429, cools the account and fails over. New sessions prefer usage under {threshold}% (5-hour bar).",

gui/src/i18n/ja.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1210,6 +1210,12 @@ export const ja: Record<TKey, string> = {
12101210
"codexAuth.autoSwitchThresholdInvalid": "1 から 100 までの整数を入力してください",
12111211
"codexAuth.autoSwitchUpdated": "使用量ベースのプロアクティブ切り替え設定を更新しました",
12121212
"codexAuth.autoSwitchUpdateFailed": "使用量ベースの切り替え更新を確認できませんでした。最後に確認された値を表示しています。",
1213+
"codexAuth.requestUserInput": "Default モードで入力を求める",
1214+
"codexAuth.requestUserInputDesc": "Default モードのセッションで Codex が一時停止し、request_user_input ツールで質問できるようにします。",
1215+
"codexAuth.requestUserInputUpdated": "機能フラグを更新しました - 新しいセッションから適用されます。",
1216+
"codexAuth.requestUserInputUpdatedRestart": "機能フラグを更新しました - 新しいセッションから適用されます。Codex アプリを再起動してください。",
1217+
"codexAuth.requestUserInputUpdateFailed": "機能フラグを更新できませんでした。変更はありません。",
1218+
"codexAuth.requestUserInputLoadFailed": "config.toml から機能フラグを読み込めませんでした。",
12131219
"anthropicPool.title": "Claude アカウントプール(実験的)",
12141220
"anthropicPool.enabledDesc": "429 時にアカウントをクールダウンしてフェイルオーバーします。新規セッションは 5 時間使用率が {threshold}% 未満のアカウントを優先します。",
12151221
"anthropicPool.disabledDesc": "アクティブな Claude アカウントのみを使用します。実験的ルーティングを受け入れる場合のみ有効にしてください。",

gui/src/i18n/ko.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -842,6 +842,12 @@ export const ko: Record<TKey, string> = {
842842
"codexAuth.autoSwitchThresholdInvalid": "1~100 사이의 정수를 입력하세요",
843843
"codexAuth.autoSwitchUpdated": "사용량 기반 선제 전환 설정을 저장했습니다",
844844
"codexAuth.autoSwitchUpdateFailed": "사용량 기반 전환 설정 변경을 확인하지 못했습니다. 마지막으로 확인된 값을 표시합니다.",
845+
"codexAuth.requestUserInput": "Default 모드에서 입력 요청",
846+
"codexAuth.requestUserInputDesc": "Default 모드 세션에서 Codex가 일시 중지하고 request_user_input 도구로 질문할 수 있게 합니다.",
847+
"codexAuth.requestUserInputUpdated": "기능 플래그가 업데이트되었습니다 - 새 세션부터 적용됩니다.",
848+
"codexAuth.requestUserInputUpdatedRestart": "기능 플래그가 업데이트되었습니다 - 새 세션부터 적용됩니다. Codex 앱을 다시 시작하세요.",
849+
"codexAuth.requestUserInputUpdateFailed": "기능 플래그를 업데이트하지 못했습니다. 변경된 내용이 없습니다.",
850+
"codexAuth.requestUserInputLoadFailed": "config.toml에서 기능 플래그를 읽지 못했습니다.",
845851
"anthropicPool.title": "Claude 계정 풀(실험적)",
846852
"anthropicPool.enabledDesc": "429 시 계정을 쿨다운하고 장애 조치합니다. 새 세션은 5시간 사용량이 {threshold}% 미만인 계정을 우선합니다.",
847853
"anthropicPool.disabledDesc": "활성 Claude 계정만 사용합니다. 실험적 라우팅을 감수할 때만 켜세요.",

gui/src/i18n/ru.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1252,6 +1252,12 @@ export const ru: Record<TKey, string> = {
12521252
"codexAuth.autoSwitchThresholdInvalid": "Введите целое число от 1 до 100",
12531253
"codexAuth.autoSwitchUpdated": "Проактивное переключение по использованию обновлено",
12541254
"codexAuth.autoSwitchUpdateFailed": "Не удалось подтвердить обновление переключения по использованию. Показано последнее подтверждённое значение.",
1255+
"codexAuth.requestUserInput": "Запрашивать ввод в режиме Default",
1256+
"codexAuth.requestUserInputDesc": "Позволяет Codex ставить сеанс Default на паузу и задавать вопросы через инструмент request_user_input.",
1257+
"codexAuth.requestUserInputUpdated": "Флаг обновлён - применяется к новым сеансам.",
1258+
"codexAuth.requestUserInputUpdatedRestart": "Флаг обновлён - применяется к новым сеансам. Перезапустите приложение Codex.",
1259+
"codexAuth.requestUserInputUpdateFailed": "Не удалось обновить флаг. Ничего не изменено.",
1260+
"codexAuth.requestUserInputLoadFailed": "Не удалось прочитать флаг из config.toml.",
12551261
"anthropicPool.title": "Пул аккаунтов Claude (экспериментально)",
12561262
"anthropicPool.enabledDesc": "При 429 аккаунт охлаждается и выполняется переключение. Новые сессии предпочитают использование ниже {threshold}% (полоса 5 часов).",
12571263
"anthropicPool.disabledDesc": "Используется только активный аккаунт Claude. Включайте только если принимаете экспериментальную маршрутизацию.",

gui/src/i18n/zh.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -835,6 +835,12 @@ export const zh: Record<TKey, string> = {
835835
"codexAuth.autoSwitchThresholdInvalid": "请输入 1 到 100 之间的整数",
836836
"codexAuth.autoSwitchUpdated": "基于用量的主动切换设置已更新",
837837
"codexAuth.autoSwitchUpdateFailed": "无法确认基于用量的切换更新。当前显示最后一次确认的值。",
838+
"codexAuth.requestUserInput": "在 Default 模式下请求输入",
839+
"codexAuth.requestUserInputDesc": "允许 Codex 在 Default 模式会话中暂停,并通过 request_user_input 工具向你提问。",
840+
"codexAuth.requestUserInputUpdated": "功能标志已更新 - 适用于新会话。",
841+
"codexAuth.requestUserInputUpdatedRestart": "功能标志已更新 - 适用于新会话。请重启 Codex 应用。",
842+
"codexAuth.requestUserInputUpdateFailed": "无法更新功能标志。未做任何更改。",
843+
"codexAuth.requestUserInputLoadFailed": "无法从 config.toml 读取功能标志。",
838844
"anthropicPool.title": "Claude 账户池(实验性)",
839845
"anthropicPool.enabledDesc": "遇到 429 时冷却该账户并故障转移。新会话优先使用 5 小时用量低于 {threshold}% 的账户。",
840846
"anthropicPool.disabledDesc": "仅使用当前活跃的 Claude 账户。仅在接受实验性路由时启用。",

gui/src/pages/CodexAuth.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useCallback, useEffect, useRef, useState } from "react";
22
import { useT } from "../i18n/shared";
33
import CodexAccountPool from "../components/CodexAccountPool";
4+
import DefaultModeRequestUserInputSetting from "../components/DefaultModeRequestUserInputSetting";
45
import { codexAccountModeState, type CodexAccountModeState } from "../codex-multi-state";
56
import { ensureOpenAiProvider, openAiAccountProviderState, OpenAiEnableError } from "../provider-payload";
67
import { readSessionListCache, writeSessionListCache } from "../session-list-cache";
@@ -174,5 +175,10 @@ export default function CodexAuth({ apiBase }: { apiBase: string }) {
174175
{enableError && <div className="notice notice-err" role="alert">{enableError}</div>}
175176
</>;
176177

177-
return <CodexAccountPool apiBase={apiBase} accountModeState={accountModeState} banner={banner} />;
178+
return (
179+
<>
180+
<CodexAccountPool apiBase={apiBase} accountModeState={accountModeState} banner={banner} />
181+
<DefaultModeRequestUserInputSetting apiBase={apiBase} />
182+
</>
183+
);
178184
}

gui/src/styles.css

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1357,6 +1357,14 @@ dialog.modal-overlay::backdrop {
13571357
.codex-auto-switch-copy { flex: 1 1 auto; min-width: 0; }
13581358
.codex-auto-switch-copy .card-sub { padding: 2px 0 0; }
13591359
.codex-auto-switch-controls { display: flex; align-items: flex-end; gap: 12px; flex: 0 0 auto; margin-left: auto; }
1360+
.codex-request-user-input-card { gap: 16px; flex-wrap: wrap; }
1361+
.codex-request-user-input-copy { flex: 1 1 auto; min-width: 0; }
1362+
.codex-request-user-input-copy .card-sub { padding: 2px 0 0; }
1363+
.codex-request-user-input-config { display: block; margin-top: 6px; overflow-wrap: anywhere; font-size: var(--text-label); color: var(--muted); }
1364+
.codex-request-user-input-controls { display: flex; align-items: flex-end; gap: 12px; flex: 0 0 auto; margin-left: auto; }
1365+
.codex-request-user-input-controls > .toggle { margin-left: auto; }
1366+
.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; }
1367+
.codex-request-user-input-feedback.is-error { color: var(--red); }
13601368
/* The toggle now lives in the slot below, which carries the auto margin itself. */
13611369
/*
13621370
Toggle slot: matches the height of the threshold compound so the 20px toggle centres against

0 commit comments

Comments
 (0)