@nextclaw/ui 0.11.23 → 0.12.1
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 +37 -0
- package/dist/assets/{ChannelsList-DVDu1xvz.js → ChannelsList-DekMP4a3.js} +1 -1
- package/dist/assets/ChatPage-Dgw4vlDt.js +43 -0
- package/dist/assets/DocBrowser-CExjX5is.js +1 -0
- package/dist/assets/{DocBrowser-BmtBLFU0.js → DocBrowser-DjcghYGO.js} +1 -1
- package/dist/assets/{DocBrowserContext-YIKkPb76.js → DocBrowserContext-CLlq7rZQ.js} +1 -1
- package/dist/assets/{LogoBadge-F7ZWdxLT.js → LogoBadge-D_dOy5U3.js} +1 -1
- package/dist/assets/{MarketplacePage-Buo9HrOz.js → MarketplacePage-BlIeNn3x.js} +2 -2
- package/dist/assets/MarketplacePage-DGfzg1LG.js +1 -0
- package/dist/assets/{McpMarketplacePage-JnkYwK7p.js → McpMarketplacePage-mz2_IX1O.js} +2 -2
- package/dist/assets/{ModelConfig-BYRhgp0c.js → ModelConfig-C_49_a9v.js} +1 -1
- package/dist/assets/{ProvidersList-DmLyyHvX.js → ProvidersList-B0RCb_Vg.js} +1 -1
- package/dist/assets/{RemoteAccessPage-CDSSvH7Z.js → RemoteAccessPage-CcfQjLtx.js} +1 -1
- package/dist/assets/RuntimeConfig-DBWzwoY-.js +1 -0
- package/dist/assets/{SearchConfig-D5f1EkLE.js → SearchConfig-jSdwlH4b.js} +1 -1
- package/dist/assets/{SecretsConfig-D61IKcYt.js → SecretsConfig-DbiS3txa.js} +1 -1
- package/dist/assets/{SessionsConfig-BRIxVTEv.js → SessionsConfig-CbIOcAp8.js} +2 -2
- package/dist/assets/{book-open-CXoF5nQC.js → book-open-BLxSL7Dk.js} +1 -1
- package/dist/assets/chat-session-display-8yW6-mtm.js +1 -0
- package/dist/assets/{chunk-JZWAC4HX-CvRWvTy5.js → chunk-JZWAC4HX-Bp0t5xoO.js} +1 -1
- package/dist/assets/{config-DJswxxE8.js → config-C96FWufn.js} +1 -1
- package/dist/assets/{createLucideIcon-CjGHOWb6.js → createLucideIcon-B_U7Nq4F.js} +1 -1
- package/dist/assets/{dist-Cl2QB-2y.js → dist-BFY-GyT4.js} +1 -1
- package/dist/assets/{dist-nqTTbVdA.js → dist-D9pHzW9z.js} +1 -1
- package/dist/assets/{external-link-tIO7zING.js → external-link-BydIQTIH.js} +1 -1
- package/dist/assets/{hash-JWUyl1pT.js → hash-Djdf0x1C.js} +1 -1
- package/dist/assets/i18n-DAekxt_G.js +1 -0
- package/dist/assets/index-CHEgQIiO.css +1 -0
- package/dist/assets/index-DqSv8Azv.js +6 -0
- package/dist/assets/{label-BIpeNu4r.js → label-Bvv4Mrea.js} +1 -1
- package/dist/assets/loader-circle-CGXXikVG.js +1 -0
- package/dist/assets/{logos-DThdM9lk.js → logos-CGJJRI5_.js} +1 -1
- package/dist/assets/{page-layout-D3Xo605Z.js → page-layout-6Nm4Cnvr.js} +1 -1
- package/dist/assets/plus-CrW9BJDy.js +1 -0
- package/dist/assets/{popover-BJRUGA_H.js → popover-b9rSYI6X.js} +1 -1
- package/dist/assets/{provider-models-bz5y28rq.js → provider-models-IJDA940D.js} +1 -1
- package/dist/assets/{react-7ZHqQtEV.js → react-CDZz_StC.js} +1 -1
- package/dist/assets/{refresh-ccw-CC6-_QuL.js → refresh-ccw-BvSSnnCw.js} +1 -1
- package/dist/assets/{save-DJM5RRWW.js → save-CAf0_-b9.js} +1 -1
- package/dist/assets/search-DgoXxocn.js +1 -0
- package/dist/assets/{security-config-DbUyWcQz.js → security-config-DF66-l25.js} +1 -1
- package/dist/assets/{select-DSkTc61S.js → select-CEIMqc0H.js} +1 -1
- package/dist/assets/skeleton-BiPUQkOD.js +1 -0
- package/dist/assets/{status-dot-LNBlDu3q.js → status-dot-CmQI5Qq2.js} +1 -1
- package/dist/assets/{switch-Bo-Y46HZ.js → switch-B7SxDXyR.js} +1 -1
- package/dist/assets/{tabs-custom-DXv507_2.js → tabs-custom-Dxt6EJJW.js} +1 -1
- package/dist/assets/{trash-2-DFZmW6Gg.js → trash-2-BnQ1PDTw.js} +1 -1
- package/dist/assets/{useConfirmDialog-COwYXDKm.js → useConfirmDialog-B-vMOmhG.js} +1 -1
- package/dist/assets/{useMutation-DrZrOgVL.js → useMutation-Bi39Z9_J.js} +1 -1
- package/dist/assets/x-PBSiWt3l.js +1 -0
- package/dist/index.html +18 -18
- package/package.json +7 -7
- package/src/App.tsx +2 -0
- package/src/api/agents.ts +26 -0
- package/src/api/types.ts +23 -5
- package/src/components/agents/AgentsPage.test.tsx +70 -0
- package/src/components/agents/AgentsPage.tsx +353 -0
- package/src/components/chat/ChatConversationPanel.test.tsx +172 -13
- package/src/components/chat/ChatConversationPanel.tsx +30 -7
- package/src/components/chat/ChatSidebar.test.tsx +48 -0
- package/src/components/chat/ChatSidebar.tsx +11 -0
- package/src/components/chat/ChatWelcome.test.tsx +30 -0
- package/src/components/chat/ChatWelcome.tsx +50 -1
- package/src/components/chat/adapters/chat-message-part.adapter.ts +5 -0
- package/src/components/chat/adapters/chat-message-tool-agent-id.test.ts +102 -0
- package/src/components/chat/adapters/chat-message-tool-agent-id.ts +47 -0
- package/src/components/chat/adapters/chat-message.adapter.test.ts +6 -0
- package/src/components/chat/adapters/chat-message.session-request-tool-card.ts +24 -15
- package/src/components/chat/chat-child-session-panel.tsx +115 -49
- package/src/components/chat/chat-page-runtime.test.ts +30 -0
- package/src/components/chat/chat-page-shell.tsx +8 -17
- package/src/components/chat/chat-session-route.ts +0 -14
- package/src/components/chat/chat-sidebar-session-item.tsx +20 -1
- package/src/components/chat/containers/chat-input-bar.container.tsx +2 -1
- package/src/components/chat/containers/chat-message-list.container.tsx +7 -0
- package/src/components/chat/ncp/NcpChatPage.tsx +77 -158
- package/src/components/chat/ncp/README.md +3 -0
- package/src/components/chat/ncp/ncp-chat-page-data.test.ts +2 -0
- package/src/components/chat/ncp/ncp-chat-thread.manager.ts +66 -10
- package/src/components/chat/ncp/ncp-session-adapter.test.ts +2 -0
- package/src/components/chat/ncp/ncp-session-adapter.ts +1 -0
- package/src/components/chat/ncp/page/ncp-chat-derived-state.ts +128 -0
- package/src/components/chat/ncp/session-conversation/use-ncp-child-session-tabs-view.ts +52 -0
- package/src/components/chat/ncp/session-conversation/use-ncp-session-conversation.test.tsx +101 -0
- package/src/components/chat/ncp/session-conversation/use-ncp-session-conversation.ts +72 -0
- package/src/components/chat/presenter/chat-presenter-context.tsx +1 -0
- package/src/components/chat/stores/chat-thread.store.ts +20 -6
- package/src/components/common/AgentAvatar.tsx +63 -0
- package/src/components/common/agent-identity/agent-identity-avatar.tsx +27 -0
- package/src/components/common/agent-identity/index.ts +3 -0
- package/src/components/common/agent-identity/use-agent-identity.ts +50 -0
- package/src/components/config/RuntimeConfig.tsx +14 -101
- package/src/components/config/runtime-config-agent.utils.ts +95 -0
- package/src/components/layout/AppLayout.tsx +3 -1
- package/src/components/layout/Sidebar.tsx +6 -1
- package/src/components/layout/app-layout.test.tsx +30 -0
- package/src/components/ui/tabs.tsx +2 -0
- package/src/hooks/README.md +3 -0
- package/src/hooks/agents/useAgents.ts +44 -0
- package/src/lib/i18n.agents.ts +66 -0
- package/src/lib/i18n.chat.ts +5 -0
- package/src/lib/i18n.ts +4 -6
- package/src/lib/ui-document-title.ts +1 -0
- package/dist/assets/ChatPage-Z9tRzm_n.js +0 -43
- package/dist/assets/DocBrowser-B9OaZjmg.js +0 -1
- package/dist/assets/MarketplacePage-D6rVQEQR.js +0 -1
- package/dist/assets/RuntimeConfig-v7a7Fe3x.js +0 -1
- package/dist/assets/chat-session-display-D0WpnuRZ.js +0 -1
- package/dist/assets/i18n-CDHMXlRZ.js +0 -1
- package/dist/assets/index-BuwbBgmT.js +0 -6
- package/dist/assets/index-bZ8cqQIS.css +0 -1
- package/dist/assets/loader-circle-Cs8XVFTw.js +0 -1
- package/dist/assets/plus-PHf8q-Ct.js +0 -1
- package/dist/assets/search-C91yH_6y.js +0 -1
- package/dist/assets/skeleton-Dzg-HOiN.js +0 -1
- package/dist/assets/x-D7Q1yqSF.js +0 -1
- /package/src/lib/{i18n → i18n-runtime}/i18n-language-owner.ts +0 -0
- /package/src/lib/{i18n → i18n-runtime}/i18n.path-picker.ts +0 -0
|
@@ -1,48 +1,36 @@
|
|
|
1
1
|
import {
|
|
2
|
-
useCallback,
|
|
3
2
|
useEffect,
|
|
4
3
|
useMemo,
|
|
5
4
|
useRef,
|
|
6
5
|
useState,
|
|
7
|
-
type MutableRefObject,
|
|
8
6
|
} from "react";
|
|
9
|
-
import { NcpHttpAgentClientEndpoint } from "@nextclaw/ncp-http-agent-client";
|
|
10
7
|
import {
|
|
11
8
|
buildNcpRequestEnvelope,
|
|
12
|
-
useHydratedNcpAgent,
|
|
13
|
-
type NcpConversationSeed,
|
|
14
|
-
type NcpConversationSeedLoader,
|
|
15
9
|
} from "@nextclaw/ncp-react";
|
|
16
10
|
import { useLocation, useNavigate, useParams } from "react-router-dom";
|
|
17
|
-
import { API_BASE } from "@/api/api-base";
|
|
18
|
-
import { fetchNcpSessionMessages } from "@/api/ncp-session";
|
|
19
11
|
import {
|
|
20
12
|
ChatPageLayout,
|
|
21
13
|
type ChatPageProps,
|
|
22
14
|
useChatSessionSync,
|
|
23
15
|
} from "@/components/chat/chat-page-shell";
|
|
24
|
-
import { sessionDisplayName } from "@/components/chat/chat-session-display";
|
|
25
16
|
import {
|
|
26
17
|
buildInlineSkillTokensFromComposer,
|
|
27
18
|
CHAT_UI_INLINE_TOKENS_METADATA_KEY,
|
|
28
19
|
} from "@/components/chat/chat-inline-token.utils";
|
|
29
|
-
import { createNcpAppClientFetch } from "@/components/chat/ncp/ncp-app-client-fetch";
|
|
30
20
|
import {
|
|
31
21
|
parseSessionKeyFromRoute,
|
|
32
|
-
resolveAgentIdFromSessionKey,
|
|
33
22
|
} from "@/components/chat/chat-session-route";
|
|
34
23
|
import { useNcpChatPageData } from "@/components/chat/ncp/ncp-chat-page-data";
|
|
35
24
|
import { NcpChatPresenter } from "@/components/chat/ncp/ncp-chat.presenter";
|
|
36
|
-
import {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
} from "@/components/chat/ncp/ncp-session-adapter";
|
|
25
|
+
import { createNcpSessionId } from "@/components/chat/ncp/ncp-session-adapter";
|
|
26
|
+
import { useNcpSessionConversation } from "@/components/chat/ncp/session-conversation/use-ncp-session-conversation";
|
|
27
|
+
import { useNcpChatDerivedState, useNcpChatSnapshotSync } from "@/components/chat/ncp/page/ncp-chat-derived-state";
|
|
40
28
|
import { ChatPresenterProvider } from "@/components/chat/presenter/chat-presenter-context";
|
|
41
29
|
import type { ResumeRunParams } from "@/components/chat/chat-stream/types";
|
|
42
30
|
import { useChatInputStore } from "@/components/chat/stores/chat-input.store";
|
|
43
31
|
import { useChatSessionListStore } from "@/components/chat/stores/chat-session-list.store";
|
|
44
|
-
import { resolveSessionTypeLabel } from "@/components/chat/useChatSessionTypeState";
|
|
45
32
|
import { useConfirmDialog } from "@/hooks/useConfirmDialog";
|
|
33
|
+
import { useAgents } from "@/hooks/agents/useAgents";
|
|
46
34
|
import { normalizeRequestedSkills } from "@/lib/chat-runtime-utils";
|
|
47
35
|
import {
|
|
48
36
|
getSessionProjectName,
|
|
@@ -50,6 +38,7 @@ import {
|
|
|
50
38
|
} from "@/lib/session-project/session-project.utils";
|
|
51
39
|
|
|
52
40
|
export function buildNcpSendMetadata(payload: {
|
|
41
|
+
agentId?: string;
|
|
53
42
|
model?: string;
|
|
54
43
|
thinkingLevel?: string;
|
|
55
44
|
sessionType?: string;
|
|
@@ -69,6 +58,9 @@ export function buildNcpSendMetadata(payload: {
|
|
|
69
58
|
if (payload.sessionType?.trim()) {
|
|
70
59
|
metadata.session_type = payload.sessionType.trim();
|
|
71
60
|
}
|
|
61
|
+
if (payload.agentId?.trim()) {
|
|
62
|
+
metadata.agent_id = payload.agentId.trim();
|
|
63
|
+
}
|
|
72
64
|
const projectRoot = normalizeSessionProjectRootValue(payload.projectRoot);
|
|
73
65
|
if (projectRoot) {
|
|
74
66
|
metadata.project_root = projectRoot;
|
|
@@ -86,48 +78,14 @@ export function buildNcpSendMetadata(payload: {
|
|
|
86
78
|
return metadata;
|
|
87
79
|
}
|
|
88
80
|
|
|
89
|
-
function
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
return
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
sessionId: string;
|
|
98
|
-
status?: string;
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
function useNcpConversationSeedLoader<T extends NcpSeedSessionSummary>(
|
|
102
|
-
sessionSummariesRef: MutableRefObject<readonly T[]>,
|
|
103
|
-
): NcpConversationSeedLoader {
|
|
104
|
-
return useCallback(
|
|
105
|
-
async (
|
|
106
|
-
sessionId: string,
|
|
107
|
-
signal: AbortSignal,
|
|
108
|
-
): Promise<NcpConversationSeed> => {
|
|
109
|
-
signal.throwIfAborted();
|
|
110
|
-
let history: Awaited<ReturnType<typeof fetchNcpSessionMessages>> | null =
|
|
111
|
-
null;
|
|
112
|
-
try {
|
|
113
|
-
history = await fetchNcpSessionMessages(sessionId, 300);
|
|
114
|
-
} catch (error) {
|
|
115
|
-
if (!isMissingNcpSessionError(error)) {
|
|
116
|
-
throw error;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
signal.throwIfAborted();
|
|
120
|
-
|
|
121
|
-
const sessionSummary =
|
|
122
|
-
sessionSummariesRef.current.find(
|
|
123
|
-
(item) => item.sessionId === sessionId,
|
|
124
|
-
) ?? null;
|
|
125
|
-
return {
|
|
126
|
-
messages: history?.messages ?? [],
|
|
127
|
-
status: sessionSummary?.status === "running" ? "running" : "idle",
|
|
128
|
-
};
|
|
129
|
-
},
|
|
130
|
-
[sessionSummariesRef],
|
|
81
|
+
export function shouldRefreshDraftSessionId(params: {
|
|
82
|
+
previousSelectedSessionKey: string | null | undefined;
|
|
83
|
+
nextSelectedSessionKey: string | null;
|
|
84
|
+
}): boolean {
|
|
85
|
+
return (
|
|
86
|
+
params.nextSelectedSessionKey === null &&
|
|
87
|
+
params.previousSelectedSessionKey !== undefined &&
|
|
88
|
+
params.previousSelectedSessionKey !== null
|
|
131
89
|
);
|
|
132
90
|
}
|
|
133
91
|
|
|
@@ -152,6 +110,7 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
152
110
|
const pendingProjectRootSessionKey = useChatInputStore(
|
|
153
111
|
(state) => state.snapshot.pendingProjectRootSessionKey,
|
|
154
112
|
);
|
|
113
|
+
const agentsQuery = useAgents();
|
|
155
114
|
const currentSelectedModel = useChatInputStore(
|
|
156
115
|
(state) => state.snapshot.selectedModel,
|
|
157
116
|
);
|
|
@@ -163,6 +122,9 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
163
122
|
}>();
|
|
164
123
|
const threadRef = useRef<HTMLDivElement | null>(null);
|
|
165
124
|
const selectedSessionKeyRef = useRef<string | null>(selectedSessionKey);
|
|
125
|
+
const previousSelectedSessionKeyRef = useRef<string | null | undefined>(
|
|
126
|
+
undefined,
|
|
127
|
+
);
|
|
166
128
|
const routeSessionKey = useMemo(
|
|
167
129
|
() => parseSessionKeyFromRoute(routeSessionIdParam),
|
|
168
130
|
[routeSessionIdParam],
|
|
@@ -198,38 +160,25 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
198
160
|
presenter.chatInputManager.setSelectedThinkingLevel,
|
|
199
161
|
});
|
|
200
162
|
|
|
201
|
-
const
|
|
202
|
-
useEffect(() => {
|
|
203
|
-
sessionSummariesRef.current = sessionSummaries;
|
|
204
|
-
}, [sessionSummaries]);
|
|
205
|
-
|
|
206
|
-
const [ncpClient] = useState(
|
|
207
|
-
() =>
|
|
208
|
-
new NcpHttpAgentClientEndpoint({
|
|
209
|
-
baseUrl: API_BASE,
|
|
210
|
-
basePath: "/api/ncp/agent",
|
|
211
|
-
fetchImpl: createNcpAppClientFetch(),
|
|
212
|
-
}),
|
|
213
|
-
);
|
|
214
|
-
|
|
215
|
-
const loadSeed = useNcpConversationSeedLoader(sessionSummariesRef);
|
|
216
|
-
|
|
217
|
-
const agent = useHydratedNcpAgent({
|
|
218
|
-
sessionId: sessionKey,
|
|
219
|
-
client: ncpClient,
|
|
220
|
-
loadSeed,
|
|
221
|
-
});
|
|
163
|
+
const agent = useNcpSessionConversation(sessionKey);
|
|
222
164
|
|
|
223
165
|
useEffect(() => {
|
|
224
166
|
presenter.setDraftSessionId(draftSessionId);
|
|
225
167
|
}, [draftSessionId, presenter]);
|
|
226
168
|
|
|
227
169
|
useEffect(() => {
|
|
228
|
-
if (
|
|
170
|
+
if (
|
|
171
|
+
shouldRefreshDraftSessionId({
|
|
172
|
+
previousSelectedSessionKey:
|
|
173
|
+
previousSelectedSessionKeyRef.current,
|
|
174
|
+
nextSelectedSessionKey: selectedSessionKey,
|
|
175
|
+
})
|
|
176
|
+
) {
|
|
229
177
|
const nextDraftSessionId = createNcpSessionId();
|
|
230
178
|
setDraftSessionId(nextDraftSessionId);
|
|
231
179
|
presenter.setDraftSessionId(nextDraftSessionId);
|
|
232
180
|
}
|
|
181
|
+
previousSelectedSessionKeyRef.current = selectedSessionKey;
|
|
233
182
|
}, [presenter, selectedSessionKey]);
|
|
234
183
|
|
|
235
184
|
const effectiveSessionProjectRoot = hasSessionProjectRootOverride
|
|
@@ -255,6 +204,7 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
255
204
|
return;
|
|
256
205
|
}
|
|
257
206
|
const metadata = buildNcpSendMetadata({
|
|
207
|
+
agentId: payload.agentId,
|
|
258
208
|
model: payload.model,
|
|
259
209
|
thinkingLevel: payload.thinkingLevel,
|
|
260
210
|
sessionType: payload.sessionType,
|
|
@@ -336,13 +286,10 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
336
286
|
view,
|
|
337
287
|
routeSessionKey,
|
|
338
288
|
selectedSessionKey,
|
|
339
|
-
selectedAgentId,
|
|
340
289
|
setSelectedSessionKey:
|
|
341
290
|
presenter.chatSessionListManager.setSelectedSessionKey,
|
|
342
|
-
setSelectedAgentId: presenter.chatSessionListManager.setSelectedAgentId,
|
|
343
291
|
selectedSessionKeyRef,
|
|
344
292
|
resetStreamState: presenter.chatStreamActionsManager.resetStreamState,
|
|
345
|
-
resolveAgentIdFromSessionKey,
|
|
346
293
|
});
|
|
347
294
|
|
|
348
295
|
useEffect(() => {
|
|
@@ -355,90 +302,62 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
355
302
|
});
|
|
356
303
|
}, [confirm, location.pathname, navigate, presenter]);
|
|
357
304
|
|
|
358
|
-
const
|
|
359
|
-
?
|
|
360
|
-
:
|
|
361
|
-
const
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
305
|
+
const availableAgents = (agentsQuery.data?.agents?.length ?? 0) > 0
|
|
306
|
+
? (agentsQuery.data?.agents ?? [])
|
|
307
|
+
: [{ id: selectedSession?.agentId ?? selectedAgentId }];
|
|
308
|
+
const {
|
|
309
|
+
currentSessionDisplayName,
|
|
310
|
+
currentAgentId,
|
|
311
|
+
currentAgent,
|
|
312
|
+
parentSession,
|
|
313
|
+
currentSessionTypeLabel
|
|
314
|
+
} = useNcpChatDerivedState({
|
|
315
|
+
selectedSession,
|
|
316
|
+
selectedAgentId,
|
|
317
|
+
availableAgents,
|
|
318
|
+
parentSessionId,
|
|
319
|
+
sessionSummaries,
|
|
320
|
+
selectedSessionType,
|
|
321
|
+
sessionTypeOptions
|
|
322
|
+
});
|
|
374
323
|
|
|
375
324
|
useEffect(() => {
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
isSending,
|
|
385
|
-
modelOptions,
|
|
386
|
-
sessionTypeOptions,
|
|
387
|
-
selectedSessionType,
|
|
388
|
-
canEditSessionType,
|
|
389
|
-
sessionTypeUnavailable,
|
|
390
|
-
skillRecords,
|
|
391
|
-
isSkillsLoading: sessionSkillsQuery.isLoading,
|
|
392
|
-
});
|
|
393
|
-
presenter.chatThreadManager.syncSnapshot({
|
|
394
|
-
isProviderStateResolved,
|
|
395
|
-
modelOptions,
|
|
396
|
-
sessionTypeUnavailable,
|
|
397
|
-
sessionTypeUnavailableMessage,
|
|
398
|
-
sessionTypeLabel: currentSessionTypeLabel,
|
|
399
|
-
sessionKey,
|
|
400
|
-
sessionDisplayName: currentSessionDisplayName,
|
|
401
|
-
sessionProjectRoot: effectiveSessionProjectRoot,
|
|
402
|
-
sessionProjectName: effectiveSessionProjectName,
|
|
403
|
-
canDeleteSession: Boolean(selectedSession),
|
|
404
|
-
threadRef,
|
|
405
|
-
isHistoryLoading: agent.isHydrating,
|
|
406
|
-
messages: agent.visibleMessages,
|
|
407
|
-
isSending,
|
|
408
|
-
isAwaitingAssistantOutput,
|
|
409
|
-
parentSessionKey: parentSession?.key ?? null,
|
|
410
|
-
parentSessionLabel: parentSession
|
|
411
|
-
? sessionDisplayName(parentSession)
|
|
412
|
-
: null,
|
|
413
|
-
});
|
|
414
|
-
}, [
|
|
415
|
-
agent.isHydrating,
|
|
416
|
-
canEditSessionType,
|
|
417
|
-
canStopCurrentRun,
|
|
418
|
-
currentSessionDisplayName,
|
|
419
|
-
currentSessionTypeLabel,
|
|
420
|
-
defaultSessionType,
|
|
421
|
-
sessionSkillsQuery.isLoading,
|
|
422
|
-
isAwaitingAssistantOutput,
|
|
325
|
+
if (!selectedSession?.agentId || selectedAgentId === selectedSession.agentId) {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
presenter.chatSessionListManager.setSelectedAgentId(selectedSession.agentId);
|
|
329
|
+
}, [presenter, selectedAgentId, selectedSession?.agentId]);
|
|
330
|
+
|
|
331
|
+
useNcpChatSnapshotSync({
|
|
332
|
+
presenter,
|
|
423
333
|
isProviderStateResolved,
|
|
424
|
-
|
|
334
|
+
defaultSessionType,
|
|
335
|
+
canStopCurrentRun,
|
|
336
|
+
stopDisabledReason,
|
|
425
337
|
lastSendError,
|
|
338
|
+
isSending,
|
|
426
339
|
modelOptions,
|
|
427
|
-
parentSession,
|
|
428
|
-
presenter,
|
|
429
|
-
effectiveSessionProjectName,
|
|
430
|
-
effectiveSessionProjectRoot,
|
|
431
|
-
selectedSession,
|
|
432
|
-
sessionKey,
|
|
433
|
-
selectedSessionType,
|
|
434
340
|
sessionTypeOptions,
|
|
341
|
+
selectedSessionType,
|
|
342
|
+
canEditSessionType,
|
|
435
343
|
sessionTypeUnavailable,
|
|
436
|
-
sessionTypeUnavailableMessage,
|
|
437
344
|
skillRecords,
|
|
438
|
-
|
|
345
|
+
isSkillsLoading: sessionSkillsQuery.isLoading,
|
|
346
|
+
sessionTypeUnavailableMessage,
|
|
347
|
+
currentSessionTypeLabel,
|
|
348
|
+
sessionKey,
|
|
349
|
+
currentAgentId,
|
|
350
|
+
currentAgent,
|
|
351
|
+
availableAgents,
|
|
352
|
+
currentSessionDisplayName,
|
|
353
|
+
effectiveSessionProjectRoot,
|
|
354
|
+
effectiveSessionProjectName,
|
|
355
|
+
selectedSession,
|
|
439
356
|
threadRef,
|
|
440
|
-
agent
|
|
441
|
-
|
|
357
|
+
agent,
|
|
358
|
+
isAwaitingAssistantOutput,
|
|
359
|
+
parentSession
|
|
360
|
+
});
|
|
442
361
|
|
|
443
362
|
return (
|
|
444
363
|
<ChatPresenterProvider presenter={presenter}>
|
|
@@ -48,10 +48,12 @@ describe('buildNcpSendMetadata', () => {
|
|
|
48
48
|
it('includes the project root in the first-message metadata when present', () => {
|
|
49
49
|
expect(
|
|
50
50
|
buildNcpSendMetadata({
|
|
51
|
+
agentId: 'engineer',
|
|
51
52
|
sessionType: 'codex',
|
|
52
53
|
projectRoot: ' /tmp/project-alpha ',
|
|
53
54
|
}),
|
|
54
55
|
).toMatchObject({
|
|
56
|
+
agent_id: 'engineer',
|
|
55
57
|
session_type: 'codex',
|
|
56
58
|
project_root: '/tmp/project-alpha',
|
|
57
59
|
});
|
|
@@ -6,7 +6,10 @@ import type { ChatSessionListManager } from '@/components/chat/managers/chat-ses
|
|
|
6
6
|
import type { ChatStreamActionsManager } from '@/components/chat/managers/chat-stream-actions.manager';
|
|
7
7
|
import type { ChatUiManager } from '@/components/chat/managers/chat-ui.manager';
|
|
8
8
|
import { useChatSessionListStore } from '@/components/chat/stores/chat-session-list.store';
|
|
9
|
-
import type {
|
|
9
|
+
import type {
|
|
10
|
+
ChatChildSessionTab,
|
|
11
|
+
ChatThreadSnapshot,
|
|
12
|
+
} from '@/components/chat/stores/chat-thread.store';
|
|
10
13
|
import { useChatThreadStore } from '@/components/chat/stores/chat-thread.store';
|
|
11
14
|
import { t } from '@/lib/i18n';
|
|
12
15
|
|
|
@@ -46,6 +49,23 @@ export class NcpChatThreadManager {
|
|
|
46
49
|
this.uiManager.goToProviders();
|
|
47
50
|
};
|
|
48
51
|
|
|
52
|
+
private upsertChildSessionTab = (tab: ChatChildSessionTab) => {
|
|
53
|
+
const { snapshot } = useChatThreadStore.getState();
|
|
54
|
+
const existingIndex = snapshot.childSessionTabs.findIndex(
|
|
55
|
+
(item) => item.sessionKey === tab.sessionKey,
|
|
56
|
+
);
|
|
57
|
+
const nextTabs =
|
|
58
|
+
existingIndex >= 0
|
|
59
|
+
? snapshot.childSessionTabs.map((item, index) =>
|
|
60
|
+
index === existingIndex ? { ...item, ...tab } : item,
|
|
61
|
+
)
|
|
62
|
+
: [...snapshot.childSessionTabs, tab];
|
|
63
|
+
useChatThreadStore.getState().setSnapshot({
|
|
64
|
+
childSessionTabs: nextTabs,
|
|
65
|
+
activeChildSessionKey: tab.sessionKey,
|
|
66
|
+
});
|
|
67
|
+
};
|
|
68
|
+
|
|
49
69
|
openSessionFromToolAction = (action: ChatToolActionViewModel) => {
|
|
50
70
|
if (action.kind !== 'open-session') {
|
|
51
71
|
return;
|
|
@@ -55,31 +75,67 @@ export class NcpChatThreadManager {
|
|
|
55
75
|
action.parentSessionId?.trim() ||
|
|
56
76
|
useChatSessionListStore.getState().snapshot.selectedSessionKey ||
|
|
57
77
|
null;
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
78
|
+
this.upsertChildSessionTab({
|
|
79
|
+
sessionKey: action.sessionId,
|
|
80
|
+
parentSessionKey,
|
|
81
|
+
label: action.label?.trim() || null,
|
|
82
|
+
agentId: action.agentId?.trim() || null,
|
|
62
83
|
});
|
|
63
84
|
return;
|
|
64
85
|
}
|
|
65
86
|
this.uiManager.goToSession(action.sessionId);
|
|
66
87
|
};
|
|
67
88
|
|
|
89
|
+
selectChildSessionDetail = (sessionKey: string) => {
|
|
90
|
+
const normalizedSessionKey = sessionKey.trim();
|
|
91
|
+
if (!normalizedSessionKey) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const { childSessionTabs } = useChatThreadStore.getState().snapshot;
|
|
95
|
+
if (!childSessionTabs.some((tab) => tab.sessionKey === normalizedSessionKey)) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
useChatThreadStore.getState().setSnapshot({
|
|
99
|
+
activeChildSessionKey: normalizedSessionKey,
|
|
100
|
+
});
|
|
101
|
+
};
|
|
102
|
+
|
|
68
103
|
closeChildSessionDetail = () => {
|
|
104
|
+
const {
|
|
105
|
+
sessionKey,
|
|
106
|
+
childSessionTabs,
|
|
107
|
+
activeChildSessionKey,
|
|
108
|
+
} = useChatThreadStore.getState().snapshot;
|
|
109
|
+
if (!sessionKey) {
|
|
110
|
+
useChatThreadStore.getState().setSnapshot({
|
|
111
|
+
childSessionTabs: [],
|
|
112
|
+
activeChildSessionKey: null,
|
|
113
|
+
});
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const nextTabs = childSessionTabs.filter(
|
|
117
|
+
(tab) => tab.parentSessionKey !== sessionKey,
|
|
118
|
+
);
|
|
119
|
+
const nextActiveKey = nextTabs.some((tab) => tab.sessionKey === activeChildSessionKey)
|
|
120
|
+
? activeChildSessionKey
|
|
121
|
+
: null;
|
|
69
122
|
useChatThreadStore.getState().setSnapshot({
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
childSessionDetailLabel: null,
|
|
123
|
+
childSessionTabs: nextTabs,
|
|
124
|
+
activeChildSessionKey: nextActiveKey,
|
|
73
125
|
});
|
|
74
126
|
};
|
|
75
127
|
|
|
76
128
|
goToParentSession = () => {
|
|
77
129
|
const {
|
|
78
130
|
parentSessionKey,
|
|
79
|
-
|
|
131
|
+
childSessionTabs,
|
|
132
|
+
activeChildSessionKey,
|
|
80
133
|
} = useChatThreadStore.getState().snapshot;
|
|
134
|
+
const activeChildParentSessionKey =
|
|
135
|
+
childSessionTabs.find((tab) => tab.sessionKey === activeChildSessionKey)
|
|
136
|
+
?.parentSessionKey ?? null;
|
|
81
137
|
const resolvedParentSessionKey =
|
|
82
|
-
parentSessionKey ??
|
|
138
|
+
parentSessionKey ?? activeChildParentSessionKey;
|
|
83
139
|
if (!resolvedParentSessionKey) {
|
|
84
140
|
return;
|
|
85
141
|
}
|
|
@@ -20,6 +20,7 @@ describe('adaptNcpSessionSummary', () => {
|
|
|
20
20
|
it('maps session metadata into shared session entry fields', () => {
|
|
21
21
|
const adapted = adaptNcpSessionSummary(
|
|
22
22
|
createSummary({
|
|
23
|
+
agentId: 'engineer',
|
|
23
24
|
metadata: {
|
|
24
25
|
label: 'NCP Planning Thread',
|
|
25
26
|
model: 'openai/gpt-5',
|
|
@@ -32,6 +33,7 @@ describe('adaptNcpSessionSummary', () => {
|
|
|
32
33
|
|
|
33
34
|
expect(adapted).toMatchObject({
|
|
34
35
|
key: 'ncp-session-1',
|
|
36
|
+
agentId: 'engineer',
|
|
35
37
|
label: 'NCP Planning Thread',
|
|
36
38
|
preferredModel: 'openai/gpt-5',
|
|
37
39
|
preferredThinking: 'medium',
|
|
@@ -253,6 +253,7 @@ export function adaptNcpSessionSummary(summary: NcpSessionSummaryView): SessionE
|
|
|
253
253
|
key: summary.sessionId,
|
|
254
254
|
createdAt: summary.updatedAt,
|
|
255
255
|
updatedAt: summary.updatedAt,
|
|
256
|
+
...(typeof summary.agentId === 'string' && summary.agentId.trim().length > 0 ? { agentId: summary.agentId.trim() } : {}),
|
|
256
257
|
...(label ? { label } : {}),
|
|
257
258
|
...context,
|
|
258
259
|
...(preferredModel ? { preferredModel } : {}),
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { useEffect, useMemo, type MutableRefObject } from 'react';
|
|
2
|
+
import type {
|
|
3
|
+
AgentProfileView,
|
|
4
|
+
NcpSessionSummaryView,
|
|
5
|
+
SessionEntryView,
|
|
6
|
+
SessionSkillEntryView
|
|
7
|
+
} from '@/api/types';
|
|
8
|
+
import { sessionDisplayName } from '@/components/chat/chat-session-display';
|
|
9
|
+
import { adaptNcpSessionSummary } from '@/components/chat/ncp/ncp-session-adapter';
|
|
10
|
+
import type { NcpChatPresenter } from '@/components/chat/ncp/ncp-chat.presenter';
|
|
11
|
+
import type { UseHydratedNcpAgentResult } from '@nextclaw/ncp-react';
|
|
12
|
+
import type { ChatModelOption } from '@/components/chat/chat-input.types';
|
|
13
|
+
import { resolveSessionTypeLabel } from '@/components/chat/useChatSessionTypeState';
|
|
14
|
+
export function useNcpChatDerivedState(params: {
|
|
15
|
+
selectedSession: SessionEntryView | null;
|
|
16
|
+
selectedAgentId: string;
|
|
17
|
+
availableAgents: AgentProfileView[];
|
|
18
|
+
parentSessionId: string | null;
|
|
19
|
+
sessionSummaries: NcpSessionSummaryView[];
|
|
20
|
+
selectedSessionType: string;
|
|
21
|
+
sessionTypeOptions: Array<{ value: string; label: string }>;
|
|
22
|
+
}) {
|
|
23
|
+
const currentSessionDisplayName = params.selectedSession
|
|
24
|
+
? sessionDisplayName(params.selectedSession)
|
|
25
|
+
: undefined;
|
|
26
|
+
const currentAgentId = params.selectedSession?.agentId ?? params.selectedAgentId;
|
|
27
|
+
const currentAgent =
|
|
28
|
+
params.availableAgents.find((agent) => agent.id === currentAgentId) ?? null;
|
|
29
|
+
const parentSession = useMemo(() => {
|
|
30
|
+
if (!params.parentSessionId) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
const parentSummary =
|
|
34
|
+
params.sessionSummaries.find(
|
|
35
|
+
(summary) => summary.sessionId === params.parentSessionId,
|
|
36
|
+
) ?? null;
|
|
37
|
+
return parentSummary ? adaptNcpSessionSummary(parentSummary) : null;
|
|
38
|
+
}, [params.parentSessionId, params.sessionSummaries]);
|
|
39
|
+
const currentSessionTypeLabel =
|
|
40
|
+
params.sessionTypeOptions.find((option) => option.value === params.selectedSessionType)
|
|
41
|
+
?.label ?? resolveSessionTypeLabel(params.selectedSessionType);
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
currentSessionDisplayName,
|
|
45
|
+
currentAgentId,
|
|
46
|
+
currentAgent,
|
|
47
|
+
parentSession,
|
|
48
|
+
currentSessionTypeLabel
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function useNcpChatSnapshotSync(params: {
|
|
53
|
+
presenter: NcpChatPresenter;
|
|
54
|
+
isProviderStateResolved: boolean;
|
|
55
|
+
defaultSessionType: string;
|
|
56
|
+
canStopCurrentRun: boolean;
|
|
57
|
+
stopDisabledReason: string | null;
|
|
58
|
+
lastSendError: string | null;
|
|
59
|
+
isSending: boolean;
|
|
60
|
+
modelOptions: ChatModelOption[];
|
|
61
|
+
sessionTypeOptions: Array<{ value: string; label: string }>;
|
|
62
|
+
selectedSessionType: string;
|
|
63
|
+
canEditSessionType: boolean;
|
|
64
|
+
sessionTypeUnavailable: boolean;
|
|
65
|
+
skillRecords: SessionSkillEntryView[];
|
|
66
|
+
isSkillsLoading: boolean;
|
|
67
|
+
sessionTypeUnavailableMessage: string | null;
|
|
68
|
+
currentSessionTypeLabel: string;
|
|
69
|
+
sessionKey: string;
|
|
70
|
+
currentAgentId: string;
|
|
71
|
+
currentAgent: AgentProfileView | null;
|
|
72
|
+
availableAgents: AgentProfileView[];
|
|
73
|
+
currentSessionDisplayName?: string;
|
|
74
|
+
effectiveSessionProjectRoot: string | null;
|
|
75
|
+
effectiveSessionProjectName: string | null;
|
|
76
|
+
selectedSession: SessionEntryView | null;
|
|
77
|
+
threadRef: MutableRefObject<HTMLDivElement | null>;
|
|
78
|
+
agent: Pick<UseHydratedNcpAgentResult, 'isHydrating' | 'visibleMessages'>;
|
|
79
|
+
isAwaitingAssistantOutput: boolean;
|
|
80
|
+
parentSession: SessionEntryView | null;
|
|
81
|
+
}) {
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
params.presenter.chatInputManager.syncSnapshot({
|
|
84
|
+
isProviderStateResolved: params.isProviderStateResolved,
|
|
85
|
+
defaultSessionType: params.defaultSessionType,
|
|
86
|
+
canStopGeneration: params.canStopCurrentRun,
|
|
87
|
+
stopDisabledReason: params.stopDisabledReason,
|
|
88
|
+
stopSupported: true,
|
|
89
|
+
stopReason: undefined,
|
|
90
|
+
sendError: params.lastSendError,
|
|
91
|
+
isSending: params.isSending,
|
|
92
|
+
modelOptions: params.modelOptions,
|
|
93
|
+
sessionTypeOptions: params.sessionTypeOptions,
|
|
94
|
+
selectedSessionType: params.selectedSessionType,
|
|
95
|
+
canEditSessionType: params.canEditSessionType,
|
|
96
|
+
sessionTypeUnavailable: params.sessionTypeUnavailable,
|
|
97
|
+
skillRecords: params.skillRecords,
|
|
98
|
+
isSkillsLoading: params.isSkillsLoading,
|
|
99
|
+
});
|
|
100
|
+
params.presenter.chatThreadManager.syncSnapshot({
|
|
101
|
+
isProviderStateResolved: params.isProviderStateResolved,
|
|
102
|
+
modelOptions: params.modelOptions,
|
|
103
|
+
sessionTypeUnavailable: params.sessionTypeUnavailable,
|
|
104
|
+
sessionTypeUnavailableMessage: params.sessionTypeUnavailableMessage,
|
|
105
|
+
sessionTypeLabel: params.currentSessionTypeLabel,
|
|
106
|
+
sessionKey: params.sessionKey,
|
|
107
|
+
agentId: params.currentAgentId,
|
|
108
|
+
agentDisplayName: params.currentAgent?.displayName ?? null,
|
|
109
|
+
agentAvatarUrl: params.currentAgent?.avatarUrl ?? null,
|
|
110
|
+
availableAgents: params.availableAgents,
|
|
111
|
+
sessionDisplayName: params.currentSessionDisplayName,
|
|
112
|
+
sessionProjectRoot: params.effectiveSessionProjectRoot,
|
|
113
|
+
sessionProjectName: params.effectiveSessionProjectName,
|
|
114
|
+
canDeleteSession: Boolean(params.selectedSession),
|
|
115
|
+
threadRef: params.threadRef,
|
|
116
|
+
isHistoryLoading: params.agent.isHydrating,
|
|
117
|
+
messages: params.agent.visibleMessages,
|
|
118
|
+
isSending: params.isSending,
|
|
119
|
+
isAwaitingAssistantOutput: params.isAwaitingAssistantOutput,
|
|
120
|
+
parentSessionKey: params.parentSession?.key ?? null,
|
|
121
|
+
parentSessionLabel: params.parentSession
|
|
122
|
+
? sessionDisplayName(params.parentSession)
|
|
123
|
+
: null,
|
|
124
|
+
});
|
|
125
|
+
}, [
|
|
126
|
+
params
|
|
127
|
+
]);
|
|
128
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import type { SessionEntryView } from '@/api/types';
|
|
3
|
+
import { sessionDisplayName } from '@/components/chat/chat-session-display';
|
|
4
|
+
import { adaptNcpSessionSummaries } from '@/components/chat/ncp/ncp-session-adapter';
|
|
5
|
+
import type { ChatChildSessionTab } from '@/components/chat/stores/chat-thread.store';
|
|
6
|
+
import { useNcpSessions } from '@/hooks/useConfig';
|
|
7
|
+
|
|
8
|
+
export type ResolvedChildSessionTab = {
|
|
9
|
+
sessionKey: string;
|
|
10
|
+
parentSessionKey: string | null;
|
|
11
|
+
title: string;
|
|
12
|
+
agentId: string | null;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function resolveChildSessionTitle(
|
|
16
|
+
tab: ChatChildSessionTab,
|
|
17
|
+
session: SessionEntryView | null,
|
|
18
|
+
): string {
|
|
19
|
+
if (tab.label?.trim()) {
|
|
20
|
+
return tab.label.trim();
|
|
21
|
+
}
|
|
22
|
+
if (session) {
|
|
23
|
+
return sessionDisplayName(session);
|
|
24
|
+
}
|
|
25
|
+
return tab.sessionKey;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function useNcpChildSessionTabsView(
|
|
29
|
+
tabs: readonly ChatChildSessionTab[],
|
|
30
|
+
): ResolvedChildSessionTab[] {
|
|
31
|
+
const sessionsQuery = useNcpSessions({ limit: 200 });
|
|
32
|
+
|
|
33
|
+
const sessionByKey = useMemo(() => {
|
|
34
|
+
const sessions = adaptNcpSessionSummaries(sessionsQuery.data?.sessions ?? []);
|
|
35
|
+
return new Map(sessions.map((session) => [session.key, session]));
|
|
36
|
+
}, [sessionsQuery.data?.sessions]);
|
|
37
|
+
|
|
38
|
+
return useMemo(
|
|
39
|
+
() =>
|
|
40
|
+
tabs.map((tab) => {
|
|
41
|
+
const session = sessionByKey.get(tab.sessionKey) ?? null;
|
|
42
|
+
const agentId = tab.agentId?.trim() || session?.agentId || null;
|
|
43
|
+
return {
|
|
44
|
+
sessionKey: tab.sessionKey,
|
|
45
|
+
parentSessionKey: tab.parentSessionKey,
|
|
46
|
+
title: resolveChildSessionTitle(tab, session),
|
|
47
|
+
agentId,
|
|
48
|
+
};
|
|
49
|
+
}),
|
|
50
|
+
[sessionByKey, tabs],
|
|
51
|
+
);
|
|
52
|
+
}
|