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:07:26 UTC
(maka) 03/10: refactor(desktop): move connection settings behind feature adapter
This is an automated email from the ASF dual-hosted git repository.
me2seeks pushed a commit to branch feat/desktop-api-key-host-onboarding
in repository https://gitbox.apache.org/repos/asf/maka.git
commit de03a7d024e49affca8022a8d684dd08c12d3f0a
Author: me2seeks <me...@gmail.com>
AuthorDate: Tue Sep 1 01:02:00 2026 +0800
refactor(desktop): move connection settings behind feature adapter
---
.../e2e/request-header-row-contract.spec.ts | 2 +-
apps/desktop/renderer-architecture.json | 78 +++-----------
.../main/__tests__/provider-add-submission.test.ts | 63 +++++++++++-
.../contracts/connection-error-cleaner.ts | 37 +++++++
.../composition/desktop-feature-services.tsx | 13 ++-
.../renderer/features/connection-settings/index.ts | 44 ++++++++
.../renderer/features/connection-settings/ports.ts | 92 +++++++++++++++++
.../connection-settings}/provider-panel-shared.ts | 112 +--------------------
.../connection-settings/services-context.tsx | 46 +++++++++
.../connection-settings}/settings-provider-copy.ts | 0
.../src/renderer/model-connection-errors.ts | 31 ++----
.../desktop/create-connection-settings-services.ts | 69 +++++++++++++
.../renderer/settings/general-settings-page.tsx | 2 +-
.../src/renderer/settings/provider-add-form.tsx | 4 +-
.../settings/provider-add-model-dialog.tsx | 2 +-
.../renderer/settings/provider-catalog-page.tsx | 4 +-
.../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 | 6 +-
.../settings/runtime-host-settings-bridge.ts | 60 -----------
.../src/renderer/settings/settings-surface.tsx | 47 ++++++---
.../src/renderer/settings/use-connection-detail.ts | 4 +-
.../src/renderer/settings/use-oauth-login-flow.ts | 2 +-
.../stories/settings/provider-settings.stories.tsx | 2 +-
.../stories/settings/settings-pages.stories.tsx | 49 +++++----
27 files changed, 457 insertions(+), 322 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 d74d340707..a6f5266c52 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": 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,22 +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.saveOnboarding": 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,
- "window.maka.connections.verifyOnboarding": 1
- },
+ "bridgePaths": {},
"environmentCapabilities": {},
"hookCalls": {},
"lifecycleMethods": {},
@@ -3988,7 +3940,6 @@
"actionFactories": [],
"dependencyPaths": {
"../../preload/bridge-contract.js": 1,
- "./provider-panel-shared.js": 1,
"./use-oauth-login-flow.js": 1
}
},
@@ -4280,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,
@@ -4295,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,
@@ -4494,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,
@@ -4576,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 05e1689cc2..7abf64b8ae 100644
--- a/apps/desktop/src/main/__tests__/provider-add-submission.test.ts
+++ b/apps/desktop/src/main/__tests__/provider-add-submission.test.ts
@@ -29,7 +29,7 @@ import {
type AddProviderDraft,
type AddProviderField,
} from '../../renderer/settings/provider-add-submission.js';
-import { runtimeHostApiKeyOnboardingBridge } from '../../renderer/settings/runtime-host-settings-bridge.js';
+import { createDesktopConnectionSettingsServices } from '../../renderer/platform/desktop/create-connection-settings-services.js';
import {
PROVIDER_REGISTRY,
providerSupportsModelDiscovery,
@@ -277,17 +277,22 @@ test('a remounted setup becomes uncertain only after save dispatch', () => {
});
test('a late old save cannot clear the uncertainty lease for a newer attempt', () => {
- const firstView = runtimeHostApiKeyOnboardingBridge({
+ const services = createDesktopConnectionSettingsServices(() => ({
+ // This test exercises only the in-process attempt lease. Transport calls
+ // remain behind the adapter and are intentionally unavailable here.
+ connections: {},
+ } as unknown as ReturnType<NonNullable<Parameters<typeof createDesktopConnectionSettingsServices>[0]>>));
+ const firstView = services.forHost({
profileId: 'local',
hostId: 'same-host',
- });
+ }).apiKeyOnboarding;
const oldAttempt = firstView.saveUncertainty.markDispatched();
firstView.saveUncertainty.restart();
- const replacementView = runtimeHostApiKeyOnboardingBridge({
+ const replacementView = services.forHost({
profileId: 'local',
hostId: 'same-host',
- });
+ }).apiKeyOnboarding;
const newAttempt = replacementView.saveUncertainty.markDispatched();
firstView.saveUncertainty.settle(oldAttempt);
assert.equal(replacementView.saveUncertainty.isUncertain(), true);
@@ -295,3 +300,51 @@ test('a late old save cannot clear the uncertainty lease for a newer attempt', (
replacementView.saveUncertainty.settle(newAttempt);
assert.equal(firstView.saveUncertainty.isUncertain(), false);
});
+
+test('save uncertainty is isolated between Runtime Host targets', () => {
+ const services = createDesktopConnectionSettingsServices(() => ({
+ connections: {},
+ } 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;
+
+ firstHost.saveUncertainty.markDispatched();
+
+ assert.equal(firstHost.saveUncertainty.isUncertain(), true);
+ assert.equal(secondHost.saveUncertainty.isUncertain(), false);
+});
+
+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/renderer/application/contracts/connection-error-cleaner.ts b/apps/desktop/src/renderer/application/contracts/connection-error-cleaner.ts
new file mode 100644
index 0000000000..05956ede6d
--- /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);
+}
+
+export 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..d58839002d
--- /dev/null
+++ b/apps/desktop/src/renderer/features/connection-settings/index.ts
@@ -0,0 +1,44 @@
+/*
+ * 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,
+} from './services-context';
+export type {
+ ApiKeyOnboardingBridge,
+ ConnectionSettingsServices,
+ ConnectionSettingsHost,
+ ConnectionsBridge,
+ DesktopConnectionOnboardingIdentity,
+ RuntimeHostSettingsConnectionsBridge,
+} from './ports';
+export {
+ categoryLabel,
+ connectionLastTestMessageDisplay,
+ connectionTestFailureFallback,
+ connectionTestFailureMessage,
+ providerPanelActionErrorMessage,
+} from './provider-panel-shared';
+export { getProviderSettingsCopy } from './settings-provider-copy';
+export type { ProviderSettingsCopy } from './settings-provider-copy';
+export type {
+ ConnectionTestTroubleshootingCopy,
+ CredentialPresenceStatus,
+} from './provider-panel-shared';
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..bb107c4dc4
--- /dev/null
+++ b/apps/desktop/src/renderer/features/connection-settings/ports.ts
@@ -0,0 +1,92 @@
+/*
+ * 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<ModelDiscoveryResult>;
+ 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: {
+ isUncertain(): boolean;
+ markDispatched(): number;
+ settle(attemptId: number): 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 55%
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 0a72af26b8..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,119 +20,11 @@
import { generalizedErrorMessage, generalizedErrorMessageChinese, redactSecrets } from '@maka/core/redaction';
import {
type ConnectionTestResult,
- type CreateConnectionInput,
- type IdentifiedLlmConnection,
- type LlmConnection,
- type ModelDiscoveryResult,
- type ModelInfo,
- 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 type DesktopConnectionOnboardingIdentity = {
- readonly connectionId: string;
- readonly revision: number;
- readonly slug: string;
- readonly providerType: ProviderType;
-};
-
-export type ApiKeyOnboardingTarget =
- | { readonly kind: 'create'; readonly providerType: ProviderType }
- | { readonly kind: 'existing'; readonly connectionId: string };
-
-export type ApiKeyOnboardingVerifyResult =
- | { readonly kind: 'verified'; readonly models: readonly ModelInfo[] }
- | {
- readonly kind: 'rejected';
- readonly reason:
- | 'provider_unsupported'
- | 'connection_not_found'
- | 'credential_not_configured'
- | 'base_url_not_configured'
- | 'catalog_full';
- }
- | {
- readonly kind: 'failed';
- readonly errorClass:
- | 'auth'
- | 'timeout'
- | 'provider_unavailable'
- | 'network'
- | 'invalid_response'
- | 'unknown';
- };
-
-export type ApiKeyOnboardingSaveResult =
- | { readonly kind: 'saved'; readonly connection: DesktopConnectionOnboardingIdentity }
- | {
- readonly kind: 'rejected';
- readonly reason:
- | 'provider_unsupported'
- | 'connection_not_found'
- | 'credential_not_configured'
- | 'base_url_not_configured'
- | 'catalog_full'
- | 'model_unavailable'
- | 'superseded';
- }
- | Exclude<ApiKeyOnboardingVerifyResult, { readonly kind: 'verified' | 'rejected' }>;
-
-export interface ApiKeyOnboardingBridge {
- readonly saveUncertainty: {
- isUncertain(): boolean;
- markDispatched(): number;
- settle(attemptId: number): void;
- restart(): void;
- };
- verify(input: {
- readonly target: ApiKeyOnboardingTarget;
- readonly apiKey: string | null;
- readonly baseUrl: string | null;
- }): Promise<ApiKeyOnboardingVerifyResult>;
- save(input: {
- readonly target: ApiKeyOnboardingTarget;
- readonly apiKey: string | null;
- readonly baseUrl: string | null;
- readonly enabledModelIds: readonly string[];
- }): Promise<
- | { readonly kind: 'result'; readonly result: ApiKeyOnboardingSaveResult }
- | { readonly kind: 'not_saved' }
- | { readonly kind: 'outcome_unknown' }
- >;
-}
-
-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..cfa3464f83
--- /dev/null
+++ b/apps/desktop/src/renderer/features/connection-settings/services-context.tsx
@@ -0,0 +1,46 @@
+/*
+ * 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, type ReactNode } from 'react';
+import type { 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());
+}
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 100%
rename from apps/desktop/src/renderer/locales/settings-provider-copy.ts
rename to apps/desktop/src/renderer/features/connection-settings/settings-provider-copy.ts
diff --git a/apps/desktop/src/renderer/model-connection-errors.ts b/apps/desktop/src/renderer/model-connection-errors.ts
index ca0c7550d1..529c018ab2 100644
--- a/apps/desktop/src/renderer/model-connection-errors.ts
+++ b/apps/desktop/src/renderer/model-connection-errors.ts
@@ -20,13 +20,17 @@
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 {
+ cleanErrorMessage,
+ cleanEventMessage,
+ 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 { cleanErrorMessage, cleanEventMessage };
export function isNoRealConnectionError(error: unknown): boolean {
return parseNoRealConnectionError(error).matched;
@@ -73,27 +77,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..f2353febc9
--- /dev/null
+++ b/apps/desktop/src/renderer/platform/desktop/create-connection-settings-services.ts
@@ -0,0 +1,69 @@
+/*
+ * 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>();
+ let nextAttemptId = 1;
+
+ 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: {
+ isUncertain: () => uncertainTargets.has(targetKey),
+ markDispatched: () => {
+ const attemptId = nextAttemptId++;
+ uncertainTargets.set(targetKey, attemptId);
+ return attemptId;
+ },
+ settle: (attemptId) => {
+ if (uncertainTargets.get(targetKey) === attemptId) {
+ uncertainTargets.delete(targetKey);
+ }
+ },
+ restart: () => uncertainTargets.delete(targetKey),
+ },
+ verify: (input) => bridge().connections.verifyOnboarding(input, host),
+ save: (input) => bridge().connections.saveOnboarding(input, host),
+ },
+ };
+ },
+ };
+}
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 61086e68bd..f43feaec38 100644
--- a/apps/desktop/src/renderer/settings/provider-add-form.tsx
+++ b/apps/desktop/src/renderer/settings/provider-add-form.tsx
@@ -40,12 +40,12 @@ import { providerDisplay } from './provider-display';
import { useActionGuard } from './use-action-guard';
import {
categoryLabel,
+ getProviderSettingsCopy,
providerPanelActionErrorMessage,
type ApiKeyOnboardingBridge,
type ConnectionsBridge,
type DesktopConnectionOnboardingIdentity,
-} from './provider-panel-shared';
-import { getProviderSettingsCopy } from '../locales/settings-provider-copy';
+} from '../features/connection-settings';
import {
newRequestHeaders,
parseRequestBodyOverlay,
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-catalog-page.tsx b/apps/desktop/src/renderer/settings/provider-catalog-page.tsx
index a76ac5d239..36406c0fd9 100644
--- a/apps/desktop/src/renderer/settings/provider-catalog-page.tsx
+++ b/apps/desktop/src/renderer/settings/provider-catalog-page.tsx
@@ -40,11 +40,11 @@ import { AddProviderForm } from './provider-add-form';
import { ProviderLogo, providerDisplay } from './provider-display';
import { OAuthLoginPanel, useOAuthCards, type OAuthCardId } from './provider-oauth-section';
import {
+ getProviderSettingsCopy,
type ApiKeyOnboardingBridge,
type ConnectionsBridge,
type DesktopConnectionOnboardingIdentity,
-} from './provider-panel-shared';
-import { getProviderSettingsCopy } from '../locales/settings-provider-copy';
+} from '../features/connection-settings';
type CatalogCategory = ProviderCatalogGroup | 'all' | 'recommended' | 'accounts';
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..9cb7572500 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';
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 d13f20f053..99a23da49d 100644
--- a/apps/desktop/src/renderer/settings/providers-panel.tsx
+++ b/apps/desktop/src/renderer/settings/providers-panel.tsx
@@ -56,18 +56,18 @@ import { SettingsRouteHeader } from './settings-route-header';
import { ProviderLogo, providerDisplay } from './provider-display';
import { oauthPanelSubtitle } from './provider-oauth-section';
import {
+ getProviderSettingsCopy,
providerPanelActionErrorMessage,
type ApiKeyOnboardingBridge,
type ConnectionsBridge,
type DesktopConnectionOnboardingIdentity,
-} from './provider-panel-shared';
-import { getProviderSettingsCopy } from '../locales/settings-provider-copy';
+} 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:
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 0a4aa702d7..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,71 +22,11 @@ import type {
DesktopRuntimeHostRef,
MakaBridge,
} from '../../preload/bridge-contract.js';
-import type {
- ApiKeyOnboardingBridge,
- 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),
- };
-}
-
-export function runtimeHostApiKeyOnboardingBridge(
- host: DesktopRuntimeHostRef,
-): ApiKeyOnboardingBridge {
- const targetKey = `${host.profileId}\u0000${host.hostId}`;
- return {
- saveUncertainty: {
- isUncertain: () => uncertainApiKeyOnboardingTargets.has(targetKey),
- markDispatched: () => {
- const attemptId = nextApiKeyOnboardingAttemptId++;
- uncertainApiKeyOnboardingTargets.set(targetKey, attemptId);
- return attemptId;
- },
- settle: (attemptId) => {
- if (uncertainApiKeyOnboardingTargets.get(targetKey) === attemptId) {
- uncertainApiKeyOnboardingTargets.delete(targetKey);
- }
- },
- restart: () => uncertainApiKeyOnboardingTargets.delete(targetKey),
- },
- verify: (input) => window.maka.connections.verifyOnboarding(input, host),
- save: (input) => window.maka.connections.saveOnboarding(input, host),
- };
-}
-
-// A Host replacement remounts the settings projection while a dispatched save
-// may still settle. Keep the safety gate target-scoped instead of form-scoped;
-// durable reconciliation across a Desktop process restart belongs to the
-// follow-up Host journal protocol.
-const uncertainApiKeyOnboardingTargets = new Map<string, number>();
-let nextApiKeyOnboardingAttemptId = 1;
-
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 043e472409..73517879bf 100644
--- a/apps/desktop/src/renderer/settings/settings-surface.tsx
+++ b/apps/desktop/src/renderer/settings/settings-surface.tsx
@@ -95,10 +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 {
- runtimeHostApiKeyOnboardingBridge,
- runtimeHostConnectionsBridge,
+ ConnectionSettingsServicesConsumer,
+ type ConnectionSettingsServices,
type RuntimeHostSettingsConnectionsBridge,
-} from './runtime-host-settings-bridge.js';
+} from '../features/connection-settings';
import {
hasRuntimeHostSettingsPatch,
projectClientOwnedSettings,
@@ -149,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;
@@ -172,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);
@@ -349,18 +368,14 @@ export function SettingsSurface(props: {
),
[runtimeHostLifecycleByProfile, runtimeHosts, selectedProfileId],
);
- const connectionsBridge = useMemo(
- () => selectedRuntimeHost
- ? runtimeHostConnectionsBridge(selectedRuntimeHost)
- : undefined,
- [selectedRuntimeHost],
- );
- const apiKeyOnboardingBridge = useMemo(
+ const connectionBridges = useMemo(
() => selectedRuntimeHost
- ? runtimeHostApiKeyOnboardingBridge(selectedRuntimeHost)
+ ? props.connectionSettingsServices.forHost(selectedRuntimeHost)
: undefined,
- [selectedRuntimeHost],
+ [props.connectionSettingsServices, selectedRuntimeHost],
);
+ const connectionsBridge = connectionBridges?.connections;
+ const apiKeyOnboardingBridge = connectionBridges?.apiKeyOnboarding;
const selectedRuntimeHostKey = selectedRuntimeHost
? runtimeHostSettingsKey(selectedRuntimeHost)
: undefined;
@@ -1078,7 +1093,9 @@ function SettingsPageBody(props: {
usageStats: UsageStats | null;
connections: ProjectedLlmConnection[];
connectionsBridge: RuntimeHostSettingsConnectionsBridge | undefined;
- apiKeyOnboardingBridge: ReturnType<typeof runtimeHostApiKeyOnboardingBridge> | undefined;
+ apiKeyOnboardingBridge:
+ | ReturnType<ConnectionSettingsServices['forHost']>['apiKeyOnboarding']
+ | undefined;
defaultSlug: string | null;
runtimeHost: DesktopRuntimeHostRef | undefined;
runtimeHostAvailabilityStatus: RuntimeHostAvailabilityStatus;
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 444481fab1..059a388cc7 100644
--- a/apps/desktop/stories/settings/provider-settings.stories.tsx
+++ b/apps/desktop/stories/settings/provider-settings.stories.tsx
@@ -35,7 +35,7 @@ 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/settings/provider-panel-shared';
+import type { ApiKeyOnboardingBridge } from '../../src/renderer/features/connection-settings';
const NOW = Date.parse('2026-07-01T08:00:00Z');
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>
</>
);
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org