diff --git a/devlog/_plan/260926_kiro_lb_parity2/100_gui_device_login_and_skip_reason.md b/devlog/_plan/260926_kiro_lb_parity2/100_gui_device_login_and_skip_reason.md index 7b6b968df0d..6f7179c8d89 100644 --- a/devlog/_plan/260926_kiro_lb_parity2/100_gui_device_login_and_skip_reason.md +++ b/devlog/_plan/260926_kiro_lb_parity2/100_gui_device_login_and_skip_reason.md @@ -215,7 +215,11 @@ cd docs-site && bun run build as a server follow-up candidate in 000 and in the PR. - **A8 In-flight terminal replies win.** The hook (and finalizer) processes a terminal reply from a status request already in flight even after Cancel; a later 404 still takes the neutral path (A3). A1's promise - becomes: success is shown only after a terminal `done` status reply is observed. + becomes: success is shown only after a terminal `done` status reply is observed. The handoff owns one + parsed status-read operation rather than cloned `Response` bodies. Its 45 s budget covers fetch, body EOF + and JSON parsing; the finalizer also cancels that reader when the flow-wide deadline wins and never waits + a full retry interval beyond the deadline. This preserves the already-sent terminal reply without letting + a stalled body retain the module-scoped singleflight entry indefinitely (#6021). - **A9 Settle split at the guard.** Two helpers: `reloadAccountsAfterLogin(provider)` (awaited `fetchAccountSets`) and `refreshDerivedAfterLogin()` (`fetchConfig`, `fetchProviderQuotas(true)`, `bumpModelsRefresh`). The existing loop keeps its generation/mounted guard **between** them, keeps its diff --git a/devlog/_plan/260927_merge_train_3/070_batch7.md b/devlog/_plan/260927_merge_train_3/070_batch7.md new file mode 100644 index 00000000000..b170783b55b --- /dev/null +++ b/devlog/_plan/260927_merge_train_3/070_batch7.md @@ -0,0 +1,29 @@ +# B7 — GUI bug fixes + +Base: `dev` `429f4e0175` (after B6 #6069). Branch `codex/train3-b7`. + +Previous D (B6): #5925 and #5977 landed. Scope note: the request covers bugs, and only enhancements must avoid the +GUI, so GUI bug fixes are in scope; earlier batches skipped them by a stricter reading. + +| PR | Author | Change | Kimi | UI visible | +|---|---|---|---|---| +| #6025 | Ingwannu | Kiro device-login status reads get one bounded, cancellable operation (fetch, body, decode), so a stalled body can no longer hang the dialog or the finalizer | LAND; its test fails on dev | no | +| #6010 | Ingwannu | The provider deep-link test stops dispatching a second `hashchange` for a changed hash | LAND; flake from CI, not reproduced locally | no (test only) | +| #6007 | Ingwannu | With provider-table routing, the dashboard and the start/sync output warn that some mobile remote thread lists hide openai-tagged history (#5848 mitigation; the issue stays open) | APPROVE; two dev tests fail without it | yes: a hint under the authless or client-compaction switch | + +The batch PR needs a screenshot for #6007. It is taken from this branch's proxy run with `HOME`, `OPENCODEX_HOME` +and `CODEX_HOME` all pointed at a temporary directory, so no real shell profile, Codex config or app integration is +touched, and uploaded through the `pr-assets` branch. + +## Build and evidence + +Carried: `960e482b9e` (#6025), `519b9d7676` (#6010), `b51e20ceb0` (#6007), each keeping Ingwannu's authorship. + +Local proof at `b51e20ceb0`: typecheck, structure and privacy exit 0; `codex-inject` and `codex-inject-integration` +160 pass; the four GUI files (Kiro device login, provider deep link, vision sidecar dashboard, locale parity) 82 pass; +`gui` `tsc -b` exit 0. + +Screenshot: a proxy from this branch on port 18477 with `HOME`, `OPENCODEX_HOME` and `CODEX_HOME` under a temporary +directory (the running proxy on 10100 kept client routing). Aside opened the dashboard, turned on "Open Codex without +signing in" in that temporary config, and captured the row; the image is `pr-assets` `e202d69d1e` +(`260927-train3-b7/remote-history-hint.png`). The temporary proxy was stopped afterwards. diff --git a/docs-site/src/content/docs/guides/codex-integration.md b/docs-site/src/content/docs/guides/codex-integration.md index 2089cd136e9..2a95f14de68 100644 --- a/docs-site/src/content/docs/guides/codex-integration.md +++ b/docs-site/src/content/docs/guides/codex-integration.md @@ -1056,6 +1056,27 @@ When returning to the root-override form, OpenCodex retains an existing `[model_ Enabling the integration in its provider-table form on a home whose `openai`-tagged conversations Codex has already paginated used to be refused outright with `history_paginated_openai_requires_native_writer`: nothing was written and the integration stayed disabled. OpenCodex now completes that transition by keeping the managed root `openai_base_url` override beside the `[model_providers.opencodex]` table. Codex merges the override onto its built-in `openai` provider, so those conversations keep reaching the proxy without being relabeled and no rollout byte or thread row is touched. Only a routing form that requires the `x-opencodex-api-key` admission header still refuses, because Codex's built-in provider cannot carry that header; its message names the two settings that resolve it — route Codex through the loopback listener so the override can be retained, or set `syncResumeHistory` to `false` to accept that those conversations resume against Codex's own OpenAI endpoint. +### Remote thread-list provider filters + +Provider-table routing changes the default provider id for new conversations to `opencodex` while +history that cannot safely be relabeled may remain tagged `openai`. Some native app-server/mobile +versions treat an omitted `thread/list.modelProviders` filter as the current default provider only, +so those existing conversations can disappear from that remote list even though their database row +and rollout are intact. A compatible list client can send `modelProviders: []` to request all +providers. OpenCodex cannot rewrite that RPC because the remote client talks directly to Codex's +native app-server rather than the inference proxy. + +`ocx sync` and `ocx start` include the warning when they apply a provider-table route. If a +user-owned root URL sends the command down the no-routing branch, the CLI omits the warning. In +client-compaction mode, the CLI can retain that URL, apply the `opencodex` provider table, and +include the warning. The dashboard shows a separate preference hint when either setting is enabled. +It appears once if both settings are enabled, regardless of the root URL. The hint reports enabled +preferences; it does not mean Authless Desktop is effective on the current route. Authless Desktop +applies only to effective loopback authless routing and is ignored for remote-client routing or +listeners that require an admission header. The warning is not a migration: OpenCodex does not edit +provider tags merely to influence a client-side list filter. Verify the conversation in native Codex +and the app-server/client version; do not rewrite paginated history to make a remote list include it. + Do not rewrite an active paginated rollout or thread row to migrate those conversations yourself. Close the affected conversation before any recovery, and report the exact error and versions without uploading private history. A backup or a successful script alone does not prove the conversation is visible again. Check the restored conversation in Codex after reopening. ## Experimental native mid-turn steering diff --git a/docs-site/src/content/docs/guides/providers.md b/docs-site/src/content/docs/guides/providers.md index 6a91e7fe200..b8f30a1ca97 100644 --- a/docs-site/src/content/docs/guides/providers.md +++ b/docs-site/src/content/docs/guides/providers.md @@ -451,7 +451,7 @@ an ambiguous token selection), when `KIROCLI_DB_PATH` / `KIRO_CLI_DB_FILE` redir from the live CLI store, or when an existing primary CLI database has no recognized token row. Repair or remove the unreadable database under the normal `kiro-cli` data path, unset those import selectors, then retry. Signing in from a machine with no existing `kiro-cli` session is unaffected. -The native dashboard choices are add-only and do not sign out `kiro-cli`. A device dialog shows the code and verification destination. Only recognized Kiro or Builder ID hosts are opened as links; an unexpected destination is shown as copyable text for review. +The native dashboard choices are add-only and do not sign out `kiro-cli`. A device dialog shows the code and verification destination. Only recognized Kiro or Builder ID hosts are opened as links; an unexpected destination is shown as copyable text for review. Closing the dialog sends cancellation and leaves a bounded background status check to reconcile a commit already in progress. A stalled status response is retried; exhausting the flow deadline produces the neutral ended outcome rather than claiming success. The account list marks Kiro accounts excluded from automatic selection with a reason, when available. ## 3. API-key catalog diff --git a/gui/src/components/use-kiro-device-login.ts b/gui/src/components/use-kiro-device-login.ts index 1e8d2c92379..e4c7e978d39 100644 --- a/gui/src/components/use-kiro-device-login.ts +++ b/gui/src/components/use-kiro-device-login.ts @@ -1,12 +1,15 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { afterOAuthCancellation } from "../oauth-cancellation-barrier"; -import { finalizeKiroDeviceFlow, observeKiroDeviceFinal, type KiroFinalOutcome } from "../kiro-device-login-finalizer"; +import { + finalizeKiroDeviceFlow, observeKiroDeviceFinal, readKiroDeviceStatus, + type KiroFinalOutcome, type KiroStatusRead, +} from "../kiro-device-login-finalizer"; import { parseKiroDeviceView, type KiroDeviceMethod, type KiroDeviceView } from "../kiro-device-login-helpers"; type Phase = "idle" | "starting" | "pending" | "done" | "expired" | "failed" | "cancelled" | "ended"; export type KiroLoginState = { phase: Phase; view?: KiroDeviceView; error?: "start" | "network" | "invalid" }; -type Session = { closed: boolean; view?: KiroDeviceView; inFlight?: Promise; - waitController?: AbortController; terminal?: KiroFinalOutcome }; +type Session = { closed: boolean; closedController: AbortController; view?: KiroDeviceView; + inFlight?: KiroStatusRead; waitController?: AbortController; terminal?: KiroFinalOutcome }; const wait = (ms: number, signal: AbortSignal) => new Promise(resolve => { if (signal.aborted) { resolve(); return; } const timer = setTimeout(() => { signal.removeEventListener("abort", stop); resolve(); }, ms); @@ -16,7 +19,19 @@ const wait = (ms: number, signal: AbortSignal) => new Promise(resolve => { const CLOSED = Symbol("closed"); /** The awaited value, or CLOSED when the session closed while it was pending (a late reply belongs to the finalizer). */ const unlessClosed = (session: Session, value: Promise): Promise => - value.then(result => (session.closed ? CLOSED : result)); + new Promise(resolve => { + if (session.closed) { resolve(CLOSED); return; } + let done = false; + const settle = (result: T | typeof CLOSED) => { + if (done) return; + done = true; + session.closedController.signal.removeEventListener("abort", stop); + resolve(result); + }; + const stop = () => settle(CLOSED); + session.closedController.signal.addEventListener("abort", stop, { once: true }); + void value.then(result => settle(session.closed ? CLOSED : result), () => settle(CLOSED)); + }); export function useKiroDeviceLogin(apiBase: string, onSettled?: (provider: string, outcome: KiroFinalOutcome) => void, pollDelay: (ms: number, signal: AbortSignal) => Promise = wait) { @@ -44,6 +59,7 @@ export function useKiroDeviceLogin(apiBase: string, onSettled?: (provider: strin const session = sessionRef.current; if (!session || session.closed) return; session.closed = true; + session.closedController.abort(); sessionRef.current = null; session.waitController?.abort(); if (mountedRef.current) setState({ phase: "cancelled" }); @@ -64,7 +80,7 @@ export function useKiroDeviceLogin(apiBase: string, onSettled?: (provider: strin const start = useCallback(async (method: KiroDeviceMethod) => { if (sessionRef.current) return; - const session: Session = { closed: false }; + const session: Session = { closed: false, closedController: new AbortController() }; sessionRef.current = session; setState({ phase: "starting" }); let response: Response | undefined; @@ -114,21 +130,18 @@ export function useKiroDeviceLogin(apiBase: string, onSettled?: (provider: strin break; } const flowId = view.flowId; - const request = fetch(`${apiBase}/api/oauth/status?provider=kiro&flowId=${encodeURIComponent(flowId)}`).catch(() => null); - session.inFlight = request.then(response => response?.clone() ?? null); - const status = await unlessClosed(session, request); + const request = readKiroDeviceStatus(apiBase, flowId); + session.inFlight = request; + const status = await unlessClosed(session, request.result); if (status === CLOSED) break; - if (status?.status === 404) { - session.inFlight = undefined; + session.inFlight = undefined; + if (status.kind === "missing") { session.terminal = "ended"; settledRef.current?.("kiro", "ended"); setState({ phase: "ended", view: session.view }); break; } - const body = status?.ok ? await unlessClosed(session, status.json().catch(() => null)) : null; - if (body === CLOSED) break; - const next = body === null ? null : parseKiroDeviceView(body); - session.inFlight = undefined; + const next = status.kind === "view" ? status.view : null; if (!next || next.flowId !== flowId) continue; session.view = next; if (next.state === "pending") { setState({ phase: "pending", view: next }); continue; } diff --git a/gui/src/i18n/de.ts b/gui/src/i18n/de.ts index 2b83d98306a..c9dfd974a3c 100644 --- a/gui/src/i18n/de.ts +++ b/gui/src/i18n/de.ts @@ -3141,6 +3141,7 @@ export const de: Record = { "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.codexClientCompaction": "Clientseitige Komprimierung verwenden", "dash.codexClientCompactionHint": "Standardmäßig aus; nur für authentifiziertes Loopback-Routing. Künftige Komprimierungen speichern portable Klartext-Zusammenfassungen, während das OpenCodeX-Provider-Routing und die V2-Subagent-Zustellung aktiv bleiben; der konfigurierte Anbieter kann sie verarbeiten und Kontingent verbrauchen. Vorhandene ocx1-Verläufe müssen weiterhin wiederhergestellt werden. Codex nach einer Änderung neu starten.", + "dash.codexRemoteHistoryHint": "Provider-Tabellen können vorhandene Threads mit openai-Kennung in manchen mobilen Remote-Listen ausblenden. Der Verlauf wird nicht gelöscht. Der Remote-Client muss alle Provider auflisten; dieser Schalter korrigiert dessen Filter nicht.", "models.newPolicyGlobal": "Neue Modelle zunächst deaktivieren", "models.newPolicyProvider": "Richtlinie für neue Modelle", "models.fastProvider": "Fast-Modus", "models.fastProviderHint": "Verbraucht Nutzungsguthaben zum doppelten Preis", "models.fastEnabled": "Fast-Modus an", "models.fastDisabled": "Fast-Modus aus", "models.fastSaveFailed": "Fast-Modus konnte nicht gespeichert werden", "models.newPolicy_inherit": "Übernehmen", "models.newPolicy_off": "Aus", "models.newPolicy_on": "An", "models.newBadge": "NEU", "models.newCount": "{count} neu, aus", diff --git a/gui/src/i18n/en.ts b/gui/src/i18n/en.ts index dad65cfc6dd..d5eeb2e9273 100644 --- a/gui/src/i18n/en.ts +++ b/gui/src/i18n/en.ts @@ -718,6 +718,7 @@ export const en = { "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.codexClientCompaction": "Use client-side compaction", "dash.codexClientCompactionHint": "Off by default; authenticated loopback only. Future compactions store portable plaintext summaries while OpenCodeX and V2 provider routing stay active; the configured provider may process them and consume quota. History is left untouched, and existing threads keep routing through the proxy via the openai_base_url override OpenCodeX manages; if you set that line yourself it is kept, and those threads follow your destination instead. Existing ocx1 history stays recoverable; recover a thread separately only before replaying it in native Codex. Restart Codex after changing this setting.", + "dash.codexRemoteHistoryHint": "Provider-table routing can hide existing openai-tagged threads in some mobile remote lists. History is not deleted. The remote client must list all providers; this switch does not repair that client filter.", "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 ce302bb3e37..5ca92bf3726 100644 --- a/gui/src/i18n/fr.ts +++ b/gui/src/i18n/fr.ts @@ -703,6 +703,7 @@ export const fr: Record = { "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.codexClientCompaction": "Utiliser la compaction côté client", "dash.codexClientCompactionHint": "Désactivé par défaut, uniquement pour le routage loopback authentifié. Les compactages futurs stockent des résumés portables en texte clair tout en conservant le routage OpenCodeX/V2 ; le fournisseur configuré peut les traiter et consommer son quota. L'historique ocx1 existant doit toujours être restauré. Redémarrez Codex après modification.", + "dash.codexRemoteHistoryHint": "Le routage par table de fournisseurs peut masquer les fils existants marqués openai dans certaines listes mobiles distantes. L’historique n’est pas supprimé. Le client distant doit lister tous les fournisseurs ; ce réglage ne corrige pas son filtre.", "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 938ee65ec43..67b3a4973f3 100644 --- a/gui/src/i18n/ja.ts +++ b/gui/src/i18n/ja.ts @@ -3163,6 +3163,7 @@ export const ja: Record = { "dash.codexDesktopAuthlessHint": "既定ではオフです。対象のローカル接続で Desktop の個別ログインを省略します。上流プロバイダーの認証情報は引き続き必要です。変更後は Codex を再起動してください。アカウントに依存する Desktop 機能が利用できない場合があります。", "dash.codexClientCompaction": "クライアント側コンパクションを使用", "dash.codexClientCompactionHint": "既定ではオフで、認証済みループバックルーティング専用です。今後のコンパクションは、OpenCodeX と V2 プロバイダーのルーティングを維持したまま移植可能な平文要約を保存します。設定済みプロバイダーが要約を処理し、割り当てを消費する場合があります。既存の ocx1 履歴は別途復旧が必要です。変更後は Codex を再起動してください。", + "dash.codexRemoteHistoryHint": "プロバイダーテーブル方式では、一部のモバイルリモート一覧で既存の openai タグ付きスレッドが表示されない場合があります。履歴は削除されていません。リモートクライアントは全プロバイダーを一覧取得する必要があり、このスイッチはそのフィルターを修正しません。", "models.newPolicyGlobal": "新しいモデルを無効で追加", "models.newPolicyProvider": "新しいモデルのポリシー", "models.fastProvider": "Fast モード", "models.fastProviderHint": "使用クレジットを 2 倍の料金で消費します", "models.fastEnabled": "Fast モードをオンにしました", "models.fastDisabled": "Fast モードをオフにしました", "models.fastSaveFailed": "Fast モードを保存できませんでした", "models.newPolicy_inherit": "継承", "models.newPolicy_off": "オフ", "models.newPolicy_on": "オン", "models.newBadge": "新着", "models.newCount": "新着 {count} 件、オフ", diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts index 80b4af705c1..ecb3c2b1003 100644 --- a/gui/src/i18n/ko.ts +++ b/gui/src/i18n/ko.ts @@ -3163,6 +3163,7 @@ export const ko: Record = { "dash.codexDesktopAuthlessHint": "기본값은 꺼짐입니다. 지원되는 로컬 연결에서 별도의 Desktop 로그인을 건너뜁니다. 업스트림 인증 정보는 여전히 필요합니다. 변경 후 Codex를 다시 시작하세요. 계정에 연결된 Desktop 기능을 사용하지 못할 수 있습니다.", "dash.codexClientCompaction": "클라이언트 측 컴팩션 사용", "dash.codexClientCompactionHint": "기본값은 꺼짐이며 인증된 루프백 라우팅에만 적용됩니다. 향후 컴팩션은 OpenCodeX 및 V2 제공자 라우팅을 유지하면서 이식 가능한 평문 요약을 저장합니다. 설정된 제공자가 요약을 처리하고 할당량을 사용할 수 있습니다. 기록은 건드리지 않으며, 기존 스레드는 OpenCodeX가 관리하는 openai_base_url override를 통해 프록시 경로를 유지합니다. 그 줄을 직접 설정해 두셨다면 그대로 보존하므로 해당 스레드는 설정하신 목적지를 따릅니다. 기존 ocx1 기록은 그대로 복구할 수 있고, 네이티브 Codex에서 해당 스레드를 재개하기 전에만 별도로 복구하세요. 변경 후 Codex를 다시 시작하세요.", + "dash.codexRemoteHistoryHint": "제공자 테이블 방식에서는 일부 모바일 원격 목록에 기존 openai 태그 스레드가 보이지 않을 수 있습니다. 기록이 삭제된 것은 아닙니다. 원격 클라이언트가 모든 제공자를 조회해야 하며, 이 스위치는 해당 목록 필터를 수정하지 않습니다.", "models.newPolicyGlobal": "새 모델을 비활성화 상태로 추가", "models.newPolicyProvider": "새 모델 정책", "models.fastProvider": "Fast 모드", "models.fastProviderHint": "사용 크레딧을 2배 가격으로 소모합니다", "models.fastEnabled": "Fast 모드를 켰습니다", "models.fastDisabled": "Fast 모드를 껐습니다", "models.fastSaveFailed": "Fast 모드를 저장하지 못했습니다", "models.newPolicy_inherit": "상속", "models.newPolicy_off": "끔", "models.newPolicy_on": "켬", "models.newBadge": "신규", "models.newCount": "신규 {count}개, 꺼짐", diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts index 2fcf9a11a33..16a41bf6975 100644 --- a/gui/src/i18n/ru.ts +++ b/gui/src/i18n/ru.ts @@ -3164,6 +3164,7 @@ export const ru: Record = { "dash.codexDesktopAuthlessHint": "По умолчанию выключено. Пропускает отдельный вход в Desktop для допустимых локальных подключений. Учётные данные провайдера по-прежнему нужны. После изменения перезапустите Codex. Функции Desktop, связанные с аккаунтом, могут быть недоступны.", "dash.codexClientCompaction": "Использовать сжатие на стороне клиента", "dash.codexClientCompactionHint": "По умолчанию выключено; только для аутентифицированной loopback-маршрутизации. Будущие сжатия сохраняют переносимые текстовые сводки, а маршрутизация OpenCodeX и V2 остаётся активной; настроенный провайдер может обрабатывать сводки и расходовать квоту. Существующую историю ocx1 всё равно нужно восстановить. После изменения перезапустите Codex.", + "dash.codexRemoteHistoryHint": "Таблица провайдеров может скрыть существующие диалоги с меткой openai в некоторых мобильных удалённых списках. История не удаляется. Удалённый клиент должен запрашивать все провайдеры; этот переключатель не исправляет его фильтр.", "models.newPolicyGlobal": "Добавлять новые модели выключенными", "models.newPolicyProvider": "Политика новых моделей", "models.fastProvider": "Режим Fast", "models.fastProviderHint": "Расходует кредиты использования по двойной цене", "models.fastEnabled": "Режим Fast включён", "models.fastDisabled": "Режим Fast выключен", "models.fastSaveFailed": "Не удалось сохранить режим Fast", "models.newPolicy_inherit": "Наследовать", "models.newPolicy_off": "Выкл.", "models.newPolicy_on": "Вкл.", "models.newBadge": "НОВАЯ", "models.newCount": "Новых: {count}, выкл.", diff --git a/gui/src/i18n/tr.ts b/gui/src/i18n/tr.ts index 4d7f56ccedd..b592e84752c 100644 --- a/gui/src/i18n/tr.ts +++ b/gui/src/i18n/tr.ts @@ -3164,6 +3164,7 @@ export const tr: Record = { "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.codexClientCompaction": "İstemci tarafı sıkıştırmayı kullan", "dash.codexClientCompactionHint": "Varsayılan olarak kapalıdır ve yalnızca kimliği doğrulanmış geri döngü yönlendirmesinde geçerlidir. Gelecekteki sıkıştırmalar, OpenCodeX ve V2 sağlayıcı yönlendirmesi etkin kalırken taşınabilir düz metin özetleri kaydeder; yapılandırılmış sağlayıcı bunları işleyip kotasını tüketebilir. Mevcut ocx1 geçmişi yine ayrıca kurtarılmalıdır. Değişiklikten sonra Codex’i yeniden başlatın.", + "dash.codexRemoteHistoryHint": "Sağlayıcı tablosuyla yönlendirme, mevcut openai etiketli konuşmaları bazı mobil uzak listelerde gizleyebilir. Geçmiş silinmez. Uzak istemci tüm sağlayıcıları listelemelidir; bu anahtar istemcinin filtresini düzeltmez.", "models.newPolicyGlobal": "Yeni modeller devre dışı başlasın", "models.newPolicyProvider": "Yeni model ilkesi", "models.fastProvider": "Fast modu", "models.fastProviderHint": "Kullanım kredilerini 2 kat fiyatla harcar", "models.fastEnabled": "Fast modu açık", "models.fastDisabled": "Fast modu kapalı", "models.fastSaveFailed": "Fast modu kaydedilemedi", "models.newPolicy_inherit": "Devral", "models.newPolicy_off": "Kapalı", "models.newPolicy_on": "Açık", "models.newBadge": "YENİ", "models.newCount": "{count} yeni, kapalı", diff --git a/gui/src/i18n/vi.ts b/gui/src/i18n/vi.ts index 5dba045198c..4be42b9ebc6 100644 --- a/gui/src/i18n/vi.ts +++ b/gui/src/i18n/vi.ts @@ -703,6 +703,7 @@ export const vi: Record = { "dash.codexDesktopAuthlessHint": "Mặc định tắt. Bỏ qua bước đăng nhập Desktop riêng cho các kết nối cục bộ hợp lệ. Vẫn cần thông tin xác thực upstream. Khởi động lại Codex sau khi thay đổi cài đặt này. Các tính năng Desktop yêu cầu tài khoản có thể không khả dụng.", "dash.codexClientCompaction": "Sử dụng tính năng thu gọn phía client (client-side compaction)", "dash.codexClientCompactionHint": "Mặc định tắt; chỉ dành cho authenticated loopback. Các compactions trong tương lai sẽ lưu trữ các bản tóm tắt văn bản thuần trong khi OpenCodeX và V2 provider routing vẫn hoạt động; provider được cấu hình có thể xử lý chúng và tiêu tốn quota. Lịch sử được giữ nguyên, và các luồng (threads) hiện tại tiếp tục định tuyến qua proxy thông qua ghi đè openai_base_url mà OpenCodeX quản lý; nếu bạn tự thiết lập dòng đó, nó sẽ được giữ lại và các luồng đó sẽ tuân theo đích đến của bạn thay thế. Lịch sử ocx1 hiện tại vẫn có thể khôi phục được; chỉ khôi phục một luồng riêng biệt trước khi phát lại (replay) trong Codex native. Khởi động lại Codex sau khi thay đổi cài đặt này.", + "dash.codexRemoteHistoryHint": "Định tuyến bằng bảng nhà cung cấp có thể ẩn các luồng mang nhãn openai hiện có khỏi một số danh sách từ xa trên điện thoại. Lịch sử không bị xóa. Client từ xa phải liệt kê mọi nhà cung cấp; công tắc này không sửa bộ lọc của client.", "models.v2Conflict": "[agents] max_threads đã được thiết lập — codex sẽ từ chối khởi động; hãy xoá nó khỏi config.toml", "models.v2Applied": "Chế độ agent con đã được cập nhật — áp dụng cho các phiên mới (khởi động lại ứng dụng Codex để làm mới picker)", "models.v2ThreadsLabel": "Luồng tối đa (Max threads)", diff --git a/gui/src/i18n/zh-TW.ts b/gui/src/i18n/zh-TW.ts index f8aee1a7a86..9285fe2a451 100644 --- a/gui/src/i18n/zh-TW.ts +++ b/gui/src/i18n/zh-TW.ts @@ -3127,6 +3127,7 @@ export const zhTW: Record = { "dash.codexDesktopAuthlessHint": "預設關閉。為符合條件的本機連線略過獨立的 Desktop 登入。仍需上游供應商憑證。變更後請重新啟動 Codex。依賴帳戶的 Desktop 功能可能無法使用。", "dash.codexClientCompaction": "使用用戶端壓縮", "dash.codexClientCompactionHint": "預設關閉,僅適用於已驗證的 loopback 路由。未來壓縮會儲存可攜的純文字摘要,同時保留 OpenCodeX 與 V2 提供方路由;已設定的提供方可能處理摘要並消耗其額度。既有 ocx1 歷程仍須另行復原。變更後請重新啟動 Codex。", + "dash.codexRemoteHistoryHint": "供應商表路由可能使既有的 openai 標記對話在部分行動版遠端清單中隱藏。歷程並未刪除。遠端用戶端必須列出所有供應商;此開關不會修正用戶端的清單篩選器。", "models.newPolicyGlobal": "新模型預設停用", "models.newPolicyProvider": "新模型策略", "models.fastProvider": "Fast 模式", "models.fastProviderHint": "以 2 倍價格消耗用量額度", "models.fastEnabled": "已開啟 Fast 模式", "models.fastDisabled": "已關閉 Fast 模式", "models.fastSaveFailed": "無法儲存 Fast 模式", "models.newPolicy_inherit": "繼承", "models.newPolicy_off": "關閉", "models.newPolicy_on": "開啟", "models.newBadge": "新增", "models.newCount": "{count} 個新增,已關閉", diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts index 0e740414da5..d30a30fe4e1 100644 --- a/gui/src/i18n/zh.ts +++ b/gui/src/i18n/zh.ts @@ -3162,6 +3162,7 @@ export const zh: Record = { "dash.codexDesktopAuthlessHint": "默认关闭。为符合条件的本地连接跳过单独的 Desktop 登录。仍需上游提供商凭据。更改后请重启 Codex。依赖账户的 Desktop 功能可能不可用。", "dash.codexClientCompaction": "使用客户端压缩", "dash.codexClientCompactionHint": "默认关闭,仅适用于已认证的 loopback 路由。未来压缩会保存可移植的明文摘要,同时保留 OpenCodeX 与 V2 提供方路由;已配置的提供方可能处理摘要并消耗其额度。已有 ocx1 历史仍需单独恢复。更改后请重启 Codex。", + "dash.codexRemoteHistoryHint": "提供商表路由可能使已有的 openai 标签会话在部分移动端远程列表中隐藏。历史记录并未删除。远程客户端必须列出所有提供商;此开关不会修复客户端的列表过滤器。", "models.newPolicyGlobal": "新模型默认停用", "models.newPolicyProvider": "新模型策略", "models.fastProvider": "Fast 模式", "models.fastProviderHint": "按 2 倍价格消耗用量额度", "models.fastEnabled": "已开启 Fast 模式", "models.fastDisabled": "已关闭 Fast 模式", "models.fastSaveFailed": "无法保存 Fast 模式", "models.newPolicy_inherit": "继承", "models.newPolicy_off": "关闭", "models.newPolicy_on": "开启", "models.newBadge": "新增", "models.newCount": "{count} 个新增,已关闭", diff --git a/gui/src/kiro-device-login-finalizer.ts b/gui/src/kiro-device-login-finalizer.ts index a2327ea397f..dff5ad25890 100644 --- a/gui/src/kiro-device-login-finalizer.ts +++ b/gui/src/kiro-device-login-finalizer.ts @@ -1,16 +1,84 @@ import { parseKiroDeviceView, type KiroDeviceView } from "./kiro-device-login-helpers"; export type KiroFinalOutcome = "added" | "ended" | "failed"; +export type KiroStatusResult = + | { kind: "view"; view: KiroDeviceView } + | { kind: "missing" } + | { kind: "retry" }; +export type KiroStatusRead = { result: Promise; cancel: () => void }; type Listener = (outcome: KiroFinalOutcome) => void; const active = new Map>(); const terminal = new Map(); const listeners = new Map>(); const keyFor = (apiBase: string, flowId: string) => JSON.stringify([apiBase, flowId]); const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); -async function boundedRead(read: Promise, ms: number): Promise { +const MAX_STATUS_BODY_BYTES = 64 * 1024; + +function cancelBody(response: Response): void { + try { void response.body?.cancel().catch(() => {}); } catch { /* best effort */ } +} + +/** Own fetch, body EOF and parsing as one cancellable operation; headers alone are not completion. */ +export function readKiroDeviceStatus(apiBase: string, flowId: string, timeoutMs = 45_000): KiroStatusRead { + const controller = new AbortController(); + let reader: ReadableStreamDefaultReader | undefined; + let settled = false; + let finish!: (result: KiroStatusResult) => void; + const result = new Promise(resolve => { finish = resolve; }); + const settle = (value: KiroStatusResult) => { + if (settled) return; + settled = true; + clearTimeout(timer); + finish(value); + }; + const cancel = () => { + if (settled) return; + controller.abort(); + try { void reader?.cancel().catch(() => {}); } catch { /* best effort */ } + // Transport abort and stream cancellation are cooperative. The caller's deadline is not. + settle({ kind: "retry" }); + }; + const timer = setTimeout(cancel, Math.max(0, timeoutMs)); + void (async () => { + try { + const response = await fetch( + `${apiBase}/api/oauth/status?provider=kiro&flowId=${encodeURIComponent(flowId)}`, + { signal: controller.signal }, + ); + if (settled) { cancelBody(response); return; } + if (response.status === 404) { cancelBody(response); settle({ kind: "missing" }); return; } + if (!response.ok || !response.body) { cancelBody(response); settle({ kind: "retry" }); return; } + reader = response.body.getReader(); + const decoder = new TextDecoder(); + let text = ""; + let bytes = 0; + while (true) { + const chunk = await reader.read(); + if (settled) return; + if (chunk.done) break; + bytes += chunk.value.byteLength; + if (bytes > MAX_STATUS_BODY_BYTES) { cancel(); return; } + text += decoder.decode(chunk.value, { stream: true }); + } + text += decoder.decode(); + let decoded: unknown; + try { decoded = JSON.parse(text); } catch { settle({ kind: "retry" }); return; } + const view = parseKiroDeviceView(decoded); + settle(view ? { kind: "view", view } : { kind: "retry" }); + } catch { settle({ kind: "retry" }); } + })(); + return { result, cancel }; +} + +async function awaitStatusRead(read: KiroStatusRead, ms: number): Promise { let timer: ReturnType | undefined; try { - return await Promise.race([read, new Promise(resolve => { timer = setTimeout(() => resolve(null), ms); })]); + return await Promise.race([ + read.result, + new Promise(resolve => { + timer = setTimeout(() => { read.cancel(); resolve({ kind: "retry" }); }, Math.max(0, ms)); + }), + ]); } finally { if (timer) clearTimeout(timer); } } @@ -45,7 +113,7 @@ export function observeKiroDeviceFinal(apiBase: string, flowId: string, view: Ki /** Detached reconciliation survives dialog and page unmount. One loop per flowId. */ export function finalizeKiroDeviceFlow(apiBase: string, flowId: string, expiresAt?: number, - inFlight?: Promise): Promise { + inFlight?: KiroStatusRead): Promise { const key = keyFor(apiBase, flowId); const prior = active.get(key); if (prior) return prior; @@ -55,27 +123,23 @@ export function finalizeKiroDeviceFlow(apiBase: string, flowId: string, expiresA const run = (async () => { let pending = inFlight; while (Date.now() < deadline) { - let response: Response | null; - try { - const remaining = deadline - Date.now(); - const timeout = Math.min(45_000, remaining); - response = await boundedRead(pending ?? fetch( - `${apiBase}/api/oauth/status?provider=kiro&flowId=${encodeURIComponent(flowId)}`, - { signal: AbortSignal.timeout(timeout) }, - ), timeout); - } catch { response = null; } + const remaining = deadline - Date.now(); + const read = pending ?? readKiroDeviceStatus(apiBase, flowId, Math.min(45_000, remaining)); pending = undefined; + const status = await awaitStatusRead(read, remaining); if (terminal.has(key)) return terminal.get(key)!; - if (response?.status === 404) return finish(apiBase, flowId, "ended"); - if (response?.ok) { - const view = parseKiroDeviceView(await response.json().catch(() => null)); - if (view) { - const result = observeKiroDeviceFinal(apiBase, flowId, view); - if (result) return result; - } + if (status.kind === "missing") return finish(apiBase, flowId, "ended"); + if (status.kind === "view") { + const result = observeKiroDeviceFinal(apiBase, flowId, status.view); + if (result) return result; } - await delay(2_000); + const retryRemaining = deadline - Date.now(); + if (retryRemaining <= 0) break; + await delay(Math.min(2_000, retryRemaining)); } + // A close can hand off after expiry. Do not leave that inherited transport alive merely + // because there was no remaining loop iteration in which the deadline wrapper could cancel it. + pending?.cancel(); return finish(apiBase, flowId, "ended"); })().finally(() => { active.delete(key); }); active.set(key, run); diff --git a/gui/src/pages/dashboard-overview-sections.tsx b/gui/src/pages/dashboard-overview-sections.tsx index 8ea50ba758c..f3e61161b88 100644 --- a/gui/src/pages/dashboard-overview-sections.tsx +++ b/gui/src/pages/dashboard-overview-sections.tsx @@ -517,6 +517,7 @@ export function DashboardSidecarPanels({ d }: { d: Dash }) {
{t("dash.codexDesktopAuthless")}
{t("dash.codexDesktopAuthlessHint")}
+ {settings?.codexDesktopAuthless &&
{t("dash.codexRemoteHistoryHint")}
} {settings?.catalogRefreshPending &&
{t("codexAuth.catalogRefreshPending")}
}