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/09/01 15:41:19 UTC

(maka) branch main updated: feat(desktop): route standard API key onboarding through Runtime Host (#4377)

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

me2seeks pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/maka.git


The following commit(s) were added to refs/heads/main by this push:
     new 0571d7b379 feat(desktop): route standard API key onboarding through Runtime Host (#4377)
0571d7b379 is described below

commit 0571d7b379d21b3a1dec411d6870ed1bc2a09e26
Author: Zhang <96...@users.noreply.github.com>
AuthorDate: Tue Sep 1 23:41:13 2026 +0800

    feat(desktop): route standard API key onboarding through Runtime Host (#4377)
    
    * feat(desktop): route standard API key onboarding through host
    
    * feat(desktop): add host-backed API key enrollment flow
    
    * refactor(desktop): move connection settings behind feature adapter
    
    * docs(desktop): refresh Astryx surface inventory
    
    * refactor(desktop): narrow connection settings exports
    
    * fix(desktop): make connection settings entry ESM-safe
    
    * chore(desktop): refresh renderer architecture ledger
    
    * fix(desktop): observe onboarding save uncertainty
    
    * fix(desktop): fail closed on onboarding response errors
    
    * fix(desktop): rebase onboarding on host model catalog
---
 .../e2e/request-header-row-contract.spec.ts        |   2 +-
 apps/desktop/renderer-architecture.json            |  76 +----
 .../main/__tests__/provider-add-submission.test.ts | 218 +++++++++++++-
 .../runtime-host-connections-ipc-main.test.ts      | 187 ++++++++++++
 apps/desktop/src/main/runtime-host-client.ts       |  12 +
 .../src/main/runtime-host-connections-ipc-main.ts  |  43 ++-
 apps/desktop/src/preload/bridge-contract.d.ts      |  16 +
 apps/desktop/src/preload/preload.ts                |   6 +
 .../contracts/connection-error-cleaner.ts          |  37 +++
 .../composition/desktop-feature-services.tsx       |  13 +-
 .../renderer/features/connection-settings/index.ts |  42 +++
 .../renderer/features/connection-settings/ports.ts |  93 ++++++
 .../connection-settings}/provider-panel-shared.ts  |  39 +--
 .../connection-settings/services-context.tsx       |  61 ++++
 .../connection-settings}/settings-provider-copy.ts |  12 +-
 .../src/renderer/model-connection-errors.ts        |  29 +-
 .../desktop/create-connection-settings-services.ts |  88 ++++++
 .../renderer/settings/general-settings-page.tsx    |   2 +-
 .../src/renderer/settings/provider-add-form.tsx    | 331 ++++++++++++++++++++-
 .../settings/provider-add-model-dialog.tsx         |   2 +-
 .../renderer/settings/provider-add-submission.ts   |  55 ++++
 .../renderer/settings/provider-catalog-page.tsx    |  16 +-
 .../settings/provider-connection-detail.tsx        |   4 +-
 .../settings/provider-connection-status.ts         |   2 +-
 .../settings/provider-enabled-model-manager.tsx    |   2 +-
 .../renderer/settings/provider-oauth-section.tsx   |   2 +-
 .../src/renderer/settings/providers-panel.tsx      | 149 ++++++++--
 .../settings/runtime-host-settings-bridge.ts       |  26 --
 .../src/renderer/settings/settings-surface.tsx     |  41 ++-
 .../src/renderer/settings/use-connection-detail.ts |   4 +-
 .../src/renderer/settings/use-oauth-login-flow.ts  |   2 +-
 .../stories/settings/provider-settings.stories.tsx | 323 +++++++++++++++++++-
 .../stories/settings/settings-pages.stories.tsx    |  49 +--
 docs/astryx-surface-file-inventory.md              |   5 +-
 docs/astryx-surface-file-inventory.paths           |   1 +
 35 files changed, 1733 insertions(+), 257 deletions(-)

diff --git a/apps/desktop/e2e/request-header-row-contract.spec.ts b/apps/desktop/e2e/request-header-row-contract.spec.ts
index 743afe201d..c820b17743 100644
--- a/apps/desktop/e2e/request-header-row-contract.spec.ts
+++ b/apps/desktop/e2e/request-header-row-contract.spec.ts
@@ -18,7 +18,7 @@
  */
 
 import { expect, test } from './fixtures';
-import { getProviderSettingsCopy } from '../src/renderer/locales/settings-provider-copy';
+import { getProviderSettingsCopy } from '../src/renderer/features/connection-settings';
 
 /**
  * The custom request header row's remove button centres on the FIELD, not on
diff --git a/apps/desktop/renderer-architecture.json b/apps/desktop/renderer-architecture.json
index db13adebe6..14d7786e93 100644
--- a/apps/desktop/renderer-architecture.json
+++ b/apps/desktop/renderer-architecture.json
@@ -80,7 +80,6 @@
     "src/renderer/locales/settings-navigation-copy.ts",
     "src/renderer/locales/settings-preferences-copy.ts",
     "src/renderer/locales/settings-projects-copy.ts",
-    "src/renderer/locales/settings-provider-copy.ts",
     "src/renderer/locales/settings-shared-copy.ts",
     "src/renderer/locales/settings-subagents-copy.ts",
     "src/renderer/locales/settings-tasks-copy.ts",
@@ -169,7 +168,6 @@
     "src/renderer/settings/provider-enabled-model-manager.tsx",
     "src/renderer/settings/provider-endpoint-presentation.ts",
     "src/renderer/settings/provider-oauth-section.tsx",
-    "src/renderer/settings/provider-panel-shared.ts",
     "src/renderer/settings/providers-panel.tsx",
     "src/renderer/settings/relay-profile-draft.ts",
     "src/renderer/settings/relay-thinking-bulk.ts",
@@ -1968,17 +1966,6 @@
           "@maka/runtime-host/operator": 1
         }
       },
-      "src/renderer/locales/settings-provider-copy.ts": {
-        "bridgePaths": {},
-        "environmentCapabilities": {},
-        "hookCalls": {},
-        "lifecycleMethods": {},
-        "unresolvedDependencies": 0,
-        "actionFactories": [],
-        "dependencyPaths": {
-          "@maka/core/ui-locale": 1
-        }
-      },
       "src/renderer/locales/settings-shared-copy.ts": {
         "bridgePaths": {},
         "environmentCapabilities": {},
@@ -2220,10 +2207,10 @@
         "unresolvedDependencies": 0,
         "actionFactories": [],
         "dependencyPaths": {
+          "./application/contracts/connection-error-cleaner.js": 1,
           "./locales/conversation-copy.js": 1,
           "./locales/shell-copy.js": 1,
           "./session-error-presentation.js": 1,
-          "@maka/core/connection-error-copy": 1,
           "@maka/core/connection-readiness": 1,
           "@maka/core/events": 1,
           "@maka/core/ui-locale": 1
@@ -3079,6 +3066,7 @@
         "unresolvedDependencies": 0,
         "actionFactories": [],
         "dependencyPaths": {
+          "../features/connection-settings": 1,
           "../locales/settings-preferences-copy.js": 1,
           "../locales/settings-shared-copy.js": 1,
           "../locales/settings-test-result-copy.js": 1,
@@ -3086,7 +3074,6 @@
           "./password-input": 1,
           "./personalization-settings-section": 1,
           "./provider-brand-marks": 1,
-          "./runtime-host-settings-bridge.js": 1,
           "./runtime-host-settings-target.js": 1,
           "./settings-error-copy": 1,
           "./settings-resource-state.js": 1,
@@ -3447,12 +3434,11 @@
         "unresolvedDependencies": 0,
         "actionFactories": [],
         "dependencyPaths": {
-          "../locales/settings-provider-copy": 1,
+          "../features/connection-settings": 1,
           "../model-catalog-choices": 1,
           "./password-input": 1,
           "./provider-add-submission": 1,
           "./provider-display": 1,
-          "./provider-panel-shared": 1,
           "./request-customization-editor": 1,
           "./use-action-guard": 1,
           "@astryxdesign/core": 1,
@@ -3473,7 +3459,7 @@
         "unresolvedDependencies": 0,
         "actionFactories": [],
         "dependencyPaths": {
-          "../locales/settings-provider-copy": 1,
+          "../features/connection-settings": 1,
           "@astryxdesign/core/Dialog": 1,
           "@astryxdesign/core/FormLayout": 1,
           "@astryxdesign/core/Layout": 1,
@@ -3540,11 +3526,10 @@
         "unresolvedDependencies": 0,
         "actionFactories": [],
         "dependencyPaths": {
-          "../locales/settings-provider-copy": 1,
+          "../features/connection-settings": 1,
           "./provider-add-form": 1,
           "./provider-display": 1,
           "./provider-oauth-section": 1,
-          "./provider-panel-shared": 1,
           "@astryxdesign/core": 1,
           "@maka/core/llm-connections": 1,
           "@maka/core/provider-registry": 1,
@@ -3573,13 +3558,12 @@
         "unresolvedDependencies": 0,
         "actionFactories": [],
         "dependencyPaths": {
-          "../locales/settings-provider-copy": 1,
+          "../features/connection-settings": 1,
           "./password-input": 1,
           "./provider-add-model-dialog": 1,
           "./provider-display": 1,
           "./provider-enabled-model-manager": 1,
           "./provider-endpoint-presentation": 1,
-          "./provider-panel-shared": 1,
           "./relay-thinking-bulk": 1,
           "./request-customization-editor": 1,
           "./runtime-host-settings-target.js": 1,
@@ -3604,7 +3588,7 @@
         "unresolvedDependencies": 0,
         "actionFactories": [],
         "dependencyPaths": {
-          "../locales/settings-provider-copy.js": 1,
+          "../features/connection-settings/index.js": 1,
           "@maka/core/llm-connections": 1,
           "@maka/core/provider-registry": 1,
           "@maka/core/ui-locale": 1,
@@ -3646,7 +3630,7 @@
         "unresolvedDependencies": 0,
         "actionFactories": [],
         "dependencyPaths": {
-          "../locales/settings-provider-copy": 1,
+          "../features/connection-settings": 1,
           "@astryxdesign/core": 1,
           "@maka/core/model-catalog": 1,
           "@maka/ui": 1
@@ -3690,7 +3674,7 @@
         "actionFactories": [],
         "dependencyPaths": {
           "../../preload/bridge-contract.js": 1,
-          "../locales/settings-provider-copy": 1,
+          "../features/connection-settings": 1,
           "./runtime-host-settings-bridge.js": 1,
           "./runtime-host-settings-target.js": 1,
           "./use-oauth-login-flow": 1,
@@ -3700,22 +3684,6 @@
           "react": 1
         }
       },
-      "src/renderer/settings/provider-panel-shared.ts": {
-        "bridgePaths": {},
-        "environmentCapabilities": {},
-        "hookCalls": {},
-        "lifecycleMethods": {},
-        "unresolvedDependencies": 0,
-        "actionFactories": [],
-        "dependencyPaths": {
-          "../../shared/desktop-connection-snapshot.js": 1,
-          "../locales/settings-provider-copy.js": 1,
-          "../model-connection-errors.js": 1,
-          "@maka/core/llm-connections": 1,
-          "@maka/core/redaction": 1,
-          "@maka/core/ui-locale": 1
-        }
-      },
       "src/renderer/settings/providers-panel.tsx": {
         "bridgePaths": {},
         "environmentCapabilities": {
@@ -3734,13 +3702,12 @@
         "unresolvedDependencies": 0,
         "actionFactories": [],
         "dependencyPaths": {
-          "../locales/settings-provider-copy": 1,
+          "../features/connection-settings": 2,
           "./provider-catalog-page": 1,
           "./provider-connection-detail": 1,
           "./provider-connection-status": 1,
           "./provider-display": 1,
           "./provider-oauth-section": 1,
-          "./provider-panel-shared": 2,
           "./runtime-host-settings-target.js": 1,
           "./settings-error-copy": 1,
           "./settings-route-focus": 1,
@@ -3965,20 +3932,7 @@
         }
       },
       "src/renderer/settings/runtime-host-settings-bridge.ts": {
-        "bridgePaths": {
-          "window.maka.connections.create": 1,
-          "window.maka.connections.delete": 1,
-          "window.maka.connections.fetchModels": 1,
-          "window.maka.connections.getRequestHeaders": 1,
-          "window.maka.connections.getSnapshot": 1,
-          "window.maka.connections.hasSecret": 1,
-          "window.maka.connections.setDefault": 1,
-          "window.maka.connections.setDefaultModel": 1,
-          "window.maka.connections.setRequestHeaders": 1,
-          "window.maka.connections.subscribeEvents": 1,
-          "window.maka.connections.test": 1,
-          "window.maka.connections.update": 1
-        },
+        "bridgePaths": {},
         "environmentCapabilities": {},
         "hookCalls": {},
         "lifecycleMethods": {},
@@ -3986,7 +3940,6 @@
         "actionFactories": [],
         "dependencyPaths": {
           "../../preload/bridge-contract.js": 1,
-          "./provider-panel-shared.js": 1,
           "./use-oauth-login-flow.js": 1
         }
       },
@@ -4278,6 +4231,7 @@
           "../../preload/bridge-contract.js": 1,
           "../../shared/settings-ownership.js": 1,
           "../browser-storage": 1,
+          "../features/connection-settings": 1,
           "../locales/settings-navigation-copy.js": 1,
           "../locales/settings-shared-copy.js": 1,
           "./about-settings-page": 1,
@@ -4293,7 +4247,6 @@
           "./projects-settings-page": 1,
           "./providers-panel": 1,
           "./runtime-host-interaction-boundary.js": 1,
-          "./runtime-host-settings-bridge.js": 1,
           "./runtime-host-settings-target.js": 1,
           "./settings-error-copy": 1,
           "./settings-nav": 1,
@@ -4492,10 +4445,9 @@
         "actionFactories": [],
         "dependencyPaths": {
           "../../preload/bridge-contract.js": 1,
-          "../locales/settings-provider-copy": 1,
+          "../features/connection-settings": 1,
           "./connection-name-draft.js": 1,
           "./provider-connection-status": 1,
-          "./provider-panel-shared": 1,
           "./relay-profile-draft": 1,
           "./relay-thinking-bulk": 1,
           "./runtime-host-settings-bridge.js": 1,
@@ -4574,7 +4526,7 @@
         "unresolvedDependencies": 0,
         "actionFactories": [],
         "dependencyPaths": {
-          "../locales/settings-provider-copy": 1,
+          "../features/connection-settings": 1,
           "./oauth-login-flow-guard": 1,
           "./runtime-host-settings-target.js": 1,
           "@maka/core/redaction": 1,
diff --git a/apps/desktop/src/main/__tests__/provider-add-submission.test.ts b/apps/desktop/src/main/__tests__/provider-add-submission.test.ts
index 81c0f2a611..6e5432cc48 100644
--- a/apps/desktop/src/main/__tests__/provider-add-submission.test.ts
+++ b/apps/desktop/src/main/__tests__/provider-add-submission.test.ts
@@ -18,13 +18,24 @@
  */
 
 import assert from 'node:assert/strict';
-import { test } from 'node:test';
+import { afterEach, test } from 'node:test';
+import { act, createElement } from 'react';
 import {
   createProviderWithDiscovery,
+  apiKeyOnboardingRoute,
+  initialOnboardingModelIds,
+  shouldShowManagedOnboardingOutcomeUnknown,
+  stableOnboardingModels,
   validateAddProviderDraft,
   type AddProviderDraft,
   type AddProviderField,
 } from '../../renderer/settings/provider-add-submission.js';
+import { createDesktopConnectionSettingsServices } from '../../renderer/platform/desktop/create-connection-settings-services.js';
+import { cleanupFakeDom, installReactRenderer } from './fake-dom.js';
+import {
+  ConnectionSaveUncertaintyObserver,
+  type ApiKeyOnboardingBridge,
+} from '../../renderer/features/connection-settings/index.js';
 import {
   PROVIDER_REGISTRY,
   providerSupportsModelDiscovery,
@@ -39,6 +50,23 @@ type NoModelRule = 'defaultModel' extends AddProviderField ? never : true;
 const _fieldGateHasNoModelRule: NoModelRule = true;
 void _fieldGateHasNoModelRule;
 
+afterEach(cleanupFakeDom);
+
+function deferred<T>() {
+  let resolve!: (value: T) => void;
+  const promise = new Promise<T>((accept) => {
+    resolve = accept;
+  });
+  return { promise, resolve };
+}
+
+const onboardingSaveInput: Parameters<ApiKeyOnboardingBridge['save']>[0] = {
+  target: { kind: 'create', providerType: 'openai' },
+  apiKey: 'test-key',
+  baseUrl: null,
+  enabledModelIds: ['gpt-5'],
+};
+
 const RELAY_TYPES: readonly ProviderType[] = ['openai-compatible', 'openai-responses-compatible'];
 
 function draft(over: Partial<AddProviderDraft> = {}): AddProviderDraft {
@@ -223,3 +251,191 @@ test('a duplicate slug outranks a missing key, so one fix is asked for at a time
     { field: 'slug', reason: 'duplicate' },
   );
 });
+
+test('routes only fixed-endpoint API-key drafts without request customization to Host onboarding', () => {
+  assert.deepEqual(apiKeyOnboardingRoute({
+    providerType: 'openai',
+    requestHeaderCount: 0,
+    hasRequestBodyOverlay: false,
+  }), { kind: 'host' });
+  assert.deepEqual(apiKeyOnboardingRoute({
+    providerType: 'openai',
+    requestHeaderCount: 1,
+    hasRequestBodyOverlay: false,
+  }), { kind: 'legacy', reason: 'request_headers' });
+  assert.deepEqual(apiKeyOnboardingRoute({
+    providerType: 'openai',
+    requestHeaderCount: 0,
+    hasRequestBodyOverlay: true,
+  }), { kind: 'legacy', reason: 'request_body' });
+  assert.deepEqual(apiKeyOnboardingRoute({
+    providerType: 'openai-compatible',
+    requestHeaderCount: 0,
+    hasRequestBodyOverlay: false,
+  }), { kind: 'legacy', reason: 'custom_endpoint' });
+  assert.deepEqual(apiKeyOnboardingRoute({
+    providerType: 'cloudflare-workers-ai',
+    requestHeaderCount: 0,
+    hasRequestBodyOverlay: false,
+  }), { kind: 'legacy', reason: 'cloudflare' });
+});
+
+test('uses a stable discovered-model order and prefers the registered recommendation', () => {
+  const models = [
+    { id: 'z-model', displayName: 'Zulu' },
+    { id: 'a-model', displayName: 'Alpha' },
+  ];
+  assert.deepEqual(stableOnboardingModels(models).map(({ id }) => id), [
+    'a-model',
+    'z-model',
+  ]);
+  assert.deepEqual(initialOnboardingModelIds(models, 'z-model'), ['z-model']);
+  assert.deepEqual(initialOnboardingModelIds(models, 'missing'), ['a-model']);
+  assert.deepEqual(initialOnboardingModelIds([], 'missing'), []);
+});
+
+test('only an idle form projects a dispatched save as outcome unknown', () => {
+  assert.equal(shouldShowManagedOnboardingOutcomeUnknown(false, false), false);
+  assert.equal(shouldShowManagedOnboardingOutcomeUnknown(true, true), false);
+  assert.equal(shouldShowManagedOnboardingOutcomeUnknown(true, false), true);
+});
+
+test('a late old save cannot clear the uncertainty lease for a newer attempt', async () => {
+  const saves = [
+    deferred<{ kind: 'not_saved' }>(),
+    deferred<{ kind: 'not_saved' }>(),
+  ];
+  let saveIndex = 0;
+  const services = createDesktopConnectionSettingsServices(() => ({
+    connections: {
+      saveOnboarding: () => saves[saveIndex++]!.promise,
+    },
+  } as unknown as ReturnType<NonNullable<Parameters<typeof createDesktopConnectionSettingsServices>[0]>>));
+  const firstView = services.forHost({
+    profileId: 'local',
+    hostId: 'same-host',
+  }).apiKeyOnboarding;
+  const oldSave = firstView.save(onboardingSaveInput);
+  firstView.saveUncertainty.restart();
+
+  const replacementView = services.forHost({
+    profileId: 'local',
+    hostId: 'same-host',
+  }).apiKeyOnboarding;
+  const newSave = replacementView.save(onboardingSaveInput);
+  saves[0]!.resolve({ kind: 'not_saved' });
+  await oldSave;
+  assert.equal(replacementView.saveUncertainty.getSnapshot(), true);
+
+  saves[1]!.resolve({ kind: 'not_saved' });
+  await newSave;
+  assert.equal(firstView.saveUncertainty.getSnapshot(), false);
+});
+
+test('save uncertainty is isolated between Runtime Host targets', async () => {
+  const pending = deferred<{ kind: 'outcome_unknown' }>();
+  const services = createDesktopConnectionSettingsServices(() => ({
+    connections: { saveOnboarding: () => pending.promise },
+  } as unknown as ReturnType<NonNullable<Parameters<typeof createDesktopConnectionSettingsServices>[0]>>));
+  const firstHost = services.forHost({ profileId: 'local', hostId: 'host-a' }).apiKeyOnboarding;
+  const secondHost = services.forHost({ profileId: 'local', hostId: 'host-b' }).apiKeyOnboarding;
+
+  const save = firstHost.save(onboardingSaveInput);
+
+  assert.equal(firstHost.saveUncertainty.getSnapshot(), true);
+  assert.equal(secondHost.saveUncertainty.getSnapshot(), false);
+  pending.resolve({ kind: 'outcome_unknown' });
+  await save;
+});
+
+test('a definitive save settlement reaches a generation-remounted React consumer', async () => {
+  const pending = deferred<{ kind: 'not_saved' }>();
+  const services = createDesktopConnectionSettingsServices(() => ({
+    connections: { saveOnboarding: () => pending.promise },
+  } as unknown as ReturnType<NonNullable<Parameters<typeof createDesktopConnectionSettingsServices>[0]>>));
+  const onboarding = services.forHost({
+    profileId: 'local',
+    hostId: 'same-host',
+  }).apiKeyOnboarding;
+  const { root } = installReactRenderer();
+  let showsUnknown = false;
+
+  function Probe(props: {
+    uncertainty: ApiKeyOnboardingBridge['saveUncertainty'];
+    busy: boolean;
+  }) {
+    return createElement(
+      ConnectionSaveUncertaintyObserver,
+      {
+        store: props.uncertainty,
+        children: (uncertain: boolean) => {
+          showsUnknown = shouldShowManagedOnboardingOutcomeUnknown(uncertain, props.busy);
+          return null;
+        },
+      },
+    );
+  }
+
+  let save!: ReturnType<ApiKeyOnboardingBridge['save']>;
+  await act(async () => {
+    root.render(createElement(Probe, {
+      key: 'generation-a',
+      uncertainty: onboarding.saveUncertainty,
+      busy: true,
+    }));
+    save = onboarding.save(onboardingSaveInput);
+  });
+  assert.equal(onboarding.saveUncertainty.getSnapshot(), true);
+  assert.equal(showsUnknown, false, 'the form that owns the pending save keeps showing progress');
+
+  await act(async () => {
+    root.render(createElement(Probe, {
+      key: 'generation-b',
+      uncertainty: onboarding.saveUncertainty,
+      busy: false,
+    }));
+  });
+  assert.equal(showsUnknown, true, 'a generation-remounted form fails closed');
+
+  await act(async () => {
+    pending.resolve({ kind: 'not_saved' });
+    await save;
+  });
+  assert.equal(onboarding.saveUncertainty.getSnapshot(), false);
+  assert.equal(showsUnknown, false, 'definitive settlement actively restores the remounted form');
+});
+
+test('onboarding transport forwards the selected Runtime Host target', async () => {
+  const calls: unknown[] = [];
+  const services = createDesktopConnectionSettingsServices(() => ({
+    connections: {
+      verifyOnboarding: async (_input: unknown, host: unknown) => {
+        calls.push(['verify', host]);
+        return { kind: 'rejected', reason: 'provider_unsupported' } as const;
+      },
+      saveOnboarding: async (_input: unknown, host: unknown) => {
+        calls.push(['save', host]);
+        return { kind: 'rejected', reason: 'provider_unsupported' } as const;
+      },
+    },
+  } as unknown as ReturnType<NonNullable<Parameters<typeof createDesktopConnectionSettingsServices>[0]>>));
+  const host = { profileId: 'remote', hostId: 'host-remote' };
+  const onboarding = services.forHost(host).apiKeyOnboarding;
+
+  await onboarding.verify({
+    target: { kind: 'create', providerType: 'openai' },
+    apiKey: 'test-key',
+    baseUrl: null,
+  });
+  await onboarding.save({
+    target: { kind: 'create', providerType: 'openai' },
+    apiKey: 'test-key',
+    baseUrl: null,
+    enabledModelIds: ['gpt-5'],
+  });
+
+  assert.deepEqual(calls, [
+    ['verify', host],
+    ['save', host],
+  ]);
+});
diff --git a/apps/desktop/src/main/__tests__/runtime-host-connections-ipc-main.test.ts b/apps/desktop/src/main/__tests__/runtime-host-connections-ipc-main.test.ts
index 6ef207b31c..8f62b36c3d 100644
--- a/apps/desktop/src/main/__tests__/runtime-host-connections-ipc-main.test.ts
+++ b/apps/desktop/src/main/__tests__/runtime-host-connections-ipc-main.test.ts
@@ -24,6 +24,10 @@ import type {
   RuntimeHostConnectionCatalogEntry as ConnectionCatalogEntry,
   RuntimeHostConnectionCatalogSnapshot as ConnectionCatalogSnapshot,
 } from '@maka/runtime-host/client';
+import {
+  RuntimeHostOperationError,
+  RuntimeHostRequestInterruptedError,
+} from '@maka/runtime-host/client';
 import {
   projectHostConnections,
   projectHostConnectionTest,
@@ -75,9 +79,192 @@ test('registers pure Connection reads for replacement-Host retry', () => {
     'connections:hasSecret',
   ]);
   assert.ok(effects.has('connections:create'));
+  assert.ok(effects.has('connections:onboardingVerify'));
+  assert.ok(effects.has('connections:onboardingSave'));
   assert.ok(effects.has('connections:test'));
 });
 
+test('forwards managed onboarding and emits only after a canonical save', async () => {
+  const handlers = new Map<string, (...args: unknown[]) => unknown>();
+  const calls: unknown[] = [];
+  let changed = 0;
+  registerRuntimeHostConnectionsIpc({
+    ipcMain: {
+      handle: (channel, handler) => {
+        handlers.set(channel, handler as (...args: unknown[]) => unknown);
+      },
+    },
+    client: {
+      verifyConnectionOnboarding: async (input: unknown) => {
+        calls.push(['verify', input]);
+        return { kind: 'verified', models: [{ id: 'gpt-5' }] };
+      },
+      saveConnectionOnboarding: async (input: unknown) => {
+        calls.push(['save', input]);
+        return {
+          kind: 'saved',
+          connection: {
+            connectionId: 'connection-openai-2',
+            revision: 1,
+            slug: 'openai-2',
+            providerType: 'openai',
+          },
+        };
+      },
+    } as never,
+    emitConnectionListChanged() {
+      changed += 1;
+    },
+  });
+
+  const base = {
+    target: { kind: 'create', providerType: 'openai' },
+    apiKey: 'test-key',
+    baseUrl: null,
+  } as const;
+  assert.deepEqual(await handlers.get('connections:onboardingVerify')?.({}, base), {
+    kind: 'verified',
+    models: [{ id: 'gpt-5' }],
+  });
+  assert.deepEqual(
+    await handlers.get('connections:onboardingSave')?.({}, {
+      ...base,
+      enabledModelIds: ['gpt-5'],
+    }),
+    {
+      kind: 'result',
+      result: {
+        kind: 'saved',
+        connection: {
+          connectionId: 'connection-openai-2',
+          revision: 1,
+          slug: 'openai-2',
+          providerType: 'openai',
+        },
+      },
+    },
+  );
+  assert.equal(changed, 1);
+  assert.equal(calls.length, 2);
+});
+
+test('keeps a dispatched onboarding save interruption outcome unknown', async () => {
+  const handlers = new Map<string, (...args: unknown[]) => unknown>();
+  registerRuntimeHostConnectionsIpc({
+    ipcMain: {
+      handle: (channel, handler) => {
+        handlers.set(channel, handler as (...args: unknown[]) => unknown);
+      },
+    },
+    client: {
+      saveConnectionOnboarding: async () => {
+        throw new RuntimeHostRequestInterruptedError(
+          'connection.onboarding.save',
+          'command',
+          'dispatched',
+          'connection_lost',
+        );
+      },
+    } as never,
+    emitConnectionListChanged() {},
+  });
+
+  assert.deepEqual(
+    await handlers.get('connections:onboardingSave')?.({}, {
+      target: { kind: 'create', providerType: 'openai' },
+      apiKey: 'test-key',
+      baseUrl: null,
+      enabledModelIds: ['gpt-5'],
+    }),
+    { kind: 'outcome_unknown' },
+  );
+});
+
+test('keeps Host commit_outcome_unknown distinct from a safe non-save', async () => {
+  const handlers = new Map<string, (...args: unknown[]) => unknown>();
+  let attempt = 0;
+  registerRuntimeHostConnectionsIpc({
+    ipcMain: {
+      handle: (channel, handler) => {
+        handlers.set(channel, handler as (...args: unknown[]) => unknown);
+      },
+    },
+    client: {
+      saveConnectionOnboarding: async () => {
+        attempt += 1;
+        if (attempt === 1) {
+          throw new RuntimeHostOperationError(
+            'connection.onboarding.save',
+            'commit_outcome_unknown',
+            'unknown',
+          );
+        }
+        throw new RuntimeHostRequestInterruptedError(
+          'connection.onboarding.save',
+          'command',
+          'not_dispatched',
+          'connection_lost',
+        );
+      },
+    } as never,
+    emitConnectionListChanged() {},
+  });
+  const input = {
+    target: { kind: 'create', providerType: 'openai' },
+    apiKey: 'test-key',
+    baseUrl: null,
+    enabledModelIds: ['gpt-5'],
+  };
+
+  assert.deepEqual(await handlers.get('connections:onboardingSave')?.({}, input), {
+    kind: 'outcome_unknown',
+  });
+  assert.deepEqual(await handlers.get('connections:onboardingSave')?.({}, input), {
+    kind: 'not_saved',
+  });
+});
+
+test('fails closed when a dispatched onboarding response cannot be decoded', async () => {
+  const handlers = new Map<string, (...args: unknown[]) => unknown>();
+  let attempt = 0;
+  registerRuntimeHostConnectionsIpc({
+    ipcMain: {
+      handle: (channel, handler) => {
+        handlers.set(channel, handler as (...args: unknown[]) => unknown);
+      },
+    },
+    client: {
+      saveConnectionOnboarding: async () => {
+        attempt += 1;
+        if (attempt === 1) {
+          // RuntimeHostConnection surfaces a malformed command response as
+          // an ordinary protocol error after the frame was dispatched.
+          throw new Error('Invalid connection onboarding save result');
+        }
+        throw new RuntimeHostOperationError(
+          'connection.onboarding.save',
+          'invalid_request',
+          'The Host definitively rejected the save',
+        );
+      },
+    } as never,
+    emitConnectionListChanged() {},
+  });
+  const input = {
+    target: { kind: 'create', providerType: 'openai' },
+    apiKey: 'test-key',
+    baseUrl: null,
+    enabledModelIds: ['gpt-5'],
+  };
+
+  assert.deepEqual(await handlers.get('connections:onboardingSave')?.({}, input), {
+    kind: 'outcome_unknown',
+  });
+  assert.deepEqual(await handlers.get('connections:onboardingSave')?.({}, input), {
+    kind: 'not_saved',
+  });
+});
+
 test('retries connection delete after a stale revision instead of failing permanently', async () => {
   const handlers = new Map<string, (...args: unknown[]) => unknown>();
   let revision = 1;
diff --git a/apps/desktop/src/main/runtime-host-client.ts b/apps/desktop/src/main/runtime-host-client.ts
index ca3a5d6b6b..6a202ce727 100644
--- a/apps/desktop/src/main/runtime-host-client.ts
+++ b/apps/desktop/src/main/runtime-host-client.ts
@@ -496,6 +496,18 @@ export class DesktopRuntimeHostClient {
     });
   }
 
+  verifyConnectionOnboarding(
+    input: OperationInput<"connection.onboarding.verify">,
+  ): Promise<OperationOutput<"connection.onboarding.verify">> {
+    return this.request("connection.onboarding.verify", input);
+  }
+
+  saveConnectionOnboarding(
+    input: OperationInput<"connection.onboarding.save">,
+  ): Promise<OperationOutput<"connection.onboarding.save">> {
+    return this.request("connection.onboarding.save", input);
+  }
+
   startOAuthLogin(
     attemptId: string,
     target: OperationInput<"oauth.login.start">["target"],
diff --git a/apps/desktop/src/main/runtime-host-connections-ipc-main.ts b/apps/desktop/src/main/runtime-host-connections-ipc-main.ts
index df1e8c5696..deeb731da8 100644
--- a/apps/desktop/src/main/runtime-host-connections-ipc-main.ts
+++ b/apps/desktop/src/main/runtime-host-connections-ipc-main.ts
@@ -40,7 +40,14 @@ import type {
   RuntimeHostConnectionCatalogSnapshot as ConnectionCatalogSnapshot,
 } from '@maka/runtime-host/client';
 import { normalizeRequestHeaderUpdates } from '@maka/core/runtime-policy';
-import type { ConnectionTestRunResult } from '@maka/runtime-host/protocol';
+import {
+  CONNECTION_EFFECT_OPERATION_SPECS,
+  type ConnectionTestRunResult,
+} from '@maka/runtime-host/protocol';
+import {
+  RuntimeHostOperationError,
+  RuntimeHostRequestInterruptedError,
+} from '@maka/runtime-host/client';
 import type { DesktopRuntimeHostClient } from './runtime-host-client.js';
 import {
   handleReconnectableRead,
@@ -56,6 +63,7 @@ import type {
   DesktopConnectionIdentity,
   DesktopConnectionSnapshot,
 } from '../shared/desktop-connection-snapshot.js';
+import type { DesktopConnectionOnboardingSaveOutcome } from '../preload/bridge-contract.js';
 
 type HostConnectionsClient = Pick<
   DesktopRuntimeHostClient,
@@ -71,6 +79,8 @@ type HostConnectionsClient = Pick<
   | 'setDefaultConnectionTarget'
   | 'testConnection'
   | 'updateConnection'
+  | 'verifyConnectionOnboarding'
+  | 'saveConnectionOnboarding'
 >;
 
 export interface RuntimeHostConnectionsIpcDeps {
@@ -156,6 +166,37 @@ export function registerRuntimeHostConnectionsIpc(
     );
     deps.emitConnectionListChanged();
   });
+  deps.ipcMain.handle('connections:onboardingVerify', async (_event, raw: unknown) => {
+    const input = CONNECTION_EFFECT_OPERATION_SPECS[
+      'connection.onboarding.verify'
+    ].decodeInput(raw);
+    return deps.client.verifyConnectionOnboarding(input);
+  });
+  deps.ipcMain.handle('connections:onboardingSave', async (_event, raw: unknown) => {
+    const input = CONNECTION_EFFECT_OPERATION_SPECS[
+      'connection.onboarding.save'
+    ].decodeInput(raw);
+    try {
+      const result = await deps.client.saveConnectionOnboarding(input);
+      if (result.kind === 'saved') deps.emitConnectionListChanged();
+      return { kind: 'result', result } satisfies DesktopConnectionOnboardingSaveOutcome;
+    } catch (error) {
+      if (error instanceof RuntimeHostOperationError) {
+        return {
+          kind: error.code === 'commit_outcome_unknown' ? 'outcome_unknown' : 'not_saved',
+        } satisfies DesktopConnectionOnboardingSaveOutcome;
+      }
+      if (error instanceof RuntimeHostRequestInterruptedError) {
+        return {
+          kind: error.dispatch === 'not_dispatched' ? 'not_saved' : 'outcome_unknown',
+        } satisfies DesktopConnectionOnboardingSaveOutcome;
+      }
+      // A protocol/decode failure can arrive only after the command response
+      // has started coming back. Without affirmative evidence that the Host
+      // did not commit, allowing another create risks duplicating the account.
+      return { kind: 'outcome_unknown' } satisfies DesktopConnectionOnboardingSaveOutcome;
+    }
+  });
   deps.ipcMain.handle('connections:create', async (_event, raw: unknown) => {
     const input = normalizeCreateInput(raw);
     const catalog = await snapshot();
diff --git a/apps/desktop/src/preload/bridge-contract.d.ts b/apps/desktop/src/preload/bridge-contract.d.ts
index 95f2c2a3bf..2bc4ce1aad 100644
--- a/apps/desktop/src/preload/bridge-contract.d.ts
+++ b/apps/desktop/src/preload/bridge-contract.d.ts
@@ -354,6 +354,14 @@ export interface DesktopRuntimeHostRef {
   readonly hostId: string;
 }
 
+export type DesktopConnectionOnboardingSaveOutcome =
+  | {
+      readonly kind: 'result';
+      readonly result: OperationOutput<'connection.onboarding.save'>;
+    }
+  | { readonly kind: 'not_saved' }
+  | { readonly kind: 'outcome_unknown' };
+
 /** Desktop-local OAuth intent. Runtime Host remains the identity allocator. */
 export type DesktopOAuthLoginTarget =
   | { readonly kind: 'create' }
@@ -1360,6 +1368,14 @@ export interface MakaBridge {
     setDefault(connection: import('../shared/desktop-connection-snapshot').DesktopConnectionIdentity | string | null, host?: DesktopRuntimeHostRef): Promise<void>;
     setDefaultModel(input: { slug: string; model: string } | null, host?: DesktopRuntimeHostRef): Promise<void>;
     create(input: CreateConnectionInput, host?: DesktopRuntimeHostRef): Promise<import('@maka/core/llm-connections').IdentifiedLlmConnection>;
+    verifyOnboarding(
+      input: OperationInput<'connection.onboarding.verify'>,
+      host?: DesktopRuntimeHostRef,
+    ): Promise<OperationOutput<'connection.onboarding.verify'>>;
+    saveOnboarding(
+      input: OperationInput<'connection.onboarding.save'>,
+      host?: DesktopRuntimeHostRef,
+    ): Promise<DesktopConnectionOnboardingSaveOutcome>;
     update(connection: import('../shared/desktop-connection-snapshot').DesktopConnectionIdentity, patch: UpdateConnectionInput, host?: DesktopRuntimeHostRef): Promise<LlmConnection>;
     delete(connection: import('../shared/desktop-connection-snapshot').DesktopConnectionIdentity, host?: DesktopRuntimeHostRef): Promise<void>;
     test(connection: import('../shared/desktop-connection-snapshot').DesktopConnectionIdentity | string, opts?: { model?: string }, host?: DesktopRuntimeHostRef): Promise<ConnectionTestResult>;
diff --git a/apps/desktop/src/preload/preload.ts b/apps/desktop/src/preload/preload.ts
index 226ce44a52..c3a7726846 100644
--- a/apps/desktop/src/preload/preload.ts
+++ b/apps/desktop/src/preload/preload.ts
@@ -2626,6 +2626,12 @@ const makaBridge = {
     create(input: CreateConnectionInput, host?: DesktopRuntimeHostRef): Promise<import('@maka/core/llm-connections').IdentifiedLlmConnection> {
       return invokeSelectedRuntimeHost(host, 'connections:create', input);
     },
+    verifyOnboarding(input, host) {
+      return invokeSelectedRuntimeHost(host, 'connections:onboardingVerify', input);
+    },
+    saveOnboarding(input, host) {
+      return invokeSelectedRuntimeHost(host, 'connections:onboardingSave', input);
+    },
     update(connection: import('../shared/desktop-connection-snapshot.js').DesktopConnectionIdentity, patch: UpdateConnectionInput, host?: DesktopRuntimeHostRef): Promise<LlmConnection> {
       return invokeSelectedRuntimeHost(host, 'connections:update', connection, patch);
     },
diff --git a/apps/desktop/src/renderer/application/contracts/connection-error-cleaner.ts b/apps/desktop/src/renderer/application/contracts/connection-error-cleaner.ts
new file mode 100644
index 0000000000..03672fa0d0
--- /dev/null
+++ b/apps/desktop/src/renderer/application/contracts/connection-error-cleaner.ts
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { parseNoRealConnectionError } from '@maka/core/connection-error-copy';
+
+export { parseNoRealConnectionError };
+
+export const NO_REAL_CONNECTION_CODE = 'NO_REAL_CONNECTION';
+const NO_REAL_CONNECTION_REASON_RE = /NO_REAL_CONNECTION:([a-z_]+): /;
+
+export function cleanErrorMessage(error: unknown): string {
+  const raw = error instanceof Error ? error.message : String(error);
+  return cleanEventMessage(raw);
+}
+
+function cleanEventMessage(message: string): string {
+  return message
+    .replace(/^Error invoking remote method '[^']+': (?:[A-Za-z_$][\w$]*)?Error: /, '')
+    .replace(NO_REAL_CONNECTION_REASON_RE, '')
+    .replace(`${NO_REAL_CONNECTION_CODE}: `, '');
+}
diff --git a/apps/desktop/src/renderer/composition/desktop-feature-services.tsx b/apps/desktop/src/renderer/composition/desktop-feature-services.tsx
index 47c188d6e6..1e968c0880 100644
--- a/apps/desktop/src/renderer/composition/desktop-feature-services.tsx
+++ b/apps/desktop/src/renderer/composition/desktop-feature-services.tsx
@@ -18,6 +18,7 @@
  */
 
 import type { ReactNode } from 'react';
+import { ConnectionSettingsServicesProvider } from '../features/connection-settings';
 import { GoalServicesProvider } from '../features/goals';
 import { ModuleHubServicesProvider } from '../features/module-hub';
 import { RuntimeHostManagementServicesProvider } from '../features/runtime-host-management';
@@ -26,6 +27,7 @@ import { SessionNavigationServicesProvider } from '../features/session-navigatio
 import { TaskEntryServicesProvider } from '../features/task-entry';
 import { WorkbarServicesProvider } from '../features/workbar';
 import { createDesktopGoalServices } from '../platform/desktop/create-goal-services';
+import { createDesktopConnectionSettingsServices } from '../platform/desktop/create-connection-settings-services';
 import { createDesktopModuleHubServices } from '../platform/desktop/create-module-hub-services';
 import { createDesktopRuntimeHostManagementServices } from '../platform/desktop/create-runtime-host-management-services';
 import { createDesktopSessionCollaborationServices } from '../platform/desktop/create-session-collaboration-services';
@@ -35,6 +37,7 @@ import { createDesktopWorkbarServices } from '../platform/desktop/create-workbar
 
 export function createDesktopFeatureServices() {
   return {
+    connectionSettings: createDesktopConnectionSettingsServices(),
     goal: createDesktopGoalServices(),
     moduleHub: createDesktopModuleHubServices(),
     runtimeHostManagement: createDesktopRuntimeHostManagementServices(),
@@ -50,8 +53,9 @@ export function DesktopFeatureServicesProvider(props: {
   readonly children?: ReactNode;
 }) {
   return (
-    <RuntimeHostManagementServicesProvider services={props.services.runtimeHostManagement}>
-      <SessionCollaborationServicesProvider services={props.services.sessionCollaboration}>
+    <ConnectionSettingsServicesProvider services={props.services.connectionSettings}>
+      <RuntimeHostManagementServicesProvider services={props.services.runtimeHostManagement}>
+        <SessionCollaborationServicesProvider services={props.services.sessionCollaboration}>
         <SessionNavigationServicesProvider services={props.services.sessionNavigation}>
           <TaskEntryServicesProvider services={props.services.taskEntry}>
             <ModuleHubServicesProvider services={props.services.moduleHub}>
@@ -63,7 +67,8 @@ export function DesktopFeatureServicesProvider(props: {
             </ModuleHubServicesProvider>
           </TaskEntryServicesProvider>
         </SessionNavigationServicesProvider>
-      </SessionCollaborationServicesProvider>
-    </RuntimeHostManagementServicesProvider>
+        </SessionCollaborationServicesProvider>
+      </RuntimeHostManagementServicesProvider>
+    </ConnectionSettingsServicesProvider>
   );
 }
diff --git a/apps/desktop/src/renderer/features/connection-settings/index.ts b/apps/desktop/src/renderer/features/connection-settings/index.ts
new file mode 100644
index 0000000000..c323608c01
--- /dev/null
+++ b/apps/desktop/src/renderer/features/connection-settings/index.ts
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export {
+  ConnectionSettingsServicesProvider,
+  ConnectionSettingsServicesConsumer,
+  ConnectionSaveUncertaintyObserver,
+} from './services-context.js';
+export type {
+  ApiKeyOnboardingBridge,
+  ConnectionSettingsServices,
+  ConnectionsBridge,
+  DesktopConnectionOnboardingIdentity,
+  RuntimeHostSettingsConnectionsBridge,
+} from './ports.js';
+export {
+  categoryLabel,
+  connectionLastTestMessageDisplay,
+  connectionTestFailureMessage,
+  providerPanelActionErrorMessage,
+} from './provider-panel-shared.js';
+export { getProviderSettingsCopy } from './settings-provider-copy.js';
+export type { ProviderSettingsCopy } from './settings-provider-copy.js';
+export type {
+  CredentialPresenceStatus,
+} from './provider-panel-shared.js';
diff --git a/apps/desktop/src/renderer/features/connection-settings/ports.ts b/apps/desktop/src/renderer/features/connection-settings/ports.ts
new file mode 100644
index 0000000000..d6980a953b
--- /dev/null
+++ b/apps/desktop/src/renderer/features/connection-settings/ports.ts
@@ -0,0 +1,93 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import type {
+  ConnectionTestResult,
+  CreateConnectionInput,
+  IdentifiedLlmConnection,
+  LlmConnection,
+  ModelDiscoveryResult,
+  RequestHeaderUpdate,
+  SavedRequestHeaders,
+  UpdateConnectionInput,
+} from '@maka/core/llm-connections';
+import type {
+  ConnectionOnboardingSaveInput,
+  ConnectionOnboardingSaveResult,
+  ConnectionOnboardingVerifyInput,
+  ConnectionOnboardingVerifyResult,
+} from '@maka/runtime-host/protocol';
+import type {
+  DesktopConnectionIdentity,
+  DesktopConnectionSnapshot,
+} from '../../../shared/desktop-connection-snapshot.js';
+export interface ConnectionSettingsHost {
+  readonly profileId: string;
+  readonly hostId: string;
+}
+
+export type DesktopConnectionOnboardingIdentity = Extract<
+  ConnectionOnboardingSaveResult,
+  { readonly kind: 'saved' }
+>['connection'];
+
+export interface ConnectionsBridge {
+  getSnapshot(): Promise<DesktopConnectionSnapshot>;
+  setDefault(connection: DesktopConnectionIdentity | null): Promise<void>;
+  create(input: CreateConnectionInput): Promise<IdentifiedLlmConnection>;
+  update(connection: DesktopConnectionIdentity, patch: UpdateConnectionInput): Promise<LlmConnection>;
+  delete(connection: DesktopConnectionIdentity): Promise<void>;
+  test(connection: DesktopConnectionIdentity, opts?: { model?: string }): Promise<ConnectionTestResult>;
+  fetchModels(connection: DesktopConnectionIdentity): Promise<
+    Pick<ModelDiscoveryResult, 'models' | 'source'>
+  >;
+  hasSecret(connection: DesktopConnectionIdentity): Promise<boolean>;
+  getRequestHeaders(connection: DesktopConnectionIdentity): Promise<SavedRequestHeaders>;
+  setRequestHeaders(
+    connection: DesktopConnectionIdentity,
+    headers: readonly RequestHeaderUpdate[],
+  ): Promise<SavedRequestHeaders>;
+  subscribeEvents?(handler: () => void): () => void;
+}
+
+export interface RuntimeHostSettingsConnectionsBridge extends ConnectionsBridge {
+  setDefaultModel(input: { slug: string; model: string } | null): Promise<void>;
+  subscribeEvents(handler: () => void): () => void;
+}
+
+export interface ApiKeyOnboardingBridge {
+  readonly saveUncertainty: {
+    getSnapshot(): boolean;
+    subscribe(listener: () => void): () => void;
+    restart(): void;
+  };
+  verify(input: ConnectionOnboardingVerifyInput): Promise<ConnectionOnboardingVerifyResult>;
+  save(input: ConnectionOnboardingSaveInput): Promise<
+    | { readonly kind: 'result'; readonly result: ConnectionOnboardingSaveResult }
+    | { readonly kind: 'not_saved' }
+    | { readonly kind: 'outcome_unknown' }
+  >;
+}
+
+export interface ConnectionSettingsServices {
+  forHost(host: ConnectionSettingsHost): {
+    readonly connections: RuntimeHostSettingsConnectionsBridge;
+    readonly apiKeyOnboarding: ApiKeyOnboardingBridge;
+  };
+}
diff --git a/apps/desktop/src/renderer/settings/provider-panel-shared.ts b/apps/desktop/src/renderer/features/connection-settings/provider-panel-shared.ts
similarity index 74%
rename from apps/desktop/src/renderer/settings/provider-panel-shared.ts
rename to apps/desktop/src/renderer/features/connection-settings/provider-panel-shared.ts
index 2f1c40df3f..92dc3a9231 100644
--- a/apps/desktop/src/renderer/settings/provider-panel-shared.ts
+++ b/apps/desktop/src/renderer/features/connection-settings/provider-panel-shared.ts
@@ -20,46 +20,11 @@
 import { generalizedErrorMessage, generalizedErrorMessageChinese, redactSecrets } from '@maka/core/redaction';
 import {
   type ConnectionTestResult,
-  type CreateConnectionInput,
-  type IdentifiedLlmConnection,
-  type LlmConnection,
-  type ModelDiscoveryResult,
-  type RequestHeaderUpdate,
-  type SavedRequestHeaders,
   type ProviderCategory,
-  type ProviderType,
-  type UpdateConnectionInput,
 } from '@maka/core/llm-connections';
 import { type UiLocale } from '@maka/core/ui-locale';
-import { getProviderSettingsCopy } from '../locales/settings-provider-copy.js';
-import { cleanErrorMessage } from '../model-connection-errors.js';
-import type {
-  DesktopConnectionIdentity,
-  DesktopConnectionSnapshot,
-} from '../../shared/desktop-connection-snapshot.js';
-
-export interface ConnectionsBridge {
-  getSnapshot(): Promise<DesktopConnectionSnapshot>;
-  setDefault(connection: DesktopConnectionIdentity | null): Promise<void>;
-  create(input: CreateConnectionInput): Promise<IdentifiedLlmConnection>;
-  update(connection: DesktopConnectionIdentity, patch: UpdateConnectionInput): Promise<LlmConnection>;
-  delete(connection: DesktopConnectionIdentity): Promise<void>;
-  test(connection: DesktopConnectionIdentity, opts?: { model?: string }): Promise<ConnectionTestResult>;
-  /**
-   * What the discovery run found, minus when it ran: the Host records that
-   * timestamp for its own invalidation, and no surface here shows it.
-   */
-  fetchModels(
-    connection: DesktopConnectionIdentity,
-  ): Promise<Pick<ModelDiscoveryResult, 'models' | 'source'>>;
-  hasSecret(connection: DesktopConnectionIdentity): Promise<boolean>;
-  getRequestHeaders(connection: DesktopConnectionIdentity): Promise<SavedRequestHeaders>;
-  setRequestHeaders(
-    connection: DesktopConnectionIdentity,
-    headers: readonly RequestHeaderUpdate[],
-  ): Promise<SavedRequestHeaders>;
-  subscribeEvents?(handler: () => void): () => void;
-}
+import { getProviderSettingsCopy } from './settings-provider-copy.js';
+import { cleanErrorMessage } from '../../application/contracts/connection-error-cleaner.js';
 
 export type CredentialPresenceStatus = boolean | 'loading' | 'error';
 
diff --git a/apps/desktop/src/renderer/features/connection-settings/services-context.tsx b/apps/desktop/src/renderer/features/connection-settings/services-context.tsx
new file mode 100644
index 0000000000..695fae2dbd
--- /dev/null
+++ b/apps/desktop/src/renderer/features/connection-settings/services-context.tsx
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { createContext, useContext, useSyncExternalStore, type ReactNode } from 'react';
+import type { ApiKeyOnboardingBridge, ConnectionSettingsServices } from './ports.js';
+
+const ConnectionSettingsServicesContext = createContext<ConnectionSettingsServices | null>(null);
+
+export function ConnectionSettingsServicesProvider(props: {
+  readonly services: ConnectionSettingsServices;
+  readonly children?: ReactNode;
+}) {
+  return (
+    <ConnectionSettingsServicesContext.Provider value={props.services}>
+      {props.children}
+    </ConnectionSettingsServicesContext.Provider>
+  );
+}
+
+export function useConnectionSettingsServices(): ConnectionSettingsServices {
+  const services = useContext(ConnectionSettingsServicesContext);
+  if (!services) throw new Error('ConnectionSettingsServicesProvider is missing');
+  return services;
+}
+
+export function ConnectionSettingsServicesConsumer(props: {
+  readonly children: (services: ConnectionSettingsServices) => ReactNode;
+}) {
+  return props.children(useConnectionSettingsServices());
+}
+
+const noSaveUncertainty = () => false;
+const subscribeNoSaveUncertainty = () => () => {};
+
+export function ConnectionSaveUncertaintyObserver(props: {
+  readonly store?: ApiKeyOnboardingBridge['saveUncertainty'];
+  readonly children: (hasSaveUncertainty: boolean) => ReactNode;
+}) {
+  const hasSaveUncertainty = useSyncExternalStore(
+    props.store?.subscribe ?? subscribeNoSaveUncertainty,
+    props.store?.getSnapshot ?? noSaveUncertainty,
+    noSaveUncertainty,
+  );
+  return props.children(hasSaveUncertainty);
+}
diff --git a/apps/desktop/src/renderer/locales/settings-provider-copy.ts b/apps/desktop/src/renderer/features/connection-settings/settings-provider-copy.ts
similarity index 92%
rename from apps/desktop/src/renderer/locales/settings-provider-copy.ts
rename to apps/desktop/src/renderer/features/connection-settings/settings-provider-copy.ts
index c2ad504ded..d46309144e 100644
--- a/apps/desktop/src/renderer/locales/settings-provider-copy.ts
+++ b/apps/desktop/src/renderer/features/connection-settings/settings-provider-copy.ts
@@ -177,7 +177,7 @@ const zhCopy = {
     tabs: { all: '全部', recommended: '推荐', accounts: '账号', plans: '模型计划', api: 'API', aggregators: '聚合服务', local: '本地' },
     loadFailed: '载入模型连接失败', loadingAria: '正在加载模型供应商', connections: '模型连接',
     retry: '点击重试。', empty: '还没有模型连接', connectionRemoved: '原连接已被删除或移除,已返回模型连接列表。',
-    connectedLoading: '账号已连接,正在载入新的模型连接…', connectedLoadFailed: '账号已连接,但暂时无法载入新的模型连接。',
+    connectedLoading: '连接已添加,正在载入详情…', connectedLoadFailed: '连接已添加,但暂时无法刷新连接列表。',
     connectionIdentityChanged: '新连接的身份与登录结果不一致,请返回连接列表后重试。',
     emptyHelp: '从下方选择一种连接方式开始。', default: '默认', setDefault: '设为默认', setDefaultTitle: '让新任务默认使用这个连接', setDefaultPending: '设置中…', setDefaultFailed: '设为默认失败', addHelp: '选择账号登录、模型计划、API、聚合服务或本地运行时。',
     categoriesAria: '模型供应商分类', searchPlaceholder: '搜索服务商', searchAria: '搜索模型服务商', noMatch: '没有匹配的服务商', clearSearch: '清除搜索',
@@ -203,6 +203,10 @@ const zhCopy = {
     saving: '保存中…', save: '保存供应商', keyRequired: (name: string) => `请填写 ${name} API Key`,
     apiKeyLabel: 'API Key', accountIdLabel: 'Cloudflare Account ID', endpointLabel: '服务地址',
     defaultModel: '默认模型', defaultModelPlaceholder: '留空即可,保存后自动拉取', defaultModelHelp: '保存后 Maka 会向该端点拉取模型目录。只有当端点不提供目录时,才需要在这里手填一个模型 ID。',
+    onboardingVerifyAndChoose: '验证并选择模型', onboardingVerifying: '正在验证密钥并获取模型…',
+    onboardingChooseModels: '选择此连接使用的模型', onboardingChooseModelsHelp: '添加后仍可在连接详情中启用其他模型。', onboardingEnabledModels: '启用的模型', onboardingSearchModels: '搜索模型', onboardingAddConnection: '添加连接', onboardingBack: '返回修改',
+    onboardingAuthFailed: '密钥验证失败,请检查后重试。', onboardingTimeout: '验证超时,请检查网络或代理后重试。', onboardingNetwork: '无法连接模型服务,请检查网络、代理或服务地址。', onboardingUnavailable: '模型连接服务暂时不可用,请稍后重试。', onboardingInvalidResponse: '模型服务返回了无法识别的结果,请稍后重试。', onboardingCatalogFull: '连接数量已达上限,请先删除不再使用的连接。', onboardingModelsChanged: '可用模型已发生变化,请重新验证后选择。', onboardingNoModels: '没有发现可用模型,当前未创建连接。', onboardingSelectModel: '请至少启用一个模型。',
+    onboardingOutcomeUnknown: '保存结果暂时无法确认', onboardingOutcomeUnknownDetail: '请勿再次添加,以免创建重复连接。重新加载连接列表并检查该连接是否已经出现;仍不确定时请先重连 Runtime Host。', onboardingReloadConnections: '重新加载连接列表', onboardingRestart: '仍要添加另一个连接',
     ...zhCapabilitiesCopy,
   },
   oauthFlow: {
@@ -333,7 +337,7 @@ const enCopy: ProviderSettingsCopy = {
     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', connectionRemoved: 'The original connection was deleted or removed. Returned to the connection list.',
-    connectedLoading: 'Account connected. Loading the new model connection…', connectedLoadFailed: 'Account connected, but the new model connection could not be loaded yet.',
+    connectedLoading: 'Connection added. Loading its details…', connectedLoadFailed: 'Connection added, but the connection list could not be refreshed yet.',
     connectionIdentityChanged: 'The new connection identity did not match the sign-in result. Return to the connection list and try again.',
     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',
@@ -359,6 +363,10 @@ const enCopy: ProviderSettingsCopy = {
     saving: 'Saving…', save: 'Save provider', keyRequired: (name: string) => `Enter the ${name} API key`,
     apiKeyLabel: 'API key', accountIdLabel: 'Cloudflare Account ID', endpointLabel: 'Service URL',
     defaultModel: 'Default model', defaultModelPlaceholder: 'Leave empty — fetched after saving', defaultModelHelp: 'Maka fetches the model catalog from this endpoint after saving. Type a model id here only if the endpoint serves no catalog.',
+    onboardingVerifyAndChoose: 'Verify and choose models', onboardingVerifying: 'Verifying the key and loading models…',
+    onboardingChooseModels: 'Choose models for this connection', onboardingChooseModelsHelp: 'You can enable more models from the connection details later.', onboardingEnabledModels: 'Enabled models', onboardingSearchModels: 'Search models', onboardingAddConnection: 'Add connection', onboardingBack: 'Back to edit',
+    onboardingAuthFailed: 'The key could not be verified. Check it and try again.', onboardingTimeout: 'Verification timed out. Check the network or proxy and try again.', onboardingNetwork: 'Could not reach the model provider. Check the network, proxy, or service URL.', onboardingUnavailable: 'The model connection service is unavailable. Try again later.', onboardingInvalidResponse: 'The model provider returned an unrecognized response. Try again later.', onboardingCatalogFull: 'The con [...]
+    onboardingOutcomeUnknown: 'The save result cannot be confirmed', onboardingOutcomeUnknownDetail: 'Do not add it again yet, because that could create a duplicate connection. Reload the connection list and check whether this connection appeared; reconnect the Runtime Host if the result is still unclear.', onboardingReloadConnections: 'Reload connection list', onboardingRestart: 'Add another connection anyway',
     ...enCapabilitiesCopy,
   },
   oauthFlow: {
diff --git a/apps/desktop/src/renderer/model-connection-errors.ts b/apps/desktop/src/renderer/model-connection-errors.ts
index ca0c7550d1..fe6785ba72 100644
--- a/apps/desktop/src/renderer/model-connection-errors.ts
+++ b/apps/desktop/src/renderer/model-connection-errors.ts
@@ -20,14 +20,14 @@
 import type { ChatConfigurationReason } from '@maka/core/connection-readiness';
 import type { SessionEvent } from '@maka/core/events';
 import type { UiLocale } from '@maka/core/ui-locale';
-import { parseNoRealConnectionError } from '@maka/core/connection-error-copy';
+import {
+  NO_REAL_CONNECTION_CODE,
+  parseNoRealConnectionError,
+} from './application/contracts/connection-error-cleaner.js';
 import { getDesktopConversationCopy } from './locales/conversation-copy.js';
 import { localizedShellErrorMessage } from './locales/shell-copy.js';
 import { describeSessionErrorReason } from './session-error-presentation.js';
 
-const NO_REAL_CONNECTION_CODE = 'NO_REAL_CONNECTION';
-const NO_REAL_CONNECTION_REASON_RE = /NO_REAL_CONNECTION:([a-z_]+): /;
-
 export function isNoRealConnectionError(error: unknown): boolean {
   return parseNoRealConnectionError(error).matched;
 }
@@ -73,27 +73,6 @@ export function sessionEventErrorMessage(
   return localizedShellErrorMessage(new Error(event.message), fallback, locale);
 }
 
-/**
- * Canonical raw-error cleaner: strips the Electron IPC wrapper so error
- * classifiers see the main-process message, not the channel name (which can
- * contain classifier keywords, e.g. the "fetch" in 'connections:fetchModels').
- * Raw output must not reach a toast unclassified — see
- * providerPanelActionErrorMessage for the display-side contract.
- */
-export function cleanErrorMessage(error: unknown): string {
-  const raw = error instanceof Error ? error.message : String(error);
-  return cleanEventMessage(raw);
-}
-
-export function cleanEventMessage(message: string): string {
-  // Electron serializes a rejected handler as `${error.name}: ${error.message}`,
-  // so custom classes arrive as e.g. "ConnectionModelDiscoveryPreconditionError: …".
-  return message
-    .replace(/^Error invoking remote method '[^']+': (?:[A-Za-z_$][\w$]*)?Error: /, '')
-    .replace(NO_REAL_CONNECTION_REASON_RE, '')
-    .replace(`${NO_REAL_CONNECTION_CODE}: `, '');
-}
-
 export function modelSetupToastCopy(
   reason: string | undefined,
   fallback: string,
diff --git a/apps/desktop/src/renderer/platform/desktop/create-connection-settings-services.ts b/apps/desktop/src/renderer/platform/desktop/create-connection-settings-services.ts
new file mode 100644
index 0000000000..781261546b
--- /dev/null
+++ b/apps/desktop/src/renderer/platform/desktop/create-connection-settings-services.ts
@@ -0,0 +1,88 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import type { MakaBridge } from '../../../preload/bridge-contract.js';
+import type { ConnectionSettingsServices } from '../../features/connection-settings';
+
+export function createDesktopConnectionSettingsServices(
+  bridge: () => Pick<MakaBridge, 'connections'> = () => window.maka,
+): ConnectionSettingsServices {
+  const uncertainTargets = new Map<string, number>();
+  const uncertaintyListeners = new Map<string, Set<() => void>>();
+  let nextAttemptId = 1;
+
+  const notifyUncertaintyChanged = (targetKey: string) => {
+    for (const listener of [...(uncertaintyListeners.get(targetKey) ?? [])]) listener();
+  };
+
+  return {
+    forHost: (host) => {
+      const targetKey = `${host.profileId}\u0000${host.hostId}`;
+      return {
+        connections: {
+          getSnapshot: () => bridge().connections.getSnapshot(undefined, host),
+          setDefault: (connection) => bridge().connections.setDefault(connection, host),
+          setDefaultModel: (input) => bridge().connections.setDefaultModel(input, host),
+          create: (input) => bridge().connections.create(input, host),
+          update: (connection, patch) => bridge().connections.update(connection, patch, host),
+          delete: (connection) => bridge().connections.delete(connection, host),
+          test: (connection, options) => bridge().connections.test(connection, options, host),
+          fetchModels: (connection) => bridge().connections.fetchModels(connection, host),
+          hasSecret: (connection) => bridge().connections.hasSecret(connection, host),
+          getRequestHeaders: (connection) => bridge().connections.getRequestHeaders(connection, host),
+          setRequestHeaders: (connection, headers) =>
+            bridge().connections.setRequestHeaders(connection, headers, host),
+          subscribeEvents: (handler) => bridge().connections.subscribeEvents(handler, host),
+        },
+        apiKeyOnboarding: {
+          saveUncertainty: {
+            getSnapshot: () => uncertainTargets.has(targetKey),
+            subscribe: (listener) => {
+              const listeners = uncertaintyListeners.get(targetKey) ?? new Set<() => void>();
+              listeners.add(listener);
+              uncertaintyListeners.set(targetKey, listeners);
+              return () => {
+                listeners.delete(listener);
+                if (listeners.size === 0) uncertaintyListeners.delete(targetKey);
+              };
+            },
+            restart: () => {
+              if (uncertainTargets.delete(targetKey)) notifyUncertaintyChanged(targetKey);
+            },
+          },
+          verify: (input) => bridge().connections.verifyOnboarding(input, host),
+          save: async (input) => {
+            const attemptId = nextAttemptId++;
+            const wasUncertain = uncertainTargets.has(targetKey);
+            uncertainTargets.set(targetKey, attemptId);
+            if (!wasUncertain) notifyUncertaintyChanged(targetKey);
+            const outcome = await bridge().connections.saveOnboarding(input, host);
+            if (outcome.kind !== 'outcome_unknown') {
+              if (uncertainTargets.get(targetKey) === attemptId) {
+                uncertainTargets.delete(targetKey);
+                notifyUncertaintyChanged(targetKey);
+              }
+            }
+            return outcome;
+          },
+        },
+      };
+    },
+  };
+}
diff --git a/apps/desktop/src/renderer/settings/general-settings-page.tsx b/apps/desktop/src/renderer/settings/general-settings-page.tsx
index 62498c493b..42b0a0dd17 100644
--- a/apps/desktop/src/renderer/settings/general-settings-page.tsx
+++ b/apps/desktop/src/renderer/settings/general-settings-page.tsx
@@ -66,7 +66,7 @@ import { useOptimisticSettingsDraft } from "./use-optimistic-settings-draft";
 import { getSettingsPreferencesCopy } from "../locales/settings-preferences-copy.js";
 import { settingsTestResultMessage } from "../locales/settings-test-result-copy.js";
 import { getShellCopy } from "../locales/shell-copy.js";
-import type { RuntimeHostSettingsConnectionsBridge } from './runtime-host-settings-bridge.js';
+import type { RuntimeHostSettingsConnectionsBridge } from '../features/connection-settings';
 import { getSettingsSharedCopy } from '../locales/settings-shared-copy.js';
 import {
   useOptionalRuntimeHostSettingsTarget,
diff --git a/apps/desktop/src/renderer/settings/provider-add-form.tsx b/apps/desktop/src/renderer/settings/provider-add-form.tsx
index a287f0180f..78b0ecc6de 100644
--- a/apps/desktop/src/renderer/settings/provider-add-form.tsx
+++ b/apps/desktop/src/renderer/settings/provider-add-form.tsx
@@ -24,7 +24,7 @@ import {
   providerAuthRequiresSecret,
   providerAuthSupportsApiKey,
 } from '@maka/core/llm-connections';
-import { Banner, HStack, VStack } from '@astryxdesign/core';
+import { Banner, HStack, MultiSelector, Text, VStack } from '@astryxdesign/core';
 import { Collapsible } from '@astryxdesign/core/Collapsible';
 import {
   Button,
@@ -40,10 +40,12 @@ import { providerDisplay } from './provider-display';
 import { useActionGuard } from './use-action-guard';
 import {
   categoryLabel,
+  getProviderSettingsCopy,
   providerPanelActionErrorMessage,
+  type ApiKeyOnboardingBridge,
   type ConnectionsBridge,
-} from './provider-panel-shared';
-import { getProviderSettingsCopy } from '../locales/settings-provider-copy';
+  type DesktopConnectionOnboardingIdentity,
+} from '../features/connection-settings';
 import {
   newRequestHeaders,
   parseRequestBodyOverlay,
@@ -52,6 +54,10 @@ import {
 } from './request-customization-editor';
 import {
   createProviderWithDiscovery,
+  apiKeyOnboardingRoute,
+  initialOnboardingModelIds,
+  shouldShowManagedOnboardingOutcomeUnknown,
+  stableOnboardingModels,
   validateAddProviderDraft,
   type AddProviderIssue,
 } from './provider-add-submission';
@@ -67,12 +73,24 @@ type ProviderFormError = {
   message: string;
 };
 
+type ManagedOnboardingPhase =
+  | { readonly kind: 'input' }
+  | {
+      readonly kind: 'models';
+      readonly models: ReturnType<typeof stableOnboardingModels>;
+      readonly selectedIds: readonly string[];
+    };
+
 export function AddProviderForm(props: {
   bridge: ConnectionsBridge;
+  apiKeyOnboardingBridge?: ApiKeyOnboardingBridge;
   providerType: ProviderType;
   existingSlugs: string[];
   onCancel(): void;
   onCreated(slug: string, modelDiscoveryError?: unknown): Promise<void>;
+  onOnboarded?(identity: DesktopConnectionOnboardingIdentity): Promise<void>;
+  onOnboardingOutcomeUnknown?(): Promise<void>;
+  hasSaveUncertainty?: boolean;
 }) {
   const locale = useUiLocale();
   const copy = getProviderSettingsCopy(locale).add;
@@ -90,7 +108,14 @@ export function AddProviderForm(props: {
   const [requestHeaders, setRequestHeaders] = useState<RequestHeaderDraft[]>([]);
   const [requestBodyText, setRequestBodyText] = useState('');
   const [advancedOpen, setAdvancedOpen] = useState(false);
-  const [error, setError] = useState<ProviderFormError | null>(null);
+  const [formState, setFormState] = useState<{
+    readonly managedPhase: ManagedOnboardingPhase;
+    readonly error: ProviderFormError | null;
+  }>(() => ({
+    managedPhase: { kind: 'input' },
+    error: null,
+  }));
+  const { managedPhase, error } = formState;
   const [busy, setBusy] = useState(false);
   const submitGuard = useActionGuard<'submit'>();
   const addProviderMountedRef = useMountedRef();
@@ -103,6 +128,27 @@ export function AddProviderForm(props: {
   const requiresApiKey = providerAuthRequiresSecret(props.providerType) && supportsApiKey;
   const usesApiKeyDialog = usesQuickApiKeyDialog(props.providerType);
 
+  function setManagedPhase(next: ManagedOnboardingPhase) {
+    setFormState((current) => ({ ...current, managedPhase: next }));
+  }
+
+  function setError(
+    next:
+      | ProviderFormError
+      | null
+      | ((current: ProviderFormError | null) => ProviderFormError | null),
+  ) {
+    setFormState((current) => ({
+      ...current,
+      error: typeof next === 'function' ? next(current.error) : next,
+    }));
+  }
+
+  function resetManagedVerification(options?: { clearKey?: boolean }) {
+    setManagedPhase({ kind: 'input' });
+    if (options?.clearKey) setApiKey('');
+  }
+
   function clearFieldError(field: ProviderFormField) {
     setError((current) =>
       current?.field === field ? null : current,
@@ -126,18 +172,133 @@ export function AddProviderForm(props: {
     return copy.accountLogin;
   }
 
+  function onboardingFailureMessage(
+    result:
+      | Exclude<Awaited<ReturnType<ApiKeyOnboardingBridge['verify']>>, { kind: 'verified' }>
+      | Exclude<
+          Extract<Awaited<ReturnType<ApiKeyOnboardingBridge['save']>>, { kind: 'result' }>['result'],
+          { kind: 'saved' }
+        >,
+  ): string {
+    if (result.kind === 'failed') {
+      if (result.errorClass === 'auth') return copy.onboardingAuthFailed;
+      if (result.errorClass === 'timeout') return copy.onboardingTimeout;
+      if (result.errorClass === 'network') return copy.onboardingNetwork;
+      if (result.errorClass === 'provider_unavailable') return copy.onboardingUnavailable;
+      return copy.onboardingInvalidResponse;
+    }
+    if (result.reason === 'catalog_full') return copy.onboardingCatalogFull;
+    if (result.reason === 'model_unavailable' || result.reason === 'superseded') {
+      return copy.onboardingModelsChanged;
+    }
+    if (result.reason === 'credential_not_configured') return copy.keyRequired(display.name);
+    return copy.onboardingUnavailable;
+  }
+
+  async function verifyManagedApiKey(normalizedApiKey: string) {
+    const onboarding = props.apiKeyOnboardingBridge;
+    if (!onboarding) return;
+    submitGuard.begin('submit');
+    setBusy(true);
+    try {
+      const result = await onboarding.verify({
+        target: { kind: 'create', providerType: props.providerType },
+        apiKey: normalizedApiKey || null,
+        baseUrl: null,
+      });
+      if (!addProviderMountedRef.current) return;
+      if (result.kind !== 'verified') {
+        setError({
+          field:
+            result.kind === 'failed' && result.errorClass === 'auth'
+              ? 'apiKey'
+              : 'form',
+          message: onboardingFailureMessage(result),
+        });
+        return;
+      }
+      const models = stableOnboardingModels(result.models);
+      const selectedIds = initialOnboardingModelIds(models, recommendedDefaultModel);
+      if (selectedIds.length === 0) {
+        setError({ field: 'form', message: copy.onboardingNoModels });
+        return;
+      }
+      setManagedPhase({ kind: 'models', models, selectedIds });
+    } catch (err) {
+      if (addProviderMountedRef.current) {
+        setError({ field: 'form', message: providerPanelActionErrorMessage(err, locale) });
+      }
+    } finally {
+      submitGuard.finish();
+      if (addProviderMountedRef.current) setBusy(false);
+    }
+  }
+
+  async function saveManagedApiKey(
+    normalizedApiKey: string,
+    phase: Extract<ManagedOnboardingPhase, { kind: 'models' }>,
+  ) {
+    const onboarding = props.apiKeyOnboardingBridge;
+    if (!onboarding || phase.selectedIds.length === 0) {
+      setError({ field: 'form', message: copy.onboardingSelectModel });
+      return;
+    }
+    const selected = new Set(phase.selectedIds);
+    const stableIds = phase.models
+      .map((model) => model.id)
+      .filter((modelId) => selected.has(modelId));
+    if (selected.has(recommendedDefaultModel)) {
+      stableIds.splice(stableIds.indexOf(recommendedDefaultModel), 1);
+      stableIds.unshift(recommendedDefaultModel);
+    }
+    submitGuard.begin('submit');
+    setBusy(true);
+    try {
+      const outcome = await onboarding.save({
+        target: { kind: 'create', providerType: props.providerType },
+        apiKey: normalizedApiKey || null,
+        baseUrl: null,
+        enabledModelIds: stableIds,
+      });
+      if (!addProviderMountedRef.current) return;
+      if (outcome.kind === 'outcome_unknown') {
+        setApiKey('');
+        return;
+      }
+      if (outcome.kind === 'not_saved') {
+        setError({ field: 'form', message: copy.onboardingUnavailable });
+        return;
+      }
+      const result = outcome.result;
+      if (result.kind === 'saved') {
+        setApiKey('');
+        await props.onOnboarded?.(result.connection);
+        return;
+      }
+      if (
+        result.kind === 'rejected' &&
+        (result.reason === 'model_unavailable' || result.reason === 'superseded')
+      ) {
+        setManagedPhase({ kind: 'input' });
+      }
+      if (result.kind === 'failed' && result.errorClass === 'auth') {
+        setManagedPhase({ kind: 'input' });
+      }
+      setError({
+        field: result.kind === 'failed' && result.errorClass === 'auth' ? 'apiKey' : 'form',
+        message: onboardingFailureMessage(result),
+      });
+    } catch {
+      if (addProviderMountedRef.current) setApiKey('');
+    } finally {
+      submitGuard.finish();
+      if (addProviderMountedRef.current) setBusy(false);
+    }
+  }
+
   async function submit() {
     if (submitGuard.current !== null) return;
     setError(null);
-    const issue = validateAddProviderDraft({
-      providerType: props.providerType,
-      slug,
-      existingSlugs: props.existingSlugs,
-      apiKey,
-      cloudflareAccountId,
-      baseUrl,
-    });
-    if (issue) return setError({ field: issue.field, message: issueMessage(issue) });
     const normalizedApiKey = apiKey.trim();
     const normalizedCloudflareAccountId = cloudflareAccountId.trim();
     const normalizedDefaultModel = defaultModel.trim();
@@ -150,6 +311,31 @@ export function AddProviderForm(props: {
       setAdvancedOpen(true);
       return setError({ field: 'advancedRequest', message: copy.requestCustomizationInvalid });
     }
+    const onboardingRoute = apiKeyOnboardingRoute({
+      providerType: props.providerType,
+      requestHeaderCount: Object.keys(normalizedRequestHeaders).length,
+      hasRequestBodyOverlay: requestBodyOverlay !== undefined,
+    });
+    if (onboardingRoute.kind === 'host' && props.apiKeyOnboardingBridge) {
+      if (requiresApiKey && !normalizedApiKey) {
+        return setError({ field: 'apiKey', message: copy.keyRequired(display.name) });
+      }
+      if (managedPhase.kind === 'models') {
+        await saveManagedApiKey(normalizedApiKey, managedPhase);
+      } else if (managedPhase.kind === 'input') {
+        await verifyManagedApiKey(normalizedApiKey);
+      }
+      return;
+    }
+    const issue = validateAddProviderDraft({
+      providerType: props.providerType,
+      slug,
+      existingSlugs: props.existingSlugs,
+      apiKey,
+      cloudflareAccountId,
+      baseUrl,
+    });
+    if (issue) return setError({ field: issue.field, message: issueMessage(issue) });
     submitGuard.begin('submit');
     setBusy(true);
     try {
@@ -203,11 +389,13 @@ export function AddProviderForm(props: {
           headers={requestHeaders}
           onHeadersChange={(headers) => {
             setRequestHeaders(headers);
+            resetManagedVerification();
             clearFieldError('advancedRequest');
           }}
           bodyText={requestBodyText}
           onBodyTextChange={(value) => {
             setRequestBodyText(value);
+            resetManagedVerification();
             clearFieldError('advancedRequest');
           }}
           disabled={busy}
@@ -229,6 +417,88 @@ export function AddProviderForm(props: {
       </VStack>
     </Collapsible>
   );
+  const quickUsesManagedOnboarding = Boolean(
+    props.apiKeyOnboardingBridge &&
+      apiKeyOnboardingRoute({
+        providerType: props.providerType,
+        requestHeaderCount: requestHeaders.length,
+        hasRequestBodyOverlay: requestBodyText.trim().length > 0,
+      }).kind === 'host',
+  );
+
+  if (
+    usesApiKeyDialog &&
+    shouldShowManagedOnboardingOutcomeUnknown(props.hasSaveUncertainty === true, busy)
+  ) {
+    return (
+      <VStack gap={3} data-maka-contract="api-key-onboarding-outcome-unknown">
+        <Banner
+          status="warning"
+          role="status"
+          title={copy.onboardingOutcomeUnknown}
+          description={copy.onboardingOutcomeUnknownDetail}
+        />
+        <HStack gap={2} justify="end">
+          <Button
+            variant="secondary"
+            label={copy.onboardingReloadConnections}
+            clickAction={async () => props.onOnboardingOutcomeUnknown?.()}
+          />
+        </HStack>
+      </VStack>
+    );
+  }
+
+  if (usesApiKeyDialog && managedPhase.kind === 'models') {
+    const options = managedPhase.models.map((model) => ({
+      value: model.id,
+      label: model.displayName?.trim() || model.id,
+    }));
+    return (
+      <VStack as="form" gap={3} onSubmit={submitApiKey} data-maka-contract="api-key-onboarding-models">
+        <VStack gap={1}>
+          <Text weight="semibold">{copy.onboardingChooseModels}</Text>
+          <Text type="supporting" color="secondary">{copy.onboardingChooseModelsHelp}</Text>
+        </VStack>
+        <MultiSelector
+          label={copy.onboardingEnabledModels}
+          options={options}
+          value={[...managedPhase.selectedIds]}
+          onChange={(selectedIds) => {
+            setManagedPhase({ ...managedPhase, selectedIds });
+            clearFieldError('form');
+          }}
+          isDisabled={busy}
+          placeholder={copy.onboardingSelectModel}
+          triggerDisplay="labels"
+          hasSearch
+          searchPlaceholder={copy.onboardingSearchModels}
+          width="100%"
+        />
+        <div role="status" aria-live="polite">
+          {busy ? <Text type="supporting">{copy.saving}</Text> : null}
+        </div>
+        {error?.field === 'form' && <Banner status="error" title={error.message} />}
+        <HStack gap={2} justify="end">
+          <Button
+            variant="ghost"
+            isDisabled={busy}
+            onClick={() => {
+              resetManagedVerification();
+              setError(null);
+            }}
+            label={copy.onboardingBack}
+          />
+          <Button
+            variant="primary"
+            type="submit"
+            isDisabled={busy || managedPhase.selectedIds.length === 0}
+            label={busy ? copy.saving : copy.onboardingAddConnection}
+          />
+        </HStack>
+      </VStack>
+    );
+  }
 
   if (usesApiKeyDialog) {
     return (
@@ -237,6 +507,7 @@ export function AddProviderForm(props: {
           value={apiKey}
           onChange={(next) => {
             setApiKey(next);
+            resetManagedVerification();
             clearFieldError('apiKey');
           }}
           placeholder={copy.apiKeyPlaceholder}
@@ -252,12 +523,30 @@ export function AddProviderForm(props: {
           hasAutoFocus
         />
         {advancedRequestEditor}
+        <div role="status" aria-live="polite">
+          {busy ? (
+            <Text type="supporting">
+              {quickUsesManagedOnboarding ? copy.onboardingVerifying : copy.saving}
+            </Text>
+          ) : null}
+        </div>
         {error?.field === 'form' && (
           <Banner status="error" title={error.message} />
         )}
         <HStack gap={2} justify="end">
           <Button variant="ghost" isDisabled={busy} onClick={props.onCancel} label={copy.cancel} />
-          <Button variant="primary" type="submit" isDisabled={busy} label={busy ? copy.saving : copy.save} />
+          <Button
+            variant="primary"
+            type="submit"
+            isDisabled={busy}
+            label={quickUsesManagedOnboarding
+              ? busy
+                ? copy.onboardingVerifying
+                : copy.onboardingVerifyAndChoose
+              : busy
+                ? copy.saving
+                : copy.save}
+          />
         </HStack>
       </VStack>
     );
@@ -277,6 +566,7 @@ export function AddProviderForm(props: {
             value={apiKey}
             onChange={(next) => {
               setApiKey(next);
+              resetManagedVerification();
               clearFieldError('apiKey');
             }}
             placeholder={copy.apiKeyPlaceholder}
@@ -295,6 +585,7 @@ export function AddProviderForm(props: {
           value={slug}
           onChange={(value) => {
             setSlug(value);
+            resetManagedVerification();
             clearFieldError('slug');
           }}
           placeholder="my-provider"
@@ -308,7 +599,10 @@ export function AddProviderForm(props: {
         />
         <TextInput
           value={name}
-          onChange={(value) => setName(value)}
+          onChange={(value) => {
+            setName(value);
+            resetManagedVerification();
+          }}
           placeholder={display.name}
           isDisabled={isExperimental || busy}
           label={copy.name}
@@ -318,6 +612,7 @@ export function AddProviderForm(props: {
             value={cloudflareAccountId}
             onChange={(value) => {
               setCloudflareAccountId(value);
+              resetManagedVerification();
               clearFieldError('accountId');
             }}
             placeholder={copy.accountIdPlaceholder}
@@ -335,6 +630,7 @@ export function AddProviderForm(props: {
             value={baseUrl}
             onChange={(value) => {
               setBaseUrl(value);
+              resetManagedVerification();
               clearFieldError('baseUrl');
             }}
             placeholder={defaults.baseUrl || 'https://…'}
@@ -351,7 +647,10 @@ export function AddProviderForm(props: {
         {showsDefaultModel && (
           <TextInput
             value={defaultModel}
-            onChange={setDefaultModel}
+            onChange={(value) => {
+              setDefaultModel(value);
+              resetManagedVerification();
+            }}
             placeholder={copy.defaultModelPlaceholder}
             isDisabled={isExperimental || busy}
             label={copy.defaultModel}
diff --git a/apps/desktop/src/renderer/settings/provider-add-model-dialog.tsx b/apps/desktop/src/renderer/settings/provider-add-model-dialog.tsx
index 794ee915f1..85ae5c35fc 100644
--- a/apps/desktop/src/renderer/settings/provider-add-model-dialog.tsx
+++ b/apps/desktop/src/renderer/settings/provider-add-model-dialog.tsx
@@ -22,7 +22,7 @@ import { Dialog, DialogHeader } from '@astryxdesign/core/Dialog';
 import { FormLayout } from '@astryxdesign/core/FormLayout';
 import { Layout, LayoutContent, LayoutFooter } from '@astryxdesign/core/Layout';
 import { Button, HStack, NumberInput, TextInput, useUiLocale } from '@maka/ui';
-import { getProviderSettingsCopy } from '../locales/settings-provider-copy';
+import { getProviderSettingsCopy } from '../features/connection-settings';
 
 /**
  * Introduce a model by exact id, for a provider whose catalog cannot grow on
diff --git a/apps/desktop/src/renderer/settings/provider-add-submission.ts b/apps/desktop/src/renderer/settings/provider-add-submission.ts
index c82bbdca2b..0006ff54fe 100644
--- a/apps/desktop/src/renderer/settings/provider-add-submission.ts
+++ b/apps/desktop/src/renderer/settings/provider-add-submission.ts
@@ -23,9 +23,64 @@ import {
   providerAuthSupportsApiKey,
   providerSupportsModelDiscovery,
   validateSlug,
+  type ModelInfo,
   type ProviderType,
 } from '@maka/core/llm-connections';
 import type { CreateConnectionInput, IdentifiedLlmConnection } from '@maka/core/llm-connections';
+export type ApiKeyOnboardingRoute =
+  | { readonly kind: 'host' }
+  | {
+      readonly kind: 'legacy';
+      readonly reason:
+        | 'provider_auth'
+        | 'custom_endpoint'
+        | 'cloudflare'
+        | 'request_headers'
+        | 'request_body';
+    };
+
+export function shouldShowManagedOnboardingOutcomeUnknown(
+  hasSaveUncertainty: boolean,
+  busy: boolean,
+): boolean {
+  return hasSaveUncertainty && !busy;
+}
+
+/** Decide the only writer before either writer performs a side effect. */
+export function apiKeyOnboardingRoute(input: {
+  readonly providerType: ProviderType;
+  readonly requestHeaderCount: number;
+  readonly hasRequestBodyOverlay: boolean;
+}): ApiKeyOnboardingRoute {
+  const definition = PROVIDER_REGISTRY[input.providerType];
+  if (!providerAuthSupportsApiKey(input.providerType) || definition.authKind !== 'api_key') {
+    return { kind: 'legacy', reason: 'provider_auth' };
+  }
+  if (input.providerType === 'cloudflare-workers-ai') {
+    return { kind: 'legacy', reason: 'cloudflare' };
+  }
+  if (!definition.baseUrl) return { kind: 'legacy', reason: 'custom_endpoint' };
+  if (input.requestHeaderCount > 0) return { kind: 'legacy', reason: 'request_headers' };
+  if (input.hasRequestBodyOverlay) return { kind: 'legacy', reason: 'request_body' };
+  return { kind: 'host' };
+}
+
+export function stableOnboardingModels(models: readonly ModelInfo[]): ModelInfo[] {
+  return [...models].sort((left, right) => {
+    const leftLabel = left.displayName?.trim() || left.id;
+    const rightLabel = right.displayName?.trim() || right.id;
+    return leftLabel.localeCompare(rightLabel) || left.id.localeCompare(right.id);
+  });
+}
+
+export function initialOnboardingModelIds(
+  models: readonly ModelInfo[],
+  recommendedModelId: string,
+): string[] {
+  if (models.some((model) => model.id === recommendedModelId)) return [recommendedModelId];
+  const first = stableOnboardingModels(models)[0];
+  return first ? [first.id] : [];
+}
 
 /**
  * The two decisions 添加连接 makes that are not layout: which fields a provider
diff --git a/apps/desktop/src/renderer/settings/provider-catalog-page.tsx b/apps/desktop/src/renderer/settings/provider-catalog-page.tsx
index 34d2782500..10e7346cc0 100644
--- a/apps/desktop/src/renderer/settings/provider-catalog-page.tsx
+++ b/apps/desktop/src/renderer/settings/provider-catalog-page.tsx
@@ -39,8 +39,12 @@ import { Button, TextInput, useUiLocale } from '@maka/ui';
 import { AddProviderForm } from './provider-add-form';
 import { ProviderLogo, providerDisplay } from './provider-display';
 import { OAuthLoginPanel, useOAuthCards, type OAuthCardId } from './provider-oauth-section';
-import { type ConnectionsBridge } from './provider-panel-shared';
-import { getProviderSettingsCopy } from '../locales/settings-provider-copy';
+import {
+  getProviderSettingsCopy,
+  type ApiKeyOnboardingBridge,
+  type ConnectionsBridge,
+  type DesktopConnectionOnboardingIdentity,
+} from '../features/connection-settings';
 
 type CatalogCategory = ProviderCatalogGroup | 'all' | 'recommended' | 'accounts';
 
@@ -225,10 +229,14 @@ export function ProviderCatalogPage(props: {
  */
 export function ProviderSetupPage(props: {
   bridge: ConnectionsBridge;
+  apiKeyOnboardingBridge?: ApiKeyOnboardingBridge;
   target: SetupTarget;
   existingSlugs: string[];
   onCancel(): void;
   onCreated(slug: string, modelDiscoveryError?: unknown): Promise<void>;
+  onOnboarded?(identity: DesktopConnectionOnboardingIdentity): Promise<void>;
+  onOnboardingOutcomeUnknown?(): Promise<void>;
+  hasSaveUncertainty?: boolean;
   onAccountCreated(connection?: CreatedOAuthConnectionIdentity): Promise<void>;
   labelledBy?: string;
 }) {
@@ -244,10 +252,14 @@ export function ProviderSetupPage(props: {
       <AddProviderForm
         key={props.target.providerType}
         bridge={props.bridge}
+        apiKeyOnboardingBridge={props.apiKeyOnboardingBridge}
         providerType={props.target.providerType}
         existingSlugs={props.existingSlugs}
         onCancel={props.onCancel}
         onCreated={props.onCreated}
+        onOnboarded={props.onOnboarded}
+        onOnboardingOutcomeUnknown={props.onOnboardingOutcomeUnknown}
+        hasSaveUncertainty={props.hasSaveUncertainty}
       />
     </div>
   );
diff --git a/apps/desktop/src/renderer/settings/provider-connection-detail.tsx b/apps/desktop/src/renderer/settings/provider-connection-detail.tsx
index 504478186d..3c78609a31 100644
--- a/apps/desktop/src/renderer/settings/provider-connection-detail.tsx
+++ b/apps/desktop/src/renderer/settings/provider-connection-detail.tsx
@@ -52,7 +52,6 @@ import {
 import { PasswordInput } from './password-input';
 import { SettingsExpandableRow } from './settings-expandable-row';
 import { SettingsRow } from './settings-section';
-import { getProviderSettingsCopy } from '../locales/settings-provider-copy';
 import { providerDisplay } from './provider-display';
 import { AddModelDialog } from './provider-add-model-dialog';
 import { EnabledModelManager } from './provider-enabled-model-manager';
@@ -64,9 +63,10 @@ import {
 } from './runtime-host-settings-target.js';
 import { useOAuthLoginFlow } from './use-oauth-login-flow';
 import {
+  getProviderSettingsCopy,
   providerPanelActionErrorMessage,
   type CredentialPresenceStatus,
-} from './provider-panel-shared';
+} from '../features/connection-settings';
 import type { StatusSemantic } from '@maka/ui';
 import {
   useConnectionDetail,
diff --git a/apps/desktop/src/renderer/settings/provider-connection-status.ts b/apps/desktop/src/renderer/settings/provider-connection-status.ts
index 1fbb461a5c..b74f2c3bab 100644
--- a/apps/desktop/src/renderer/settings/provider-connection-status.ts
+++ b/apps/desktop/src/renderer/settings/provider-connection-status.ts
@@ -26,7 +26,7 @@ import type { LlmConnection } from '@maka/core/llm-connections';
 import { isRetiredProvider } from '@maka/core/provider-registry';
 
 import type { UiLocale } from '@maka/core/ui-locale';
-import { getProviderSettingsCopy } from '../locales/settings-provider-copy.js';
+import { getProviderSettingsCopy } from '../features/connection-settings/index.js';
 import type { StatusSemantic } from '@maka/ui';
 
 export interface ConnectionChipStatus {
diff --git a/apps/desktop/src/renderer/settings/provider-enabled-model-manager.tsx b/apps/desktop/src/renderer/settings/provider-enabled-model-manager.tsx
index 4ae778fd61..fa8776d9ae 100644
--- a/apps/desktop/src/renderer/settings/provider-enabled-model-manager.tsx
+++ b/apps/desktop/src/renderer/settings/provider-enabled-model-manager.tsx
@@ -20,7 +20,7 @@
 import { MultiSelector } from '@astryxdesign/core';
 import type { ModelCatalogEntry } from '@maka/core/model-catalog';
 import { useUiLocale } from '@maka/ui';
-import { getProviderSettingsCopy } from '../locales/settings-provider-copy';
+import { getProviderSettingsCopy } from '../features/connection-settings';
 
 /**
  * Adapts Maka's model catalog to Astryx's multi-select field.
diff --git a/apps/desktop/src/renderer/settings/provider-oauth-section.tsx b/apps/desktop/src/renderer/settings/provider-oauth-section.tsx
index d1eb34de33..781de7e3a3 100644
--- a/apps/desktop/src/renderer/settings/provider-oauth-section.tsx
+++ b/apps/desktop/src/renderer/settings/provider-oauth-section.tsx
@@ -27,7 +27,7 @@ import {
   useMountedRef,
   useUiLocale,
 } from '@maka/ui';
-import { getProviderSettingsCopy, type ProviderSettingsCopy } from '../locales/settings-provider-copy';
+import { getProviderSettingsCopy, type ProviderSettingsCopy } from '../features/connection-settings';
 import {
   useOAuthLoginFlow,
   subscriptionActionErrorMessage,
diff --git a/apps/desktop/src/renderer/settings/providers-panel.tsx b/apps/desktop/src/renderer/settings/providers-panel.tsx
index b4d7767b9f..ffa68347b0 100644
--- a/apps/desktop/src/renderer/settings/providers-panel.tsx
+++ b/apps/desktop/src/renderer/settings/providers-panel.tsx
@@ -55,11 +55,20 @@ import { useSettingsRouteFocus } from './settings-route-focus';
 import { SettingsRouteHeader } from './settings-route-header';
 import { ProviderLogo, providerDisplay } from './provider-display';
 import { oauthPanelSubtitle } from './provider-oauth-section';
-import { providerPanelActionErrorMessage, type ConnectionsBridge } from './provider-panel-shared';
-import { getProviderSettingsCopy } from '../locales/settings-provider-copy';
-import { useRuntimeHostSettingsErrorReporter } from './runtime-host-settings-target.js';
+import {
+  getProviderSettingsCopy,
+  providerPanelActionErrorMessage,
+  ConnectionSaveUncertaintyObserver,
+  type ApiKeyOnboardingBridge,
+  type ConnectionsBridge,
+  type DesktopConnectionOnboardingIdentity,
+} from '../features/connection-settings';
+import {
+  RuntimeHostSettingsGenerationBoundary,
+  useRuntimeHostSettingsErrorReporter,
+} from './runtime-host-settings-target.js';
 
-export type { ConnectionsBridge } from './provider-panel-shared';
+export type { ConnectionsBridge } from '../features/connection-settings';
 
 /**
  * Where the panel is. Four levels, one container, one back affordance:
@@ -81,8 +90,15 @@ export type { ConnectionsBridge } from './provider-panel-shared';
 type PanelRoute =
   | { kind: 'list' }
   | { kind: 'catalog' }
-  | { kind: 'setup'; target: SetupTarget; origin: 'list' | 'catalog' }
-  | { kind: 'adopting-oauth'; identity: CreatedOAuthConnectionIdentity }
+  | {
+      kind: 'setup';
+      target: SetupTarget;
+      origin: 'list' | 'catalog';
+    }
+  | {
+      kind: 'adopting-connection';
+      identity: DesktopConnectionOnboardingIdentity | CreatedOAuthConnectionIdentity;
+    }
   | { kind: 'detail'; connectionId: string };
 
 function backTarget(route: PanelRoute): PanelRoute {
@@ -90,8 +106,9 @@ function backTarget(route: PanelRoute): PanelRoute {
   return { kind: 'list' };
 }
 
-export function ProvidersPanel({ bridge, initialPage = 'connections', initialConnectionSlug, initialCreateProviderType, onInitialCatalogConsumed, onInitialCreateProviderConsumed }: {
+type ProvidersPanelProps = {
   bridge: ConnectionsBridge;
+  apiKeyOnboardingBridge?: ApiKeyOnboardingBridge;
   initialPage?: 'connections' | 'catalog';
   /**
    * When set, open this connection's detail once the list has loaded.
@@ -107,6 +124,23 @@ export function ProvidersPanel({ bridge, initialPage = 'connections', initialCon
   onInitialCatalogConsumed?: () => void;
   /** Called once the setup level has been entered. */
   onInitialCreateProviderConsumed?: () => void;
+};
+
+export function ProvidersPanel(props: ProvidersPanelProps) {
+  return (
+    <ConnectionSaveUncertaintyObserver store={props.apiKeyOnboardingBridge?.saveUncertainty}>
+      {(hasOnboardingUncertainty) => (
+        <ProvidersPanelContent
+          {...props}
+          hasOnboardingUncertainty={hasOnboardingUncertainty}
+        />
+      )}
+    </ConnectionSaveUncertaintyObserver>
+  );
+}
+
+function ProvidersPanelContent({ bridge, apiKeyOnboardingBridge, initialPage = 'connections', initialConnectionSlug, initialCreateProviderType, onInitialCatalogConsumed, onInitialCreateProviderConsumed, hasOnboardingUncertainty }: ProvidersPanelProps & {
+  hasOnboardingUncertainty: boolean;
 }) {
   const reportHostError = useRuntimeHostSettingsErrorReporter();
   // Projected, not merely identified: the detail editor renders the Host's
@@ -169,15 +203,20 @@ export function ProvidersPanel({ bridge, initialPage = 'connections', initialCon
 
   const initialCatalogOpenedRef = useRef(false);
   useEffect(() => {
-    if (loading || initialPage !== 'catalog' || initialCatalogOpenedRef.current) return;
+    if (
+      loading ||
+      hasOnboardingUncertainty ||
+      initialPage !== 'catalog' ||
+      initialCatalogOpenedRef.current
+    ) return;
     initialCatalogOpenedRef.current = true;
     setRoute({ kind: 'catalog' });
     onInitialCatalogConsumed?.();
-  }, [initialPage, loading, onInitialCatalogConsumed]);
+  }, [hasOnboardingUncertainty, initialPage, loading, onInitialCatalogConsumed]);
 
   const initialConnectionDetailOpenedRef = useRef(false);
   useEffect(() => {
-    if (route.kind === 'adopting-oauth') {
+    if (route.kind === 'adopting-connection') {
       const created = connections.find(
         (connection) => connection.connectionId === route.identity.connectionId,
       );
@@ -203,7 +242,12 @@ export function ProvidersPanel({ bridge, initialPage = 'connections', initialCon
 
   const initialCreateOpenedRef = useRef(false);
   useEffect(() => {
-    if (loading || !initialCreateProviderType || initialCreateOpenedRef.current) return;
+    if (
+      loading ||
+      hasOnboardingUncertainty ||
+      !initialCreateProviderType ||
+      initialCreateOpenedRef.current
+    ) return;
     initialCreateOpenedRef.current = true;
     // Straight to the form, so `origin` is the list: the user never saw a
     // catalog and must not be dropped into one on the way out.
@@ -217,7 +261,7 @@ export function ProvidersPanel({ bridge, initialPage = 'connections', initialCon
       },
     });
     onInitialCreateProviderConsumed?.();
-  }, [loading, initialCreateProviderType, onInitialCreateProviderConsumed]);
+  }, [hasOnboardingUncertainty, loading, initialCreateProviderType, onInitialCreateProviderConsumed]);
 
   function goBack() {
     setRoute(backTarget(route));
@@ -240,12 +284,12 @@ export function ProvidersPanel({ bridge, initialPage = 'connections', initialCon
   const selected = route.kind === 'detail'
     ? connections.find((connection) => connection.connectionId === route.connectionId) ?? null
     : null;
-  const pendingOAuthIdentity = route.kind === 'adopting-oauth' ? route.identity : null;
+  const pendingConnectionIdentity = route.kind === 'adopting-connection' ? route.identity : null;
 
   // A detail route whose connection vanished (deleted in another window) is an
   // unsatisfiable route, not a state to correct: the list is what it renders
   // as. Deriving that beats scheduling a setState from inside render.
-  const level: Exclude<PanelRoute['kind'], 'adopting-oauth'> = route.kind === 'adopting-oauth'
+  const level: Exclude<PanelRoute['kind'], 'adopting-connection'> = route.kind === 'adopting-connection'
     ? 'list'
     : route.kind === 'detail' && !selected
       ? 'list'
@@ -398,6 +442,7 @@ export function ProvidersPanel({ bridge, initialPage = 'connections', initialCon
         <VStack gap={5}>
           <SettingsRouteHeader
             onBack={goBack}
+            isBackDisabled={hasOnboardingUncertainty}
             backLabel={route.origin === 'catalog' ? copy.backToCatalog : copy.backToList}
             logo={<ProviderLogo type={route.target.providerType} compact />}
             titleId={setupTitleId}
@@ -408,18 +453,29 @@ export function ProvidersPanel({ bridge, initialPage = 'connections', initialCon
               ? oauthPanelSubtitle(route.target.cardId, providerCopy.oauthSection)
               : copy.createSubtitle}
           />
-          <ProviderSetupPage
-            bridge={bridge}
-            target={route.target}
-            existingSlugs={connections.map((connection) => connection.slug)}
-            labelledBy={setupTitleId}
-            onCancel={goBack}
-            onAccountCreated={async (identity) => {
-              if (!identity) return;
-              setRoute({ kind: 'adopting-oauth', identity });
-              await reload();
-            }}
-            onCreated={async (slug, modelDiscoveryError) => {
+          <RuntimeHostSettingsGenerationBoundary>
+            <ProviderSetupPage
+              bridge={bridge}
+              apiKeyOnboardingBridge={apiKeyOnboardingBridge}
+              target={route.target}
+              existingSlugs={connections.map((connection) => connection.slug)}
+              labelledBy={setupTitleId}
+              hasSaveUncertainty={hasOnboardingUncertainty}
+              onCancel={goBack}
+              onAccountCreated={async (identity) => {
+                if (!identity) return;
+                setRoute({ kind: 'adopting-connection', identity });
+                await reload();
+              }}
+              onOnboarded={async (identity) => {
+                setRoute({ kind: 'adopting-connection', identity });
+                await reload();
+              }}
+              onOnboardingOutcomeUnknown={async () => {
+                setRoute({ kind: 'list' });
+                await reload();
+              }}
+              onCreated={async (slug, modelDiscoveryError) => {
               const snapshot = await reload();
               if (!snapshot || !providersPanelMountedRef.current) return;
               // The new connection's detail, not the list: creating it is the
@@ -442,8 +498,9 @@ export function ProvidersPanel({ bridge, initialPage = 'connections', initialCon
                   ),
                 );
               }
-            }}
-          />
+              }}
+            />
+          </RuntimeHostSettingsGenerationBoundary>
         </VStack>
       ) : (
         <>
@@ -462,23 +519,49 @@ export function ProvidersPanel({ bridge, initialPage = 'connections', initialCon
               variant="primary"
               label={copy.addConnection}
               onClick={openCatalog}
-              isDisabled={pendingOAuthIdentity !== null}
+              isDisabled={pendingConnectionIdentity !== null || hasOnboardingUncertainty}
               data-maka-contract="add-connection"
             />
           </HStack>
-          {pendingOAuthIdentity && loadError ? (
+          {hasOnboardingUncertainty && (
+            <Banner
+              status="warning"
+              role="status"
+              title={providerCopy.add.onboardingOutcomeUnknown}
+              description={providerCopy.add.onboardingOutcomeUnknownDetail}
+              endContent={(
+                <HStack gap={2}>
+                  <Button
+                    variant="ghost"
+                    label={providerCopy.add.onboardingReloadConnections}
+                    onClick={() => void reload()}
+                  />
+                  <Button
+                    variant="secondary"
+                    label={providerCopy.add.onboardingRestart}
+                    onClick={() => {
+                      apiKeyOnboardingBridge?.saveUncertainty.restart();
+                      openCatalog();
+                    }}
+                  />
+                </HStack>
+              )}
+            />
+          )}
+          {pendingConnectionIdentity && loadError ? (
             <Banner
               status="warning"
               role="status"
               title={copy.connectedLoadFailed}
-              description={loadError}
+              description={`${pendingConnectionIdentity.slug} · ${loadError}`}
               endContent={<Button variant="ghost" label={copy.retry} onClick={() => void reload()} />}
             />
-          ) : pendingOAuthIdentity ? (
+          ) : pendingConnectionIdentity ? (
             <Banner
               status="info"
               role="status"
               title={copy.connectedLoading}
+              description={pendingConnectionIdentity.slug}
               endContent={<Button variant="ghost" label={copy.retry} onClick={() => void reload()} />}
             />
           ) : loadError ? (
@@ -493,7 +576,7 @@ export function ProvidersPanel({ bridge, initialPage = 'connections', initialCon
               icon={<Cpu size={ICON_SIZE.empty} />}
               title={copy.empty}
               description={copy.emptyHelp}
-              actions={<Button variant="primary" label={copy.addConnection} onClick={openCatalog} isDisabled={pendingOAuthIdentity !== null} />}
+              actions={<Button variant="primary" label={copy.addConnection} onClick={openCatalog} isDisabled={pendingConnectionIdentity !== null || hasOnboardingUncertainty} />}
             />
           ) : (
             <List hasDividers>
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 041bd4d09c..de724af825 100644
--- a/apps/desktop/src/renderer/settings/runtime-host-settings-bridge.ts
+++ b/apps/desktop/src/renderer/settings/runtime-host-settings-bridge.ts
@@ -22,37 +22,11 @@ import type {
   DesktopRuntimeHostRef,
   MakaBridge,
 } from '../../preload/bridge-contract.js';
-import type { ConnectionsBridge } from './provider-panel-shared.js';
 import type {
   OAuthAccountFlowBridge,
   OAuthAuthorizationFlowBridge,
 } from './use-oauth-login-flow.js';
 
-export type RuntimeHostSettingsConnectionsBridge = ConnectionsBridge & {
-  setDefaultModel(input: { slug: string; model: string } | null): Promise<void>;
-};
-
-export function runtimeHostConnectionsBridge(
-  host: DesktopRuntimeHostRef,
-): RuntimeHostSettingsConnectionsBridge {
-  return {
-    getSnapshot: () => window.maka.connections.getSnapshot(undefined, host),
-    setDefault: (connection) => window.maka.connections.setDefault(connection, host),
-    setDefaultModel: (input) => window.maka.connections.setDefaultModel(input, host),
-    create: (input) => window.maka.connections.create(input, host),
-    update: (connection, patch) => window.maka.connections.update(connection, patch, host),
-    delete: (connection) => window.maka.connections.delete(connection, host),
-    test: (connection, options) => window.maka.connections.test(connection, options, host),
-    fetchModels: (connection) => window.maka.connections.fetchModels(connection, host),
-    hasSecret: (connection) => window.maka.connections.hasSecret(connection, host),
-    getRequestHeaders: (connection) => window.maka.connections.getRequestHeaders(connection, host),
-    setRequestHeaders: (connection, headers) =>
-      window.maka.connections.setRequestHeaders(connection, headers, host),
-    subscribeEvents: (handler) =>
-      window.maka.connections.subscribeEvents(handler, host),
-  };
-}
-
 type RuntimeHostOAuthBridge = MakaBridge['openAiCodex'] | MakaBridge['xaiOAuth'];
 
 export function runtimeHostOAuthAuthorizationBridge(
diff --git a/apps/desktop/src/renderer/settings/settings-surface.tsx b/apps/desktop/src/renderer/settings/settings-surface.tsx
index 9b29393de4..73517879bf 100644
--- a/apps/desktop/src/renderer/settings/settings-surface.tsx
+++ b/apps/desktop/src/renderer/settings/settings-surface.tsx
@@ -95,9 +95,10 @@ import { WebSearchSettingsPage } from './web-search-settings-page';
 import type { UiLocaleUpdateGate } from './ui-locale-update-gate';
 import { getSettingsSharedCopy } from '../locales/settings-shared-copy.js';
 import {
-  runtimeHostConnectionsBridge,
+  ConnectionSettingsServicesConsumer,
+  type ConnectionSettingsServices,
   type RuntimeHostSettingsConnectionsBridge,
-} from './runtime-host-settings-bridge.js';
+} from '../features/connection-settings';
 import {
   hasRuntimeHostSettingsPatch,
   projectClientOwnedSettings,
@@ -148,7 +149,7 @@ function readyRuntimeHost(
   return { profileId: entry.profile.id, hostId: entry.hostId };
 }
 
-export function SettingsSurface(props: {
+type SettingsSurfaceProps = {
   onClose(): void;
   themePref: ThemePreference;
   onThemeChange(pref: ThemePreference): void;
@@ -171,7 +172,26 @@ export function SettingsSurface(props: {
   onRemoteHostAdded(profileId: string): void;
   onSelectedRuntimeHostProfileIdChange(profileId: string | undefined): void;
   snapshotCache?: SettingsSnapshotCache;
-}) {
+};
+
+export function SettingsSurface(props: SettingsSurfaceProps) {
+  return (
+    <ConnectionSettingsServicesConsumer>
+      {(connectionSettingsServices) => (
+        <SettingsSurfaceContent
+          {...props}
+          connectionSettingsServices={connectionSettingsServices}
+        />
+      )}
+    </ConnectionSettingsServicesConsumer>
+  );
+}
+
+function SettingsSurfaceContent(
+  props: SettingsSurfaceProps & {
+    readonly connectionSettingsServices: ConnectionSettingsServices;
+  },
+) {
   const locale = useUiLocale();
   const copy = getSettingsSharedCopy(locale);
   const localizedNav = groupedNav(locale);
@@ -348,12 +368,14 @@ export function SettingsSurface(props: {
     ),
     [runtimeHostLifecycleByProfile, runtimeHosts, selectedProfileId],
   );
-  const connectionsBridge = useMemo(
+  const connectionBridges = useMemo(
     () => selectedRuntimeHost
-      ? runtimeHostConnectionsBridge(selectedRuntimeHost)
+      ? props.connectionSettingsServices.forHost(selectedRuntimeHost)
       : undefined,
-    [selectedRuntimeHost],
+    [props.connectionSettingsServices, selectedRuntimeHost],
   );
+  const connectionsBridge = connectionBridges?.connections;
+  const apiKeyOnboardingBridge = connectionBridges?.apiKeyOnboarding;
   const selectedRuntimeHostKey = selectedRuntimeHost
     ? runtimeHostSettingsKey(selectedRuntimeHost)
     : undefined;
@@ -1015,6 +1037,7 @@ export function SettingsSurface(props: {
                             }
                             connections={connections}
                             connectionsBridge={connectionsBridge}
+                            apiKeyOnboardingBridge={apiKeyOnboardingBridge}
                             defaultSlug={defaultSlug}
                             runtimeHost={selectedRuntimeHost}
                             runtimeHostAvailabilityStatus={runtimeHostAvailabilityStatus}
@@ -1070,6 +1093,9 @@ function SettingsPageBody(props: {
   usageStats: UsageStats | null;
   connections: ProjectedLlmConnection[];
   connectionsBridge: RuntimeHostSettingsConnectionsBridge | undefined;
+  apiKeyOnboardingBridge:
+    | ReturnType<ConnectionSettingsServices['forHost']>['apiKeyOnboarding']
+    | undefined;
   defaultSlug: string | null;
   runtimeHost: DesktopRuntimeHostRef | undefined;
   runtimeHostAvailabilityStatus: RuntimeHostAvailabilityStatus;
@@ -1116,6 +1142,7 @@ function SettingsPageBody(props: {
         <SettingsPage className="settingsModelsPage">
           <ProvidersPanel
             bridge={props.connectionsBridge}
+            apiKeyOnboardingBridge={props.apiKeyOnboardingBridge}
             initialPage={props.openProviderCatalog ? 'catalog' : 'connections'}
             initialConnectionSlug={props.initialConnectionSlug}
             initialCreateProviderType={props.initialCreateProviderType}
diff --git a/apps/desktop/src/renderer/settings/use-connection-detail.ts b/apps/desktop/src/renderer/settings/use-connection-detail.ts
index dcbbfb709b..75ac12bf57 100644
--- a/apps/desktop/src/renderer/settings/use-connection-detail.ts
+++ b/apps/desktop/src/renderer/settings/use-connection-detail.ts
@@ -46,7 +46,6 @@ import {
   providerSupportsModelDiscovery,
 } from '@maka/core/llm-connections';
 import { useMountedRef, useToast, useUiLocale } from '@maka/ui';
-import { getProviderSettingsCopy } from '../locales/settings-provider-copy';
 import { connectionChipStatus } from './provider-connection-status';
 import { relayProfileDraftReseedPlan, relayProfileDraftSeed } from './relay-profile-draft';
 import { applyBulkThinkingLevel, relayProfileWithThinkingLevels } from './relay-thinking-bulk';
@@ -58,10 +57,11 @@ import type {
 import {
   connectionLastTestMessageDisplay,
   connectionTestFailureMessage,
+  getProviderSettingsCopy,
   providerPanelActionErrorMessage,
   type ConnectionsBridge,
   type CredentialPresenceStatus,
-} from './provider-panel-shared';
+} from '../features/connection-settings';
 import {
   useRuntimeHostSettingsErrorReporter,
   useRuntimeHostSettingsTarget,
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 42ab18ee47..034b16d365 100644
--- a/apps/desktop/src/renderer/settings/use-oauth-login-flow.ts
+++ b/apps/desktop/src/renderer/settings/use-oauth-login-flow.ts
@@ -26,7 +26,7 @@ import {
   useUiLocale,
 } from '@maka/ui';
 import { createOneShotActionGuard, teardownPendingAuthorization } from './oauth-login-flow-guard';
-import { getProviderSettingsCopy } from '../locales/settings-provider-copy';
+import { getProviderSettingsCopy } from '../features/connection-settings';
 import { useRuntimeHostSettingsErrorReporter } from './runtime-host-settings-target.js';
 
 // Shared browser-assisted OAuth login-flow controller (device-code polling).
diff --git a/apps/desktop/stories/settings/provider-settings.stories.tsx b/apps/desktop/stories/settings/provider-settings.stories.tsx
index 39649ee0e4..41953596da 100644
--- a/apps/desktop/stories/settings/provider-settings.stories.tsx
+++ b/apps/desktop/stories/settings/provider-settings.stories.tsx
@@ -21,20 +21,20 @@ import { useEffect, useRef, type ReactNode } from 'react';
 import { resolveConnectionModelCatalog } from '@maka/core/model-catalog';
 import type { ProjectedLlmConnection } from '@maka/core/llm-connections';
 import type { Meta, StoryObj } from '@storybook/react-vite';
-import { expect, userEvent, within } from 'storybook/test';
+import { expect, userEvent, waitFor, within } from 'storybook/test';
 import { Layout, LayoutContent, LayoutHeader } from '@astryxdesign/core';
 import { ToastProvider } from '@maka/ui';
 import type {
   ConnectionTestResult,
   IdentifiedLlmConnection,
   LlmConnection,
-  ModelDiscoveryResult,
   ProviderType,
 } from '@maka/core/llm-connections';
 import { buildChatModelChoices } from '@maka/core/chat-model-choice';
 import { ProvidersPanel, type ConnectionsBridge } from '../../src/renderer/settings/providers-panel';
 import { RuntimeHostSettingsTarget } from '../../src/renderer/settings/runtime-host-settings-target';
 import { SettingsPage } from '../../src/renderer/settings/settings-section';
+import type { ApiKeyOnboardingBridge } from '../../src/renderer/features/connection-settings';
 
 const NOW = Date.parse('2026-07-01T08:00:00Z');
 
@@ -247,16 +247,24 @@ const oauthConnections = [
   }),
 ];
 
+interface StoryConnectionsBridge extends ConnectionsBridge {
+  addFixtureConnection(connection: ProjectedLlmConnection): void;
+}
+
 function createBridge(input: {
   connections?: ProjectedLlmConnection[];
   defaultSlug?: string | null;
   failLoad?: boolean;
   loading?: boolean;
-}): ConnectionsBridge {
+}): StoryConnectionsBridge {
   let connections = [...(input.connections ?? [])];
   let defaultSlug: string | null = input.defaultSlug ?? connections[0]?.slug ?? null;
 
   return {
+    addFixtureConnection(connection) {
+      connections = [...connections, connection];
+      defaultSlug ??= connection.slug;
+    },
     async getSnapshot() {
       if (input.loading) return new Promise<never>(() => undefined);
       if (input.failLoad) throw new Error('模型连接服务暂时不可用');
@@ -318,14 +326,13 @@ function createBridge(input: {
       }
       return { ok: true, latencyMs: 328, modelTested: 'glm-4.7' };
     },
-    async fetchModels(identity): Promise<ModelDiscoveryResult> {
+    async fetchModels(identity) {
       return {
         models: [
           { id: identity.slug.includes('openai') ? 'gpt-5' : 'glm-4.7' },
           { id: identity.slug.includes('openai') ? 'gpt-4o' : 'glm-4.6' },
         ],
         source: 'fetched',
-        fetchedAt: NOW,
       };
     },
     async hasSecret() {
@@ -343,6 +350,112 @@ function createBridge(input: {
   };
 }
 
+function createApiKeyOnboardingFixture(options: {
+  save?: 'saved' | 'outcome_unknown' | 'auth_failed';
+  failRefreshAfterSave?: boolean;
+  emptyCatalog?: boolean;
+} = {}) {
+  const bridge = createBridge({
+    connections: options.emptyCatalog
+      ? []
+      : [
+          ...configuredConnections,
+          makeConnection({
+            slug: 'deepseek',
+            name: 'DeepSeek',
+            providerType: 'deepseek',
+            defaultModel: 'deepseek-chat',
+          }),
+        ],
+    defaultSlug: options.emptyCatalog ? undefined : 'zai-live',
+  });
+  let failNextSnapshot = false;
+  const projectedBridge: ConnectionsBridge = {
+    ...bridge,
+    async getSnapshot() {
+      if (failNextSnapshot) {
+        failNextSnapshot = false;
+        throw new Error('模型连接服务暂时不可用');
+      }
+      return bridge.getSnapshot();
+    },
+  };
+  let uncertainAttemptId: number | undefined;
+  let nextAttemptId = 1;
+  const uncertaintyListeners = new Set<() => void>();
+  const settleUncertainty = (attemptId: number) => {
+    if (uncertainAttemptId !== attemptId) return;
+    uncertainAttemptId = undefined;
+    for (const listener of [...uncertaintyListeners]) listener();
+  };
+  const apiKeyOnboardingBridge: ApiKeyOnboardingBridge = {
+    saveUncertainty: {
+      getSnapshot: () => uncertainAttemptId !== undefined,
+      subscribe: (listener) => {
+        uncertaintyListeners.add(listener);
+        return () => uncertaintyListeners.delete(listener);
+      },
+      restart: () => {
+        if (uncertainAttemptId === undefined) return;
+        uncertainAttemptId = undefined;
+        for (const listener of [...uncertaintyListeners]) listener();
+      },
+    },
+    async verify() {
+      return {
+        kind: 'verified',
+        models: [
+          { id: 'deepseek-reasoner', displayName: 'DeepSeek Reasoner' },
+          { id: 'deepseek-chat', displayName: 'DeepSeek Chat' },
+        ],
+      };
+    },
+    async save() {
+      const attemptId = nextAttemptId++;
+      const wasUncertain = uncertainAttemptId !== undefined;
+      uncertainAttemptId = attemptId;
+      if (!wasUncertain) {
+        for (const listener of [...uncertaintyListeners]) listener();
+      }
+      if (options.save === 'outcome_unknown') return { kind: 'outcome_unknown' };
+      if (options.save === 'auth_failed') {
+        settleUncertainty(attemptId);
+        return {
+          kind: 'result',
+          result: { kind: 'failed', errorClass: 'auth' },
+        };
+      }
+      const connection = makeConnection({
+        slug: 'deepseek-2',
+        name: 'DeepSeek',
+        providerType: 'deepseek',
+        defaultModel: '',
+        models: [
+          { id: 'deepseek-reasoner', displayName: 'DeepSeek Reasoner' },
+          { id: 'deepseek-chat', displayName: 'DeepSeek Chat' },
+        ],
+        modelSource: 'fetched',
+      });
+      bridge.addFixtureConnection(connection);
+      failNextSnapshot = options.failRefreshAfterSave === true;
+      settleUncertainty(attemptId);
+      return {
+        kind: 'result',
+        result: {
+          kind: 'saved',
+          connection: {
+            connectionId: connection.connectionId,
+            revision: 1,
+            slug: connection.slug,
+            providerType: connection.providerType,
+          },
+        },
+      };
+    },
+  };
+  return { bridge: projectedBridge, apiKeyOnboardingBridge };
+}
+
 function createOAuthSuccessLifecycleFixture() {
   const bridge = createBridge({ connections: oauthConnections });
   let eventHandler: (() => void) | undefined;
@@ -451,6 +564,7 @@ function browserSubscriptionFixture(state: {
 
 function ProviderStoryFrame(props: {
   bridge: ConnectionsBridge;
+  apiKeyOnboardingBridge?: ApiKeyOnboardingBridge;
   autoOpen?: AutoOpenTarget;
   onOAuthComplete?: () => void;
 }) {
@@ -513,7 +627,10 @@ function ProviderStoryFrame(props: {
             content={(
               <LayoutContent padding={6} isScrollable={false}>
                 <SettingsPage className="settingsModelsPage">
-                  <ProvidersPanel bridge={props.bridge} />
+                  <ProvidersPanel
+                    bridge={props.bridge}
+                    apiKeyOnboardingBridge={props.apiKeyOnboardingBridge}
+                  />
                 </SettingsPage>
               </LayoutContent>
             )}
@@ -590,16 +707,31 @@ function clickAutoOpenTarget(root: HTMLElement, target: AutoOpenTarget): boolean
 
 function ProviderStory(props: {
   bridge: ConnectionsBridge;
+  apiKeyOnboardingBridge?: ApiKeyOnboardingBridge;
   autoOpen?: AutoOpenTarget;
   onOAuthComplete?: () => void;
 }): ReactNode {
   return (
     <RuntimeHostSettingsTarget host={{ profileId: 'local', hostId: 'storybook-local-host' }}>
-      <ProviderStoryFrame bridge={props.bridge} autoOpen={props.autoOpen} onOAuthComplete={props.onOAuthComplete} />
+      <ProviderStoryFrame
+        bridge={props.bridge}
+        apiKeyOnboardingBridge={props.apiKeyOnboardingBridge}
+        autoOpen={props.autoOpen}
+        onOAuthComplete={props.onOAuthComplete}
+      />
     </RuntimeHostSettingsTarget>
   );
 }
 
+async function findApiKeyInput(canvasElement: HTMLElement): Promise<HTMLInputElement> {
+  let input: HTMLInputElement | null = null;
+  await waitFor(() => {
+    input = canvasElement.querySelector<HTMLInputElement>('input[type="password"]');
+    expect(input).not.toBeNull();
+  }, { timeout: 5_000 });
+  return input!;
+}
+
 // Real path: same page with several healthy connections and one of them set as default.
 export const ConfiguredProviders: Story = {
   render: () => <ProviderStory bridge={createBridge({ connections: configuredConnections, defaultSlug: 'zai-live' })} />,
@@ -772,3 +904,180 @@ export const AddProvider: Story = {
     />
   ),
 };
+
+// Real path: 设置 → 模型 → 添加连接 → DeepSeek. The common fixed-endpoint
+// API-key path is Host-owned before any write happens.
+export const ApiKeyOnboardingInput: Story = {
+  render: () => {
+    const fixture = createApiKeyOnboardingFixture();
+    return (
+      <ProviderStory
+        bridge={fixture.bridge}
+        apiKeyOnboardingBridge={fixture.apiKeyOnboardingBridge}
+        autoOpen="add"
+      />
+    );
+  },
+};
+
+export const ApiKeyOnboardingModels: Story = {
+  render: () => {
+    const fixture = createApiKeyOnboardingFixture();
+    return (
+      <ProviderStory
+        bridge={fixture.bridge}
+        apiKeyOnboardingBridge={fixture.apiKeyOnboardingBridge}
+        autoOpen="add"
+      />
+    );
+  },
+  play: async ({ canvasElement }) => {
+    const canvas = within(canvasElement);
+    await userEvent.type(await findApiKeyInput(canvasElement), 'sk-storybook');
+    await userEvent.click(await canvas.findByRole('button', { name: '验证并选择模型' }));
+    await expect(canvas.findByText('选择此连接使用的模型')).resolves.toBeTruthy();
+    await expect(canvas.findByRole('button', { name: '添加连接' })).resolves.toBeTruthy();
+  },
+};
+
+export const ApiKeyOnboardingBackInvalidatesVerification: Story = {
+  render: () => {
+    const fixture = createApiKeyOnboardingFixture();
+    return (
+      <ProviderStory
+        bridge={fixture.bridge}
+        apiKeyOnboardingBridge={fixture.apiKeyOnboardingBridge}
+        autoOpen="add"
+      />
+    );
+  },
+  play: async ({ canvasElement }) => {
+    const canvas = within(canvasElement);
+    const key = await findApiKeyInput(canvasElement);
+    await userEvent.type(key, 'sk-first');
+    await userEvent.click(await canvas.findByRole('button', { name: '验证并选择模型' }));
+    await userEvent.click(await canvas.findByRole('button', { name: '返回修改' }));
+    await userEvent.clear(await findApiKeyInput(canvasElement));
+    await userEvent.type(await findApiKeyInput(canvasElement), 'sk-second');
+    await expect(canvas.queryByText('选择此连接使用的模型')).toBeNull();
+    await expect(canvas.findByRole('button', { name: '验证并选择模型' })).resolves.toBeTruthy();
+  },
+};
+
+export const ApiKeyOnboardingAdoptsExactConnection: Story = {
+  render: () => {
+    const fixture = createApiKeyOnboardingFixture();
+    return (
+      <ProviderStory
+        bridge={fixture.bridge}
+        apiKeyOnboardingBridge={fixture.apiKeyOnboardingBridge}
+        autoOpen="add"
+      />
+    );
+  },
+  play: async ({ canvasElement }) => {
+    const canvas = within(canvasElement);
+    await userEvent.type(await findApiKeyInput(canvasElement), 'sk-storybook');
+    await userEvent.click(await canvas.findByRole('button', { name: '验证并选择模型' }));
+    await userEvent.click(await canvas.findByRole('button', { name: '添加连接' }));
+    await expect(canvas.findByRole('region', { name: 'DeepSeek · deepseek-2' })).resolves.toBeTruthy();
+  },
+};
+
+export const ApiKeyOnboardingRefreshWarning: Story = {
+  render: () => {
+    const fixture = createApiKeyOnboardingFixture({ failRefreshAfterSave: true });
+    return (
+      <ProviderStory
+        bridge={fixture.bridge}
+        apiKeyOnboardingBridge={fixture.apiKeyOnboardingBridge}
+        autoOpen="add"
+      />
+    );
+  },
+  play: async ({ canvasElement }) => {
+    const canvas = within(canvasElement);
+    await userEvent.type(await findApiKeyInput(canvasElement), 'sk-storybook');
+    await userEvent.click(await canvas.findByRole('button', { name: '验证并选择模型' }));
+    await userEvent.click(await canvas.findByRole('button', { name: '添加连接' }));
+    await expect(canvas.findByText('连接已添加,但暂时无法刷新连接列表。')).resolves.toBeTruthy();
+  },
+};
+
+export const ApiKeyOnboardingOutcomeUnknown: Story = {
+  render: () => {
+    const fixture = createApiKeyOnboardingFixture({ save: 'outcome_unknown' });
+    return (
+      <ProviderStory
+        bridge={fixture.bridge}
+        apiKeyOnboardingBridge={fixture.apiKeyOnboardingBridge}
+        autoOpen="add"
+      />
+    );
+  },
+  play: async ({ canvasElement }) => {
+    const canvas = within(canvasElement);
+    await userEvent.type(await findApiKeyInput(canvasElement), 'sk-storybook');
+    await userEvent.click(await canvas.findByRole('button', { name: '验证并选择模型' }));
+    await userEvent.click(await canvas.findByRole('button', { name: '添加连接' }));
+    await expect(canvas.findByText('保存结果暂时无法确认')).resolves.toBeTruthy();
+    await userEvent.click(await canvas.findByRole('button', { name: '重新加载连接列表' }));
+    await expect(canvas.findByRole('button', { name: '添加连接' })).resolves.toBeDisabled();
+    await expect(canvas.findByRole('button', { name: '仍要添加另一个连接' })).resolves.toBeTruthy();
+    const row = canvasElement.querySelector<HTMLElement>('[data-connection-slug="deepseek"]');
+    await expect(row).not.toBeNull();
+    await userEvent.click(within(row!).getByRole('button'));
+    await userEvent.click(await canvas.findByRole('button', { name: '返回模型连接' }));
+    await expect(canvas.findByText('保存结果暂时无法确认')).resolves.toBeTruthy();
+    await expect(canvas.findByRole('button', { name: '添加连接' })).resolves.toBeDisabled();
+  },
+};
+
+export const ApiKeyOnboardingOutcomeUnknownEmptyCatalog: Story = {
+  render: () => {
+    const fixture = createApiKeyOnboardingFixture({
+      save: 'outcome_unknown',
+      emptyCatalog: true,
+    });
+    return (
+      <ProviderStory
+        bridge={fixture.bridge}
+        apiKeyOnboardingBridge={fixture.apiKeyOnboardingBridge}
+        autoOpen="add"
+      />
+    );
+  },
+  play: async ({ canvasElement }) => {
+    const canvas = within(canvasElement);
+    await userEvent.type(await findApiKeyInput(canvasElement), 'sk-storybook');
+    await userEvent.click(await canvas.findByRole('button', { name: '验证并选择模型' }));
+    await userEvent.click(await canvas.findByRole('button', { name: '添加连接' }));
+    await userEvent.click(await canvas.findByRole('button', { name: '重新加载连接列表' }));
+    const addButtons = await canvas.findAllByRole('button', { name: '添加连接' });
+    for (const button of addButtons) await expect(button).toBeDisabled();
+  },
+};
+
+export const ApiKeyOnboardingSaveAuthFailure: Story = {
+  render: () => {
+    const fixture = createApiKeyOnboardingFixture({ save: 'auth_failed' });
+    return (
+      <ProviderStory
+        bridge={fixture.bridge}
+        apiKeyOnboardingBridge={fixture.apiKeyOnboardingBridge}
+        autoOpen="add"
+      />
+    );
+  },
+  play: async ({ canvasElement }) => {
+    const canvas = within(canvasElement);
+    await userEvent.type(await findApiKeyInput(canvasElement), 'sk-storybook');
+    await userEvent.click(await canvas.findByRole('button', { name: '验证并选择模型' }));
+    await userEvent.click(await canvas.findByRole('button', { name: '添加连接' }));
+    const key = await findApiKeyInput(canvasElement);
+    await waitFor(() => {
+      expect(canvas.queryAllByText('密钥验证失败,请检查后重试。')).toHaveLength(1);
+    });
+    await expect(key).toHaveValue('sk-storybook');
+  },
+};
diff --git a/apps/desktop/stories/settings/settings-pages.stories.tsx b/apps/desktop/stories/settings/settings-pages.stories.tsx
index 8fcc2fa5a3..5195fe534f 100644
--- a/apps/desktop/stories/settings/settings-pages.stories.tsx
+++ b/apps/desktop/stories/settings/settings-pages.stories.tsx
@@ -55,6 +55,8 @@ import { buildHealthSnapshot } from '@maka/core/health';
 import { createDefaultSettings, mergeSettings } from '@maka/core/settings';
 import { DEFAULT_DAILY_REVIEW_CONFIG } from '@maka/core/daily-review';
 import { SettingsSurface } from '../../src/renderer/settings/settings-surface';
+import { ConnectionSettingsServicesProvider } from '../../src/renderer/features/connection-settings';
+import { createDesktopConnectionSettingsServices } from '../../src/renderer/platform/desktop/create-connection-settings-services';
 import { createUiLocaleUpdateGate } from '../../src/renderer/settings/ui-locale-update-gate';
 import {
   createSettingsSnapshotCache,
@@ -1599,6 +1601,9 @@ function SettingsStoryFrame(props: SettingsStoryProps) {
   // holds both in AppShell state and applies them optimistically on click.
   const [themePref, setThemePref] = useState<ThemePreference>('auto');
   const [themePalette, setThemePalette] = useState<ThemePalette>('default');
+  const [connectionSettingsServices] = useState(
+    createDesktopConnectionSettingsServices,
+  );
 
   return (
     <>
@@ -1616,27 +1621,29 @@ function SettingsStoryFrame(props: SettingsStoryProps) {
           minHeight: 640,
         }}
       >
-        <SettingsSurface
-          onClose={noop}
-          themePref={themePref}
-          onThemeChange={setThemePref}
-          themePalette={themePalette}
-          onThemePaletteChange={setThemePalette}
-          onUiLocalePreferenceChange={noop}
-          uiLocaleUpdateGate={uiLocaleUpdateGate}
-          onDefaultPermissionModeChange={noop}
-          request={{ section: props.section }}
-          openProviderCatalog={props.openProviderCatalog}
-          initialConnectionSlug={props.initialConnectionSlug}
-          initialFocusRef={initialFocusRef}
-          onOpenDailyReview={noop}
-          onOpenSession={noop}
-          archivedTasks={archivedTasks}
-          onTaskImported={noop}
-          onRemoteHostAdded={noop}
-          onSelectedRuntimeHostProfileIdChange={noop}
-          snapshotCache={snapshotCache}
-        />
+        <ConnectionSettingsServicesProvider services={connectionSettingsServices}>
+          <SettingsSurface
+            onClose={noop}
+            themePref={themePref}
+            onThemeChange={setThemePref}
+            themePalette={themePalette}
+            onThemePaletteChange={setThemePalette}
+            onUiLocalePreferenceChange={noop}
+            uiLocaleUpdateGate={uiLocaleUpdateGate}
+            onDefaultPermissionModeChange={noop}
+            request={{ section: props.section }}
+            openProviderCatalog={props.openProviderCatalog}
+            initialConnectionSlug={props.initialConnectionSlug}
+            initialFocusRef={initialFocusRef}
+            onOpenDailyReview={noop}
+            onOpenSession={noop}
+            archivedTasks={archivedTasks}
+            onTaskImported={noop}
+            onRemoteHostAdded={noop}
+            onSelectedRuntimeHostProfileIdChange={noop}
+            snapshotCache={snapshotCache}
+          />
+        </ConnectionSettingsServicesProvider>
       </div>
     </>
   );
diff --git a/docs/astryx-surface-file-inventory.md b/docs/astryx-surface-file-inventory.md
index f6ed5c7566..84509c2cb2 100644
--- a/docs/astryx-surface-file-inventory.md
+++ b/docs/astryx-surface-file-inventory.md
@@ -6,7 +6,7 @@ Generated against `@astryxdesign/core@0.5.2` (194 component exports).
 
 Wiki bar: Design Conventions · API Use-the-System · Theming · Container Padding.
 
-**Totals:** 235 files — blocker 0, reimplementation 0, polish 1, aligned 234.
+**Totals:** 236 files — blocker 0, reimplementation 0, polish 1, aligned 235.
 
 ## Exclusions (explicit)
 
@@ -42,6 +42,7 @@ Wiki bar: Design Conventions · API Use-the-System · Theming · Container Paddi
 | `apps/desktop/src/renderer/composition/desktop-feature-services.tsx` | other | none | aligned — no raw controls; no Astryx JSX usage | aligned |
 | `apps/desktop/src/renderer/custom-pet-companion.tsx` | other | none | aligned — no raw controls; no Astryx JSX usage | aligned |
 | `apps/desktop/src/renderer/error-boundary.tsx` | other | Button, Card | aligned — uses Astryx (Button, Card) | aligned |
+| `apps/desktop/src/renderer/features/connection-settings/services-context.tsx` | other | none | aligned — no raw controls; no Astryx JSX usage | aligned |
 | `apps/desktop/src/renderer/features/goals/services-context.tsx` | other | none | aligned — no raw controls; no Astryx JSX usage | aligned |
 | `apps/desktop/src/renderer/features/goals/ui/goal-dialog.tsx` | dialog-overlay | Button, Dialog, DialogHeader, HStack, Layout, LayoutContent, LayoutFooter, Text, TextArea, TextInput, VStack | aligned — uses Astryx (Button, Dialog, DialogHeader, HStack, Layout, LayoutContent, LayoutFooter, Text) | aligned |
 | `apps/desktop/src/renderer/features/goals/ui/goal-host.tsx` | other | none | aligned — no raw controls; no Astryx JSX usage | aligned |
@@ -103,7 +104,7 @@ Wiki bar: Design Conventions · API Use-the-System · Theming · Container Paddi
 | `apps/desktop/src/renderer/settings/permission-center-page.tsx` | settings-page | Banner, Button, Collapsible, CollapsibleGroup, HStack, List, ListItem, MetadataList, MetadataListItem, StatusDot, Text, VStack | aligned — uses Astryx (Banner, Button, Collapsible, CollapsibleGroup, HStack, List, ListItem, MetadataList) | aligned |
 | `apps/desktop/src/renderer/settings/personalization-settings-section.tsx` | settings-module | SegmentedControl, SegmentedControlItem, TextArea, TextInput | aligned — uses Astryx (SegmentedControl, SegmentedControlItem, TextArea, TextInput) | aligned |
 | `apps/desktop/src/renderer/settings/projects-settings-page.tsx` | settings-page | Badge, Banner, Button, EmptyState, HStack, List, ListItem, MoreMenu, TextInput | aligned — uses Astryx (Badge, Banner, Button, EmptyState, HStack, List, ListItem, MoreMenu) | aligned |
-| `apps/desktop/src/renderer/settings/provider-add-form.tsx` | settings-module | Banner, Button, Collapsible, FormLayout, HStack, TextInput, VStack | aligned — uses Astryx (Banner, Button, Collapsible, FormLayout, HStack, TextInput, VStack) | aligned |
+| `apps/desktop/src/renderer/settings/provider-add-form.tsx` | settings-module | Banner, Button, Collapsible, FormLayout, HStack, MultiSelector, Text, TextInput, VStack | aligned — uses Astryx (Banner, Button, Collapsible, FormLayout, HStack, MultiSelector, Text, TextInput) | aligned |
 | `apps/desktop/src/renderer/settings/provider-add-model-dialog.tsx` | settings-module | Button, Dialog, DialogHeader, FormLayout, HStack, Layout, LayoutContent, LayoutFooter, NumberInput, TextInput | aligned — uses Astryx (Button, Dialog, DialogHeader, FormLayout, HStack, Layout, LayoutContent, LayoutFooter) | aligned |
 | `apps/desktop/src/renderer/settings/provider-brand-marks.tsx` | settings-module | none | aligned — no raw controls; no Astryx JSX usage | aligned |
 | `apps/desktop/src/renderer/settings/provider-catalog-page.tsx` | settings-page | Banner, Button, EmptyState, HStack, List, ListItem, Section, Selector, TextInput, VStack | aligned — uses Astryx (Banner, Button, EmptyState, HStack, List, ListItem, Section, Selector) | aligned |
diff --git a/docs/astryx-surface-file-inventory.paths b/docs/astryx-surface-file-inventory.paths
index 1371e38522..cbd8244e02 100644
--- a/docs/astryx-surface-file-inventory.paths
+++ b/docs/astryx-surface-file-inventory.paths
@@ -13,6 +13,7 @@ apps/desktop/src/renderer/composer-mentions.tsx
 apps/desktop/src/renderer/composition/desktop-feature-services.tsx
 apps/desktop/src/renderer/custom-pet-companion.tsx
 apps/desktop/src/renderer/error-boundary.tsx
+apps/desktop/src/renderer/features/connection-settings/services-context.tsx
 apps/desktop/src/renderer/features/goals/services-context.tsx
 apps/desktop/src/renderer/features/goals/ui/goal-dialog.tsx
 apps/desktop/src/renderer/features/goals/ui/goal-host.tsx


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