@nextclaw/ui 0.12.3 → 0.12.5
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 +49 -0
- package/dist/assets/{ChannelsList-DZWam3Ob.js → ChannelsList-C6-lh55g.js} +2 -2
- package/dist/assets/ChatPage-DOW0gPc2.js +45 -0
- package/dist/assets/DocBrowser-CGyeswYP.js +1 -0
- package/dist/assets/{DocBrowser-C7-1sXqo.js → DocBrowser-QUZ3nfmH.js} +1 -1
- package/dist/assets/{DocBrowserContext-DN5tjUoS.js → DocBrowserContext-CpiIfhJO.js} +1 -1
- package/dist/assets/{LogoBadge-DDS1sU_U.js → LogoBadge-BUK13xK5.js} +1 -1
- package/dist/assets/MarketplacePage-BDVwhIYE.js +1 -0
- package/dist/assets/MarketplacePage-LnKKL3xK.js +49 -0
- package/dist/assets/McpMarketplacePage-BG4T_Pcx.js +40 -0
- package/dist/assets/ModelConfig-LtWuogIw.js +1 -0
- package/dist/assets/ProviderScopedModelInput-DGn6sFEN.js +1 -0
- package/dist/assets/ProvidersList-ma-_MlLo.js +1 -0
- package/dist/assets/{RemoteAccessPage-COnjm8_x.js → RemoteAccessPage-ff15qO-c.js} +1 -1
- package/dist/assets/RuntimeConfig-TgPandXF.js +1 -0
- package/dist/assets/SearchConfig-C9iBt7pl.js +1 -0
- package/dist/assets/{SecretsConfig-Cefg1LFJ.js → SecretsConfig-Bew4EF2A.js} +2 -2
- package/dist/assets/{SessionsConfig-BZnmVTIu.js → SessionsConfig-2r2yAGZg.js} +2 -2
- package/dist/assets/{book-open-DvWqOode.js → book-open-CJG8Yz3U.js} +1 -1
- package/dist/assets/{chat-session-display-D4bYa0b8.js → chat-session-display-DkAC5OMC.js} +1 -1
- package/dist/assets/{chunk-JZWAC4HX-CxfKRD7X.js → chunk-JZWAC4HX-D5b3Iyas.js} +1 -1
- package/dist/assets/{config-BeGwf2Ao.js → config-zvnxSXSP.js} +1 -1
- package/dist/assets/{createLucideIcon-C7MmdIX3.js → createLucideIcon-_FMJqZw2.js} +1 -1
- package/dist/assets/{dist-B6VMuIQN.js → dist-B1fpOuON.js} +1 -1
- package/dist/assets/{dist-RWNFhxvR.js → dist-BCXX7FD-.js} +2 -2
- package/dist/assets/{external-link-U86Acd1t.js → external-link-b7gAJWYY.js} +1 -1
- package/dist/assets/{hash-D-OVfV3Z.js → hash-Bhy4TwfZ.js} +1 -1
- package/dist/assets/i18n-DJg9BPYk.js +1 -0
- package/dist/assets/index-BoJbxdvZ.css +1 -0
- package/dist/assets/index-CtlT4E9Y.js +6 -0
- package/dist/assets/infiniteQueryBehavior-CTcVlD9s.js +1 -0
- package/dist/assets/loader-circle-B60I0hEk.js +1 -0
- package/dist/assets/{logos-U1_qDA3U.js → logos-GMeYU9vc.js} +1 -1
- package/dist/assets/{page-layout-Z1klaUFW.js → page-layout-C8UbWuMt.js} +1 -1
- package/dist/assets/plus-CR7RfK3H.js +1 -0
- package/dist/assets/{popover-xWbqMnIN.js → popover-8HSx9wQj.js} +1 -1
- package/dist/assets/react-BB4jko2M.js +1 -0
- package/dist/assets/{refresh-ccw-JQh1lwq-.js → refresh-ccw-CA4_C7Zg.js} +1 -1
- package/dist/assets/{save-4VRlzkii.js → save-BtvMy4lk.js} +1 -1
- package/dist/assets/search-C60UA27E.js +1 -0
- package/dist/assets/security-config-BkFDYZ6j.js +1 -0
- package/dist/assets/{select-DF-AUoie.js → select-xp_Ac8ip.js} +1 -1
- package/dist/assets/skeleton-uxz_5h3A.js +1 -0
- package/dist/assets/{status-dot-Bq_8Ojvv.js → status-dot-Cn4Pp7DZ.js} +1 -1
- package/dist/assets/{switch-D7JF_RZ-.js → switch-BTi6UOij.js} +1 -1
- package/dist/assets/{tabs-custom-CLksZ2bO.js → tabs-custom-BiiN8DME.js} +1 -1
- package/dist/assets/{trash-2-VV8jvziy.js → trash-2-BpsF0N-r.js} +1 -1
- package/dist/assets/use-infinite-scroll-loader-C8jBv11-.js +1 -0
- package/dist/assets/{useConfirmDialog-CuQqiPx7.js → useConfirmDialog-BJIwUZjH.js} +1 -1
- package/dist/assets/{useMutation-DBTWPbTg.js → useMutation-BjBOKHj_.js} +1 -1
- package/dist/assets/x-BfTu-g7D.js +1 -0
- package/dist/index.html +19 -18
- package/package.json +5 -5
- package/src/account/components/account-panel.tsx +46 -4
- package/src/account/managers/account.manager.ts +19 -4
- package/src/api/remote.ts +9 -0
- package/src/api/remote.types.ts +5 -0
- package/src/components/chat/ChatConversationPanel.test.tsx +183 -141
- package/src/components/chat/ChatSidebar.test.tsx +168 -28
- package/src/components/chat/ChatSidebar.tsx +103 -28
- package/src/components/chat/adapters/chat-message-tool-agent-id.test.ts +11 -11
- package/src/components/chat/adapters/chat-message.adapter.test.ts +43 -6
- package/src/components/chat/adapters/chat-message.session-request-tool-card.ts +182 -44
- package/src/components/chat/adapters/chat-message.session-spawn-tool-card.test.ts +104 -0
- package/src/components/chat/chat-child-session-panel.tsx +103 -45
- package/src/components/chat/chat-page-runtime.test.ts +16 -19
- package/src/components/chat/chat-session-preference-sync.test.ts +13 -0
- package/src/components/chat/chat-session-preference-sync.ts +9 -7
- package/src/components/chat/chat-sidebar-list-mode-switch.tsx +43 -0
- package/src/components/chat/chat-sidebar-project-groups.tsx +152 -0
- package/src/components/chat/hooks/use-chat-session-project.test.tsx +5 -5
- package/src/components/chat/hooks/use-chat-session-project.ts +0 -5
- package/src/components/chat/hooks/use-chat-session-update.test.tsx +75 -0
- package/src/components/chat/hooks/use-chat-session-update.ts +4 -2
- package/src/components/chat/managers/chat-session-list.manager.test.ts +46 -6
- package/src/components/chat/managers/chat-session-list.manager.ts +19 -6
- package/src/components/chat/ncp/NcpChatPage.tsx +33 -38
- package/src/components/chat/ncp/ncp-chat-input.manager.ts +3 -5
- package/src/components/chat/ncp/ncp-chat-page-data.ts +0 -1
- package/src/components/chat/ncp/ncp-chat.presenter.ts +2 -16
- package/src/components/chat/ncp/session-conversation/use-ncp-child-session-tabs-view.ts +20 -7
- package/src/components/chat/session-header/chat-session-project-badge.test.tsx +16 -0
- package/src/components/chat/session-header/chat-session-project-badge.tsx +2 -2
- package/src/components/chat/stores/chat-session-list.store.ts +3 -0
- package/src/components/chat/useChatSessionTypeState.test.tsx +0 -3
- package/src/components/chat/useChatSessionTypeState.ts +3 -5
- package/src/components/config/ChannelsList.test.tsx +68 -0
- package/src/components/config/ChannelsList.tsx +22 -4
- package/src/components/config/ProvidersList.tsx +17 -3
- package/src/components/config/providers-list.test.tsx +68 -0
- package/src/components/layout/Sidebar.tsx +13 -13
- package/src/components/layout/sidebar.layout.test.tsx +32 -1
- package/src/components/marketplace/MarketplacePage.tsx +30 -30
- package/src/components/marketplace/marketplace-page-parts.tsx +16 -24
- package/src/components/marketplace/mcp/McpMarketplacePage.tsx +28 -26
- package/src/hooks/marketplace-list-pages.ts +27 -0
- package/src/hooks/use-infinite-scroll-loader.ts +88 -0
- package/src/hooks/useMarketplace.ts +14 -3
- package/src/hooks/useMcpMarketplace.ts +14 -3
- package/src/lib/i18n.chat.ts +3 -0
- package/src/lib/i18n.remote.ts +15 -0
- package/dist/assets/ChatPage-YBL7iJ1X.js +0 -43
- package/dist/assets/DocBrowser-DQjtSsY3.js +0 -1
- package/dist/assets/MarketplacePage-2tWWgwAb.js +0 -49
- package/dist/assets/MarketplacePage-BorWJftJ.js +0 -1
- package/dist/assets/McpMarketplacePage-N-fB4HID.js +0 -40
- package/dist/assets/ModelConfig-DvsBTUiE.js +0 -1
- package/dist/assets/ProviderScopedModelInput-D9woCARc.js +0 -1
- package/dist/assets/ProvidersList-D-qPGgC4.js +0 -1
- package/dist/assets/RuntimeConfig-BHpqcaHm.js +0 -1
- package/dist/assets/SearchConfig-DIT6M65Q.js +0 -1
- package/dist/assets/i18n-hM3v-3YG.js +0 -1
- package/dist/assets/index-CpxuJa9o.css +0 -1
- package/dist/assets/index-DHmCjcxq.js +0 -6
- package/dist/assets/label-CHJ1ATds.js +0 -1
- package/dist/assets/loader-circle-C8cpaL0w.js +0 -1
- package/dist/assets/marketplace-localization-CxSTG9wr.js +0 -1
- package/dist/assets/plus-CrkO1kob.js +0 -1
- package/dist/assets/react-3YE87-lE.js +0 -1
- package/dist/assets/search-EX-Papzl.js +0 -1
- package/dist/assets/security-config-DEgOD4VX.js +0 -1
- package/dist/assets/skeleton-B0mmt1vo.js +0 -1
- package/dist/assets/x-B4sxJkGY.js +0 -1
|
@@ -9,6 +9,7 @@ import type { ChatToolPartViewModel } from "@nextclaw/agent-chat-ui";
|
|
|
9
9
|
type ToolCardViewSource = ToolCard & {
|
|
10
10
|
statusTone: ChatToolPartViewModel["statusTone"];
|
|
11
11
|
statusLabel: string;
|
|
12
|
+
input?: string;
|
|
12
13
|
action?: ChatToolPartViewModel["action"];
|
|
13
14
|
};
|
|
14
15
|
|
|
@@ -31,15 +32,30 @@ type SessionRequestResult = {
|
|
|
31
32
|
sessionId?: string;
|
|
32
33
|
agentId?: string;
|
|
33
34
|
isChildSession?: boolean;
|
|
35
|
+
lifecycle?: string;
|
|
34
36
|
title?: string;
|
|
35
37
|
task?: string;
|
|
36
38
|
status?: string;
|
|
39
|
+
notify?: string;
|
|
40
|
+
spawnedByRequestId?: string;
|
|
37
41
|
message?: unknown;
|
|
38
42
|
finalResponseText?: unknown;
|
|
39
43
|
error?: unknown;
|
|
40
44
|
parentSessionId?: string;
|
|
41
45
|
};
|
|
42
46
|
|
|
47
|
+
type SessionSpawnResult = {
|
|
48
|
+
kind: string;
|
|
49
|
+
sessionId?: string;
|
|
50
|
+
agentId?: string;
|
|
51
|
+
isChildSession?: boolean;
|
|
52
|
+
title?: string;
|
|
53
|
+
sessionType?: string;
|
|
54
|
+
lifecycle?: string;
|
|
55
|
+
createdAt?: string;
|
|
56
|
+
parentSessionId?: string;
|
|
57
|
+
};
|
|
58
|
+
|
|
43
59
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
44
60
|
return typeof value === "object" && value !== null;
|
|
45
61
|
}
|
|
@@ -59,6 +75,33 @@ function readSessionRequestResult(value: unknown): SessionRequestResult | null {
|
|
|
59
75
|
return value as SessionRequestResult;
|
|
60
76
|
}
|
|
61
77
|
|
|
78
|
+
function readSessionSpawnResult(value: unknown): SessionSpawnResult | null {
|
|
79
|
+
if (!isRecord(value) || value.kind !== "nextclaw.session") {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
return value as SessionSpawnResult;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function parseStructuredValue(value: unknown): unknown {
|
|
86
|
+
if (typeof value !== "string") {
|
|
87
|
+
return value;
|
|
88
|
+
}
|
|
89
|
+
const trimmed = value.trim();
|
|
90
|
+
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) {
|
|
91
|
+
return value;
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
return JSON.parse(trimmed) as unknown;
|
|
95
|
+
} catch {
|
|
96
|
+
return value;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function buildStructuredInput(value: unknown): string | undefined {
|
|
101
|
+
const text = stringifyUnknown(parseStructuredValue(value)).trim();
|
|
102
|
+
return text || undefined;
|
|
103
|
+
}
|
|
104
|
+
|
|
62
105
|
function buildSessionRequestDetail(
|
|
63
106
|
result: SessionRequestResult,
|
|
64
107
|
fallbackArgs: unknown,
|
|
@@ -78,11 +121,32 @@ function buildSessionRequestDetail(
|
|
|
78
121
|
return detailParts.join(" · ") || summarizeToolArgs(fallbackArgs);
|
|
79
122
|
}
|
|
80
123
|
|
|
124
|
+
function buildSessionSpawnDetail(
|
|
125
|
+
result: SessionSpawnResult,
|
|
126
|
+
fallbackArgs: unknown,
|
|
127
|
+
): string | undefined {
|
|
128
|
+
const detailParts = [
|
|
129
|
+
readOptionalString(result.title)
|
|
130
|
+
? `title: ${result.title?.trim()}`
|
|
131
|
+
: null,
|
|
132
|
+
readOptionalString(result.sessionId)
|
|
133
|
+
? `session: ${result.sessionId?.trim()}`
|
|
134
|
+
: null,
|
|
135
|
+
].filter((value): value is string => Boolean(value));
|
|
136
|
+
|
|
137
|
+
return detailParts.join(" · ") || summarizeToolArgs(fallbackArgs);
|
|
138
|
+
}
|
|
139
|
+
|
|
81
140
|
function buildSessionRequestOutput(result: SessionRequestResult): string | undefined {
|
|
82
141
|
const requestId = readOptionalString(result.requestId);
|
|
83
142
|
const sessionId = readOptionalString(result.sessionId);
|
|
84
143
|
const title = readOptionalString(result.title);
|
|
85
144
|
const task = readOptionalString(result.task);
|
|
145
|
+
const status = readOptionalString(result.status);
|
|
146
|
+
const notify = readOptionalString(result.notify);
|
|
147
|
+
const lifecycle = readOptionalString(result.lifecycle);
|
|
148
|
+
const parentSessionId = readOptionalString(result.parentSessionId);
|
|
149
|
+
const spawnedByRequestId = readOptionalString(result.spawnedByRequestId);
|
|
86
150
|
const messageText =
|
|
87
151
|
typeof result.message !== "undefined"
|
|
88
152
|
? stringifyUnknown(result.message).trim()
|
|
@@ -102,6 +166,11 @@ function buildSessionRequestOutput(result: SessionRequestResult): string | undef
|
|
|
102
166
|
typeof result.isChildSession === "boolean"
|
|
103
167
|
? `Target: ${result.isChildSession ? "child" : "session"}`
|
|
104
168
|
: null,
|
|
169
|
+
status ? `Status: ${status}` : null,
|
|
170
|
+
notify ? `Notify: ${notify}` : null,
|
|
171
|
+
lifecycle ? `Lifecycle: ${lifecycle}` : null,
|
|
172
|
+
parentSessionId ? `Parent Session ID: ${parentSessionId}` : null,
|
|
173
|
+
spawnedByRequestId ? `Spawned By Request ID: ${spawnedByRequestId}` : null,
|
|
105
174
|
title ? `Title: ${title}` : null,
|
|
106
175
|
task ? `Task:\n${task}` : null,
|
|
107
176
|
finalResponseText
|
|
@@ -116,6 +185,29 @@ function buildSessionRequestOutput(result: SessionRequestResult): string | undef
|
|
|
116
185
|
return sections.length > 0 ? sections.join("\n\n") : undefined;
|
|
117
186
|
}
|
|
118
187
|
|
|
188
|
+
function buildSessionSpawnOutput(result: SessionSpawnResult): string | undefined {
|
|
189
|
+
const sessionId = readOptionalString(result.sessionId);
|
|
190
|
+
const title = readOptionalString(result.title);
|
|
191
|
+
const sessionType = readOptionalString(result.sessionType);
|
|
192
|
+
const lifecycle = readOptionalString(result.lifecycle);
|
|
193
|
+
const parentSessionId = readOptionalString(result.parentSessionId);
|
|
194
|
+
const createdAt = readOptionalString(result.createdAt);
|
|
195
|
+
|
|
196
|
+
const sections = [
|
|
197
|
+
sessionId ? `Session ID: ${sessionId}` : null,
|
|
198
|
+
typeof result.isChildSession === "boolean"
|
|
199
|
+
? `Target: ${result.isChildSession ? "child" : "session"}`
|
|
200
|
+
: null,
|
|
201
|
+
title ? `Title: ${title}` : null,
|
|
202
|
+
sessionType ? `Session Type: ${sessionType}` : null,
|
|
203
|
+
lifecycle ? `Lifecycle: ${lifecycle}` : null,
|
|
204
|
+
parentSessionId ? `Parent Session ID: ${parentSessionId}` : null,
|
|
205
|
+
createdAt ? `Created At: ${createdAt}` : null,
|
|
206
|
+
].filter((value): value is string => Boolean(value));
|
|
207
|
+
|
|
208
|
+
return sections.length > 0 ? sections.join("\n\n") : undefined;
|
|
209
|
+
}
|
|
210
|
+
|
|
119
211
|
export function buildSessionRequestToolCard(params: {
|
|
120
212
|
invocation: SessionRequestInvocation;
|
|
121
213
|
texts: SessionRequestToolCardTexts;
|
|
@@ -123,77 +215,123 @@ export function buildSessionRequestToolCard(params: {
|
|
|
123
215
|
const { invocation, texts } = params;
|
|
124
216
|
const { toolName, toolCallId, args, result } = invocation;
|
|
125
217
|
|
|
126
|
-
if (
|
|
218
|
+
if (
|
|
219
|
+
toolName !== "spawn" &&
|
|
220
|
+
toolName !== "sessions_request" &&
|
|
221
|
+
toolName !== "sessions_spawn"
|
|
222
|
+
) {
|
|
127
223
|
return null;
|
|
128
224
|
}
|
|
129
225
|
|
|
130
226
|
const sessionRequest = readSessionRequestResult(result);
|
|
131
|
-
if (
|
|
132
|
-
|
|
133
|
-
|
|
227
|
+
if (sessionRequest) {
|
|
228
|
+
const normalizedStatus = readOptionalString(sessionRequest.status)?.toLowerCase();
|
|
229
|
+
const detail = buildSessionRequestDetail(sessionRequest, args);
|
|
230
|
+
const output = buildSessionRequestOutput(sessionRequest);
|
|
231
|
+
const targetSessionId = readOptionalString(sessionRequest.sessionId);
|
|
232
|
+
const agentId = resolveToolInvocationAgentId({ args, result: sessionRequest });
|
|
233
|
+
const action =
|
|
234
|
+
targetSessionId
|
|
235
|
+
? {
|
|
236
|
+
kind: "open-session" as const,
|
|
237
|
+
sessionId: targetSessionId,
|
|
238
|
+
sessionKind: sessionRequest.isChildSession === true ? ("child" as const) : ("session" as const),
|
|
239
|
+
...(agentId
|
|
240
|
+
? { agentId }
|
|
241
|
+
: {}),
|
|
242
|
+
...(readOptionalString(sessionRequest.title)
|
|
243
|
+
? { label: sessionRequest.title!.trim() }
|
|
244
|
+
: {}),
|
|
245
|
+
...(readOptionalString(sessionRequest.parentSessionId)
|
|
246
|
+
? { parentSessionId: sessionRequest.parentSessionId!.trim() }
|
|
247
|
+
: {}),
|
|
248
|
+
}
|
|
249
|
+
: undefined;
|
|
134
250
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
251
|
+
if (normalizedStatus === "failed") {
|
|
252
|
+
return {
|
|
253
|
+
kind: "result",
|
|
254
|
+
name: toolName,
|
|
255
|
+
detail,
|
|
256
|
+
input: buildStructuredInput(args),
|
|
257
|
+
text: output,
|
|
258
|
+
callId: toolCallId || undefined,
|
|
259
|
+
hasResult: Boolean(output),
|
|
260
|
+
statusTone: "error",
|
|
261
|
+
statusLabel: texts.toolStatusFailedLabel,
|
|
262
|
+
...(agentId ? { agentId } : {}),
|
|
263
|
+
...(action ? { action } : {}),
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (normalizedStatus === "completed") {
|
|
268
|
+
return {
|
|
269
|
+
kind: "result",
|
|
270
|
+
name: toolName,
|
|
271
|
+
detail,
|
|
272
|
+
input: buildStructuredInput(args),
|
|
273
|
+
text: output,
|
|
274
|
+
callId: toolCallId || undefined,
|
|
275
|
+
hasResult: Boolean(output),
|
|
276
|
+
statusTone: "success",
|
|
277
|
+
statusLabel: texts.toolStatusCompletedLabel,
|
|
278
|
+
...(agentId ? { agentId } : {}),
|
|
279
|
+
...(action ? { action } : {}),
|
|
280
|
+
};
|
|
281
|
+
}
|
|
157
282
|
|
|
158
|
-
if (normalizedStatus === "failed") {
|
|
159
283
|
return {
|
|
160
284
|
kind: "result",
|
|
161
285
|
name: toolName,
|
|
162
286
|
detail,
|
|
287
|
+
input: buildStructuredInput(args),
|
|
163
288
|
text: output,
|
|
164
289
|
callId: toolCallId || undefined,
|
|
165
290
|
hasResult: Boolean(output),
|
|
166
|
-
statusTone: "
|
|
167
|
-
statusLabel: texts.
|
|
291
|
+
statusTone: "running",
|
|
292
|
+
statusLabel: texts.toolStatusRunningLabel,
|
|
168
293
|
...(agentId ? { agentId } : {}),
|
|
169
294
|
...(action ? { action } : {}),
|
|
170
295
|
};
|
|
171
296
|
}
|
|
172
297
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
name: toolName,
|
|
177
|
-
detail,
|
|
178
|
-
text: output,
|
|
179
|
-
callId: toolCallId || undefined,
|
|
180
|
-
hasResult: Boolean(output),
|
|
181
|
-
statusTone: "success",
|
|
182
|
-
statusLabel: texts.toolStatusCompletedLabel,
|
|
183
|
-
...(agentId ? { agentId } : {}),
|
|
184
|
-
...(action ? { action } : {}),
|
|
185
|
-
};
|
|
298
|
+
const sessionSpawn = readSessionSpawnResult(result);
|
|
299
|
+
if (!sessionSpawn) {
|
|
300
|
+
return null;
|
|
186
301
|
}
|
|
187
302
|
|
|
303
|
+
const detail = buildSessionSpawnDetail(sessionSpawn, args);
|
|
304
|
+
const output = buildSessionSpawnOutput(sessionSpawn);
|
|
305
|
+
const targetSessionId = readOptionalString(sessionSpawn.sessionId);
|
|
306
|
+
const agentId = resolveToolInvocationAgentId({ args, result: sessionSpawn });
|
|
307
|
+
const action =
|
|
308
|
+
targetSessionId
|
|
309
|
+
? {
|
|
310
|
+
kind: "open-session" as const,
|
|
311
|
+
sessionId: targetSessionId,
|
|
312
|
+
sessionKind: sessionSpawn.isChildSession === true ? ("child" as const) : ("session" as const),
|
|
313
|
+
...(agentId
|
|
314
|
+
? { agentId }
|
|
315
|
+
: {}),
|
|
316
|
+
...(readOptionalString(sessionSpawn.title)
|
|
317
|
+
? { label: sessionSpawn.title!.trim() }
|
|
318
|
+
: {}),
|
|
319
|
+
...(readOptionalString(sessionSpawn.parentSessionId)
|
|
320
|
+
? { parentSessionId: sessionSpawn.parentSessionId!.trim() }
|
|
321
|
+
: {}),
|
|
322
|
+
}
|
|
323
|
+
: undefined;
|
|
324
|
+
|
|
188
325
|
return {
|
|
189
326
|
kind: "result",
|
|
190
327
|
name: toolName,
|
|
191
328
|
detail,
|
|
329
|
+
input: buildStructuredInput(args),
|
|
192
330
|
text: output,
|
|
193
331
|
callId: toolCallId || undefined,
|
|
194
332
|
hasResult: Boolean(output),
|
|
195
|
-
statusTone: "
|
|
196
|
-
statusLabel: texts.
|
|
333
|
+
statusTone: "success",
|
|
334
|
+
statusLabel: texts.toolStatusCompletedLabel,
|
|
197
335
|
...(agentId ? { agentId } : {}),
|
|
198
336
|
...(action ? { action } : {}),
|
|
199
337
|
};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { ToolInvocationStatus, type UiMessage } from "@nextclaw/agent-chat";
|
|
2
|
+
import { adaptChatMessages } from "@/components/chat/adapters/chat-message.adapter";
|
|
3
|
+
import type { ChatMessageSource } from "@/components/chat/adapters/chat-message.adapter";
|
|
4
|
+
|
|
5
|
+
const defaultTexts = {
|
|
6
|
+
roleLabels: {
|
|
7
|
+
user: "You",
|
|
8
|
+
assistant: "Assistant",
|
|
9
|
+
tool: "Tool",
|
|
10
|
+
system: "System",
|
|
11
|
+
fallback: "Message",
|
|
12
|
+
},
|
|
13
|
+
reasoningLabel: "Reasoning",
|
|
14
|
+
toolCallLabel: "Tool Call",
|
|
15
|
+
toolResultLabel: "Tool Result",
|
|
16
|
+
toolInputLabel: "Input",
|
|
17
|
+
toolNoOutputLabel: "No output",
|
|
18
|
+
toolOutputLabel: "Output",
|
|
19
|
+
toolStatusPreparingLabel: "Preparing",
|
|
20
|
+
toolStatusRunningLabel: "Running",
|
|
21
|
+
toolStatusCompletedLabel: "Completed",
|
|
22
|
+
toolStatusFailedLabel: "Failed",
|
|
23
|
+
toolStatusCancelledLabel: "Cancelled",
|
|
24
|
+
imageAttachmentLabel: "Image attachment",
|
|
25
|
+
fileAttachmentLabel: "File attachment",
|
|
26
|
+
unknownPartLabel: "Unknown Part",
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function adapt(uiMessages: UiMessage[]) {
|
|
30
|
+
return adaptChatMessages({
|
|
31
|
+
uiMessages: uiMessages as unknown as ChatMessageSource[],
|
|
32
|
+
formatTimestamp: (value) => `formatted:${value}`,
|
|
33
|
+
texts: defaultTexts,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
it("renders child-session creation cards for sessions_spawn and keeps child-panel navigation", () => {
|
|
38
|
+
const adapted = adapt([
|
|
39
|
+
{
|
|
40
|
+
id: "assistant-child-session-create",
|
|
41
|
+
role: "assistant",
|
|
42
|
+
parts: [
|
|
43
|
+
{
|
|
44
|
+
type: "tool-invocation",
|
|
45
|
+
toolInvocation: {
|
|
46
|
+
status: ToolInvocationStatus.RESULT,
|
|
47
|
+
toolCallId: "sessions-spawn-child-only-1",
|
|
48
|
+
toolName: "sessions_spawn",
|
|
49
|
+
args: '{"scope":"child","title":"Verifier","task":"Prepare a child workspace"}',
|
|
50
|
+
result: {
|
|
51
|
+
kind: "nextclaw.session",
|
|
52
|
+
sessionId: "child-session-2",
|
|
53
|
+
agentId: "verifier-agent",
|
|
54
|
+
isChildSession: true,
|
|
55
|
+
title: "Verifier",
|
|
56
|
+
sessionType: "native",
|
|
57
|
+
lifecycle: "persistent",
|
|
58
|
+
parentSessionId: "parent-session-2",
|
|
59
|
+
createdAt: "2026-04-09T09:00:00.000Z",
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
},
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
expect(adapted[0]?.parts[0]).toMatchObject({
|
|
68
|
+
type: "tool-card",
|
|
69
|
+
card: {
|
|
70
|
+
toolName: "sessions_spawn",
|
|
71
|
+
agentId: "verifier-agent",
|
|
72
|
+
summary: "title: Verifier · session: child-session-2",
|
|
73
|
+
input: `{
|
|
74
|
+
"scope": "child",
|
|
75
|
+
"title": "Verifier",
|
|
76
|
+
"task": "Prepare a child workspace"
|
|
77
|
+
}`,
|
|
78
|
+
output: [
|
|
79
|
+
"Session ID: child-session-2",
|
|
80
|
+
"",
|
|
81
|
+
"Target: child",
|
|
82
|
+
"",
|
|
83
|
+
"Title: Verifier",
|
|
84
|
+
"",
|
|
85
|
+
"Session Type: native",
|
|
86
|
+
"",
|
|
87
|
+
"Lifecycle: persistent",
|
|
88
|
+
"",
|
|
89
|
+
"Parent Session ID: parent-session-2",
|
|
90
|
+
"",
|
|
91
|
+
"Created At: 2026-04-09T09:00:00.000Z",
|
|
92
|
+
].join("\n"),
|
|
93
|
+
statusTone: "success",
|
|
94
|
+
action: {
|
|
95
|
+
kind: "open-session",
|
|
96
|
+
sessionId: "child-session-2",
|
|
97
|
+
sessionKind: "child",
|
|
98
|
+
agentId: "verifier-agent",
|
|
99
|
+
label: "Verifier",
|
|
100
|
+
parentSessionId: "parent-session-2",
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -1,13 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
import {
|
|
10
|
-
import type {
|
|
1
|
+
import { useRef } from "react";
|
|
2
|
+
import { ArrowLeft, Loader2, X } from "lucide-react";
|
|
3
|
+
import { useStickyBottomScroll } from "@nextclaw/agent-chat-ui";
|
|
4
|
+
import { ChatMessageListContainer } from "@/components/chat/containers/chat-message-list.container";
|
|
5
|
+
import {
|
|
6
|
+
useNcpChildSessionTabsView,
|
|
7
|
+
type ResolvedChildSessionTab,
|
|
8
|
+
} from "@/components/chat/ncp/session-conversation/use-ncp-child-session-tabs-view";
|
|
9
|
+
import { useNcpSessionConversation } from "@/components/chat/ncp/session-conversation/use-ncp-session-conversation";
|
|
10
|
+
import type { ChatChildSessionTab } from "@/components/chat/stores/chat-thread.store";
|
|
11
|
+
import { AgentIdentityAvatar } from "@/components/common/agent-identity";
|
|
12
|
+
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
13
|
+
import { t } from "@/lib/i18n";
|
|
14
|
+
import { cn } from "@/lib/utils";
|
|
15
|
+
import type { ChatToolActionViewModel } from "@nextclaw/agent-chat-ui";
|
|
11
16
|
|
|
12
17
|
type ChatChildSessionPanelProps = {
|
|
13
18
|
tabs: readonly ChatChildSessionTab[];
|
|
@@ -27,39 +32,84 @@ function ChildSessionPanelConversation({
|
|
|
27
32
|
}) {
|
|
28
33
|
const agent = useNcpSessionConversation(sessionKey);
|
|
29
34
|
const messages = agent.visibleMessages;
|
|
35
|
+
const scrollRef = useRef<HTMLDivElement>(null);
|
|
36
|
+
const { onScroll } = useStickyBottomScroll({
|
|
37
|
+
scrollRef,
|
|
38
|
+
resetKey: sessionKey,
|
|
39
|
+
isLoading: agent.isHydrating,
|
|
40
|
+
hasContent: messages.length > 0,
|
|
41
|
+
contentVersion: messages[messages.length - 1] ?? null,
|
|
42
|
+
stickyThresholdPx: 20,
|
|
43
|
+
});
|
|
30
44
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
45
|
+
return (
|
|
46
|
+
<div
|
|
47
|
+
ref={scrollRef}
|
|
48
|
+
onScroll={onScroll}
|
|
49
|
+
className="h-full overflow-y-auto custom-scrollbar"
|
|
50
|
+
>
|
|
51
|
+
{agent.isHydrating ? (
|
|
52
|
+
<div className="flex h-full items-center justify-center text-sm text-gray-500">
|
|
53
|
+
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
54
|
+
{t("chatChildSessionLoading")}
|
|
55
|
+
</div>
|
|
56
|
+
) : agent.hydrateError ? (
|
|
57
|
+
<div className="px-4 py-5 text-sm text-rose-600">
|
|
58
|
+
{agent.hydrateError.message}
|
|
59
|
+
</div>
|
|
60
|
+
) : messages.length === 0 && !agent.isRunning ? (
|
|
61
|
+
<div className="px-4 py-5 text-sm text-gray-500">
|
|
62
|
+
{t("chatChildSessionEmpty")}
|
|
63
|
+
</div>
|
|
64
|
+
) : (
|
|
65
|
+
<div className="px-4 py-5">
|
|
66
|
+
<ChatMessageListContainer
|
|
67
|
+
messages={messages}
|
|
68
|
+
isSending={agent.isRunning}
|
|
69
|
+
onToolAction={onToolAction}
|
|
70
|
+
/>
|
|
71
|
+
</div>
|
|
72
|
+
)}
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
39
76
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
77
|
+
function ChildSessionPanelMetaChip({ value }: { value: string }) {
|
|
78
|
+
return (
|
|
79
|
+
<span className="inline-flex max-w-full items-center rounded-full border border-gray-200/80 bg-gray-50/90 px-2.5 py-1 text-[11px] font-medium text-gray-600">
|
|
80
|
+
<span className="truncate">{value}</span>
|
|
81
|
+
</span>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
47
84
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
85
|
+
function ChildSessionPanelMetaStrip({ tab }: { tab: ResolvedChildSessionTab }) {
|
|
86
|
+
const metaItems = [
|
|
87
|
+
tab.sessionTypeLabel,
|
|
88
|
+
tab.preferredModel,
|
|
89
|
+
tab.projectName,
|
|
90
|
+
].filter((value): value is string => Boolean(value?.trim()));
|
|
91
|
+
|
|
92
|
+
if (metaItems.length === 0 && !tab.projectRoot) {
|
|
93
|
+
return null;
|
|
54
94
|
}
|
|
55
95
|
|
|
56
96
|
return (
|
|
57
|
-
<div className="
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
97
|
+
<div className="mt-3 space-y-2">
|
|
98
|
+
{metaItems.length > 0 ? (
|
|
99
|
+
<div className="flex flex-wrap gap-1.5">
|
|
100
|
+
{metaItems.map((item) => (
|
|
101
|
+
<ChildSessionPanelMetaChip key={item} value={item} />
|
|
102
|
+
))}
|
|
103
|
+
</div>
|
|
104
|
+
) : null}
|
|
105
|
+
{tab.projectRoot ? (
|
|
106
|
+
<div
|
|
107
|
+
title={tab.projectRoot}
|
|
108
|
+
className="truncate rounded-xl border border-gray-200/70 bg-gray-50/80 px-2.5 py-2 font-mono text-[11px] text-gray-500"
|
|
109
|
+
>
|
|
110
|
+
{tab.projectRoot}
|
|
111
|
+
</div>
|
|
112
|
+
) : null}
|
|
63
113
|
</div>
|
|
64
114
|
);
|
|
65
115
|
}
|
|
@@ -77,7 +127,9 @@ export function ChatChildSessionPanel({
|
|
|
77
127
|
resolvedTabs.find((tab) => tab.sessionKey === activeSessionKey) ??
|
|
78
128
|
resolvedTabs[0] ??
|
|
79
129
|
null;
|
|
80
|
-
const hasParentSession = resolvedTabs.some((tab) =>
|
|
130
|
+
const hasParentSession = resolvedTabs.some((tab) =>
|
|
131
|
+
Boolean(tab.parentSessionKey),
|
|
132
|
+
);
|
|
81
133
|
const shouldShowTabs = resolvedTabs.length > 1;
|
|
82
134
|
|
|
83
135
|
if (!activeTab) {
|
|
@@ -93,18 +145,18 @@ export function ChatChildSessionPanel({
|
|
|
93
145
|
type="button"
|
|
94
146
|
onClick={onBackToParent}
|
|
95
147
|
className={cn(
|
|
96
|
-
|
|
97
|
-
!hasParentSession &&
|
|
148
|
+
"inline-flex items-center gap-1 text-xs font-medium text-gray-600 transition-colors hover:text-gray-900",
|
|
149
|
+
!hasParentSession && "pointer-events-none opacity-0",
|
|
98
150
|
)}
|
|
99
151
|
>
|
|
100
152
|
<ArrowLeft className="h-3.5 w-3.5" />
|
|
101
|
-
<span>{t(
|
|
153
|
+
<span>{t("chatBackToParent")}</span>
|
|
102
154
|
</button>
|
|
103
155
|
<button
|
|
104
156
|
type="button"
|
|
105
157
|
onClick={onClose}
|
|
106
158
|
className="rounded-full border border-gray-200/80 p-1.5 text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-900"
|
|
107
|
-
aria-label={t(
|
|
159
|
+
aria-label={t("chatChildSessionClosePanel")}
|
|
108
160
|
>
|
|
109
161
|
<X className="h-4 w-4" />
|
|
110
162
|
</button>
|
|
@@ -138,20 +190,26 @@ export function ChatChildSessionPanel({
|
|
|
138
190
|
className="h-4 w-4 shrink-0"
|
|
139
191
|
/>
|
|
140
192
|
) : null}
|
|
141
|
-
<span className="max-w-[132px] truncate">
|
|
193
|
+
<span className="max-w-[132px] truncate">
|
|
194
|
+
{tab.title}
|
|
195
|
+
</span>
|
|
142
196
|
</TabsTrigger>
|
|
143
197
|
))}
|
|
144
198
|
</TabsList>
|
|
145
199
|
</Tabs>
|
|
146
200
|
</div>
|
|
147
201
|
) : null}
|
|
202
|
+
<ChildSessionPanelMetaStrip tab={activeTab} />
|
|
148
203
|
</div>
|
|
149
204
|
|
|
150
|
-
<div className="flex-1 min-h-0
|
|
205
|
+
<div className="flex-1 min-h-0">
|
|
151
206
|
{resolvedTabs.map((tab) => (
|
|
152
207
|
<div
|
|
153
208
|
key={tab.sessionKey}
|
|
154
|
-
className={cn(
|
|
209
|
+
className={cn(
|
|
210
|
+
"h-full",
|
|
211
|
+
tab.sessionKey === activeSessionKey ? "block" : "hidden",
|
|
212
|
+
)}
|
|
155
213
|
>
|
|
156
214
|
<ChildSessionPanelConversation
|
|
157
215
|
sessionKey={tab.sessionKey}
|
|
@@ -6,7 +6,9 @@ import {
|
|
|
6
6
|
resolveSelectedModelValue,
|
|
7
7
|
resolveSelectedThinkingLevelValue
|
|
8
8
|
} from '@/components/chat/chat-session-preference-governance';
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
shouldClearPendingProjectRootOverride
|
|
11
|
+
} from '@/components/chat/ncp/NcpChatPage';
|
|
10
12
|
|
|
11
13
|
const modelOptions = [
|
|
12
14
|
{
|
|
@@ -155,33 +157,28 @@ describe('resolveSelectedModelValue', () => {
|
|
|
155
157
|
});
|
|
156
158
|
});
|
|
157
159
|
|
|
158
|
-
describe('
|
|
159
|
-
it('does not
|
|
160
|
+
describe('shouldClearPendingProjectRootOverride', () => {
|
|
161
|
+
it('does not clear an unrelated session project override', () => {
|
|
160
162
|
expect(
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
163
|
+
shouldClearPendingProjectRootOverride({
|
|
164
|
+
pendingProjectRoot: '/tmp/project-alpha',
|
|
165
|
+
pendingProjectRootSessionKey: 'draft-project-alpha',
|
|
166
|
+
sessionKey: 'session-existing',
|
|
167
|
+
selectedSessionProjectRoot: '/tmp/project-alpha'
|
|
164
168
|
})
|
|
165
169
|
).toBe(false);
|
|
166
170
|
});
|
|
167
171
|
|
|
168
|
-
it('
|
|
172
|
+
it('clears the override only after the bound session reflects the same project root', () => {
|
|
169
173
|
expect(
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
174
|
+
shouldClearPendingProjectRootOverride({
|
|
175
|
+
pendingProjectRoot: '/tmp/project-alpha',
|
|
176
|
+
pendingProjectRootSessionKey: 'draft-after-refresh',
|
|
177
|
+
sessionKey: 'draft-after-refresh',
|
|
178
|
+
selectedSessionProjectRoot: '/tmp/project-alpha'
|
|
173
179
|
})
|
|
174
180
|
).toBe(true);
|
|
175
181
|
});
|
|
176
|
-
|
|
177
|
-
it('does not replace the draft session id while staying on the same session', () => {
|
|
178
|
-
expect(
|
|
179
|
-
shouldRefreshDraftSessionId({
|
|
180
|
-
previousSelectedSessionKey: 'session-1',
|
|
181
|
-
nextSelectedSessionKey: 'session-1'
|
|
182
|
-
})
|
|
183
|
-
).toBe(false);
|
|
184
|
-
});
|
|
185
182
|
});
|
|
186
183
|
|
|
187
184
|
describe('resolveRecentSessionPreferredModel', () => {
|