@nextclaw/ui 0.11.20 → 0.11.22

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 (125) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/assets/{ChannelsList-DAx7wv0_.js → ChannelsList-Zeys_w43.js} +6 -6
  3. package/dist/assets/ChatPage-DWOU_8P6.js +43 -0
  4. package/dist/assets/DocBrowser-B9OaZjmg.js +1 -0
  5. package/dist/assets/{DocBrowser-DKkE3Y4I.js → DocBrowser-BmtBLFU0.js} +1 -1
  6. package/dist/assets/{DocBrowserContext-BcZRBsCg.js → DocBrowserContext-YIKkPb76.js} +1 -1
  7. package/dist/assets/{LogoBadge-BIPDLEwK.js → LogoBadge-F7ZWdxLT.js} +1 -1
  8. package/dist/assets/MarketplacePage-BfaTTqN6.js +1 -0
  9. package/dist/assets/{MarketplacePage-Dlp5BgCh.js → MarketplacePage-Cd4faegU.js} +2 -2
  10. package/dist/assets/{McpMarketplacePage-CwKtAil8.js → McpMarketplacePage-C09Ngs7O.js} +2 -2
  11. package/dist/assets/ModelConfig-DJgdcgvQ.js +1 -0
  12. package/dist/assets/ProvidersList-w0rVFIBf.js +1 -0
  13. package/dist/assets/RemoteAccessPage-BJ_ckkOV.js +1 -0
  14. package/dist/assets/RuntimeConfig-Cmn2xPQO.js +1 -0
  15. package/dist/assets/{SearchConfig-v46R5a2U.js → SearchConfig-BT13qpR_.js} +1 -1
  16. package/dist/assets/{SecretsConfig-CXvUpbB_.js → SecretsConfig-CvqEVn0B.js} +2 -2
  17. package/dist/assets/{SessionsConfig-7vUHMtOh.js → SessionsConfig-DHHcYznk.js} +2 -2
  18. package/dist/assets/{book-open-DzSduAaw.js → book-open-CXoF5nQC.js} +1 -1
  19. package/dist/assets/chat-session-display-VW6ZMvZP.js +1 -0
  20. package/dist/assets/{chunk-JZWAC4HX-C1vpvW4r.js → chunk-JZWAC4HX-CvRWvTy5.js} +1 -1
  21. package/dist/assets/{config-Df97LeLR.js → config-DJswxxE8.js} +1 -1
  22. package/dist/assets/{createLucideIcon-CcR5wVoU.js → createLucideIcon-CjGHOWb6.js} +1 -1
  23. package/dist/assets/{dist-Dii9v3X9.js → dist-Cl2QB-2y.js} +1 -1
  24. package/dist/assets/{dist-BMlnBah3.js → dist-nqTTbVdA.js} +1 -1
  25. package/dist/assets/{external-link-CnSDrvJE.js → external-link-tIO7zING.js} +1 -1
  26. package/dist/assets/{hash-CAnX6PNt.js → hash-JWUyl1pT.js} +1 -1
  27. package/dist/assets/i18n-CDHMXlRZ.js +1 -0
  28. package/dist/assets/index-BlH4-cBw.css +1 -0
  29. package/dist/assets/{index-B0DzQqwv.js → index-C6d0xmtm.js} +3 -3
  30. package/dist/assets/{label-CtIFj7_6.js → label-BIpeNu4r.js} +1 -1
  31. package/dist/assets/loader-circle-Cs8XVFTw.js +1 -0
  32. package/dist/assets/{logos-3KFNiOej.js → logos-DThdM9lk.js} +1 -1
  33. package/dist/assets/{page-layout-BMwpn87D.js → page-layout-D3Xo605Z.js} +1 -1
  34. package/dist/assets/plus-PHf8q-Ct.js +1 -0
  35. package/dist/assets/{popover-BIzq25oH.js → popover-BJRUGA_H.js} +1 -1
  36. package/dist/assets/provider-models-bz5y28rq.js +1 -0
  37. package/dist/assets/{react-ji6GGP_j.js → react-7ZHqQtEV.js} +1 -1
  38. package/dist/assets/refresh-ccw-CC6-_QuL.js +1 -0
  39. package/dist/assets/{save-CMgYkJ-y.js → save-DJM5RRWW.js} +1 -1
  40. package/dist/assets/search-C91yH_6y.js +1 -0
  41. package/dist/assets/{security-config-Xi5DYW7j.js → security-config-T5zpg16O.js} +1 -1
  42. package/dist/assets/{select-Cz82gl01.js → select-DSkTc61S.js} +1 -1
  43. package/dist/assets/skeleton-Dzg-HOiN.js +1 -0
  44. package/dist/assets/{status-dot-C7q1HvLH.js → status-dot-LNBlDu3q.js} +1 -1
  45. package/dist/assets/{switch-DYswvkYj.js → switch-Bo-Y46HZ.js} +1 -1
  46. package/dist/assets/tabs-custom-DXv507_2.js +1 -0
  47. package/dist/assets/{trash-2-DfXI7-ap.js → trash-2-DFZmW6Gg.js} +1 -1
  48. package/dist/assets/useConfirmDialog-Bs5Ll17m.js +1 -0
  49. package/dist/assets/{useMutation-s2sn2yzh.js → useMutation-DrZrOgVL.js} +1 -1
  50. package/dist/assets/x-D7Q1yqSF.js +1 -0
  51. package/dist/index.html +18 -18
  52. package/package.json +6 -6
  53. package/src/api/ncp-session.test.ts +37 -0
  54. package/src/api/ncp-session.ts +29 -1
  55. package/src/api/server-path.ts +23 -0
  56. package/src/api/types.ts +41 -0
  57. package/src/components/chat/ChatConversationPanel.test.tsx +43 -7
  58. package/src/components/chat/ChatConversationPanel.tsx +23 -17
  59. package/src/components/chat/ChatSidebar.test.tsx +2 -2
  60. package/src/components/chat/ChatSidebar.tsx +2 -2
  61. package/src/components/chat/adapters/chat-input-bar.adapter.test.ts +1 -0
  62. package/src/components/chat/adapters/chat-input-bar.adapter.ts +7 -2
  63. package/src/components/chat/adapters/chat-message-part.adapter.ts +81 -6
  64. package/src/components/chat/adapters/chat-message.adapter.test.ts +393 -3
  65. package/src/components/chat/adapters/chat-message.partial-json.ts +89 -0
  66. package/src/components/chat/adapters/file-operation/card.ts +330 -0
  67. package/src/components/chat/adapters/file-operation/diff.ts +398 -0
  68. package/src/components/chat/adapters/file-operation/line-builder.ts +249 -0
  69. package/src/components/chat/adapters/file-operation/record-readers.ts +233 -0
  70. package/src/components/chat/chat-composer-state.ts +3 -3
  71. package/src/components/chat/chat-session-display.test.ts +21 -0
  72. package/src/components/chat/chat-session-display.ts +6 -1
  73. package/src/components/chat/containers/chat-input-bar.container.tsx +29 -32
  74. package/src/components/chat/containers/chat-message-list.container.tsx +1 -0
  75. package/src/components/chat/hooks/use-chat-session-label.ts +19 -0
  76. package/src/components/chat/hooks/use-chat-session-project.test.tsx +117 -0
  77. package/src/components/chat/hooks/use-chat-session-project.ts +40 -0
  78. package/src/components/chat/{chat-session-label.service.ts → hooks/use-chat-session-update.ts} +11 -7
  79. package/src/components/chat/managers/chat-session-list.manager.ts +5 -1
  80. package/src/components/chat/ncp/NcpChatPage.tsx +55 -17
  81. package/src/components/chat/ncp/ncp-chat-page-data.test.ts +33 -0
  82. package/src/components/chat/ncp/ncp-chat-page-data.ts +21 -15
  83. package/src/components/chat/ncp/ncp-session-adapter.test.ts +176 -0
  84. package/src/components/chat/ncp/ncp-session-adapter.ts +16 -0
  85. package/src/components/chat/session-header/chat-session-header-actions.test.tsx +63 -0
  86. package/src/components/chat/session-header/chat-session-header-actions.tsx +95 -0
  87. package/src/components/chat/session-header/chat-session-header-menu-item.tsx +35 -0
  88. package/src/components/chat/session-header/chat-session-project-badge.test.tsx +66 -0
  89. package/src/components/chat/session-header/chat-session-project-badge.tsx +102 -0
  90. package/src/components/chat/session-header/chat-session-project-dialog.tsx +34 -0
  91. package/src/components/chat/stores/chat-input.store.ts +6 -3
  92. package/src/components/chat/stores/chat-thread.store.ts +6 -2
  93. package/src/components/chat/useNcpAgentRuntime.test.tsx +90 -0
  94. package/src/components/path-picker/server-path-picker-dialog.test.tsx +92 -0
  95. package/src/components/path-picker/server-path-picker-dialog.tsx +282 -0
  96. package/src/hooks/server-path/use-server-path-browse.ts +19 -0
  97. package/src/hooks/useConfig.ts +26 -1
  98. package/src/lib/i18n/i18n-language-owner.ts +94 -0
  99. package/src/lib/i18n/i18n.path-picker.ts +12 -0
  100. package/src/lib/i18n.chat.ts +25 -1
  101. package/src/lib/i18n.ts +21 -84
  102. package/src/lib/session-project/session-project.utils.ts +30 -0
  103. package/src/remote/remote-access-feedback.service.test.ts +18 -0
  104. package/src/remote/remote-access-feedback.service.ts +10 -1
  105. package/dist/assets/ChatPage-l2PYwCeB.js +0 -38
  106. package/dist/assets/DocBrowser-CIHLqoIm.js +0 -1
  107. package/dist/assets/MarketplacePage-TVeyVOuO.js +0 -1
  108. package/dist/assets/ModelConfig-Dg6F3Ldb.js +0 -1
  109. package/dist/assets/ProvidersList-f7bQdRxA.js +0 -1
  110. package/dist/assets/RemoteAccessPage-w_dY7P4T.js +0 -1
  111. package/dist/assets/RuntimeConfig-M4OKjmgU.js +0 -1
  112. package/dist/assets/chat-session-display-CGfXhJoT.js +0 -1
  113. package/dist/assets/i18n-CXBpwAwA.js +0 -1
  114. package/dist/assets/index-BahpXJg8.css +0 -1
  115. package/dist/assets/loader-circle-qgU4zQDw.js +0 -1
  116. package/dist/assets/plus-C9cYVbL-.js +0 -1
  117. package/dist/assets/provider-models-C8JQUd1E.js +0 -1
  118. package/dist/assets/search-sl1OeJFl.js +0 -1
  119. package/dist/assets/skeleton-rgIt7a5q.js +0 -1
  120. package/dist/assets/tabs-custom-DKYQxrx1.js +0 -1
  121. package/dist/assets/useConfirmDialog-CXDAxtRL.js +0 -1
  122. package/dist/assets/x-MIimOGs6.js +0 -1
  123. /package/dist/assets/{config-hints-fGnUjDe9.js → config-hints-WtpHP_DW.js} +0 -0
  124. /package/dist/assets/{config-layout-B-7erZRN.js → config-layout-LQ10ozRC.js} +0 -0
  125. /package/dist/assets/{marketplace-localization-CXeGRf6E.js → marketplace-localization-CxSTG9wr.js} +0 -0
@@ -12,6 +12,7 @@ export type ChatThinkingLevel = 'off' | 'minimal' | 'low' | 'medium' | 'high' |
12
12
  export type ChatSkillRecord = {
13
13
  key: string;
14
14
  label: string;
15
+ scopeLabel?: string;
15
16
  description?: string;
16
17
  descriptionZh?: string;
17
18
  badgeLabel?: string;
@@ -43,6 +44,7 @@ const SLASH_ITEM_MATCH_SCORE = {
43
44
  export type ChatInputBarAdapterTexts = {
44
45
  slashSkillSubtitle: string;
45
46
  slashSkillSpecLabel: string;
47
+ slashSkillScopeLabel: string;
46
48
  noSkillDescription: string;
47
49
  recentSkillsLabel: string;
48
50
  allSkillsLabel: string;
@@ -171,7 +173,7 @@ function prioritizeSkillRecords(skillRecords: ChatSkillRecord[], recentSkillValu
171
173
  export function buildChatSlashItems(
172
174
  skillRecords: ChatSkillRecord[],
173
175
  normalizedSlashQuery: string,
174
- texts: Pick<ChatInputBarAdapterTexts, 'slashSkillSubtitle' | 'slashSkillSpecLabel' | 'noSkillDescription'>,
176
+ texts: Pick<ChatInputBarAdapterTexts, 'slashSkillSubtitle' | 'slashSkillSpecLabel' | 'slashSkillScopeLabel' | 'noSkillDescription'>,
175
177
  recentSkillValues: string[] = []
176
178
  ): ChatSlashItem[] {
177
179
  const skillSortCollator = new Intl.Collator(undefined, { sensitivity: 'base', numeric: true });
@@ -211,7 +213,10 @@ export function buildChatSlashItems(
211
213
  title: record.label || record.key,
212
214
  subtitle: texts.slashSkillSubtitle,
213
215
  description: (record.descriptionZh ?? record.description ?? '').trim() || texts.noSkillDescription,
214
- detailLines: [`${texts.slashSkillSpecLabel}: ${record.key}`],
216
+ detailLines: [
217
+ `${texts.slashSkillSpecLabel}: ${record.key}`,
218
+ ...(record.scopeLabel ? [`${texts.slashSkillScopeLabel}: ${record.scopeLabel}`] : [])
219
+ ],
215
220
  value: record.key
216
221
  }));
217
222
  }
@@ -3,13 +3,12 @@ import {
3
3
  summarizeToolArgs,
4
4
  type ToolCard,
5
5
  } from "@/lib/chat-message";
6
- import {
7
- type ChatInlineTokenSource,
8
- } from "@/components/chat/chat-inline-token.utils";
6
+ import { type ChatInlineTokenSource } from "@/components/chat/chat-inline-token.utils";
9
7
  import {
10
8
  buildRenderableText,
11
9
  buildTextPart,
12
10
  } from "@/components/chat/adapters/chat-message-inline-content.adapter";
11
+ import { buildFileOperationCardData } from "@/components/chat/adapters/file-operation/card";
13
12
  import { buildSubagentToolCard } from "@/components/chat/adapters/chat-message.subagent-tool-card";
14
13
  import type {
15
14
  ChatMessagePartViewModel,
@@ -27,6 +26,7 @@ export type ChatMessageAdapterTexts = {
27
26
  reasoningLabel: string;
28
27
  toolCallLabel: string;
29
28
  toolResultLabel: string;
29
+ toolInputLabel: string;
30
30
  toolNoOutputLabel: string;
31
31
  toolOutputLabel: string;
32
32
  toolStatusPreparingLabel: string;
@@ -77,6 +77,8 @@ export type ChatMessagePartSource =
77
77
  type ToolCardViewSource = ToolCard & {
78
78
  statusTone: ChatToolPartViewModel["statusTone"];
79
79
  statusLabel: string;
80
+ fileOperation?: ChatToolPartViewModel["fileOperation"];
81
+ outputData?: unknown;
80
82
  };
81
83
 
82
84
  type ChatMessagePartAdapterParams = {
@@ -112,6 +114,23 @@ function readOptionalNumber(value: unknown): number | null {
112
114
  return Number.isFinite(parsed) && parsed >= 0 ? parsed : null;
113
115
  }
114
116
 
117
+ function isTerminalResultRecord(
118
+ value: unknown,
119
+ ): 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,13 @@ function buildToolCard(
161
180
  kind: toolCard.kind,
162
181
  toolName: toolCard.name,
163
182
  summary: toolCard.detail,
183
+ inputLabel: texts.toolInputLabel,
184
+ input:
185
+ "input" in toolCard && typeof toolCard.input === "string"
186
+ ? toolCard.input
187
+ : undefined,
164
188
  output: toolCard.text,
189
+ outputData: toolCard.outputData,
165
190
  hasResult: Boolean(toolCard.hasResult),
166
191
  statusTone: toolCard.statusTone,
167
192
  statusLabel: toolCard.statusLabel,
@@ -169,6 +194,9 @@ function buildToolCard(
169
194
  toolCard.kind === "call" ? texts.toolCallLabel : texts.toolResultLabel,
170
195
  outputLabel: texts.toolOutputLabel,
171
196
  emptyLabel: texts.toolNoOutputLabel,
197
+ ...("fileOperation" in toolCard && toolCard.fileOperation
198
+ ? { fileOperation: toolCard.fileOperation }
199
+ : {}),
172
200
  };
173
201
  }
174
202
 
@@ -216,7 +244,7 @@ function resolveToolCardStatus(params: {
216
244
  kind: "call",
217
245
  hasResult: false,
218
246
  statusTone: "running",
219
- statusLabel: params.texts.toolStatusPreparingLabel,
247
+ statusLabel: params.texts.toolStatusRunningLabel,
220
248
  };
221
249
  }
222
250
  return {
@@ -227,6 +255,30 @@ function resolveToolCardStatus(params: {
227
255
  };
228
256
  }
229
257
 
258
+ function parseStructuredValue(value: unknown): unknown {
259
+ if (typeof value !== "string") {
260
+ return value;
261
+ }
262
+ const trimmed = value.trim();
263
+ if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) {
264
+ return value;
265
+ }
266
+ try {
267
+ return JSON.parse(trimmed) as unknown;
268
+ } catch {
269
+ return value;
270
+ }
271
+ }
272
+
273
+ function buildToolInvocationInput(
274
+ args?: unknown,
275
+ parsedArgs?: unknown,
276
+ ): string | undefined {
277
+ const source = parsedArgs ?? parseStructuredValue(args);
278
+ const text = stringifyUnknown(source).trim();
279
+ return text || undefined;
280
+ }
281
+
230
282
  function buildReasoningPart(
231
283
  part: Extract<ChatMessagePartSource, { type: "reasoning" }>,
232
284
  texts: ChatMessageAdapterTexts,
@@ -296,22 +348,45 @@ function buildToolInvocationPart(
296
348
  result: invocation.result,
297
349
  texts,
298
350
  });
299
- const detail = summarizeToolArgs(invocation.parsedArgs ?? invocation.args);
351
+ const fileOperationCardData = buildFileOperationCardData({
352
+ toolName: invocation.toolName,
353
+ status: invocation.status,
354
+ toolCallId: invocation.toolCallId,
355
+ args: invocation.args,
356
+ parsedArgs: invocation.parsedArgs,
357
+ result: invocation.result,
358
+ });
359
+ const detail =
360
+ fileOperationCardData?.summary ??
361
+ summarizeToolArgs(invocation.parsedArgs ?? invocation.args);
362
+ const input = fileOperationCardData
363
+ ? undefined
364
+ : buildToolInvocationInput(invocation.args, invocation.parsedArgs);
300
365
  const rawResult =
301
366
  typeof invocation.error === "string" && invocation.error.trim()
302
367
  ? invocation.error.trim()
303
368
  : invocation.result != null
304
369
  ? stringifyUnknown(invocation.result).trim()
305
370
  : "";
371
+ const shouldHideStructuredTerminalJson =
372
+ !invocation.error && isTerminalResultRecord(invocation.result);
373
+ const shouldShowRawResult =
374
+ (!fileOperationCardData?.fileOperation || Boolean(invocation.error)) &&
375
+ !shouldHideStructuredTerminalJson;
306
376
  const card: ToolCardViewSource = {
307
377
  kind: statusView.kind,
308
378
  name: invocation.toolName,
309
379
  detail,
310
- text: rawResult || undefined,
380
+ ...(input ? { input } : {}),
381
+ text: shouldShowRawResult && rawResult ? rawResult : undefined,
382
+ outputData: invocation.result,
311
383
  callId: invocation.toolCallId || undefined,
312
384
  hasResult: statusView.hasResult,
313
385
  statusTone: statusView.statusTone,
314
386
  statusLabel: statusView.statusLabel,
387
+ ...(fileOperationCardData?.fileOperation
388
+ ? { fileOperation: fileOperationCardData.fileOperation }
389
+ : {}),
315
390
  };
316
391
  return {
317
392
  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,311 @@ 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
+ const editLines =
492
+ adapted[0]?.parts[0]?.type === "tool-card"
493
+ ? (adapted[0].parts[0].card.fileOperation?.blocks[0]?.lines ?? [])
494
+ : [];
495
+
496
+ expect(adapted[0]?.parts[0]).toMatchObject({
497
+ type: "tool-card",
498
+ card: {
499
+ toolName: "edit_file",
500
+ summary: "src/app.ts",
501
+ statusTone: "running",
502
+ fileOperation: {
503
+ blocks: [
504
+ {
505
+ path: "src/app.ts",
506
+ lines: [
507
+ {
508
+ kind: "remove",
509
+ text: "const color = 'red';",
510
+ },
511
+ {
512
+ kind: "add",
513
+ text: "const color = 'blue';",
514
+ },
515
+ ],
516
+ },
517
+ ],
518
+ },
519
+ },
520
+ });
521
+ expect(editLines[0]).not.toHaveProperty("oldLineNumber");
522
+ expect(editLines[1]).not.toHaveProperty("newLineNumber");
523
+ });
524
+
525
+ it("uses structured edit-file result line numbers after the tool finishes", () => {
526
+ const adapted = adapt([
527
+ {
528
+ id: "assistant-edit-result",
529
+ role: "assistant",
530
+ parts: [
531
+ {
532
+ type: "tool-invocation",
533
+ toolInvocation: {
534
+ status: ToolInvocationStatus.RESULT,
535
+ toolCallId: "edit-result-1",
536
+ toolName: "edit_file",
537
+ args: JSON.stringify({
538
+ path: "src/app.ts",
539
+ oldText: "const color = 'red';",
540
+ newText: "const color = 'blue';",
541
+ }),
542
+ parsedArgs: {
543
+ path: "src/app.ts",
544
+ oldText: "const color = 'red';",
545
+ newText: "const color = 'blue';",
546
+ },
547
+ result: {
548
+ path: "src/app.ts",
549
+ oldStartLine: 27,
550
+ newStartLine: 27,
551
+ message: "Edited src/app.ts",
552
+ },
553
+ },
554
+ },
555
+ ],
556
+ },
557
+ ] as unknown as ChatMessageSource[]);
558
+
559
+ expect(adapted[0]?.parts[0]).toMatchObject({
560
+ type: "tool-card",
561
+ card: {
562
+ toolName: "edit_file",
563
+ summary: "src/app.ts",
564
+ statusTone: "success",
565
+ fileOperation: {
566
+ blocks: [
567
+ {
568
+ path: "src/app.ts",
569
+ lines: [
570
+ {
571
+ kind: "remove",
572
+ text: "const color = 'red';",
573
+ oldLineNumber: 27,
574
+ },
575
+ {
576
+ kind: "add",
577
+ text: "const color = 'blue';",
578
+ newLineNumber: 27,
579
+ },
580
+ ],
581
+ },
582
+ ],
583
+ },
584
+ },
585
+ });
586
+ });
587
+
588
+ it("builds write-file previews from partial native args before the JSON is complete", () => {
589
+ const adapted = adapt([
590
+ {
591
+ id: "assistant-write-preview",
592
+ role: "assistant",
593
+ parts: [
594
+ {
595
+ type: "tool-invocation",
596
+ toolInvocation: {
597
+ status: ToolInvocationStatus.PARTIAL_CALL,
598
+ toolCallId: "write-call-1",
599
+ toolName: "write_file",
600
+ args: '{"path":"games/snake.html","content":"<!DOCTYPE html>\\n<canvas id=\\"game\\"></canvas>\\n<script>const score = 1;',
601
+ },
602
+ },
603
+ ],
604
+ },
605
+ ] as unknown as ChatMessageSource[]);
606
+
607
+ expect(adapted[0]?.parts[0]).toMatchObject({
608
+ type: "tool-card",
609
+ card: {
610
+ toolName: "write_file",
611
+ summary: "games/snake.html",
612
+ statusTone: "running",
613
+ statusLabel: "Running",
614
+ fileOperation: {
615
+ blocks: [
616
+ {
617
+ display: "preview",
618
+ path: "games/snake.html",
619
+ lines: expect.arrayContaining([
620
+ expect.objectContaining({
621
+ kind: "add",
622
+ text: "<!DOCTYPE html>",
623
+ }),
624
+ expect.objectContaining({
625
+ kind: "add",
626
+ text: '<canvas id="game"></canvas>',
627
+ }),
628
+ ]),
629
+ },
630
+ ],
631
+ },
632
+ },
633
+ });
634
+ });
635
+
636
+ it("keeps completed write-file cards in preview mode instead of falling back to raw byte summaries", () => {
637
+ const adapted = adapt([
638
+ {
639
+ id: "assistant-write-result",
640
+ role: "assistant",
641
+ parts: [
642
+ {
643
+ type: "tool-invocation",
644
+ toolInvocation: {
645
+ status: ToolInvocationStatus.RESULT,
646
+ toolCallId: "write-result-1",
647
+ toolName: "write_file",
648
+ args: JSON.stringify({
649
+ path: "games/snake.html",
650
+ content: '<!DOCTYPE html>\n<canvas id="game"></canvas>',
651
+ }),
652
+ result: "Wrote 3906 bytes to games/snake.html",
653
+ },
654
+ },
655
+ ],
656
+ },
657
+ ] as unknown as ChatMessageSource[]);
658
+
659
+ expect(adapted[0]?.parts[0]).toMatchObject({
660
+ type: "tool-card",
661
+ card: {
662
+ toolName: "write_file",
663
+ summary: "games/snake.html",
664
+ statusTone: "success",
665
+ fileOperation: {
666
+ blocks: [
667
+ {
668
+ display: "preview",
669
+ path: "games/snake.html",
670
+ lines: [
671
+ {
672
+ kind: "add",
673
+ text: "<!DOCTYPE html>",
674
+ newLineNumber: 1,
675
+ },
676
+ {
677
+ kind: "add",
678
+ text: '<canvas id="game"></canvas>',
679
+ newLineNumber: 2,
680
+ },
681
+ ],
682
+ },
683
+ ],
684
+ },
685
+ },
686
+ });
687
+ expect(adapted[0]?.parts[0]).not.toMatchObject({
688
+ type: "tool-card",
689
+ card: {
690
+ output: "Wrote 3906 bytes to games/snake.html",
691
+ },
692
+ });
693
+ });
694
+
695
+ it("renders codex file_change results as structured diff previews", () => {
696
+ const adapted = adapt([
697
+ {
698
+ id: "assistant-file-change",
699
+ role: "assistant",
700
+ parts: [
701
+ {
702
+ type: "tool-invocation",
703
+ toolInvocation: {
704
+ status: ToolInvocationStatus.RESULT,
705
+ toolCallId: "file-change-1",
706
+ toolName: "file_change",
707
+ args: JSON.stringify({
708
+ changes: [
709
+ {
710
+ path: "src/main.ts",
711
+ diff: [
712
+ "--- a/src/main.ts",
713
+ "+++ b/src/main.ts",
714
+ "@@ -109,1 +109,1 @@",
715
+ "-console.log('old');",
716
+ "+console.log('new');",
717
+ ].join("\n"),
718
+ },
719
+ ],
720
+ }),
721
+ result: {
722
+ status: "completed",
723
+ changes: [
724
+ {
725
+ path: "src/main.ts",
726
+ diff: [
727
+ "--- a/src/main.ts",
728
+ "+++ b/src/main.ts",
729
+ "@@ -109,1 +109,1 @@",
730
+ "-console.log('old');",
731
+ "+console.log('new');",
732
+ ].join("\n"),
733
+ },
734
+ ],
735
+ },
736
+ },
737
+ },
738
+ ],
739
+ },
740
+ ] as unknown as ChatMessageSource[]);
741
+
742
+ expect(adapted[0]?.parts[0]).toMatchObject({
743
+ type: "tool-card",
744
+ card: {
745
+ toolName: "file_change",
746
+ summary: "src/main.ts",
747
+ statusTone: "success",
748
+ fileOperation: {
749
+ blocks: [
750
+ {
751
+ path: "src/main.ts",
752
+ lines: [
753
+ {
754
+ kind: "remove",
755
+ text: "console.log('old');",
756
+ oldLineNumber: 109,
757
+ },
758
+ {
759
+ kind: "add",
760
+ text: "console.log('new');",
761
+ newLineNumber: 109,
762
+ },
763
+ ],
764
+ },
765
+ ],
766
+ },
767
+ },
768
+ });
769
+ });