From 9d8d11abdee0151c2a283ff4c3ace728a02a0455 Mon Sep 17 00:00:00 2001 From: x3M3x <98298256+x3M3x@users.noreply.github.com> Date: Mon, 7 Sep 2026 18:24:13 +0900 Subject: [PATCH 1/6] fix(service): carry startup-health cache portion of #3863 [skip ci] Path-filtered source commit: 960621616c439e69b967981c290f2377ba9465fa. Config-route wiring excluded under lane ownership. Co-authored-by: x3M3x <98298256+x3M3x@users.noreply.github.com> (cherry picked from commit 197bf2e2bff362b9a135389741f1acf91670ded0) --- src/server/startup-health-cache.ts | 16 +++++++++++ tests/service/autostart-health.test.ts | 39 +++++++++++++++++++++++++- 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/src/server/startup-health-cache.ts b/src/server/startup-health-cache.ts index 70380eb4ed..571d81b549 100644 --- a/src/server/startup-health-cache.ts +++ b/src/server/startup-health-cache.ts @@ -50,6 +50,22 @@ export interface StartupHealthCacheDeps { ) => Promise; } +/** + * Return the last completed probe immediately and refresh it in the background. + * + * Settings are consumed by several dashboard controls. They must not block on a + * Windows service-manager probe; the dedicated /api/startup-health route owns + * the fresh, bounded diagnostic read. + */ +export function getStartupHealthSnapshot( + config: Pick, + deps: StartupHealthCacheDeps = {}, +): StartupHealth { + const now = deps.now ?? Date.now; + if (!cached || now() - cached.timestamp >= CACHE_TTL_MS) refreshInBackground(config, deps); + return cached ? markStartupHealthDiagnosticStale(cached.value) : conservativeFallback(config); +} + export function markStartupHealthDiagnosticStale(value: StartupHealth): StartupHealth { if (!value.localRoutingDependency) return { ...value, diagnosticStale: true }; return { diff --git a/tests/service/autostart-health.test.ts b/tests/service/autostart-health.test.ts index 639f1b34c3..48bb7b5395 100644 --- a/tests/service/autostart-health.test.ts +++ b/tests/service/autostart-health.test.ts @@ -3,7 +3,7 @@ import { deriveStartupHealth, formatStartupRoutingDetail, startupHealthSummary } import { unusedProxyWarningLines } from "../../src/cli/status"; import { classifyCodexRouting, hasInjectedCodexRouting } from "../../src/codex/inject"; import { handleManagementAPI } from "../../src/server/management-api"; -import { getCachedStartupHealth, invalidateStartupHealthCache, markStartupHealthDiagnosticStale } from "../../src/server/startup-health-cache"; +import { getCachedStartupHealth, getStartupHealthSnapshot, invalidateStartupHealthCache, markStartupHealthDiagnosticStale } from "../../src/server/startup-health-cache"; import type { OcxConfig } from "../../src/types"; const base = { @@ -277,6 +277,43 @@ describe("Codex startup health", () => { await pendingProbe; invalidateStartupHealthCache(); }); + + test("settings snapshot starts a probe without waiting for it", async () => { + invalidateStartupHealthCache(); + let releaseProbe!: (value: ReturnType) => void; + const pendingProbe = new Promise>(resolve => { + releaseProbe = resolve; + }); + + const health = getStartupHealthSnapshot( + { codexAutoStart: true }, + { probe: async () => pendingProbe }, + ); + + expect(health.diagnosticStale).toBe(true); + releaseProbe(deriveStartupHealth({ ...base, routingKind: "native" })); + await pendingProbe; + invalidateStartupHealthCache(); + }); + + test("settings GET uses the non-blocking startup-health snapshot in production", async () => { + invalidateStartupHealthCache(); + const url = new URL("http://localhost/api/settings"); + + const response = await Promise.race([ + handleManagementAPI( + new Request(url), + url, + { port: 10100, providers: {}, defaultProvider: "openai", codexAutoStart: true } as OcxConfig, + ), + new Promise(resolve => setTimeout(() => resolve(null), 100)), + ]); + + expect(response?.status).toBe(200); + const body = await response!.json() as { startupHealth?: { diagnosticStale?: boolean } }; + expect(body.startupHealth?.diagnosticStale).toBe(true); + invalidateStartupHealthCache(); + }); }); import { ManagementRequest as Request } from "../helpers/management-auth"; From 91b54b79bc780011031b00354621cc9fcaef360e Mon Sep 17 00:00:00 2001 From: t Date: Mon, 7 Sep 2026 18:25:01 +0900 Subject: [PATCH 2/6] fix(service): preserve fresh health and contain detached probe failures [skip ci] Co-authored-by: x3M3x <98298256+x3M3x@users.noreply.github.com> --- src/server/startup-health-cache.ts | 26 ++++++---- tests/service/autostart-health.test.ts | 72 +++++++++++++++++++++----- 2 files changed, 75 insertions(+), 23 deletions(-) diff --git a/src/server/startup-health-cache.ts b/src/server/startup-health-cache.ts index 571d81b549..2c12e0bbc3 100644 --- a/src/server/startup-health-cache.ts +++ b/src/server/startup-health-cache.ts @@ -62,7 +62,8 @@ export function getStartupHealthSnapshot( deps: StartupHealthCacheDeps = {}, ): StartupHealth { const now = deps.now ?? Date.now; - if (!cached || now() - cached.timestamp >= CACHE_TTL_MS) refreshInBackground(config, deps); + if (cached && now() - cached.timestamp < CACHE_TTL_MS) return cached.value; + refreshInBackground(config, deps); return cached ? markStartupHealthDiagnosticStale(cached.value) : conservativeFallback(config); } @@ -150,15 +151,20 @@ function refreshInBackground( ): void { if (inflight) return; const startedGeneration = generation; - const probe = (deps.probe ?? runProbe)(config).then(value => { - if (startedGeneration === generation) { - cached = { timestamp: (deps.now ?? Date.now)(), value }; - } - return value; - }); - inflight = probe.finally(() => { - if (inflight === probe || startedGeneration === generation) inflight = null; - }); + const probe: Promise = Promise.resolve() + .then(() => (deps.probe ?? runProbe)(config)) + .then(value => { + if (startedGeneration === generation) { + cached = { timestamp: (deps.now ?? Date.now)(), value }; + } + return value; + }) + .catch(() => cached ? markStartupHealthDiagnosticStale(cached.value) : conservativeFallback(config)) + .finally(() => { + // An invalidated probe must never clear the newer generation's flight. + if (inflight === probe) inflight = null; + }); + inflight = probe; } /** Stale-while-revalidate: service-manager probes never hold open a model/UI request. */ diff --git a/tests/service/autostart-health.test.ts b/tests/service/autostart-health.test.ts index 48bb7b5395..213a118e2b 100644 --- a/tests/service/autostart-health.test.ts +++ b/tests/service/autostart-health.test.ts @@ -296,22 +296,68 @@ describe("Codex startup health", () => { invalidateStartupHealthCache(); }); - test("settings GET uses the non-blocking startup-health snapshot in production", async () => { + test("snapshot preserves fresh protection and returns expired protection before a controlled probe settles", async () => { invalidateStartupHealthCache(); - const url = new URL("http://localhost/api/settings"); + let now = 1_000; + const config = { codexAutoStart: true }; + const protectedHealth = deriveStartupHealth({ ...base, serviceInstalled: true, serviceViable: true, serviceEnabled: true, serviceRunning: true }); + await getCachedStartupHealth(config, { now: () => now, probe: async () => protectedHealth, waitForProbe: probe => probe }); + let calls = 0; + let release!: (value: typeof protectedHealth) => void; + const pending = new Promise(resolve => { release = resolve; }); + const deps = { now: () => now, probe: () => { calls += 1; return pending; }, waitForProbe: (probe: Promise) => probe }; + expect(getStartupHealthSnapshot(config, deps)).toBe(protectedHealth); + expect(calls).toBe(0); + now += 30_000; + const snapshot = getStartupHealthSnapshot(config, deps); + expect(snapshot).toMatchObject({ diagnosticStale: true, status: "at-risk", rebootSafe: false }); + // Snapshot has returned while the manually controlled probe remains unresolved. + expect(getStartupHealthSnapshot(config, deps)).toEqual(snapshot); + const fresh = getCachedStartupHealth(config, deps); + const replacement = deriveStartupHealth({ ...base, routingKind: "custom-remote" }); + release(replacement); + expect(await fresh).toBe(replacement); + expect(calls).toBe(1); + invalidateStartupHealthCache(); + }); - const response = await Promise.race([ - handleManagementAPI( - new Request(url), - url, - { port: 10100, providers: {}, defaultProvider: "openai", codexAutoStart: true } as OcxConfig, - ), - new Promise(resolve => setTimeout(() => resolve(null), 100)), - ]); + test.each(["reject", "throw"])("detached snapshot probe handles %s and permits a later retry", async (failure) => { + invalidateStartupHealthCache(); + const config = { codexAutoStart: true }; + const failed = getStartupHealthSnapshot(config, { probe: () => { + if (failure === "throw") throw new Error("controlled probe failure"); + return Promise.reject(new Error("controlled probe failure")); + } }); + expect(failed.diagnosticStale).toBe(true); + const settled = await getCachedStartupHealth(config, { waitForProbe: probe => probe }); + expect(settled.diagnosticStale).toBe(true); + const replacement = deriveStartupHealth({ ...base, routingKind: "native" }); + expect(await getCachedStartupHealth(config, { probe: async () => replacement, waitForProbe: probe => probe })).toBe(replacement); + invalidateStartupHealthCache(); + }); - expect(response?.status).toBe(200); - const body = await response!.json() as { startupHealth?: { diagnosticStale?: boolean } }; - expect(body.startupHealth?.diagnosticStale).toBe(true); + test("invalidated probe cannot replace or clear a newer flight", async () => { + invalidateStartupHealthCache(); + const config = { codexAutoStart: true }; + type Health = ReturnType; + let oldRelease!: (value: Health) => void; + let newRelease!: (value: Health) => void; + const oldProbe = new Promise(resolve => { oldRelease = resolve; }); + const newProbe = new Promise(resolve => { newRelease = resolve; }); + getStartupHealthSnapshot(config, { probe: () => oldProbe }); + const oldWait = getCachedStartupHealth(config, { waitForProbe: probe => probe }); + invalidateStartupHealthCache(); + getStartupHealthSnapshot(config, { probe: () => newProbe }); + const newer = getCachedStartupHealth(config, { waitForProbe: probe => probe }); + oldRelease(deriveStartupHealth(base)); + await oldWait; + let spuriousCalls = 0; + getStartupHealthSnapshot(config, { probe: async () => { spuriousCalls += 1; return deriveStartupHealth(base); } }); + const expected = deriveStartupHealth({ ...base, routingKind: "native" }); + newRelease(expected); + expect(await newer).toBe(expected); + expect(getStartupHealthSnapshot(config)).toBe(expected); + expect(spuriousCalls).toBe(0); invalidateStartupHealthCache(); }); }); From eb5ff6c582d694db96e94e5d0a58ed5dbc6843f3 Mon Sep 17 00:00:00 2001 From: Robin Bially <7304732+RobinBially@users.noreply.github.com> Date: Mon, 7 Sep 2026 09:57:05 +0200 Subject: [PATCH 3/6] feat(gui): add opt-in Codex Desktop sign-in toggle [skip ci] (cherry picked from commit 0f21769f346e64c2c6b2a03ff8d09b3deaab9562) Co-authored-by: RobinBially <7304732+RobinBially@users.noreply.github.com> --- .../content/docs/guides/codex-integration.md | 9 ++ docs/pr-assets/codex-desktop-opt-in.jpg | Bin 0 -> 29051 bytes gui/src/i18n/de.ts | 2 + gui/src/i18n/en.ts | 2 + gui/src/i18n/fr.ts | 2 + gui/src/i18n/ja.ts | 2 + gui/src/i18n/ko.ts | 2 + gui/src/i18n/ru.ts | 2 + gui/src/i18n/tr.ts | 2 + gui/src/i18n/zh-TW.ts | 2 + gui/src/i18n/zh.ts | 2 + gui/src/pages/dashboard-overview-sections.tsx | 28 ++++++- gui/src/pages/dashboard-shared.ts | 2 + gui/src/pages/use-dashboard-data.ts | 23 +++-- gui/tests/vision-sidecar-dashboard.test.tsx | 79 +++++++++++++++++- tests/codex-integration/codex-inject.test.ts | 4 +- 16 files changed, 146 insertions(+), 17 deletions(-) create mode 100644 docs/pr-assets/codex-desktop-opt-in.jpg diff --git a/docs-site/src/content/docs/guides/codex-integration.md b/docs-site/src/content/docs/guides/codex-integration.md index 7d66e72c3f..ff2df04fdd 100644 --- a/docs-site/src/content/docs/guides/codex-integration.md +++ b/docs-site/src/content/docs/guides/codex-integration.md @@ -215,6 +215,15 @@ HTTP/SSE. ### Authless Codex Desktop (opt-in) +In **Dashboard → Overview**, **Open Codex without signing in** controls this existing +opt-in preference. The switch defaults to **off** when the setting is absent or false; +an existing explicit `codexDesktopAuthless: true` stays enabled. The dashboard saves +the preference and runs a full sync. Restart Codex Desktop after changing it. +If synchronization fails, the saved preference remains and the dashboard shows the error; +retry **Sync** before restarting. Account-gated Desktop features may be unavailable +when enabled. Upstream credentials, local eligibility, remote admission authentication +and user-owned gateway settings retain their existing requirements. + Codex Desktop shows its ChatGPT login screen whenever the active provider requires OpenAI auth. If your OpenCodex setup never uses ChatGPT credentials (routed providers only, or a blocked `chatgpt.com`), you can opt out of that gate: diff --git a/docs/pr-assets/codex-desktop-opt-in.jpg b/docs/pr-assets/codex-desktop-opt-in.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d06203e7113cc54e87b1272e272d8f943c5b4805 GIT binary patch literal 29051 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 literal 0 HcmV?d00001 diff --git a/gui/src/i18n/de.ts b/gui/src/i18n/de.ts index 5faab4b356..495104bd42 100644 --- a/gui/src/i18n/de.ts +++ b/gui/src/i18n/de.ts @@ -299,6 +299,8 @@ export const de: Record = { "models.staleBanner": "Codex zeigt eine ältere Modellliste als dieser Katalog. Starte Codex neu, um sie neu zu laden.", "dash.codexAutoStart": "opencodex mit Codex starten", "dash.codexAutoStartHint": "Erlaubt einem installierten Launcher-Shim, ocx ensure auszuführen. Diese Einstellung installiert keinen Neustartschutz; prüfe den effektiven Zustand unter Startsicherheit.", + "dash.codexDesktopAuthless": "Codex ohne Anmeldung öffnen", + "dash.codexDesktopAuthlessHint": "Standardmäßig aus. Überspringt die separate Desktop-Anmeldung bei geeigneten lokalen Verbindungen. Zugangsdaten für den Anbieter bleiben erforderlich. Codex nach einer Änderung neu starten. Kontogebundene Desktop-Funktionen können fehlen.", "dash.searchModel": "Such-Sidecar-Modell", "dash.searchModelHint": "Modell für web_search bei nicht über OpenAI gerouteten Modellen. Erfordert ChatGPT-Login.", "dash.searchReasoning": "Such-Reasoning-Aufwand", diff --git a/gui/src/i18n/en.ts b/gui/src/i18n/en.ts index c71208942f..22a3807858 100644 --- a/gui/src/i18n/en.ts +++ b/gui/src/i18n/en.ts @@ -311,6 +311,8 @@ export const en = { "models.staleBanner": "Codex is showing an older model list than this catalog. Restart Codex to reload it.", "dash.codexAutoStart": "Start opencodex with Codex", "dash.codexAutoStartHint": "Allows an installed launcher shim to run ocx ensure. This setting does not install restart protection; check Startup safety for the effective state.", + "dash.codexDesktopAuthless": "Open Codex without signing in", + "dash.codexDesktopAuthlessHint": "Off by default. Skip the separate Desktop sign-in for eligible local connections. Upstream credentials are still required. Restart Codex after changing this setting. Account-gated Desktop features may be unavailable.", "dash.searchModel": "Search sidecar model", "dash.searchModelHint": "Model used for web_search on non-OpenAI routed models. Requires ChatGPT login.", "dash.searchReasoning": "Search reasoning effort", diff --git a/gui/src/i18n/fr.ts b/gui/src/i18n/fr.ts index e1b3519ef1..9f0f265173 100644 --- a/gui/src/i18n/fr.ts +++ b/gui/src/i18n/fr.ts @@ -301,6 +301,8 @@ export const fr: Record = { "models.staleBanner": "Codex affiche une liste de modèles plus ancienne que ce catalogue. Redémarrez Codex pour la recharger.", "dash.codexAutoStart": "Démarrer opencodex avec Codex", "dash.codexAutoStartHint": "Permet à un mécanisme de lancement installé d’exécuter ocx ensure. Ce réglage n’installe pas de protection au redémarrage ; consultez Sécurité du démarrage pour connaître l’état effectif.", + "dash.codexDesktopAuthless": "Ouvrir Codex sans se connecter", + "dash.codexDesktopAuthlessHint": "Désactivé par défaut. Ignore la connexion Desktop séparée pour les connexions locales admissibles. Les identifiants du fournisseur restent nécessaires. Redémarrez Codex après toute modification. Certaines fonctions Desktop liées au compte peuvent être indisponibles.", "dash.searchModel": "Modèle auxiliaire de recherche", "dash.searchModelHint": "Modèle utilisé pour web_search sur les modèles routés autres qu’OpenAI. Nécessite une connexion à ChatGPT.", "dash.searchReasoning": "Effort de raisonnement pour la recherche", diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts index cf94831583..55a6fe2495 100644 --- a/gui/src/i18n/ja.ts +++ b/gui/src/i18n/ja.ts @@ -308,6 +308,8 @@ export const ja: Record = { "models.staleBanner": "Codex はこのカタログより古いモデル一覧を表示しています。Codex を再起動すると読み直されます。", "dash.codexAutoStart": "Codex と一緒に opencodex を起動", "dash.codexAutoStartHint": "インストール済み launcher shim に ocx ensure の実行を許可します。この設定だけでは再起動保護はインストールされません。起動安全性で実際の状態を確認してください。", + "dash.codexDesktopAuthless": "ログインせずに Codex を開く", + "dash.codexDesktopAuthlessHint": "既定ではオフです。対象のローカル接続で Desktop の個別ログインを省略します。上流プロバイダーの認証情報は引き続き必要です。変更後は Codex を再起動してください。アカウントに依存する Desktop 機能が利用できない場合があります。", "dash.searchModel": "検索サイドカーモデル", "dash.searchModelHint": "非 OpenAI ルーティングモデルで web_search に使うモデル。ChatGPT ログインが必要です。", "dash.searchReasoning": "検索の推論負荷", diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts index c1959482b7..19285b150e 100644 --- a/gui/src/i18n/ko.ts +++ b/gui/src/i18n/ko.ts @@ -303,6 +303,8 @@ export const ko: Record = { "models.staleBanner": "Codex가 이 카탈로그보다 오래된 모델 목록을 보여주고 있습니다. Codex를 재시작하면 새로 읽습니다.", "dash.codexAutoStart": "Codex 실행 시 opencodex 시작", "dash.codexAutoStartHint": "설치된 launcher shim이 ocx ensure를 실행하도록 허용합니다. 이 설정은 재부팅 보호를 설치하지 않으므로 시작 안전성에서 실제 상태를 확인하세요.", + "dash.codexDesktopAuthless": "로그인 없이 Codex 열기", + "dash.codexDesktopAuthlessHint": "기본값은 꺼짐입니다. 지원되는 로컬 연결에서 별도의 Desktop 로그인을 건너뜁니다. 업스트림 인증 정보는 여전히 필요합니다. 변경 후 Codex를 다시 시작하세요. 계정에 연결된 Desktop 기능을 사용하지 못할 수 있습니다.", "dash.searchModel": "서치 사이드카 모델", "dash.searchModelHint": "비-OpenAI 라우팅 모델의 web_search에 사용되는 모델입니다. ChatGPT 로그인 필요.", "dash.searchReasoning": "서치 추론 강도", diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts index 0109f5ebdc..87704912ac 100644 --- a/gui/src/i18n/ru.ts +++ b/gui/src/i18n/ru.ts @@ -308,6 +308,8 @@ export const ru: Record = { "models.staleBanner": "Codex показывает список моделей старее этого каталога. Перезапустите Codex, чтобы перечитать его.", "dash.codexAutoStart": "Запускать opencodex вместе с Codex", "dash.codexAutoStartHint": "Разрешает установленному launcher shim выполнять ocx ensure. Эта настройка не устанавливает защиту перезапуска; проверьте фактическое состояние в разделе безопасности запуска.", + "dash.codexDesktopAuthless": "Открывать Codex без входа", + "dash.codexDesktopAuthlessHint": "По умолчанию выключено. Пропускает отдельный вход в Desktop для допустимых локальных подключений. Учётные данные провайдера по-прежнему нужны. После изменения перезапустите Codex. Функции Desktop, связанные с аккаунтом, могут быть недоступны.", "dash.searchModel": "Модель сайдкара поиска", "dash.searchModelHint": "Модель, используемая для web_search на маршрутизируемых моделях, отличных от OpenAI. Требуется вход в аккаунт ChatGPT.", "dash.searchReasoning": "Уровень рассуждений для поиска", diff --git a/gui/src/i18n/tr.ts b/gui/src/i18n/tr.ts index fa8b8e9c25..807eeae32b 100644 --- a/gui/src/i18n/tr.ts +++ b/gui/src/i18n/tr.ts @@ -309,6 +309,8 @@ export const tr: Record = { "models.staleBanner": "Codex, bu katalogdan daha eski bir model listesi gösteriyor. Yeniden okumak için Codex'i yeniden başlatın.", "dash.codexAutoStart": "opencodex'i Codex ile başlat", "dash.codexAutoStartHint": "Yüklü bir shim'in ocx ensure çalıştırmasına izin verir. Arka plan servisi veya yeniden başlatma koruması kurmaz; sistem durumu için Başlatma Güvenliği'ne bakın.", + "dash.codexDesktopAuthless": "Codex’i oturum açmadan başlat", + "dash.codexDesktopAuthlessHint": "Varsayılan olarak kapalıdır. Uygun yerel bağlantılarda ayrı Desktop oturum açma adımını atlar. Sağlayıcı kimlik bilgileri yine gereklidir. Değişiklikten sonra Codex’i yeniden başlatın. Hesaba bağlı Desktop özellikleri kullanılamayabilir.", "dash.searchModel": "Arama yan araç modeli", "dash.searchModelHint": "OpenAI dışı yönlendirilen modellerde web_search için kullanılan model. ChatGPT girişi gerektirir.", "dash.searchReasoning": "Arama akıl yürütme çabası", diff --git a/gui/src/i18n/zh-TW.ts b/gui/src/i18n/zh-TW.ts index 3bc246543a..62e1f07110 100644 --- a/gui/src/i18n/zh-TW.ts +++ b/gui/src/i18n/zh-TW.ts @@ -200,6 +200,8 @@ export const zhTW: Record = { "models.staleBanner": "Codex 顯示的模型清單比目前的目錄舊。重新啟動 Codex 即可重新讀取。", "dash.codexAutoStart": "隨 Codex 啟動 opencodex", "dash.codexAutoStartHint": "允許已安裝的 launcher shim 執行 ocx ensure。此設定不會安裝重新啟動保護;請在啟動安全中檢查實際狀態。", + "dash.codexDesktopAuthless": "無需登入即可開啟 Codex", + "dash.codexDesktopAuthlessHint": "預設關閉。為符合條件的本機連線略過獨立的 Desktop 登入。仍需上游供應商憑證。變更後請重新啟動 Codex。依賴帳戶的 Desktop 功能可能無法使用。", "dash.searchModel": "搜尋附屬模型", "dash.searchModelHint": "用於非 OpenAI 路由模型的 web_search 的模型。需要 ChatGPT 登入。", "dash.searchReasoning": "搜尋推理強度", diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts index b10c48688d..9946914426 100644 --- a/gui/src/i18n/zh.ts +++ b/gui/src/i18n/zh.ts @@ -303,6 +303,8 @@ export const zh: Record = { "models.staleBanner": "Codex 显示的模型列表比当前目录旧。重启 Codex 即可重新读取。", "dash.codexAutoStart": "随 Codex 启动 opencodex", "dash.codexAutoStartHint": "允许已安装的 launcher shim 运行 ocx ensure。此设置不会安装重启保护;请在启动安全中检查实际状态。", + "dash.codexDesktopAuthless": "无需登录即可打开 Codex", + "dash.codexDesktopAuthlessHint": "默认关闭。为符合条件的本地连接跳过单独的 Desktop 登录。仍需上游提供商凭据。更改后请重启 Codex。依赖账户的 Desktop 功能可能不可用。", "dash.searchModel": "搜索附属模型", "dash.searchModelHint": "用于非 OpenAI 路由模型的 web_search 的模型。需要 ChatGPT 登录。", "dash.searchReasoning": "搜索推理强度", diff --git a/gui/src/pages/dashboard-overview-sections.tsx b/gui/src/pages/dashboard-overview-sections.tsx index 8da531f97c..6606c4f560 100644 --- a/gui/src/pages/dashboard-overview-sections.tsx +++ b/gui/src/pages/dashboard-overview-sections.tsx @@ -163,7 +163,7 @@ export function DashboardInjectionPanel({ d }: { apiBase: string; d: Dash }) { export function DashboardMaintenancePanel({ d }: { d: Dash }) { const { - t, runSync, syncing, updateTriggerRef, openUpdateDialog, updateLoading, updateOpen, + t, runSync, syncing, settingsSaving, updateTriggerRef, openUpdateDialog, updateLoading, updateOpen, syncResult, syncError, updateJob, reconnecting, clearSyncFeedback, } = d; const syncHoldsWarning = !!syncResult && ( @@ -211,7 +211,7 @@ export function DashboardMaintenancePanel({ d }: { d: Dash }) {
{t("dash.syncModelsHint")}
-
+
+
+
+
{t("dash.codexDesktopAuthless")}
+
{t("dash.codexDesktopAuthlessHint")}
+ {settings?.catalogRefreshPending &&
{t("codexAuth.catalogRefreshPending")}
} +
+ +
+
+
{/* Both sidecar cards wear the DashboardInjectionPanel shell: the PANEL is the flex row, copy left, controls right. */} diff --git a/gui/src/pages/dashboard-shared.ts b/gui/src/pages/dashboard-shared.ts index 0793a7def2..d240510286 100644 --- a/gui/src/pages/dashboard-shared.ts +++ b/gui/src/pages/dashboard-shared.ts @@ -48,6 +48,8 @@ export interface ProviderInfo { name: string; adapter: string; baseUrl: string; export interface ModelInfo { id: string; provider: string; namespaced: string; owned_by?: string; reasoningEfforts?: string[] } export interface SettingsData { codexAutoStart: boolean; + codexDesktopAuthless?: boolean; + catalogRefreshPending?: boolean; /** Whether a login may open a browser on the machine running the proxy. */ oauthOpenBrowser?: boolean; port: number; diff --git a/gui/src/pages/use-dashboard-data.ts b/gui/src/pages/use-dashboard-data.ts index 6f84950ce1..6da776ea18 100644 --- a/gui/src/pages/use-dashboard-data.ts +++ b/gui/src/pages/use-dashboard-data.ts @@ -607,23 +607,24 @@ export function useDashboardData(apiBase: string) { finally { setInjectionSaving(false); } }; - const toggleCodexAutoStart = async () => { - if (!settings || settingsSaving) return; - const next = !settings.codexAutoStart; + const toggleCodexSetting = async (key: "codexAutoStart" | "codexDesktopAuthless") => { + if (!settings || settingsSaving || syncing) return; + const next = !(settings[key] ?? (key === "codexAutoStart")); setSettingsSaving(true); settingsMutationInFlightRef.current = true; - setSettings({ ...settings, codexAutoStart: next }); + setSettings({ ...settings, [key]: next }); try { const res = await fetch(`${apiBase}/api/settings`, { method: "PUT", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ codexAutoStart: next }), + body: JSON.stringify({ [key]: next }), }); - const data = await requireJson<{ codexAutoStart: boolean; startupHealth?: SettingsData["startupHealth"] }>(res, "save failed"); + const data = await requireJson(res, "save failed"); settingsMutationEpochRef.current += 1; - setSettings(prev => prev ? { ...prev, codexAutoStart: data.codexAutoStart, startupHealth: data.startupHealth ?? prev.startupHealth } : prev); + setSettings(prev => prev ? { ...prev, [key]: data[key], catalogRefreshPending: key === "codexDesktopAuthless" ? data.catalogRefreshPending : prev.catalogRefreshPending, startupHealth: data.startupHealth ?? prev.startupHealth } : prev); + if (key === "codexDesktopAuthless") await runSync(); } catch { - setSettings(prev => prev ? { ...prev, codexAutoStart: !next } : prev); + setSettings(prev => prev ? { ...prev, [key]: !next } : prev); setError(true); } finally { settingsMutationInFlightRef.current = false; @@ -631,6 +632,9 @@ export function useDashboardData(apiBase: string) { } }; + const toggleCodexAutoStart = () => toggleCodexSetting("codexAutoStart"); + const toggleCodexDesktopAuthless = () => toggleCodexSetting("codexDesktopAuthless"); + // Clears the sync result/error in this hook. The dashboard toast owns its own dismissal // timer but must publish the dismissal here: syncResult/syncError live above the dashboard // tabs, so a component-local flag alone would let a stale result remount as a fresh toast @@ -649,6 +653,7 @@ export function useDashboardData(apiBase: string) { const res = await fetch(`${apiBase}/api/sync`, { method: "POST" }); const data = await requireJson(res, "sync failed"); setSyncResult(data); + setSettings(prev => prev ? { ...prev, catalogRefreshPending: false } : prev); if (data.projectConfigGrouped) setProjectConfigWarnings(data.projectConfigGrouped); } catch (err) { setSyncError(err instanceof Error ? err.message : String(err)); @@ -789,7 +794,7 @@ export function useDashboardData(apiBase: string) { effortCapHelpTriggerRef, updateTriggerRef, maHelpTriggerRef, shadowCallHelpTriggerRef, effortCapHelpDialogRef, updateDialogRef, maHelpDialogRef, shadowCallHelpDialogRef, filteredGroups, sidecarModels, visionModels, - saveSidecar, saveShadowCall, switchMaMode, toggleCodexAutoStart, runSync, clearSyncFeedback, + saveSidecar, saveShadowCall, switchMaMode, toggleCodexAutoStart, toggleCodexDesktopAuthless, runSync, clearSyncFeedback, fetchUpdateCheck, closeUpdateDialog, openUpdateDialog, changeUpdateChannel, runUpdate, }; } diff --git a/gui/tests/vision-sidecar-dashboard.test.tsx b/gui/tests/vision-sidecar-dashboard.test.tsx index dc762de58f..994a409126 100644 --- a/gui/tests/vision-sidecar-dashboard.test.tsx +++ b/gui/tests/vision-sidecar-dashboard.test.tsx @@ -12,7 +12,7 @@ import { LanguageProvider } from "../src/i18n/provider"; import { DashboardSidecarPanels } from "../src/pages/dashboard-overview-sections"; import type { SidecarData, SidecarPatch } from "../src/pages/dashboard-shared"; import { mergeSidecarSetting } from "../src/pages/dashboard-shared"; -import type { useDashboardData } from "../src/pages/use-dashboard-data"; +import { useDashboardData } from "../src/pages/use-dashboard-data"; const globals = ["document", "window", "navigator", "IS_REACT_ACT_ENVIRONMENT"] as const; let previousGlobals: Record<(typeof globals)[number], PropertyDescriptor | undefined>; @@ -382,4 +382,79 @@ test("model and reasoning saves still omit enabled, limit, and timeout", async ( expect(patches).toHaveLength(2); expect(patches[1]).toEqual({ vision: { reasoning: "high" } }); assertVisionControlFieldsOmitted(patches[1]!); -}); \ No newline at end of file +}); + +test("Desktop login switch defaults off, preserves explicit opt-in, and disables while saving", async () => { + const { d } = harness(); + let clicks = 0; + d.toggleCodexDesktopAuthless = async () => { clicks += 1; }; + d.settings = { codexAutoStart: true, port: 10100, hostname: "127.0.0.1" }; + await mount(d); + const toggle = () => host.querySelector(`button[aria-label="${en["dash.codexDesktopAuthless"]}"]`)!; + expect(toggle().getAttribute("aria-pressed")).toBe("false"); + d.settings.codexDesktopAuthless = true; + await mount(d); + expect(toggle().getAttribute("aria-pressed")).toBe("true"); + await act(async () => { toggle().click(); }); + expect(clicks).toBe(1); + d.settings.codexDesktopAuthless = false; + d.settings.catalogRefreshPending = true; + d.settingsSaving = true; + await mount(d); + expect(toggle().getAttribute("aria-pressed")).toBe("false"); + expect(toggle().disabled).toBe(true); + expect(host.textContent).toContain(en["codexAuth.catalogRefreshPending"]); +}); + + +test.each([undefined, false, true])("Desktop login preference %s persists before full sync; sync failure keeps the saved preference", async (initial) => { + const originalFetch = globalThis.fetch; + const writes: Array<{ path: string; body: unknown }> = []; + let latest: Dash | undefined; + let saved = initial; + const apiBase = `/authless-test-${String(initial)}`; + globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + const path = String(input); + if (init?.method === "PUT") { + const body = JSON.parse(String(init.body)); + writes.push({ path, body }); + if (body.codexDesktopAuthless !== undefined) { + saved = body.codexDesktopAuthless; + return Response.json({ codexDesktopAuthless: saved, catalogRefreshPending: true }); + } + return Response.json({ codexAutoStart: body.codexAutoStart, catalogRefreshPending: false }); + } + if (path.endsWith("/api/sync")) { + writes.push({ path, body: null }); + return Response.json({ error: "sync unavailable" }, { status: 503 }); + } + if (path.endsWith("/api/settings")) { + return Response.json({ codexAutoStart: true, codexDesktopAuthless: saved, port: 10100, hostname: "127.0.0.1" }); + } + return Response.json({}, { status: 503 }); + }) as typeof fetch; + function Harness() { latest = useDashboardData(apiBase); return null; } + try { + const { createRoot } = await import("react-dom/client"); + await act(async () => { + root = createRoot(host); + root.render(); + }); + expect(latest?.settings?.codexDesktopAuthless).toBe(initial); + await act(async () => { await latest!.toggleCodexDesktopAuthless(); }); + expect(writes).toEqual([ + { path: `${apiBase}/api/settings`, body: { codexDesktopAuthless: !initial } }, + { path: `${apiBase}/api/sync`, body: null }, + ]); + expect(latest?.settings?.codexDesktopAuthless).toBe(!initial); + expect(latest?.syncError).toBe("sync unavailable"); + expect(latest?.settings?.catalogRefreshPending).toBe(true); + await act(async () => { await latest!.toggleCodexAutoStart(); }); + expect(latest?.settings?.codexAutoStart).toBe(false); + expect(latest?.settings?.catalogRefreshPending).toBe(true); + } finally { + await act(async () => { root?.unmount(); }); + root = null; + globalThis.fetch = originalFetch; + } +}); diff --git a/tests/codex-integration/codex-inject.test.ts b/tests/codex-integration/codex-inject.test.ts index 84ac5f67b6..b6be3c2f69 100644 --- a/tests/codex-integration/codex-inject.test.ts +++ b/tests/codex-integration/codex-inject.test.ts @@ -31,8 +31,8 @@ describe("Codex config injection", () => { }); describe("authless Codex Desktop opt-in (#1107)", () => { - test("default target on loopback stays Design B and byte-identical", () => { - const target = standaloneCodexRoutingTarget(10100, {}); + test.each([undefined, false])("disabled preference %s on loopback stays Design B and byte-identical", (codexDesktopAuthless) => { + const target = standaloneCodexRoutingTarget(10100, { codexDesktopAuthless }); expect(target.desktopAuthless).toBeUndefined(); expect(buildProfileFile(target, null)).toBe(buildProfileFile(10100, null)); expect(buildProviderTableBlock(target)).toContain("requires_openai_auth = true"); From c1f96f0a5af6bf98e663d287eeffaabcdb0d0028 Mon Sep 17 00:00:00 2001 From: t Date: Mon, 7 Sep 2026 18:25:44 +0900 Subject: [PATCH 4/6] fix(gui): retain pending Desktop preference when sync is skipped [skip ci] Co-authored-by: RobinBially <7304732+RobinBially@users.noreply.github.com> --- gui/src/i18n/de.ts | 4 +-- gui/src/i18n/en.ts | 4 +-- gui/src/i18n/fr.ts | 4 +-- gui/src/i18n/ja.ts | 4 +-- gui/src/i18n/ko.ts | 4 +-- gui/src/i18n/ru.ts | 4 +-- gui/src/i18n/tr.ts | 4 +-- gui/src/i18n/zh-TW.ts | 4 +-- gui/src/i18n/zh.ts | 4 +-- gui/src/pages/dashboard-shared.ts | 1 + gui/src/pages/use-dashboard-data.ts | 4 ++- gui/tests/vision-sidecar-dashboard.test.tsx | 27 +++++++++++++++++++++ 12 files changed, 49 insertions(+), 19 deletions(-) diff --git a/gui/src/i18n/de.ts b/gui/src/i18n/de.ts index 495104bd42..74533a04d3 100644 --- a/gui/src/i18n/de.ts +++ b/gui/src/i18n/de.ts @@ -299,8 +299,6 @@ export const de: Record = { "models.staleBanner": "Codex zeigt eine ältere Modellliste als dieser Katalog. Starte Codex neu, um sie neu zu laden.", "dash.codexAutoStart": "opencodex mit Codex starten", "dash.codexAutoStartHint": "Erlaubt einem installierten Launcher-Shim, ocx ensure auszuführen. Diese Einstellung installiert keinen Neustartschutz; prüfe den effektiven Zustand unter Startsicherheit.", - "dash.codexDesktopAuthless": "Codex ohne Anmeldung öffnen", - "dash.codexDesktopAuthlessHint": "Standardmäßig aus. Überspringt die separate Desktop-Anmeldung bei geeigneten lokalen Verbindungen. Zugangsdaten für den Anbieter bleiben erforderlich. Codex nach einer Änderung neu starten. Kontogebundene Desktop-Funktionen können fehlen.", "dash.searchModel": "Such-Sidecar-Modell", "dash.searchModelHint": "Modell für web_search bei nicht über OpenAI gerouteten Modellen. Erfordert ChatGPT-Login.", "dash.searchReasoning": "Such-Reasoning-Aufwand", @@ -2471,6 +2469,8 @@ export const de: Record = { "dash.visionTimeout": "Timeout", "dash.visionTimeoutInvalid": "Geben Sie eine ganze Zahl von {min} bis {max} Millisekunden ein.", "dash.visionAdvancedPopover": "Erweiterte Vision-Einstellungen", + "dash.codexDesktopAuthless": "Codex ohne Anmeldung öffnen", + "dash.codexDesktopAuthlessHint": "Standardmäßig aus. Überspringt die separate Desktop-Anmeldung bei geeigneten lokalen Verbindungen. Zugangsdaten für den Anbieter bleiben erforderlich. Codex nach einer Änderung neu starten. Kontogebundene Desktop-Funktionen können fehlen.", "models.newPolicyGlobal": "Neue Modelle zunächst deaktivieren", "models.newPolicyProvider": "Richtlinie für neue Modelle", "models.newPolicy_inherit": "Übernehmen", "models.newPolicy_off": "Aus", "models.newPolicy_on": "An", "models.newBadge": "NEU", "models.newCount": "{count} neu, aus", "models.aliases": "Aliase", diff --git a/gui/src/i18n/en.ts b/gui/src/i18n/en.ts index 22a3807858..2197dcb735 100644 --- a/gui/src/i18n/en.ts +++ b/gui/src/i18n/en.ts @@ -311,8 +311,6 @@ export const en = { "models.staleBanner": "Codex is showing an older model list than this catalog. Restart Codex to reload it.", "dash.codexAutoStart": "Start opencodex with Codex", "dash.codexAutoStartHint": "Allows an installed launcher shim to run ocx ensure. This setting does not install restart protection; check Startup safety for the effective state.", - "dash.codexDesktopAuthless": "Open Codex without signing in", - "dash.codexDesktopAuthlessHint": "Off by default. Skip the separate Desktop sign-in for eligible local connections. Upstream credentials are still required. Restart Codex after changing this setting. Account-gated Desktop features may be unavailable.", "dash.searchModel": "Search sidecar model", "dash.searchModelHint": "Model used for web_search on non-OpenAI routed models. Requires ChatGPT login.", "dash.searchReasoning": "Search reasoning effort", @@ -577,6 +575,8 @@ export const en = { "models.keepNativeOnV1Hint": "ChatGPT encrypts v2 child tasks only when a ChatGPT-native parent stays on v2, so Grok and Claude cannot read them. Turn this on to keep Sol/Terra on v1 and avoid that encryption. Routed parents keep v2.", "models.v2Help": "Controls the multi-agent surface for all models.\n\nv1: Classic single-thread agent. Every model uses the v1 collab surface.\nbase: Upstream defaults — sol/terra use v2, luna uses v1, others follow the codex feature flag.\nv2: Multi-thread agent with spawn_agent. Every model uses the v2 collab surface.\n\nOn v2, Keep ChatGPT on v1 leaves Sol/Terra on the v1 surface so they can still spawn Grok or Claude. ChatGPT encrypts v2 child tasks; routed models cannot read them. Routed parents stay on v2.\n\nChanges apply to new sessions.", "dash.multiAgent": "Sub-agent", + "dash.codexDesktopAuthless": "Open Codex without signing in", + "dash.codexDesktopAuthlessHint": "Off by default. Skip the separate Desktop sign-in for eligible local connections. Upstream credentials are still required. Restart Codex after changing this setting. Account-gated Desktop features may be unavailable.", "models.v2Conflict": "[agents] max_threads is set — codex will refuse to start; remove it from config.toml", "models.v2Applied": "Sub-agent mode updated — applies to new sessions (restart the Codex app to refresh the picker)", "models.v2ThreadsLabel": "Max threads", diff --git a/gui/src/i18n/fr.ts b/gui/src/i18n/fr.ts index 9f0f265173..cca4b70347 100644 --- a/gui/src/i18n/fr.ts +++ b/gui/src/i18n/fr.ts @@ -301,8 +301,6 @@ export const fr: Record = { "models.staleBanner": "Codex affiche une liste de modèles plus ancienne que ce catalogue. Redémarrez Codex pour la recharger.", "dash.codexAutoStart": "Démarrer opencodex avec Codex", "dash.codexAutoStartHint": "Permet à un mécanisme de lancement installé d’exécuter ocx ensure. Ce réglage n’installe pas de protection au redémarrage ; consultez Sécurité du démarrage pour connaître l’état effectif.", - "dash.codexDesktopAuthless": "Ouvrir Codex sans se connecter", - "dash.codexDesktopAuthlessHint": "Désactivé par défaut. Ignore la connexion Desktop séparée pour les connexions locales admissibles. Les identifiants du fournisseur restent nécessaires. Redémarrez Codex après toute modification. Certaines fonctions Desktop liées au compte peuvent être indisponibles.", "dash.searchModel": "Modèle auxiliaire de recherche", "dash.searchModelHint": "Modèle utilisé pour web_search sur les modèles routés autres qu’OpenAI. Nécessite une connexion à ChatGPT.", "dash.searchReasoning": "Effort de raisonnement pour la recherche", @@ -562,6 +560,8 @@ export const fr: Record = { "models.keepNativeOnV1Hint": "ChatGPT chiffre les tâches enfants v2 uniquement lorsqu’un parent natif ChatGPT reste sur v2, de sorte que Grok et Claude ne peuvent pas les lire. Activez cette option pour garder Sol/Terra sur v1 et éviter ce chiffrement. Les parents routés restent sur v2.", "models.v2Help": "Contrôle l’interface multi-agent pour tous les modèles.\n\nv1 : agent classique à fil unique. Tous les modèles utilisent l’interface collab v1.\nbase : valeurs par défaut en amont — sol/terra utilisent v2, luna utilise v1 et les autres suivent l’indicateur de fonctionnalité codex.\nv2 : agent multifil avec spawn_agent. Tous les modèles utilisent l’interface collab v2.\n\nEn v2, « Garder ChatGPT sur v1 » laisse Sol/Terra sur l’interface v1 afin qu’ils puissent encore lancer Grok ou Claude. ChatGPT chiffre les tâches enfants v2 ; les modèles routés ne peuvent pas les lire. Les parents routés restent sur v2.\n\nLes modifications s’appliquent aux nouvelles sessions.", "dash.multiAgent": "Sous-agent", + "dash.codexDesktopAuthless": "Ouvrir Codex sans se connecter", + "dash.codexDesktopAuthlessHint": "Désactivé par défaut. Ignore la connexion Desktop séparée pour les connexions locales admissibles. Les identifiants du fournisseur restent nécessaires. Redémarrez Codex après toute modification. Certaines fonctions Desktop liées au compte peuvent être indisponibles.", "models.v2Conflict": "[agents] max_threads est défini — codex refusera de démarrer ; supprimez-le de config.toml", "models.v2Applied": "Mode sous-agent mis à jour — s’applique aux nouvelles sessions (redémarrez l’application Codex pour actualiser le sélecteur)", "models.v2ThreadsLabel": "Nombre maximal de fils", diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts index 55a6fe2495..97aa85a6fb 100644 --- a/gui/src/i18n/ja.ts +++ b/gui/src/i18n/ja.ts @@ -308,8 +308,6 @@ export const ja: Record = { "models.staleBanner": "Codex はこのカタログより古いモデル一覧を表示しています。Codex を再起動すると読み直されます。", "dash.codexAutoStart": "Codex と一緒に opencodex を起動", "dash.codexAutoStartHint": "インストール済み launcher shim に ocx ensure の実行を許可します。この設定だけでは再起動保護はインストールされません。起動安全性で実際の状態を確認してください。", - "dash.codexDesktopAuthless": "ログインせずに Codex を開く", - "dash.codexDesktopAuthlessHint": "既定ではオフです。対象のローカル接続で Desktop の個別ログインを省略します。上流プロバイダーの認証情報は引き続き必要です。変更後は Codex を再起動してください。アカウントに依存する Desktop 機能が利用できない場合があります。", "dash.searchModel": "検索サイドカーモデル", "dash.searchModelHint": "非 OpenAI ルーティングモデルで web_search に使うモデル。ChatGPT ログインが必要です。", "dash.searchReasoning": "検索の推論負荷", @@ -2492,6 +2490,8 @@ export const ja: Record = { "dash.visionTimeout": "タイムアウト", "dash.visionTimeoutInvalid": "{min} から {max} ミリ秒の整数を入力してください。", "dash.visionAdvancedPopover": "詳細なビジョン設定", + "dash.codexDesktopAuthless": "ログインせずに Codex を開く", + "dash.codexDesktopAuthlessHint": "既定ではオフです。対象のローカル接続で Desktop の個別ログインを省略します。上流プロバイダーの認証情報は引き続き必要です。変更後は Codex を再起動してください。アカウントに依存する Desktop 機能が利用できない場合があります。", "models.newPolicyGlobal": "新しいモデルを無効で追加", "models.newPolicyProvider": "新しいモデルのポリシー", "models.newPolicy_inherit": "継承", "models.newPolicy_off": "オフ", "models.newPolicy_on": "オン", "models.newBadge": "新着", "models.newCount": "新着 {count} 件、オフ", "models.aliases": "エイリアス", diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts index 19285b150e..4e66544528 100644 --- a/gui/src/i18n/ko.ts +++ b/gui/src/i18n/ko.ts @@ -303,8 +303,6 @@ export const ko: Record = { "models.staleBanner": "Codex가 이 카탈로그보다 오래된 모델 목록을 보여주고 있습니다. Codex를 재시작하면 새로 읽습니다.", "dash.codexAutoStart": "Codex 실행 시 opencodex 시작", "dash.codexAutoStartHint": "설치된 launcher shim이 ocx ensure를 실행하도록 허용합니다. 이 설정은 재부팅 보호를 설치하지 않으므로 시작 안전성에서 실제 상태를 확인하세요.", - "dash.codexDesktopAuthless": "로그인 없이 Codex 열기", - "dash.codexDesktopAuthlessHint": "기본값은 꺼짐입니다. 지원되는 로컬 연결에서 별도의 Desktop 로그인을 건너뜁니다. 업스트림 인증 정보는 여전히 필요합니다. 변경 후 Codex를 다시 시작하세요. 계정에 연결된 Desktop 기능을 사용하지 못할 수 있습니다.", "dash.searchModel": "서치 사이드카 모델", "dash.searchModelHint": "비-OpenAI 라우팅 모델의 web_search에 사용되는 모델입니다. ChatGPT 로그인 필요.", "dash.searchReasoning": "서치 추론 강도", @@ -2493,6 +2491,8 @@ export const ko: Record = { "dash.visionTimeout": "제한 시간", "dash.visionTimeoutInvalid": "{min}에서 {max} 밀리초 사이의 정수를 입력하세요.", "dash.visionAdvancedPopover": "고급 비전 설정", + "dash.codexDesktopAuthless": "로그인 없이 Codex 열기", + "dash.codexDesktopAuthlessHint": "기본값은 꺼짐입니다. 지원되는 로컬 연결에서 별도의 Desktop 로그인을 건너뜁니다. 업스트림 인증 정보는 여전히 필요합니다. 변경 후 Codex를 다시 시작하세요. 계정에 연결된 Desktop 기능을 사용하지 못할 수 있습니다.", "models.newPolicyGlobal": "새 모델을 비활성화 상태로 추가", "models.newPolicyProvider": "새 모델 정책", "models.newPolicy_inherit": "상속", "models.newPolicy_off": "끔", "models.newPolicy_on": "켬", "models.newBadge": "신규", "models.newCount": "신규 {count}개, 꺼짐", "models.aliases": "별칭", diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts index 87704912ac..e7f199a06a 100644 --- a/gui/src/i18n/ru.ts +++ b/gui/src/i18n/ru.ts @@ -308,8 +308,6 @@ export const ru: Record = { "models.staleBanner": "Codex показывает список моделей старее этого каталога. Перезапустите Codex, чтобы перечитать его.", "dash.codexAutoStart": "Запускать opencodex вместе с Codex", "dash.codexAutoStartHint": "Разрешает установленному launcher shim выполнять ocx ensure. Эта настройка не устанавливает защиту перезапуска; проверьте фактическое состояние в разделе безопасности запуска.", - "dash.codexDesktopAuthless": "Открывать Codex без входа", - "dash.codexDesktopAuthlessHint": "По умолчанию выключено. Пропускает отдельный вход в Desktop для допустимых локальных подключений. Учётные данные провайдера по-прежнему нужны. После изменения перезапустите Codex. Функции Desktop, связанные с аккаунтом, могут быть недоступны.", "dash.searchModel": "Модель сайдкара поиска", "dash.searchModelHint": "Модель, используемая для web_search на маршрутизируемых моделях, отличных от OpenAI. Требуется вход в аккаунт ChatGPT.", "dash.searchReasoning": "Уровень рассуждений для поиска", @@ -2494,6 +2492,8 @@ export const ru: Record = { "dash.visionTimeout": "Таймаут", "dash.visionTimeoutInvalid": "Введите целое число от {min} до {max} миллисекунд.", "dash.visionAdvancedPopover": "Дополнительные настройки изображений", + "dash.codexDesktopAuthless": "Открывать Codex без входа", + "dash.codexDesktopAuthlessHint": "По умолчанию выключено. Пропускает отдельный вход в Desktop для допустимых локальных подключений. Учётные данные провайдера по-прежнему нужны. После изменения перезапустите Codex. Функции Desktop, связанные с аккаунтом, могут быть недоступны.", "models.newPolicyGlobal": "Добавлять новые модели выключенными", "models.newPolicyProvider": "Политика новых моделей", "models.newPolicy_inherit": "Наследовать", "models.newPolicy_off": "Выкл.", "models.newPolicy_on": "Вкл.", "models.newBadge": "НОВАЯ", "models.newCount": "Новых: {count}, выкл.", "models.aliases": "Псевдонимы", diff --git a/gui/src/i18n/tr.ts b/gui/src/i18n/tr.ts index 807eeae32b..8c8f9ff802 100644 --- a/gui/src/i18n/tr.ts +++ b/gui/src/i18n/tr.ts @@ -309,8 +309,6 @@ export const tr: Record = { "models.staleBanner": "Codex, bu katalogdan daha eski bir model listesi gösteriyor. Yeniden okumak için Codex'i yeniden başlatın.", "dash.codexAutoStart": "opencodex'i Codex ile başlat", "dash.codexAutoStartHint": "Yüklü bir shim'in ocx ensure çalıştırmasına izin verir. Arka plan servisi veya yeniden başlatma koruması kurmaz; sistem durumu için Başlatma Güvenliği'ne bakın.", - "dash.codexDesktopAuthless": "Codex’i oturum açmadan başlat", - "dash.codexDesktopAuthlessHint": "Varsayılan olarak kapalıdır. Uygun yerel bağlantılarda ayrı Desktop oturum açma adımını atlar. Sağlayıcı kimlik bilgileri yine gereklidir. Değişiklikten sonra Codex’i yeniden başlatın. Hesaba bağlı Desktop özellikleri kullanılamayabilir.", "dash.searchModel": "Arama yan araç modeli", "dash.searchModelHint": "OpenAI dışı yönlendirilen modellerde web_search için kullanılan model. ChatGPT girişi gerektirir.", "dash.searchReasoning": "Arama akıl yürütme çabası", @@ -2494,6 +2492,8 @@ export const tr: Record = { "dash.visionTimeout": "Zaman aşımı", "dash.visionTimeoutInvalid": "{min} ile {max} milisaniye arasında bir tam sayı girin.", "dash.visionAdvancedPopover": "Gelişmiş görsel ayarları", + "dash.codexDesktopAuthless": "Codex’i oturum açmadan başlat", + "dash.codexDesktopAuthlessHint": "Varsayılan olarak kapalıdır. Uygun yerel bağlantılarda ayrı Desktop oturum açma adımını atlar. Sağlayıcı kimlik bilgileri yine gereklidir. Değişiklikten sonra Codex’i yeniden başlatın. Hesaba bağlı Desktop özellikleri kullanılamayabilir.", "models.newPolicyGlobal": "Yeni modeller devre dışı başlasın", "models.newPolicyProvider": "Yeni model ilkesi", "models.newPolicy_inherit": "Devral", "models.newPolicy_off": "Kapalı", "models.newPolicy_on": "Açık", "models.newBadge": "YENİ", "models.newCount": "{count} yeni, kapalı", "models.aliases": "Takma adlar", diff --git a/gui/src/i18n/zh-TW.ts b/gui/src/i18n/zh-TW.ts index 62e1f07110..eeec289911 100644 --- a/gui/src/i18n/zh-TW.ts +++ b/gui/src/i18n/zh-TW.ts @@ -200,8 +200,6 @@ export const zhTW: Record = { "models.staleBanner": "Codex 顯示的模型清單比目前的目錄舊。重新啟動 Codex 即可重新讀取。", "dash.codexAutoStart": "隨 Codex 啟動 opencodex", "dash.codexAutoStartHint": "允許已安裝的 launcher shim 執行 ocx ensure。此設定不會安裝重新啟動保護;請在啟動安全中檢查實際狀態。", - "dash.codexDesktopAuthless": "無需登入即可開啟 Codex", - "dash.codexDesktopAuthlessHint": "預設關閉。為符合條件的本機連線略過獨立的 Desktop 登入。仍需上游供應商憑證。變更後請重新啟動 Codex。依賴帳戶的 Desktop 功能可能無法使用。", "dash.searchModel": "搜尋附屬模型", "dash.searchModelHint": "用於非 OpenAI 路由模型的 web_search 的模型。需要 ChatGPT 登入。", "dash.searchReasoning": "搜尋推理強度", @@ -2456,6 +2454,8 @@ export const zhTW: Record = { "dash.visionTimeout": "逾時", "dash.visionTimeoutInvalid": "請輸入 {min} 到 {max} 毫秒之間的整數。", "dash.visionAdvancedPopover": "進階視覺設定", + "dash.codexDesktopAuthless": "無需登入即可開啟 Codex", + "dash.codexDesktopAuthlessHint": "預設關閉。為符合條件的本機連線略過獨立的 Desktop 登入。仍需上游供應商憑證。變更後請重新啟動 Codex。依賴帳戶的 Desktop 功能可能無法使用。", "models.newPolicyGlobal": "新模型預設停用", "models.newPolicyProvider": "新模型策略", "models.newPolicy_inherit": "繼承", "models.newPolicy_off": "關閉", "models.newPolicy_on": "開啟", "models.newBadge": "新增", "models.newCount": "{count} 個新增,已關閉", "models.aliases": "別名", diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts index 9946914426..a62e9b2d6b 100644 --- a/gui/src/i18n/zh.ts +++ b/gui/src/i18n/zh.ts @@ -303,8 +303,6 @@ export const zh: Record = { "models.staleBanner": "Codex 显示的模型列表比当前目录旧。重启 Codex 即可重新读取。", "dash.codexAutoStart": "随 Codex 启动 opencodex", "dash.codexAutoStartHint": "允许已安装的 launcher shim 运行 ocx ensure。此设置不会安装重启保护;请在启动安全中检查实际状态。", - "dash.codexDesktopAuthless": "无需登录即可打开 Codex", - "dash.codexDesktopAuthlessHint": "默认关闭。为符合条件的本地连接跳过单独的 Desktop 登录。仍需上游提供商凭据。更改后请重启 Codex。依赖账户的 Desktop 功能可能不可用。", "dash.searchModel": "搜索附属模型", "dash.searchModelHint": "用于非 OpenAI 路由模型的 web_search 的模型。需要 ChatGPT 登录。", "dash.searchReasoning": "搜索推理强度", @@ -2492,6 +2490,8 @@ export const zh: Record = { "dash.visionTimeout": "超时", "dash.visionTimeoutInvalid": "请输入 {min} 到 {max} 毫秒之间的整数。", "dash.visionAdvancedPopover": "高级视觉设置", + "dash.codexDesktopAuthless": "无需登录即可打开 Codex", + "dash.codexDesktopAuthlessHint": "默认关闭。为符合条件的本地连接跳过单独的 Desktop 登录。仍需上游提供商凭据。更改后请重启 Codex。依赖账户的 Desktop 功能可能不可用。", "models.newPolicyGlobal": "新模型默认停用", "models.newPolicyProvider": "新模型策略", "models.newPolicy_inherit": "继承", "models.newPolicy_off": "关闭", "models.newPolicy_on": "开启", "models.newBadge": "新增", "models.newCount": "{count} 个新增,已关闭", "models.aliases": "别名", diff --git a/gui/src/pages/dashboard-shared.ts b/gui/src/pages/dashboard-shared.ts index d240510286..b6914586b6 100644 --- a/gui/src/pages/dashboard-shared.ts +++ b/gui/src/pages/dashboard-shared.ts @@ -128,6 +128,7 @@ export type Installer = "npm" | "bun" | "source"; export type UpdateJobStatus = "running" | "restarting" | "succeeded" | "failed"; export interface SyncResult { ok: boolean; + status?: "applied" | "skipped" | "catalog-only" | "refused"; added: number; catalogPath: string | null; catalogExists: boolean; diff --git a/gui/src/pages/use-dashboard-data.ts b/gui/src/pages/use-dashboard-data.ts index 6da776ea18..e87e6494bd 100644 --- a/gui/src/pages/use-dashboard-data.ts +++ b/gui/src/pages/use-dashboard-data.ts @@ -653,7 +653,9 @@ export function useDashboardData(apiBase: string) { const res = await fetch(`${apiBase}/api/sync`, { method: "POST" }); const data = await requireJson(res, "sync failed"); setSyncResult(data); - setSettings(prev => prev ? { ...prev, catalogRefreshPending: false } : prev); + if (data.ok && data.status === "applied") { + setSettings(prev => prev ? { ...prev, catalogRefreshPending: false } : prev); + } if (data.projectConfigGrouped) setProjectConfigWarnings(data.projectConfigGrouped); } catch (err) { setSyncError(err instanceof Error ? err.message : String(err)); diff --git a/gui/tests/vision-sidecar-dashboard.test.tsx b/gui/tests/vision-sidecar-dashboard.test.tsx index 994a409126..386196cc77 100644 --- a/gui/tests/vision-sidecar-dashboard.test.tsx +++ b/gui/tests/vision-sidecar-dashboard.test.tsx @@ -458,3 +458,30 @@ test.each([undefined, false, true])("Desktop login preference %s persists before globalThis.fetch = originalFetch; } }); + + +test.each(["skipped", "catalog-only", "applied"])("Desktop preference pending state follows %s sync application evidence", async (syncStatus) => { + const originalFetch = globalThis.fetch; + let latest: Dash | undefined; + const apiBase = `/authless-sync-${syncStatus}`; + globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + const path = String(input); + if (init?.method === "PUT") return Response.json({ codexDesktopAuthless: true, catalogRefreshPending: true }); + if (path.endsWith("/api/sync")) return Response.json({ ok: true, status: syncStatus, message: syncStatus }); + if (path.endsWith("/api/settings")) return Response.json({ codexAutoStart: true, codexDesktopAuthless: false, port: 10100, hostname: "127.0.0.1" }); + return Response.json({}, { status: 503 }); + }) as typeof fetch; + function Harness() { latest = useDashboardData(apiBase); return null; } + try { + const { createRoot } = await import("react-dom/client"); + await act(async () => { root = createRoot(host); root.render(); }); + await act(async () => { await latest!.toggleCodexDesktopAuthless(); }); + expect(latest?.settings?.codexDesktopAuthless).toBe(true); + expect(latest?.settings?.catalogRefreshPending).toBe(syncStatus !== "applied"); + expect(latest?.syncResult?.status).toBe(syncStatus); + } finally { + await act(async () => { root?.unmount(); }); + root = null; + globalThis.fetch = originalFetch; + } +}); From 734d41c528039a29b4002d455405afacfecf922c Mon Sep 17 00:00:00 2001 From: t Date: Mon, 7 Sep 2026 18:31:54 +0900 Subject: [PATCH 5/6] fix(gui): retain unapplied Desktop status across settings polls [skip ci] Co-authored-by: RobinBially <7304732+RobinBially@users.noreply.github.com> --- gui/src/pages/use-dashboard-data.ts | 7 ++++++- gui/tests/vision-sidecar-dashboard.test.tsx | 9 +++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/gui/src/pages/use-dashboard-data.ts b/gui/src/pages/use-dashboard-data.ts index e87e6494bd..8634e7caa8 100644 --- a/gui/src/pages/use-dashboard-data.ts +++ b/gui/src/pages/use-dashboard-data.ts @@ -361,7 +361,12 @@ export function useDashboardData(apiBase: string) { useEffect(() => { const data = settingsPoll.data; if (!data) return; - if (data.settings !== undefined) setSettings(data.settings); + if (data.settings !== undefined) { + const next = data.settings; + // GET settings does not report application receipts. Keep a saved preference's + // pending indication until an affirmative sync result clears it. + setSettings(prev => ({ ...next, catalogRefreshPending: prev?.catalogRefreshPending === true || next.catalogRefreshPending })); + } // Latest-wins: only seed from settings when no newer dedicated probe has committed // while this settings poll was in flight. Always merge against the live ref. if ( diff --git a/gui/tests/vision-sidecar-dashboard.test.tsx b/gui/tests/vision-sidecar-dashboard.test.tsx index 386196cc77..738ed866e6 100644 --- a/gui/tests/vision-sidecar-dashboard.test.tsx +++ b/gui/tests/vision-sidecar-dashboard.test.tsx @@ -13,6 +13,7 @@ import { DashboardSidecarPanels } from "../src/pages/dashboard-overview-sections import type { SidecarData, SidecarPatch } from "../src/pages/dashboard-shared"; import { mergeSidecarSetting } from "../src/pages/dashboard-shared"; import { useDashboardData } from "../src/pages/use-dashboard-data"; +import { setClientResourceData } from "../src/client-resource"; const globals = ["document", "window", "navigator", "IS_REACT_ACT_ENVIRONMENT"] as const; let previousGlobals: Record<(typeof globals)[number], PropertyDescriptor | undefined>; @@ -479,6 +480,14 @@ test.each(["skipped", "catalog-only", "applied"])("Desktop preference pending st expect(latest?.settings?.codexDesktopAuthless).toBe(true); expect(latest?.settings?.catalogRefreshPending).toBe(syncStatus !== "applied"); expect(latest?.syncResult?.status).toBe(syncStatus); + // A fresh settings poll has no application receipt and cannot erase pending. + await act(async () => { + setClientResourceData(`dashboard-settings:${apiBase}`, { + settings: { codexAutoStart: true, codexDesktopAuthless: true, port: 10100, hostname: "127.0.0.1" }, + }); + }); + expect(latest?.settings?.codexDesktopAuthless).toBe(true); + expect(latest?.settings?.catalogRefreshPending === true).toBe(syncStatus !== "applied"); } finally { await act(async () => { root?.unmount(); }); root = null; From abbdb227f385443228656c8440282187cb5dec75 Mon Sep 17 00:00:00 2001 From: t Date: Mon, 7 Sep 2026 19:07:56 +0900 Subject: [PATCH 6/6] docs: translate code-mode host rules; docs handoff from lane E [skip ci] --- devlog/_plan/260907_lane_c/040_desktop.md | 4 ++++ docs-site/src/content/docs/fr/guides/codex-integration.md | 8 ++++++++ docs-site/src/content/docs/ja/guides/codex-integration.md | 8 ++++++++ docs-site/src/content/docs/ko/guides/codex-integration.md | 7 +++++++ docs-site/src/content/docs/ru/guides/codex-integration.md | 8 ++++++++ docs-site/src/content/docs/tr/guides/codex-integration.md | 8 ++++++++ .../src/content/docs/zh-cn/guides/codex-integration.md | 6 ++++++ .../src/content/docs/zh-tw/guides/codex-integration.md | 6 ++++++ 8 files changed, 55 insertions(+) diff --git a/devlog/_plan/260907_lane_c/040_desktop.md b/devlog/_plan/260907_lane_c/040_desktop.md index ad7fb60177..4eb4ebffd5 100644 --- a/devlog/_plan/260907_lane_c/040_desktop.md +++ b/devlog/_plan/260907_lane_c/040_desktop.md @@ -402,3 +402,7 @@ index 84ac5f67b..b6be3c2f6 100644 ``` Audit amendment: clear catalogRefreshPending only if sync status is affirmative success, not HTTP 200 skipped. Add skipped/no-write regression. + +## Lane E documentation handoff + +After run 34106956362 reported a GUI lint failure, include the separately prepared code-mode host-rule translations in the seven fr/ja/ko/ru/tr/zh-cn/zh-tw Codex integration guides. The patch adds 51 documentation lines matching the existing English paragraph; it does not modify runtime code or provider guides. Apply on the Desktop layer, record `docs handoff from lane E` and `[skip ci]` in its own commit, then cascade the fallback layer and dispatch the top CI again. Local documentation install/build remains NOT RUN. diff --git a/docs-site/src/content/docs/fr/guides/codex-integration.md b/docs-site/src/content/docs/fr/guides/codex-integration.md index 02ffa8a211..a351adeae5 100644 --- a/docs-site/src/content/docs/fr/guides/codex-integration.md +++ b/docs-site/src/content/docs/fr/guides/codex-integration.md @@ -233,6 +233,14 @@ opencodex encode cette déclaration et son historique sous forme d'outil de fonc cycle de vie diffusé de l'appel de fonction en `custom_tool_call` avant que Codex ne le reçoive. Le routage natif par transfert OpenAI et l'outil personnalisé `apply_patch`, qui est pris en charge, restent inchangés. +Avant le premier appel, les tours routés en mode code reçoivent aussi les règles de l'hôte pour les +outils auxiliaires imbriqués : `tools.apply_patch` prend une seule chaîne qui commence et se termine +par les lignes de marqueur de patch seules, sans habillage ; l'isolate ne dispose pas de `import`, +et les commandes longues sont interrogées via `write_stdin`. Lorsqu'un résultat exec en mode code +sur le chemin natif Responses routé, Kiro ou Cursor contient encore l'un des messages d'échec de +l'hôte, opencodex ajoute une indication d'une ligne qui nomme la règle. Cette modification ne +réécrit ni le code du modèle ni le texte de son patch. + Le fournisseur sélectionné doit prendre en charge les appels de fonctions ou d'outils. Un fournisseur purement textuel dépourvu de cette prise en charge ne peut pas utiliser `exec`, Browser ni Computer Use. Les lignes OpenAI natives conservent leur mode d'outil en amont. diff --git a/docs-site/src/content/docs/ja/guides/codex-integration.md b/docs-site/src/content/docs/ja/guides/codex-integration.md index d1d977b35c..46c33320f2 100644 --- a/docs-site/src/content/docs/ja/guides/codex-integration.md +++ b/docs-site/src/content/docs/ja/guides/codex-integration.md @@ -147,6 +147,14 @@ Codex の `exec` custom-tool grammar を受け付けない key-auth Responses pr `custom_tool_call` へ復元します。ネイティブ OpenAI の forward routing と、対応済みの `apply_patch` custom tool は 変更されません。 +ルーティングされた code-mode のターンには、最初の呼び出し前に、ネストされたヘルパーに関する +ホストの規則も伝えられます。`tools.apply_patch` は、装飾を付けないパッチマーカー行で始まり、 +同様のマーカー行で終わる単一の文字列を受け取ります。isolate では `import` を使用できず、 +長時間実行されるコマンドは `write_stdin` でポーリングします。ネイティブのルーティング済み Responses、 +Kiro、または Cursor の経路で、code-mode の exec 結果にホストの失敗メッセージがまだ含まれている場合、 +opencodex は該当する規則を示す 1 行のヒントを追加します。この変更でモデルのコードやパッチのテキストを +書き換えることはありません。 + 選択した provider は function/tool calling をサポートしている必要があります。tool call に対応しない text-only provider では `exec`、Browser、Computer Use は使用できません。ネイティブ OpenAI の項目は上流の tool mode を そのまま維持します。 diff --git a/docs-site/src/content/docs/ko/guides/codex-integration.md b/docs-site/src/content/docs/ko/guides/codex-integration.md index 44551de837..1f324adaf2 100644 --- a/docs-site/src/content/docs/ko/guides/codex-integration.md +++ b/docs-site/src/content/docs/ko/guides/codex-integration.md @@ -149,6 +149,13 @@ history를 업스트림 function tool로 인코딩한 다음 스트리밍된 fun `custom_tool_call`로 복원합니다. 네이티브 OpenAI forward routing과 지원되는 `apply_patch` custom tool은 변경되지 않습니다. +라우팅된 code-mode 턴에는 첫 호출 전에 중첩 helper에 대한 호스트 규칙도 전달됩니다. +`tools.apply_patch`는 별도 장식 없이 패치 마커만 있는 줄로 시작하고 끝나는 하나의 문자열을 받습니다. +isolate에서는 `import`를 사용할 수 없으며, 오래 실행되는 명령은 `write_stdin`으로 폴링합니다. +네이티브 라우팅 Responses, Kiro 또는 Cursor 경로의 code-mode exec 결과에 호스트의 실패 메시지 중 +하나가 여전히 포함되어 있으면, opencodex는 해당 규칙을 명시하는 한 줄짜리 힌트를 덧붙입니다. +이 변경은 모델의 코드나 패치 텍스트를 다시 작성하지 않습니다. + 선택한 provider는 function/tool calling을 지원해야 합니다. tool call을 지원하지 않는 text-only provider에서는 `exec`, Browser 또는 Computer Use를 사용할 수 없습니다. 네이티브 OpenAI 항목은 업스트림 tool mode를 그대로 유지합니다. diff --git a/docs-site/src/content/docs/ru/guides/codex-integration.md b/docs-site/src/content/docs/ru/guides/codex-integration.md index 9707a3ea44..23581e56f6 100644 --- a/docs-site/src/content/docs/ru/guides/codex-integration.md +++ b/docs-site/src/content/docs/ru/guides/codex-integration.md @@ -224,6 +224,14 @@ opencodex кодирует объявление и историю как functio потоковый lifecycle function call в `custom_tool_call` до передачи в Codex. Нативная forward- маршрутизация OpenAI и поддерживаемый custom tool `apply_patch` остаются без изменений. +Перед первым вызовом маршрутизируемые ходы в code-mode также получают правила хоста для вложенных +вспомогательных инструментов: `tools.apply_patch` принимает одну строку, которая начинается и +заканчивается отдельными строками маркеров патча без дополнительного оформления; в isolate нет +`import`, а длительные команды опрашиваются через `write_stdin`. Если результат exec в code-mode +на нативном маршрутизируемом пути Responses, Kiro или Cursor всё ещё содержит одно из сообщений +хоста об ошибке, opencodex добавляет однострочную подсказку с указанием правила. Это изменение +не переписывает код модели или текст её патча. + Выбранный provider должен поддерживать function/tool calling. Text-only provider без tool calls не может использовать `exec`, Browser или Computer Use. Нативные записи OpenAI сохраняют свой upstream tool mode без изменений. diff --git a/docs-site/src/content/docs/tr/guides/codex-integration.md b/docs-site/src/content/docs/tr/guides/codex-integration.md index 02fae0f468..d13af52274 100644 --- a/docs-site/src/content/docs/tr/guides/codex-integration.md +++ b/docs-site/src/content/docs/tr/guides/codex-integration.md @@ -262,6 +262,14 @@ fonksiyon aracı olarak kodlar, ardından akışlı fonksiyon çağrısı yaşam Codex görmeden önce `custom_tool_call`'a geri yükler. Yerel OpenAI iletme yönlendirmesi ve desteklenen `apply_patch` özel aracı değişmeden kalır. +Yönlendirilen code-mode turlarına, ilk çağrıdan önce iç içe geçmiş yardımcılar için geçerli olan +ana makine kuralları da bildirilir: `tools.apply_patch`, yalnızca yama işaretçilerinden oluşan +satırlarla başlayan ve biten tek bir dize alır; isolate içinde `import` yoktur ve uzun süren +komutlar `write_stdin` üzerinden yoklanır. Yerel yönlendirilmiş Responses, Kiro veya Cursor yolundaki +bir code-mode exec sonucu hâlâ ana makinenin hata mesajlarından birini içeriyorsa opencodex, +ilgili kuralı belirten tek satırlık bir ipucu ekler. Bu değişiklik modelin kodunu veya yama metnini +yeniden yazmaz. + Seçilen sağlayıcı fonksiyon/araç çağrısını desteklemelidir. Araç çağrısı desteği olmayan salt metin bir sağlayıcı `exec`, Tarayıcı veya Bilgisayar Kullanımını kullanamaz. Yerel OpenAI satırları yukarı akış araç modunu değiştirmeden tutar. diff --git a/docs-site/src/content/docs/zh-cn/guides/codex-integration.md b/docs-site/src/content/docs/zh-cn/guides/codex-integration.md index 552c0c3f53..0c3df0c0e2 100644 --- a/docs-site/src/content/docs/zh-cn/guides/codex-integration.md +++ b/docs-site/src/content/docs/zh-cn/guides/codex-integration.md @@ -196,6 +196,12 @@ Codex 显示的模型来自一个磁盘上的 catalog(默认是 `$CODEX_HOME/o 历史记录编码成上游 function tool,再在 Codex 收到结果前,把流式 function-call lifecycle 还原成 `custom_tool_call`。原生 OpenAI forward routing 和已支持的 `apply_patch` custom tool 保持不变。 +路由的 code-mode 轮次还会在首次调用前收到宿主对嵌套辅助工具的规则:`tools.apply_patch` +接收一个字符串,首尾必须是没有额外包装的独立补丁标记行;isolate 中没有 `import`,长时间运行的 +命令通过 `write_stdin` 轮询。如果原生路由 Responses、Kiro 或 Cursor 路径上的 code-mode exec +结果仍包含宿主的某条失败消息,opencodex 会追加一行提示,指出对应规则。此变更不会重写模型的 +代码或补丁文本。 + 所选 provider 必须支持 function/tool calling。不支持 tool call 的 text-only provider 无法使用 `exec`、 Browser 或 Computer Use。原生 OpenAI 条目会保持其上游 tool mode 不变。 diff --git a/docs-site/src/content/docs/zh-tw/guides/codex-integration.md b/docs-site/src/content/docs/zh-tw/guides/codex-integration.md index f371457be9..44ee5bbb3b 100644 --- a/docs-site/src/content/docs/zh-tw/guides/codex-integration.md +++ b/docs-site/src/content/docs/zh-tw/guides/codex-integration.md @@ -201,6 +201,12 @@ metadata,使用 Codex 的 `low | medium | high | xhigh | max | ultra` 檔位 歷史編碼成上游 function tool,再於 Codex 看見前將串流 function-call lifecycle 還原成 `custom_tool_call`。原生 OpenAI forward 路由與受支援的 `apply_patch` custom tool 維持不變。 +路由的 code-mode 回合也會在首次呼叫前收到主機對巢狀輔助工具的規則:`tools.apply_patch` +接收一個字串,開頭與結尾必須是沒有額外包裝的獨立補丁標記行;isolate 中沒有 `import`,長時間執行的 +命令透過 `write_stdin` 輪詢。如果原生路由 Responses、Kiro 或 Cursor 路徑上的 code-mode exec +結果仍包含主機的某則失敗訊息,opencodex 會附加一行提示,指出對應規則。這項變更不會重寫模型的 +程式碼或補丁文字。 + 所選 provider 必須支援 function/tool calling。不支援 tool call 的純文字 provider 無法使用 `exec`、 Browser 或 Computer Use。原生 OpenAI 列保留上游 tool mode 不變。