@nextclaw/ui 0.11.0 → 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 (87) hide show
  1. package/CHANGELOG.md +16 -2
  2. package/dist/assets/{ChannelsList-BqsOYnXz.js → ChannelsList-CKl1Zg8f.js} +3 -3
  3. package/dist/assets/ChatPage-BJgO27mk.js +37 -0
  4. package/dist/assets/{DocBrowser-BmL0QXBZ.js → DocBrowser-DYRBs4-z.js} +1 -1
  5. package/dist/assets/{LogoBadge-C1HiPZPf.js → LogoBadge-33Qlv3Hg.js} +1 -1
  6. package/dist/assets/MarketplacePage-B8BZVtjV.js +49 -0
  7. package/dist/assets/{McpMarketplacePage-CLHFnNBd.js → McpMarketplacePage-BRuE5fJJ.js} +2 -2
  8. package/dist/assets/{ModelConfig-LQSR58tc.js → ModelConfig-BiFblwO-.js} +1 -1
  9. package/dist/assets/ProvidersList-9goRgHE4.js +1 -0
  10. package/dist/assets/RemoteAccessPage-5vCxZPS6.js +1 -0
  11. package/dist/assets/RuntimeConfig-BmDFHBdW.js +1 -0
  12. package/dist/assets/{SearchConfig-Chzo_JGs.js → SearchConfig-CJx5CKwG.js} +1 -1
  13. package/dist/assets/{SecretsConfig-CEIbjZYA.js → SecretsConfig-B91efXoK.js} +2 -2
  14. package/dist/assets/SessionsConfig-CbFPVmx3.js +2 -0
  15. package/dist/assets/index-BtAuUyww.css +1 -0
  16. package/dist/assets/index-COJomMe9.js +8 -0
  17. package/dist/assets/{label-GACO2RzW.js → label-BnSDpjhL.js} +1 -1
  18. package/dist/assets/ncp-session-adapter-w8ZHprab.js +1 -0
  19. package/dist/assets/{page-layout-DjXaK3A3.js → page-layout-B1RIu5-r.js} +1 -1
  20. package/dist/assets/popover-ChzbCIfO.js +1 -0
  21. package/dist/assets/security-config-eYa6Ovfa.js +1 -0
  22. package/dist/assets/skeleton-D4Eyop0R.js +1 -0
  23. package/dist/assets/{status-dot-IWEBezqb.js → status-dot-CrCw5tkJ.js} +1 -1
  24. package/dist/assets/{switch-DCHAJSrA.js → switch-C3vVTpfU.js} +1 -1
  25. package/dist/assets/tabs-custom-Ilrgt6n1.js +1 -0
  26. package/dist/assets/useConfirmDialog-BeaFLDO8.js +1 -0
  27. package/dist/assets/{vendor-CNhxtHCf.js → vendor-waGu-koL.js} +101 -86
  28. package/dist/index.html +3 -3
  29. package/package.json +5 -5
  30. package/src/App.test.tsx +42 -10
  31. package/src/App.tsx +5 -40
  32. package/src/api/api-base.test.ts +37 -0
  33. package/src/api/api-base.ts +0 -4
  34. package/src/api/config.ts +2 -270
  35. package/src/api/types.ts +0 -117
  36. package/src/components/chat/ChatPage.tsx +1 -11
  37. package/src/components/chat/ChatSidebar.test.tsx +1 -50
  38. package/src/components/chat/ChatSidebar.tsx +0 -5
  39. package/src/components/chat/README.md +2 -0
  40. package/src/components/chat/adapters/chat-message.adapter.test.ts +71 -4
  41. package/src/components/chat/adapters/chat-message.adapter.ts +195 -78
  42. package/src/components/chat/chat-attachment-upload-limit.test.ts +41 -0
  43. package/src/components/chat/chat-session-display.ts +9 -0
  44. package/src/components/chat/chat-session-label.service.ts +3 -12
  45. package/src/components/chat/chat-session-preference-sync.test.ts +10 -13
  46. package/src/components/chat/chat-stream/types.ts +4 -57
  47. package/src/components/chat/containers/chat-message-list.container.tsx +7 -0
  48. package/src/components/chat/ncp/NcpChatPage.tsx +3 -3
  49. package/src/components/chat/useHydratedNcpAgent.test.tsx +77 -0
  50. package/src/components/config/README.md +2 -0
  51. package/src/components/config/SessionsConfig.tsx +152 -132
  52. package/src/hooks/use-auth.test.ts +3 -3
  53. package/src/hooks/use-auth.ts +16 -4
  54. package/src/hooks/use-realtime-query-bridge.ts +0 -24
  55. package/src/hooks/useConfig.ts +10 -137
  56. package/src/lib/i18n.chat.ts +7 -0
  57. package/src/lib/session-run-status.ts +1 -63
  58. package/src/vite-env.d.ts +1 -0
  59. package/vite.config.ts +4 -4
  60. package/dist/assets/ChatPage-CJBYKR-Y.js +0 -38
  61. package/dist/assets/MarketplacePage-BIRP0NRS.js +0 -49
  62. package/dist/assets/ProvidersList-CwI-mxah.js +0 -1
  63. package/dist/assets/RemoteAccessPage-Cw5BqZb6.js +0 -1
  64. package/dist/assets/RuntimeConfig-DbowSRAb.js +0 -1
  65. package/dist/assets/SessionsConfig-BR8GfGWL.js +0 -2
  66. package/dist/assets/chat-message-CPG7zxRR.js +0 -3
  67. package/dist/assets/index-j6A_-1b6.js +0 -8
  68. package/dist/assets/index-kaPUhd-8.css +0 -1
  69. package/dist/assets/popover-DTaFiTmU.js +0 -1
  70. package/dist/assets/security-config-Dk-yoKvK.js +0 -1
  71. package/dist/assets/skeleton-Dm2xOBSA.js +0 -1
  72. package/dist/assets/tabs-custom-DKSbDSB9.js +0 -1
  73. package/dist/assets/useConfirmDialog-ByJ8A8n7.js +0 -1
  74. package/src/api/config.stream.test.ts +0 -115
  75. package/src/components/chat/chat-chain.test.ts +0 -22
  76. package/src/components/chat/chat-chain.ts +0 -23
  77. package/src/components/chat/chat-page-data.ts +0 -171
  78. package/src/components/chat/chat-page-runtime.ts +0 -190
  79. package/src/components/chat/chat-stream/nextbot-parsers.ts +0 -52
  80. package/src/components/chat/chat-stream/nextbot-runtime-agent.ts +0 -413
  81. package/src/components/chat/chat-stream/stream-event-adapter.ts +0 -98
  82. package/src/components/chat/chat-stream/transport.ts +0 -253
  83. package/src/components/chat/legacy/LegacyChatPage.tsx +0 -223
  84. package/src/components/chat/managers/chat-input.manager.ts +0 -228
  85. package/src/components/chat/managers/chat-thread.manager.ts +0 -87
  86. package/src/components/chat/presenter/chat.presenter.ts +0 -32
  87. package/src/components/chat/useChatRuntimeController.ts +0 -134
@@ -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
  }
@@ -0,0 +1,41 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import {
3
+ DEFAULT_NCP_ATTACHMENT_MAX_BYTES,
4
+ uploadFilesAsNcpDraftAttachments
5
+ } from '../../../../ncp-packages/nextclaw-ncp-react/src/attachments/ncp-attachments.ts';
6
+
7
+ describe('ncp attachment upload limit', () => {
8
+ it('accepts files larger than the previous 10MB cap', async () => {
9
+ expect(DEFAULT_NCP_ATTACHMENT_MAX_BYTES).toBe(200 * 1024 * 1024);
10
+
11
+ const file = new File([new Uint8Array(12 * 1024 * 1024)], 'large-image.png', {
12
+ type: 'image/png'
13
+ });
14
+ const uploadBatch = vi.fn(async (files: File[]) =>
15
+ files.map((entry) => ({
16
+ id: entry.name,
17
+ name: entry.name,
18
+ mimeType: entry.type,
19
+ sizeBytes: entry.size,
20
+ assetUri: `asset://store/${entry.name}`,
21
+ }))
22
+ );
23
+
24
+ const result = await uploadFilesAsNcpDraftAttachments([file], {
25
+ uploadBatch
26
+ });
27
+
28
+ expect(result.rejected).toEqual([]);
29
+ expect(uploadBatch).toHaveBeenCalledOnce();
30
+ expect(uploadBatch).toHaveBeenCalledWith([file]);
31
+ expect(result.attachments).toEqual([
32
+ {
33
+ id: 'large-image.png',
34
+ name: 'large-image.png',
35
+ mimeType: 'image/png',
36
+ sizeBytes: 12 * 1024 * 1024,
37
+ assetUri: 'asset://store/large-image.png',
38
+ }
39
+ ]);
40
+ });
41
+ });
@@ -0,0 +1,9 @@
1
+ import type { SessionEntryView } from '@/api/types';
2
+
3
+ export function sessionDisplayName(session: SessionEntryView): string {
4
+ if (session.label && session.label.trim()) {
5
+ return session.label.trim();
6
+ }
7
+ const chunks = session.key.split(':');
8
+ return chunks[chunks.length - 1] || session.key;
9
+ }
@@ -1,12 +1,9 @@
1
1
  import { useQueryClient } from '@tanstack/react-query';
2
2
  import { toast } from 'sonner';
3
- import { updateSession } from '@/api/config';
4
3
  import { updateNcpSession } from '@/api/ncp-session';
5
- import type { ChatChain } from '@/components/chat/chat-chain';
6
4
  import { t } from '@/lib/i18n';
7
5
 
8
6
  type UpdateChatSessionLabelParams = {
9
- chatChain: ChatChain;
10
7
  sessionKey: string;
11
8
  label: string | null;
12
9
  };
@@ -16,15 +13,9 @@ export function useChatSessionLabelService() {
16
13
 
17
14
  return async (params: UpdateChatSessionLabelParams): Promise<void> => {
18
15
  try {
19
- if (params.chatChain === 'ncp') {
20
- await updateNcpSession(params.sessionKey, { label: params.label });
21
- queryClient.invalidateQueries({ queryKey: ['ncp-sessions'] });
22
- queryClient.invalidateQueries({ queryKey: ['ncp-session-messages', params.sessionKey] });
23
- } else {
24
- await updateSession(params.sessionKey, { label: params.label });
25
- queryClient.invalidateQueries({ queryKey: ['sessions'] });
26
- queryClient.invalidateQueries({ queryKey: ['session-history', params.sessionKey] });
27
- }
16
+ await updateNcpSession(params.sessionKey, { label: params.label });
17
+ queryClient.invalidateQueries({ queryKey: ['ncp-sessions'] });
18
+ queryClient.invalidateQueries({ queryKey: ['ncp-session-messages', params.sessionKey] });
28
19
  toast.success(t('configSavedApplied'));
29
20
  } catch (error) {
30
21
  toast.error(t('configSaveFailed') + ': ' + (error instanceof Error ? error.message : String(error)));
@@ -1,19 +1,16 @@
1
1
  import { afterEach, describe, expect, it, vi } from 'vitest';
2
- import { updateSession } from '@/api/config';
2
+ import { updateNcpSession } from '@/api/ncp-session';
3
3
  import { ChatSessionPreferenceSync } from '@/components/chat/chat-session-preference-sync';
4
4
  import { useChatInputStore } from '@/components/chat/stores/chat-input.store';
5
5
  import { useChatSessionListStore } from '@/components/chat/stores/chat-session-list.store';
6
6
 
7
- vi.mock('@/api/config', () => ({
8
- updateSession: vi.fn(async () => ({
9
- key: 'session-1',
10
- totalMessages: 0,
11
- totalEvents: 0,
12
- sessionType: 'native',
13
- sessionTypeMutable: false,
14
- metadata: {},
15
- messages: [],
16
- events: []
7
+ vi.mock('@/api/ncp-session', () => ({
8
+ updateNcpSession: vi.fn(async () => ({
9
+ sessionId: 'session-1',
10
+ messageCount: 0,
11
+ updatedAt: new Date().toISOString(),
12
+ status: 'idle',
13
+ metadata: {}
17
14
  }))
18
15
  }));
19
16
 
@@ -50,10 +47,10 @@ describe('ChatSessionPreferenceSync', () => {
50
47
  }
51
48
  }));
52
49
 
53
- const sync = new ChatSessionPreferenceSync(updateSession);
50
+ const sync = new ChatSessionPreferenceSync(updateNcpSession);
54
51
  sync.syncSelectedSessionPreferences();
55
52
  await vi.waitFor(() => {
56
- expect(updateSession).toHaveBeenCalledWith('session-1', {
53
+ expect(updateNcpSession).toHaveBeenCalledWith('session-1', {
57
54
  preferredModel: 'openai/gpt-5',
58
55
  preferredThinking: 'high'
59
56
  });
@@ -1,18 +1,9 @@
1
1
  import type { ChatComposerNode } from '@nextclaw/agent-chat-ui';
2
2
  import type { NcpMessagePart } from '@nextclaw/ncp';
3
3
  import type { NcpDraftAttachment } from '@nextclaw/ncp-react';
4
- import type { Dispatch, MutableRefObject, SetStateAction } from 'react';
5
- import type {
6
- ChatRunView,
7
- ChatTurnStreamDeltaEvent,
8
- ChatTurnStreamReadyEvent,
9
- ChatTurnStreamSessionEvent,
10
- SessionMessageView,
11
- ThinkingLevel
12
- } from '@/api/types';
4
+ import type { ThinkingLevel } from '@/api/types';
13
5
 
14
6
  export type SendMessageParams = {
15
- runId?: string;
16
7
  message: string;
17
8
  sessionKey: string;
18
9
  agentId: string;
@@ -28,58 +19,14 @@ export type SendMessageParams = {
28
19
  composerNodes?: ChatComposerNode[];
29
20
  };
30
21
 
31
- export type ActiveRunState = {
32
- localRunId: number;
22
+ export type ResumeRunParams = {
33
23
  sessionKey: string;
34
- agentId?: string;
35
- backendRunId?: string;
36
- backendStopSupported: boolean;
37
- backendStopReason?: string;
38
- };
39
-
40
- export type StreamReadyPayload = {
41
- sessionKey: string;
42
- runId?: string;
43
- stopSupported?: boolean;
44
- stopReason?: string;
45
- requestedAt?: string;
46
- };
47
-
48
- export type StreamReadyEvent = ChatTurnStreamReadyEvent;
49
- export type StreamDeltaEvent = ChatTurnStreamDeltaEvent;
50
- export type StreamSessionEvent = ChatTurnStreamSessionEvent;
51
-
52
- export type NextbotAgentRunMetadata =
53
- | {
54
- driver: 'nextbot-stream';
55
- mode: 'send';
56
- payload: SendMessageParams;
57
- requestedSkills: string[];
58
- }
59
- | {
60
- driver: 'nextbot-stream';
61
- mode: 'resume';
62
- runId: string;
63
- fromEventIndex?: number;
64
- sessionKey?: string;
65
- agentId?: string;
66
- stopSupported?: boolean;
67
- stopReason?: string;
68
- };
69
-
70
- export type UseChatStreamControllerParams = {
71
- selectedSessionKeyRef: MutableRefObject<string | null>;
72
- setSelectedSessionKey: Dispatch<SetStateAction<string | null>>;
73
- setDraft: Dispatch<SetStateAction<string>>;
74
- setComposerNodes: Dispatch<SetStateAction<ChatComposerNode[]>>;
75
- refetchSessions: () => Promise<unknown>;
76
- refetchHistory: () => Promise<unknown>;
77
24
  };
78
25
 
79
26
  export type ChatStreamActions = {
80
27
  sendMessage: (payload: SendMessageParams) => Promise<void>;
81
28
  stopCurrentRun: () => Promise<void>;
82
- resumeRun: (run: ChatRunView) => Promise<void>;
29
+ resumeRun: (run: ResumeRunParams) => Promise<void>;
83
30
  resetStreamState: () => void;
84
- applyHistoryMessages: (messages: SessionMessageView[], options?: { isLoading?: boolean }) => void;
31
+ applyHistoryMessages: (messages: unknown[], options?: { isLoading?: boolean }) => void;
85
32
  };
@@ -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"),