@nextclaw/ui 0.12.8 → 0.12.9

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 (142) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/dist/assets/ChannelsList-Ita2Zm1_.js +8 -0
  3. package/dist/assets/{DocBrowser-BMxf9CIK.js → DocBrowser-6ReNjvzF.js} +1 -1
  4. package/dist/assets/DocBrowser-BNwbPHf4.js +1 -0
  5. package/dist/assets/{DocBrowserContext-Ce28gRXt.js → DocBrowserContext-B6SpA7Qs.js} +1 -1
  6. package/dist/assets/{LogoBadge-o92MOA2L.js → LogoBadge-ByNLYg65.js} +1 -1
  7. package/dist/assets/MarketplacePage-CjX2MWww.js +1 -0
  8. package/dist/assets/{MarketplacePage-BySqkYDh.js → MarketplacePage-D0sDlYX4.js} +1 -1
  9. package/dist/assets/McpMarketplacePage-BGKJm1sJ.js +40 -0
  10. package/dist/assets/{ModelConfig-IrmzoslW.js → ModelConfig-BzZenCH-.js} +1 -1
  11. package/dist/assets/{ProviderScopedModelInput-CmTIzgI7.js → ProviderScopedModelInput-Da7khnBA.js} +1 -1
  12. package/dist/assets/{ProvidersList-8_Kalfwl.js → ProvidersList-BbVzRxjY.js} +1 -1
  13. package/dist/assets/RemoteAccessPage-BaDH_X1Q.js +1 -0
  14. package/dist/assets/RuntimeConfig-F_XKGgLm.js +1 -0
  15. package/dist/assets/{SearchConfig-DNBR-UbE.js → SearchConfig-BGkzXQP-.js} +1 -1
  16. package/dist/assets/{SecretsConfig-Ba1RPJaG.js → SecretsConfig-D281Rotl.js} +2 -2
  17. package/dist/assets/{SessionsConfig-Doqp5ghH.js → SessionsConfig-ChHQ7M5c.js} +2 -2
  18. package/dist/assets/{app-query-client-DniXoIN5.js → app-query-client-VnFElj4E.js} +1 -1
  19. package/dist/assets/{book-open-DocgeQtR.js → book-open-BdcxxoQu.js} +1 -1
  20. package/dist/assets/chat-page-Doe0yTtB.js +58 -0
  21. package/dist/assets/chat-session-display-cw78aiI_.js +1 -0
  22. package/dist/assets/{chunk-JZWAC4HX-BvKvh1R8.js → chunk-JZWAC4HX-DK5HPmIK.js} +1 -1
  23. package/dist/assets/{client-CVqPF5ie.js → client-_i4MU2bB.js} +1 -1
  24. package/dist/assets/{config-Bop2oB18.js → config-DtIQwrHF.js} +1 -1
  25. package/dist/assets/{createLucideIcon-DVv8taGY.js → createLucideIcon-BSeTgkZW.js} +1 -1
  26. package/dist/assets/desktop-update-config-Dpcf4BKG.js +1 -0
  27. package/dist/assets/{dist-Da5Gm_pO.js → dist-6TrrnPCR.js} +1 -1
  28. package/dist/assets/{dist-DmAlInRu.js → dist-ccBFUi-o.js} +1 -1
  29. package/dist/assets/download-BhDxnyvU.js +1 -0
  30. package/dist/assets/{external-link-DFjw3x1B.js → external-link-BgErLCNT.js} +1 -1
  31. package/dist/assets/{hash-DJtaCejM.js → hash-Bl7dr_UG.js} +1 -1
  32. package/dist/assets/i18n-eDHeDY0n.js +1 -0
  33. package/dist/assets/index-CF9xve0E.js +6 -0
  34. package/dist/assets/index-FgA52VBt.css +1 -0
  35. package/dist/assets/{infiniteQueryBehavior-DHSEQ3OH.js → infiniteQueryBehavior-ZDS92Qpp.js} +1 -1
  36. package/dist/assets/loader-circle-ACM1s51e.js +1 -0
  37. package/dist/assets/{logos-DEFUIR12.js → logos-x89HbrZ4.js} +1 -1
  38. package/dist/assets/{page-layout-Da3i3r6G.js → page-layout-vZnghcFy.js} +1 -1
  39. package/dist/assets/play-CFUwCA2E.js +1 -0
  40. package/dist/assets/plus-rYsv72JG.js +1 -0
  41. package/dist/assets/{popover-C_mWOFzI.js → popover-Bg1VoTZ6.js} +1 -1
  42. package/dist/assets/{refresh-ccw-D6HkNtfz.js → refresh-ccw-DT98i__E.js} +1 -1
  43. package/dist/assets/{refresh-cw-DRcvRrnc.js → refresh-cw-C47QSEwg.js} +1 -1
  44. package/dist/assets/{rotate-cw-BmDKfXtH.js → rotate-cw-JtFzpNn6.js} +1 -1
  45. package/dist/assets/{save-DHGmi2e9.js → save-3S6-H3Xw.js} +1 -1
  46. package/dist/assets/search-3kFR_zh9.js +1 -0
  47. package/dist/assets/{security-config-CbXfPZzr.js → security-config-BWaiARNk.js} +1 -1
  48. package/dist/assets/{select-Caud8QvU.js → select-DJ2MUjBB.js} +1 -1
  49. package/dist/assets/skeleton-ByQepn0M.js +1 -0
  50. package/dist/assets/{status-dot-DurKKSwA.js → status-dot-vbanNPFU.js} +1 -1
  51. package/dist/assets/{switch-0rmPBRKI.js → switch-BsLtHOH-.js} +1 -1
  52. package/dist/assets/{tabs-custom-5JLVL6v8.js → tabs-custom-D3HYMt6k.js} +1 -1
  53. package/dist/assets/{trash-2-C6caKPoz.js → trash-2-G48scll7.js} +1 -1
  54. package/dist/assets/{use-infinite-scroll-loader-dwnaa_qi.js → use-infinite-scroll-loader-DkNhD-42.js} +1 -1
  55. package/dist/assets/{useConfirmDialog-mMeWD_yo.js → useConfirmDialog-BkvTN-vd.js} +1 -1
  56. package/dist/assets/{useMutation-BmxxvCNf.js → useMutation-CBWjE2uj.js} +1 -1
  57. package/dist/assets/x-ByDbItbq.js +1 -0
  58. package/dist/index.html +95 -21
  59. package/dist/manifest.webmanifest +30 -0
  60. package/dist/offline.html +102 -0
  61. package/dist/pwa-192.png +0 -0
  62. package/dist/pwa-512.png +0 -0
  63. package/dist/sw.js +80 -0
  64. package/index.html +73 -1
  65. package/package.json +6 -6
  66. package/public/manifest.webmanifest +30 -0
  67. package/public/offline.html +102 -0
  68. package/public/pwa-192.png +0 -0
  69. package/public/pwa-512.png +0 -0
  70. package/public/sw.js +80 -0
  71. package/src/api/server-path.ts +27 -4
  72. package/src/api/types.ts +17 -10
  73. package/src/app.tsx +9 -0
  74. package/src/components/chat/ChatSidebar.test.tsx +43 -1
  75. package/src/components/chat/ChatSidebar.tsx +24 -0
  76. package/src/components/chat/adapters/chat-message.summary-truncation.test.ts +66 -0
  77. package/src/components/chat/adapters/file-operation/card.ts +9 -0
  78. package/src/components/chat/adapters/file-operation/diff.ts +14 -0
  79. package/src/components/chat/{ChatConversationPanel.test.tsx → chat-conversation-panel.test.tsx} +107 -206
  80. package/src/components/chat/chat-conversation-panel.tsx +412 -0
  81. package/src/components/chat/chat-page-shell.tsx +1 -1
  82. package/src/components/chat/chat-session-workspace-file-preview.test.tsx +91 -0
  83. package/src/components/chat/chat-session-workspace-file-preview.tsx +307 -0
  84. package/src/components/chat/chat-session-workspace-panel-nav.tsx +197 -0
  85. package/src/components/chat/chat-session-workspace-panel.tsx +318 -0
  86. package/src/components/chat/chat-sidebar-session-item.tsx +32 -2
  87. package/src/components/chat/containers/chat-message-list.container.test.tsx +49 -0
  88. package/src/components/chat/containers/chat-message-list.container.tsx +4 -0
  89. package/src/components/chat/managers/chat-session-list.manager.test.ts +12 -0
  90. package/src/components/chat/managers/chat-session-list.manager.ts +7 -0
  91. package/src/components/chat/ncp/ncp-chat-page.tsx +7 -7
  92. package/src/components/chat/ncp/ncp-chat-thread.manager.ts +179 -41
  93. package/src/components/chat/ncp/ncp-session-adapter.test.ts +35 -1
  94. package/src/components/chat/ncp/ncp-session-adapter.ts +17 -0
  95. package/src/components/chat/ncp/page/ncp-chat-derived-state.ts +54 -11
  96. package/src/components/chat/ncp/tests/ncp-chat-thread.manager.test.ts +189 -0
  97. package/src/components/chat/presenter/chat-presenter-context.tsx +13 -2
  98. package/src/components/chat/session-header/chat-session-header-actions.test.tsx +26 -0
  99. package/src/components/chat/session-header/chat-session-header-actions.tsx +19 -1
  100. package/src/components/chat/stores/chat-thread.store.ts +24 -0
  101. package/src/components/config/RuntimeConfig.tsx +141 -2
  102. package/src/components/layout/AppLayout.tsx +1 -1
  103. package/src/components/providers/ThemeProvider.tsx +5 -0
  104. package/src/hooks/server-path/use-server-path-read.ts +20 -0
  105. package/src/lib/chat-message.ts +14 -3
  106. package/src/lib/i18n.chat.ts +12 -1
  107. package/src/lib/i18n.pwa.ts +62 -0
  108. package/src/lib/i18n.ts +2 -2
  109. package/src/pwa/components/pwa-install-entry.test.tsx +110 -0
  110. package/src/pwa/components/pwa-install-entry.tsx +205 -0
  111. package/src/pwa/managers/pwa-install.manager.test.ts +160 -0
  112. package/src/pwa/managers/pwa-install.manager.ts +232 -0
  113. package/src/pwa/managers/pwa-runtime.manager.ts +196 -0
  114. package/src/pwa/managers/pwa-shell-theme.manager.test.ts +30 -0
  115. package/src/pwa/managers/pwa-shell-theme.manager.ts +46 -0
  116. package/src/pwa/pwa-install-banner.storage.ts +55 -0
  117. package/src/pwa/pwa.types.ts +22 -0
  118. package/src/pwa/register-pwa.ts +14 -0
  119. package/src/pwa/stores/pwa.store.ts +17 -0
  120. package/src/vite-env.d.ts +9 -0
  121. package/dist/assets/ChannelsList-KIQIxluX.js +0 -8
  122. package/dist/assets/DocBrowser-CyDgAtO9.js +0 -1
  123. package/dist/assets/MarketplacePage-C0olZaek.js +0 -1
  124. package/dist/assets/McpMarketplacePage-DqKaiXO9.js +0 -40
  125. package/dist/assets/RemoteAccessPage-CyQlSjPf.js +0 -1
  126. package/dist/assets/RuntimeConfig-Bk0uYBhf.js +0 -1
  127. package/dist/assets/chat-page-Bph8M5zo.js +0 -58
  128. package/dist/assets/chat-session-display-CoN3Wmn-.js +0 -1
  129. package/dist/assets/desktop-update-config-1KBrqLBC.js +0 -1
  130. package/dist/assets/i18n-CwHZ-9vt.js +0 -1
  131. package/dist/assets/index-DafCdM4F.css +0 -1
  132. package/dist/assets/index-DdksE6U3.js +0 -6
  133. package/dist/assets/loader-circle-PsSP0H9n.js +0 -1
  134. package/dist/assets/play-DBQbBxTA.js +0 -1
  135. package/dist/assets/plus-DUOVbsyQ.js +0 -1
  136. package/dist/assets/search-MChQRYR1.js +0 -1
  137. package/dist/assets/skeleton-B-4vRq_Z.js +0 -1
  138. package/dist/assets/x-DuMhMATD.js +0 -1
  139. package/src/components/chat/ChatConversationPanel.tsx +0 -256
  140. package/src/components/chat/chat-child-session-panel.tsx +0 -270
  141. /package/dist/assets/{config-hints-BZoDjXye.js → config-hints-BhTmc9P1.js} +0 -0
  142. /package/dist/assets/{config-layout-DmlGaay2.js → config-layout-CHs0mAaR.js} +0 -0
@@ -1,15 +1,17 @@
1
1
  import { api } from './client';
2
- import type { ServerPathBrowseView } from './types';
2
+ import type { ServerPathBrowseView, ServerPathReadView } from './types';
3
3
 
4
4
  export async function fetchServerPathBrowse(params?: {
5
5
  path?: string | null;
6
6
  includeFiles?: boolean;
7
7
  }): Promise<ServerPathBrowseView> {
8
+ const path = typeof params?.path === 'string' ? params.path.trim() : '';
9
+ const includeFiles = Boolean(params?.includeFiles);
8
10
  const query = new URLSearchParams();
9
- if (typeof params?.path === 'string' && params.path.trim().length > 0) {
10
- query.set('path', params.path.trim());
11
+ if (path) {
12
+ query.set('path', path);
11
13
  }
12
- if (params?.includeFiles) {
14
+ if (includeFiles) {
13
15
  query.set('includeFiles', '1');
14
16
  }
15
17
  const suffix = query.toString();
@@ -21,3 +23,24 @@ export async function fetchServerPathBrowse(params?: {
21
23
  }
22
24
  return response.data;
23
25
  }
26
+
27
+ export async function fetchServerPathRead(params: {
28
+ path: string;
29
+ basePath?: string | null;
30
+ }): Promise<ServerPathReadView> {
31
+ const { path } = params;
32
+ const basePath =
33
+ typeof params.basePath === 'string' ? params.basePath.trim() : '';
34
+ const query = new URLSearchParams();
35
+ query.set('path', path.trim());
36
+ if (basePath) {
37
+ query.set('basePath', basePath);
38
+ }
39
+ const response = await api.get<ServerPathReadView>(
40
+ `/api/server-paths/read?${query.toString()}`
41
+ );
42
+ if (!response.ok) {
43
+ throw new Error(response.error.message);
44
+ }
45
+ return response.data;
46
+ }
package/src/api/types.ts CHANGED
@@ -246,6 +246,13 @@ export type SessionConfigView = {
246
246
  dmScope?: "main" | "per-peer" | "per-channel-peer" | "per-account-channel-peer";
247
247
  };
248
248
 
249
+ export type RuntimeEntryView = {
250
+ enabled?: boolean;
251
+ label?: string;
252
+ type: string;
253
+ config?: Record<string, unknown>;
254
+ };
255
+
249
256
  export type SessionEntryView = {
250
257
  key: string;
251
258
  createdAt: string;
@@ -347,17 +354,9 @@ export type SessionPatchUpdate = {
347
354
  clearHistory?: boolean;
348
355
  };
349
356
 
350
- export type ServerPathEntryView = {
351
- name: string;
352
- path: string;
353
- kind: "directory" | "file";
354
- hidden: boolean;
355
- };
357
+ export type ServerPathEntryView = { name: string; path: string; kind: "directory" | "file"; hidden: boolean };
356
358
 
357
- export type ServerPathBreadcrumbView = {
358
- label: string;
359
- path: string;
360
- };
359
+ export type ServerPathBreadcrumbView = { label: string; path: string };
361
360
 
362
361
  export type ServerPathBrowseView = {
363
362
  currentPath: string;
@@ -367,6 +366,8 @@ export type ServerPathBrowseView = {
367
366
  entries: ServerPathEntryView[];
368
367
  };
369
368
 
369
+ export type ServerPathReadView = { requestedPath: string; resolvedPath: string; kind: "text" | "markdown" | "binary"; sizeBytes: number; truncated: boolean; text?: string; languageHint?: string | null };
370
+
370
371
  export type {
371
372
  ChatSessionTypeCtaView,
372
373
  ChatSessionTypeOptionView,
@@ -431,6 +432,9 @@ export type RuntimeConfigUpdate = {
431
432
  engine?: string;
432
433
  engineConfig?: Record<string, unknown>;
433
434
  };
435
+ runtimes?: {
436
+ entries?: Record<string, RuntimeEntryView> | null;
437
+ };
434
438
  list?: AgentProfileView[];
435
439
  };
436
440
  bindings?: AgentBindingView[];
@@ -500,6 +504,9 @@ export type ConfigView = {
500
504
  contextTokens?: number;
501
505
  maxToolIterations?: number;
502
506
  };
507
+ runtimes?: {
508
+ entries?: Record<string, RuntimeEntryView>;
509
+ };
503
510
  list?: AgentProfileView[];
504
511
  context?: {
505
512
  bootstrap?: {
package/src/app.tsx CHANGED
@@ -7,8 +7,11 @@ import { AppLayout } from '@/components/layout/AppLayout';
7
7
  import { isTransientAuthStatusBootstrapError, useAuthStatus } from '@/hooks/use-auth';
8
8
  import { useRealtimeQueryBridge } from '@/hooks/use-realtime-query-bridge';
9
9
  import { AppPresenterProvider } from '@/presenter/app-presenter-context';
10
+ import { PwaInstallBanner, PwaUpdateBanner } from '@/pwa/components/pwa-install-entry';
11
+ import { startNextClawPwa } from '@/pwa/register-pwa';
10
12
  import { Toaster } from 'sonner';
11
13
  import { Routes, Route, Navigate } from 'react-router-dom';
14
+ import { useEffect } from 'react';
12
15
 
13
16
  const ModelConfigPage = lazy(async () => ({ default: (await import('@/components/config/ModelConfig')).ModelConfig }));
14
17
  const ChatPage = lazy(async () => ({ default: (await import('@/components/chat/chat-page')).ChatPage }));
@@ -89,9 +92,15 @@ function AuthGate() {
89
92
  }
90
93
 
91
94
  export default function AppContent() {
95
+ useEffect(() => {
96
+ startNextClawPwa();
97
+ }, []);
98
+
92
99
  return (
93
100
  <QueryClientProvider client={appQueryClient}>
94
101
  <AuthGate />
102
+ <PwaInstallBanner />
103
+ <PwaUpdateBanner />
95
104
  <Toaster position="top-right" richColors />
96
105
  </QueryClientProvider>
97
106
  );
@@ -11,6 +11,7 @@ const mocks = vi.hoisted(() => ({
11
11
  setQuery: vi.fn(),
12
12
  setListMode: vi.fn(),
13
13
  selectSession: vi.fn(),
14
+ openChildSessionPanel: vi.fn(),
14
15
  docOpen: vi.fn(),
15
16
  updateNcpSession: vi.fn(),
16
17
  agents: [] as Array<{ id: string; displayName?: string; avatarUrl?: string | null }>,
@@ -36,7 +37,10 @@ vi.mock('@/components/chat/presenter/chat-presenter-context', () => ({
36
37
  sessionKey: string | null | undefined,
37
38
  readAt: string | null | undefined,
38
39
  ) => (sessionKey ? useChatSessionListStore.getState().markSessionRead(sessionKey, readAt) : undefined),
39
- }
40
+ },
41
+ chatThreadManager: {
42
+ openChildSessionPanel: mocks.openChildSessionPanel,
43
+ },
40
44
  })
41
45
  }));
42
46
 
@@ -113,6 +117,7 @@ function resetSidebarTestState() {
113
117
  mocks.setQuery.mockReset();
114
118
  mocks.setListMode.mockReset();
115
119
  mocks.selectSession.mockReset();
120
+ mocks.openChildSessionPanel.mockReset();
116
121
  mocks.docOpen.mockReset();
117
122
  mocks.updateNcpSession.mockReset();
118
123
  mocks.updateNcpSession.mockResolvedValue({});
@@ -630,4 +635,41 @@ describe('ChatSidebar session item interactions', () => {
630
635
 
631
636
  expect(screen.queryByLabelText('Session has unread updates')).toBeNull();
632
637
  });
638
+
639
+ it('opens the child-session browser from a parent session row', () => {
640
+ mocks.sessionItems = [
641
+ createSessionItem({
642
+ key: 'session:parent-1',
643
+ createdAt: '2026-03-19T09:00:00.000Z',
644
+ updatedAt: '2026-03-19T09:05:00.000Z',
645
+ label: 'Parent Task',
646
+ sessionType: 'native',
647
+ sessionTypeMutable: false,
648
+ messageCount: 1
649
+ }),
650
+ createSessionItem({
651
+ key: 'session:child-1',
652
+ createdAt: '2026-03-19T09:06:00.000Z',
653
+ updatedAt: '2026-03-19T09:07:00.000Z',
654
+ label: 'Child Task',
655
+ sessionType: 'native',
656
+ sessionTypeMutable: false,
657
+ messageCount: 1,
658
+ parentSessionId: 'session:parent-1'
659
+ })
660
+ ];
661
+
662
+ render(
663
+ <MemoryRouter>
664
+ <ChatSidebar />
665
+ </MemoryRouter>
666
+ );
667
+
668
+ fireEvent.click(screen.getByLabelText('View child sessions'));
669
+
670
+ expect(mocks.openChildSessionPanel).toHaveBeenCalledWith({
671
+ parentSessionKey: 'session:parent-1',
672
+ activeChildSessionKey: 'session:child-1',
673
+ });
674
+ });
633
675
  });
@@ -201,6 +201,22 @@ export function ChatSidebar() {
201
201
  [agentsQuery.data?.agents]
202
202
  );
203
203
  const sortedItems = useMemo(() => sortSessionItemsByUpdatedAtDesc(items), [items]);
204
+ const childSessionsByParentKey = useMemo(() => {
205
+ const grouped = new Map<string, NcpSessionListItemView[]>();
206
+ for (const item of items) {
207
+ const parentSessionKey = item.session.parentSessionId?.trim();
208
+ if (!parentSessionKey) {
209
+ continue;
210
+ }
211
+ const bucket = grouped.get(parentSessionKey) ?? [];
212
+ bucket.push(item);
213
+ grouped.set(parentSessionKey, bucket);
214
+ }
215
+ for (const bucket of grouped.values()) {
216
+ bucket.sort((left, right) => getSessionUpdatedAtTimestamp(right) - getSessionUpdatedAtTimestamp(left));
217
+ }
218
+ return grouped;
219
+ }, [items]);
204
220
  const groups = useMemo(() => groupSessionsByDate(sortedItems), [sortedItems]);
205
221
  const projectGroups = useMemo(() => groupSessionsByProject(sortedItems), [sortedItems]);
206
222
  const defaultSessionType = inputSnapshot.defaultSessionType || 'native';
@@ -263,6 +279,7 @@ export function ChatSidebar() {
263
279
  const context = resolveSessionContextView(session, inputSnapshot.sessionTypeOptions);
264
280
  const isEditing = editingSessionKey === session.key;
265
281
  const isSaving = savingSessionKey === session.key;
282
+ const childSessions = childSessionsByParentKey.get(session.key) ?? [];
266
283
  return (
267
284
  <ChatSidebarSessionItem
268
285
  key={session.key}
@@ -275,10 +292,17 @@ export function ChatSidebar() {
275
292
  agentId={session.agentId ?? null}
276
293
  agentLabel={session.agentId ? (agentsById.get(session.agentId)?.displayName ?? session.agentId) : null}
277
294
  agentAvatarUrl={session.agentId ? (agentsById.get(session.agentId)?.avatarUrl ?? null) : null}
295
+ childSessionCount={childSessions.length}
278
296
  isEditing={isEditing}
279
297
  draftLabel={draftLabel}
280
298
  isSaving={isSaving}
281
299
  onSelect={() => presenter.chatSessionListManager.selectSession(session.key)}
300
+ onOpenChildSessions={() =>
301
+ presenter.chatThreadManager.openChildSessionPanel({
302
+ parentSessionKey: session.key,
303
+ activeChildSessionKey: childSessions[0]?.session.key ?? null,
304
+ })
305
+ }
282
306
  onStartEditing={() => startEditingSessionLabel(session)}
283
307
  onDraftLabelChange={setDraftLabel}
284
308
  onSave={() => saveSessionLabel(session)}
@@ -0,0 +1,66 @@
1
+ import { ToolInvocationStatus, type UiMessage } from "@nextclaw/agent-chat";
2
+ import { adaptChatMessages } from "@/components/chat/adapters/chat-message.adapter";
3
+ import type { ChatMessageSource } from "@/components/chat/adapters/chat-message.adapter";
4
+ import type { ChatMessagePartViewModel } from "@nextclaw/agent-chat-ui";
5
+
6
+ const defaultTexts = {
7
+ roleLabels: {
8
+ user: "You",
9
+ assistant: "Assistant",
10
+ tool: "Tool",
11
+ system: "System",
12
+ fallback: "Message",
13
+ },
14
+ reasoningLabel: "Reasoning",
15
+ toolCallLabel: "Tool Call",
16
+ toolResultLabel: "Tool Result",
17
+ toolInputLabel: "Input",
18
+ toolNoOutputLabel: "No output",
19
+ toolOutputLabel: "Output",
20
+ toolStatusPreparingLabel: "Preparing",
21
+ toolStatusRunningLabel: "Running",
22
+ toolStatusCompletedLabel: "Completed",
23
+ toolStatusFailedLabel: "Failed",
24
+ toolStatusCancelledLabel: "Cancelled",
25
+ imageAttachmentLabel: "Image attachment",
26
+ fileAttachmentLabel: "File attachment",
27
+ unknownPartLabel: "Unknown Part",
28
+ };
29
+
30
+ function adapt(uiMessages: UiMessage[]) {
31
+ return adaptChatMessages({
32
+ uiMessages: uiMessages as unknown as ChatMessageSource[],
33
+ formatTimestamp: (value) => `formatted:${value}`,
34
+ texts: defaultTexts,
35
+ });
36
+ }
37
+
38
+ it("truncates long structured tool summaries into a single-line ellipsis detail", () => {
39
+ const adapted = adapt([
40
+ {
41
+ id: "assistant-long-tool-summary",
42
+ role: "assistant",
43
+ parts: [
44
+ {
45
+ type: "tool-invocation",
46
+ toolInvocation: {
47
+ status: ToolInvocationStatus.PARTIAL_CALL,
48
+ toolCallId: "call-long-tool-summary",
49
+ toolName: "terminal",
50
+ args: JSON.stringify({
51
+ command:
52
+ "ls -la /Users/peiwang/.nextclaw/workspace/skills/bird/snapshots/archive/releases/2026-04-17/builds/current/output 2>/dev/null | sed -n '1,160p' && echo 'done'",
53
+ }),
54
+ },
55
+ },
56
+ ],
57
+ },
58
+ ]);
59
+
60
+ const summary = (
61
+ adapted[0]?.parts[0] as Extract<ChatMessagePartViewModel, { type: "tool-card" }> | undefined
62
+ )?.card.summary;
63
+
64
+ expect(summary?.startsWith("command: ls -la /Users/peiwang/.nextclaw/workspace/skills/bird/")).toBe(true);
65
+ expect(summary?.endsWith("…")).toBe(true);
66
+ });
@@ -54,7 +54,16 @@ function finalizeParsedBlocks(
54
54
  display: block.display,
55
55
  ...(block.caption ? { caption: block.caption } : {}),
56
56
  lines: block.lines,
57
+ ...(block.fullLines ? { fullLines: block.fullLines } : {}),
57
58
  ...(block.rawText ? { rawText: block.rawText } : {}),
59
+ ...(block.beforeText ? { beforeText: block.beforeText } : {}),
60
+ ...(block.afterText ? { afterText: block.afterText } : {}),
61
+ ...(typeof block.oldStartLine === "number"
62
+ ? { oldStartLine: block.oldStartLine }
63
+ : {}),
64
+ ...(typeof block.newStartLine === "number"
65
+ ? { newStartLine: block.newStartLine }
66
+ : {}),
58
67
  ...(block.truncated ? { truncated: true } : {}),
59
68
  }))
60
69
  .filter((block) => block.lines.length > 0 || Boolean(block.rawText));
@@ -13,7 +13,12 @@ export type ParsedBlock = {
13
13
  display: "preview" | "diff";
14
14
  caption?: string;
15
15
  lines: ChatFileOperationLineViewModel[];
16
+ fullLines?: ChatFileOperationLineViewModel[];
16
17
  rawText?: string;
18
+ beforeText?: string;
19
+ afterText?: string;
20
+ oldStartLine?: number;
21
+ newStartLine?: number;
17
22
  truncated?: boolean;
18
23
  };
19
24
 
@@ -114,6 +119,9 @@ export function buildRawPreviewBlock(params: {
114
119
  lines,
115
120
  }),
116
121
  lines,
122
+ rawText: previewText,
123
+ oldStartLine,
124
+ newStartLine,
117
125
  };
118
126
  }
119
127
 
@@ -144,6 +152,11 @@ export function buildFullReplaceBlock(params: {
144
152
  lines,
145
153
  }),
146
154
  lines: limited.lines,
155
+ ...(limited.truncated ? { fullLines: lines } : {}),
156
+ ...(params.beforeText != null ? { beforeText: params.beforeText } : {}),
157
+ ...(params.afterText != null ? { afterText: params.afterText } : {}),
158
+ ...(typeof oldStartLine === "number" ? { oldStartLine } : {}),
159
+ ...(typeof newStartLine === "number" ? { newStartLine } : {}),
147
160
  truncated: limited.truncated,
148
161
  };
149
162
  }
@@ -162,6 +175,7 @@ function buildParsedPatchBlock(params: {
162
175
  lines: params.lines,
163
176
  }),
164
177
  lines: limited.lines,
178
+ ...(limited.truncated ? { fullLines: params.lines } : {}),
165
179
  truncated: limited.truncated,
166
180
  };
167
181
  }