@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
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-Cn6_2To7.js"></script>
10
- <link rel="modulepreload" crossorigin href="/assets/vendor-TlME1INH.js">
11
- <link rel="stylesheet" crossorigin href="/assets/index-nEYGCJTC.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.11",
3
+ "version": "0.6.13",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/App.tsx CHANGED
@@ -16,6 +16,7 @@ const queryClient = new QueryClient({
16
16
 
17
17
  const ModelConfigPage = lazy(async () => ({ default: (await import('@/components/config/ModelConfig')).ModelConfig }));
18
18
  const ChatPage = lazy(async () => ({ default: (await import('@/components/chat/ChatPage')).ChatPage }));
19
+ const SearchConfigPage = lazy(async () => ({ default: (await import('@/components/config/SearchConfig')).SearchConfig }));
19
20
  const ProvidersListPage = lazy(async () => ({ default: (await import('@/components/config/ProvidersList')).ProvidersList }));
20
21
  const ChannelsListPage = lazy(async () => ({ default: (await import('@/components/config/ChannelsList')).ChannelsList }));
21
22
  const RuntimeConfigPage = lazy(async () => ({ default: (await import('@/components/config/RuntimeConfig')).RuntimeConfig }));
@@ -45,6 +46,7 @@ function AppContent() {
45
46
  <Route path="/skills" element={<LazyRoute><ChatPage view="skills" /></LazyRoute>} />
46
47
  <Route path="/cron" element={<LazyRoute><ChatPage view="cron" /></LazyRoute>} />
47
48
  <Route path="/model" element={<LazyRoute><ModelConfigPage /></LazyRoute>} />
49
+ <Route path="/search" element={<LazyRoute><SearchConfigPage /></LazyRoute>} />
48
50
  <Route path="/providers" element={<LazyRoute><ProvidersListPage /></LazyRoute>} />
49
51
  <Route path="/channels" element={<LazyRoute><ChannelsListPage /></LazyRoute>} />
50
52
  <Route path="/runtime" element={<LazyRoute><RuntimeConfigPage /></LazyRoute>} />
package/src/api/config.ts CHANGED
@@ -14,6 +14,8 @@ import type {
14
14
  ProviderAuthPollRequest,
15
15
  ProviderAuthPollResult,
16
16
  ProviderAuthImportResult,
17
+ SearchConfigUpdate,
18
+ SearchConfigView,
17
19
  ProviderCreateRequest,
18
20
  ProviderCreateResult,
19
21
  ProviderDeleteResult,
@@ -91,6 +93,17 @@ export async function updateModel(data: {
91
93
  return response.data;
92
94
  }
93
95
 
96
+ // PUT /api/config/search
97
+ export async function updateSearch(
98
+ data: SearchConfigUpdate
99
+ ): Promise<SearchConfigView> {
100
+ const response = await api.put<SearchConfigView>('/api/config/search', data);
101
+ if (!response.ok) {
102
+ throw new Error(response.error.message);
103
+ }
104
+ return response.data;
105
+ }
106
+
94
107
  // PUT /api/config/providers/:provider
95
108
  export async function updateProvider(
96
109
  provider: string,
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 & {
@@ -74,6 +78,52 @@ export type ProviderConnectionTestResult = {
74
78
  hint?: string;
75
79
  };
76
80
 
81
+ export type SearchProviderName = "bocha" | "brave";
82
+ export type BochaFreshnessValue = "noLimit" | "oneDay" | "oneWeek" | "oneMonth" | "oneYear" | string;
83
+
84
+ export type SearchProviderConfigView = {
85
+ enabled: boolean;
86
+ apiKeySet: boolean;
87
+ apiKeyMasked?: string;
88
+ baseUrl: string;
89
+ docsUrl?: string;
90
+ summary?: boolean;
91
+ freshness?: BochaFreshnessValue;
92
+ };
93
+
94
+ export type SearchConfigView = {
95
+ provider: SearchProviderName;
96
+ enabledProviders: SearchProviderName[];
97
+ defaults: {
98
+ maxResults: number;
99
+ };
100
+ providers: {
101
+ bocha: SearchProviderConfigView;
102
+ brave: SearchProviderConfigView;
103
+ };
104
+ };
105
+
106
+ export type SearchConfigUpdate = {
107
+ provider?: SearchProviderName;
108
+ enabledProviders?: SearchProviderName[];
109
+ defaults?: {
110
+ maxResults?: number;
111
+ };
112
+ providers?: {
113
+ bocha?: {
114
+ apiKey?: string | null;
115
+ baseUrl?: string | null;
116
+ docsUrl?: string | null;
117
+ summary?: boolean;
118
+ freshness?: BochaFreshnessValue | null;
119
+ };
120
+ brave?: {
121
+ apiKey?: string | null;
122
+ baseUrl?: string | null;
123
+ };
124
+ };
125
+ };
126
+
77
127
  export type ProviderAuthStartResult = {
78
128
  provider: string;
79
129
  kind: "device_code";
@@ -445,6 +495,7 @@ export type ConfigView = {
445
495
  };
446
496
  };
447
497
  providers: Record<string, ProviderConfigView>;
498
+ search: SearchConfigView;
448
499
  channels: Record<string, Record<string, unknown>>;
449
500
  bindings?: AgentBindingView[];
450
501
  session?: SessionConfigView;
@@ -507,8 +558,18 @@ export type ChannelSpecView = {
507
558
  };
508
559
  };
509
560
 
561
+ export type SearchProviderSpecView = {
562
+ name: SearchProviderName;
563
+ displayName: string;
564
+ description: string;
565
+ docsUrl?: string;
566
+ isDefault?: boolean;
567
+ supportsSummary?: boolean;
568
+ };
569
+
510
570
  export type ConfigMetaView = {
511
571
  providers: ProviderSpecView[];
572
+ search: SearchProviderSpecView[];
512
573
  channels: ChannelSpecView[];
513
574
  };
514
575
 
@@ -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,