@nextclaw/ui 0.7.0 → 0.8.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 +14 -0
- package/dist/assets/{ChannelsList-DF2U-LY1.js → ChannelsList-DBcoVJRW.js} +1 -1
- package/dist/assets/ChatPage-CD3cxyyM.js +37 -0
- package/dist/assets/{DocBrowser-B9ws5JL7.js → DocBrowser-DDX2HMXW.js} +1 -1
- package/dist/assets/{LogoBadge-DvGAzkZ3.js → LogoBadge-J53F_3JA.js} +1 -1
- package/dist/assets/{MarketplacePage-DG5mHWJ8.js → MarketplacePage-0BZ4bza0.js} +2 -2
- package/dist/assets/{ModelConfig-BL_HsOsm.js → ModelConfig-Wzq9wGHV.js} +1 -1
- package/dist/assets/{ProvidersList-CH5z00YT.js → ProvidersList-kwzRS8_M.js} +1 -1
- package/dist/assets/RuntimeConfig-N771_AM6.js +1 -0
- package/dist/assets/{SearchConfig-BhaI0fUf.js → SearchConfig-DVt5QVa_.js} +1 -1
- package/dist/assets/{SecretsConfig-CFoimOh9.js → SecretsConfig-CkwauPa8.js} +2 -2
- package/dist/assets/SessionsConfig-C3mnHzkZ.js +2 -0
- package/dist/assets/{session-run-status-TkIuGbVw.js → chat-message-pxr79GDs.js} +3 -3
- package/dist/assets/{index-X5J6Mm--.js → index-BIvFMkN4.js} +1 -1
- package/dist/assets/index-CzkY1reu.js +8 -0
- package/dist/assets/{index-uMsNsQX6.js → index-GdpEEKnz.js} +1 -1
- package/dist/assets/index-RZ0kHHRI.css +1 -0
- package/dist/assets/{label-D8ly4a2P.js → label-CmksBHgc.js} +1 -1
- package/dist/assets/{page-layout-BSYfvwbp.js → page-layout-Db0GbnhS.js} +1 -1
- package/dist/assets/security-config-CjLFME5Q.js +1 -0
- package/dist/assets/skeleton-CkpQeVWN.js +1 -0
- package/dist/assets/{switch-Ce_g9lpN.js → switch-C24d-UJU.js} +1 -1
- package/dist/assets/tabs-custom-D89bh-fc.js +1 -0
- package/dist/assets/{useConfirmDialog-A8Ek8Wu7.js → useConfirmDialog-BeP35LcG.js} +2 -2
- package/dist/assets/{vendor-B7ozqnFC.js → vendor-psXJBy9u.js} +65 -70
- package/dist/index.html +3 -3
- package/package.json +5 -2
- package/src/api/config.ts +38 -0
- package/src/api/types.ts +19 -0
- package/src/components/chat/ChatPage.tsx +10 -324
- package/src/components/chat/adapters/chat-message.adapter.test.ts +1 -0
- 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-shell.tsx +103 -0
- package/src/components/chat/containers/chat-message-list.container.tsx +5 -1
- package/src/components/chat/legacy/LegacyChatPage.tsx +228 -0
- package/src/components/chat/ncp/NcpChatPage.tsx +349 -0
- package/src/components/chat/ncp/ncp-chat-input.manager.ts +173 -0
- package/src/components/chat/ncp/ncp-chat-page-data.ts +134 -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 +49 -0
- package/src/components/chat/ncp/ncp-session-adapter.ts +194 -0
- package/src/components/chat/presenter/chat-presenter-context.tsx +43 -4
- package/src/hooks/useConfig.ts +42 -0
- package/src/lib/i18n.ts +1 -1
- package/tailwind.config.js +8 -3
- package/tsconfig.json +4 -1
- package/dist/assets/ChatPage-BX39y0U5.js +0 -36
- 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/security-config-DlKEYHNN.js +0 -1
- package/dist/assets/skeleton-CWbsNx2h.js +0 -1
- package/dist/assets/tabs-custom-Cf5azvT5.js +0 -1
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { useLocation, useNavigate, useParams } from 'react-router-dom';
|
|
3
|
+
import { useConfirmDialog } from '@/hooks/useConfirmDialog';
|
|
4
|
+
import { useSessionRunStatus } from '@/components/chat/chat-page-runtime';
|
|
5
|
+
import { useChatPageData, sessionDisplayName } from '@/components/chat/chat-page-data';
|
|
6
|
+
import { ChatPageLayout, type ChatPageProps, useChatSessionSync } from '@/components/chat/chat-page-shell';
|
|
7
|
+
import { parseSessionKeyFromRoute, resolveAgentIdFromSessionKey } from '@/components/chat/chat-session-route';
|
|
8
|
+
import { ChatPresenterProvider } from '@/components/chat/presenter/chat-presenter-context';
|
|
9
|
+
import { ChatPresenter } from '@/components/chat/presenter/chat.presenter';
|
|
10
|
+
import { useChatRuntimeController } from '@/components/chat/useChatRuntimeController';
|
|
11
|
+
import { useChatInputStore } from '@/components/chat/stores/chat-input.store';
|
|
12
|
+
import { useChatSessionListStore } from '@/components/chat/stores/chat-session-list.store';
|
|
13
|
+
|
|
14
|
+
export function LegacyChatPage({ view }: ChatPageProps) {
|
|
15
|
+
const [presenter] = useState(() => new ChatPresenter());
|
|
16
|
+
const query = useChatSessionListStore((state) => state.snapshot.query);
|
|
17
|
+
const selectedSessionKey = useChatSessionListStore((state) => state.snapshot.selectedSessionKey);
|
|
18
|
+
const selectedAgentId = useChatSessionListStore((state) => state.snapshot.selectedAgentId);
|
|
19
|
+
const pendingSessionType = useChatInputStore((state) => state.snapshot.pendingSessionType);
|
|
20
|
+
const { confirm, ConfirmDialog } = useConfirmDialog();
|
|
21
|
+
const location = useLocation();
|
|
22
|
+
const navigate = useNavigate();
|
|
23
|
+
const { sessionId: routeSessionIdParam } = useParams<{ sessionId?: string }>();
|
|
24
|
+
const threadRef = useRef<HTMLDivElement | null>(null);
|
|
25
|
+
const selectedSessionKeyRef = useRef<string | null>(selectedSessionKey);
|
|
26
|
+
const thinkingHydratedSessionKeyRef = useRef<string | null>(null);
|
|
27
|
+
const routeSessionKey = useMemo(
|
|
28
|
+
() => parseSessionKeyFromRoute(routeSessionIdParam),
|
|
29
|
+
[routeSessionIdParam]
|
|
30
|
+
);
|
|
31
|
+
const {
|
|
32
|
+
sessionsQuery,
|
|
33
|
+
installedSkillsQuery,
|
|
34
|
+
chatCapabilitiesQuery,
|
|
35
|
+
historyQuery,
|
|
36
|
+
isProviderStateResolved,
|
|
37
|
+
modelOptions,
|
|
38
|
+
sessions,
|
|
39
|
+
skillRecords,
|
|
40
|
+
selectedSession,
|
|
41
|
+
historyMessages,
|
|
42
|
+
selectedSessionThinkingLevel,
|
|
43
|
+
sessionTypeOptions,
|
|
44
|
+
defaultSessionType,
|
|
45
|
+
selectedSessionType,
|
|
46
|
+
canEditSessionType,
|
|
47
|
+
sessionTypeUnavailable,
|
|
48
|
+
sessionTypeUnavailableMessage
|
|
49
|
+
} = useChatPageData({
|
|
50
|
+
query,
|
|
51
|
+
selectedSessionKey,
|
|
52
|
+
selectedAgentId,
|
|
53
|
+
pendingSessionType,
|
|
54
|
+
setPendingSessionType: presenter.chatInputManager.setPendingSessionType,
|
|
55
|
+
setSelectedModel: presenter.chatInputManager.setSelectedModel
|
|
56
|
+
});
|
|
57
|
+
const {
|
|
58
|
+
uiMessages,
|
|
59
|
+
isSending,
|
|
60
|
+
isAwaitingAssistantOutput,
|
|
61
|
+
canStopCurrentRun,
|
|
62
|
+
stopDisabledReason,
|
|
63
|
+
lastSendError,
|
|
64
|
+
activeBackendRunId,
|
|
65
|
+
sendMessage,
|
|
66
|
+
stopCurrentRun,
|
|
67
|
+
resumeRun,
|
|
68
|
+
resetStreamState,
|
|
69
|
+
applyHistoryMessages
|
|
70
|
+
} = useChatRuntimeController(
|
|
71
|
+
{
|
|
72
|
+
selectedSessionKeyRef,
|
|
73
|
+
setSelectedSessionKey: presenter.chatSessionListManager.setSelectedSessionKey,
|
|
74
|
+
setDraft: presenter.chatInputManager.setDraft,
|
|
75
|
+
refetchSessions: sessionsQuery.refetch,
|
|
76
|
+
refetchHistory: historyQuery.refetch
|
|
77
|
+
},
|
|
78
|
+
presenter.chatController
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
presenter.chatStreamActionsManager.bind({
|
|
83
|
+
sendMessage,
|
|
84
|
+
stopCurrentRun,
|
|
85
|
+
resumeRun,
|
|
86
|
+
resetStreamState,
|
|
87
|
+
applyHistoryMessages
|
|
88
|
+
});
|
|
89
|
+
}, [applyHistoryMessages, presenter, resetStreamState, resumeRun, sendMessage, stopCurrentRun]);
|
|
90
|
+
|
|
91
|
+
const { sessionRunStatusByKey } = useSessionRunStatus({
|
|
92
|
+
view,
|
|
93
|
+
selectedSessionKey,
|
|
94
|
+
activeBackendRunId,
|
|
95
|
+
isLocallyRunning: isSending || Boolean(activeBackendRunId),
|
|
96
|
+
resumeRun: presenter.chatStreamActionsManager.resumeRun
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
useChatSessionSync({
|
|
100
|
+
view,
|
|
101
|
+
routeSessionKey,
|
|
102
|
+
selectedSessionKey,
|
|
103
|
+
selectedAgentId,
|
|
104
|
+
setSelectedSessionKey: presenter.chatSessionListManager.setSelectedSessionKey,
|
|
105
|
+
setSelectedAgentId: presenter.chatSessionListManager.setSelectedAgentId,
|
|
106
|
+
selectedSessionKeyRef,
|
|
107
|
+
resetStreamState: presenter.chatStreamActionsManager.resetStreamState,
|
|
108
|
+
resolveAgentIdFromSessionKey
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
presenter.chatStreamActionsManager.applyHistoryMessages(historyMessages, {
|
|
113
|
+
isLoading: historyQuery.isLoading
|
|
114
|
+
});
|
|
115
|
+
}, [historyMessages, historyQuery.isLoading, presenter]);
|
|
116
|
+
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
presenter.chatUiManager.syncState({
|
|
119
|
+
pathname: location.pathname
|
|
120
|
+
});
|
|
121
|
+
presenter.chatUiManager.bindActions({
|
|
122
|
+
navigate,
|
|
123
|
+
confirm
|
|
124
|
+
});
|
|
125
|
+
}, [confirm, location.pathname, navigate, presenter]);
|
|
126
|
+
|
|
127
|
+
const currentSessionDisplayName = selectedSession ? sessionDisplayName(selectedSession) : undefined;
|
|
128
|
+
|
|
129
|
+
useEffect(() => {
|
|
130
|
+
presenter.chatThreadManager.bindActions({
|
|
131
|
+
refetchSessions: sessionsQuery.refetch
|
|
132
|
+
});
|
|
133
|
+
}, [presenter, sessionsQuery.refetch]);
|
|
134
|
+
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
const shouldHydrateThinkingFromHistory =
|
|
137
|
+
!isSending &&
|
|
138
|
+
!isAwaitingAssistantOutput &&
|
|
139
|
+
!historyQuery.isLoading &&
|
|
140
|
+
selectedSessionKey !== thinkingHydratedSessionKeyRef.current;
|
|
141
|
+
|
|
142
|
+
presenter.chatInputManager.syncSnapshot({
|
|
143
|
+
isProviderStateResolved,
|
|
144
|
+
defaultSessionType,
|
|
145
|
+
canStopGeneration: canStopCurrentRun,
|
|
146
|
+
stopDisabledReason,
|
|
147
|
+
stopSupported: chatCapabilitiesQuery.data?.stopSupported ?? false,
|
|
148
|
+
stopReason: chatCapabilitiesQuery.data?.stopReason,
|
|
149
|
+
sendError: lastSendError,
|
|
150
|
+
isSending,
|
|
151
|
+
modelOptions,
|
|
152
|
+
sessionTypeOptions,
|
|
153
|
+
selectedSessionType,
|
|
154
|
+
...(shouldHydrateThinkingFromHistory ? { selectedThinkingLevel: selectedSessionThinkingLevel } : {}),
|
|
155
|
+
canEditSessionType,
|
|
156
|
+
sessionTypeUnavailable,
|
|
157
|
+
skillRecords,
|
|
158
|
+
isSkillsLoading: installedSkillsQuery.isLoading
|
|
159
|
+
});
|
|
160
|
+
if (shouldHydrateThinkingFromHistory) {
|
|
161
|
+
thinkingHydratedSessionKeyRef.current = selectedSessionKey;
|
|
162
|
+
}
|
|
163
|
+
if (!selectedSessionKey) {
|
|
164
|
+
thinkingHydratedSessionKeyRef.current = null;
|
|
165
|
+
}
|
|
166
|
+
presenter.chatSessionListManager.syncSnapshot({
|
|
167
|
+
sessions,
|
|
168
|
+
query,
|
|
169
|
+
isLoading: sessionsQuery.isLoading
|
|
170
|
+
});
|
|
171
|
+
presenter.chatRunStatusManager.syncSnapshot({
|
|
172
|
+
sessionRunStatusByKey,
|
|
173
|
+
isLocallyRunning: isSending || Boolean(activeBackendRunId),
|
|
174
|
+
activeBackendRunId
|
|
175
|
+
});
|
|
176
|
+
presenter.chatThreadManager.syncSnapshot({
|
|
177
|
+
isProviderStateResolved,
|
|
178
|
+
modelOptions,
|
|
179
|
+
sessionTypeUnavailable,
|
|
180
|
+
sessionTypeUnavailableMessage,
|
|
181
|
+
selectedSessionKey,
|
|
182
|
+
sessionDisplayName: currentSessionDisplayName,
|
|
183
|
+
canDeleteSession: Boolean(selectedSession),
|
|
184
|
+
threadRef,
|
|
185
|
+
isHistoryLoading: historyQuery.isLoading,
|
|
186
|
+
uiMessages,
|
|
187
|
+
isSending,
|
|
188
|
+
isAwaitingAssistantOutput
|
|
189
|
+
});
|
|
190
|
+
}, [
|
|
191
|
+
activeBackendRunId,
|
|
192
|
+
canEditSessionType,
|
|
193
|
+
canStopCurrentRun,
|
|
194
|
+
currentSessionDisplayName,
|
|
195
|
+
chatCapabilitiesQuery.data?.stopReason,
|
|
196
|
+
chatCapabilitiesQuery.data?.stopSupported,
|
|
197
|
+
defaultSessionType,
|
|
198
|
+
historyQuery.isLoading,
|
|
199
|
+
installedSkillsQuery.isLoading,
|
|
200
|
+
isAwaitingAssistantOutput,
|
|
201
|
+
isProviderStateResolved,
|
|
202
|
+
isSending,
|
|
203
|
+
lastSendError,
|
|
204
|
+
modelOptions,
|
|
205
|
+
presenter,
|
|
206
|
+
query,
|
|
207
|
+
selectedSession,
|
|
208
|
+
selectedSessionKey,
|
|
209
|
+
selectedSessionThinkingLevel,
|
|
210
|
+
selectedSessionType,
|
|
211
|
+
sessionRunStatusByKey,
|
|
212
|
+
sessionTypeOptions,
|
|
213
|
+
sessionTypeUnavailable,
|
|
214
|
+
sessionTypeUnavailableMessage,
|
|
215
|
+
sessions,
|
|
216
|
+
sessionsQuery.isLoading,
|
|
217
|
+
skillRecords,
|
|
218
|
+
stopDisabledReason,
|
|
219
|
+
threadRef,
|
|
220
|
+
uiMessages
|
|
221
|
+
]);
|
|
222
|
+
|
|
223
|
+
return (
|
|
224
|
+
<ChatPresenterProvider presenter={presenter}>
|
|
225
|
+
<ChatPageLayout view={view} confirmDialog={<ConfirmDialog />} />
|
|
226
|
+
</ChatPresenterProvider>
|
|
227
|
+
);
|
|
228
|
+
}
|
|
@@ -0,0 +1,349 @@
|
|
|
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/config';
|
|
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, 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 { useConfirmDialog } from '@/hooks/useConfirmDialog';
|
|
18
|
+
import { normalizeRequestedSkills } from '@/lib/chat-runtime-utils';
|
|
19
|
+
|
|
20
|
+
function createFetchWithCredentials(): typeof fetch {
|
|
21
|
+
return (input, init) =>
|
|
22
|
+
fetch(input, {
|
|
23
|
+
credentials: 'include',
|
|
24
|
+
...init
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function buildNcpSendMetadata(payload: {
|
|
29
|
+
model?: string;
|
|
30
|
+
thinkingLevel?: string;
|
|
31
|
+
sessionType?: string;
|
|
32
|
+
requestedSkills?: string[];
|
|
33
|
+
}): Record<string, unknown> {
|
|
34
|
+
const metadata: Record<string, unknown> = {};
|
|
35
|
+
if (payload.model?.trim()) {
|
|
36
|
+
metadata.model = payload.model.trim();
|
|
37
|
+
metadata.preferred_model = payload.model.trim();
|
|
38
|
+
}
|
|
39
|
+
if (payload.thinkingLevel?.trim()) {
|
|
40
|
+
metadata.thinking = payload.thinkingLevel.trim();
|
|
41
|
+
metadata.preferred_thinking = payload.thinkingLevel.trim();
|
|
42
|
+
}
|
|
43
|
+
if (payload.sessionType?.trim()) {
|
|
44
|
+
metadata.session_type = payload.sessionType.trim();
|
|
45
|
+
}
|
|
46
|
+
const requestedSkills = normalizeRequestedSkills(payload.requestedSkills);
|
|
47
|
+
if (requestedSkills.length > 0) {
|
|
48
|
+
metadata.requested_skills = requestedSkills;
|
|
49
|
+
}
|
|
50
|
+
return metadata;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isMissingNcpSessionError(error: unknown): boolean {
|
|
54
|
+
if (!(error instanceof Error)) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
return error.message.includes('ncp session not found:');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function NcpChatPage({ view }: ChatPageProps) {
|
|
61
|
+
const [presenter] = useState(() => new NcpChatPresenter());
|
|
62
|
+
const [draftSessionId, setDraftSessionId] = useState(() => createNcpSessionId());
|
|
63
|
+
const query = useChatSessionListStore((state) => state.snapshot.query);
|
|
64
|
+
const selectedSessionKey = useChatSessionListStore((state) => state.snapshot.selectedSessionKey);
|
|
65
|
+
const selectedAgentId = useChatSessionListStore((state) => state.snapshot.selectedAgentId);
|
|
66
|
+
const pendingSessionType = useChatInputStore((state) => state.snapshot.pendingSessionType);
|
|
67
|
+
const { confirm, ConfirmDialog } = useConfirmDialog();
|
|
68
|
+
const location = useLocation();
|
|
69
|
+
const navigate = useNavigate();
|
|
70
|
+
const { sessionId: routeSessionIdParam } = useParams<{ sessionId?: string }>();
|
|
71
|
+
const threadRef = useRef<HTMLDivElement | null>(null);
|
|
72
|
+
const selectedSessionKeyRef = useRef<string | null>(selectedSessionKey);
|
|
73
|
+
const thinkingHydratedSessionKeyRef = useRef<string | null>(null);
|
|
74
|
+
const routeSessionKey = useMemo(
|
|
75
|
+
() => parseSessionKeyFromRoute(routeSessionIdParam),
|
|
76
|
+
[routeSessionIdParam]
|
|
77
|
+
);
|
|
78
|
+
const {
|
|
79
|
+
sessionsQuery,
|
|
80
|
+
installedSkillsQuery,
|
|
81
|
+
isProviderStateResolved,
|
|
82
|
+
modelOptions,
|
|
83
|
+
sessionSummaries,
|
|
84
|
+
sessions,
|
|
85
|
+
skillRecords,
|
|
86
|
+
selectedSession,
|
|
87
|
+
selectedSessionThinkingLevel,
|
|
88
|
+
sessionTypeOptions,
|
|
89
|
+
defaultSessionType,
|
|
90
|
+
selectedSessionType,
|
|
91
|
+
canEditSessionType,
|
|
92
|
+
sessionTypeUnavailable,
|
|
93
|
+
sessionTypeUnavailableMessage
|
|
94
|
+
} = useNcpChatPageData({
|
|
95
|
+
query,
|
|
96
|
+
selectedSessionKey,
|
|
97
|
+
pendingSessionType,
|
|
98
|
+
setPendingSessionType: presenter.chatInputManager.setPendingSessionType,
|
|
99
|
+
setSelectedModel: presenter.chatInputManager.setSelectedModel
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const activeSessionId = selectedSessionKey ?? draftSessionId;
|
|
103
|
+
const sessionSummariesRef = useRef(sessionSummaries);
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
sessionSummariesRef.current = sessionSummaries;
|
|
106
|
+
}, [sessionSummaries]);
|
|
107
|
+
|
|
108
|
+
const [ncpClient] = useState(
|
|
109
|
+
() =>
|
|
110
|
+
new NcpHttpAgentClientEndpoint({
|
|
111
|
+
baseUrl: API_BASE,
|
|
112
|
+
basePath: '/api/ncp/agent',
|
|
113
|
+
fetchImpl: createFetchWithCredentials()
|
|
114
|
+
})
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
const loadSeed = useCallback(async (sessionId: string, signal: AbortSignal): Promise<NcpConversationSeed> => {
|
|
118
|
+
signal.throwIfAborted();
|
|
119
|
+
let history: Awaited<ReturnType<typeof fetchNcpSessionMessages>> | null = null;
|
|
120
|
+
try {
|
|
121
|
+
history = await fetchNcpSessionMessages(sessionId, 300);
|
|
122
|
+
} catch (error) {
|
|
123
|
+
if (!isMissingNcpSessionError(error)) {
|
|
124
|
+
throw error;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
signal.throwIfAborted();
|
|
128
|
+
|
|
129
|
+
const sessionSummary = sessionSummariesRef.current.find((item) => item.sessionId === sessionId) ?? null;
|
|
130
|
+
return {
|
|
131
|
+
messages: history?.messages ?? [],
|
|
132
|
+
status: sessionSummary?.status === 'running' ? 'running' : 'idle'
|
|
133
|
+
};
|
|
134
|
+
}, []);
|
|
135
|
+
|
|
136
|
+
const agent = useHydratedNcpAgent({
|
|
137
|
+
sessionId: activeSessionId,
|
|
138
|
+
client: ncpClient,
|
|
139
|
+
loadSeed
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
useEffect(() => {
|
|
143
|
+
presenter.setDraftSessionId(draftSessionId);
|
|
144
|
+
}, [draftSessionId, presenter]);
|
|
145
|
+
|
|
146
|
+
useEffect(() => {
|
|
147
|
+
if (selectedSessionKey === null) {
|
|
148
|
+
const nextDraftSessionId = createNcpSessionId();
|
|
149
|
+
setDraftSessionId(nextDraftSessionId);
|
|
150
|
+
presenter.setDraftSessionId(nextDraftSessionId);
|
|
151
|
+
}
|
|
152
|
+
}, [presenter, selectedSessionKey]);
|
|
153
|
+
|
|
154
|
+
const uiMessages = useMemo(
|
|
155
|
+
() => adaptNcpMessagesToUiMessages(agent.visibleMessages),
|
|
156
|
+
[agent.visibleMessages]
|
|
157
|
+
);
|
|
158
|
+
const isSending = agent.isSending || agent.isRunning;
|
|
159
|
+
const isAwaitingAssistantOutput = agent.isRunning;
|
|
160
|
+
const canStopCurrentRun = agent.isRunning;
|
|
161
|
+
const stopDisabledReason = agent.isRunning ? null : '__preparing__';
|
|
162
|
+
const lastSendError = agent.hydrateError?.message ?? agent.snapshot.error?.message ?? null;
|
|
163
|
+
const activeBackendRunId = agent.activeRunId;
|
|
164
|
+
const sessionRunStatusByKey = useMemo(() => {
|
|
165
|
+
const map = new Map<string, 'running'>();
|
|
166
|
+
for (const sessionSummary of sessionSummaries) {
|
|
167
|
+
if (sessionSummary.status === 'running') {
|
|
168
|
+
map.set(sessionSummary.sessionId, 'running');
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return map;
|
|
172
|
+
}, [sessionSummaries]);
|
|
173
|
+
|
|
174
|
+
useEffect(() => {
|
|
175
|
+
presenter.chatStreamActionsManager.bind({
|
|
176
|
+
sendMessage: async (payload) => {
|
|
177
|
+
if (payload.sessionKey !== activeSessionId) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const metadata = buildNcpSendMetadata({
|
|
181
|
+
model: payload.model,
|
|
182
|
+
thinkingLevel: payload.thinkingLevel,
|
|
183
|
+
sessionType: payload.sessionType,
|
|
184
|
+
requestedSkills: payload.requestedSkills
|
|
185
|
+
});
|
|
186
|
+
try {
|
|
187
|
+
void sessionsQuery.refetch();
|
|
188
|
+
await agent.send({
|
|
189
|
+
sessionId: payload.sessionKey,
|
|
190
|
+
message: {
|
|
191
|
+
id: `user-${Date.now().toString(36)}`,
|
|
192
|
+
sessionId: payload.sessionKey,
|
|
193
|
+
role: 'user',
|
|
194
|
+
status: 'final',
|
|
195
|
+
parts: [{ type: 'text', text: payload.message }],
|
|
196
|
+
timestamp: new Date().toISOString(),
|
|
197
|
+
...(Object.keys(metadata).length > 0 ? { metadata } : {})
|
|
198
|
+
},
|
|
199
|
+
...(Object.keys(metadata).length > 0 ? { metadata } : {})
|
|
200
|
+
});
|
|
201
|
+
await sessionsQuery.refetch();
|
|
202
|
+
} catch (error) {
|
|
203
|
+
if (payload.restoreDraftOnError) {
|
|
204
|
+
presenter.chatInputManager.setDraft((currentDraft) =>
|
|
205
|
+
currentDraft.trim().length === 0 ? payload.message : currentDraft
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
throw error;
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
stopCurrentRun: async () => {
|
|
212
|
+
await agent.abort();
|
|
213
|
+
await sessionsQuery.refetch();
|
|
214
|
+
},
|
|
215
|
+
resumeRun: async (run: ChatRunView) => {
|
|
216
|
+
if (run.sessionKey !== activeSessionId) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
await agent.streamRun();
|
|
220
|
+
},
|
|
221
|
+
resetStreamState: () => {
|
|
222
|
+
selectedSessionKeyRef.current = null;
|
|
223
|
+
},
|
|
224
|
+
applyHistoryMessages: () => {}
|
|
225
|
+
});
|
|
226
|
+
}, [activeSessionId, agent, presenter, sessionsQuery]);
|
|
227
|
+
|
|
228
|
+
useChatSessionSync({
|
|
229
|
+
view,
|
|
230
|
+
routeSessionKey,
|
|
231
|
+
selectedSessionKey,
|
|
232
|
+
selectedAgentId,
|
|
233
|
+
setSelectedSessionKey: presenter.chatSessionListManager.setSelectedSessionKey,
|
|
234
|
+
setSelectedAgentId: presenter.chatSessionListManager.setSelectedAgentId,
|
|
235
|
+
selectedSessionKeyRef,
|
|
236
|
+
resetStreamState: presenter.chatStreamActionsManager.resetStreamState,
|
|
237
|
+
resolveAgentIdFromSessionKey
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
useEffect(() => {
|
|
241
|
+
presenter.chatUiManager.syncState({
|
|
242
|
+
pathname: location.pathname
|
|
243
|
+
});
|
|
244
|
+
presenter.chatUiManager.bindActions({
|
|
245
|
+
navigate,
|
|
246
|
+
confirm
|
|
247
|
+
});
|
|
248
|
+
}, [confirm, location.pathname, navigate, presenter]);
|
|
249
|
+
|
|
250
|
+
const currentSessionDisplayName = selectedSession ? sessionDisplayName(selectedSession) : undefined;
|
|
251
|
+
|
|
252
|
+
useEffect(() => {
|
|
253
|
+
presenter.chatThreadManager.bindActions({
|
|
254
|
+
refetchSessions: sessionsQuery.refetch
|
|
255
|
+
});
|
|
256
|
+
}, [presenter, sessionsQuery.refetch]);
|
|
257
|
+
|
|
258
|
+
useEffect(() => {
|
|
259
|
+
const shouldHydrateThinkingFromSession =
|
|
260
|
+
!isSending &&
|
|
261
|
+
!isAwaitingAssistantOutput &&
|
|
262
|
+
!agent.isHydrating &&
|
|
263
|
+
selectedSessionKey !== thinkingHydratedSessionKeyRef.current;
|
|
264
|
+
|
|
265
|
+
presenter.chatInputManager.syncSnapshot({
|
|
266
|
+
isProviderStateResolved,
|
|
267
|
+
defaultSessionType,
|
|
268
|
+
canStopGeneration: canStopCurrentRun,
|
|
269
|
+
stopDisabledReason,
|
|
270
|
+
stopSupported: true,
|
|
271
|
+
stopReason: undefined,
|
|
272
|
+
sendError: lastSendError,
|
|
273
|
+
isSending,
|
|
274
|
+
modelOptions,
|
|
275
|
+
sessionTypeOptions,
|
|
276
|
+
selectedSessionType,
|
|
277
|
+
...(shouldHydrateThinkingFromSession ? { selectedThinkingLevel: selectedSessionThinkingLevel } : {}),
|
|
278
|
+
canEditSessionType,
|
|
279
|
+
sessionTypeUnavailable,
|
|
280
|
+
skillRecords,
|
|
281
|
+
isSkillsLoading: installedSkillsQuery.isLoading
|
|
282
|
+
});
|
|
283
|
+
if (shouldHydrateThinkingFromSession) {
|
|
284
|
+
thinkingHydratedSessionKeyRef.current = selectedSessionKey;
|
|
285
|
+
}
|
|
286
|
+
if (!selectedSessionKey) {
|
|
287
|
+
thinkingHydratedSessionKeyRef.current = null;
|
|
288
|
+
}
|
|
289
|
+
presenter.chatSessionListManager.syncSnapshot({
|
|
290
|
+
sessions,
|
|
291
|
+
query,
|
|
292
|
+
isLoading: sessionsQuery.isLoading
|
|
293
|
+
});
|
|
294
|
+
presenter.chatRunStatusManager.syncSnapshot({
|
|
295
|
+
sessionRunStatusByKey,
|
|
296
|
+
isLocallyRunning: isSending || Boolean(activeBackendRunId),
|
|
297
|
+
activeBackendRunId
|
|
298
|
+
});
|
|
299
|
+
presenter.chatThreadManager.syncSnapshot({
|
|
300
|
+
isProviderStateResolved,
|
|
301
|
+
modelOptions,
|
|
302
|
+
sessionTypeUnavailable,
|
|
303
|
+
sessionTypeUnavailableMessage,
|
|
304
|
+
selectedSessionKey,
|
|
305
|
+
sessionDisplayName: currentSessionDisplayName,
|
|
306
|
+
canDeleteSession: Boolean(selectedSession),
|
|
307
|
+
threadRef,
|
|
308
|
+
isHistoryLoading: agent.isHydrating,
|
|
309
|
+
uiMessages,
|
|
310
|
+
isSending,
|
|
311
|
+
isAwaitingAssistantOutput
|
|
312
|
+
});
|
|
313
|
+
}, [
|
|
314
|
+
activeBackendRunId,
|
|
315
|
+
agent.isHydrating,
|
|
316
|
+
canEditSessionType,
|
|
317
|
+
canStopCurrentRun,
|
|
318
|
+
currentSessionDisplayName,
|
|
319
|
+
defaultSessionType,
|
|
320
|
+
installedSkillsQuery.isLoading,
|
|
321
|
+
isAwaitingAssistantOutput,
|
|
322
|
+
isProviderStateResolved,
|
|
323
|
+
isSending,
|
|
324
|
+
lastSendError,
|
|
325
|
+
modelOptions,
|
|
326
|
+
presenter,
|
|
327
|
+
query,
|
|
328
|
+
selectedSession,
|
|
329
|
+
selectedSessionKey,
|
|
330
|
+
selectedSessionThinkingLevel,
|
|
331
|
+
selectedSessionType,
|
|
332
|
+
sessionRunStatusByKey,
|
|
333
|
+
sessionTypeOptions,
|
|
334
|
+
sessionTypeUnavailable,
|
|
335
|
+
sessionTypeUnavailableMessage,
|
|
336
|
+
sessions,
|
|
337
|
+
sessionsQuery.isLoading,
|
|
338
|
+
skillRecords,
|
|
339
|
+
stopDisabledReason,
|
|
340
|
+
threadRef,
|
|
341
|
+
uiMessages
|
|
342
|
+
]);
|
|
343
|
+
|
|
344
|
+
return (
|
|
345
|
+
<ChatPresenterProvider presenter={presenter}>
|
|
346
|
+
<ChatPageLayout view={view} confirmDialog={<ConfirmDialog />} />
|
|
347
|
+
</ChatPresenterProvider>
|
|
348
|
+
);
|
|
349
|
+
}
|