@nextclaw/ui 0.11.19 → 0.11.21

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 (72) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/assets/{ChannelsList-DAx7wv0_.js → ChannelsList-ByHWHkQS.js} +1 -1
  3. package/dist/assets/ChatPage-FdT3pDnw.js +42 -0
  4. package/dist/assets/{DocBrowser-DKkE3Y4I.js → DocBrowser-3y_NHZ71.js} +1 -1
  5. package/dist/assets/DocBrowser-CMdPdbZj.js +1 -0
  6. package/dist/assets/{DocBrowserContext-BcZRBsCg.js → DocBrowserContext-CVJuwCcw.js} +1 -1
  7. package/dist/assets/{LogoBadge-BIPDLEwK.js → LogoBadge-D8fyilO-.js} +1 -1
  8. package/dist/assets/MarketplacePage-9oKmxN2n.js +1 -0
  9. package/dist/assets/{MarketplacePage-Dlp5BgCh.js → MarketplacePage-CmhsZXr1.js} +1 -1
  10. package/dist/assets/{McpMarketplacePage-CwKtAil8.js → McpMarketplacePage-C7PkCYbp.js} +1 -1
  11. package/dist/assets/{ModelConfig-Dg6F3Ldb.js → ModelConfig-DmCY6jWM.js} +1 -1
  12. package/dist/assets/{ProvidersList-f7bQdRxA.js → ProvidersList-ClT-34aX.js} +1 -1
  13. package/dist/assets/RemoteAccessPage-B6hUZl1O.js +1 -0
  14. package/dist/assets/{RuntimeConfig-M4OKjmgU.js → RuntimeConfig-C5aqliGk.js} +1 -1
  15. package/dist/assets/{SearchConfig-v46R5a2U.js → SearchConfig-Dm7r2yfp.js} +1 -1
  16. package/dist/assets/{SecretsConfig-CXvUpbB_.js → SecretsConfig-BBP_mbQh.js} +1 -1
  17. package/dist/assets/{SessionsConfig-7vUHMtOh.js → SessionsConfig-6wNJloZN.js} +1 -1
  18. package/dist/assets/{book-open-DzSduAaw.js → book-open-B26jGBjY.js} +1 -1
  19. package/dist/assets/{chat-session-display-CGfXhJoT.js → chat-session-display-Bjmn4aIZ.js} +1 -1
  20. package/dist/assets/{chunk-JZWAC4HX-C1vpvW4r.js → chunk-JZWAC4HX-B-4B29RN.js} +1 -1
  21. package/dist/assets/{config-Df97LeLR.js → config-BaC29Qf-.js} +1 -1
  22. package/dist/assets/{createLucideIcon-CcR5wVoU.js → createLucideIcon-DiFAvXmK.js} +1 -1
  23. package/dist/assets/{dist-BMlnBah3.js → dist-kW_O3kyZ.js} +1 -1
  24. package/dist/assets/{dist-Dii9v3X9.js → dist-pCfWPG1A.js} +1 -1
  25. package/dist/assets/{external-link-CnSDrvJE.js → external-link-D5-p-Gmm.js} +1 -1
  26. package/dist/assets/{hash-CAnX6PNt.js → hash-BlwrSV0q.js} +1 -1
  27. package/dist/assets/i18n-CSytxMFI.js +1 -0
  28. package/dist/assets/{index-BahpXJg8.css → index-CUy6doWo.css} +1 -1
  29. package/dist/assets/{index-B0DzQqwv.js → index-DvKS3L9j.js} +3 -3
  30. package/dist/assets/{label-CtIFj7_6.js → label-RyXfZqkP.js} +1 -1
  31. package/dist/assets/loader-circle-B2J777gj.js +1 -0
  32. package/dist/assets/{logos-3KFNiOej.js → logos-Bpl8QTgI.js} +1 -1
  33. package/dist/assets/{page-layout-BMwpn87D.js → page-layout--S0YBU0W.js} +1 -1
  34. package/dist/assets/plus-CM9XJ0Tf.js +1 -0
  35. package/dist/assets/{popover-BIzq25oH.js → popover-BEjfbEwy.js} +1 -1
  36. package/dist/assets/{react-ji6GGP_j.js → react-BuSP2-8B.js} +1 -1
  37. package/dist/assets/{save-CMgYkJ-y.js → save-DPPPpD_c.js} +1 -1
  38. package/dist/assets/search-Ctaw34Kp.js +1 -0
  39. package/dist/assets/{security-config-Xi5DYW7j.js → security-config-6t78Ph-I.js} +1 -1
  40. package/dist/assets/{select-Cz82gl01.js → select-CT50pzod.js} +1 -1
  41. package/dist/assets/skeleton-Bycyb0zU.js +1 -0
  42. package/dist/assets/{status-dot-C7q1HvLH.js → status-dot-BbBqRHfh.js} +1 -1
  43. package/dist/assets/{switch-DYswvkYj.js → switch-D3l6AcCk.js} +1 -1
  44. package/dist/assets/{tabs-custom-DKYQxrx1.js → tabs-custom-TZQ5WPWP.js} +1 -1
  45. package/dist/assets/{trash-2-DfXI7-ap.js → trash-2-B2_AGVE3.js} +1 -1
  46. package/dist/assets/{useConfirmDialog-CXDAxtRL.js → useConfirmDialog-BDpdjfIO.js} +1 -1
  47. package/dist/assets/{useMutation-s2sn2yzh.js → useMutation-BzCrO8j-.js} +1 -1
  48. package/dist/assets/x-CHOBE-63.js +1 -0
  49. package/dist/index.html +18 -18
  50. package/package.json +6 -6
  51. package/src/components/chat/adapters/chat-message-part.adapter.ts +74 -3
  52. package/src/components/chat/adapters/chat-message.adapter.test.ts +321 -3
  53. package/src/components/chat/adapters/chat-message.file-operation-card.ts +437 -0
  54. package/src/components/chat/adapters/chat-message.file-operation-diff.ts +408 -0
  55. package/src/components/chat/adapters/chat-message.partial-json.ts +89 -0
  56. package/src/components/chat/containers/chat-input-bar.container.tsx +8 -8
  57. package/src/components/chat/containers/chat-message-list.container.tsx +1 -0
  58. package/src/components/chat/ncp/ncp-session-adapter.test.ts +173 -0
  59. package/src/components/chat/useNcpAgentRuntime.test.tsx +90 -0
  60. package/src/lib/i18n.chat.ts +2 -1
  61. package/src/remote/remote-access-feedback.service.test.ts +18 -0
  62. package/src/remote/remote-access-feedback.service.ts +10 -1
  63. package/dist/assets/ChatPage-l2PYwCeB.js +0 -38
  64. package/dist/assets/DocBrowser-CIHLqoIm.js +0 -1
  65. package/dist/assets/MarketplacePage-TVeyVOuO.js +0 -1
  66. package/dist/assets/RemoteAccessPage-w_dY7P4T.js +0 -1
  67. package/dist/assets/i18n-CXBpwAwA.js +0 -1
  68. package/dist/assets/loader-circle-qgU4zQDw.js +0 -1
  69. package/dist/assets/plus-C9cYVbL-.js +0 -1
  70. package/dist/assets/search-sl1OeJFl.js +0 -1
  71. package/dist/assets/skeleton-rgIt7a5q.js +0 -1
  72. package/dist/assets/x-MIimOGs6.js +0 -1
package/dist/index.html CHANGED
@@ -6,24 +6,24 @@
6
6
  <link rel="icon" type="image/svg+xml" href="/logo.svg" />
7
7
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
8
8
  <title>NextClaw</title>
9
- <script type="module" crossorigin src="/assets/index-B0DzQqwv.js"></script>
10
- <link rel="modulepreload" crossorigin href="/assets/i18n-CXBpwAwA.js">
11
- <link rel="modulepreload" crossorigin href="/assets/chunk-JZWAC4HX-C1vpvW4r.js">
12
- <link rel="modulepreload" crossorigin href="/assets/dist-Dii9v3X9.js">
13
- <link rel="modulepreload" crossorigin href="/assets/createLucideIcon-CcR5wVoU.js">
14
- <link rel="modulepreload" crossorigin href="/assets/select-Cz82gl01.js">
15
- <link rel="modulepreload" crossorigin href="/assets/dist-BMlnBah3.js">
16
- <link rel="modulepreload" crossorigin href="/assets/react-ji6GGP_j.js">
17
- <link rel="modulepreload" crossorigin href="/assets/useMutation-s2sn2yzh.js">
18
- <link rel="modulepreload" crossorigin href="/assets/book-open-DzSduAaw.js">
19
- <link rel="modulepreload" crossorigin href="/assets/external-link-CnSDrvJE.js">
20
- <link rel="modulepreload" crossorigin href="/assets/plus-C9cYVbL-.js">
21
- <link rel="modulepreload" crossorigin href="/assets/search-sl1OeJFl.js">
22
- <link rel="modulepreload" crossorigin href="/assets/x-MIimOGs6.js">
23
- <link rel="modulepreload" crossorigin href="/assets/DocBrowserContext-BcZRBsCg.js">
24
- <link rel="modulepreload" crossorigin href="/assets/DocBrowser-DKkE3Y4I.js">
25
- <link rel="modulepreload" crossorigin href="/assets/config-Df97LeLR.js">
26
- <link rel="stylesheet" crossorigin href="/assets/index-BahpXJg8.css">
9
+ <script type="module" crossorigin src="/assets/index-DvKS3L9j.js"></script>
10
+ <link rel="modulepreload" crossorigin href="/assets/i18n-CSytxMFI.js">
11
+ <link rel="modulepreload" crossorigin href="/assets/chunk-JZWAC4HX-B-4B29RN.js">
12
+ <link rel="modulepreload" crossorigin href="/assets/dist-pCfWPG1A.js">
13
+ <link rel="modulepreload" crossorigin href="/assets/createLucideIcon-DiFAvXmK.js">
14
+ <link rel="modulepreload" crossorigin href="/assets/select-CT50pzod.js">
15
+ <link rel="modulepreload" crossorigin href="/assets/dist-kW_O3kyZ.js">
16
+ <link rel="modulepreload" crossorigin href="/assets/react-BuSP2-8B.js">
17
+ <link rel="modulepreload" crossorigin href="/assets/useMutation-BzCrO8j-.js">
18
+ <link rel="modulepreload" crossorigin href="/assets/book-open-B26jGBjY.js">
19
+ <link rel="modulepreload" crossorigin href="/assets/external-link-D5-p-Gmm.js">
20
+ <link rel="modulepreload" crossorigin href="/assets/plus-CM9XJ0Tf.js">
21
+ <link rel="modulepreload" crossorigin href="/assets/search-Ctaw34Kp.js">
22
+ <link rel="modulepreload" crossorigin href="/assets/x-CHOBE-63.js">
23
+ <link rel="modulepreload" crossorigin href="/assets/DocBrowserContext-CVJuwCcw.js">
24
+ <link rel="modulepreload" crossorigin href="/assets/DocBrowser-3y_NHZ71.js">
25
+ <link rel="modulepreload" crossorigin href="/assets/config-BaC29Qf-.js">
26
+ <link rel="stylesheet" crossorigin href="/assets/index-CUy6doWo.css">
27
27
  </head>
28
28
 
29
29
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextclaw/ui",
3
- "version": "0.11.19",
3
+ "version": "0.11.21",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -28,11 +28,11 @@
28
28
  "tailwind-merge": "^2.5.4",
29
29
  "zod": "^3.23.8",
30
30
  "zustand": "^5.0.2",
31
- "@nextclaw/ncp-react": "0.4.10",
32
- "@nextclaw/ncp-http-agent-client": "0.3.8",
33
- "@nextclaw/ncp": "0.4.4",
34
- "@nextclaw/agent-chat": "0.1.5",
35
- "@nextclaw/agent-chat-ui": "0.2.17"
31
+ "@nextclaw/ncp-react": "0.4.12",
32
+ "@nextclaw/ncp": "0.4.6",
33
+ "@nextclaw/ncp-http-agent-client": "0.3.10",
34
+ "@nextclaw/agent-chat-ui": "0.2.19",
35
+ "@nextclaw/agent-chat": "0.1.6"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@testing-library/react": "^16.3.0",
@@ -10,6 +10,7 @@ import {
10
10
  buildRenderableText,
11
11
  buildTextPart,
12
12
  } from "@/components/chat/adapters/chat-message-inline-content.adapter";
13
+ import { buildFileOperationCardData } from "@/components/chat/adapters/chat-message.file-operation-card";
13
14
  import { buildSubagentToolCard } from "@/components/chat/adapters/chat-message.subagent-tool-card";
14
15
  import type {
15
16
  ChatMessagePartViewModel,
@@ -27,6 +28,7 @@ export type ChatMessageAdapterTexts = {
27
28
  reasoningLabel: string;
28
29
  toolCallLabel: string;
29
30
  toolResultLabel: string;
31
+ toolInputLabel: string;
30
32
  toolNoOutputLabel: string;
31
33
  toolOutputLabel: string;
32
34
  toolStatusPreparingLabel: string;
@@ -77,6 +79,8 @@ export type ChatMessagePartSource =
77
79
  type ToolCardViewSource = ToolCard & {
78
80
  statusTone: ChatToolPartViewModel["statusTone"];
79
81
  statusLabel: string;
82
+ fileOperation?: ChatToolPartViewModel["fileOperation"];
83
+ outputData?: unknown;
80
84
  };
81
85
 
82
86
  type ChatMessagePartAdapterParams = {
@@ -112,6 +116,21 @@ function readOptionalNumber(value: unknown): number | null {
112
116
  return Number.isFinite(parsed) && parsed >= 0 ? parsed : null;
113
117
  }
114
118
 
119
+ function isTerminalResultRecord(value: unknown): value is Record<string, unknown> {
120
+ if (!isRecord(value)) {
121
+ return false;
122
+ }
123
+ return (
124
+ "command" in value ||
125
+ "workingDir" in value ||
126
+ "exitCode" in value ||
127
+ "stdout" in value ||
128
+ "stderr" in value ||
129
+ "aggregated_output" in value ||
130
+ "combinedOutput" in value
131
+ );
132
+ }
133
+
115
134
  function extractAssetFileView(
116
135
  value: unknown,
117
136
  texts: ChatMessageAdapterTexts,
@@ -161,7 +180,12 @@ function buildToolCard(
161
180
  kind: toolCard.kind,
162
181
  toolName: toolCard.name,
163
182
  summary: toolCard.detail,
183
+ inputLabel: texts.toolInputLabel,
184
+ input: "input" in toolCard && typeof toolCard.input === "string"
185
+ ? toolCard.input
186
+ : undefined,
164
187
  output: toolCard.text,
188
+ outputData: toolCard.outputData,
165
189
  hasResult: Boolean(toolCard.hasResult),
166
190
  statusTone: toolCard.statusTone,
167
191
  statusLabel: toolCard.statusLabel,
@@ -169,6 +193,9 @@ function buildToolCard(
169
193
  toolCard.kind === "call" ? texts.toolCallLabel : texts.toolResultLabel,
170
194
  outputLabel: texts.toolOutputLabel,
171
195
  emptyLabel: texts.toolNoOutputLabel,
196
+ ...("fileOperation" in toolCard && toolCard.fileOperation
197
+ ? { fileOperation: toolCard.fileOperation }
198
+ : {}),
172
199
  };
173
200
  }
174
201
 
@@ -216,7 +243,7 @@ function resolveToolCardStatus(params: {
216
243
  kind: "call",
217
244
  hasResult: false,
218
245
  statusTone: "running",
219
- statusLabel: params.texts.toolStatusPreparingLabel,
246
+ statusLabel: params.texts.toolStatusRunningLabel,
220
247
  };
221
248
  }
222
249
  return {
@@ -227,6 +254,27 @@ function resolveToolCardStatus(params: {
227
254
  };
228
255
  }
229
256
 
257
+ function parseStructuredValue(value: unknown): unknown {
258
+ if (typeof value !== "string") {
259
+ return value;
260
+ }
261
+ const trimmed = value.trim();
262
+ if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) {
263
+ return value;
264
+ }
265
+ try {
266
+ return JSON.parse(trimmed) as unknown;
267
+ } catch {
268
+ return value;
269
+ }
270
+ }
271
+
272
+ function buildToolInvocationInput(args?: unknown, parsedArgs?: unknown): string | undefined {
273
+ const source = parsedArgs ?? parseStructuredValue(args);
274
+ const text = stringifyUnknown(source).trim();
275
+ return text || undefined;
276
+ }
277
+
230
278
  function buildReasoningPart(
231
279
  part: Extract<ChatMessagePartSource, { type: "reasoning" }>,
232
280
  texts: ChatMessageAdapterTexts,
@@ -296,22 +344,45 @@ function buildToolInvocationPart(
296
344
  result: invocation.result,
297
345
  texts,
298
346
  });
299
- const detail = summarizeToolArgs(invocation.parsedArgs ?? invocation.args);
347
+ const fileOperationCardData = buildFileOperationCardData({
348
+ toolName: invocation.toolName,
349
+ status: invocation.status,
350
+ toolCallId: invocation.toolCallId,
351
+ args: invocation.args,
352
+ parsedArgs: invocation.parsedArgs,
353
+ result: invocation.result,
354
+ });
355
+ const detail =
356
+ fileOperationCardData?.summary ??
357
+ summarizeToolArgs(invocation.parsedArgs ?? invocation.args);
358
+ const input = fileOperationCardData
359
+ ? undefined
360
+ : buildToolInvocationInput(invocation.args, invocation.parsedArgs);
300
361
  const rawResult =
301
362
  typeof invocation.error === "string" && invocation.error.trim()
302
363
  ? invocation.error.trim()
303
364
  : invocation.result != null
304
365
  ? stringifyUnknown(invocation.result).trim()
305
366
  : "";
367
+ const shouldHideStructuredTerminalJson =
368
+ !invocation.error && isTerminalResultRecord(invocation.result);
369
+ const shouldShowRawResult =
370
+ (!fileOperationCardData?.fileOperation || Boolean(invocation.error)) &&
371
+ !shouldHideStructuredTerminalJson;
306
372
  const card: ToolCardViewSource = {
307
373
  kind: statusView.kind,
308
374
  name: invocation.toolName,
309
375
  detail,
310
- text: rawResult || undefined,
376
+ ...(input ? { input } : {}),
377
+ text: shouldShowRawResult && rawResult ? rawResult : undefined,
378
+ outputData: invocation.result,
311
379
  callId: invocation.toolCallId || undefined,
312
380
  hasResult: statusView.hasResult,
313
381
  statusTone: statusView.statusTone,
314
382
  statusLabel: statusView.statusLabel,
383
+ ...(fileOperationCardData?.fileOperation
384
+ ? { fileOperation: fileOperationCardData.fileOperation }
385
+ : {}),
315
386
  };
316
387
  return {
317
388
  type: "tool-card",
@@ -17,8 +17,9 @@ const defaultTexts = {
17
17
  reasoningLabel: "Reasoning",
18
18
  toolCallLabel: "Tool Call",
19
19
  toolResultLabel: "Tool Result",
20
+ toolInputLabel: "Input",
20
21
  toolNoOutputLabel: "No output",
21
- toolOutputLabel: "View Output",
22
+ toolOutputLabel: "Output",
22
23
  toolStatusPreparingLabel: "Preparing",
23
24
  toolStatusRunningLabel: "Running",
24
25
  toolStatusCompletedLabel: "Completed",
@@ -88,7 +89,7 @@ it("maps markdown, reasoning, and tool parts into UI view models", () => {
88
89
  statusLabel: "Completed",
89
90
  statusTone: "success",
90
91
  titleLabel: "Tool Result",
91
- outputLabel: "View Output",
92
+ outputLabel: "Output",
92
93
  },
93
94
  });
94
95
  });
@@ -126,7 +127,7 @@ it("maps tool lifecycle statuses into visible card state feedback", () => {
126
127
  type: "tool-card",
127
128
  card: {
128
129
  statusTone: "running",
129
- statusLabel: "Preparing",
130
+ statusLabel: "Running",
130
131
  titleLabel: "Tool Call",
131
132
  },
132
133
  });
@@ -141,6 +142,87 @@ it("maps tool lifecycle statuses into visible card state feedback", () => {
141
142
  });
142
143
  });
143
144
 
145
+ it("preserves full generic tool args for the expanded body while keeping the header summary short", () => {
146
+ const adapted = adapt([
147
+ {
148
+ id: "assistant-generic-tool-input",
149
+ role: "assistant",
150
+ parts: [
151
+ {
152
+ type: "tool-invocation",
153
+ toolInvocation: {
154
+ status: ToolInvocationStatus.PARTIAL_CALL,
155
+ toolCallId: "call-generic-input",
156
+ toolName: "open_url",
157
+ args: JSON.stringify({
158
+ url: "https://example.com/really/long/path",
159
+ headers: {
160
+ authorization: "Bearer secret-token",
161
+ },
162
+ mode: "reader",
163
+ }),
164
+ },
165
+ },
166
+ ],
167
+ },
168
+ ] as unknown as ChatMessageSource[]);
169
+
170
+ expect(adapted[0]?.parts[0]).toMatchObject({
171
+ type: "tool-card",
172
+ card: {
173
+ toolName: "open_url",
174
+ statusTone: "running",
175
+ summary: "url: https://example.com/really/long/path",
176
+ input: `{
177
+ "url": "https://example.com/really/long/path",
178
+ "headers": {
179
+ "authorization": "Bearer secret-token"
180
+ },
181
+ "mode": "reader"
182
+ }`,
183
+ },
184
+ });
185
+ });
186
+
187
+ it("keeps structured terminal results as structured data instead of raw json output", () => {
188
+ const terminalResult = {
189
+ status: "completed",
190
+ command: "python3 -m http.server 8765",
191
+ aggregated_output: "",
192
+ exit_code: 0,
193
+ };
194
+
195
+ const adapted = adapt([
196
+ {
197
+ id: "assistant-terminal-result",
198
+ role: "assistant",
199
+ parts: [
200
+ {
201
+ type: "tool-invocation",
202
+ toolInvocation: {
203
+ status: ToolInvocationStatus.RESULT,
204
+ toolCallId: "call-terminal-result",
205
+ toolName: "command_execution",
206
+ args: '{"command":"python3 -m http.server 8765"}',
207
+ result: terminalResult,
208
+ },
209
+ },
210
+ ],
211
+ },
212
+ ] as unknown as ChatMessageSource[]);
213
+
214
+ expect(adapted[0]?.parts[0]).toMatchObject({
215
+ type: "tool-card",
216
+ card: {
217
+ toolName: "command_execution",
218
+ summary: "command: python3 -m http.server 8765",
219
+ output: undefined,
220
+ outputData: terminalResult,
221
+ statusTone: "success",
222
+ },
223
+ });
224
+ });
225
+
144
226
  it("renders spawn tool cards from structured subagent status updates", () => {
145
227
  const adapted = adapt([
146
228
  {
@@ -377,3 +459,239 @@ it("renders asset tool results as previewable files", () => {
377
459
  },
378
460
  });
379
461
  });
462
+
463
+ it("builds edit-file previews from structured args before the tool finishes", () => {
464
+ const adapted = adapt([
465
+ {
466
+ id: "assistant-edit-preview",
467
+ role: "assistant",
468
+ parts: [
469
+ {
470
+ type: "tool-invocation",
471
+ toolInvocation: {
472
+ status: ToolInvocationStatus.CALL,
473
+ toolCallId: "edit-call-1",
474
+ toolName: "edit_file",
475
+ args: JSON.stringify({
476
+ path: "src/app.ts",
477
+ oldText: "const color = 'red';",
478
+ newText: "const color = 'blue';",
479
+ }),
480
+ parsedArgs: {
481
+ path: "src/app.ts",
482
+ oldText: "const color = 'red';",
483
+ newText: "const color = 'blue';",
484
+ },
485
+ },
486
+ },
487
+ ],
488
+ },
489
+ ] as unknown as ChatMessageSource[]);
490
+
491
+ expect(adapted[0]?.parts[0]).toMatchObject({
492
+ type: "tool-card",
493
+ card: {
494
+ toolName: "edit_file",
495
+ summary: "src/app.ts",
496
+ statusTone: "running",
497
+ fileOperation: {
498
+ blocks: [
499
+ {
500
+ path: "src/app.ts",
501
+ lines: [
502
+ {
503
+ kind: "remove",
504
+ text: "const color = 'red';",
505
+ },
506
+ {
507
+ kind: "add",
508
+ text: "const color = 'blue';",
509
+ },
510
+ ],
511
+ },
512
+ ],
513
+ },
514
+ },
515
+ });
516
+ });
517
+
518
+ it("builds write-file previews from partial native args before the JSON is complete", () => {
519
+ const adapted = adapt([
520
+ {
521
+ id: "assistant-write-preview",
522
+ role: "assistant",
523
+ parts: [
524
+ {
525
+ type: "tool-invocation",
526
+ toolInvocation: {
527
+ status: ToolInvocationStatus.PARTIAL_CALL,
528
+ toolCallId: "write-call-1",
529
+ toolName: "write_file",
530
+ args: '{"path":"games/snake.html","content":"<!DOCTYPE html>\\n<canvas id=\\"game\\"></canvas>\\n<script>const score = 1;',
531
+ },
532
+ },
533
+ ],
534
+ },
535
+ ] as unknown as ChatMessageSource[]);
536
+
537
+ expect(adapted[0]?.parts[0]).toMatchObject({
538
+ type: "tool-card",
539
+ card: {
540
+ toolName: "write_file",
541
+ summary: "games/snake.html",
542
+ statusTone: "running",
543
+ statusLabel: "Running",
544
+ fileOperation: {
545
+ blocks: [
546
+ {
547
+ display: "preview",
548
+ path: "games/snake.html",
549
+ lines: expect.arrayContaining([
550
+ expect.objectContaining({
551
+ kind: "add",
552
+ text: "<!DOCTYPE html>",
553
+ }),
554
+ expect.objectContaining({
555
+ kind: "add",
556
+ text: '<canvas id="game"></canvas>',
557
+ }),
558
+ ]),
559
+ },
560
+ ],
561
+ },
562
+ },
563
+ });
564
+ });
565
+
566
+ it("keeps completed write-file cards in preview mode instead of falling back to raw byte summaries", () => {
567
+ const adapted = adapt([
568
+ {
569
+ id: "assistant-write-result",
570
+ role: "assistant",
571
+ parts: [
572
+ {
573
+ type: "tool-invocation",
574
+ toolInvocation: {
575
+ status: ToolInvocationStatus.RESULT,
576
+ toolCallId: "write-result-1",
577
+ toolName: "write_file",
578
+ args: JSON.stringify({
579
+ path: "games/snake.html",
580
+ content: "<!DOCTYPE html>\n<canvas id=\"game\"></canvas>",
581
+ }),
582
+ result: "Wrote 3906 bytes to games/snake.html",
583
+ },
584
+ },
585
+ ],
586
+ },
587
+ ] as unknown as ChatMessageSource[]);
588
+
589
+ expect(adapted[0]?.parts[0]).toMatchObject({
590
+ type: "tool-card",
591
+ card: {
592
+ toolName: "write_file",
593
+ summary: "games/snake.html",
594
+ statusTone: "success",
595
+ fileOperation: {
596
+ blocks: [
597
+ {
598
+ display: "preview",
599
+ path: "games/snake.html",
600
+ lines: [
601
+ {
602
+ kind: "add",
603
+ text: "<!DOCTYPE html>",
604
+ newLineNumber: 1,
605
+ },
606
+ {
607
+ kind: "add",
608
+ text: "<canvas id=\"game\"></canvas>",
609
+ newLineNumber: 2,
610
+ },
611
+ ],
612
+ },
613
+ ],
614
+ },
615
+ },
616
+ });
617
+ expect(adapted[0]?.parts[0]).not.toMatchObject({
618
+ type: "tool-card",
619
+ card: {
620
+ output: "Wrote 3906 bytes to games/snake.html",
621
+ },
622
+ });
623
+ });
624
+
625
+ it("renders codex file_change results as structured diff previews", () => {
626
+ const adapted = adapt([
627
+ {
628
+ id: "assistant-file-change",
629
+ role: "assistant",
630
+ parts: [
631
+ {
632
+ type: "tool-invocation",
633
+ toolInvocation: {
634
+ status: ToolInvocationStatus.RESULT,
635
+ toolCallId: "file-change-1",
636
+ toolName: "file_change",
637
+ args: JSON.stringify({
638
+ changes: [
639
+ {
640
+ path: "src/main.ts",
641
+ diff: [
642
+ "--- a/src/main.ts",
643
+ "+++ b/src/main.ts",
644
+ "@@",
645
+ "-console.log('old');",
646
+ "+console.log('new');",
647
+ ].join("\n"),
648
+ },
649
+ ],
650
+ }),
651
+ result: {
652
+ status: "completed",
653
+ changes: [
654
+ {
655
+ path: "src/main.ts",
656
+ diff: [
657
+ "--- a/src/main.ts",
658
+ "+++ b/src/main.ts",
659
+ "@@",
660
+ "-console.log('old');",
661
+ "+console.log('new');",
662
+ ].join("\n"),
663
+ },
664
+ ],
665
+ },
666
+ },
667
+ },
668
+ ],
669
+ },
670
+ ] as unknown as ChatMessageSource[]);
671
+
672
+ expect(adapted[0]?.parts[0]).toMatchObject({
673
+ type: "tool-card",
674
+ card: {
675
+ toolName: "file_change",
676
+ summary: "src/main.ts",
677
+ statusTone: "success",
678
+ fileOperation: {
679
+ blocks: [
680
+ {
681
+ path: "src/main.ts",
682
+ lines: [
683
+ {
684
+ kind: "remove",
685
+ text: "console.log('old');",
686
+ },
687
+ {
688
+ kind: "add",
689
+ text: "console.log('new');",
690
+ },
691
+ ],
692
+ },
693
+ ],
694
+ },
695
+ },
696
+ });
697
+ });