@nextclaw/ui 0.15.23 → 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 (119) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/dist/assets/{api-Glw2OGXK.js → api-FK7tikI1.js} +4 -4
  3. package/dist/assets/appearance-settings-page-BYbvl2wj.js +1 -0
  4. package/dist/assets/{channels-list-page-C7E4Sxwd.js → channels-list-page-CocdcIko.js} +2 -2
  5. package/dist/assets/{chat-page-BT6dNI-4.js → chat-page-C5E-LLVd.js} +1 -1
  6. package/dist/assets/config-split-page-CA_a0AJ5.js +1 -0
  7. package/dist/assets/confirm-dialog-CxZfenjK.js +5 -0
  8. package/dist/assets/desktop-update-config-BEKC8Ly9.js +1 -0
  9. package/dist/assets/{dist-dv_SUR54.js → dist-BH7W5yCi.js} +1 -1
  10. package/dist/assets/{dist-CtLyEJeO.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-BIBjkwfV.js → doc-browser-context-FDPs3gJf.js} +1 -1
  14. package/dist/assets/form-actions-0c0aw_bl.js +1 -0
  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-Bc_jI5iH.js +9 -0
  20. package/dist/assets/mcp-marketplace-page-CzCieD9r.js +1 -0
  21. package/dist/assets/model-config-page-C4jw-pcj.js +1 -0
  22. package/dist/assets/{navigation-link-CB0OQbnY.js → navigation-link-CVy0_kVl.js} +2 -2
  23. package/dist/assets/plus-CG1RwZQq.js +1 -0
  24. package/dist/assets/provider-scoped-model-input-CmH4BFvc.js +1 -0
  25. package/dist/assets/providers-config-page-CJQjwzaw.js +1 -0
  26. package/dist/assets/react-DjFBVA0n.js +8 -0
  27. package/dist/assets/{refresh-cw-Ba0LZou4.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-DTOAvqzt.js → save-BKNrtK81.js} +1 -1
  32. package/dist/assets/{search-D4X5DNMe.js → search-CkOm65W_.js} +1 -1
  33. package/dist/assets/{search-config-page-Cr6254K5.js → search-config-page-BaIijtmO.js} +1 -1
  34. package/dist/assets/{secrets-config-page-W2Hx332d.js → secrets-config-page-CJjtdFAl.js} +2 -2
  35. package/dist/assets/security-config-DB8apj09.js +1 -0
  36. package/dist/assets/{select-CDv0g2Pj.js → select-Ea7ffKCE.js} +2 -2
  37. package/dist/assets/{settings-2-CoxzNABp.js → settings-2-CtyN3m_V.js} +1 -1
  38. package/dist/assets/skeleton-Bt6zxcAB.js +1 -0
  39. package/dist/assets/{tag-chip-CjkZEoS2.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/conversation/hooks/__tests__/use-session-provider-model-catalog.test.tsx +54 -2
  46. package/src/features/chat/features/conversation/hooks/use-chat-composer-reference-intent.ts +10 -1
  47. package/src/features/chat/features/conversation/hooks/use-session-provider-model-catalog.ts +76 -1
  48. package/src/features/chat/features/conversation/utils/session-conversation-input-toolbar.utils.ts +6 -1
  49. package/src/features/chat/features/input/utils/__tests__/chat-inline-token.utils.test.ts +29 -0
  50. package/src/features/chat/features/input/utils/__tests__/chat-input-bar.utils.test.ts +12 -3
  51. package/src/features/chat/features/input/utils/chat-composer-token-protocol.utils.ts +5 -0
  52. package/src/features/chat/features/input/utils/chat-conversation-excerpt-token.utils.ts +59 -0
  53. package/src/features/chat/features/input/utils/chat-inline-token.utils.ts +42 -16
  54. package/src/features/chat/features/input/utils/chat-input-toolbar.utils.ts +50 -14
  55. package/src/features/chat/features/message/components/chat-message-list.container.tsx +40 -0
  56. package/src/features/chat/features/message/utils/chat-message-texts.utils.ts +5 -0
  57. package/src/features/chat/features/ncp/hooks/__tests__/use-hydrated-ncp-agent.test.tsx +40 -3
  58. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-file-preview.selection.test.tsx +66 -0
  59. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-file-preview.test.tsx +13 -1
  60. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel-content.test.tsx +573 -90
  61. package/src/features/chat/features/workspace/components/__tests__/workspace-text-selection-menu.test.tsx +11 -4
  62. package/src/features/chat/features/workspace/components/chat-session-workspace-directory-browser.tsx +389 -288
  63. package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumbs.tsx +54 -35
  64. package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx +79 -148
  65. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-content.tsx +149 -94
  66. package/src/features/chat/features/workspace/components/project-files/__tests__/workspace-directory-entry-menu.test.tsx +50 -0
  67. package/src/features/chat/features/workspace/components/project-files/workspace-directory-entry-menu.tsx +209 -0
  68. package/src/features/chat/features/workspace/components/project-files/workspace-directory-tree.tsx +411 -0
  69. package/src/features/chat/features/workspace/components/project-files/workspace-new-folder-tree-item.tsx +112 -0
  70. package/src/features/chat/features/workspace/components/project-files/workspace-project-files-toolbar.tsx +64 -0
  71. package/src/features/chat/features/workspace/components/workspace-text-selection-menu.tsx +17 -195
  72. package/src/features/chat/features/workspace/hooks/use-workspace-explorer-layout.ts +88 -0
  73. package/src/features/chat/features/workspace/hooks/use-workspace-file-actions.ts +335 -0
  74. package/src/features/chat/features/workspace/utils/chat-workspace-file-path-mutation.utils.ts +58 -0
  75. package/src/features/chat/features/workspace/utils/chat-workspace-file-tab.utils.ts +23 -22
  76. package/src/features/chat/features/workspace/utils/chat-workspace-panel-layout.utils.ts +13 -4
  77. package/src/features/chat/managers/__tests__/chat-composer-intent.manager.test.ts +94 -45
  78. package/src/features/chat/managers/__tests__/chat-thread.manager.test.ts +103 -99
  79. package/src/features/chat/managers/chat-composer-intent.manager.ts +83 -34
  80. package/src/features/chat/managers/chat-thread.manager.ts +132 -128
  81. package/src/features/chat/stores/__tests__/chat-thread.store.test.ts +20 -40
  82. package/src/features/chat/stores/chat-thread.store.ts +47 -74
  83. package/src/features/chat/types/chat-input-bar.types.ts +3 -0
  84. package/src/features/side-dock/components/__tests__/side-dock.test.tsx +1 -0
  85. package/src/features/side-dock/configs/side-dock-built-in-items.config.ts +0 -9
  86. package/src/shared/components/resizable-right-panel/__tests__/resizable-right-panel.test.tsx +35 -38
  87. package/src/shared/components/resizable-right-panel/resizable-right-panel.tsx +25 -19
  88. package/src/shared/components/ui/context-menu/__tests__/context-menu.test.tsx +60 -34
  89. package/src/shared/components/ui/context-menu/context-menu.tsx +95 -68
  90. package/src/shared/lib/api/managers/client.manager.ts +31 -17
  91. package/src/shared/lib/api/server-path/server-path.types.ts +66 -0
  92. package/src/shared/lib/api/types.ts +145 -116
  93. package/src/shared/lib/api/utils/server-path.utils.ts +35 -11
  94. package/src/shared/lib/i18n/locales/en-US/chat.json +49 -1
  95. package/src/shared/lib/i18n/locales/zh-CN/chat.json +49 -1
  96. package/dist/assets/appearance-settings-page-Bl_W_9m7.js +0 -1
  97. package/dist/assets/config-split-page-D5BgtKrm.js +0 -1
  98. package/dist/assets/confirm-dialog-Bf907Azu.js +0 -5
  99. package/dist/assets/desktop-update-config-BwuyBPJb.js +0 -1
  100. package/dist/assets/doc-browser-C23NDGh5.js +0 -1
  101. package/dist/assets/doc-browser-DlOOhBxI.js +0 -1
  102. package/dist/assets/form-actions-DAzi68Cn.js +0 -1
  103. package/dist/assets/index-BLNg2yg0.css +0 -1
  104. package/dist/assets/index-D3EKjEyK.js +0 -107
  105. package/dist/assets/loader-circle-BSg5u0r4.js +0 -1
  106. package/dist/assets/mcp-marketplace-page-ByhvQQj5.js +0 -9
  107. package/dist/assets/mcp-marketplace-page-CytXwnUs.js +0 -1
  108. package/dist/assets/model-config-page-bgEx3MjQ.js +0 -1
  109. package/dist/assets/plus-CNjWdLhZ.js +0 -1
  110. package/dist/assets/provider-scoped-model-input-8F6JMd2n.js +0 -1
  111. package/dist/assets/providers-config-page-CxpTjR81.js +0 -1
  112. package/dist/assets/react-Bk2ydNTe.js +0 -8
  113. package/dist/assets/remote-Dv38PuMV.js +0 -1
  114. package/dist/assets/rotate-cw-IkYVk9JX.js +0 -1
  115. package/dist/assets/runtime-config-page-C0ByyUVr.js +0 -1
  116. package/dist/assets/security-config-1E8OF4kR.js +0 -1
  117. package/dist/assets/skeleton-DW018fJv.js +0 -1
  118. package/dist/assets/use-confirm-dialog-CavtY1vJ.js +0 -1
  119. package/dist/assets/x-PTXYTEwz.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
+ }