@nextclaw/ui 0.14.1-beta.0 → 0.14.1-beta.1

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 (126) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/assets/{api-C6EDHBDR.js → api-dQ6uQSZG.js} +3 -3
  3. package/dist/assets/{channels-list-page-DKj6m1Hc.js → channels-list-page-DfpjB8XB.js} +2 -2
  4. package/dist/assets/chat-page-D3QaEqjs.js +105 -0
  5. package/dist/assets/{config-split-page-CjlmJy8X.js → config-split-page-CeFfbaqc.js} +1 -1
  6. package/dist/assets/{confirm-dialog-B39X0pYp.js → confirm-dialog-C95Y4QPC.js} +1 -1
  7. package/dist/assets/{desktop-update-config-DfUQ8nly.js → desktop-update-config-CcK6jiEb.js} +1 -1
  8. package/dist/assets/{dist-CmMro3nV.js → dist-DTGzeil6.js} +1 -1
  9. package/dist/assets/{dist-BIYV2-Ia.js → dist-fGogBmPA.js} +1 -1
  10. package/dist/assets/doc-browser-CuBTcBEZ.js +1 -0
  11. package/dist/assets/{doc-browser-CDtUtcim.js → doc-browser-DVHhEb96.js} +1 -1
  12. package/dist/assets/{doc-browser-context-DZbC9zVB.js → doc-browser-context-BfMXwnrz.js} +1 -1
  13. package/dist/assets/{ellipsis-DDpgTzgx.js → ellipsis-D2F4KMiU.js} +1 -1
  14. package/dist/assets/{external-link-D2cBOCqn.js → external-link-CcgtBK26.js} +1 -1
  15. package/dist/assets/index-ByJCbQqr.css +1 -0
  16. package/dist/assets/index-DjXqiloY.js +103 -0
  17. package/dist/assets/mcp-marketplace-page-98-V__3u.js +1 -0
  18. package/dist/assets/mcp-marketplace-page-BNq_PNmO.js +40 -0
  19. package/dist/assets/model-config-page-aAUkZkSX.js +1 -0
  20. package/dist/assets/plus-1Rxl5FlV.js +1 -0
  21. package/dist/assets/provider-scoped-model-input-we4_Z4gP.js +1 -0
  22. package/dist/assets/providers-config-page-CtMkPSyu.js +1 -0
  23. package/dist/assets/{react-DYQXXGbH.js → react-CTqdhFMk.js} +1 -1
  24. package/dist/assets/remote-fQ7N_r8u.js +1 -0
  25. package/dist/assets/rotate-cw-CzfIeAJj.js +1 -0
  26. package/dist/assets/runtime-config-page-TD1UrTbG.js +1 -0
  27. package/dist/assets/{save-BNVTgBVn.js → save-C9fKWfVB.js} +1 -1
  28. package/dist/assets/{search-CKCzwLId.js → search-D5FQ5q0X.js} +1 -1
  29. package/dist/assets/{search-config-page-DG5Q2hF1.js → search-config-page-AMEJJNIS.js} +1 -1
  30. package/dist/assets/secrets-config-page-CcgJnvBO.js +3 -0
  31. package/dist/assets/{select-CIRwTNP4.js → select-Cu5n0E-3.js} +2 -2
  32. package/dist/assets/{tag-chip-DKhVFtTE.js → tag-chip-C5aOtjxu.js} +1 -1
  33. package/dist/assets/use-config-D8TESeo6.js +1 -0
  34. package/dist/assets/x-1qETdQ70.js +1 -0
  35. package/dist/index.html +14 -14
  36. package/package.json +9 -9
  37. package/src/features/chat/components/conversation/__tests__/chat-conversation-panel.test.tsx +12 -63
  38. package/src/features/chat/components/conversation/chat-conversation-content.tsx +11 -50
  39. package/src/features/chat/components/conversation/chat-conversation-panel.tsx +15 -2
  40. package/src/features/chat/features/input/components/chat-input-bar.container.tsx +84 -6
  41. package/src/features/chat/features/input/hooks/use-chat-input-bar-query-state.ts +2 -0
  42. package/src/features/chat/features/input/hooks/use-chat-model-favorites.ts +87 -0
  43. package/src/features/chat/features/input/utils/__tests__/chat-input-bar.utils.test.ts +64 -24
  44. package/src/features/chat/features/input/utils/__tests__/ncp-chat-input-availability.utils.test.ts +1 -0
  45. package/src/features/chat/features/input/utils/chat-input-bar.utils.ts +21 -12
  46. package/src/features/chat/features/input/utils/chat-input-toolbar.utils.ts +80 -25
  47. package/src/features/chat/features/message/utils/__tests__/chat-message-file-operation.utils.test.ts +310 -0
  48. package/src/features/chat/features/message/utils/__tests__/chat-message-test.utils.ts +43 -0
  49. package/src/features/chat/features/message/utils/__tests__/chat-message.utils.test.ts +1 -346
  50. package/src/features/chat/features/message/utils/chat-message-tool-card.utils.ts +10 -9
  51. package/src/features/chat/features/message/utils/file-operation/diff.utils.ts +39 -36
  52. package/src/features/chat/features/message/utils/file-operation/line-builder.utils.ts +46 -38
  53. package/src/features/chat/features/message/utils/file-operation/parsed-block.utils.ts +18 -15
  54. package/src/features/chat/features/session/components/session-header/__tests__/chat-session-project-badge.test.tsx +25 -25
  55. package/src/features/chat/features/session/components/session-header/chat-session-project-badge.tsx +15 -2
  56. package/src/features/chat/features/session-type/components/__tests__/chat-sidebar-create-menu.test.tsx +24 -0
  57. package/src/features/chat/features/session-type/components/chat-sidebar-create-menu.tsx +10 -1
  58. package/src/features/chat/features/welcome/components/__tests__/chat-conversation-welcome.test.tsx +193 -0
  59. package/src/features/chat/features/welcome/components/__tests__/chat-welcome.test.tsx +123 -0
  60. package/src/features/chat/features/welcome/components/chat-conversation-welcome.tsx +104 -0
  61. package/src/features/chat/features/welcome/components/chat-welcome-agent-picker.tsx +72 -0
  62. package/src/features/chat/features/welcome/components/chat-welcome-capability-grid.tsx +54 -0
  63. package/src/features/chat/features/welcome/components/chat-welcome-project-picker.tsx +132 -0
  64. package/src/features/chat/features/welcome/components/chat-welcome-session-type-picker.tsx +82 -0
  65. package/src/features/chat/features/welcome/components/chat-welcome.tsx +121 -0
  66. package/src/features/chat/features/welcome/utils/__tests__/chat-welcome-project-options.utils.test.ts +53 -0
  67. package/src/features/chat/features/welcome/utils/__tests__/chat-welcome-selection.utils.test.ts +63 -0
  68. package/src/features/chat/features/welcome/utils/__tests__/chat-welcome-visibility.utils.test.ts +34 -0
  69. package/src/features/chat/features/welcome/utils/chat-welcome-project-options.utils.ts +71 -0
  70. package/src/features/chat/features/welcome/utils/chat-welcome-selection.utils.ts +79 -0
  71. package/src/features/chat/features/welcome/utils/chat-welcome-visibility.utils.ts +17 -0
  72. package/src/features/chat/managers/__tests__/chat-input.manager.test.ts +65 -0
  73. package/src/features/chat/managers/chat-input.manager.ts +24 -2
  74. package/src/features/chat/stores/chat-input.store.ts +2 -0
  75. package/src/features/chat/types/chat-input-bar.types.ts +5 -0
  76. package/src/features/marketplace/utils/marketplace-installed-cache.utils.ts +27 -23
  77. package/src/features/settings/components/config/provider-form.tsx +284 -387
  78. package/src/features/settings/components/config/provider-status-badge.tsx +4 -4
  79. package/src/features/settings/components/config/secrets-config-form.tsx +11 -135
  80. package/src/features/settings/hooks/use-provider-auth-flow.ts +140 -0
  81. package/src/features/settings/pages/__tests__/model-config-page.test.tsx +16 -0
  82. package/src/features/settings/pages/secrets-config-page.tsx +2 -1
  83. package/src/features/settings/utils/__tests__/provider-form-support.utils.test.ts +60 -1
  84. package/src/features/settings/utils/__tests__/secrets-config-form.utils.test.ts +66 -0
  85. package/src/features/settings/utils/provider-form-context.utils.ts +131 -0
  86. package/src/features/settings/utils/provider-form-model.utils.ts +101 -0
  87. package/src/features/settings/utils/provider-form-support.utils.ts +102 -1
  88. package/src/features/settings/utils/secrets-config-form.utils.ts +139 -0
  89. package/src/features/system-status/components/config/runtime-agent-list-card.tsx +17 -11
  90. package/src/features/system-status/components/config/runtime-binding-list-card.tsx +14 -9
  91. package/src/features/system-status/components/config/runtime-config-editor.tsx +8 -5
  92. package/src/features/system-status/components/config/runtime-entry-list-card.tsx +14 -9
  93. package/src/features/system-status/components/config/runtime-settings-card.tsx +16 -7
  94. package/src/features/system-status/components/runtime-security-card.tsx +79 -25
  95. package/src/shared/components/common/__tests__/searchable-model-input.test.tsx +34 -0
  96. package/src/shared/components/common/provider-scoped-model-input.tsx +1 -0
  97. package/src/shared/components/common/searchable-model-input.tsx +102 -87
  98. package/src/shared/components/ui/README.md +7 -0
  99. package/src/shared/components/ui/popover.tsx +19 -3
  100. package/src/shared/components/ui/select.tsx +14 -4
  101. package/src/shared/hooks/use-infinite-scroll-loader.ts +14 -10
  102. package/src/shared/lib/api/index.ts +2 -0
  103. package/src/shared/lib/api/preferences/preference.types.ts +22 -0
  104. package/src/shared/lib/api/preferences/preference.utils.ts +40 -0
  105. package/src/shared/lib/i18n/locales/en-US/chat.json +16 -1
  106. package/src/shared/lib/i18n/locales/en-US/core.json +1 -0
  107. package/src/shared/lib/i18n/locales/zh-CN/chat.json +16 -1
  108. package/src/shared/lib/i18n/locales/zh-CN/core.json +1 -0
  109. package/dist/assets/chat-page-BXngLJY6.js +0 -105
  110. package/dist/assets/doc-browser-B6Wc_TIY.js +0 -1
  111. package/dist/assets/index-DTb_XVIH.js +0 -103
  112. package/dist/assets/index-ayiPYhf1.css +0 -1
  113. package/dist/assets/mcp-marketplace-page-DZ9H0v05.js +0 -1
  114. package/dist/assets/mcp-marketplace-page-cnVWf7QQ.js +0 -40
  115. package/dist/assets/model-config-page-Bcb35AVv.js +0 -1
  116. package/dist/assets/plus-CYE1onU2.js +0 -1
  117. package/dist/assets/provider-scoped-model-input-kdoNp4HG.js +0 -1
  118. package/dist/assets/providers-config-page-CQSibSwO.js +0 -1
  119. package/dist/assets/remote-BlJDbXOI.js +0 -1
  120. package/dist/assets/rotate-cw-BWbvFDAX.js +0 -1
  121. package/dist/assets/runtime-config-page-Tn2yLQ0c.js +0 -1
  122. package/dist/assets/secrets-config-page-BXn5Uuwp.js +0 -3
  123. package/dist/assets/use-config-DgKM9VIU.js +0 -1
  124. package/dist/assets/x-Cm3JjYut.js +0 -1
  125. package/src/features/chat/components/__tests__/chat-welcome.test.tsx +0 -45
  126. package/src/features/chat/components/chat-welcome.tsx +0 -102
@@ -1,15 +1,11 @@
1
- import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
- import { useQueryClient } from '@tanstack/react-query';
1
+ import { useEffect, useMemo, useState } from 'react';
3
2
  import { CircleDotDashed, Trash2 } from 'lucide-react';
4
3
  import { toast } from 'sonner';
5
4
  import {
6
5
  useConfigSchema,
7
6
  useDeleteProvider,
8
- useImportProviderAuthFromCli,
9
- usePollProviderAuth,
10
7
  useProviders,
11
8
  useProviderTemplates,
12
- useStartProviderAuth,
13
9
  useTestProviderConnection,
14
10
  useUpdateProvider
15
11
  } from '@/shared/hooks/use-config';
@@ -17,9 +13,8 @@ import { MaskedInput } from '@/shared/components/common/masked-input';
17
13
  import { Button } from '@/shared/components/ui/button';
18
14
  import { Input } from '@/shared/components/ui/input';
19
15
  import { Label } from '@/shared/components/ui/label';
20
- import { hintForPath } from '@/shared/lib/config-hints';
21
16
  import { getLanguage, t } from '@/shared/lib/i18n';
22
- import type { ProviderConfigUpdate, ProviderConnectionTestRequest, ThinkingLevel } from '@/shared/lib/api';
17
+ import type { ThinkingLevel } from '@/shared/lib/api';
23
18
  import {
24
19
  ConfigSplitDetailPane,
25
20
  ConfigSplitEmptyPane,
@@ -30,44 +25,82 @@ import {
30
25
  import { ProviderAdvancedSettingsSection } from '@/features/settings/components/config/provider-advanced-settings-section';
31
26
  import { ProviderAuthSection } from '@/features/settings/components/config/provider-auth-section';
32
27
  import {
33
- EMPTY_PROVIDER_CONFIG,
28
+ buildProviderConnectionTestPayload,
29
+ buildProviderSavePayload,
34
30
  formatThinkingLevelLabel,
35
- headersEqual,
36
- modelListsEqual,
37
- modelConfigEqual,
38
- mergeModelConfig,
39
- normalizeHeaders,
40
- normalizeModelList,
31
+ hasProviderFormChanges,
41
32
  normalizeModelConfigForModels,
42
- resolveEditableModels,
43
- resolvePreferredAuthMethodId,
44
- serializeModelsForSave,
45
- shouldUsePillSelector,
46
33
  THINKING_LEVELS,
47
- toProviderLocalModelId,
48
34
  type ModelConfig,
49
35
  type WireApiType
50
36
  } from '@/features/settings/utils/provider-form-support.utils';
37
+ import {
38
+ addProviderLocalModel,
39
+ removeProviderLocalModel,
40
+ setModelThinkingDefaultInConfig,
41
+ setModelVisionInConfig,
42
+ toggleModelThinkingLevelInConfig
43
+ } from '@/features/settings/utils/provider-form-model.utils';
44
+ import { resolveProviderFormContext } from '@/features/settings/utils/provider-form-context.utils';
51
45
  import { ProviderModelsSection } from './provider-models-section';
52
- import type { PillSelectOption } from '@/features/settings/components/config/provider-pill-selector';
53
46
  import { ProviderStatusBadge } from '@/features/settings/components/config/provider-status-badge';
54
- import { hostCapabilityManager } from '@/shared/lib/host-capabilities';
47
+ import { useProviderAuthFlow } from '@/features/settings/hooks/use-provider-auth-flow';
55
48
  type ProviderFormProps = {
56
49
  providerName?: string;
57
50
  onProviderDeleted?: (providerName: string) => void;
58
51
  };
52
+ type ProviderFormContext = ReturnType<typeof resolveProviderFormContext>;
53
+ type ProviderFormDetailPaneProps = {
54
+ context: ProviderFormContext;
55
+ language: ReturnType<typeof getLanguage>;
56
+ providerTitle: string;
57
+ providerDisplayName: string;
58
+ apiKey: string;
59
+ apiBase: string;
60
+ extraHeaders: Record<string, string> | null;
61
+ wireApi: WireApiType;
62
+ models: string[];
63
+ modelConfig: ModelConfig;
64
+ modelDraft: string;
65
+ showAdvanced: boolean;
66
+ showModelInput: boolean;
67
+ resolvedAuthMethodId: string;
68
+ authSessionId: string | null;
69
+ authStatusMessage: string;
70
+ hasChanges: boolean;
71
+ isDeletePending: boolean;
72
+ isUpdatePending: boolean;
73
+ isTestPending: boolean;
74
+ startPending: boolean;
75
+ importPending: boolean;
76
+ onProviderDisplayNameChange: (value: string) => void;
77
+ onApiKeyChange: (value: string) => void;
78
+ onApiBaseChange: (value: string) => void;
79
+ onAuthMethodChange: (value: string) => void;
80
+ onStartProviderAuth: () => void;
81
+ onImportProviderAuthFromCli: () => void;
82
+ onModelDraftChange: (value: string) => void;
83
+ onShowModelInputChange: (show: boolean) => void;
84
+ onAddModel: () => void;
85
+ onRemoveModel: (modelName: string) => void;
86
+ onToggleModelThinkingLevel: (modelName: string, level: ThinkingLevel) => void;
87
+ onSetModelThinkingDefault: (modelName: string, level: ThinkingLevel | null) => void;
88
+ onSetModelVision: (modelName: string, vision: boolean) => void;
89
+ onShowAdvancedChange: (show: boolean) => void;
90
+ onWireApiChange: (wireApi: WireApiType) => void;
91
+ onExtraHeadersChange: (headers: Record<string, string> | null) => void;
92
+ onDeleteProvider: () => void;
93
+ onSubmit: React.FormEventHandler<HTMLFormElement>;
94
+ onTestConnection: () => void;
95
+ };
59
96
 
60
97
  export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormProps) {
61
- const queryClient = useQueryClient();
62
98
  const { data: providersView } = useProviders();
63
99
  const { data: templatesView } = useProviderTemplates();
64
100
  const { data: schema } = useConfigSchema();
65
101
  const updateProvider = useUpdateProvider();
66
102
  const deleteProvider = useDeleteProvider();
67
103
  const testProviderConnection = useTestProviderConnection();
68
- const startProviderAuth = useStartProviderAuth();
69
- const pollProviderAuth = usePollProviderAuth();
70
- const importProviderAuthFromCli = useImportProviderAuthFromCli();
71
104
 
72
105
  const [apiKey, setApiKey] = useState('');
73
106
  const [apiBase, setApiBase] = useState('');
@@ -79,74 +112,30 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
79
112
  const [providerDisplayName, setProviderDisplayName] = useState('');
80
113
  const [showAdvanced, setShowAdvanced] = useState(false);
81
114
  const [showModelInput, setShowModelInput] = useState(false);
82
- const [authSessionId, setAuthSessionId] = useState<string | null>(null);
83
- const [authStatusMessage, setAuthStatusMessage] = useState('');
84
115
  const [authMethodId, setAuthMethodId] = useState('');
85
- const authPollTimerRef = useRef<number | null>(null);
86
116
 
87
- const providerConfig = providerName ? providersView?.providers[providerName] : null;
88
- const providerSpec = templatesView?.providerTemplates.find((template) => template.providerType === providerConfig?.providerType);
89
- const resolvedProviderConfig = providerConfig ?? EMPTY_PROVIDER_CONFIG;
90
- const uiHints = schema?.uiHints;
91
- const isCustomProvider = Boolean(providerConfig?.isCustom);
92
- const apiKeyHint = providerName ? hintForPath(`providers.${providerName}.apiKey`, uiHints) : undefined;
93
- const apiBaseHint = providerName ? hintForPath(`providers.${providerName}.apiBase`, uiHints) : undefined;
94
- const extraHeadersHint = providerName ? hintForPath(`providers.${providerName}.extraHeaders`, uiHints) : undefined;
95
- const wireApiHint = providerName ? hintForPath(`providers.${providerName}.wireApi`, uiHints) : undefined;
96
- const defaultDisplayName = providerSpec?.displayName || providerConfig?.displayName || providerName || '';
97
- const currentDisplayName = (resolvedProviderConfig.displayName || '').trim();
98
- const effectiveDisplayName = currentDisplayName || defaultDisplayName;
99
- const currentEnabled = resolvedProviderConfig.enabled !== false;
100
- const providerTitle = providerDisplayName.trim() || effectiveDisplayName || providerName || t('providersSelectPlaceholder');
101
- const providerModelAliases = useMemo(
102
- () => normalizeModelList([providerSpec?.modelPrefix || providerSpec?.providerType || '', providerName || '']),
103
- [providerName, providerSpec?.modelPrefix, providerSpec?.providerType]
104
- );
105
- const defaultApiBase = providerSpec?.defaultApiBase || '';
106
- const currentApiBase = resolvedProviderConfig.apiBase || defaultApiBase;
107
- const currentHeaders = normalizeHeaders(resolvedProviderConfig.extraHeaders || null);
108
- const currentWireApi = (resolvedProviderConfig.wireApi || providerSpec?.defaultWireApi || 'auto') as WireApiType;
109
- const defaultModels = useMemo(
110
- () =>
111
- normalizeModelList(
112
- (providerSpec?.defaultModels ?? []).map((model) => toProviderLocalModelId(model, providerModelAliases))
113
- ),
114
- [providerSpec?.defaultModels, providerModelAliases]
115
- );
116
- const currentModels = useMemo(
117
- () =>
118
- normalizeModelList(
119
- (resolvedProviderConfig.models ?? []).map((model) => toProviderLocalModelId(model, providerModelAliases))
120
- ),
121
- [resolvedProviderConfig.models, providerModelAliases]
122
- );
123
- const currentEditableModels = useMemo(() => resolveEditableModels(defaultModels, currentModels), [defaultModels, currentModels]);
124
- const currentModelConfig = useMemo(
125
- () =>
126
- normalizeModelConfigForModels(
127
- mergeModelConfig(providerSpec?.modelConfig, resolvedProviderConfig.modelConfig, providerModelAliases),
128
- currentEditableModels
129
- ),
130
- [currentEditableModels, providerModelAliases, providerSpec?.modelConfig, resolvedProviderConfig.modelConfig]
131
- );
132
117
  const language = getLanguage();
133
- const apiBaseHelpText =
134
- providerSpec?.apiBaseHelp?.[language] || providerSpec?.apiBaseHelp?.en || apiBaseHint?.help || t('providerApiBaseHelp');
135
- const providerAuth = providerSpec?.auth;
136
- const providerAuthMethods = useMemo(() => providerAuth?.methods ?? [], [providerAuth?.methods]);
137
- const supportsWireApi = Boolean(providerSpec?.supportsWireApi) || isCustomProvider;
138
- const providerAuthMethodOptions = useMemo(
139
- () =>
140
- providerAuthMethods.map((method) => ({
141
- value: method.id,
142
- label: method.label?.[language] || method.label?.en || method.id
143
- })),
144
- [providerAuthMethods, language]
145
- );
146
- const preferredAuthMethodId = useMemo(
147
- () => resolvePreferredAuthMethodId({ providerName: providerSpec?.providerType ?? providerName, methods: providerAuthMethods, defaultMethodId: providerAuth?.defaultMethodId, language }),
148
- [providerName, providerAuth?.defaultMethodId, providerAuthMethods, language, providerSpec?.providerType]
118
+ const providerFormContext = useMemo(
119
+ () => resolveProviderFormContext({ providerName, providersView, templatesView, schema, language }),
120
+ [language, providerName, providersView, schema, templatesView]
149
121
  );
122
+ const {
123
+ currentApiBase,
124
+ currentEditableModels,
125
+ currentHeaders,
126
+ currentModelConfig,
127
+ currentWireApi,
128
+ defaultApiBase,
129
+ effectiveDisplayName,
130
+ providerAuth,
131
+ providerAuthMethods,
132
+ providerConfig,
133
+ providerModelAliases,
134
+ resolvedProviderConfig,
135
+ preferredAuthMethodId,
136
+ supportsWireApi,
137
+ } = providerFormContext;
138
+ const providerTitle = providerDisplayName.trim() || effectiveDisplayName || providerName || t('providersSelectPlaceholder');
150
139
  const resolvedAuthMethodId = useMemo(() => {
151
140
  if (!providerAuthMethods.length) {
152
141
  return '';
@@ -157,72 +146,14 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
157
146
  }
158
147
  return preferredAuthMethodId || providerAuthMethods[0]?.id || '';
159
148
  }, [authMethodId, preferredAuthMethodId, providerAuthMethods]);
160
- const selectedAuthMethod = useMemo(
161
- () => providerAuthMethods.find((method) => method.id === resolvedAuthMethodId),
162
- [providerAuthMethods, resolvedAuthMethodId]
163
- );
164
- const shouldUseAuthMethodPills = shouldUsePillSelector({
165
- required: providerAuth?.kind === 'device_code',
166
- hasDefault: Boolean(providerAuth?.defaultMethodId?.trim()),
167
- optionCount: providerAuthMethods.length
168
- });
169
- const wireApiSelectOptions: PillSelectOption[] = (providerSpec?.wireApiOptions || ['auto', 'chat', 'responses']).map((option) => ({
170
- value: option,
171
- label: option === 'chat' ? t('wireApiChat') : option === 'responses' ? t('wireApiResponses') : t('wireApiAuto')
172
- }));
173
- const shouldUseWireApiPills = shouldUsePillSelector({
174
- required: supportsWireApi,
175
- hasDefault: typeof providerSpec?.defaultWireApi === 'string' && providerSpec.defaultWireApi.length > 0,
176
- optionCount: wireApiSelectOptions.length
177
- });
178
-
179
- const clearAuthPollTimer = useCallback(() => {
180
- if (authPollTimerRef.current !== null) {
181
- window.clearTimeout(authPollTimerRef.current);
182
- authPollTimerRef.current = null;
183
- }
184
- }, []);
185
-
186
- const scheduleProviderAuthPoll = useCallback(
187
- (sessionId: string, delayMs: number) => {
188
- clearAuthPollTimer();
189
- authPollTimerRef.current = window.setTimeout(() => {
190
- void (async () => {
191
- if (!providerName) {
192
- return;
193
- }
194
- try {
195
- const result = await pollProviderAuth.mutateAsync({ provider: providerName, data: { sessionId } });
196
- if (result.status === 'pending') {
197
- setAuthStatusMessage(t('providerAuthWaitingBrowser'));
198
- scheduleProviderAuthPoll(sessionId, result.nextPollMs ?? delayMs);
199
- return;
200
- }
201
- if (result.status === 'authorized') {
202
- setAuthSessionId(null);
203
- clearAuthPollTimer();
204
- setAuthStatusMessage(t('providerAuthCompleted'));
205
- toast.success(t('providerAuthCompleted'));
206
- queryClient.invalidateQueries({ queryKey: ['config'] });
207
- queryClient.invalidateQueries({ queryKey: ['providers'] });
208
- return;
209
- }
210
- setAuthSessionId(null);
211
- clearAuthPollTimer();
212
- setAuthStatusMessage(result.message || `Authorization ${result.status}.`);
213
- toast.error(result.message || `Authorization ${result.status}.`);
214
- } catch (error) {
215
- setAuthSessionId(null);
216
- clearAuthPollTimer();
217
- const message = error instanceof Error ? error.message : String(error);
218
- setAuthStatusMessage(message);
219
- toast.error(`Authorization failed: ${message}`);
220
- }
221
- })();
222
- }, Math.max(1000, delayMs));
223
- },
224
- [clearAuthPollTimer, pollProviderAuth, providerName, queryClient]
225
- );
149
+ const {
150
+ authSessionId,
151
+ authStatusMessage,
152
+ importAuthFromCli,
153
+ importPending,
154
+ startAuth,
155
+ startPending
156
+ } = useProviderAuthFlow({ providerName, providerAuth, resolvedAuthMethodId });
226
157
 
227
158
  useEffect(() => {
228
159
  if (!providerName) {
@@ -234,10 +165,7 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
234
165
  setModelConfig({});
235
166
  setModelDraft('');
236
167
  setProviderDisplayName('');
237
- setAuthSessionId(null);
238
- setAuthStatusMessage('');
239
168
  setAuthMethodId('');
240
- clearAuthPollTimer();
241
169
  return;
242
170
  }
243
171
 
@@ -249,10 +177,7 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
249
177
  setModelConfig(currentModelConfig);
250
178
  setModelDraft('');
251
179
  setProviderDisplayName(effectiveDisplayName);
252
- setAuthSessionId(null);
253
- setAuthStatusMessage('');
254
180
  setAuthMethodId(preferredAuthMethodId);
255
- clearAuthPollTimer();
256
181
  }, [
257
182
  providerName,
258
183
  currentApiBase,
@@ -261,26 +186,29 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
261
186
  currentEditableModels,
262
187
  currentModelConfig,
263
188
  effectiveDisplayName,
264
- preferredAuthMethodId,
265
- clearAuthPollTimer
189
+ preferredAuthMethodId
266
190
  ]);
267
191
 
268
- useEffect(() => () => clearAuthPollTimer(), [clearAuthPollTimer]);
269
192
  useEffect(() => setModelConfig((prev) => normalizeModelConfigForModels(prev, models)), [models]);
270
193
 
271
194
  const hasChanges = useMemo(() => {
272
- if (!providerName) {
273
- return false;
274
- }
275
- return (
276
- apiKey.trim().length > 0 ||
277
- apiBase.trim() !== currentApiBase.trim() ||
278
- !headersEqual(extraHeaders, currentHeaders) ||
279
- (supportsWireApi && wireApi !== currentWireApi) ||
280
- !modelListsEqual(models, currentEditableModels) ||
281
- !modelConfigEqual(modelConfig, currentModelConfig) ||
282
- providerDisplayName.trim() !== effectiveDisplayName
283
- );
195
+ return hasProviderFormChanges({
196
+ providerName,
197
+ apiKey,
198
+ apiBase,
199
+ currentApiBase,
200
+ extraHeaders,
201
+ currentHeaders,
202
+ supportsWireApi,
203
+ wireApi,
204
+ currentWireApi,
205
+ models,
206
+ currentEditableModels,
207
+ modelConfig,
208
+ currentModelConfig,
209
+ providerDisplayName,
210
+ effectiveDisplayName
211
+ });
284
212
  }, [
285
213
  providerName,
286
214
  apiKey,
@@ -300,89 +228,25 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
300
228
  ]);
301
229
 
302
230
  const handleAddModel = () => {
303
- const next = toProviderLocalModelId(modelDraft, providerModelAliases);
304
- if (!next) {
305
- return;
306
- }
307
- if (next.includes('/')) {
231
+ const result = addProviderLocalModel(models, modelDraft, providerModelAliases);
232
+ if (result.errorKey) {
308
233
  toast.error(t('providerModelInvalidProviderPrefix'));
309
234
  return;
310
235
  }
311
- if (models.includes(next)) {
312
- setModelDraft('');
313
- return;
314
- }
315
- setModels((prev) => [...prev, next]);
316
- setModelDraft('');
236
+ setModels(result.models);
237
+ setModelDraft(result.draft);
317
238
  };
318
239
 
319
240
  const toggleModelThinkingLevel = (modelName: string, level: ThinkingLevel) => {
320
- setModelConfig((prev) => {
321
- const currentEntry = prev[modelName];
322
- const currentLevels = currentEntry?.thinking?.supported ?? [];
323
- const nextLevels = currentLevels.includes(level)
324
- ? currentLevels.filter((item) => item !== level)
325
- : THINKING_LEVELS.filter((item) => item === level || currentLevels.includes(item));
326
- const nextDefault =
327
- currentEntry?.thinking?.default && nextLevels.includes(currentEntry.thinking.default)
328
- ? currentEntry.thinking.default
329
- : undefined;
330
- const nextEntry = {
331
- ...currentEntry,
332
- thinking:
333
- nextLevels.length > 0
334
- ? nextDefault
335
- ? { supported: nextLevels, default: nextDefault }
336
- : { supported: nextLevels }
337
- : undefined
338
- };
339
- const next = { ...prev };
340
- if (nextEntry.thinking || nextEntry.vision === true) {
341
- next[modelName] = nextEntry;
342
- } else {
343
- delete next[modelName];
344
- }
345
- return next;
346
- });
241
+ setModelConfig((prev) => toggleModelThinkingLevelInConfig(prev, modelName, level));
347
242
  };
348
243
 
349
244
  const setModelThinkingDefault = (modelName: string, level: ThinkingLevel | null) => {
350
- setModelConfig((prev) => {
351
- const currentEntry = prev[modelName];
352
- const currentThinking = currentEntry?.thinking;
353
- if (!currentThinking || currentThinking.supported.length === 0) {
354
- return prev;
355
- }
356
- if (level && !currentThinking.supported.includes(level)) {
357
- return prev;
358
- }
359
- return {
360
- ...prev,
361
- [modelName]: {
362
- ...currentEntry,
363
- thinking: level
364
- ? { supported: currentThinking.supported, default: level }
365
- : { supported: currentThinking.supported }
366
- }
367
- };
368
- });
245
+ setModelConfig((prev) => setModelThinkingDefaultInConfig(prev, modelName, level));
369
246
  };
370
247
 
371
248
  const setModelVision = (modelName: string, vision: boolean) => {
372
- setModelConfig((prev) => {
373
- const currentEntry = prev[modelName];
374
- const nextEntry = {
375
- ...currentEntry,
376
- vision: vision ? true : undefined
377
- };
378
- const next = { ...prev };
379
- if (nextEntry.thinking || nextEntry.vision === true) {
380
- next[modelName] = nextEntry;
381
- } else {
382
- delete next[modelName];
383
- }
384
- return next;
385
- });
249
+ setModelConfig((prev) => setModelVisionInConfig(prev, modelName, vision));
386
250
  };
387
251
 
388
252
  const handleSubmit = (e: React.FormEvent) => {
@@ -391,57 +255,46 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
391
255
  return;
392
256
  }
393
257
 
394
- const payload: ProviderConfigUpdate = {};
395
- const trimmedApiKey = apiKey.trim();
396
- const trimmedApiBase = apiBase.trim();
397
- const normalizedHeaders = normalizeHeaders(extraHeaders);
398
- const trimmedDisplayName = providerDisplayName.trim();
399
-
400
- if (trimmedDisplayName !== effectiveDisplayName) {
401
- payload.displayName = trimmedDisplayName.length > 0 ? trimmedDisplayName : null;
402
- }
403
- if (trimmedApiKey.length > 0) {
404
- payload.apiKey = trimmedApiKey;
405
- }
406
- if (trimmedApiBase !== currentApiBase.trim()) {
407
- payload.apiBase = trimmedApiBase.length > 0 && trimmedApiBase !== defaultApiBase ? trimmedApiBase : null;
408
- }
409
- if (!headersEqual(normalizedHeaders, currentHeaders)) {
410
- payload.extraHeaders = normalizedHeaders;
411
- }
412
- if (supportsWireApi && wireApi !== currentWireApi) {
413
- payload.wireApi = wireApi;
414
- }
415
- if (!modelListsEqual(models, currentEditableModels)) {
416
- payload.models = serializeModelsForSave(models, providerName);
417
- }
418
- if (!modelConfigEqual(modelConfig, currentModelConfig)) {
419
- payload.modelConfig = normalizeModelConfigForModels(modelConfig, models);
420
- }
421
-
422
- updateProvider.mutate({ provider: providerName, data: payload });
258
+ updateProvider.mutate({
259
+ provider: providerName,
260
+ data: buildProviderSavePayload({
261
+ providerName,
262
+ apiKey,
263
+ apiBase,
264
+ currentApiBase,
265
+ defaultApiBase,
266
+ extraHeaders,
267
+ currentHeaders,
268
+ supportsWireApi,
269
+ wireApi,
270
+ currentWireApi,
271
+ models,
272
+ currentEditableModels,
273
+ modelConfig,
274
+ currentModelConfig,
275
+ providerDisplayName,
276
+ effectiveDisplayName
277
+ })
278
+ });
423
279
  };
424
280
 
425
281
  const handleTestConnection = async () => {
426
282
  if (!providerName) {
427
283
  return;
428
284
  }
429
- const preferredModel = models.find((modelName) => modelName.trim().length > 0) ?? '';
430
- const testModel = toProviderLocalModelId(preferredModel, providerModelAliases);
431
- const payload: ProviderConnectionTestRequest = {
432
- apiBase: apiBase.trim(),
433
- extraHeaders: normalizeHeaders(extraHeaders),
434
- model: testModel || null
435
- };
436
- if (apiKey.trim().length > 0) {
437
- payload.apiKey = apiKey.trim();
438
- }
439
- if (supportsWireApi) {
440
- payload.wireApi = wireApi;
441
- }
442
-
443
285
  try {
444
- const result = await testProviderConnection.mutateAsync({ provider: providerName, data: payload });
286
+ const result = await testProviderConnection.mutateAsync({
287
+ provider: providerName,
288
+ data: buildProviderConnectionTestPayload({
289
+ apiKey,
290
+ apiBase,
291
+ extraHeaders,
292
+ supportsWireApi,
293
+ wireApi,
294
+ models,
295
+ providerModelAliases
296
+ })
297
+ });
445
298
  if (result.success) {
446
299
  toast.success(`${t('providerTestConnectionSuccess')} (${result.latencyMs}ms)`);
447
300
  return;
@@ -472,52 +325,6 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
472
325
  }
473
326
  };
474
327
 
475
- const handleStartProviderAuth = async () => {
476
- if (!providerName || providerAuth?.kind !== 'device_code') {
477
- return;
478
- }
479
-
480
- try {
481
- setAuthStatusMessage('');
482
- const result = await startProviderAuth.mutateAsync({
483
- provider: providerName,
484
- data: resolvedAuthMethodId ? { methodId: resolvedAuthMethodId } : {}
485
- });
486
- if (!result.sessionId || !result.verificationUri) {
487
- throw new Error(t('providerAuthStartFailed'));
488
- }
489
- setAuthSessionId(result.sessionId);
490
- setAuthStatusMessage(`${t('providerAuthOpenPrompt')}${result.userCode}${t('providerAuthOpenPromptSuffix')}`);
491
- await hostCapabilityManager.openExternalUrl(result.verificationUri);
492
- scheduleProviderAuthPoll(result.sessionId, result.intervalMs);
493
- } catch (error) {
494
- const message = error instanceof Error ? error.message : String(error);
495
- setAuthSessionId(null);
496
- clearAuthPollTimer();
497
- setAuthStatusMessage(message);
498
- toast.error(`${t('providerAuthStartFailed')}: ${message}`);
499
- }
500
- };
501
-
502
- const handleImportProviderAuthFromCli = async () => {
503
- if (!providerName || providerAuth?.kind !== 'device_code') {
504
- return;
505
- }
506
- try {
507
- clearAuthPollTimer();
508
- setAuthSessionId(null);
509
- const result = await importProviderAuthFromCli.mutateAsync({ provider: providerName });
510
- setAuthStatusMessage(`${t('providerAuthImportStatusPrefix')}${result.expiresAt ? ` (expires: ${result.expiresAt})` : ''}`);
511
- toast.success(t('providerAuthImportSuccess'));
512
- queryClient.invalidateQueries({ queryKey: ['config'] });
513
- queryClient.invalidateQueries({ queryKey: ['providers'] });
514
- } catch (error) {
515
- const message = error instanceof Error ? error.message : String(error);
516
- setAuthStatusMessage(message);
517
- toast.error(`${t('providerAuthImportFailed')}: ${message}`);
518
- }
519
- };
520
-
521
328
  if (!providerName || !providerConfig) {
522
329
  return (
523
330
  <ConfigSplitEmptyPane>
@@ -529,6 +336,103 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
529
336
  );
530
337
  }
531
338
 
339
+ return (
340
+ <ProviderFormDetailPane
341
+ context={providerFormContext}
342
+ language={language}
343
+ providerTitle={providerTitle}
344
+ providerDisplayName={providerDisplayName}
345
+ apiKey={apiKey}
346
+ apiBase={apiBase}
347
+ extraHeaders={extraHeaders}
348
+ wireApi={wireApi}
349
+ models={models}
350
+ modelConfig={modelConfig}
351
+ modelDraft={modelDraft}
352
+ showAdvanced={showAdvanced}
353
+ showModelInput={showModelInput}
354
+ resolvedAuthMethodId={resolvedAuthMethodId}
355
+ authSessionId={authSessionId}
356
+ authStatusMessage={authStatusMessage}
357
+ hasChanges={hasChanges}
358
+ isDeletePending={deleteProvider.isPending}
359
+ isUpdatePending={updateProvider.isPending}
360
+ isTestPending={testProviderConnection.isPending}
361
+ startPending={startPending}
362
+ importPending={importPending}
363
+ onProviderDisplayNameChange={setProviderDisplayName}
364
+ onApiKeyChange={setApiKey}
365
+ onApiBaseChange={setApiBase}
366
+ onAuthMethodChange={setAuthMethodId}
367
+ onStartProviderAuth={startAuth}
368
+ onImportProviderAuthFromCli={importAuthFromCli}
369
+ onModelDraftChange={setModelDraft}
370
+ onShowModelInputChange={setShowModelInput}
371
+ onAddModel={handleAddModel}
372
+ onRemoveModel={(modelName) => {
373
+ const next = removeProviderLocalModel(models, modelConfig, modelName);
374
+ setModels(next.models);
375
+ setModelConfig(next.modelConfig);
376
+ }}
377
+ onToggleModelThinkingLevel={toggleModelThinkingLevel}
378
+ onSetModelThinkingDefault={setModelThinkingDefault}
379
+ onSetModelVision={setModelVision}
380
+ onShowAdvancedChange={setShowAdvanced}
381
+ onWireApiChange={setWireApi}
382
+ onExtraHeadersChange={setExtraHeaders}
383
+ onDeleteProvider={handleDeleteProvider}
384
+ onSubmit={handleSubmit}
385
+ onTestConnection={handleTestConnection}
386
+ />
387
+ );
388
+ }
389
+
390
+ function ProviderFormDetailPane(props: ProviderFormDetailPaneProps) {
391
+ const {
392
+ context,
393
+ language,
394
+ providerTitle,
395
+ providerDisplayName,
396
+ apiKey,
397
+ apiBase,
398
+ extraHeaders,
399
+ wireApi,
400
+ models,
401
+ modelConfig,
402
+ modelDraft,
403
+ showAdvanced,
404
+ showModelInput,
405
+ resolvedAuthMethodId,
406
+ authSessionId,
407
+ authStatusMessage,
408
+ hasChanges,
409
+ isDeletePending,
410
+ isUpdatePending,
411
+ isTestPending,
412
+ startPending,
413
+ importPending,
414
+ onProviderDisplayNameChange,
415
+ onApiKeyChange,
416
+ onApiBaseChange,
417
+ onAuthMethodChange,
418
+ onStartProviderAuth,
419
+ onImportProviderAuthFromCli,
420
+ onModelDraftChange,
421
+ onShowModelInputChange,
422
+ onAddModel,
423
+ onRemoveModel,
424
+ onToggleModelThinkingLevel,
425
+ onSetModelThinkingDefault,
426
+ onSetModelVision,
427
+ onShowAdvancedChange,
428
+ onWireApiChange,
429
+ onExtraHeadersChange,
430
+ onDeleteProvider,
431
+ onSubmit,
432
+ onTestConnection
433
+ } = props;
434
+ const selectedAuthMethod = context.providerAuthMethods.find((method) => method.id === resolvedAuthMethodId);
435
+
532
436
  return (
533
437
  <ConfigSplitDetailPane>
534
438
  <ConfigSplitPaneHeader className='px-6 py-4'>
@@ -537,19 +441,19 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
537
441
  <div className='flex items-center gap-3'>
538
442
  <button
539
443
  type='button'
540
- onClick={handleDeleteProvider}
541
- disabled={deleteProvider.isPending}
444
+ onClick={onDeleteProvider}
445
+ disabled={isDeletePending}
542
446
  className='text-gray-400 transition-colors hover:text-red-500'
543
447
  title={t('providerDelete')}
544
448
  >
545
449
  <Trash2 className='h-4 w-4' />
546
450
  </button>
547
- <ProviderStatusBadge enabled={currentEnabled} apiKeySet={resolvedProviderConfig.apiKeySet} />
451
+ <ProviderStatusBadge enabled={context.currentEnabled} apiKeySet={context.resolvedProviderConfig.apiKeySet} />
548
452
  </div>
549
453
  </div>
550
454
  </ConfigSplitPaneHeader>
551
455
 
552
- <form onSubmit={handleSubmit} className='flex min-h-0 flex-1 flex-col'>
456
+ <form onSubmit={onSubmit} className='flex min-h-0 flex-1 flex-col'>
553
457
  <ConfigSplitPaneBody className='space-y-5 px-6 py-5'>
554
458
  <div className='space-y-2'>
555
459
  <Label htmlFor='providerDisplayName' className='text-sm font-medium text-gray-900'>
@@ -559,8 +463,8 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
559
463
  id='providerDisplayName'
560
464
  type='text'
561
465
  value={providerDisplayName}
562
- onChange={(e) => setProviderDisplayName(e.target.value)}
563
- placeholder={defaultDisplayName || t('providerDisplayNamePlaceholder')}
466
+ onChange={(e) => onProviderDisplayNameChange(e.target.value)}
467
+ placeholder={context.defaultDisplayName || t('providerDisplayNamePlaceholder')}
564
468
  className='rounded-xl'
565
469
  />
566
470
  <p className='text-xs text-gray-500'>{t('providerDisplayNameHelpShort')}</p>
@@ -568,49 +472,49 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
568
472
 
569
473
  <div className='space-y-2'>
570
474
  <Label htmlFor='apiKey' className='text-sm font-medium text-gray-900'>
571
- {apiKeyHint?.label ?? t('apiKey')}
475
+ {context.apiKeyHint?.label ?? t('apiKey')}
572
476
  </Label>
573
477
  <MaskedInput
574
478
  id='apiKey'
575
479
  value={apiKey}
576
- isSet={resolvedProviderConfig.apiKeySet}
577
- onChange={(e) => setApiKey(e.target.value)}
578
- placeholder={apiKeyHint?.placeholder ?? t('enterApiKey')}
480
+ isSet={context.resolvedProviderConfig.apiKeySet}
481
+ onChange={(e) => onApiKeyChange(e.target.value)}
482
+ placeholder={context.apiKeyHint?.placeholder ?? t('enterApiKey')}
579
483
  className='rounded-xl'
580
484
  />
581
485
  <p className='text-xs text-gray-500'>{t('leaveBlankToKeepUnchanged')}</p>
582
486
  </div>
583
487
 
584
488
  <ProviderAuthSection
585
- providerAuth={providerAuth}
586
- providerAuthNote={providerAuth?.note?.[language] || providerAuth?.note?.en || providerAuth?.displayName || ''}
587
- providerAuthMethodOptions={providerAuthMethodOptions}
588
- providerAuthMethodsCount={providerAuthMethods.length}
489
+ providerAuth={context.providerAuth}
490
+ providerAuthNote={context.providerAuth?.note?.[language] || context.providerAuth?.note?.en || context.providerAuth?.displayName || ''}
491
+ providerAuthMethodOptions={context.providerAuthMethodOptions}
492
+ providerAuthMethodsCount={context.providerAuthMethods.length}
589
493
  selectedAuthMethodHint={selectedAuthMethod?.hint?.[language] || selectedAuthMethod?.hint?.en || ''}
590
- shouldUseAuthMethodPills={shouldUseAuthMethodPills}
494
+ shouldUseAuthMethodPills={context.shouldUseAuthMethodPills}
591
495
  resolvedAuthMethodId={resolvedAuthMethodId}
592
- onAuthMethodChange={setAuthMethodId}
593
- onStartProviderAuth={handleStartProviderAuth}
594
- onImportProviderAuthFromCli={handleImportProviderAuthFromCli}
595
- startPending={startProviderAuth.isPending}
596
- importPending={importProviderAuthFromCli.isPending}
496
+ onAuthMethodChange={onAuthMethodChange}
497
+ onStartProviderAuth={onStartProviderAuth}
498
+ onImportProviderAuthFromCli={onImportProviderAuthFromCli}
499
+ startPending={startPending}
500
+ importPending={importPending}
597
501
  authSessionId={authSessionId}
598
502
  authStatusMessage={authStatusMessage}
599
503
  />
600
504
 
601
505
  <div className='space-y-2'>
602
506
  <Label htmlFor='apiBase' className='text-sm font-medium text-gray-900'>
603
- {apiBaseHint?.label ?? t('apiBase')}
507
+ {context.apiBaseHint?.label ?? t('apiBase')}
604
508
  </Label>
605
509
  <Input
606
510
  id='apiBase'
607
511
  type='text'
608
512
  value={apiBase}
609
- onChange={(e) => setApiBase(e.target.value)}
610
- placeholder={defaultApiBase || apiBaseHint?.placeholder || 'https://api.example.com'}
513
+ onChange={(e) => onApiBaseChange(e.target.value)}
514
+ placeholder={context.defaultApiBase || context.apiBaseHint?.placeholder || 'https://api.example.com'}
611
515
  className='rounded-xl'
612
516
  />
613
- <p className='text-xs text-gray-500'>{apiBaseHelpText}</p>
517
+ <p className='text-xs text-gray-500'>{context.apiBaseHelpText}</p>
614
518
  </div>
615
519
 
616
520
  <ProviderModelsSection
@@ -618,46 +522,39 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
618
522
  modelConfig={modelConfig}
619
523
  modelDraft={modelDraft}
620
524
  showModelInput={showModelInput}
621
- onModelDraftChange={setModelDraft}
622
- onShowModelInputChange={setShowModelInput}
623
- onAddModel={handleAddModel}
624
- onRemoveModel={(modelName) => {
625
- setModels((prev) => prev.filter((name) => name !== modelName));
626
- setModelConfig((prev) => {
627
- const next = { ...prev };
628
- delete next[modelName];
629
- return next;
630
- });
631
- }}
632
- onToggleModelThinkingLevel={toggleModelThinkingLevel}
633
- onSetModelThinkingDefault={setModelThinkingDefault}
634
- onSetModelVision={setModelVision}
525
+ onModelDraftChange={onModelDraftChange}
526
+ onShowModelInputChange={onShowModelInputChange}
527
+ onAddModel={onAddModel}
528
+ onRemoveModel={onRemoveModel}
529
+ onToggleModelThinkingLevel={onToggleModelThinkingLevel}
530
+ onSetModelThinkingDefault={onSetModelThinkingDefault}
531
+ onSetModelVision={onSetModelVision}
635
532
  thinkingLevels={THINKING_LEVELS}
636
533
  formatThinkingLevelLabel={formatThinkingLevelLabel}
637
534
  />
638
535
 
639
536
  <ProviderAdvancedSettingsSection
640
537
  showAdvanced={showAdvanced}
641
- onShowAdvancedChange={setShowAdvanced}
642
- supportsWireApi={supportsWireApi}
643
- wireApiLabel={wireApiHint?.label ?? t('wireApi')}
538
+ onShowAdvancedChange={onShowAdvancedChange}
539
+ supportsWireApi={context.supportsWireApi}
540
+ wireApiLabel={context.wireApiHint?.label ?? t('wireApi')}
644
541
  wireApi={wireApi}
645
- onWireApiChange={setWireApi}
646
- shouldUseWireApiPills={shouldUseWireApiPills}
647
- wireApiSelectOptions={wireApiSelectOptions}
648
- extraHeadersLabel={extraHeadersHint?.label ?? t('extraHeaders')}
542
+ onWireApiChange={onWireApiChange}
543
+ shouldUseWireApiPills={context.shouldUseWireApiPills}
544
+ wireApiSelectOptions={context.wireApiSelectOptions}
545
+ extraHeadersLabel={context.extraHeadersHint?.label ?? t('extraHeaders')}
649
546
  extraHeaders={extraHeaders}
650
- onExtraHeadersChange={setExtraHeaders}
547
+ onExtraHeadersChange={onExtraHeadersChange}
651
548
  />
652
549
  </ConfigSplitPaneBody>
653
550
 
654
551
  <ConfigSplitPaneFooter className='flex items-center justify-between px-6 py-4'>
655
- <Button type='button' variant='outline' size='sm' onClick={handleTestConnection} disabled={testProviderConnection.isPending}>
552
+ <Button type='button' variant='outline' size='sm' onClick={onTestConnection} disabled={isTestPending}>
656
553
  <CircleDotDashed className='mr-1.5 h-4 w-4' />
657
- {testProviderConnection.isPending ? t('providerTestingConnection') : t('providerTestConnection')}
554
+ {isTestPending ? t('providerTestingConnection') : t('providerTestConnection')}
658
555
  </Button>
659
- <Button type='submit' disabled={updateProvider.isPending || !hasChanges}>
660
- {updateProvider.isPending ? t('saving') : hasChanges ? t('save') : t('unchanged')}
556
+ <Button type='submit' disabled={isUpdatePending || !hasChanges}>
557
+ {isUpdatePending ? t('saving') : hasChanges ? t('save') : t('unchanged')}
661
558
  </Button>
662
559
  </ConfigSplitPaneFooter>
663
560
  </form>