@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.
- package/CHANGELOG.md +12 -0
- package/dist/assets/ChannelsList-ZBPiF0y2.js +1 -0
- package/dist/assets/ChatPage-BOgoolWK.js +38 -0
- package/dist/assets/{DocBrowser-LpzGe8An.js → DocBrowser-BUYNHg0Y.js} +1 -1
- package/dist/assets/LogoBadge-DXPq99LJ.js +1 -0
- package/dist/assets/MarketplacePage-Dx7nexYN.js +49 -0
- package/dist/assets/McpMarketplacePage-064wdotP.js +40 -0
- package/dist/assets/{ModelConfig-DuImUHIX.js → ModelConfig-BDIfLesG.js} +1 -1
- package/dist/assets/ProvidersList-DrlIr46m.js +1 -0
- package/dist/assets/RemoteAccessPage-ZkUBA-Av.js +1 -0
- package/dist/assets/{RuntimeConfig-C6iqpJR_.js → RuntimeConfig-BPxXEGzM.js} +1 -1
- package/dist/assets/{SearchConfig-Dvp1TAXu.js → SearchConfig-BIqnlpne.js} +1 -1
- package/dist/assets/{SecretsConfig-D5Ymlvt9.js → SecretsConfig-jKZEVF2q.js} +2 -2
- package/dist/assets/{SessionsConfig-CIA_jA1P.js → SessionsConfig-C_FXgVe1.js} +2 -2
- package/dist/assets/{chat-message-B60Fh9kI.js → chat-message-DmzpZJc_.js} +1 -1
- package/dist/assets/index-Byfw276e.js +8 -0
- package/dist/assets/{index-CPDASUXh.js → index-Ct7FQpxN.js} +1 -1
- package/dist/assets/index-bhNuQis7.css +1 -0
- package/dist/assets/{label-D4fGx6Wb.js → label-B1MloEtn.js} +1 -1
- package/dist/assets/marketplace-localization-Dk31LJJJ.js +1 -0
- package/dist/assets/{page-layout-twy8gmBE.js → page-layout-BGg1EhM5.js} +1 -1
- package/dist/assets/{popover-DYbYpt1j.js → popover-jJMv74Fp.js} +1 -1
- package/dist/assets/{security-config-BcIZ4rpb.js → security-config-Boh9NIMz.js} +1 -1
- package/dist/assets/skeleton-CmATs_b3.js +1 -0
- package/dist/assets/status-dot-DNyCdxPZ.js +1 -0
- package/dist/assets/{switch-DqA6r5XR.js → switch-DE_MYk7x.js} +1 -1
- package/dist/assets/{tabs-custom-C6enKKs1.js → tabs-custom-B-zErYPr.js} +1 -1
- package/dist/assets/{useConfirmDialog-CHBf5Of7.js → useConfirmDialog-BqQ6QfhB.js} +2 -2
- package/dist/assets/{vendor-DKBNiC31.js → vendor-CwsIoNvJ.js} +128 -93
- package/dist/index.html +3 -3
- package/package.json +4 -4
- package/src/App.tsx +4 -0
- package/src/api/auth.types.ts +24 -0
- package/src/api/chat-session-type.types.ts +21 -0
- package/src/api/marketplace.ts +8 -2
- package/src/api/mcp-marketplace.ts +138 -0
- package/src/api/remote.ts +57 -0
- package/src/api/remote.types.ts +80 -0
- package/src/api/types.ts +91 -37
- package/src/components/chat/ChatSidebar.test.tsx +31 -2
- package/src/components/chat/ChatSidebar.tsx +26 -2
- package/src/components/chat/chat-page-data.ts +37 -53
- package/src/components/chat/chat-page-runtime.test.ts +122 -2
- package/src/components/chat/chat-page-runtime.ts +1 -118
- package/src/components/chat/chat-session-preference-governance.ts +303 -0
- package/src/components/chat/legacy/LegacyChatPage.tsx +4 -34
- package/src/components/chat/ncp/NcpChatPage.tsx +4 -34
- package/src/components/chat/ncp/ncp-chat-page-data.test.ts +36 -0
- package/src/components/chat/ncp/ncp-chat-page-data.ts +63 -36
- package/src/components/chat/ncp/ncp-session-adapter.test.ts +2 -0
- package/src/components/chat/ncp/ncp-session-adapter.ts +2 -0
- package/src/components/chat/stores/chat-input.store.ts +14 -1
- package/src/components/chat/useChatSessionTypeState.test.tsx +29 -0
- package/src/components/chat/useChatSessionTypeState.ts +55 -12
- package/src/components/layout/Sidebar.tsx +11 -1
- package/src/components/marketplace/MarketplacePage.test.tsx +152 -0
- package/src/components/marketplace/MarketplacePage.tsx +52 -199
- package/src/components/marketplace/marketplace-installed-cache.test.ts +110 -0
- package/src/components/marketplace/marketplace-installed-cache.ts +149 -0
- package/src/components/marketplace/marketplace-localization.ts +77 -0
- package/src/components/marketplace/marketplace-page-parts.tsx +102 -0
- package/src/components/marketplace/mcp/McpMarketplacePage.test.tsx +208 -0
- package/src/components/marketplace/mcp/McpMarketplacePage.tsx +578 -0
- package/src/components/remote/RemoteAccessPage.tsx +320 -0
- package/src/components/ui/input.tsx +1 -1
- package/src/components/ui/label.tsx +1 -1
- package/src/hooks/useMarketplace.ts +36 -7
- package/src/hooks/useMcpMarketplace.ts +99 -0
- package/src/hooks/useRemoteAccess.ts +92 -0
- package/src/hooks/useWebSocket.ts +25 -16
- package/src/lib/i18n.marketplace.ts +91 -0
- package/src/lib/i18n.remote.ts +115 -0
- package/src/lib/i18n.ts +10 -68
- package/dist/assets/ChannelsList-DhvjpZcs.js +0 -1
- package/dist/assets/ChatPage-B8VBaMQm.js +0 -38
- package/dist/assets/LogoBadge-Be4lktJN.js +0 -1
- package/dist/assets/MarketplacePage-Cx9AI3_h.js +0 -49
- package/dist/assets/ProvidersList-Ccleg25k.js +0 -1
- package/dist/assets/index-BiPDnzv0.js +0 -8
- package/dist/assets/index-C8GsgIUn.css +0 -1
- 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
|
+
});
|