diff --git a/app/components/workbench/FileTree.tsx b/app/components/workbench/FileTree.tsx
index 0882e1f43..eb185cbba 100644
--- a/app/components/workbench/FileTree.tsx
+++ b/app/components/workbench/FileTree.tsx
@@ -2,6 +2,7 @@ import { memo, useEffect, useMemo, useState, type ReactNode } from 'react';
import type { FileMap } from '~/lib/stores/files';
import { classNames } from '~/utils/classNames';
import { createScopedLogger, renderLogger } from '~/utils/logger';
+import * as ContextMenu from '@radix-ui/react-context-menu';
const logger = createScopedLogger('FileTree');
@@ -110,6 +111,22 @@ export const FileTree = memo(
});
};
+ const onCopyPath = (fileOrFolder: FileNode | FolderNode) => {
+ try {
+ navigator.clipboard.writeText(fileOrFolder.fullPath);
+ } catch (error) {
+ logger.error(error);
+ }
+ };
+
+ const onCopyRelativePath = (fileOrFolder: FileNode | FolderNode) => {
+ try {
+ navigator.clipboard.writeText(fileOrFolder.fullPath.substring((rootFolder || '').length));
+ } catch (error) {
+ logger.error(error);
+ }
+ };
+
return (
{filteredFileList.map((fileOrFolder) => {
@@ -121,6 +138,12 @@ export const FileTree = memo(
selected={selectedFile === fileOrFolder.fullPath}
file={fileOrFolder}
unsavedChanges={unsavedFiles?.has(fileOrFolder.fullPath)}
+ onCopyPath={() => {
+ onCopyPath(fileOrFolder);
+ }}
+ onCopyRelativePath={() => {
+ onCopyRelativePath(fileOrFolder);
+ }}
onClick={() => {
onFileSelect?.(fileOrFolder.fullPath);
}}
@@ -134,6 +157,12 @@ export const FileTree = memo(
folder={fileOrFolder}
selected={allowFolderSelection && selectedFile === fileOrFolder.fullPath}
collapsed={collapsedFolders.has(fileOrFolder.fullPath)}
+ onCopyPath={() => {
+ onCopyPath(fileOrFolder);
+ }}
+ onCopyRelativePath={() => {
+ onCopyRelativePath(fileOrFolder);
+ }}
onClick={() => {
toggleCollapseState(fileOrFolder.fullPath);
}}
@@ -156,26 +185,67 @@ interface FolderProps {
folder: FolderNode;
collapsed: boolean;
selected?: boolean;
+ onCopyPath: () => void;
+ onCopyRelativePath: () => void;
onClick: () => void;
}
-function Folder({ folder: { depth, name }, collapsed, selected = false, onClick }: FolderProps) {
+interface FolderContextMenuProps {
+ onCopyPath?: () => void;
+ onCopyRelativePath?: () => void;
+ children: ReactNode;
+}
+
+function ContextMenuItem({ onSelect, children }: { onSelect?: () => void; children: ReactNode }) {
return (
-
- {name}
-
+
+
{children}
+
+ );
+}
+
+function FileContextMenu({ onCopyPath, onCopyRelativePath, children }: FolderContextMenuProps) {
+ return (
+
+ {children}
+
+
+
+ Copy path
+ Copy relative path
+
+
+
+
+ );
+}
+
+function Folder({ folder, collapsed, selected = false, onCopyPath, onCopyRelativePath, onClick }: FolderProps) {
+ return (
+
+
+ {folder.name}
+
+
);
}
@@ -183,31 +253,43 @@ interface FileProps {
file: FileNode;
selected: boolean;
unsavedChanges?: boolean;
+ onCopyPath: () => void;
+ onCopyRelativePath: () => void;
onClick: () => void;
}
-function File({ file: { depth, name }, onClick, selected, unsavedChanges = false }: FileProps) {
+function File({
+ file: { depth, name },
+ onClick,
+ onCopyPath,
+ onCopyRelativePath,
+ selected,
+ unsavedChanges = false,
+}: FileProps) {
return (
-
-
+
- {name}
- {unsavedChanges && }
-
-
+
+
{name}
+ {unsavedChanges &&
}
+
+
+
);
}
diff --git a/package.json b/package.json
index 7c8740cbf..878c44cdd 100644
--- a/package.json
+++ b/package.json
@@ -58,6 +58,7 @@
"@octokit/rest": "^21.0.2",
"@octokit/types": "^13.6.2",
"@openrouter/ai-sdk-provider": "^0.0.5",
+ "@radix-ui/react-context-menu": "^2.2.2",
"@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-dropdown-menu": "^2.1.2",
"@radix-ui/react-separator": "^1.1.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index e355d04ea..17bdc8999 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -95,6 +95,9 @@ importers:
'@openrouter/ai-sdk-provider':
specifier: ^0.0.5
version: 0.0.5(zod@3.23.8)
+ '@radix-ui/react-context-menu':
+ specifier: ^2.2.2
+ version: 2.2.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-dialog':
specifier: ^1.1.2
version: 1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -1557,6 +1560,19 @@ packages:
'@types/react':
optional: true
+ '@radix-ui/react-context-menu@2.2.2':
+ resolution: {integrity: sha512-99EatSTpW+hRYHt7m8wdDlLtkmTovEe8Z/hnxUPV+SKuuNL5HWNhQI4QSdjZqNSgXHay2z4M3Dym73j9p2Gx5Q==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
'@radix-ui/react-context@1.1.0':
resolution: {integrity: sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==}
peerDependencies:
@@ -7032,6 +7048,20 @@ snapshots:
optionalDependencies:
'@types/react': 18.3.12
+ '@radix-ui/react-context-menu@2.2.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.0
+ '@radix-ui/react-context': 1.1.1(@types/react@18.3.12)(react@18.3.1)
+ '@radix-ui/react-menu': 2.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.12)(react@18.3.1)
+ '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.12)(react@18.3.1)
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ optionalDependencies:
+ '@types/react': 18.3.12
+ '@types/react-dom': 18.3.1
+
'@radix-ui/react-context@1.1.0(@types/react@18.3.12)(react@18.3.1)':
dependencies:
react: 18.3.1