@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.
- package/CHANGELOG.md +26 -0
- package/dist/assets/api-Cyxp93Fn.js +13 -0
- package/dist/assets/appearance-settings-page-DsMl931U.js +1 -0
- package/dist/assets/{book-open-BOJLGVsx.js → book-open-Cl5CzL0g.js} +1 -1
- package/dist/assets/{channels-list-page-BPy2mXoD.js → channels-list-page-D_B7JGoZ.js} +2 -2
- package/dist/assets/chat-page-D3ZeULTv.js +1 -0
- package/dist/assets/config-split-page-C-YVl2mf.js +1 -0
- package/dist/assets/{confirm-dialog-Bsg9A62_.js → confirm-dialog-Bz3ieDQ5.js} +2 -2
- package/dist/assets/desktop-update-config-DO051-QG.js +1 -0
- package/dist/assets/{dist-IBUY9_9h.js → dist-CDPl8mm7.js} +1 -1
- package/dist/assets/{dist-DyUY9uQ0.js → dist-HFDWY6-K.js} +1 -1
- package/dist/assets/doc-browser-CWhs0JQN.js +1 -0
- package/dist/assets/doc-browser-D6H_nPzO.js +1 -0
- package/dist/assets/{doc-browser-context-BM6Af8gN.js → doc-browser-context-BKwz_j44.js} +1 -1
- package/dist/assets/form-actions-BUZOCfPR.js +1 -0
- package/dist/assets/index-DR3GLGme.css +1 -0
- package/dist/assets/index-j6DBnvlr.js +109 -0
- package/dist/assets/loader-circle-s65a4ZzL.js +1 -0
- package/dist/assets/mcp-marketplace-page-BJvzGt7h.js +9 -0
- package/dist/assets/mcp-marketplace-page-EpFHeJzw.js +1 -0
- package/dist/assets/model-config-page-CBoh0DVh.js +1 -0
- package/dist/assets/{navigation-link-DtPZZ_Q_.js → navigation-link-CDMYpkVv.js} +2 -2
- package/dist/assets/plus-XDIkqpkt.js +1 -0
- package/dist/assets/{provider-scoped-model-input-RWZz36z7.js → provider-scoped-model-input-C2iUtZi7.js} +1 -1
- package/dist/assets/providers-config-page-DX1pISTo.js +1 -0
- package/dist/assets/react-B7Paq2b3.js +8 -0
- package/dist/assets/{refresh-cw-BQHORcAs.js → refresh-cw-XrnNVwuQ.js} +1 -1
- package/dist/assets/remote-D_7_OGoj.js +1 -0
- package/dist/assets/rotate-cw-BhIWpLaU.js +1 -0
- package/dist/assets/runtime-config-page-BVO9B8Ns.js +1 -0
- package/dist/assets/{save-BgiS2u-T.js → save-_nSNAYQu.js} +1 -1
- package/dist/assets/{search-B6mO7mmo.js → search-0OTHFRMx.js} +1 -1
- package/dist/assets/{search-config-page-QS-kqNDZ.js → search-config-page-BwyeUjKt.js} +1 -1
- package/dist/assets/secrets-config-page-2O-TzroJ.js +3 -0
- package/dist/assets/security-config-BgIIrg1s.js +1 -0
- package/dist/assets/{select-DToos7Rq.js → select-BZHvnwQB.js} +2 -2
- package/dist/assets/{settings-2-ZAWXrq81.js → settings-2-CM_PvDNk.js} +1 -1
- package/dist/assets/skeleton-Bv4MMbH7.js +1 -0
- package/dist/assets/{tag-chip-urB3N-kk.js → tag-chip-Ce53PMTL.js} +1 -1
- package/dist/assets/x-CCjWN5ys.js +1 -0
- package/dist/index.html +20 -20
- package/package.json +6 -6
- package/src/app/components/layout/sidebar-items.tsx +49 -15
- package/src/app/configs/__tests__/app-navigation.config.test.ts +21 -0
- package/src/app/configs/app-navigation.config.ts +10 -1
- package/src/app/index.tsx +4 -0
- package/src/features/chat/components/conversation/chat-conversation-content.tsx +7 -0
- package/src/features/chat/components/layout/chat-page-shell.tsx +4 -1
- package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +26 -22
- package/src/features/chat/features/conversation/components/session-conversation-area.tsx +2 -0
- package/src/features/chat/features/input/input-surface-plugins/__tests__/context-reference-plugin.test.ts +1 -0
- package/src/features/chat/features/input/input-surface-plugins/__tests__/slash-command-plugin.test.ts +1 -0
- package/src/features/chat/features/message/components/chat-inline-panel-app-card.tsx +1 -3
- package/src/features/chat/features/message/components/chat-message-list.container.tsx +15 -1
- package/src/features/chat/features/message/components/chat-tool-call-stress-harness.tsx +178 -0
- package/src/features/chat/features/message/utils/__tests__/chat-message-process-summary.utils.test.ts +21 -0
- package/src/features/chat/features/message/utils/__tests__/chat-tool-call-stress-fixture.utils.test.ts +56 -0
- package/src/features/chat/features/message/utils/chat-message-core.utils.ts +1 -1
- package/src/features/chat/features/message/utils/chat-message-part.utils.ts +3 -10
- package/src/features/chat/features/message/utils/chat-message-process-summary.utils.ts +29 -1
- package/src/features/chat/features/message/utils/chat-message-texts.utils.ts +3 -0
- package/src/features/chat/features/message/utils/chat-message-tool-card.utils.ts +3 -28
- package/src/features/chat/features/message/utils/chat-tool-call-stress-fixture.utils.ts +122 -0
- package/src/features/chat/features/ncp/hooks/__tests__/use-ncp-session-conversation.test.tsx +69 -0
- package/src/features/chat/features/ncp/hooks/use-ncp-session-conversation.ts +29 -0
- package/src/features/chat/features/ncp/hooks/use-ncp-session-message-history.ts +106 -1
- package/src/features/chat/features/session/utils/ncp-session-adapter.utils.ts +2 -12
- package/src/features/chat/managers/__tests__/chat-ui.manager.test.ts +4 -2
- package/src/features/panel-apps/components/__tests__/panel-app-list-item.test.tsx +44 -1
- package/src/features/panel-apps/components/__tests__/panel-app-main-sidebar-nav.test.tsx +82 -0
- package/src/features/panel-apps/components/__tests__/panel-app-toolbar.test.tsx +13 -6
- package/src/features/panel-apps/components/__tests__/panel-apps-list.test.tsx +7 -1
- package/src/features/panel-apps/components/panel-app-icon.tsx +48 -0
- package/src/features/panel-apps/components/panel-app-list-item.tsx +19 -28
- package/src/features/panel-apps/components/panel-app-main-sidebar-nav.tsx +72 -0
- package/src/features/panel-apps/components/panel-app-toolbar.tsx +81 -20
- package/src/features/panel-apps/components/panel-apps-list.tsx +13 -26
- package/src/features/panel-apps/hooks/__tests__/use-panel-apps.test.tsx +105 -0
- package/src/features/panel-apps/hooks/use-panel-app-client-grant.ts +50 -0
- package/src/features/panel-apps/hooks/use-panel-app-main-runtime.ts +66 -0
- package/src/features/panel-apps/hooks/use-panel-apps.ts +102 -13
- package/src/features/panel-apps/index.ts +2 -0
- package/src/features/panel-apps/managers/__tests__/panel-app-bridge.manager.test.ts +1 -14
- package/src/features/panel-apps/managers/panel-app-bridge.manager.ts +9 -5
- package/src/features/panel-apps/pages/__tests__/panel-app-main-page.test.tsx +183 -0
- package/src/features/panel-apps/pages/panel-app-main-page.tsx +125 -0
- package/src/features/panel-apps/utils/__tests__/panel-app-view.utils.test.ts +2 -1
- package/src/features/panel-apps/utils/panel-app-doc-browser.utils.tsx +5 -4
- package/src/features/right-panel-resources/configs/right-panel-resource-routes.config.ts +12 -25
- package/src/features/right-panel-resources/index.ts +1 -0
- package/src/features/right-panel-resources/utils/__tests__/right-panel-resource-uri.utils.test.ts +22 -0
- package/src/features/right-panel-resources/utils/right-panel-resource-uri.utils.ts +31 -2
- package/src/main.tsx +24 -11
- package/src/shared/components/doc-browser/__tests__/doc-browser.test.tsx +76 -0
- package/src/shared/lib/api/ncp-session.types.ts +1 -0
- package/src/shared/lib/api/types.ts +3 -0
- package/src/shared/lib/api/utils/ncp-session.utils.ts +24 -1
- package/src/shared/lib/i18n/locales/en-US/chat.json +4 -0
- package/src/shared/lib/i18n/locales/en-US/doc-browser.json +10 -0
- package/src/shared/lib/i18n/locales/zh-CN/chat.json +4 -0
- package/src/shared/lib/i18n/locales/zh-CN/doc-browser.json +10 -0
- package/dist/assets/api-BSsQ0I4l.js +0 -13
- package/dist/assets/appearance-settings-page-2ilQwMl0.js +0 -1
- package/dist/assets/chat-page-DDR-Q4Kf.js +0 -1
- package/dist/assets/config-split-page-aUTKtWJu.js +0 -1
- package/dist/assets/desktop-update-config-Ds4pN7kz.js +0 -1
- package/dist/assets/doc-browser-D0arfzHY.js +0 -1
- package/dist/assets/doc-browser-D73yN8ts.js +0 -1
- package/dist/assets/form-actions-Cpd7glh8.js +0 -1
- package/dist/assets/index-Bsoscjk7.css +0 -1
- package/dist/assets/index-C2N1bYyb.js +0 -109
- package/dist/assets/loader-circle-C1fL3kBZ.js +0 -1
- package/dist/assets/mcp-marketplace-page-D8V0GU4Y.js +0 -9
- package/dist/assets/mcp-marketplace-page-q6T-vKoH.js +0 -1
- package/dist/assets/model-config-page-B7QR33ED.js +0 -1
- package/dist/assets/plus-B9V7Z_Eu.js +0 -1
- package/dist/assets/providers-config-page-B5yakiZe.js +0 -1
- package/dist/assets/react-D-jOo51s.js +0 -8
- package/dist/assets/remote-BfRYDhkz.js +0 -1
- package/dist/assets/rotate-cw-DuCF-3PX.js +0 -1
- package/dist/assets/runtime-config-page-Big45YWb.js +0 -1
- package/dist/assets/secrets-config-page-DE3kmDHk.js +0 -3
- package/dist/assets/security-config-Ds_2SXBh.js +0 -1
- package/dist/assets/skeleton-Bs2eO2CA.js +0 -1
- 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
|
-
|
|
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=
|
|
112
|
-
<
|
|
113
|
-
{
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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={
|
|
@@ -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
|
-
}, [
|
|
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
|
|
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
|
-
:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
}
|