@nextclaw/ui 0.6.11 → 0.6.13

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 (62) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/assets/ChannelsList-CXLzowHj.js +1 -0
  3. package/dist/assets/ChatPage-CvtonrzM.js +36 -0
  4. package/dist/assets/DocBrowser-4NK6-Q_u.js +1 -0
  5. package/dist/assets/LogoBadge-NI7KQCLa.js +1 -0
  6. package/dist/assets/{MarketplacePage-BOzko5s9.js → MarketplacePage-n7y-pif2.js} +2 -2
  7. package/dist/assets/ModelConfig-DztCs0mA.js +1 -0
  8. package/dist/assets/ProvidersList-hSzfE0pG.js +1 -0
  9. package/dist/assets/{RuntimeConfig-Dt9pLB9P.js → RuntimeConfig-CKFGVus7.js} +1 -1
  10. package/dist/assets/SearchConfig-Cxs1744q.js +1 -0
  11. package/dist/assets/{SecretsConfig-C1PU0Yy8.js → SecretsConfig-C90UckNB.js} +2 -2
  12. package/dist/assets/SessionsConfig-CRor418P.js +2 -0
  13. package/dist/assets/{card-C7Gtw2Vs.js → card-BQiPUGaa.js} +1 -1
  14. package/dist/assets/config-layout-BHnOoweL.js +1 -0
  15. package/dist/assets/index-BCfS4UY1.css +1 -0
  16. package/dist/assets/index-CB5eJOGS.js +8 -0
  17. package/dist/assets/index-CkqvHQAt.js +1 -0
  18. package/dist/assets/{input-oBvxsnV9.js → input-DmFFMdAk.js} +1 -1
  19. package/dist/assets/{label-C7F8lMpQ.js → label-BHvlZoIz.js} +1 -1
  20. package/dist/assets/{page-layout-DO8BlScF.js → page-layout-COPE9JyG.js} +1 -1
  21. package/dist/assets/popover-gcypYeec.js +1 -0
  22. package/dist/assets/provider-models-D3B_xWXx.js +1 -0
  23. package/dist/assets/{session-run-status-Kg0FwAPn.js → session-run-status-BgNvd_-a.js} +1 -1
  24. package/dist/assets/{switch-C6a5GyZB.js → switch-BampMwqT.js} +1 -1
  25. package/dist/assets/{tabs-custom-BatFap5k.js → tabs-custom-DSQeYaKd.js} +1 -1
  26. package/dist/assets/useConfirmDialog-DZUn23Li.js +5 -0
  27. package/dist/assets/{vendor-TlME1INH.js → vendor-BKtTvQYU.js} +69 -64
  28. package/dist/index.html +3 -3
  29. package/package.json +1 -1
  30. package/src/App.tsx +2 -0
  31. package/src/api/config.ts +13 -0
  32. package/src/api/types.ts +61 -0
  33. package/src/components/chat/ChatPage.tsx +16 -0
  34. package/src/components/chat/chat-input/ChatInputBottomToolbar.tsx +12 -0
  35. package/src/components/chat/chat-input/components/ChatInputSlashPanelSection.tsx +3 -3
  36. package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputThinkingSelector.tsx +74 -0
  37. package/src/components/chat/chat-input/useChatInputBarController.ts +11 -2
  38. package/src/components/chat/chat-input.types.ts +8 -0
  39. package/src/components/chat/chat-page-data.ts +40 -3
  40. package/src/components/chat/chat-stream/transport.ts +3 -0
  41. package/src/components/chat/chat-stream/types.ts +3 -1
  42. package/src/components/chat/managers/chat-input.manager.ts +51 -0
  43. package/src/components/chat/stores/chat-input.store.ts +5 -1
  44. package/src/components/common/SearchableModelInput.tsx +22 -5
  45. package/src/components/config/ModelConfig.tsx +13 -12
  46. package/src/components/config/ProviderForm.tsx +292 -19
  47. package/src/components/config/SearchConfig.tsx +297 -0
  48. package/src/components/layout/Sidebar.tsx +6 -1
  49. package/src/hooks/useConfig.ts +17 -0
  50. package/src/lib/i18n.ts +37 -0
  51. package/src/lib/provider-models.ts +91 -3
  52. package/dist/assets/ChannelsList-C49JQ-Zt.js +0 -1
  53. package/dist/assets/ChatPage-DIx05c6s.js +0 -36
  54. package/dist/assets/DocBrowser-CpOosDEI.js +0 -1
  55. package/dist/assets/LogoBadge-CL_8ZPXU.js +0 -1
  56. package/dist/assets/ModelConfig-BZ4ZfaQB.js +0 -1
  57. package/dist/assets/ProvidersList-fPpJ5gl6.js +0 -1
  58. package/dist/assets/SessionsConfig-EskBOofQ.js +0 -2
  59. package/dist/assets/index-Cn6_2To7.js +0 -8
  60. package/dist/assets/index-nEYGCJTC.css +0 -1
  61. package/dist/assets/provider-models-y4mUDcGF.js +0 -1
  62. package/dist/assets/useConfirmDialog-zJzVKMdu.js +0 -5
@@ -1,4 +1,4 @@
1
- import { useMemo, useState } from 'react';
1
+ import { useEffect, useMemo, useState } from 'react';
2
2
  import { Check, ChevronsUpDown } from 'lucide-react';
3
3
  import { Input } from '@/components/ui/input';
4
4
  import { cn } from '@/lib/utils';
@@ -8,6 +8,7 @@ type SearchableModelInputProps = {
8
8
  value: string;
9
9
  onChange: (value: string) => void;
10
10
  options: string[];
11
+ disabled?: boolean;
11
12
  placeholder?: string;
12
13
  className?: string;
13
14
  inputClassName?: string;
@@ -33,6 +34,7 @@ export function SearchableModelInput({
33
34
  value,
34
35
  onChange,
35
36
  options,
37
+ disabled = false,
36
38
  placeholder,
37
39
  className,
38
40
  inputClassName,
@@ -43,6 +45,12 @@ export function SearchableModelInput({
43
45
  }: SearchableModelInputProps) {
44
46
  const [open, setOpen] = useState(false);
45
47
 
48
+ useEffect(() => {
49
+ if (disabled && open) {
50
+ setOpen(false);
51
+ }
52
+ }, [disabled, open]);
53
+
46
54
  const normalizedOptions = useMemo(() => normalizeOptions(options), [options]);
47
55
  const query = value.trim().toLowerCase();
48
56
 
@@ -80,10 +88,15 @@ export function SearchableModelInput({
80
88
  <Input
81
89
  id={id}
82
90
  value={value}
83
- onFocus={() => setOpen(true)}
91
+ disabled={disabled}
92
+ onFocus={() => {
93
+ if (!disabled) {
94
+ setOpen(true);
95
+ }
96
+ }}
84
97
  onChange={(event) => {
85
98
  onChange(event.target.value);
86
- if (!open) {
99
+ if (!open && !disabled) {
87
100
  setOpen(true);
88
101
  }
89
102
  }}
@@ -103,13 +116,17 @@ export function SearchableModelInput({
103
116
  type="button"
104
117
  onMouseDown={(event) => event.preventDefault()}
105
118
  onClick={() => setOpen((prev) => !prev)}
106
- className="absolute inset-y-0 right-0 inline-flex w-10 items-center justify-center text-gray-400 hover:text-gray-600"
119
+ disabled={disabled}
120
+ className={cn(
121
+ 'absolute inset-y-0 right-0 inline-flex w-10 items-center justify-center',
122
+ disabled ? 'cursor-not-allowed text-gray-300' : 'text-gray-400 hover:text-gray-600'
123
+ )}
107
124
  aria-label="toggle model options"
108
125
  >
109
126
  <ChevronsUpDown className="h-4 w-4" />
110
127
  </button>
111
128
 
112
- {open && (
129
+ {open && !disabled && (
113
130
  <div className="absolute z-20 mt-1 w-full overflow-hidden rounded-xl border border-gray-200 bg-white shadow-lg">
114
131
  <div className="max-h-60 overflow-y-auto py-1">
115
132
  {!hasExactMatch && value.trim().length > 0 && (
@@ -33,33 +33,26 @@ export function ModelConfig() {
33
33
  const workspaceHint = hintForPath('agents.defaults.workspace', uiHints);
34
34
 
35
35
  const providerCatalog = useMemo(
36
- () => buildProviderModelCatalog({ meta, config }),
36
+ () => buildProviderModelCatalog({ meta, config, onlyConfigured: true }),
37
37
  [config, meta]
38
38
  );
39
39
 
40
40
  const providerMap = useMemo(() => new Map(providerCatalog.map((provider) => [provider.name, provider])), [providerCatalog]);
41
- const selectedProvider = providerMap.get(providerName) ?? providerCatalog[0];
41
+ const selectedProvider = providerMap.get(providerName);
42
42
  const selectedProviderName = selectedProvider?.name ?? '';
43
43
  const selectedProviderAliases = useMemo(() => selectedProvider?.aliases ?? [], [selectedProvider]);
44
44
  const selectedProviderModels = useMemo(() => selectedProvider?.models ?? [], [selectedProvider]);
45
45
 
46
- useEffect(() => {
47
- if (providerName || providerCatalog.length === 0) {
48
- return;
49
- }
50
- setProviderName(providerCatalog[0].name);
51
- }, [providerName, providerCatalog]);
52
-
53
46
  useEffect(() => {
54
47
  if (!config?.agents?.defaults) {
55
48
  return;
56
49
  }
57
50
  const currentModel = (config.agents.defaults.model || '').trim();
58
51
  const matchedProvider = findProviderByModel(currentModel, providerCatalog);
59
- const effectiveProvider = matchedProvider ?? providerCatalog[0]?.name ?? '';
52
+ const effectiveProvider = matchedProvider ?? '';
60
53
  const aliases = providerMap.get(effectiveProvider)?.aliases ?? [];
61
54
  setProviderName(effectiveProvider);
62
- setModelId(toProviderLocalModel(currentModel, aliases));
55
+ setModelId(effectiveProvider ? toProviderLocalModel(currentModel, aliases) : '');
63
56
  setWorkspace(config.agents.defaults.workspace || '');
64
57
  }, [config, providerCatalog, providerMap]);
65
58
 
@@ -75,11 +68,14 @@ export function ModelConfig() {
75
68
  }, [selectedProviderModels]);
76
69
 
77
70
  const composedModel = useMemo(() => {
71
+ if (!selectedProvider) {
72
+ return '';
73
+ }
78
74
  const normalizedModelId = toProviderLocalModel(modelId, selectedProviderAliases);
79
75
  if (!normalizedModelId) {
80
76
  return '';
81
77
  }
82
- return composeProviderModel(selectedProvider?.prefix ?? '', normalizedModelId);
78
+ return composeProviderModel(selectedProvider.prefix, normalizedModelId);
83
79
  }, [modelId, selectedProvider, selectedProviderAliases]);
84
80
 
85
81
  const modelHelpText = t('modelIdentifierHelp') || modelHint?.help || '';
@@ -90,6 +86,10 @@ export function ModelConfig() {
90
86
  };
91
87
 
92
88
  const handleModelChange = (nextModelId: string) => {
89
+ if (!selectedProvider) {
90
+ setModelId('');
91
+ return;
92
+ }
93
93
  setModelId(toProviderLocalModel(nextModelId, selectedProviderAliases));
94
94
  };
95
95
 
@@ -165,6 +165,7 @@ export function ModelConfig() {
165
165
  value={modelId}
166
166
  onChange={handleModelChange}
167
167
  options={modelOptions}
168
+ disabled={!selectedProviderName}
168
169
  placeholder={modelHint?.placeholder ?? 'gpt-5.1'}
169
170
  className="sm:flex-1"
170
171
  inputClassName="h-10 rounded-xl"
@@ -15,17 +15,19 @@ import { Button } from '@/components/ui/button';
15
15
  import { Input } from '@/components/ui/input';
16
16
  import { Label } from '@/components/ui/label';
17
17
  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
18
+ import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
18
19
  import { MaskedInput } from '@/components/common/MaskedInput';
19
20
  import { KeyValueEditor } from '@/components/common/KeyValueEditor';
20
21
  import { StatusDot } from '@/components/ui/status-dot';
21
22
  import { getLanguage, t } from '@/lib/i18n';
22
23
  import { hintForPath } from '@/lib/config-hints';
23
- import type { ProviderConfigView, ProviderConfigUpdate, ProviderConnectionTestRequest } from '@/api/types';
24
+ import type { ProviderConfigView, ProviderConfigUpdate, ProviderConnectionTestRequest, ThinkingLevel } from '@/api/types';
24
25
  import { CircleDotDashed, Plus, X, Trash2, ChevronDown, Settings2 } from 'lucide-react';
25
26
  import { toast } from 'sonner';
26
27
  import { CONFIG_DETAIL_CARD_CLASS, CONFIG_EMPTY_DETAIL_CARD_CLASS } from './config-layout';
27
28
 
28
29
  type WireApiType = 'auto' | 'chat' | 'responses';
30
+ type ModelThinkingConfig = Record<string, { supported: ThinkingLevel[]; default?: ThinkingLevel | null }>;
29
31
 
30
32
  type ProviderFormProps = {
31
33
  providerName?: string;
@@ -48,8 +50,11 @@ const EMPTY_PROVIDER_CONFIG: ProviderConfigView = {
48
50
  apiBase: null,
49
51
  extraHeaders: null,
50
52
  wireApi: null,
51
- models: []
53
+ models: [],
54
+ modelThinking: {}
52
55
  };
56
+ const THINKING_LEVELS: ThinkingLevel[] = ['off', 'minimal', 'low', 'medium', 'high', 'adaptive', 'xhigh'];
57
+ const THINKING_LEVEL_SET = new Set<string>(THINKING_LEVELS);
53
58
 
54
59
  function normalizeHeaders(input: Record<string, string> | null | undefined): Record<string, string> | null {
55
60
  if (!input) {
@@ -160,6 +165,128 @@ function serializeModelsForSave(models: string[], defaultModels: string[]): stri
160
165
  return models;
161
166
  }
162
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
+
163
290
  function resolvePreferredAuthMethodId(params: {
164
291
  providerName?: string;
165
292
  methods: ProviderAuthMethodOption[];
@@ -272,6 +399,7 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
272
399
  const [extraHeaders, setExtraHeaders] = useState<Record<string, string> | null>(null);
273
400
  const [wireApi, setWireApi] = useState<WireApiType>('auto');
274
401
  const [models, setModels] = useState<string[]>([]);
402
+ const [modelThinking, setModelThinking] = useState<ModelThinkingConfig>({});
275
403
  const [modelDraft, setModelDraft] = useState('');
276
404
  const [providerDisplayName, setProviderDisplayName] = useState('');
277
405
  const [showAdvanced, setShowAdvanced] = useState(false);
@@ -323,6 +451,14 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
323
451
  () => resolveEditableModels(defaultModels, currentModels),
324
452
  [defaultModels, currentModels]
325
453
  );
454
+ const currentModelThinking = useMemo(
455
+ () =>
456
+ normalizeModelThinkingForModels(
457
+ normalizeModelThinkingConfig(resolvedProviderConfig.modelThinking, providerModelAliases),
458
+ currentEditableModels
459
+ ),
460
+ [currentEditableModels, providerModelAliases, resolvedProviderConfig.modelThinking]
461
+ );
326
462
  const language = getLanguage();
327
463
  const apiBaseHelpText =
328
464
  providerSpec?.apiBaseHelp?.[language] ||
@@ -443,6 +579,7 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
443
579
  setExtraHeaders(null);
444
580
  setWireApi('auto');
445
581
  setModels([]);
582
+ setModelThinking({});
446
583
  setModelDraft('');
447
584
  setProviderDisplayName('');
448
585
  setAuthSessionId(null);
@@ -457,6 +594,7 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
457
594
  setExtraHeaders(resolvedProviderConfig.extraHeaders || null);
458
595
  setWireApi(currentWireApi);
459
596
  setModels(currentEditableModels);
597
+ setModelThinking(currentModelThinking);
460
598
  setModelDraft('');
461
599
  setProviderDisplayName(effectiveDisplayName);
462
600
  setAuthSessionId(null);
@@ -469,6 +607,7 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
469
607
  resolvedProviderConfig.extraHeaders,
470
608
  currentWireApi,
471
609
  currentEditableModels,
610
+ currentModelThinking,
472
611
  effectiveDisplayName,
473
612
  preferredAuthMethodId,
474
613
  clearAuthPollTimer
@@ -476,6 +615,10 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
476
615
 
477
616
  useEffect(() => () => clearAuthPollTimer(), [clearAuthPollTimer]);
478
617
 
618
+ useEffect(() => {
619
+ setModelThinking((prev) => normalizeModelThinkingForModels(prev, models));
620
+ }, [models]);
621
+
479
622
  const hasChanges = useMemo(() => {
480
623
  if (!providerName) {
481
624
  return false;
@@ -485,11 +628,20 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
485
628
  const headersChanged = !headersEqual(extraHeaders, currentHeaders);
486
629
  const wireApiChanged = providerSpec?.supportsWireApi ? wireApi !== currentWireApi : false;
487
630
  const modelsChanged = !modelListsEqual(models, currentEditableModels);
631
+ const modelThinkingChanged = !modelThinkingEqual(modelThinking, currentModelThinking);
488
632
  const displayNameChanged = isCustomProvider
489
633
  ? providerDisplayName.trim() !== effectiveDisplayName
490
634
  : false;
491
635
 
492
- return apiKeyChanged || apiBaseChanged || headersChanged || wireApiChanged || modelsChanged || displayNameChanged;
636
+ return (
637
+ apiKeyChanged ||
638
+ apiBaseChanged ||
639
+ headersChanged ||
640
+ wireApiChanged ||
641
+ modelsChanged ||
642
+ modelThinkingChanged ||
643
+ displayNameChanged
644
+ );
493
645
  }, [
494
646
  providerName,
495
647
  isCustomProvider,
@@ -504,7 +656,9 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
504
656
  wireApi,
505
657
  currentWireApi,
506
658
  models,
507
- currentEditableModels
659
+ currentEditableModels,
660
+ modelThinking,
661
+ currentModelThinking
508
662
  ]);
509
663
 
510
664
  const handleAddModel = () => {
@@ -520,6 +674,45 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
520
674
  setModelDraft('');
521
675
  };
522
676
 
677
+ const toggleModelThinkingLevel = (modelName: string, level: ThinkingLevel) => {
678
+ setModelThinking((prev) => {
679
+ const currentEntry = prev[modelName];
680
+ const currentLevels = currentEntry?.supported ?? [];
681
+ const nextLevels = currentLevels.includes(level)
682
+ ? currentLevels.filter((item) => item !== level)
683
+ : THINKING_LEVELS.filter((item) => item === level || currentLevels.includes(item));
684
+ if (nextLevels.length === 0) {
685
+ const next = { ...prev };
686
+ delete next[modelName];
687
+ return next;
688
+ }
689
+ const nextDefault =
690
+ currentEntry?.default && nextLevels.includes(currentEntry.default) ? currentEntry.default : undefined;
691
+ return {
692
+ ...prev,
693
+ [modelName]: nextDefault ? { supported: nextLevels, default: nextDefault } : { supported: nextLevels }
694
+ };
695
+ });
696
+ };
697
+
698
+ const setModelThinkingDefault = (modelName: string, level: ThinkingLevel | null) => {
699
+ setModelThinking((prev) => {
700
+ const currentEntry = prev[modelName];
701
+ if (!currentEntry || currentEntry.supported.length === 0) {
702
+ return prev;
703
+ }
704
+ if (level && !currentEntry.supported.includes(level)) {
705
+ return prev;
706
+ }
707
+ return {
708
+ ...prev,
709
+ [modelName]: level
710
+ ? { supported: currentEntry.supported, default: level }
711
+ : { supported: currentEntry.supported }
712
+ };
713
+ });
714
+ };
715
+
523
716
  const handleSubmit = (e: React.FormEvent) => {
524
717
  e.preventDefault();
525
718
 
@@ -555,6 +748,9 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
555
748
  if (!modelListsEqual(models, currentEditableModels)) {
556
749
  payload.models = serializeModelsForSave(models, defaultModels);
557
750
  }
751
+ if (!modelThinkingEqual(modelThinking, currentModelThinking)) {
752
+ payload.modelThinking = normalizeModelThinkingForModels(modelThinking, models);
753
+ }
558
754
 
559
755
  updateProvider.mutate({ provider: providerName, data: payload });
560
756
  };
@@ -876,22 +1072,99 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
876
1072
  </div>
877
1073
  ) : (
878
1074
  <div className="flex flex-wrap gap-2">
879
- {models.map((modelName) => (
880
- <div
881
- key={modelName}
882
- className="group inline-flex max-w-full items-center gap-1 rounded-full border border-gray-200 bg-white px-3 py-1.5"
883
- >
884
- <span className="max-w-[180px] truncate text-sm text-gray-800 sm:max-w-[240px]">{modelName}</span>
885
- <button
886
- type="button"
887
- onClick={() => setModels((prev) => prev.filter((name) => name !== modelName))}
888
- 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"
889
- aria-label={t('remove')}
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"
890
1083
  >
891
- <X className="h-3 w-3" />
892
- </button>
893
- </div>
894
- ))}
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
+ })}
895
1168
  </div>
896
1169
  )}
897
1170
  </div>