@nextclaw/ui 0.9.6 → 0.9.8

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 (69) hide show
  1. package/CHANGELOG.md +16 -2
  2. package/dist/assets/ChannelsList-CIMYaIji.js +1 -0
  3. package/dist/assets/{ChatPage-DM1ewbWf.js → ChatPage-B5UpeEIp.js} +2 -2
  4. package/dist/assets/{DocBrowser-BLv77lJ0.js → DocBrowser-BJ610SPa.js} +1 -1
  5. package/dist/assets/{LogoBadge-D7j1al-w.js → LogoBadge-BKq1GKWP.js} +1 -1
  6. package/dist/assets/MarketplacePage-Bs3sLsgx.js +49 -0
  7. package/dist/assets/{McpMarketplacePage-DpMjaD3m.js → McpMarketplacePage-BWTguHCs.js} +2 -2
  8. package/dist/assets/ModelConfig-B-oTP-Bc.js +1 -0
  9. package/dist/assets/ProvidersList-r7bD0-R0.js +1 -0
  10. package/dist/assets/RemoteAccessPage-D7On6waK.js +1 -0
  11. package/dist/assets/{RuntimeConfig-BbX4yFKy.js → RuntimeConfig-C11xVxH9.js} +1 -1
  12. package/dist/assets/{SearchConfig-BmmmeyJd.js → SearchConfig-BVZdCxiM.js} +1 -1
  13. package/dist/assets/{SecretsConfig-CWG8J01H.js → SecretsConfig-DuEDdC3X.js} +2 -2
  14. package/dist/assets/SessionsConfig-Y-Blf_-K.js +2 -0
  15. package/dist/assets/{chat-message-CGXiVhyN.js → chat-message-B6VCCEXF.js} +1 -1
  16. package/dist/assets/index-DfEAJJsA.css +1 -0
  17. package/dist/assets/index-DvA7S11O.js +8 -0
  18. package/dist/assets/{label-CCSffS1D.js → label-DzwitL78.js} +1 -1
  19. package/dist/assets/{page-layout-ud8wZ8gX.js → page-layout-DEq5N_8L.js} +1 -1
  20. package/dist/assets/popover-CY54V8F6.js +1 -0
  21. package/dist/assets/provider-models-BOeNnjk9.js +1 -0
  22. package/dist/assets/{security-config-DJJUCMov.js → security-config-CgbYP57d.js} +1 -1
  23. package/dist/assets/skeleton-zjQZMWu9.js +1 -0
  24. package/dist/assets/{status-dot-Fz9-eKsl.js → status-dot-CU_P0tvO.js} +1 -1
  25. package/dist/assets/{switch-B-_SrMSL.js → switch-PvjTvlcs.js} +1 -1
  26. package/dist/assets/{tabs-custom-6Tm1ZHfS.js → tabs-custom-Bke5J9ny.js} +1 -1
  27. package/dist/assets/useConfirmDialog-8tzzp_oW.js +1 -0
  28. package/dist/assets/vendor-CmQZsDAE.js +436 -0
  29. package/dist/index.html +3 -3
  30. package/package.json +4 -4
  31. package/src/App.tsx +36 -39
  32. package/src/account/components/account-panel.tsx +93 -0
  33. package/src/account/managers/account.manager.ts +179 -0
  34. package/src/account/stores/account.store.ts +68 -0
  35. package/src/api/types.ts +2 -0
  36. package/src/app-query-client.ts +10 -0
  37. package/src/components/config/ProviderForm.tsx +91 -641
  38. package/src/components/config/ProvidersList.tsx +10 -5
  39. package/src/components/config/provider-advanced-settings-section.tsx +92 -0
  40. package/src/components/config/provider-auth-section.tsx +113 -0
  41. package/src/components/config/provider-enabled-field.tsx +20 -0
  42. package/src/components/config/provider-form-support.ts +344 -0
  43. package/src/components/config/provider-models-section.tsx +198 -0
  44. package/src/components/config/provider-pill-selector.tsx +39 -0
  45. package/src/components/config/provider-status-badge.tsx +21 -0
  46. package/src/components/layout/Sidebar.tsx +26 -0
  47. package/src/components/remote/RemoteAccessPage.tsx +162 -442
  48. package/src/hooks/useRemoteAccess.ts +7 -6
  49. package/src/lib/i18n.remote.ts +108 -4
  50. package/src/lib/provider-models.ts +2 -2
  51. package/src/presenter/app-presenter-context.tsx +20 -0
  52. package/src/presenter/app.presenter.ts +12 -0
  53. package/src/remote/managers/remote-access.manager.ts +196 -0
  54. package/src/remote/remote-access.query.ts +78 -0
  55. package/src/remote/stores/remote-access.store.ts +44 -0
  56. package/dist/assets/ChannelsList-Byfj2R01.js +0 -1
  57. package/dist/assets/MarketplacePage-DuskLKYh.js +0 -49
  58. package/dist/assets/ModelConfig-ubaecweS.js +0 -1
  59. package/dist/assets/ProvidersList-w8MJH2LI.js +0 -1
  60. package/dist/assets/RemoteAccessPage-D79_5Kbn.js +0 -1
  61. package/dist/assets/SessionsConfig-D-vg_Lgv.js +0 -2
  62. package/dist/assets/index-COrhpAdh.css +0 -1
  63. package/dist/assets/index-CeRbsQ90.js +0 -8
  64. package/dist/assets/index-Ct7FQpxN.js +0 -1
  65. package/dist/assets/popover-Bfoe6YBX.js +0 -1
  66. package/dist/assets/provider-models-D3B_xWXx.js +0 -1
  67. package/dist/assets/skeleton-IOOTmHzP.js +0 -1
  68. package/dist/assets/useConfirmDialog-BeOW2bOI.js +0 -5
  69. package/dist/assets/vendor-CwsIoNvJ.js +0 -442
@@ -14,374 +14,45 @@ import {
14
14
  import { Button } from '@/components/ui/button';
15
15
  import { Input } from '@/components/ui/input';
16
16
  import { Label } from '@/components/ui/label';
17
- import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
18
- import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
19
17
  import { MaskedInput } from '@/components/common/MaskedInput';
20
- import { KeyValueEditor } from '@/components/common/KeyValueEditor';
21
- import { StatusDot } from '@/components/ui/status-dot';
22
18
  import { getLanguage, t } from '@/lib/i18n';
23
19
  import { hintForPath } from '@/lib/config-hints';
24
- import type { ProviderConfigView, ProviderConfigUpdate, ProviderConnectionTestRequest, ThinkingLevel } from '@/api/types';
25
- import { CircleDotDashed, Plus, X, Trash2, ChevronDown, Settings2 } from 'lucide-react';
20
+ import type { ProviderConfigUpdate, ProviderConnectionTestRequest, ThinkingLevel } from '@/api/types';
21
+ import { CircleDotDashed, Trash2 } from 'lucide-react';
26
22
  import { toast } from 'sonner';
27
23
  import { CONFIG_DETAIL_CARD_CLASS, CONFIG_EMPTY_DETAIL_CARD_CLASS } from './config-layout';
28
-
29
- type WireApiType = 'auto' | 'chat' | 'responses';
30
- type ModelThinkingConfig = Record<string, { supported: ThinkingLevel[]; default?: ThinkingLevel | null }>;
24
+ import { ProviderAdvancedSettingsSection } from './provider-advanced-settings-section';
25
+ import { ProviderAuthSection } from './provider-auth-section';
26
+ import { ProviderEnabledField } from './provider-enabled-field';
27
+ import {
28
+ applyEnabledPatch,
29
+ EMPTY_PROVIDER_CONFIG,
30
+ formatThinkingLevelLabel,
31
+ headersEqual,
32
+ modelListsEqual,
33
+ modelThinkingEqual,
34
+ normalizeHeaders,
35
+ normalizeModelList,
36
+ normalizeModelThinkingConfig,
37
+ normalizeModelThinkingForModels,
38
+ resolveEditableModels,
39
+ resolvePreferredAuthMethodId,
40
+ serializeModelsForSave,
41
+ shouldUsePillSelector,
42
+ THINKING_LEVELS,
43
+ toProviderLocalModelId,
44
+ type ModelThinkingConfig,
45
+ type WireApiType
46
+ } from './provider-form-support';
47
+ import { ProviderModelsSection } from './provider-models-section';
48
+ import type { PillSelectOption } from './provider-pill-selector';
49
+ import { ProviderStatusBadge } from './provider-status-badge';
31
50
 
32
51
  type ProviderFormProps = {
33
52
  providerName?: string;
34
53
  onProviderDeleted?: (providerName: string) => void;
35
54
  };
36
55
 
37
- type ProviderAuthMethodOption = {
38
- id: string;
39
- };
40
-
41
- type PillSelectOption = {
42
- value: string;
43
- label: string;
44
- };
45
-
46
- const EMPTY_PROVIDER_CONFIG: ProviderConfigView = {
47
- displayName: '',
48
- apiKeySet: false,
49
- apiKeyMasked: undefined,
50
- apiBase: null,
51
- extraHeaders: null,
52
- wireApi: null,
53
- models: [],
54
- modelThinking: {}
55
- };
56
- const THINKING_LEVELS: ThinkingLevel[] = ['off', 'minimal', 'low', 'medium', 'high', 'adaptive', 'xhigh'];
57
- const THINKING_LEVEL_SET = new Set<string>(THINKING_LEVELS);
58
-
59
- function normalizeHeaders(input: Record<string, string> | null | undefined): Record<string, string> | null {
60
- if (!input) {
61
- return null;
62
- }
63
- const entries = Object.entries(input)
64
- .map(([key, value]) => [key.trim(), value] as const)
65
- .filter(([key]) => key.length > 0);
66
- if (entries.length === 0) {
67
- return null;
68
- }
69
- return Object.fromEntries(entries);
70
- }
71
-
72
- function headersEqual(
73
- left: Record<string, string> | null | undefined,
74
- right: Record<string, string> | null | undefined
75
- ): boolean {
76
- const a = normalizeHeaders(left);
77
- const b = normalizeHeaders(right);
78
- if (a === null && b === null) {
79
- return true;
80
- }
81
- if (!a || !b) {
82
- return false;
83
- }
84
- const aEntries = Object.entries(a).sort(([ak], [bk]) => ak.localeCompare(bk));
85
- const bEntries = Object.entries(b).sort(([ak], [bk]) => ak.localeCompare(bk));
86
- if (aEntries.length !== bEntries.length) {
87
- return false;
88
- }
89
- return aEntries.every(([key, value], index) => key === bEntries[index][0] && value === bEntries[index][1]);
90
- }
91
-
92
- function normalizeModelList(input: string[] | null | undefined): string[] {
93
- if (!input || input.length === 0) {
94
- return [];
95
- }
96
- const deduped = new Set<string>();
97
- for (const item of input) {
98
- const trimmed = item.trim();
99
- if (trimmed) {
100
- deduped.add(trimmed);
101
- }
102
- }
103
- return [...deduped];
104
- }
105
-
106
- function stripProviderPrefix(model: string, prefix: string): string {
107
- const trimmed = model.trim();
108
- if (!trimmed || !prefix.trim()) {
109
- return trimmed;
110
- }
111
- const fullPrefix = `${prefix.trim()}/`;
112
- if (trimmed.startsWith(fullPrefix)) {
113
- return trimmed.slice(fullPrefix.length);
114
- }
115
- return trimmed;
116
- }
117
-
118
- function toProviderLocalModelId(model: string, aliases: string[]): string {
119
- let normalized = model.trim();
120
- if (!normalized) {
121
- return '';
122
- }
123
- for (const alias of aliases) {
124
- const cleanAlias = alias.trim();
125
- if (!cleanAlias) {
126
- continue;
127
- }
128
- normalized = stripProviderPrefix(normalized, cleanAlias);
129
- }
130
- return normalized.trim();
131
- }
132
-
133
- function modelListsEqual(left: string[], right: string[]): boolean {
134
- if (left.length !== right.length) {
135
- return false;
136
- }
137
- return left.every((item, index) => item === right[index]);
138
- }
139
-
140
- function mergeModelLists(base: string[], extra: string[]): string[] {
141
- const merged = [...base];
142
- for (const item of extra) {
143
- if (!merged.includes(item)) {
144
- merged.push(item);
145
- }
146
- }
147
- return merged;
148
- }
149
-
150
- function resolveEditableModels(defaultModels: string[], savedModels: string[]): string[] {
151
- if (savedModels.length === 0) {
152
- return defaultModels;
153
- }
154
- const looksLikeLegacyCustomList = savedModels.every((model) => !defaultModels.includes(model));
155
- if (looksLikeLegacyCustomList) {
156
- return mergeModelLists(defaultModels, savedModels);
157
- }
158
- return savedModels;
159
- }
160
-
161
- function serializeModelsForSave(models: string[], defaultModels: string[]): string[] {
162
- if (modelListsEqual(models, defaultModels)) {
163
- return [];
164
- }
165
- return models;
166
- }
167
-
168
- function parseThinkingLevel(value: unknown): ThinkingLevel | null {
169
- if (typeof value !== 'string') {
170
- return null;
171
- }
172
- const normalized = value.trim().toLowerCase();
173
- if (!normalized) {
174
- return null;
175
- }
176
- return THINKING_LEVEL_SET.has(normalized) ? (normalized as ThinkingLevel) : null;
177
- }
178
-
179
- function normalizeThinkingLevels(values: unknown): ThinkingLevel[] {
180
- if (!Array.isArray(values)) {
181
- return [];
182
- }
183
- const deduped: ThinkingLevel[] = [];
184
- for (const value of values) {
185
- const level = parseThinkingLevel(value);
186
- if (!level || deduped.includes(level)) {
187
- continue;
188
- }
189
- deduped.push(level);
190
- }
191
- return deduped;
192
- }
193
-
194
- function normalizeModelThinkingConfig(
195
- input: ProviderConfigView['modelThinking'],
196
- aliases: string[]
197
- ): ModelThinkingConfig {
198
- if (!input) {
199
- return {};
200
- }
201
- const normalized: ModelThinkingConfig = {};
202
- for (const [rawModel, rawValue] of Object.entries(input)) {
203
- const model = toProviderLocalModelId(rawModel, aliases);
204
- if (!model) {
205
- continue;
206
- }
207
- const supported = normalizeThinkingLevels(rawValue?.supported);
208
- if (supported.length === 0) {
209
- continue;
210
- }
211
- const defaultLevel = parseThinkingLevel(rawValue?.default);
212
- normalized[model] =
213
- defaultLevel && supported.includes(defaultLevel)
214
- ? { supported, default: defaultLevel }
215
- : { supported };
216
- }
217
- return normalized;
218
- }
219
-
220
- function normalizeModelThinkingForModels(modelThinking: ModelThinkingConfig, models: string[]): ModelThinkingConfig {
221
- const modelSet = new Set(models.map((item) => item.trim()).filter(Boolean));
222
- const normalized: ModelThinkingConfig = {};
223
- for (const [model, entry] of Object.entries(modelThinking)) {
224
- if (!modelSet.has(model)) {
225
- continue;
226
- }
227
- const supported = normalizeThinkingLevels(entry.supported);
228
- if (supported.length === 0) {
229
- continue;
230
- }
231
- const defaultLevel = parseThinkingLevel(entry.default);
232
- normalized[model] =
233
- defaultLevel && supported.includes(defaultLevel)
234
- ? { supported, default: defaultLevel }
235
- : { supported };
236
- }
237
- return normalized;
238
- }
239
-
240
- function modelThinkingEqual(left: ModelThinkingConfig, right: ModelThinkingConfig): boolean {
241
- const leftKeys = Object.keys(left).sort();
242
- const rightKeys = Object.keys(right).sort();
243
- if (leftKeys.length !== rightKeys.length) {
244
- return false;
245
- }
246
- for (let index = 0; index < leftKeys.length; index += 1) {
247
- const key = leftKeys[index];
248
- if (key !== rightKeys[index]) {
249
- return false;
250
- }
251
- const leftEntry = left[key];
252
- const rightEntry = right[key];
253
- if (!leftEntry || !rightEntry) {
254
- return false;
255
- }
256
- const leftSupported = [...leftEntry.supported].sort();
257
- const rightSupported = [...rightEntry.supported].sort();
258
- if (!modelListsEqual(leftSupported, rightSupported)) {
259
- return false;
260
- }
261
- if ((leftEntry.default ?? null) !== (rightEntry.default ?? null)) {
262
- return false;
263
- }
264
- }
265
- return true;
266
- }
267
-
268
- function formatThinkingLevelLabel(level: ThinkingLevel): string {
269
- if (level === 'off') {
270
- return t('chatThinkingLevelOff');
271
- }
272
- if (level === 'minimal') {
273
- return t('chatThinkingLevelMinimal');
274
- }
275
- if (level === 'low') {
276
- return t('chatThinkingLevelLow');
277
- }
278
- if (level === 'medium') {
279
- return t('chatThinkingLevelMedium');
280
- }
281
- if (level === 'high') {
282
- return t('chatThinkingLevelHigh');
283
- }
284
- if (level === 'adaptive') {
285
- return t('chatThinkingLevelAdaptive');
286
- }
287
- return t('chatThinkingLevelXhigh');
288
- }
289
-
290
- function resolvePreferredAuthMethodId(params: {
291
- providerName?: string;
292
- methods: ProviderAuthMethodOption[];
293
- defaultMethodId?: string;
294
- language: 'zh' | 'en';
295
- }): string {
296
- const { providerName, methods, defaultMethodId, language } = params;
297
- if (methods.length === 0) {
298
- return '';
299
- }
300
-
301
- const methodIdMap = new Map<string, string>();
302
- for (const method of methods) {
303
- const methodId = method.id.trim();
304
- if (methodId) {
305
- methodIdMap.set(methodId.toLowerCase(), methodId);
306
- }
307
- }
308
-
309
- const pick = (...candidates: string[]): string | undefined => {
310
- for (const candidate of candidates) {
311
- const resolved = methodIdMap.get(candidate.toLowerCase());
312
- if (resolved) {
313
- return resolved;
314
- }
315
- }
316
- return undefined;
317
- };
318
-
319
- const normalizedDefault = defaultMethodId?.trim();
320
- if (providerName === 'minimax-portal') {
321
- if (language === 'zh') {
322
- return pick('cn', 'china-mainland') ?? pick(normalizedDefault ?? '') ?? methods[0]?.id ?? '';
323
- }
324
- if (language === 'en') {
325
- return pick('global', 'intl', 'international') ?? pick(normalizedDefault ?? '') ?? methods[0]?.id ?? '';
326
- }
327
- }
328
-
329
- if (normalizedDefault) {
330
- const matchedDefault = pick(normalizedDefault);
331
- if (matchedDefault) {
332
- return matchedDefault;
333
- }
334
- }
335
-
336
- if (language === 'zh') {
337
- return pick('cn') ?? methods[0]?.id ?? '';
338
- }
339
- if (language === 'en') {
340
- return pick('global') ?? methods[0]?.id ?? '';
341
- }
342
-
343
- return methods[0]?.id ?? '';
344
- }
345
-
346
- function shouldUsePillSelector(params: {
347
- required: boolean;
348
- hasDefault: boolean;
349
- optionCount: number;
350
- }): boolean {
351
- return params.required && params.hasDefault && params.optionCount > 1 && params.optionCount <= 3;
352
- }
353
-
354
- function PillSelector(props: {
355
- value: string;
356
- onChange: (value: string) => void;
357
- options: PillSelectOption[];
358
- }) {
359
- const { value, onChange, options } = props;
360
-
361
- return (
362
- <div className="flex flex-wrap gap-2">
363
- {options.map((option) => {
364
- const selected = option.value === value;
365
- return (
366
- <button
367
- key={option.value}
368
- type="button"
369
- onClick={() => onChange(option.value)}
370
- aria-pressed={selected}
371
- className={`rounded-full border px-3 py-1.5 text-xs font-medium transition-colors ${
372
- selected
373
- ? 'border-primary bg-primary text-white shadow-sm'
374
- : 'border-gray-200 bg-white text-gray-700 hover:border-primary/40 hover:text-primary'
375
- }`}
376
- >
377
- {option.label}
378
- </button>
379
- );
380
- })}
381
- </div>
382
- );
383
- }
384
-
385
56
  export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormProps) {
386
57
  const queryClient = useQueryClient();
387
58
  const { data: config } = useConfig();
@@ -395,6 +66,7 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
395
66
  const importProviderAuthFromCli = useImportProviderAuthFromCli();
396
67
 
397
68
  const [apiKey, setApiKey] = useState('');
69
+ const [enabled, setEnabled] = useState(true);
398
70
  const [apiBase, setApiBase] = useState('');
399
71
  const [extraHeaders, setExtraHeaders] = useState<Record<string, string> | null>(null);
400
72
  const [wireApi, setWireApi] = useState<WireApiType>('auto');
@@ -422,6 +94,7 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
422
94
  const defaultDisplayName = providerSpec?.displayName || providerName || '';
423
95
  const currentDisplayName = (resolvedProviderConfig.displayName || '').trim();
424
96
  const effectiveDisplayName = currentDisplayName || defaultDisplayName;
97
+ const currentEnabled = resolvedProviderConfig.enabled !== false;
425
98
 
426
99
  const providerTitle = providerDisplayName.trim() || effectiveDisplayName || providerName || t('providersSelectPlaceholder');
427
100
  const providerModelPrefix = providerSpec?.modelPrefix || providerName || '';
@@ -575,6 +248,7 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
575
248
  useEffect(() => {
576
249
  if (!providerName) {
577
250
  setApiKey('');
251
+ setEnabled(true);
578
252
  setApiBase('');
579
253
  setExtraHeaders(null);
580
254
  setWireApi('auto');
@@ -590,6 +264,7 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
590
264
  }
591
265
 
592
266
  setApiKey('');
267
+ setEnabled(currentEnabled);
593
268
  setApiBase(currentApiBase);
594
269
  setExtraHeaders(resolvedProviderConfig.extraHeaders || null);
595
270
  setWireApi(currentWireApi);
@@ -604,6 +279,7 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
604
279
  }, [
605
280
  providerName,
606
281
  currentApiBase,
282
+ currentEnabled,
607
283
  resolvedProviderConfig.extraHeaders,
608
284
  currentWireApi,
609
285
  currentEditableModels,
@@ -635,6 +311,7 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
635
311
 
636
312
  return (
637
313
  apiKeyChanged ||
314
+ enabled !== currentEnabled ||
638
315
  apiBaseChanged ||
639
316
  headersChanged ||
640
317
  wireApiChanged ||
@@ -648,6 +325,8 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
648
325
  providerDisplayName,
649
326
  effectiveDisplayName,
650
327
  apiKey,
328
+ enabled,
329
+ currentEnabled,
651
330
  apiBase,
652
331
  currentApiBase,
653
332
  extraHeaders,
@@ -733,6 +412,7 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
733
412
  if (trimmedApiKey.length > 0) {
734
413
  payload.apiKey = trimmedApiKey;
735
414
  }
415
+ applyEnabledPatch(payload, enabled, currentEnabled);
736
416
 
737
417
  if (trimmedApiBase !== currentApiBase.trim()) {
738
418
  payload.apiBase = trimmedApiBase.length > 0 && trimmedApiBase !== defaultApiBase ? trimmedApiBase : null;
@@ -868,8 +548,6 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
868
548
  );
869
549
  }
870
550
 
871
- const statusLabel = resolvedProviderConfig.apiKeySet ? t('statusReady') : t('statusSetup');
872
-
873
551
  return (
874
552
  <div className={CONFIG_DETAIL_CARD_CLASS}>
875
553
  <div className="border-b border-gray-100 px-6 py-4">
@@ -887,13 +565,15 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
887
565
  <Trash2 className="h-4 w-4" />
888
566
  </button>
889
567
  )}
890
- <StatusDot status={resolvedProviderConfig.apiKeySet ? 'ready' : 'setup'} label={statusLabel} />
568
+ <ProviderStatusBadge enabled={currentEnabled} apiKeySet={resolvedProviderConfig.apiKeySet} />
891
569
  </div>
892
570
  </div>
893
571
  </div>
894
572
 
895
573
  <form onSubmit={handleSubmit} className="flex min-h-0 flex-1 flex-col">
896
574
  <div className="min-h-0 flex-1 space-y-5 overflow-y-auto px-6 py-5">
575
+ <ProviderEnabledField enabled={enabled} onChange={setEnabled} />
576
+
897
577
  {isCustomProvider && (
898
578
  <div className="space-y-2">
899
579
  <Label htmlFor="providerDisplayName" className="text-sm font-medium text-gray-900">
@@ -926,76 +606,22 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
926
606
  <p className="text-xs text-gray-500">{t('leaveBlankToKeepUnchanged')}</p>
927
607
  </div>
928
608
 
929
- {providerAuth?.kind === 'device_code' && (
930
- <div className="space-y-2 rounded-xl border border-primary/20 bg-primary-50/50 p-3">
931
- <Label className="text-sm font-medium text-gray-900">
932
- {providerAuth.displayName || t('providerAuthSectionTitle')}
933
- </Label>
934
- {providerAuthNote ? (
935
- <p className="text-xs text-gray-600">{providerAuthNote}</p>
936
- ) : null}
937
- {providerAuthMethods.length > 1 ? (
938
- <div className="space-y-2">
939
- <Label className="text-xs font-medium text-gray-700">{t('providerAuthMethodLabel')}</Label>
940
- {shouldUseAuthMethodPills ? (
941
- <PillSelector
942
- value={resolvedAuthMethodId}
943
- onChange={setAuthMethodId}
944
- options={providerAuthMethodOptions}
945
- />
946
- ) : (
947
- <Select value={resolvedAuthMethodId} onValueChange={setAuthMethodId}>
948
- <SelectTrigger className="h-8 rounded-lg bg-white">
949
- <SelectValue placeholder={t('providerAuthMethodPlaceholder')} />
950
- </SelectTrigger>
951
- <SelectContent>
952
- {providerAuthMethodOptions.map((method) => (
953
- <SelectItem key={method.value} value={method.value}>
954
- {method.label}
955
- </SelectItem>
956
- ))}
957
- </SelectContent>
958
- </Select>
959
- )}
960
- {selectedAuthMethodHint ? (
961
- <p className="text-xs text-gray-500">{selectedAuthMethodHint}</p>
962
- ) : null}
963
- </div>
964
- ) : null}
965
- <div className="flex flex-wrap items-center gap-2">
966
- <Button
967
- type="button"
968
- variant="outline"
969
- size="sm"
970
- onClick={handleStartProviderAuth}
971
- disabled={startProviderAuth.isPending || Boolean(authSessionId)}
972
- >
973
- {startProviderAuth.isPending
974
- ? t('providerAuthStarting')
975
- : authSessionId
976
- ? t('providerAuthAuthorizing')
977
- : t('providerAuthAuthorizeInBrowser')}
978
- </Button>
979
- {providerAuth.supportsCliImport ? (
980
- <Button
981
- type="button"
982
- variant="outline"
983
- size="sm"
984
- onClick={handleImportProviderAuthFromCli}
985
- disabled={importProviderAuthFromCli.isPending}
986
- >
987
- {importProviderAuthFromCli.isPending ? t('providerAuthImporting') : t('providerAuthImportFromCli')}
988
- </Button>
989
- ) : null}
990
- {authSessionId ? (
991
- <span className="text-xs text-gray-500">{t('providerAuthSessionLabel')}: {authSessionId.slice(0, 8)}…</span>
992
- ) : null}
993
- </div>
994
- {authStatusMessage ? (
995
- <p className="text-xs text-gray-600">{authStatusMessage}</p>
996
- ) : null}
997
- </div>
998
- )}
609
+ <ProviderAuthSection
610
+ providerAuth={providerAuth}
611
+ providerAuthNote={providerAuthNote}
612
+ providerAuthMethodOptions={providerAuthMethodOptions}
613
+ providerAuthMethodsCount={providerAuthMethods.length}
614
+ selectedAuthMethodHint={selectedAuthMethodHint}
615
+ shouldUseAuthMethodPills={shouldUseAuthMethodPills}
616
+ resolvedAuthMethodId={resolvedAuthMethodId}
617
+ onAuthMethodChange={setAuthMethodId}
618
+ onStartProviderAuth={handleStartProviderAuth}
619
+ onImportProviderAuthFromCli={handleImportProviderAuthFromCli}
620
+ startPending={startProviderAuth.isPending}
621
+ importPending={importProviderAuthFromCli.isPending}
622
+ authSessionId={authSessionId}
623
+ authStatusMessage={authStatusMessage}
624
+ />
999
625
 
1000
626
  <div className="space-y-2">
1001
627
  <Label htmlFor="apiBase" className="text-sm font-medium text-gray-900">
@@ -1012,217 +638,41 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
1012
638
  <p className="text-xs text-gray-500">{apiBaseHelpText}</p>
1013
639
  </div>
1014
640
 
1015
- <div className="space-y-2">
1016
- <div className="flex items-center justify-between">
1017
- <Label className="text-sm font-medium text-gray-900">
1018
- {t('providerModelsTitle')}
1019
- </Label>
1020
- {!showModelInput && (
1021
- <button
1022
- type="button"
1023
- onClick={() => setShowModelInput(true)}
1024
- className="text-xs text-primary hover:text-primary/80 font-medium flex items-center gap-1"
1025
- >
1026
- <Plus className="h-3 w-3" />
1027
- {t('providerAddModel')}
1028
- </button>
1029
- )}
1030
- </div>
1031
-
1032
- {showModelInput && (
1033
- <div className="flex items-center gap-2">
1034
- <Input
1035
- value={modelDraft}
1036
- onChange={(event) => setModelDraft(event.target.value)}
1037
- onKeyDown={(event) => {
1038
- if (event.key === 'Enter') {
1039
- event.preventDefault();
1040
- handleAddModel();
1041
- }
1042
- if (event.key === 'Escape') {
1043
- setShowModelInput(false);
1044
- setModelDraft('');
1045
- }
1046
- }}
1047
- placeholder={t('providerModelInputPlaceholder')}
1048
- className="flex-1 rounded-xl"
1049
- autoFocus
1050
- />
1051
- <Button type="button" size="sm" onClick={handleAddModel} disabled={modelDraft.trim().length === 0}>
1052
- {t('add')}
1053
- </Button>
1054
- <Button type="button" size="sm" variant="ghost" onClick={() => { setShowModelInput(false); setModelDraft(''); }}>
1055
- <X className="h-4 w-4" />
1056
- </Button>
1057
- </div>
1058
- )}
1059
-
1060
- {models.length === 0 ? (
1061
- <div className="rounded-xl border border-dashed border-gray-200 bg-gray-50 px-4 py-6 text-center">
1062
- <p className="text-sm text-gray-500">{t('providerModelsEmptyShort')}</p>
1063
- {!showModelInput && (
1064
- <button
1065
- type="button"
1066
- onClick={() => setShowModelInput(true)}
1067
- className="mt-2 text-sm text-primary hover:text-primary/80 font-medium"
1068
- >
1069
- {t('providerAddFirstModel')}
1070
- </button>
1071
- )}
1072
- </div>
1073
- ) : (
1074
- <div className="flex flex-wrap gap-2">
1075
- {models.map((modelName) => {
1076
- const thinkingEntry = modelThinking[modelName];
1077
- const supportedLevels = thinkingEntry?.supported ?? [];
1078
- const defaultThinkingLevel = thinkingEntry?.default ?? null;
1079
- return (
1080
- <div
1081
- key={modelName}
1082
- className="group inline-flex max-w-full items-center gap-1 rounded-full border border-gray-200 bg-white px-3 py-1.5"
1083
- >
1084
- <span className="max-w-[140px] truncate text-sm text-gray-800 sm:max-w-[220px]">{modelName}</span>
1085
- <Popover>
1086
- <PopoverTrigger asChild>
1087
- <button
1088
- type="button"
1089
- className="inline-flex h-5 w-5 items-center justify-center rounded-full text-gray-400 transition-opacity hover:bg-gray-100 hover:text-gray-600 opacity-100 md:opacity-0 md:group-hover:opacity-100 md:group-focus-within:opacity-100"
1090
- aria-label={t('providerModelThinkingTitle')}
1091
- title={t('providerModelThinkingTitle')}
1092
- >
1093
- <Settings2 className="h-3 w-3" />
1094
- </button>
1095
- </PopoverTrigger>
1096
- <PopoverContent className="w-80 space-y-3">
1097
- <div className="space-y-1">
1098
- <p className="text-xs font-semibold text-gray-800">{t('providerModelThinkingTitle')}</p>
1099
- <p className="text-xs text-gray-500">{t('providerModelThinkingHint')}</p>
1100
- </div>
1101
- <div className="flex flex-wrap gap-1.5">
1102
- {THINKING_LEVELS.map((level) => {
1103
- const selected = supportedLevels.includes(level);
1104
- return (
1105
- <button
1106
- key={level}
1107
- type="button"
1108
- onClick={() => toggleModelThinkingLevel(modelName, level)}
1109
- className={`rounded-full border px-2.5 py-1 text-xs font-medium transition-colors ${
1110
- selected
1111
- ? 'border-primary bg-primary text-white'
1112
- : 'border-gray-200 bg-white text-gray-600 hover:border-primary/40 hover:text-primary'
1113
- }`}
1114
- >
1115
- {formatThinkingLevelLabel(level)}
1116
- </button>
1117
- );
1118
- })}
1119
- </div>
1120
- <div className="space-y-1.5">
1121
- <Label className="text-xs font-medium text-gray-700">{t('providerModelThinkingDefault')}</Label>
1122
- <Select
1123
- value={defaultThinkingLevel ?? '__none__'}
1124
- onValueChange={(value) =>
1125
- setModelThinkingDefault(
1126
- modelName,
1127
- value === '__none__' ? null : (value as ThinkingLevel)
1128
- )
1129
- }
1130
- disabled={supportedLevels.length === 0}
1131
- >
1132
- <SelectTrigger className="h-8 rounded-lg bg-white text-xs">
1133
- <SelectValue />
1134
- </SelectTrigger>
1135
- <SelectContent>
1136
- <SelectItem value="__none__">{t('providerModelThinkingDefaultNone')}</SelectItem>
1137
- {supportedLevels.map((level) => (
1138
- <SelectItem key={level} value={level}>
1139
- {formatThinkingLevelLabel(level)}
1140
- </SelectItem>
1141
- ))}
1142
- </SelectContent>
1143
- </Select>
1144
- {supportedLevels.length === 0 ? (
1145
- <p className="text-xs text-gray-500">{t('providerModelThinkingNoSupported')}</p>
1146
- ) : null}
1147
- </div>
1148
- </PopoverContent>
1149
- </Popover>
1150
- <button
1151
- type="button"
1152
- onClick={() => {
1153
- setModels((prev) => prev.filter((name) => name !== modelName));
1154
- setModelThinking((prev) => {
1155
- const next = { ...prev };
1156
- delete next[modelName];
1157
- return next;
1158
- });
1159
- }}
1160
- className="inline-flex h-5 w-5 items-center justify-center rounded-full text-gray-400 transition-opacity hover:bg-gray-100 hover:text-gray-600 opacity-100 md:opacity-0 md:group-hover:opacity-100 md:group-focus-within:opacity-100"
1161
- aria-label={t('remove')}
1162
- >
1163
- <X className="h-3 w-3" />
1164
- </button>
1165
- </div>
1166
- );
1167
- })}
1168
- </div>
1169
- )}
1170
- </div>
1171
-
1172
- {/* Advanced Settings - Collapsible */}
1173
- <div className="border-t border-gray-100 pt-4">
1174
- <button
1175
- type="button"
1176
- onClick={() => setShowAdvanced(!showAdvanced)}
1177
- className="flex w-full items-center justify-between text-sm text-gray-600 hover:text-gray-900 transition-colors"
1178
- >
1179
- <span className="flex items-center gap-1.5">
1180
- <Settings2 className="h-3.5 w-3.5" />
1181
- {t('providerAdvancedSettings')}
1182
- </span>
1183
- <ChevronDown className={`h-4 w-4 transition-transform ${showAdvanced ? 'rotate-180' : ''}`} />
1184
- </button>
1185
-
1186
- {showAdvanced && (
1187
- <div className="mt-4 space-y-5">
1188
- {providerSpec.supportsWireApi && (
1189
- <div className="space-y-2">
1190
- <Label htmlFor="wireApi" className="text-sm font-medium text-gray-900">
1191
- {wireApiHint?.label ?? t('wireApi')}
1192
- </Label>
1193
- {shouldUseWireApiPills ? (
1194
- <PillSelector
1195
- value={wireApi}
1196
- onChange={(v) => setWireApi(v as WireApiType)}
1197
- options={wireApiSelectOptions}
1198
- />
1199
- ) : (
1200
- <Select value={wireApi} onValueChange={(v) => setWireApi(v as WireApiType)}>
1201
- <SelectTrigger className="rounded-xl">
1202
- <SelectValue />
1203
- </SelectTrigger>
1204
- <SelectContent>
1205
- {wireApiSelectOptions.map((option) => (
1206
- <SelectItem key={option.value} value={option.value}>
1207
- {option.label}
1208
- </SelectItem>
1209
- ))}
1210
- </SelectContent>
1211
- </Select>
1212
- )}
1213
- </div>
1214
- )}
1215
-
1216
- <div className="space-y-2">
1217
- <Label className="text-sm font-medium text-gray-900">
1218
- {extraHeadersHint?.label ?? t('extraHeaders')}
1219
- </Label>
1220
- <KeyValueEditor value={extraHeaders} onChange={setExtraHeaders} />
1221
- <p className="text-xs text-gray-500">{t('providerExtraHeadersHelpShort')}</p>
1222
- </div>
1223
- </div>
1224
- )}
1225
- </div>
641
+ <ProviderModelsSection
642
+ models={models}
643
+ modelThinking={modelThinking}
644
+ modelDraft={modelDraft}
645
+ showModelInput={showModelInput}
646
+ onModelDraftChange={setModelDraft}
647
+ onShowModelInputChange={setShowModelInput}
648
+ onAddModel={handleAddModel}
649
+ onRemoveModel={(modelName) => {
650
+ setModels((prev) => prev.filter((name) => name !== modelName));
651
+ setModelThinking((prev) => {
652
+ const next = { ...prev };
653
+ delete next[modelName];
654
+ return next;
655
+ });
656
+ }}
657
+ onToggleModelThinkingLevel={toggleModelThinkingLevel}
658
+ onSetModelThinkingDefault={setModelThinkingDefault}
659
+ thinkingLevels={THINKING_LEVELS}
660
+ formatThinkingLevelLabel={formatThinkingLevelLabel}
661
+ />
662
+
663
+ <ProviderAdvancedSettingsSection
664
+ showAdvanced={showAdvanced}
665
+ onShowAdvancedChange={setShowAdvanced}
666
+ supportsWireApi={Boolean(providerSpec.supportsWireApi)}
667
+ wireApiLabel={wireApiHint?.label ?? t('wireApi')}
668
+ wireApi={wireApi}
669
+ onWireApiChange={setWireApi}
670
+ shouldUseWireApiPills={shouldUseWireApiPills}
671
+ wireApiSelectOptions={wireApiSelectOptions}
672
+ extraHeadersLabel={extraHeadersHint?.label ?? t('extraHeaders')}
673
+ extraHeaders={extraHeaders}
674
+ onExtraHeadersChange={setExtraHeaders}
675
+ />
1226
676
  </div>
1227
677
 
1228
678
  <div className="flex items-center justify-between border-t border-gray-100 px-6 py-4">