@nextclaw/ui 0.15.26 → 0.15.27
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 +16 -0
- package/dist/assets/{api-CK3Fro7z.js → api-a_GaOFc7.js} +4 -4
- package/dist/assets/{appearance-settings-page-zqwWO8Dy.js → appearance-settings-page-DlGXLgff.js} +1 -1
- package/dist/assets/{channels-list-page-CVCZBlCL.js → channels-list-page-BEEk4Uc9.js} +2 -2
- package/dist/assets/{chat-page-D4wehRTz.js → chat-page-DUQXXqQm.js} +1 -1
- package/dist/assets/{config-split-page-CceNFvlY.js → config-split-page-ps1Aod3g.js} +1 -1
- package/dist/assets/{confirm-dialog-CWeNYWZY.js → confirm-dialog-DJSUNG33.js} +2 -2
- package/dist/assets/desktop-update-config--zagjHic.js +1 -0
- package/dist/assets/{dist-D9xtGgdL.js → dist-BkTYNzm_.js} +1 -1
- package/dist/assets/{dist-AYxkFx2B.js → dist-DkR7_Dpl.js} +1 -1
- package/dist/assets/doc-browser-DahzJEWj.js +1 -0
- package/dist/assets/doc-browser-Q67TOGVS.js +1 -0
- package/dist/assets/{doc-browser-context-Cb7Z0j90.js → doc-browser-context-BQim8SqD.js} +1 -1
- package/dist/assets/form-actions-B2-bpFie.js +1 -0
- package/dist/assets/index-Ba_o5fu9.js +109 -0
- package/dist/assets/index-DSLK7zgR.css +1 -0
- package/dist/assets/loader-circle-DyXZU92l.js +1 -0
- package/dist/assets/mcp-marketplace-page-B0dBE5CV.js +1 -0
- package/dist/assets/mcp-marketplace-page-w8panjZG.js +9 -0
- package/dist/assets/{model-config-page-DfOLyQqR.js → model-config-page-rrhFaque.js} +1 -1
- package/dist/assets/{navigation-link-MiiyoIjH.js → navigation-link-mpHFXwRr.js} +2 -2
- package/dist/assets/plus-Dhb4KuMH.js +1 -0
- package/dist/assets/{provider-scoped-model-input-DXzjr5l0.js → provider-scoped-model-input-CFsfWEt9.js} +1 -1
- package/dist/assets/{providers-config-page-CaO8WfZH.js → providers-config-page-D-TYFZsF.js} +1 -1
- package/dist/assets/react-BlXwvOLo.js +8 -0
- package/dist/assets/{refresh-cw-a2VVeeAF.js → refresh-cw-JFuyDj_P.js} +1 -1
- package/dist/assets/remote-BwVv_Qq-.js +1 -0
- package/dist/assets/rotate-cw-DjbbiIW_.js +1 -0
- package/dist/assets/{runtime-config-page-BuA4A02z.js → runtime-config-page-Br9j9Vxe.js} +1 -1
- package/dist/assets/{save-CGm23Yww.js → save-BL8P8CLX.js} +1 -1
- package/dist/assets/{search-CH2t--sH.js → search-bZL75Pgc.js} +1 -1
- package/dist/assets/{search-config-page-BAmYqoAz.js → search-config-page-B3fvzMd1.js} +1 -1
- package/dist/assets/{secrets-config-page-BlPOuql_.js → secrets-config-page-ClwTxtXd.js} +2 -2
- package/dist/assets/security-config-DmJw9QZA.js +1 -0
- package/dist/assets/{select-DnxxDpcV.js → select-rUZ8nIZu.js} +2 -2
- package/dist/assets/{settings-2-agnMYc8a.js → settings-2-BI4SCkxQ.js} +1 -1
- package/dist/assets/skeleton-CVmEPG43.js +1 -0
- package/dist/assets/{tag-chip-CPOc8b2N.js → tag-chip-BdI_yIOA.js} +1 -1
- package/dist/assets/x-BcEayhV6.js +1 -0
- package/dist/index.html +19 -19
- package/package.json +7 -7
- package/src/app/components/layout/app-layout.tsx +38 -2
- package/src/app/hooks/__tests__/use-app-event-consumers.test.ts +20 -0
- package/src/app/hooks/use-app-event-consumers.ts +21 -0
- package/src/app/presenters/app.presenter.ts +6 -0
- package/src/features/apps/components/__tests__/app-packages-panel.test.tsx +92 -14
- package/src/features/apps/components/__tests__/apps-panel.test.tsx +58 -0
- package/src/features/apps/components/app-artwork.tsx +49 -0
- package/src/features/apps/components/app-marketplace-catalog.tsx +332 -0
- package/src/features/apps/components/app-marketplace-cover.tsx +64 -0
- package/src/features/apps/components/app-marketplace-detail.tsx +161 -0
- package/src/features/apps/components/app-marketplace-dialog.tsx +107 -217
- package/src/features/apps/components/app-marketplace-operation.tsx +84 -0
- package/src/features/apps/components/app-package-card.tsx +201 -52
- package/src/features/apps/components/app-packages-panel.tsx +98 -26
- package/src/features/apps/components/apps-panel.tsx +28 -27
- package/src/features/apps/hooks/use-app-marketplace.test.ts +144 -0
- package/src/features/apps/hooks/use-app-marketplace.ts +55 -85
- package/src/features/apps/hooks/use-app-packages.ts +59 -4
- package/src/features/apps/index.ts +1 -0
- package/src/features/apps/managers/__tests__/app-package-operation-settlement.manager.test.ts +81 -0
- package/src/features/apps/managers/app-package-operation-settlement.manager.ts +80 -0
- package/src/features/apps/services/app-marketplace.service.ts +223 -0
- package/src/features/apps/types/app-marketplace.types.ts +75 -0
- package/src/features/chat/features/conversation/components/__tests__/session-conversation-input.streaming.test.tsx +64 -3
- package/src/features/chat/features/conversation/components/session-queued-input-rows.tsx +24 -14
- package/src/features/chat/features/conversation/hooks/__tests__/use-session-conversation-controller.test.tsx +53 -0
- package/src/features/chat/features/conversation/hooks/__tests__/use-session-run-queue.test.tsx +18 -0
- package/src/features/chat/features/conversation/hooks/use-chat-composer-reference-intent.ts +4 -1
- package/src/features/chat/features/conversation/hooks/use-session-conversation-controller.ts +126 -32
- package/src/features/chat/features/conversation/hooks/use-session-run-queue.ts +12 -2
- package/src/features/chat/features/input/utils/__tests__/chat-inline-token.utils.test.ts +31 -0
- package/src/features/chat/features/input/utils/chat-composer-token-protocol.utils.ts +5 -0
- package/src/features/chat/features/input/utils/chat-inline-token-composer.utils.ts +14 -0
- package/src/features/chat/features/input/utils/chat-inline-token.utils.ts +25 -0
- package/src/features/chat/features/message/hooks/use-chat-inline-token-actions.ts +8 -1
- package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel-content.test.tsx +77 -20
- package/src/features/chat/features/workspace/components/chat-session-workspace-directory-browser.tsx +56 -50
- package/src/features/chat/features/workspace/components/project-files/workspace-directory-tree.tsx +33 -11
- package/src/features/chat/features/workspace/components/project-files/workspace-project-tree-view.tsx +93 -0
- package/src/features/chat/features/workspace/stores/__tests__/workspace-project-tree.store.test.ts +38 -0
- package/src/features/chat/features/workspace/stores/workspace-project-tree.store.ts +160 -0
- package/src/features/chat/index.ts +1 -0
- package/src/features/chat/managers/__tests__/chat-composer-intent.manager.test.ts +29 -0
- package/src/features/chat/managers/chat-composer-intent.manager.ts +29 -2
- package/src/features/chat/presenters/chat.presenter.ts +2 -2
- package/src/features/panel-apps/components/__tests__/panel-apps-list.test.tsx +1 -1
- package/src/features/panel-apps/components/panel-apps-list.tsx +1 -6
- package/src/features/right-panel-resources/utils/__tests__/right-panel-resource-uri.utils.test.ts +34 -0
- package/src/features/right-panel-resources/utils/right-panel-resource-uri.utils.ts +29 -0
- package/src/features/service-apps/components/service-apps-panel.tsx +3 -17
- package/src/platforms/desktop/components/desktop-app-shell.tsx +5 -0
- package/src/platforms/mobile/components/mobile-app-shell.tsx +9 -1
- package/src/shared/components/actions/__tests__/navigation-link.test.tsx +12 -0
- package/src/shared/components/actions/navigation-link.tsx +6 -3
- package/src/shared/components/doc-browser/__tests__/doc-browser.test.tsx +34 -0
- package/src/shared/components/doc-browser/doc-browser-tab-strip.tsx +5 -0
- package/src/shared/components/doc-browser/doc-browser.tsx +11 -1
- package/src/shared/components/doc-browser/managers/doc-browser.manager.ts +13 -0
- package/src/shared/components/ui/tabs.tsx +24 -1
- package/src/shared/hooks/use-server-path-watch.ts +42 -0
- package/src/shared/lib/api/server-path/server-path.types.ts +10 -0
- package/src/shared/lib/api/types.ts +3 -0
- package/src/shared/lib/api/utils/server-path.utils.ts +10 -0
- package/src/shared/lib/i18n/locales/en-US/chat.json +1 -0
- package/src/shared/lib/i18n/locales/en-US/doc-browser.json +49 -7
- package/src/shared/lib/i18n/locales/zh-CN/chat.json +1 -0
- package/src/shared/lib/i18n/locales/zh-CN/doc-browser.json +49 -7
- package/vite.config.ts +32 -1
- package/dist/assets/desktop-update-config-Dirc3bpt.js +0 -1
- package/dist/assets/doc-browser-4Sud6dWw.js +0 -1
- package/dist/assets/doc-browser-D4x_yH5F.js +0 -1
- package/dist/assets/form-actions-B2q4jgjE.js +0 -1
- package/dist/assets/index-CdyTsSBu.css +0 -1
- package/dist/assets/index-Das_bw2h.js +0 -107
- package/dist/assets/loader-circle-CpmBdaon.js +0 -1
- package/dist/assets/mcp-marketplace-page-CdOY3Sop.js +0 -1
- package/dist/assets/mcp-marketplace-page-DvjrfqEt.js +0 -9
- package/dist/assets/plus-C8FkjpSZ.js +0 -1
- package/dist/assets/react-C8XFFvuz.js +0 -8
- package/dist/assets/remote-BJHXa4mB.js +0 -1
- package/dist/assets/rotate-cw-DJ7UF3F2.js +0 -1
- package/dist/assets/security-config-CbDiY2xr.js +0 -1
- package/dist/assets/skeleton-XpqeWL62.js +0 -1
- package/dist/assets/x-odTHAtLZ.js +0 -1
package/src/features/chat/features/workspace/components/chat-session-workspace-directory-browser.tsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useMemo, type ReactNode } from 'react';
|
|
2
|
+
import { useQueryClient } from '@tanstack/react-query';
|
|
2
3
|
import type { ChatFileOpenActionViewModel } from '@nextclaw/agent-chat-ui';
|
|
3
4
|
import { ChevronsUp, Copy, FilePlus2, FolderPlus, LocateFixed, Upload, RefreshCw } from 'lucide-react';
|
|
4
5
|
import { toast } from 'sonner';
|
|
@@ -10,6 +11,10 @@ import {
|
|
|
10
11
|
} from '@/features/chat/features/workspace/components/project-files/workspace-directory-tree';
|
|
11
12
|
import { WorkspaceNewFolderTreeItem } from '@/features/chat/features/workspace/components/project-files/workspace-new-folder-tree-item';
|
|
12
13
|
import { WorkspaceProjectFilesToolbar } from '@/features/chat/features/workspace/components/project-files/workspace-project-files-toolbar';
|
|
14
|
+
import {
|
|
15
|
+
isWorkspaceProjectBrowseQuery,
|
|
16
|
+
WorkspaceProjectTree,
|
|
17
|
+
} from '@/features/chat/features/workspace/components/project-files/workspace-project-tree-view';
|
|
13
18
|
import {
|
|
14
19
|
useWorkspaceProjectFilesController,
|
|
15
20
|
type RefreshDirectory,
|
|
@@ -18,10 +23,15 @@ import {
|
|
|
18
23
|
} from '@/features/chat/features/workspace/hooks/use-workspace-file-actions';
|
|
19
24
|
import { ContextMenu, type ContextMenuGroup } from '@/shared/components/ui/context-menu/context-menu';
|
|
20
25
|
import { useConfirmDialog } from '@/shared/hooks/use-confirm-dialog';
|
|
26
|
+
import { useServerPathWatch } from '@/shared/hooks/use-server-path-watch';
|
|
21
27
|
import type { useServerPathBrowse } from '@/shared/hooks/use-server-path-browse';
|
|
22
28
|
import type { ServerPathEntryView } from '@/shared/lib/api';
|
|
23
29
|
import { hostCapabilityManager } from '@/shared/lib/host-capabilities';
|
|
24
30
|
import { t } from '@/shared/lib/i18n';
|
|
31
|
+
import {
|
|
32
|
+
normalizeWorkspaceProjectTreeRootKey,
|
|
33
|
+
useWorkspaceProjectTreeStore,
|
|
34
|
+
} from '@/features/chat/features/workspace/stores/workspace-project-tree.store';
|
|
25
35
|
|
|
26
36
|
type ChatSessionWorkspaceDirectoryBrowserProps = {
|
|
27
37
|
activeRelativePath?: string | null;
|
|
@@ -32,6 +42,8 @@ type ChatSessionWorkspaceDirectoryBrowserProps = {
|
|
|
32
42
|
showRoot?: boolean;
|
|
33
43
|
};
|
|
34
44
|
|
|
45
|
+
const EMPTY_EXPANDED_PATHS: readonly string[] = [];
|
|
46
|
+
|
|
35
47
|
function readPathName(path: string): string {
|
|
36
48
|
return path.split(/[\\/]/).filter(Boolean).pop() ?? path;
|
|
37
49
|
}
|
|
@@ -39,7 +51,6 @@ function readPathName(path: string): string {
|
|
|
39
51
|
function WorkspaceDirectoryEntries({
|
|
40
52
|
activeRelativePath,
|
|
41
53
|
busy,
|
|
42
|
-
collapseVersion,
|
|
43
54
|
createTarget,
|
|
44
55
|
entries,
|
|
45
56
|
fallbackParentTarget,
|
|
@@ -60,10 +71,10 @@ function WorkspaceDirectoryEntries({
|
|
|
60
71
|
revealPath,
|
|
61
72
|
selectedPath,
|
|
62
73
|
showRoot,
|
|
74
|
+
treeRootKey,
|
|
63
75
|
}: {
|
|
64
76
|
activeRelativePath: string | null;
|
|
65
77
|
busy: boolean;
|
|
66
|
-
collapseVersion: number;
|
|
67
78
|
createTarget: WorkspaceDirectoryActionTarget | null;
|
|
68
79
|
entries: readonly ServerPathEntryView[];
|
|
69
80
|
fallbackParentTarget: WorkspaceDirectoryActionTarget;
|
|
@@ -84,6 +95,7 @@ function WorkspaceDirectoryEntries({
|
|
|
84
95
|
revealPath: string | null;
|
|
85
96
|
selectedPath: string | null;
|
|
86
97
|
showRoot: boolean;
|
|
98
|
+
treeRootKey: string | null;
|
|
87
99
|
}) {
|
|
88
100
|
if (entries.length === 0) {
|
|
89
101
|
return <div className="px-4 py-8 text-center text-xs text-gray-400">{t('chatWorkspaceDirectoryEmpty')}</div>;
|
|
@@ -94,7 +106,6 @@ function WorkspaceDirectoryEntries({
|
|
|
94
106
|
key={entry.path}
|
|
95
107
|
browseParentRefresh={refresh}
|
|
96
108
|
busy={busy}
|
|
97
|
-
collapseVersion={collapseVersion}
|
|
98
109
|
createTarget={createTarget}
|
|
99
110
|
entry={entry}
|
|
100
111
|
level={showRoot ? 1 : 0}
|
|
@@ -113,6 +124,7 @@ function WorkspaceDirectoryEntries({
|
|
|
113
124
|
renderCreateFolder={renderCreateFolder}
|
|
114
125
|
revealPath={revealPath}
|
|
115
126
|
selectedPath={selectedPath}
|
|
127
|
+
treeRootKey={treeRootKey}
|
|
116
128
|
/>
|
|
117
129
|
));
|
|
118
130
|
}
|
|
@@ -207,45 +219,6 @@ function buildRootContextMenuGroups({
|
|
|
207
219
|
];
|
|
208
220
|
}
|
|
209
221
|
|
|
210
|
-
function WorkspaceProjectTree({
|
|
211
|
-
children,
|
|
212
|
-
currentPath,
|
|
213
|
-
onBlankContext,
|
|
214
|
-
onContextMenu,
|
|
215
|
-
rootDraft,
|
|
216
|
-
showRoot,
|
|
217
|
-
}: {
|
|
218
|
-
children: ReactNode;
|
|
219
|
-
currentPath: string | null;
|
|
220
|
-
onBlankContext: () => void;
|
|
221
|
-
onContextMenu?: MouseEventHandler<HTMLDivElement>;
|
|
222
|
-
rootDraft: ReactNode;
|
|
223
|
-
showRoot: boolean;
|
|
224
|
-
}) {
|
|
225
|
-
return (
|
|
226
|
-
<div
|
|
227
|
-
data-testid="workspace-directory-browser"
|
|
228
|
-
role="tree"
|
|
229
|
-
tabIndex={-1}
|
|
230
|
-
aria-label={t('chatWorkspaceProjectFiles')}
|
|
231
|
-
className="min-h-0 flex-1 overflow-auto py-1 custom-scrollbar"
|
|
232
|
-
onContextMenu={(event) => {
|
|
233
|
-
if (!(event.target as HTMLElement).closest('[data-workspace-tree-entry]')) onBlankContext();
|
|
234
|
-
onContextMenu?.(event);
|
|
235
|
-
}}
|
|
236
|
-
>
|
|
237
|
-
{showRoot && currentPath ? (
|
|
238
|
-
<>
|
|
239
|
-
{rootDraft}
|
|
240
|
-
{children}
|
|
241
|
-
</>
|
|
242
|
-
) : (
|
|
243
|
-
children
|
|
244
|
-
)}
|
|
245
|
-
</div>
|
|
246
|
-
);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
222
|
function WorkspaceDirectoryBrowserReady({
|
|
250
223
|
activeRelativePath,
|
|
251
224
|
browseQuery,
|
|
@@ -271,6 +244,7 @@ function WorkspaceDirectoryBrowserReady({
|
|
|
271
244
|
rootPath: string | null;
|
|
272
245
|
showRoot: boolean;
|
|
273
246
|
}) {
|
|
247
|
+
const queryClient = useQueryClient();
|
|
274
248
|
const {
|
|
275
249
|
cancelCreateFolder,
|
|
276
250
|
clearSelection,
|
|
@@ -298,8 +272,15 @@ function WorkspaceDirectoryBrowserReady({
|
|
|
298
272
|
selectedPath,
|
|
299
273
|
} = controller.state;
|
|
300
274
|
const busy = pendingAction !== null;
|
|
301
|
-
const
|
|
302
|
-
const
|
|
275
|
+
const treeRootKey = normalizeWorkspaceProjectTreeRootKey(rootPath);
|
|
276
|
+
const collapseAll = useWorkspaceProjectTreeStore((state) => state.collapseAll);
|
|
277
|
+
const rootRefresh = () =>
|
|
278
|
+
rootPath
|
|
279
|
+
? queryClient.refetchQueries({
|
|
280
|
+
predicate: (query) => isWorkspaceProjectBrowseQuery(query.queryKey, rootPath),
|
|
281
|
+
type: 'active',
|
|
282
|
+
})
|
|
283
|
+
: browseQuery.refetch();
|
|
303
284
|
const rootTarget: WorkspaceDirectoryActionTarget | null = rootPath
|
|
304
285
|
? { label: rootLabel, path: rootPath, refresh: rootRefresh }
|
|
305
286
|
: null;
|
|
@@ -328,7 +309,6 @@ function WorkspaceDirectoryBrowserReady({
|
|
|
328
309
|
<WorkspaceDirectoryEntries
|
|
329
310
|
activeRelativePath={activeRelativePath}
|
|
330
311
|
busy={busy}
|
|
331
|
-
collapseVersion={collapseVersion}
|
|
332
312
|
createTarget={createTarget}
|
|
333
313
|
entries={entries}
|
|
334
314
|
fallbackParentTarget={fallbackParentTarget}
|
|
@@ -349,6 +329,7 @@ function WorkspaceDirectoryBrowserReady({
|
|
|
349
329
|
revealPath={revealPath}
|
|
350
330
|
selectedPath={selectedPath}
|
|
351
331
|
showRoot={false}
|
|
332
|
+
treeRootKey={treeRootKey || null}
|
|
352
333
|
/>
|
|
353
334
|
);
|
|
354
335
|
const tree = (
|
|
@@ -364,10 +345,12 @@ function WorkspaceDirectoryBrowserReady({
|
|
|
364
345
|
const rootMenuGroups = rootTarget
|
|
365
346
|
? buildRootContextMenuGroups({
|
|
366
347
|
busy,
|
|
367
|
-
onCollapseAll: () =>
|
|
348
|
+
onCollapseAll: () => {
|
|
349
|
+
if (treeRootKey) collapseAll(treeRootKey);
|
|
350
|
+
},
|
|
368
351
|
onCreateFile: () => openCreateFile(rootTarget),
|
|
369
352
|
onCreateFolder: () => openCreateFolder(rootTarget),
|
|
370
|
-
onRefresh: () => void
|
|
353
|
+
onRefresh: () => void rootRefresh(),
|
|
371
354
|
onRevealPath,
|
|
372
355
|
onUpload: () => requestUpload(rootTarget),
|
|
373
356
|
target: rootTarget,
|
|
@@ -385,10 +368,12 @@ function WorkspaceDirectoryBrowserReady({
|
|
|
385
368
|
<WorkspaceProjectFilesToolbar
|
|
386
369
|
disabled={busy}
|
|
387
370
|
rootLabel={rootLabel}
|
|
388
|
-
onCollapseAll={() =>
|
|
371
|
+
onCollapseAll={() => {
|
|
372
|
+
if (treeRootKey) collapseAll(treeRootKey);
|
|
373
|
+
}}
|
|
389
374
|
onNewFile={() => openCreateFile(toolbarTarget)}
|
|
390
375
|
onNewFolder={() => openCreateFolder(toolbarTarget)}
|
|
391
|
-
onRefresh={() => void
|
|
376
|
+
onRefresh={() => void rootRefresh()}
|
|
392
377
|
/>
|
|
393
378
|
) : null}
|
|
394
379
|
{rootPath ? (
|
|
@@ -427,6 +412,27 @@ export function ChatSessionWorkspaceDirectoryBrowser({
|
|
|
427
412
|
const entries = browseQuery.data?.entries ?? [];
|
|
428
413
|
const errorMessage = readBrowseError(browseQuery);
|
|
429
414
|
const rootPath = showRoot ? (browseQuery.data?.currentPath ?? null) : null;
|
|
415
|
+
const treeRootKey = normalizeWorkspaceProjectTreeRootKey(rootPath);
|
|
416
|
+
const expandedPaths = useWorkspaceProjectTreeStore((state) =>
|
|
417
|
+
treeRootKey ? (state.trees[treeRootKey]?.expandedPaths ?? EMPTY_EXPANDED_PATHS) : EMPTY_EXPANDED_PATHS,
|
|
418
|
+
);
|
|
419
|
+
const watchedDirectories = useMemo(() => {
|
|
420
|
+
if (!rootPath) return [];
|
|
421
|
+
const separator = rootPath.includes('\\') && !rootPath.includes('/') ? '\\' : '/';
|
|
422
|
+
const normalizedRoot = rootPath.replace(/[\\/]+$/g, '');
|
|
423
|
+
const expandedPathSet = new Set(expandedPaths);
|
|
424
|
+
const visibleExpandedPaths = expandedPaths.filter((relativePath) => {
|
|
425
|
+
const segments = relativePath.split('/');
|
|
426
|
+
return segments.slice(0, -1).every((_, index) => expandedPathSet.has(segments.slice(0, index + 1).join('/')));
|
|
427
|
+
});
|
|
428
|
+
return [
|
|
429
|
+
rootPath,
|
|
430
|
+
...visibleExpandedPaths.map(
|
|
431
|
+
(relativePath) => `${normalizedRoot}${separator}${relativePath.replace(/[\\/]+/g, separator)}`,
|
|
432
|
+
),
|
|
433
|
+
];
|
|
434
|
+
}, [expandedPaths, rootPath]);
|
|
435
|
+
useServerPathWatch(watchedDirectories);
|
|
430
436
|
const rootLabel = rootPath ? readPathName(rootPath) : '';
|
|
431
437
|
const controller = useWorkspaceProjectFilesController({
|
|
432
438
|
activeRelativePath,
|
package/src/features/chat/features/workspace/components/project-files/workspace-directory-tree.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useCallback, useLayoutEffect, useMemo, useRef, useState, type ReactNode } from 'react';
|
|
2
2
|
import type { ChatFileOpenActionViewModel } from '@nextclaw/agent-chat-ui';
|
|
3
3
|
import {
|
|
4
4
|
ChevronDown,
|
|
@@ -26,13 +26,16 @@ import { buildServerPathContentUrl, type ServerPathEntryView } from '@/shared/li
|
|
|
26
26
|
import { t } from '@/shared/lib/i18n';
|
|
27
27
|
import { cn } from '@/shared/lib/utils';
|
|
28
28
|
import { buildWorkspaceEntryContextMenuGroups } from '@/features/chat/features/workspace/components/project-files/workspace-directory-entry-menu';
|
|
29
|
+
import {
|
|
30
|
+
normalizeWorkspaceProjectTreeRelativePath,
|
|
31
|
+
useWorkspaceProjectTreeStore,
|
|
32
|
+
} from '@/features/chat/features/workspace/stores/workspace-project-tree.store';
|
|
29
33
|
|
|
30
34
|
export { copyWorkspacePath } from '@/features/chat/features/workspace/components/project-files/workspace-directory-entry-menu';
|
|
31
35
|
|
|
32
36
|
type WorkspaceDirectoryTreeActionProps = {
|
|
33
37
|
activeRelativePath: string | null;
|
|
34
38
|
busy: boolean;
|
|
35
|
-
collapseVersion: number;
|
|
36
39
|
createTarget: WorkspaceDirectoryActionTarget | null;
|
|
37
40
|
onAddToChat?: (entry: ServerPathEntryView, relativePath: string) => void;
|
|
38
41
|
onCreateFolder?: (target: WorkspaceDirectoryActionTarget) => void;
|
|
@@ -45,6 +48,7 @@ type WorkspaceDirectoryTreeActionProps = {
|
|
|
45
48
|
renderCreateFolder: (level: number) => ReactNode;
|
|
46
49
|
revealPath: string | null;
|
|
47
50
|
selectedPath: string | null;
|
|
51
|
+
treeRootKey: string | null;
|
|
48
52
|
};
|
|
49
53
|
|
|
50
54
|
function buildEntryLabel(entry: ServerPathEntryView): string {
|
|
@@ -206,11 +210,31 @@ function isWorkspaceTreeEntryExpanded(
|
|
|
206
210
|
);
|
|
207
211
|
}
|
|
208
212
|
|
|
213
|
+
function useWorkspaceTreeEntryExpansion(treeRootKey: string | null, relativePath: string | null) {
|
|
214
|
+
const [locallyExpanded, setLocallyExpanded] = useState(false);
|
|
215
|
+
const relativePathKey = normalizeWorkspaceProjectTreeRelativePath(relativePath);
|
|
216
|
+
const persistedExpanded = useWorkspaceProjectTreeStore((state) =>
|
|
217
|
+
Boolean(treeRootKey && relativePathKey && state.trees[treeRootKey]?.expandedPaths.includes(relativePathKey)),
|
|
218
|
+
);
|
|
219
|
+
const setDirectoryExpanded = useWorkspaceProjectTreeStore((state) => state.setDirectoryExpanded);
|
|
220
|
+
const manuallyExpanded = treeRootKey && relativePathKey ? persistedExpanded : locallyExpanded;
|
|
221
|
+
const updateExpanded = useCallback(
|
|
222
|
+
(expanded: boolean) => {
|
|
223
|
+
if (treeRootKey && relativePathKey) {
|
|
224
|
+
setDirectoryExpanded(treeRootKey, relativePathKey, expanded);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
setLocallyExpanded(expanded);
|
|
228
|
+
},
|
|
229
|
+
[relativePathKey, setDirectoryExpanded, treeRootKey],
|
|
230
|
+
);
|
|
231
|
+
return { manuallyExpanded, updateExpanded };
|
|
232
|
+
}
|
|
233
|
+
|
|
209
234
|
export function WorkspaceDirectoryTreeEntry({
|
|
210
235
|
activeRelativePath,
|
|
211
236
|
browseParentRefresh,
|
|
212
237
|
busy,
|
|
213
|
-
collapseVersion,
|
|
214
238
|
createTarget,
|
|
215
239
|
entry,
|
|
216
240
|
level,
|
|
@@ -229,6 +253,7 @@ export function WorkspaceDirectoryTreeEntry({
|
|
|
229
253
|
renderCreateFolder,
|
|
230
254
|
revealPath,
|
|
231
255
|
selectedPath,
|
|
256
|
+
treeRootKey,
|
|
232
257
|
}: WorkspaceDirectoryTreeActionProps & {
|
|
233
258
|
browseParentRefresh: RefreshDirectory;
|
|
234
259
|
entry: ServerPathEntryView;
|
|
@@ -241,7 +266,7 @@ export function WorkspaceDirectoryTreeEntry({
|
|
|
241
266
|
const rowRef = useRef<HTMLDivElement | null>(null);
|
|
242
267
|
const entryButtonRef = useRef<HTMLButtonElement | null>(null);
|
|
243
268
|
const isDirectory = entry.kind === 'directory';
|
|
244
|
-
const
|
|
269
|
+
const { manuallyExpanded, updateExpanded } = useWorkspaceTreeEntryExpansion(treeRootKey, relativePath);
|
|
245
270
|
const isExpanded = isWorkspaceTreeEntryExpanded(
|
|
246
271
|
manuallyExpanded,
|
|
247
272
|
isDirectory,
|
|
@@ -254,9 +279,6 @@ export function WorkspaceDirectoryTreeEntry({
|
|
|
254
279
|
onRename,
|
|
255
280
|
refresh: browseParentRefresh,
|
|
256
281
|
});
|
|
257
|
-
useEffect(() => {
|
|
258
|
-
if (collapseVersion > 0) setIsExpanded(false);
|
|
259
|
-
}, [collapseVersion]);
|
|
260
282
|
const browseQuery = useServerPathBrowse({
|
|
261
283
|
path: entry.path,
|
|
262
284
|
includeFiles: true,
|
|
@@ -271,10 +293,10 @@ export function WorkspaceDirectoryTreeEntry({
|
|
|
271
293
|
label: entry.name,
|
|
272
294
|
path: entry.path,
|
|
273
295
|
refresh: () => refreshDirectory(),
|
|
274
|
-
expand: () =>
|
|
296
|
+
expand: () => updateExpanded(true),
|
|
275
297
|
}
|
|
276
298
|
: parentCreateTarget,
|
|
277
|
-
[entry.name, entry.path, isDirectory, parentCreateTarget, refreshDirectory],
|
|
299
|
+
[entry.name, entry.path, isDirectory, parentCreateTarget, refreshDirectory, updateExpanded],
|
|
278
300
|
);
|
|
279
301
|
const selection = useMemo<WorkspaceTreeSelection>(
|
|
280
302
|
() => ({ path: entry.path, createTarget: ownCreateTarget }),
|
|
@@ -301,7 +323,7 @@ export function WorkspaceDirectoryTreeEntry({
|
|
|
301
323
|
|
|
302
324
|
const activateEntry = () => {
|
|
303
325
|
onSelect(selection);
|
|
304
|
-
if (isDirectory)
|
|
326
|
+
if (isDirectory) updateExpanded(!isExpanded);
|
|
305
327
|
else onFileOpen({ path: entry.path, label: entry.name, viewMode: 'preview' });
|
|
306
328
|
};
|
|
307
329
|
|
|
@@ -433,7 +455,6 @@ export function WorkspaceDirectoryTreeEntry({
|
|
|
433
455
|
activeRelativePath={activeRelativePath}
|
|
434
456
|
browseQuery={browseQuery}
|
|
435
457
|
busy={busy}
|
|
436
|
-
collapseVersion={collapseVersion}
|
|
437
458
|
createTarget={createTarget}
|
|
438
459
|
level={level}
|
|
439
460
|
onAddToChat={onAddToChat}
|
|
@@ -451,6 +472,7 @@ export function WorkspaceDirectoryTreeEntry({
|
|
|
451
472
|
renderCreateFolder={renderCreateFolder}
|
|
452
473
|
revealPath={revealPath}
|
|
453
474
|
selectedPath={selectedPath}
|
|
475
|
+
treeRootKey={treeRootKey}
|
|
454
476
|
/>
|
|
455
477
|
</div>
|
|
456
478
|
) : null}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { useEffect, useLayoutEffect, useRef, type MouseEventHandler, type ReactNode } from 'react';
|
|
2
|
+
import type { QueryKey } from '@tanstack/react-query';
|
|
3
|
+
import {
|
|
4
|
+
normalizeWorkspaceProjectTreeRootKey,
|
|
5
|
+
useWorkspaceProjectTreeStore,
|
|
6
|
+
} from '@/features/chat/features/workspace/stores/workspace-project-tree.store';
|
|
7
|
+
import { t } from '@/shared/lib/i18n';
|
|
8
|
+
|
|
9
|
+
function normalizeComparablePath(path: string): string {
|
|
10
|
+
const normalized = path.trim().replace(/\\/g, '/').replace(/\/+$/g, '') || '/';
|
|
11
|
+
return /^[a-z]:/i.test(normalized) ? normalized.toLowerCase() : normalized;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function isWorkspaceProjectBrowseQuery(queryKey: QueryKey, rootPath: string): boolean {
|
|
15
|
+
if (queryKey[0] !== 'server-path-browse' || queryKey[3] !== true || typeof queryKey[1] !== 'string') {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
const root = normalizeComparablePath(rootPath);
|
|
19
|
+
const candidate = normalizeComparablePath(queryKey[1]);
|
|
20
|
+
return candidate === root || (root === '/' ? candidate.startsWith('/') : candidate.startsWith(`${root}/`));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function WorkspaceProjectTree({
|
|
24
|
+
children,
|
|
25
|
+
currentPath,
|
|
26
|
+
onBlankContext,
|
|
27
|
+
onContextMenu,
|
|
28
|
+
rootDraft,
|
|
29
|
+
showRoot,
|
|
30
|
+
}: {
|
|
31
|
+
children: ReactNode;
|
|
32
|
+
currentPath: string | null;
|
|
33
|
+
onBlankContext: () => void;
|
|
34
|
+
onContextMenu?: MouseEventHandler<HTMLDivElement>;
|
|
35
|
+
rootDraft: ReactNode;
|
|
36
|
+
showRoot: boolean;
|
|
37
|
+
}) {
|
|
38
|
+
const treeRef = useRef<HTMLDivElement | null>(null);
|
|
39
|
+
const pendingScrollTopRef = useRef<number | null>(null);
|
|
40
|
+
const scrollPersistTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
41
|
+
const treeRootKey = normalizeWorkspaceProjectTreeRootKey(currentPath);
|
|
42
|
+
const savedScrollTop = useWorkspaceProjectTreeStore((state) =>
|
|
43
|
+
treeRootKey ? (state.trees[treeRootKey]?.scrollTop ?? 0) : 0,
|
|
44
|
+
);
|
|
45
|
+
const setScrollTop = useWorkspaceProjectTreeStore((state) => state.setScrollTop);
|
|
46
|
+
|
|
47
|
+
useLayoutEffect(() => {
|
|
48
|
+
if (treeRef.current && treeRootKey) treeRef.current.scrollTop = savedScrollTop;
|
|
49
|
+
}, [savedScrollTop, treeRootKey]);
|
|
50
|
+
|
|
51
|
+
useEffect(
|
|
52
|
+
() => () => {
|
|
53
|
+
if (scrollPersistTimerRef.current) clearTimeout(scrollPersistTimerRef.current);
|
|
54
|
+
if (treeRootKey && pendingScrollTopRef.current !== null) {
|
|
55
|
+
setScrollTop(treeRootKey, pendingScrollTopRef.current);
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
[setScrollTop, treeRootKey],
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<div
|
|
63
|
+
ref={treeRef}
|
|
64
|
+
data-testid="workspace-directory-browser"
|
|
65
|
+
role="tree"
|
|
66
|
+
tabIndex={-1}
|
|
67
|
+
aria-label={t('chatWorkspaceProjectFiles')}
|
|
68
|
+
className="min-h-0 flex-1 overflow-auto py-1 custom-scrollbar"
|
|
69
|
+
onContextMenu={(event) => {
|
|
70
|
+
if (!(event.target as HTMLElement).closest('[data-workspace-tree-entry]')) onBlankContext();
|
|
71
|
+
onContextMenu?.(event);
|
|
72
|
+
}}
|
|
73
|
+
onScroll={(event) => {
|
|
74
|
+
if (!treeRootKey) return;
|
|
75
|
+
pendingScrollTopRef.current = event.currentTarget.scrollTop;
|
|
76
|
+
if (scrollPersistTimerRef.current) clearTimeout(scrollPersistTimerRef.current);
|
|
77
|
+
scrollPersistTimerRef.current = setTimeout(() => {
|
|
78
|
+
scrollPersistTimerRef.current = null;
|
|
79
|
+
if (pendingScrollTopRef.current !== null) setScrollTop(treeRootKey, pendingScrollTopRef.current);
|
|
80
|
+
}, 120);
|
|
81
|
+
}}
|
|
82
|
+
>
|
|
83
|
+
{showRoot && currentPath ? (
|
|
84
|
+
<>
|
|
85
|
+
{rootDraft}
|
|
86
|
+
{children}
|
|
87
|
+
</>
|
|
88
|
+
) : (
|
|
89
|
+
children
|
|
90
|
+
)}
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
}
|
package/src/features/chat/features/workspace/stores/__tests__/workspace-project-tree.store.test.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import { useWorkspaceProjectTreeStore } from '@/features/chat/features/workspace/stores/workspace-project-tree.store';
|
|
3
|
+
|
|
4
|
+
const ROOT = '/Users/peiwang/Projects/nextbot';
|
|
5
|
+
|
|
6
|
+
describe('workspace project tree store', () => {
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
window.localStorage.clear();
|
|
9
|
+
useWorkspaceProjectTreeStore.setState({ trees: {} });
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('keeps expansion and scroll view state isolated by project root', () => {
|
|
13
|
+
const store = useWorkspaceProjectTreeStore.getState();
|
|
14
|
+
store.setDirectoryExpanded(ROOT, 'src/features', true);
|
|
15
|
+
store.setScrollTop(ROOT, 128.4);
|
|
16
|
+
store.setDirectoryExpanded('/tmp/another-project', 'docs', true);
|
|
17
|
+
|
|
18
|
+
expect(useWorkspaceProjectTreeStore.getState().trees[ROOT]).toMatchObject({
|
|
19
|
+
expandedPaths: ['src/features'],
|
|
20
|
+
scrollTop: 128,
|
|
21
|
+
});
|
|
22
|
+
expect(useWorkspaceProjectTreeStore.getState().trees['/tmp/another-project']).toMatchObject({
|
|
23
|
+
expandedPaths: ['docs'],
|
|
24
|
+
scrollTop: 0,
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('normalizes paths and collapses only the requested project', () => {
|
|
29
|
+
const store = useWorkspaceProjectTreeStore.getState();
|
|
30
|
+
store.setDirectoryExpanded(`${ROOT}/`, '/src\\features/', true);
|
|
31
|
+
store.setDirectoryExpanded(ROOT, 'src/features', true);
|
|
32
|
+
store.setDirectoryExpanded('/tmp/another-project', 'docs', true);
|
|
33
|
+
useWorkspaceProjectTreeStore.getState().collapseAll(ROOT);
|
|
34
|
+
|
|
35
|
+
expect(useWorkspaceProjectTreeStore.getState().trees[ROOT]?.expandedPaths).toEqual([]);
|
|
36
|
+
expect(useWorkspaceProjectTreeStore.getState().trees['/tmp/another-project']?.expandedPaths).toEqual(['docs']);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { create } from 'zustand';
|
|
2
|
+
import { createJSONStorage, persist } from 'zustand/middleware';
|
|
3
|
+
|
|
4
|
+
const WORKSPACE_PROJECT_TREE_STORAGE_KEY = 'nextclaw.chat.workspace-project-tree.state';
|
|
5
|
+
const WORKSPACE_PROJECT_TREE_STORAGE_VERSION = 1;
|
|
6
|
+
const MAX_PERSISTED_PROJECT_TREES = 12;
|
|
7
|
+
const MAX_PERSISTED_EXPANDED_PATHS = 256;
|
|
8
|
+
const MAX_PERSISTED_SCROLL_TOP = 10_000_000;
|
|
9
|
+
|
|
10
|
+
export type WorkspaceProjectTreeViewState = {
|
|
11
|
+
expandedPaths: string[];
|
|
12
|
+
scrollTop: number;
|
|
13
|
+
updatedAt: number;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
type WorkspaceProjectTreeStore = {
|
|
17
|
+
trees: Record<string, WorkspaceProjectTreeViewState>;
|
|
18
|
+
collapseAll: (rootPath: string) => void;
|
|
19
|
+
setDirectoryExpanded: (rootPath: string, relativePath: string, expanded: boolean) => void;
|
|
20
|
+
setScrollTop: (rootPath: string, scrollTop: number) => void;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type PersistedWorkspaceProjectTreeStore = Pick<WorkspaceProjectTreeStore, 'trees'>;
|
|
24
|
+
|
|
25
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
26
|
+
return !!value && typeof value === 'object';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function normalizeWorkspaceProjectTreeRootKey(path: string | null | undefined): string {
|
|
30
|
+
const normalized = path?.trim().replace(/\\/g, '/').replace(/\/+$/g, '') ?? '';
|
|
31
|
+
return normalized || (path?.trim() === '/' ? '/' : '');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function normalizeWorkspaceProjectTreeRelativePath(path: string | null | undefined): string {
|
|
35
|
+
return path?.trim().replace(/\\/g, '/').replace(/^\/+|\/+$/g, '').replace(/\/{2,}/g, '/') ?? '';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function normalizeScrollTop(value: unknown): number {
|
|
39
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) return 0;
|
|
40
|
+
return Math.min(Math.max(0, Math.round(value)), MAX_PERSISTED_SCROLL_TOP);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function normalizeUpdatedAt(value: unknown): number {
|
|
44
|
+
return typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function normalizeExpandedPaths(value: unknown): string[] {
|
|
48
|
+
if (!Array.isArray(value)) return [];
|
|
49
|
+
return Array.from(
|
|
50
|
+
new Set(
|
|
51
|
+
value
|
|
52
|
+
.map((path) => (typeof path === 'string' ? normalizeWorkspaceProjectTreeRelativePath(path) : ''))
|
|
53
|
+
.filter(Boolean),
|
|
54
|
+
),
|
|
55
|
+
).slice(0, MAX_PERSISTED_EXPANDED_PATHS);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function normalizePersistedTrees(value: unknown): Record<string, WorkspaceProjectTreeViewState> {
|
|
59
|
+
if (!isRecord(value)) return {};
|
|
60
|
+
return Object.entries(value)
|
|
61
|
+
.map(([rootPath, tree]) => {
|
|
62
|
+
const rootKey = normalizeWorkspaceProjectTreeRootKey(rootPath);
|
|
63
|
+
if (!rootKey || !isRecord(tree)) return null;
|
|
64
|
+
return [
|
|
65
|
+
rootKey,
|
|
66
|
+
{
|
|
67
|
+
expandedPaths: normalizeExpandedPaths(tree.expandedPaths),
|
|
68
|
+
scrollTop: normalizeScrollTop(tree.scrollTop),
|
|
69
|
+
updatedAt: normalizeUpdatedAt(tree.updatedAt),
|
|
70
|
+
},
|
|
71
|
+
] as const;
|
|
72
|
+
})
|
|
73
|
+
.filter((entry): entry is readonly [string, WorkspaceProjectTreeViewState] => entry !== null)
|
|
74
|
+
.sort((left, right) => right[1].updatedAt - left[1].updatedAt)
|
|
75
|
+
.slice(0, MAX_PERSISTED_PROJECT_TREES)
|
|
76
|
+
.reduce<Record<string, WorkspaceProjectTreeViewState>>((trees, [rootKey, tree]) => {
|
|
77
|
+
trees[rootKey] = tree;
|
|
78
|
+
return trees;
|
|
79
|
+
}, {});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function retainRecentTrees(
|
|
83
|
+
trees: Record<string, WorkspaceProjectTreeViewState>,
|
|
84
|
+
): Record<string, WorkspaceProjectTreeViewState> {
|
|
85
|
+
return normalizePersistedTrees(trees);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function createTreeViewState(): WorkspaceProjectTreeViewState {
|
|
89
|
+
return { expandedPaths: [], scrollTop: 0, updatedAt: Date.now() };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export const useWorkspaceProjectTreeStore = create<WorkspaceProjectTreeStore>()(
|
|
93
|
+
persist(
|
|
94
|
+
(set) => ({
|
|
95
|
+
trees: {},
|
|
96
|
+
collapseAll: (rootPath) => {
|
|
97
|
+
const rootKey = normalizeWorkspaceProjectTreeRootKey(rootPath);
|
|
98
|
+
if (!rootKey) return;
|
|
99
|
+
set((state) => {
|
|
100
|
+
const current = state.trees[rootKey];
|
|
101
|
+
if (!current || current.expandedPaths.length === 0) return state;
|
|
102
|
+
return {
|
|
103
|
+
trees: retainRecentTrees({
|
|
104
|
+
...state.trees,
|
|
105
|
+
[rootKey]: { ...current, expandedPaths: [], updatedAt: Date.now() },
|
|
106
|
+
}),
|
|
107
|
+
};
|
|
108
|
+
});
|
|
109
|
+
},
|
|
110
|
+
setDirectoryExpanded: (rootPath, relativePath, expanded) => {
|
|
111
|
+
const rootKey = normalizeWorkspaceProjectTreeRootKey(rootPath);
|
|
112
|
+
const pathKey = normalizeWorkspaceProjectTreeRelativePath(relativePath);
|
|
113
|
+
if (!rootKey || !pathKey) return;
|
|
114
|
+
set((state) => {
|
|
115
|
+
const current = state.trees[rootKey] ?? createTreeViewState();
|
|
116
|
+
const expandedPaths = expanded
|
|
117
|
+
? Array.from(new Set([...current.expandedPaths, pathKey])).slice(0, MAX_PERSISTED_EXPANDED_PATHS)
|
|
118
|
+
: current.expandedPaths.filter((path) => path !== pathKey);
|
|
119
|
+
if (
|
|
120
|
+
expandedPaths.length === current.expandedPaths.length &&
|
|
121
|
+
expandedPaths.every((path, index) => path === current.expandedPaths[index])
|
|
122
|
+
) {
|
|
123
|
+
return state;
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
trees: retainRecentTrees({
|
|
127
|
+
...state.trees,
|
|
128
|
+
[rootKey]: { ...current, expandedPaths, updatedAt: Date.now() },
|
|
129
|
+
}),
|
|
130
|
+
};
|
|
131
|
+
});
|
|
132
|
+
},
|
|
133
|
+
setScrollTop: (rootPath, scrollTop) => {
|
|
134
|
+
const rootKey = normalizeWorkspaceProjectTreeRootKey(rootPath);
|
|
135
|
+
if (!rootKey) return;
|
|
136
|
+
const normalizedScrollTop = normalizeScrollTop(scrollTop);
|
|
137
|
+
set((state) => {
|
|
138
|
+
const current = state.trees[rootKey] ?? createTreeViewState();
|
|
139
|
+
if (current.scrollTop === normalizedScrollTop) return state;
|
|
140
|
+
return {
|
|
141
|
+
trees: retainRecentTrees({
|
|
142
|
+
...state.trees,
|
|
143
|
+
[rootKey]: { ...current, scrollTop: normalizedScrollTop, updatedAt: Date.now() },
|
|
144
|
+
}),
|
|
145
|
+
};
|
|
146
|
+
});
|
|
147
|
+
},
|
|
148
|
+
}),
|
|
149
|
+
{
|
|
150
|
+
name: WORKSPACE_PROJECT_TREE_STORAGE_KEY,
|
|
151
|
+
version: WORKSPACE_PROJECT_TREE_STORAGE_VERSION,
|
|
152
|
+
storage: createJSONStorage(() => window.localStorage),
|
|
153
|
+
partialize: (state): PersistedWorkspaceProjectTreeStore => ({ trees: state.trees }),
|
|
154
|
+
merge: (persistedState, currentState) => ({
|
|
155
|
+
...currentState,
|
|
156
|
+
trees: normalizePersistedTrees(isRecord(persistedState) ? persistedState.trees : null),
|
|
157
|
+
}),
|
|
158
|
+
},
|
|
159
|
+
),
|
|
160
|
+
);
|
|
@@ -4,6 +4,7 @@ export { ChatPresenterProvider } from "./components/providers/chat-presenter.pro
|
|
|
4
4
|
export { usePresenter } from "./components/providers/chat-presenter.provider";
|
|
5
5
|
export { ChatPresenter } from "./presenters/chat.presenter";
|
|
6
6
|
export { ChatDraftIntentManager } from "./managers/chat-draft-intent.manager";
|
|
7
|
+
export { ChatComposerIntentManager } from "./managers/chat-composer-intent.manager";
|
|
7
8
|
export { ChatCompletionNotificationManager } from "./managers/chat-completion-notification.manager";
|
|
8
9
|
export { buildSessionPath, CHAT_DRAFT_SESSION_PATH } from "./features/session/utils/chat-session-route.utils";
|
|
9
10
|
export { useChatSessionListStore } from "./stores/chat-session-list.store";
|
|
@@ -152,4 +152,33 @@ describe('ChatComposerIntentManager', () => {
|
|
|
152
152
|
}),
|
|
153
153
|
);
|
|
154
154
|
});
|
|
155
|
+
|
|
156
|
+
it('publishes an immutable UI resource reference to the targeted composer', () => {
|
|
157
|
+
const manager = new ChatComposerIntentManager();
|
|
158
|
+
const listener = vi.fn();
|
|
159
|
+
manager.subscribe('session-1', listener);
|
|
160
|
+
const reference = {
|
|
161
|
+
uri: 'nextclaw://panel-app/task-board',
|
|
162
|
+
resourceKind: 'panel-app',
|
|
163
|
+
title: 'Task board',
|
|
164
|
+
currentUrl: '/api/panel-apps/task-board/content',
|
|
165
|
+
contentParams: { boardId: 'today' },
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
manager.requestUiResourceReference({
|
|
169
|
+
targetSessionKey: 'session-1',
|
|
170
|
+
reference,
|
|
171
|
+
});
|
|
172
|
+
reference.contentParams.boardId = 'mutated';
|
|
173
|
+
|
|
174
|
+
expect(listener).toHaveBeenCalledWith(expect.objectContaining({
|
|
175
|
+
kind: 'ui_resource',
|
|
176
|
+
targetSessionKey: 'session-1',
|
|
177
|
+
tokenKey: 'nextclaw://panel-app/task-board',
|
|
178
|
+
label: 'Task board',
|
|
179
|
+
reference: expect.objectContaining({
|
|
180
|
+
contentParams: { boardId: 'today' },
|
|
181
|
+
}),
|
|
182
|
+
}));
|
|
183
|
+
});
|
|
155
184
|
});
|