@nextclaw/ui 0.9.1 → 0.9.3

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 (81) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/assets/ChannelsList-ZBPiF0y2.js +1 -0
  3. package/dist/assets/ChatPage-BOgoolWK.js +38 -0
  4. package/dist/assets/{DocBrowser-LpzGe8An.js → DocBrowser-BUYNHg0Y.js} +1 -1
  5. package/dist/assets/LogoBadge-DXPq99LJ.js +1 -0
  6. package/dist/assets/MarketplacePage-Dx7nexYN.js +49 -0
  7. package/dist/assets/McpMarketplacePage-064wdotP.js +40 -0
  8. package/dist/assets/{ModelConfig-DuImUHIX.js → ModelConfig-BDIfLesG.js} +1 -1
  9. package/dist/assets/ProvidersList-DrlIr46m.js +1 -0
  10. package/dist/assets/RemoteAccessPage-ZkUBA-Av.js +1 -0
  11. package/dist/assets/{RuntimeConfig-C6iqpJR_.js → RuntimeConfig-BPxXEGzM.js} +1 -1
  12. package/dist/assets/{SearchConfig-Dvp1TAXu.js → SearchConfig-BIqnlpne.js} +1 -1
  13. package/dist/assets/{SecretsConfig-D5Ymlvt9.js → SecretsConfig-jKZEVF2q.js} +2 -2
  14. package/dist/assets/{SessionsConfig-CIA_jA1P.js → SessionsConfig-C_FXgVe1.js} +2 -2
  15. package/dist/assets/{chat-message-B60Fh9kI.js → chat-message-DmzpZJc_.js} +1 -1
  16. package/dist/assets/index-Byfw276e.js +8 -0
  17. package/dist/assets/{index-CPDASUXh.js → index-Ct7FQpxN.js} +1 -1
  18. package/dist/assets/index-bhNuQis7.css +1 -0
  19. package/dist/assets/{label-D4fGx6Wb.js → label-B1MloEtn.js} +1 -1
  20. package/dist/assets/marketplace-localization-Dk31LJJJ.js +1 -0
  21. package/dist/assets/{page-layout-twy8gmBE.js → page-layout-BGg1EhM5.js} +1 -1
  22. package/dist/assets/{popover-DYbYpt1j.js → popover-jJMv74Fp.js} +1 -1
  23. package/dist/assets/{security-config-BcIZ4rpb.js → security-config-Boh9NIMz.js} +1 -1
  24. package/dist/assets/skeleton-CmATs_b3.js +1 -0
  25. package/dist/assets/status-dot-DNyCdxPZ.js +1 -0
  26. package/dist/assets/{switch-DqA6r5XR.js → switch-DE_MYk7x.js} +1 -1
  27. package/dist/assets/{tabs-custom-C6enKKs1.js → tabs-custom-B-zErYPr.js} +1 -1
  28. package/dist/assets/{useConfirmDialog-CHBf5Of7.js → useConfirmDialog-BqQ6QfhB.js} +2 -2
  29. package/dist/assets/{vendor-DKBNiC31.js → vendor-CwsIoNvJ.js} +128 -93
  30. package/dist/index.html +3 -3
  31. package/package.json +4 -4
  32. package/src/App.tsx +4 -0
  33. package/src/api/auth.types.ts +24 -0
  34. package/src/api/chat-session-type.types.ts +21 -0
  35. package/src/api/marketplace.ts +8 -2
  36. package/src/api/mcp-marketplace.ts +138 -0
  37. package/src/api/remote.ts +57 -0
  38. package/src/api/remote.types.ts +80 -0
  39. package/src/api/types.ts +91 -37
  40. package/src/components/chat/ChatSidebar.test.tsx +31 -2
  41. package/src/components/chat/ChatSidebar.tsx +26 -2
  42. package/src/components/chat/chat-page-data.ts +37 -53
  43. package/src/components/chat/chat-page-runtime.test.ts +122 -2
  44. package/src/components/chat/chat-page-runtime.ts +1 -118
  45. package/src/components/chat/chat-session-preference-governance.ts +303 -0
  46. package/src/components/chat/legacy/LegacyChatPage.tsx +4 -34
  47. package/src/components/chat/ncp/NcpChatPage.tsx +4 -34
  48. package/src/components/chat/ncp/ncp-chat-page-data.test.ts +36 -0
  49. package/src/components/chat/ncp/ncp-chat-page-data.ts +63 -36
  50. package/src/components/chat/ncp/ncp-session-adapter.test.ts +2 -0
  51. package/src/components/chat/ncp/ncp-session-adapter.ts +2 -0
  52. package/src/components/chat/stores/chat-input.store.ts +14 -1
  53. package/src/components/chat/useChatSessionTypeState.test.tsx +29 -0
  54. package/src/components/chat/useChatSessionTypeState.ts +55 -12
  55. package/src/components/layout/Sidebar.tsx +11 -1
  56. package/src/components/marketplace/MarketplacePage.test.tsx +152 -0
  57. package/src/components/marketplace/MarketplacePage.tsx +52 -199
  58. package/src/components/marketplace/marketplace-installed-cache.test.ts +110 -0
  59. package/src/components/marketplace/marketplace-installed-cache.ts +149 -0
  60. package/src/components/marketplace/marketplace-localization.ts +77 -0
  61. package/src/components/marketplace/marketplace-page-parts.tsx +102 -0
  62. package/src/components/marketplace/mcp/McpMarketplacePage.test.tsx +208 -0
  63. package/src/components/marketplace/mcp/McpMarketplacePage.tsx +578 -0
  64. package/src/components/remote/RemoteAccessPage.tsx +320 -0
  65. package/src/components/ui/input.tsx +1 -1
  66. package/src/components/ui/label.tsx +1 -1
  67. package/src/hooks/useMarketplace.ts +36 -7
  68. package/src/hooks/useMcpMarketplace.ts +99 -0
  69. package/src/hooks/useRemoteAccess.ts +92 -0
  70. package/src/hooks/useWebSocket.ts +25 -16
  71. package/src/lib/i18n.marketplace.ts +91 -0
  72. package/src/lib/i18n.remote.ts +115 -0
  73. package/src/lib/i18n.ts +10 -68
  74. package/dist/assets/ChannelsList-DhvjpZcs.js +0 -1
  75. package/dist/assets/ChatPage-B8VBaMQm.js +0 -38
  76. package/dist/assets/LogoBadge-Be4lktJN.js +0 -1
  77. package/dist/assets/MarketplacePage-Cx9AI3_h.js +0 -49
  78. package/dist/assets/ProvidersList-Ccleg25k.js +0 -1
  79. package/dist/assets/index-BiPDnzv0.js +0 -8
  80. package/dist/assets/index-C8GsgIUn.css +0 -1
  81. package/dist/assets/skeleton-DypBy7jp.js +0 -1
@@ -0,0 +1,303 @@
1
+ import { useEffect, useRef } from 'react';
2
+ import type { Dispatch, SetStateAction } from 'react';
3
+ import type { SessionEntryView, ThinkingLevel } from '@/api/types';
4
+ import type { ChatModelOption } from '@/components/chat/chat-input.types';
5
+
6
+ function normalizeSessionType(value: string | null | undefined): string {
7
+ const normalized = value?.trim().toLowerCase();
8
+ return normalized || 'native';
9
+ }
10
+
11
+ function hasModelOption(modelOptions: ChatModelOption[], value: unknown): value is string {
12
+ if (typeof value !== 'string') {
13
+ return false;
14
+ }
15
+ const normalized = value.trim();
16
+ if (!normalized) {
17
+ return false;
18
+ }
19
+ return modelOptions.some((option) => option.value === normalized);
20
+ }
21
+
22
+ function hasThinkingLevelOption(levels: readonly ThinkingLevel[], value: unknown): value is ThinkingLevel {
23
+ return typeof value === 'string' && levels.includes(value as ThinkingLevel);
24
+ }
25
+
26
+ function resolveFallbackThinkingLevel(levels: readonly ThinkingLevel[]): ThinkingLevel | null {
27
+ if (levels.length === 0) {
28
+ return null;
29
+ }
30
+ if (levels.includes('off')) {
31
+ return 'off';
32
+ }
33
+ return levels[0] ?? null;
34
+ }
35
+
36
+ type ResolveSessionPreferenceValueParams<T> = {
37
+ currentValue: unknown;
38
+ selectedSessionPreferredValue?: unknown;
39
+ fallbackPreferredValue?: unknown;
40
+ defaultValue?: unknown;
41
+ isValueSupported: (value: unknown) => value is T;
42
+ firstAvailableValue: T;
43
+ preferSessionPreferredValue?: boolean;
44
+ preserveCurrentValueOnSessionChange?: boolean;
45
+ };
46
+
47
+ export function resolveSessionPreferenceValue<T>(params: ResolveSessionPreferenceValueParams<T>): T {
48
+ const {
49
+ currentValue,
50
+ selectedSessionPreferredValue,
51
+ fallbackPreferredValue,
52
+ defaultValue,
53
+ isValueSupported,
54
+ firstAvailableValue,
55
+ preferSessionPreferredValue = false,
56
+ preserveCurrentValueOnSessionChange = false
57
+ } = params;
58
+ if (isValueSupported(currentValue) && (!preferSessionPreferredValue || preserveCurrentValueOnSessionChange)) {
59
+ return currentValue;
60
+ }
61
+ if (isValueSupported(selectedSessionPreferredValue)) {
62
+ return selectedSessionPreferredValue;
63
+ }
64
+ if (isValueSupported(fallbackPreferredValue)) {
65
+ return fallbackPreferredValue;
66
+ }
67
+ if (isValueSupported(defaultValue)) {
68
+ return defaultValue;
69
+ }
70
+ return firstAvailableValue;
71
+ }
72
+
73
+ export function resolveSelectedModelValue(params: {
74
+ currentSelectedModel?: string;
75
+ modelOptions: ChatModelOption[];
76
+ selectedSessionPreferredModel?: string;
77
+ fallbackPreferredModel?: string;
78
+ defaultModel?: string;
79
+ preferSessionPreferredModel?: boolean;
80
+ preserveCurrentSelectedModelOnSessionChange?: boolean;
81
+ }): string {
82
+ const { modelOptions } = params;
83
+ if (modelOptions.length === 0) {
84
+ return '';
85
+ }
86
+ return resolveSessionPreferenceValue<string>({
87
+ currentValue: params.currentSelectedModel,
88
+ selectedSessionPreferredValue: params.selectedSessionPreferredModel,
89
+ fallbackPreferredValue: params.fallbackPreferredModel,
90
+ defaultValue: params.defaultModel,
91
+ isValueSupported: (value): value is string => hasModelOption(modelOptions, value),
92
+ firstAvailableValue: modelOptions[0]?.value ?? '',
93
+ preferSessionPreferredValue: params.preferSessionPreferredModel,
94
+ preserveCurrentValueOnSessionChange: params.preserveCurrentSelectedModelOnSessionChange
95
+ });
96
+ }
97
+
98
+ export function resolveSelectedThinkingLevelValue(params: {
99
+ currentSelectedThinkingLevel?: ThinkingLevel | null;
100
+ supportedThinkingLevels: readonly ThinkingLevel[];
101
+ selectedSessionPreferredThinking?: ThinkingLevel | null;
102
+ fallbackPreferredThinking?: ThinkingLevel | null;
103
+ defaultThinkingLevel?: ThinkingLevel | null;
104
+ preferSessionPreferredThinking?: boolean;
105
+ preserveCurrentSelectedThinkingOnSessionChange?: boolean;
106
+ }): ThinkingLevel | null {
107
+ const { supportedThinkingLevels } = params;
108
+ if (supportedThinkingLevels.length === 0) {
109
+ return null;
110
+ }
111
+ return resolveSessionPreferenceValue<ThinkingLevel>({
112
+ currentValue: params.currentSelectedThinkingLevel,
113
+ selectedSessionPreferredValue: params.selectedSessionPreferredThinking,
114
+ fallbackPreferredValue: params.fallbackPreferredThinking,
115
+ defaultValue: params.defaultThinkingLevel,
116
+ isValueSupported: (value): value is ThinkingLevel => hasThinkingLevelOption(supportedThinkingLevels, value),
117
+ firstAvailableValue: resolveFallbackThinkingLevel(supportedThinkingLevels) ?? 'off',
118
+ preferSessionPreferredValue: params.preferSessionPreferredThinking,
119
+ preserveCurrentValueOnSessionChange: params.preserveCurrentSelectedThinkingOnSessionChange
120
+ });
121
+ }
122
+
123
+ export function resolveRecentSessionPreferredValue<T>(params: {
124
+ sessions: readonly SessionEntryView[];
125
+ selectedSessionKey?: string | null;
126
+ sessionType?: string | null;
127
+ readPreference: (session: SessionEntryView) => T | null | undefined;
128
+ }): T | undefined {
129
+ const targetSessionType = normalizeSessionType(params.sessionType);
130
+ let bestValue: T | undefined;
131
+ let bestTimestamp = Number.NEGATIVE_INFINITY;
132
+ for (const session of params.sessions) {
133
+ if (session.key === params.selectedSessionKey) {
134
+ continue;
135
+ }
136
+ if (normalizeSessionType(session.sessionType) !== targetSessionType) {
137
+ continue;
138
+ }
139
+ const value = params.readPreference(session);
140
+ if (value === null || value === undefined) {
141
+ continue;
142
+ }
143
+ const updatedAtTimestamp = Date.parse(session.updatedAt);
144
+ const comparableTimestamp = Number.isFinite(updatedAtTimestamp) ? updatedAtTimestamp : Number.NEGATIVE_INFINITY;
145
+ if (bestValue === undefined || comparableTimestamp > bestTimestamp) {
146
+ bestValue = value;
147
+ bestTimestamp = comparableTimestamp;
148
+ }
149
+ }
150
+ return bestValue;
151
+ }
152
+
153
+ export function resolveRecentSessionPreferredModel(params: {
154
+ sessions: readonly SessionEntryView[];
155
+ selectedSessionKey?: string | null;
156
+ sessionType?: string | null;
157
+ }): string | undefined {
158
+ return resolveRecentSessionPreferredValue<string>({
159
+ sessions: params.sessions,
160
+ selectedSessionKey: params.selectedSessionKey,
161
+ sessionType: params.sessionType,
162
+ readPreference: (session) => {
163
+ const preferredModel = session.preferredModel?.trim();
164
+ return preferredModel || undefined;
165
+ }
166
+ });
167
+ }
168
+
169
+ export function resolveRecentSessionPreferredThinking(params: {
170
+ sessions: readonly SessionEntryView[];
171
+ selectedSessionKey?: string | null;
172
+ sessionType?: string | null;
173
+ }): ThinkingLevel | undefined {
174
+ return resolveRecentSessionPreferredValue<ThinkingLevel>({
175
+ sessions: params.sessions,
176
+ selectedSessionKey: params.selectedSessionKey,
177
+ sessionType: params.sessionType,
178
+ readPreference: (session) => session.preferredThinking ?? undefined
179
+ });
180
+ }
181
+
182
+ type UseSyncSessionPreferenceParams<T> = {
183
+ isPreferenceAvailable: boolean;
184
+ emptyValue: T;
185
+ selectedSessionKey?: string | null;
186
+ selectedSessionExists?: boolean;
187
+ setValue: Dispatch<SetStateAction<T>>;
188
+ resolveValue: (params: {
189
+ currentValue: T;
190
+ sessionChanged: boolean;
191
+ preserveCurrentValueOnSessionChange: boolean;
192
+ }) => T;
193
+ };
194
+
195
+ function useSyncSessionPreference<T>(params: UseSyncSessionPreferenceParams<T>) {
196
+ const {
197
+ isPreferenceAvailable,
198
+ emptyValue,
199
+ selectedSessionKey,
200
+ selectedSessionExists = false,
201
+ setValue,
202
+ resolveValue
203
+ } = params;
204
+ const previousSessionKeyRef = useRef<string | null | undefined>(undefined);
205
+ const resolveValueRef = useRef(resolveValue);
206
+
207
+ useEffect(() => {
208
+ resolveValueRef.current = resolveValue;
209
+ }, [resolveValue]);
210
+
211
+ useEffect(() => {
212
+ const sessionChanged = previousSessionKeyRef.current !== selectedSessionKey;
213
+ if (!isPreferenceAvailable) {
214
+ setValue(emptyValue);
215
+ previousSessionKeyRef.current = selectedSessionKey;
216
+ return;
217
+ }
218
+ setValue((prev) =>
219
+ resolveValueRef.current({
220
+ currentValue: prev,
221
+ sessionChanged,
222
+ preserveCurrentValueOnSessionChange: sessionChanged && Boolean(selectedSessionKey) && !selectedSessionExists
223
+ })
224
+ );
225
+ previousSessionKeyRef.current = selectedSessionKey;
226
+ }, [emptyValue, isPreferenceAvailable, selectedSessionExists, selectedSessionKey, setValue]);
227
+ }
228
+
229
+ export function useSyncSelectedModel(params: {
230
+ modelOptions: ChatModelOption[];
231
+ selectedSessionKey?: string | null;
232
+ selectedSessionExists?: boolean;
233
+ selectedSessionPreferredModel?: string;
234
+ fallbackPreferredModel?: string;
235
+ defaultModel?: string;
236
+ setSelectedModel: Dispatch<SetStateAction<string>>;
237
+ }) {
238
+ const {
239
+ modelOptions,
240
+ selectedSessionKey,
241
+ selectedSessionExists = false,
242
+ selectedSessionPreferredModel,
243
+ fallbackPreferredModel,
244
+ defaultModel,
245
+ setSelectedModel
246
+ } = params;
247
+
248
+ useSyncSessionPreference<string>({
249
+ isPreferenceAvailable: modelOptions.length > 0,
250
+ emptyValue: '',
251
+ selectedSessionKey,
252
+ selectedSessionExists,
253
+ setValue: setSelectedModel,
254
+ resolveValue: ({ currentValue, sessionChanged, preserveCurrentValueOnSessionChange }) =>
255
+ resolveSelectedModelValue({
256
+ currentSelectedModel: currentValue,
257
+ modelOptions,
258
+ selectedSessionPreferredModel,
259
+ fallbackPreferredModel,
260
+ defaultModel,
261
+ preferSessionPreferredModel: sessionChanged,
262
+ preserveCurrentSelectedModelOnSessionChange: preserveCurrentValueOnSessionChange
263
+ })
264
+ });
265
+ }
266
+
267
+ export function useSyncSelectedThinking(params: {
268
+ supportedThinkingLevels: readonly ThinkingLevel[];
269
+ selectedSessionKey?: string | null;
270
+ selectedSessionExists?: boolean;
271
+ selectedSessionPreferredThinking?: ThinkingLevel | null;
272
+ fallbackPreferredThinking?: ThinkingLevel | null;
273
+ defaultThinkingLevel?: ThinkingLevel | null;
274
+ setSelectedThinkingLevel: Dispatch<SetStateAction<ThinkingLevel | null>>;
275
+ }) {
276
+ const {
277
+ supportedThinkingLevels,
278
+ selectedSessionKey,
279
+ selectedSessionExists = false,
280
+ selectedSessionPreferredThinking,
281
+ fallbackPreferredThinking,
282
+ defaultThinkingLevel,
283
+ setSelectedThinkingLevel
284
+ } = params;
285
+
286
+ useSyncSessionPreference<ThinkingLevel | null>({
287
+ isPreferenceAvailable: supportedThinkingLevels.length > 0,
288
+ emptyValue: null,
289
+ selectedSessionKey,
290
+ selectedSessionExists,
291
+ setValue: setSelectedThinkingLevel,
292
+ resolveValue: ({ currentValue, sessionChanged, preserveCurrentValueOnSessionChange }) =>
293
+ resolveSelectedThinkingLevelValue({
294
+ currentSelectedThinkingLevel: currentValue,
295
+ supportedThinkingLevels,
296
+ selectedSessionPreferredThinking,
297
+ fallbackPreferredThinking,
298
+ defaultThinkingLevel,
299
+ preferSessionPreferredThinking: sessionChanged,
300
+ preserveCurrentSelectedThinkingOnSessionChange: preserveCurrentValueOnSessionChange
301
+ })
302
+ });
303
+ }
@@ -18,14 +18,13 @@ export function LegacyChatPage({ view }: ChatPageProps) {
18
18
  const selectedSessionKey = useChatSessionListStore((state) => state.snapshot.selectedSessionKey);
19
19
  const selectedAgentId = useChatSessionListStore((state) => state.snapshot.selectedAgentId);
20
20
  const pendingSessionType = useChatInputStore((state) => state.snapshot.pendingSessionType);
21
+ const currentSelectedModel = useChatInputStore((state) => state.snapshot.selectedModel);
21
22
  const { confirm, ConfirmDialog } = useConfirmDialog();
22
23
  const location = useLocation();
23
24
  const navigate = useNavigate();
24
25
  const { sessionId: routeSessionIdParam } = useParams<{ sessionId?: string }>();
25
26
  const threadRef = useRef<HTMLDivElement | null>(null);
26
27
  const selectedSessionKeyRef = useRef<string | null>(selectedSessionKey);
27
- const modelHydratedSessionKeyRef = useRef<string | null>(null);
28
- const thinkingHydratedSessionKeyRef = useRef<string | null>(null);
29
28
  const routeSessionKey = useMemo(
30
29
  () => parseSessionKeyFromRoute(routeSessionIdParam),
31
30
  [routeSessionIdParam]
@@ -40,9 +39,7 @@ export function LegacyChatPage({ view }: ChatPageProps) {
40
39
  sessions,
41
40
  skillRecords,
42
41
  selectedSession,
43
- hydratedSessionModel,
44
42
  historyMessages,
45
- selectedSessionThinkingLevel,
46
43
  sessionTypeOptions,
47
44
  defaultSessionType,
48
45
  selectedSessionType,
@@ -53,9 +50,11 @@ export function LegacyChatPage({ view }: ChatPageProps) {
53
50
  query,
54
51
  selectedSessionKey,
55
52
  selectedAgentId,
53
+ currentSelectedModel,
56
54
  pendingSessionType,
57
55
  setPendingSessionType: presenter.chatInputManager.setPendingSessionType,
58
- setSelectedModel: presenter.chatInputManager.setSelectedModel
56
+ setSelectedModel: presenter.chatInputManager.setSelectedModel,
57
+ setSelectedThinkingLevel: presenter.chatInputManager.setSelectedThinkingLevel
59
58
  });
60
59
  const {
61
60
  uiMessages,
@@ -140,21 +139,6 @@ export function LegacyChatPage({ view }: ChatPageProps) {
140
139
  }, [presenter, sessionsQuery.refetch]);
141
140
 
142
141
  useEffect(() => {
143
- const shouldHydrateModelFromSession =
144
- !isSending &&
145
- !isAwaitingAssistantOutput &&
146
- !sessionsQuery.isLoading &&
147
- isProviderStateResolved &&
148
- modelOptions.length > 0 &&
149
- selectedSessionKey !== modelHydratedSessionKeyRef.current;
150
- const shouldHydrateThinkingFromHistory =
151
- !isSending &&
152
- !isAwaitingAssistantOutput &&
153
- !historyQuery.isLoading &&
154
- isProviderStateResolved &&
155
- modelOptions.length > 0 &&
156
- selectedSessionKey !== thinkingHydratedSessionKeyRef.current;
157
-
158
142
  presenter.chatInputManager.syncSnapshot({
159
143
  isProviderStateResolved,
160
144
  defaultSessionType,
@@ -165,25 +149,13 @@ export function LegacyChatPage({ view }: ChatPageProps) {
165
149
  sendError: lastSendError,
166
150
  isSending,
167
151
  modelOptions,
168
- ...(shouldHydrateModelFromSession ? { selectedModel: hydratedSessionModel } : {}),
169
152
  sessionTypeOptions,
170
153
  selectedSessionType,
171
- ...(shouldHydrateThinkingFromHistory ? { selectedThinkingLevel: selectedSessionThinkingLevel } : {}),
172
154
  canEditSessionType,
173
155
  sessionTypeUnavailable,
174
156
  skillRecords,
175
157
  isSkillsLoading: installedSkillsQuery.isLoading
176
158
  });
177
- if (shouldHydrateModelFromSession) {
178
- modelHydratedSessionKeyRef.current = selectedSessionKey;
179
- }
180
- if (shouldHydrateThinkingFromHistory) {
181
- thinkingHydratedSessionKeyRef.current = selectedSessionKey;
182
- }
183
- if (!selectedSessionKey) {
184
- modelHydratedSessionKeyRef.current = null;
185
- thinkingHydratedSessionKeyRef.current = null;
186
- }
187
159
  presenter.chatSessionListManager.syncSnapshot({
188
160
  sessions,
189
161
  query,
@@ -221,7 +193,6 @@ export function LegacyChatPage({ view }: ChatPageProps) {
221
193
  historyQuery.isLoading,
222
194
  installedSkillsQuery.isLoading,
223
195
  isAwaitingAssistantOutput,
224
- hydratedSessionModel,
225
196
  isProviderStateResolved,
226
197
  isSending,
227
198
  lastSendError,
@@ -231,7 +202,6 @@ export function LegacyChatPage({ view }: ChatPageProps) {
231
202
  query,
232
203
  selectedSession,
233
204
  selectedSessionKey,
234
- selectedSessionThinkingLevel,
235
205
  selectedSessionType,
236
206
  sessionRunStatusByKey,
237
207
  sessionTypeOptions,
@@ -65,14 +65,13 @@ export function NcpChatPage({ view }: ChatPageProps) {
65
65
  const selectedSessionKey = useChatSessionListStore((state) => state.snapshot.selectedSessionKey);
66
66
  const selectedAgentId = useChatSessionListStore((state) => state.snapshot.selectedAgentId);
67
67
  const pendingSessionType = useChatInputStore((state) => state.snapshot.pendingSessionType);
68
+ const currentSelectedModel = useChatInputStore((state) => state.snapshot.selectedModel);
68
69
  const { confirm, ConfirmDialog } = useConfirmDialog();
69
70
  const location = useLocation();
70
71
  const navigate = useNavigate();
71
72
  const { sessionId: routeSessionIdParam } = useParams<{ sessionId?: string }>();
72
73
  const threadRef = useRef<HTMLDivElement | null>(null);
73
74
  const selectedSessionKeyRef = useRef<string | null>(selectedSessionKey);
74
- const modelHydratedSessionKeyRef = useRef<string | null>(null);
75
- const thinkingHydratedSessionKeyRef = useRef<string | null>(null);
76
75
  const routeSessionKey = useMemo(
77
76
  () => parseSessionKeyFromRoute(routeSessionIdParam),
78
77
  [routeSessionIdParam]
@@ -86,8 +85,6 @@ export function NcpChatPage({ view }: ChatPageProps) {
86
85
  sessions,
87
86
  skillRecords,
88
87
  selectedSession,
89
- hydratedSessionModel,
90
- selectedSessionThinkingLevel,
91
88
  sessionTypeOptions,
92
89
  defaultSessionType,
93
90
  selectedSessionType,
@@ -97,9 +94,11 @@ export function NcpChatPage({ view }: ChatPageProps) {
97
94
  } = useNcpChatPageData({
98
95
  query,
99
96
  selectedSessionKey,
97
+ currentSelectedModel,
100
98
  pendingSessionType,
101
99
  setPendingSessionType: presenter.chatInputManager.setPendingSessionType,
102
- setSelectedModel: presenter.chatInputManager.setSelectedModel
100
+ setSelectedModel: presenter.chatInputManager.setSelectedModel,
101
+ setSelectedThinkingLevel: presenter.chatInputManager.setSelectedThinkingLevel
103
102
  });
104
103
  const refetchSessions = sessionsQuery.refetch;
105
104
 
@@ -278,21 +277,6 @@ export function NcpChatPage({ view }: ChatPageProps) {
278
277
  }, [presenter, sessionsQuery.refetch]);
279
278
 
280
279
  useEffect(() => {
281
- const shouldHydrateModelFromSession =
282
- !isSending &&
283
- !isAwaitingAssistantOutput &&
284
- !sessionsQuery.isLoading &&
285
- isProviderStateResolved &&
286
- modelOptions.length > 0 &&
287
- selectedSessionKey !== modelHydratedSessionKeyRef.current;
288
- const shouldHydrateThinkingFromSession =
289
- !isSending &&
290
- !isAwaitingAssistantOutput &&
291
- !agent.isHydrating &&
292
- isProviderStateResolved &&
293
- modelOptions.length > 0 &&
294
- selectedSessionKey !== thinkingHydratedSessionKeyRef.current;
295
-
296
280
  presenter.chatInputManager.syncSnapshot({
297
281
  isProviderStateResolved,
298
282
  defaultSessionType,
@@ -303,25 +287,13 @@ export function NcpChatPage({ view }: ChatPageProps) {
303
287
  sendError: lastSendError,
304
288
  isSending,
305
289
  modelOptions,
306
- ...(shouldHydrateModelFromSession ? { selectedModel: hydratedSessionModel } : {}),
307
290
  sessionTypeOptions,
308
291
  selectedSessionType,
309
- ...(shouldHydrateThinkingFromSession ? { selectedThinkingLevel: selectedSessionThinkingLevel } : {}),
310
292
  canEditSessionType,
311
293
  sessionTypeUnavailable,
312
294
  skillRecords,
313
295
  isSkillsLoading: installedSkillsQuery.isLoading
314
296
  });
315
- if (shouldHydrateModelFromSession) {
316
- modelHydratedSessionKeyRef.current = selectedSessionKey;
317
- }
318
- if (shouldHydrateThinkingFromSession) {
319
- thinkingHydratedSessionKeyRef.current = selectedSessionKey;
320
- }
321
- if (!selectedSessionKey) {
322
- modelHydratedSessionKeyRef.current = null;
323
- thinkingHydratedSessionKeyRef.current = null;
324
- }
325
297
  presenter.chatSessionListManager.syncSnapshot({
326
298
  sessions,
327
299
  query,
@@ -357,7 +329,6 @@ export function NcpChatPage({ view }: ChatPageProps) {
357
329
  defaultSessionType,
358
330
  installedSkillsQuery.isLoading,
359
331
  isAwaitingAssistantOutput,
360
- hydratedSessionModel,
361
332
  isProviderStateResolved,
362
333
  isSending,
363
334
  lastSendError,
@@ -367,7 +338,6 @@ export function NcpChatPage({ view }: ChatPageProps) {
367
338
  query,
368
339
  selectedSession,
369
340
  selectedSessionKey,
370
- selectedSessionThinkingLevel,
371
341
  selectedSessionType,
372
342
  sessionRunStatusByKey,
373
343
  sessionTypeOptions,
@@ -0,0 +1,36 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { filterModelOptionsBySessionType } from '@/components/chat/ncp/ncp-chat-page-data';
3
+ import type { ChatModelOption } from '@/components/chat/chat-input.types';
4
+
5
+ const modelOptions: ChatModelOption[] = [
6
+ {
7
+ value: 'dashscope/qwen3-coder-next',
8
+ modelLabel: 'qwen3-coder-next',
9
+ providerLabel: 'DashScope'
10
+ },
11
+ {
12
+ value: 'anthropic/claude-sonnet-4-5',
13
+ modelLabel: 'claude-sonnet-4-5',
14
+ providerLabel: 'Anthropic'
15
+ }
16
+ ];
17
+
18
+ describe('filterModelOptionsBySessionType', () => {
19
+ it('keeps only session-type-supported models when the runtime publishes a filtered list', () => {
20
+ expect(
21
+ filterModelOptionsBySessionType({
22
+ modelOptions,
23
+ supportedModels: ['dashscope/qwen3-coder-next']
24
+ })
25
+ ).toEqual([modelOptions[0]]);
26
+ });
27
+
28
+ it('falls back to the full model catalog when the advertised models do not match the current catalog', () => {
29
+ expect(
30
+ filterModelOptionsBySessionType({
31
+ modelOptions,
32
+ supportedModels: ['unknown/model']
33
+ })
34
+ ).toEqual(modelOptions);
35
+ });
36
+ });