You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by zh...@apache.org on 2026/08/31 05:07:04 UTC

(maka) 01/03: feat(desktop): expose OAuth connection accounts

This is an automated email from the ASF dual-hosted git repository.

me2seeks pushed a commit to branch feat/4312-desktop-oauth-multi-account
in repository https://gitbox.apache.org/repos/asf/maka.git

commit 93731bf14a4375a3e86ac9dba43c7385fd56677f
Author: me2seeks <me...@gmail.com>
AuthorDate: Mon Aug 31 12:34:28 2026 +0800

    feat(desktop): expose OAuth connection accounts
---
 apps/desktop/renderer-architecture.json            |   8 +-
 .../__tests__/runtime-host-oauth-ipc-main.test.ts  |  78 ++++++++++---
 .../src/main/runtime-host-oauth-ipc-main.ts        | 102 +++++++++--------
 apps/desktop/src/preload/bridge-contract.d.ts      |  92 +++++++++------
 apps/desktop/src/preload/preload.ts                |  26 +++--
 .../src/renderer/locales/settings-provider-copy.ts |  14 ++-
 .../renderer/settings/provider-catalog-page.tsx    |  23 +++-
 .../settings/provider-connection-detail.tsx        |  20 +++-
 .../renderer/settings/provider-oauth-section.tsx   | 113 ++++++++----------
 .../src/renderer/settings/providers-panel.tsx      | 126 ++++++++++++++++-----
 .../settings/runtime-host-settings-bridge.ts       |  75 +++++++++---
 .../src/renderer/settings/use-connection-detail.ts |  24 +++-
 .../src/renderer/settings/use-oauth-login-flow.ts  | 107 +++++++++++------
 13 files changed, 531 insertions(+), 277 deletions(-)

diff --git a/apps/desktop/renderer-architecture.json b/apps/desktop/renderer-architecture.json
index 2f8f24e25e..4b1534321a 100644
--- a/apps/desktop/renderer-architecture.json
+++ b/apps/desktop/renderer-architecture.json
@@ -3676,9 +3676,7 @@
           "window.maka.githubCopilotSubscription.logout": 1,
           "window.maka.githubCopilotSubscription.refreshTokens": 1,
           "window.maka.openAiCodex": 1,
-          "window.maka.openAiCodex.getAccountState": 1,
-          "window.maka.xaiOAuth": 1,
-          "window.maka.xaiOAuth.getAccountState": 1
+          "window.maka.xaiOAuth": 1
         },
         "environmentCapabilities": {},
         "hookCalls": {
@@ -3985,9 +3983,7 @@
           "window.maka.connections.test": 1,
           "window.maka.connections.update": 1
         },
-        "environmentCapabilities": {
-          "window": 2
-        },
+        "environmentCapabilities": {},
         "hookCalls": {},
         "lifecycleMethods": {},
         "unresolvedDependencies": 0,
diff --git a/apps/desktop/src/main/__tests__/runtime-host-oauth-ipc-main.test.ts b/apps/desktop/src/main/__tests__/runtime-host-oauth-ipc-main.test.ts
index 64f824e4be..a5389b4073 100644
--- a/apps/desktop/src/main/__tests__/runtime-host-oauth-ipc-main.test.ts
+++ b/apps/desktop/src/main/__tests__/runtime-host-oauth-ipc-main.test.ts
@@ -167,9 +167,18 @@ test('adapts every Host OAuth provider through one Desktop flow', async () => {
   const authorization = await invoke(
     handlers,
     'openai-codex:get-auth-url',
-    catalog.connections[0]?.connectionId,
+    { kind: 'existing', connectionId: catalog.connections[0]?.connectionId },
   );
-  assert.deepEqual(authorization, { authRequestId: attemptId, stateHint: 'STATE-HINT' });
+  const expectedConnection = {
+    connectionId: catalog.connections[0]?.connectionId,
+    slug: 'codex-subscription',
+    providerType: provider,
+  };
+  assert.deepEqual(authorization, {
+    authRequestId: attemptId,
+    stateHint: 'STATE-HINT',
+    connection: expectedConnection,
+  });
   assert.deepEqual(opened, ['https://codex.example/authorize']);
   assert.deepEqual(
     await invoke(
@@ -178,7 +187,7 @@ test('adapts every Host OAuth provider through one Desktop flow', async () => {
       attemptId,
       'authorization-code#state',
     ),
-    { ok: true },
+    { ok: true, connection: expectedConnection },
   );
   assert.equal(changed, 1);
   assert.deepEqual(catalog.defaultTarget, {
@@ -187,7 +196,11 @@ test('adapts every Host OAuth provider through one Desktop flow', async () => {
   });
   // No quota: reporting it required the retired provider's own client identity,
   // so the account state carries the runtime state alone.
-  assert.deepEqual(await invoke(handlers, 'openai-codex:get-account-state'), {
+  assert.deepEqual(await invoke(
+    handlers,
+    'openai-codex:get-account-state',
+    catalog.connections[0]?.connectionId,
+  ), {
     provider,
     runtimeState: 'authenticated',
   });
@@ -231,7 +244,10 @@ test('provider-scoped OAuth IPC rejects a Connection ID owned by another provide
   });
 
   assert.deepEqual(
-    await invoke(handlers, 'openai-codex:get-auth-url', xaiConnection.connectionId),
+    await invoke(handlers, 'openai-codex:get-auth-url', {
+      kind: 'existing',
+      connectionId: xaiConnection.connectionId,
+    }),
     {
       ok: false,
       reason: 'unknown',
@@ -240,14 +256,18 @@ test('provider-scoped OAuth IPC rejects a Connection ID owned by another provide
   );
   assert.deepEqual(
     await invoke(handlers, 'openai-codex:get-account-state', xaiConnection.connectionId),
-    { provider: 'openai-codex', runtimeState: 'not_logged_in' },
+    {
+      ok: false,
+      reason: 'unknown',
+      message: 'OAuth account does not match this provider',
+    },
   );
   assert.deepEqual(
     await invoke(handlers, 'openai-codex:refresh-tokens', xaiConnection.connectionId),
     {
       ok: false,
       reason: 'refresh_failed',
-      message: 'OAuth account is not connected',
+      message: 'OAuth account does not match this provider',
     },
   );
   assert.deepEqual(await invoke(handlers, 'openai-codex:logout', xaiConnection.connectionId), {
@@ -395,9 +415,9 @@ test('a second OAuth start cannot replace or cancel a pending active attempt', a
     isProviderEnabled: () => true,
   });
 
-  const firstAuthorization = invoke(handlers, 'openai-codex:get-auth-url');
+  const firstAuthorization = invoke(handlers, 'openai-codex:get-auth-url', { kind: 'create' });
   await firstPresentationPoll;
-  assert.deepEqual(await invoke(handlers, 'openai-codex:get-auth-url'), {
+  assert.deepEqual(await invoke(handlers, 'openai-codex:get-auth-url', { kind: 'create' }), {
     ok: false,
     reason: 'unknown',
     message: 'Another OAuth login is already in progress',
@@ -413,6 +433,11 @@ test('a second OAuth start cannot replace or cancel a pending active attempt', a
   assert.deepEqual(await firstAuthorization, {
     authRequestId: firstAttemptId,
     stateHint: 'FIRST',
+    connection: {
+      connectionId,
+      slug: 'codex-subscription',
+      providerType: provider,
+    },
   });
   assert.deepEqual(
     await invoke(handlers, 'openai-codex:logout', foreignConnection.connectionId),
@@ -425,7 +450,7 @@ test('a second OAuth start cannot replace or cancel a pending active attempt', a
   assert.deepEqual(await invoke(handlers, 'openai-codex:logout'), {
     ok: false,
     reason: 'unknown',
-    message: 'Select a specific OAuth account to log out',
+    message: 'Invalid OAuth Connection identity',
   });
   assert.equal(cancels, 0);
   assert.deepEqual(
@@ -435,7 +460,14 @@ test('a second OAuth start cannot replace or cancel a pending active attempt', a
   assert.equal(cancels, 0);
   assert.deepEqual(
     await invoke(handlers, 'openai-codex:complete-authorization', firstAttemptId),
-    { ok: true },
+    {
+      ok: true,
+      connection: {
+        connectionId,
+        slug: 'codex-subscription',
+        providerType: provider,
+      },
+    },
   );
   assert.equal(cancels, 0);
   assertNoUnexpectedClientCalls();
@@ -475,7 +507,7 @@ test('completion rejects a terminal projection that changes Connection identity'
     isProviderEnabled: () => true,
   });
 
-  await invoke(handlers, 'openai-codex:get-auth-url');
+  await invoke(handlers, 'openai-codex:get-auth-url', { kind: 'create' });
   assert.deepEqual(await invoke(handlers, 'openai-codex:complete-authorization', attemptId), {
     ok: false,
     reason: 'unknown',
@@ -575,18 +607,34 @@ test('keeps a committed OAuth login successful when model discovery fails', asyn
     isProviderEnabled: () => true,
   });
 
-  assert.deepEqual(await invoke(handlers, 'openai-codex:get-auth-url'), {
+  assert.deepEqual(await invoke(handlers, 'openai-codex:get-auth-url', { kind: 'create' }), {
     authRequestId: attemptId,
     stateHint: 'DEVICE-CODE',
+    connection: {
+      connectionId: created.connectionId,
+      slug: created.slug,
+      providerType: provider,
+    },
   });
   assert.deepEqual(
     await invoke(handlers, 'openai-codex:complete-authorization', attemptId, undefined),
-    { ok: true },
+    {
+      ok: true,
+      connection: {
+        connectionId: created.connectionId,
+        slug: created.slug,
+        providerType: provider,
+      },
+    },
   );
   assert.equal(changed, 1);
   assert.deepEqual(fetchedConnectionIds, [created.connectionId]);
   assert.deepEqual(catalog.defaultTarget, { connectionId: created.connectionId, modelId });
-  assert.deepEqual(await invoke(handlers, 'openai-codex:get-account-state'), {
+  assert.deepEqual(await invoke(
+    handlers,
+    'openai-codex:get-account-state',
+    created.connectionId,
+  ), {
     provider,
     runtimeState: 'authenticated',
   });
diff --git a/apps/desktop/src/main/runtime-host-oauth-ipc-main.ts b/apps/desktop/src/main/runtime-host-oauth-ipc-main.ts
index e151449c56..2ed0c5b3c7 100644
--- a/apps/desktop/src/main/runtime-host-oauth-ipc-main.ts
+++ b/apps/desktop/src/main/runtime-host-oauth-ipc-main.ts
@@ -94,9 +94,9 @@ export function registerRuntimeHostOAuthIpc(deps: RuntimeHostOAuthIpcDeps): void
     if (provider !== 'xai-oauth') {
       deps.ipcMain.handle(channel('is-experimental-enabled'), () => providerEnabled(provider));
     }
-    deps.ipcMain.handle(channel('get-auth-url'), async (_event, rawConnectionId: unknown) => {
+    deps.ipcMain.handle(channel('get-auth-url'), async (_event, rawTarget: unknown) => {
       if (!providerEnabled(provider)) return providerDisabled();
-      const selection = decodeOAuthConnectionSelection(rawConnectionId);
+      const selection = decodeOAuthLoginSelection(rawTarget);
       if (selection.kind === 'invalid') return invalidConnectionIdentity();
       const connectionId = selection.kind === 'exact' ? selection.connectionId : undefined;
       if (connectionId) {
@@ -129,7 +129,11 @@ export function registerRuntimeHostOAuthIpc(deps: RuntimeHostOAuthIpcDeps): void
           provider,
           connection: started.connection,
         });
-        return { authRequestId: attemptId, stateHint: presented.stateHint };
+        return {
+          authRequestId: attemptId,
+          stateHint: presented.stateHint,
+          connection: started.connection,
+        };
       } catch (error) {
         expectation?.cancel(error);
         if (startedOnHost) {
@@ -176,7 +180,7 @@ export function registerRuntimeHostOAuthIpc(deps: RuntimeHostOAuthIpcDeps): void
             terminal.connection.connectionId,
           ).catch(() => undefined);
           deps.emitConnectionListChanged();
-          return { ok: true as const };
+          return { ok: true as const, connection: terminal.connection };
         } catch {
           activeAttempts.delete(attemptId);
           return actionFailure('Unable to complete OAuth authorization');
@@ -192,45 +196,44 @@ export function registerRuntimeHostOAuthIpc(deps: RuntimeHostOAuthIpcDeps): void
       return { ok: true as const };
     });
     handleReconnectableRead(deps.ipcMain, channel('get-account-state'), async (_event, rawConnectionId: unknown) => {
-      const selection = decodeOAuthConnectionSelection(rawConnectionId);
-      if (selection.kind === 'invalid') return invalidConnectionIdentity();
-      const connectionId = selection.kind === 'exact' ? selection.connectionId : undefined;
+      const connectionId = decodeExactOAuthConnectionId(rawConnectionId);
+      if (!connectionId) return invalidConnectionIdentity();
       const candidates = oauthAccountCandidates(
         await deps.client.loadConnectionCatalog(),
         provider,
         connectionId,
       );
+      if (candidates.length === 0) {
+        return actionFailure('OAuth account does not match this provider');
+      }
       const authorizing = [...activeAttempts.values()].some(
         (attempt) =>
           attempt.provider === provider &&
-          (connectionId === undefined || attempt.connection.connectionId === connectionId),
+          attempt.connection.connectionId === connectionId,
       );
-      if (candidates.length === 0) {
-        return accountState(provider, authorizing ? 'authorizing' : 'not_logged_in');
-      }
       if ((await configuredOAuthAccountConnections(deps.client, candidates)).length > 0) {
         return accountState(provider, 'authenticated');
       }
       return accountState(provider, authorizing ? 'authorizing' : 'not_logged_in');
     });
     deps.ipcMain.handle(channel('refresh-tokens'), async (_event, rawConnectionId: unknown) => {
-      const selection = decodeOAuthConnectionSelection(rawConnectionId);
-      if (selection.kind === 'invalid') return invalidConnectionIdentity('refresh_failed');
-      const connectionId = selection.kind === 'exact' ? selection.connectionId : undefined;
+      const connectionId = decodeExactOAuthConnectionId(rawConnectionId);
+      if (!connectionId) return invalidConnectionIdentity('refresh_failed');
+      const candidates = oauthAccountCandidates(
+        await deps.client.loadConnectionCatalog(),
+        provider,
+        connectionId,
+      );
+      if (candidates.length === 0) {
+        return actionFailure('OAuth account does not match this provider', 'refresh_failed');
+      }
       const connections = await configuredOAuthAccountConnections(
         deps.client,
-        oauthAccountCandidates(
-          await deps.client.loadConnectionCatalog(),
-          provider,
-          connectionId,
-        ),
+        candidates,
       );
       if (connections.length === 0) {
         return actionFailure('OAuth account is not connected', 'refresh_failed');
       }
-      if (connectionId === undefined && connections.length > 1) {
-        return actionFailure('Select a specific OAuth account to refresh', 'refresh_failed');
-      }
       const connection = connections[0]!;
       const refreshed = await deps.client.fetchConnectionModels(connection.connectionId);
       return refreshed.kind === 'committed'
@@ -238,34 +241,25 @@ export function registerRuntimeHostOAuthIpc(deps: RuntimeHostOAuthIpcDeps): void
         : actionFailure('Unable to refresh OAuth account', 'refresh_failed');
     });
     deps.ipcMain.handle(channel('logout'), async (_event, rawConnectionId: unknown) => {
-      const selection = decodeOAuthConnectionSelection(rawConnectionId);
-      if (selection.kind === 'invalid') return invalidConnectionIdentity();
-      const connectionId = selection.kind === 'exact' ? selection.connectionId : undefined;
+      const connectionId = decodeExactOAuthConnectionId(rawConnectionId);
+      if (!connectionId) return invalidConnectionIdentity();
       try {
         const candidates = oauthAccountCandidates(
           await deps.client.loadConnectionCatalog(),
           provider,
           connectionId,
         );
-        if (connectionId !== undefined && candidates.length === 0) {
+        if (candidates.length === 0) {
           return actionFailure('OAuth account does not match this provider');
         }
-        const connections =
-          connectionId !== undefined
-            ? candidates
-            : await configuredOAuthAccountConnections(deps.client, candidates);
-        if (connectionId === undefined && connections.length > 1) {
-          return actionFailure('Select a specific OAuth account to log out');
-        }
-        const connection = connections[0];
+        const connection = candidates[0]!;
         await cancelProviderAttempts(
           deps,
           activeAttempts,
           provider,
-          connection?.connectionId,
+          connection.connectionId,
         );
-        if (connection)
-          await disableRuntimeHostAccountConnectionById(deps.client, connection.connectionId);
+        await disableRuntimeHostAccountConnectionById(deps.client, connection.connectionId);
       } catch {
         return actionFailure('Unable to remove OAuth account');
       }
@@ -370,30 +364,40 @@ function accountState(
 function oauthAccountCandidates(
   catalog: Awaited<ReturnType<OAuthClient['loadConnectionCatalog']>>,
   provider: OAuthLoginProvider,
-  connectionId: string | undefined,
+  connectionId: string,
 ): ConnectionCatalogEntry[] {
-  if (connectionId !== undefined) {
-    const connection = findRuntimeHostAccountConnectionById(catalog, connectionId);
-    return connection?.providerType === provider ? [connection] : [];
-  }
-  return catalog.connections.filter((connection) => connection.providerType === provider);
+  const connection = findRuntimeHostAccountConnectionById(catalog, connectionId);
+  return connection?.providerType === provider ? [connection] : [];
 }
 
-type OAuthConnectionSelection =
-  | { readonly kind: 'aggregate' }
+type OAuthLoginSelection =
+  | { readonly kind: 'create' }
   | { readonly kind: 'exact'; readonly connectionId: string }
   | { readonly kind: 'invalid' };
 
-function decodeOAuthConnectionSelection(value: unknown): OAuthConnectionSelection {
-  if (value === undefined) return { kind: 'aggregate' };
-  if (typeof value !== 'string') return { kind: 'invalid' };
+function decodeOAuthLoginSelection(value: unknown): OAuthLoginSelection {
+  if (!value || typeof value !== 'object') return { kind: 'invalid' };
+  const candidate = value as { readonly kind?: unknown; readonly connectionId?: unknown };
+  if (candidate.kind === 'create') return { kind: 'create' };
+  if (candidate.kind !== 'existing' || typeof candidate.connectionId !== 'string') {
+    return { kind: 'invalid' };
+  }
   try {
-    return { kind: 'exact', connectionId: decodeRuntimePolicyEntityId(value) };
+    return { kind: 'exact', connectionId: decodeRuntimePolicyEntityId(candidate.connectionId) };
   } catch {
     return { kind: 'invalid' };
   }
 }
 
+function decodeExactOAuthConnectionId(value: unknown): string | undefined {
+  if (typeof value !== 'string') return undefined;
+  try {
+    return decodeRuntimePolicyEntityId(value);
+  } catch {
+    return undefined;
+  }
+}
+
 function sameOAuthConnectionIdentity(
   left: OAuthConnectionIdentity,
   right: OAuthConnectionIdentity,
diff --git a/apps/desktop/src/preload/bridge-contract.d.ts b/apps/desktop/src/preload/bridge-contract.d.ts
index 10fc8cf377..75adb306a6 100644
--- a/apps/desktop/src/preload/bridge-contract.d.ts
+++ b/apps/desktop/src/preload/bridge-contract.d.ts
@@ -350,6 +350,26 @@ export interface DesktopRuntimeHostRef {
   readonly hostId: string;
 }
 
+/** Desktop-local OAuth intent. Runtime Host remains the identity allocator. */
+export type DesktopOAuthLoginTarget =
+  | { readonly kind: 'create' }
+  | { readonly kind: 'existing'; readonly connectionId: string };
+
+/** Secret-free canonical identity returned by the Runtime Host OAuth flow. */
+export interface DesktopOAuthConnectionIdentity {
+  readonly connectionId: string;
+  readonly slug: string;
+  readonly providerType: 'openai-codex' | 'xai-oauth';
+}
+
+export type DesktopOAuthAuthorizationStartResult =
+  | (AuthorizationUrlPayload & { readonly connection: DesktopOAuthConnectionIdentity })
+  | Exclude<SubscriptionActionResult, { readonly ok: true }>;
+
+export type DesktopOAuthAuthorizationResult =
+  | { readonly ok: true; readonly connection: DesktopOAuthConnectionIdentity }
+  | Exclude<SubscriptionActionResult, { readonly ok: true }>;
+
 export type DesktopNewTaskHostRef = DesktopRuntimeHostRef;
 
 export interface DesktopNewTaskTarget extends DesktopRuntimeHostRef {
@@ -1438,45 +1458,51 @@ export interface MakaBridge {
   };
   openAiCodex: {
     isExperimentalEnabled(host?: DesktopRuntimeHostRef): Promise<boolean>;
-    getAuthUrl(host?: DesktopRuntimeHostRef, connectionId?: string): Promise<AuthorizationUrlPayload | SubscriptionActionResult>;
+    getAuthUrl(host: DesktopRuntimeHostRef | undefined, target: DesktopOAuthLoginTarget): Promise<DesktopOAuthAuthorizationStartResult>;
     openAuthUrl(authRequestId: string, host?: DesktopRuntimeHostRef): Promise<SubscriptionActionResult>;
-    completeAuthorization(authRequestId: string, host?: DesktopRuntimeHostRef): Promise<SubscriptionActionResult>;
+    completeAuthorization(authRequestId: string, host?: DesktopRuntimeHostRef): Promise<DesktopOAuthAuthorizationResult>;
     cancelAuthorization(authRequestId?: string, host?: DesktopRuntimeHostRef): Promise<{ ok: true }>;
-    getAccountState(host?: DesktopRuntimeHostRef, connectionId?: string): Promise<{
-      provider: 'openai-codex';
-      runtimeState:
-        | 'not_logged_in'
-        | 'authorizing'
-        | 'authenticated'
-        | 'refreshing'
-        | 'refresh_failed';
-      accountId?: string;
-      email?: string;
-      plan?: string;
-      picture?: string;
-      errorMessage?: string;
-    }>;
-    refreshTokens(host?: DesktopRuntimeHostRef, connectionId?: string): Promise<SubscriptionActionResult>;
-    logout(host?: DesktopRuntimeHostRef, connectionId?: string): Promise<SubscriptionActionResult>;
+    getAccountState(host: DesktopRuntimeHostRef | undefined, connectionId: string): Promise<
+      | {
+          provider: 'openai-codex';
+          runtimeState:
+            | 'not_logged_in'
+            | 'authorizing'
+            | 'authenticated'
+            | 'refreshing'
+            | 'refresh_failed';
+          accountId?: string;
+          email?: string;
+          plan?: string;
+          picture?: string;
+          errorMessage?: string;
+        }
+      | Exclude<SubscriptionActionResult, { readonly ok: true }>
+    >;
+    refreshTokens(host: DesktopRuntimeHostRef | undefined, connectionId: string): Promise<SubscriptionActionResult>;
+    logout(host: DesktopRuntimeHostRef | undefined, connectionId: string): Promise<SubscriptionActionResult>;
   };
   xaiOAuth: {
-    getAuthUrl(host?: DesktopRuntimeHostRef, connectionId?: string): Promise<AuthorizationUrlPayload | SubscriptionActionResult>;
+    getAuthUrl(host: DesktopRuntimeHostRef | undefined, target: DesktopOAuthLoginTarget): Promise<DesktopOAuthAuthorizationStartResult>;
     openAuthUrl(authRequestId: string, host?: DesktopRuntimeHostRef): Promise<SubscriptionActionResult>;
-    completeAuthorization(authRequestId: string, host?: DesktopRuntimeHostRef): Promise<SubscriptionActionResult>;
+    completeAuthorization(authRequestId: string, host?: DesktopRuntimeHostRef): Promise<DesktopOAuthAuthorizationResult>;
     cancelAuthorization(authRequestId?: string, host?: DesktopRuntimeHostRef): Promise<{ ok: true }>;
-    getAccountState(host?: DesktopRuntimeHostRef, connectionId?: string): Promise<{
-      provider: 'xai-oauth';
-      runtimeState:
-        | 'not_logged_in'
-        | 'authorizing'
-        | 'authenticated'
-        | 'refreshing'
-        | 'refresh_failed'
-        | 'storage_failed';
-      errorMessage?: string;
-    }>;
-    refreshTokens(host?: DesktopRuntimeHostRef, connectionId?: string): Promise<SubscriptionActionResult>;
-    logout(host?: DesktopRuntimeHostRef, connectionId?: string): Promise<SubscriptionActionResult>;
+    getAccountState(host: DesktopRuntimeHostRef | undefined, connectionId: string): Promise<
+      | {
+          provider: 'xai-oauth';
+          runtimeState:
+            | 'not_logged_in'
+            | 'authorizing'
+            | 'authenticated'
+            | 'refreshing'
+            | 'refresh_failed'
+            | 'storage_failed';
+          errorMessage?: string;
+        }
+      | Exclude<SubscriptionActionResult, { readonly ok: true }>
+    >;
+    refreshTokens(host: DesktopRuntimeHostRef | undefined, connectionId: string): Promise<SubscriptionActionResult>;
+    logout(host: DesktopRuntimeHostRef | undefined, connectionId: string): Promise<SubscriptionActionResult>;
   };
   githubCopilotSubscription: {
     connectExistingLogin(host?: DesktopRuntimeHostRef): Promise<SubscriptionActionResult>;
diff --git a/apps/desktop/src/preload/preload.ts b/apps/desktop/src/preload/preload.ts
index 157a758e6e..097b89000f 100644
--- a/apps/desktop/src/preload/preload.ts
+++ b/apps/desktop/src/preload/preload.ts
@@ -62,6 +62,8 @@ import type {
   DesktopNewTaskHostRef,
   DesktopNewTaskTarget,
   DesktopRuntimeHostRef,
+  DesktopOAuthLoginTarget,
+  DesktopOAuthAuthorizationResult,
   DesktopProjectSnapshot,
   DesktopAppInfo,
   DesktopSessionTracePage,
@@ -2763,19 +2765,19 @@ const makaBridge = {
     isExperimentalEnabled(host?: DesktopRuntimeHostRef): Promise<boolean> {
       return invokeSelectedRuntimeHost(host, 'openai-codex:is-experimental-enabled');
     },
-    getAuthUrl(host?: DesktopRuntimeHostRef, connectionId?: string): Promise<AuthorizationUrlPayload | SubscriptionActionResult> {
-      return invokeSelectedRuntimeHost(host, 'openai-codex:get-auth-url', connectionId);
+    getAuthUrl(host: DesktopRuntimeHostRef | undefined, target: DesktopOAuthLoginTarget) {
+      return invokeSelectedRuntimeHost(host, 'openai-codex:get-auth-url', target);
     },
     openAuthUrl(authRequestId: string, host?: DesktopRuntimeHostRef): Promise<SubscriptionActionResult> {
       return invokeSelectedRuntimeHost(host, 'openai-codex:open-auth-url', authRequestId);
     },
-    completeAuthorization(authRequestId: string, host?: DesktopRuntimeHostRef): Promise<SubscriptionActionResult> {
+    completeAuthorization(authRequestId: string, host?: DesktopRuntimeHostRef): Promise<DesktopOAuthAuthorizationResult> {
       return invokeSelectedRuntimeHost(host, 'openai-codex:complete-authorization', authRequestId);
     },
     cancelAuthorization(authRequestId?: string, host?: DesktopRuntimeHostRef): Promise<{ ok: true }> {
       return invokeSelectedRuntimeHost(host, 'openai-codex:cancel-authorization', authRequestId);
     },
-    getAccountState(host?: DesktopRuntimeHostRef, connectionId?: string): Promise<{
+    getAccountState(host: DesktopRuntimeHostRef | undefined, connectionId: string): Promise<{
       provider: 'openai-codex';
       runtimeState: 'not_logged_in' | 'authorizing' | 'authenticated' | 'refreshing' | 'refresh_failed';
       accountId?: string;
@@ -2786,27 +2788,27 @@ const makaBridge = {
     }> {
       return invokeSelectedRuntimeHost(host, 'openai-codex:get-account-state', connectionId);
     },
-    refreshTokens(host?: DesktopRuntimeHostRef, connectionId?: string): Promise<SubscriptionActionResult> {
+    refreshTokens(host: DesktopRuntimeHostRef | undefined, connectionId: string): Promise<SubscriptionActionResult> {
       return invokeSelectedRuntimeHost(host, 'openai-codex:refresh-tokens', connectionId);
     },
-    logout(host?: DesktopRuntimeHostRef, connectionId?: string): Promise<SubscriptionActionResult> {
+    logout(host: DesktopRuntimeHostRef | undefined, connectionId: string): Promise<SubscriptionActionResult> {
       return invokeSelectedRuntimeHost(host, 'openai-codex:logout', connectionId);
     },
   },
   xaiOAuth: {
-    getAuthUrl(host?: DesktopRuntimeHostRef, connectionId?: string): Promise<AuthorizationUrlPayload | SubscriptionActionResult> {
-      return invokeSelectedRuntimeHost(host, 'xai-oauth:get-auth-url', connectionId);
+    getAuthUrl(host: DesktopRuntimeHostRef | undefined, target: DesktopOAuthLoginTarget) {
+      return invokeSelectedRuntimeHost(host, 'xai-oauth:get-auth-url', target);
     },
     openAuthUrl(authRequestId: string, host?: DesktopRuntimeHostRef): Promise<SubscriptionActionResult> {
       return invokeSelectedRuntimeHost(host, 'xai-oauth:open-auth-url', authRequestId);
     },
-    completeAuthorization(authRequestId: string, host?: DesktopRuntimeHostRef): Promise<SubscriptionActionResult> {
+    completeAuthorization(authRequestId: string, host?: DesktopRuntimeHostRef): Promise<DesktopOAuthAuthorizationResult> {
       return invokeSelectedRuntimeHost(host, 'xai-oauth:complete-authorization', authRequestId);
     },
     cancelAuthorization(authRequestId?: string, host?: DesktopRuntimeHostRef): Promise<{ ok: true }> {
       return invokeSelectedRuntimeHost(host, 'xai-oauth:cancel-authorization', authRequestId);
     },
-    getAccountState(host?: DesktopRuntimeHostRef, connectionId?: string): Promise<{
+    getAccountState(host: DesktopRuntimeHostRef | undefined, connectionId: string): Promise<{
       provider: 'xai-oauth';
       runtimeState:
         | 'not_logged_in'
@@ -2819,10 +2821,10 @@ const makaBridge = {
     }> {
       return invokeSelectedRuntimeHost(host, 'xai-oauth:get-account-state', connectionId);
     },
-    refreshTokens(host?: DesktopRuntimeHostRef, connectionId?: string): Promise<SubscriptionActionResult> {
+    refreshTokens(host: DesktopRuntimeHostRef | undefined, connectionId: string): Promise<SubscriptionActionResult> {
       return invokeSelectedRuntimeHost(host, 'xai-oauth:refresh-tokens', connectionId);
     },
-    logout(host?: DesktopRuntimeHostRef, connectionId?: string): Promise<SubscriptionActionResult> {
+    logout(host: DesktopRuntimeHostRef | undefined, connectionId: string): Promise<SubscriptionActionResult> {
       return invokeSelectedRuntimeHost(host, 'xai-oauth:logout', connectionId);
     },
   },
diff --git a/apps/desktop/src/renderer/locales/settings-provider-copy.ts b/apps/desktop/src/renderer/locales/settings-provider-copy.ts
index 6a2633bd39..b73b966a87 100644
--- a/apps/desktop/src/renderer/locales/settings-provider-copy.ts
+++ b/apps/desktop/src/renderer/locales/settings-provider-copy.ts
@@ -176,7 +176,7 @@ const zhCopy = {
   panel: {
     tabs: { all: '全部', recommended: '推荐', accounts: '账号', plans: '模型计划', api: 'API', aggregators: '聚合服务', local: '本地' },
     loadFailed: '载入模型连接失败', loadingAria: '正在加载模型供应商', connections: '模型连接',
-    retry: '点击重试。', empty: '还没有模型连接',
+    retry: '点击重试。', empty: '还没有模型连接', connectionRemoved: '原账号已被删除或移除,已返回模型连接列表。',
     emptyHelp: '从下方选择一种连接方式开始。', default: '默认', setDefault: '设为默认', setDefaultTitle: '让新任务默认使用这个连接', setDefaultPending: '设置中…', setDefaultFailed: '设为默认失败', addHelp: '选择账号登录、模型计划、API、聚合服务或本地运行时。',
     categoriesAria: '模型供应商分类', searchPlaceholder: '搜索服务商', searchAria: '搜索模型服务商', noMatch: '没有匹配的服务商', clearSearch: '清除搜索',
     createSubtitle: '完成必要配置后,连接会出现在模型页上方。', connection: '模型连接',
@@ -214,7 +214,8 @@ const zhCopy = {
     logoutTitle: (name: string) => `退出 ${name} 登录?`,
   },
   oauthSection: {
-    signedIn: '已登录', codexDescription: 'ChatGPT Plus / Pro 订阅账号登录。', xaiDescription: 'SuperGrok / X Premium 账号登录。',
+    signedIn: '已登录', codexDescription: '使用 ChatGPT Plus / Pro 账号添加连接。', xaiDescription: '使用 SuperGrok / X Premium 账号添加连接。',
+    configuredConnections: (count: number) => `已有 ${count} 个连接 · 添加另一个账号`,
     copilotDescription: '导入兼容 GitHub 凭据连接 Copilot 订阅。', serviceUnavailable: '登录服务暂时不可用,请检查网络后重试。',
     aria: 'OAuth 登录',
     staleState: 'OAuth 登录状态暂时没刷新成功,已保留上一次状态。',
@@ -225,7 +226,7 @@ const zhCopy = {
     reimport: '重新导入', importCredential: '导入兼容凭据', verifying: '验证中…', reverify: '重新验证', removing: '移除中…', removeLocal: '移除本地登录',
     loadingAccount: '正在加载账号状态…', authorizing: '请在弹出的浏览器窗口完成登录。', refreshing: '正在刷新访问令牌…', refreshTokenFailed: '令牌刷新失败,请重新登录。',
     cardAria: (name: string, status: string | undefined, description: string) => `打开 OAuth 登录:${name}${status ? `,状态:${status}` : ''},${description.replace(/[。.!!??]+$/u, '')}`,
-    connectTitle: (name: string) => `连接 ${name}`, login: (name: string) => `登录 ${name}`, signedOut: (name: string) => `${name} 尚未登录。`,
+    connectTitle: (name: string) => `连接 ${name}`, addAccountTitle: (name: string) => `添加 ${name} 账号`, login: (name: string) => `登录 ${name}`, loginAndAdd: '登录并添加', signedOut: (name: string) => `${name} 尚未登录。`,
     storageFailed: (name: string) => `${name} 本地凭据读取失败,请重新登录。`, providerUnavailable: (name: string) => `${name} 已登录,但当前 provider 状态不可用。`,
   },
 } as const;
@@ -329,7 +330,7 @@ const enCopy: ProviderSettingsCopy = {
   panel: {
     tabs: { all: 'All', recommended: 'Recommended', accounts: 'Accounts', plans: 'Model plans', api: 'API', aggregators: 'Aggregators', local: 'Local' },
     loadFailed: 'Failed to load model connections', loadingAria: 'Loading model providers', connections: 'Connections',
-    retry: 'Select to retry.', empty: 'No model connections yet',
+    retry: 'Select to retry.', empty: 'No model connections yet', connectionRemoved: 'The original account was deleted or removed. Returned to the connection list.',
     emptyHelp: 'Choose a connection method below to begin.', default: 'Default', setDefault: 'Set as default', setDefaultTitle: 'New chats will use this connection', setDefaultPending: 'Setting…', setDefaultFailed: 'Could not set as default', addHelp: 'Choose account sign-in, a model plan, API, aggregator, or local runtime.',
     categoriesAria: 'Model provider categories', searchPlaceholder: 'Search providers', searchAria: 'Search model providers', noMatch: 'No matching providers', clearSearch: 'Clear search',
     createSubtitle: 'After required setup, the connection appears above on the Models page.', connection: 'Model connection',
@@ -367,7 +368,8 @@ const enCopy: ProviderSettingsCopy = {
     logoutTitle: (name: string) => `Sign out of ${name}?`,
   },
   oauthSection: {
-    signedIn: 'Signed in', codexDescription: 'Sign in with a ChatGPT Plus / Pro subscription.', xaiDescription: 'Sign in with SuperGrok or X Premium.',
+    signedIn: 'Signed in', codexDescription: 'Use a ChatGPT Plus / Pro account to add a connection.', xaiDescription: 'Use a SuperGrok or X Premium account to add a connection.',
+    configuredConnections: (count: number) => `${count} ${count === 1 ? 'connection' : 'connections'} configured · Add another account`,
     copilotDescription: 'Import compatible GitHub credentials to connect a Copilot subscription.', serviceUnavailable: 'The sign-in service is temporarily unavailable. Check the network and try again.',
     aria: 'OAuth sign-in',
     staleState: 'OAuth sign-in status could not be refreshed. The last known state is preserved. ',
@@ -378,7 +380,7 @@ const enCopy: ProviderSettingsCopy = {
     reimport: 'Reimport', importCredential: 'Import compatible credentials', verifying: 'Verifying…', reverify: 'Verify again', removing: 'Removing…', removeLocal: 'Remove local sign-in',
     loadingAccount: 'Loading account status…', authorizing: 'Complete sign-in in the browser window.', refreshing: 'Refreshing access token…', refreshTokenFailed: 'Token refresh failed. Sign in again.',
     cardAria: (name: string, status: string | undefined, description: string) => `Open OAuth sign-in: ${name}${status ? `; status: ${status}` : ''}; ${description.replace(/[。.!!??]+$/u, '')}`,
-    connectTitle: (name: string) => `Connect ${name}`, login: (name: string) => `Sign in to ${name}`, signedOut: (name: string) => `${name} is signed out.`,
+    connectTitle: (name: string) => `Connect ${name}`, addAccountTitle: (name: string) => `Add ${name} account`, login: (name: string) => `Sign in to ${name}`, loginAndAdd: 'Sign in and add', signedOut: (name: string) => `${name} is signed out.`,
     storageFailed: (name: string) => `Could not read local credentials for ${name}. Sign in again.`, providerUnavailable: (name: string) => `${name} is signed in, but the provider status is currently unavailable.`,
   },
 };
diff --git a/apps/desktop/src/renderer/settings/provider-catalog-page.tsx b/apps/desktop/src/renderer/settings/provider-catalog-page.tsx
index 364407fcfa..228ce44a72 100644
--- a/apps/desktop/src/renderer/settings/provider-catalog-page.tsx
+++ b/apps/desktop/src/renderer/settings/provider-catalog-page.tsx
@@ -34,7 +34,7 @@ import {
   type ProviderCatalogGroup,
   type ProviderType,
 } from '@maka/core/provider-registry';
-import { PROVIDER_DEFAULTS } from '@maka/core/llm-connections';
+import { PROVIDER_DEFAULTS, type LlmConnection } from '@maka/core/llm-connections';
 import { Button, TextInput, useUiLocale } from '@maka/ui';
 import { AddProviderForm } from './provider-add-form';
 import { ProviderLogo, providerDisplay } from './provider-display';
@@ -66,6 +66,12 @@ export interface CatalogFilter {
 
 export const CATALOG_INITIAL_FILTER: CatalogFilter = { query: '', category: 'all' };
 
+interface CreatedOAuthConnectionIdentity {
+  connectionId: string;
+  slug: string;
+  providerType: 'openai-codex' | 'xai-oauth';
+}
+
 /**
  * One provider being set up. `credentials` is a form, `account` is a browser
  * login — two bodies of one level, not two levels: they are reached the same
@@ -87,6 +93,7 @@ export type SetupTarget =
  */
 export function ProviderCatalogPage(props: {
   filter: CatalogFilter;
+  connections: readonly LlmConnection[];
   onFilterChange(filter: CatalogFilter): void;
   onPick(target: SetupTarget): void;
 }) {
@@ -96,7 +103,10 @@ export function ProviderCatalogPage(props: {
   const catalogCopy = providerCopy.catalog;
   const { query, category } = props.filter;
   const showsOAuth = category === 'all' || category === 'recommended' || category === 'accounts';
-  const oauth = useOAuthCards({ query: showsOAuth ? query : undefined });
+  const oauth = useOAuthCards({
+    query: showsOAuth ? query : undefined,
+    connections: props.connections,
+  });
 
   // Category is a Selector, not a second TabList: the page header already owns
   // one level of navigation, and six tabs beside a search field made the
@@ -215,17 +225,18 @@ export function ProviderSetupPage(props: {
   existingSlugs: string[];
   onCancel(): void;
   onCreated(slug: string, modelDiscoveryError?: unknown): Promise<void>;
-  onAccountChanged(): Promise<void>;
+  onAccountCreated(connection?: CreatedOAuthConnectionIdentity): Promise<void>;
+  labelledBy?: string;
 }) {
   if (props.target.method === 'account') {
     return (
-      <div tabIndex={-1} className="settingsRouteLevel" data-maka-contract="provider-setup">
-        <OAuthLoginPanel cardId={props.target.cardId} onLoginSuccess={props.onAccountChanged} />
+      <div tabIndex={-1} role="region" aria-labelledby={props.labelledBy} className="settingsRouteLevel" data-maka-contract="provider-setup">
+        <OAuthLoginPanel cardId={props.target.cardId} onLoginSuccess={props.onAccountCreated} />
       </div>
     );
   }
   return (
-    <div tabIndex={-1} className="settingsRouteLevel" data-maka-contract="provider-setup">
+    <div tabIndex={-1} role="region" aria-labelledby={props.labelledBy} className="settingsRouteLevel" data-maka-contract="provider-setup">
       <AddProviderForm
         key={props.target.providerType}
         bridge={props.bridge}
diff --git a/apps/desktop/src/renderer/settings/provider-connection-detail.tsx b/apps/desktop/src/renderer/settings/provider-connection-detail.tsx
index d54149911e..c1130622a5 100644
--- a/apps/desktop/src/renderer/settings/provider-connection-detail.tsx
+++ b/apps/desktop/src/renderer/settings/provider-connection-detail.tsx
@@ -1104,9 +1104,12 @@ function OAuthReloginNoticeForCurrentGeneration(props: {
   const providerCopy = getProviderSettingsCopy(useUiLocale());
   const copy = providerCopy.detail;
   const flow = useOAuthLoginFlow({
-    bridge: props.service.bridge,
+    mode: 'existing',
+    authorizationBridge: props.service.authorizationBridge,
+    accountBridge: props.service.accountBridge,
     display: props.service.display,
-    onLoginSuccess: props.onRelogin,
+    onLoginSuccess: () => props.onRelogin(),
+    onAccountChanged: props.onRelogin,
   });
   const { hasSecret } = props;
   const loggedIn = hasSecret === true;
@@ -1139,6 +1142,7 @@ function OAuthReloginNoticeForCurrentGeneration(props: {
         </>
       ) : detail}
       endContent={!loading ? (
+        <HStack gap={2}>
           <Button
             variant="primary"
             size="sm"
@@ -1146,6 +1150,18 @@ function OAuthReloginNoticeForCurrentGeneration(props: {
             onClick={() => void flow.startLogin()}
             label={flow.pendingAction === 'login' ? copy.loggingIn : loggedIn ? copy.relogin : copy.login}
           />
+          {loggedIn && flow.logout && (
+            <Button
+              variant="ghost"
+              size="sm"
+              isDisabled={flow.actionBusy}
+              onClick={() => void flow.logout?.()}
+              label={flow.pendingAction === 'logout'
+                ? providerCopy.oauthSection.loggingOut
+                : providerCopy.oauthSection.logout}
+            />
+          )}
+        </HStack>
       ) : undefined} />
   );
 }
diff --git a/apps/desktop/src/renderer/settings/provider-oauth-section.tsx b/apps/desktop/src/renderer/settings/provider-oauth-section.tsx
index 10d86d72f6..b10ab880d8 100644
--- a/apps/desktop/src/renderer/settings/provider-oauth-section.tsx
+++ b/apps/desktop/src/renderer/settings/provider-oauth-section.tsx
@@ -19,7 +19,7 @@
 
 import { useEffect, useRef, useState } from 'react';
 import { Banner, HStack, Text, VStack } from '@astryxdesign/core';
-import { type ProviderType } from '@maka/core/llm-connections';
+import { type LlmConnection, type ProviderType } from '@maka/core/llm-connections';
 import type { DesktopRuntimeHostRef } from '../../preload/bridge-contract.js';
 import {
   Badge,
@@ -31,8 +31,7 @@ import { getProviderSettingsCopy, type ProviderSettingsCopy } from '../locales/s
 import {
   useOAuthLoginFlow,
   subscriptionActionErrorMessage,
-  subscriptionResultMessage,
-  type OAuthLoginFlowBridge,
+  type OAuthConnectionIdentity,
   type SubscriptionSnapshot,
 } from './use-oauth-login-flow';
 import {
@@ -40,7 +39,9 @@ import {
   useRuntimeHostSettingsGenerationKey,
   useRuntimeHostSettingsTarget,
 } from './runtime-host-settings-target.js';
-import { runtimeHostOAuthLoginBridge } from './runtime-host-settings-bridge.js';
+import {
+  runtimeHostOAuthAuthorizationBridge,
+} from './runtime-host-settings-bridge.js';
 
 export type OAuthCardId = 'codex' | 'github-copilot' | 'xai';
 
@@ -64,8 +65,8 @@ function emptyOAuthCardStates(): Record<OAuthCardId, SubscriptionSnapshot | null
 }
 
 /**
- * Account sign-in rows for the provider catalog, plus the refresh that keeps
- * their badges live.
+ * Account enrollment rows for the provider catalog. Codex and xAI are pure
+ * add intents; only the singleton Copilot import retains aggregate state.
  *
  * This used to be a self-contained `ModelOAuthSection` that rendered both the
  * rows and a Dialog per service. The rows and the login body are now two
@@ -76,7 +77,10 @@ function emptyOAuthCardStates(): Record<OAuthCardId, SubscriptionSnapshot | null
  * login remounts it, which re-reads every account state; that is the refresh,
  * and it is why nothing here has to be pushed across a level boundary.
  */
-export function useOAuthCards(props: { query?: string }) {
+export function useOAuthCards(props: {
+  query?: string;
+  connections: readonly LlmConnection[];
+}) {
   const host = useRuntimeHostSettingsTarget();
   const generationKey = useRuntimeHostSettingsGenerationKey();
   const locale = useUiLocale();
@@ -104,7 +108,7 @@ export function useOAuthCards(props: { query?: string }) {
     const ticket = refreshTicketRef.current + 1;
     refreshTicketRef.current = ticket;
     const results = await Promise.all(
-      cards.map(async (card) => {
+      cards.filter((card) => card.id === 'github-copilot').map(async (card) => {
         try {
           const snapshot = await getSubscriptionSnapshot(card.id, host);
           return { id: card.id, snapshot } as const;
@@ -155,17 +159,25 @@ export function useOAuthCards(props: { query?: string }) {
     .filter(matchesQuery)
     .map((card) => {
       const snapshot = cardStates[card.id];
+      const connectionCount = props.connections.filter(
+        (connection) => connection.providerType === card.providerType,
+      ).length;
       const runtimeState = snapshot?.runtimeState ?? 'unknown';
-      const isLoggedIn =
+      const isLoggedIn = card.id === 'github-copilot' && (
         runtimeState === 'authenticated' ||
         runtimeState === 'refreshing' ||
         runtimeState === 'quota_unavailable' ||
-        runtimeState === 'provider_rejected';
+        runtimeState === 'provider_rejected'
+      );
       return {
         id: card.id,
         providerType: card.providerType,
         name: card.name,
-        description: isLoggedIn && snapshot?.email ? snapshot.email : card.description,
+        description: card.id !== 'github-copilot' && connectionCount > 0
+          ? copy.configuredConnections(connectionCount)
+          : isLoggedIn && snapshot?.email
+            ? snapshot.email
+            : card.description,
         ...(isLoggedIn ? { status: copy.signedIn } : {}),
         isLoggedIn,
       };
@@ -179,7 +191,10 @@ export function useOAuthCards(props: { query?: string }) {
  * this as its setup level; the header and the back affordance belong to that
  * level, the same ones the catalog and the connection detail use.
  */
-export function OAuthLoginPanel(props: { cardId: OAuthCardId; onLoginSuccess(): void | Promise<void> }) {
+export function OAuthLoginPanel(props: {
+  cardId: OAuthCardId;
+  onLoginSuccess(connection?: OAuthConnectionIdentity): void | Promise<void>;
+}) {
   return (
     <RuntimeHostSettingsGenerationBoundary>
       <OAuthLoginPanelForCurrentGeneration {...props} />
@@ -189,7 +204,7 @@ export function OAuthLoginPanel(props: { cardId: OAuthCardId; onLoginSuccess():
 
 function OAuthLoginPanelForCurrentGeneration(props: {
   cardId: OAuthCardId;
-  onLoginSuccess(): void | Promise<void>;
+  onLoginSuccess(connection?: OAuthConnectionIdentity): void | Promise<void>;
 }) {
   if (props.cardId === 'github-copilot') {
     return <GitHubCopilotLoginPanel onLoginSuccess={props.onLoginSuccess} />;
@@ -219,11 +234,10 @@ function modelOAuthCards(copy: ProviderSettingsCopy['oauthSection']): ReadonlyAr
 
 function SubscriptionLoginPanel(props: {
   service: 'codex' | 'xai';
-  onLoginSuccess(): void | Promise<void>;
+  onLoginSuccess(connection: OAuthConnectionIdentity): void | Promise<void>;
 }) {
   const host = useRuntimeHostSettingsTarget();
-  const locale = useUiLocale();
-  const copy = getProviderSettingsCopy(locale).oauthSection;
+  const copy = getProviderSettingsCopy(useUiLocale()).oauthSection;
   const isXai = props.service === 'xai';
   const display: SubscriptionDisplay = isXai
     ? { name: 'xAI Grok', shortName: 'SuperGrok / X Premium', detail: copy.xaiDetail }
@@ -234,9 +248,11 @@ function SubscriptionLoginPanel(props: {
   // localized toast copy) lives in useOAuthLoginFlow so the connection detail
   // page can drive the exact same flow behind its relogin button.
   const flow = useOAuthLoginFlow({
-    bridge: runtimeHostOAuthLoginBridge(
+    mode: 'create',
+    authorizationBridge: runtimeHostOAuthAuthorizationBridge(
       isXai ? window.maka.xaiOAuth : window.maka.openAiCodex,
       host,
+      { kind: 'create' },
     ),
     display: { name: display.name, shortName: display.shortName },
     onLoginSuccess: props.onLoginSuccess,
@@ -244,7 +260,7 @@ function SubscriptionLoginPanel(props: {
 
   return (
     <VStack gap={3} data-status={flow.runtimeState}>
-      <Text type="body">{presentSnapshotDetail(flow.state, display, locale)}</Text>
+      <Text type="body">{display.detail}</Text>
       {!isXai && flow.stateHint && (
         <Text type="supporting" color="secondary">
           {copy.deviceCode} {flow.stateHint}
@@ -254,21 +270,12 @@ function SubscriptionLoginPanel(props: {
         <Banner status="error" role="alert" title={flow.errorMessage} />
       )}
       <HStack gap={2} hAlign="end">
-        {!flow.isLoggedIn ? (
-          <Button
-            variant="primary"
-            onClick={() => void flow.startLogin()}
-            isDisabled={flow.actionBusy}
-            label={flow.pendingAction === 'login' ? copy.openingBrowser : copy.login(display.shortName)}
-          />
-        ) : (
-          <Button
-            variant="ghost"
-            onClick={() => void flow.logout()}
-            isDisabled={flow.actionBusy}
-            label={flow.pendingAction === 'logout' ? copy.loggingOut : copy.logout}
-          />
-        )}
+        <Button
+          variant="primary"
+          onClick={() => void flow.startLogin()}
+          isDisabled={flow.actionBusy}
+          label={flow.pendingAction === 'login' ? copy.openingBrowser : copy.loginAndAdd}
+        />
       </HStack>
     </VStack>
   );
@@ -283,10 +290,11 @@ function GitHubCopilotLoginPanel(props: { onLoginSuccess(): void | Promise<void>
   // no browser handoff, no logout confirm) instead of owning a separate
   // pending-action state machine here (#1042).
   const flow = useOAuthLoginFlow({
-    bridge: {
+    mode: 'direct',
+    accountBridge: {
       getAccountState: () => window.maka.githubCopilotSubscription.getAccountState(host),
       logout: () => window.maka.githubCopilotSubscription.logout(host),
-    } as OAuthLoginFlowBridge,
+    },
     display: { name: 'GitHub Copilot', shortName: 'GitHub Copilot' },
     onLoginSuccess: props.onLoginSuccess,
     direct: {
@@ -310,7 +318,7 @@ function GitHubCopilotLoginPanel(props: { onLoginSuccess(): void | Promise<void>
         {loggedIn && (
           <>
             <Button variant="secondary" onClick={() => void refreshTokens?.()} isDisabled={flow.actionBusy} label={flow.pendingAction === 'refresh' ? copy.verifying : copy.reverify} />
-            <Button variant="ghost" onClick={() => void flow.logout()} isDisabled={flow.actionBusy} label={flow.pendingAction === 'logout' ? copy.removing : copy.removeLocal} />
+            <Button variant="ghost" onClick={() => void flow.logout?.()} isDisabled={flow.actionBusy} label={flow.pendingAction === 'logout' ? copy.removing : copy.removeLocal} />
           </>
         )}
       </HStack>
@@ -325,10 +333,7 @@ async function getSubscriptionSnapshot(
   if (serviceId === 'github-copilot') {
     return window.maka.githubCopilotSubscription.getAccountState(host);
   }
-  if (serviceId === 'xai') {
-    return window.maka.xaiOAuth.getAccountState(host);
-  }
-  return (await window.maka.openAiCodex.getAccountState(host)) as SubscriptionSnapshot;
+  throw new Error(`Aggregate OAuth state is not available for ${serviceId}`);
 }
 
 interface SubscriptionDisplay {
@@ -336,31 +341,3 @@ interface SubscriptionDisplay {
   shortName: string;
   detail: string;
 }
-
-function presentSnapshotDetail(state: SubscriptionSnapshot | null, display: SubscriptionDisplay, locale: 'zh' | 'en'): string {
-  const copy = getProviderSettingsCopy(locale).oauthSection;
-  if (!state) return copy.loadingAccount;
-  switch (state.runtimeState) {
-    case 'not_logged_in':
-      return copy.signedOut(display.name);
-    case 'authorizing':
-      return copy.authorizing;
-    case 'authenticated': {
-      const parts = [copy.signedIn];
-      if (state.email) parts.push(state.email);
-      if (state.plan) parts.push(state.plan);
-      return parts.join(' · ');
-    }
-    case 'refreshing':
-      return copy.refreshing;
-    case 'refresh_failed':
-      return subscriptionResultMessage(state.errorMessage, copy.refreshTokenFailed, locale);
-    case 'storage_failed':
-      return subscriptionResultMessage(state.errorMessage, copy.storageFailed(display.name), locale);
-    case 'quota_unavailable':
-    case 'provider_rejected':
-      return subscriptionResultMessage(state.errorMessage, copy.providerUnavailable(display.name), locale);
-  }
-  const _exhaustive: never = state.runtimeState;
-  return _exhaustive;
-}
diff --git a/apps/desktop/src/renderer/settings/providers-panel.tsx b/apps/desktop/src/renderer/settings/providers-panel.tsx
index f38af09d46..5a809ed6bc 100644
--- a/apps/desktop/src/renderer/settings/providers-panel.tsx
+++ b/apps/desktop/src/renderer/settings/providers-panel.tsx
@@ -80,7 +80,7 @@ type PanelRoute =
   | { kind: 'list' }
   | { kind: 'catalog' }
   | { kind: 'setup'; target: SetupTarget; origin: 'list' | 'catalog' }
-  | { kind: 'detail'; slug: string };
+  | { kind: 'detail'; connectionId: string };
 
 function backTarget(route: PanelRoute): PanelRoute {
   if (route.kind === 'setup' && route.origin === 'catalog') return { kind: 'catalog' };
@@ -119,29 +119,34 @@ export function ProvidersPanel({ bridge, initialPage = 'connections', initialCon
   const addButtonRef = useRef<HTMLButtonElement>(null);
   // Which row the user left the list from, so returning puts focus back where
   // they were rather than on the page's primary action.
-  const listReturnFocusRef = useRef<string | null>(null);
+  const returnFocusRef = useRef<
+    | { level: 'list'; connectionId: string }
+    | { level: 'catalog'; providerType: ProviderType }
+    | null
+  >(null);
   const detailTitleId = useId();
+  const setupTitleId = useId();
   const locale = useUiLocale();
   const providerCopy = getProviderSettingsCopy(locale);
   const copy = providerCopy.panel;
 
-  async function reload(): Promise<boolean> {
+  async function reload(): Promise<Awaited<ReturnType<ConnectionsBridge['getSnapshot']>> | null> {
     const ticket = ++providersReloadTicketRef.current;
     try {
       const snapshot = await bridge.getSnapshot();
-      if (!providersPanelMountedRef.current || providersReloadTicketRef.current !== ticket) return false;
+      if (!providersPanelMountedRef.current || providersReloadTicketRef.current !== ticket) return null;
       setConnections(snapshot.connections);
       setDefaultSlug(snapshot.defaultConnection);
       setLoadError(null);
       setLoading(false);
-      return true;
+      return snapshot;
     } catch (error) {
-      if (!providersPanelMountedRef.current || providersReloadTicketRef.current !== ticket) return false;
+      if (!providersPanelMountedRef.current || providersReloadTicketRef.current !== ticket) return null;
       const message = providerPanelActionErrorMessage(error, locale);
       setLoadError(message);
       setLoading(false);
       reportHostError(copy.loadFailed, message);
-      return false;
+      return null;
     }
   }
 
@@ -167,9 +172,10 @@ export function ProvidersPanel({ bridge, initialPage = 'connections', initialCon
   const initialConnectionDetailOpenedRef = useRef(false);
   useEffect(() => {
     if (loading || !initialConnectionSlug || initialConnectionDetailOpenedRef.current) return;
-    if (!connections.some((candidate) => candidate.slug === initialConnectionSlug)) return;
+    const connection = connections.find((candidate) => candidate.slug === initialConnectionSlug);
+    if (!connection?.connectionId) return;
     initialConnectionDetailOpenedRef.current = true;
-    setRoute({ kind: 'detail', slug: initialConnectionSlug });
+    setRoute({ kind: 'detail', connectionId: connection.connectionId });
   }, [loading, initialConnectionSlug, connections]);
 
   const initialCreateOpenedRef = useRef(false);
@@ -198,18 +204,22 @@ export function ProvidersPanel({ bridge, initialPage = 'connections', initialCon
     setRoute({ kind: 'list' });
   }
 
-  function openDetail(slug: string) {
-    listReturnFocusRef.current = slug;
-    setRoute({ kind: 'detail', slug });
+  function openDetail(connection: LlmConnection) {
+    if (!connection.connectionId) {
+      reportHostError(copy.loadFailed, copy.connectionRemoved);
+      return;
+    }
+    returnFocusRef.current = { level: 'list', connectionId: connection.connectionId };
+    setRoute({ kind: 'detail', connectionId: connection.connectionId });
   }
 
   function openCatalog() {
-    listReturnFocusRef.current = null;
+    returnFocusRef.current = null;
     setRoute({ kind: 'catalog' });
   }
 
   const selected = route.kind === 'detail'
-    ? connections.find((connection) => connection.slug === route.slug) ?? null
+    ? connections.find((connection) => connection.connectionId === route.connectionId) ?? null
     : null;
 
   // A detail route whose connection vanished (deleted in another window) is an
@@ -223,7 +233,13 @@ export function ProvidersPanel({ bridge, initialPage = 'connections', initialCon
     isReady: !loading,
     resolveTarget: (current) => {
       const find = (selector: string) => document.querySelector<HTMLElement>(selector);
-      if (current === 'catalog') return find('[data-maka-contract="provider-catalog"] input');
+      if (current === 'catalog') {
+        const target = returnFocusRef.current;
+        returnFocusRef.current = null;
+        return (target?.level === 'catalog'
+          ? find(`[data-provider="${target.providerType}"] button`)
+          : null) ?? find('[data-maka-contract="provider-catalog"] input');
+      }
       if (current === 'setup') {
         return find('[data-maka-contract="provider-setup"] input')
           ?? find('[data-maka-contract="provider-setup"]');
@@ -235,9 +251,11 @@ export function ProvidersPanel({ bridge, initialPage = 'connections', initialCon
       // survive the levels in between. The row the user came from may be gone —
       // that is exactly what a deletion does — so the primary action is the
       // fallback, not the default.
-      const returnToSlug = listReturnFocusRef.current;
-      listReturnFocusRef.current = null;
-      return (returnToSlug ? find(`[data-connection-slug="${returnToSlug}"] button`) : null)
+      const target = returnFocusRef.current;
+      returnFocusRef.current = null;
+      return (target?.level === 'list'
+        ? find(`[data-connection-id="${target.connectionId}"] button`)
+        : null)
         ?? addButtonRef.current;
     },
   });
@@ -268,7 +286,7 @@ export function ProvidersPanel({ bridge, initialPage = 'connections', initialCon
             backLabel={copy.backToList}
             logo={<ProviderLogo type={selected.providerType} compact />}
             titleId={detailTitleId}
-            title={selected.name}
+            title={connectionDisplayName(selected, connections)}
             /* The control sits in the slot that shows the state. Setting the
                default connection was reachable only from the command palette,
                while this page displayed a 默认 Badge and offered no way to
@@ -322,7 +340,7 @@ export function ProvidersPanel({ bridge, initialPage = 'connections', initialCon
             subtitle={connectionSubtitle(selected, locale)}
           />
           <ConnectionDetail
-            key={selected.slug}
+            key={selected.connectionId ?? selected.slug}
             bridge={bridge}
             connection={selected}
             isDefault={selected.slug === defaultSlug}
@@ -344,8 +362,12 @@ export function ProvidersPanel({ bridge, initialPage = 'connections', initialCon
           />
           <ProviderCatalogPage
             filter={catalogFilter}
+            connections={connections}
             onFilterChange={setCatalogFilter}
-            onPick={(target) => setRoute({ kind: 'setup', target, origin: 'catalog' })}
+            onPick={(target) => {
+              returnFocusRef.current = { level: 'catalog', providerType: target.providerType };
+              setRoute({ kind: 'setup', target, origin: 'catalog' });
+            }}
           />
         </VStack>
       ) : level === 'setup' && route.kind === 'setup' ? (
@@ -354,7 +376,10 @@ export function ProvidersPanel({ bridge, initialPage = 'connections', initialCon
             onBack={goBack}
             backLabel={route.origin === 'catalog' ? copy.backToCatalog : copy.backToList}
             logo={<ProviderLogo type={route.target.providerType} compact />}
-            title={copy.connectTitle(route.target.name)}
+            titleId={setupTitleId}
+            title={route.target.method === 'account' && route.target.cardId !== 'github-copilot'
+              ? providerCopy.oauthSection.addAccountTitle(route.target.name)
+              : copy.connectTitle(route.target.name)}
             subtitle={route.target.method === 'account'
               ? oauthPanelSubtitle(route.target.cardId, providerCopy.oauthSection)
               : copy.createSubtitle}
@@ -363,16 +388,35 @@ export function ProvidersPanel({ bridge, initialPage = 'connections', initialCon
             bridge={bridge}
             target={route.target}
             existingSlugs={connections.map((connection) => connection.slug)}
+            labelledBy={setupTitleId}
             onCancel={goBack}
-            onAccountChanged={async () => { await reload(); }}
+            onAccountCreated={async (identity) => {
+              const snapshot = await reload();
+              if (!snapshot || !providersPanelMountedRef.current) return;
+              if (!identity) return;
+              const created = snapshot.connections.find(
+                (connection) => connection.connectionId === identity.connectionId,
+              );
+              if (!created || created.slug !== identity.slug || created.providerType !== identity.providerType) {
+                reportHostError(copy.loadFailed, copy.connectionRemoved);
+                return;
+              }
+              returnFocusRef.current = null;
+              setRoute({ kind: 'detail', connectionId: created.connectionId });
+            }}
             onCreated={async (slug, modelDiscoveryError) => {
-              await reload();
-              if (!providersPanelMountedRef.current) return;
+              const snapshot = await reload();
+              if (!snapshot || !providersPanelMountedRef.current) return;
               // The new connection's detail, not the list: creating it is the
               // start of setting it up, and every next move — pick the default
               // model, enable models, fix the endpoint the discovery error just
               // complained about — is on that page.
-              setRoute({ kind: 'detail', slug });
+              const created = snapshot.connections.find((connection) => connection.slug === slug);
+              if (!created?.connectionId) {
+                reportHostError(copy.loadFailed, copy.connectionRemoved);
+                return;
+              }
+              setRoute({ kind: 'detail', connectionId: created.connectionId });
               if (modelDiscoveryError) {
                 const providerName = providerDisplay(route.target.providerType, locale).name;
                 reportHostError(
@@ -388,6 +432,9 @@ export function ProvidersPanel({ bridge, initialPage = 'connections', initialCon
         </VStack>
       ) : (
         <>
+          {route.kind === 'detail' && !selected && (
+            <Banner status="warning" role="status" title={copy.connectionRemoved} />
+          )}
           <HStack gap={2} vAlign="center" hAlign="between">
             <HStack gap={2} vAlign="center">
               <Heading level={3}>{copy.connections}</Heading>
@@ -424,15 +471,16 @@ export function ProvidersPanel({ bridge, initialPage = 'connections', initialCon
                 const isDefault = connection.slug === defaultSlug;
                 return (
                   <ListItem
-                    key={connection.slug}
+                    key={connection.connectionId ?? connection.slug}
                     className="connectionRow"
+                    data-connection-id={connection.connectionId}
                     data-connection-slug={connection.slug}
                     data-disabled={connection.enabled ? undefined : 'true'}
                     startContent={<ProviderLogo type={connection.providerType} compact />}
                     label={(
                       <HStack gap={2} vAlign="center">
                         {/* a11y-allow: this label names the ROW, not the span. Astryx's Item puts consumer props on its outer wrapper and renders a separate invisible <button> for the click target, so an aria-label on the Item never reaches that button — measured. The button is named from its content, and this span is how the status reaches that name. Removing it drops the runtime error from the row's accessible name (settings.spec:226).*/}
-                        <span aria-label={chipAriaLabel(connection, isDefault)}>{connection.name}</span>
+                        <span aria-label={chipAriaLabel(connection, isDefault)}>{connectionDisplayName(connection, connections)}</span>
                         {isDefault && <Badge variant="neutral" label={copy.default} />}
                       </HStack>
                     )}
@@ -448,7 +496,7 @@ export function ProvidersPanel({ bridge, initialPage = 'connections', initialCon
                         <ChevronRight size={ICON_SIZE.chrome} aria-hidden="true" />
                       </HStack>
                     )}
-                    onClick={() => openDetail(connection.slug)}
+                    onClick={() => openDetail(connection)}
                   />
                 );
               })}
@@ -462,7 +510,12 @@ export function ProvidersPanel({ bridge, initialPage = 'connections', initialCon
   function chipAriaLabel(connection: LlmConnection, isDefault: boolean): string {
     const provider = providerDisplay(connection.providerType, locale).name;
     const status = connectionChipStatus(connection, locale);
-    return copy.chipAria(connection.name, provider, isDefault, status?.label);
+    return copy.chipAria(
+      connectionDisplayName(connection, connections),
+      provider,
+      isDefault,
+      status?.label,
+    );
   }
 }
 
@@ -473,3 +526,16 @@ function connectionSubtitle(connection: LlmConnection, locale: 'zh' | 'en'): str
   if (connection.defaultModel) parts.push(connection.defaultModel);
   return parts.join(' · ');
 }
+
+function connectionDisplayName(
+  connection: LlmConnection,
+  connections: readonly LlmConnection[],
+): string {
+  const ambiguous = connections.some(
+    (candidate) =>
+      candidate.connectionId !== connection.connectionId &&
+      candidate.providerType === connection.providerType &&
+      candidate.name === connection.name,
+  );
+  return ambiguous ? `${connection.name} · ${connection.slug}` : connection.name;
+}
diff --git a/apps/desktop/src/renderer/settings/runtime-host-settings-bridge.ts b/apps/desktop/src/renderer/settings/runtime-host-settings-bridge.ts
index a4a4aa1b20..f9ed6ef707 100644
--- a/apps/desktop/src/renderer/settings/runtime-host-settings-bridge.ts
+++ b/apps/desktop/src/renderer/settings/runtime-host-settings-bridge.ts
@@ -17,9 +17,16 @@
  * under the License.
  */
 
-import type { DesktopRuntimeHostRef } from '../../preload/bridge-contract.js';
+import type {
+  DesktopOAuthLoginTarget,
+  DesktopRuntimeHostRef,
+  MakaBridge,
+} from '../../preload/bridge-contract.js';
 import type { ConnectionsBridge } from './provider-panel-shared.js';
-import type { OAuthLoginFlowBridge } from './use-oauth-login-flow.js';
+import type {
+  OAuthAccountFlowBridge,
+  OAuthAuthorizationFlowBridge,
+} from './use-oauth-login-flow.js';
 
 export type RuntimeHostSettingsConnectionsBridge = ConnectionsBridge & {
   setDefaultModel(input: { slug: string; model: string } | null): Promise<void>;
@@ -46,20 +53,62 @@ export function runtimeHostConnectionsBridge(
   };
 }
 
-export function runtimeHostOAuthLoginBridge(
-  bridge: typeof window.maka.openAiCodex | typeof window.maka.xaiOAuth,
+type RuntimeHostOAuthBridge = MakaBridge['openAiCodex'] | MakaBridge['xaiOAuth'];
+
+export function runtimeHostOAuthAuthorizationBridge(
+  bridge: RuntimeHostOAuthBridge,
   host: DesktopRuntimeHostRef,
-  connectionId?: string,
-): OAuthLoginFlowBridge {
+  target: DesktopOAuthLoginTarget,
+): OAuthAuthorizationFlowBridge {
   return {
-    getAuthUrl: () =>
-      bridge.getAuthUrl(host, connectionId) as ReturnType<OAuthLoginFlowBridge['getAuthUrl']>,
+    getAuthUrl: () => bridge.getAuthUrl(host, target),
     openAuthUrl: (authRequestId) => bridge.openAuthUrl(authRequestId, host),
-    completeAuthorization: (authRequestId) =>
-      bridge.completeAuthorization(authRequestId, host),
-    cancelAuthorization: (authRequestId) =>
-      bridge.cancelAuthorization(authRequestId, host),
-    getAccountState: () => bridge.getAccountState(host, connectionId),
+    completeAuthorization: (authRequestId) => bridge.completeAuthorization(authRequestId, host),
+    cancelAuthorization: (authRequestId) => bridge.cancelAuthorization(authRequestId, host),
+  };
+}
+
+export function runtimeHostOAuthAccountBridge(
+  bridge: RuntimeHostOAuthBridge,
+  host: DesktopRuntimeHostRef,
+  connectionId: string,
+): OAuthAccountFlowBridge {
+  return {
+    getAccountState: async () => requireOAuthAccountState(
+      await bridge.getAccountState(host, connectionId),
+    ),
     logout: () => bridge.logout(host, connectionId),
   };
 }
+
+function requireOAuthAccountState(value: unknown): unknown {
+  if (
+    value &&
+    typeof value === 'object' &&
+    'ok' in value &&
+    value.ok === false &&
+    'message' in value &&
+    typeof value.message === 'string'
+  ) {
+    throw new Error(value.message);
+  }
+  return value;
+}
+
+export function runtimeHostOAuthExistingLoginBridges(
+  bridge: RuntimeHostOAuthBridge,
+  host: DesktopRuntimeHostRef,
+  connectionId: string,
+): {
+  authorizationBridge: OAuthAuthorizationFlowBridge;
+  accountBridge: OAuthAccountFlowBridge;
+} {
+  return {
+    authorizationBridge: runtimeHostOAuthAuthorizationBridge(
+      bridge,
+      host,
+      { kind: 'existing', connectionId },
+    ),
+    accountBridge: runtimeHostOAuthAccountBridge(bridge, host, connectionId),
+  };
+}
diff --git a/apps/desktop/src/renderer/settings/use-connection-detail.ts b/apps/desktop/src/renderer/settings/use-connection-detail.ts
index e9e9859a04..443e6337fe 100644
--- a/apps/desktop/src/renderer/settings/use-connection-detail.ts
+++ b/apps/desktop/src/renderer/settings/use-connection-detail.ts
@@ -50,7 +50,10 @@ import { connectionChipStatus } from './provider-connection-status';
 import { relayProfileDraftReseedPlan, relayProfileDraftSeed } from './relay-profile-draft';
 import { applyBulkThinkingLevel, relayProfileWithThinkingLevels } from './relay-thinking-bulk';
 import { useKeyedActionGuard } from './use-action-guard';
-import type { OAuthLoginFlowBridge } from './use-oauth-login-flow';
+import type {
+  OAuthAccountFlowBridge,
+  OAuthAuthorizationFlowBridge,
+} from './use-oauth-login-flow';
 import {
   connectionLastTestMessageDisplay,
   connectionTestFailureMessage,
@@ -62,7 +65,9 @@ import {
   useRuntimeHostSettingsErrorReporter,
   useRuntimeHostSettingsTarget,
 } from './runtime-host-settings-target.js';
-import { runtimeHostOAuthLoginBridge } from './runtime-host-settings-bridge.js';
+import {
+  runtimeHostOAuthExistingLoginBridges,
+} from './runtime-host-settings-bridge.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
@@ -70,7 +75,8 @@ import { runtimeHostOAuthLoginBridge } from './runtime-host-settings-bridge.js';
 // here; plain API-key providers return null so the notice falls back to
 // prose instead of rendering a dead button.
 export interface OAuthLoginService {
-  bridge: OAuthLoginFlowBridge;
+  authorizationBridge: OAuthAuthorizationFlowBridge;
+  accountBridge: OAuthAccountFlowBridge;
   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
@@ -87,13 +93,21 @@ export function oauthLoginServiceFor(
   switch (providerType) {
     case 'openai-codex':
       return {
-        bridge: runtimeHostOAuthLoginBridge(window.maka.openAiCodex, host, connectionId),
+        ...runtimeHostOAuthExistingLoginBridges(
+          window.maka.openAiCodex,
+          host,
+          connectionId,
+        ),
         display: { name: 'OpenAI Codex', shortName: 'Codex' },
         showsDeviceCode: true,
       };
     case 'xai-oauth':
       return {
-        bridge: runtimeHostOAuthLoginBridge(window.maka.xaiOAuth, host, connectionId),
+        ...runtimeHostOAuthExistingLoginBridges(
+          window.maka.xaiOAuth,
+          host,
+          connectionId,
+        ),
         display: { name: 'xAI Grok', shortName: 'SuperGrok / X Premium' },
         showsDeviceCode: false,
       };
diff --git a/apps/desktop/src/renderer/settings/use-oauth-login-flow.ts b/apps/desktop/src/renderer/settings/use-oauth-login-flow.ts
index 57006a0c82..cc4b8b9564 100644
--- a/apps/desktop/src/renderer/settings/use-oauth-login-flow.ts
+++ b/apps/desktop/src/renderer/settings/use-oauth-login-flow.ts
@@ -61,17 +61,32 @@ export interface SubscriptionSnapshot {
   errorMessage?: string;
 }
 
-export interface OAuthLoginFlowBridge {
+export interface OAuthConnectionIdentity {
+  connectionId: string;
+  slug: string;
+  providerType: 'openai-codex' | 'xai-oauth';
+}
+
+export interface OAuthAuthorizationFlowBridge {
   getAuthUrl(): Promise<
-    { authRequestId: string; stateHint: string } | { ok: boolean; reason?: string; message: string }
+    { authRequestId: string; stateHint: string; connection: OAuthConnectionIdentity }
+    | { ok: boolean; reason?: string; message: string }
   >;
   openAuthUrl(authRequestId: string): Promise<{ ok: true } | { ok: false; reason: string; message: string }>;
-  completeAuthorization(authRequestId: string): Promise<{ ok: true } | { ok: false; reason: string; message: string }>;
+  completeAuthorization(authRequestId: string): Promise<
+    { ok: true; connection: OAuthConnectionIdentity }
+    | { ok: false; reason: string; message: string }
+  >;
   cancelAuthorization(authRequestId?: string): Promise<{ ok: true }>;
+}
+
+export interface OAuthAccountFlowBridge {
   getAccountState(): Promise<unknown>;
   logout(): Promise<{ ok: true } | { ok: false; reason: string; message: string }>;
 }
 
+export type OAuthLoginFlowBridge = OAuthAuthorizationFlowBridge & OAuthAccountFlowBridge;
+
 export interface OAuthLoginFlowDisplay {
   name: string;
   shortName: string;
@@ -103,30 +118,43 @@ export interface OAuthLoginFlowController {
   errorMessage: string | null;
   actionBusy: boolean;
   startLogin(): Promise<void>;
-  logout(): Promise<void>;
+  logout: (() => Promise<void>) | undefined;
   refresh(): Promise<boolean>;
   // Direct account flows only (GitHub Copilot 重新验证); undefined for the
   // browser-assisted services so they cannot render a dead action.
   refreshTokens: (() => Promise<void>) | undefined;
 }
 
-export function useOAuthLoginFlow(params: {
-  bridge: OAuthLoginFlowBridge;
-  display: OAuthLoginFlowDisplay;
-  // Fired after a successful completeAuthorization (browser handoff done).
-  // The detail sheet uses it to re-probe hasSecret + reload connection status;
-  // catalog modals use it to refresh both their account card and the shared
-  // model connection list without waiting for the modal to close.
-  onLoginSuccess?: () => void | Promise<void>;
-  // When present, startLogin runs this one-shot import instead of the
-  // getAuthUrl -> openAuthUrl -> completeAuthorization handoff, and the
-  // controller exposes the extra `refreshTokens` action.
-  direct?: OAuthDirectAccountFlow;
-}): OAuthLoginFlowController {
-  const { bridge, display } = params;
+type OAuthLoginFlowParams =
+  | {
+      mode: 'create';
+      authorizationBridge: OAuthAuthorizationFlowBridge;
+      display: OAuthLoginFlowDisplay;
+      onLoginSuccess(connection: OAuthConnectionIdentity): void | Promise<void>;
+    }
+  | {
+      mode: 'existing';
+      authorizationBridge: OAuthAuthorizationFlowBridge;
+      accountBridge: OAuthAccountFlowBridge;
+      display: OAuthLoginFlowDisplay;
+      onLoginSuccess?: (connection: OAuthConnectionIdentity) => void | Promise<void>;
+      onAccountChanged?: () => void | Promise<void>;
+    }
+  | {
+      mode: 'direct';
+      accountBridge: OAuthAccountFlowBridge;
+      display: OAuthLoginFlowDisplay;
+      onLoginSuccess?: () => void | Promise<void>;
+      direct: OAuthDirectAccountFlow;
+    };
+
+export function useOAuthLoginFlow(params: OAuthLoginFlowParams): OAuthLoginFlowController {
+  const { display } = params;
   const locale = useUiLocale();
   const copy = getProviderSettingsCopy(locale).oauthFlow;
-  const direct = params.direct;
+  const direct = params.mode === 'direct' ? params.direct : undefined;
+  const authorizationBridge = params.mode === 'direct' ? undefined : params.authorizationBridge;
+  const accountBridge = params.mode === 'create' ? undefined : params.accountBridge;
   const toast = useToast();
   const reportHostError = useRuntimeHostSettingsErrorReporter();
   const [state, setState] = useState<SubscriptionSnapshot | null>(null);
@@ -139,8 +167,9 @@ export function useOAuthLoginFlow(params: {
   const oauthLoginFlowMountedRef = useMountedRef();
 
   async function refresh(): Promise<boolean> {
+    if (!accountBridge) return true;
     try {
-      const next = (await bridge.getAccountState()) as SubscriptionSnapshot;
+      const next = (await accountBridge.getAccountState()) as SubscriptionSnapshot;
       if (!oauthLoginFlowMountedRef.current) return false;
       setState(next);
       setErrorMessage(null);
@@ -155,10 +184,15 @@ export function useOAuthLoginFlow(params: {
   }
 
   useEffect(() => {
-    void refresh();
+    if (accountBridge) void refresh();
     return () => {
       pendingGuard.finish();
-      teardownPendingAuthorization(authRequestIdRef, (id) => void bridge.cancelAuthorization(id));
+      if (authorizationBridge) {
+        teardownPendingAuthorization(
+          authRequestIdRef,
+          (id) => void authorizationBridge.cancelAuthorization(id),
+        );
+      }
     };
     // eslint-disable-next-line react-hooks/exhaustive-deps
   }, []);
@@ -191,7 +225,9 @@ export function useOAuthLoginFlow(params: {
         }
         await refresh();
         if (!oauthLoginFlowMountedRef.current) return;
-        if (result.ok && params.onLoginSuccess) await params.onLoginSuccess();
+        if (result.ok && params.mode === 'direct' && params.onLoginSuccess) {
+          await params.onLoginSuccess();
+        }
       } catch (error) {
         if (!oauthLoginFlowMountedRef.current) return;
         reportHostError(
@@ -204,7 +240,8 @@ export function useOAuthLoginFlow(params: {
       return;
     }
     try {
-      const payload = await bridge.getAuthUrl();
+      if (!authorizationBridge || params.mode === 'direct') return;
+      const payload = await authorizationBridge.getAuthUrl();
       if ('ok' in payload) {
         if (!oauthLoginFlowMountedRef.current) return;
         const failureMessage = payload.ok ? copy.retry : subscriptionResultMessage(payload.message, copy.startFailedRetry, locale);
@@ -215,18 +252,18 @@ export function useOAuthLoginFlow(params: {
       authRequestIdRef.current = payload.authRequestId;
       if (!oauthLoginFlowMountedRef.current) {
         authRequestIdRef.current = null;
-        void bridge.cancelAuthorization(payload.authRequestId);
+        void authorizationBridge.cancelAuthorization(payload.authRequestId);
         return;
       }
       setAuthRequestId(payload.authRequestId);
       setStateHint(payload.stateHint);
-      const opened = await bridge.openAuthUrl(payload.authRequestId);
+      const opened = await authorizationBridge.openAuthUrl(payload.authRequestId);
       if (!oauthLoginFlowMountedRef.current) return;
       if (!opened.ok) {
         const message = subscriptionResultMessage(opened.message, copy.openFailedRetry, locale);
         reportHostError(copy.openFailed, message);
         setErrorMessage(message);
-        void bridge.cancelAuthorization(payload.authRequestId);
+        void authorizationBridge.cancelAuthorization(payload.authRequestId);
         authRequestIdRef.current = null;
         setAuthRequestId(null);
         setStateHint(null);
@@ -235,7 +272,7 @@ export function useOAuthLoginFlow(params: {
       const refreshed = await refresh();
       if (!oauthLoginFlowMountedRef.current || !refreshed) return;
       // Wait for the backend to finish polling the provider.
-      const result = await bridge.completeAuthorization(payload.authRequestId);
+      const result = await authorizationBridge.completeAuthorization(payload.authRequestId);
       if (!oauthLoginFlowMountedRef.current) return;
       authRequestIdRef.current = null;
       setAuthRequestId(null);
@@ -244,7 +281,7 @@ export function useOAuthLoginFlow(params: {
         toast.success(copy.loginSuccess, copy.bound(display.name));
         await refresh();
         if (!oauthLoginFlowMountedRef.current) return;
-        if (params.onLoginSuccess) await params.onLoginSuccess();
+        if (params.onLoginSuccess) await params.onLoginSuccess(result.connection);
       } else {
         const message = subscriptionResultMessage(result.message, copy.incompleteRetry, locale);
         reportHostError(copy.incomplete, message);
@@ -254,7 +291,9 @@ export function useOAuthLoginFlow(params: {
       if (!oauthLoginFlowMountedRef.current) return;
       const pendingAuthRequestId = authRequestIdRef.current;
       authRequestIdRef.current = null;
-      if (pendingAuthRequestId) void bridge.cancelAuthorization(pendingAuthRequestId);
+      if (pendingAuthRequestId && authorizationBridge) {
+        void authorizationBridge.cancelAuthorization(pendingAuthRequestId);
+      }
       setAuthRequestId(null);
       setStateHint(null);
       const message = subscriptionActionErrorMessage(error, locale);
@@ -266,6 +305,7 @@ export function useOAuthLoginFlow(params: {
   }
 
   async function logout() {
+    if (!accountBridge) return;
     if (!beginPendingAction('logout')) return;
     try {
       // Direct-import flows keep their original no-confirm, silent-success
@@ -281,13 +321,16 @@ export function useOAuthLoginFlow(params: {
         });
         if (!ok) return;
       }
-      const result = await bridge.logout();
+      const result = await accountBridge.logout();
       if (!oauthLoginFlowMountedRef.current) return;
       if (result.ok) {
         if (!direct) {
           toast.success(copy.loggedOut, copy.credentialsCleared);
         }
         await refresh();
+        if (params.mode === 'existing' && params.onAccountChanged) {
+          await params.onAccountChanged();
+        }
       } else if (direct) {
         reportHostError(
           copy.accountActionFailed(display.name),
@@ -355,7 +398,7 @@ export function useOAuthLoginFlow(params: {
     errorMessage,
     actionBusy,
     startLogin,
-    logout,
+    logout: accountBridge ? logout : undefined,
     refresh,
     refreshTokens: direct ? refreshTokens : undefined,
   };


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org