@nextclaw/ui 0.11.18 → 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.
Files changed (40) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/assets/{ChannelsList-eZfHzvxb.js → ChannelsList-DAx7wv0_.js} +1 -1
  3. package/dist/assets/{ChatPage-DKD5hcD8.js → ChatPage-l2PYwCeB.js} +8 -8
  4. package/dist/assets/{MarketplacePage-D0iqC5o7.js → MarketplacePage-Dlp5BgCh.js} +1 -1
  5. package/dist/assets/MarketplacePage-TVeyVOuO.js +1 -0
  6. package/dist/assets/{McpMarketplacePage-CCmRjGwl.js → McpMarketplacePage-CwKtAil8.js} +1 -1
  7. package/dist/assets/{ModelConfig-BiWp8Ymp.js → ModelConfig-Dg6F3Ldb.js} +1 -1
  8. package/dist/assets/{ProvidersList-HaCAzF9F.js → ProvidersList-f7bQdRxA.js} +1 -1
  9. package/dist/assets/{RemoteAccessPage-DOF4oEHW.js → RemoteAccessPage-w_dY7P4T.js} +1 -1
  10. package/dist/assets/{RuntimeConfig-BnkWf6Eb.js → RuntimeConfig-M4OKjmgU.js} +1 -1
  11. package/dist/assets/{SearchConfig-3ofKM9W4.js → SearchConfig-v46R5a2U.js} +1 -1
  12. package/dist/assets/{SecretsConfig-BRbC2hfo.js → SecretsConfig-CXvUpbB_.js} +1 -1
  13. package/dist/assets/{SessionsConfig-BpoD_0WD.js → SessionsConfig-7vUHMtOh.js} +1 -1
  14. package/dist/assets/{index-CjPeKafH.js → index-B0DzQqwv.js} +2 -2
  15. package/dist/assets/index-BahpXJg8.css +1 -0
  16. package/dist/assets/{security-config-BcbOF17w.js → security-config-Xi5DYW7j.js} +1 -1
  17. package/dist/assets/{useConfirmDialog-Dk15Fj1n.js → useConfirmDialog-CXDAxtRL.js} +1 -1
  18. package/dist/index.html +2 -2
  19. package/package.json +5 -5
  20. package/src/components/chat/adapters/chat-input-bar.adapter.test.ts +103 -1
  21. package/src/components/chat/adapters/chat-input-bar.adapter.ts +80 -2
  22. package/src/components/chat/adapters/chat-message-inline-content.adapter.ts +95 -0
  23. package/src/components/chat/adapters/chat-message-part.adapter.ts +384 -0
  24. package/src/components/chat/adapters/chat-message.adapter.test.ts +37 -0
  25. package/src/components/chat/adapters/chat-message.adapter.ts +18 -366
  26. package/src/components/chat/chat-composer-state.test.ts +2 -6
  27. package/src/components/chat/chat-composer-state.ts +27 -6
  28. package/src/components/chat/chat-inline-token.utils.test.ts +87 -0
  29. package/src/components/chat/chat-inline-token.utils.ts +146 -0
  30. package/src/components/chat/chat-input/chat-input-bar.controller.test.tsx +24 -0
  31. package/src/components/chat/chat-input/chat-input-bar.controller.ts +81 -44
  32. package/src/components/chat/chat-recent-skills.manager.ts +8 -0
  33. package/src/components/chat/containers/chat-input-bar.container.tsx +31 -4
  34. package/src/components/chat/containers/chat-message-list.container.test.tsx +45 -0
  35. package/src/components/chat/containers/chat-message-list.container.tsx +11 -5
  36. package/src/components/chat/ncp/NcpChatPage.tsx +10 -1
  37. package/src/components/chat/ncp/ncp-chat-input.manager.ts +18 -4
  38. package/src/components/chat/presenter/chat-presenter-context.tsx +1 -0
  39. package/dist/assets/MarketplacePage-CMPjqEmN.js +0 -1
  40. package/dist/assets/index-DMy_fKKh.css +0 -1
@@ -0,0 +1,384 @@
1
+ import {
2
+ stringifyUnknown,
3
+ summarizeToolArgs,
4
+ type ToolCard,
5
+ } from "@/lib/chat-message";
6
+ import {
7
+ type ChatInlineTokenSource,
8
+ } from "@/components/chat/chat-inline-token.utils";
9
+ import {
10
+ buildRenderableText,
11
+ buildTextPart,
12
+ } from "@/components/chat/adapters/chat-message-inline-content.adapter";
13
+ import { buildSubagentToolCard } from "@/components/chat/adapters/chat-message.subagent-tool-card";
14
+ import type {
15
+ ChatMessagePartViewModel,
16
+ ChatToolPartViewModel,
17
+ } from "@nextclaw/agent-chat-ui";
18
+
19
+ export type ChatMessageAdapterTexts = {
20
+ roleLabels: {
21
+ user: string;
22
+ assistant: string;
23
+ tool: string;
24
+ system: string;
25
+ fallback: string;
26
+ };
27
+ reasoningLabel: string;
28
+ toolCallLabel: string;
29
+ toolResultLabel: string;
30
+ toolNoOutputLabel: string;
31
+ toolOutputLabel: string;
32
+ toolStatusPreparingLabel: string;
33
+ toolStatusRunningLabel: string;
34
+ toolStatusCompletedLabel: string;
35
+ toolStatusFailedLabel: string;
36
+ toolStatusCancelledLabel: string;
37
+ imageAttachmentLabel: string;
38
+ fileAttachmentLabel: string;
39
+ unknownPartLabel: string;
40
+ };
41
+
42
+ export type ChatMessagePartSource =
43
+ | {
44
+ type: "text";
45
+ text: string;
46
+ }
47
+ | {
48
+ type: "file";
49
+ mimeType: string;
50
+ data: string;
51
+ url?: string;
52
+ name?: string;
53
+ sizeBytes?: number;
54
+ }
55
+ | {
56
+ type: "reasoning";
57
+ reasoning: string;
58
+ }
59
+ | {
60
+ type: "tool-invocation";
61
+ toolInvocation: {
62
+ status?: string;
63
+ toolName: string;
64
+ args?: unknown;
65
+ parsedArgs?: unknown;
66
+ result?: unknown;
67
+ error?: string;
68
+ cancelled?: boolean;
69
+ toolCallId?: string;
70
+ };
71
+ }
72
+ | {
73
+ type: string;
74
+ [key: string]: unknown;
75
+ };
76
+
77
+ type ToolCardViewSource = ToolCard & {
78
+ statusTone: ChatToolPartViewModel["statusTone"];
79
+ statusLabel: string;
80
+ };
81
+
82
+ type ChatMessagePartAdapterParams = {
83
+ part: ChatMessagePartSource;
84
+ inlineTokens: readonly ChatInlineTokenSource[];
85
+ texts: ChatMessageAdapterTexts;
86
+ };
87
+
88
+ function isRecord(value: unknown): value is Record<string, unknown> {
89
+ return typeof value === "object" && value !== null;
90
+ }
91
+
92
+ function readOptionalString(value: unknown): string | null {
93
+ if (typeof value !== "string") {
94
+ return null;
95
+ }
96
+ const trimmed = value.trim();
97
+ return trimmed.length > 0 ? trimmed : null;
98
+ }
99
+
100
+ function readOptionalNumber(value: unknown): number | null {
101
+ if (typeof value === "number" && Number.isFinite(value) && value >= 0) {
102
+ return value;
103
+ }
104
+ if (typeof value !== "string") {
105
+ return null;
106
+ }
107
+ const trimmed = value.trim();
108
+ if (!trimmed) {
109
+ return null;
110
+ }
111
+ const parsed = Number(trimmed);
112
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : null;
113
+ }
114
+
115
+ function extractAssetFileView(
116
+ value: unknown,
117
+ texts: ChatMessageAdapterTexts,
118
+ ): Extract<ChatMessagePartViewModel, { type: "file" }> | null {
119
+ if (!isRecord(value)) {
120
+ return null;
121
+ }
122
+ const assetCandidate = isRecord(value.asset)
123
+ ? value.asset
124
+ : Array.isArray(value.assets) &&
125
+ value.assets.length > 0 &&
126
+ isRecord(value.assets[0])
127
+ ? value.assets[0]
128
+ : null;
129
+ if (!assetCandidate) {
130
+ return null;
131
+ }
132
+ const url = readOptionalString(assetCandidate.url);
133
+ const mimeType =
134
+ readOptionalString(assetCandidate.mimeType) ?? "application/octet-stream";
135
+ const sizeBytes = readOptionalNumber(assetCandidate.sizeBytes);
136
+ if (!url) {
137
+ return null;
138
+ }
139
+ const label =
140
+ readOptionalString(assetCandidate.name) ??
141
+ (mimeType.startsWith("image/")
142
+ ? texts.imageAttachmentLabel
143
+ : texts.fileAttachmentLabel);
144
+ return {
145
+ type: "file",
146
+ file: {
147
+ label,
148
+ mimeType,
149
+ dataUrl: url,
150
+ ...(sizeBytes != null ? { sizeBytes } : {}),
151
+ isImage: mimeType.startsWith("image/"),
152
+ },
153
+ };
154
+ }
155
+
156
+ function buildToolCard(
157
+ toolCard: ToolCardViewSource,
158
+ texts: ChatMessageAdapterTexts,
159
+ ): ChatToolPartViewModel {
160
+ return {
161
+ kind: toolCard.kind,
162
+ toolName: toolCard.name,
163
+ summary: toolCard.detail,
164
+ output: toolCard.text,
165
+ hasResult: Boolean(toolCard.hasResult),
166
+ statusTone: toolCard.statusTone,
167
+ statusLabel: toolCard.statusLabel,
168
+ titleLabel:
169
+ toolCard.kind === "call" ? texts.toolCallLabel : texts.toolResultLabel,
170
+ outputLabel: texts.toolOutputLabel,
171
+ emptyLabel: texts.toolNoOutputLabel,
172
+ };
173
+ }
174
+
175
+ function resolveToolCardStatus(params: {
176
+ status?: string;
177
+ error?: string;
178
+ cancelled?: boolean;
179
+ result?: unknown;
180
+ texts: ChatMessageAdapterTexts;
181
+ }): Pick<
182
+ ChatToolPartViewModel,
183
+ "kind" | "hasResult" | "statusTone" | "statusLabel"
184
+ > {
185
+ const rawStatus =
186
+ typeof params.status === "string" ? params.status.trim().toLowerCase() : "";
187
+ const hasError =
188
+ typeof params.error === "string" && params.error.trim().length > 0;
189
+ const isCancelled = params.cancelled === true || rawStatus === "cancelled";
190
+ if (isCancelled) {
191
+ return {
192
+ kind: "result",
193
+ hasResult: true,
194
+ statusTone: "cancelled",
195
+ statusLabel: params.texts.toolStatusCancelledLabel,
196
+ };
197
+ }
198
+ if (hasError || rawStatus === "error") {
199
+ return {
200
+ kind: "result",
201
+ hasResult: true,
202
+ statusTone: "error",
203
+ statusLabel: params.texts.toolStatusFailedLabel,
204
+ };
205
+ }
206
+ if (rawStatus === "result" || params.result != null) {
207
+ return {
208
+ kind: "result",
209
+ hasResult: true,
210
+ statusTone: "success",
211
+ statusLabel: params.texts.toolStatusCompletedLabel,
212
+ };
213
+ }
214
+ if (rawStatus === "partial-call") {
215
+ return {
216
+ kind: "call",
217
+ hasResult: false,
218
+ statusTone: "running",
219
+ statusLabel: params.texts.toolStatusPreparingLabel,
220
+ };
221
+ }
222
+ return {
223
+ kind: "call",
224
+ hasResult: false,
225
+ statusTone: "running",
226
+ statusLabel: params.texts.toolStatusRunningLabel,
227
+ };
228
+ }
229
+
230
+ function buildReasoningPart(
231
+ part: Extract<ChatMessagePartSource, { type: "reasoning" }>,
232
+ texts: ChatMessageAdapterTexts,
233
+ ): Extract<ChatMessagePartViewModel, { type: "reasoning" }> | null {
234
+ const text = buildRenderableText(part.reasoning);
235
+ if (!text) {
236
+ return null;
237
+ }
238
+ return {
239
+ type: "reasoning",
240
+ text,
241
+ label: texts.reasoningLabel,
242
+ };
243
+ }
244
+
245
+ function buildFilePart(
246
+ part: Extract<ChatMessagePartSource, { type: "file" }>,
247
+ texts: ChatMessageAdapterTexts,
248
+ ): Extract<ChatMessagePartViewModel, { type: "file" }> {
249
+ const isImage = part.mimeType.startsWith("image/");
250
+ const sizeBytes = readOptionalNumber(part.sizeBytes);
251
+ return {
252
+ type: "file",
253
+ file: {
254
+ label:
255
+ typeof part.name === "string" && part.name.trim()
256
+ ? part.name.trim()
257
+ : isImage
258
+ ? texts.imageAttachmentLabel
259
+ : texts.fileAttachmentLabel,
260
+ mimeType: part.mimeType,
261
+ dataUrl:
262
+ typeof part.url === "string" && part.url.trim().length > 0
263
+ ? part.url.trim()
264
+ : `data:${part.mimeType};base64,${part.data}`,
265
+ ...(sizeBytes != null ? { sizeBytes } : {}),
266
+ isImage,
267
+ },
268
+ };
269
+ }
270
+
271
+ function buildToolInvocationPart(
272
+ part: Extract<ChatMessagePartSource, { type: "tool-invocation" }>,
273
+ texts: ChatMessageAdapterTexts,
274
+ ): Extract<ChatMessagePartViewModel, { type: "tool-card" | "file" }> {
275
+ const invocation = part.toolInvocation;
276
+ const assetFileView = extractAssetFileView(invocation.result, texts);
277
+ if (assetFileView) {
278
+ return assetFileView;
279
+ }
280
+
281
+ const subagentToolCard = buildSubagentToolCard({
282
+ invocation,
283
+ texts,
284
+ });
285
+ if (subagentToolCard) {
286
+ return {
287
+ type: "tool-card",
288
+ card: buildToolCard(subagentToolCard, texts),
289
+ };
290
+ }
291
+
292
+ const statusView = resolveToolCardStatus({
293
+ status: invocation.status,
294
+ error: invocation.error,
295
+ cancelled: invocation.cancelled,
296
+ result: invocation.result,
297
+ texts,
298
+ });
299
+ const detail = summarizeToolArgs(invocation.parsedArgs ?? invocation.args);
300
+ const rawResult =
301
+ typeof invocation.error === "string" && invocation.error.trim()
302
+ ? invocation.error.trim()
303
+ : invocation.result != null
304
+ ? stringifyUnknown(invocation.result).trim()
305
+ : "";
306
+ const card: ToolCardViewSource = {
307
+ kind: statusView.kind,
308
+ name: invocation.toolName,
309
+ detail,
310
+ text: rawResult || undefined,
311
+ callId: invocation.toolCallId || undefined,
312
+ hasResult: statusView.hasResult,
313
+ statusTone: statusView.statusTone,
314
+ statusLabel: statusView.statusLabel,
315
+ };
316
+ return {
317
+ type: "tool-card",
318
+ card: buildToolCard(card, texts),
319
+ };
320
+ }
321
+
322
+ function buildUnknownPart(
323
+ part: ChatMessagePartSource,
324
+ texts: ChatMessageAdapterTexts,
325
+ ): Extract<ChatMessagePartViewModel, { type: "unknown" }> {
326
+ return {
327
+ type: "unknown",
328
+ label: texts.unknownPartLabel,
329
+ rawType: typeof part.type === "string" ? part.type : "unknown",
330
+ text: stringifyUnknown(part),
331
+ };
332
+ }
333
+
334
+ function isTextPart(
335
+ part: ChatMessagePartSource,
336
+ ): part is Extract<ChatMessagePartSource, { type: "text" }> {
337
+ return part.type === "text" && typeof part.text === "string";
338
+ }
339
+
340
+ function isReasoningPart(
341
+ part: ChatMessagePartSource,
342
+ ): part is Extract<ChatMessagePartSource, { type: "reasoning" }> {
343
+ return part.type === "reasoning" && typeof part.reasoning === "string";
344
+ }
345
+
346
+ function isFilePart(
347
+ part: ChatMessagePartSource,
348
+ ): part is Extract<ChatMessagePartSource, { type: "file" }> {
349
+ return (
350
+ part.type === "file" &&
351
+ typeof part.mimeType === "string" &&
352
+ typeof part.data === "string"
353
+ );
354
+ }
355
+
356
+ function isToolInvocationPart(
357
+ part: ChatMessagePartSource,
358
+ ): part is Extract<ChatMessagePartSource, { type: "tool-invocation" }> {
359
+ if (part.type !== "tool-invocation") {
360
+ return false;
361
+ }
362
+ if (!isRecord(part.toolInvocation)) {
363
+ return false;
364
+ }
365
+ return typeof part.toolInvocation.toolName === "string";
366
+ }
367
+
368
+ export function adaptChatMessagePart(
369
+ params: ChatMessagePartAdapterParams,
370
+ ): ChatMessagePartViewModel | null {
371
+ if (isTextPart(params.part)) {
372
+ return buildTextPart(params.part, params.inlineTokens);
373
+ }
374
+ if (isReasoningPart(params.part)) {
375
+ return buildReasoningPart(params.part, params.texts);
376
+ }
377
+ if (isFilePart(params.part)) {
378
+ return buildFilePart(params.part, params.texts);
379
+ }
380
+ if (isToolInvocationPart(params.part)) {
381
+ return buildToolInvocationPart(params.part, params.texts);
382
+ }
383
+ return buildUnknownPart(params.part, params.texts);
384
+ }
@@ -270,6 +270,43 @@ it("maps file parts into previewable attachment view models", () => {
270
270
  });
271
271
  });
272
272
 
273
+ it("renders inline skill tokens as structured inline content parts", () => {
274
+ const adapted = adapt([
275
+ {
276
+ id: "user-inline-skill",
277
+ role: "user",
278
+ meta: {
279
+ inlineTokens: [
280
+ {
281
+ kind: "skill",
282
+ key: "weather",
283
+ label: "Weather",
284
+ rawText: "$weather",
285
+ },
286
+ ],
287
+ },
288
+ parts: [{ type: "text", text: "please use $weather now" }],
289
+ },
290
+ ] as unknown as ChatMessageSource[]);
291
+
292
+ expect(adapted[0]?.parts[0]).toEqual({
293
+ type: "inline-content",
294
+ segments: [
295
+ { type: "markdown", text: "please use " },
296
+ {
297
+ type: "token",
298
+ token: {
299
+ kind: "skill",
300
+ key: "weather",
301
+ label: "Weather",
302
+ rawText: "$weather",
303
+ },
304
+ },
305
+ { type: "markdown", text: " now" },
306
+ ],
307
+ });
308
+ });
309
+
273
310
  it("keeps named non-image files as downloadable attachments", () => {
274
311
  const adapted = adapt([
275
312
  {