@nextclaw/ui 0.9.2 → 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 +6 -0
- package/dist/assets/ChannelsList-ZBPiF0y2.js +1 -0
- package/dist/assets/ChatPage-BOgoolWK.js +38 -0
- package/dist/assets/{DocBrowser-CVwUDJMO.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-CsX-_fyy.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-CX2TGEG1.js → RuntimeConfig-BPxXEGzM.js} +1 -1
- package/dist/assets/{SearchConfig-C-WBTcWi.js → SearchConfig-BIqnlpne.js} +1 -1
- package/dist/assets/{SecretsConfig-9kbR0ZCB.js → SecretsConfig-jKZEVF2q.js} +2 -2
- package/dist/assets/{SessionsConfig-Bohn3P1q.js → SessionsConfig-C_FXgVe1.js} +2 -2
- package/dist/assets/{chat-message-AWIcksDK.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-DD61y-4v.js → label-B1MloEtn.js} +1 -1
- package/dist/assets/marketplace-localization-Dk31LJJJ.js +1 -0
- package/dist/assets/{page-layout-CfnoVycc.js → page-layout-BGg1EhM5.js} +1 -1
- package/dist/assets/{popover-DsugZ6rp.js → popover-jJMv74Fp.js} +1 -1
- package/dist/assets/{security-config-DIrf2Z0O.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-NX5OmUXQ.js → switch-DE_MYk7x.js} +1 -1
- package/dist/assets/{tabs-custom-9ihB5Jem.js → tabs-custom-B-zErYPr.js} +1 -1
- package/dist/assets/{useConfirmDialog-BuQnVTeR.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 +28 -34
- 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 +36 -38
- package/src/components/chat/chat-page-runtime.test.ts +96 -2
- package/src/components/chat/chat-page-runtime.ts +1 -135
- package/src/components/chat/chat-session-preference-governance.ts +303 -0
- package/src/components/chat/legacy/LegacyChatPage.tsx +4 -19
- package/src/components/chat/ncp/NcpChatPage.tsx +4 -19
- package/src/components/chat/ncp/ncp-chat-page-data.test.ts +36 -0
- package/src/components/chat/ncp/ncp-chat-page-data.ts +62 -21
- 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-DKD6Llid.js +0 -1
- package/dist/assets/ChatPage-BK9X4Tin.js +0 -38
- package/dist/assets/LogoBadge-CYQ_b7jk.js +0 -1
- package/dist/assets/MarketplacePage-B_2z3ii_.js +0 -49
- package/dist/assets/ProvidersList-CZstsyv7.js +0 -1
- package/dist/assets/index-BEgClaDH.js +0 -8
- package/dist/assets/index-C8GsgIUn.css +0 -1
- package/dist/assets/skeleton-DJ-Wen2o.js +0 -1
|
@@ -18,13 +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 thinkingHydratedSessionKeyRef = useRef<string | null>(null);
|
|
28
28
|
const routeSessionKey = useMemo(
|
|
29
29
|
() => parseSessionKeyFromRoute(routeSessionIdParam),
|
|
30
30
|
[routeSessionIdParam]
|
|
@@ -40,7 +40,6 @@ export function LegacyChatPage({ view }: ChatPageProps) {
|
|
|
40
40
|
skillRecords,
|
|
41
41
|
selectedSession,
|
|
42
42
|
historyMessages,
|
|
43
|
-
selectedSessionThinkingLevel,
|
|
44
43
|
sessionTypeOptions,
|
|
45
44
|
defaultSessionType,
|
|
46
45
|
selectedSessionType,
|
|
@@ -51,9 +50,11 @@ export function LegacyChatPage({ view }: ChatPageProps) {
|
|
|
51
50
|
query,
|
|
52
51
|
selectedSessionKey,
|
|
53
52
|
selectedAgentId,
|
|
53
|
+
currentSelectedModel,
|
|
54
54
|
pendingSessionType,
|
|
55
55
|
setPendingSessionType: presenter.chatInputManager.setPendingSessionType,
|
|
56
|
-
setSelectedModel: presenter.chatInputManager.setSelectedModel
|
|
56
|
+
setSelectedModel: presenter.chatInputManager.setSelectedModel,
|
|
57
|
+
setSelectedThinkingLevel: presenter.chatInputManager.setSelectedThinkingLevel
|
|
57
58
|
});
|
|
58
59
|
const {
|
|
59
60
|
uiMessages,
|
|
@@ -138,14 +139,6 @@ export function LegacyChatPage({ view }: ChatPageProps) {
|
|
|
138
139
|
}, [presenter, sessionsQuery.refetch]);
|
|
139
140
|
|
|
140
141
|
useEffect(() => {
|
|
141
|
-
const shouldHydrateThinkingFromHistory =
|
|
142
|
-
!isSending &&
|
|
143
|
-
!isAwaitingAssistantOutput &&
|
|
144
|
-
!historyQuery.isLoading &&
|
|
145
|
-
isProviderStateResolved &&
|
|
146
|
-
modelOptions.length > 0 &&
|
|
147
|
-
selectedSessionKey !== thinkingHydratedSessionKeyRef.current;
|
|
148
|
-
|
|
149
142
|
presenter.chatInputManager.syncSnapshot({
|
|
150
143
|
isProviderStateResolved,
|
|
151
144
|
defaultSessionType,
|
|
@@ -158,18 +151,11 @@ export function LegacyChatPage({ view }: ChatPageProps) {
|
|
|
158
151
|
modelOptions,
|
|
159
152
|
sessionTypeOptions,
|
|
160
153
|
selectedSessionType,
|
|
161
|
-
...(shouldHydrateThinkingFromHistory ? { selectedThinkingLevel: selectedSessionThinkingLevel } : {}),
|
|
162
154
|
canEditSessionType,
|
|
163
155
|
sessionTypeUnavailable,
|
|
164
156
|
skillRecords,
|
|
165
157
|
isSkillsLoading: installedSkillsQuery.isLoading
|
|
166
158
|
});
|
|
167
|
-
if (shouldHydrateThinkingFromHistory) {
|
|
168
|
-
thinkingHydratedSessionKeyRef.current = selectedSessionKey;
|
|
169
|
-
}
|
|
170
|
-
if (!selectedSessionKey) {
|
|
171
|
-
thinkingHydratedSessionKeyRef.current = null;
|
|
172
|
-
}
|
|
173
159
|
presenter.chatSessionListManager.syncSnapshot({
|
|
174
160
|
sessions,
|
|
175
161
|
query,
|
|
@@ -216,7 +202,6 @@ export function LegacyChatPage({ view }: ChatPageProps) {
|
|
|
216
202
|
query,
|
|
217
203
|
selectedSession,
|
|
218
204
|
selectedSessionKey,
|
|
219
|
-
selectedSessionThinkingLevel,
|
|
220
205
|
selectedSessionType,
|
|
221
206
|
sessionRunStatusByKey,
|
|
222
207
|
sessionTypeOptions,
|
|
@@ -65,13 +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 thinkingHydratedSessionKeyRef = useRef<string | null>(null);
|
|
75
75
|
const routeSessionKey = useMemo(
|
|
76
76
|
() => parseSessionKeyFromRoute(routeSessionIdParam),
|
|
77
77
|
[routeSessionIdParam]
|
|
@@ -85,7 +85,6 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
85
85
|
sessions,
|
|
86
86
|
skillRecords,
|
|
87
87
|
selectedSession,
|
|
88
|
-
selectedSessionThinkingLevel,
|
|
89
88
|
sessionTypeOptions,
|
|
90
89
|
defaultSessionType,
|
|
91
90
|
selectedSessionType,
|
|
@@ -95,9 +94,11 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
95
94
|
} = useNcpChatPageData({
|
|
96
95
|
query,
|
|
97
96
|
selectedSessionKey,
|
|
97
|
+
currentSelectedModel,
|
|
98
98
|
pendingSessionType,
|
|
99
99
|
setPendingSessionType: presenter.chatInputManager.setPendingSessionType,
|
|
100
|
-
setSelectedModel: presenter.chatInputManager.setSelectedModel
|
|
100
|
+
setSelectedModel: presenter.chatInputManager.setSelectedModel,
|
|
101
|
+
setSelectedThinkingLevel: presenter.chatInputManager.setSelectedThinkingLevel
|
|
101
102
|
});
|
|
102
103
|
const refetchSessions = sessionsQuery.refetch;
|
|
103
104
|
|
|
@@ -276,14 +277,6 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
276
277
|
}, [presenter, sessionsQuery.refetch]);
|
|
277
278
|
|
|
278
279
|
useEffect(() => {
|
|
279
|
-
const shouldHydrateThinkingFromSession =
|
|
280
|
-
!isSending &&
|
|
281
|
-
!isAwaitingAssistantOutput &&
|
|
282
|
-
!agent.isHydrating &&
|
|
283
|
-
isProviderStateResolved &&
|
|
284
|
-
modelOptions.length > 0 &&
|
|
285
|
-
selectedSessionKey !== thinkingHydratedSessionKeyRef.current;
|
|
286
|
-
|
|
287
280
|
presenter.chatInputManager.syncSnapshot({
|
|
288
281
|
isProviderStateResolved,
|
|
289
282
|
defaultSessionType,
|
|
@@ -296,18 +289,11 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
296
289
|
modelOptions,
|
|
297
290
|
sessionTypeOptions,
|
|
298
291
|
selectedSessionType,
|
|
299
|
-
...(shouldHydrateThinkingFromSession ? { selectedThinkingLevel: selectedSessionThinkingLevel } : {}),
|
|
300
292
|
canEditSessionType,
|
|
301
293
|
sessionTypeUnavailable,
|
|
302
294
|
skillRecords,
|
|
303
295
|
isSkillsLoading: installedSkillsQuery.isLoading
|
|
304
296
|
});
|
|
305
|
-
if (shouldHydrateThinkingFromSession) {
|
|
306
|
-
thinkingHydratedSessionKeyRef.current = selectedSessionKey;
|
|
307
|
-
}
|
|
308
|
-
if (!selectedSessionKey) {
|
|
309
|
-
thinkingHydratedSessionKeyRef.current = null;
|
|
310
|
-
}
|
|
311
297
|
presenter.chatSessionListManager.syncSnapshot({
|
|
312
298
|
sessions,
|
|
313
299
|
query,
|
|
@@ -352,7 +338,6 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
352
338
|
query,
|
|
353
339
|
selectedSession,
|
|
354
340
|
selectedSessionKey,
|
|
355
|
-
selectedSessionThinkingLevel,
|
|
356
341
|
selectedSessionType,
|
|
357
342
|
sessionRunStatusByKey,
|
|
358
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
|
+
});
|
|
@@ -1,16 +1,15 @@
|
|
|
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
|
-
import {
|
|
6
|
-
adaptNcpSessionSummaries,
|
|
7
|
-
readNcpSessionPreferredThinking
|
|
8
|
-
} from '@/components/chat/ncp/ncp-session-adapter';
|
|
5
|
+
import { adaptNcpSessionSummaries } from '@/components/chat/ncp/ncp-session-adapter';
|
|
9
6
|
import { useChatSessionTypeState } from '@/components/chat/useChatSessionTypeState';
|
|
10
7
|
import {
|
|
8
|
+
resolveRecentSessionPreferredThinking,
|
|
11
9
|
resolveRecentSessionPreferredModel,
|
|
12
|
-
useSyncSelectedModel
|
|
13
|
-
|
|
10
|
+
useSyncSelectedModel,
|
|
11
|
+
useSyncSelectedThinking
|
|
12
|
+
} from '@/components/chat/chat-session-preference-governance';
|
|
14
13
|
import {
|
|
15
14
|
useConfig,
|
|
16
15
|
useConfigMeta,
|
|
@@ -23,9 +22,11 @@ import { buildProviderModelCatalog, composeProviderModel, resolveModelThinkingCa
|
|
|
23
22
|
type UseNcpChatPageDataParams = {
|
|
24
23
|
query: string;
|
|
25
24
|
selectedSessionKey: string | null;
|
|
25
|
+
currentSelectedModel: string;
|
|
26
26
|
pendingSessionType: string;
|
|
27
27
|
setPendingSessionType: Dispatch<SetStateAction<string>>;
|
|
28
28
|
setSelectedModel: Dispatch<SetStateAction<string>>;
|
|
29
|
+
setSelectedThinkingLevel: Dispatch<SetStateAction<ThinkingLevel | null>>;
|
|
29
30
|
};
|
|
30
31
|
|
|
31
32
|
function filterSessionsByQuery(sessions: SessionEntryView[], query: string): SessionEntryView[] {
|
|
@@ -36,6 +37,18 @@ function filterSessionsByQuery(sessions: SessionEntryView[], query: string): Ses
|
|
|
36
37
|
return sessions.filter((session) => session.key.toLowerCase().includes(normalizedQuery));
|
|
37
38
|
}
|
|
38
39
|
|
|
40
|
+
export function filterModelOptionsBySessionType(params: {
|
|
41
|
+
modelOptions: ChatModelOption[];
|
|
42
|
+
supportedModels?: string[];
|
|
43
|
+
}): ChatModelOption[] {
|
|
44
|
+
if (!params.supportedModels || params.supportedModels.length === 0) {
|
|
45
|
+
return params.modelOptions;
|
|
46
|
+
}
|
|
47
|
+
const supportedModelSet = new Set(params.supportedModels);
|
|
48
|
+
const filtered = params.modelOptions.filter((option) => supportedModelSet.has(option.value));
|
|
49
|
+
return filtered.length > 0 ? filtered : params.modelOptions;
|
|
50
|
+
}
|
|
51
|
+
|
|
39
52
|
export function useNcpChatPageData(params: UseNcpChatPageDataParams) {
|
|
40
53
|
const configQuery = useConfig();
|
|
41
54
|
const configMetaQuery = useConfigMeta();
|
|
@@ -94,19 +107,10 @@ export function useNcpChatPageData(params: UseNcpChatPageDataParams) {
|
|
|
94
107
|
() => allSessions.find((session) => session.key === params.selectedSessionKey) ?? null,
|
|
95
108
|
[allSessions, params.selectedSessionKey]
|
|
96
109
|
);
|
|
97
|
-
const selectedSessionSummary = useMemo(
|
|
98
|
-
() => sessionSummaries.find((session) => session.sessionId === params.selectedSessionKey) ?? null,
|
|
99
|
-
[params.selectedSessionKey, sessionSummaries]
|
|
100
|
-
);
|
|
101
110
|
const skillRecords = useMemo(
|
|
102
111
|
() => installedSkillsQuery.data?.records ?? [],
|
|
103
112
|
[installedSkillsQuery.data?.records]
|
|
104
113
|
);
|
|
105
|
-
const selectedSessionThinkingLevel = useMemo(
|
|
106
|
-
() => (selectedSessionSummary ? readNcpSessionPreferredThinking(selectedSessionSummary) : null),
|
|
107
|
-
[selectedSessionSummary]
|
|
108
|
-
);
|
|
109
|
-
|
|
110
114
|
const sessionTypeState = useChatSessionTypeState({
|
|
111
115
|
selectedSession,
|
|
112
116
|
selectedSessionKey: params.selectedSessionKey,
|
|
@@ -114,6 +118,14 @@ export function useNcpChatPageData(params: UseNcpChatPageDataParams) {
|
|
|
114
118
|
setPendingSessionType: params.setPendingSessionType,
|
|
115
119
|
sessionTypesData: sessionTypesQuery.data
|
|
116
120
|
});
|
|
121
|
+
const filteredModelOptions = useMemo(
|
|
122
|
+
() =>
|
|
123
|
+
filterModelOptionsBySessionType({
|
|
124
|
+
modelOptions,
|
|
125
|
+
supportedModels: sessionTypeState.selectedSessionTypeOption?.supportedModels
|
|
126
|
+
}),
|
|
127
|
+
[modelOptions, sessionTypeState.selectedSessionTypeOption?.supportedModels]
|
|
128
|
+
);
|
|
117
129
|
const recentSessionPreferredModel = useMemo(
|
|
118
130
|
() =>
|
|
119
131
|
resolveRecentSessionPreferredModel({
|
|
@@ -123,16 +135,46 @@ export function useNcpChatPageData(params: UseNcpChatPageDataParams) {
|
|
|
123
135
|
}),
|
|
124
136
|
[allSessions, params.selectedSessionKey, sessionTypeState.selectedSessionType]
|
|
125
137
|
);
|
|
138
|
+
const currentModelOption = useMemo(
|
|
139
|
+
() => filteredModelOptions.find((option) => option.value === params.currentSelectedModel),
|
|
140
|
+
[filteredModelOptions, params.currentSelectedModel]
|
|
141
|
+
);
|
|
142
|
+
const supportedThinkingLevels = useMemo(
|
|
143
|
+
() => (currentModelOption?.thinkingCapability?.supported as ThinkingLevel[] | undefined) ?? [],
|
|
144
|
+
[currentModelOption?.thinkingCapability?.supported]
|
|
145
|
+
);
|
|
146
|
+
const defaultThinkingLevel = useMemo(
|
|
147
|
+
() => (currentModelOption?.thinkingCapability?.default as ThinkingLevel | null | undefined) ?? null,
|
|
148
|
+
[currentModelOption?.thinkingCapability?.default]
|
|
149
|
+
);
|
|
150
|
+
const recentSessionPreferredThinking = useMemo(
|
|
151
|
+
() =>
|
|
152
|
+
resolveRecentSessionPreferredThinking({
|
|
153
|
+
sessions: allSessions,
|
|
154
|
+
selectedSessionKey: params.selectedSessionKey,
|
|
155
|
+
sessionType: sessionTypeState.selectedSessionType
|
|
156
|
+
}),
|
|
157
|
+
[allSessions, params.selectedSessionKey, sessionTypeState.selectedSessionType]
|
|
158
|
+
);
|
|
126
159
|
|
|
127
160
|
useSyncSelectedModel({
|
|
128
|
-
modelOptions,
|
|
161
|
+
modelOptions: filteredModelOptions,
|
|
129
162
|
selectedSessionKey: params.selectedSessionKey,
|
|
130
163
|
selectedSessionExists: Boolean(selectedSession),
|
|
131
164
|
selectedSessionPreferredModel: selectedSession?.preferredModel,
|
|
132
|
-
fallbackPreferredModel: recentSessionPreferredModel,
|
|
133
|
-
defaultModel: configQuery.data?.agents.defaults.model,
|
|
165
|
+
fallbackPreferredModel: sessionTypeState.selectedSessionTypeOption?.recommendedModel ?? recentSessionPreferredModel,
|
|
166
|
+
defaultModel: sessionTypeState.selectedSessionTypeOption?.recommendedModel ?? configQuery.data?.agents.defaults.model,
|
|
134
167
|
setSelectedModel: params.setSelectedModel
|
|
135
168
|
});
|
|
169
|
+
useSyncSelectedThinking({
|
|
170
|
+
supportedThinkingLevels,
|
|
171
|
+
selectedSessionKey: params.selectedSessionKey,
|
|
172
|
+
selectedSessionExists: Boolean(selectedSession),
|
|
173
|
+
selectedSessionPreferredThinking: selectedSession?.preferredThinking ?? null,
|
|
174
|
+
fallbackPreferredThinking: recentSessionPreferredThinking ?? null,
|
|
175
|
+
defaultThinkingLevel,
|
|
176
|
+
setSelectedThinkingLevel: params.setSelectedThinkingLevel
|
|
177
|
+
});
|
|
136
178
|
|
|
137
179
|
return {
|
|
138
180
|
configQuery,
|
|
@@ -141,12 +183,11 @@ export function useNcpChatPageData(params: UseNcpChatPageDataParams) {
|
|
|
141
183
|
sessionTypesQuery,
|
|
142
184
|
installedSkillsQuery,
|
|
143
185
|
isProviderStateResolved,
|
|
144
|
-
modelOptions,
|
|
186
|
+
modelOptions: filteredModelOptions,
|
|
145
187
|
sessionSummaries,
|
|
146
188
|
sessions,
|
|
147
189
|
skillRecords,
|
|
148
190
|
selectedSession,
|
|
149
|
-
selectedSessionThinkingLevel,
|
|
150
191
|
...sessionTypeState
|
|
151
192
|
};
|
|
152
193
|
}
|
|
@@ -22,6 +22,7 @@ describe('adaptNcpSessionSummary', () => {
|
|
|
22
22
|
metadata: {
|
|
23
23
|
label: 'NCP Planning Thread',
|
|
24
24
|
model: 'openai/gpt-5',
|
|
25
|
+
preferred_thinking: 'medium',
|
|
25
26
|
session_type: 'native'
|
|
26
27
|
}
|
|
27
28
|
})
|
|
@@ -31,6 +32,7 @@ describe('adaptNcpSessionSummary', () => {
|
|
|
31
32
|
key: 'ncp-session-1',
|
|
32
33
|
label: 'NCP Planning Thread',
|
|
33
34
|
preferredModel: 'openai/gpt-5',
|
|
35
|
+
preferredThinking: 'medium',
|
|
34
36
|
sessionType: 'native',
|
|
35
37
|
sessionTypeMutable: false,
|
|
36
38
|
messageCount: 3
|
|
@@ -175,12 +175,14 @@ export function adaptNcpMessagesToUiMessages(messages: readonly NcpMessageView[]
|
|
|
175
175
|
export function adaptNcpSessionSummary(summary: NcpSessionSummaryView): SessionEntryView {
|
|
176
176
|
const label = readNcpSessionLabel(summary);
|
|
177
177
|
const preferredModel = readNcpSessionPreferredModel(summary);
|
|
178
|
+
const preferredThinking = readNcpSessionPreferredThinking(summary);
|
|
178
179
|
return {
|
|
179
180
|
key: summary.sessionId,
|
|
180
181
|
createdAt: summary.updatedAt,
|
|
181
182
|
updatedAt: summary.updatedAt,
|
|
182
183
|
...(label ? { label } : {}),
|
|
183
184
|
...(preferredModel ? { preferredModel } : {}),
|
|
185
|
+
...(preferredThinking ? { preferredThinking } : {}),
|
|
184
186
|
sessionType: readNcpSessionType(summary),
|
|
185
187
|
sessionTypeMutable: false,
|
|
186
188
|
messageCount: summary.messageCount
|
|
@@ -18,7 +18,20 @@ export type ChatInputSnapshot = {
|
|
|
18
18
|
modelOptions: ChatModelOption[];
|
|
19
19
|
selectedModel: string;
|
|
20
20
|
selectedThinkingLevel: ThinkingLevel | null;
|
|
21
|
-
sessionTypeOptions: Array<{
|
|
21
|
+
sessionTypeOptions: Array<{
|
|
22
|
+
value: string;
|
|
23
|
+
label: string;
|
|
24
|
+
ready?: boolean;
|
|
25
|
+
reason?: string | null;
|
|
26
|
+
reasonMessage?: string | null;
|
|
27
|
+
supportedModels?: string[];
|
|
28
|
+
recommendedModel?: string | null;
|
|
29
|
+
cta?: {
|
|
30
|
+
kind: string;
|
|
31
|
+
label?: string;
|
|
32
|
+
href?: string;
|
|
33
|
+
} | null;
|
|
34
|
+
}>;
|
|
22
35
|
selectedSessionType?: string;
|
|
23
36
|
stopSupported: boolean;
|
|
24
37
|
stopReason?: string;
|
|
@@ -55,4 +55,33 @@ describe('useChatSessionTypeState', () => {
|
|
|
55
55
|
|
|
56
56
|
expect(setPendingSessionType).toHaveBeenCalledWith('codex-sdk');
|
|
57
57
|
});
|
|
58
|
+
|
|
59
|
+
it('marks the selected draft session type as unavailable when runtime setup is incomplete', () => {
|
|
60
|
+
const setPendingSessionType = vi.fn();
|
|
61
|
+
|
|
62
|
+
const { result } = renderHook(() =>
|
|
63
|
+
useChatSessionTypeState({
|
|
64
|
+
selectedSession: null,
|
|
65
|
+
selectedSessionKey: null,
|
|
66
|
+
pendingSessionType: 'claude',
|
|
67
|
+
setPendingSessionType,
|
|
68
|
+
sessionTypesData: {
|
|
69
|
+
defaultType: 'native',
|
|
70
|
+
options: [
|
|
71
|
+
{ value: 'native', label: 'Native', ready: true },
|
|
72
|
+
{
|
|
73
|
+
value: 'claude',
|
|
74
|
+
label: 'Claude',
|
|
75
|
+
ready: false,
|
|
76
|
+
reasonMessage: 'Configure a provider API key first.'
|
|
77
|
+
}
|
|
78
|
+
]
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
expect(result.current.selectedSessionTypeOption?.ready).toBe(false);
|
|
84
|
+
expect(result.current.sessionTypeUnavailable).toBe(true);
|
|
85
|
+
expect(result.current.sessionTypeUnavailableMessage).toBe('Configure a provider API key first.');
|
|
86
|
+
});
|
|
58
87
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useEffect, useMemo, useRef } from 'react';
|
|
2
2
|
import type { Dispatch, SetStateAction } from 'react';
|
|
3
|
-
import type { SessionEntryView } from '@/api/types';
|
|
3
|
+
import type { ChatSessionTypeOptionView, SessionEntryView } from '@/api/types';
|
|
4
4
|
import { t } from '@/lib/i18n';
|
|
5
5
|
|
|
6
6
|
export const DEFAULT_SESSION_TYPE = 'native';
|
|
@@ -8,6 +8,16 @@ export const DEFAULT_SESSION_TYPE = 'native';
|
|
|
8
8
|
export type ChatSessionTypeOption = {
|
|
9
9
|
value: string;
|
|
10
10
|
label: string;
|
|
11
|
+
ready: boolean;
|
|
12
|
+
reason?: string | null;
|
|
13
|
+
reasonMessage?: string | null;
|
|
14
|
+
supportedModels?: string[];
|
|
15
|
+
recommendedModel?: string | null;
|
|
16
|
+
cta?: {
|
|
17
|
+
kind: string;
|
|
18
|
+
label?: string;
|
|
19
|
+
href?: string;
|
|
20
|
+
} | null;
|
|
11
21
|
};
|
|
12
22
|
|
|
13
23
|
type UseChatSessionTypeStateParams = {
|
|
@@ -17,7 +27,7 @@ type UseChatSessionTypeStateParams = {
|
|
|
17
27
|
setPendingSessionType: Dispatch<SetStateAction<string>>;
|
|
18
28
|
sessionTypesData?: {
|
|
19
29
|
defaultType?: string;
|
|
20
|
-
options?:
|
|
30
|
+
options?: ChatSessionTypeOptionView[];
|
|
21
31
|
} | null;
|
|
22
32
|
};
|
|
23
33
|
|
|
@@ -46,20 +56,32 @@ export function resolveSessionTypeLabel(sessionType: string, fallbackLabel?: str
|
|
|
46
56
|
}
|
|
47
57
|
|
|
48
58
|
function buildSessionTypeOptions(
|
|
49
|
-
options:
|
|
59
|
+
options: ChatSessionTypeOptionView[]
|
|
50
60
|
): ChatSessionTypeOption[] {
|
|
51
61
|
const deduped = new Map<string, ChatSessionTypeOption>();
|
|
52
62
|
for (const option of options) {
|
|
53
63
|
const value = normalizeSessionType(option.value);
|
|
54
64
|
deduped.set(value, {
|
|
55
65
|
value,
|
|
56
|
-
label: option.label?.trim() || resolveSessionTypeLabel(value)
|
|
66
|
+
label: option.label?.trim() || resolveSessionTypeLabel(value),
|
|
67
|
+
ready: option.ready ?? true,
|
|
68
|
+
reason: option.reason ?? null,
|
|
69
|
+
reasonMessage: option.reasonMessage ?? null,
|
|
70
|
+
supportedModels: option.supportedModels,
|
|
71
|
+
recommendedModel: option.recommendedModel ?? null,
|
|
72
|
+
cta: option.cta ?? null
|
|
57
73
|
});
|
|
58
74
|
}
|
|
59
75
|
if (!deduped.has(DEFAULT_SESSION_TYPE)) {
|
|
60
76
|
deduped.set(DEFAULT_SESSION_TYPE, {
|
|
61
77
|
value: DEFAULT_SESSION_TYPE,
|
|
62
|
-
label: resolveSessionTypeLabel(DEFAULT_SESSION_TYPE)
|
|
78
|
+
label: resolveSessionTypeLabel(DEFAULT_SESSION_TYPE),
|
|
79
|
+
ready: true,
|
|
80
|
+
reason: null,
|
|
81
|
+
reasonMessage: null,
|
|
82
|
+
supportedModels: undefined,
|
|
83
|
+
recommendedModel: null,
|
|
84
|
+
cta: null
|
|
63
85
|
});
|
|
64
86
|
}
|
|
65
87
|
return Array.from(deduped.values()).sort((left, right) => {
|
|
@@ -75,6 +97,7 @@ function buildSessionTypeOptions(
|
|
|
75
97
|
|
|
76
98
|
export function useChatSessionTypeState(params: UseChatSessionTypeStateParams): {
|
|
77
99
|
sessionTypeOptions: ChatSessionTypeOption[];
|
|
100
|
+
selectedSessionTypeOption: ChatSessionTypeOption | null;
|
|
78
101
|
defaultSessionType: string;
|
|
79
102
|
selectedSessionType: string;
|
|
80
103
|
canEditSessionType: boolean;
|
|
@@ -99,7 +122,13 @@ export function useChatSessionTypeState(params: UseChatSessionTypeStateParams):
|
|
|
99
122
|
if (!options.some((option) => option.value === currentSessionType)) {
|
|
100
123
|
options.push({
|
|
101
124
|
value: currentSessionType,
|
|
102
|
-
label: resolveSessionTypeLabel(currentSessionType)
|
|
125
|
+
label: resolveSessionTypeLabel(currentSessionType),
|
|
126
|
+
ready: true,
|
|
127
|
+
reason: null,
|
|
128
|
+
reasonMessage: null,
|
|
129
|
+
supportedModels: undefined,
|
|
130
|
+
recommendedModel: null,
|
|
131
|
+
cta: null
|
|
103
132
|
});
|
|
104
133
|
}
|
|
105
134
|
return options.sort((left, right) => {
|
|
@@ -121,6 +150,10 @@ export function useChatSessionTypeState(params: UseChatSessionTypeStateParams):
|
|
|
121
150
|
() => normalizeSessionType(selectedSession?.sessionType ?? pendingSessionType ?? defaultSessionType),
|
|
122
151
|
[defaultSessionType, pendingSessionType, selectedSession?.sessionType]
|
|
123
152
|
);
|
|
153
|
+
const selectedSessionTypeOption = useMemo(
|
|
154
|
+
() => sessionTypeOptions.find((option) => option.value === selectedSessionType) ?? null,
|
|
155
|
+
[selectedSessionType, sessionTypeOptions]
|
|
156
|
+
);
|
|
124
157
|
|
|
125
158
|
useEffect(() => {
|
|
126
159
|
if (selectedSessionKey) {
|
|
@@ -147,15 +180,25 @@ export function useChatSessionTypeState(params: UseChatSessionTypeStateParams):
|
|
|
147
180
|
() => new Set(runtimeSessionTypeOptions.map((option) => option.value)),
|
|
148
181
|
[runtimeSessionTypeOptions]
|
|
149
182
|
);
|
|
150
|
-
const sessionTypeUnavailable =
|
|
151
|
-
selectedSession && !availableSessionTypeSet.has(normalizeSessionType(selectedSession.sessionType))
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
183
|
+
const sessionTypeUnavailable = useMemo(() => {
|
|
184
|
+
if (selectedSession && !availableSessionTypeSet.has(normalizeSessionType(selectedSession.sessionType))) {
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
return selectedSessionTypeOption?.ready === false;
|
|
188
|
+
}, [availableSessionTypeSet, selectedSession, selectedSessionTypeOption?.ready]);
|
|
189
|
+
const sessionTypeUnavailableMessage = useMemo(() => {
|
|
190
|
+
if (selectedSession && !availableSessionTypeSet.has(normalizeSessionType(selectedSession.sessionType))) {
|
|
191
|
+
return `${resolveSessionTypeLabel(selectedSessionType)} ${t('chatSessionTypeUnavailableSuffix')}`;
|
|
192
|
+
}
|
|
193
|
+
if (selectedSessionTypeOption?.ready === false) {
|
|
194
|
+
return selectedSessionTypeOption.reasonMessage?.trim() || `${selectedSessionTypeOption.label} setup required`;
|
|
195
|
+
}
|
|
196
|
+
return null;
|
|
197
|
+
}, [availableSessionTypeSet, selectedSession, selectedSessionType, selectedSessionTypeOption]);
|
|
156
198
|
|
|
157
199
|
return {
|
|
158
200
|
sessionTypeOptions,
|
|
201
|
+
selectedSessionTypeOption,
|
|
159
202
|
defaultSessionType,
|
|
160
203
|
selectedSessionType,
|
|
161
204
|
canEditSessionType,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { cn } from '@/lib/utils';
|
|
2
2
|
import { LANGUAGE_OPTIONS, t, type I18nLanguage } from '@/lib/i18n';
|
|
3
3
|
import { THEME_OPTIONS, type UiTheme } from '@/lib/theme';
|
|
4
|
-
import { Cpu, GitBranch, History, MessageCircle, MessageSquare, Sparkles, BookOpen, Plug, BrainCircuit, AlarmClock, Languages, Palette, KeyRound, Settings, ArrowLeft, Search, Shield } from 'lucide-react';
|
|
4
|
+
import { Cpu, GitBranch, History, MessageCircle, MessageSquare, Sparkles, BookOpen, Plug, BrainCircuit, AlarmClock, Languages, Palette, KeyRound, Settings, ArrowLeft, Search, Shield, Wrench, Wifi } from 'lucide-react';
|
|
5
5
|
import { NavLink } from 'react-router-dom';
|
|
6
6
|
import { useDocBrowser } from '@/components/doc-browser';
|
|
7
7
|
import { BrandHeader } from '@/components/common/BrandHeader';
|
|
@@ -82,6 +82,11 @@ export function Sidebar({ mode }: SidebarProps) {
|
|
|
82
82
|
label: t('runtime'),
|
|
83
83
|
icon: GitBranch,
|
|
84
84
|
},
|
|
85
|
+
{
|
|
86
|
+
target: '/remote',
|
|
87
|
+
label: t('remote'),
|
|
88
|
+
icon: Wifi,
|
|
89
|
+
},
|
|
85
90
|
{
|
|
86
91
|
target: '/security',
|
|
87
92
|
label: t('security'),
|
|
@@ -101,6 +106,11 @@ export function Sidebar({ mode }: SidebarProps) {
|
|
|
101
106
|
target: '/marketplace/plugins',
|
|
102
107
|
label: t('marketplaceFilterPlugins'),
|
|
103
108
|
icon: Plug,
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
target: '/marketplace/mcp',
|
|
112
|
+
label: t('marketplaceFilterMcp'),
|
|
113
|
+
icon: Wrench,
|
|
104
114
|
}
|
|
105
115
|
];
|
|
106
116
|
const navItems = mode === 'main' ? mainNavItems : settingsNavItems;
|