@nextclaw/ui 0.15.5 → 0.15.7
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 +41 -0
- package/dist/assets/api-V8m62-13.js +13 -0
- package/dist/assets/app-presenter-provider-Bzj9h_ZS.js +106 -0
- package/dist/assets/{appearance-settings-page-80fLa4-9.js → appearance-settings-page-DwBwu1AU.js} +1 -1
- package/dist/assets/{book-open-CN7Shpij.js → book-open-DtVSr5O9.js} +1 -1
- package/dist/assets/{channels-list-page-R7M6pzp6.js → channels-list-page-CYIqMliJ.js} +2 -2
- package/dist/assets/{chat-page-152YuwfU.js → chat-page-BC0icTtI.js} +1 -1
- package/dist/assets/{config-split-page-BlfvfmTm.js → config-split-page-B88prd8s.js} +1 -1
- package/dist/assets/{confirm-dialog-Dt8Kdbgp.js → confirm-dialog-BB09IC2u.js} +1 -1
- package/dist/assets/desktop-update-config-B-q0OhxO.js +1 -0
- package/dist/assets/{dist-D5ycbwCo.js → dist-Bytser-a.js} +1 -1
- package/dist/assets/{doc-browser-C9u7N9vH.js → doc-browser-B1FXvZm6.js} +1 -1
- package/dist/assets/doc-browser-Bodhf0fj2.js +1 -0
- package/dist/assets/{doc-browser-context-CCdW9TJf.js → doc-browser-context-D5VLNoj3.js} +1 -1
- package/dist/assets/{download-CyPgglKr.js → download-CdO6NoHj.js} +1 -1
- package/dist/assets/{external-link-BahW1sl3.js → external-link-C5NFoh5I.js} +1 -1
- package/dist/assets/{eye-D8gGXMkD.js → eye-B51oNRRc.js} +1 -1
- package/dist/assets/{folder-DGWqFDvE.js → folder-DxH7Nef-.js} +1 -1
- package/dist/assets/{form-actions-D2imZLnU.js → form-actions-C9pUwtYV.js} +1 -1
- package/dist/assets/index-DtWf7kk9.css +1 -0
- package/dist/assets/index-UnY4HtVa.js +2 -0
- package/dist/assets/loader-circle-BRSfaztA.js +1 -0
- package/dist/assets/{logo-badge-CuPetu-v.js → logo-badge-D2vORX3a.js} +1 -1
- package/dist/assets/mcp-marketplace-page-CGVu7mfL.js +9 -0
- package/dist/assets/mcp-marketplace-page-DnV0e1JS.js +1 -0
- package/dist/assets/{model-config-page-DiJcvBML.js → model-config-page-DYpchGVE.js} +1 -1
- package/dist/assets/plus-BgMBV9HJ.js +1 -0
- package/dist/assets/{popover-Btp7KVc3.js → popover-CONujDHf.js} +1 -1
- package/dist/assets/{provider-scoped-model-input-DZMtoAJG.js → provider-scoped-model-input-DA3Ua9v8.js} +1 -1
- package/dist/assets/providers-config-page-DXsoyMrh.js +1 -0
- package/dist/assets/{react-BY-U3h3t.js → react-Io6Pfq8D.js} +1 -1
- package/dist/assets/{refresh-cw-Cme5qFej.js → refresh-cw-C6j2tjul.js} +1 -1
- package/dist/assets/remote-C1y3ARHu.js +1 -0
- package/dist/assets/rotate-cw-Bv2B6fAV.js +1 -0
- package/dist/assets/runtime-config-page-eYrrKBrA.js +1 -0
- package/dist/assets/{save-DNg2yJTS.js → save-CBBZVvZ-.js} +1 -1
- package/dist/assets/{search-3ykfrAFt.js → search-DFF842z2.js} +1 -1
- package/dist/assets/{search-config-page-DrVKa_Ls.js → search-config-page-DUyuwD7d.js} +1 -1
- package/dist/assets/{secrets-config-page-B39uQ5kF.js → secrets-config-page-DwzKQJ76.js} +2 -2
- package/dist/assets/security-config-D8me3pXl.js +1 -0
- package/dist/assets/{select-B5EecXJt.js → select-BiOpo2kV.js} +1 -1
- package/dist/assets/{setting-row-Qv1Ps79v.js → setting-row-C3WQXLtS.js} +1 -1
- package/dist/assets/{settings-2-D-3um9Az.js → settings-2-DyOvA_6K.js} +1 -1
- package/dist/assets/{skeleton-BgkzQd4U.js → skeleton-CQIAEbA4.js} +1 -1
- package/dist/assets/{tabs-BVD8gp3y.js → tabs-BTSENlI2.js} +1 -1
- package/dist/assets/{tag-chip-0qn-f3Bo.js → tag-chip-BG5JIGwY.js} +1 -1
- package/dist/assets/{trash-2-cnsMppRC.js → trash-2-CTIfwpt1.js} +1 -1
- package/dist/assets/use-config-CY4uF2Ou.js +1 -0
- package/dist/assets/{use-confirm-dialog-C_o0eQeH.js → use-confirm-dialog-Bj4u78F9.js} +1 -1
- package/dist/assets/{use-viewport-layout-BWav4KCL.js → use-viewport-layout-ChJUcSQ6.js} +1 -1
- package/dist/assets/x-C6cM8wkT.js +1 -0
- package/dist/index.html +30 -29
- package/index.html +1 -0
- package/package.json +8 -8
- package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +97 -3
- package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +13 -2
- package/src/features/chat/components/layout/__tests__/chat-sidebar-test.utils.ts +22 -0
- package/src/features/chat/components/layout/__tests__/chat-sidebar.test.tsx +13 -24
- package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +16 -4
- package/src/features/chat/components/layout/chat-sidebar.tsx +38 -2
- package/src/features/chat/features/conversation/components/__tests__/session-conversation-area.test.tsx +1 -0
- package/src/features/chat/features/conversation/components/__tests__/session-conversation-input.streaming.test.tsx +70 -2
- package/src/features/chat/features/conversation/components/session-conversation-area.tsx +22 -0
- package/src/features/chat/features/conversation/components/session-conversation-input.tsx +8 -9
- package/src/features/chat/features/conversation/hooks/__tests__/use-session-conversation-input-state.test.tsx +55 -0
- package/src/features/chat/features/conversation/hooks/use-session-conversation-input-state.ts +12 -11
- package/src/features/chat/features/conversation/hooks/use-session-conversation-preference-actions.ts +94 -0
- package/src/features/chat/features/input/hooks/__tests__/use-chat-input-surface-state.test.tsx +82 -0
- package/src/features/chat/features/input/hooks/use-chat-input-surface-state.ts +187 -63
- package/src/features/chat/features/input/input-surface-plugins/__tests__/context-reference-plugin.test.ts +225 -0
- package/src/features/chat/features/input/input-surface-plugins/__tests__/slash-command-plugin.test.ts +23 -0
- package/src/features/chat/features/input/input-surface-plugins/chat-input-product-plugin-adapters.types.ts +6 -1
- package/src/features/chat/features/input/input-surface-plugins/context-reference-plugin.utils.ts +357 -0
- package/src/features/chat/features/input/input-surface-plugins/slash-command-plugin.utils.ts +4 -1
- package/src/features/chat/features/input/utils/__tests__/chat-composer-state.utils.test.ts +26 -0
- package/src/features/chat/features/input/utils/__tests__/chat-inline-token.utils.test.ts +67 -5
- package/src/features/chat/features/input/utils/chat-composer-state.utils.ts +4 -10
- package/src/features/chat/features/input/utils/chat-composer-token-protocol.utils.ts +29 -0
- package/src/features/chat/features/input/utils/chat-inline-token.utils.ts +83 -8
- package/src/features/chat/features/input/utils/chat-input-bar.utils.ts +1 -0
- package/src/features/chat/features/message/components/__tests__/chat-inline-file-preview.test.tsx +159 -0
- package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +92 -26
- package/src/features/chat/features/message/components/chat-inline-file-preview.tsx +90 -21
- package/src/features/chat/features/message/components/chat-message-list.container.tsx +24 -3
- package/src/features/chat/features/message/utils/chat-message-texts.utils.ts +1 -0
- package/src/features/chat/features/ncp/hooks/__tests__/use-hydrated-ncp-agent.test.tsx +96 -3
- package/src/features/chat/features/project/components/__tests__/chat-project-create-dialog.test.tsx +67 -0
- package/src/features/chat/features/project/components/chat-project-create-dialog.tsx +203 -0
- package/src/features/chat/features/session/components/session-header/__tests__/chat-session-header-actions.test.tsx +7 -7
- package/src/features/chat/features/session/components/session-header/__tests__/chat-session-project-dialog.test.tsx +47 -0
- package/src/features/chat/features/session/components/session-header/chat-session-header-actions.tsx +6 -3
- package/src/features/chat/features/session/components/session-header/chat-session-project-badge.tsx +3 -0
- package/src/features/chat/features/session/components/session-header/chat-session-project-dialog.tsx +3 -0
- package/src/features/chat/features/session/components/session-header/chat-session-switcher-groups.tsx +245 -0
- package/src/features/chat/features/session/components/session-header/chat-session-title-switcher.tsx +82 -128
- package/src/features/chat/features/session/utils/__tests__/chat-sidebar-session-groups.utils.test.ts +22 -0
- package/src/features/chat/features/session/utils/chat-run-metadata.utils.ts +6 -6
- package/src/features/chat/features/session/utils/chat-sidebar-session-groups.utils.ts +12 -1
- package/src/features/chat/features/welcome/components/chat-conversation-welcome.tsx +3 -0
- package/src/features/chat/features/welcome/components/chat-welcome.tsx +1 -0
- package/src/features/chat/features/welcome/utils/__tests__/chat-welcome-project-options.utils.test.ts +20 -0
- package/src/features/chat/features/welcome/utils/chat-welcome-project-options.utils.ts +15 -2
- package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-file-preview.test.tsx +5 -6
- package/src/features/chat/features/workspace/components/__tests__/workspace-file-content-preview.test.tsx +67 -0
- package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx +9 -7
- package/src/features/chat/features/workspace/components/workspace-file-content-preview.tsx +95 -4
- package/src/features/chat/pages/__tests__/ncp-chat-page.test.ts +23 -0
- package/src/features/marketplace/components/__tests__/marketplace-page-detail.test.tsx +1 -0
- package/src/features/marketplace/components/__tests__/marketplace-page.test.tsx +62 -0
- package/src/features/marketplace/components/curated-shelves/__tests__/marketplace-curated-scene-route.test.tsx +1 -0
- package/src/features/marketplace/components/curated-shelves/marketplace-curated-shelves.tsx +2 -15
- package/src/features/marketplace/components/marketplace-page.tsx +10 -2
- package/src/features/marketplace/hooks/__tests__/use-marketplace.test.tsx +26 -1
- package/src/features/marketplace/hooks/use-marketplace-curated-scene-route.ts +11 -0
- package/src/features/marketplace/hooks/use-marketplace-list-model.ts +1 -1
- package/src/features/marketplace/hooks/use-marketplace.ts +28 -1
- package/src/features/settings/components/config/provider-form.tsx +0 -4
- package/src/features/settings/utils/__tests__/provider-form-support.utils.test.ts +7 -9
- package/src/features/settings/utils/provider-form-model.utils.ts +0 -4
- package/src/features/system-status/hooks/use-current-version-release-notes-link.ts +33 -0
- package/src/features/system-status/index.ts +9 -0
- package/src/features/system-status/utils/update-release-notes.utils.ts +44 -3
- package/src/index.css +10 -16
- package/src/shared/components/common/__tests__/brand-header.test.tsx +110 -3
- package/src/shared/components/common/brand-header.tsx +121 -35
- package/src/shared/components/path-picker/__tests__/server-path-picker-dialog.test.tsx +288 -38
- package/src/shared/components/path-picker/server-path-picker-dialog.tsx +290 -199
- package/src/shared/components/path-picker/server-path-picker-directory-list.tsx +103 -0
- package/src/shared/components/path-picker/server-path-picker-footer.tsx +58 -0
- package/src/shared/components/path-picker/server-path-picker-locations.tsx +148 -0
- package/src/shared/components/path-picker/server-path-picker-navigation-bar.tsx +198 -0
- package/src/shared/components/path-picker/server-path-picker-new-folder.tsx +59 -0
- package/src/shared/components/path-picker/server-path-picker-toolbar.tsx +25 -0
- package/src/shared/hooks/use-projects.ts +28 -0
- package/src/shared/hooks/use-server-path-create-directory.ts +6 -0
- package/src/shared/hooks/use-server-path-search.ts +40 -0
- package/src/shared/lib/api/index.ts +1 -0
- package/src/shared/lib/api/projects/project.utils.ts +14 -0
- package/src/shared/lib/api/types.ts +27 -0
- package/src/shared/lib/api/utils/server-path.utils.ts +21 -1
- package/src/shared/lib/i18n/locales/en-US/chat.json +40 -2
- package/src/shared/lib/i18n/locales/en-US/core.json +2 -3
- package/src/shared/lib/i18n/locales/en-US/desktop-update.json +5 -1
- package/src/shared/lib/i18n/locales/en-US/marketplace.json +1 -0
- package/src/shared/lib/i18n/locales/en-US/path-picker.json +25 -0
- package/src/shared/lib/i18n/locales/zh-CN/chat.json +40 -2
- package/src/shared/lib/i18n/locales/zh-CN/core.json +2 -3
- package/src/shared/lib/i18n/locales/zh-CN/desktop-update.json +5 -1
- package/src/shared/lib/i18n/locales/zh-CN/marketplace.json +1 -0
- package/src/shared/lib/i18n/locales/zh-CN/path-picker.json +25 -0
- package/dist/assets/api-IlX_CbwU.js +0 -13
- package/dist/assets/app-presenter-provider-Bbl_MVbg.js +0 -106
- package/dist/assets/desktop-update-config-DreMJu9z.js +0 -1
- package/dist/assets/doc-browser-DJQ5XPsi2.js +0 -1
- package/dist/assets/index-0av6WO9-.css +0 -1
- package/dist/assets/index-CT0iYlxV.js +0 -2
- package/dist/assets/loader-circle-DtKek-qG.js +0 -1
- package/dist/assets/mcp-marketplace-page-4qMwFDpz.js +0 -1
- package/dist/assets/mcp-marketplace-page-BbxN5aZF.js +0 -9
- package/dist/assets/plus-BwgE-Zwf.js +0 -1
- package/dist/assets/providers-config-page-DfiWveEF.js +0 -1
- package/dist/assets/remote-BaVECEZw.js +0 -1
- package/dist/assets/rotate-cw-CMRZxIkH.js +0 -1
- package/dist/assets/runtime-config-page-wygwvMB4.js +0 -1
- package/dist/assets/security-config-BPzet76z.js +0 -1
- package/dist/assets/use-config-D6yx0lb0.js +0 -1
- package/dist/assets/x-B3o1dWR5.js +0 -1
- package/src/features/chat/features/input/input-surface-plugins/__tests__/panel-app-reference-plugin.test.ts +0 -77
- package/src/features/chat/features/input/input-surface-plugins/panel-app-reference-plugin.utils.ts +0 -144
package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { render, screen } from "@testing-library/react";
|
|
1
|
+
import { fireEvent, render, screen } from "@testing-library/react";
|
|
2
2
|
import { isValidElement } from "react";
|
|
3
3
|
import type { NcpMessage } from "@nextclaw/ncp";
|
|
4
4
|
import { beforeEach, expect, it, vi } from "vitest";
|
|
@@ -20,6 +20,7 @@ const captures = vi.hoisted(() => ({
|
|
|
20
20
|
handleToolAction: vi.fn(),
|
|
21
21
|
showContent: vi.fn(),
|
|
22
22
|
filePreviewProps: [] as Array<{ showBreadcrumbs?: boolean }>,
|
|
23
|
+
selectedSession: null as null | { projectRoot: string; workingDir?: string | null },
|
|
23
24
|
}));
|
|
24
25
|
|
|
25
26
|
vi.mock("@nextclaw/agent-chat-ui", async (importOriginal) => {
|
|
@@ -31,6 +32,7 @@ vi.mock("@nextclaw/agent-chat-ui", async (importOriginal) => {
|
|
|
31
32
|
onToolAction?: (action: unknown) => void;
|
|
32
33
|
onFileOpen?: (action: unknown) => void;
|
|
33
34
|
onAttachmentOpen?: (file: unknown) => void;
|
|
35
|
+
onInlineTokenClick?: (token: unknown) => void;
|
|
34
36
|
renderInlineDisplay?: (display: unknown) => unknown;
|
|
35
37
|
renderPanelAppCard?: (panelApp: unknown) => unknown;
|
|
36
38
|
texts?: Record<string, unknown>;
|
|
@@ -53,6 +55,10 @@ vi.mock("@/features/chat/components/providers/chat-presenter.provider", () => ({
|
|
|
53
55
|
}),
|
|
54
56
|
}));
|
|
55
57
|
|
|
58
|
+
vi.mock("@/features/chat/features/ncp/hooks/use-ncp-chat-derived-state", () => ({
|
|
59
|
+
useNcpChatSelectedSession: () => captures.selectedSession,
|
|
60
|
+
}));
|
|
61
|
+
|
|
56
62
|
vi.mock("@/app/components/i18n-provider", () => ({
|
|
57
63
|
useI18n: () => ({ language: captures.language }),
|
|
58
64
|
}));
|
|
@@ -81,6 +87,7 @@ beforeEach(() => {
|
|
|
81
87
|
captures.handleToolAction.mockReset();
|
|
82
88
|
captures.showContent.mockReset();
|
|
83
89
|
captures.filePreviewProps = [];
|
|
90
|
+
captures.selectedSession = null;
|
|
84
91
|
});
|
|
85
92
|
|
|
86
93
|
it("reuses adapted message references when the source message object is unchanged", () => {
|
|
@@ -100,9 +107,7 @@ it("reuses adapted message references when the source message object is unchange
|
|
|
100
107
|
const firstMessages =
|
|
101
108
|
captures.renders[captures.renders.length - 1]?.messages ?? [];
|
|
102
109
|
|
|
103
|
-
rerender(
|
|
104
|
-
<ChatMessageListContainer messages={[message]} isSending={false} />,
|
|
105
|
-
);
|
|
110
|
+
rerender(<ChatMessageListContainer messages={[message]} isSending={false} />);
|
|
106
111
|
|
|
107
112
|
const secondMessages =
|
|
108
113
|
captures.renders[captures.renders.length - 1]?.messages ?? [];
|
|
@@ -223,7 +228,7 @@ it("adds a completed assistant process summary without inventing a duration", ()
|
|
|
223
228
|
toolCallId: "tool-1",
|
|
224
229
|
toolName: "exec_command",
|
|
225
230
|
state: "result",
|
|
226
|
-
args:
|
|
231
|
+
args: '{"cmd":"git status"}',
|
|
227
232
|
result: "clean",
|
|
228
233
|
},
|
|
229
234
|
{
|
|
@@ -270,7 +275,7 @@ it("derives completed assistant process duration from message lifecycle", () =>
|
|
|
270
275
|
toolCallId: "tool-1",
|
|
271
276
|
toolName: "exec_command",
|
|
272
277
|
state: "result",
|
|
273
|
-
args:
|
|
278
|
+
args: '{"cmd":"git status"}',
|
|
274
279
|
result: "clean",
|
|
275
280
|
},
|
|
276
281
|
{
|
|
@@ -322,6 +327,53 @@ it("wires markdown file link actions to the workspace file preview manager", ()
|
|
|
322
327
|
expect(captures.openFilePreview).toHaveBeenCalledWith(action);
|
|
323
328
|
});
|
|
324
329
|
|
|
330
|
+
it("opens workspace file and directory tokens from the message", () => {
|
|
331
|
+
captures.selectedSession = {
|
|
332
|
+
projectRoot: "/tmp/project",
|
|
333
|
+
workingDir: "/tmp/project/packages/ui",
|
|
334
|
+
};
|
|
335
|
+
render(<ChatMessageListContainer messages={[]} isSending={false} />);
|
|
336
|
+
const onInlineTokenClick = captures.renders[captures.renders.length - 1]?.onInlineTokenClick;
|
|
337
|
+
|
|
338
|
+
onInlineTokenClick?.({
|
|
339
|
+
kind: "workspace_file",
|
|
340
|
+
key: "docs/guide.md",
|
|
341
|
+
label: "guide.md",
|
|
342
|
+
rawText: "@file:docs%2Fguide.md",
|
|
343
|
+
});
|
|
344
|
+
onInlineTokenClick?.({
|
|
345
|
+
kind: "workspace_directory",
|
|
346
|
+
key: "packages/server",
|
|
347
|
+
label: "server",
|
|
348
|
+
rawText: "@folder:packages%2Fserver",
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
expect(captures.openFilePreview).toHaveBeenNthCalledWith(1, {
|
|
352
|
+
path: "/tmp/project/docs/guide.md",
|
|
353
|
+
label: "guide.md",
|
|
354
|
+
viewMode: "preview",
|
|
355
|
+
});
|
|
356
|
+
expect(captures.openFilePreview).toHaveBeenNthCalledWith(2, {
|
|
357
|
+
path: "/tmp/project/packages/server",
|
|
358
|
+
label: "server",
|
|
359
|
+
viewMode: "preview",
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it("opens panel app tokens through the existing content owner", () => {
|
|
364
|
+
render(<ChatMessageListContainer messages={[]} isSending={false} />);
|
|
365
|
+
captures.renders[captures.renders.length - 1]?.onInlineTokenClick?.({
|
|
366
|
+
kind: "panel_app",
|
|
367
|
+
key: "task-board",
|
|
368
|
+
label: "Task Board",
|
|
369
|
+
rawText: "@panel-app:task-board",
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
expect(captures.showContent).toHaveBeenCalledWith({
|
|
373
|
+
target: { type: "panel_app", payload: { appId: "task-board" } },
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
|
|
325
377
|
it("opens message attachments in the workspace file preview", () => {
|
|
326
378
|
const message = {
|
|
327
379
|
id: "assistant-attachment",
|
|
@@ -385,12 +437,16 @@ it("renders context inheritance as a divider without repeating inherited message
|
|
|
385
437
|
isSending={false}
|
|
386
438
|
/>,
|
|
387
439
|
);
|
|
388
|
-
const renderedMessages = captures.renders.flatMap(
|
|
440
|
+
const renderedMessages = captures.renders.flatMap(
|
|
441
|
+
(rendered) => rendered.messages,
|
|
442
|
+
);
|
|
389
443
|
const renderedPayload = JSON.stringify(renderedMessages);
|
|
390
444
|
|
|
391
445
|
expect(screen.getByText("chatContextInheritanceInherited")).toBeTruthy();
|
|
392
446
|
expect(container.querySelector('[title*="parent-session"]')).toBeTruthy();
|
|
393
|
-
expect(
|
|
447
|
+
expect(
|
|
448
|
+
container.querySelector('[title*="chatContextInheritanceMessages: 1"]'),
|
|
449
|
+
).toBeTruthy();
|
|
394
450
|
expect(renderedPayload).toContain("child visible request");
|
|
395
451
|
expect(renderedPayload).not.toContain("parent context");
|
|
396
452
|
});
|
|
@@ -416,7 +472,7 @@ it("keeps Hermes tool invocation parts as tool cards instead of flattening them
|
|
|
416
472
|
toolCallId: "hermes-inline-tool-1",
|
|
417
473
|
toolName: "search_files",
|
|
418
474
|
state: "call",
|
|
419
|
-
args:
|
|
475
|
+
args: '{"pattern":"*.py"}',
|
|
420
476
|
},
|
|
421
477
|
{
|
|
422
478
|
type: "text",
|
|
@@ -495,10 +551,12 @@ it("delegates tool actions to the chat thread manager owner", () => {
|
|
|
495
551
|
it("passes the inline panel app renderer to the shared chat UI", () => {
|
|
496
552
|
render(<ChatMessageListContainer messages={[]} isSending={false} />);
|
|
497
553
|
|
|
498
|
-
expect(
|
|
554
|
+
expect(
|
|
555
|
+
captures.renders[captures.renders.length - 1]?.renderPanelAppCard,
|
|
556
|
+
).toEqual(expect.any(Function));
|
|
499
557
|
});
|
|
500
558
|
|
|
501
|
-
it("
|
|
559
|
+
it("connects inline HTML actions to the chat thread manager", () => {
|
|
502
560
|
render(<ChatMessageListContainer messages={[]} isSending={false} />);
|
|
503
561
|
|
|
504
562
|
const renderInlineDisplay =
|
|
@@ -514,24 +572,30 @@ it("renders inline file displays through the workspace file preview", () => {
|
|
|
514
572
|
description: "Rendered HTML",
|
|
515
573
|
});
|
|
516
574
|
|
|
517
|
-
if (!isValidElement
|
|
575
|
+
if (!isValidElement(rendered)) {
|
|
518
576
|
throw new Error("Expected inline file renderer to return a React element");
|
|
519
577
|
}
|
|
520
|
-
expect(rendered.props.display).toEqual({
|
|
521
|
-
target: {
|
|
522
|
-
type: "file",
|
|
523
|
-
payload: { path: "preview.html", viewer: "rendered" },
|
|
524
|
-
},
|
|
525
|
-
title: "Preview",
|
|
526
|
-
description: "Rendered HTML",
|
|
527
|
-
});
|
|
528
|
-
|
|
529
578
|
render(rendered);
|
|
530
579
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
expect(captures.
|
|
580
|
+
fireEvent.click(screen.getByLabelText("chatPanelCardExpand"));
|
|
581
|
+
fireEvent.click(screen.getByLabelText("chatWorkspaceOpenSource"));
|
|
582
|
+
|
|
583
|
+
expect(captures.openFilePreview).toHaveBeenNthCalledWith(1, {
|
|
584
|
+
path: "preview.html",
|
|
585
|
+
label: "Preview",
|
|
586
|
+
viewMode: "preview",
|
|
587
|
+
previewViewer: "rendered",
|
|
588
|
+
line: undefined,
|
|
589
|
+
column: undefined,
|
|
590
|
+
});
|
|
591
|
+
expect(captures.openFilePreview).toHaveBeenNthCalledWith(2, {
|
|
592
|
+
path: "preview.html",
|
|
593
|
+
label: "Preview",
|
|
594
|
+
viewMode: "preview",
|
|
595
|
+
previewViewer: "source",
|
|
596
|
+
line: undefined,
|
|
597
|
+
column: undefined,
|
|
598
|
+
});
|
|
535
599
|
});
|
|
536
600
|
|
|
537
601
|
it("keeps inline panel app displays expandable from the card header", () => {
|
|
@@ -548,7 +612,9 @@ it("keeps inline panel app displays expandable from the card header", () => {
|
|
|
548
612
|
});
|
|
549
613
|
|
|
550
614
|
if (!isValidElement<{ showExpandAction?: boolean }>(rendered)) {
|
|
551
|
-
throw new Error(
|
|
615
|
+
throw new Error(
|
|
616
|
+
"Expected inline panel app renderer to return a React element",
|
|
617
|
+
);
|
|
552
618
|
}
|
|
553
619
|
expect(rendered.props.showExpandAction).toBeUndefined();
|
|
554
620
|
});
|
|
@@ -1,10 +1,14 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
1
2
|
import type {
|
|
2
3
|
ChatFileOpenActionViewModel,
|
|
3
4
|
ChatInlineDisplayViewModel,
|
|
4
5
|
} from "@nextclaw/agent-chat-ui";
|
|
5
|
-
import { FileText } from "lucide-react";
|
|
6
|
+
import { Code2, FileText, PanelRightOpen } from "lucide-react";
|
|
6
7
|
import { ChatSessionWorkspaceFilePreview } from "@/features/chat/features/workspace/components/chat-session-workspace-file-preview";
|
|
7
8
|
import { createWorkspaceFileTab } from "@/features/chat/features/workspace/utils/chat-workspace-file-tab.utils";
|
|
9
|
+
import { IconActionButton } from "@/shared/components/ui/actions/icon-action-button";
|
|
10
|
+
import { t } from "@/shared/lib/i18n";
|
|
11
|
+
import { cn } from "@/shared/lib/utils";
|
|
8
12
|
|
|
9
13
|
type ChatInlineFilePreviewProps = {
|
|
10
14
|
display: ChatInlineDisplayViewModel;
|
|
@@ -14,9 +18,7 @@ type ChatInlineFilePreviewProps = {
|
|
|
14
18
|
onFileOpen: (action: ChatFileOpenActionViewModel) => void;
|
|
15
19
|
};
|
|
16
20
|
|
|
17
|
-
|
|
18
|
-
return path.split(/[\\/]/).filter(Boolean).pop() ?? path;
|
|
19
|
-
}
|
|
21
|
+
const INLINE_HTML_MIN_HEIGHT = 240;
|
|
20
22
|
|
|
21
23
|
export function ChatInlineFilePreview({
|
|
22
24
|
display,
|
|
@@ -25,6 +27,9 @@ export function ChatInlineFilePreview({
|
|
|
25
27
|
sessionWorkingDir,
|
|
26
28
|
onFileOpen,
|
|
27
29
|
}: ChatInlineFilePreviewProps) {
|
|
30
|
+
const [htmlContentHeight, setHtmlContentHeight] = useState<number | null>(
|
|
31
|
+
null,
|
|
32
|
+
);
|
|
28
33
|
if (display.target.type !== "file") {
|
|
29
34
|
return null;
|
|
30
35
|
}
|
|
@@ -43,35 +48,99 @@ export function ChatInlineFilePreview({
|
|
|
43
48
|
if (!file) {
|
|
44
49
|
return null;
|
|
45
50
|
}
|
|
46
|
-
const title =
|
|
51
|
+
const title =
|
|
52
|
+
file.label ?? file.path.split(/[\\/]/).filter(Boolean).pop() ?? file.path;
|
|
53
|
+
const isRenderedHtml =
|
|
54
|
+
file.previewViewer === "rendered" && /\.html?$/i.test(file.path);
|
|
55
|
+
const openInWorkspace = (previewViewer: "rendered" | "source") => {
|
|
56
|
+
onFileOpen({
|
|
57
|
+
path: file.path,
|
|
58
|
+
label: file.label ?? undefined,
|
|
59
|
+
viewMode: "preview",
|
|
60
|
+
previewViewer,
|
|
61
|
+
line: file.line ?? undefined,
|
|
62
|
+
column: file.column ?? undefined,
|
|
63
|
+
});
|
|
64
|
+
};
|
|
47
65
|
|
|
48
66
|
return (
|
|
49
67
|
<section
|
|
50
|
-
className=
|
|
68
|
+
className={cn(
|
|
69
|
+
"my-2 w-full max-w-[48rem]",
|
|
70
|
+
isRenderedHtml
|
|
71
|
+
? "group/inline-html relative overflow-visible"
|
|
72
|
+
: "overflow-hidden rounded-lg border border-border bg-card shadow-sm",
|
|
73
|
+
)}
|
|
51
74
|
data-chat-inline-file-preview="true"
|
|
52
75
|
data-chat-message-wide-content="true"
|
|
53
76
|
>
|
|
54
|
-
|
|
55
|
-
<div
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
</div>
|
|
62
|
-
<p
|
|
63
|
-
className="mt-1 truncate font-mono text-[11px] font-normal text-muted-foreground"
|
|
64
|
-
title={file.path}
|
|
77
|
+
{isRenderedHtml ? (
|
|
78
|
+
<div
|
|
79
|
+
className={cn(
|
|
80
|
+
"pointer-events-none absolute bottom-full left-1/2 z-10 -translate-x-1/2 pb-2 opacity-0 transition-opacity duration-150",
|
|
81
|
+
"group-hover/inline-html:pointer-events-auto group-hover/inline-html:opacity-100 group-focus-within/inline-html:pointer-events-auto group-focus-within/inline-html:opacity-100",
|
|
82
|
+
)}
|
|
83
|
+
data-chat-inline-file-actions="true"
|
|
65
84
|
>
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
85
|
+
<div
|
|
86
|
+
className="flex items-center gap-0.5 rounded-lg bg-background/95 p-0.5 shadow-md ring-1 ring-border/60 backdrop-blur-sm"
|
|
87
|
+
data-chat-inline-file-actions-surface="true"
|
|
88
|
+
>
|
|
89
|
+
<IconActionButton
|
|
90
|
+
size="sm"
|
|
91
|
+
icon={<PanelRightOpen className="h-3.5 w-3.5" />}
|
|
92
|
+
label={t("chatPanelCardExpand")}
|
|
93
|
+
tooltipSide="top"
|
|
94
|
+
onClick={() => openInWorkspace("rendered")}
|
|
95
|
+
/>
|
|
96
|
+
<IconActionButton
|
|
97
|
+
size="sm"
|
|
98
|
+
icon={<Code2 className="h-3.5 w-3.5" />}
|
|
99
|
+
label={t("chatWorkspaceOpenSource")}
|
|
100
|
+
tooltipSide="top"
|
|
101
|
+
onClick={() => openInWorkspace("source")}
|
|
102
|
+
/>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
) : (
|
|
106
|
+
<header className="border-b border-border bg-muted/45 px-3 py-2">
|
|
107
|
+
<div className="flex min-w-0 items-center gap-2 text-xs font-medium text-foreground">
|
|
108
|
+
<FileText
|
|
109
|
+
className="h-3.5 w-3.5 shrink-0 text-muted-foreground"
|
|
110
|
+
aria-hidden
|
|
111
|
+
/>
|
|
112
|
+
<span className="truncate">{title}</span>
|
|
113
|
+
</div>
|
|
114
|
+
<p
|
|
115
|
+
className="mt-1 truncate font-mono text-[11px] font-normal text-muted-foreground"
|
|
116
|
+
title={file.path}
|
|
117
|
+
>
|
|
118
|
+
{file.path}
|
|
119
|
+
</p>
|
|
120
|
+
</header>
|
|
121
|
+
)}
|
|
122
|
+
<div
|
|
123
|
+
className={cn(
|
|
124
|
+
"min-h-[240px] overflow-hidden",
|
|
125
|
+
isRenderedHtml
|
|
126
|
+
? "h-[240px] max-h-[min(80vh,720px)] rounded-lg"
|
|
127
|
+
: "h-[420px] max-h-[min(60vh,420px)]",
|
|
128
|
+
)}
|
|
129
|
+
data-chat-inline-file-viewport="true"
|
|
130
|
+
style={
|
|
131
|
+
isRenderedHtml && htmlContentHeight
|
|
132
|
+
? { height: Math.max(INLINE_HTML_MIN_HEIGHT, htmlContentHeight) }
|
|
133
|
+
: undefined
|
|
134
|
+
}
|
|
135
|
+
>
|
|
70
136
|
<ChatSessionWorkspaceFilePreview
|
|
71
137
|
file={file}
|
|
72
138
|
sessionProjectRoot={sessionProjectRoot}
|
|
73
139
|
sessionWorkingDir={sessionWorkingDir}
|
|
74
140
|
showBreadcrumbs={false}
|
|
141
|
+
onHtmlContentHeightChange={
|
|
142
|
+
isRenderedHtml ? setHtmlContentHeight : undefined
|
|
143
|
+
}
|
|
75
144
|
onFileOpen={onFileOpen}
|
|
76
145
|
/>
|
|
77
146
|
</div>
|
|
@@ -14,7 +14,10 @@ import {
|
|
|
14
14
|
buildChatMessageAdapterTexts,
|
|
15
15
|
buildChatMessageTexts,
|
|
16
16
|
} from "@/features/chat/features/message/utils/chat-message-texts.utils";
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
readInlineTokensFromMetadata,
|
|
19
|
+
resolveWorkspaceReferencePath,
|
|
20
|
+
} from "@/features/chat/features/input/utils/chat-inline-token.utils";
|
|
18
21
|
import { adaptNcpMessageToUiMessage } from "@/features/chat/features/session/utils/ncp-session-adapter.utils";
|
|
19
22
|
import {
|
|
20
23
|
readContextCompactionTimeline,
|
|
@@ -362,9 +365,27 @@ export function ChatMessageListContainer({
|
|
|
362
365
|
const sessionSkillsQuery = useChatQueryStore((state) => state.snapshot.sessionSkillsQuery);
|
|
363
366
|
const handleInlineTokenClick = useCallback(
|
|
364
367
|
(token: ChatInlineTokenViewModel) => {
|
|
365
|
-
if (token.kind
|
|
368
|
+
if (token.kind === "panel_app") {
|
|
369
|
+
void presenter.chatUiManager.showContent({
|
|
370
|
+
target: { type: "panel_app", payload: { appId: token.key } },
|
|
371
|
+
});
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
if (token.kind === "workspace_file" || token.kind === "workspace_directory") {
|
|
375
|
+
const path = resolveWorkspaceReferencePath({
|
|
376
|
+
projectRoot: selectedSession?.projectRoot,
|
|
377
|
+
relativePath: token.key,
|
|
378
|
+
});
|
|
379
|
+
if (path) {
|
|
380
|
+
presenter.chatThreadManager.openFilePreview({
|
|
381
|
+
path,
|
|
382
|
+
label: token.label,
|
|
383
|
+
viewMode: "preview",
|
|
384
|
+
});
|
|
385
|
+
}
|
|
366
386
|
return;
|
|
367
387
|
}
|
|
388
|
+
if (token.kind !== "skill") return;
|
|
368
389
|
const skillKey = token.key.trim();
|
|
369
390
|
if (!skillKey) {
|
|
370
391
|
return;
|
|
@@ -382,7 +403,7 @@ export function ChatMessageListContainer({
|
|
|
382
403
|
previewViewer: "rendered",
|
|
383
404
|
});
|
|
384
405
|
},
|
|
385
|
-
[presenter.chatThreadManager, sessionSkillsQuery],
|
|
406
|
+
[presenter.chatThreadManager, presenter.chatUiManager, selectedSession?.projectRoot, sessionSkillsQuery],
|
|
386
407
|
);
|
|
387
408
|
const handleAttachmentOpen = useCallback(
|
|
388
409
|
(file: { label: string; mimeType: string; dataUrl?: string; sizeBytes?: number; isImage: boolean }) => {
|
|
@@ -107,6 +107,7 @@ export function buildChatMessageTexts(
|
|
|
107
107
|
copyMessageLabel: t("chatMessageCopy"),
|
|
108
108
|
copiedMessageLabel: t("chatMessageCopied"),
|
|
109
109
|
mermaidDiagramLabel: t("chatMermaidDiagram"),
|
|
110
|
+
mermaidLoadingLabel: t("chatMermaidLoading"),
|
|
110
111
|
mermaidRenderErrorLabel: t("chatMermaidRenderError"),
|
|
111
112
|
typingLabel: t("chatTyping"),
|
|
112
113
|
reasoningCharacterCountTemplates: {
|
|
@@ -1,19 +1,57 @@
|
|
|
1
|
-
import { renderHook, waitFor } from "@testing-library/react";
|
|
2
|
-
import type
|
|
3
|
-
import { useHydratedNcpAgent } from "@nextclaw/ncp-react";
|
|
1
|
+
import { act, renderHook, waitFor } from "@testing-library/react";
|
|
2
|
+
import { NcpEventType, type NcpAgentClientEndpoint } from "@nextclaw/ncp";
|
|
3
|
+
import { useHydratedNcpAgent, useNcpAgentRuntime } from "@nextclaw/ncp-react";
|
|
4
|
+
import { DefaultNcpAgentConversationStateManager } from "@nextclaw/ncp-toolkit";
|
|
4
5
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
6
|
|
|
6
7
|
const mocks = vi.hoisted(() => ({
|
|
8
|
+
send: vi.fn(),
|
|
7
9
|
stream: vi.fn(),
|
|
8
10
|
stop: vi.fn(),
|
|
9
11
|
}));
|
|
10
12
|
|
|
11
13
|
describe("useHydratedNcpAgent", () => {
|
|
12
14
|
beforeEach(() => {
|
|
15
|
+
mocks.send.mockReset();
|
|
13
16
|
mocks.stream.mockReset();
|
|
14
17
|
mocks.stop.mockReset();
|
|
15
18
|
});
|
|
16
19
|
|
|
20
|
+
it("clears a draft conversation error when a valid retry starts", async () => {
|
|
21
|
+
const client = {
|
|
22
|
+
send: mocks.send.mockResolvedValue(null),
|
|
23
|
+
stop: mocks.stop.mockResolvedValue(undefined),
|
|
24
|
+
stream: mocks.stream.mockResolvedValue(undefined),
|
|
25
|
+
subscribe: vi.fn(() => () => {}),
|
|
26
|
+
} as unknown as NcpAgentClientEndpoint;
|
|
27
|
+
const manager = new DefaultNcpAgentConversationStateManager();
|
|
28
|
+
const { result } = renderHook(() =>
|
|
29
|
+
useNcpAgentRuntime({
|
|
30
|
+
client,
|
|
31
|
+
manager,
|
|
32
|
+
}),
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
await act(async () => {
|
|
36
|
+
await manager.dispatch({
|
|
37
|
+
occurredAt: new Date().toISOString(),
|
|
38
|
+
type: NcpEventType.EndpointError,
|
|
39
|
+
payload: {
|
|
40
|
+
code: "runtime-error",
|
|
41
|
+
message: "network error",
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
expect(result.current.snapshot.error?.message).toBe("network error");
|
|
46
|
+
|
|
47
|
+
await act(async () => {
|
|
48
|
+
await result.current.send("retry");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
expect(result.current.snapshot.error).toBeNull();
|
|
52
|
+
expect(mocks.send).toHaveBeenCalledOnce();
|
|
53
|
+
});
|
|
54
|
+
|
|
17
55
|
it("treats a newly selected session as hydrating immediately on rerender", async () => {
|
|
18
56
|
const client = {
|
|
19
57
|
stop: mocks.stop.mockResolvedValue(undefined),
|
|
@@ -54,4 +92,59 @@ describe("useHydratedNcpAgent", () => {
|
|
|
54
92
|
expect(mocks.stream).toHaveBeenCalledWith({ sessionId: "session-b" });
|
|
55
93
|
expect(mocks.stream).toHaveBeenCalledTimes(2);
|
|
56
94
|
});
|
|
95
|
+
|
|
96
|
+
it("keeps a selected session hydrated when an earlier empty-session reset finishes late", async () => {
|
|
97
|
+
const stopResolvers: Array<() => void> = [];
|
|
98
|
+
const client = {
|
|
99
|
+
stop: mocks.stop.mockImplementation(
|
|
100
|
+
() => new Promise<void>((resolve) => stopResolvers.push(resolve)),
|
|
101
|
+
),
|
|
102
|
+
stream: mocks.stream.mockResolvedValue(undefined),
|
|
103
|
+
subscribe: vi.fn(() => () => {}),
|
|
104
|
+
} as unknown as NcpAgentClientEndpoint;
|
|
105
|
+
const historyMessage = {
|
|
106
|
+
id: "message-1",
|
|
107
|
+
sessionId: "session-mobile",
|
|
108
|
+
role: "user",
|
|
109
|
+
status: "final",
|
|
110
|
+
parts: [{ type: "text", text: "Existing history" }],
|
|
111
|
+
timestamp: "2026-07-16T00:00:00.000Z",
|
|
112
|
+
} as const;
|
|
113
|
+
const loadSeed = vi.fn().mockResolvedValue({
|
|
114
|
+
messages: [historyMessage],
|
|
115
|
+
status: "idle",
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const { result, rerender } = renderHook(
|
|
119
|
+
({ sessionId }: { sessionId: string | undefined }) =>
|
|
120
|
+
useHydratedNcpAgent({
|
|
121
|
+
sessionId,
|
|
122
|
+
client: client as never,
|
|
123
|
+
loadSeed,
|
|
124
|
+
}),
|
|
125
|
+
{
|
|
126
|
+
initialProps: {
|
|
127
|
+
sessionId: undefined as string | undefined,
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
await waitFor(() => expect(stopResolvers).toHaveLength(1));
|
|
133
|
+
rerender({ sessionId: "session-mobile" });
|
|
134
|
+
await waitFor(() => expect(stopResolvers).toHaveLength(2));
|
|
135
|
+
|
|
136
|
+
await act(async () => {
|
|
137
|
+
stopResolvers[1]?.();
|
|
138
|
+
await Promise.resolve();
|
|
139
|
+
});
|
|
140
|
+
await waitFor(() => {
|
|
141
|
+
expect(result.current.visibleMessages).toEqual([historyMessage]);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
await act(async () => {
|
|
145
|
+
stopResolvers[0]?.();
|
|
146
|
+
await Promise.resolve();
|
|
147
|
+
});
|
|
148
|
+
expect(result.current.visibleMessages).toEqual([historyMessage]);
|
|
149
|
+
});
|
|
57
150
|
});
|
package/src/features/chat/features/project/components/__tests__/chat-project-create-dialog.test.tsx
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { ChatProjectCreateDialog } from '@/features/chat/features/project/components/chat-project-create-dialog';
|
|
5
|
+
import { setLanguage } from '@/shared/lib/i18n';
|
|
6
|
+
|
|
7
|
+
const pathPickerSpy = vi.fn();
|
|
8
|
+
|
|
9
|
+
vi.mock('@/shared/components/path-picker/server-path-picker-dialog', () => ({
|
|
10
|
+
ServerPathPickerDialog: (props: unknown) => pathPickerSpy(props),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
describe('ChatProjectCreateDialog', () => {
|
|
14
|
+
it('selects the optional target directory with the shared path picker', async () => {
|
|
15
|
+
setLanguage('zh');
|
|
16
|
+
const user = userEvent.setup();
|
|
17
|
+
const onCreate = vi.fn(async () => undefined);
|
|
18
|
+
|
|
19
|
+
render(
|
|
20
|
+
<ChatProjectCreateDialog
|
|
21
|
+
open
|
|
22
|
+
defaultWorkspacePath="/workspace"
|
|
23
|
+
templates={[
|
|
24
|
+
{ id: 'empty', name: 'Empty', description: 'Empty project' },
|
|
25
|
+
{
|
|
26
|
+
id: 'knowledge-base',
|
|
27
|
+
name: 'Knowledge base',
|
|
28
|
+
description: 'Knowledge base project',
|
|
29
|
+
},
|
|
30
|
+
]}
|
|
31
|
+
isCreating={false}
|
|
32
|
+
onOpenChange={() => {}}
|
|
33
|
+
onCreate={onCreate}
|
|
34
|
+
/>,
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
await user.type(screen.getByLabelText('项目名称'), '个人知识库');
|
|
38
|
+
fireEvent.change(document.querySelector('select')!, {
|
|
39
|
+
target: { value: 'knowledge-base' },
|
|
40
|
+
});
|
|
41
|
+
await user.click(screen.getByRole('button', { name: '浏览' }));
|
|
42
|
+
expect(pathPickerSpy).toHaveBeenLastCalledWith(
|
|
43
|
+
expect.objectContaining({
|
|
44
|
+
open: true,
|
|
45
|
+
currentPath: '',
|
|
46
|
+
defaultWorkspacePath: '/workspace',
|
|
47
|
+
title: '选择项目目标目录',
|
|
48
|
+
}),
|
|
49
|
+
);
|
|
50
|
+
const pickerProps = pathPickerSpy.mock.lastCall?.[0] as {
|
|
51
|
+
onConfirm: (path: string) => void;
|
|
52
|
+
};
|
|
53
|
+
act(() => pickerProps.onConfirm('/srv/knowledge'));
|
|
54
|
+
expect((screen.getByLabelText('目标目录(可选)') as HTMLInputElement).value).toBe(
|
|
55
|
+
'/srv/knowledge',
|
|
56
|
+
);
|
|
57
|
+
await user.click(screen.getByRole('button', { name: '创建项目' }));
|
|
58
|
+
|
|
59
|
+
await waitFor(() => {
|
|
60
|
+
expect(onCreate).toHaveBeenCalledWith({
|
|
61
|
+
name: '个人知识库',
|
|
62
|
+
rootPath: '/srv/knowledge',
|
|
63
|
+
template: 'knowledge-base',
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
});
|