diff --git a/web/src/components/workspace/FileExplorerPanel.tsx b/web/src/components/workspace/FileExplorerPanel.tsx
index 11c936f..5af9502 100644
--- a/web/src/components/workspace/FileExplorerPanel.tsx
+++ b/web/src/components/workspace/FileExplorerPanel.tsx
@@ -5,11 +5,16 @@ import {
FilePlus,
Folder,
FolderPlus,
- Pencil,
RefreshCw,
- Trash2,
} from "lucide-react";
-import type { ReactNode } from "react";
+import {
+ useEffect,
+ useLayoutEffect,
+ useMemo,
+ useRef,
+ useState,
+ type ReactNode,
+} from "react";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
@@ -28,42 +33,48 @@ interface FileExplorerPanelProps {
workspaceId: string | null;
}
+type OnRowContextMenu = (
+ entry: FileEntry,
+ point: { x: number; y: number },
+) => void;
interface FileTreeDirectoryProps {
workspaceId: string;
entry: FileEntry;
depth: number;
onSelectFile: (path: string) => void;
- onRename: (entry: FileEntry) => void;
- onDelete: (entry: FileEntry) => void;
+ onContextMenu: OnRowContextMenu;
}
interface FileTreeFileProps {
entry: FileEntry;
depth: number;
onSelectFile: (path: string) => void;
- onRename: (entry: FileEntry) => void;
- onDelete: (entry: FileEntry) => void;
+ onContextMenu: OnRowContextMenu;
}
function FileTreeFile({
entry,
depth,
onSelectFile,
- onRename,
- onDelete,
+ onContextMenu,
}: FileTreeFileProps) {
const selectedPath = useFileTreeStore((s) => s.selectedPath);
const isSelected = selectedPath === entry.path;
const paddingLeft = `${depth * 0.875 + 0.75}rem`;
return (
-
+
-
-
-
-
);
}
@@ -105,8 +92,7 @@ function FileTreeDirectory({
entry,
depth,
onSelectFile,
- onRename,
- onDelete,
+ onContextMenu,
}: FileTreeDirectoryProps) {
const expanded = useFileTreeStore((s) => s.expandedPaths.has(entry.path));
const toggleExpanded = useFileTreeStore((s) => s.toggleExpanded);
@@ -118,12 +104,17 @@ function FileTreeDirectory({
const paddingLeft = `${depth * 0.875 + 0.75}rem`;
return (
-
+
-
-
-
-
{expanded && (
{isLoading && (
@@ -182,8 +149,7 @@ function FileTreeDirectory({
entry={child}
depth={depth + 1}
onSelectFile={onSelectFile}
- onRename={onRename}
- onDelete={onDelete}
+ onContextMenu={onContextMenu}
/>
) : (
),
)}
@@ -223,6 +188,23 @@ export function FileExplorerPanel({ workspaceId }: FileExplorerPanelProps) {
const removeMutation = useFileRemove();
const renameMutation = useFileRename();
+ const [contextMenu, setContextMenu] = useState<{
+ entry: FileEntry;
+ x: number;
+ y: number;
+ } | null>(null);
+ const [adjustedPos, setAdjustedPos] = useState<{
+ x: number;
+ y: number;
+ } | null>(null);
+ const menuRef = useRef(null);
+ const hasMeasuredRef = useRef(false);
+
+ const menuPos = useMemo(
+ () => adjustedPos ?? (contextMenu ? { x: contextMenu.x, y: contextMenu.y } : null),
+ [adjustedPos, contextMenu],
+ );
+
const onSelectFile = (path: string) => {
setSelectedPath(path);
setActiveFilePath(path);
@@ -266,6 +248,46 @@ export function FileExplorerPanel({ workspaceId }: FileExplorerPanelProps) {
removeMutation.mutate({ workspaceId, path: entry.path });
};
+ const handleRowContextMenu: OnRowContextMenu = (entry, point) => {
+ setContextMenu({ entry, x: point.x, y: point.y });
+ };
+
+ useEffect(() => {
+ setAdjustedPos(null);
+ hasMeasuredRef.current = false;
+ }, [contextMenu]);
+
+ useLayoutEffect(() => {
+ if (!menuRef.current || !menuPos || hasMeasuredRef.current) return;
+ const rect = menuRef.current.getBoundingClientRect();
+ const nextX = Math.min(menuPos.x, window.innerWidth - rect.width - 8);
+ const nextY = Math.min(menuPos.y, window.innerHeight - rect.height - 8);
+ hasMeasuredRef.current = true;
+ if (nextX !== menuPos.x || nextY !== menuPos.y) {
+ setAdjustedPos({ x: nextX, y: nextY });
+ }
+ }, [menuPos]);
+
+ useEffect(() => {
+ if (!contextMenu) return;
+ const handleMouseDown = (e: MouseEvent) => {
+ if (!menuRef.current?.contains(e.target as Node)) {
+ setContextMenu(null);
+ }
+ };
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if (e.key === "Escape") {
+ setContextMenu(null);
+ }
+ };
+ document.addEventListener("mousedown", handleMouseDown);
+ document.addEventListener("keydown", handleKeyDown);
+ return () => {
+ document.removeEventListener("mousedown", handleMouseDown);
+ document.removeEventListener("keydown", handleKeyDown);
+ };
+ }, [contextMenu]);
+
const lastError = [
writeMutation,
mkdirMutation,
@@ -355,8 +377,7 @@ export function FileExplorerPanel({ workspaceId }: FileExplorerPanelProps) {
entry={entry}
depth={0}
onSelectFile={onSelectFile}
- onRename={handleRename}
- onDelete={handleDelete}
+ onContextMenu={handleRowContextMenu}
/>
) : (
),
)}
+ {contextMenu && menuPos && (
+
+
+
+ {contextMenu.entry.isDir && (
+ <>
+
+
+
+ >
+ )}
+
+ )}
);
}