@nextclaw/ui 0.10.5 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/CHANGELOG.md +23 -2
  2. package/dist/assets/{ChannelsList-Nu7Ig6_-.js → ChannelsList-CVPqrxns.js} +4 -4
  3. package/dist/assets/ChatPage-BO1VUrAY.js +37 -0
  4. package/dist/assets/{DocBrowser-3CfKmJA6.js → DocBrowser-FBwg8iji.js} +1 -1
  5. package/dist/assets/{LogoBadge-DdthDJOp.js → LogoBadge-BCmJfRT8.js} +1 -1
  6. package/dist/assets/MarketplacePage-DWxXUOCx.js +49 -0
  7. package/dist/assets/{McpMarketplacePage-Dg8GSZh6.js → McpMarketplacePage-Bth9X_hu.js} +2 -2
  8. package/dist/assets/{ModelConfig-DyQ6cC92.js → ModelConfig-PkSp_ioc.js} +1 -1
  9. package/dist/assets/ProvidersList-DVDge8wa.js +1 -0
  10. package/dist/assets/RemoteAccessPage-BVkzfEaL.js +1 -0
  11. package/dist/assets/RuntimeConfig-ByJs3khh.js +1 -0
  12. package/dist/assets/{SearchConfig-R1BcCLWO.js → SearchConfig-KZUAqYJN.js} +1 -1
  13. package/dist/assets/{SecretsConfig-D-jZMHeY.js → SecretsConfig-qwB_Y_Ka.js} +2 -2
  14. package/dist/assets/SessionsConfig-CGCl4UTr.js +2 -0
  15. package/dist/assets/index-CrilScMo.css +1 -0
  16. package/dist/assets/{index-BulnQWr6.js → index-D41ntvb7.js} +6 -6
  17. package/dist/assets/{label-C7yzBvzK.js → label-7JEFhkur.js} +1 -1
  18. package/dist/assets/ncp-session-adapter-BOqhkrc-.js +1 -0
  19. package/dist/assets/{page-layout-DF0xpax2.js → page-layout-B7q511TE.js} +1 -1
  20. package/dist/assets/popover-CywJGmPr.js +1 -0
  21. package/dist/assets/security-config-zi2UxN5r.js +1 -0
  22. package/dist/assets/skeleton-qUJZQ03S.js +1 -0
  23. package/dist/assets/{status-dot-B9opOZ22.js → status-dot-BilwNdTT.js} +1 -1
  24. package/dist/assets/{switch-l1P0ev4D.js → switch-BLp2Pno1.js} +1 -1
  25. package/dist/assets/tabs-custom-CgIdQMGC.js +1 -0
  26. package/dist/assets/useConfirmDialog-BitswAkv.js +1 -0
  27. package/dist/assets/{vendor-CNhxtHCf.js → vendor-D_JxmsLV.js} +87 -87
  28. package/dist/index.html +3 -3
  29. package/package.json +4 -4
  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/ncp-attachments.ts +12 -12
  36. package/src/api/types.ts +4 -121
  37. package/src/components/chat/ChatPage.tsx +1 -11
  38. package/src/components/chat/ChatSidebar.test.tsx +1 -50
  39. package/src/components/chat/ChatSidebar.tsx +0 -5
  40. package/src/components/chat/README.md +2 -0
  41. package/src/components/chat/adapters/chat-message.adapter.test.ts +39 -0
  42. package/src/components/chat/adapters/chat-message.adapter.ts +56 -0
  43. package/src/components/chat/chat-attachment-upload-limit.test.ts +41 -0
  44. package/src/components/chat/chat-composer-state.test.ts +4 -4
  45. package/src/components/chat/chat-composer-state.ts +1 -1
  46. package/src/components/chat/chat-session-display.ts +9 -0
  47. package/src/components/chat/chat-session-label.service.ts +3 -12
  48. package/src/components/chat/chat-session-preference-sync.test.ts +10 -13
  49. package/src/components/chat/chat-stream/types.ts +4 -57
  50. package/src/components/chat/containers/chat-input-bar.container.tsx +2 -2
  51. package/src/components/chat/ncp/NcpChatPage.tsx +3 -3
  52. package/src/components/chat/ncp/ncp-chat-input.manager.ts +1 -1
  53. package/src/components/chat/useHydratedNcpAgent.test.tsx +77 -0
  54. package/src/components/config/README.md +2 -0
  55. package/src/components/config/SessionsConfig.tsx +152 -132
  56. package/src/hooks/use-auth.test.ts +3 -3
  57. package/src/hooks/use-auth.ts +16 -4
  58. package/src/hooks/use-realtime-query-bridge.ts +0 -24
  59. package/src/hooks/useConfig.ts +10 -137
  60. package/src/lib/session-run-status.ts +1 -63
  61. package/src/vite-env.d.ts +1 -0
  62. package/vite.config.ts +4 -4
  63. package/dist/assets/ChatPage-CBCFSk4e.js +0 -38
  64. package/dist/assets/MarketplacePage-inGGiv1T.js +0 -49
  65. package/dist/assets/ProvidersList-B2T8Lc_i.js +0 -1
  66. package/dist/assets/RemoteAccessPage-C9LxgK-C.js +0 -1
  67. package/dist/assets/RuntimeConfig-Ey4VIqTW.js +0 -1
  68. package/dist/assets/SessionsConfig-Cawoh4_2.js +0 -2
  69. package/dist/assets/chat-message-BbuIK4dQ.js +0 -3
  70. package/dist/assets/index-kaPUhd-8.css +0 -1
  71. package/dist/assets/popover-DjaScZDJ.js +0 -1
  72. package/dist/assets/security-config-Bg2eriNx.js +0 -1
  73. package/dist/assets/skeleton-DycBJAJF.js +0 -1
  74. package/dist/assets/tabs-custom-BG9y2JhC.js +0 -1
  75. package/dist/assets/useConfirmDialog-DTducNfn.js +0 -1
  76. package/src/api/config.stream.test.ts +0 -115
  77. package/src/components/chat/chat-chain.test.ts +0 -22
  78. package/src/components/chat/chat-chain.ts +0 -23
  79. package/src/components/chat/chat-page-data.ts +0 -171
  80. package/src/components/chat/chat-page-runtime.ts +0 -190
  81. package/src/components/chat/chat-stream/nextbot-parsers.ts +0 -52
  82. package/src/components/chat/chat-stream/nextbot-runtime-agent.ts +0 -413
  83. package/src/components/chat/chat-stream/stream-event-adapter.ts +0 -98
  84. package/src/components/chat/chat-stream/transport.ts +0 -253
  85. package/src/components/chat/legacy/LegacyChatPage.tsx +0 -223
  86. package/src/components/chat/managers/chat-input.manager.ts +0 -228
  87. package/src/components/chat/managers/chat-thread.manager.ts +0 -87
  88. package/src/components/chat/presenter/chat.presenter.ts +0 -32
  89. package/src/components/chat/useChatRuntimeController.ts +0 -134
package/src/api/types.ts CHANGED
@@ -211,11 +211,6 @@ export type SessionEntryView = {
211
211
  lastTimestamp?: string;
212
212
  };
213
213
 
214
- export type SessionsListView = {
215
- sessions: SessionEntryView[];
216
- total: number;
217
- };
218
-
219
214
  export type SessionMessageView = {
220
215
  role: string;
221
216
  content: unknown;
@@ -233,17 +228,6 @@ export type SessionEventView = {
233
228
  message?: SessionMessageView;
234
229
  };
235
230
 
236
- export type SessionHistoryView = {
237
- key: string;
238
- totalMessages: number;
239
- totalEvents: number;
240
- sessionType: string;
241
- sessionTypeMutable: boolean;
242
- metadata: Record<string, unknown>;
243
- messages: SessionMessageView[];
244
- events: SessionEventView[];
245
- };
246
-
247
231
  export type NcpSessionSummaryView = NcpSessionSummary;
248
232
 
249
233
  export type NcpSessionsListView = {
@@ -259,17 +243,17 @@ export type NcpSessionMessagesView = {
259
243
  total: number;
260
244
  };
261
245
 
262
- export type NcpAttachmentView = {
246
+ export type NcpAssetView = {
263
247
  id: string;
264
248
  name: string;
265
249
  mimeType: string;
266
250
  sizeBytes: number;
267
- attachmentUri: string;
251
+ assetUri: string;
268
252
  url: string;
269
253
  };
270
254
 
271
- export type NcpAttachmentUploadView = {
272
- attachments: NcpAttachmentView[];
255
+ export type NcpAssetPutView = {
256
+ assets: NcpAssetView[];
273
257
  };
274
258
 
275
259
  export type NcpSessionStatusView = NcpSessionStatus;
@@ -282,112 +266,12 @@ export type SessionPatchUpdate = {
282
266
  clearHistory?: boolean;
283
267
  };
284
268
 
285
- export type ChatTurnRequest = {
286
- message: string;
287
- sessionKey?: string;
288
- agentId?: string;
289
- channel?: string;
290
- chatId?: string;
291
- model?: string;
292
- metadata?: Record<string, unknown>;
293
- };
294
-
295
- export type ChatTurnView = {
296
- reply: string;
297
- sessionKey: string;
298
- agentId?: string;
299
- model?: string;
300
- requestedAt: string;
301
- completedAt: string;
302
- durationMs: number;
303
- };
304
-
305
- export type ChatTurnStreamReadyEvent = {
306
- sessionKey: string;
307
- requestedAt?: string;
308
- runId?: string;
309
- stopSupported?: boolean;
310
- stopReason?: string;
311
- };
312
-
313
269
  export type {
314
270
  ChatSessionTypeCtaView,
315
271
  ChatSessionTypeOptionView,
316
272
  ChatSessionTypesView,
317
273
  } from './chat-session-type.types';
318
274
 
319
- export type ChatTurnStreamDeltaEvent = {
320
- delta: string;
321
- };
322
-
323
- export type ChatTurnStreamSessionEvent = {
324
- data: SessionEventView;
325
- };
326
-
327
- export type ChatTurnStreamErrorEvent = {
328
- code?: string;
329
- message?: string;
330
- };
331
-
332
- export type ChatCapabilitiesView = {
333
- stopSupported: boolean;
334
- stopReason?: string;
335
- };
336
-
337
- export type ChatCommandOptionView = {
338
- name: string;
339
- description: string;
340
- type: 'string' | 'boolean' | 'number';
341
- required?: boolean;
342
- };
343
-
344
- export type ChatCommandView = {
345
- name: string;
346
- description: string;
347
- options?: ChatCommandOptionView[];
348
- };
349
-
350
- export type ChatCommandsView = {
351
- commands: ChatCommandView[];
352
- total: number;
353
- };
354
-
355
- export type ChatTurnStopRequest = {
356
- runId: string;
357
- sessionKey?: string;
358
- agentId?: string;
359
- };
360
-
361
- export type ChatTurnStopResult = {
362
- stopped: boolean;
363
- runId: string;
364
- sessionKey?: string;
365
- reason?: string;
366
- };
367
-
368
- export type ChatRunState = 'queued' | 'running' | 'completed' | 'failed' | 'aborted';
369
-
370
- export type ChatRunView = {
371
- runId: string;
372
- sessionKey: string;
373
- agentId?: string;
374
- model?: string;
375
- state: ChatRunState;
376
- requestedAt: string;
377
- startedAt?: string;
378
- completedAt?: string;
379
- stopSupported: boolean;
380
- stopReason?: string;
381
- error?: string;
382
- reply?: string;
383
- eventCount: number;
384
- };
385
-
386
- export type ChatRunListView = {
387
- runs: ChatRunView[];
388
- total: number;
389
- };
390
-
391
275
  export type CronScheduleView =
392
276
  | { kind: "at"; atMs?: number | null }
393
277
  | { kind: "every"; everyMs?: number | null }
@@ -684,7 +568,6 @@ export type ConfigActionExecuteResult = {
684
568
  // WebSocket events
685
569
  export type WsEvent =
686
570
  | { type: 'config.updated'; payload: { path: string } }
687
- | { type: 'run.updated'; payload: { run: ChatRunView } }
688
571
  | { type: 'session.updated'; payload: { sessionKey: string } }
689
572
  | { type: 'config.reload.started'; payload?: Record<string, unknown> }
690
573
  | { type: 'config.reload.finished'; payload?: Record<string, unknown> }
@@ -1,16 +1,6 @@
1
- import { useLocation } from 'react-router-dom';
2
- import { resolveChatChain } from '@/components/chat/chat-chain';
3
1
  import type { ChatPageProps } from '@/components/chat/chat-page-shell';
4
- import { LegacyChatPage } from '@/components/chat/legacy/LegacyChatPage';
5
2
  import { NcpChatPage } from '@/components/chat/ncp/NcpChatPage';
6
3
 
7
4
  export function ChatPage({ view }: ChatPageProps) {
8
- const location = useLocation();
9
- const chatChain = resolveChatChain(location.search);
10
-
11
- if (chatChain === 'ncp') {
12
- return <NcpChatPage view={view} />;
13
- }
14
-
15
- return <LegacyChatPage view={view} />;
5
+ return <NcpChatPage view={view} />;
16
6
  }
@@ -11,7 +11,6 @@ const mocks = vi.hoisted(() => ({
11
11
  setQuery: vi.fn(),
12
12
  selectSession: vi.fn(),
13
13
  docOpen: vi.fn(),
14
- updateSession: vi.fn(),
15
14
  updateNcpSession: vi.fn()
16
15
  }));
17
16
 
@@ -33,15 +32,9 @@ vi.mock('@/components/doc-browser', () => ({
33
32
 
34
33
  vi.mock('@/components/chat/chat-session-label.service', () => ({
35
34
  useChatSessionLabelService: () => async (params: {
36
- chatChain: 'legacy' | 'ncp';
37
35
  sessionKey: string;
38
36
  label: string | null;
39
- }) => {
40
- if (params.chatChain === 'ncp') {
41
- return mocks.updateNcpSession(params.sessionKey, { label: params.label });
42
- }
43
- return mocks.updateSession(params.sessionKey, { label: params.label });
44
- }
37
+ }) => mocks.updateNcpSession(params.sessionKey, { label: params.label })
45
38
  }));
46
39
 
47
40
  vi.mock('@/components/common/BrandHeader', () => ({
@@ -77,9 +70,7 @@ describe('ChatSidebar', () => {
77
70
  mocks.setQuery.mockReset();
78
71
  mocks.selectSession.mockReset();
79
72
  mocks.docOpen.mockReset();
80
- mocks.updateSession.mockReset();
81
73
  mocks.updateNcpSession.mockReset();
82
- mocks.updateSession.mockResolvedValue({});
83
74
  mocks.updateNcpSession.mockResolvedValue({});
84
75
 
85
76
  useChatInputStore.setState({
@@ -285,48 +276,9 @@ describe('ChatSidebar', () => {
285
276
  label: 'Renamed Label'
286
277
  });
287
278
  });
288
- expect(mocks.updateSession).not.toHaveBeenCalled();
289
279
  expect(screen.getByText('Renamed Label')).not.toBeNull();
290
280
  });
291
281
 
292
- it('routes inline session label edits to the legacy session api when chatChain=legacy', async () => {
293
- useChatSessionListStore.setState({
294
- snapshot: {
295
- ...useChatSessionListStore.getState().snapshot,
296
- sessions: [
297
- {
298
- key: 'session:legacy-1',
299
- createdAt: '2026-03-19T09:00:00.000Z',
300
- updatedAt: '2026-03-19T09:05:00.000Z',
301
- label: 'Legacy Label',
302
- sessionType: 'native',
303
- sessionTypeMutable: false,
304
- messageCount: 1
305
- }
306
- ]
307
- }
308
- });
309
-
310
- render(
311
- <MemoryRouter initialEntries={['/chat?chatChain=legacy']}>
312
- <ChatSidebar />
313
- </MemoryRouter>
314
- );
315
-
316
- fireEvent.click(screen.getByLabelText('Edit'));
317
- fireEvent.change(screen.getByPlaceholderText('Session label (optional)'), {
318
- target: { value: 'Legacy Renamed' }
319
- });
320
- fireEvent.click(screen.getByLabelText('Save'));
321
-
322
- await waitFor(() => {
323
- expect(mocks.updateSession).toHaveBeenCalledWith('session:legacy-1', {
324
- label: 'Legacy Renamed'
325
- });
326
- });
327
- expect(mocks.updateNcpSession).not.toHaveBeenCalled();
328
- });
329
-
330
282
  it('cancels inline session label editing without saving', () => {
331
283
  useChatSessionListStore.setState({
332
284
  snapshot: {
@@ -357,7 +309,6 @@ describe('ChatSidebar', () => {
357
309
  });
358
310
  fireEvent.click(screen.getByLabelText('Cancel'));
359
311
 
360
- expect(mocks.updateSession).not.toHaveBeenCalled();
361
312
  expect(mocks.updateNcpSession).not.toHaveBeenCalled();
362
313
  expect(screen.queryByDisplayValue('Should Not Persist')).toBeNull();
363
314
  expect(screen.getByText('Cancelable Label')).not.toBeNull();
@@ -12,7 +12,6 @@ import { usePresenter } from '@/components/chat/presenter/chat-presenter-context
12
12
  import { useChatInputStore } from '@/components/chat/stores/chat-input.store';
13
13
  import { useChatRunStatusStore } from '@/components/chat/stores/chat-run-status.store';
14
14
  import { useChatSessionListStore } from '@/components/chat/stores/chat-session-list.store';
15
- import { resolveChatChain } from '@/components/chat/chat-chain';
16
15
  import { cn } from '@/lib/utils';
17
16
  import { LANGUAGE_OPTIONS, t, type I18nLanguage } from '@/lib/i18n';
18
17
  import { THEME_OPTIONS, type UiTheme } from '@/lib/theme';
@@ -21,7 +20,6 @@ import { useTheme } from '@/components/providers/ThemeProvider';
21
20
  import { useDocBrowser } from '@/components/doc-browser';
22
21
  import { SidebarActionItem, SidebarNavLinkItem, SidebarSelectItem } from '@/components/layout/sidebar-items';
23
22
  import { useUiStore } from '@/stores/ui.store';
24
- import { useLocation } from 'react-router-dom';
25
23
  import {
26
24
  AlarmClock,
27
25
  BookOpen,
@@ -117,7 +115,6 @@ const navItems = [
117
115
  export function ChatSidebar() {
118
116
  const presenter = usePresenter();
119
117
  const docBrowser = useDocBrowser();
120
- const location = useLocation();
121
118
  const [isCreateMenuOpen, setIsCreateMenuOpen] = useState(false);
122
119
  const [editingSessionKey, setEditingSessionKey] = useState<string | null>(null);
123
120
  const [draftLabel, setDraftLabel] = useState('');
@@ -129,7 +126,6 @@ export function ChatSidebar() {
129
126
  const { language, setLanguage } = useI18n();
130
127
  const { theme, setTheme } = useTheme();
131
128
  const updateSessionLabel = useChatSessionLabelService();
132
- const chatChain = resolveChatChain(location.search);
133
129
  const currentThemeLabel = t(THEME_OPTIONS.find((o) => o.value === theme)?.labelKey ?? 'themeWarm');
134
130
  const currentLanguageLabel = LANGUAGE_OPTIONS.find((o) => o.value === language)?.label ?? language;
135
131
 
@@ -182,7 +178,6 @@ export function ChatSidebar() {
182
178
  setSavingSessionKey(session.key);
183
179
  try {
184
180
  await updateSessionLabel({
185
- chatChain,
186
181
  sessionKey: session.key,
187
182
  label: normalizedLabel || null
188
183
  });
@@ -0,0 +1,2 @@
1
+ ## 目录预算豁免
2
+ - 原因:聊天入口层需要并列承载页面壳、会话侧栏、会话偏好同步、展示组件与配套测试,且已通过 `ncp/` 等子目录承接更细分的实现,短期内仍需保留这组扁平入口文件。
@@ -191,3 +191,42 @@ it("keeps named non-image files as downloadable attachments", () => {
191
191
  },
192
192
  });
193
193
  });
194
+
195
+ it("renders asset tool results as previewable files", () => {
196
+ const adapted = adapt([
197
+ {
198
+ id: "assistant-asset",
199
+ role: "assistant",
200
+ parts: [
201
+ {
202
+ type: "tool-invocation",
203
+ toolInvocation: {
204
+ status: ToolInvocationStatus.RESULT,
205
+ toolCallId: "call-asset-1",
206
+ toolName: "asset_put",
207
+ args: { path: "/tmp/output.png" },
208
+ result: {
209
+ ok: true,
210
+ asset: {
211
+ uri: "asset://store/2026/03/27/asset_1",
212
+ name: "output.png",
213
+ mimeType: "image/png",
214
+ url: "/api/ncp/assets/content?uri=asset%3A%2F%2Fstore%2F2026%2F03%2F27%2Fasset_1",
215
+ },
216
+ },
217
+ },
218
+ },
219
+ ],
220
+ },
221
+ ] as unknown as ChatMessageSource[]);
222
+
223
+ expect(adapted[0]?.parts[0]).toEqual({
224
+ type: "file",
225
+ file: {
226
+ label: "output.png",
227
+ mimeType: "image/png",
228
+ dataUrl: "/api/ncp/assets/content?uri=asset%3A%2F%2Fstore%2F2026%2F03%2F27%2Fasset_1",
229
+ isImage: true,
230
+ },
231
+ });
232
+ });
@@ -76,6 +76,58 @@ function isRecord(value: unknown): value is Record<string, unknown> {
76
76
  return typeof value === 'object' && value !== null;
77
77
  }
78
78
 
79
+ function readOptionalString(value: unknown): string | null {
80
+ if (typeof value !== 'string') {
81
+ return null;
82
+ }
83
+ const trimmed = value.trim();
84
+ return trimmed.length > 0 ? trimmed : null;
85
+ }
86
+
87
+ function extractAssetFileView(
88
+ 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 {
101
+ if (!isRecord(value)) {
102
+ return null;
103
+ }
104
+ const assetCandidate = isRecord(value.asset)
105
+ ? value.asset
106
+ : Array.isArray(value.assets) && value.assets.length > 0 && isRecord(value.assets[0])
107
+ ? value.assets[0]
108
+ : null;
109
+ if (!assetCandidate) {
110
+ return null;
111
+ }
112
+ const url = readOptionalString(assetCandidate.url);
113
+ const mimeType = readOptionalString(assetCandidate.mimeType) ?? 'application/octet-stream';
114
+ if (!url) {
115
+ return null;
116
+ }
117
+ const label =
118
+ readOptionalString(assetCandidate.name) ??
119
+ (mimeType.startsWith('image/') ? texts.imageAttachmentLabel : texts.fileAttachmentLabel);
120
+ return {
121
+ type: 'file',
122
+ file: {
123
+ label,
124
+ mimeType,
125
+ dataUrl: url,
126
+ isImage: mimeType.startsWith('image/')
127
+ }
128
+ };
129
+ }
130
+
79
131
  function isTextPart(part: ChatMessagePartSource): part is Extract<ChatMessagePartSource, { type: 'text' }> {
80
132
  return part.type === 'text' && typeof part.text === 'string';
81
133
  }
@@ -223,6 +275,10 @@ export function adaptChatMessages(params: {
223
275
  }
224
276
  if (isToolInvocationPart(part)) {
225
277
  const invocation = part.toolInvocation;
278
+ const assetFileView = extractAssetFileView(invocation.result, params.texts);
279
+ if (assetFileView) {
280
+ return assetFileView;
281
+ }
226
282
  const detail = summarizeToolArgs(invocation.parsedArgs ?? invocation.args);
227
283
  const rawResult =
228
284
  typeof invocation.error === 'string' && invocation.error.trim()
@@ -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
+ });
@@ -87,8 +87,8 @@ describe('deriveNcpMessagePartsFromComposer', () => {
87
87
  name: 'config.json',
88
88
  mimeType: 'application/json',
89
89
  sizeBytes: 18,
90
- attachmentUri: 'attachment://local/2026/03/26/att_123',
91
- url: '/api/ncp/attachments/content?uri=attachment%3A%2F%2Flocal%2F2026%2F03%2F26%2Fatt_123'
90
+ assetUri: 'asset://store/2026/03/26/asset_123',
91
+ url: '/api/ncp/assets/content?uri=asset%3A%2F%2Fstore%2F2026%2F03%2F26%2Fasset_123'
92
92
  }
93
93
  ]
94
94
  );
@@ -98,8 +98,8 @@ describe('deriveNcpMessagePartsFromComposer', () => {
98
98
  type: 'file',
99
99
  name: 'config.json',
100
100
  mimeType: 'application/json',
101
- attachmentUri: 'attachment://local/2026/03/26/att_123',
102
- url: '/api/ncp/attachments/content?uri=attachment%3A%2F%2Flocal%2F2026%2F03%2F26%2Fatt_123',
101
+ assetUri: 'asset://store/2026/03/26/asset_123',
102
+ url: '/api/ncp/assets/content?uri=asset%3A%2F%2Fstore%2F2026%2F03%2F26%2Fasset_123',
103
103
  sizeBytes: 18
104
104
  }
105
105
  ]);
@@ -108,7 +108,7 @@ export function deriveNcpMessagePartsFromComposer(
108
108
  type: 'file',
109
109
  name: attachment.name,
110
110
  mimeType: attachment.mimeType,
111
- ...(attachment.attachmentUri ? { attachmentUri: attachment.attachmentUri } : {}),
111
+ ...(attachment.assetUri ? { assetUri: attachment.assetUri } : {}),
112
112
  ...(attachment.url ? { url: attachment.url } : {}),
113
113
  ...(attachment.contentBase64 ? { contentBase64: attachment.contentBase64 } : {}),
114
114
  sizeBytes: attachment.sizeBytes
@@ -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
  };