diff --git a/apps/server/src/wsServer.ts b/apps/server/src/wsServer.ts index 3ded330ad..367fc5d53 100644 --- a/apps/server/src/wsServer.ts +++ b/apps/server/src/wsServer.ts @@ -1035,6 +1035,24 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return< }; } + case WS_METHODS.projectsDeleteEntry: { + const body = stripRequestTag(request.body); + const target = yield* resolveWorkspaceWritePath({ + workspaceRoot: body.cwd, + relativePath: body.relativePath, + path, + }); + yield* fileSystem.remove(target.absolutePath, { recursive: true }).pipe( + Effect.mapError( + (cause) => + new RouteRequestError({ + message: `Failed to delete entry: ${String(cause)}`, + }), + ), + ); + return {}; + } + case WS_METHODS.shellOpenInEditor: { const body = stripRequestTag(request.body); return yield* openInEditor(body); diff --git a/apps/web/src/components/WorkspaceFileTree.tsx b/apps/web/src/components/WorkspaceFileTree.tsx index 16b396eb6..09828a804 100644 --- a/apps/web/src/components/WorkspaceFileTree.tsx +++ b/apps/web/src/components/WorkspaceFileTree.tsx @@ -1,5 +1,5 @@ import { type ProjectDirectoryEntry, type ProjectEntry } from "@okcode/contracts"; -import { useQuery } from "@tanstack/react-query"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; import { ChevronRightIcon, FolderClosedIcon, @@ -27,9 +27,9 @@ import { toastManager } from "./ui/toast"; const TREE_ROW_LEFT_PADDING = 8; const TREE_ROW_DEPTH_OFFSET = 14; -type WorkspaceFileAction = "open" | "open-in-editor" | "reveal-in-finder" | "copy-path"; -type WorkspaceDirectoryAction = "expand" | "collapse" | "open-in-finder" | "copy-path"; -type WorkspaceSearchDirectoryAction = "reveal-in-tree" | "open-in-finder" | "copy-path"; +type WorkspaceFileAction = "open" | "open-in-editor" | "reveal-in-finder" | "copy-path" | "delete"; +type WorkspaceDirectoryAction = "expand" | "collapse" | "open-in-finder" | "copy-path" | "delete"; +type WorkspaceSearchDirectoryAction = "reveal-in-tree" | "open-in-finder" | "copy-path" | "delete"; export const WorkspaceFileTree = memo(function WorkspaceFileTree(props: { cwd: string; @@ -205,6 +205,30 @@ export const WorkspaceFileTree = memo(function WorkspaceFileTree(props: { setFiltersOpen(false); }, []); + const queryClient = useQueryClient(); + const deleteEntry = useCallback( + async (pathValue: string) => { + const api = readNativeApi(); + if (!api) return; + const name = basenameOfPath(pathValue); + const confirmed = await api.dialog.confirm(`Are you sure you want to delete "${name}"?`); + if (!confirmed) return; + try { + await api.projects.deleteEntry({ cwd: props.cwd, relativePath: pathValue }); + toastManager.add({ type: "success", title: `Deleted ${name}` }); + void queryClient.invalidateQueries({ queryKey: ["projectListDirectory"] }); + void queryClient.invalidateQueries({ queryKey: ["projectSearchEntries"] }); + } catch (error) { + toastManager.add({ + type: "error", + title: "Failed to delete", + description: error instanceof Error ? error.message : "An error occurred.", + }); + } + }, + [props.cwd, queryClient], + ); + return (
@@ -273,6 +297,7 @@ export const WorkspaceFileTree = memo(function WorkspaceFileTree(props: { isLoading={searchResultsQuery.isLoading} fileManagerName={fileManagerName} onCopyPath={copyWorkspacePath} + onDeleteEntry={deleteEntry} onOpenDirectoryInFileManager={openDirectoryInFileManager} onOpenFileInEditor={openFileInNativeEditor} onOpenFile={openFile} @@ -288,6 +313,7 @@ export const WorkspaceFileTree = memo(function WorkspaceFileTree(props: { expandedDirectories={expandedDirectories} fileManagerName={fileManagerName} onCopyPath={copyWorkspacePath} + onDeleteEntry={deleteEntry} onOpenDirectoryInFileManager={openDirectoryInFileManager} onOpenFileInEditor={openFileInNativeEditor} onOpenFile={openFile} @@ -310,6 +336,7 @@ const WorkspaceSearchResults = memo(function WorkspaceSearchResults(props: { truncated: boolean; resolvedTheme: "light" | "dark"; onCopyPath: (pathValue: string) => void; + onDeleteEntry: (pathValue: string) => void; onOpenDirectoryInFileManager: (pathValue: string) => void; onOpenFileInEditor: (pathValue: string) => void; onOpenFile: (pathValue: string, event?: { metaKey?: boolean; ctrlKey?: boolean }) => void; @@ -344,6 +371,7 @@ const WorkspaceSearchResults = memo(function WorkspaceSearchResults(props: { entry={entry} fileManagerName={props.fileManagerName} onCopyPath={props.onCopyPath} + onDeleteEntry={props.onDeleteEntry} onOpenDirectoryInFileManager={props.onOpenDirectoryInFileManager} onOpenFileInEditor={props.onOpenFileInEditor} onOpenFile={props.onOpenFile} @@ -367,6 +395,7 @@ const WorkspaceSearchResultRow = memo(function WorkspaceSearchResultRow(props: { fileManagerName: string; resolvedTheme: "light" | "dark"; onCopyPath: (pathValue: string) => void; + onDeleteEntry: (pathValue: string) => void; onOpenDirectoryInFileManager: (pathValue: string) => void; onOpenFileInEditor: (pathValue: string) => void; onOpenFile: (pathValue: string, event?: { metaKey?: boolean; ctrlKey?: boolean }) => void; @@ -387,6 +416,7 @@ const WorkspaceSearchResultRow = memo(function WorkspaceSearchResultRow(props: { { id: "reveal-in-tree", label: "Reveal in tree" }, { id: "open-in-finder", label: `Open in ${props.fileManagerName}` }, { id: "copy-path", label: "Copy path" }, + { id: "delete", label: "Delete", destructive: true }, ], { x: event.clientX, y: event.clientY }, ); @@ -396,6 +426,8 @@ const WorkspaceSearchResultRow = memo(function WorkspaceSearchResultRow(props: { props.onOpenDirectoryInFileManager(props.entry.path); } else if (clicked === "copy-path") { props.onCopyPath(props.entry.path); + } else if (clicked === "delete") { + props.onDeleteEntry(props.entry.path); } return; } @@ -406,6 +438,7 @@ const WorkspaceSearchResultRow = memo(function WorkspaceSearchResultRow(props: { { id: "open-in-editor", label: "Open in editor" }, { id: "reveal-in-finder", label: `Reveal in ${props.fileManagerName}` }, { id: "copy-path", label: "Copy path" }, + { id: "delete", label: "Delete", destructive: true }, ], { x: event.clientX, y: event.clientY }, ); @@ -418,6 +451,8 @@ const WorkspaceSearchResultRow = memo(function WorkspaceSearchResultRow(props: { props.onRevealFileInFileManager(props.entry.path); } else if (clicked === "copy-path") { props.onCopyPath(props.entry.path); + } else if (clicked === "delete") { + props.onDeleteEntry(props.entry.path); } }, [isDirectory, props], @@ -468,6 +503,7 @@ const WorkspaceFileTreeDirectory = memo(function WorkspaceFileTreeDirectory(prop fileManagerName: string; resolvedTheme: "light" | "dark"; onCopyPath: (pathValue: string) => void; + onDeleteEntry: (pathValue: string) => void; onOpenDirectoryInFileManager: (pathValue: string) => void; onOpenFileInEditor: (pathValue: string) => void; onToggleDirectory: (pathValue: string) => void; @@ -521,6 +557,7 @@ const WorkspaceFileTreeDirectory = memo(function WorkspaceFileTreeDirectory(prop fileManagerName={props.fileManagerName} isExpanded={isExpanded} onCopyPath={props.onCopyPath} + onDeleteEntry={props.onDeleteEntry} onOpenDirectoryInFileManager={props.onOpenDirectoryInFileManager} onToggleDirectory={props.onToggleDirectory} /> @@ -532,6 +569,7 @@ const WorkspaceFileTreeDirectory = memo(function WorkspaceFileTreeDirectory(prop expandedDirectories={props.expandedDirectories} fileManagerName={props.fileManagerName} onCopyPath={props.onCopyPath} + onDeleteEntry={props.onDeleteEntry} onOpenDirectoryInFileManager={props.onOpenDirectoryInFileManager} onOpenFileInEditor={props.onOpenFileInEditor} onOpenFile={props.onOpenFile} @@ -551,6 +589,7 @@ const WorkspaceFileTreeDirectory = memo(function WorkspaceFileTreeDirectory(prop entry={entry} fileManagerName={props.fileManagerName} onCopyPath={props.onCopyPath} + onDeleteEntry={props.onDeleteEntry} onOpenFileInEditor={props.onOpenFileInEditor} onOpenFile={props.onOpenFile} onRevealFileInFileManager={props.onRevealFileInFileManager} @@ -573,6 +612,7 @@ const WorkspaceDirectoryRow = memo(function WorkspaceDirectoryRow(props: { fileManagerName: string; isExpanded: boolean; onCopyPath: (pathValue: string) => void; + onDeleteEntry: (pathValue: string) => void; onOpenDirectoryInFileManager: (pathValue: string) => void; onToggleDirectory: (pathValue: string) => void; }) { @@ -590,6 +630,7 @@ const WorkspaceDirectoryRow = memo(function WorkspaceDirectoryRow(props: { }, { id: "open-in-finder", label: `Open in ${props.fileManagerName}` }, { id: "copy-path", label: "Copy path" }, + { id: "delete", label: "Delete", destructive: true }, ], { x: event.clientX, y: event.clientY }, ); @@ -599,6 +640,8 @@ const WorkspaceDirectoryRow = memo(function WorkspaceDirectoryRow(props: { props.onOpenDirectoryInFileManager(props.entry.path); } else if (clicked === "copy-path") { props.onCopyPath(props.entry.path); + } else if (clicked === "delete") { + props.onDeleteEntry(props.entry.path); } }, [props], @@ -649,6 +692,7 @@ const WorkspaceFileRow = memo(function WorkspaceFileRow(props: { fileManagerName: string; resolvedTheme: "light" | "dark"; onCopyPath: (pathValue: string) => void; + onDeleteEntry: (pathValue: string) => void; onOpenFileInEditor: (pathValue: string) => void; onOpenFile: (pathValue: string, event?: { metaKey?: boolean; ctrlKey?: boolean }) => void; onRevealFileInFileManager: (pathValue: string) => void; @@ -665,6 +709,7 @@ const WorkspaceFileRow = memo(function WorkspaceFileRow(props: { { id: "open-in-editor", label: "Open in editor" }, { id: "reveal-in-finder", label: `Reveal in ${props.fileManagerName}` }, { id: "copy-path", label: "Copy path" }, + { id: "delete", label: "Delete", destructive: true }, ], { x: event.clientX, y: event.clientY }, ); @@ -676,6 +721,8 @@ const WorkspaceFileRow = memo(function WorkspaceFileRow(props: { props.onRevealFileInFileManager(props.entry.path); } else if (clicked === "copy-path") { props.onCopyPath(props.entry.path); + } else if (clicked === "delete") { + props.onDeleteEntry(props.entry.path); } }, [props], diff --git a/apps/web/src/wsNativeApi.ts b/apps/web/src/wsNativeApi.ts index c8aa17f32..9015e40b2 100644 --- a/apps/web/src/wsNativeApi.ts +++ b/apps/web/src/wsNativeApi.ts @@ -193,6 +193,7 @@ export function createWsNativeApi(): NativeApi { listDirectory: (input) => transport.request(WS_METHODS.projectsListDirectory, input), writeFile: (input) => transport.request(WS_METHODS.projectsWriteFile, input), readFile: (input) => transport.request(WS_METHODS.projectsReadFile, input), + deleteEntry: (input) => transport.request(WS_METHODS.projectsDeleteEntry, input), }, shell: { openInEditor: (cwd, editor) => diff --git a/packages/contracts/src/ipc.ts b/packages/contracts/src/ipc.ts index f0436fb10..93f3fcff1 100644 --- a/packages/contracts/src/ipc.ts +++ b/packages/contracts/src/ipc.ts @@ -24,6 +24,7 @@ import type { GitStatusResult, } from "./git"; import type { + ProjectDeleteEntryInput, ProjectListDirectoryInput, ProjectListDirectoryResult, ProjectReadFileInput, @@ -313,6 +314,7 @@ export interface NativeApi { listDirectory: (input: ProjectListDirectoryInput) => Promise; writeFile: (input: ProjectWriteFileInput) => Promise; readFile: (input: ProjectReadFileInput) => Promise; + deleteEntry: (input: ProjectDeleteEntryInput) => Promise; }; shell: { openInEditor: (cwd: string, editor: EditorId) => Promise; diff --git a/packages/contracts/src/project.ts b/packages/contracts/src/project.ts index c76991790..99f7c0f07 100644 --- a/packages/contracts/src/project.ts +++ b/packages/contracts/src/project.ts @@ -83,3 +83,9 @@ export const ProjectReadFileResult = Schema.Struct({ imageDataUrl: Schema.optional(Schema.String), }); export type ProjectReadFileResult = typeof ProjectReadFileResult.Type; + +export const ProjectDeleteEntryInput = Schema.Struct({ + cwd: TrimmedNonEmptyString, + relativePath: TrimmedNonEmptyString.check(Schema.isMaxLength(PROJECT_DIRECTORY_PATH_MAX_LENGTH)), +}); +export type ProjectDeleteEntryInput = typeof ProjectDeleteEntryInput.Type; diff --git a/packages/contracts/src/ws.ts b/packages/contracts/src/ws.ts index aa28f2bbe..3017f1c8c 100644 --- a/packages/contracts/src/ws.ts +++ b/packages/contracts/src/ws.ts @@ -58,6 +58,7 @@ import { SaveProjectEnvironmentVariablesInput, } from "./environment"; import { + ProjectDeleteEntryInput, ProjectListDirectoryInput, ProjectReadFileInput, ProjectSearchEntriesInput, @@ -93,6 +94,7 @@ export const WS_METHODS = { projectsListDirectory: "projects.listDirectory", projectsWriteFile: "projects.writeFile", projectsReadFile: "projects.readFile", + projectsDeleteEntry: "projects.deleteEntry", // Shell methods shellOpenInEditor: "shell.openInEditor", @@ -204,6 +206,7 @@ const WebSocketRequestBody = Schema.Union([ tagRequestBody(WS_METHODS.projectsListDirectory, ProjectListDirectoryInput), tagRequestBody(WS_METHODS.projectsWriteFile, ProjectWriteFileInput), tagRequestBody(WS_METHODS.projectsReadFile, ProjectReadFileInput), + tagRequestBody(WS_METHODS.projectsDeleteEntry, ProjectDeleteEntryInput), // Shell methods tagRequestBody(WS_METHODS.shellOpenInEditor, OpenInEditorInput),