@nextclaw/ui 0.15.24 → 0.15.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/assets/{api-BxPNDp30.js → api-FK7tikI1.js} +3 -3
  3. package/dist/assets/appearance-settings-page-BYbvl2wj.js +1 -0
  4. package/dist/assets/{channels-list-page-DRaNWAxC.js → channels-list-page-CocdcIko.js} +2 -2
  5. package/dist/assets/chat-page-C5E-LLVd.js +1 -0
  6. package/dist/assets/{config-split-page-BVWVDbvG.js → config-split-page-CA_a0AJ5.js} +1 -1
  7. package/dist/assets/{confirm-dialog-mA2iVzxh.js → confirm-dialog-CxZfenjK.js} +1 -1
  8. package/dist/assets/{desktop-update-config-DyQb1LXr.js → desktop-update-config-BEKC8Ly9.js} +1 -1
  9. package/dist/assets/{dist-B4Pf5X3Q.js → dist-BH7W5yCi.js} +1 -1
  10. package/dist/assets/{dist-DHEWkGWX.js → dist-CaYdVVeO.js} +1 -1
  11. package/dist/assets/doc-browser-Cl2ldE3f.js +1 -0
  12. package/dist/assets/doc-browser-DXCSd63E.js +1 -0
  13. package/dist/assets/{doc-browser-context-CuGI6PQc.js → doc-browser-context-FDPs3gJf.js} +1 -1
  14. package/dist/assets/{form-actions-BOfIyG3O.js → form-actions-0c0aw_bl.js} +1 -1
  15. package/dist/assets/i18n-provider-DkITJNz8.js +1 -0
  16. package/dist/assets/index-CQ2LcI2p.css +1 -0
  17. package/dist/assets/index-CrdfvBVg.js +107 -0
  18. package/dist/assets/loader-circle-DHtUxCIA.js +1 -0
  19. package/dist/assets/{mcp-marketplace-page-ofDqbOIF.js → mcp-marketplace-page-Bc_jI5iH.js} +2 -2
  20. package/dist/assets/mcp-marketplace-page-CzCieD9r.js +1 -0
  21. package/dist/assets/{model-config-page-D73Tu3jW.js → model-config-page-C4jw-pcj.js} +1 -1
  22. package/dist/assets/{navigation-link-CAFOcXNg.js → navigation-link-CVy0_kVl.js} +1 -1
  23. package/dist/assets/plus-CG1RwZQq.js +1 -0
  24. package/dist/assets/{provider-scoped-model-input-BaJJJYMX.js → provider-scoped-model-input-CmH4BFvc.js} +1 -1
  25. package/dist/assets/{providers-config-page-BL6IRVBm.js → providers-config-page-CJQjwzaw.js} +1 -1
  26. package/dist/assets/{react-BC_orpAj.js → react-DjFBVA0n.js} +1 -1
  27. package/dist/assets/{refresh-cw-CWDp_JnN.js → refresh-cw-j2bl79Ea.js} +1 -1
  28. package/dist/assets/remote-BIlpD3yL.js +1 -0
  29. package/dist/assets/rotate-cw-bKG8zH8M.js +1 -0
  30. package/dist/assets/runtime-config-page-B_x2Glz5.js +1 -0
  31. package/dist/assets/{save-Cnr71JON.js → save-BKNrtK81.js} +1 -1
  32. package/dist/assets/{search-CCX11X2m.js → search-CkOm65W_.js} +1 -1
  33. package/dist/assets/{search-config-page-BB6d5gT7.js → search-config-page-BaIijtmO.js} +1 -1
  34. package/dist/assets/{secrets-config-page-DYsDeT9K.js → secrets-config-page-CJjtdFAl.js} +2 -2
  35. package/dist/assets/security-config-DB8apj09.js +1 -0
  36. package/dist/assets/{select-DvwBu99K.js → select-Ea7ffKCE.js} +1 -1
  37. package/dist/assets/{settings-2-Dmv6zMne.js → settings-2-CtyN3m_V.js} +1 -1
  38. package/dist/assets/skeleton-Bt6zxcAB.js +1 -0
  39. package/dist/assets/{tag-chip-gMPvx7n_.js → tag-chip-C9l4xsKp.js} +1 -1
  40. package/dist/assets/x-CowtCu9Z.js +1 -0
  41. package/dist/index.html +20 -20
  42. package/package.json +8 -8
  43. package/src/features/chat/features/conversation/components/__tests__/session-conversation-area.test.tsx +30 -1
  44. package/src/features/chat/features/conversation/components/session-conversation-area.tsx +1 -1
  45. package/src/features/chat/features/ncp/hooks/__tests__/use-hydrated-ncp-agent.test.tsx +40 -3
  46. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-file-preview.selection.test.tsx +66 -0
  47. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-file-preview.test.tsx +13 -1
  48. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel-content.test.tsx +573 -90
  49. package/src/features/chat/features/workspace/components/chat-session-workspace-directory-browser.tsx +389 -288
  50. package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumbs.tsx +54 -35
  51. package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx +79 -148
  52. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-content.tsx +149 -94
  53. package/src/features/chat/features/workspace/components/project-files/__tests__/workspace-directory-entry-menu.test.tsx +50 -0
  54. package/src/features/chat/features/workspace/components/project-files/workspace-directory-entry-menu.tsx +209 -0
  55. package/src/features/chat/features/workspace/components/project-files/workspace-directory-tree.tsx +411 -0
  56. package/src/features/chat/features/workspace/components/project-files/workspace-new-folder-tree-item.tsx +112 -0
  57. package/src/features/chat/features/workspace/components/project-files/workspace-project-files-toolbar.tsx +64 -0
  58. package/src/features/chat/features/workspace/hooks/use-workspace-explorer-layout.ts +88 -0
  59. package/src/features/chat/features/workspace/hooks/use-workspace-file-actions.ts +335 -0
  60. package/src/features/chat/features/workspace/utils/chat-workspace-file-path-mutation.utils.ts +58 -0
  61. package/src/features/chat/features/workspace/utils/chat-workspace-file-tab.utils.ts +23 -22
  62. package/src/features/chat/features/workspace/utils/chat-workspace-panel-layout.utils.ts +13 -4
  63. package/src/features/chat/managers/__tests__/chat-composer-intent.manager.test.ts +86 -60
  64. package/src/features/chat/managers/__tests__/chat-thread.manager.test.ts +103 -99
  65. package/src/features/chat/managers/chat-composer-intent.manager.ts +37 -33
  66. package/src/features/chat/managers/chat-thread.manager.ts +132 -128
  67. package/src/features/chat/stores/__tests__/chat-thread.store.test.ts +20 -40
  68. package/src/features/chat/stores/chat-thread.store.ts +47 -74
  69. package/src/shared/components/resizable-right-panel/__tests__/resizable-right-panel.test.tsx +35 -38
  70. package/src/shared/components/resizable-right-panel/resizable-right-panel.tsx +25 -19
  71. package/src/shared/components/ui/context-menu/__tests__/context-menu.test.tsx +60 -34
  72. package/src/shared/components/ui/context-menu/context-menu.tsx +95 -68
  73. package/src/shared/lib/api/managers/client.manager.ts +31 -17
  74. package/src/shared/lib/api/server-path/server-path.types.ts +66 -0
  75. package/src/shared/lib/api/types.ts +145 -116
  76. package/src/shared/lib/api/utils/server-path.utils.ts +35 -11
  77. package/src/shared/lib/i18n/locales/en-US/chat.json +42 -0
  78. package/src/shared/lib/i18n/locales/zh-CN/chat.json +42 -0
  79. package/dist/assets/appearance-settings-page-DpVJjvkB.js +0 -1
  80. package/dist/assets/chat-page-BuFDmG5_.js +0 -1
  81. package/dist/assets/doc-browser-CtO_HtRy.js +0 -1
  82. package/dist/assets/doc-browser-eMUagyBV.js +0 -1
  83. package/dist/assets/index-BjBdWHV1.js +0 -107
  84. package/dist/assets/index-DmpmjAGQ.css +0 -1
  85. package/dist/assets/loader-circle-Df8wF2Pr.js +0 -1
  86. package/dist/assets/mcp-marketplace-page-DI49IKxp.js +0 -1
  87. package/dist/assets/plus-DVU5RLCS.js +0 -1
  88. package/dist/assets/remote-T24O58lH.js +0 -1
  89. package/dist/assets/rotate-cw-BqhwSV5p.js +0 -1
  90. package/dist/assets/runtime-config-page-CXUx017t.js +0 -1
  91. package/dist/assets/security-config-CVPgqL7G.js +0 -1
  92. package/dist/assets/skeleton-C5tWata3.js +0 -1
  93. package/dist/assets/use-confirm-dialog-D_3-QpZU.js +0 -1
  94. package/dist/assets/x-BEWSqeux.js +0 -1
@@ -0,0 +1,209 @@
1
+ import { copyText } from "@nextclaw/agent-chat-ui";
2
+ import {
3
+ Copy,
4
+ Download,
5
+ ExternalLink,
6
+ FilePlus2,
7
+ FolderPlus,
8
+ LocateFixed,
9
+ MessageSquarePlus,
10
+ Pencil,
11
+ Trash2,
12
+ Upload,
13
+ } from "lucide-react";
14
+ import { toast } from "sonner";
15
+ import type {
16
+ RefreshDirectory,
17
+ WorkspaceDirectoryActionTarget,
18
+ } from "@/features/chat/features/workspace/hooks/use-workspace-file-actions";
19
+ import type { ContextMenuGroup } from "@/shared/components/ui/context-menu/context-menu";
20
+ import type { ServerPathEntryView } from "@/shared/lib/api";
21
+ import { t } from "@/shared/lib/i18n";
22
+
23
+ export async function copyWorkspacePath(path: string) {
24
+ const copied = await copyText(path);
25
+ toast[copied ? "success" : "error"](
26
+ t(copied ? "chatCodeCopied" : "chatWorkspaceCopyPathFailed"),
27
+ );
28
+ }
29
+
30
+ export function buildWorkspaceEntryContextMenuGroups({
31
+ busy,
32
+ downloadUrl,
33
+ entry,
34
+ onAddToChat,
35
+ onCreateFolder,
36
+ onCreateFile,
37
+ onDelete,
38
+ onOpen,
39
+ onRenameRequest,
40
+ onRevealPath,
41
+ onUpload,
42
+ ownCreateTarget,
43
+ parentRefresh,
44
+ relativePath,
45
+ }: {
46
+ busy: boolean;
47
+ downloadUrl: string | null;
48
+ entry: ServerPathEntryView;
49
+ onAddToChat?: (entry: ServerPathEntryView, relativePath: string) => void;
50
+ onCreateFolder?: (target: WorkspaceDirectoryActionTarget) => void;
51
+ onCreateFile?: (target: WorkspaceDirectoryActionTarget) => void;
52
+ onDelete?: (entry: ServerPathEntryView, refresh: RefreshDirectory) => void;
53
+ onOpen: () => void;
54
+ onRenameRequest?: () => void;
55
+ onRevealPath?: (path: string) => void;
56
+ onUpload?: (target: WorkspaceDirectoryActionTarget) => void;
57
+ ownCreateTarget: WorkspaceDirectoryActionTarget;
58
+ parentRefresh: RefreshDirectory;
59
+ relativePath: string | null;
60
+ }): ContextMenuGroup[] {
61
+ const isDirectory = entry.kind === "directory";
62
+ const openGroup: ContextMenuGroup = {
63
+ key: "open",
64
+ items: [
65
+ {
66
+ key: "open",
67
+ label: t("chatWorkspaceOpen"),
68
+ icon: <ExternalLink className="h-4 w-4" />,
69
+ onSelect: onOpen,
70
+ },
71
+ ],
72
+ };
73
+ const manageGroup: ContextMenuGroup = {
74
+ key: "manage",
75
+ items: isDirectory
76
+ ? [
77
+ ...(onCreateFile
78
+ ? [
79
+ {
80
+ key: "new-file",
81
+ label: t("chatWorkspaceNewFile"),
82
+ icon: <FilePlus2 className="h-4 w-4" />,
83
+ disabled: busy,
84
+ restoreFocus: false,
85
+ onSelect: () => onCreateFile(ownCreateTarget),
86
+ },
87
+ ]
88
+ : []),
89
+ ...(onCreateFolder
90
+ ? [
91
+ {
92
+ key: "new-folder",
93
+ label: t("chatWorkspaceNewFolder"),
94
+ icon: <FolderPlus className="h-4 w-4" />,
95
+ disabled: busy,
96
+ restoreFocus: false,
97
+ onSelect: () => onCreateFolder(ownCreateTarget),
98
+ },
99
+ ]
100
+ : []),
101
+ ...(onUpload
102
+ ? [
103
+ {
104
+ key: "upload",
105
+ label: t("chatWorkspaceUploadFilesHere"),
106
+ icon: <Upload className="h-4 w-4" />,
107
+ disabled: busy,
108
+ restoreFocus: false,
109
+ onSelect: () => onUpload(ownCreateTarget),
110
+ },
111
+ ]
112
+ : []),
113
+ ]
114
+ : [],
115
+ };
116
+ const nextclawGroup: ContextMenuGroup = {
117
+ key: "nextclaw",
118
+ items: [
119
+ ...(relativePath && onAddToChat
120
+ ? [
121
+ {
122
+ key: "add-to-chat",
123
+ label: t("chatWorkspaceAddToChat"),
124
+ icon: <MessageSquarePlus className="h-4 w-4" />,
125
+ restoreFocus: false,
126
+ onSelect: () => onAddToChat(entry, relativePath),
127
+ },
128
+ ]
129
+ : []),
130
+ ...(downloadUrl
131
+ ? [
132
+ {
133
+ key: "download",
134
+ label: t("chatWorkspaceDownloadFile"),
135
+ icon: <Download className="h-4 w-4" />,
136
+ href: downloadUrl,
137
+ download: entry.name,
138
+ },
139
+ ]
140
+ : []),
141
+ ],
142
+ };
143
+ const pathGroup: ContextMenuGroup = {
144
+ key: "path",
145
+ items: [
146
+ ...(onRevealPath
147
+ ? [
148
+ {
149
+ key: "reveal",
150
+ label: t("chatWorkspaceRevealInFileManager"),
151
+ icon: <LocateFixed className="h-4 w-4" />,
152
+ onSelect: () => onRevealPath(entry.path),
153
+ },
154
+ ]
155
+ : []),
156
+ {
157
+ key: "copy-path",
158
+ label: t("chatWorkspaceCopyPath"),
159
+ icon: <Copy className="h-4 w-4" />,
160
+ onSelect: () => void copyWorkspacePath(entry.path),
161
+ },
162
+ ...(relativePath
163
+ ? [
164
+ {
165
+ key: "copy-relative-path",
166
+ label: t("chatWorkspaceCopyRelativePath"),
167
+ icon: <Copy className="h-4 w-4" />,
168
+ onSelect: () => void copyWorkspacePath(relativePath),
169
+ },
170
+ ]
171
+ : []),
172
+ ],
173
+ };
174
+ const renameGroup: ContextMenuGroup = {
175
+ key: "rename",
176
+ items: onRenameRequest
177
+ ? [
178
+ {
179
+ key: "rename",
180
+ label: t("chatWorkspaceRename"),
181
+ icon: <Pencil className="h-4 w-4" />,
182
+ disabled: busy,
183
+ restoreFocus: false,
184
+ onSelect: onRenameRequest,
185
+ },
186
+ ]
187
+ : [],
188
+ };
189
+ const dangerGroup: ContextMenuGroup = {
190
+ key: "danger",
191
+ items: onDelete
192
+ ? [
193
+ {
194
+ key: "delete",
195
+ label: t("chatWorkspaceDelete"),
196
+ icon: <Trash2 className="h-4 w-4" />,
197
+ destructive: true,
198
+ disabled: busy,
199
+ restoreFocus: false,
200
+ onSelect: () => onDelete(entry, parentRefresh),
201
+ },
202
+ ]
203
+ : [],
204
+ };
205
+
206
+ return isDirectory
207
+ ? [nextclawGroup, manageGroup, pathGroup, renameGroup, dangerGroup]
208
+ : [openGroup, nextclawGroup, pathGroup, renameGroup, dangerGroup];
209
+ }
@@ -0,0 +1,411 @@
1
+ import { useEffect, useLayoutEffect, useMemo, useRef, useState, type ReactNode } from 'react';
2
+ import type { ChatFileOpenActionViewModel } from '@nextclaw/agent-chat-ui';
3
+ import {
4
+ ChevronDown,
5
+ ChevronRight,
6
+ Check,
7
+ Folder,
8
+ FolderOpen,
9
+ MoreHorizontal,
10
+ X,
11
+ } from 'lucide-react';
12
+ import type {
13
+ RefreshDirectory,
14
+ WorkspaceDirectoryActionTarget,
15
+ WorkspaceTreeSelection,
16
+ } from '@/features/chat/features/workspace/hooks/use-workspace-file-actions';
17
+ import { FileTypeIcon } from '@/shared/components/file-type-icon';
18
+ import { IconActionButton } from '@/shared/components/ui/actions/icon-action-button';
19
+ import { Input } from '@/shared/components/ui/input';
20
+ import {
21
+ ContextMenu,
22
+ ContextMenuTrigger,
23
+ } from '@/shared/components/ui/context-menu/context-menu';
24
+ import { useServerPathBrowse } from '@/shared/hooks/use-server-path-browse';
25
+ import { buildServerPathContentUrl, type ServerPathEntryView } from '@/shared/lib/api';
26
+ import { t } from '@/shared/lib/i18n';
27
+ import { cn } from '@/shared/lib/utils';
28
+ import { buildWorkspaceEntryContextMenuGroups } from '@/features/chat/features/workspace/components/project-files/workspace-directory-entry-menu';
29
+
30
+ export { copyWorkspacePath } from '@/features/chat/features/workspace/components/project-files/workspace-directory-entry-menu';
31
+
32
+ type WorkspaceDirectoryTreeActionProps = {
33
+ busy: boolean;
34
+ collapseVersion: number;
35
+ createTarget: WorkspaceDirectoryActionTarget | null;
36
+ onAddToChat?: (entry: ServerPathEntryView, relativePath: string) => void;
37
+ onCreateFolder?: (target: WorkspaceDirectoryActionTarget) => void;
38
+ onCreateFile?: (target: WorkspaceDirectoryActionTarget) => void;
39
+ onDelete?: (entry: ServerPathEntryView, refresh: RefreshDirectory) => void;
40
+ onRename?: (entry: ServerPathEntryView, name: string, refresh: RefreshDirectory) => Promise<unknown>;
41
+ onRevealComplete: () => void;
42
+ onSelect: (selection: WorkspaceTreeSelection) => void;
43
+ onUpload?: (target: WorkspaceDirectoryActionTarget) => void;
44
+ renderCreateFolder: (level: number) => ReactNode;
45
+ revealPath: string | null;
46
+ selectedPath: string | null;
47
+ };
48
+
49
+ function buildEntryLabel(entry: ServerPathEntryView): string {
50
+ const actionLabel = entry.kind === 'directory' ? t('chatWorkspaceOpenDirectory') : t('chatWorkspaceOpenFile');
51
+ return `${actionLabel}: ${entry.name}`;
52
+ }
53
+
54
+ function readErrorMessage(error: unknown): string {
55
+ return error instanceof Error ? error.message : String(error);
56
+ }
57
+
58
+ export function readBrowseError(browseQuery: ReturnType<typeof useServerPathBrowse>): string | null {
59
+ return browseQuery.error ? readErrorMessage(browseQuery.error) : null;
60
+ }
61
+
62
+ function EntryIcon({
63
+ fileName,
64
+ isDirectory,
65
+ isExpanded,
66
+ }: {
67
+ fileName: string;
68
+ isDirectory: boolean;
69
+ isExpanded: boolean;
70
+ }) {
71
+ if (!isDirectory) {
72
+ return <FileTypeIcon fileName={fileName} />;
73
+ }
74
+ return isExpanded ? (
75
+ <FolderOpen className="h-4 w-4 shrink-0 text-amber-500" />
76
+ ) : (
77
+ <Folder className="h-4 w-4 shrink-0 text-amber-500" />
78
+ );
79
+ }
80
+
81
+ function WorkspaceDirectoryTreeChildren({
82
+ browseQuery,
83
+ level,
84
+ onFileOpen,
85
+ onRevealPath,
86
+ parentCreateTarget,
87
+ parentRelativePath,
88
+ ...actions
89
+ }: WorkspaceDirectoryTreeActionProps & {
90
+ browseQuery: ReturnType<typeof useServerPathBrowse>;
91
+ level: number;
92
+ onFileOpen: (action: ChatFileOpenActionViewModel) => void;
93
+ onRevealPath?: (path: string) => void;
94
+ parentCreateTarget: WorkspaceDirectoryActionTarget;
95
+ parentRelativePath: string | null;
96
+ }) {
97
+ const entries = browseQuery.data?.entries ?? [];
98
+ const errorMessage = readBrowseError(browseQuery);
99
+ const statusStyle = { paddingLeft: `${36 + level * 14}px` };
100
+
101
+ if (browseQuery.isLoading && !browseQuery.data) {
102
+ return (
103
+ <div className="h-7 truncate pr-2 text-[11px] leading-7 text-gray-400" style={statusStyle}>
104
+ {t('chatWorkspaceLoadingDirectory')}
105
+ </div>
106
+ );
107
+ }
108
+ if (errorMessage && !browseQuery.data) {
109
+ return (
110
+ <div className="h-7 truncate pr-2 text-[11px] leading-7 text-rose-600" style={statusStyle} title={errorMessage}>
111
+ {t('chatWorkspaceDirectoryLoadFailed')}
112
+ </div>
113
+ );
114
+ }
115
+ if (entries.length === 0) {
116
+ return (
117
+ <div className="h-7 truncate pr-2 text-[11px] leading-7 text-gray-400" style={statusStyle}>
118
+ {t('chatWorkspaceDirectoryEmpty')}
119
+ </div>
120
+ );
121
+ }
122
+ return entries.map((entry) => (
123
+ <WorkspaceDirectoryTreeEntry
124
+ key={entry.path}
125
+ {...actions}
126
+ browseParentRefresh={() => browseQuery.refetch()}
127
+ entry={entry}
128
+ level={level + 1}
129
+ onFileOpen={onFileOpen}
130
+ onRevealPath={onRevealPath}
131
+ parentCreateTarget={parentCreateTarget}
132
+ relativePath={parentRelativePath ? `${parentRelativePath}/${entry.name}` : null}
133
+ />
134
+ ));
135
+ }
136
+
137
+ function useWorkspaceEntryRename({
138
+ entry,
139
+ onRename,
140
+ refresh,
141
+ }: {
142
+ entry: ServerPathEntryView;
143
+ onRename?: WorkspaceDirectoryTreeActionProps['onRename'];
144
+ refresh: RefreshDirectory;
145
+ }) {
146
+ const [active, setActive] = useState(false);
147
+ const [name, setName] = useState(entry.name);
148
+ const [error, setError] = useState<string | null>(null);
149
+ const cancel = () => setActive(false);
150
+ const start = () => {
151
+ setName(entry.name);
152
+ setError(null);
153
+ setActive(true);
154
+ };
155
+ const updateName = (value: string) => {
156
+ setName(value);
157
+ setError(null);
158
+ };
159
+ const submit = async () => {
160
+ const normalizedName = name.trim();
161
+ if (
162
+ !onRename ||
163
+ !normalizedName ||
164
+ normalizedName === '.' ||
165
+ normalizedName === '..' ||
166
+ normalizedName.includes('/') ||
167
+ normalizedName.includes('\\')
168
+ ) {
169
+ return;
170
+ }
171
+ if (normalizedName === entry.name) {
172
+ cancel();
173
+ return;
174
+ }
175
+ try {
176
+ await onRename(entry, normalizedName, refresh);
177
+ cancel();
178
+ } catch (renameError) {
179
+ setError(`${t('chatWorkspaceRenameFailed')}: ${readErrorMessage(renameError)}`);
180
+ }
181
+ };
182
+ return { active, cancel, error, name, start, submit, updateName };
183
+ }
184
+
185
+ export function WorkspaceDirectoryTreeEntry({
186
+ browseParentRefresh,
187
+ busy,
188
+ collapseVersion,
189
+ createTarget,
190
+ entry,
191
+ level,
192
+ onAddToChat,
193
+ onCreateFolder,
194
+ onCreateFile,
195
+ onDelete,
196
+ onRename,
197
+ onFileOpen,
198
+ onRevealComplete,
199
+ onRevealPath,
200
+ onSelect,
201
+ onUpload,
202
+ parentCreateTarget,
203
+ relativePath,
204
+ renderCreateFolder,
205
+ revealPath,
206
+ selectedPath,
207
+ }: WorkspaceDirectoryTreeActionProps & {
208
+ browseParentRefresh: RefreshDirectory;
209
+ entry: ServerPathEntryView;
210
+ level: number;
211
+ onFileOpen: (action: ChatFileOpenActionViewModel) => void;
212
+ onRevealPath?: (path: string) => void;
213
+ parentCreateTarget: WorkspaceDirectoryActionTarget;
214
+ relativePath: string | null;
215
+ }) {
216
+ const rowRef = useRef<HTMLDivElement | null>(null);
217
+ const entryButtonRef = useRef<HTMLButtonElement | null>(null);
218
+ const isDirectory = entry.kind === 'directory';
219
+ const [isExpanded, setIsExpanded] = useState(false);
220
+ const rename = useWorkspaceEntryRename({
221
+ entry,
222
+ onRename,
223
+ refresh: browseParentRefresh,
224
+ });
225
+ useEffect(() => {
226
+ if (collapseVersion > 0) setIsExpanded(false);
227
+ }, [collapseVersion]);
228
+ const browseQuery = useServerPathBrowse({
229
+ path: entry.path,
230
+ includeFiles: true,
231
+ enabled: isDirectory && isExpanded,
232
+ });
233
+ const refreshDirectory = browseQuery.refetch;
234
+ const downloadUrl = isDirectory ? null : buildServerPathContentUrl(entry.path);
235
+ const ownCreateTarget = useMemo<WorkspaceDirectoryActionTarget>(
236
+ () =>
237
+ isDirectory
238
+ ? {
239
+ label: entry.name,
240
+ path: entry.path,
241
+ refresh: () => refreshDirectory(),
242
+ expand: () => setIsExpanded(true),
243
+ }
244
+ : parentCreateTarget,
245
+ [entry.name, entry.path, isDirectory, parentCreateTarget, refreshDirectory],
246
+ );
247
+ const selection = useMemo<WorkspaceTreeSelection>(
248
+ () => ({ path: entry.path, createTarget: ownCreateTarget }),
249
+ [entry.path, ownCreateTarget],
250
+ );
251
+
252
+ useLayoutEffect(() => {
253
+ if (revealPath !== entry.path) return;
254
+ rowRef.current?.scrollIntoView?.({ block: 'nearest' });
255
+ onSelect(selection);
256
+ entryButtonRef.current?.focus();
257
+ onRevealComplete();
258
+ }, [entry.path, onRevealComplete, onSelect, revealPath, selection]);
259
+
260
+ const activateEntry = () => {
261
+ onSelect(selection);
262
+ if (isDirectory) setIsExpanded((value) => !value);
263
+ else onFileOpen({ path: entry.path, label: entry.name, viewMode: 'preview' });
264
+ };
265
+
266
+ const contextMenuGroups = buildWorkspaceEntryContextMenuGroups({
267
+ busy,
268
+ downloadUrl,
269
+ entry,
270
+ onAddToChat,
271
+ onCreateFolder,
272
+ onCreateFile,
273
+ onDelete,
274
+ onOpen: activateEntry,
275
+ onRenameRequest: onRename ? rename.start : undefined,
276
+ onRevealPath,
277
+ onUpload,
278
+ ownCreateTarget,
279
+ parentRefresh: browseParentRefresh,
280
+ relativePath,
281
+ });
282
+ return (
283
+ <div role="treeitem" aria-expanded={isDirectory ? isExpanded : undefined} aria-label={buildEntryLabel(entry)}>
284
+ <ContextMenu groups={contextMenuGroups} label={t('chatWorkspaceFileContextMenu')}>
285
+ <div
286
+ ref={rowRef}
287
+ data-workspace-tree-entry=""
288
+ className={cn(
289
+ 'group flex h-[26px] w-full min-w-0 items-center text-gray-700 transition-colors hover:bg-gray-100 focus-within:bg-gray-100',
290
+ selectedPath === entry.path ? 'bg-primary/10' : null,
291
+ entry.hidden ? 'opacity-65' : null,
292
+ )}
293
+ style={{ paddingLeft: `${6 + level * 12}px` }}
294
+ onContextMenu={() => onSelect(selection)}
295
+ >
296
+ {rename.active ? (
297
+ <form
298
+ className="flex h-full min-w-0 flex-1 items-center gap-1"
299
+ onSubmit={(event) => {
300
+ event.preventDefault();
301
+ void rename.submit();
302
+ }}
303
+ >
304
+ <Input
305
+ autoFocus
306
+ aria-label={t('chatWorkspaceRenameName')}
307
+ className="h-6 min-w-0 flex-1 px-1.5 text-xs"
308
+ value={rename.name}
309
+ onChange={(event) => rename.updateName(event.target.value)}
310
+ onKeyDown={(event) => {
311
+ if (event.key === 'Escape') {
312
+ event.preventDefault();
313
+ rename.cancel();
314
+ }
315
+ }}
316
+ />
317
+ <IconActionButton
318
+ icon={<Check className="h-3.5 w-3.5" />}
319
+ label={t('chatWorkspaceRename')}
320
+ size="sm"
321
+ onClick={() => void rename.submit()}
322
+ />
323
+ <IconActionButton
324
+ icon={<X className="h-3.5 w-3.5" />}
325
+ label={t('cancel')}
326
+ size="sm"
327
+ onClick={rename.cancel}
328
+ />
329
+ </form>
330
+ ) : (
331
+ <button
332
+ ref={entryButtonRef}
333
+ type="button"
334
+ aria-label={entry.name}
335
+ className="flex h-full min-w-0 flex-1 items-center gap-1 text-left text-[13px] focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-border"
336
+ onClick={activateEntry}
337
+ onKeyDown={(event) => {
338
+ if (event.key === 'F2' && onRename) {
339
+ event.preventDefault();
340
+ rename.start();
341
+ }
342
+ }}
343
+ >
344
+ <span className="inline-flex h-3.5 w-3.5 shrink-0 items-center justify-center text-gray-400">
345
+ {isDirectory ? (
346
+ isExpanded ? (
347
+ <ChevronDown className="h-3.5 w-3.5" />
348
+ ) : (
349
+ <ChevronRight className="h-3.5 w-3.5" />
350
+ )
351
+ ) : null}
352
+ </span>
353
+ <EntryIcon fileName={entry.name} isDirectory={isDirectory} isExpanded={isExpanded} />
354
+ <span className="min-w-0 flex-1 truncate" title={entry.name}>
355
+ {entry.name}
356
+ </span>
357
+ </button>
358
+ )}
359
+ {!rename.active ? (
360
+ <ContextMenuTrigger>
361
+ <IconActionButton
362
+ className="mr-1 opacity-0 pointer-events-none group-hover:pointer-events-auto group-hover:opacity-100 group-focus-within:pointer-events-auto group-focus-within:opacity-100 data-[context-menu-open]:pointer-events-auto data-[context-menu-open]:opacity-100"
363
+ icon={<MoreHorizontal className="h-3.5 w-3.5" />}
364
+ label={t('chatWorkspaceMoreActionsFor').replace('{name}', entry.name)}
365
+ size="sm"
366
+ tooltipSide="left"
367
+ onClick={() => onSelect(selection)}
368
+ />
369
+ </ContextMenuTrigger>
370
+ ) : null}
371
+ </div>
372
+ </ContextMenu>
373
+ {rename.error ? (
374
+ <p
375
+ role="alert"
376
+ className="py-1 pr-2 text-[11px] text-destructive"
377
+ style={{ paddingLeft: `${36 + level * 14}px` }}
378
+ >
379
+ {rename.error}
380
+ </p>
381
+ ) : null}
382
+ {isDirectory && isExpanded ? (
383
+ <div role="group">
384
+ {createTarget?.path === entry.path ? renderCreateFolder(level + 1) : null}
385
+ <WorkspaceDirectoryTreeChildren
386
+ browseQuery={browseQuery}
387
+ busy={busy}
388
+ collapseVersion={collapseVersion}
389
+ createTarget={createTarget}
390
+ level={level}
391
+ onAddToChat={onAddToChat}
392
+ onCreateFolder={onCreateFolder}
393
+ onCreateFile={onCreateFile}
394
+ onDelete={onDelete}
395
+ onRename={onRename}
396
+ onFileOpen={onFileOpen}
397
+ onRevealComplete={onRevealComplete}
398
+ onRevealPath={onRevealPath}
399
+ onSelect={onSelect}
400
+ onUpload={onUpload}
401
+ parentCreateTarget={ownCreateTarget}
402
+ parentRelativePath={relativePath}
403
+ renderCreateFolder={renderCreateFolder}
404
+ revealPath={revealPath}
405
+ selectedPath={selectedPath}
406
+ />
407
+ </div>
408
+ ) : null}
409
+ </div>
410
+ );
411
+ }