@nextclaw/ui 0.11.1 → 0.11.2

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 (36) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/assets/{ChannelsList-CVPqrxns.js → ChannelsList-CKl1Zg8f.js} +4 -4
  3. package/dist/assets/{ChatPage-BO1VUrAY.js → ChatPage-BJgO27mk.js} +25 -25
  4. package/dist/assets/{DocBrowser-FBwg8iji.js → DocBrowser-DYRBs4-z.js} +1 -1
  5. package/dist/assets/{LogoBadge-BCmJfRT8.js → LogoBadge-33Qlv3Hg.js} +1 -1
  6. package/dist/assets/{MarketplacePage-DWxXUOCx.js → MarketplacePage-B8BZVtjV.js} +2 -2
  7. package/dist/assets/{McpMarketplacePage-Bth9X_hu.js → McpMarketplacePage-BRuE5fJJ.js} +1 -1
  8. package/dist/assets/{ModelConfig-PkSp_ioc.js → ModelConfig-BiFblwO-.js} +1 -1
  9. package/dist/assets/{ProvidersList-DVDge8wa.js → ProvidersList-9goRgHE4.js} +1 -1
  10. package/dist/assets/{RemoteAccessPage-BVkzfEaL.js → RemoteAccessPage-5vCxZPS6.js} +1 -1
  11. package/dist/assets/{RuntimeConfig-ByJs3khh.js → RuntimeConfig-BmDFHBdW.js} +1 -1
  12. package/dist/assets/{SearchConfig-KZUAqYJN.js → SearchConfig-CJx5CKwG.js} +1 -1
  13. package/dist/assets/{SecretsConfig-qwB_Y_Ka.js → SecretsConfig-B91efXoK.js} +2 -2
  14. package/dist/assets/{SessionsConfig-CGCl4UTr.js → SessionsConfig-CbFPVmx3.js} +2 -2
  15. package/dist/assets/index-BtAuUyww.css +1 -0
  16. package/dist/assets/index-COJomMe9.js +8 -0
  17. package/dist/assets/{label-7JEFhkur.js → label-BnSDpjhL.js} +1 -1
  18. package/dist/assets/{ncp-session-adapter-BOqhkrc-.js → ncp-session-adapter-w8ZHprab.js} +1 -1
  19. package/dist/assets/{page-layout-B7q511TE.js → page-layout-B1RIu5-r.js} +1 -1
  20. package/dist/assets/{popover-CywJGmPr.js → popover-ChzbCIfO.js} +1 -1
  21. package/dist/assets/{security-config-zi2UxN5r.js → security-config-eYa6Ovfa.js} +1 -1
  22. package/dist/assets/skeleton-D4Eyop0R.js +1 -0
  23. package/dist/assets/{status-dot-BilwNdTT.js → status-dot-CrCw5tkJ.js} +1 -1
  24. package/dist/assets/{switch-BLp2Pno1.js → switch-C3vVTpfU.js} +1 -1
  25. package/dist/assets/{tabs-custom-CgIdQMGC.js → tabs-custom-Ilrgt6n1.js} +1 -1
  26. package/dist/assets/{useConfirmDialog-BitswAkv.js → useConfirmDialog-BeaFLDO8.js} +1 -1
  27. package/dist/assets/{vendor-D_JxmsLV.js → vendor-waGu-koL.js} +84 -69
  28. package/dist/index.html +3 -3
  29. package/package.json +4 -4
  30. package/src/components/chat/adapters/chat-message.adapter.test.ts +71 -4
  31. package/src/components/chat/adapters/chat-message.adapter.ts +195 -78
  32. package/src/components/chat/containers/chat-message-list.container.tsx +7 -0
  33. package/src/lib/i18n.chat.ts +7 -0
  34. package/dist/assets/index-CrilScMo.css +0 -1
  35. package/dist/assets/index-D41ntvb7.js +0 -8
  36. package/dist/assets/skeleton-qUJZQ03S.js +0 -1
package/dist/index.html CHANGED
@@ -6,9 +6,9 @@
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-D41ntvb7.js"></script>
10
- <link rel="modulepreload" crossorigin href="/assets/vendor-D_JxmsLV.js">
11
- <link rel="stylesheet" crossorigin href="/assets/index-CrilScMo.css">
9
+ <script type="module" crossorigin src="/assets/index-COJomMe9.js"></script>
10
+ <link rel="modulepreload" crossorigin href="/assets/vendor-waGu-koL.js">
11
+ <link rel="stylesheet" crossorigin href="/assets/index-BtAuUyww.css">
12
12
  </head>
13
13
 
14
14
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextclaw/ui",
3
- "version": "0.11.1",
3
+ "version": "0.11.2",
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/agent-chat": "0.1.3",
32
31
  "@nextclaw/ncp-http-agent-client": "0.3.4",
33
- "@nextclaw/agent-chat-ui": "0.2.5",
32
+ "@nextclaw/ncp-react": "0.4.1",
33
+ "@nextclaw/agent-chat": "0.1.3",
34
34
  "@nextclaw/ncp": "0.4.0",
35
- "@nextclaw/ncp-react": "0.4.1"
35
+ "@nextclaw/agent-chat-ui": "0.2.6"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@testing-library/react": "^16.3.0",
@@ -17,8 +17,15 @@ const defaultTexts = {
17
17
  reasoningLabel: "Reasoning",
18
18
  toolCallLabel: "Tool Call",
19
19
  toolResultLabel: "Tool Result",
20
+ toolInputLabel: "Input Summary",
21
+ toolCallIdLabel: "Call ID",
20
22
  toolNoOutputLabel: "No output",
21
23
  toolOutputLabel: "View Output",
24
+ toolStatusPreparingLabel: "Preparing",
25
+ toolStatusRunningLabel: "Running",
26
+ toolStatusCompletedLabel: "Completed",
27
+ toolStatusFailedLabel: "Failed",
28
+ toolStatusCancelledLabel: "Cancelled",
22
29
  imageAttachmentLabel: "Image attachment",
23
30
  fileAttachmentLabel: "File attachment",
24
31
  unknownPartLabel: "Unknown Part",
@@ -66,9 +73,7 @@ it("maps markdown, reasoning, and tool parts into UI view models", () => {
66
73
 
67
74
  expect(adapted).toHaveLength(1);
68
75
  expect(adapted[0]?.roleLabel).toBe("Assistant");
69
- expect(adapted[0]?.timestampLabel).toBe(
70
- "formatted:2026-03-17T10:00:00.000Z",
71
- );
76
+ expect(adapted[0]?.timestampLabel).toBe("formatted:2026-03-17T10:00:00.000Z");
72
77
  expect(adapted[0]?.parts.map((part) => part.type)).toEqual([
73
78
  "markdown",
74
79
  "reasoning",
@@ -82,12 +87,67 @@ it("maps markdown, reasoning, and tool parts into UI view models", () => {
82
87
  expect(adapted[0]?.parts[2]).toMatchObject({
83
88
  type: "tool-card",
84
89
  card: {
90
+ callId: "call-1",
91
+ callIdLabel: "Call ID",
92
+ inputLabel: "Input Summary",
93
+ statusLabel: "Completed",
94
+ statusTone: "success",
85
95
  titleLabel: "Tool Result",
86
96
  outputLabel: "View Output",
87
97
  },
88
98
  });
89
99
  });
90
100
 
101
+ it("maps tool lifecycle statuses into visible card state feedback", () => {
102
+ const adapted = adapt([
103
+ {
104
+ id: "assistant-tool-statuses",
105
+ role: "assistant",
106
+ parts: [
107
+ {
108
+ type: "tool-invocation",
109
+ toolInvocation: {
110
+ status: ToolInvocationStatus.PARTIAL_CALL,
111
+ toolCallId: "call-prep",
112
+ toolName: "web_search",
113
+ args: '{"q":"latest"}',
114
+ },
115
+ },
116
+ {
117
+ type: "tool-invocation",
118
+ toolInvocation: {
119
+ status: ToolInvocationStatus.ERROR,
120
+ toolCallId: "call-error",
121
+ toolName: "exec_command",
122
+ args: '{"cmd":"exit 1"}',
123
+ error: "Command failed",
124
+ },
125
+ },
126
+ ],
127
+ },
128
+ ] as unknown as ChatMessageSource[]);
129
+
130
+ expect(adapted[0]?.parts[0]).toMatchObject({
131
+ type: "tool-card",
132
+ card: {
133
+ statusTone: "running",
134
+ statusLabel: "Preparing",
135
+ titleLabel: "Tool Call",
136
+ callId: "call-prep",
137
+ },
138
+ });
139
+ expect(adapted[0]?.parts[1]).toMatchObject({
140
+ type: "tool-card",
141
+ card: {
142
+ statusTone: "error",
143
+ statusLabel: "Failed",
144
+ titleLabel: "Tool Result",
145
+ output: "Command failed",
146
+ callId: "call-error",
147
+ },
148
+ });
149
+ });
150
+
91
151
  it("maps non-standard roles back to the generic message role", () => {
92
152
  const adapted = adapt([
93
153
  {
@@ -149,6 +209,7 @@ it("maps file parts into previewable attachment view models", () => {
149
209
  type: "file",
150
210
  mimeType: "image/png",
151
211
  data: "ZmFrZS1pbWFnZQ==",
212
+ sizeBytes: 4096,
152
213
  },
153
214
  ],
154
215
  },
@@ -160,6 +221,7 @@ it("maps file parts into previewable attachment view models", () => {
160
221
  label: "Image attachment",
161
222
  mimeType: "image/png",
162
223
  dataUrl: "data:image/png;base64,ZmFrZS1pbWFnZQ==",
224
+ sizeBytes: 4096,
163
225
  isImage: true,
164
226
  },
165
227
  });
@@ -176,6 +238,7 @@ it("keeps named non-image files as downloadable attachments", () => {
176
238
  name: "spec.pdf",
177
239
  mimeType: "application/pdf",
178
240
  data: "cGRm",
241
+ sizeBytes: 2048,
179
242
  },
180
243
  ],
181
244
  },
@@ -187,6 +250,7 @@ it("keeps named non-image files as downloadable attachments", () => {
187
250
  label: "spec.pdf",
188
251
  mimeType: "application/pdf",
189
252
  dataUrl: "data:application/pdf;base64,cGRm",
253
+ sizeBytes: 2048,
190
254
  isImage: false,
191
255
  },
192
256
  });
@@ -212,6 +276,7 @@ it("renders asset tool results as previewable files", () => {
212
276
  name: "output.png",
213
277
  mimeType: "image/png",
214
278
  url: "/api/ncp/assets/content?uri=asset%3A%2F%2Fstore%2F2026%2F03%2F27%2Fasset_1",
279
+ sizeBytes: 5120,
215
280
  },
216
281
  },
217
282
  },
@@ -225,7 +290,9 @@ it("renders asset tool results as previewable files", () => {
225
290
  file: {
226
291
  label: "output.png",
227
292
  mimeType: "image/png",
228
- dataUrl: "/api/ncp/assets/content?uri=asset%3A%2F%2Fstore%2F2026%2F03%2F27%2Fasset_1",
293
+ dataUrl:
294
+ "/api/ncp/assets/content?uri=asset%3A%2F%2Fstore%2F2026%2F03%2F27%2Fasset_1",
295
+ sizeBytes: 5120,
229
296
  isImage: true,
230
297
  },
231
298
  });
@@ -1,32 +1,33 @@
1
1
  import {
2
2
  stringifyUnknown,
3
3
  summarizeToolArgs,
4
- type ToolCard
5
- } from '@/lib/chat-message';
4
+ type ToolCard,
5
+ } from "@/lib/chat-message";
6
6
  import type {
7
7
  ChatMessageRole,
8
8
  ChatMessageViewModel,
9
- ChatToolPartViewModel
10
- } from '@nextclaw/agent-chat-ui';
9
+ ChatToolPartViewModel,
10
+ } from "@nextclaw/agent-chat-ui";
11
11
 
12
12
  export type ChatMessagePartSource =
13
13
  | {
14
- type: 'text';
14
+ type: "text";
15
15
  text: string;
16
16
  }
17
17
  | {
18
- type: 'file';
18
+ type: "file";
19
19
  mimeType: string;
20
20
  data: string;
21
21
  url?: string;
22
22
  name?: string;
23
+ sizeBytes?: number;
23
24
  }
24
25
  | {
25
- type: 'reasoning';
26
+ type: "reasoning";
26
27
  reasoning: string;
27
28
  }
28
29
  | {
29
- type: 'tool-invocation';
30
+ type: "tool-invocation";
30
31
  toolInvocation: {
31
32
  status?: string;
32
33
  toolName: string;
@@ -34,6 +35,7 @@ export type ChatMessagePartSource =
34
35
  parsedArgs?: unknown;
35
36
  result?: unknown;
36
37
  error?: string;
38
+ cancelled?: boolean;
37
39
  toolCallId?: string;
38
40
  };
39
41
  }
@@ -63,8 +65,15 @@ export type ChatMessageAdapterTexts = {
63
65
  reasoningLabel: string;
64
66
  toolCallLabel: string;
65
67
  toolResultLabel: string;
68
+ toolInputLabel: string;
69
+ toolCallIdLabel: string;
66
70
  toolNoOutputLabel: string;
67
71
  toolOutputLabel: string;
72
+ toolStatusPreparingLabel: string;
73
+ toolStatusRunningLabel: string;
74
+ toolStatusCompletedLabel: string;
75
+ toolStatusFailedLabel: string;
76
+ toolStatusCancelledLabel: string;
68
77
  imageAttachmentLabel: string;
69
78
  fileAttachmentLabel: string;
70
79
  unknownPartLabel: string;
@@ -73,91 +82,114 @@ export type ChatMessageAdapterTexts = {
73
82
  const INVISIBLE_ONLY_TEXT_PATTERN = /\u200B|\u200C|\u200D|\u2060|\uFEFF/g;
74
83
 
75
84
  function isRecord(value: unknown): value is Record<string, unknown> {
76
- return typeof value === 'object' && value !== null;
85
+ return typeof value === "object" && value !== null;
77
86
  }
78
87
 
79
88
  function readOptionalString(value: unknown): string | null {
80
- if (typeof value !== 'string') {
89
+ if (typeof value !== "string") {
81
90
  return null;
82
91
  }
83
92
  const trimmed = value.trim();
84
93
  return trimmed.length > 0 ? trimmed : null;
85
94
  }
86
95
 
96
+ function readOptionalNumber(value: unknown): number | null {
97
+ if (typeof value === "number" && Number.isFinite(value) && value >= 0) {
98
+ return value;
99
+ }
100
+ if (typeof value !== "string") {
101
+ return null;
102
+ }
103
+ const trimmed = value.trim();
104
+ if (!trimmed) {
105
+ return null;
106
+ }
107
+ const parsed = Number(trimmed);
108
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : null;
109
+ }
110
+
87
111
  function extractAssetFileView(
88
112
  value: unknown,
89
- texts: ChatMessageAdapterTexts
90
- ):
91
- | {
92
- type: 'file';
93
- file: {
94
- label: string;
95
- mimeType: string;
96
- dataUrl: string;
97
- isImage: boolean;
98
- };
99
- }
100
- | null {
113
+ texts: ChatMessageAdapterTexts,
114
+ ): {
115
+ type: "file";
116
+ file: {
117
+ label: string;
118
+ mimeType: string;
119
+ dataUrl: string;
120
+ sizeBytes?: number;
121
+ isImage: boolean;
122
+ };
123
+ } | null {
101
124
  if (!isRecord(value)) {
102
125
  return null;
103
126
  }
104
127
  const assetCandidate = isRecord(value.asset)
105
128
  ? value.asset
106
- : Array.isArray(value.assets) && value.assets.length > 0 && isRecord(value.assets[0])
129
+ : Array.isArray(value.assets) &&
130
+ value.assets.length > 0 &&
131
+ isRecord(value.assets[0])
107
132
  ? value.assets[0]
108
133
  : null;
109
134
  if (!assetCandidate) {
110
135
  return null;
111
136
  }
112
137
  const url = readOptionalString(assetCandidate.url);
113
- const mimeType = readOptionalString(assetCandidate.mimeType) ?? 'application/octet-stream';
138
+ const mimeType =
139
+ readOptionalString(assetCandidate.mimeType) ?? "application/octet-stream";
140
+ const sizeBytes = readOptionalNumber(assetCandidate.sizeBytes);
114
141
  if (!url) {
115
142
  return null;
116
143
  }
117
144
  const label =
118
145
  readOptionalString(assetCandidate.name) ??
119
- (mimeType.startsWith('image/') ? texts.imageAttachmentLabel : texts.fileAttachmentLabel);
146
+ (mimeType.startsWith("image/")
147
+ ? texts.imageAttachmentLabel
148
+ : texts.fileAttachmentLabel);
120
149
  return {
121
- type: 'file',
150
+ type: "file",
122
151
  file: {
123
152
  label,
124
153
  mimeType,
125
154
  dataUrl: url,
126
- isImage: mimeType.startsWith('image/')
127
- }
155
+ ...(sizeBytes != null ? { sizeBytes } : {}),
156
+ isImage: mimeType.startsWith("image/"),
157
+ },
128
158
  };
129
159
  }
130
160
 
131
- function isTextPart(part: ChatMessagePartSource): part is Extract<ChatMessagePartSource, { type: 'text' }> {
132
- return part.type === 'text' && typeof part.text === 'string';
161
+ function isTextPart(
162
+ part: ChatMessagePartSource,
163
+ ): part is Extract<ChatMessagePartSource, { type: "text" }> {
164
+ return part.type === "text" && typeof part.text === "string";
133
165
  }
134
166
 
135
167
  function isReasoningPart(
136
- part: ChatMessagePartSource
137
- ): part is Extract<ChatMessagePartSource, { type: 'reasoning' }> {
138
- return part.type === 'reasoning' && typeof part.reasoning === 'string';
168
+ part: ChatMessagePartSource,
169
+ ): part is Extract<ChatMessagePartSource, { type: "reasoning" }> {
170
+ return part.type === "reasoning" && typeof part.reasoning === "string";
139
171
  }
140
172
 
141
173
  function isFilePart(
142
- part: ChatMessagePartSource
143
- ): part is Extract<ChatMessagePartSource, { type: 'file' }> {
174
+ part: ChatMessagePartSource,
175
+ ): part is Extract<ChatMessagePartSource, { type: "file" }> {
144
176
  return (
145
- part.type === 'file' &&
146
- typeof part.mimeType === 'string' &&
147
- typeof part.data === 'string'
177
+ part.type === "file" &&
178
+ typeof part.mimeType === "string" &&
179
+ typeof part.data === "string"
148
180
  );
149
181
  }
150
182
 
151
183
  function isToolInvocationPart(
152
- part: ChatMessagePartSource
153
- ): part is Extract<ChatMessagePartSource, { type: 'tool-invocation' }> {
154
- if (part.type !== 'tool-invocation') {
184
+ part: ChatMessagePartSource,
185
+ ): part is Extract<ChatMessagePartSource, { type: "tool-invocation" }> {
186
+ if (part.type !== "tool-invocation") {
155
187
  return false;
156
188
  }
157
189
  if (!isRecord(part.toolInvocation)) {
158
190
  return false;
159
191
  }
160
- return typeof part.toolInvocation.toolName === 'string';
192
+ return typeof part.toolInvocation.toolName === "string";
161
193
  }
162
194
 
163
195
  function resolveMessageTimestamp(message: ChatMessageSource): string {
@@ -170,33 +202,38 @@ function resolveMessageTimestamp(message: ChatMessageSource): string {
170
202
 
171
203
  function resolveRoleLabel(
172
204
  role: string,
173
- texts: ChatMessageAdapterTexts['roleLabels']
205
+ texts: ChatMessageAdapterTexts["roleLabels"],
174
206
  ): string {
175
- if (role === 'user') {
207
+ if (role === "user") {
176
208
  return texts.user;
177
209
  }
178
- if (role === 'assistant') {
210
+ if (role === "assistant") {
179
211
  return texts.assistant;
180
212
  }
181
- if (role === 'tool') {
213
+ if (role === "tool") {
182
214
  return texts.tool;
183
215
  }
184
- if (role === 'system') {
216
+ if (role === "system") {
185
217
  return texts.system;
186
218
  }
187
219
  return texts.fallback;
188
220
  }
189
221
 
190
222
  function resolveUiRole(role: string): ChatMessageRole {
191
- if (role === 'user' || role === 'assistant' || role === 'tool' || role === 'system') {
223
+ if (
224
+ role === "user" ||
225
+ role === "assistant" ||
226
+ role === "tool" ||
227
+ role === "system"
228
+ ) {
192
229
  return role;
193
230
  }
194
- return 'message';
231
+ return "message";
195
232
  }
196
233
 
197
234
  function buildToolCard(
198
- toolCard: ToolCard,
199
- texts: ChatMessageAdapterTexts
235
+ toolCard: ToolCardViewSource,
236
+ texts: ChatMessageAdapterTexts,
200
237
  ): ChatToolPartViewModel {
201
238
  return {
202
239
  kind: toolCard.kind,
@@ -204,9 +241,75 @@ function buildToolCard(
204
241
  summary: toolCard.detail,
205
242
  output: toolCard.text,
206
243
  hasResult: Boolean(toolCard.hasResult),
207
- titleLabel: toolCard.kind === 'call' ? texts.toolCallLabel : texts.toolResultLabel,
244
+ statusTone: toolCard.statusTone,
245
+ statusLabel: toolCard.statusLabel,
246
+ titleLabel:
247
+ toolCard.kind === "call" ? texts.toolCallLabel : texts.toolResultLabel,
248
+ inputLabel: texts.toolInputLabel,
208
249
  outputLabel: texts.toolOutputLabel,
209
- emptyLabel: texts.toolNoOutputLabel
250
+ emptyLabel: texts.toolNoOutputLabel,
251
+ callIdLabel: texts.toolCallIdLabel,
252
+ callId: toolCard.callId,
253
+ };
254
+ }
255
+
256
+ type ToolCardViewSource = ToolCard & {
257
+ statusTone: ChatToolPartViewModel["statusTone"];
258
+ statusLabel: string;
259
+ };
260
+
261
+ function resolveToolCardStatus(params: {
262
+ status?: string;
263
+ error?: string;
264
+ cancelled?: boolean;
265
+ result?: unknown;
266
+ texts: ChatMessageAdapterTexts;
267
+ }): Pick<
268
+ ChatToolPartViewModel,
269
+ "kind" | "hasResult" | "statusTone" | "statusLabel"
270
+ > {
271
+ const rawStatus =
272
+ typeof params.status === "string" ? params.status.trim().toLowerCase() : "";
273
+ const hasError =
274
+ typeof params.error === "string" && params.error.trim().length > 0;
275
+ const isCancelled = params.cancelled === true || rawStatus === "cancelled";
276
+ if (isCancelled) {
277
+ return {
278
+ kind: "result",
279
+ hasResult: true,
280
+ statusTone: "cancelled",
281
+ statusLabel: params.texts.toolStatusCancelledLabel,
282
+ };
283
+ }
284
+ if (hasError || rawStatus === "error") {
285
+ return {
286
+ kind: "result",
287
+ hasResult: true,
288
+ statusTone: "error",
289
+ statusLabel: params.texts.toolStatusFailedLabel,
290
+ };
291
+ }
292
+ if (rawStatus === "result" || params.result != null) {
293
+ return {
294
+ kind: "result",
295
+ hasResult: true,
296
+ statusTone: "success",
297
+ statusLabel: params.texts.toolStatusCompletedLabel,
298
+ };
299
+ }
300
+ if (rawStatus === "partial-call") {
301
+ return {
302
+ kind: "call",
303
+ hasResult: false,
304
+ statusTone: "running",
305
+ statusLabel: params.texts.toolStatusPreparingLabel,
306
+ };
307
+ }
308
+ return {
309
+ kind: "call",
310
+ hasResult: false,
311
+ statusTone: "running",
312
+ statusLabel: params.texts.toolStatusRunningLabel,
210
313
  };
211
314
  }
212
315
 
@@ -238,8 +341,8 @@ export function adaptChatMessages(params: {
238
341
  return null;
239
342
  }
240
343
  return {
241
- type: 'markdown' as const,
242
- text
344
+ type: "markdown" as const,
345
+ text,
243
346
  };
244
347
  }
245
348
  if (isReasoningPart(part)) {
@@ -248,66 +351,80 @@ export function adaptChatMessages(params: {
248
351
  return null;
249
352
  }
250
353
  return {
251
- type: 'reasoning' as const,
354
+ type: "reasoning" as const,
252
355
  text,
253
- label: params.texts.reasoningLabel
356
+ label: params.texts.reasoningLabel,
254
357
  };
255
358
  }
256
359
  if (isFilePart(part)) {
257
- const isImage = part.mimeType.startsWith('image/');
360
+ const isImage = part.mimeType.startsWith("image/");
361
+ const sizeBytes = readOptionalNumber(part.sizeBytes);
258
362
  return {
259
- type: 'file' as const,
363
+ type: "file" as const,
260
364
  file: {
261
365
  label:
262
- typeof part.name === 'string' && part.name.trim()
366
+ typeof part.name === "string" && part.name.trim()
263
367
  ? part.name.trim()
264
368
  : isImage
265
369
  ? params.texts.imageAttachmentLabel
266
370
  : params.texts.fileAttachmentLabel,
267
371
  mimeType: part.mimeType,
268
372
  dataUrl:
269
- typeof part.url === 'string' && part.url.trim().length > 0
373
+ typeof part.url === "string" && part.url.trim().length > 0
270
374
  ? part.url.trim()
271
375
  : `data:${part.mimeType};base64,${part.data}`,
272
- isImage
273
- }
376
+ ...(sizeBytes != null ? { sizeBytes } : {}),
377
+ isImage,
378
+ },
274
379
  };
275
380
  }
276
381
  if (isToolInvocationPart(part)) {
277
382
  const invocation = part.toolInvocation;
278
- const assetFileView = extractAssetFileView(invocation.result, params.texts);
383
+ const assetFileView = extractAssetFileView(
384
+ invocation.result,
385
+ params.texts,
386
+ );
279
387
  if (assetFileView) {
280
388
  return assetFileView;
281
389
  }
282
- const detail = summarizeToolArgs(invocation.parsedArgs ?? invocation.args);
390
+ const statusView = resolveToolCardStatus({
391
+ status: invocation.status,
392
+ error: invocation.error,
393
+ cancelled: invocation.cancelled,
394
+ result: invocation.result,
395
+ texts: params.texts,
396
+ });
397
+ const detail = summarizeToolArgs(
398
+ invocation.parsedArgs ?? invocation.args,
399
+ );
283
400
  const rawResult =
284
- typeof invocation.error === 'string' && invocation.error.trim()
401
+ typeof invocation.error === "string" && invocation.error.trim()
285
402
  ? invocation.error.trim()
286
403
  : invocation.result != null
287
404
  ? stringifyUnknown(invocation.result).trim()
288
- : '';
289
- const hasResult =
290
- invocation.status === 'result' || invocation.status === 'error' || invocation.status === 'cancelled';
291
- const card: ToolCard = {
292
- kind: hasResult ? 'result' : 'call',
405
+ : "";
406
+ const card: ToolCardViewSource = {
407
+ kind: statusView.kind,
293
408
  name: invocation.toolName,
294
409
  detail,
295
410
  text: rawResult || undefined,
296
411
  callId: invocation.toolCallId || undefined,
297
- hasResult
412
+ hasResult: statusView.hasResult,
413
+ statusTone: statusView.statusTone,
414
+ statusLabel: statusView.statusLabel,
298
415
  };
299
416
  return {
300
- type: 'tool-card' as const,
301
- card: buildToolCard(card, params.texts)
417
+ type: "tool-card" as const,
418
+ card: buildToolCard(card, params.texts),
302
419
  };
303
420
  }
304
421
  return {
305
- type: 'unknown' as const,
422
+ type: "unknown" as const,
306
423
  label: params.texts.unknownPartLabel,
307
- rawType: typeof part.type === 'string' ? part.type : 'unknown',
308
- text: stringifyUnknown(part)
424
+ rawType: typeof part.type === "string" ? part.type : "unknown",
425
+ text: stringifyUnknown(part),
309
426
  };
310
427
  })
311
- .filter((part) => part !== null)
428
+ .filter((part) => part !== null),
312
429
  }));
313
430
  }
@@ -46,8 +46,15 @@ export function ChatMessageListContainer(props: ChatMessageListContainerProps) {
46
46
  reasoningLabel: t("chatReasoning"),
47
47
  toolCallLabel: t("chatToolCall"),
48
48
  toolResultLabel: t("chatToolResult"),
49
+ toolInputLabel: t("chatToolInput"),
50
+ toolCallIdLabel: t("chatToolCallId"),
49
51
  toolNoOutputLabel: t("chatToolNoOutput"),
50
52
  toolOutputLabel: t("chatToolOutput"),
53
+ toolStatusPreparingLabel: t("chatToolStatusPreparing"),
54
+ toolStatusRunningLabel: t("chatToolStatusRunning"),
55
+ toolStatusCompletedLabel: t("chatToolStatusCompleted"),
56
+ toolStatusFailedLabel: t("chatToolStatusFailed"),
57
+ toolStatusCancelledLabel: t("chatToolStatusCancelled"),
51
58
  imageAttachmentLabel: t("chatImageAttachment"),
52
59
  fileAttachmentLabel: t("chatFileAttachment"),
53
60
  unknownPartLabel: t("chatUnknownPart"),
@@ -70,10 +70,17 @@ export const CHAT_LABELS: Record<string, { zh: string; en: string }> = {
70
70
  chatRoleMessage: { zh: '消息', en: 'Message' },
71
71
  chatToolCall: { zh: '工具调用', en: 'Tool Call' },
72
72
  chatToolResult: { zh: '工具结果', en: 'Tool Result' },
73
+ chatToolInput: { zh: '输入摘要', en: 'Input Summary' },
74
+ chatToolCallId: { zh: '调用 ID', en: 'Call ID' },
73
75
  chatToolWorkflow: { zh: '工具工作流', en: 'Tool Workflow' },
74
76
  chatToolWorkflowDetails: { zh: '展开查看参数和结果', en: 'Expand to view params and results' },
75
77
  chatToolOutput: { zh: '查看输出', en: 'View Output' },
76
78
  chatToolNoOutput: { zh: '无输出(执行完成)', en: 'No output (completed)' },
79
+ chatToolStatusPreparing: { zh: '准备中', en: 'Preparing' },
80
+ chatToolStatusRunning: { zh: '执行中', en: 'Running' },
81
+ chatToolStatusCompleted: { zh: '已完成', en: 'Completed' },
82
+ chatToolStatusFailed: { zh: '失败', en: 'Failed' },
83
+ chatToolStatusCancelled: { zh: '已取消', en: 'Cancelled' },
77
84
  chatReasoning: { zh: '推理过程', en: 'Reasoning' },
78
85
  chatImageAttachment: { zh: '图片附件', en: 'Image attachment' },
79
86
  chatFileAttachment: { zh: '文件附件', en: 'File attachment' },