Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
29 changes: 29 additions & 0 deletions devlog/_plan/260927_merge_train_3/070_batch7.md
Original file line number Diff line number Diff line change
@@ -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.
21 changes: 21 additions & 0 deletions docs-site/src/content/docs/guides/codex-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs-site/src/content/docs/guides/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
41 changes: 27 additions & 14 deletions gui/src/components/use-kiro-device-login.ts
Original file line number Diff line number Diff line change
@@ -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<Response | null>;
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<void>(resolve => {
if (signal.aborted) { resolve(); return; }
const timer = setTimeout(() => { signal.removeEventListener("abort", stop); resolve(); }, ms);
Expand All @@ -16,7 +19,19 @@ const wait = (ms: number, signal: AbortSignal) => new Promise<void>(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 = <T,>(session: Session, value: Promise<T>): Promise<T | typeof CLOSED> =>
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<void> = wait) {
Expand Down Expand Up @@ -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" });
Expand All @@ -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;
Expand Down Expand Up @@ -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; }
Expand Down
1 change: 1 addition & 0 deletions gui/src/i18n/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3141,6 +3141,7 @@ export const de: Record<TKey, string> = {
"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",
Expand Down
1 change: 1 addition & 0 deletions gui/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions gui/src/i18n/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -703,6 +703,7 @@ export const fr: Record<TKey, string> = {
"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",
Expand Down
1 change: 1 addition & 0 deletions gui/src/i18n/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3163,6 +3163,7 @@ export const ja: Record<TKey, string> = {
"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} 件、オフ",
Expand Down
Loading
Loading