@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.
Files changed (43) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/assets/{ChannelsList-Zeys_w43.js → ChannelsList-DVDu1xvz.js} +1 -1
  3. package/dist/assets/ChatPage-Z9tRzm_n.js +43 -0
  4. package/dist/assets/{MarketplacePage-Cd4faegU.js → MarketplacePage-Buo9HrOz.js} +1 -1
  5. package/dist/assets/MarketplacePage-D6rVQEQR.js +1 -0
  6. package/dist/assets/{McpMarketplacePage-C09Ngs7O.js → McpMarketplacePage-JnkYwK7p.js} +1 -1
  7. package/dist/assets/{ModelConfig-DJgdcgvQ.js → ModelConfig-BYRhgp0c.js} +1 -1
  8. package/dist/assets/{ProvidersList-w0rVFIBf.js → ProvidersList-DmLyyHvX.js} +1 -1
  9. package/dist/assets/{RemoteAccessPage-BJ_ckkOV.js → RemoteAccessPage-CDSSvH7Z.js} +1 -1
  10. package/dist/assets/{RuntimeConfig-Cmn2xPQO.js → RuntimeConfig-v7a7Fe3x.js} +1 -1
  11. package/dist/assets/{SearchConfig-BT13qpR_.js → SearchConfig-D5f1EkLE.js} +1 -1
  12. package/dist/assets/{SecretsConfig-CvqEVn0B.js → SecretsConfig-D61IKcYt.js} +1 -1
  13. package/dist/assets/{SessionsConfig-DHHcYznk.js → SessionsConfig-BRIxVTEv.js} +2 -2
  14. package/dist/assets/chat-session-display-D0WpnuRZ.js +1 -0
  15. package/dist/assets/{index-C6d0xmtm.js → index-BuwbBgmT.js} +2 -2
  16. package/dist/assets/index-bZ8cqQIS.css +1 -0
  17. package/dist/assets/{security-config-T5zpg16O.js → security-config-DbUyWcQz.js} +1 -1
  18. package/dist/assets/{useConfirmDialog-Bs5Ll17m.js → useConfirmDialog-COwYXDKm.js} +1 -1
  19. package/dist/index.html +2 -2
  20. package/package.json +6 -6
  21. package/src/api/types.ts +4 -0
  22. package/src/components/chat/ChatConversationPanel.test.tsx +10 -2
  23. package/src/components/chat/ChatConversationPanel.tsx +114 -77
  24. package/src/components/chat/adapters/chat-message-part.adapter.ts +13 -5
  25. package/src/components/chat/adapters/chat-message.adapter.test.ts +83 -9
  26. package/src/components/chat/adapters/chat-message.session-request-tool-card.ts +191 -0
  27. package/src/components/chat/chat-child-session-panel.tsx +100 -0
  28. package/src/components/chat/chat-page-runtime.test.ts +1 -0
  29. package/src/components/chat/chat-session-display.test.ts +1 -0
  30. package/src/components/chat/containers/chat-message-list.container.tsx +4 -0
  31. package/src/components/chat/ncp/NcpChatPage.tsx +179 -114
  32. package/src/components/chat/ncp/ncp-chat-thread.manager.ts +49 -0
  33. package/src/components/chat/ncp/ncp-session-adapter.test.ts +21 -0
  34. package/src/components/chat/ncp/ncp-session-adapter.ts +31 -0
  35. package/src/components/chat/ncp/use-ncp-session-list-view.ts +10 -1
  36. package/src/components/chat/presenter/chat-presenter-context.tsx +4 -1
  37. package/src/components/chat/stores/chat-thread.store.ts +11 -1
  38. package/src/components/chat/useHydratedNcpAgent.test.tsx +30 -23
  39. package/dist/assets/ChatPage-DWOU_8P6.js +0 -43
  40. package/dist/assets/MarketplacePage-BfaTTqN6.js +0 -1
  41. package/dist/assets/chat-session-display-VW6ZMvZP.js +0 -1
  42. package/dist/assets/index-BlH4-cBw.css +0 -1
  43. 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 flex-col overflow-hidden bg-gradient-to-b from-gray-50/60 to-white">
83
- <div
84
- className={cn(
85
- "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",
86
- shouldShowSessionHeader
87
- ? "py-3 opacity-100"
88
- : "h-0 py-0 opacity-0 border-b-0",
89
- )}
90
- >
91
- <div className="min-w-0 flex-1 flex items-center gap-2">
92
- <span className="text-sm font-medium text-gray-700 truncate">
93
- {sessionHeaderTitle}
94
- </span>
95
- {snapshot.sessionTypeLabel ? (
96
- <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">
97
- {snapshot.sessionTypeLabel}
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
- ) : null}
100
- {snapshot.sessionProjectName ? (
101
- <ChatSessionProjectBadge
102
- sessionKey={snapshot.sessionKey ?? "draft"}
103
- projectName={snapshot.sessionProjectName}
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
- persistToServer={snapshot.canDeleteSession}
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
- {snapshot.sessionTypeUnavailable &&
136
- snapshot.sessionTypeUnavailableMessage?.trim() && (
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
- {snapshot.sessionTypeUnavailableMessage}
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
- <div
145
- ref={threadRef}
146
- onScroll={handleScroll}
147
- className="flex-1 min-h-0 overflow-y-auto custom-scrollbar"
148
- >
149
- {showWelcome ? (
150
- <ChatWelcome
151
- onCreateSession={presenter.chatThreadManager.createSession}
152
- />
153
- ) : hideEmptyHint ? (
154
- <div className="h-full" />
155
- ) : snapshot.messages.length === 0 ? (
156
- <div className="px-5 py-5 text-sm text-gray-500">
157
- {t("chatNoMessages")}
158
- </div>
159
- ) : (
160
- <div className="mx-auto w-full max-w-[min(1120px,100%)] px-6 py-5">
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
- </div>
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
- <ChatInputBarContainer />
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 { buildSubagentToolCard } from "@/components/chat/adapters/chat-message.subagent-tool-card";
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 subagentToolCard = buildSubagentToolCard({
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 (subagentToolCard) {
345
+ if (sessionRequestToolCard) {
338
346
  return {
339
347
  type: "tool-card",
340
- card: buildToolCard(subagentToolCard, texts),
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 spawn tool cards from structured subagent status updates", () => {
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.subagent_run",
241
- runId: "subagent-1",
242
- label: "Verifier",
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
- result: "Verified 1+1=2.",
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: "label: Verifier · run: subagent-1 · task: Verify 1+1=2",
260
+ summary: "title: Verifier · session: child-session-1 · task: Verify 1+1=2",
258
261
  output: [
259
- "Run ID: subagent-1",
262
+ "Request ID: request-1",
260
263
  "",
261
- "Label: Verifier",
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
- "Result:",
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
+ }