@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
@@ -1,282 +1,408 @@
1
- import { useState, type ReactNode } from "react";
2
- import { copyText, type ChatFileOpenActionViewModel } from "@nextclaw/agent-chat-ui";
1
+ import { useState, type MouseEventHandler, type ReactNode } from 'react';
2
+ import type { ChatFileOpenActionViewModel } from '@nextclaw/agent-chat-ui';
3
+ import { ChevronsUp, Copy, FilePlus2, FolderPlus, LocateFixed, Upload, RefreshCw } from 'lucide-react';
4
+ import { toast } from 'sonner';
5
+ import { usePresenter } from '@/features/chat/components/providers/chat-presenter.provider';
3
6
  import {
4
- ChevronDown,
5
- ChevronRight,
6
- Copy,
7
- Folder,
8
- FolderOpen,
9
- LocateFixed,
10
- MessageSquarePlus,
11
- } from "lucide-react";
12
- import { toast } from "sonner";
13
- import { usePresenter } from "@/features/chat/components/providers/chat-presenter.provider";
14
- import { FileTypeIcon } from "@/shared/components/file-type-icon";
7
+ copyWorkspacePath,
8
+ readBrowseError,
9
+ WorkspaceDirectoryTreeEntry,
10
+ } from '@/features/chat/features/workspace/components/project-files/workspace-directory-tree';
11
+ import { WorkspaceNewFolderTreeItem } from '@/features/chat/features/workspace/components/project-files/workspace-new-folder-tree-item';
12
+ import { WorkspaceProjectFilesToolbar } from '@/features/chat/features/workspace/components/project-files/workspace-project-files-toolbar';
15
13
  import {
16
- ContextMenu,
17
- type ContextMenuGroup,
18
- } from "@/shared/components/ui/context-menu/context-menu";
19
- import { useServerPathBrowse } from "@/shared/hooks/use-server-path-browse";
20
- import type { ServerPathEntryView } from "@/shared/lib/api";
21
- import { hostCapabilityManager } from "@/shared/lib/host-capabilities";
22
- import { t } from "@/shared/lib/i18n";
23
- import { cn } from "@/shared/lib/utils";
14
+ useWorkspaceProjectFilesController,
15
+ type RefreshDirectory,
16
+ type WorkspaceDirectoryActionTarget,
17
+ type WorkspaceTreeSelection,
18
+ } from '@/features/chat/features/workspace/hooks/use-workspace-file-actions';
19
+ import { ContextMenu, type ContextMenuGroup } from '@/shared/components/ui/context-menu/context-menu';
20
+ import { useConfirmDialog } from '@/shared/hooks/use-confirm-dialog';
21
+ import type { useServerPathBrowse } from '@/shared/hooks/use-server-path-browse';
22
+ import type { ServerPathEntryView } from '@/shared/lib/api';
23
+ import { hostCapabilityManager } from '@/shared/lib/host-capabilities';
24
+ import { t } from '@/shared/lib/i18n';
24
25
 
25
26
  type ChatSessionWorkspaceDirectoryBrowserProps = {
26
27
  browseQuery: ReturnType<typeof useServerPathBrowse>;
27
28
  onFileOpen: (action: ChatFileOpenActionViewModel) => void;
28
29
  sessionKey?: string | null;
29
- renderStatus: (params: {
30
- text: string;
31
- tone?: "muted" | "error";
32
- }) => ReactNode;
30
+ renderStatus: (params: { text: string; tone?: 'muted' | 'error' }) => ReactNode;
33
31
  showRoot?: boolean;
34
32
  };
35
33
 
36
- function buildDirectoryEntryLabel(entry: ServerPathEntryView): string {
37
- const actionLabel =
38
- entry.kind === "directory"
39
- ? t("chatWorkspaceOpenDirectory")
40
- : t("chatWorkspaceOpenFile");
41
- return `${actionLabel}: ${entry.name}`;
42
- }
43
-
44
34
  function readPathName(path: string): string {
45
35
  return path.split(/[\\/]/).filter(Boolean).pop() ?? path;
46
36
  }
47
37
 
48
- async function copyWorkspacePath(path: string) {
49
- const copied = await copyText(path);
50
- toast[copied ? "success" : "error"](
51
- t(copied ? "chatCodeCopied" : "chatWorkspaceCopyPathFailed"),
52
- );
53
- }
54
-
55
- function readBrowseError(
56
- browseQuery: ReturnType<typeof useServerPathBrowse>,
57
- ): string | null {
58
- if (!browseQuery.error) {
59
- return null;
60
- }
61
- return browseQuery.error instanceof Error
62
- ? browseQuery.error.message
63
- : String(browseQuery.error);
64
- }
65
-
66
- function WorkspaceDirectoryEntryIcon({
67
- fileName,
68
- isDirectory,
69
- isExpanded,
70
- }: {
71
- fileName: string;
72
- isDirectory: boolean;
73
- isExpanded: boolean;
74
- }) {
75
- if (!isDirectory) {
76
- return <FileTypeIcon fileName={fileName} />;
77
- }
78
- return isExpanded ? (
79
- <FolderOpen className="h-4 w-4 shrink-0 text-amber-500" />
80
- ) : (
81
- <Folder className="h-4 w-4 shrink-0 text-amber-500" />
82
- );
83
- }
84
-
85
- function WorkspaceDirectoryTreeChildren({
86
- browseQuery,
87
- level,
88
- onAddFileToChat,
38
+ function WorkspaceDirectoryEntries({
39
+ busy,
40
+ collapseVersion,
41
+ createTarget,
42
+ entries,
43
+ fallbackParentTarget,
44
+ mutable,
45
+ onAddToChat,
46
+ onCreateFile,
47
+ onCreateFolder,
48
+ onDelete,
89
49
  onFileOpen,
50
+ onRevealComplete,
51
+ onRename,
90
52
  onRevealPath,
91
- parentRelativePath,
53
+ onSelect,
54
+ onUpload,
55
+ refresh,
56
+ relativePaths,
57
+ renderCreateFolder,
58
+ revealPath,
59
+ selectedPath,
60
+ showRoot,
92
61
  }: {
93
- browseQuery: ReturnType<typeof useServerPathBrowse>;
94
- level: number;
95
- onAddFileToChat?: (entry: ServerPathEntryView, relativePath: string) => void;
62
+ busy: boolean;
63
+ collapseVersion: number;
64
+ createTarget: WorkspaceDirectoryActionTarget | null;
65
+ entries: readonly ServerPathEntryView[];
66
+ fallbackParentTarget: WorkspaceDirectoryActionTarget;
67
+ mutable: boolean;
68
+ onAddToChat?: (entry: ServerPathEntryView, relativePath: string) => void;
69
+ onCreateFile: (target: WorkspaceDirectoryActionTarget) => void;
70
+ onCreateFolder: (target: WorkspaceDirectoryActionTarget) => void;
71
+ onDelete: (entry: ServerPathEntryView, refresh: RefreshDirectory) => void;
96
72
  onFileOpen: (action: ChatFileOpenActionViewModel) => void;
73
+ onRevealComplete: () => void;
74
+ onRename: (entry: ServerPathEntryView, name: string, refresh: RefreshDirectory) => Promise<unknown>;
97
75
  onRevealPath?: (path: string) => void;
98
- parentRelativePath: string | null;
76
+ onSelect: (selection: WorkspaceTreeSelection) => void;
77
+ onUpload: (target: WorkspaceDirectoryActionTarget) => void;
78
+ refresh: RefreshDirectory;
79
+ relativePaths: boolean;
80
+ renderCreateFolder: (level: number) => ReactNode;
81
+ revealPath: string | null;
82
+ selectedPath: string | null;
83
+ showRoot: boolean;
99
84
  }) {
100
- const entries = browseQuery.data?.entries ?? [];
101
- const errorMessage = readBrowseError(browseQuery);
102
- const statusStyle = { paddingLeft: `${36 + level * 14}px` };
103
-
104
- if (browseQuery.isLoading && !browseQuery.data) {
105
- return (
106
- <div
107
- className="h-7 truncate pr-2 text-[11px] leading-7 text-gray-400"
108
- style={statusStyle}
109
- >
110
- {t("chatWorkspaceLoadingDirectory")}
111
- </div>
112
- );
113
- }
114
- if (errorMessage && !browseQuery.data) {
115
- return (
116
- <div
117
- className="h-7 truncate pr-2 text-[11px] leading-7 text-rose-600"
118
- style={statusStyle}
119
- title={errorMessage}
120
- >
121
- {t("chatWorkspaceDirectoryLoadFailed")}
122
- </div>
123
- );
124
- }
125
85
  if (entries.length === 0) {
126
- return (
127
- <div
128
- className="h-7 truncate pr-2 text-[11px] leading-7 text-gray-400"
129
- style={statusStyle}
130
- >
131
- {t("chatWorkspaceDirectoryEmpty")}
132
- </div>
133
- );
86
+ return <div className="px-4 py-8 text-center text-xs text-gray-400">{t('chatWorkspaceDirectoryEmpty')}</div>;
134
87
  }
135
88
  return entries.map((entry) => (
136
89
  <WorkspaceDirectoryTreeEntry
137
90
  key={entry.path}
91
+ browseParentRefresh={refresh}
92
+ busy={busy}
93
+ collapseVersion={collapseVersion}
94
+ createTarget={createTarget}
138
95
  entry={entry}
139
- level={level + 1}
140
- onAddFileToChat={onAddFileToChat}
96
+ level={showRoot ? 1 : 0}
97
+ onAddToChat={onAddToChat}
98
+ onCreateFile={mutable ? onCreateFile : undefined}
99
+ onCreateFolder={mutable ? onCreateFolder : undefined}
100
+ onDelete={mutable ? onDelete : undefined}
141
101
  onFileOpen={onFileOpen}
102
+ onRevealComplete={onRevealComplete}
103
+ onRename={onRename}
142
104
  onRevealPath={onRevealPath}
143
- relativePath={
144
- parentRelativePath
145
- ? `${parentRelativePath}/${entry.name}`
146
- : null
147
- }
105
+ onSelect={onSelect}
106
+ onUpload={mutable ? onUpload : undefined}
107
+ parentCreateTarget={fallbackParentTarget}
108
+ relativePath={relativePaths ? entry.name : null}
109
+ renderCreateFolder={renderCreateFolder}
110
+ revealPath={revealPath}
111
+ selectedPath={selectedPath}
148
112
  />
149
113
  ));
150
114
  }
151
115
 
152
- function WorkspaceDirectoryTreeEntry({
153
- entry,
154
- level,
155
- onAddFileToChat,
156
- onFileOpen,
116
+ function buildRootContextMenuGroups({
117
+ busy,
118
+ onCollapseAll,
119
+ onCreateFile,
120
+ onCreateFolder,
121
+ onRefresh,
157
122
  onRevealPath,
158
- relativePath,
123
+ onUpload,
124
+ target,
159
125
  }: {
160
- entry: ServerPathEntryView;
161
- level: number;
162
- onAddFileToChat?: (entry: ServerPathEntryView, relativePath: string) => void;
163
- onFileOpen: (action: ChatFileOpenActionViewModel) => void;
126
+ busy: boolean;
127
+ onCollapseAll: () => void;
128
+ onCreateFile: () => void;
129
+ onCreateFolder: () => void;
130
+ onRefresh: () => void;
164
131
  onRevealPath?: (path: string) => void;
165
- relativePath: string | null;
166
- }) {
167
- const isDirectory = entry.kind === "directory";
168
- const [isExpanded, setIsExpanded] = useState(false);
169
- const browseQuery = useServerPathBrowse({
170
- path: entry.path,
171
- includeFiles: true,
172
- enabled: isDirectory && isExpanded,
173
- });
174
- const contextMenuGroups: ContextMenuGroup[] = [
132
+ onUpload: () => void;
133
+ target: WorkspaceDirectoryActionTarget;
134
+ }): ContextMenuGroup[] {
135
+ return [
175
136
  {
176
- key: "primary",
137
+ key: 'manage',
177
138
  items: [
178
- ...(entry.kind === "file" && relativePath && onAddFileToChat
179
- ? [{
180
- key: "add-to-chat",
181
- label: t("chatWorkspaceAddToChat"),
182
- icon: <MessageSquarePlus className="h-4 w-4" />,
183
- restoreFocus: false,
184
- onSelect: () => onAddFileToChat(entry, relativePath),
185
- }]
186
- : []),
187
- ...(onRevealPath
188
- ? [{
189
- key: "reveal",
190
- label: t("chatWorkspaceRevealInFileManager"),
191
- icon: <LocateFixed className="h-4 w-4" />,
192
- onSelect: () => onRevealPath(entry.path),
193
- }]
194
- : []),
139
+ {
140
+ key: 'new-file',
141
+ label: t('chatWorkspaceNewFile'),
142
+ icon: <FilePlus2 className="h-4 w-4" />,
143
+ disabled: busy,
144
+ restoreFocus: false,
145
+ onSelect: onCreateFile,
146
+ },
147
+ {
148
+ key: 'new-folder',
149
+ label: t('chatWorkspaceNewFolder'),
150
+ icon: <FolderPlus className="h-4 w-4" />,
151
+ disabled: busy,
152
+ restoreFocus: false,
153
+ onSelect: onCreateFolder,
154
+ },
155
+ {
156
+ key: 'upload',
157
+ label: t('chatWorkspaceUploadFilesHere'),
158
+ icon: <Upload className="h-4 w-4" />,
159
+ disabled: busy,
160
+ restoreFocus: false,
161
+ onSelect: onUpload,
162
+ },
195
163
  ],
196
164
  },
197
165
  {
198
- key: "copy",
166
+ key: 'view',
199
167
  items: [
200
168
  {
201
- key: "copy-path",
202
- label: t("chatWorkspaceCopyPath"),
203
- icon: <Copy className="h-4 w-4" />,
204
- onSelect: () => void copyWorkspacePath(entry.path),
169
+ key: 'refresh',
170
+ label: t('chatWorkspaceRefreshExplorer'),
171
+ icon: <RefreshCw className="h-4 w-4" />,
172
+ onSelect: onRefresh,
205
173
  },
206
- ...(relativePath
207
- ? [{
208
- key: "copy-relative-path",
209
- label: t("chatWorkspaceCopyRelativePath"),
210
- icon: <Copy className="h-4 w-4" />,
211
- onSelect: () => void copyWorkspacePath(relativePath),
212
- }]
174
+ {
175
+ key: 'collapse',
176
+ label: t('chatWorkspaceCollapseAll'),
177
+ icon: <ChevronsUp className="h-4 w-4" />,
178
+ onSelect: onCollapseAll,
179
+ },
180
+ ],
181
+ },
182
+ {
183
+ key: 'path',
184
+ items: [
185
+ ...(onRevealPath
186
+ ? [
187
+ {
188
+ key: 'reveal',
189
+ label: t('chatWorkspaceRevealInFileManager'),
190
+ icon: <LocateFixed className="h-4 w-4" />,
191
+ onSelect: () => onRevealPath(target.path),
192
+ },
193
+ ]
213
194
  : []),
195
+ {
196
+ key: 'copy-path',
197
+ label: t('chatWorkspaceCopyPath'),
198
+ icon: <Copy className="h-4 w-4" />,
199
+ onSelect: () => void copyWorkspacePath(target.path),
200
+ },
214
201
  ],
215
202
  },
216
203
  ];
217
- const activateEntry = () => {
218
- if (isDirectory) {
219
- setIsExpanded((value) => !value);
220
- return;
221
- }
222
- onFileOpen({
223
- path: entry.path,
224
- label: entry.name,
225
- viewMode: "preview",
226
- });
227
- };
204
+ }
228
205
 
206
+ function WorkspaceProjectTree({
207
+ children,
208
+ currentPath,
209
+ onBlankContext,
210
+ onContextMenu,
211
+ rootDraft,
212
+ showRoot,
213
+ }: {
214
+ children: ReactNode;
215
+ currentPath: string | null;
216
+ onBlankContext: () => void;
217
+ onContextMenu?: MouseEventHandler<HTMLDivElement>;
218
+ rootDraft: ReactNode;
219
+ showRoot: boolean;
220
+ }) {
229
221
  return (
230
222
  <div
231
- role="treeitem"
232
- aria-expanded={isDirectory ? isExpanded : undefined}
233
- aria-label={buildDirectoryEntryLabel(entry)}
223
+ data-testid="workspace-directory-browser"
224
+ role="tree"
225
+ tabIndex={-1}
226
+ aria-label={t('chatWorkspaceProjectFiles')}
227
+ className="min-h-0 flex-1 overflow-auto py-1 custom-scrollbar"
228
+ onContextMenu={(event) => {
229
+ if (!(event.target as HTMLElement).closest('[data-workspace-tree-entry]')) onBlankContext();
230
+ onContextMenu?.(event);
231
+ }}
234
232
  >
235
- <ContextMenu
236
- groups={contextMenuGroups}
237
- label={t("chatWorkspaceFileContextMenu")}
238
- >
239
- <button
240
- type="button"
241
- className={cn(
242
- "flex h-7 w-full min-w-0 items-center gap-1.5 pr-2 text-left text-[13px] text-gray-700 transition-colors hover:bg-gray-100 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-border",
243
- entry.hidden ? "opacity-65" : null,
244
- )}
245
- style={{ paddingLeft: `${8 + level * 14}px` }}
246
- onClick={activateEntry}
247
- >
248
- <span className="inline-flex h-4 w-4 shrink-0 items-center justify-center text-gray-400">
249
- {isDirectory ? (
250
- isExpanded ? (
251
- <ChevronDown className="h-3.5 w-3.5" />
252
- ) : (
253
- <ChevronRight className="h-3.5 w-3.5" />
254
- )
255
- ) : null}
256
- </span>
257
- <WorkspaceDirectoryEntryIcon
258
- fileName={entry.name}
259
- isDirectory={isDirectory}
260
- isExpanded={isExpanded}
261
- />
262
- <span className="min-w-0 flex-1 truncate">{entry.name}</span>
263
- </button>
264
- </ContextMenu>
233
+ {showRoot && currentPath ? (
234
+ <>
235
+ {rootDraft}
236
+ {children}
237
+ </>
238
+ ) : (
239
+ children
240
+ )}
241
+ </div>
242
+ );
243
+ }
265
244
 
266
- {isDirectory && isExpanded ? (
267
- <div role="group">
268
- <WorkspaceDirectoryTreeChildren
269
- browseQuery={browseQuery}
270
- level={level}
271
- onAddFileToChat={onAddFileToChat}
272
- onFileOpen={onFileOpen}
273
- onRevealPath={onRevealPath}
274
- parentRelativePath={relativePath}
275
- />
276
- </div>
245
+ function WorkspaceDirectoryBrowserReady({
246
+ browseQuery,
247
+ ConfirmDialog,
248
+ controller,
249
+ entries,
250
+ onAddToChat,
251
+ onFileOpen,
252
+ onRevealPath,
253
+ rootLabel,
254
+ rootPath,
255
+ showRoot,
256
+ }: {
257
+ browseQuery: ReturnType<typeof useServerPathBrowse>;
258
+ ConfirmDialog: () => ReactNode;
259
+ controller: ReturnType<typeof useWorkspaceProjectFilesController>;
260
+ entries: readonly ServerPathEntryView[];
261
+ onAddToChat?: (entry: ServerPathEntryView, relativePath: string) => void;
262
+ onFileOpen: (action: ChatFileOpenActionViewModel) => void;
263
+ onRevealPath?: (path: string) => void;
264
+ rootLabel: string;
265
+ rootPath: string | null;
266
+ showRoot: boolean;
267
+ }) {
268
+ const {
269
+ actions,
270
+ cancelCreateFolder,
271
+ clearSelection,
272
+ completeReveal,
273
+ submitCreate,
274
+ createError,
275
+ createKind,
276
+ createName,
277
+ createTarget,
278
+ deleteEntry,
279
+ fileInputRef,
280
+ openCreateFile,
281
+ openCreateFolder,
282
+ requestUpload,
283
+ renameEntry,
284
+ revealPath,
285
+ selectEntry,
286
+ selectedCreateTarget,
287
+ selectedPath,
288
+ setCreateError,
289
+ setCreateName,
290
+ uploadFiles,
291
+ } = controller;
292
+ const busy = actions.pendingAction !== null;
293
+ const [collapseVersion, setCollapseVersion] = useState(0);
294
+ const rootRefresh = () => browseQuery.refetch();
295
+ const rootTarget: WorkspaceDirectoryActionTarget | null = rootPath
296
+ ? { label: rootLabel, path: rootPath, refresh: rootRefresh }
297
+ : null;
298
+ const toolbarTarget = selectedCreateTarget ?? rootTarget;
299
+ const renderCreateFolder = (level: number) => (
300
+ <WorkspaceNewFolderTreeItem
301
+ error={createError}
302
+ kind={createKind}
303
+ isCreating={actions.pendingAction === 'create-directory' || actions.pendingAction === 'create-file'}
304
+ level={level}
305
+ name={createName}
306
+ onCancel={cancelCreateFolder}
307
+ onNameChange={(name) => {
308
+ setCreateName(name);
309
+ setCreateError(null);
310
+ }}
311
+ onSubmit={() => void submitCreate()}
312
+ />
313
+ );
314
+ const fallbackParentTarget = rootTarget ?? {
315
+ label: '',
316
+ path: browseQuery.data?.currentPath ?? '',
317
+ refresh: rootRefresh,
318
+ };
319
+ const treeEntries = (
320
+ <WorkspaceDirectoryEntries
321
+ busy={busy}
322
+ collapseVersion={collapseVersion}
323
+ createTarget={createTarget}
324
+ entries={entries}
325
+ fallbackParentTarget={fallbackParentTarget}
326
+ mutable={Boolean(rootPath)}
327
+ onAddToChat={onAddToChat}
328
+ onCreateFile={openCreateFile}
329
+ onCreateFolder={openCreateFolder}
330
+ onDelete={(target, refresh) => void deleteEntry(target, refresh)}
331
+ onFileOpen={onFileOpen}
332
+ onRevealComplete={completeReveal}
333
+ onRename={renameEntry}
334
+ onRevealPath={onRevealPath}
335
+ onSelect={selectEntry}
336
+ onUpload={requestUpload}
337
+ refresh={rootRefresh}
338
+ relativePaths={Boolean(onAddToChat)}
339
+ renderCreateFolder={renderCreateFolder}
340
+ revealPath={revealPath}
341
+ selectedPath={selectedPath}
342
+ showRoot={false}
343
+ />
344
+ );
345
+ const tree = (
346
+ <WorkspaceProjectTree
347
+ currentPath={rootPath}
348
+ onBlankContext={clearSelection}
349
+ rootDraft={createTarget?.path === rootPath ? renderCreateFolder(0) : null}
350
+ showRoot={showRoot}
351
+ >
352
+ {treeEntries}
353
+ </WorkspaceProjectTree>
354
+ );
355
+ const rootMenuGroups = rootTarget
356
+ ? buildRootContextMenuGroups({
357
+ busy,
358
+ onCollapseAll: () => setCollapseVersion((value) => value + 1),
359
+ onCreateFile: () => openCreateFile(rootTarget),
360
+ onCreateFolder: () => openCreateFolder(rootTarget),
361
+ onRefresh: () => void browseQuery.refetch(),
362
+ onRevealPath,
363
+ onUpload: () => requestUpload(rootTarget),
364
+ target: rootTarget,
365
+ })
366
+ : [];
367
+
368
+ const content = (
369
+ <div
370
+ className="flex h-full min-h-0 flex-col bg-white"
371
+ onContextMenu={(event) => {
372
+ if (!(event.target as HTMLElement).closest('[data-workspace-tree-entry]')) clearSelection();
373
+ }}
374
+ >
375
+ {rootPath && toolbarTarget ? (
376
+ <WorkspaceProjectFilesToolbar
377
+ disabled={busy}
378
+ rootLabel={rootLabel}
379
+ onCollapseAll={() => setCollapseVersion((value) => value + 1)}
380
+ onNewFile={() => openCreateFile(toolbarTarget)}
381
+ onNewFolder={() => openCreateFolder(toolbarTarget)}
382
+ onRefresh={() => void browseQuery.refetch()}
383
+ />
384
+ ) : null}
385
+ {rootPath ? (
386
+ <input
387
+ ref={fileInputRef}
388
+ type="file"
389
+ multiple
390
+ className="sr-only"
391
+ aria-label={t('chatWorkspaceUploadFiles')}
392
+ onChange={(event) => void uploadFiles(Array.from(event.currentTarget.files ?? []))}
393
+ />
277
394
  ) : null}
395
+ {tree}
396
+ <ConfirmDialog />
278
397
  </div>
279
398
  );
399
+ return rootTarget ? (
400
+ <ContextMenu groups={rootMenuGroups} label={t('chatWorkspaceRootContextMenu')}>
401
+ {content}
402
+ </ContextMenu>
403
+ ) : (
404
+ content
405
+ );
280
406
  }
281
407
 
282
408
  export function ChatSessionWorkspaceDirectoryBrowser({
@@ -287,85 +413,60 @@ export function ChatSessionWorkspaceDirectoryBrowser({
287
413
  showRoot = false,
288
414
  }: ChatSessionWorkspaceDirectoryBrowserProps) {
289
415
  const presenter = usePresenter();
416
+ const { confirm, ConfirmDialog } = useConfirmDialog();
290
417
  const entries = browseQuery.data?.entries ?? [];
291
418
  const errorMessage = readBrowseError(browseQuery);
419
+ const rootPath = showRoot ? (browseQuery.data?.currentPath ?? null) : null;
420
+ const rootLabel = rootPath ? readPathName(rootPath) : '';
421
+ const controller = useWorkspaceProjectFilesController({
422
+ confirm,
423
+ onEntryDeleted: presenter.chatThreadManager.removeWorkspacePath,
424
+ onEntryRenamed: presenter.chatThreadManager.renameWorkspacePath,
425
+ rootPath,
426
+ });
292
427
  const onRevealPath = hostCapabilityManager.canRevealPath()
293
428
  ? (path: string) => {
294
429
  void hostCapabilityManager.revealPath(path).then((result) => {
295
- if (!result.revealed) {
296
- toast.error(t("chatWorkspaceRevealPathFailed"));
297
- }
298
- });
299
- }
300
- : undefined;
301
- const onAddFileToChat = sessionKey !== undefined
302
- ? (entry: ServerPathEntryView, relativePath: string) => {
303
- presenter.chatComposerIntentManager.requestFileReference({
304
- targetSessionKey: sessionKey,
305
- tokenKey: relativePath,
306
- label: entry.name,
430
+ if (!result.revealed) toast.error(t('chatWorkspaceRevealPathFailed'));
307
431
  });
308
432
  }
309
433
  : undefined;
434
+ const onAddToChat =
435
+ sessionKey !== undefined
436
+ ? (entry: ServerPathEntryView, relativePath: string) => {
437
+ const request =
438
+ entry.kind === 'directory'
439
+ ? presenter.chatComposerIntentManager.requestDirectoryReference
440
+ : presenter.chatComposerIntentManager.requestFileReference;
441
+ request({
442
+ targetSessionKey: sessionKey,
443
+ tokenKey: relativePath,
444
+ label: entry.name,
445
+ });
446
+ }
447
+ : undefined;
310
448
 
311
449
  if (browseQuery.isLoading && !browseQuery.data) {
312
- return renderStatus({ text: t("chatWorkspaceLoadingDirectory") });
450
+ return renderStatus({ text: t('chatWorkspaceLoadingDirectory') });
313
451
  }
314
-
315
452
  if (errorMessage && !browseQuery.data) {
316
453
  return renderStatus({
317
- tone: "error",
318
- text: `${t("chatWorkspaceDirectoryLoadFailed")}: ${errorMessage}`,
454
+ tone: 'error',
455
+ text: `${t('chatWorkspaceDirectoryLoadFailed')}: ${errorMessage}`,
319
456
  });
320
457
  }
321
-
322
- if (entries.length === 0) {
323
- return renderStatus({ text: t("chatWorkspaceDirectoryEmpty") });
324
- }
325
-
326
458
  return (
327
- <div
328
- data-testid="workspace-directory-browser"
329
- role="tree"
330
- aria-label={t("chatWorkspaceProjectFiles")}
331
- className="h-full overflow-auto bg-white py-2 custom-scrollbar"
332
- >
333
- {showRoot && browseQuery.data ? (
334
- <div role="treeitem" aria-expanded="true">
335
- <div className="flex h-8 items-center gap-1.5 border-b border-gray-100 px-2 text-xs font-semibold text-gray-700">
336
- <ChevronDown className="h-3.5 w-3.5 shrink-0 text-gray-400" />
337
- <FolderOpen className="h-4 w-4 shrink-0 text-amber-500" />
338
- <span className="min-w-0 flex-1 truncate" title={browseQuery.data.currentPath}>
339
- {readPathName(browseQuery.data.currentPath)}
340
- </span>
341
- </div>
342
- <div role="group" className="pt-1">
343
- {entries.map((entry) => (
344
- <WorkspaceDirectoryTreeEntry
345
- key={entry.path}
346
- entry={entry}
347
- level={1}
348
- onAddFileToChat={onAddFileToChat}
349
- onFileOpen={onFileOpen}
350
- onRevealPath={onRevealPath}
351
- relativePath={onAddFileToChat ? entry.name : null}
352
- />
353
- ))}
354
- </div>
355
- </div>
356
- ) : (
357
- entries.map((entry) => (
358
- <WorkspaceDirectoryTreeEntry
359
- key={entry.path}
360
- entry={entry}
361
- level={0}
362
- onAddFileToChat={onAddFileToChat}
363
- onFileOpen={onFileOpen}
364
- onRevealPath={onRevealPath}
365
- relativePath={onAddFileToChat ? entry.name : null}
366
- />
367
- ))
368
- )}
369
- </div>
459
+ <WorkspaceDirectoryBrowserReady
460
+ browseQuery={browseQuery}
461
+ ConfirmDialog={ConfirmDialog}
462
+ controller={controller}
463
+ entries={entries}
464
+ onAddToChat={onAddToChat}
465
+ onFileOpen={onFileOpen}
466
+ onRevealPath={onRevealPath}
467
+ rootLabel={rootLabel}
468
+ rootPath={rootPath}
469
+ showRoot={showRoot}
470
+ />
370
471
  );
371
472
  }