@nextclaw/ui 0.13.3 → 0.13.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/CHANGELOG.md +128 -0
  2. package/dist/assets/api-BAyyGaoz.js +15 -0
  3. package/dist/assets/app-presenter-provider-DqHHfC1K.js +3 -0
  4. package/dist/assets/arrow-left-Bn-3Pj3p.js +1 -0
  5. package/dist/assets/{boxes-BvA-I63l.js → boxes-ClVuotcz.js} +1 -1
  6. package/dist/assets/channels-list-page-BnGxv6ma.js +8 -0
  7. package/dist/assets/chat-page-DInyWLb4.js +105 -0
  8. package/dist/assets/config-split-page-CWN2YG9U.js +1 -0
  9. package/dist/assets/confirm-dialog-CVbgKuHp.js +5 -0
  10. package/dist/assets/{createLucideIcon-Br1EFMEr.js → createLucideIcon-CjPUPAAK.js} +1 -1
  11. package/dist/assets/desktop-update-config-abmYzHSy.js +1 -0
  12. package/dist/assets/{dist-rzakR_bu.js → dist-hkrb6Ynt.js} +1 -1
  13. package/dist/assets/doc-browser-BlrpUKVJ.js +1 -0
  14. package/dist/assets/doc-browser-context-Cb1sEUT0.js +1 -0
  15. package/dist/assets/doc-browser-kJ2ClG0H.js +1 -0
  16. package/dist/assets/ellipsis-CdkxFkVZ.js +1 -0
  17. package/dist/assets/{external-link-DHjFdCpy.js → external-link-CT9_qFwN.js} +1 -1
  18. package/dist/assets/index-BGXTR0F_.css +1 -0
  19. package/dist/assets/index-Ccbswzt9.js +101 -0
  20. package/dist/assets/{key-round-D41Oe35x.js → key-round-xJfR4iRe.js} +1 -1
  21. package/dist/assets/{book-open-CRotBOF2.js → loader-circle-BI6HJDg5.js} +1 -1
  22. package/dist/assets/{mcp-marketplace-page-Dvi8mCBd.js → mcp-marketplace-page-CmUKUeno.js} +2 -2
  23. package/dist/assets/mcp-marketplace-page-DH11bSVx.js +1 -0
  24. package/dist/assets/model-config-Dbo3zWDl.js +1 -0
  25. package/dist/assets/notice-card-CFTFtJky.js +1 -0
  26. package/dist/assets/play-DyBWW2j4.js +1 -0
  27. package/dist/assets/plus-BlnBmcKE.js +1 -0
  28. package/dist/assets/popover-DPqa_Mfb.js +1 -0
  29. package/dist/assets/provider-models-CXUgDxGR.js +1 -0
  30. package/dist/assets/provider-scoped-model-input-C6xL7zSJ.js +1 -0
  31. package/dist/assets/providers-list-DOj_7GL8.js +1 -0
  32. package/dist/assets/react-oAHNoVya.js +1 -0
  33. package/dist/assets/refresh-cw-o8NSmIYV.js +1 -0
  34. package/dist/assets/remote-fCE5fLsj.js +1 -0
  35. package/dist/assets/rotate-cw-DA8EqNAH.js +1 -0
  36. package/dist/assets/runtime-config-page-Dg8aEgWx.js +1 -0
  37. package/dist/assets/{save-aYdSdK97.js → save-D4-g0EAq.js} +1 -1
  38. package/dist/assets/search-G2cMn3M5.js +1 -0
  39. package/dist/assets/{search-config-G2O1fcRb.js → search-config-DAibVFGR.js} +1 -1
  40. package/dist/assets/{secrets-config-TenzxYbE.js → secrets-config-CoJzy13_.js} +2 -2
  41. package/dist/assets/{status-dot-BSMG7ZaC.js → status-dot-D8xx5ZR8.js} +1 -1
  42. package/dist/assets/{tabs-D1CjpWNX.js → tabs-DxmND60Q.js} +1 -1
  43. package/dist/assets/{tabs-custom-CRSN1ekg.js → tabs-custom-Cjo0bHw8.js} +1 -1
  44. package/dist/assets/{tag-chip-DEashiw6.js → tag-chip-DnNWFRMu.js} +1 -1
  45. package/dist/assets/{trash-2-DSWTsARW.js → trash-2-C9tc-9sQ.js} +1 -1
  46. package/dist/assets/x-DmLl2yG6.js +1 -0
  47. package/dist/index.html +24 -23
  48. package/dist/logos/mimo.svg +9 -0
  49. package/package.json +8 -8
  50. package/public/logos/mimo.svg +9 -0
  51. package/src/app/components/layout/app-layout.tsx +10 -2
  52. package/src/features/agents/components/agents-page.tsx +6 -4
  53. package/src/features/chat/hooks/use-ncp-chat-page-data.ts +17 -10
  54. package/src/features/marketplace/components/marketplace-list-card.tsx +22 -1
  55. package/src/features/marketplace/components/marketplace-page.tsx +3 -1
  56. package/src/features/marketplace/hooks/use-marketplace.ts +3 -1
  57. package/src/features/marketplace/utils/marketplace-installed-cache.utils.ts +8 -0
  58. package/src/features/right-panel-resources/configs/right-panel-resource-routes.config.ts +70 -6
  59. package/src/features/right-panel-resources/utils/right-panel-resource-route-resolver.utils.test.ts +25 -1
  60. package/src/features/right-panel-resources/utils/right-panel-resource-route-resolver.utils.ts +1 -1
  61. package/src/features/right-panel-resources/utils/right-panel-resource-uri.utils.test.ts +42 -0
  62. package/src/features/right-panel-resources/utils/right-panel-resource-uri.utils.ts +22 -0
  63. package/src/features/service-apps/components/service-apps-panel.test.tsx +88 -0
  64. package/src/features/service-apps/components/service-apps-panel.tsx +246 -56
  65. package/src/features/service-apps/hooks/use-service-apps.ts +14 -0
  66. package/src/features/side-dock/components/side-dock.test.tsx +58 -3
  67. package/src/features/side-dock/components/side-dock.tsx +77 -24
  68. package/src/features/side-dock/configs/side-dock-built-in-items.config.ts +37 -34
  69. package/src/features/side-dock/index.ts +2 -0
  70. package/src/features/side-dock/managers/side-dock.manager.test.ts +84 -3
  71. package/src/features/side-dock/managers/side-dock.manager.ts +135 -12
  72. package/src/features/side-dock/types/side-dock.types.ts +11 -4
  73. package/src/features/side-dock/utils/side-dock-item.utils.test.ts +8 -8
  74. package/src/features/side-dock/utils/side-dock-item.utils.ts +13 -5
  75. package/src/platforms/desktop/components/desktop-app-shell.tsx +5 -2
  76. package/src/platforms/mobile/components/mobile-app-shell.tsx +4 -1
  77. package/src/shared/components/config/provider-form-support.test.ts +21 -0
  78. package/src/shared/components/config/provider-form-support.ts +38 -78
  79. package/src/shared/components/config/provider-form.tsx +54 -67
  80. package/src/shared/components/config/providers-list.tsx +211 -73
  81. package/src/shared/components/doc-browser/doc-browser-context.test.tsx +73 -0
  82. package/src/shared/components/doc-browser/doc-browser-context.tsx +3 -0
  83. package/src/shared/components/doc-browser/doc-browser-tab-strip.tsx +25 -2
  84. package/src/shared/components/doc-browser/doc-browser.test.tsx +42 -0
  85. package/src/shared/components/doc-browser/doc-browser.tsx +38 -7
  86. package/src/shared/components/doc-browser/index.ts +1 -0
  87. package/src/shared/components/doc-browser/managers/doc-browser.manager.ts +30 -6
  88. package/src/shared/components/doc-browser/stores/doc-browser.store.ts +29 -0
  89. package/src/shared/components/doc-browser/types/doc-browser.types.ts +23 -0
  90. package/src/shared/components/doc-browser/utils/doc-browser-state.utils.ts +7 -0
  91. package/src/shared/components/model-config.tsx +5 -4
  92. package/src/shared/components/provider-auth-section.tsx +2 -2
  93. package/src/shared/components/ui/switch.tsx +5 -2
  94. package/src/shared/hooks/use-config.ts +27 -6
  95. package/src/shared/lib/api/marketplace.types.ts +193 -0
  96. package/src/shared/lib/api/types.ts +23 -200
  97. package/src/shared/lib/api/utils/config.utils.ts +17 -7
  98. package/src/shared/lib/i18n/index.ts +10 -12
  99. package/src/shared/lib/i18n/{marketplace.ts → marketplace-labels.utils.ts} +3 -0
  100. package/src/shared/lib/i18n/runtime/doc-browser-labels.utils.ts +46 -2
  101. package/src/shared/lib/provider-models/index.test.ts +93 -15
  102. package/src/shared/lib/provider-models/index.ts +35 -20
  103. package/src/shared/lib/transport/request-raw-api-response.utils.ts +1 -1
  104. package/dist/assets/api-DruAcori.js +0 -15
  105. package/dist/assets/app-presenter-provider-DIcnNB9s.js +0 -3
  106. package/dist/assets/channels-list-page-wK3l7p5R.js +0 -8
  107. package/dist/assets/chat-page-DgehvdJT.js +0 -105
  108. package/dist/assets/config-split-page-BnTO1mAM.js +0 -1
  109. package/dist/assets/confirm-dialog-CGrKHbAW.js +0 -5
  110. package/dist/assets/desktop-update-config-CehWxSSg.js +0 -1
  111. package/dist/assets/dist-CpwDZDtv.js +0 -1
  112. package/dist/assets/doc-browser-BgxmVsTx.js +0 -1
  113. package/dist/assets/doc-browser-CZn5-Qbu.js +0 -1
  114. package/dist/assets/doc-browser-context-B8gk7HMr.js +0 -1
  115. package/dist/assets/index-DORmh0Cy.css +0 -1
  116. package/dist/assets/index-DcFpvnge.js +0 -101
  117. package/dist/assets/mcp-marketplace-page-DBLhJWOJ.js +0 -1
  118. package/dist/assets/model-config-B-EJXGtO.js +0 -1
  119. package/dist/assets/play-C-0vo7hi.js +0 -1
  120. package/dist/assets/plus-Cr_akLp2.js +0 -1
  121. package/dist/assets/popover-YieElO8c.js +0 -1
  122. package/dist/assets/provider-scoped-model-input-BPRNGqYn.js +0 -1
  123. package/dist/assets/providers-list-CtBpL1ZY.js +0 -1
  124. package/dist/assets/react-B2X_ph0a.js +0 -1
  125. package/dist/assets/remote-BGHbHoHf.js +0 -1
  126. package/dist/assets/rotate-cw-jueVCNJQ.js +0 -1
  127. package/dist/assets/runtime-config-page-C1Hv7mad.js +0 -1
  128. package/dist/assets/search-CdhgFNwf.js +0 -1
  129. package/dist/assets/setting-row-BNu0qhOM.js +0 -1
  130. package/dist/assets/x-D1v_vb54.js +0 -1
  131. package/src/shared/components/provider-enabled-field.tsx +0 -23
  132. /package/dist/assets/{config-hints-A7G_jMTU.js → config-hints-TmNgpFHv.js} +0 -0
@@ -9,6 +9,7 @@ const SIDE_DOCK_BUILTIN_ICON_NAMES: SideDockIconName[] = [
9
9
  'apps',
10
10
  'docs',
11
11
  'new-tab',
12
+ 'panel-app',
12
13
  'service-apps',
13
14
  ];
14
15
 
@@ -39,6 +40,13 @@ function normalizeSideDockIcon(value: unknown): SideDockItemIcon | null {
39
40
  };
40
41
  }
41
42
 
43
+ if (value.type === 'text' && typeof value.value === 'string' && value.value.trim().length > 0) {
44
+ return {
45
+ type: 'text',
46
+ value: value.value.trim(),
47
+ };
48
+ }
49
+
42
50
  return null;
43
51
  }
44
52
 
@@ -47,8 +55,8 @@ export function normalizeSideDockPinnedItem(value: unknown): SideDockPinnedItem
47
55
  !isRecord(value)
48
56
  || typeof value.id !== 'string'
49
57
  || value.id.trim().length === 0
50
- || typeof value.labelKey !== 'string'
51
- || value.labelKey.trim().length === 0
58
+ || typeof value.label !== 'string'
59
+ || value.label.trim().length === 0
52
60
  || typeof value.createdAt !== 'string'
53
61
  || value.createdAt.trim().length === 0
54
62
  || !isRecord(value.target)
@@ -68,7 +76,7 @@ export function normalizeSideDockPinnedItem(value: unknown): SideDockPinnedItem
68
76
  createdAt: value.createdAt,
69
77
  icon,
70
78
  id: value.id.trim(),
71
- labelKey: value.labelKey.trim(),
79
+ label: value.label.trim(),
72
80
  target: {
73
81
  type: 'right-panel-resource',
74
82
  uri: value.target.uri.trim(),
@@ -96,7 +104,7 @@ export function createPinnedSideDockItem(item: SideDockItem, createdAt: string):
96
104
  createdAt,
97
105
  icon: item.icon,
98
106
  id: item.id,
99
- labelKey: item.labelKey,
107
+ label: item.label,
100
108
  target: item.target,
101
109
  };
102
110
  }
@@ -106,7 +114,7 @@ export function createSideDockItemFromPinnedItem(item: SideDockPinnedItem): Side
106
114
  builtIn: false,
107
115
  icon: item.icon,
108
116
  id: item.id,
109
- labelKey: item.labelKey,
117
+ label: item.label,
110
118
  removable: true,
111
119
  target: item.target,
112
120
  };
@@ -9,6 +9,7 @@ import { DesktopWindowChrome } from "@/platforms/desktop/components/desktop-wind
9
9
  import { isWindowsDesktopHost } from "@/platforms/desktop/utils/desktop-host.utils";
10
10
  import { MobileBottomNav } from "@/platforms/mobile";
11
11
  import type { DocBrowserCustomTabRenderers } from "@/shared/components/doc-browser/doc-browser-renderer.types";
12
+ import type { DocBrowserDockControls } from "@/shared/components/doc-browser/doc-browser-context";
12
13
  import { cn } from "@/shared/lib/utils";
13
14
 
14
15
  const DocBrowser = lazy(async () => ({
@@ -20,6 +21,7 @@ type DesktopAppShellProps = {
20
21
  isMobileLayout?: boolean;
21
22
  isDocBrowserOpen: boolean;
22
23
  docBrowserMode: "floating" | "docked";
24
+ docBrowserDockControls?: DocBrowserDockControls;
23
25
  docBrowserRenderers?: DocBrowserCustomTabRenderers;
24
26
  sideDock?: React.ReactNode;
25
27
  children: React.ReactNode;
@@ -30,6 +32,7 @@ export function DesktopAppShell({
30
32
  isMobileLayout = false,
31
33
  isDocBrowserOpen,
32
34
  docBrowserMode,
35
+ docBrowserDockControls,
33
36
  docBrowserRenderers = {},
34
37
  sideDock,
35
38
  children,
@@ -67,7 +70,7 @@ export function DesktopAppShell({
67
70
  </div>
68
71
  {isDocBrowserOpen && docBrowserMode === "docked" ? (
69
72
  <Suspense fallback={null}>
70
- <DocBrowser customTabRenderers={docBrowserRenderers} />
73
+ <DocBrowser customTabRenderers={docBrowserRenderers} dockControls={docBrowserDockControls} />
71
74
  </Suspense>
72
75
  ) : null}
73
76
  {sideDock}
@@ -76,7 +79,7 @@ export function DesktopAppShell({
76
79
  {showMobileBottomNav ? <MobileBottomNav /> : null}
77
80
  {isDocBrowserOpen && docBrowserMode === "floating" ? (
78
81
  <Suspense fallback={null}>
79
- <DocBrowser customTabRenderers={docBrowserRenderers} />
82
+ <DocBrowser customTabRenderers={docBrowserRenderers} dockControls={docBrowserDockControls} />
80
83
  </Suspense>
81
84
  ) : null}
82
85
  </div>
@@ -6,6 +6,7 @@ import {
6
6
  import { MobileBottomNav } from "@/platforms/mobile/components/mobile-bottom-nav";
7
7
  import { MobileTopbar } from "@/platforms/mobile/components/mobile-topbar";
8
8
  import type { DocBrowserCustomTabRenderers } from "@/shared/components/doc-browser/doc-browser-renderer.types";
9
+ import type { DocBrowserDockControls } from "@/shared/components/doc-browser/doc-browser-context";
9
10
 
10
11
  const DocBrowser = lazy(async () => ({
11
12
  default: (await import("@/shared/components/doc-browser/doc-browser")).DocBrowser,
@@ -14,6 +15,7 @@ const DocBrowser = lazy(async () => ({
14
15
  type MobileAppShellProps = {
15
16
  pathname: string;
16
17
  isDocBrowserOpen: boolean;
18
+ docBrowserDockControls?: DocBrowserDockControls;
17
19
  docBrowserRenderers?: DocBrowserCustomTabRenderers;
18
20
  topbarLeadingInset?: string;
19
21
  children: React.ReactNode;
@@ -22,6 +24,7 @@ type MobileAppShellProps = {
22
24
  export function MobileAppShell({
23
25
  pathname,
24
26
  isDocBrowserOpen,
27
+ docBrowserDockControls,
25
28
  docBrowserRenderers = {},
26
29
  topbarLeadingInset,
27
30
  children,
@@ -45,7 +48,7 @@ export function MobileAppShell({
45
48
  {showBottomNav ? <MobileBottomNav /> : null}
46
49
  {isDocBrowserOpen ? (
47
50
  <Suspense fallback={null}>
48
- <DocBrowser customTabRenderers={docBrowserRenderers} displayMode="fullscreen" />
51
+ <DocBrowser customTabRenderers={docBrowserRenderers} displayMode="fullscreen" dockControls={docBrowserDockControls} />
49
52
  </Suspense>
50
53
  ) : null}
51
54
  </div>
@@ -0,0 +1,21 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { resolveEditableModels, serializeModelsForSave } from './provider-form-support';
3
+
4
+ describe('provider form model defaults', () => {
5
+ it('keeps persisted empty models empty', () => {
6
+ expect(resolveEditableModels(['gpt-5.5', 'gpt-5.4'], [])).toEqual([]);
7
+ });
8
+
9
+ it('keeps a non-empty saved override without resurrecting removed defaults', () => {
10
+ expect(resolveEditableModels(['deepseek-chat', 'deepseek-reasoner'], ['deepseek-chat'])).toEqual([
11
+ 'deepseek-chat'
12
+ ]);
13
+ });
14
+
15
+ it('serializes local model ids with the providerId prefix', () => {
16
+ expect(serializeModelsForSave(['gpt-5.5', 'openai-work/gpt-5.4'], 'openai-work')).toEqual([
17
+ 'openai-work/gpt-5.5',
18
+ 'openai-work/gpt-5.4'
19
+ ]);
20
+ });
21
+ });
@@ -1,17 +1,25 @@
1
1
  import { t } from '@/shared/lib/i18n';
2
2
  import type { getLanguage } from '@/shared/lib/i18n';
3
- import type { ProviderConfigUpdate, ProviderConfigView, ThinkingLevel } from '@/shared/lib/api';
3
+ import type { ProviderConfigView, ThinkingLevel } from '@/shared/lib/api';
4
+ import {
5
+ normalizeModelConfigMap,
6
+ normalizeThinkingLevels,
7
+ parseThinkingLevel,
8
+ THINKING_LEVELS,
9
+ type ModelConfig as ModelConfigEntry
10
+ } from '@/shared/lib/provider-models';
4
11
 
5
12
  type WireApiType = 'auto' | 'chat' | 'responses';
6
- type ModelConfig = Record<string, {
7
- thinking?: { supported: ThinkingLevel[]; default?: ThinkingLevel | null };
8
- vision?: boolean;
9
- }>;
13
+ type ModelConfig = Record<string, ModelConfigEntry>;
10
14
  type ProviderAuthMethodOption = {
11
15
  id: string;
12
16
  };
13
17
 
14
18
  const EMPTY_PROVIDER_CONFIG: ProviderConfigView = {
19
+ providerId: '',
20
+ providerType: null,
21
+ isBuiltInType: false,
22
+ isCustom: true,
15
23
  enabled: true,
16
24
  displayName: '',
17
25
  apiKeySet: false,
@@ -23,9 +31,6 @@ const EMPTY_PROVIDER_CONFIG: ProviderConfigView = {
23
31
  modelConfig: {}
24
32
  };
25
33
 
26
- const THINKING_LEVELS: ThinkingLevel[] = ['off', 'minimal', 'low', 'medium', 'high', 'adaptive', 'xhigh'];
27
- const THINKING_LEVEL_SET = new Set<string>(THINKING_LEVELS);
28
-
29
34
  function normalizeHeaders(input: Record<string, string> | null | undefined): Record<string, string> | null {
30
35
  if (!input) {
31
36
  return null;
@@ -107,76 +112,20 @@ function modelListsEqual(left: string[], right: string[]): boolean {
107
112
  return left.every((item, index) => item === right[index]);
108
113
  }
109
114
 
110
- function resolveEditableModels(_defaultModels: string[], savedModels: string[]): string[] {
111
- return savedModels;
112
- }
113
-
114
- function serializeModelsForSave(models: string[], _defaultModels: string[]): string[] {
115
- return models;
115
+ function resolveEditableModels(defaultModels: string[], savedModels: string[]): string[] {
116
+ void defaultModels;
117
+ return normalizeModelList(savedModels);
116
118
  }
117
119
 
118
- function applyEnabledPatch(payload: ProviderConfigUpdate, enabled: boolean, currentEnabled: boolean): void {
119
- if (enabled !== currentEnabled) {
120
- payload.enabled = enabled;
121
- }
122
- }
123
-
124
- function parseThinkingLevel(value: unknown): ThinkingLevel | null {
125
- if (typeof value !== 'string') {
126
- return null;
127
- }
128
- const normalized = value.trim().toLowerCase();
129
- if (!normalized) {
130
- return null;
131
- }
132
- return THINKING_LEVEL_SET.has(normalized) ? (normalized as ThinkingLevel) : null;
133
- }
134
-
135
- function normalizeThinkingLevels(values: unknown): ThinkingLevel[] {
136
- if (!Array.isArray(values)) {
137
- return [];
138
- }
139
- const deduped: ThinkingLevel[] = [];
140
- for (const value of values) {
141
- const level = parseThinkingLevel(value);
142
- if (!level || deduped.includes(level)) {
143
- continue;
120
+ function serializeModelsForSave(models: string[], providerId: string): string[] {
121
+ const normalizedModels = normalizeModelList(models);
122
+ return normalizedModels.map((model) => {
123
+ const trimmed = model.trim();
124
+ if (!trimmed || trimmed.startsWith(`${providerId}/`)) {
125
+ return trimmed;
144
126
  }
145
- deduped.push(level);
146
- }
147
- return deduped;
148
- }
149
-
150
- function normalizeModelConfig(
151
- input: ProviderConfigView['modelConfig'],
152
- aliases: string[]
153
- ): ModelConfig {
154
- if (!input) {
155
- return {};
156
- }
157
- const normalized: ModelConfig = {};
158
- for (const [rawModel, rawValue] of Object.entries(input)) {
159
- const model = toProviderLocalModelId(rawModel, aliases);
160
- if (!model) {
161
- continue;
162
- }
163
- const entry: NonNullable<ModelConfig[string]> = {};
164
- const supported = normalizeThinkingLevels(rawValue?.thinking?.supported);
165
- if (supported.length > 0) {
166
- const defaultLevel = parseThinkingLevel(rawValue?.thinking?.default);
167
- entry.thinking =
168
- defaultLevel && supported.includes(defaultLevel)
169
- ? { supported, default: defaultLevel }
170
- : { supported };
171
- }
172
- if (rawValue?.vision === true) {
173
- entry.vision = true;
174
- }
175
- if (entry.thinking || entry.vision === true) {
176
- normalized[model] = entry;
177
- }
178
- }
179
- return normalized;
127
+ return `${providerId}/${trimmed}`;
128
+ });
180
129
  }
181
130
 
182
131
  function normalizeModelConfigForModels(modelConfig: ModelConfig, models: string[]): ModelConfig {
@@ -205,6 +154,17 @@ function normalizeModelConfigForModels(modelConfig: ModelConfig, models: string[
205
154
  return normalized;
206
155
  }
207
156
 
157
+ function mergeModelConfig(
158
+ base: ProviderConfigView['modelConfig'],
159
+ override: ProviderConfigView['modelConfig'],
160
+ aliases: string[]
161
+ ): ModelConfig {
162
+ return {
163
+ ...normalizeModelConfigMap(base, aliases),
164
+ ...normalizeModelConfigMap(override, aliases)
165
+ };
166
+ }
167
+
208
168
  function modelConfigEqual(left: ModelConfig, right: ModelConfig): boolean {
209
169
  const leftKeys = Object.keys(left).sort();
210
170
  const rightKeys = Object.keys(right).sort();
@@ -319,20 +279,20 @@ function shouldUsePillSelector(params: {
319
279
  hasDefault: boolean;
320
280
  optionCount: number;
321
281
  }): boolean {
322
- return params.required && params.hasDefault && params.optionCount > 1 && params.optionCount <= 3;
282
+ const { required, hasDefault, optionCount } = params;
283
+ return required && hasDefault && optionCount > 1 && optionCount <= 3;
323
284
  }
324
285
 
325
286
  export type { ModelConfig, ProviderAuthMethodOption, WireApiType };
326
287
  export {
327
- applyEnabledPatch,
328
288
  EMPTY_PROVIDER_CONFIG,
329
289
  formatThinkingLevelLabel,
330
290
  headersEqual,
331
291
  modelListsEqual,
332
292
  modelConfigEqual,
293
+ mergeModelConfig,
333
294
  normalizeHeaders,
334
295
  normalizeModelList,
335
- normalizeModelConfig,
336
296
  normalizeModelConfigForModels,
337
297
  resolveEditableModels,
338
298
  resolvePreferredAuthMethodId,
@@ -3,12 +3,12 @@ import { useQueryClient } from '@tanstack/react-query';
3
3
  import { CircleDotDashed, Trash2 } from 'lucide-react';
4
4
  import { toast } from 'sonner';
5
5
  import {
6
- useConfig,
7
- useConfigMeta,
8
6
  useConfigSchema,
9
7
  useDeleteProvider,
10
8
  useImportProviderAuthFromCli,
11
9
  usePollProviderAuth,
10
+ useProviders,
11
+ useProviderTemplates,
12
12
  useStartProviderAuth,
13
13
  useTestProviderConnection,
14
14
  useUpdateProvider
@@ -26,20 +26,18 @@ import {
26
26
  ConfigSplitPaneBody,
27
27
  ConfigSplitPaneFooter,
28
28
  ConfigSplitPaneHeader
29
- } from '../config-split-page';
30
- import { ProviderAdvancedSettingsSection } from '../provider-advanced-settings-section';
31
- import { ProviderAuthSection } from '../provider-auth-section';
32
- import { ProviderEnabledField } from '../provider-enabled-field';
29
+ } from '@/shared/components/config-split-page';
30
+ import { ProviderAdvancedSettingsSection } from '@/shared/components/provider-advanced-settings-section';
31
+ import { ProviderAuthSection } from '@/shared/components/provider-auth-section';
33
32
  import {
34
- applyEnabledPatch,
35
33
  EMPTY_PROVIDER_CONFIG,
36
34
  formatThinkingLevelLabel,
37
35
  headersEqual,
38
36
  modelListsEqual,
39
37
  modelConfigEqual,
38
+ mergeModelConfig,
40
39
  normalizeHeaders,
41
40
  normalizeModelList,
42
- normalizeModelConfig,
43
41
  normalizeModelConfigForModels,
44
42
  resolveEditableModels,
45
43
  resolvePreferredAuthMethodId,
@@ -51,8 +49,8 @@ import {
51
49
  type WireApiType
52
50
  } from './provider-form-support';
53
51
  import { ProviderModelsSection } from './provider-models-section';
54
- import type { PillSelectOption } from '../provider-pill-selector';
55
- import { ProviderStatusBadge } from '../provider-status-badge';
52
+ import type { PillSelectOption } from '@/shared/components/provider-pill-selector';
53
+ import { ProviderStatusBadge } from '@/shared/components/provider-status-badge';
56
54
 
57
55
  type ProviderFormProps = {
58
56
  providerName?: string;
@@ -61,8 +59,8 @@ type ProviderFormProps = {
61
59
 
62
60
  export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormProps) {
63
61
  const queryClient = useQueryClient();
64
- const { data: config } = useConfig();
65
- const { data: meta } = useConfigMeta();
62
+ const { data: providersView } = useProviders();
63
+ const { data: templatesView } = useProviderTemplates();
66
64
  const { data: schema } = useConfigSchema();
67
65
  const updateProvider = useUpdateProvider();
68
66
  const deleteProvider = useDeleteProvider();
@@ -72,7 +70,6 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
72
70
  const importProviderAuthFromCli = useImportProviderAuthFromCli();
73
71
 
74
72
  const [apiKey, setApiKey] = useState('');
75
- const [enabled, setEnabled] = useState(true);
76
73
  const [apiBase, setApiBase] = useState('');
77
74
  const [extraHeaders, setExtraHeaders] = useState<Record<string, string> | null>(null);
78
75
  const [wireApi, setWireApi] = useState<WireApiType>('auto');
@@ -87,23 +84,23 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
87
84
  const [authMethodId, setAuthMethodId] = useState('');
88
85
  const authPollTimerRef = useRef<number | null>(null);
89
86
 
90
- const providerSpec = meta?.providers.find((p) => p.name === providerName);
91
- const providerConfig = providerName ? config?.providers[providerName] : null;
87
+ const providerConfig = providerName ? providersView?.providers[providerName] : null;
88
+ const providerSpec = templatesView?.providerTemplates.find((template) => template.providerType === providerConfig?.providerType);
92
89
  const resolvedProviderConfig = providerConfig ?? EMPTY_PROVIDER_CONFIG;
93
90
  const uiHints = schema?.uiHints;
94
- const isCustomProvider = Boolean(providerSpec?.isCustom);
91
+ const isCustomProvider = Boolean(providerConfig?.isCustom);
95
92
  const apiKeyHint = providerName ? hintForPath(`providers.${providerName}.apiKey`, uiHints) : undefined;
96
93
  const apiBaseHint = providerName ? hintForPath(`providers.${providerName}.apiBase`, uiHints) : undefined;
97
94
  const extraHeadersHint = providerName ? hintForPath(`providers.${providerName}.extraHeaders`, uiHints) : undefined;
98
95
  const wireApiHint = providerName ? hintForPath(`providers.${providerName}.wireApi`, uiHints) : undefined;
99
- const defaultDisplayName = providerSpec?.displayName || providerName || '';
96
+ const defaultDisplayName = providerSpec?.displayName || providerConfig?.displayName || providerName || '';
100
97
  const currentDisplayName = (resolvedProviderConfig.displayName || '').trim();
101
98
  const effectiveDisplayName = currentDisplayName || defaultDisplayName;
102
99
  const currentEnabled = resolvedProviderConfig.enabled !== false;
103
100
  const providerTitle = providerDisplayName.trim() || effectiveDisplayName || providerName || t('providersSelectPlaceholder');
104
101
  const providerModelAliases = useMemo(
105
- () => normalizeModelList([providerSpec?.modelPrefix || providerName || '', providerName || '']),
106
- [providerName, providerSpec?.modelPrefix]
102
+ () => normalizeModelList([providerSpec?.modelPrefix || providerSpec?.providerType || '', providerName || '']),
103
+ [providerName, providerSpec?.modelPrefix, providerSpec?.providerType]
107
104
  );
108
105
  const defaultApiBase = providerSpec?.defaultApiBase || '';
109
106
  const currentApiBase = resolvedProviderConfig.apiBase || defaultApiBase;
@@ -127,7 +124,7 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
127
124
  const currentModelConfig = useMemo(
128
125
  () =>
129
126
  normalizeModelConfigForModels(
130
- normalizeModelConfig(resolvedProviderConfig.modelConfig ?? providerSpec?.modelConfig, providerModelAliases),
127
+ mergeModelConfig(providerSpec?.modelConfig, resolvedProviderConfig.modelConfig, providerModelAliases),
131
128
  currentEditableModels
132
129
  ),
133
130
  [currentEditableModels, providerModelAliases, providerSpec?.modelConfig, resolvedProviderConfig.modelConfig]
@@ -136,7 +133,7 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
136
133
  const apiBaseHelpText =
137
134
  providerSpec?.apiBaseHelp?.[language] || providerSpec?.apiBaseHelp?.en || apiBaseHint?.help || t('providerApiBaseHelp');
138
135
  const providerAuth = providerSpec?.auth;
139
- const providerAuthMethods = providerAuth?.methods ?? [];
136
+ const providerAuthMethods = useMemo(() => providerAuth?.methods ?? [], [providerAuth?.methods]);
140
137
  const supportsWireApi = Boolean(providerSpec?.supportsWireApi) || isCustomProvider;
141
138
  const providerAuthMethodOptions = useMemo(
142
139
  () =>
@@ -147,8 +144,8 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
147
144
  [providerAuthMethods, language]
148
145
  );
149
146
  const preferredAuthMethodId = useMemo(
150
- () => resolvePreferredAuthMethodId({ providerName, methods: providerAuthMethods, defaultMethodId: providerAuth?.defaultMethodId, language }),
151
- [providerName, providerAuth?.defaultMethodId, providerAuthMethods, language]
147
+ () => resolvePreferredAuthMethodId({ providerName: providerSpec?.providerType ?? providerName, methods: providerAuthMethods, defaultMethodId: providerAuth?.defaultMethodId, language }),
148
+ [providerName, providerAuth?.defaultMethodId, providerAuthMethods, language, providerSpec?.providerType]
152
149
  );
153
150
  const resolvedAuthMethodId = useMemo(() => {
154
151
  if (!providerAuthMethods.length) {
@@ -207,7 +204,7 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
207
204
  setAuthStatusMessage(t('providerAuthCompleted'));
208
205
  toast.success(t('providerAuthCompleted'));
209
206
  queryClient.invalidateQueries({ queryKey: ['config'] });
210
- queryClient.invalidateQueries({ queryKey: ['config-meta'] });
207
+ queryClient.invalidateQueries({ queryKey: ['providers'] });
211
208
  return;
212
209
  }
213
210
  setAuthSessionId(null);
@@ -230,7 +227,6 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
230
227
  useEffect(() => {
231
228
  if (!providerName) {
232
229
  setApiKey('');
233
- setEnabled(true);
234
230
  setApiBase('');
235
231
  setExtraHeaders(null);
236
232
  setWireApi('auto');
@@ -246,7 +242,6 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
246
242
  }
247
243
 
248
244
  setApiKey('');
249
- setEnabled(currentEnabled);
250
245
  setApiBase(currentApiBase);
251
246
  setExtraHeaders(resolvedProviderConfig.extraHeaders || null);
252
247
  setWireApi(currentWireApi);
@@ -261,7 +256,6 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
261
256
  }, [
262
257
  providerName,
263
258
  currentApiBase,
264
- currentEnabled,
265
259
  resolvedProviderConfig.extraHeaders,
266
260
  currentWireApi,
267
261
  currentEditableModels,
@@ -280,19 +274,16 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
280
274
  }
281
275
  return (
282
276
  apiKey.trim().length > 0 ||
283
- enabled !== currentEnabled ||
284
277
  apiBase.trim() !== currentApiBase.trim() ||
285
278
  !headersEqual(extraHeaders, currentHeaders) ||
286
279
  (supportsWireApi && wireApi !== currentWireApi) ||
287
280
  !modelListsEqual(models, currentEditableModels) ||
288
281
  !modelConfigEqual(modelConfig, currentModelConfig) ||
289
- (isCustomProvider && providerDisplayName.trim() !== effectiveDisplayName)
282
+ providerDisplayName.trim() !== effectiveDisplayName
290
283
  );
291
284
  }, [
292
285
  providerName,
293
286
  apiKey,
294
- enabled,
295
- currentEnabled,
296
287
  apiBase,
297
288
  currentApiBase,
298
289
  extraHeaders,
@@ -304,7 +295,6 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
304
295
  currentEditableModels,
305
296
  modelConfig,
306
297
  currentModelConfig,
307
- isCustomProvider,
308
298
  providerDisplayName,
309
299
  effectiveDisplayName
310
300
  ]);
@@ -314,6 +304,10 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
314
304
  if (!next) {
315
305
  return;
316
306
  }
307
+ if (next.includes('/')) {
308
+ toast.error(t('providerModelInvalidProviderPrefix'));
309
+ return;
310
+ }
317
311
  if (models.includes(next)) {
318
312
  setModelDraft('');
319
313
  return;
@@ -403,13 +397,12 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
403
397
  const normalizedHeaders = normalizeHeaders(extraHeaders);
404
398
  const trimmedDisplayName = providerDisplayName.trim();
405
399
 
406
- if (isCustomProvider && trimmedDisplayName !== effectiveDisplayName) {
400
+ if (trimmedDisplayName !== effectiveDisplayName) {
407
401
  payload.displayName = trimmedDisplayName.length > 0 ? trimmedDisplayName : null;
408
402
  }
409
403
  if (trimmedApiKey.length > 0) {
410
404
  payload.apiKey = trimmedApiKey;
411
405
  }
412
- applyEnabledPatch(payload, enabled, currentEnabled);
413
406
  if (trimmedApiBase !== currentApiBase.trim()) {
414
407
  payload.apiBase = trimmedApiBase.length > 0 && trimmedApiBase !== defaultApiBase ? trimmedApiBase : null;
415
408
  }
@@ -420,7 +413,7 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
420
413
  payload.wireApi = wireApi;
421
414
  }
422
415
  if (!modelListsEqual(models, currentEditableModels)) {
423
- payload.models = serializeModelsForSave(models, defaultModels);
416
+ payload.models = serializeModelsForSave(models, providerName);
424
417
  }
425
418
  if (!modelConfigEqual(modelConfig, currentModelConfig)) {
426
419
  payload.modelConfig = normalizeModelConfigForModels(modelConfig, models);
@@ -465,7 +458,7 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
465
458
  };
466
459
 
467
460
  const handleDeleteProvider = async () => {
468
- if (!providerName || !isCustomProvider) {
461
+ if (!providerName) {
469
462
  return;
470
463
  }
471
464
  if (!window.confirm(t('providerDeleteConfirm'))) {
@@ -517,7 +510,7 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
517
510
  setAuthStatusMessage(`${t('providerAuthImportStatusPrefix')}${result.expiresAt ? ` (expires: ${result.expiresAt})` : ''}`);
518
511
  toast.success(t('providerAuthImportSuccess'));
519
512
  queryClient.invalidateQueries({ queryKey: ['config'] });
520
- queryClient.invalidateQueries({ queryKey: ['config-meta'] });
513
+ queryClient.invalidateQueries({ queryKey: ['providers'] });
521
514
  } catch (error) {
522
515
  const message = error instanceof Error ? error.message : String(error);
523
516
  setAuthStatusMessage(message);
@@ -525,7 +518,7 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
525
518
  }
526
519
  };
527
520
 
528
- if (!providerName || !providerSpec) {
521
+ if (!providerName || !providerConfig) {
529
522
  return (
530
523
  <ConfigSplitEmptyPane>
531
524
  <div>
@@ -542,17 +535,15 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
542
535
  <div className='flex items-center justify-between'>
543
536
  <h3 className='truncate text-lg font-semibold text-gray-900'>{providerTitle}</h3>
544
537
  <div className='flex items-center gap-3'>
545
- {isCustomProvider && (
546
- <button
547
- type='button'
548
- onClick={handleDeleteProvider}
549
- disabled={deleteProvider.isPending}
550
- className='text-gray-400 transition-colors hover:text-red-500'
551
- title={t('providerDelete')}
552
- >
553
- <Trash2 className='h-4 w-4' />
554
- </button>
555
- )}
538
+ <button
539
+ type='button'
540
+ onClick={handleDeleteProvider}
541
+ disabled={deleteProvider.isPending}
542
+ className='text-gray-400 transition-colors hover:text-red-500'
543
+ title={t('providerDelete')}
544
+ >
545
+ <Trash2 className='h-4 w-4' />
546
+ </button>
556
547
  <ProviderStatusBadge enabled={currentEnabled} apiKeySet={resolvedProviderConfig.apiKeySet} />
557
548
  </div>
558
549
  </div>
@@ -560,24 +551,20 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
560
551
 
561
552
  <form onSubmit={handleSubmit} className='flex min-h-0 flex-1 flex-col'>
562
553
  <ConfigSplitPaneBody className='space-y-5 px-6 py-5'>
563
- <ProviderEnabledField enabled={enabled} onChange={setEnabled} />
564
-
565
- {isCustomProvider && (
566
- <div className='space-y-2'>
567
- <Label htmlFor='providerDisplayName' className='text-sm font-medium text-gray-900'>
568
- {t('providerDisplayName')}
569
- </Label>
570
- <Input
571
- id='providerDisplayName'
572
- type='text'
573
- value={providerDisplayName}
574
- onChange={(e) => setProviderDisplayName(e.target.value)}
575
- placeholder={defaultDisplayName || t('providerDisplayNamePlaceholder')}
576
- className='rounded-xl'
577
- />
578
- <p className='text-xs text-gray-500'>{t('providerDisplayNameHelpShort')}</p>
579
- </div>
580
- )}
554
+ <div className='space-y-2'>
555
+ <Label htmlFor='providerDisplayName' className='text-sm font-medium text-gray-900'>
556
+ {t('providerDisplayName')}
557
+ </Label>
558
+ <Input
559
+ id='providerDisplayName'
560
+ type='text'
561
+ value={providerDisplayName}
562
+ onChange={(e) => setProviderDisplayName(e.target.value)}
563
+ placeholder={defaultDisplayName || t('providerDisplayNamePlaceholder')}
564
+ className='rounded-xl'
565
+ />
566
+ <p className='text-xs text-gray-500'>{t('providerDisplayNameHelpShort')}</p>
567
+ </div>
581
568
 
582
569
  <div className='space-y-2'>
583
570
  <Label htmlFor='apiKey' className='text-sm font-medium text-gray-900'>