@nextclaw/ui 0.12.9 → 0.12.10

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 (178) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/dist/assets/ChannelsList-M9FTK1Ak.js +8 -0
  3. package/dist/assets/DocBrowser-CH7-GxlL.js +1 -0
  4. package/dist/assets/{DocBrowser-6ReNjvzF.js → DocBrowser-DMfr0Oow.js} +1 -1
  5. package/dist/assets/{DocBrowserContext-B6SpA7Qs.js → DocBrowserContext-BXydqby-.js} +1 -1
  6. package/dist/assets/{LogoBadge-ByNLYg65.js → LogoBadge-hO7tY7hE.js} +1 -1
  7. package/dist/assets/ModelConfig-CNIgLf0e.js +1 -0
  8. package/dist/assets/{ProviderScopedModelInput-Da7khnBA.js → ProviderScopedModelInput-B3HWP4oz.js} +1 -1
  9. package/dist/assets/ProvidersList-CHjMnRhX.js +1 -0
  10. package/dist/assets/RuntimeConfig-psp8nMSG.js +1 -0
  11. package/dist/assets/SearchConfig-CSoKip1f.js +1 -0
  12. package/dist/assets/{SecretsConfig-D281Rotl.js → SecretsConfig-MEt6MjuD.js} +2 -2
  13. package/dist/assets/SessionsConfig-DifCiXwR.js +2 -0
  14. package/dist/assets/{app-query-client-VnFElj4E.js → app-query-client-9jNewezV.js} +1 -1
  15. package/dist/assets/{book-open-BdcxxoQu.js → book-open-DzdUViDm.js} +1 -1
  16. package/dist/assets/chat-page-CLp0UV0Y.js +58 -0
  17. package/dist/assets/chat-session-display-DsYHx0RZ.js +1 -0
  18. package/dist/assets/{chunk-JZWAC4HX-DK5HPmIK.js → chunk-JZWAC4HX-C5dEc8hV.js} +1 -1
  19. package/dist/assets/{client-_i4MU2bB.js → client-C-8fH7-c.js} +1 -1
  20. package/dist/assets/{config-DtIQwrHF.js → config-CBScxsdV.js} +1 -1
  21. package/dist/assets/config-split-page-BUout_Ak.js +1 -0
  22. package/dist/assets/{createLucideIcon-BSeTgkZW.js → createLucideIcon-dy5ie7Ox.js} +1 -1
  23. package/dist/assets/desktop-update-config-2BS6BMkW.js +1 -0
  24. package/dist/assets/{dist-ccBFUi-o.js → dist-BruyLa92.js} +1 -1
  25. package/dist/assets/{dist-6TrrnPCR.js → dist-Cy7_j6hA.js} +1 -1
  26. package/dist/assets/{download-BhDxnyvU.js → download-BD0ETkB-.js} +1 -1
  27. package/dist/assets/{external-link-BgErLCNT.js → external-link-kZSAO8nT.js} +1 -1
  28. package/dist/assets/{hash-Bl7dr_UG.js → hash-BHJC2Ovu.js} +1 -1
  29. package/dist/assets/{i18n-eDHeDY0n.js → i18n-CpTZLchQ.js} +1 -1
  30. package/dist/assets/index-mW8W2FUu.css +1 -0
  31. package/dist/assets/index-zDZfXoI4.js +6 -0
  32. package/dist/assets/{infiniteQueryBehavior-ZDS92Qpp.js → infiniteQueryBehavior-CyER9hv0.js} +1 -1
  33. package/dist/assets/loader-circle-Bc2gCU33.js +1 -0
  34. package/dist/assets/{logos-x89HbrZ4.js → logos-B7gRObP8.js} +1 -1
  35. package/dist/assets/marketplace-page-3qVMnF3d.js +1 -0
  36. package/dist/assets/marketplace-page-BhFIeQzI.js +49 -0
  37. package/dist/assets/mcp-marketplace-page-DYfteJ1D.js +40 -0
  38. package/dist/assets/{page-layout-vZnghcFy.js → page-layout-0UcO9H9Z.js} +1 -1
  39. package/dist/assets/play-CKDjSQFL.js +1 -0
  40. package/dist/assets/plus-CG0QrVY_.js +1 -0
  41. package/dist/assets/{refresh-ccw-DT98i__E.js → refresh-ccw-COVhNHtN.js} +1 -1
  42. package/dist/assets/{refresh-cw-C47QSEwg.js → refresh-cw-Bcv40SXy.js} +1 -1
  43. package/dist/assets/remote-access-page-CWHG-sug.js +1 -0
  44. package/dist/assets/{rotate-cw-JtFzpNn6.js → rotate-cw-oHMKJMC8.js} +1 -1
  45. package/dist/assets/{save-3S6-H3Xw.js → save-EqJPOF0G.js} +1 -1
  46. package/dist/assets/search-BCAlB8nz.js +1 -0
  47. package/dist/assets/security-config-Slh0Mayz.js +1 -0
  48. package/dist/assets/select-CVz0t7MF.js +41 -0
  49. package/dist/assets/setting-row-CbVHAuQt.js +1 -0
  50. package/dist/assets/skeleton-D5rdKvzy.js +1 -0
  51. package/dist/assets/{status-dot-vbanNPFU.js → status-dot-DpPtVzQT.js} +1 -1
  52. package/dist/assets/{switch-BsLtHOH-.js → switch-CM29eCAR.js} +1 -1
  53. package/dist/assets/{tabs-custom-D3HYMt6k.js → tabs-custom-YcZUWn3o.js} +1 -1
  54. package/dist/assets/tag-chip-DMXdnLcj.js +1 -0
  55. package/dist/assets/{trash-2-G48scll7.js → trash-2-mJT6oWa2.js} +1 -1
  56. package/dist/assets/{use-infinite-scroll-loader-DkNhD-42.js → use-infinite-scroll-loader-DJ1L81Dz.js} +1 -1
  57. package/dist/assets/{useConfirmDialog-BkvTN-vd.js → useConfirmDialog-BsVuqu1x.js} +1 -1
  58. package/dist/assets/{useMutation-CBWjE2uj.js → useMutation-CNcz2fgt.js} +1 -1
  59. package/dist/assets/x-Czwxm82I.js +1 -0
  60. package/dist/index.html +22 -22
  61. package/dist/runtime-icons/claude.ico +0 -0
  62. package/dist/runtime-icons/codex-openai.svg +6 -0
  63. package/dist/runtime-icons/hermes-agent.png +0 -0
  64. package/package.json +6 -6
  65. package/public/runtime-icons/claude.ico +0 -0
  66. package/public/runtime-icons/codex-openai.svg +6 -0
  67. package/public/runtime-icons/hermes-agent.png +0 -0
  68. package/src/account/components/account-panel.tsx +217 -97
  69. package/src/account/managers/account.manager.ts +3 -2
  70. package/src/api/chat-session-type.types.ts +7 -0
  71. package/src/api/runtime-control.types.ts +8 -0
  72. package/src/api/types.ts +8 -0
  73. package/src/app.tsx +221 -57
  74. package/src/components/agents/agent-dialogs.tsx +499 -0
  75. package/src/components/agents/agents-page.test.tsx +238 -0
  76. package/src/components/agents/agents-page.tsx +435 -0
  77. package/src/components/chat/ChatSidebar.tsx +11 -35
  78. package/src/components/chat/chat-conversation-panel.test.tsx +20 -0
  79. package/src/components/chat/chat-conversation-panel.tsx +83 -13
  80. package/src/components/chat/chat-page-shell.tsx +19 -13
  81. package/src/components/chat/chat-session-type-option-item.test.tsx +46 -0
  82. package/src/components/chat/chat-session-type-option-item.tsx +68 -0
  83. package/src/components/chat/chat-session-workspace-file-preview.test.tsx +87 -0
  84. package/src/components/chat/chat-session-workspace-file-preview.tsx +14 -43
  85. package/src/components/chat/chat-session-workspace-panel-nav.tsx +8 -2
  86. package/src/components/chat/chat-sidebar-project-groups.tsx +11 -36
  87. package/src/components/chat/ncp/__tests__/ncp-session-adapter.cancelled-tool.test.ts +77 -0
  88. package/src/components/chat/ncp/ncp-chat-page.tsx +2 -0
  89. package/src/components/chat/ncp/ncp-session-adapter.test.ts +1 -0
  90. package/src/components/chat/ncp/ncp-session-adapter.ts +3 -0
  91. package/src/components/chat/ncp/page/ncp-chat-derived-state.ts +10 -4
  92. package/src/components/chat/stores/chat-input.store.ts +2 -1
  93. package/src/components/chat/stores/chat-thread.store.ts +3 -1
  94. package/src/components/chat/useChatSessionTypeState.ts +10 -1
  95. package/src/components/chat/workspace/chat-session-workspace-file-breadcrumbs.tsx +86 -0
  96. package/src/components/common/BrandHeader.tsx +3 -1
  97. package/src/components/common/session-context-icon.tsx +15 -2
  98. package/src/components/common/{TagInput.tsx → tag-input.tsx} +25 -17
  99. package/src/components/config/ChannelForm.test.tsx +89 -3
  100. package/src/components/config/ChannelForm.tsx +157 -188
  101. package/src/components/config/ChannelsList.test.tsx +163 -119
  102. package/src/components/config/ChannelsList.tsx +90 -101
  103. package/src/components/config/ProviderForm.tsx +108 -146
  104. package/src/components/config/ProvidersList.tsx +100 -123
  105. package/src/components/config/SearchConfig.tsx +423 -393
  106. package/src/components/config/channel-form-fields-section.tsx +70 -37
  107. package/src/components/config/config-split-page.tsx +109 -0
  108. package/src/components/config/provider-enabled-field.tsx +17 -10
  109. package/src/components/config/runtime-control-card.test.tsx +56 -0
  110. package/src/components/config/runtime-control-card.tsx +25 -0
  111. package/src/components/config/runtime-presence-card.tsx +93 -79
  112. package/src/components/layout/AppLayout.tsx +25 -37
  113. package/src/components/layout/app-layout.test.tsx +46 -14
  114. package/src/components/layout/runtime-status-entry.test.tsx +157 -0
  115. package/src/components/layout/runtime-status-entry.tsx +143 -0
  116. package/src/components/marketplace/marketplace-detail-doc.ts +93 -0
  117. package/src/components/marketplace/marketplace-list-card.tsx +288 -0
  118. package/src/components/marketplace/marketplace-page-data.ts +129 -0
  119. package/src/components/marketplace/marketplace-page.test.tsx +339 -0
  120. package/src/components/marketplace/marketplace-page.tsx +596 -0
  121. package/src/components/marketplace/mcp/mcp-marketplace-card.tsx +128 -0
  122. package/src/components/marketplace/mcp/mcp-marketplace-dialogs.tsx +191 -0
  123. package/src/components/marketplace/mcp/mcp-marketplace-doc.ts +152 -0
  124. package/src/components/marketplace/mcp/mcp-marketplace-page.test.tsx +223 -0
  125. package/src/components/marketplace/mcp/mcp-marketplace-page.tsx +414 -0
  126. package/src/components/remote/remote-access-page.test.tsx +105 -0
  127. package/src/components/remote/remote-access-page.tsx +248 -0
  128. package/src/components/ui/notice-card.tsx +129 -0
  129. package/src/components/ui/setting-row.tsx +51 -0
  130. package/src/components/ui/tag-chip.tsx +39 -0
  131. package/src/components/ui/textarea.tsx +19 -0
  132. package/src/hooks/useConfig.ts +2 -1
  133. package/src/index.css +24 -0
  134. package/src/lib/app-resource-uri.test.ts +20 -0
  135. package/src/lib/app-resource-uri.ts +29 -0
  136. package/src/lib/i18n.remote.ts +1 -1
  137. package/src/lib/i18n.runtime-control.ts +31 -0
  138. package/src/lib/i18n.ts +5 -8
  139. package/src/lib/session-context.utils.test.ts +71 -0
  140. package/src/lib/session-context.utils.ts +28 -3
  141. package/src/lib/session-project/workspace-file-breadcrumb.test.ts +83 -0
  142. package/src/lib/session-project/workspace-file-breadcrumb.ts +188 -0
  143. package/dist/assets/ChannelsList-Ita2Zm1_.js +0 -8
  144. package/dist/assets/DocBrowser-BNwbPHf4.js +0 -1
  145. package/dist/assets/MarketplacePage-CjX2MWww.js +0 -1
  146. package/dist/assets/MarketplacePage-D0sDlYX4.js +0 -49
  147. package/dist/assets/McpMarketplacePage-BGKJm1sJ.js +0 -40
  148. package/dist/assets/ModelConfig-BzZenCH-.js +0 -1
  149. package/dist/assets/ProvidersList-BbVzRxjY.js +0 -1
  150. package/dist/assets/RemoteAccessPage-BaDH_X1Q.js +0 -1
  151. package/dist/assets/RuntimeConfig-F_XKGgLm.js +0 -1
  152. package/dist/assets/SearchConfig-BGkzXQP-.js +0 -1
  153. package/dist/assets/SessionsConfig-ChHQ7M5c.js +0 -2
  154. package/dist/assets/chat-page-Doe0yTtB.js +0 -58
  155. package/dist/assets/chat-session-display-cw78aiI_.js +0 -1
  156. package/dist/assets/config-layout-CHs0mAaR.js +0 -1
  157. package/dist/assets/desktop-update-config-Dpcf4BKG.js +0 -1
  158. package/dist/assets/index-CF9xve0E.js +0 -6
  159. package/dist/assets/index-FgA52VBt.css +0 -1
  160. package/dist/assets/loader-circle-ACM1s51e.js +0 -1
  161. package/dist/assets/play-CFUwCA2E.js +0 -1
  162. package/dist/assets/plus-rYsv72JG.js +0 -1
  163. package/dist/assets/popover-Bg1VoTZ6.js +0 -1
  164. package/dist/assets/search-3kFR_zh9.js +0 -1
  165. package/dist/assets/security-config-BWaiARNk.js +0 -1
  166. package/dist/assets/select-DJ2MUjBB.js +0 -41
  167. package/dist/assets/skeleton-ByQepn0M.js +0 -1
  168. package/dist/assets/x-ByDbItbq.js +0 -1
  169. package/src/components/agents/AgentDialogs.tsx +0 -400
  170. package/src/components/agents/AgentsPage.test.tsx +0 -217
  171. package/src/components/agents/AgentsPage.tsx +0 -352
  172. package/src/components/config/config-layout.ts +0 -10
  173. package/src/components/marketplace/MarketplacePage.test.tsx +0 -322
  174. package/src/components/marketplace/MarketplacePage.tsx +0 -827
  175. package/src/components/marketplace/mcp/McpMarketplacePage.test.tsx +0 -208
  176. package/src/components/marketplace/mcp/McpMarketplacePage.tsx +0 -580
  177. package/src/components/remote/RemoteAccessPage.test.tsx +0 -103
  178. package/src/components/remote/RemoteAccessPage.tsx +0 -144
@@ -1,5 +1,7 @@
1
1
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
2
  import { useQueryClient } from '@tanstack/react-query';
3
+ import { CircleDotDashed, Trash2 } from 'lucide-react';
4
+ import { toast } from 'sonner';
3
5
  import {
4
6
  useConfig,
5
7
  useConfigMeta,
@@ -11,16 +13,20 @@ import {
11
13
  useTestProviderConnection,
12
14
  useUpdateProvider
13
15
  } from '@/hooks/useConfig';
16
+ import { MaskedInput } from '@/components/common/MaskedInput';
14
17
  import { Button } from '@/components/ui/button';
15
18
  import { Input } from '@/components/ui/input';
16
19
  import { Label } from '@/components/ui/label';
17
- import { MaskedInput } from '@/components/common/MaskedInput';
18
- import { getLanguage, t } from '@/lib/i18n';
19
20
  import { hintForPath } from '@/lib/config-hints';
21
+ import { getLanguage, t } from '@/lib/i18n';
20
22
  import type { ProviderConfigUpdate, ProviderConnectionTestRequest, ThinkingLevel } from '@/api/types';
21
- import { CircleDotDashed, Trash2 } from 'lucide-react';
22
- import { toast } from 'sonner';
23
- import { CONFIG_DETAIL_CARD_CLASS, CONFIG_EMPTY_DETAIL_CARD_CLASS } from './config-layout';
23
+ import {
24
+ ConfigSplitDetailPane,
25
+ ConfigSplitEmptyPane,
26
+ ConfigSplitPaneBody,
27
+ ConfigSplitPaneFooter,
28
+ ConfigSplitPaneHeader
29
+ } from './config-split-page';
24
30
  import { ProviderAdvancedSettingsSection } from './provider-advanced-settings-section';
25
31
  import { ProviderAuthSection } from './provider-auth-section';
26
32
  import { ProviderEnabledField } from './provider-enabled-field';
@@ -86,7 +92,6 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
86
92
  const resolvedProviderConfig = providerConfig ?? EMPTY_PROVIDER_CONFIG;
87
93
  const uiHints = schema?.uiHints;
88
94
  const isCustomProvider = Boolean(providerSpec?.isCustom);
89
-
90
95
  const apiKeyHint = providerName ? hintForPath(`providers.${providerName}.apiKey`, uiHints) : undefined;
91
96
  const apiBaseHint = providerName ? hintForPath(`providers.${providerName}.apiBase`, uiHints) : undefined;
92
97
  const extraHeadersHint = providerName ? hintForPath(`providers.${providerName}.extraHeaders`, uiHints) : undefined;
@@ -95,12 +100,10 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
95
100
  const currentDisplayName = (resolvedProviderConfig.displayName || '').trim();
96
101
  const effectiveDisplayName = currentDisplayName || defaultDisplayName;
97
102
  const currentEnabled = resolvedProviderConfig.enabled !== false;
98
-
99
103
  const providerTitle = providerDisplayName.trim() || effectiveDisplayName || providerName || t('providersSelectPlaceholder');
100
- const providerModelPrefix = providerSpec?.modelPrefix || providerName || '';
101
104
  const providerModelAliases = useMemo(
102
- () => normalizeModelList([providerModelPrefix, providerName || '']),
103
- [providerModelPrefix, providerName]
105
+ () => normalizeModelList([providerSpec?.modelPrefix || providerName || '', providerName || '']),
106
+ [providerName, providerSpec?.modelPrefix]
104
107
  );
105
108
  const defaultApiBase = providerSpec?.defaultApiBase || '';
106
109
  const currentApiBase = resolvedProviderConfig.apiBase || defaultApiBase;
@@ -120,10 +123,7 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
120
123
  ),
121
124
  [resolvedProviderConfig.models, providerModelAliases]
122
125
  );
123
- const currentEditableModels = useMemo(
124
- () => resolveEditableModels(defaultModels, currentModels),
125
- [defaultModels, currentModels]
126
- );
126
+ const currentEditableModels = useMemo(() => resolveEditableModels(defaultModels, currentModels), [defaultModels, currentModels]);
127
127
  const currentModelThinking = useMemo(
128
128
  () =>
129
129
  normalizeModelThinkingForModels(
@@ -134,15 +134,9 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
134
134
  );
135
135
  const language = getLanguage();
136
136
  const apiBaseHelpText =
137
- providerSpec?.apiBaseHelp?.[language] ||
138
- providerSpec?.apiBaseHelp?.en ||
139
- apiBaseHint?.help ||
140
- t('providerApiBaseHelp');
137
+ providerSpec?.apiBaseHelp?.[language] || providerSpec?.apiBaseHelp?.en || apiBaseHint?.help || t('providerApiBaseHelp');
141
138
  const providerAuth = providerSpec?.auth;
142
- const providerAuthMethods = useMemo(
143
- () => providerAuth?.methods ?? [],
144
- [providerAuth?.methods]
145
- );
139
+ const providerAuthMethods = providerAuth?.methods ?? [];
146
140
  const supportsWireApi = Boolean(providerSpec?.supportsWireApi) || isCustomProvider;
147
141
  const providerAuthMethodOptions = useMemo(
148
142
  () =>
@@ -153,12 +147,7 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
153
147
  [providerAuthMethods, language]
154
148
  );
155
149
  const preferredAuthMethodId = useMemo(
156
- () => resolvePreferredAuthMethodId({
157
- providerName,
158
- methods: providerAuthMethods,
159
- defaultMethodId: providerAuth?.defaultMethodId,
160
- language
161
- }),
150
+ () => resolvePreferredAuthMethodId({ providerName, methods: providerAuthMethods, defaultMethodId: providerAuth?.defaultMethodId, language }),
162
151
  [providerName, providerAuth?.defaultMethodId, providerAuthMethods, language]
163
152
  );
164
153
  const resolvedAuthMethodId = useMemo(() => {
@@ -180,8 +169,7 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
180
169
  hasDefault: Boolean(providerAuth?.defaultMethodId?.trim()),
181
170
  optionCount: providerAuthMethods.length
182
171
  });
183
- const wireApiOptions = providerSpec?.wireApiOptions || ['auto', 'chat', 'responses'];
184
- const wireApiSelectOptions: PillSelectOption[] = wireApiOptions.map((option) => ({
172
+ const wireApiSelectOptions: PillSelectOption[] = (providerSpec?.wireApiOptions || ['auto', 'chat', 'responses']).map((option) => ({
185
173
  value: option,
186
174
  label: option === 'chat' ? t('wireApiChat') : option === 'responses' ? t('wireApiResponses') : t('wireApiAuto')
187
175
  }));
@@ -198,46 +186,46 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
198
186
  }
199
187
  }, []);
200
188
 
201
- const scheduleProviderAuthPoll = useCallback((sessionId: string, delayMs: number) => {
202
- clearAuthPollTimer();
203
- authPollTimerRef.current = window.setTimeout(() => {
204
- void (async () => {
205
- if (!providerName) {
206
- return;
207
- }
208
- try {
209
- const result = await pollProviderAuth.mutateAsync({
210
- provider: providerName,
211
- data: { sessionId }
212
- });
213
- if (result.status === 'pending') {
214
- setAuthStatusMessage(t('providerAuthWaitingBrowser'));
215
- scheduleProviderAuthPoll(sessionId, result.nextPollMs ?? delayMs);
189
+ const scheduleProviderAuthPoll = useCallback(
190
+ (sessionId: string, delayMs: number) => {
191
+ clearAuthPollTimer();
192
+ authPollTimerRef.current = window.setTimeout(() => {
193
+ void (async () => {
194
+ if (!providerName) {
216
195
  return;
217
196
  }
218
- if (result.status === 'authorized') {
197
+ try {
198
+ const result = await pollProviderAuth.mutateAsync({ provider: providerName, data: { sessionId } });
199
+ if (result.status === 'pending') {
200
+ setAuthStatusMessage(t('providerAuthWaitingBrowser'));
201
+ scheduleProviderAuthPoll(sessionId, result.nextPollMs ?? delayMs);
202
+ return;
203
+ }
204
+ if (result.status === 'authorized') {
205
+ setAuthSessionId(null);
206
+ clearAuthPollTimer();
207
+ setAuthStatusMessage(t('providerAuthCompleted'));
208
+ toast.success(t('providerAuthCompleted'));
209
+ queryClient.invalidateQueries({ queryKey: ['config'] });
210
+ queryClient.invalidateQueries({ queryKey: ['config-meta'] });
211
+ return;
212
+ }
219
213
  setAuthSessionId(null);
220
214
  clearAuthPollTimer();
221
- setAuthStatusMessage(t('providerAuthCompleted'));
222
- toast.success(t('providerAuthCompleted'));
223
- queryClient.invalidateQueries({ queryKey: ['config'] });
224
- queryClient.invalidateQueries({ queryKey: ['config-meta'] });
225
- return;
215
+ setAuthStatusMessage(result.message || `Authorization ${result.status}.`);
216
+ toast.error(result.message || `Authorization ${result.status}.`);
217
+ } catch (error) {
218
+ setAuthSessionId(null);
219
+ clearAuthPollTimer();
220
+ const message = error instanceof Error ? error.message : String(error);
221
+ setAuthStatusMessage(message);
222
+ toast.error(`Authorization failed: ${message}`);
226
223
  }
227
- setAuthSessionId(null);
228
- clearAuthPollTimer();
229
- setAuthStatusMessage(result.message || `Authorization ${result.status}.`);
230
- toast.error(result.message || `Authorization ${result.status}.`);
231
- } catch (error) {
232
- setAuthSessionId(null);
233
- clearAuthPollTimer();
234
- const message = error instanceof Error ? error.message : String(error);
235
- setAuthStatusMessage(message);
236
- toast.error(`Authorization failed: ${message}`);
237
- }
238
- })();
239
- }, Math.max(1000, delayMs));
240
- }, [clearAuthPollTimer, pollProviderAuth, providerName, queryClient]);
224
+ })();
225
+ }, Math.max(1000, delayMs));
226
+ },
227
+ [clearAuthPollTimer, pollProviderAuth, providerName, queryClient]
228
+ );
241
229
 
242
230
  useEffect(() => {
243
231
  if (!providerName) {
@@ -284,40 +272,24 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
284
272
  ]);
285
273
 
286
274
  useEffect(() => () => clearAuthPollTimer(), [clearAuthPollTimer]);
287
-
288
- useEffect(() => {
289
- setModelThinking((prev) => normalizeModelThinkingForModels(prev, models));
290
- }, [models]);
275
+ useEffect(() => setModelThinking((prev) => normalizeModelThinkingForModels(prev, models)), [models]);
291
276
 
292
277
  const hasChanges = useMemo(() => {
293
278
  if (!providerName) {
294
279
  return false;
295
280
  }
296
- const apiKeyChanged = apiKey.trim().length > 0;
297
- const apiBaseChanged = apiBase.trim() !== currentApiBase.trim();
298
- const headersChanged = !headersEqual(extraHeaders, currentHeaders);
299
- const wireApiChanged = supportsWireApi ? wireApi !== currentWireApi : false;
300
- const modelsChanged = !modelListsEqual(models, currentEditableModels);
301
- const modelThinkingChanged = !modelThinkingEqual(modelThinking, currentModelThinking);
302
- const displayNameChanged = isCustomProvider
303
- ? providerDisplayName.trim() !== effectiveDisplayName
304
- : false;
305
-
306
281
  return (
307
- apiKeyChanged ||
282
+ apiKey.trim().length > 0 ||
308
283
  enabled !== currentEnabled ||
309
- apiBaseChanged ||
310
- headersChanged ||
311
- wireApiChanged ||
312
- modelsChanged ||
313
- modelThinkingChanged ||
314
- displayNameChanged
284
+ apiBase.trim() !== currentApiBase.trim() ||
285
+ !headersEqual(extraHeaders, currentHeaders) ||
286
+ (supportsWireApi && wireApi !== currentWireApi) ||
287
+ !modelListsEqual(models, currentEditableModels) ||
288
+ !modelThinkingEqual(modelThinking, currentModelThinking) ||
289
+ (isCustomProvider && providerDisplayName.trim() !== effectiveDisplayName)
315
290
  );
316
291
  }, [
317
292
  providerName,
318
- isCustomProvider,
319
- providerDisplayName,
320
- effectiveDisplayName,
321
293
  apiKey,
322
294
  enabled,
323
295
  currentEnabled,
@@ -331,7 +303,10 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
331
303
  models,
332
304
  currentEditableModels,
333
305
  modelThinking,
334
- currentModelThinking
306
+ currentModelThinking,
307
+ isCustomProvider,
308
+ providerDisplayName,
309
+ effectiveDisplayName
335
310
  ]);
336
311
 
337
312
  const handleAddModel = () => {
@@ -379,16 +354,13 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
379
354
  }
380
355
  return {
381
356
  ...prev,
382
- [modelName]: level
383
- ? { supported: currentEntry.supported, default: level }
384
- : { supported: currentEntry.supported }
357
+ [modelName]: level ? { supported: currentEntry.supported, default: level } : { supported: currentEntry.supported }
385
358
  };
386
359
  });
387
360
  };
388
361
 
389
362
  const handleSubmit = (e: React.FormEvent) => {
390
363
  e.preventDefault();
391
-
392
364
  if (!providerName) {
393
365
  return;
394
366
  }
@@ -402,20 +374,16 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
402
374
  if (isCustomProvider && trimmedDisplayName !== effectiveDisplayName) {
403
375
  payload.displayName = trimmedDisplayName.length > 0 ? trimmedDisplayName : null;
404
376
  }
405
-
406
377
  if (trimmedApiKey.length > 0) {
407
378
  payload.apiKey = trimmedApiKey;
408
379
  }
409
380
  applyEnabledPatch(payload, enabled, currentEnabled);
410
-
411
381
  if (trimmedApiBase !== currentApiBase.trim()) {
412
382
  payload.apiBase = trimmedApiBase.length > 0 && trimmedApiBase !== defaultApiBase ? trimmedApiBase : null;
413
383
  }
414
-
415
384
  if (!headersEqual(normalizedHeaders, currentHeaders)) {
416
385
  payload.extraHeaders = normalizedHeaders;
417
386
  }
418
-
419
387
  if (supportsWireApi && wireApi !== currentWireApi) {
420
388
  payload.wireApi = wireApi;
421
389
  }
@@ -433,7 +401,6 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
433
401
  if (!providerName) {
434
402
  return;
435
403
  }
436
-
437
404
  const preferredModel = models.find((modelName) => modelName.trim().length > 0) ?? '';
438
405
  const testModel = toProviderLocalModelId(preferredModel, providerModelAliases);
439
406
  const payload: ProviderConnectionTestRequest = {
@@ -449,10 +416,7 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
449
416
  }
450
417
 
451
418
  try {
452
- const result = await testProviderConnection.mutateAsync({
453
- provider: providerName,
454
- data: payload
455
- });
419
+ const result = await testProviderConnection.mutateAsync({ provider: providerName, data: payload });
456
420
  if (result.success) {
457
421
  toast.success(`${t('providerTestConnectionSuccess')} (${result.latencyMs}ms)`);
458
422
  return;
@@ -472,8 +436,7 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
472
436
  if (!providerName || !isCustomProvider) {
473
437
  return;
474
438
  }
475
- const confirmed = window.confirm(t('providerDeleteConfirm'));
476
- if (!confirmed) {
439
+ if (!window.confirm(t('providerDeleteConfirm'))) {
477
440
  return;
478
441
  }
479
442
  try {
@@ -519,8 +482,7 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
519
482
  clearAuthPollTimer();
520
483
  setAuthSessionId(null);
521
484
  const result = await importProviderAuthFromCli.mutateAsync({ provider: providerName });
522
- const expiresText = result.expiresAt ? ` (expires: ${result.expiresAt})` : '';
523
- setAuthStatusMessage(`${t('providerAuthImportStatusPrefix')}${expiresText}`);
485
+ setAuthStatusMessage(`${t('providerAuthImportStatusPrefix')}${result.expiresAt ? ` (expires: ${result.expiresAt})` : ''}`);
524
486
  toast.success(t('providerAuthImportSuccess'));
525
487
  queryClient.invalidateQueries({ queryKey: ['config'] });
526
488
  queryClient.invalidateQueries({ queryKey: ['config-meta'] });
@@ -533,71 +495,71 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
533
495
 
534
496
  if (!providerName || !providerSpec) {
535
497
  return (
536
- <div className={CONFIG_EMPTY_DETAIL_CARD_CLASS}>
498
+ <ConfigSplitEmptyPane>
537
499
  <div>
538
- <h3 className="text-base font-semibold text-gray-900">{t('providersSelectTitle')}</h3>
539
- <p className="mt-2 text-sm text-gray-500">{t('providersSelectDescription')}</p>
500
+ <h3 className='text-base font-semibold text-gray-900'>{t('providersSelectTitle')}</h3>
501
+ <p className='mt-2 text-sm text-gray-500'>{t('providersSelectDescription')}</p>
540
502
  </div>
541
- </div>
503
+ </ConfigSplitEmptyPane>
542
504
  );
543
505
  }
544
506
 
545
507
  return (
546
- <div className={CONFIG_DETAIL_CARD_CLASS}>
547
- <div className="border-b border-gray-100 px-6 py-4">
548
- <div className="flex items-center justify-between">
549
- <h3 className="truncate text-lg font-semibold text-gray-900">{providerTitle}</h3>
550
- <div className="flex items-center gap-3">
508
+ <ConfigSplitDetailPane>
509
+ <ConfigSplitPaneHeader className='px-6 py-4'>
510
+ <div className='flex items-center justify-between'>
511
+ <h3 className='truncate text-lg font-semibold text-gray-900'>{providerTitle}</h3>
512
+ <div className='flex items-center gap-3'>
551
513
  {isCustomProvider && (
552
514
  <button
553
- type="button"
515
+ type='button'
554
516
  onClick={handleDeleteProvider}
555
517
  disabled={deleteProvider.isPending}
556
- className="text-gray-400 hover:text-red-500 transition-colors"
518
+ className='text-gray-400 transition-colors hover:text-red-500'
557
519
  title={t('providerDelete')}
558
520
  >
559
- <Trash2 className="h-4 w-4" />
521
+ <Trash2 className='h-4 w-4' />
560
522
  </button>
561
523
  )}
562
524
  <ProviderStatusBadge enabled={currentEnabled} apiKeySet={resolvedProviderConfig.apiKeySet} />
563
525
  </div>
564
526
  </div>
565
- </div>
527
+ </ConfigSplitPaneHeader>
566
528
 
567
- <form onSubmit={handleSubmit} className="flex min-h-0 flex-1 flex-col">
568
- <div className="min-h-0 flex-1 space-y-5 overflow-y-auto px-6 py-5">
529
+ <form onSubmit={handleSubmit} className='flex min-h-0 flex-1 flex-col'>
530
+ <ConfigSplitPaneBody className='space-y-5 px-6 py-5'>
569
531
  <ProviderEnabledField enabled={enabled} onChange={setEnabled} />
570
532
 
571
533
  {isCustomProvider && (
572
- <div className="space-y-2">
573
- <Label htmlFor="providerDisplayName" className="text-sm font-medium text-gray-900">
534
+ <div className='space-y-2'>
535
+ <Label htmlFor='providerDisplayName' className='text-sm font-medium text-gray-900'>
574
536
  {t('providerDisplayName')}
575
537
  </Label>
576
538
  <Input
577
- id="providerDisplayName"
578
- type="text"
539
+ id='providerDisplayName'
540
+ type='text'
579
541
  value={providerDisplayName}
580
542
  onChange={(e) => setProviderDisplayName(e.target.value)}
581
543
  placeholder={defaultDisplayName || t('providerDisplayNamePlaceholder')}
582
- className="rounded-xl"
544
+ className='rounded-xl'
583
545
  />
584
- <p className="text-xs text-gray-500">{t('providerDisplayNameHelpShort')}</p>
546
+ <p className='text-xs text-gray-500'>{t('providerDisplayNameHelpShort')}</p>
585
547
  </div>
586
548
  )}
587
549
 
588
- <div className="space-y-2">
589
- <Label htmlFor="apiKey" className="text-sm font-medium text-gray-900">
550
+ <div className='space-y-2'>
551
+ <Label htmlFor='apiKey' className='text-sm font-medium text-gray-900'>
590
552
  {apiKeyHint?.label ?? t('apiKey')}
591
553
  </Label>
592
554
  <MaskedInput
593
- id="apiKey"
555
+ id='apiKey'
594
556
  value={apiKey}
595
557
  isSet={resolvedProviderConfig.apiKeySet}
596
558
  onChange={(e) => setApiKey(e.target.value)}
597
559
  placeholder={apiKeyHint?.placeholder ?? t('enterApiKey')}
598
- className="rounded-xl"
560
+ className='rounded-xl'
599
561
  />
600
- <p className="text-xs text-gray-500">{t('leaveBlankToKeepUnchanged')}</p>
562
+ <p className='text-xs text-gray-500'>{t('leaveBlankToKeepUnchanged')}</p>
601
563
  </div>
602
564
 
603
565
  <ProviderAuthSection
@@ -617,19 +579,19 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
617
579
  authStatusMessage={authStatusMessage}
618
580
  />
619
581
 
620
- <div className="space-y-2">
621
- <Label htmlFor="apiBase" className="text-sm font-medium text-gray-900">
582
+ <div className='space-y-2'>
583
+ <Label htmlFor='apiBase' className='text-sm font-medium text-gray-900'>
622
584
  {apiBaseHint?.label ?? t('apiBase')}
623
585
  </Label>
624
586
  <Input
625
- id="apiBase"
626
- type="text"
587
+ id='apiBase'
588
+ type='text'
627
589
  value={apiBase}
628
590
  onChange={(e) => setApiBase(e.target.value)}
629
591
  placeholder={defaultApiBase || apiBaseHint?.placeholder || 'https://api.example.com'}
630
- className="rounded-xl"
592
+ className='rounded-xl'
631
593
  />
632
- <p className="text-xs text-gray-500">{apiBaseHelpText}</p>
594
+ <p className='text-xs text-gray-500'>{apiBaseHelpText}</p>
633
595
  </div>
634
596
 
635
597
  <ProviderModelsSection
@@ -667,18 +629,18 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
667
629
  extraHeaders={extraHeaders}
668
630
  onExtraHeadersChange={setExtraHeaders}
669
631
  />
670
- </div>
632
+ </ConfigSplitPaneBody>
671
633
 
672
- <div className="flex items-center justify-between border-t border-gray-100 px-6 py-4">
673
- <Button type="button" variant="outline" size="sm" onClick={handleTestConnection} disabled={testProviderConnection.isPending}>
674
- <CircleDotDashed className="mr-1.5 h-4 w-4" />
634
+ <ConfigSplitPaneFooter className='flex items-center justify-between px-6 py-4'>
635
+ <Button type='button' variant='outline' size='sm' onClick={handleTestConnection} disabled={testProviderConnection.isPending}>
636
+ <CircleDotDashed className='mr-1.5 h-4 w-4' />
675
637
  {testProviderConnection.isPending ? t('providerTestingConnection') : t('providerTestConnection')}
676
638
  </Button>
677
- <Button type="submit" disabled={updateProvider.isPending || !hasChanges}>
639
+ <Button type='submit' disabled={updateProvider.isPending || !hasChanges}>
678
640
  {updateProvider.isPending ? t('saving') : hasChanges ? t('save') : t('unchanged')}
679
641
  </Button>
680
- </div>
642
+ </ConfigSplitPaneFooter>
681
643
  </form>
682
- </div>
644
+ </ConfigSplitDetailPane>
683
645
  );
684
646
  }