From fcb678773ce4851b1b2d2b0eec0762f4fc79a659 Mon Sep 17 00:00:00 2001 From: "tao.chen" <93983997+taochen-ct@users.noreply.github.com> Date: Fri, 3 Jul 2026 17:17:55 +0800 Subject: [PATCH] feat(web): suppress browser ctrl+s and add file tree CRUD Co-Authored-By: Claude --- web/src/components/editor/EditorPanel.tsx | 14 ++ .../workspace/FileExplorerPanel.tsx | 195 ++++++++++++++++-- web/src/lib/api/files.ts | 101 +++++++++ 3 files changed, 292 insertions(+), 18 deletions(-) diff --git a/web/src/components/editor/EditorPanel.tsx b/web/src/components/editor/EditorPanel.tsx index 4aa227f..a3f108e 100644 --- a/web/src/components/editor/EditorPanel.tsx +++ b/web/src/components/editor/EditorPanel.tsx @@ -42,6 +42,20 @@ export function EditorPanel({ workspaceId, path, language }: EditorPanelProps) { workspaceIdRef.current = workspaceId; }, [buffer, savedContent, path, workspaceId]); + // Suppress the browser's Save Page dialog on Cmd/Ctrl+S when a file is open. + // Monaco's own keybinding still handles saving. + useEffect(() => { + if (!path) return; + const handler = (e: KeyboardEvent) => { + if ((e.key === "s" || e.key === "S") && (e.ctrlKey || e.metaKey)) { + e.preventDefault(); + e.stopPropagation(); + } + }; + window.addEventListener("keydown", handler); + return () => window.removeEventListener("keydown", handler); + }, [path]); + // Wire Cmd/Ctrl+S to save the current buffer. const handleMount: OnMount = (editor, monaco) => { editor.addCommand( diff --git a/web/src/components/workspace/FileExplorerPanel.tsx b/web/src/components/workspace/FileExplorerPanel.tsx index 86e4090..11c936f 100644 --- a/web/src/components/workspace/FileExplorerPanel.tsx +++ b/web/src/components/workspace/FileExplorerPanel.tsx @@ -1,9 +1,25 @@ import { useQueryClient, useIsFetching } from "@tanstack/react-query"; -import { ChevronRight, FileCode2, Folder, RefreshCw } from "lucide-react"; +import { + ChevronRight, + FileCode2, + FilePlus, + Folder, + FolderPlus, + Pencil, + RefreshCw, + Trash2, +} from "lucide-react"; import type { ReactNode } from "react"; import { ScrollArea } from "@/components/ui/scroll-area"; -import { useFileList, type FileEntry } from "@/lib/api/files"; +import { + useFileList, + useFileMkdir, + useFileRemove, + useFileRename, + useFileWrite, + type FileEntry, +} from "@/lib/api/files"; import { useFileTreeStore } from "@/lib/store/file-tree-store"; import { useWorkspaceUiStore } from "@/lib/store/workspace-ui-store"; import { cn } from "@/lib/utils"; @@ -12,31 +28,42 @@ interface FileExplorerPanelProps { workspaceId: string | null; } + interface FileTreeDirectoryProps { workspaceId: string; entry: FileEntry; depth: number; onSelectFile: (path: string) => void; + onRename: (entry: FileEntry) => void; + onDelete: (entry: FileEntry) => void; } interface FileTreeFileProps { entry: FileEntry; depth: number; onSelectFile: (path: string) => void; + onRename: (entry: FileEntry) => void; + onDelete: (entry: FileEntry) => void; } -function FileTreeFile({ entry, depth, onSelectFile }: FileTreeFileProps) { +function FileTreeFile({ + entry, + depth, + onSelectFile, + onRename, + onDelete, +}: FileTreeFileProps) { const selectedPath = useFileTreeStore((s) => s.selectedPath); const isSelected = selectedPath === entry.path; const paddingLeft = `${depth * 0.875 + 0.75}rem`; return ( -
  • +
  • +
    + + +
  • ); } @@ -54,6 +105,8 @@ function FileTreeDirectory({ entry, depth, onSelectFile, + onRename, + onDelete, }: FileTreeDirectoryProps) { const expanded = useFileTreeStore((s) => s.expandedPaths.has(entry.path)); const toggleExpanded = useFileTreeStore((s) => s.toggleExpanded); @@ -65,12 +118,12 @@ function FileTreeDirectory({ const paddingLeft = `${depth * 0.875 + 0.75}rem`; return ( -
  • +
  • +
    + + +
    {expanded && (