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