From dfe2323d669b2af8513c99d27a4060ea1be21334 Mon Sep 17 00:00:00 2001 From: Sonui Date: Fri, 21 Aug 2026 02:41:20 +0800 Subject: [PATCH] fix(desktop): show the Codex device sign-in code on connection-detail re-login MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The connection detail sheet's re-login notice drives the same browser-assisted OAuth flow as the provider catalog panel, but never rendered the flow's stateHint. For Codex that hint is the 9-digit device user code the authorization page requires — the verification URL does not embed it — so a re-login started from the notice could never be completed. - The relogin service mapping moves to a leaf module (oauth-relogin-service.ts) and gains showsDeviceCode: true for Codex, false for xAI, whose page needs no manual code (mirrors the catalog panel's !isXai guard). The leaf shape keeps the mapping loadable by the node:test suite. - OAuthReloginNotice appends the sign-in code to the banner description while authorization is pending, reusing the catalog's deviceCode copy. Fixes #3357 Generated-by: Claude Code (Fable 5) Co-Authored-By: Claude Fable 5 --- .../oauth-relogin-device-code.test.ts | 68 +++++++++++++++++++ .../settings/oauth-relogin-service.ts | 44 ++++++++++++ .../settings/provider-connection-detail.tsx | 14 +++- .../settings/use-connection-detail.ts | 34 +--------- 4 files changed, 124 insertions(+), 36 deletions(-) create mode 100644 apps/desktop/src/main/__tests__/oauth-relogin-device-code.test.ts create mode 100644 apps/desktop/src/renderer/settings/oauth-relogin-service.ts diff --git a/apps/desktop/src/main/__tests__/oauth-relogin-device-code.test.ts b/apps/desktop/src/main/__tests__/oauth-relogin-device-code.test.ts new file mode 100644 index 0000000000..9d91a285ec --- /dev/null +++ b/apps/desktop/src/main/__tests__/oauth-relogin-device-code.test.ts @@ -0,0 +1,68 @@ +import { strict as assert } from 'node:assert'; +import { afterEach, describe, it } from 'node:test'; +import type { DesktopRuntimeHostRef } from '../../preload/bridge-contract.js'; +import { oauthLoginServiceFor } from '../../renderer/settings/oauth-relogin-service.js'; + +// Pins the per-service device-code contract behind the connection detail's +// 重新登录 notice. Codex's device-authorization page has no code in its URL — +// the user must type the `stateHint` the flow surfaces, so the notice must +// render it (it silently dropped it before this pin existed). xAI's page +// needs no manual code, matching the catalog panel's `!isXai` guard. + +const HOST: DesktopRuntimeHostRef = { profileId: 'default', hostId: 'host-a' }; + +type MakaWindow = { maka: Record }; +const previousWindow = (globalThis as { window?: unknown }).window; + +function installBridgeStubs(): { codexCalls: string[] } { + const codexCalls: string[] = []; + const codexStub = new Proxy( + {}, + { + get: + (_target, method: string) => + (...args: unknown[]) => { + codexCalls.push(`${method}:${JSON.stringify(args)}`); + return Promise.resolve({ ok: true }); + }, + }, + ); + (globalThis as unknown as { window: MakaWindow }).window = { + maka: { openAiCodex: codexStub, xaiOAuth: {} }, + }; + return { codexCalls }; +} + +afterEach(() => { + (globalThis as { window?: unknown }).window = previousWindow; +}); + +describe('oauthLoginServiceFor device-code contract', () => { + it('marks Codex as needing the device sign-in code shown', () => { + installBridgeStubs(); + const service = oauthLoginServiceFor('openai-codex', HOST); + assert.ok(service, 'codex must be re-login capable'); + assert.equal(service.showsDeviceCode, true); + }); + + it('keeps xAI on the no-code browser flow', () => { + installBridgeStubs(); + const service = oauthLoginServiceFor('xai-oauth', HOST); + assert.ok(service, 'xai must be re-login capable'); + assert.equal(service.showsDeviceCode, false); + }); + + it('returns null for providers without a browser-assisted re-login', () => { + installBridgeStubs(); + assert.equal(oauthLoginServiceFor('openai-compatible', HOST), null); + assert.equal(oauthLoginServiceFor('claude-subscription', HOST), null); + }); + + it('routes the codex bridge through the host-scoped preload surface', async () => { + const { codexCalls } = installBridgeStubs(); + const service = oauthLoginServiceFor('openai-codex', HOST); + assert.ok(service); + await service.bridge.getAccountState(); + assert.deepEqual(codexCalls, [`getAccountState:${JSON.stringify([HOST])}`]); + }); +}); diff --git a/apps/desktop/src/renderer/settings/oauth-relogin-service.ts b/apps/desktop/src/renderer/settings/oauth-relogin-service.ts new file mode 100644 index 0000000000..d09c00b675 --- /dev/null +++ b/apps/desktop/src/renderer/settings/oauth-relogin-service.ts @@ -0,0 +1,44 @@ +import type { ProviderType } from '@maka/core/llm-connections'; +import type { DesktopRuntimeHostRef } from '../../preload/bridge-contract.js'; +import { runtimeHostOAuthLoginBridge } from './runtime-host-settings-bridge.js'; +import type { OAuthLoginFlowBridge } from './use-oauth-login-flow.js'; + +// Maps an OAuth model-connection provider type to the browser-assisted login +// service that can re-run its authorization from inside the connection dialog. Only +// the browser-assisted services (Codex and xAI) are one-button-drivable +// here; Claude's paste-code flow and plain API-key providers return null so the +// notice falls back to prose instead of rendering a dead button. +// +// A leaf module (no React, no hook imports) so the mapping stays loadable by +// the node:test suite that pins the device-code contract. +export interface OAuthLoginService { + bridge: OAuthLoginFlowBridge; + display: { name: string; shortName: string }; + // Codex's device-authorization page requires the user to type the code the + // flow surfaces as `stateHint` — the verification URL does not embed it, so + // the notice must show it or the login cannot be completed. xAI's page + // needs no manual code, mirroring the catalog panel's `!isXai` guard. + showsDeviceCode: boolean; +} + +export function oauthLoginServiceFor( + providerType: ProviderType, + host: DesktopRuntimeHostRef, +): OAuthLoginService | null { + switch (providerType) { + case 'openai-codex': + return { + bridge: runtimeHostOAuthLoginBridge(window.maka.openAiCodex, host), + display: { name: 'OpenAI Codex', shortName: 'Codex' }, + showsDeviceCode: true, + }; + case 'xai-oauth': + return { + bridge: runtimeHostOAuthLoginBridge(window.maka.xaiOAuth, host), + display: { name: 'xAI Grok', shortName: 'SuperGrok / X Premium' }, + showsDeviceCode: false, + }; + default: + return null; + } +} diff --git a/apps/desktop/src/renderer/settings/provider-connection-detail.tsx b/apps/desktop/src/renderer/settings/provider-connection-detail.tsx index 9dc89d7bdd..b9e4f821e5 100644 --- a/apps/desktop/src/renderer/settings/provider-connection-detail.tsx +++ b/apps/desktop/src/renderer/settings/provider-connection-detail.tsx @@ -45,8 +45,8 @@ import type { StatusSemantic } from '@maka/ui'; import { useConnectionDetail, type ConnectionDetailProps, - type OAuthLoginService, } from './use-connection-detail'; +import type { OAuthLoginService } from './oauth-relogin-service.js'; import { formatRequestBodyOverlay, parseRequestBodyOverlay, @@ -828,7 +828,8 @@ function OAuthReloginNotice(props: { hasSecret: CredentialPresenceStatus; onRelogin(): Promise; }) { - const copy = getProviderSettingsCopy(useUiLocale()).detail; + const providerCopy = getProviderSettingsCopy(useUiLocale()); + const copy = providerCopy.detail; const flow = useOAuthLoginFlow({ bridge: props.service.bridge, display: props.service.display, @@ -852,11 +853,18 @@ function OAuthReloginNotice(props: { : errored ? copy.oauthUnknownDetail : copy.oauthStartDetail; + // Codex's device page has no code in its URL — the user must type the + // code shown here, so hiding it makes the re-login impossible to finish. + const deviceCode = props.service.showsDeviceCode ? flow.stateHint : null; return ( + {detail} {providerCopy.oauthSection.deviceCode} {deviceCode} + + ) : detail} endContent={!loading ? (