@nextclaw/ui 0.6.12 → 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 (53) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/assets/{ChannelsList-DBDjwf-X.js → ChannelsList-CXLzowHj.js} +1 -1
  3. package/dist/assets/ChatPage-CvtonrzM.js +36 -0
  4. package/dist/assets/{DocBrowser-ZOplDEMS.js → DocBrowser-4NK6-Q_u.js} +1 -1
  5. package/dist/assets/{LogoBadge-2LMzEMwe.js → LogoBadge-NI7KQCLa.js} +1 -1
  6. package/dist/assets/{MarketplacePage-D4JHYcB5.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-4sb3mpkd.js → RuntimeConfig-CKFGVus7.js} +1 -1
  10. package/dist/assets/{SearchConfig-B4u_MxRG.js → SearchConfig-Cxs1744q.js} +1 -1
  11. package/dist/assets/{SecretsConfig-BQXblZvb.js → SecretsConfig-C90UckNB.js} +2 -2
  12. package/dist/assets/SessionsConfig-CRor418P.js +2 -0
  13. package/dist/assets/{card-BekAnCgX.js → card-BQiPUGaa.js} +1 -1
  14. package/dist/assets/index-BCfS4UY1.css +1 -0
  15. package/dist/assets/index-CB5eJOGS.js +8 -0
  16. package/dist/assets/index-CkqvHQAt.js +1 -0
  17. package/dist/assets/{input-MMn_Na9q.js → input-DmFFMdAk.js} +1 -1
  18. package/dist/assets/{label-Dg2ydpN0.js → label-BHvlZoIz.js} +1 -1
  19. package/dist/assets/{page-layout-7K0rcz0I.js → page-layout-COPE9JyG.js} +1 -1
  20. package/dist/assets/popover-gcypYeec.js +1 -0
  21. package/dist/assets/provider-models-D3B_xWXx.js +1 -0
  22. package/dist/assets/{session-run-status-CAdjSqeb.js → session-run-status-BgNvd_-a.js} +1 -1
  23. package/dist/assets/{switch-DnDMlDVu.js → switch-BampMwqT.js} +1 -1
  24. package/dist/assets/{tabs-custom-khLM8lWj.js → tabs-custom-DSQeYaKd.js} +1 -1
  25. package/dist/assets/useConfirmDialog-DZUn23Li.js +5 -0
  26. package/dist/assets/{vendor-d7E8OgNx.js → vendor-BKtTvQYU.js} +69 -64
  27. package/dist/index.html +3 -3
  28. package/package.json +1 -1
  29. package/src/api/types.ts +4 -0
  30. package/src/components/chat/ChatPage.tsx +16 -0
  31. package/src/components/chat/chat-input/ChatInputBottomToolbar.tsx +12 -0
  32. package/src/components/chat/chat-input/components/ChatInputSlashPanelSection.tsx +3 -3
  33. package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputThinkingSelector.tsx +74 -0
  34. package/src/components/chat/chat-input/useChatInputBarController.ts +11 -2
  35. package/src/components/chat/chat-input.types.ts +8 -0
  36. package/src/components/chat/chat-page-data.ts +40 -3
  37. package/src/components/chat/chat-stream/transport.ts +3 -0
  38. package/src/components/chat/chat-stream/types.ts +3 -1
  39. package/src/components/chat/managers/chat-input.manager.ts +51 -0
  40. package/src/components/chat/stores/chat-input.store.ts +5 -1
  41. package/src/components/common/SearchableModelInput.tsx +22 -5
  42. package/src/components/config/ModelConfig.tsx +13 -12
  43. package/src/components/config/ProviderForm.tsx +292 -19
  44. package/src/lib/i18n.ts +15 -0
  45. package/src/lib/provider-models.ts +91 -3
  46. package/dist/assets/ChatPage-C18sGGk1.js +0 -36
  47. package/dist/assets/ModelConfig-DZVvdLFq.js +0 -1
  48. package/dist/assets/ProvidersList-Dum31480.js +0 -1
  49. package/dist/assets/SessionsConfig-Jk29xjQU.js +0 -2
  50. package/dist/assets/index-BXwjfCEO.css +0 -1
  51. package/dist/assets/index-Dl6t70wA.js +0 -8
  52. package/dist/assets/provider-models-y4mUDcGF.js +0 -1
  53. package/dist/assets/useConfirmDialog-BYA1XnVU.js +0 -5
package/dist/index.html CHANGED
@@ -6,9 +6,9 @@
6
6
  <link rel="icon" type="image/svg+xml" href="/logo.svg" />
7
7
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
8
8
  <title>NextClaw - 系统配置</title>
9
- <script type="module" crossorigin src="/assets/index-Dl6t70wA.js"></script>
10
- <link rel="modulepreload" crossorigin href="/assets/vendor-d7E8OgNx.js">
11
- <link rel="stylesheet" crossorigin href="/assets/index-BXwjfCEO.css">
9
+ <script type="module" crossorigin src="/assets/index-CB5eJOGS.js"></script>
10
+ <link rel="modulepreload" crossorigin href="/assets/vendor-BKtTvQYU.js">
11
+ <link rel="stylesheet" crossorigin href="/assets/index-BCfS4UY1.css">
12
12
  </head>
13
13
 
14
14
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextclaw/ui",
3
- "version": "0.6.12",
3
+ "version": "0.6.13",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/api/types.ts CHANGED
@@ -14,6 +14,8 @@ export type AppMetaView = {
14
14
  productVersion: string;
15
15
  };
16
16
 
17
+ export type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "adaptive" | "xhigh";
18
+
17
19
  export type ProviderConfigView = {
18
20
  displayName?: string;
19
21
  apiKeySet: boolean;
@@ -22,6 +24,7 @@ export type ProviderConfigView = {
22
24
  extraHeaders?: Record<string, string> | null;
23
25
  wireApi?: "auto" | "chat" | "responses" | null;
24
26
  models?: string[];
27
+ modelThinking?: Record<string, { supported: ThinkingLevel[]; default?: ThinkingLevel | null }>;
25
28
  };
26
29
 
27
30
  export type ProviderConfigUpdate = {
@@ -31,6 +34,7 @@ export type ProviderConfigUpdate = {
31
34
  extraHeaders?: Record<string, string> | null;
32
35
  wireApi?: "auto" | "chat" | "responses" | null;
33
36
  models?: string[] | null;
37
+ modelThinking?: Record<string, { supported?: ThinkingLevel[]; default?: ThinkingLevel | null }> | null;
34
38
  };
35
39
 
36
40
  export type ProviderConnectionTestRequest = ProviderConfigUpdate & {
@@ -122,6 +122,7 @@ export function ChatPage({ view }: ChatPageProps) {
122
122
  const { sessionId: routeSessionIdParam } = useParams<{ sessionId?: string }>();
123
123
  const threadRef = useRef<HTMLDivElement | null>(null);
124
124
  const selectedSessionKeyRef = useRef<string | null>(selectedSessionKey);
125
+ const thinkingHydratedSessionKeyRef = useRef<string | null>(null);
125
126
  const routeSessionKey = useMemo(
126
127
  () => parseSessionKeyFromRoute(routeSessionIdParam),
127
128
  [routeSessionIdParam]
@@ -137,6 +138,7 @@ export function ChatPage({ view }: ChatPageProps) {
137
138
  skillRecords,
138
139
  selectedSession,
139
140
  historyMessages,
141
+ selectedSessionThinkingLevel,
140
142
  sessionTypeOptions,
141
143
  defaultSessionType,
142
144
  selectedSessionType,
@@ -232,6 +234,12 @@ export function ChatPage({ view }: ChatPageProps) {
232
234
  ]);
233
235
 
234
236
  useEffect(() => {
237
+ const shouldHydrateThinkingFromHistory =
238
+ !isSending &&
239
+ !isAwaitingAssistantOutput &&
240
+ !historyQuery.isLoading &&
241
+ selectedSessionKey !== thinkingHydratedSessionKeyRef.current;
242
+
235
243
  presenter.chatInputManager.syncSnapshot({
236
244
  isProviderStateResolved,
237
245
  defaultSessionType,
@@ -244,11 +252,18 @@ export function ChatPage({ view }: ChatPageProps) {
244
252
  modelOptions,
245
253
  sessionTypeOptions,
246
254
  selectedSessionType,
255
+ ...(shouldHydrateThinkingFromHistory ? { selectedThinkingLevel: selectedSessionThinkingLevel } : {}),
247
256
  canEditSessionType,
248
257
  sessionTypeUnavailable,
249
258
  skillRecords,
250
259
  isSkillsLoading: installedSkillsQuery.isLoading
251
260
  });
261
+ if (shouldHydrateThinkingFromHistory) {
262
+ thinkingHydratedSessionKeyRef.current = selectedSessionKey;
263
+ }
264
+ if (!selectedSessionKey) {
265
+ thinkingHydratedSessionKeyRef.current = null;
266
+ }
252
267
  presenter.chatSessionListManager.syncSnapshot({
253
268
  sessions,
254
269
  query,
@@ -292,6 +307,7 @@ export function ChatPage({ view }: ChatPageProps) {
292
307
  presenter,
293
308
  query,
294
309
  selectedSession,
310
+ selectedSessionThinkingLevel,
295
311
  selectedSessionKey,
296
312
  selectedAgentId,
297
313
  selectedSessionType,
@@ -5,6 +5,7 @@ import { ChatInputAttachButton } from '@/components/chat/chat-input/components/b
5
5
  import { ChatInputModelSelector } from '@/components/chat/chat-input/components/bottom-toolbar/ChatInputModelSelector';
6
6
  import { ChatInputSendControls } from '@/components/chat/chat-input/components/bottom-toolbar/ChatInputSendControls';
7
7
  import { ChatInputSessionTypeSelector } from '@/components/chat/chat-input/components/bottom-toolbar/ChatInputSessionTypeSelector';
8
+ import { ChatInputThinkingSelector } from '@/components/chat/chat-input/components/bottom-toolbar/ChatInputThinkingSelector';
8
9
  import { t } from '@/lib/i18n';
9
10
 
10
11
  export function ChatInputBottomToolbar() {
@@ -27,6 +28,9 @@ export function ChatInputBottomToolbar() {
27
28
  snapshot.stopDisabledReason === '__preparing__'
28
29
  ? t('chatStopPreparing')
29
30
  : snapshot.stopDisabledReason?.trim() || t('chatStopUnavailable');
31
+ const selectedModelThinkingCapability = selectedModelOption?.thinkingCapability;
32
+ const thinkingSupportedLevels = selectedModelThinkingCapability?.supported ?? [];
33
+ const shouldShowThinkingSelector = thinkingSupportedLevels.length > 0;
30
34
 
31
35
  return (
32
36
  <div className="flex items-center justify-between px-3 pb-3">
@@ -53,6 +57,14 @@ export function ChatInputBottomToolbar() {
53
57
  isModelOptionsLoading={isModelOptionsLoading}
54
58
  hasModelOptions={hasModelOptions}
55
59
  />
60
+ {shouldShowThinkingSelector ? (
61
+ <ChatInputThinkingSelector
62
+ supportedLevels={thinkingSupportedLevels}
63
+ selectedThinkingLevel={snapshot.selectedThinkingLevel}
64
+ defaultThinkingLevel={selectedModelThinkingCapability?.default ?? null}
65
+ onSelectedThinkingLevelChange={presenter.chatInputManager.selectThinkingLevel}
66
+ />
67
+ ) : null}
56
68
  <ChatInputAttachButton />
57
69
  </div>
58
70
  <ChatInputSendControls
@@ -43,10 +43,10 @@ export function ChatInputSlashPanelSection({
43
43
  onOpenAutoFocus={(event) => event.preventDefault()}
44
44
  style={resolvedSlashPanelWidth ? { width: `${resolvedSlashPanelWidth}px` } : undefined}
45
45
  >
46
- <div className="grid min-h-[240px] grid-cols-[minmax(260px,340px)_minmax(0,1fr)]">
46
+ <div className="grid min-h-[240px] grid-cols-[minmax(220px,300px)_minmax(0,1fr)]">
47
47
  <div
48
48
  ref={slashListRef}
49
- className="max-h-[320px] overflow-y-auto border-r border-gray-200 p-3 custom-scrollbar"
49
+ className="max-h-[320px] overflow-y-auto border-r border-gray-200 p-2.5 custom-scrollbar"
50
50
  >
51
51
  {isSlashPanelLoading ? (
52
52
  <div className="p-2 text-xs text-gray-500">{t('chatSlashLoading')}</div>
@@ -82,7 +82,7 @@ export function ChatInputSlashPanelSection({
82
82
  </>
83
83
  )}
84
84
  </div>
85
- <div className="p-4">
85
+ <div className="max-w-[320px] p-3.5">
86
86
  {activeSlashItem ? (
87
87
  <div className="space-y-3">
88
88
  <div className="flex items-center gap-2">
@@ -0,0 +1,74 @@
1
+ import type { ThinkingLevel } from '@/api/types';
2
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
3
+ import { t } from '@/lib/i18n';
4
+ import { Brain } from 'lucide-react';
5
+
6
+ type ChatInputThinkingSelectorProps = {
7
+ supportedLevels: ThinkingLevel[];
8
+ selectedThinkingLevel: ThinkingLevel | null;
9
+ defaultThinkingLevel?: ThinkingLevel | null;
10
+ onSelectedThinkingLevelChange: (value: ThinkingLevel) => void;
11
+ };
12
+
13
+ function thinkingLabel(level: ThinkingLevel): string {
14
+ if (level === 'off') {
15
+ return t('chatThinkingLevelOff');
16
+ }
17
+ if (level === 'minimal') {
18
+ return t('chatThinkingLevelMinimal');
19
+ }
20
+ if (level === 'low') {
21
+ return t('chatThinkingLevelLow');
22
+ }
23
+ if (level === 'medium') {
24
+ return t('chatThinkingLevelMedium');
25
+ }
26
+ if (level === 'high') {
27
+ return t('chatThinkingLevelHigh');
28
+ }
29
+ if (level === 'adaptive') {
30
+ return t('chatThinkingLevelAdaptive');
31
+ }
32
+ return t('chatThinkingLevelXhigh');
33
+ }
34
+
35
+ function normalizeLevels(levels: ThinkingLevel[]): ThinkingLevel[] {
36
+ const deduped: ThinkingLevel[] = [];
37
+ for (const level of ['off', ...levels] as ThinkingLevel[]) {
38
+ if (!deduped.includes(level)) {
39
+ deduped.push(level);
40
+ }
41
+ }
42
+ return deduped;
43
+ }
44
+
45
+ export function ChatInputThinkingSelector(props: ChatInputThinkingSelectorProps) {
46
+ if (props.supportedLevels.length === 0) {
47
+ return null;
48
+ }
49
+
50
+ const options = normalizeLevels(props.supportedLevels);
51
+ const fallback = options.includes('off') ? 'off' : options[0];
52
+ const resolvedValue =
53
+ (props.selectedThinkingLevel && options.includes(props.selectedThinkingLevel) && props.selectedThinkingLevel) ||
54
+ (props.defaultThinkingLevel && options.includes(props.defaultThinkingLevel) && props.defaultThinkingLevel) ||
55
+ fallback;
56
+
57
+ return (
58
+ <Select value={resolvedValue} onValueChange={(value) => props.onSelectedThinkingLevelChange(value as ThinkingLevel)}>
59
+ <SelectTrigger className="h-8 w-auto min-w-[150px] rounded-lg border-0 bg-transparent px-3 text-xs font-medium text-gray-600 shadow-none hover:bg-gray-100 focus:ring-0">
60
+ <div className="flex min-w-0 items-center gap-2 text-left">
61
+ <Brain className="h-3.5 w-3.5 shrink-0 text-gray-500" />
62
+ <span className="truncate text-xs font-semibold text-gray-700">{thinkingLabel(resolvedValue)}</span>
63
+ </div>
64
+ </SelectTrigger>
65
+ <SelectContent className="w-[180px]">
66
+ {options.map((level) => (
67
+ <SelectItem key={level} value={level}>
68
+ {thinkingLabel(level)}
69
+ </SelectItem>
70
+ ))}
71
+ </SelectContent>
72
+ </Select>
73
+ );
74
+ }
@@ -4,7 +4,9 @@ import type { MarketplaceInstalledRecord } from '@/api/types';
4
4
  import { t } from '@/lib/i18n';
5
5
  import type { ChatInputBarSlashItem } from '@/components/chat/chat-input.types';
6
6
 
7
- const SLASH_PANEL_MAX_WIDTH = 920;
7
+ const SLASH_PANEL_MAX_WIDTH = 680;
8
+ const SLASH_PANEL_DESKTOP_SHRINK_RATIO = 0.82;
9
+ const SLASH_PANEL_DESKTOP_MIN_WIDTH = 560;
8
10
 
9
11
  type RankedSkill = {
10
12
  record: MarketplaceInstalledRecord;
@@ -162,7 +164,14 @@ function useSlashPanelController(params: SlashPanelControllerParams) {
162
164
  const isSlashPanelOpen = slashQuery !== null && !dismissedSlashPanel;
163
165
  const activeSlashItem = slashItems[activeSlashIndex] ?? null;
164
166
  const isSlashPanelLoading = params.isSkillsLoading;
165
- const resolvedSlashPanelWidth = slashPanelWidth ? Math.min(slashPanelWidth, SLASH_PANEL_MAX_WIDTH) : undefined;
167
+ const resolvedSlashPanelWidth = slashPanelWidth
168
+ ? Math.min(
169
+ slashPanelWidth > SLASH_PANEL_DESKTOP_MIN_WIDTH
170
+ ? slashPanelWidth * SLASH_PANEL_DESKTOP_SHRINK_RATIO
171
+ : slashPanelWidth,
172
+ SLASH_PANEL_MAX_WIDTH
173
+ )
174
+ : undefined;
166
175
 
167
176
  useEffect(() => {
168
177
  const anchor = slashAnchorRef.current;
@@ -1,7 +1,15 @@
1
+ import type { ThinkingLevel } from '@/api/types';
2
+
3
+ export type ChatModelThinkingCapability = {
4
+ supported: ThinkingLevel[];
5
+ default?: ThinkingLevel | null;
6
+ };
7
+
1
8
  export type ChatModelOption = {
2
9
  value: string;
3
10
  modelLabel: string;
4
11
  providerLabel: string;
12
+ thinkingCapability?: ChatModelThinkingCapability | null;
5
13
  };
6
14
 
7
15
  export type ChatInputBarSlashItem = {
@@ -1,6 +1,6 @@
1
1
  import { useMemo } from 'react';
2
2
  import type { Dispatch, SetStateAction } from 'react';
3
- import type { SessionEntryView } from '@/api/types';
3
+ import type { SessionEntryView, ThinkingLevel } from '@/api/types';
4
4
  import type { ChatModelOption } from '@/components/chat/chat-input.types';
5
5
  import { useChatSessionTypeState } from '@/components/chat/useChatSessionTypeState';
6
6
  import { useSyncSelectedModel } from '@/components/chat/chat-page-runtime';
@@ -13,7 +13,7 @@ import {
13
13
  useSessions,
14
14
  } from '@/hooks/useConfig';
15
15
  import { useMarketplaceInstalled } from '@/hooks/useMarketplace';
16
- import { buildProviderModelCatalog, composeProviderModel } from '@/lib/provider-models';
16
+ import { buildProviderModelCatalog, composeProviderModel, resolveModelThinkingCapability } from '@/lib/provider-models';
17
17
 
18
18
  type UseChatPageDataParams = {
19
19
  query: string;
@@ -24,6 +24,19 @@ type UseChatPageDataParams = {
24
24
  setSelectedModel: Dispatch<SetStateAction<string>>;
25
25
  };
26
26
 
27
+ const THINKING_LEVEL_SET = new Set<string>(['off', 'minimal', 'low', 'medium', 'high', 'adaptive', 'xhigh']);
28
+
29
+ function parseThinkingLevel(value: unknown): ThinkingLevel | null {
30
+ if (typeof value !== 'string') {
31
+ return null;
32
+ }
33
+ const normalized = value.trim().toLowerCase();
34
+ if (!normalized) {
35
+ return null;
36
+ }
37
+ return THINKING_LEVEL_SET.has(normalized) ? (normalized as ThinkingLevel) : null;
38
+ }
39
+
27
40
  export function useChatPageData(params: UseChatPageDataParams) {
28
41
  const configQuery = useConfig();
29
42
  const configMetaQuery = useConfigMeta();
@@ -57,7 +70,8 @@ export function useChatPageData(params: UseChatPageDataParams) {
57
70
  options.push({
58
71
  value,
59
72
  modelLabel: localModel,
60
- providerLabel: provider.displayName
73
+ providerLabel: provider.displayName,
74
+ thinkingCapability: resolveModelThinkingCapability(provider.modelThinking, localModel, provider.aliases)
61
75
  });
62
76
  }
63
77
  }
@@ -93,6 +107,28 @@ export function useChatPageData(params: UseChatPageDataParams) {
93
107
  });
94
108
 
95
109
  const historyMessages = useMemo(() => historyQuery.data?.messages ?? [], [historyQuery.data?.messages]);
110
+ const selectedSessionThinkingLevel = useMemo(() => {
111
+ if (!params.selectedSessionKey) {
112
+ return null;
113
+ }
114
+ const metadata = historyQuery.data?.metadata;
115
+ if (!metadata || typeof metadata !== 'object') {
116
+ return null;
117
+ }
118
+ const candidates = [
119
+ metadata.preferred_thinking,
120
+ metadata.thinking,
121
+ metadata.thinking_level,
122
+ metadata.thinkingLevel
123
+ ];
124
+ for (const value of candidates) {
125
+ const level = parseThinkingLevel(value);
126
+ if (level) {
127
+ return level;
128
+ }
129
+ }
130
+ return null;
131
+ }, [historyQuery.data?.metadata, params.selectedSessionKey]);
96
132
 
97
133
  return {
98
134
  configQuery,
@@ -108,6 +144,7 @@ export function useChatPageData(params: UseChatPageDataParams) {
108
144
  skillRecords,
109
145
  selectedSession,
110
146
  historyMessages,
147
+ selectedSessionThinkingLevel,
111
148
  ...sessionTypeState
112
149
  };
113
150
  }
@@ -6,6 +6,9 @@ function buildSendTurnPayload(item: SendMessageParams, requestedSkills: string[]
6
6
  if (item.sessionType) {
7
7
  metadata.session_type = item.sessionType;
8
8
  }
9
+ if (item.thinkingLevel) {
10
+ metadata.thinking = item.thinkingLevel;
11
+ }
9
12
  if (requestedSkills.length > 0) {
10
13
  metadata.requested_skills = requestedSkills;
11
14
  }
@@ -4,7 +4,8 @@ import type {
4
4
  ChatTurnStreamDeltaEvent,
5
5
  ChatTurnStreamReadyEvent,
6
6
  ChatTurnStreamSessionEvent,
7
- SessionMessageView
7
+ SessionMessageView,
8
+ ThinkingLevel
8
9
  } from '@/api/types';
9
10
 
10
11
  export type SendMessageParams = {
@@ -14,6 +15,7 @@ export type SendMessageParams = {
14
15
  agentId: string;
15
16
  sessionType?: string;
16
17
  model?: string;
18
+ thinkingLevel?: ThinkingLevel;
17
19
  requestedSkills?: string[];
18
20
  stopSupported?: boolean;
19
21
  stopReason?: string;
@@ -7,6 +7,8 @@ import { normalizeSessionType } from '@/components/chat/useChatSessionTypeState'
7
7
  import type { ChatInputSnapshot } from '@/components/chat/stores/chat-input.store';
8
8
  import type { SetStateAction } from 'react';
9
9
  import type { ChatStreamActionsManager } from '@/components/chat/managers/chat-stream-actions.manager';
10
+ import type { ThinkingLevel } from '@/api/types';
11
+ import type { ChatModelOption } from '@/components/chat/chat-input.types';
10
12
 
11
13
  export class ChatInputManager {
12
14
  constructor(
@@ -36,6 +38,14 @@ export class ChatInputManager {
36
38
  return;
37
39
  }
38
40
  useChatInputStore.getState().setSnapshot(patch);
41
+ if (
42
+ Object.prototype.hasOwnProperty.call(patch, 'modelOptions') ||
43
+ Object.prototype.hasOwnProperty.call(patch, 'selectedModel') ||
44
+ Object.prototype.hasOwnProperty.call(patch, 'selectedThinkingLevel')
45
+ ) {
46
+ const snapshot = useChatInputStore.getState().snapshot;
47
+ this.reconcileThinkingForModel(snapshot.selectedModel);
48
+ }
39
49
  };
40
50
 
41
51
  setDraft = (next: SetStateAction<string>) => {
@@ -77,6 +87,7 @@ export class ChatInputManager {
77
87
  agentId: sessionSnapshot.selectedAgentId,
78
88
  sessionType: inputSnapshot.selectedSessionType,
79
89
  model: inputSnapshot.selectedModel || undefined,
90
+ thinkingLevel: inputSnapshot.selectedThinkingLevel ?? undefined,
80
91
  stopSupported: inputSnapshot.stopSupported,
81
92
  stopReason: inputSnapshot.stopReason,
82
93
  requestedSkills,
@@ -99,6 +110,16 @@ export class ChatInputManager {
99
110
  return;
100
111
  }
101
112
  useChatInputStore.getState().setSnapshot({ selectedModel: value });
113
+ this.reconcileThinkingForModel(value);
114
+ };
115
+
116
+ setSelectedThinkingLevel = (next: SetStateAction<ThinkingLevel | null>) => {
117
+ const prev = useChatInputStore.getState().snapshot.selectedThinkingLevel;
118
+ const value = this.resolveUpdateValue(prev, next);
119
+ if (value === prev) {
120
+ return;
121
+ }
122
+ useChatInputStore.getState().setSnapshot({ selectedThinkingLevel: value });
102
123
  };
103
124
 
104
125
  selectSessionType = (value: string) => {
@@ -120,10 +141,40 @@ export class ChatInputManager {
120
141
  this.setSelectedModel(value);
121
142
  };
122
143
 
144
+ selectThinkingLevel = (value: ThinkingLevel) => {
145
+ this.setSelectedThinkingLevel(value);
146
+ };
147
+
123
148
  selectSkills = (next: string[]) => {
124
149
  this.setSelectedSkills(next);
125
150
  };
126
151
 
152
+ private resolveThinkingForModel(modelOption: ChatModelOption | undefined, current: ThinkingLevel | null): ThinkingLevel | null {
153
+ const capability = modelOption?.thinkingCapability;
154
+ if (!capability || capability.supported.length === 0) {
155
+ return null;
156
+ }
157
+ if (current === 'off') {
158
+ return 'off';
159
+ }
160
+ if (current && capability.supported.includes(current)) {
161
+ return current;
162
+ }
163
+ if (capability.default && capability.supported.includes(capability.default)) {
164
+ return capability.default;
165
+ }
166
+ return 'off';
167
+ }
168
+
169
+ private reconcileThinkingForModel(model: string): void {
170
+ const snapshot = useChatInputStore.getState().snapshot;
171
+ const modelOption = snapshot.modelOptions.find((option) => option.value === model);
172
+ const nextThinking = this.resolveThinkingForModel(modelOption, snapshot.selectedThinkingLevel);
173
+ if (nextThinking !== snapshot.selectedThinkingLevel) {
174
+ useChatInputStore.getState().setSnapshot({ selectedThinkingLevel: nextThinking });
175
+ }
176
+ }
177
+
127
178
  private syncRemoteSessionType = async (normalizedType: string) => {
128
179
  const sessionSnapshot = useChatSessionListStore.getState().snapshot;
129
180
  const selectedSessionKey = sessionSnapshot.selectedSessionKey;
@@ -1,5 +1,7 @@
1
1
  import { create } from 'zustand';
2
2
  import type { MarketplaceInstalledRecord } from '@/api/types';
3
+ import type { ThinkingLevel } from '@/api/types';
4
+ import type { ChatModelOption } from '@/components/chat/chat-input.types';
3
5
 
4
6
  export type ChatInputSnapshot = {
5
7
  isProviderStateResolved: boolean;
@@ -10,8 +12,9 @@ export type ChatInputSnapshot = {
10
12
  stopDisabledReason: string | null;
11
13
  sendError: string | null;
12
14
  isSending: boolean;
13
- modelOptions: Array<{ value: string; modelLabel: string; providerLabel: string }>;
15
+ modelOptions: ChatModelOption[];
14
16
  selectedModel: string;
17
+ selectedThinkingLevel: ThinkingLevel | null;
15
18
  sessionTypeOptions: Array<{ value: string; label: string }>;
16
19
  selectedSessionType?: string;
17
20
  stopSupported: boolean;
@@ -39,6 +42,7 @@ const initialSnapshot: ChatInputSnapshot = {
39
42
  isSending: false,
40
43
  modelOptions: [],
41
44
  selectedModel: '',
45
+ selectedThinkingLevel: null,
42
46
  sessionTypeOptions: [],
43
47
  selectedSessionType: undefined,
44
48
  stopSupported: false,
@@ -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"