@nextclaw/ui 0.20.0-beta.0 → 0.20.1

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 (67) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/dist/assets/{api-C2iYhdTe.js → api-BBFMpRGl.js} +1 -1
  3. package/dist/assets/{appearance-settings-page-BwQDQr0n.js → appearance-settings-page-C1Erayzl.js} +1 -1
  4. package/dist/assets/{book-open-C0IlT9zs.js → book-open-BhTIpBKg.js} +1 -1
  5. package/dist/assets/{channels-list-page-B0z4bh8e.js → channels-list-page-OTCkjMOW.js} +1 -1
  6. package/dist/assets/{config-split-page-VDTf3zZb.js → config-split-page-By7p-4X-.js} +1 -1
  7. package/dist/assets/{confirm-dialog-BI_Vt2s_.js → confirm-dialog-xpl1Yl7r.js} +1 -1
  8. package/dist/assets/{createLucideIcon-Bydi9Gc5.js → createLucideIcon-B_8khHn8.js} +1 -1
  9. package/dist/assets/{desktop-update-config-DVoD6NxG.js → desktop-update-config-CGFfa7-Z.js} +1 -1
  10. package/dist/assets/{dist-CmHtkFbs.js → dist-CXN62DSs.js} +1 -1
  11. package/dist/assets/{dist-A5btQciA.js → dist-DdE-kCbi.js} +1 -1
  12. package/dist/assets/{doc-browser-Cm_tMZ_C.js → doc-browser-5xrHcQ2z.js} +1 -1
  13. package/dist/assets/doc-browser-c2yO477-.js +1 -0
  14. package/dist/assets/{doc-browser-context-Bxw0ora-.js → doc-browser-context-B8NCuMv5.js} +1 -1
  15. package/dist/assets/{extensions-CG4voDCh.js → extensions-CMctxyTf.js} +1 -1
  16. package/dist/assets/{form-actions-lv47uGIB.js → form-actions-BFgTdxqI.js} +1 -1
  17. package/dist/assets/index-DUCA8m2w.js +109 -0
  18. package/dist/assets/loader-circle-CbdTrSp3.js +1 -0
  19. package/dist/assets/{mcp-marketplace-page-BSk0qUOX.js → mcp-marketplace-page-DGve0muV.js} +1 -1
  20. package/dist/assets/mcp-marketplace-page-_37VzG2q.js +1 -0
  21. package/dist/assets/{model-config-page-c8RnUGoH.js → model-config-page-DyC5H0bu.js} +1 -1
  22. package/dist/assets/{navigation-link-D0agTj5t.js → navigation-link-BwWrm4k1.js} +1 -1
  23. package/dist/assets/plus-DbeE4r-Q.js +1 -0
  24. package/dist/assets/{privacy-settings-page-CPkyTvEE.js → privacy-settings-page--MOnhAZ6.js} +1 -1
  25. package/dist/assets/{providers-config-page-CGUEb5Dx.js → providers-config-page-ChL3FUvK.js} +1 -1
  26. package/dist/assets/react-DVMmnPWz.js +8 -0
  27. package/dist/assets/{refresh-cw-DjgY2Eju.js → refresh-cw-B7NmPJ_Z.js} +1 -1
  28. package/dist/assets/remote-DFoxj4ls.js +1 -0
  29. package/dist/assets/{rotate-cw-Dot4PVh0.js → rotate-cw-B0ajMaHu.js} +1 -1
  30. package/dist/assets/{runtime-config-page-C6nzqTHg.js → runtime-config-page-BQdT3nmd.js} +1 -1
  31. package/dist/assets/{save-B-jdTe_c.js → save-DWnYYpgQ.js} +1 -1
  32. package/dist/assets/{search-CEp2AIiV.js → search-D6pX7FC-.js} +1 -1
  33. package/dist/assets/{search-config-page-Dp5guS5O.js → search-config-page-Cx3o_675.js} +1 -1
  34. package/dist/assets/{secrets-config-page-C7nRMjiv.js → secrets-config-page-tyFl7Rvt.js} +1 -1
  35. package/dist/assets/security-config-CUTUm6JT.js +1 -0
  36. package/dist/assets/{select-BNA3cma8.js → select-qEtFTWB5.js} +1 -1
  37. package/dist/assets/skeleton-7MYPzn8w.js +1 -0
  38. package/dist/assets/{tag-chip-DmCNi4-9.js → tag-chip-vcjAYdV_.js} +1 -1
  39. package/dist/assets/x-DThaS5ZL.js +1 -0
  40. package/dist/index.html +20 -20
  41. package/package.json +8 -8
  42. package/src/features/chat/features/message/components/chat-message-list.container.tsx +33 -4
  43. package/src/features/chat/features/message/utils/__tests__/chat-message-file-operation.utils.test.ts +186 -0
  44. package/src/features/chat/features/message/utils/__tests__/chat-message-texts.utils.test.ts +9 -0
  45. package/src/features/chat/features/message/utils/__tests__/chat-message-timeline.utils.test.ts +71 -0
  46. package/src/features/chat/features/message/utils/__tests__/chat-message-trigger-details.utils.test.ts +112 -0
  47. package/src/features/chat/features/message/utils/chat-message-part.utils.ts +1 -1
  48. package/src/features/chat/features/message/utils/chat-message-texts.utils.ts +36 -0
  49. package/src/features/chat/features/message/utils/chat-message-trigger-details.utils.ts +118 -0
  50. package/src/features/chat/features/ncp/hooks/__tests__/use-ncp-agent-runtime.test.tsx +90 -1
  51. package/src/features/chat/features/session/utils/ncp-session-context-metadata.utils.ts +5 -1
  52. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel-content.test.tsx +5 -5
  53. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-content.tsx +3 -40
  54. package/src/features/chat/managers/__tests__/chat-completion-notification.manager.test.ts +70 -0
  55. package/src/features/chat/managers/chat-completion-notification.manager.ts +23 -6
  56. package/src/shared/lib/i18n/locales/en-US/chat.json +21 -0
  57. package/src/shared/lib/i18n/locales/zh-CN/chat.json +21 -0
  58. package/dist/assets/doc-browser-Dto54tCf.js +0 -1
  59. package/dist/assets/index-BTSwNDVi.js +0 -109
  60. package/dist/assets/loader-circle-CkaqeEne.js +0 -1
  61. package/dist/assets/mcp-marketplace-page-BgZGc-S_.js +0 -1
  62. package/dist/assets/plus-BiMjkv3R.js +0 -1
  63. package/dist/assets/react-BMYFkaGi.js +0 -8
  64. package/dist/assets/remote-1LxkpRED.js +0 -1
  65. package/dist/assets/security-config-DvHUDxSg.js +0 -1
  66. package/dist/assets/skeleton-CbroEO8Q.js +0 -1
  67. package/dist/assets/x-BTo5-A8h.js +0 -1
@@ -154,6 +154,12 @@ it("uses structured edit-file result line numbers after the tool finishes", () =
154
154
  },
155
155
  },
156
156
  });
157
+ expect(adapted[0]?.parts[0]).not.toMatchObject({
158
+ type: "tool-card",
159
+ card: {
160
+ outputData: expect.anything(),
161
+ },
162
+ });
157
163
  });
158
164
 
159
165
  it("builds write-file previews from partial native args before the JSON is complete", () => {
@@ -261,6 +267,12 @@ it("keeps completed write-file cards in preview mode instead of falling back to
261
267
  output: "Wrote 3906 bytes to games/snake.html",
262
268
  },
263
269
  });
270
+ expect(adapted[0]?.parts[0]).not.toMatchObject({
271
+ type: "tool-card",
272
+ card: {
273
+ outputData: expect.anything(),
274
+ },
275
+ });
264
276
  });
265
277
 
266
278
  it("renders codex file_change results as structured diff previews", () => {
@@ -337,4 +349,178 @@ it("renders codex file_change results as structured diff previews", () => {
337
349
  },
338
350
  },
339
351
  });
352
+ expect(adapted[0]?.parts[0]).not.toMatchObject({
353
+ type: "tool-card",
354
+ card: {
355
+ outputData: expect.anything(),
356
+ },
357
+ });
358
+ });
359
+
360
+ it("uses a read-file preview as the single visible representation of file content", () => {
361
+ const adapted = adapt([
362
+ {
363
+ id: "assistant-read-result",
364
+ role: "assistant",
365
+ parts: [
366
+ {
367
+ type: "tool-invocation",
368
+ toolInvocation: {
369
+ status: ToolInvocationStatus.RESULT,
370
+ toolCallId: "read-result-1",
371
+ toolName: "read_file",
372
+ args: JSON.stringify({ path: "docs/readme.md" }),
373
+ parsedArgs: { path: "docs/readme.md" },
374
+ result: "1: first line\n2: second line\n</content>",
375
+ },
376
+ },
377
+ ],
378
+ },
379
+ ] as unknown as ChatMessageSource[]);
380
+
381
+ expect(adapted[0]?.parts[0]).toMatchObject({
382
+ type: "tool-card",
383
+ card: {
384
+ toolName: "read_file",
385
+ summary: "docs/readme.md",
386
+ fileOperation: {
387
+ blocks: [
388
+ {
389
+ display: "preview",
390
+ path: "docs/readme.md",
391
+ lines: expect.arrayContaining([
392
+ expect.objectContaining({ text: "1: first line" }),
393
+ expect.objectContaining({ text: "2: second line" }),
394
+ ]),
395
+ },
396
+ ],
397
+ },
398
+ },
399
+ });
400
+ expect(adapted[0]?.parts[0]).not.toMatchObject({
401
+ type: "tool-card",
402
+ card: {
403
+ outputData: expect.anything(),
404
+ },
405
+ });
406
+ });
407
+
408
+ it("uses an apply-patch preview as the single visible representation of the patch", () => {
409
+ const patch = [
410
+ "*** Begin Patch",
411
+ "*** Update File: src/app.ts",
412
+ "@@",
413
+ "-const color = 'red';",
414
+ "+const color = 'blue';",
415
+ "*** End Patch",
416
+ ].join("\n");
417
+ const adapted = adapt([
418
+ {
419
+ id: "assistant-apply-patch-result",
420
+ role: "assistant",
421
+ parts: [
422
+ {
423
+ type: "tool-invocation",
424
+ toolInvocation: {
425
+ status: ToolInvocationStatus.RESULT,
426
+ toolCallId: "apply-patch-result-1",
427
+ toolName: "apply_patch",
428
+ args: patch,
429
+ result: "Done!",
430
+ },
431
+ },
432
+ ],
433
+ },
434
+ ] as unknown as ChatMessageSource[]);
435
+
436
+ expect(adapted[0]?.parts[0]).toMatchObject({
437
+ type: "tool-card",
438
+ card: {
439
+ toolName: "apply_patch",
440
+ summary: "src/app.ts",
441
+ fileOperation: {
442
+ blocks: [
443
+ {
444
+ path: "src/app.ts",
445
+ lines: [
446
+ expect.objectContaining({ kind: "remove", text: "const color = 'red';" }),
447
+ expect.objectContaining({ kind: "add", text: "const color = 'blue';" }),
448
+ ],
449
+ },
450
+ ],
451
+ },
452
+ },
453
+ });
454
+ expect(adapted[0]?.parts[0]).not.toMatchObject({
455
+ type: "tool-card",
456
+ card: {
457
+ outputData: expect.anything(),
458
+ },
459
+ });
460
+ });
461
+
462
+ it("keeps raw file-tool results when no structured preview can be built", () => {
463
+ const adapted = adapt([
464
+ {
465
+ id: "assistant-read-result-without-path",
466
+ role: "assistant",
467
+ parts: [
468
+ {
469
+ type: "tool-invocation",
470
+ toolInvocation: {
471
+ status: ToolInvocationStatus.RESULT,
472
+ toolCallId: "read-result-without-path-1",
473
+ toolName: "read_file",
474
+ result: "raw file content",
475
+ },
476
+ },
477
+ ],
478
+ },
479
+ ] as unknown as ChatMessageSource[]);
480
+
481
+ expect(adapted[0]?.parts[0]).toMatchObject({
482
+ type: "tool-card",
483
+ card: {
484
+ toolName: "read_file",
485
+ outputData: "raw file content",
486
+ },
487
+ });
488
+ });
489
+
490
+ it("keeps file-tool errors visible even when input can build a preview", () => {
491
+ const adapted = adapt([
492
+ {
493
+ id: "assistant-write-error",
494
+ role: "assistant",
495
+ parts: [
496
+ {
497
+ type: "tool-invocation",
498
+ toolInvocation: {
499
+ status: ToolInvocationStatus.RESULT,
500
+ toolCallId: "write-error-1",
501
+ toolName: "write_file",
502
+ parsedArgs: {
503
+ path: "src/app.ts",
504
+ content: "const color = 'blue';",
505
+ },
506
+ error: "Permission denied",
507
+ result: "write failed",
508
+ },
509
+ },
510
+ ],
511
+ },
512
+ ] as unknown as ChatMessageSource[]);
513
+
514
+ expect(adapted[0]?.parts[0]).toMatchObject({
515
+ type: "tool-card",
516
+ card: {
517
+ toolName: "write_file",
518
+ statusTone: "error",
519
+ output: "Permission denied",
520
+ outputData: "write failed",
521
+ fileOperation: {
522
+ blocks: [expect.objectContaining({ path: "src/app.ts" })],
523
+ },
524
+ },
525
+ });
340
526
  });
@@ -1,6 +1,7 @@
1
1
  import { expect, it, vi } from "vitest";
2
2
  import {
3
3
  buildChatMessageExecutionLabels,
4
+ buildChatMessageTriggerLabels,
4
5
  buildChatMessageTexts,
5
6
  } from "@/features/chat/features/message/utils/chat-message-texts.utils";
6
7
 
@@ -25,6 +26,14 @@ it("provides localized status keys for built-in tool categories", () => {
25
26
  });
26
27
  });
27
28
 
29
+ it("provides localized labels for run trigger details", () => {
30
+ expect(buildChatMessageTriggerLabels("zh")).toMatchObject({
31
+ viewTrigger: "chatRunTriggerView",
32
+ fields: { sourceModel: "chatRunTriggerSourceModel" },
33
+ actors: { agent: "chatRunTriggerActorAgent" },
34
+ });
35
+ });
36
+
28
37
  it("provides localized labels for the AI execution metadata action", () => {
29
38
  expect(buildChatMessageExecutionLabels("zh")).toMatchObject({
30
39
  moreActions: "chatMessageMoreActions",
@@ -125,7 +125,9 @@ describe("chat message timeline visibility", () => {
125
125
  })).toBe(false);
126
126
  expect(isVisibleChatMessage(visibleMessage)).toBe(true);
127
127
  });
128
+ });
128
129
 
130
+ describe("chat message continuation projection", () => {
129
131
  it("projects continued assistant output into the interrupted message", () => {
130
132
  const messages = projectVisibleChatMessages([
131
133
  {
@@ -253,3 +255,72 @@ describe("chat message timeline visibility", () => {
253
255
  ]).map((message) => message.id)).toEqual(["user-1", "assistant-1"]);
254
256
  });
255
257
  });
258
+
259
+ describe("child session timeline visibility", () => {
260
+ it("does not repeat inherited compaction markers", () => {
261
+ const childMessage = {
262
+ ...visibleMessage,
263
+ id: "child-user",
264
+ role: "user" as const,
265
+ timestamp: "2026-08-24T15:50:23.415Z",
266
+ parts: [{ type: "text" as const, text: "child request" }],
267
+ };
268
+ const inheritedCompactions = Array.from({ length: 5 }, (_, index) => ({
269
+ ...visibleMessage,
270
+ id: `child-session:inherited:${index + 1}`,
271
+ role: "service" as const,
272
+ timestamp: `2026-08-24T15:0${index}:00.000Z`,
273
+ parts: [{ type: "text" as const, text: "Earlier context was auto-compacted" }],
274
+ metadata: {
275
+ inherited_from_session_id: "parent-session",
276
+ inherited_from_message_id: `parent-compaction-${index + 1}`,
277
+ nextclaw_timeline_kind: "context_compaction",
278
+ checkpoint: {
279
+ id: "parent-checkpoint",
280
+ status: "compressed",
281
+ summary: "Compressed parent context",
282
+ coveredMessageCount: (index + 1) * 100,
283
+ coveredSessionMessageCount: (index + 1) * 100,
284
+ originalEstimatedTokens: 76_000,
285
+ projectedEstimatedTokens: 51_000,
286
+ createdAt: "2026-08-24T15:00:00.000Z",
287
+ updatedAt: `2026-08-24T15:0${index}:00.000Z`,
288
+ },
289
+ },
290
+ } satisfies NcpMessage));
291
+ const items = buildChatMessageTimelineItems({
292
+ rawMessages: [...inheritedCompactions, childMessage],
293
+ messages: [{ id: childMessage.id } as never],
294
+ });
295
+
296
+ expect(items.map((item) => item.kind)).toEqual([
297
+ "context-inheritance",
298
+ "message",
299
+ ]);
300
+
301
+ const childCompaction = {
302
+ ...inheritedCompactions[0]!,
303
+ id: "child-compaction",
304
+ metadata: {
305
+ ...inheritedCompactions[0]!.metadata,
306
+ inherited_from_session_id: undefined,
307
+ inherited_from_message_id: undefined,
308
+ },
309
+ } satisfies NcpMessage;
310
+ const childAssistant = {
311
+ ...visibleMessage,
312
+ id: "child-assistant",
313
+ timestamp: "2026-08-24T15:51:00.000Z",
314
+ };
315
+
316
+ expect(buildChatMessageTimelineItems({
317
+ rawMessages: [...inheritedCompactions, childMessage, childCompaction, childAssistant],
318
+ messages: [{ id: childMessage.id } as never, { id: childAssistant.id } as never],
319
+ }).map((item) => item.kind)).toEqual([
320
+ "context-inheritance",
321
+ "message",
322
+ "compaction",
323
+ "message",
324
+ ]);
325
+ });
326
+ });
@@ -0,0 +1,112 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ buildChatMessageTriggerDetails,
4
+ mergeChatMessageMoreActions,
5
+ } from "@/features/chat/features/message/utils/chat-message-trigger-details.utils";
6
+
7
+ const labels = {
8
+ moreActions: "More actions",
9
+ viewTrigger: "View trigger details",
10
+ title: "Run trigger",
11
+ description: "Why this run started.",
12
+ close: "Close",
13
+ notAvailable: "Not available",
14
+ fields: {
15
+ actor: "Actor",
16
+ source: "Source",
17
+ triggeredAt: "Triggered at",
18
+ targetRunId: "Target run",
19
+ sourceSessionId: "Source session",
20
+ sourceMessageId: "Source message",
21
+ sourceRunId: "Source run",
22
+ sourceToolCallId: "Source tool call",
23
+ sourceRequestId: "Source request",
24
+ sourceModel: "Source model",
25
+ targetModel: "Run model",
26
+ sourceContext: "Source context",
27
+ raw: "Raw metadata",
28
+ },
29
+ actors: {
30
+ human: "Human",
31
+ agent: "Agent",
32
+ automation: "Automation",
33
+ system: "System",
34
+ },
35
+ };
36
+
37
+ describe("buildChatMessageTriggerDetails", () => {
38
+ it("projects structured and raw provenance rows", () => {
39
+ const details = buildChatMessageTriggerDetails({
40
+ labels,
41
+ message: {
42
+ metadata: {
43
+ run_trigger: {
44
+ version: 1,
45
+ actor: "agent",
46
+ source: "sessions_spawn",
47
+ triggeredAt: "2026-08-25T00:00:00.000Z",
48
+ targetRunId: "target-run",
49
+ sourceSessionId: "source-session",
50
+ sourceMessageId: "source-message",
51
+ sourceRunId: "source-run",
52
+ sourceToolCallId: "source-tool-call",
53
+ sourceRequestId: "source-request",
54
+ sourceModel: "openai/gpt-5.6",
55
+ sourceContext: { channel: "telegram", chatId: "chat-1" },
56
+ },
57
+ run_spec: {
58
+ runId: "target-run",
59
+ model: "minimax/MiniMax-M3",
60
+ },
61
+ },
62
+ },
63
+ });
64
+
65
+ expect(details?.triggerLabel).toBe("More actions");
66
+ expect(details?.items[0]?.dialog.rows).toEqual(expect.arrayContaining([
67
+ { label: "Actor", value: "Agent" },
68
+ { label: "Source model", value: "openai/gpt-5.6" },
69
+ { label: "Run model", value: "minimax/MiniMax-M3" },
70
+ { label: "Source context", value: expect.stringContaining("telegram") },
71
+ expect.objectContaining({ label: "Raw metadata" }),
72
+ ]));
73
+ expect(details?.items[0]?.dialog.rows.find(
74
+ (row) => row.label === "Raw metadata",
75
+ )?.value).toContain("run_spec");
76
+ });
77
+
78
+ it("does not create an empty action for legacy messages", () => {
79
+ expect(buildChatMessageTriggerDetails({ labels, message: {} })).toBeNull();
80
+ });
81
+
82
+ it("keeps trigger-only messages actionable and merges assistant details in order", () => {
83
+ const triggerDetails = buildChatMessageTriggerDetails({
84
+ labels,
85
+ message: {
86
+ metadata: {
87
+ run_trigger: {
88
+ version: 1,
89
+ actor: "human",
90
+ source: "ui-http",
91
+ triggeredAt: "2026-08-25T00:00:00.000Z",
92
+ targetRunId: "run-1",
93
+ },
94
+ },
95
+ },
96
+ });
97
+ expect(mergeChatMessageMoreActions(null, triggerDetails)?.items.map(
98
+ (item) => item.key,
99
+ )).toEqual(["run-trigger-metadata"]);
100
+ expect(mergeChatMessageMoreActions({
101
+ triggerLabel: "More actions",
102
+ items: [{
103
+ key: "ai-execution-metadata",
104
+ label: "View run metadata",
105
+ dialog: { title: "Run", closeLabel: "Close", rows: [] },
106
+ }],
107
+ }, triggerDetails)?.items.map((item) => item.key)).toEqual([
108
+ "ai-execution-metadata",
109
+ "run-trigger-metadata",
110
+ ]);
111
+ });
112
+ });
@@ -174,7 +174,7 @@ function buildToolInvocationPart(
174
174
  detail,
175
175
  inputData: fileOperationCardData ? undefined : invocation.parsedArgs ?? invocation.args,
176
176
  text: shouldShowRawResult && rawResult ? rawResult : undefined,
177
- outputData: invocation.result,
177
+ outputData: shouldShowRawResult ? invocation.result : undefined,
178
178
  callId: invocation.toolCallId || undefined,
179
179
  toolCallId: invocation.toolCallId || undefined,
180
180
  ...(invocation.execution ? { execution: invocation.execution } : {}),
@@ -1,5 +1,6 @@
1
1
  import type { ChatMessageTexts } from "@nextclaw/agent-chat-ui";
2
2
  import type { ChatMessageExecutionPresentationLabels } from "./chat-message-execution-summary.utils";
3
+ import type { ChatMessageTriggerDetailsLabels } from "./chat-message-trigger-details.utils";
3
4
  import type { ChatMessageAdapterTexts } from "@/features/chat/features/message/utils/chat-message.utils";
4
5
  import { t } from "@/shared/lib/i18n";
5
6
 
@@ -140,6 +141,41 @@ export function buildChatMessageExecutionLabels(
140
141
  };
141
142
  }
142
143
 
144
+ export function buildChatMessageTriggerLabels(
145
+ language: string,
146
+ ): ChatMessageTriggerDetailsLabels {
147
+ void language;
148
+ return {
149
+ moreActions: t("chatMessageMoreActions"),
150
+ viewTrigger: t("chatRunTriggerView"),
151
+ title: t("chatRunTriggerTitle"),
152
+ description: t("chatRunTriggerDescription"),
153
+ close: t("chatRunTriggerClose"),
154
+ notAvailable: t("chatAiExecutionNotAvailable"),
155
+ fields: {
156
+ actor: t("chatRunTriggerActor"),
157
+ source: t("chatRunTriggerSource"),
158
+ triggeredAt: t("chatRunTriggerTime"),
159
+ targetRunId: t("chatRunTriggerTargetRun"),
160
+ sourceSessionId: t("chatRunTriggerSourceSession"),
161
+ sourceMessageId: t("chatRunTriggerSourceMessage"),
162
+ sourceRunId: t("chatRunTriggerSourceRun"),
163
+ sourceToolCallId: t("chatRunTriggerSourceToolCall"),
164
+ sourceRequestId: t("chatRunTriggerSourceRequest"),
165
+ sourceModel: t("chatRunTriggerSourceModel"),
166
+ targetModel: t("chatRunTriggerTargetModel"),
167
+ sourceContext: t("chatRunTriggerSourceContext"),
168
+ raw: t("chatRunTriggerRaw"),
169
+ },
170
+ actors: {
171
+ human: t("chatRunTriggerActorHuman"),
172
+ agent: t("chatRunTriggerActorAgent"),
173
+ automation: t("chatRunTriggerActorAutomation"),
174
+ system: t("chatRunTriggerActorSystem"),
175
+ },
176
+ };
177
+ }
178
+
143
179
  export function buildChatMessageTexts(
144
180
  language: string,
145
181
  ): ChatMessageTexts {
@@ -0,0 +1,118 @@
1
+ import type { ChatMessageMoreActionsViewModel } from "@nextclaw/agent-chat-ui";
2
+ import {
3
+ readNcpAiExecutionMetadata,
4
+ readNcpRunTriggerMetadata,
5
+ type NcpMessage,
6
+ type NcpRunTriggerActor,
7
+ } from "@nextclaw/ncp";
8
+
9
+ export type ChatMessageTriggerDetailsLabels = {
10
+ moreActions: string;
11
+ viewTrigger: string;
12
+ title: string;
13
+ description: string;
14
+ close: string;
15
+ notAvailable: string;
16
+ fields: {
17
+ actor: string;
18
+ source: string;
19
+ triggeredAt: string;
20
+ targetRunId: string;
21
+ sourceSessionId: string;
22
+ sourceMessageId: string;
23
+ sourceRunId: string;
24
+ sourceToolCallId: string;
25
+ sourceRequestId: string;
26
+ sourceModel: string;
27
+ targetModel: string;
28
+ sourceContext: string;
29
+ raw: string;
30
+ };
31
+ actors: Record<NcpRunTriggerActor, string>;
32
+ };
33
+
34
+ export function mergeChatMessageMoreActions(
35
+ ...candidates: Array<ChatMessageMoreActionsViewModel | null | undefined>
36
+ ): ChatMessageMoreActionsViewModel | undefined {
37
+ const available = candidates.filter(
38
+ (candidate): candidate is ChatMessageMoreActionsViewModel =>
39
+ Boolean(candidate?.items.length),
40
+ );
41
+ if (available.length === 0) return undefined;
42
+ return {
43
+ triggerLabel: available[0]!.triggerLabel,
44
+ items: available.flatMap((candidate) => candidate.items),
45
+ };
46
+ }
47
+
48
+ function readRunSpec(
49
+ metadata: Record<string, unknown> | null | undefined,
50
+ ): Record<string, unknown> | null {
51
+ const value = metadata?.run_spec;
52
+ return value && typeof value === "object" && !Array.isArray(value)
53
+ ? value as Record<string, unknown>
54
+ : null;
55
+ }
56
+
57
+ function readTargetModel(
58
+ metadata: Record<string, unknown> | null | undefined,
59
+ ): string | undefined {
60
+ const runSpecModel = readRunSpec(metadata)?.model;
61
+ if (typeof runSpecModel === "string" && runSpecModel.trim()) {
62
+ return runSpecModel.trim();
63
+ }
64
+ return readNcpAiExecutionMetadata(metadata)?.model;
65
+ }
66
+
67
+ export function buildChatMessageTriggerDetails(params: {
68
+ labels: ChatMessageTriggerDetailsLabels;
69
+ message: Pick<NcpMessage, "metadata">;
70
+ }): ChatMessageMoreActionsViewModel | null {
71
+ const { labels, message } = params;
72
+ const trigger = readNcpRunTriggerMetadata(message.metadata);
73
+ if (!trigger) return null;
74
+ const { fields } = labels;
75
+ const runSpec = readRunSpec(message.metadata);
76
+ const rows = [
77
+ { label: fields.actor, value: labels.actors[trigger.actor] },
78
+ { label: fields.source, value: trigger.source },
79
+ { label: fields.triggeredAt, value: trigger.triggeredAt },
80
+ { label: fields.targetRunId, value: trigger.targetRunId },
81
+ { label: fields.sourceSessionId, value: trigger.sourceSessionId },
82
+ { label: fields.sourceMessageId, value: trigger.sourceMessageId },
83
+ { label: fields.sourceRunId, value: trigger.sourceRunId },
84
+ { label: fields.sourceToolCallId, value: trigger.sourceToolCallId },
85
+ { label: fields.sourceRequestId, value: trigger.sourceRequestId },
86
+ { label: fields.sourceModel, value: trigger.sourceModel },
87
+ { label: fields.targetModel, value: readTargetModel(message.metadata) },
88
+ {
89
+ label: fields.sourceContext,
90
+ value: trigger.sourceContext
91
+ ? JSON.stringify(trigger.sourceContext, null, 2)
92
+ : undefined,
93
+ },
94
+ ].map((row) => ({
95
+ label: row.label,
96
+ value: row.value ?? labels.notAvailable,
97
+ }));
98
+ rows.push({
99
+ label: fields.raw,
100
+ value: JSON.stringify({
101
+ run_trigger: trigger,
102
+ ...(runSpec ? { run_spec: runSpec } : {}),
103
+ }, null, 2),
104
+ });
105
+ return {
106
+ triggerLabel: labels.moreActions,
107
+ items: [{
108
+ key: "run-trigger-metadata",
109
+ label: labels.viewTrigger,
110
+ dialog: {
111
+ title: labels.title,
112
+ description: labels.description,
113
+ closeLabel: labels.close,
114
+ rows,
115
+ },
116
+ }],
117
+ };
118
+ }