@nextclaw/ui 0.18.4 → 0.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/dist/assets/api-Cyxp93Fn.js +13 -0
- package/dist/assets/appearance-settings-page-NlQIHbXh.js +1 -0
- package/dist/assets/{book-open-BOJLGVsx.js → book-open-Cl5CzL0g.js} +1 -1
- package/dist/assets/{channels-list-page-DKrUSeNz.js → channels-list-page-DhXfsjxF.js} +2 -2
- package/dist/assets/chat-page-C4DHLN6T.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-CpsXBUJK.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-Bfr0I0Ga.js +109 -0
- package/dist/assets/index-DcqrfCMy.css +1 -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-BRScCkS-.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-BFzyywsF.js → provider-scoped-model-input-CD93ZOgZ.js} +1 -1
- package/dist/assets/providers-config-page-CFdgth2A.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-DDN70hyU.js +1 -0
- package/dist/assets/rotate-cw-BhIWpLaU.js +1 -0
- package/dist/assets/runtime-config-page-BxJlm6qT.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-ydeDD1OQ.js → search-config-page-2VAarRcG.js} +1 -1
- package/dist/assets/secrets-config-page-KoFrETds.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 +5 -5
- 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 +57 -2
- package/src/features/panel-apps/components/__tests__/panel-app-main-sidebar-nav.test.tsx +133 -0
- package/src/features/panel-apps/components/__tests__/panel-app-toolbar.test.tsx +68 -7
- 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 +7 -29
- package/src/features/panel-apps/components/panel-app-main-sidebar-menu-item.tsx +37 -0
- package/src/features/panel-apps/components/panel-app-main-sidebar-nav.tsx +123 -0
- package/src/features/panel-apps/components/panel-app-toolbar.tsx +61 -20
- package/src/features/panel-apps/components/panel-apps-list.tsx +5 -27
- 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 +2 -0
- package/src/features/right-panel-resources/utils/__tests__/right-panel-resource-uri.utils.test.ts +21 -0
- package/src/features/right-panel-resources/utils/right-panel-resource-uri.utils.ts +37 -0
- package/src/main.tsx +24 -11
- package/src/shared/components/doc-browser/__tests__/doc-browser.test.tsx +102 -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-CEPxG1w0.js +0 -1
- package/dist/assets/chat-page-sGhIjLSr.js +0 -1
- package/dist/assets/config-split-page-aUTKtWJu.js +0 -1
- package/dist/assets/desktop-update-config-VgvIATTT.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-CtPLj_hk.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-DQ-iQFSO.js +0 -1
- package/dist/assets/plus-B9V7Z_Eu.js +0 -1
- package/dist/assets/providers-config-page-DUYwVuYe.js +0 -1
- package/dist/assets/react-D-jOo51s.js +0 -8
- package/dist/assets/remote-CNFQ6bFY.js +0 -1
- package/dist/assets/rotate-cw-DuCF-3PX.js +0 -1
- package/dist/assets/runtime-config-page-CaWoWoQk.js +0 -1
- package/dist/assets/secrets-config-page-BZYqw8BF.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
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { ChatMessageViewModel } from "@nextclaw/agent-chat-ui";
|
|
2
|
+
import type { NcpMessage } from "@nextclaw/ncp";
|
|
3
|
+
import { adaptNcpMessagePartsForChat, adaptNcpMessageToUiMessage } from "@/features/chat/features/session/utils/ncp-session-adapter.utils";
|
|
4
|
+
import { adaptChatMessage } from "./chat-message.utils";
|
|
5
|
+
|
|
6
|
+
export type ChatToolCallStressConfig = {
|
|
7
|
+
toolCallCount: number;
|
|
8
|
+
argumentBytesPerCall: number;
|
|
9
|
+
resultBytesPerCall: number;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const DEFAULT_CHAT_TOOL_CALL_STRESS_CONFIG: ChatToolCallStressConfig = {
|
|
13
|
+
toolCallCount: 200,
|
|
14
|
+
argumentBytesPerCall: 8_192,
|
|
15
|
+
resultBytesPerCall: 8_192,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const STRESS_TOOL_NAMES = ["exec_command", "read_file", "web_fetch"];
|
|
19
|
+
const FILLER = "0123456789abcdefghijklmnopqrstuvwxyz";
|
|
20
|
+
|
|
21
|
+
const STRESS_ADAPTER_TEXTS = {
|
|
22
|
+
roleLabels: {
|
|
23
|
+
user: "User",
|
|
24
|
+
assistant: "Assistant",
|
|
25
|
+
tool: "Tool",
|
|
26
|
+
system: "System",
|
|
27
|
+
fallback: "Message",
|
|
28
|
+
},
|
|
29
|
+
reasoningLabel: "Reasoning",
|
|
30
|
+
toolCallLabel: "Tool call",
|
|
31
|
+
toolResultLabel: "Tool result",
|
|
32
|
+
toolInputLabel: "Input",
|
|
33
|
+
toolNoOutputLabel: "No output",
|
|
34
|
+
toolOutputLabel: "Output",
|
|
35
|
+
toolStatusPreparingLabel: "Preparing",
|
|
36
|
+
toolStatusRunningLabel: "Running",
|
|
37
|
+
toolStatusCompletedLabel: "Completed",
|
|
38
|
+
toolStatusFailedLabel: "Failed",
|
|
39
|
+
toolStatusCancelledLabel: "Cancelled",
|
|
40
|
+
imageAttachmentLabel: "Image attachment",
|
|
41
|
+
fileAttachmentLabel: "File attachment",
|
|
42
|
+
unknownPartLabel: "Unknown part",
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
function createPayload(bytes: number, callIndex: number, kind: "argument" | "result") {
|
|
46
|
+
return {
|
|
47
|
+
request: {
|
|
48
|
+
id: `${kind}-${callIndex}`,
|
|
49
|
+
metadata: {
|
|
50
|
+
callIndex,
|
|
51
|
+
kind,
|
|
52
|
+
labels: ["chat", "stress", "nested-payload"],
|
|
53
|
+
},
|
|
54
|
+
nested: {
|
|
55
|
+
levelOne: {
|
|
56
|
+
levelTwo: {
|
|
57
|
+
levelThree: {
|
|
58
|
+
payload: FILLER.repeat(Math.ceil(bytes / FILLER.length)).slice(0, bytes),
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function createChatToolCallStressMessage(
|
|
68
|
+
config: ChatToolCallStressConfig = DEFAULT_CHAT_TOOL_CALL_STRESS_CONFIG,
|
|
69
|
+
messageId = "chat-tool-call-stress-message",
|
|
70
|
+
): NcpMessage {
|
|
71
|
+
const parts: NcpMessage["parts"] = [
|
|
72
|
+
{
|
|
73
|
+
type: "reasoning",
|
|
74
|
+
text: "Simulating a dense tool-call execution trace.",
|
|
75
|
+
},
|
|
76
|
+
];
|
|
77
|
+
for (let index = 0; index < config.toolCallCount; index += 1) {
|
|
78
|
+
parts.push({
|
|
79
|
+
type: "tool-invocation",
|
|
80
|
+
toolCallId: `stress-call-${index}`,
|
|
81
|
+
toolName: STRESS_TOOL_NAMES[index % STRESS_TOOL_NAMES.length]!,
|
|
82
|
+
state: "result",
|
|
83
|
+
args: createPayload(config.argumentBytesPerCall, index, "argument"),
|
|
84
|
+
result: createPayload(config.resultBytesPerCall, index, "result"),
|
|
85
|
+
execution: {
|
|
86
|
+
durationMs: 15 + (index % 200),
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
parts.push({
|
|
91
|
+
type: "text",
|
|
92
|
+
text: "Stress fixture completed.",
|
|
93
|
+
});
|
|
94
|
+
return {
|
|
95
|
+
id: messageId,
|
|
96
|
+
sessionId: "chat-tool-call-stress-session",
|
|
97
|
+
role: "assistant",
|
|
98
|
+
status: "final",
|
|
99
|
+
timestamp: "2026-08-19T00:00:00.000Z",
|
|
100
|
+
parts,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function createChatToolCallStressViewModel(
|
|
105
|
+
config: ChatToolCallStressConfig = DEFAULT_CHAT_TOOL_CALL_STRESS_CONFIG,
|
|
106
|
+
messageId?: string,
|
|
107
|
+
): ChatMessageViewModel {
|
|
108
|
+
const message = createChatToolCallStressMessage(config, messageId);
|
|
109
|
+
const uiMessage = adaptNcpMessageToUiMessage(message);
|
|
110
|
+
return adaptChatMessage(
|
|
111
|
+
{
|
|
112
|
+
id: uiMessage.id,
|
|
113
|
+
role: uiMessage.role,
|
|
114
|
+
meta: uiMessage.meta,
|
|
115
|
+
parts: adaptNcpMessagePartsForChat(message.parts),
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
texts: STRESS_ADAPTER_TEXTS,
|
|
119
|
+
formatTimestamp: (value) => value,
|
|
120
|
+
},
|
|
121
|
+
);
|
|
122
|
+
}
|
package/src/features/chat/features/ncp/hooks/__tests__/use-ncp-session-conversation.test.tsx
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
const mocks = vi.hoisted(() => ({
|
|
11
11
|
acceptRun: vi.fn(),
|
|
12
12
|
continueRun: vi.fn(),
|
|
13
|
+
fetchNcpSessionMessageDetail: vi.fn(),
|
|
13
14
|
fetchNcpSessionMessages: vi.fn(),
|
|
14
15
|
editMessage: vi.fn(),
|
|
15
16
|
prependHistory: vi.fn(),
|
|
@@ -47,6 +48,7 @@ vi.mock("@/shared/lib/api", async (importOriginal) => {
|
|
|
47
48
|
const actual = await importOriginal<typeof SharedApi>();
|
|
48
49
|
return {
|
|
49
50
|
...actual,
|
|
51
|
+
fetchNcpSessionMessageDetail: mocks.fetchNcpSessionMessageDetail,
|
|
50
52
|
fetchNcpSessionMessages: mocks.fetchNcpSessionMessages,
|
|
51
53
|
nextclawClient: {
|
|
52
54
|
...actual.nextclawClient,
|
|
@@ -89,6 +91,7 @@ vi.mock("@/features/system-status", () => ({
|
|
|
89
91
|
function resetConversationMocks(): void {
|
|
90
92
|
mocks.acceptRun.mockReset();
|
|
91
93
|
mocks.continueRun.mockReset();
|
|
94
|
+
mocks.fetchNcpSessionMessageDetail.mockReset();
|
|
92
95
|
mocks.fetchNcpSessionMessages.mockReset();
|
|
93
96
|
mocks.editMessage.mockReset();
|
|
94
97
|
mocks.prependHistory.mockReset();
|
|
@@ -134,6 +137,7 @@ describe("useNcpSessionConversation", () => {
|
|
|
134
137
|
|
|
135
138
|
expect(mocks.fetchNcpSessionMessages).toHaveBeenCalledWith("session-1", {
|
|
136
139
|
limit: 300,
|
|
140
|
+
toolPayload: "summary",
|
|
137
141
|
signal: expect.any(AbortSignal),
|
|
138
142
|
});
|
|
139
143
|
expect(result).toEqual({
|
|
@@ -152,6 +156,7 @@ describe("useNcpSessionConversation", () => {
|
|
|
152
156
|
compactedMessageCount: 0,
|
|
153
157
|
updatedAt: "2026-05-05T00:00:00.000Z",
|
|
154
158
|
},
|
|
159
|
+
deferredToolPayloads: {},
|
|
155
160
|
total: 1,
|
|
156
161
|
pageInfo: { startCursor: "cursor-1", hasPreviousPage: false },
|
|
157
162
|
});
|
|
@@ -171,6 +176,7 @@ describe("useNcpSessionConversation", () => {
|
|
|
171
176
|
messages: [],
|
|
172
177
|
status: "idle",
|
|
173
178
|
total: 0,
|
|
179
|
+
deferredToolPayloads: {},
|
|
174
180
|
pageInfo: { startCursor: null, hasPreviousPage: false },
|
|
175
181
|
});
|
|
176
182
|
});
|
|
@@ -433,6 +439,7 @@ describe("useNcpSessionConversation history and hydration", () => {
|
|
|
433
439
|
{
|
|
434
440
|
limit: 40,
|
|
435
441
|
cursor: "cursor-2",
|
|
442
|
+
toolPayload: "summary",
|
|
436
443
|
signal: expect.any(AbortSignal),
|
|
437
444
|
},
|
|
438
445
|
);
|
|
@@ -448,6 +455,68 @@ describe("useNcpSessionConversation history and hydration", () => {
|
|
|
448
455
|
expect(result.current.messageTotal).toBe(5);
|
|
449
456
|
});
|
|
450
457
|
|
|
458
|
+
it("loads and caches one complete message when deferred tool details are requested", async () => {
|
|
459
|
+
const summaryMessage: NcpMessage = {
|
|
460
|
+
id: "assistant-large",
|
|
461
|
+
sessionId: "session-1",
|
|
462
|
+
role: "assistant",
|
|
463
|
+
status: "final",
|
|
464
|
+
timestamp: "2026-08-19T00:00:00.000Z",
|
|
465
|
+
parts: [{
|
|
466
|
+
type: "tool-invocation",
|
|
467
|
+
toolCallId: "tool-1",
|
|
468
|
+
toolName: "exec_command",
|
|
469
|
+
state: "result",
|
|
470
|
+
args: { command: "pnpm test" },
|
|
471
|
+
}],
|
|
472
|
+
};
|
|
473
|
+
const fullMessage: NcpMessage = {
|
|
474
|
+
...summaryMessage,
|
|
475
|
+
parts: [{
|
|
476
|
+
...summaryMessage.parts[0] as Extract<NcpMessage["parts"][number], { type: "tool-invocation" }>,
|
|
477
|
+
result: { output: "complete output" },
|
|
478
|
+
}],
|
|
479
|
+
};
|
|
480
|
+
mocks.visibleMessages.push(summaryMessage);
|
|
481
|
+
mocks.fetchNcpSessionMessages
|
|
482
|
+
.mockResolvedValueOnce({
|
|
483
|
+
sessionId: "session-1",
|
|
484
|
+
status: "idle",
|
|
485
|
+
total: 1,
|
|
486
|
+
pageInfo: { startCursor: "cursor-1", hasPreviousPage: false },
|
|
487
|
+
messages: [summaryMessage],
|
|
488
|
+
deferredToolPayloads: { "assistant-large": { cursor: "cursor-detail" } },
|
|
489
|
+
});
|
|
490
|
+
mocks.fetchNcpSessionMessageDetail.mockResolvedValue(fullMessage);
|
|
491
|
+
const { result } = renderHook(() => useNcpSessionConversation("session-1"));
|
|
492
|
+
const loadSeed = mocks.hydratedCalls[0]?.loadSeed as (
|
|
493
|
+
sessionId: string,
|
|
494
|
+
signal: AbortSignal,
|
|
495
|
+
) => Promise<{ messages: unknown[]; status: string }>;
|
|
496
|
+
await act(async () => {
|
|
497
|
+
await loadSeed("session-1", new AbortController().signal);
|
|
498
|
+
});
|
|
499
|
+
expect(result.current.messageDetailStates["assistant-large"]).toBe("summary");
|
|
500
|
+
|
|
501
|
+
await act(async () => {
|
|
502
|
+
await Promise.all([
|
|
503
|
+
result.current.loadMessageDetails("assistant-large"),
|
|
504
|
+
result.current.loadMessageDetails("assistant-large"),
|
|
505
|
+
]);
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
expect(mocks.fetchNcpSessionMessageDetail).toHaveBeenCalledWith(
|
|
509
|
+
"session-1",
|
|
510
|
+
"assistant-large",
|
|
511
|
+
"cursor-detail",
|
|
512
|
+
expect.any(AbortSignal),
|
|
513
|
+
);
|
|
514
|
+
expect(mocks.fetchNcpSessionMessageDetail).toHaveBeenCalledTimes(1);
|
|
515
|
+
expect(mocks.fetchNcpSessionMessages).toHaveBeenCalledTimes(1);
|
|
516
|
+
expect(result.current.messageDetailStates["assistant-large"]).toBe("ready");
|
|
517
|
+
expect(result.current.visibleMessages[0]).toBe(fullMessage);
|
|
518
|
+
});
|
|
519
|
+
|
|
451
520
|
it("drops an in-flight history page when the viewer switches sessions", async () => {
|
|
452
521
|
let resolvePreviousPage = (_value: unknown): void => {
|
|
453
522
|
throw new Error("Previous page request was not initialized.");
|
|
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
|
2
2
|
import { NcpHttpAgentClientEndpoint } from "@nextclaw/ncp-http-agent-client";
|
|
3
3
|
import { useHydratedNcpAgent } from "@nextclaw/ncp-react";
|
|
4
4
|
import type {
|
|
5
|
+
NcpMessage,
|
|
5
6
|
NcpRunHandle,
|
|
6
7
|
} from "@nextclaw/ncp";
|
|
7
8
|
import type {
|
|
@@ -74,6 +75,21 @@ function useSyncReadyRetryVersion(
|
|
|
74
75
|
}, [readyRetrySignature, syncReadyRetryVersion]);
|
|
75
76
|
}
|
|
76
77
|
|
|
78
|
+
function overlaySessionMessageDetails(
|
|
79
|
+
messages: readonly NcpMessage[],
|
|
80
|
+
details: Readonly<Record<string, NcpMessage>>,
|
|
81
|
+
) {
|
|
82
|
+
return messages.map((message) => {
|
|
83
|
+
const detail = details[message.id];
|
|
84
|
+
if (!detail) return message;
|
|
85
|
+
if (!message.metadata && !detail.metadata) return detail;
|
|
86
|
+
return {
|
|
87
|
+
...detail,
|
|
88
|
+
metadata: { ...(message.metadata ?? {}), ...(detail.metadata ?? {}) },
|
|
89
|
+
};
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
77
93
|
export function useNcpSessionConversation(
|
|
78
94
|
sessionId: string | undefined,
|
|
79
95
|
options: UseNcpSessionConversationOptions = {},
|
|
@@ -85,6 +101,7 @@ export function useNcpSessionConversation(
|
|
|
85
101
|
const pendingCommandSessionIdRef = useRef<string | null>(null);
|
|
86
102
|
const {
|
|
87
103
|
loadSeed,
|
|
104
|
+
loadMessageDetails,
|
|
88
105
|
loadPreviousMessages: loadPreviousHistory,
|
|
89
106
|
state: visibleHistoryState,
|
|
90
107
|
} = useNcpSessionMessageHistory({
|
|
@@ -180,9 +197,17 @@ export function useNcpSessionConversation(
|
|
|
180
197
|
useSyncReadyRetryVersion(readyRetrySignature, () => {
|
|
181
198
|
setHydrationRetryVersion((current) => current + 1);
|
|
182
199
|
});
|
|
200
|
+
const visibleMessages = useMemo(
|
|
201
|
+
() => overlaySessionMessageDetails(
|
|
202
|
+
agent.visibleMessages,
|
|
203
|
+
visibleHistoryState.messageDetails,
|
|
204
|
+
),
|
|
205
|
+
[agent.visibleMessages, visibleHistoryState.messageDetails],
|
|
206
|
+
);
|
|
183
207
|
return useMemo(
|
|
184
208
|
() => ({
|
|
185
209
|
...agent,
|
|
210
|
+
visibleMessages,
|
|
186
211
|
isSending:
|
|
187
212
|
agent.isSending || pendingCommandSessionId === sessionId,
|
|
188
213
|
editMessage,
|
|
@@ -196,16 +221,20 @@ export function useNcpSessionConversation(
|
|
|
196
221
|
historyError: visibleHistoryState.error,
|
|
197
222
|
isLoadingPreviousMessages: visibleHistoryState.isLoading,
|
|
198
223
|
loadPreviousMessages,
|
|
224
|
+
loadMessageDetails,
|
|
225
|
+
messageDetailStates: visibleHistoryState.messageDetailStates,
|
|
199
226
|
messageTotal: visibleHistoryState.total,
|
|
200
227
|
}),
|
|
201
228
|
[
|
|
202
229
|
agent,
|
|
203
230
|
continueRun,
|
|
204
231
|
editMessage,
|
|
232
|
+
loadMessageDetails,
|
|
205
233
|
loadPreviousMessages,
|
|
206
234
|
pendingCommandSessionId,
|
|
207
235
|
sessionId,
|
|
208
236
|
visibleHistoryState,
|
|
237
|
+
visibleMessages,
|
|
209
238
|
],
|
|
210
239
|
);
|
|
211
240
|
}
|
|
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useRef, useState } from "react";
|
|
|
2
2
|
import type { NcpMessage, NcpSessionMessagePageInfo } from "@nextclaw/ncp";
|
|
3
3
|
import type { NcpConversationSeed } from "@nextclaw/ncp-react";
|
|
4
4
|
import {
|
|
5
|
+
fetchNcpSessionMessageDetail,
|
|
5
6
|
fetchNcpSessionMessages,
|
|
6
7
|
type SessionContextWindowView,
|
|
7
8
|
} from "@/shared/lib/api";
|
|
@@ -10,10 +11,13 @@ export const DEFAULT_NCP_SESSION_MESSAGE_LIMIT = 40;
|
|
|
10
11
|
|
|
11
12
|
type NcpConversationSeedWithContextWindow = NcpConversationSeed & {
|
|
12
13
|
contextWindow?: SessionContextWindowView | null;
|
|
14
|
+
deferredToolPayloads: Record<string, { cursor: string }>;
|
|
13
15
|
total: number;
|
|
14
16
|
pageInfo: NcpSessionMessagePageInfo;
|
|
15
17
|
};
|
|
16
18
|
|
|
19
|
+
export type SessionMessageToolPayloadState = "summary" | "loading" | "ready" | "error";
|
|
20
|
+
|
|
17
21
|
type SessionHistoryState = {
|
|
18
22
|
sessionId: string | null;
|
|
19
23
|
contextWindow: SessionContextWindowView | null;
|
|
@@ -22,6 +26,9 @@ type SessionHistoryState = {
|
|
|
22
26
|
hasPreviousPage: boolean;
|
|
23
27
|
isLoading: boolean;
|
|
24
28
|
error: Error | null;
|
|
29
|
+
deferredToolPayloads: Record<string, { cursor: string }>;
|
|
30
|
+
messageDetails: Record<string, NcpMessage>;
|
|
31
|
+
messageDetailStates: Record<string, SessionMessageToolPayloadState>;
|
|
25
32
|
};
|
|
26
33
|
|
|
27
34
|
const EMPTY_SESSION_HISTORY_STATE: SessionHistoryState = {
|
|
@@ -32,6 +39,9 @@ const EMPTY_SESSION_HISTORY_STATE: SessionHistoryState = {
|
|
|
32
39
|
hasPreviousPage: false,
|
|
33
40
|
isLoading: false,
|
|
34
41
|
error: null,
|
|
42
|
+
deferredToolPayloads: {},
|
|
43
|
+
messageDetails: {},
|
|
44
|
+
messageDetailStates: {},
|
|
35
45
|
};
|
|
36
46
|
|
|
37
47
|
function isMissingNcpSessionError(error: unknown): boolean {
|
|
@@ -49,6 +59,7 @@ export async function fetchNcpSessionConversationSeed(
|
|
|
49
59
|
try {
|
|
50
60
|
const response = await fetchNcpSessionMessages(sessionId, {
|
|
51
61
|
limit: messageLimit,
|
|
62
|
+
toolPayload: "summary",
|
|
52
63
|
signal,
|
|
53
64
|
});
|
|
54
65
|
signal.throwIfAborted();
|
|
@@ -56,6 +67,7 @@ export async function fetchNcpSessionConversationSeed(
|
|
|
56
67
|
messages: response.messages,
|
|
57
68
|
status: response.status ?? "idle",
|
|
58
69
|
contextWindow: response.contextWindow ?? null,
|
|
70
|
+
deferredToolPayloads: response.deferredToolPayloads ?? {},
|
|
59
71
|
total: response.total,
|
|
60
72
|
pageInfo: response.pageInfo,
|
|
61
73
|
};
|
|
@@ -68,11 +80,80 @@ export async function fetchNcpSessionConversationSeed(
|
|
|
68
80
|
messages: [],
|
|
69
81
|
status: "idle",
|
|
70
82
|
total: 0,
|
|
83
|
+
deferredToolPayloads: {},
|
|
71
84
|
pageInfo: { startCursor: null, hasPreviousPage: false },
|
|
72
85
|
};
|
|
73
86
|
}
|
|
74
87
|
}
|
|
75
88
|
|
|
89
|
+
type UpdateSessionHistoryState = (
|
|
90
|
+
sessionId: string,
|
|
91
|
+
update: (current: SessionHistoryState) => SessionHistoryState,
|
|
92
|
+
) => void;
|
|
93
|
+
|
|
94
|
+
function useSessionMessageDetailLoader(params: {
|
|
95
|
+
sessionId: string | undefined;
|
|
96
|
+
historyStateRef: { current: SessionHistoryState };
|
|
97
|
+
updateHistoryState: UpdateSessionHistoryState;
|
|
98
|
+
}) {
|
|
99
|
+
const { historyStateRef, sessionId, updateHistoryState } = params;
|
|
100
|
+
const requestsRef = useRef(new Map<string, {
|
|
101
|
+
controller: AbortController;
|
|
102
|
+
promise: Promise<void>;
|
|
103
|
+
}>());
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
const requests = requestsRef.current;
|
|
106
|
+
for (const request of requests.values()) request.controller.abort();
|
|
107
|
+
requests.clear();
|
|
108
|
+
return () => {
|
|
109
|
+
for (const request of requests.values()) request.controller.abort();
|
|
110
|
+
requests.clear();
|
|
111
|
+
};
|
|
112
|
+
}, [sessionId]);
|
|
113
|
+
return useCallback(async (messageId: string): Promise<void> => {
|
|
114
|
+
if (!sessionId) return;
|
|
115
|
+
const history = historyStateRef.current;
|
|
116
|
+
if (history.sessionId !== sessionId || history.messageDetails[messageId]) return;
|
|
117
|
+
const cursor = history.deferredToolPayloads[messageId]?.cursor;
|
|
118
|
+
if (!cursor) return;
|
|
119
|
+
const existing = requestsRef.current.get(messageId);
|
|
120
|
+
if (existing) return await existing.promise;
|
|
121
|
+
const controller = new AbortController();
|
|
122
|
+
updateHistoryState(sessionId, (current) => ({
|
|
123
|
+
...current,
|
|
124
|
+
messageDetailStates: { ...current.messageDetailStates, [messageId]: "loading" },
|
|
125
|
+
}));
|
|
126
|
+
const promise = (async () => {
|
|
127
|
+
try {
|
|
128
|
+
const message = await fetchNcpSessionMessageDetail(
|
|
129
|
+
sessionId,
|
|
130
|
+
messageId,
|
|
131
|
+
cursor,
|
|
132
|
+
controller.signal,
|
|
133
|
+
);
|
|
134
|
+
if (controller.signal.aborted) return;
|
|
135
|
+
updateHistoryState(sessionId, (current) => ({
|
|
136
|
+
...current,
|
|
137
|
+
messageDetails: { ...current.messageDetails, [messageId]: message },
|
|
138
|
+
messageDetailStates: { ...current.messageDetailStates, [messageId]: "ready" },
|
|
139
|
+
}));
|
|
140
|
+
} catch {
|
|
141
|
+
if (controller.signal.aborted) return;
|
|
142
|
+
updateHistoryState(sessionId, (current) => ({
|
|
143
|
+
...current,
|
|
144
|
+
messageDetailStates: { ...current.messageDetailStates, [messageId]: "error" },
|
|
145
|
+
}));
|
|
146
|
+
} finally {
|
|
147
|
+
if (requestsRef.current.get(messageId)?.controller === controller) {
|
|
148
|
+
requestsRef.current.delete(messageId);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
})();
|
|
152
|
+
requestsRef.current.set(messageId, { controller, promise });
|
|
153
|
+
await promise;
|
|
154
|
+
}, [historyStateRef, sessionId, updateHistoryState]);
|
|
155
|
+
}
|
|
156
|
+
|
|
76
157
|
export function useNcpSessionMessageHistory(params: {
|
|
77
158
|
sessionId: string | undefined;
|
|
78
159
|
messageLimit: number;
|
|
@@ -104,8 +185,15 @@ export function useNcpSessionMessageHistory(params: {
|
|
|
104
185
|
useEffect(() => {
|
|
105
186
|
historyRequestRef.current?.abort();
|
|
106
187
|
historyRequestRef.current = null;
|
|
107
|
-
return () =>
|
|
188
|
+
return () => {
|
|
189
|
+
historyRequestRef.current?.abort();
|
|
190
|
+
};
|
|
108
191
|
}, [sessionId]);
|
|
192
|
+
const loadMessageDetails = useSessionMessageDetailLoader({
|
|
193
|
+
historyStateRef,
|
|
194
|
+
sessionId,
|
|
195
|
+
updateHistoryState,
|
|
196
|
+
});
|
|
109
197
|
const loadSeed = useCallback(
|
|
110
198
|
async (targetSessionId: string, signal: AbortSignal) => {
|
|
111
199
|
void hydrationRetryVersion;
|
|
@@ -118,6 +206,11 @@ export function useNcpSessionMessageHistory(params: {
|
|
|
118
206
|
updateHistoryState(targetSessionId, (current) => ({
|
|
119
207
|
...current,
|
|
120
208
|
contextWindow: seed.contextWindow ?? null,
|
|
209
|
+
deferredToolPayloads: seed.deferredToolPayloads,
|
|
210
|
+
messageDetails: {},
|
|
211
|
+
messageDetailStates: Object.fromEntries(
|
|
212
|
+
Object.keys(seed.deferredToolPayloads).map((messageId) => [messageId, "summary"]),
|
|
213
|
+
),
|
|
121
214
|
total: seed.total,
|
|
122
215
|
cursor: seed.pageInfo.startCursor,
|
|
123
216
|
hasPreviousPage: seed.pageInfo.hasPreviousPage,
|
|
@@ -151,6 +244,7 @@ export function useNcpSessionMessageHistory(params: {
|
|
|
151
244
|
const response = await fetchNcpSessionMessages(sessionId, {
|
|
152
245
|
limit: messageLimit,
|
|
153
246
|
cursor: history.cursor,
|
|
247
|
+
toolPayload: "summary",
|
|
154
248
|
signal: controller.signal,
|
|
155
249
|
});
|
|
156
250
|
if (
|
|
@@ -163,6 +257,16 @@ export function useNcpSessionMessageHistory(params: {
|
|
|
163
257
|
updateHistoryState(sessionId, (current) => ({
|
|
164
258
|
...current,
|
|
165
259
|
contextWindow: response.contextWindow ?? current.contextWindow,
|
|
260
|
+
deferredToolPayloads: {
|
|
261
|
+
...current.deferredToolPayloads,
|
|
262
|
+
...(response.deferredToolPayloads ?? {}),
|
|
263
|
+
},
|
|
264
|
+
messageDetailStates: {
|
|
265
|
+
...Object.fromEntries(
|
|
266
|
+
Object.keys(response.deferredToolPayloads ?? {}).map((messageId) => [messageId, "summary"]),
|
|
267
|
+
),
|
|
268
|
+
...current.messageDetailStates,
|
|
269
|
+
},
|
|
166
270
|
total: response.total,
|
|
167
271
|
cursor: response.pageInfo.startCursor,
|
|
168
272
|
hasPreviousPage: response.pageInfo.hasPreviousPage,
|
|
@@ -189,6 +293,7 @@ export function useNcpSessionMessageHistory(params: {
|
|
|
189
293
|
return {
|
|
190
294
|
loadSeed,
|
|
191
295
|
loadPreviousMessages,
|
|
296
|
+
loadMessageDetails,
|
|
192
297
|
state:
|
|
193
298
|
historyState.sessionId === sessionId
|
|
194
299
|
? historyState
|
|
@@ -29,17 +29,6 @@ const SESSION_ACTIVITY_PREVIEW_STATUS_KIND_SET = new Set<NonNullable<SessionActi
|
|
|
29
29
|
'run-interrupted'
|
|
30
30
|
]);
|
|
31
31
|
|
|
32
|
-
function stringifyUnknown(value: unknown): string {
|
|
33
|
-
if (typeof value === 'string') {
|
|
34
|
-
return value;
|
|
35
|
-
}
|
|
36
|
-
try {
|
|
37
|
-
return JSON.stringify(value ?? {});
|
|
38
|
-
} catch {
|
|
39
|
-
return String(value ?? '');
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
32
|
function buildNcpAssetContentUrl(assetUri: string): string {
|
|
44
33
|
const query = new URLSearchParams({ uri: assetUri });
|
|
45
34
|
return `${API_BASE}/api/ncp/assets/content?${query.toString()}`;
|
|
@@ -283,7 +272,8 @@ function toUiParts(parts: NcpMessagePart[]): UIMessage['parts'] {
|
|
|
283
272
|
status: mapToolStatus(part),
|
|
284
273
|
toolCallId: part.toolCallId ?? `${part.toolName}-${Math.random().toString(36).slice(2, 8)}`,
|
|
285
274
|
toolName: part.toolName,
|
|
286
|
-
args:
|
|
275
|
+
args: typeof part.args === "string" ? part.args : "",
|
|
276
|
+
parsedArgs: part.args,
|
|
287
277
|
result: part.result,
|
|
288
278
|
...(part.execution ? { execution: { ...part.execution } } : {})
|
|
289
279
|
}
|
|
@@ -34,6 +34,7 @@ function createPanelAppEntry(overrides: Partial<PanelAppEntryView> = {}): PanelA
|
|
|
34
34
|
contentPath: '/api/panel-apps/encoded-reader/content',
|
|
35
35
|
createdAt: '2026-06-09T00:00:00.000Z',
|
|
36
36
|
favorite: false,
|
|
37
|
+
mainSidebar: false,
|
|
37
38
|
fileName: 'reader.panel',
|
|
38
39
|
id: 'encoded-reader',
|
|
39
40
|
kind: 'folder',
|
|
@@ -97,13 +98,14 @@ describe('ChatUiManager content display', () => {
|
|
|
97
98
|
});
|
|
98
99
|
|
|
99
100
|
expect(docBrowserManager.openTarget).toHaveBeenCalledWith({
|
|
100
|
-
dedupeKey: 'panel-app:
|
|
101
|
+
dedupeKey: 'panel-app:reader',
|
|
101
102
|
contentParams: {
|
|
102
103
|
file: { path: '/tmp/photo.png' },
|
|
103
104
|
},
|
|
105
|
+
dockIcon: undefined,
|
|
104
106
|
historyPolicy: 'managed',
|
|
105
107
|
kind: 'panel-app',
|
|
106
|
-
resourceUri: 'nextclaw://panel-app/
|
|
108
|
+
resourceUri: 'nextclaw://panel-app/reader',
|
|
107
109
|
title: 'Reader',
|
|
108
110
|
url: '/api/panel-apps/encoded-reader/content',
|
|
109
111
|
}, {
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import { render, screen } from '@testing-library/react';
|
|
2
2
|
import userEvent from '@testing-library/user-event';
|
|
3
|
-
import { describe, expect, it, vi } from 'vitest';
|
|
4
|
-
import { PanelAppListItem } from '
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { PanelAppListItem } from '@/features/panel-apps/components/panel-app-list-item';
|
|
5
|
+
|
|
6
|
+
const mainSidebarMutation = vi.hoisted(() => ({
|
|
7
|
+
isPending: false,
|
|
8
|
+
mutate: vi.fn(),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
vi.mock('@/features/panel-apps/hooks/use-panel-apps', () => ({
|
|
12
|
+
useUpdatePanelAppPreferences: () => mainSidebarMutation,
|
|
13
|
+
}));
|
|
5
14
|
|
|
6
15
|
const baseEntry = {
|
|
7
16
|
id: 'demo',
|
|
@@ -15,12 +24,18 @@ const baseEntry = {
|
|
|
15
24
|
updatedAt: '2026-05-28T09:00:00.000Z',
|
|
16
25
|
sizeBytes: 12,
|
|
17
26
|
favorite: false,
|
|
27
|
+
mainSidebar: false,
|
|
18
28
|
clientDeclared: false,
|
|
19
29
|
clientGranted: false,
|
|
20
30
|
openCount: 0,
|
|
21
31
|
};
|
|
22
32
|
|
|
23
33
|
describe('PanelAppListItem', () => {
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
mainSidebarMutation.isPending = false;
|
|
36
|
+
mainSidebarMutation.mutate.mockReset();
|
|
37
|
+
});
|
|
38
|
+
|
|
24
39
|
it('keeps panel app metadata compact below the icon-title row', () => {
|
|
25
40
|
const { container } = render(
|
|
26
41
|
<PanelAppListItem
|
|
@@ -61,4 +76,44 @@ describe('PanelAppListItem', () => {
|
|
|
61
76
|
|
|
62
77
|
expect(onDelete).toHaveBeenCalledTimes(1);
|
|
63
78
|
});
|
|
79
|
+
|
|
80
|
+
it('keeps main-sidebar placement in the low-frequency more-actions menu', async () => {
|
|
81
|
+
const user = userEvent.setup();
|
|
82
|
+
const { rerender } = render(
|
|
83
|
+
<PanelAppListItem
|
|
84
|
+
deletePending={false}
|
|
85
|
+
entry={baseEntry}
|
|
86
|
+
favoritePending={false}
|
|
87
|
+
onDelete={vi.fn()}
|
|
88
|
+
onOpen={vi.fn()}
|
|
89
|
+
onToggleFavorite={vi.fn()}
|
|
90
|
+
/>,
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
expect(screen.queryByRole('button', { name: 'Add to main sidebar' })).toBeNull();
|
|
94
|
+
await user.click(screen.getByRole('button', { name: 'More panel app actions' }));
|
|
95
|
+
await user.click(screen.getByRole('button', { name: 'Add to main sidebar' }));
|
|
96
|
+
expect(mainSidebarMutation.mutate).toHaveBeenCalledWith({
|
|
97
|
+
id: 'demo',
|
|
98
|
+
preferences: { mainSidebar: true },
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
rerender(
|
|
102
|
+
<PanelAppListItem
|
|
103
|
+
deletePending={false}
|
|
104
|
+
entry={{ ...baseEntry, mainSidebar: true }}
|
|
105
|
+
favoritePending={false}
|
|
106
|
+
onDelete={vi.fn()}
|
|
107
|
+
onOpen={vi.fn()}
|
|
108
|
+
onToggleFavorite={vi.fn()}
|
|
109
|
+
/>,
|
|
110
|
+
);
|
|
111
|
+
expect(screen.queryByRole('button', { name: 'Remove from main sidebar' })).toBeNull();
|
|
112
|
+
await user.click(screen.getByRole('button', { name: 'More panel app actions' }));
|
|
113
|
+
await user.click(screen.getByRole('button', { name: 'Remove from main sidebar' }));
|
|
114
|
+
expect(mainSidebarMutation.mutate).toHaveBeenLastCalledWith({
|
|
115
|
+
id: 'demo',
|
|
116
|
+
preferences: { mainSidebar: false },
|
|
117
|
+
});
|
|
118
|
+
});
|
|
64
119
|
});
|