@nextclaw/ui 0.11.18 → 0.11.20

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 +26 -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 +6 -6
  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
@@ -1,49 +1,18 @@
1
- import {
2
- stringifyUnknown,
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 ChatMessagePartSource =
14
- | {
15
- type: "text";
16
- text: string;
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
- if (isTextPart(part)) {
339
- const text = toRenderableText(part.text);
340
- if (!text) {
341
- return null;
342
- }
343
- return {
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
  }
@@ -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 skipping skill tokens', () => {
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
- if (node.text.length > 0) {
90
- parts.push({
91
- type: 'text',
92
- text: node.text
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
+ });