@nextclaw/ui 0.18.3 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/assets/api-Cyxp93Fn.js +13 -0
  3. package/dist/assets/appearance-settings-page-DsMl931U.js +1 -0
  4. package/dist/assets/{book-open-BOJLGVsx.js → book-open-Cl5CzL0g.js} +1 -1
  5. package/dist/assets/{channels-list-page-BPy2mXoD.js → channels-list-page-D_B7JGoZ.js} +2 -2
  6. package/dist/assets/chat-page-D3ZeULTv.js +1 -0
  7. package/dist/assets/config-split-page-C-YVl2mf.js +1 -0
  8. package/dist/assets/{confirm-dialog-Bsg9A62_.js → confirm-dialog-Bz3ieDQ5.js} +2 -2
  9. package/dist/assets/desktop-update-config-DO051-QG.js +1 -0
  10. package/dist/assets/{dist-IBUY9_9h.js → dist-CDPl8mm7.js} +1 -1
  11. package/dist/assets/{dist-DyUY9uQ0.js → dist-HFDWY6-K.js} +1 -1
  12. package/dist/assets/doc-browser-CWhs0JQN.js +1 -0
  13. package/dist/assets/doc-browser-D6H_nPzO.js +1 -0
  14. package/dist/assets/{doc-browser-context-BM6Af8gN.js → doc-browser-context-BKwz_j44.js} +1 -1
  15. package/dist/assets/form-actions-BUZOCfPR.js +1 -0
  16. package/dist/assets/index-DR3GLGme.css +1 -0
  17. package/dist/assets/index-j6DBnvlr.js +109 -0
  18. package/dist/assets/loader-circle-s65a4ZzL.js +1 -0
  19. package/dist/assets/mcp-marketplace-page-BJvzGt7h.js +9 -0
  20. package/dist/assets/mcp-marketplace-page-EpFHeJzw.js +1 -0
  21. package/dist/assets/model-config-page-CBoh0DVh.js +1 -0
  22. package/dist/assets/{navigation-link-DtPZZ_Q_.js → navigation-link-CDMYpkVv.js} +2 -2
  23. package/dist/assets/plus-XDIkqpkt.js +1 -0
  24. package/dist/assets/{provider-scoped-model-input-RWZz36z7.js → provider-scoped-model-input-C2iUtZi7.js} +1 -1
  25. package/dist/assets/providers-config-page-DX1pISTo.js +1 -0
  26. package/dist/assets/react-B7Paq2b3.js +8 -0
  27. package/dist/assets/{refresh-cw-BQHORcAs.js → refresh-cw-XrnNVwuQ.js} +1 -1
  28. package/dist/assets/remote-D_7_OGoj.js +1 -0
  29. package/dist/assets/rotate-cw-BhIWpLaU.js +1 -0
  30. package/dist/assets/runtime-config-page-BVO9B8Ns.js +1 -0
  31. package/dist/assets/{save-BgiS2u-T.js → save-_nSNAYQu.js} +1 -1
  32. package/dist/assets/{search-B6mO7mmo.js → search-0OTHFRMx.js} +1 -1
  33. package/dist/assets/{search-config-page-QS-kqNDZ.js → search-config-page-BwyeUjKt.js} +1 -1
  34. package/dist/assets/secrets-config-page-2O-TzroJ.js +3 -0
  35. package/dist/assets/security-config-BgIIrg1s.js +1 -0
  36. package/dist/assets/{select-DToos7Rq.js → select-BZHvnwQB.js} +2 -2
  37. package/dist/assets/{settings-2-ZAWXrq81.js → settings-2-CM_PvDNk.js} +1 -1
  38. package/dist/assets/skeleton-Bv4MMbH7.js +1 -0
  39. package/dist/assets/{tag-chip-urB3N-kk.js → tag-chip-Ce53PMTL.js} +1 -1
  40. package/dist/assets/x-CCjWN5ys.js +1 -0
  41. package/dist/index.html +20 -20
  42. package/package.json +6 -6
  43. package/src/app/components/layout/sidebar-items.tsx +49 -15
  44. package/src/app/configs/__tests__/app-navigation.config.test.ts +21 -0
  45. package/src/app/configs/app-navigation.config.ts +10 -1
  46. package/src/app/index.tsx +4 -0
  47. package/src/features/chat/components/conversation/chat-conversation-content.tsx +7 -0
  48. package/src/features/chat/components/layout/chat-page-shell.tsx +4 -1
  49. package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +26 -22
  50. package/src/features/chat/features/conversation/components/session-conversation-area.tsx +2 -0
  51. package/src/features/chat/features/input/input-surface-plugins/__tests__/context-reference-plugin.test.ts +1 -0
  52. package/src/features/chat/features/input/input-surface-plugins/__tests__/slash-command-plugin.test.ts +1 -0
  53. package/src/features/chat/features/message/components/chat-inline-panel-app-card.tsx +1 -3
  54. package/src/features/chat/features/message/components/chat-message-list.container.tsx +15 -1
  55. package/src/features/chat/features/message/components/chat-tool-call-stress-harness.tsx +178 -0
  56. package/src/features/chat/features/message/utils/__tests__/chat-message-process-summary.utils.test.ts +21 -0
  57. package/src/features/chat/features/message/utils/__tests__/chat-tool-call-stress-fixture.utils.test.ts +56 -0
  58. package/src/features/chat/features/message/utils/chat-message-core.utils.ts +1 -1
  59. package/src/features/chat/features/message/utils/chat-message-part.utils.ts +3 -10
  60. package/src/features/chat/features/message/utils/chat-message-process-summary.utils.ts +29 -1
  61. package/src/features/chat/features/message/utils/chat-message-texts.utils.ts +3 -0
  62. package/src/features/chat/features/message/utils/chat-message-tool-card.utils.ts +3 -28
  63. package/src/features/chat/features/message/utils/chat-tool-call-stress-fixture.utils.ts +122 -0
  64. package/src/features/chat/features/ncp/hooks/__tests__/use-ncp-session-conversation.test.tsx +69 -0
  65. package/src/features/chat/features/ncp/hooks/use-ncp-session-conversation.ts +29 -0
  66. package/src/features/chat/features/ncp/hooks/use-ncp-session-message-history.ts +106 -1
  67. package/src/features/chat/features/session/utils/ncp-session-adapter.utils.ts +2 -12
  68. package/src/features/chat/managers/__tests__/chat-ui.manager.test.ts +4 -2
  69. package/src/features/panel-apps/components/__tests__/panel-app-list-item.test.tsx +44 -1
  70. package/src/features/panel-apps/components/__tests__/panel-app-main-sidebar-nav.test.tsx +82 -0
  71. package/src/features/panel-apps/components/__tests__/panel-app-toolbar.test.tsx +13 -6
  72. package/src/features/panel-apps/components/__tests__/panel-apps-list.test.tsx +7 -1
  73. package/src/features/panel-apps/components/panel-app-icon.tsx +48 -0
  74. package/src/features/panel-apps/components/panel-app-list-item.tsx +19 -28
  75. package/src/features/panel-apps/components/panel-app-main-sidebar-nav.tsx +72 -0
  76. package/src/features/panel-apps/components/panel-app-toolbar.tsx +81 -20
  77. package/src/features/panel-apps/components/panel-apps-list.tsx +13 -26
  78. package/src/features/panel-apps/hooks/__tests__/use-panel-apps.test.tsx +105 -0
  79. package/src/features/panel-apps/hooks/use-panel-app-client-grant.ts +50 -0
  80. package/src/features/panel-apps/hooks/use-panel-app-main-runtime.ts +66 -0
  81. package/src/features/panel-apps/hooks/use-panel-apps.ts +102 -13
  82. package/src/features/panel-apps/index.ts +2 -0
  83. package/src/features/panel-apps/managers/__tests__/panel-app-bridge.manager.test.ts +1 -14
  84. package/src/features/panel-apps/managers/panel-app-bridge.manager.ts +9 -5
  85. package/src/features/panel-apps/pages/__tests__/panel-app-main-page.test.tsx +183 -0
  86. package/src/features/panel-apps/pages/panel-app-main-page.tsx +125 -0
  87. package/src/features/panel-apps/utils/__tests__/panel-app-view.utils.test.ts +2 -1
  88. package/src/features/panel-apps/utils/panel-app-doc-browser.utils.tsx +5 -4
  89. package/src/features/right-panel-resources/configs/right-panel-resource-routes.config.ts +12 -25
  90. package/src/features/right-panel-resources/index.ts +1 -0
  91. package/src/features/right-panel-resources/utils/__tests__/right-panel-resource-uri.utils.test.ts +22 -0
  92. package/src/features/right-panel-resources/utils/right-panel-resource-uri.utils.ts +31 -2
  93. package/src/main.tsx +24 -11
  94. package/src/shared/components/doc-browser/__tests__/doc-browser.test.tsx +76 -0
  95. package/src/shared/lib/api/ncp-session.types.ts +1 -0
  96. package/src/shared/lib/api/types.ts +3 -0
  97. package/src/shared/lib/api/utils/ncp-session.utils.ts +24 -1
  98. package/src/shared/lib/i18n/locales/en-US/chat.json +4 -0
  99. package/src/shared/lib/i18n/locales/en-US/doc-browser.json +10 -0
  100. package/src/shared/lib/i18n/locales/zh-CN/chat.json +4 -0
  101. package/src/shared/lib/i18n/locales/zh-CN/doc-browser.json +10 -0
  102. package/dist/assets/api-BSsQ0I4l.js +0 -13
  103. package/dist/assets/appearance-settings-page-2ilQwMl0.js +0 -1
  104. package/dist/assets/chat-page-DDR-Q4Kf.js +0 -1
  105. package/dist/assets/config-split-page-aUTKtWJu.js +0 -1
  106. package/dist/assets/desktop-update-config-Ds4pN7kz.js +0 -1
  107. package/dist/assets/doc-browser-D0arfzHY.js +0 -1
  108. package/dist/assets/doc-browser-D73yN8ts.js +0 -1
  109. package/dist/assets/form-actions-Cpd7glh8.js +0 -1
  110. package/dist/assets/index-Bsoscjk7.css +0 -1
  111. package/dist/assets/index-C2N1bYyb.js +0 -109
  112. package/dist/assets/loader-circle-C1fL3kBZ.js +0 -1
  113. package/dist/assets/mcp-marketplace-page-D8V0GU4Y.js +0 -9
  114. package/dist/assets/mcp-marketplace-page-q6T-vKoH.js +0 -1
  115. package/dist/assets/model-config-page-B7QR33ED.js +0 -1
  116. package/dist/assets/plus-B9V7Z_Eu.js +0 -1
  117. package/dist/assets/providers-config-page-B5yakiZe.js +0 -1
  118. package/dist/assets/react-D-jOo51s.js +0 -8
  119. package/dist/assets/remote-BfRYDhkz.js +0 -1
  120. package/dist/assets/rotate-cw-DuCF-3PX.js +0 -1
  121. package/dist/assets/runtime-config-page-Big45YWb.js +0 -1
  122. package/dist/assets/secrets-config-page-DE3kmDHk.js +0 -3
  123. package/dist/assets/security-config-Ds_2SXBh.js +0 -1
  124. package/dist/assets/skeleton-Bs2eO2CA.js +0 -1
  125. package/dist/assets/x-BkjTUiww.js +0 -1
@@ -15,6 +15,7 @@ import { ChatConversationTrack } from "@/features/chat/components/conversation/c
15
15
  import { IconActionButton } from "@/shared/components/ui/actions/icon-action-button";
16
16
  import { SCROLL_BOTTOM_EDGE_FADE_CLASS } from "@/shared/components/ui/scroll-area";
17
17
  import { t } from "@/shared/lib/i18n";
18
+ import type { SessionMessageToolPayloadState } from "@/features/chat/features/ncp/hooks/use-ncp-session-message-history";
18
19
 
19
20
  type ChatConversationContentProps = {
20
21
  bottomSlot?: ReactNode;
@@ -27,9 +28,11 @@ type ChatConversationContentProps = {
27
28
  isSending: boolean;
28
29
  messageActionsDisabled?: boolean;
29
30
  messages: readonly NcpMessage[];
31
+ messageDetailStates?: Readonly<Record<string, SessionMessageToolPayloadState>>;
30
32
  sessionKey: string | null;
31
33
  showWelcome: boolean;
32
34
  onLoadPreviousMessages: () => Promise<void>;
35
+ onLoadMessageDetails?: (messageId: string) => Promise<void>;
33
36
  onContinueRun?: () => Promise<void> | void;
34
37
  onEditMessage?: (payload: {
35
38
  readonly message: NcpMessage;
@@ -49,9 +52,11 @@ export function ChatConversationContent({
49
52
  isSending,
50
53
  messageActionsDisabled = false,
51
54
  messages,
55
+ messageDetailStates,
52
56
  sessionKey,
53
57
  showWelcome,
54
58
  onLoadPreviousMessages,
59
+ onLoadMessageDetails,
55
60
  onContinueRun,
56
61
  onEditMessage,
57
62
  welcomeSlot,
@@ -121,10 +126,12 @@ export function ChatConversationContent({
121
126
  <ChatMessageListContainer
122
127
  canContinue={canContinue}
123
128
  messages={messages}
129
+ messageDetailStates={messageDetailStates}
124
130
  isSending={isSending}
125
131
  messageActionsDisabled={messageActionsDisabled}
126
132
  onContinueRun={onContinueRun}
127
133
  onEditMessage={onEditMessage}
134
+ onLoadMessageDetails={onLoadMessageDetails}
128
135
  scrollRef={threadRef}
129
136
  sessionKey={sessionKey}
130
137
  />
@@ -7,7 +7,8 @@ import { MarketplacePage } from "@/features/marketplace";
7
7
  import { useViewportLayout } from "@/app/hooks/use-viewport-layout";
8
8
  import { ChatMobileShell } from "@/platforms/mobile";
9
9
  import { InboxPage } from "@/features/inbox";
10
- export type MainPanelView = "chat" | "cron" | "skills" | "agents" | "inbox";
10
+ import { PanelAppMainPage } from "@/features/panel-apps";
11
+ export type MainPanelView = "chat" | "cron" | "skills" | "agents" | "inbox" | "panel-app";
11
12
  export type ChatPageProps = {
12
13
  view: MainPanelView;
13
14
  };
@@ -48,6 +49,8 @@ export function ChatPageLayout({ view, confirmDialog }: ChatPageLayoutProps) {
48
49
  <div className={`mx-auto flex h-full min-h-0 w-full flex-col py-4 sm:px-6 sm:py-5 ${MANAGEMENT_PAGE_CANVAS_WIDTH_CLASS}`}>
49
50
  <InboxPage />
50
51
  </div>
52
+ ) : view === "panel-app" ? (
53
+ <PanelAppMainPage />
51
54
  ) : view === "cron" ? (
52
55
  <div className="h-full overflow-auto custom-scrollbar">
53
56
  <div className={`mx-auto w-full px-4 py-4 sm:px-6 sm:py-5 ${MANAGEMENT_PAGE_CANVAS_WIDTH_CLASS}`}>
@@ -25,6 +25,7 @@ import {
25
25
  import { useInboxUnreadCount } from "@/features/inbox";
26
26
  import { ChatSidebarSessionList } from "@/features/chat/features/session/components/chat-sidebar-session-list";
27
27
  import { ChatSidebarUtilityMenu } from "@/features/chat/components/layout/chat-sidebar-utility-menu";
28
+ import { PanelAppMainSidebarNav } from "@/features/panel-apps";
28
29
  import { isWindowsDesktopHost } from "@/platforms/desktop";
29
30
  import { viewportLayoutManager } from "@/app/managers/viewport-layout.manager";
30
31
  import {
@@ -108,28 +109,31 @@ export function ChatSidebarDesktopNav({
108
109
  }) {
109
110
  const unreadCount = useInboxUnreadCount();
110
111
  return (
111
- <div className={cn("pb-1", isCollapsed ? "px-0" : "px-3")}>
112
- <ul className={isCollapsed ? SIDEBAR_RAIL_STACK_CLASS : "space-y-0.5"}>
113
- {navItems.map((item) => (
114
- <li
115
- key={item.target}
116
- className={isCollapsed ? "flex justify-center" : undefined}
117
- >
118
- <SidebarNavLinkItem
119
- to={item.target}
120
- label={item.label()}
121
- icon={item.icon}
122
- density="compact"
123
- collapsed={isCollapsed}
124
- indicator={item.target === "/inbox" && unreadCount > 0}
125
- trailing={item.target === "/inbox" && unreadCount > 0
126
- ? unreadCount > 99 ? "99+" : unreadCount
127
- : undefined}
128
- className={isCollapsed ? undefined : "rounded-lg px-2.5 py-1.5"}
129
- />
130
- </li>
131
- ))}
132
- </ul>
112
+ <div className="pb-1">
113
+ <div className={isCollapsed ? "px-0" : "px-3"}>
114
+ <ul className={isCollapsed ? SIDEBAR_RAIL_STACK_CLASS : "space-y-0.5"}>
115
+ {navItems.map((item) => (
116
+ <li
117
+ key={item.target}
118
+ className={isCollapsed ? "flex justify-center" : undefined}
119
+ >
120
+ <SidebarNavLinkItem
121
+ to={item.target}
122
+ label={item.label()}
123
+ icon={item.icon}
124
+ density="compact"
125
+ collapsed={isCollapsed}
126
+ indicator={item.target === "/inbox" && unreadCount > 0}
127
+ trailing={item.target === "/inbox" && unreadCount > 0
128
+ ? unreadCount > 99 ? "99+" : unreadCount
129
+ : undefined}
130
+ className={isCollapsed ? undefined : "rounded-lg px-2.5 py-1.5"}
131
+ />
132
+ </li>
133
+ ))}
134
+ </ul>
135
+ </div>
136
+ <PanelAppMainSidebarNav isCollapsed={isCollapsed} />
133
137
  </div>
134
138
  );
135
139
  }
@@ -424,9 +424,11 @@ export function SessionConversationArea(props: SessionConversationAreaProps) {
424
424
  )}
425
425
  bottomSlot={showWelcome ? null : conversationFailureSlot}
426
426
  messages={agent.visibleMessages}
427
+ messageDetailStates={agent.messageDetailStates}
427
428
  sessionKey={sessionKey}
428
429
  showWelcome={showWelcome}
429
430
  onLoadPreviousMessages={agent.loadPreviousMessages}
431
+ onLoadMessageDetails={agent.loadMessageDetails}
430
432
  onContinueRun={() => controllerRef.current.continueRun()}
431
433
  onEditMessage={(payload) => controllerRef.current.editMessage(payload)}
432
434
  welcomeSlot={
@@ -23,6 +23,7 @@ function createPanelApp(): PanelAppEntryView {
23
23
  updatedAt: '2026-06-18T00:00:00.000Z',
24
24
  sizeBytes: 100,
25
25
  favorite: false,
26
+ mainSidebar: false,
26
27
  clientDeclared: false,
27
28
  clientGranted: false,
28
29
  openCount: 0,
@@ -18,6 +18,7 @@ function createPanelApp(
18
18
  updatedAt: overrides.updatedAt ?? '2026-06-18T00:00:00.000Z',
19
19
  sizeBytes: overrides.sizeBytes ?? 100,
20
20
  favorite: overrides.favorite ?? false,
21
+ mainSidebar: overrides.mainSidebar ?? false,
21
22
  clientDeclared: overrides.clientDeclared ?? false,
22
23
  clientGranted: overrides.clientGranted ?? false,
23
24
  lastOpenedAt: overrides.lastOpenedAt,
@@ -102,13 +102,11 @@ export function ChatInlinePanelAppCard({
102
102
  getPresenter().panelAppBridgeManager.handleIframeMessage({
103
103
  event,
104
104
  iframe: iframeRef.current,
105
- iframeInstanceId,
106
- tab,
107
105
  });
108
106
  };
109
107
  window.addEventListener("message", handleMessage);
110
108
  return () => window.removeEventListener("message", handleMessage);
111
- }, [iframeInstanceId, tab, url]);
109
+ }, [url]);
112
110
 
113
111
  if (!url) {
114
112
  return (
@@ -61,12 +61,14 @@ import {
61
61
  import { useChatMessageActions } from "@/features/chat/features/message/hooks/use-chat-message-actions";
62
62
  import { useChatInlineTokenActions } from "@/features/chat/features/message/hooks/use-chat-inline-token-actions";
63
63
  import { buildServerPathContentUrl } from "@/shared/lib/api";
64
- import { formatDateTime, t } from "@/shared/lib/i18n";
64
+ import { formatDateTime, formatNumber, t } from "@/shared/lib/i18n";
65
65
  import { cn } from "@/shared/lib/utils";
66
+ import type { SessionMessageToolPayloadState } from "@/features/chat/features/ncp/hooks/use-ncp-session-message-history";
66
67
 
67
68
  type ChatMessageListContainerProps = {
68
69
  canContinue?: boolean;
69
70
  messages: readonly NcpMessage[];
71
+ messageDetailStates?: Readonly<Record<string, SessionMessageToolPayloadState>>;
70
72
  isSending: boolean;
71
73
  messageActionsDisabled?: boolean;
72
74
  onContinueRun?: () => Promise<void> | void;
@@ -74,6 +76,7 @@ type ChatMessageListContainerProps = {
74
76
  readonly message: NcpMessage;
75
77
  readonly messageId: string;
76
78
  }) => Promise<void> | void;
79
+ onLoadMessageDetails?: (messageId: string) => Promise<void>;
77
80
  sessionKey: string | null;
78
81
  scrollRef: RefObject<HTMLDivElement | null>;
79
82
  className?: string;
@@ -105,6 +108,13 @@ class ChatMessageViewModelAdapter {
105
108
  const processSummary = buildChatMessageProcessSummary({
106
109
  message,
107
110
  processedLabel,
111
+ formatDeferredToolSummary: (toolCallCount, toolNames) => {
112
+ const countLabel = t("chatProcessSummaryToolCalls", language)
113
+ .replace("{count}", formatNumber(toolCallCount, language));
114
+ if (toolNames.length === 0) return countLabel;
115
+ const separator = language === "zh" ? "、" : ", ";
116
+ return `${countLabel} · ${toolNames.join(separator)}`;
117
+ },
108
118
  });
109
119
  const processSummaryLabel = processSummary?.label ?? null;
110
120
  const executionPresentation = buildChatMessageExecutionPresentation({
@@ -199,10 +209,12 @@ function isAwaitingAssistantOutputRow(
199
209
  export function ChatMessageListContainer({
200
210
  canContinue = false,
201
211
  messages: rawMessages,
212
+ messageDetailStates,
202
213
  isSending,
203
214
  messageActionsDisabled = false,
204
215
  onContinueRun,
205
216
  onEditMessage,
217
+ onLoadMessageDetails,
206
218
  scrollRef,
207
219
  sessionKey,
208
220
  className,
@@ -414,6 +426,8 @@ export function ChatMessageListContainer({
414
426
  onAttachmentOpen={handleAttachmentOpen}
415
427
  onInlineTokenClick={handleInlineTokenClick}
416
428
  onMessageAction={handleMessageAction}
429
+ resolveMessageToolPayloadState={(messageId) => messageDetailStates?.[messageId]}
430
+ onMessageToolPayloadRequest={onLoadMessageDetails}
417
431
  resolveFileContentUrl={resolveFileContentUrl}
418
432
  renderCustomPart={renderCustomPart}
419
433
  renderInlineDisplay={renderInlineDisplayWithFiles}
@@ -0,0 +1,178 @@
1
+ import { Profiler, useCallback, useEffect, useMemo, useRef, useState } from "react";
2
+ import { ChatMessageList, type ChatMessageTexts } from "@nextclaw/agent-chat-ui";
3
+ import {
4
+ createChatToolCallStressViewModel,
5
+ DEFAULT_CHAT_TOOL_CALL_STRESS_CONFIG,
6
+ type ChatToolCallStressConfig,
7
+ } from "@/features/chat/features/message/utils/chat-tool-call-stress-fixture.utils";
8
+
9
+ type StressMetric = {
10
+ action: string;
11
+ commitMs: number;
12
+ nextFrameMs: number;
13
+ };
14
+
15
+ declare global {
16
+ interface Window {
17
+ __nextclawChatToolStress?: {
18
+ latestMetric: StressMetric | null;
19
+ config: ChatToolCallStressConfig;
20
+ };
21
+ }
22
+ }
23
+
24
+ const STRESS_TEXTS: ChatMessageTexts = {
25
+ copyCodeLabel: "Copy",
26
+ copiedCodeLabel: "Copied",
27
+ copyMessageLabel: "Copy",
28
+ copiedMessageLabel: "Copied",
29
+ typingLabel: "Thinking…",
30
+ toolActivitySegmentTemplates: {
31
+ read: { one: "Read 1 file", other: "Read {count} files" },
32
+ edit: { one: "Edit 1 file", other: "Edit {count} files" },
33
+ directory: { one: "View 1 directory", other: "View {count} directories" },
34
+ search: { one: "Search 1 time", other: "Search {count} times" },
35
+ bash: { one: "Run 1 command", other: "Run {count} commands" },
36
+ web: { one: "Open 1 page", other: "Open {count} pages" },
37
+ agent: { one: "Start 1 subtask", other: "Start {count} subtasks" },
38
+ panel: { one: "Show 1 result", other: "Show {count} results" },
39
+ other: { one: "Use 1 tool", other: "Use {count} tools" },
40
+ },
41
+ toolActivityFailedLabel: "failed",
42
+ toolActivityCancelledLabel: "cancelled",
43
+ };
44
+
45
+ function readBoundedNumber(value: string, fallback: number, maximum: number): number {
46
+ const parsed = Number(value);
47
+ return Number.isFinite(parsed)
48
+ ? Math.max(1, Math.min(maximum, Math.round(parsed)))
49
+ : fallback;
50
+ }
51
+
52
+ export function ChatToolCallStressHarness() {
53
+ const [config, setConfig] = useState(DEFAULT_CHAT_TOOL_CALL_STRESS_CONFIG);
54
+ const [renderVersion, setRenderVersion] = useState(0);
55
+ const [metric, setMetric] = useState<StressMetric | null>(null);
56
+ const pendingMeasurement = useRef<{ action: string; startedAt: number } | null>(null);
57
+ const viewModel = useMemo(
58
+ () => createChatToolCallStressViewModel(config, `chat-tool-call-stress-message-${renderVersion}`),
59
+ [config, renderVersion],
60
+ );
61
+ const publishMetric = useCallback((nextMetric: StressMetric | null) => {
62
+ setMetric(nextMetric);
63
+ window.__nextclawChatToolStress = { latestMetric: nextMetric, config };
64
+ }, [config]);
65
+ const startMeasurement = useCallback((action: string, update: () => void) => {
66
+ pendingMeasurement.current = { action, startedAt: performance.now() };
67
+ update();
68
+ }, []);
69
+
70
+ useEffect(() => {
71
+ window.__nextclawChatToolStress = { latestMetric: metric, config };
72
+ }, [config, metric]);
73
+
74
+ return (
75
+ <main className="min-h-screen bg-background p-6 text-foreground">
76
+ <section className="mx-auto max-w-5xl space-y-5">
77
+ <header className="space-y-2">
78
+ <h1 className="text-xl font-semibold">Chat tool-call stress harness</h1>
79
+ <p className="max-w-3xl text-sm text-muted-foreground">
80
+ One assistant message with dense, nested NCP tool invocations. This data remains in the browser and is never written into a session.
81
+ </p>
82
+ </header>
83
+ <div className="grid gap-3 rounded-lg border border-border p-4 sm:grid-cols-3">
84
+ {([
85
+ ["toolCallCount", "Tool calls", 1_000],
86
+ ["argumentBytesPerCall", "Argument bytes / call", 65_536],
87
+ ["resultBytesPerCall", "Result bytes / call", 65_536],
88
+ ] as const).map(([key, label, maximum]) => (
89
+ <label key={key} className="grid gap-1 text-sm">
90
+ <span>{label}</span>
91
+ <input
92
+ aria-label={label}
93
+ className="rounded border border-input bg-background px-2 py-1"
94
+ min="1"
95
+ max={maximum}
96
+ type="number"
97
+ value={config[key]}
98
+ onChange={(event) => setConfig((current) => ({
99
+ ...current,
100
+ [key]: readBoundedNumber(event.target.value, current[key], maximum),
101
+ }))}
102
+ />
103
+ </label>
104
+ ))}
105
+ </div>
106
+ <div className="flex flex-wrap gap-2">
107
+ <button
108
+ type="button"
109
+ className="rounded bg-primary px-3 py-1.5 text-sm text-primary-foreground"
110
+ onClick={() => startMeasurement("rerender", () => setRenderVersion((value) => value + 1))}
111
+ >
112
+ Re-render message
113
+ </button>
114
+ <button
115
+ type="button"
116
+ className="rounded border border-border px-3 py-1.5 text-sm"
117
+ onClick={() => {
118
+ const summary = Array.from(
119
+ document.querySelectorAll<HTMLButtonElement>("[data-chat-process-meta-row]"),
120
+ ).find((candidate) => candidate.parentElement?.classList.contains("group/tool-activity"));
121
+ if (!summary) return;
122
+ const startedAt = performance.now();
123
+ const observer = new MutationObserver(() => {
124
+ observer.disconnect();
125
+ requestAnimationFrame(() => publishMetric({
126
+ action: summary.getAttribute("aria-expanded") === "true" ? "expand" : "collapse",
127
+ commitMs: performance.now() - startedAt,
128
+ nextFrameMs: performance.now() - startedAt,
129
+ }));
130
+ });
131
+ observer.observe(document.body, { childList: true, subtree: true });
132
+ summary.click();
133
+ }}
134
+ >
135
+ Toggle tool details
136
+ </button>
137
+ <button
138
+ type="button"
139
+ className="rounded border border-border px-3 py-1.5 text-sm"
140
+ onClick={() => startMeasurement("stream-update", () => setConfig((current) => ({
141
+ ...current,
142
+ resultBytesPerCall: Math.min(65_536, current.resultBytesPerCall + 1),
143
+ })))}
144
+ >
145
+ Simulate stream update
146
+ </button>
147
+ </div>
148
+ <output aria-live="polite" data-stress-metric="true" className="block rounded bg-muted px-3 py-2 font-mono text-xs">
149
+ {metric
150
+ ? `${metric.action}: commit ${metric.commitMs.toFixed(1)}ms, next frame ${metric.nextFrameMs.toFixed(1)}ms`
151
+ : "Run an action to record a browser metric."}
152
+ </output>
153
+ <Profiler
154
+ id="chat-tool-call-stress"
155
+ onRender={(_id, _phase, actualDuration) => {
156
+ const pending = pendingMeasurement.current;
157
+ if (!pending) return;
158
+ pendingMeasurement.current = null;
159
+ requestAnimationFrame(() => publishMetric({
160
+ action: pending.action,
161
+ commitMs: actualDuration,
162
+ nextFrameMs: performance.now() - pending.startedAt,
163
+ }));
164
+ }}
165
+ >
166
+ <section data-stress-message="true" className="rounded-lg border border-border p-4">
167
+ <ChatMessageList
168
+ messages={[viewModel]}
169
+ isSending={false}
170
+ hasAssistantDraft={false}
171
+ texts={STRESS_TEXTS}
172
+ />
173
+ </section>
174
+ </Profiler>
175
+ </section>
176
+ </main>
177
+ );
178
+ }
@@ -66,4 +66,25 @@ describe("buildChatMessageProcessSummary", () => {
66
66
  expect(summary?.label.toLowerCase()).not.toContain("bash");
67
67
  expect(summary?.label.toLowerCase()).not.toContain("read");
68
68
  });
69
+
70
+ it("shows the bounded aggregate for a deferred history payload", () => {
71
+ const summary = buildChatMessageProcessSummary({
72
+ message: {
73
+ ...baseAssistantMessage,
74
+ metadata: {
75
+ nextclawUiHistoryToolPayloadSummary: {
76
+ toolCallCount: 500,
77
+ toolNames: ["exec_command", "read_file"],
78
+ },
79
+ },
80
+ },
81
+ processedLabel: "Processed",
82
+ formatDeferredToolSummary: (count, names) =>
83
+ `${count} tool calls · ${names.join(", ")}`,
84
+ });
85
+
86
+ expect(summary?.label).toBe(
87
+ "Processed · 500 tool calls · exec_command, read_file",
88
+ );
89
+ });
69
90
  });
@@ -0,0 +1,56 @@
1
+ import { expect, it } from "vitest";
2
+ import {
3
+ createChatToolCallStressMessage,
4
+ createChatToolCallStressViewModel,
5
+ } from "@/features/chat/features/message/utils/chat-tool-call-stress-fixture.utils";
6
+
7
+ it("builds one assistant message with dense nested NCP tool invocations", () => {
8
+ const message = createChatToolCallStressMessage({
9
+ toolCallCount: 120,
10
+ argumentBytesPerCall: 4_096,
11
+ resultBytesPerCall: 6_144,
12
+ });
13
+
14
+ expect(message.parts).toHaveLength(122);
15
+ const firstTool = message.parts[1];
16
+ expect(firstTool).toMatchObject({
17
+ type: "tool-invocation",
18
+ toolCallId: "stress-call-0",
19
+ state: "result",
20
+ });
21
+ expect(firstTool).toMatchObject({
22
+ args: {
23
+ request: {
24
+ nested: { levelOne: { levelTwo: { levelThree: { payload: expect.any(String) } } } },
25
+ },
26
+ },
27
+ });
28
+ });
29
+
30
+ it("adapts the stress message through the regular tool-card presentation path", () => {
31
+ const message = createChatToolCallStressViewModel({
32
+ toolCallCount: 120,
33
+ argumentBytesPerCall: 4_096,
34
+ resultBytesPerCall: 6_144,
35
+ });
36
+
37
+ const toolCards = message.parts.filter((part) => part.type === "tool-card");
38
+ expect(toolCards).toHaveLength(120);
39
+ expect(toolCards[0]).toMatchObject({
40
+ card: {
41
+ toolCallId: "stress-call-0",
42
+ input: undefined,
43
+ output: undefined,
44
+ inputData: {
45
+ request: {
46
+ metadata: { labels: expect.arrayContaining(["nested-payload"]) },
47
+ },
48
+ },
49
+ outputData: {
50
+ request: {
51
+ metadata: { labels: expect.arrayContaining(["nested-payload"]) },
52
+ },
53
+ },
54
+ },
55
+ });
56
+ });
@@ -93,7 +93,7 @@ export function summarizeToolArgs(args: unknown): string | undefined {
93
93
  if (items.length > 0) {
94
94
  return truncateInlineText(items.join(' · '), 120);
95
95
  }
96
- return truncateInlineText(stringifyUnknown(parsed), 140);
96
+ return undefined;
97
97
  }
98
98
 
99
99
  function toToolName(value: unknown): string {
@@ -1,7 +1,4 @@
1
- import {
2
- stringifyUnknown,
3
- summarizeToolArgs,
4
- } from "@/features/chat/features/message/utils/chat-message-core.utils";
1
+ import { stringifyUnknown, summarizeToolArgs } from "@/features/chat/features/message/utils/chat-message-core.utils";
5
2
  import { type ChatInlineTokenSource } from "@/features/chat/features/input/utils/chat-inline-token.utils";
6
3
  import {
7
4
  buildRenderableText,
@@ -9,7 +6,6 @@ import {
9
6
  } from "./chat-message-markdown-part.utils";
10
7
  import {
11
8
  buildToolCard,
12
- buildToolInvocationInput,
13
9
  extractAssetFileView,
14
10
  isTerminalResultRecord,
15
11
  readOptionalNumber,
@@ -155,15 +151,12 @@ function buildToolInvocationPart(
155
151
  const detail = invalidArgsIssue
156
152
  ? `Invalid arguments: ${invalidArgsIssue}`
157
153
  : fileOperationCardData?.summary ?? summarizeToolArgs(invocation.parsedArgs ?? invocation.args);
158
- const input = fileOperationCardData ? undefined : buildToolInvocationInput(invocation.args, invocation.parsedArgs);
159
154
  const rawResult =
160
155
  invalidArgsIssue
161
156
  ? invalidArgsIssue
162
157
  : typeof invocation.error === "string" && invocation.error.trim()
163
158
  ? invocation.error.trim()
164
- : invocation.result != null
165
- ? stringifyUnknown(invocation.result).trim()
166
- : "";
159
+ : "";
167
160
  const shouldHideStructuredTerminalJson =
168
161
  !invocation.error && isTerminalResultRecord(invocation.result);
169
162
  const shouldShowRawResult =
@@ -175,7 +168,7 @@ function buildToolInvocationPart(
175
168
  name: invocation.toolName,
176
169
  ...(agentId ? { agentId } : {}),
177
170
  detail,
178
- ...(input ? { input } : {}),
171
+ inputData: fileOperationCardData ? undefined : invocation.parsedArgs ?? invocation.args,
179
172
  text: shouldShowRawResult && rawResult ? rawResult : undefined,
180
173
  outputData: invocation.result,
181
174
  callId: invocation.toolCallId || undefined,
@@ -4,8 +4,26 @@ import type { ChatMessageProcessSummarySource } from "@/features/chat/types/chat
4
4
  type BuildChatMessageProcessSummaryParams = {
5
5
  message: NcpMessage;
6
6
  processedLabel: string;
7
+ formatDeferredToolSummary?: (toolCallCount: number, toolNames: readonly string[]) => string;
7
8
  };
8
9
 
10
+ const UI_HISTORY_TOOL_PAYLOAD_SUMMARY_METADATA_KEY =
11
+ "nextclawUiHistoryToolPayloadSummary";
12
+
13
+ function readDeferredToolSummary(
14
+ message: NcpMessage,
15
+ ): { toolCallCount: number; toolNames: string[] } | null {
16
+ const value = message.metadata?.[UI_HISTORY_TOOL_PAYLOAD_SUMMARY_METADATA_KEY];
17
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
18
+ const record = value as Record<string, unknown>;
19
+ const { toolCallCount } = record;
20
+ if (!Number.isInteger(toolCallCount) || (toolCallCount as number) <= 0) return null;
21
+ const toolNames = Array.isArray(record.toolNames)
22
+ ? record.toolNames.filter((name): name is string => typeof name === "string" && name.trim().length > 0)
23
+ : [];
24
+ return { toolCallCount: toolCallCount as number, toolNames };
25
+ }
26
+
9
27
  function isAssistantProcessPart(part: NcpMessage["parts"][number]): boolean {
10
28
  return part.type === "reasoning" || part.type === "tool-invocation";
11
29
  }
@@ -74,6 +92,7 @@ function formatLifecycleDuration(message: NcpMessage): string | null {
74
92
  * and must not be mixed into this summary.
75
93
  */
76
94
  export function buildChatMessageProcessSummary({
95
+ formatDeferredToolSummary,
77
96
  message,
78
97
  processedLabel,
79
98
  }: BuildChatMessageProcessSummaryParams): ChatMessageProcessSummarySource | undefined {
@@ -81,5 +100,14 @@ export function buildChatMessageProcessSummary({
81
100
  return undefined;
82
101
  }
83
102
  const duration = formatLifecycleDuration(message);
84
- return { label: duration ? `${processedLabel} ${duration}` : processedLabel };
103
+ const baseLabel = duration ? `${processedLabel} ${duration}` : processedLabel;
104
+ const deferredToolSummary = readDeferredToolSummary(message);
105
+ return {
106
+ label: deferredToolSummary && formatDeferredToolSummary
107
+ ? `${baseLabel} · ${formatDeferredToolSummary(
108
+ deferredToolSummary.toolCallCount,
109
+ deferredToolSummary.toolNames,
110
+ )}`
111
+ : baseLabel,
112
+ };
85
113
  }
@@ -247,5 +247,8 @@ export function buildChatMessageTexts(
247
247
  },
248
248
  toolActivityFailedLabel: t("chatProcessSummaryFailed"),
249
249
  toolActivityCancelledLabel: t("chatProcessSummaryCancelled"),
250
+ toolPayloadLoadingLabel: t("chatToolPayloadLoading"),
251
+ toolPayloadLoadFailedLabel: t("chatToolPayloadLoadFailed"),
252
+ toolActivityShowMoreTemplate: t("chatToolActivityShowMore"),
250
253
  };
251
254
  }
@@ -1,7 +1,4 @@
1
- import {
2
- stringifyUnknown,
3
- type ToolCard,
4
- } from "@/features/chat/features/message/utils/chat-message-core.utils";
1
+ import type { ToolCard } from "@/features/chat/features/message/utils/chat-message-core.utils";
5
2
  import type {
6
3
  ChatMessagePartViewModel,
7
4
  ChatToolPartViewModel,
@@ -14,6 +11,7 @@ export type ToolCardViewSource = ToolCard & {
14
11
  agentId?: string;
15
12
  action?: ChatToolPartViewModel["action"];
16
13
  fileOperation?: ChatToolPartViewModel["fileOperation"];
14
+ inputData?: unknown;
17
15
  outputData?: unknown;
18
16
  execution?: ChatToolPartViewModel["execution"];
19
17
  toolCallId?: string;
@@ -168,6 +166,7 @@ export function buildToolCard(
168
166
  "input" in toolCard && typeof toolCard.input === "string"
169
167
  ? toolCard.input
170
168
  : undefined,
169
+ inputData: toolCard.inputData,
171
170
  output: toolCard.text,
172
171
  outputData: toolCard.outputData,
173
172
  ...(toolCard.execution ? { execution: { ...toolCard.execution } } : {}),
@@ -245,27 +244,3 @@ export function resolveToolCardStatus(params: {
245
244
  statusLabel: texts.toolStatusRunningLabel,
246
245
  };
247
246
  }
248
-
249
- function parseStructuredValue(value: unknown): unknown {
250
- if (typeof value !== "string") {
251
- return value;
252
- }
253
- const trimmed = value.trim();
254
- if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) {
255
- return value;
256
- }
257
- try {
258
- return JSON.parse(trimmed) as unknown;
259
- } catch {
260
- return value;
261
- }
262
- }
263
-
264
- export function buildToolInvocationInput(
265
- args?: unknown,
266
- parsedArgs?: unknown,
267
- ): string | undefined {
268
- const source = parsedArgs ?? parseStructuredValue(args);
269
- const text = stringifyUnknown(source).trim();
270
- return text || undefined;
271
- }