@nextclaw/ui 0.7.0 → 0.9.0
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 +30 -0
- package/dist/assets/ChannelsList-C7F_As4r.js +1 -0
- package/dist/assets/ChatPage-Oo7-OUsx.js +37 -0
- package/dist/assets/{DocBrowser-B9ws5JL7.js → DocBrowser-Dsd8Dlq8.js} +1 -1
- package/dist/assets/{LogoBadge-DvGAzkZ3.js → LogoBadge-2ChEc_oz.js} +1 -1
- package/dist/assets/MarketplacePage-BXck6-X3.js +49 -0
- package/dist/assets/{ModelConfig-BL_HsOsm.js → ModelConfig-CgHRSD0b.js} +1 -1
- package/dist/assets/ProvidersList-PPfZucvS.js +1 -0
- package/dist/assets/RuntimeConfig-ClLEKNTN.js +1 -0
- package/dist/assets/{SearchConfig-BhaI0fUf.js → SearchConfig-CuXVCbrf.js} +1 -1
- package/dist/assets/{SecretsConfig-CFoimOh9.js → SecretsConfig-udJz6Ake.js} +2 -2
- package/dist/assets/SessionsConfig-C1XnFfiC.js +2 -0
- package/dist/assets/{session-run-status-TkIuGbVw.js → chat-message-BETwXLD4.js} +3 -3
- package/dist/assets/{index-uMsNsQX6.js → index-COJdlL0e.js} +1 -1
- package/dist/assets/index-CsvP4CER.js +8 -0
- package/dist/assets/index-D-bXl7qL.css +1 -0
- package/dist/assets/{label-D8ly4a2P.js → label-BGL-ztxh.js} +1 -1
- package/dist/assets/{page-layout-BSYfvwbp.js → page-layout-aw88k7tG.js} +1 -1
- package/dist/assets/popover-DyEvzhmV.js +1 -0
- package/dist/assets/security-config-BuPAQn82.js +1 -0
- package/dist/assets/skeleton-drzO_tdU.js +1 -0
- package/dist/assets/{switch-Ce_g9lpN.js → switch-BK8jIzto.js} +1 -1
- package/dist/assets/{tabs-custom-Cf5azvT5.js → tabs-custom-Da3cEOji.js} +1 -1
- package/dist/assets/{useConfirmDialog-A8Ek8Wu7.js → useConfirmDialog-z0CE92iS.js} +2 -2
- package/dist/assets/{vendor-B7ozqnFC.js → vendor-CkJHmX1g.js} +65 -70
- package/dist/index.html +3 -3
- package/package.json +5 -2
- package/src/api/config.ts +9 -0
- package/src/api/ncp-session.ts +50 -0
- package/src/api/types.ts +20 -0
- package/src/components/chat/ChatConversationPanel.test.tsx +65 -0
- package/src/components/chat/ChatConversationPanel.tsx +21 -12
- package/src/components/chat/ChatPage.tsx +10 -324
- package/src/components/chat/ChatSidebar.test.tsx +203 -0
- package/src/components/chat/ChatSidebar.tsx +97 -7
- package/src/components/chat/adapters/chat-message.adapter.test.ts +132 -81
- package/src/components/chat/adapters/chat-message.adapter.ts +27 -9
- package/src/components/chat/chat-chain.test.ts +22 -0
- package/src/components/chat/chat-chain.ts +23 -0
- package/src/components/chat/chat-page-data.ts +30 -1
- package/src/components/chat/chat-page-runtime.test.ts +181 -0
- package/src/components/chat/chat-page-runtime.ts +101 -15
- package/src/components/chat/chat-page-shell.tsx +103 -0
- package/src/components/chat/chat-session-preference-sync.test.ts +62 -0
- package/src/components/chat/chat-session-preference-sync.ts +75 -0
- package/src/components/chat/containers/chat-input-bar.container.tsx +0 -22
- package/src/components/chat/containers/chat-message-list.container.tsx +34 -26
- package/src/components/chat/legacy/LegacyChatPage.tsx +252 -0
- package/src/components/chat/managers/chat-input.manager.ts +5 -0
- package/src/components/chat/managers/chat-session-list.manager.test.ts +39 -0
- package/src/components/chat/managers/chat-session-list.manager.ts +9 -3
- package/src/components/chat/ncp/NcpChatPage.tsx +381 -0
- package/src/components/chat/ncp/ncp-chat-input.manager.ts +179 -0
- package/src/components/chat/ncp/ncp-chat-page-data.ts +166 -0
- package/src/components/chat/ncp/ncp-chat-thread.manager.ts +89 -0
- package/src/components/chat/ncp/ncp-chat.presenter.ts +33 -0
- package/src/components/chat/ncp/ncp-session-adapter.test.ts +75 -0
- package/src/components/chat/ncp/ncp-session-adapter.ts +214 -0
- package/src/components/chat/presenter/chat-presenter-context.tsx +43 -4
- package/src/components/chat/stores/chat-thread.store.ts +2 -0
- package/src/components/chat/useChatSessionTypeState.test.tsx +58 -0
- package/src/components/chat/useChatSessionTypeState.ts +25 -8
- package/src/hooks/use-ncp-chat-session-types.ts +11 -0
- package/src/hooks/useConfig.ts +41 -1
- package/src/hooks/useMarketplace.ts +7 -4
- package/src/hooks/useWebSocket.ts +23 -2
- package/src/lib/i18n.ts +1 -1
- package/tailwind.config.js +8 -3
- package/tsconfig.json +4 -1
- package/dist/assets/ChannelsList-DF2U-LY1.js +0 -1
- package/dist/assets/ChatPage-BX39y0U5.js +0 -36
- package/dist/assets/MarketplacePage-DG5mHWJ8.js +0 -49
- package/dist/assets/ProvidersList-CH5z00YT.js +0 -1
- package/dist/assets/RuntimeConfig-BplBgkwo.js +0 -1
- package/dist/assets/SessionsConfig-BHTAYn9T.js +0 -2
- package/dist/assets/index-BLeJkJ0o.css +0 -1
- package/dist/assets/index-DK4TS5ev.js +0 -8
- package/dist/assets/index-X5J6Mm--.js +0 -1
- package/dist/assets/security-config-DlKEYHNN.js +0 -1
- package/dist/assets/skeleton-CWbsNx2h.js +0 -1
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { NcpHttpAgentClientEndpoint } from '@nextclaw/ncp-http-agent-client';
|
|
3
|
+
import { useHydratedNcpAgent, type NcpConversationSeed } from '@nextclaw/ncp-react';
|
|
4
|
+
import { useLocation, useNavigate, useParams } from 'react-router-dom';
|
|
5
|
+
import { API_BASE } from '@/api/client';
|
|
6
|
+
import { fetchNcpSessionMessages } from '@/api/ncp-session';
|
|
7
|
+
import type { ChatRunView } from '@/api/types';
|
|
8
|
+
import { sessionDisplayName } from '@/components/chat/chat-page-data';
|
|
9
|
+
import { ChatPageLayout, type ChatPageProps, useChatSessionSync } from '@/components/chat/chat-page-shell';
|
|
10
|
+
import { parseSessionKeyFromRoute, resolveAgentIdFromSessionKey } from '@/components/chat/chat-session-route';
|
|
11
|
+
import { useNcpChatPageData } from '@/components/chat/ncp/ncp-chat-page-data';
|
|
12
|
+
import { NcpChatPresenter } from '@/components/chat/ncp/ncp-chat.presenter';
|
|
13
|
+
import { adaptNcpMessagesToUiMessages, buildNcpSessionRunStatusByKey, createNcpSessionId } from '@/components/chat/ncp/ncp-session-adapter';
|
|
14
|
+
import { ChatPresenterProvider } from '@/components/chat/presenter/chat-presenter-context';
|
|
15
|
+
import { useChatInputStore } from '@/components/chat/stores/chat-input.store';
|
|
16
|
+
import { useChatSessionListStore } from '@/components/chat/stores/chat-session-list.store';
|
|
17
|
+
import { resolveSessionTypeLabel } from '@/components/chat/useChatSessionTypeState';
|
|
18
|
+
import { useConfirmDialog } from '@/hooks/useConfirmDialog';
|
|
19
|
+
import { normalizeRequestedSkills } from '@/lib/chat-runtime-utils';
|
|
20
|
+
|
|
21
|
+
function createFetchWithCredentials(): typeof fetch {
|
|
22
|
+
return (input, init) =>
|
|
23
|
+
fetch(input, {
|
|
24
|
+
credentials: 'include',
|
|
25
|
+
...init
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function buildNcpSendMetadata(payload: {
|
|
30
|
+
model?: string;
|
|
31
|
+
thinkingLevel?: string;
|
|
32
|
+
sessionType?: string;
|
|
33
|
+
requestedSkills?: string[];
|
|
34
|
+
}): Record<string, unknown> {
|
|
35
|
+
const metadata: Record<string, unknown> = {};
|
|
36
|
+
if (payload.model?.trim()) {
|
|
37
|
+
metadata.model = payload.model.trim();
|
|
38
|
+
metadata.preferred_model = payload.model.trim();
|
|
39
|
+
}
|
|
40
|
+
if (payload.thinkingLevel?.trim()) {
|
|
41
|
+
metadata.thinking = payload.thinkingLevel.trim();
|
|
42
|
+
metadata.preferred_thinking = payload.thinkingLevel.trim();
|
|
43
|
+
}
|
|
44
|
+
if (payload.sessionType?.trim()) {
|
|
45
|
+
metadata.session_type = payload.sessionType.trim();
|
|
46
|
+
}
|
|
47
|
+
const requestedSkills = normalizeRequestedSkills(payload.requestedSkills);
|
|
48
|
+
if (requestedSkills.length > 0) {
|
|
49
|
+
metadata.requested_skills = requestedSkills;
|
|
50
|
+
}
|
|
51
|
+
return metadata;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function isMissingNcpSessionError(error: unknown): boolean {
|
|
55
|
+
if (!(error instanceof Error)) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
return error.message.includes('ncp session not found:');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function NcpChatPage({ view }: ChatPageProps) {
|
|
62
|
+
const [presenter] = useState(() => new NcpChatPresenter());
|
|
63
|
+
const [draftSessionId, setDraftSessionId] = useState(() => createNcpSessionId());
|
|
64
|
+
const query = useChatSessionListStore((state) => state.snapshot.query);
|
|
65
|
+
const selectedSessionKey = useChatSessionListStore((state) => state.snapshot.selectedSessionKey);
|
|
66
|
+
const selectedAgentId = useChatSessionListStore((state) => state.snapshot.selectedAgentId);
|
|
67
|
+
const pendingSessionType = useChatInputStore((state) => state.snapshot.pendingSessionType);
|
|
68
|
+
const { confirm, ConfirmDialog } = useConfirmDialog();
|
|
69
|
+
const location = useLocation();
|
|
70
|
+
const navigate = useNavigate();
|
|
71
|
+
const { sessionId: routeSessionIdParam } = useParams<{ sessionId?: string }>();
|
|
72
|
+
const threadRef = useRef<HTMLDivElement | null>(null);
|
|
73
|
+
const selectedSessionKeyRef = useRef<string | null>(selectedSessionKey);
|
|
74
|
+
const modelHydratedSessionKeyRef = useRef<string | null>(null);
|
|
75
|
+
const thinkingHydratedSessionKeyRef = useRef<string | null>(null);
|
|
76
|
+
const routeSessionKey = useMemo(
|
|
77
|
+
() => parseSessionKeyFromRoute(routeSessionIdParam),
|
|
78
|
+
[routeSessionIdParam]
|
|
79
|
+
);
|
|
80
|
+
const {
|
|
81
|
+
sessionsQuery,
|
|
82
|
+
installedSkillsQuery,
|
|
83
|
+
isProviderStateResolved,
|
|
84
|
+
modelOptions,
|
|
85
|
+
sessionSummaries,
|
|
86
|
+
sessions,
|
|
87
|
+
skillRecords,
|
|
88
|
+
selectedSession,
|
|
89
|
+
hydratedSessionModel,
|
|
90
|
+
selectedSessionThinkingLevel,
|
|
91
|
+
sessionTypeOptions,
|
|
92
|
+
defaultSessionType,
|
|
93
|
+
selectedSessionType,
|
|
94
|
+
canEditSessionType,
|
|
95
|
+
sessionTypeUnavailable,
|
|
96
|
+
sessionTypeUnavailableMessage
|
|
97
|
+
} = useNcpChatPageData({
|
|
98
|
+
query,
|
|
99
|
+
selectedSessionKey,
|
|
100
|
+
pendingSessionType,
|
|
101
|
+
setPendingSessionType: presenter.chatInputManager.setPendingSessionType,
|
|
102
|
+
setSelectedModel: presenter.chatInputManager.setSelectedModel
|
|
103
|
+
});
|
|
104
|
+
const refetchSessions = sessionsQuery.refetch;
|
|
105
|
+
|
|
106
|
+
const activeSessionId = selectedSessionKey ?? draftSessionId;
|
|
107
|
+
const sessionSummariesRef = useRef(sessionSummaries);
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
sessionSummariesRef.current = sessionSummaries;
|
|
110
|
+
}, [sessionSummaries]);
|
|
111
|
+
|
|
112
|
+
const [ncpClient] = useState(
|
|
113
|
+
() =>
|
|
114
|
+
new NcpHttpAgentClientEndpoint({
|
|
115
|
+
baseUrl: API_BASE,
|
|
116
|
+
basePath: '/api/ncp/agent',
|
|
117
|
+
fetchImpl: createFetchWithCredentials()
|
|
118
|
+
})
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
const loadSeed = useCallback(async (sessionId: string, signal: AbortSignal): Promise<NcpConversationSeed> => {
|
|
122
|
+
signal.throwIfAborted();
|
|
123
|
+
let history: Awaited<ReturnType<typeof fetchNcpSessionMessages>> | null = null;
|
|
124
|
+
try {
|
|
125
|
+
history = await fetchNcpSessionMessages(sessionId, 300);
|
|
126
|
+
} catch (error) {
|
|
127
|
+
if (!isMissingNcpSessionError(error)) {
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
signal.throwIfAborted();
|
|
132
|
+
|
|
133
|
+
const sessionSummary = sessionSummariesRef.current.find((item) => item.sessionId === sessionId) ?? null;
|
|
134
|
+
return {
|
|
135
|
+
messages: history?.messages ?? [],
|
|
136
|
+
status: sessionSummary?.status === 'running' ? 'running' : 'idle'
|
|
137
|
+
};
|
|
138
|
+
}, []);
|
|
139
|
+
|
|
140
|
+
const agent = useHydratedNcpAgent({
|
|
141
|
+
sessionId: activeSessionId,
|
|
142
|
+
client: ncpClient,
|
|
143
|
+
loadSeed
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
useEffect(() => {
|
|
147
|
+
presenter.setDraftSessionId(draftSessionId);
|
|
148
|
+
}, [draftSessionId, presenter]);
|
|
149
|
+
|
|
150
|
+
useEffect(() => {
|
|
151
|
+
if (selectedSessionKey === null) {
|
|
152
|
+
const nextDraftSessionId = createNcpSessionId();
|
|
153
|
+
setDraftSessionId(nextDraftSessionId);
|
|
154
|
+
presenter.setDraftSessionId(nextDraftSessionId);
|
|
155
|
+
}
|
|
156
|
+
}, [presenter, selectedSessionKey]);
|
|
157
|
+
|
|
158
|
+
const uiMessages = useMemo(
|
|
159
|
+
() => adaptNcpMessagesToUiMessages(agent.visibleMessages),
|
|
160
|
+
[agent.visibleMessages]
|
|
161
|
+
);
|
|
162
|
+
const isSending = agent.isSending || agent.isRunning;
|
|
163
|
+
const isAwaitingAssistantOutput = agent.isRunning;
|
|
164
|
+
const canStopCurrentRun = agent.isRunning;
|
|
165
|
+
const stopDisabledReason = agent.isRunning ? null : '__preparing__';
|
|
166
|
+
const lastSendError = agent.hydrateError?.message ?? agent.snapshot.error?.message ?? null;
|
|
167
|
+
const activeBackendRunId = agent.activeRunId;
|
|
168
|
+
const sessionRunStatusByKey = useMemo(
|
|
169
|
+
() =>
|
|
170
|
+
buildNcpSessionRunStatusByKey({
|
|
171
|
+
summaries: sessionSummaries,
|
|
172
|
+
activeSessionId,
|
|
173
|
+
isLocallyRunning: isSending || Boolean(activeBackendRunId)
|
|
174
|
+
}),
|
|
175
|
+
[activeBackendRunId, activeSessionId, isSending, sessionSummaries]
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
useEffect(() => {
|
|
179
|
+
if (!isSending && !activeBackendRunId) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
void refetchSessions();
|
|
183
|
+
}, [activeBackendRunId, isSending, refetchSessions]);
|
|
184
|
+
|
|
185
|
+
useEffect(() => {
|
|
186
|
+
presenter.chatStreamActionsManager.bind({
|
|
187
|
+
sendMessage: async (payload) => {
|
|
188
|
+
if (payload.sessionKey !== activeSessionId) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const metadata = buildNcpSendMetadata({
|
|
192
|
+
model: payload.model,
|
|
193
|
+
thinkingLevel: payload.thinkingLevel,
|
|
194
|
+
sessionType: payload.sessionType,
|
|
195
|
+
requestedSkills: payload.requestedSkills
|
|
196
|
+
});
|
|
197
|
+
try {
|
|
198
|
+
void sessionsQuery.refetch();
|
|
199
|
+
await agent.send({
|
|
200
|
+
sessionId: payload.sessionKey,
|
|
201
|
+
message: {
|
|
202
|
+
id: `user-${Date.now().toString(36)}`,
|
|
203
|
+
sessionId: payload.sessionKey,
|
|
204
|
+
role: 'user',
|
|
205
|
+
status: 'final',
|
|
206
|
+
parts: [{ type: 'text', text: payload.message }],
|
|
207
|
+
timestamp: new Date().toISOString(),
|
|
208
|
+
...(Object.keys(metadata).length > 0 ? { metadata } : {})
|
|
209
|
+
},
|
|
210
|
+
...(Object.keys(metadata).length > 0 ? { metadata } : {})
|
|
211
|
+
});
|
|
212
|
+
await sessionsQuery.refetch();
|
|
213
|
+
} catch (error) {
|
|
214
|
+
if (payload.restoreDraftOnError) {
|
|
215
|
+
presenter.chatInputManager.setDraft((currentDraft) =>
|
|
216
|
+
currentDraft.trim().length === 0 ? payload.message : currentDraft
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
throw error;
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
stopCurrentRun: async () => {
|
|
223
|
+
await agent.abort();
|
|
224
|
+
await sessionsQuery.refetch();
|
|
225
|
+
},
|
|
226
|
+
resumeRun: async (run: ChatRunView) => {
|
|
227
|
+
if (run.sessionKey !== activeSessionId) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
await agent.streamRun();
|
|
231
|
+
},
|
|
232
|
+
resetStreamState: () => {
|
|
233
|
+
selectedSessionKeyRef.current = null;
|
|
234
|
+
},
|
|
235
|
+
applyHistoryMessages: () => {}
|
|
236
|
+
});
|
|
237
|
+
}, [activeSessionId, agent, presenter, sessionsQuery]);
|
|
238
|
+
|
|
239
|
+
useChatSessionSync({
|
|
240
|
+
view,
|
|
241
|
+
routeSessionKey,
|
|
242
|
+
selectedSessionKey,
|
|
243
|
+
selectedAgentId,
|
|
244
|
+
setSelectedSessionKey: presenter.chatSessionListManager.setSelectedSessionKey,
|
|
245
|
+
setSelectedAgentId: presenter.chatSessionListManager.setSelectedAgentId,
|
|
246
|
+
selectedSessionKeyRef,
|
|
247
|
+
resetStreamState: presenter.chatStreamActionsManager.resetStreamState,
|
|
248
|
+
resolveAgentIdFromSessionKey
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
useEffect(() => {
|
|
252
|
+
presenter.chatUiManager.syncState({
|
|
253
|
+
pathname: location.pathname
|
|
254
|
+
});
|
|
255
|
+
presenter.chatUiManager.bindActions({
|
|
256
|
+
navigate,
|
|
257
|
+
confirm
|
|
258
|
+
});
|
|
259
|
+
}, [confirm, location.pathname, navigate, presenter]);
|
|
260
|
+
|
|
261
|
+
const currentSessionDisplayName = selectedSession ? sessionDisplayName(selectedSession) : undefined;
|
|
262
|
+
const currentSessionTypeLabel =
|
|
263
|
+
sessionTypeOptions.find((option) => option.value === selectedSessionType)?.label ??
|
|
264
|
+
resolveSessionTypeLabel(selectedSessionType);
|
|
265
|
+
|
|
266
|
+
useEffect(() => {
|
|
267
|
+
presenter.chatThreadManager.bindActions({
|
|
268
|
+
refetchSessions: sessionsQuery.refetch
|
|
269
|
+
});
|
|
270
|
+
}, [presenter, sessionsQuery.refetch]);
|
|
271
|
+
|
|
272
|
+
useEffect(() => {
|
|
273
|
+
const shouldHydrateModelFromSession =
|
|
274
|
+
!isSending &&
|
|
275
|
+
!isAwaitingAssistantOutput &&
|
|
276
|
+
!sessionsQuery.isLoading &&
|
|
277
|
+
isProviderStateResolved &&
|
|
278
|
+
modelOptions.length > 0 &&
|
|
279
|
+
selectedSessionKey !== modelHydratedSessionKeyRef.current;
|
|
280
|
+
const shouldHydrateThinkingFromSession =
|
|
281
|
+
!isSending &&
|
|
282
|
+
!isAwaitingAssistantOutput &&
|
|
283
|
+
!agent.isHydrating &&
|
|
284
|
+
isProviderStateResolved &&
|
|
285
|
+
modelOptions.length > 0 &&
|
|
286
|
+
selectedSessionKey !== thinkingHydratedSessionKeyRef.current;
|
|
287
|
+
|
|
288
|
+
presenter.chatInputManager.syncSnapshot({
|
|
289
|
+
isProviderStateResolved,
|
|
290
|
+
defaultSessionType,
|
|
291
|
+
canStopGeneration: canStopCurrentRun,
|
|
292
|
+
stopDisabledReason,
|
|
293
|
+
stopSupported: true,
|
|
294
|
+
stopReason: undefined,
|
|
295
|
+
sendError: lastSendError,
|
|
296
|
+
isSending,
|
|
297
|
+
modelOptions,
|
|
298
|
+
...(shouldHydrateModelFromSession ? { selectedModel: hydratedSessionModel } : {}),
|
|
299
|
+
sessionTypeOptions,
|
|
300
|
+
selectedSessionType,
|
|
301
|
+
...(shouldHydrateThinkingFromSession ? { selectedThinkingLevel: selectedSessionThinkingLevel } : {}),
|
|
302
|
+
canEditSessionType,
|
|
303
|
+
sessionTypeUnavailable,
|
|
304
|
+
skillRecords,
|
|
305
|
+
isSkillsLoading: installedSkillsQuery.isLoading
|
|
306
|
+
});
|
|
307
|
+
if (shouldHydrateModelFromSession) {
|
|
308
|
+
modelHydratedSessionKeyRef.current = selectedSessionKey;
|
|
309
|
+
}
|
|
310
|
+
if (shouldHydrateThinkingFromSession) {
|
|
311
|
+
thinkingHydratedSessionKeyRef.current = selectedSessionKey;
|
|
312
|
+
}
|
|
313
|
+
if (!selectedSessionKey) {
|
|
314
|
+
modelHydratedSessionKeyRef.current = null;
|
|
315
|
+
thinkingHydratedSessionKeyRef.current = null;
|
|
316
|
+
}
|
|
317
|
+
presenter.chatSessionListManager.syncSnapshot({
|
|
318
|
+
sessions,
|
|
319
|
+
query,
|
|
320
|
+
isLoading: sessionsQuery.isLoading
|
|
321
|
+
});
|
|
322
|
+
presenter.chatRunStatusManager.syncSnapshot({
|
|
323
|
+
sessionRunStatusByKey,
|
|
324
|
+
isLocallyRunning: isSending || Boolean(activeBackendRunId),
|
|
325
|
+
activeBackendRunId
|
|
326
|
+
});
|
|
327
|
+
presenter.chatThreadManager.syncSnapshot({
|
|
328
|
+
isProviderStateResolved,
|
|
329
|
+
modelOptions,
|
|
330
|
+
sessionTypeUnavailable,
|
|
331
|
+
sessionTypeUnavailableMessage,
|
|
332
|
+
sessionTypeLabel: currentSessionTypeLabel,
|
|
333
|
+
selectedSessionKey,
|
|
334
|
+
sessionDisplayName: currentSessionDisplayName,
|
|
335
|
+
canDeleteSession: Boolean(selectedSession),
|
|
336
|
+
threadRef,
|
|
337
|
+
isHistoryLoading: agent.isHydrating,
|
|
338
|
+
uiMessages,
|
|
339
|
+
isSending,
|
|
340
|
+
isAwaitingAssistantOutput
|
|
341
|
+
});
|
|
342
|
+
}, [
|
|
343
|
+
activeBackendRunId,
|
|
344
|
+
agent.isHydrating,
|
|
345
|
+
canEditSessionType,
|
|
346
|
+
canStopCurrentRun,
|
|
347
|
+
currentSessionDisplayName,
|
|
348
|
+
currentSessionTypeLabel,
|
|
349
|
+
defaultSessionType,
|
|
350
|
+
installedSkillsQuery.isLoading,
|
|
351
|
+
isAwaitingAssistantOutput,
|
|
352
|
+
hydratedSessionModel,
|
|
353
|
+
isProviderStateResolved,
|
|
354
|
+
isSending,
|
|
355
|
+
lastSendError,
|
|
356
|
+
modelOptions.length,
|
|
357
|
+
modelOptions,
|
|
358
|
+
presenter,
|
|
359
|
+
query,
|
|
360
|
+
selectedSession,
|
|
361
|
+
selectedSessionKey,
|
|
362
|
+
selectedSessionThinkingLevel,
|
|
363
|
+
selectedSessionType,
|
|
364
|
+
sessionRunStatusByKey,
|
|
365
|
+
sessionTypeOptions,
|
|
366
|
+
sessionTypeUnavailable,
|
|
367
|
+
sessionTypeUnavailableMessage,
|
|
368
|
+
sessions,
|
|
369
|
+
sessionsQuery.isLoading,
|
|
370
|
+
skillRecords,
|
|
371
|
+
stopDisabledReason,
|
|
372
|
+
threadRef,
|
|
373
|
+
uiMessages
|
|
374
|
+
]);
|
|
375
|
+
|
|
376
|
+
return (
|
|
377
|
+
<ChatPresenterProvider presenter={presenter}>
|
|
378
|
+
<ChatPageLayout view={view} confirmDialog={<ConfirmDialog />} />
|
|
379
|
+
</ChatPresenterProvider>
|
|
380
|
+
);
|
|
381
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import type { SetStateAction } from 'react';
|
|
2
|
+
import type { ThinkingLevel } from '@/api/types';
|
|
3
|
+
import { updateNcpSession } from '@/api/ncp-session';
|
|
4
|
+
import { useChatInputStore } from '@/components/chat/stores/chat-input.store';
|
|
5
|
+
import { useChatSessionListStore } from '@/components/chat/stores/chat-session-list.store';
|
|
6
|
+
import type { ChatInputSnapshot } from '@/components/chat/stores/chat-input.store';
|
|
7
|
+
import type { ChatStreamActionsManager } from '@/components/chat/managers/chat-stream-actions.manager';
|
|
8
|
+
import type { ChatUiManager } from '@/components/chat/managers/chat-ui.manager';
|
|
9
|
+
import { ChatSessionPreferenceSync } from '@/components/chat/chat-session-preference-sync';
|
|
10
|
+
import type { ChatModelOption } from '@/components/chat/chat-input.types';
|
|
11
|
+
import { normalizeSessionType } from '@/components/chat/useChatSessionTypeState';
|
|
12
|
+
|
|
13
|
+
export class NcpChatInputManager {
|
|
14
|
+
private readonly sessionPreferenceSync = new ChatSessionPreferenceSync(updateNcpSession);
|
|
15
|
+
|
|
16
|
+
constructor(
|
|
17
|
+
private uiManager: ChatUiManager,
|
|
18
|
+
private streamActionsManager: ChatStreamActionsManager,
|
|
19
|
+
private getDraftSessionId: () => string
|
|
20
|
+
) {}
|
|
21
|
+
|
|
22
|
+
private hasSnapshotChanges = (patch: Partial<ChatInputSnapshot>): boolean => {
|
|
23
|
+
const current = useChatInputStore.getState().snapshot;
|
|
24
|
+
for (const [key, value] of Object.entries(patch) as Array<[keyof ChatInputSnapshot, ChatInputSnapshot[keyof ChatInputSnapshot]]>) {
|
|
25
|
+
if (!Object.is(current[key], value)) {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return false;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
private resolveUpdateValue = <T>(prev: T, next: SetStateAction<T>): T => {
|
|
33
|
+
if (typeof next === 'function') {
|
|
34
|
+
return (next as (value: T) => T)(prev);
|
|
35
|
+
}
|
|
36
|
+
return next;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
syncSnapshot = (patch: Partial<ChatInputSnapshot>) => {
|
|
40
|
+
if (!this.hasSnapshotChanges(patch)) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
useChatInputStore.getState().setSnapshot(patch);
|
|
44
|
+
if (
|
|
45
|
+
Object.prototype.hasOwnProperty.call(patch, 'modelOptions') ||
|
|
46
|
+
Object.prototype.hasOwnProperty.call(patch, 'selectedModel') ||
|
|
47
|
+
Object.prototype.hasOwnProperty.call(patch, 'selectedThinkingLevel')
|
|
48
|
+
) {
|
|
49
|
+
const snapshot = useChatInputStore.getState().snapshot;
|
|
50
|
+
this.reconcileThinkingForModel(snapshot.selectedModel);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
setDraft = (next: SetStateAction<string>) => {
|
|
55
|
+
const prev = useChatInputStore.getState().snapshot.draft;
|
|
56
|
+
const value = this.resolveUpdateValue(prev, next);
|
|
57
|
+
if (value === prev) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
useChatInputStore.getState().setSnapshot({ draft: value });
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
setPendingSessionType = (next: SetStateAction<string>) => {
|
|
64
|
+
const prev = useChatInputStore.getState().snapshot.pendingSessionType;
|
|
65
|
+
const value = this.resolveUpdateValue(prev, next);
|
|
66
|
+
if (value === prev) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
useChatInputStore.getState().setSnapshot({ pendingSessionType: value });
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
send = async () => {
|
|
73
|
+
const inputSnapshot = useChatInputStore.getState().snapshot;
|
|
74
|
+
const sessionSnapshot = useChatSessionListStore.getState().snapshot;
|
|
75
|
+
const message = inputSnapshot.draft.trim();
|
|
76
|
+
if (!message) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const requestedSkills = inputSnapshot.selectedSkills;
|
|
80
|
+
const sessionKey = sessionSnapshot.selectedSessionKey ?? this.getDraftSessionId();
|
|
81
|
+
if (!sessionSnapshot.selectedSessionKey) {
|
|
82
|
+
this.uiManager.goToSession(sessionKey, { replace: true });
|
|
83
|
+
}
|
|
84
|
+
this.setDraft('');
|
|
85
|
+
this.setSelectedSkills([]);
|
|
86
|
+
await this.streamActionsManager.sendMessage({
|
|
87
|
+
message,
|
|
88
|
+
sessionKey,
|
|
89
|
+
agentId: sessionSnapshot.selectedAgentId,
|
|
90
|
+
sessionType: inputSnapshot.selectedSessionType,
|
|
91
|
+
model: inputSnapshot.selectedModel || undefined,
|
|
92
|
+
thinkingLevel: inputSnapshot.selectedThinkingLevel ?? undefined,
|
|
93
|
+
stopSupported: true,
|
|
94
|
+
requestedSkills,
|
|
95
|
+
restoreDraftOnError: true
|
|
96
|
+
});
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
stop = async () => {
|
|
100
|
+
await this.streamActionsManager.stopCurrentRun();
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
goToProviders = () => {
|
|
104
|
+
this.uiManager.goToProviders();
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
setSelectedModel = (next: SetStateAction<string>) => {
|
|
108
|
+
const prev = useChatInputStore.getState().snapshot.selectedModel;
|
|
109
|
+
const value = this.resolveUpdateValue(prev, next);
|
|
110
|
+
if (value === prev) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
useChatInputStore.getState().setSnapshot({ selectedModel: value });
|
|
114
|
+
this.reconcileThinkingForModel(value);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
setSelectedThinkingLevel = (next: SetStateAction<ThinkingLevel | null>) => {
|
|
118
|
+
const prev = useChatInputStore.getState().snapshot.selectedThinkingLevel;
|
|
119
|
+
const value = this.resolveUpdateValue(prev, next);
|
|
120
|
+
if (value === prev) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
useChatInputStore.getState().setSnapshot({ selectedThinkingLevel: value });
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
selectSessionType = (value: string) => {
|
|
127
|
+
const normalized = normalizeSessionType(value);
|
|
128
|
+
useChatInputStore.getState().setSnapshot({ selectedSessionType: normalized, pendingSessionType: normalized });
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
setSelectedSkills = (next: SetStateAction<string[]>) => {
|
|
132
|
+
const prev = useChatInputStore.getState().snapshot.selectedSkills;
|
|
133
|
+
const value = this.resolveUpdateValue(prev, next);
|
|
134
|
+
if (Object.is(value, prev)) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
useChatInputStore.getState().setSnapshot({ selectedSkills: value });
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
selectModel = (value: string) => {
|
|
141
|
+
this.setSelectedModel(value);
|
|
142
|
+
this.sessionPreferenceSync.syncSelectedSessionPreferences();
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
selectThinkingLevel = (value: ThinkingLevel) => {
|
|
146
|
+
this.setSelectedThinkingLevel(value);
|
|
147
|
+
this.sessionPreferenceSync.syncSelectedSessionPreferences();
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
selectSkills = (next: string[]) => {
|
|
151
|
+
this.setSelectedSkills(next);
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
private resolveThinkingForModel(modelOption: ChatModelOption | undefined, current: ThinkingLevel | null): ThinkingLevel | null {
|
|
155
|
+
const capability = modelOption?.thinkingCapability;
|
|
156
|
+
if (!capability || capability.supported.length === 0) {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
if (current === 'off') {
|
|
160
|
+
return 'off';
|
|
161
|
+
}
|
|
162
|
+
if (current && capability.supported.includes(current)) {
|
|
163
|
+
return current;
|
|
164
|
+
}
|
|
165
|
+
if (capability.default && capability.supported.includes(capability.default)) {
|
|
166
|
+
return capability.default;
|
|
167
|
+
}
|
|
168
|
+
return 'off';
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private reconcileThinkingForModel(model: string): void {
|
|
172
|
+
const snapshot = useChatInputStore.getState().snapshot;
|
|
173
|
+
const modelOption = snapshot.modelOptions.find((option) => option.value === model);
|
|
174
|
+
const nextThinking = this.resolveThinkingForModel(modelOption, snapshot.selectedThinkingLevel);
|
|
175
|
+
if (nextThinking !== snapshot.selectedThinkingLevel) {
|
|
176
|
+
useChatInputStore.getState().setSnapshot({ selectedThinkingLevel: nextThinking });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|