@nextclaw/ui 0.11.17 → 0.11.19
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 +26 -0
- package/dist/assets/ChannelsList-DAx7wv0_.js +8 -0
- package/dist/assets/{ChatPage-C47h6sfA.js → ChatPage-l2PYwCeB.js} +9 -7
- package/dist/assets/DocBrowser-CIHLqoIm.js +1 -0
- package/dist/assets/{DocBrowser-C_C7daBv.js → DocBrowser-DKkE3Y4I.js} +1 -1
- package/dist/assets/{DocBrowserContext-CJ-YKtWh.js → DocBrowserContext-BcZRBsCg.js} +1 -1
- package/dist/assets/{LogoBadge-DRDmIa7o.js → LogoBadge-BIPDLEwK.js} +1 -1
- package/dist/assets/{MarketplacePage-DaSRsFUA.js → MarketplacePage-Dlp5BgCh.js} +1 -1
- package/dist/assets/MarketplacePage-TVeyVOuO.js +1 -0
- package/dist/assets/{McpMarketplacePage-B7HZn8zG.js → McpMarketplacePage-CwKtAil8.js} +1 -1
- package/dist/assets/{ModelConfig-MSi8VF9p.js → ModelConfig-Dg6F3Ldb.js} +1 -1
- package/dist/assets/{ProvidersList-_NBpSQWn.js → ProvidersList-f7bQdRxA.js} +1 -1
- package/dist/assets/{RemoteAccessPage-DSmdSsCJ.js → RemoteAccessPage-w_dY7P4T.js} +1 -1
- package/dist/assets/{RuntimeConfig-msA8NZOj.js → RuntimeConfig-M4OKjmgU.js} +1 -1
- package/dist/assets/{SearchConfig-BBtxHIN_.js → SearchConfig-v46R5a2U.js} +1 -1
- package/dist/assets/{SecretsConfig-BMAqj52o.js → SecretsConfig-CXvUpbB_.js} +1 -1
- package/dist/assets/{SessionsConfig-CEJqgz8F.js → SessionsConfig-7vUHMtOh.js} +1 -1
- package/dist/assets/{book-open-1agbn9dT.js → book-open-DzSduAaw.js} +1 -1
- package/dist/assets/{chat-session-display-DBBUJOYN.js → chat-session-display-CGfXhJoT.js} +1 -1
- package/dist/assets/{chunk-JZWAC4HX-BUooP92l.js → chunk-JZWAC4HX-C1vpvW4r.js} +1 -1
- package/dist/assets/{config-jOAXZWun.js → config-Df97LeLR.js} +1 -1
- package/dist/assets/{createLucideIcon-B8FV3fzy.js → createLucideIcon-CcR5wVoU.js} +1 -1
- package/dist/assets/{dist-D3OJg9V0.js → dist-BMlnBah3.js} +1 -1
- package/dist/assets/{dist-Cy668qFZ.js → dist-Dii9v3X9.js} +1 -1
- package/dist/assets/{external-link-DI4ZmR3r.js → external-link-CnSDrvJE.js} +1 -1
- package/dist/assets/{hash-DoXBhX9w.js → hash-CAnX6PNt.js} +1 -1
- package/dist/assets/i18n-CXBpwAwA.js +1 -0
- package/dist/assets/{index-bAeWRAyo.js → index-B0DzQqwv.js} +3 -3
- package/dist/assets/index-BahpXJg8.css +1 -0
- package/dist/assets/{label-Cz0q8fx4.js → label-CtIFj7_6.js} +1 -1
- package/dist/assets/loader-circle-qgU4zQDw.js +1 -0
- package/dist/assets/{logos-DjrINZ7P.js → logos-3KFNiOej.js} +1 -1
- package/dist/assets/{page-layout-Hr-Dvq4o.js → page-layout-BMwpn87D.js} +1 -1
- package/dist/assets/plus-C9cYVbL-.js +1 -0
- package/dist/assets/{popover-_nEUAtWY.js → popover-BIzq25oH.js} +1 -1
- package/dist/assets/{react-Bsr_GLhi.js → react-ji6GGP_j.js} +1 -1
- package/dist/assets/{save-Caodcm4q.js → save-CMgYkJ-y.js} +1 -1
- package/dist/assets/search-sl1OeJFl.js +1 -0
- package/dist/assets/{security-config-Zf1RBeS1.js → security-config-Xi5DYW7j.js} +1 -1
- package/dist/assets/{select-D60QRHg9.js → select-Cz82gl01.js} +1 -1
- package/dist/assets/skeleton-rgIt7a5q.js +1 -0
- package/dist/assets/{status-dot-D43lBF1a.js → status-dot-C7q1HvLH.js} +1 -1
- package/dist/assets/{switch-CcBS0F3U.js → switch-DYswvkYj.js} +1 -1
- package/dist/assets/{tabs-custom-UTbefkqB.js → tabs-custom-DKYQxrx1.js} +1 -1
- package/dist/assets/{trash-2-DvPrU1xO.js → trash-2-DfXI7-ap.js} +1 -1
- package/dist/assets/{useConfirmDialog-B89bxcd6.js → useConfirmDialog-CXDAxtRL.js} +1 -1
- package/dist/assets/{useMutation-BpXHE2OV.js → useMutation-s2sn2yzh.js} +1 -1
- package/dist/assets/x-MIimOGs6.js +1 -0
- package/dist/index.html +18 -18
- package/package.json +6 -6
- package/src/components/chat/ChatConversationPanel.tsx +1 -0
- package/src/components/chat/adapters/chat-input-bar.adapter.test.ts +103 -1
- package/src/components/chat/adapters/chat-input-bar.adapter.ts +80 -2
- package/src/components/chat/adapters/chat-message-inline-content.adapter.ts +95 -0
- package/src/components/chat/adapters/chat-message-part.adapter.ts +384 -0
- package/src/components/chat/adapters/chat-message.adapter.test.ts +49 -2
- package/src/components/chat/adapters/chat-message.adapter.ts +18 -366
- package/src/components/chat/adapters/chat-message.subagent-tool-card.ts +46 -17
- package/src/components/chat/chat-composer-state.test.ts +2 -6
- package/src/components/chat/chat-composer-state.ts +27 -6
- package/src/components/chat/chat-inline-token.utils.test.ts +87 -0
- package/src/components/chat/chat-inline-token.utils.ts +146 -0
- package/src/components/chat/chat-input/chat-input-bar.controller.test.tsx +24 -0
- package/src/components/chat/chat-input/chat-input-bar.controller.ts +81 -44
- package/src/components/chat/chat-recent-skills.manager.ts +8 -0
- package/src/components/chat/containers/chat-input-bar.container.tsx +31 -4
- package/src/components/chat/containers/chat-message-list.container.test.tsx +45 -0
- package/src/components/chat/containers/chat-message-list.container.tsx +11 -5
- package/src/components/chat/ncp/NcpChatPage.tsx +10 -1
- package/src/components/chat/ncp/ncp-chat-input.manager.ts +18 -4
- package/src/components/chat/presenter/chat-presenter-context.tsx +1 -0
- package/src/components/config/ChannelForm.tsx +71 -39
- package/src/components/config/channel-form-fields.test.ts +28 -0
- package/src/components/config/channel-form-fields.ts +95 -30
- package/src/components/config/weixin-channel-auth-section.test.tsx +26 -0
- package/src/components/config/weixin-channel-auth-section.tsx +6 -2
- package/src/lib/i18n.channel-auth.ts +5 -0
- package/dist/assets/ChannelsList-askIl_uW.js +0 -8
- package/dist/assets/DocBrowser-Cf7uSIoM.js +0 -1
- package/dist/assets/MarketplacePage-q12sRrvZ.js +0 -1
- package/dist/assets/i18n-Cn8SErDV.js +0 -1
- package/dist/assets/index-B2VeWxfm.css +0 -1
- package/dist/assets/loader-circle-d_mzMi2S.js +0 -1
- package/dist/assets/plus-BnGg0mB-.js +0 -1
- package/dist/assets/search-CQCQaN4Z.js +0 -1
- package/dist/assets/skeleton-BvV_2nf3.js +0 -1
- package/dist/assets/x-C8AWDn7c.js +0 -1
|
@@ -1,49 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
summarizeToolArgs,
|
|
4
|
-
type ToolCard,
|
|
5
|
-
} from "@/lib/chat-message";
|
|
6
|
-
import { buildSubagentToolCard } from "@/components/chat/adapters/chat-message.subagent-tool-card";
|
|
1
|
+
import { adaptChatMessagePart } from "@/components/chat/adapters/chat-message-part.adapter";
|
|
2
|
+
import type { ChatInlineTokenSource } from "@/components/chat/chat-inline-token.utils";
|
|
7
3
|
import type {
|
|
8
4
|
ChatMessageRole,
|
|
9
5
|
ChatMessageViewModel,
|
|
10
|
-
ChatToolPartViewModel,
|
|
11
6
|
} from "@nextclaw/agent-chat-ui";
|
|
7
|
+
import type {
|
|
8
|
+
ChatMessageAdapterTexts,
|
|
9
|
+
ChatMessagePartSource,
|
|
10
|
+
} from "@/components/chat/adapters/chat-message-part.adapter";
|
|
12
11
|
|
|
13
|
-
export type
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
18
|
-
| {
|
|
19
|
-
type: "file";
|
|
20
|
-
mimeType: string;
|
|
21
|
-
data: string;
|
|
22
|
-
url?: string;
|
|
23
|
-
name?: string;
|
|
24
|
-
sizeBytes?: number;
|
|
25
|
-
}
|
|
26
|
-
| {
|
|
27
|
-
type: "reasoning";
|
|
28
|
-
reasoning: string;
|
|
29
|
-
}
|
|
30
|
-
| {
|
|
31
|
-
type: "tool-invocation";
|
|
32
|
-
toolInvocation: {
|
|
33
|
-
status?: string;
|
|
34
|
-
toolName: string;
|
|
35
|
-
args?: unknown;
|
|
36
|
-
parsedArgs?: unknown;
|
|
37
|
-
result?: unknown;
|
|
38
|
-
error?: string;
|
|
39
|
-
cancelled?: boolean;
|
|
40
|
-
toolCallId?: string;
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
| {
|
|
44
|
-
type: string;
|
|
45
|
-
[key: string]: unknown;
|
|
46
|
-
};
|
|
12
|
+
export type {
|
|
13
|
+
ChatMessageAdapterTexts,
|
|
14
|
+
ChatMessagePartSource,
|
|
15
|
+
} from "@/components/chat/adapters/chat-message-part.adapter";
|
|
47
16
|
|
|
48
17
|
export type ChatMessageSource = {
|
|
49
18
|
id: string;
|
|
@@ -51,146 +20,11 @@ export type ChatMessageSource = {
|
|
|
51
20
|
meta?: {
|
|
52
21
|
timestamp?: string;
|
|
53
22
|
status?: string;
|
|
23
|
+
inlineTokens?: ChatInlineTokenSource[];
|
|
54
24
|
};
|
|
55
25
|
parts: ChatMessagePartSource[];
|
|
56
26
|
};
|
|
57
27
|
|
|
58
|
-
export type ChatMessageAdapterTexts = {
|
|
59
|
-
roleLabels: {
|
|
60
|
-
user: string;
|
|
61
|
-
assistant: string;
|
|
62
|
-
tool: string;
|
|
63
|
-
system: string;
|
|
64
|
-
fallback: string;
|
|
65
|
-
};
|
|
66
|
-
reasoningLabel: string;
|
|
67
|
-
toolCallLabel: string;
|
|
68
|
-
toolResultLabel: string;
|
|
69
|
-
toolNoOutputLabel: string;
|
|
70
|
-
toolOutputLabel: string;
|
|
71
|
-
toolStatusPreparingLabel: string;
|
|
72
|
-
toolStatusRunningLabel: string;
|
|
73
|
-
toolStatusCompletedLabel: string;
|
|
74
|
-
toolStatusFailedLabel: string;
|
|
75
|
-
toolStatusCancelledLabel: string;
|
|
76
|
-
imageAttachmentLabel: string;
|
|
77
|
-
fileAttachmentLabel: string;
|
|
78
|
-
unknownPartLabel: string;
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
const INVISIBLE_ONLY_TEXT_PATTERN = /\u200B|\u200C|\u200D|\u2060|\uFEFF/g;
|
|
82
|
-
|
|
83
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
84
|
-
return typeof value === "object" && value !== null;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function readOptionalString(value: unknown): string | null {
|
|
88
|
-
if (typeof value !== "string") {
|
|
89
|
-
return null;
|
|
90
|
-
}
|
|
91
|
-
const trimmed = value.trim();
|
|
92
|
-
return trimmed.length > 0 ? trimmed : null;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function readOptionalNumber(value: unknown): number | null {
|
|
96
|
-
if (typeof value === "number" && Number.isFinite(value) && value >= 0) {
|
|
97
|
-
return value;
|
|
98
|
-
}
|
|
99
|
-
if (typeof value !== "string") {
|
|
100
|
-
return null;
|
|
101
|
-
}
|
|
102
|
-
const trimmed = value.trim();
|
|
103
|
-
if (!trimmed) {
|
|
104
|
-
return null;
|
|
105
|
-
}
|
|
106
|
-
const parsed = Number(trimmed);
|
|
107
|
-
return Number.isFinite(parsed) && parsed >= 0 ? parsed : null;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function extractAssetFileView(
|
|
111
|
-
value: unknown,
|
|
112
|
-
texts: ChatMessageAdapterTexts,
|
|
113
|
-
): {
|
|
114
|
-
type: "file";
|
|
115
|
-
file: {
|
|
116
|
-
label: string;
|
|
117
|
-
mimeType: string;
|
|
118
|
-
dataUrl: string;
|
|
119
|
-
sizeBytes?: number;
|
|
120
|
-
isImage: boolean;
|
|
121
|
-
};
|
|
122
|
-
} | null {
|
|
123
|
-
if (!isRecord(value)) {
|
|
124
|
-
return null;
|
|
125
|
-
}
|
|
126
|
-
const assetCandidate = isRecord(value.asset)
|
|
127
|
-
? value.asset
|
|
128
|
-
: Array.isArray(value.assets) &&
|
|
129
|
-
value.assets.length > 0 &&
|
|
130
|
-
isRecord(value.assets[0])
|
|
131
|
-
? value.assets[0]
|
|
132
|
-
: null;
|
|
133
|
-
if (!assetCandidate) {
|
|
134
|
-
return null;
|
|
135
|
-
}
|
|
136
|
-
const url = readOptionalString(assetCandidate.url);
|
|
137
|
-
const mimeType =
|
|
138
|
-
readOptionalString(assetCandidate.mimeType) ?? "application/octet-stream";
|
|
139
|
-
const sizeBytes = readOptionalNumber(assetCandidate.sizeBytes);
|
|
140
|
-
if (!url) {
|
|
141
|
-
return null;
|
|
142
|
-
}
|
|
143
|
-
const label =
|
|
144
|
-
readOptionalString(assetCandidate.name) ??
|
|
145
|
-
(mimeType.startsWith("image/")
|
|
146
|
-
? texts.imageAttachmentLabel
|
|
147
|
-
: texts.fileAttachmentLabel);
|
|
148
|
-
return {
|
|
149
|
-
type: "file",
|
|
150
|
-
file: {
|
|
151
|
-
label,
|
|
152
|
-
mimeType,
|
|
153
|
-
dataUrl: url,
|
|
154
|
-
...(sizeBytes != null ? { sizeBytes } : {}),
|
|
155
|
-
isImage: mimeType.startsWith("image/"),
|
|
156
|
-
},
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function isTextPart(
|
|
161
|
-
part: ChatMessagePartSource,
|
|
162
|
-
): part is Extract<ChatMessagePartSource, { type: "text" }> {
|
|
163
|
-
return part.type === "text" && typeof part.text === "string";
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function isReasoningPart(
|
|
167
|
-
part: ChatMessagePartSource,
|
|
168
|
-
): part is Extract<ChatMessagePartSource, { type: "reasoning" }> {
|
|
169
|
-
return part.type === "reasoning" && typeof part.reasoning === "string";
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
function isFilePart(
|
|
173
|
-
part: ChatMessagePartSource,
|
|
174
|
-
): part is Extract<ChatMessagePartSource, { type: "file" }> {
|
|
175
|
-
return (
|
|
176
|
-
part.type === "file" &&
|
|
177
|
-
typeof part.mimeType === "string" &&
|
|
178
|
-
typeof part.data === "string"
|
|
179
|
-
);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function isToolInvocationPart(
|
|
183
|
-
part: ChatMessagePartSource,
|
|
184
|
-
): part is Extract<ChatMessagePartSource, { type: "tool-invocation" }> {
|
|
185
|
-
if (part.type !== "tool-invocation") {
|
|
186
|
-
return false;
|
|
187
|
-
}
|
|
188
|
-
if (!isRecord(part.toolInvocation)) {
|
|
189
|
-
return false;
|
|
190
|
-
}
|
|
191
|
-
return typeof part.toolInvocation.toolName === "string";
|
|
192
|
-
}
|
|
193
|
-
|
|
194
28
|
function resolveMessageTimestamp(message: ChatMessageSource): string {
|
|
195
29
|
const candidate = message.meta?.timestamp;
|
|
196
30
|
if (candidate && Number.isFinite(Date.parse(candidate))) {
|
|
@@ -230,94 +64,6 @@ function resolveUiRole(role: string): ChatMessageRole {
|
|
|
230
64
|
return "message";
|
|
231
65
|
}
|
|
232
66
|
|
|
233
|
-
function buildToolCard(
|
|
234
|
-
toolCard: ToolCardViewSource,
|
|
235
|
-
texts: ChatMessageAdapterTexts,
|
|
236
|
-
): ChatToolPartViewModel {
|
|
237
|
-
return {
|
|
238
|
-
kind: toolCard.kind,
|
|
239
|
-
toolName: toolCard.name,
|
|
240
|
-
summary: toolCard.detail,
|
|
241
|
-
output: toolCard.text,
|
|
242
|
-
hasResult: Boolean(toolCard.hasResult),
|
|
243
|
-
statusTone: toolCard.statusTone,
|
|
244
|
-
statusLabel: toolCard.statusLabel,
|
|
245
|
-
titleLabel:
|
|
246
|
-
toolCard.kind === "call" ? texts.toolCallLabel : texts.toolResultLabel,
|
|
247
|
-
outputLabel: texts.toolOutputLabel,
|
|
248
|
-
emptyLabel: texts.toolNoOutputLabel,
|
|
249
|
-
};
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
type ToolCardViewSource = ToolCard & {
|
|
253
|
-
statusTone: ChatToolPartViewModel["statusTone"];
|
|
254
|
-
statusLabel: string;
|
|
255
|
-
};
|
|
256
|
-
|
|
257
|
-
function resolveToolCardStatus(params: {
|
|
258
|
-
status?: string;
|
|
259
|
-
error?: string;
|
|
260
|
-
cancelled?: boolean;
|
|
261
|
-
result?: unknown;
|
|
262
|
-
texts: ChatMessageAdapterTexts;
|
|
263
|
-
}): Pick<
|
|
264
|
-
ChatToolPartViewModel,
|
|
265
|
-
"kind" | "hasResult" | "statusTone" | "statusLabel"
|
|
266
|
-
> {
|
|
267
|
-
const rawStatus =
|
|
268
|
-
typeof params.status === "string" ? params.status.trim().toLowerCase() : "";
|
|
269
|
-
const hasError =
|
|
270
|
-
typeof params.error === "string" && params.error.trim().length > 0;
|
|
271
|
-
const isCancelled = params.cancelled === true || rawStatus === "cancelled";
|
|
272
|
-
if (isCancelled) {
|
|
273
|
-
return {
|
|
274
|
-
kind: "result",
|
|
275
|
-
hasResult: true,
|
|
276
|
-
statusTone: "cancelled",
|
|
277
|
-
statusLabel: params.texts.toolStatusCancelledLabel,
|
|
278
|
-
};
|
|
279
|
-
}
|
|
280
|
-
if (hasError || rawStatus === "error") {
|
|
281
|
-
return {
|
|
282
|
-
kind: "result",
|
|
283
|
-
hasResult: true,
|
|
284
|
-
statusTone: "error",
|
|
285
|
-
statusLabel: params.texts.toolStatusFailedLabel,
|
|
286
|
-
};
|
|
287
|
-
}
|
|
288
|
-
if (rawStatus === "result" || params.result != null) {
|
|
289
|
-
return {
|
|
290
|
-
kind: "result",
|
|
291
|
-
hasResult: true,
|
|
292
|
-
statusTone: "success",
|
|
293
|
-
statusLabel: params.texts.toolStatusCompletedLabel,
|
|
294
|
-
};
|
|
295
|
-
}
|
|
296
|
-
if (rawStatus === "partial-call") {
|
|
297
|
-
return {
|
|
298
|
-
kind: "call",
|
|
299
|
-
hasResult: false,
|
|
300
|
-
statusTone: "running",
|
|
301
|
-
statusLabel: params.texts.toolStatusPreparingLabel,
|
|
302
|
-
};
|
|
303
|
-
}
|
|
304
|
-
return {
|
|
305
|
-
kind: "call",
|
|
306
|
-
hasResult: false,
|
|
307
|
-
statusTone: "running",
|
|
308
|
-
statusLabel: params.texts.toolStatusRunningLabel,
|
|
309
|
-
};
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
function toRenderableText(value: string): string | null {
|
|
313
|
-
const trimmed = value.trim();
|
|
314
|
-
if (!trimmed) {
|
|
315
|
-
return null;
|
|
316
|
-
}
|
|
317
|
-
const visible = trimmed.replace(INVISIBLE_ONLY_TEXT_PATTERN, "").trim();
|
|
318
|
-
return visible ? trimmed : null;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
67
|
type ChatMessageAdapterParams = {
|
|
322
68
|
texts: ChatMessageAdapterTexts;
|
|
323
69
|
formatTimestamp: (value: string) => string;
|
|
@@ -334,107 +80,13 @@ export function adaptChatMessage(
|
|
|
334
80
|
timestampLabel: params.formatTimestamp(resolveMessageTimestamp(message)),
|
|
335
81
|
status: message.meta?.status,
|
|
336
82
|
parts: message.parts
|
|
337
|
-
.map((part) =>
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
type: "markdown" as const,
|
|
345
|
-
text,
|
|
346
|
-
};
|
|
347
|
-
}
|
|
348
|
-
if (isReasoningPart(part)) {
|
|
349
|
-
const text = toRenderableText(part.reasoning);
|
|
350
|
-
if (!text) {
|
|
351
|
-
return null;
|
|
352
|
-
}
|
|
353
|
-
return {
|
|
354
|
-
type: "reasoning" as const,
|
|
355
|
-
text,
|
|
356
|
-
label: params.texts.reasoningLabel,
|
|
357
|
-
};
|
|
358
|
-
}
|
|
359
|
-
if (isFilePart(part)) {
|
|
360
|
-
const isImage = part.mimeType.startsWith("image/");
|
|
361
|
-
const sizeBytes = readOptionalNumber(part.sizeBytes);
|
|
362
|
-
return {
|
|
363
|
-
type: "file" as const,
|
|
364
|
-
file: {
|
|
365
|
-
label:
|
|
366
|
-
typeof part.name === "string" && part.name.trim()
|
|
367
|
-
? part.name.trim()
|
|
368
|
-
: isImage
|
|
369
|
-
? params.texts.imageAttachmentLabel
|
|
370
|
-
: params.texts.fileAttachmentLabel,
|
|
371
|
-
mimeType: part.mimeType,
|
|
372
|
-
dataUrl:
|
|
373
|
-
typeof part.url === "string" && part.url.trim().length > 0
|
|
374
|
-
? part.url.trim()
|
|
375
|
-
: `data:${part.mimeType};base64,${part.data}`,
|
|
376
|
-
...(sizeBytes != null ? { sizeBytes } : {}),
|
|
377
|
-
isImage,
|
|
378
|
-
},
|
|
379
|
-
};
|
|
380
|
-
}
|
|
381
|
-
if (isToolInvocationPart(part)) {
|
|
382
|
-
const invocation = part.toolInvocation;
|
|
383
|
-
const assetFileView = extractAssetFileView(
|
|
384
|
-
invocation.result,
|
|
385
|
-
params.texts,
|
|
386
|
-
);
|
|
387
|
-
if (assetFileView) {
|
|
388
|
-
return assetFileView;
|
|
389
|
-
}
|
|
390
|
-
const subagentToolCard = buildSubagentToolCard({
|
|
391
|
-
invocation,
|
|
392
|
-
texts: params.texts,
|
|
393
|
-
});
|
|
394
|
-
if (subagentToolCard) {
|
|
395
|
-
return {
|
|
396
|
-
type: "tool-card" as const,
|
|
397
|
-
card: buildToolCard(subagentToolCard, params.texts),
|
|
398
|
-
};
|
|
399
|
-
}
|
|
400
|
-
const statusView = resolveToolCardStatus({
|
|
401
|
-
status: invocation.status,
|
|
402
|
-
error: invocation.error,
|
|
403
|
-
cancelled: invocation.cancelled,
|
|
404
|
-
result: invocation.result,
|
|
405
|
-
texts: params.texts,
|
|
406
|
-
});
|
|
407
|
-
const detail = summarizeToolArgs(
|
|
408
|
-
invocation.parsedArgs ?? invocation.args,
|
|
409
|
-
);
|
|
410
|
-
const rawResult =
|
|
411
|
-
typeof invocation.error === "string" && invocation.error.trim()
|
|
412
|
-
? invocation.error.trim()
|
|
413
|
-
: invocation.result != null
|
|
414
|
-
? stringifyUnknown(invocation.result).trim()
|
|
415
|
-
: "";
|
|
416
|
-
const card: ToolCardViewSource = {
|
|
417
|
-
kind: statusView.kind,
|
|
418
|
-
name: invocation.toolName,
|
|
419
|
-
detail,
|
|
420
|
-
text: rawResult || undefined,
|
|
421
|
-
callId: invocation.toolCallId || undefined,
|
|
422
|
-
hasResult: statusView.hasResult,
|
|
423
|
-
statusTone: statusView.statusTone,
|
|
424
|
-
statusLabel: statusView.statusLabel,
|
|
425
|
-
};
|
|
426
|
-
return {
|
|
427
|
-
type: "tool-card" as const,
|
|
428
|
-
card: buildToolCard(card, params.texts),
|
|
429
|
-
};
|
|
430
|
-
}
|
|
431
|
-
return {
|
|
432
|
-
type: "unknown" as const,
|
|
433
|
-
label: params.texts.unknownPartLabel,
|
|
434
|
-
rawType: typeof part.type === "string" ? part.type : "unknown",
|
|
435
|
-
text: stringifyUnknown(part),
|
|
436
|
-
};
|
|
437
|
-
})
|
|
83
|
+
.map((part) =>
|
|
84
|
+
adaptChatMessagePart({
|
|
85
|
+
part,
|
|
86
|
+
inlineTokens: message.meta?.inlineTokens ?? [],
|
|
87
|
+
texts: params.texts,
|
|
88
|
+
}),
|
|
89
|
+
)
|
|
438
90
|
.filter((part) => part !== null),
|
|
439
91
|
};
|
|
440
92
|
}
|
|
@@ -57,6 +57,47 @@ function readSubagentRunResult(value: unknown): SubagentRunResult | null {
|
|
|
57
57
|
return null;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
function buildSubagentDetail(subagentRun: SubagentRunResult, fallbackArgs: unknown): string | undefined {
|
|
61
|
+
const detailParts = [
|
|
62
|
+
readOptionalString(subagentRun.label)
|
|
63
|
+
? `label: ${subagentRun.label?.trim()}`
|
|
64
|
+
: null,
|
|
65
|
+
readOptionalString(subagentRun.runId)
|
|
66
|
+
? `run: ${subagentRun.runId?.trim()}`
|
|
67
|
+
: null,
|
|
68
|
+
readOptionalString(subagentRun.task)
|
|
69
|
+
? `task: ${subagentRun.task?.trim()}`
|
|
70
|
+
: null,
|
|
71
|
+
].filter((value): value is string => Boolean(value));
|
|
72
|
+
|
|
73
|
+
return detailParts.join(" · ") || summarizeToolArgs(fallbackArgs);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function buildSubagentOutput(subagentRun: SubagentRunResult): string | undefined {
|
|
77
|
+
const runId = readOptionalString(subagentRun.runId);
|
|
78
|
+
const label = readOptionalString(subagentRun.label);
|
|
79
|
+
const task = readOptionalString(subagentRun.task);
|
|
80
|
+
const status = readOptionalString(subagentRun.status)?.toLowerCase();
|
|
81
|
+
const resultText =
|
|
82
|
+
typeof subagentRun.result !== "undefined"
|
|
83
|
+
? stringifyUnknown(subagentRun.result).trim()
|
|
84
|
+
: "";
|
|
85
|
+
const messageText = readOptionalString(subagentRun.message);
|
|
86
|
+
|
|
87
|
+
const sections = [
|
|
88
|
+
runId ? `Run ID: ${runId}` : null,
|
|
89
|
+
label ? `Label: ${label}` : null,
|
|
90
|
+
task ? `Task:\n${task}` : null,
|
|
91
|
+
resultText
|
|
92
|
+
? `${status === "failed" ? "Error" : "Result"}:\n${resultText}`
|
|
93
|
+
: messageText
|
|
94
|
+
? `Status:\n${messageText}`
|
|
95
|
+
: null,
|
|
96
|
+
].filter((value): value is string => Boolean(value));
|
|
97
|
+
|
|
98
|
+
return sections.length > 0 ? sections.join("\n\n") : undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
60
101
|
export function buildSubagentToolCard(params: {
|
|
61
102
|
invocation: SpawnToolInvocation;
|
|
62
103
|
texts: SubagentToolCardTexts;
|
|
@@ -70,27 +111,15 @@ export function buildSubagentToolCard(params: {
|
|
|
70
111
|
return null;
|
|
71
112
|
}
|
|
72
113
|
|
|
73
|
-
const detailParts = [
|
|
74
|
-
readOptionalString(subagentRun.label)
|
|
75
|
-
? `label: ${subagentRun.label?.trim()}`
|
|
76
|
-
: null,
|
|
77
|
-
readOptionalString(subagentRun.task)
|
|
78
|
-
? `task: ${subagentRun.task?.trim()}`
|
|
79
|
-
: null,
|
|
80
|
-
].filter((value): value is string => Boolean(value));
|
|
81
114
|
const normalizedStatus = readOptionalString(subagentRun.status)?.toLowerCase();
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
? stringifyUnknown(subagentRun.result).trim()
|
|
85
|
-
: "") ||
|
|
86
|
-
readOptionalString(subagentRun.message) ||
|
|
87
|
-
undefined;
|
|
115
|
+
const detail = buildSubagentDetail(subagentRun, params.invocation.args);
|
|
116
|
+
const output = buildSubagentOutput(subagentRun);
|
|
88
117
|
|
|
89
118
|
if (normalizedStatus === "failed") {
|
|
90
119
|
return {
|
|
91
120
|
kind: "result",
|
|
92
121
|
name: params.invocation.toolName,
|
|
93
|
-
detail
|
|
122
|
+
detail,
|
|
94
123
|
text: output,
|
|
95
124
|
callId: params.invocation.toolCallId || undefined,
|
|
96
125
|
hasResult: Boolean(output),
|
|
@@ -103,7 +132,7 @@ export function buildSubagentToolCard(params: {
|
|
|
103
132
|
return {
|
|
104
133
|
kind: "result",
|
|
105
134
|
name: params.invocation.toolName,
|
|
106
|
-
detail
|
|
135
|
+
detail,
|
|
107
136
|
text: output,
|
|
108
137
|
callId: params.invocation.toolCallId || undefined,
|
|
109
138
|
hasResult: Boolean(output),
|
|
@@ -115,7 +144,7 @@ export function buildSubagentToolCard(params: {
|
|
|
115
144
|
return {
|
|
116
145
|
kind: "result",
|
|
117
146
|
name: params.invocation.toolName,
|
|
118
|
-
detail
|
|
147
|
+
detail,
|
|
119
148
|
text: output,
|
|
120
149
|
callId: params.invocation.toolCallId || undefined,
|
|
121
150
|
hasResult: Boolean(output),
|
|
@@ -2,7 +2,7 @@ import { createChatComposerTextNode, createChatComposerTokenNode } from '@nextcl
|
|
|
2
2
|
import { deriveNcpMessagePartsFromComposer } from '@/components/chat/chat-composer-state';
|
|
3
3
|
|
|
4
4
|
describe('deriveNcpMessagePartsFromComposer', () => {
|
|
5
|
-
it('preserves interleaved text and image token order while
|
|
5
|
+
it('preserves interleaved text and image token order while serializing skill tokens inline', () => {
|
|
6
6
|
const parts = deriveNcpMessagePartsFromComposer(
|
|
7
7
|
[
|
|
8
8
|
createChatComposerTextNode('before '),
|
|
@@ -56,11 +56,7 @@ describe('deriveNcpMessagePartsFromComposer', () => {
|
|
|
56
56
|
},
|
|
57
57
|
{
|
|
58
58
|
type: 'text',
|
|
59
|
-
text: ' between '
|
|
60
|
-
},
|
|
61
|
-
{
|
|
62
|
-
type: 'text',
|
|
63
|
-
text: 'after'
|
|
59
|
+
text: ' between $web-searchafter'
|
|
64
60
|
},
|
|
65
61
|
{
|
|
66
62
|
type: 'file',
|
|
@@ -11,6 +11,27 @@ import {
|
|
|
11
11
|
serializeChatComposerPlainText
|
|
12
12
|
} from '@nextclaw/agent-chat-ui';
|
|
13
13
|
|
|
14
|
+
const CHAT_SKILL_TOKEN_PREFIX = '$';
|
|
15
|
+
|
|
16
|
+
function serializeSkillTokenText(skillSpec: string): string {
|
|
17
|
+
return `${CHAT_SKILL_TOKEN_PREFIX}${skillSpec}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function appendTextPart(parts: NcpMessagePart[], text: string): void {
|
|
21
|
+
if (text.length === 0) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const previous = parts[parts.length - 1];
|
|
25
|
+
if (previous?.type === 'text') {
|
|
26
|
+
previous.text += text;
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
parts.push({
|
|
30
|
+
type: 'text',
|
|
31
|
+
text
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
14
35
|
export function createInitialChatComposerNodes(): ChatComposerNode[] {
|
|
15
36
|
return createEmptyChatComposerNodes();
|
|
16
37
|
}
|
|
@@ -86,12 +107,12 @@ export function deriveNcpMessagePartsFromComposer(
|
|
|
86
107
|
|
|
87
108
|
for (const node of nodes) {
|
|
88
109
|
if (node.type === 'text') {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
110
|
+
appendTextPart(parts, node.text);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (node.tokenKind === 'skill') {
|
|
115
|
+
appendTextPart(parts, serializeSkillTokenText(node.tokenKey));
|
|
95
116
|
continue;
|
|
96
117
|
}
|
|
97
118
|
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { createChatComposerTextNode, createChatComposerTokenNode } from "@nextclaw/agent-chat-ui";
|
|
2
|
+
import {
|
|
3
|
+
buildInlineSkillTokensFromComposer,
|
|
4
|
+
CHAT_UI_INLINE_TOKENS_METADATA_KEY,
|
|
5
|
+
readInlineTokensFromMetadata,
|
|
6
|
+
splitTextByInlineTokens,
|
|
7
|
+
} from "@/components/chat/chat-inline-token.utils";
|
|
8
|
+
|
|
9
|
+
describe("chat-inline-token utils", () => {
|
|
10
|
+
it("builds ordered inline skill tokens from composer nodes", () => {
|
|
11
|
+
expect(
|
|
12
|
+
buildInlineSkillTokensFromComposer([
|
|
13
|
+
createChatComposerTextNode("before "),
|
|
14
|
+
createChatComposerTokenNode({
|
|
15
|
+
tokenKind: "skill",
|
|
16
|
+
tokenKey: "weather",
|
|
17
|
+
label: "Weather",
|
|
18
|
+
}),
|
|
19
|
+
createChatComposerTokenNode({
|
|
20
|
+
tokenKind: "skill",
|
|
21
|
+
tokenKey: "docs",
|
|
22
|
+
label: "Docs",
|
|
23
|
+
}),
|
|
24
|
+
]),
|
|
25
|
+
).toEqual([
|
|
26
|
+
{
|
|
27
|
+
kind: "skill",
|
|
28
|
+
key: "weather",
|
|
29
|
+
label: "Weather",
|
|
30
|
+
rawText: "$weather",
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
kind: "skill",
|
|
34
|
+
key: "docs",
|
|
35
|
+
label: "Docs",
|
|
36
|
+
rawText: "$docs",
|
|
37
|
+
},
|
|
38
|
+
]);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("reads generic inline token metadata safely", () => {
|
|
42
|
+
expect(
|
|
43
|
+
readInlineTokensFromMetadata({
|
|
44
|
+
[CHAT_UI_INLINE_TOKENS_METADATA_KEY]: [
|
|
45
|
+
{
|
|
46
|
+
kind: "skill",
|
|
47
|
+
key: "weather",
|
|
48
|
+
label: "Weather",
|
|
49
|
+
rawText: "$weather",
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
}),
|
|
53
|
+
).toEqual([
|
|
54
|
+
{
|
|
55
|
+
kind: "skill",
|
|
56
|
+
key: "weather",
|
|
57
|
+
label: "Weather",
|
|
58
|
+
rawText: "$weather",
|
|
59
|
+
},
|
|
60
|
+
]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("splits text into inline text and token fragments", () => {
|
|
64
|
+
expect(
|
|
65
|
+
splitTextByInlineTokens("please use $weather now", [
|
|
66
|
+
{
|
|
67
|
+
kind: "skill",
|
|
68
|
+
key: "weather",
|
|
69
|
+
label: "Weather",
|
|
70
|
+
rawText: "$weather",
|
|
71
|
+
},
|
|
72
|
+
]),
|
|
73
|
+
).toEqual([
|
|
74
|
+
{ type: "text", text: "please use " },
|
|
75
|
+
{
|
|
76
|
+
type: "token",
|
|
77
|
+
token: {
|
|
78
|
+
kind: "skill",
|
|
79
|
+
key: "weather",
|
|
80
|
+
label: "Weather",
|
|
81
|
+
rawText: "$weather",
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
{ type: "text", text: " now" },
|
|
85
|
+
]);
|
|
86
|
+
});
|
|
87
|
+
});
|