@nextclaw/ui 0.11.22 → 0.11.23
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 +10 -0
- package/dist/assets/{ChannelsList-Zeys_w43.js → ChannelsList-DVDu1xvz.js} +1 -1
- package/dist/assets/ChatPage-Z9tRzm_n.js +43 -0
- package/dist/assets/{MarketplacePage-Cd4faegU.js → MarketplacePage-Buo9HrOz.js} +1 -1
- package/dist/assets/MarketplacePage-D6rVQEQR.js +1 -0
- package/dist/assets/{McpMarketplacePage-C09Ngs7O.js → McpMarketplacePage-JnkYwK7p.js} +1 -1
- package/dist/assets/{ModelConfig-DJgdcgvQ.js → ModelConfig-BYRhgp0c.js} +1 -1
- package/dist/assets/{ProvidersList-w0rVFIBf.js → ProvidersList-DmLyyHvX.js} +1 -1
- package/dist/assets/{RemoteAccessPage-BJ_ckkOV.js → RemoteAccessPage-CDSSvH7Z.js} +1 -1
- package/dist/assets/{RuntimeConfig-Cmn2xPQO.js → RuntimeConfig-v7a7Fe3x.js} +1 -1
- package/dist/assets/{SearchConfig-BT13qpR_.js → SearchConfig-D5f1EkLE.js} +1 -1
- package/dist/assets/{SecretsConfig-CvqEVn0B.js → SecretsConfig-D61IKcYt.js} +1 -1
- package/dist/assets/{SessionsConfig-DHHcYznk.js → SessionsConfig-BRIxVTEv.js} +2 -2
- package/dist/assets/chat-session-display-D0WpnuRZ.js +1 -0
- package/dist/assets/{index-C6d0xmtm.js → index-BuwbBgmT.js} +2 -2
- package/dist/assets/index-bZ8cqQIS.css +1 -0
- package/dist/assets/{security-config-T5zpg16O.js → security-config-DbUyWcQz.js} +1 -1
- package/dist/assets/{useConfirmDialog-Bs5Ll17m.js → useConfirmDialog-COwYXDKm.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +6 -6
- package/src/api/types.ts +4 -0
- package/src/components/chat/ChatConversationPanel.test.tsx +10 -2
- package/src/components/chat/ChatConversationPanel.tsx +114 -77
- package/src/components/chat/adapters/chat-message-part.adapter.ts +13 -5
- package/src/components/chat/adapters/chat-message.adapter.test.ts +83 -9
- package/src/components/chat/adapters/chat-message.session-request-tool-card.ts +191 -0
- package/src/components/chat/chat-child-session-panel.tsx +100 -0
- package/src/components/chat/chat-page-runtime.test.ts +1 -0
- package/src/components/chat/chat-session-display.test.ts +1 -0
- package/src/components/chat/containers/chat-message-list.container.tsx +4 -0
- package/src/components/chat/ncp/NcpChatPage.tsx +179 -114
- package/src/components/chat/ncp/ncp-chat-thread.manager.ts +49 -0
- package/src/components/chat/ncp/ncp-session-adapter.test.ts +21 -0
- package/src/components/chat/ncp/ncp-session-adapter.ts +31 -0
- package/src/components/chat/ncp/use-ncp-session-list-view.ts +10 -1
- package/src/components/chat/presenter/chat-presenter-context.tsx +4 -1
- package/src/components/chat/stores/chat-thread.store.ts +11 -1
- package/src/components/chat/useHydratedNcpAgent.test.tsx +30 -23
- package/dist/assets/ChatPage-DWOU_8P6.js +0 -43
- package/dist/assets/MarketplacePage-BfaTTqN6.js +0 -1
- package/dist/assets/chat-session-display-VW6ZMvZP.js +0 -1
- package/dist/assets/index-BlH4-cBw.css +0 -1
- package/src/components/chat/adapters/chat-message.subagent-tool-card.ts +0 -154
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { useRef } from "react";
|
|
2
|
+
import { ArrowLeft } from "lucide-react";
|
|
2
3
|
import { useStickyBottomScroll } from "@nextclaw/agent-chat-ui";
|
|
3
4
|
import {
|
|
4
5
|
ChatInputBarContainer,
|
|
5
6
|
ChatMessageListContainer,
|
|
6
7
|
} from "@/components/chat/nextclaw";
|
|
8
|
+
import { ChatChildSessionPanel } from "@/components/chat/chat-child-session-panel";
|
|
7
9
|
import { ChatWelcome } from "@/components/chat/ChatWelcome";
|
|
8
10
|
import { usePresenter } from "@/components/chat/presenter/chat-presenter-context";
|
|
9
11
|
import { ChatSessionHeaderActions } from "@/components/chat/session-header/chat-session-header-actions";
|
|
@@ -45,6 +47,10 @@ export function ChatConversationPanel() {
|
|
|
45
47
|
const snapshot = useChatThreadStore((state) => state.snapshot);
|
|
46
48
|
const fallbackThreadRef = useRef<HTMLDivElement | null>(null);
|
|
47
49
|
const threadRef = snapshot.threadRef ?? fallbackThreadRef;
|
|
50
|
+
const detailSessionKey =
|
|
51
|
+
snapshot.childSessionDetailParentSessionKey === snapshot.sessionKey
|
|
52
|
+
? snapshot.childSessionDetailSessionKey
|
|
53
|
+
: null;
|
|
48
54
|
const shouldShowSessionHeader = Boolean(
|
|
49
55
|
snapshot.sessionKey || snapshot.sessionTypeLabel,
|
|
50
56
|
);
|
|
@@ -79,97 +85,128 @@ export function ChatConversationPanel() {
|
|
|
79
85
|
}
|
|
80
86
|
|
|
81
87
|
return (
|
|
82
|
-
<section className="flex-1 min-h-0 flex
|
|
83
|
-
<div
|
|
84
|
-
|
|
85
|
-
"
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
88
|
+
<section className="flex-1 min-h-0 flex overflow-hidden bg-gradient-to-b from-gray-50/60 to-white">
|
|
89
|
+
<div className="flex min-h-0 flex-1 flex-col overflow-hidden">
|
|
90
|
+
{snapshot.parentSessionKey ? (
|
|
91
|
+
<div className="border-b border-gray-200/60 bg-white/75 px-5 py-2 backdrop-blur-sm">
|
|
92
|
+
<button
|
|
93
|
+
type="button"
|
|
94
|
+
onClick={presenter.chatThreadManager.goToParentSession}
|
|
95
|
+
className="inline-flex items-center gap-2 text-xs font-medium text-gray-600 transition-colors hover:text-gray-900"
|
|
96
|
+
>
|
|
97
|
+
<ArrowLeft className="h-3.5 w-3.5" />
|
|
98
|
+
<span>
|
|
99
|
+
Back to parent
|
|
100
|
+
{snapshot.parentSessionLabel?.trim()
|
|
101
|
+
? ` · ${snapshot.parentSessionLabel.trim()}`
|
|
102
|
+
: ""}
|
|
103
|
+
</span>
|
|
104
|
+
</button>
|
|
105
|
+
</div>
|
|
106
|
+
) : null}
|
|
107
|
+
|
|
108
|
+
<div
|
|
109
|
+
className={cn(
|
|
110
|
+
"px-5 border-b border-gray-200/60 bg-white/80 backdrop-blur-sm flex items-center justify-between shrink-0 overflow-hidden transition-all duration-200",
|
|
111
|
+
shouldShowSessionHeader
|
|
112
|
+
? "py-3 opacity-100"
|
|
113
|
+
: "h-0 py-0 opacity-0 border-b-0",
|
|
114
|
+
)}
|
|
115
|
+
>
|
|
116
|
+
<div className="min-w-0 flex-1 flex items-center gap-2">
|
|
117
|
+
<span className="text-sm font-medium text-gray-700 truncate">
|
|
118
|
+
{sessionHeaderTitle}
|
|
98
119
|
</span>
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
120
|
+
{snapshot.sessionTypeLabel ? (
|
|
121
|
+
<span className="shrink-0 rounded-full border border-gray-200 bg-gray-100 px-2 py-0.5 text-[11px] font-medium text-gray-600">
|
|
122
|
+
{snapshot.sessionTypeLabel}
|
|
123
|
+
</span>
|
|
124
|
+
) : null}
|
|
125
|
+
{snapshot.sessionProjectName ? (
|
|
126
|
+
<ChatSessionProjectBadge
|
|
127
|
+
sessionKey={snapshot.sessionKey ?? "draft"}
|
|
128
|
+
projectName={snapshot.sessionProjectName}
|
|
129
|
+
projectRoot={snapshot.sessionProjectRoot}
|
|
130
|
+
persistToServer={snapshot.canDeleteSession}
|
|
131
|
+
/>
|
|
132
|
+
) : null}
|
|
133
|
+
</div>
|
|
134
|
+
{snapshot.sessionKey ? (
|
|
135
|
+
<ChatSessionHeaderActions
|
|
136
|
+
sessionKey={snapshot.sessionKey}
|
|
137
|
+
canDeleteSession={snapshot.canDeleteSession}
|
|
138
|
+
isDeletePending={snapshot.isDeletePending}
|
|
104
139
|
projectRoot={snapshot.sessionProjectRoot}
|
|
105
|
-
|
|
140
|
+
onDeleteSession={presenter.chatThreadManager.deleteSession}
|
|
106
141
|
/>
|
|
107
142
|
) : null}
|
|
108
143
|
</div>
|
|
109
|
-
{snapshot.sessionKey ? (
|
|
110
|
-
<ChatSessionHeaderActions
|
|
111
|
-
sessionKey={snapshot.sessionKey}
|
|
112
|
-
canDeleteSession={snapshot.canDeleteSession}
|
|
113
|
-
isDeletePending={snapshot.isDeletePending}
|
|
114
|
-
projectRoot={snapshot.sessionProjectRoot}
|
|
115
|
-
onDeleteSession={presenter.chatThreadManager.deleteSession}
|
|
116
|
-
/>
|
|
117
|
-
) : null}
|
|
118
|
-
</div>
|
|
119
|
-
|
|
120
|
-
{shouldShowProviderHint && (
|
|
121
|
-
<div className="px-5 py-2.5 border-b border-amber-200/70 bg-amber-50/70 flex items-center justify-between gap-3 shrink-0">
|
|
122
|
-
<span className="text-xs text-amber-800">
|
|
123
|
-
{t("chatModelNoOptions")}
|
|
124
|
-
</span>
|
|
125
|
-
<button
|
|
126
|
-
type="button"
|
|
127
|
-
onClick={presenter.chatThreadManager.goToProviders}
|
|
128
|
-
className="text-xs font-semibold text-amber-900 underline-offset-2 hover:underline"
|
|
129
|
-
>
|
|
130
|
-
{t("chatGoConfigureProvider")}
|
|
131
|
-
</button>
|
|
132
|
-
</div>
|
|
133
|
-
)}
|
|
134
144
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
<div className="px-5 py-2.5 border-b border-amber-200/70 bg-amber-50/70 shrink-0">
|
|
145
|
+
{shouldShowProviderHint && (
|
|
146
|
+
<div className="px-5 py-2.5 border-b border-amber-200/70 bg-amber-50/70 flex items-center justify-between gap-3 shrink-0">
|
|
138
147
|
<span className="text-xs text-amber-800">
|
|
139
|
-
{
|
|
148
|
+
{t("chatModelNoOptions")}
|
|
140
149
|
</span>
|
|
150
|
+
<button
|
|
151
|
+
type="button"
|
|
152
|
+
onClick={presenter.chatThreadManager.goToProviders}
|
|
153
|
+
className="text-xs font-semibold text-amber-900 underline-offset-2 hover:underline"
|
|
154
|
+
>
|
|
155
|
+
{t("chatGoConfigureProvider")}
|
|
156
|
+
</button>
|
|
141
157
|
</div>
|
|
142
158
|
)}
|
|
143
159
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
<ChatMessageListContainer
|
|
162
|
-
key={snapshot.sessionKey ?? "draft"}
|
|
163
|
-
messages={snapshot.messages}
|
|
164
|
-
isSending={
|
|
165
|
-
snapshot.isSending && snapshot.isAwaitingAssistantOutput
|
|
166
|
-
}
|
|
160
|
+
{snapshot.sessionTypeUnavailable &&
|
|
161
|
+
snapshot.sessionTypeUnavailableMessage?.trim() && (
|
|
162
|
+
<div className="px-5 py-2.5 border-b border-amber-200/70 bg-amber-50/70 shrink-0">
|
|
163
|
+
<span className="text-xs text-amber-800">
|
|
164
|
+
{snapshot.sessionTypeUnavailableMessage}
|
|
165
|
+
</span>
|
|
166
|
+
</div>
|
|
167
|
+
)}
|
|
168
|
+
|
|
169
|
+
<div
|
|
170
|
+
ref={threadRef}
|
|
171
|
+
onScroll={handleScroll}
|
|
172
|
+
className="flex-1 min-h-0 overflow-y-auto custom-scrollbar"
|
|
173
|
+
>
|
|
174
|
+
{showWelcome ? (
|
|
175
|
+
<ChatWelcome
|
|
176
|
+
onCreateSession={presenter.chatThreadManager.createSession}
|
|
167
177
|
/>
|
|
168
|
-
|
|
169
|
-
|
|
178
|
+
) : hideEmptyHint ? (
|
|
179
|
+
<div className="h-full" />
|
|
180
|
+
) : snapshot.messages.length === 0 ? (
|
|
181
|
+
<div className="px-5 py-5 text-sm text-gray-500">
|
|
182
|
+
{t("chatNoMessages")}
|
|
183
|
+
</div>
|
|
184
|
+
) : (
|
|
185
|
+
<div className="mx-auto w-full max-w-[min(1120px,100%)] px-6 py-5">
|
|
186
|
+
<ChatMessageListContainer
|
|
187
|
+
key={snapshot.sessionKey ?? "draft"}
|
|
188
|
+
messages={snapshot.messages}
|
|
189
|
+
isSending={
|
|
190
|
+
snapshot.isSending && snapshot.isAwaitingAssistantOutput
|
|
191
|
+
}
|
|
192
|
+
onToolAction={presenter.chatThreadManager.openSessionFromToolAction}
|
|
193
|
+
/>
|
|
194
|
+
</div>
|
|
195
|
+
)}
|
|
196
|
+
</div>
|
|
197
|
+
|
|
198
|
+
<ChatInputBarContainer />
|
|
170
199
|
</div>
|
|
171
200
|
|
|
172
|
-
|
|
201
|
+
{detailSessionKey ? (
|
|
202
|
+
<ChatChildSessionPanel
|
|
203
|
+
sessionKey={detailSessionKey}
|
|
204
|
+
title={snapshot.childSessionDetailLabel}
|
|
205
|
+
onClose={presenter.chatThreadManager.closeChildSessionDetail}
|
|
206
|
+
onBackToParent={presenter.chatThreadManager.goToParentSession}
|
|
207
|
+
onToolAction={presenter.chatThreadManager.openSessionFromToolAction}
|
|
208
|
+
/>
|
|
209
|
+
) : null}
|
|
173
210
|
</section>
|
|
174
211
|
);
|
|
175
212
|
}
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
buildTextPart,
|
|
10
10
|
} from "@/components/chat/adapters/chat-message-inline-content.adapter";
|
|
11
11
|
import { buildFileOperationCardData } from "@/components/chat/adapters/file-operation/card";
|
|
12
|
-
import {
|
|
12
|
+
import { buildSessionRequestToolCard } from "@/components/chat/adapters/chat-message.session-request-tool-card";
|
|
13
13
|
import type {
|
|
14
14
|
ChatMessagePartViewModel,
|
|
15
15
|
ChatToolPartViewModel,
|
|
@@ -77,6 +77,7 @@ export type ChatMessagePartSource =
|
|
|
77
77
|
type ToolCardViewSource = ToolCard & {
|
|
78
78
|
statusTone: ChatToolPartViewModel["statusTone"];
|
|
79
79
|
statusLabel: string;
|
|
80
|
+
action?: ChatToolPartViewModel["action"];
|
|
80
81
|
fileOperation?: ChatToolPartViewModel["fileOperation"];
|
|
81
82
|
outputData?: unknown;
|
|
82
83
|
};
|
|
@@ -194,6 +195,9 @@ function buildToolCard(
|
|
|
194
195
|
toolCard.kind === "call" ? texts.toolCallLabel : texts.toolResultLabel,
|
|
195
196
|
outputLabel: texts.toolOutputLabel,
|
|
196
197
|
emptyLabel: texts.toolNoOutputLabel,
|
|
198
|
+
...("action" in toolCard && toolCard.action
|
|
199
|
+
? { action: toolCard.action }
|
|
200
|
+
: {}),
|
|
197
201
|
...("fileOperation" in toolCard && toolCard.fileOperation
|
|
198
202
|
? { fileOperation: toolCard.fileOperation }
|
|
199
203
|
: {}),
|
|
@@ -330,14 +334,18 @@ function buildToolInvocationPart(
|
|
|
330
334
|
return assetFileView;
|
|
331
335
|
}
|
|
332
336
|
|
|
333
|
-
const
|
|
337
|
+
const sessionRequestToolCard = buildSessionRequestToolCard({
|
|
334
338
|
invocation,
|
|
335
|
-
texts
|
|
339
|
+
texts: {
|
|
340
|
+
toolStatusRunningLabel: texts.toolStatusRunningLabel,
|
|
341
|
+
toolStatusCompletedLabel: texts.toolStatusCompletedLabel,
|
|
342
|
+
toolStatusFailedLabel: texts.toolStatusFailedLabel,
|
|
343
|
+
},
|
|
336
344
|
});
|
|
337
|
-
if (
|
|
345
|
+
if (sessionRequestToolCard) {
|
|
338
346
|
return {
|
|
339
347
|
type: "tool-card",
|
|
340
|
-
card: buildToolCard(
|
|
348
|
+
card: buildToolCard(sessionRequestToolCard, texts),
|
|
341
349
|
};
|
|
342
350
|
}
|
|
343
351
|
|
|
@@ -223,7 +223,7 @@ it("keeps structured terminal results as structured data instead of raw json out
|
|
|
223
223
|
});
|
|
224
224
|
});
|
|
225
225
|
|
|
226
|
-
it("renders
|
|
226
|
+
it("renders session request tool cards from structured child-session status updates", () => {
|
|
227
227
|
const adapted = adapt([
|
|
228
228
|
{
|
|
229
229
|
id: "assistant-subagent",
|
|
@@ -237,12 +237,15 @@ it("renders spawn tool cards from structured subagent status updates", () => {
|
|
|
237
237
|
toolName: "spawn",
|
|
238
238
|
args: '{"label":"Verifier","task":"Verify 1+1=2"}',
|
|
239
239
|
result: {
|
|
240
|
-
kind: "nextclaw.
|
|
241
|
-
|
|
242
|
-
|
|
240
|
+
kind: "nextclaw.session_request",
|
|
241
|
+
requestId: "request-1",
|
|
242
|
+
sessionId: "child-session-1",
|
|
243
|
+
isChildSession: true,
|
|
244
|
+
title: "Verifier",
|
|
243
245
|
task: "Verify 1+1=2",
|
|
244
246
|
status: "completed",
|
|
245
|
-
|
|
247
|
+
finalResponseText: "Verified 1+1=2.",
|
|
248
|
+
parentSessionId: "parent-session-1",
|
|
246
249
|
},
|
|
247
250
|
},
|
|
248
251
|
},
|
|
@@ -254,21 +257,92 @@ it("renders spawn tool cards from structured subagent status updates", () => {
|
|
|
254
257
|
type: "tool-card",
|
|
255
258
|
card: {
|
|
256
259
|
toolName: "spawn",
|
|
257
|
-
summary: "
|
|
260
|
+
summary: "title: Verifier · session: child-session-1 · task: Verify 1+1=2",
|
|
258
261
|
output: [
|
|
259
|
-
"
|
|
262
|
+
"Request ID: request-1",
|
|
260
263
|
"",
|
|
261
|
-
"
|
|
264
|
+
"Session ID: child-session-1",
|
|
265
|
+
"",
|
|
266
|
+
"Target: child",
|
|
267
|
+
"",
|
|
268
|
+
"Title: Verifier",
|
|
262
269
|
"",
|
|
263
270
|
"Task:",
|
|
264
271
|
"Verify 1+1=2",
|
|
265
272
|
"",
|
|
266
|
-
"
|
|
273
|
+
"Final Response:",
|
|
267
274
|
"Verified 1+1=2.",
|
|
268
275
|
].join("\n"),
|
|
269
276
|
statusTone: "success",
|
|
270
277
|
statusLabel: "Completed",
|
|
271
278
|
titleLabel: "Tool Result",
|
|
279
|
+
action: {
|
|
280
|
+
kind: "open-session",
|
|
281
|
+
sessionId: "child-session-1",
|
|
282
|
+
sessionKind: "child",
|
|
283
|
+
label: "Verifier",
|
|
284
|
+
parentSessionId: "parent-session-1",
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it("renders regular session request tool cards with session navigation instead of child navigation", () => {
|
|
291
|
+
const adapted = adapt([
|
|
292
|
+
{
|
|
293
|
+
id: "assistant-session-request",
|
|
294
|
+
role: "assistant",
|
|
295
|
+
parts: [
|
|
296
|
+
{
|
|
297
|
+
type: "tool-invocation",
|
|
298
|
+
toolInvocation: {
|
|
299
|
+
status: ToolInvocationStatus.RESULT,
|
|
300
|
+
toolCallId: "session-request-call-1",
|
|
301
|
+
toolName: "sessions_request",
|
|
302
|
+
args: '{"sessionId":"session-2","task":"Summarize the latest findings"}',
|
|
303
|
+
result: {
|
|
304
|
+
kind: "nextclaw.session_request",
|
|
305
|
+
requestId: "request-2",
|
|
306
|
+
sessionId: "session-2",
|
|
307
|
+
isChildSession: false,
|
|
308
|
+
title: "Research thread",
|
|
309
|
+
task: "Summarize the latest findings",
|
|
310
|
+
status: "completed",
|
|
311
|
+
finalResponseText: "Here is the summary.",
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
],
|
|
316
|
+
},
|
|
317
|
+
] as unknown as ChatMessageSource[]);
|
|
318
|
+
|
|
319
|
+
expect(adapted[0]?.parts[0]).toMatchObject({
|
|
320
|
+
type: "tool-card",
|
|
321
|
+
card: {
|
|
322
|
+
toolName: "sessions_request",
|
|
323
|
+
summary: "title: Research thread · session: session-2 · task: Summarize the latest findings",
|
|
324
|
+
output: [
|
|
325
|
+
"Request ID: request-2",
|
|
326
|
+
"",
|
|
327
|
+
"Session ID: session-2",
|
|
328
|
+
"",
|
|
329
|
+
"Target: session",
|
|
330
|
+
"",
|
|
331
|
+
"Title: Research thread",
|
|
332
|
+
"",
|
|
333
|
+
"Task:",
|
|
334
|
+
"Summarize the latest findings",
|
|
335
|
+
"",
|
|
336
|
+
"Final Response:",
|
|
337
|
+
"Here is the summary.",
|
|
338
|
+
].join("\n"),
|
|
339
|
+
statusTone: "success",
|
|
340
|
+
action: {
|
|
341
|
+
kind: "open-session",
|
|
342
|
+
sessionId: "session-2",
|
|
343
|
+
sessionKind: "session",
|
|
344
|
+
label: "Research thread",
|
|
345
|
+
},
|
|
272
346
|
},
|
|
273
347
|
});
|
|
274
348
|
});
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import {
|
|
2
|
+
stringifyUnknown,
|
|
3
|
+
summarizeToolArgs,
|
|
4
|
+
type ToolCard,
|
|
5
|
+
} from "@/lib/chat-message";
|
|
6
|
+
import type { ChatToolPartViewModel } from "@nextclaw/agent-chat-ui";
|
|
7
|
+
|
|
8
|
+
type ToolCardViewSource = ToolCard & {
|
|
9
|
+
statusTone: ChatToolPartViewModel["statusTone"];
|
|
10
|
+
statusLabel: string;
|
|
11
|
+
action?: ChatToolPartViewModel["action"];
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type SessionRequestInvocation = {
|
|
15
|
+
toolName: string;
|
|
16
|
+
toolCallId?: string;
|
|
17
|
+
args?: unknown;
|
|
18
|
+
result?: unknown;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type SessionRequestToolCardTexts = {
|
|
22
|
+
toolStatusRunningLabel: string;
|
|
23
|
+
toolStatusCompletedLabel: string;
|
|
24
|
+
toolStatusFailedLabel: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
type SessionRequestResult = {
|
|
28
|
+
kind: string;
|
|
29
|
+
requestId?: string;
|
|
30
|
+
sessionId?: string;
|
|
31
|
+
isChildSession?: boolean;
|
|
32
|
+
title?: string;
|
|
33
|
+
task?: string;
|
|
34
|
+
status?: string;
|
|
35
|
+
message?: unknown;
|
|
36
|
+
finalResponseText?: unknown;
|
|
37
|
+
error?: unknown;
|
|
38
|
+
parentSessionId?: string;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
42
|
+
return typeof value === "object" && value !== null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function readOptionalString(value: unknown): string | null {
|
|
46
|
+
if (typeof value !== "string") {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
const trimmed = value.trim();
|
|
50
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function readSessionRequestResult(value: unknown): SessionRequestResult | null {
|
|
54
|
+
if (!isRecord(value) || value.kind !== "nextclaw.session_request") {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
return value as SessionRequestResult;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function buildSessionRequestDetail(
|
|
61
|
+
result: SessionRequestResult,
|
|
62
|
+
fallbackArgs: unknown,
|
|
63
|
+
): string | undefined {
|
|
64
|
+
const detailParts = [
|
|
65
|
+
readOptionalString(result.title)
|
|
66
|
+
? `title: ${result.title?.trim()}`
|
|
67
|
+
: null,
|
|
68
|
+
readOptionalString(result.sessionId)
|
|
69
|
+
? `session: ${result.sessionId?.trim()}`
|
|
70
|
+
: null,
|
|
71
|
+
readOptionalString(result.task)
|
|
72
|
+
? `task: ${result.task?.trim()}`
|
|
73
|
+
: null,
|
|
74
|
+
].filter((value): value is string => Boolean(value));
|
|
75
|
+
|
|
76
|
+
return detailParts.join(" · ") || summarizeToolArgs(fallbackArgs);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function buildSessionRequestOutput(result: SessionRequestResult): string | undefined {
|
|
80
|
+
const requestId = readOptionalString(result.requestId);
|
|
81
|
+
const sessionId = readOptionalString(result.sessionId);
|
|
82
|
+
const title = readOptionalString(result.title);
|
|
83
|
+
const task = readOptionalString(result.task);
|
|
84
|
+
const messageText =
|
|
85
|
+
typeof result.message !== "undefined"
|
|
86
|
+
? stringifyUnknown(result.message).trim()
|
|
87
|
+
: "";
|
|
88
|
+
const finalResponseText =
|
|
89
|
+
typeof result.finalResponseText !== "undefined"
|
|
90
|
+
? stringifyUnknown(result.finalResponseText).trim()
|
|
91
|
+
: "";
|
|
92
|
+
const errorText =
|
|
93
|
+
typeof result.error !== "undefined"
|
|
94
|
+
? stringifyUnknown(result.error).trim()
|
|
95
|
+
: "";
|
|
96
|
+
|
|
97
|
+
const sections = [
|
|
98
|
+
requestId ? `Request ID: ${requestId}` : null,
|
|
99
|
+
sessionId ? `Session ID: ${sessionId}` : null,
|
|
100
|
+
typeof result.isChildSession === "boolean"
|
|
101
|
+
? `Target: ${result.isChildSession ? "child" : "session"}`
|
|
102
|
+
: null,
|
|
103
|
+
title ? `Title: ${title}` : null,
|
|
104
|
+
task ? `Task:\n${task}` : null,
|
|
105
|
+
finalResponseText
|
|
106
|
+
? `Final Response:\n${finalResponseText}`
|
|
107
|
+
: errorText
|
|
108
|
+
? `Error:\n${errorText}`
|
|
109
|
+
: messageText
|
|
110
|
+
? `Status:\n${messageText}`
|
|
111
|
+
: null,
|
|
112
|
+
].filter((value): value is string => Boolean(value));
|
|
113
|
+
|
|
114
|
+
return sections.length > 0 ? sections.join("\n\n") : undefined;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function buildSessionRequestToolCard(params: {
|
|
118
|
+
invocation: SessionRequestInvocation;
|
|
119
|
+
texts: SessionRequestToolCardTexts;
|
|
120
|
+
}): ToolCardViewSource | null {
|
|
121
|
+
if (
|
|
122
|
+
params.invocation.toolName !== "spawn" &&
|
|
123
|
+
params.invocation.toolName !== "sessions_request"
|
|
124
|
+
) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const sessionRequest = readSessionRequestResult(params.invocation.result);
|
|
129
|
+
if (!sessionRequest) {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const normalizedStatus = readOptionalString(sessionRequest.status)?.toLowerCase();
|
|
134
|
+
const detail = buildSessionRequestDetail(sessionRequest, params.invocation.args);
|
|
135
|
+
const output = buildSessionRequestOutput(sessionRequest);
|
|
136
|
+
const targetSessionId = readOptionalString(sessionRequest.sessionId);
|
|
137
|
+
const action =
|
|
138
|
+
targetSessionId
|
|
139
|
+
? {
|
|
140
|
+
kind: "open-session" as const,
|
|
141
|
+
sessionId: targetSessionId,
|
|
142
|
+
sessionKind: sessionRequest.isChildSession === true ? ("child" as const) : ("session" as const),
|
|
143
|
+
...(readOptionalString(sessionRequest.title)
|
|
144
|
+
? { label: sessionRequest.title!.trim() }
|
|
145
|
+
: {}),
|
|
146
|
+
...(readOptionalString(sessionRequest.parentSessionId)
|
|
147
|
+
? { parentSessionId: sessionRequest.parentSessionId!.trim() }
|
|
148
|
+
: {}),
|
|
149
|
+
}
|
|
150
|
+
: undefined;
|
|
151
|
+
|
|
152
|
+
if (normalizedStatus === "failed") {
|
|
153
|
+
return {
|
|
154
|
+
kind: "result",
|
|
155
|
+
name: params.invocation.toolName,
|
|
156
|
+
detail,
|
|
157
|
+
text: output,
|
|
158
|
+
callId: params.invocation.toolCallId || undefined,
|
|
159
|
+
hasResult: Boolean(output),
|
|
160
|
+
statusTone: "error",
|
|
161
|
+
statusLabel: params.texts.toolStatusFailedLabel,
|
|
162
|
+
...(action ? { action } : {}),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (normalizedStatus === "completed") {
|
|
167
|
+
return {
|
|
168
|
+
kind: "result",
|
|
169
|
+
name: params.invocation.toolName,
|
|
170
|
+
detail,
|
|
171
|
+
text: output,
|
|
172
|
+
callId: params.invocation.toolCallId || undefined,
|
|
173
|
+
hasResult: Boolean(output),
|
|
174
|
+
statusTone: "success",
|
|
175
|
+
statusLabel: params.texts.toolStatusCompletedLabel,
|
|
176
|
+
...(action ? { action } : {}),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
kind: "result",
|
|
182
|
+
name: params.invocation.toolName,
|
|
183
|
+
detail,
|
|
184
|
+
text: output,
|
|
185
|
+
callId: params.invocation.toolCallId || undefined,
|
|
186
|
+
hasResult: Boolean(output),
|
|
187
|
+
statusTone: "running",
|
|
188
|
+
statusLabel: params.texts.toolStatusRunningLabel,
|
|
189
|
+
...(action ? { action } : {}),
|
|
190
|
+
};
|
|
191
|
+
}
|