Skip to content

Commit 910738b

Browse files
authored
Add right-click delete for workspace entries (#202)
- Add a workspace entry delete action over WebSocket - Wire context-menu delete into the file tree and search results - Invalidate directory and search queries after deletion
1 parent 7e8f4f6 commit 910738b

6 files changed

Lines changed: 81 additions & 4 deletions

File tree

apps/server/src/wsServer.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1035,6 +1035,24 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return<
10351035
};
10361036
}
10371037

1038+
case WS_METHODS.projectsDeleteEntry: {
1039+
const body = stripRequestTag(request.body);
1040+
const target = yield* resolveWorkspaceWritePath({
1041+
workspaceRoot: body.cwd,
1042+
relativePath: body.relativePath,
1043+
path,
1044+
});
1045+
yield* fileSystem.remove(target.absolutePath, { recursive: true }).pipe(
1046+
Effect.mapError(
1047+
(cause) =>
1048+
new RouteRequestError({
1049+
message: `Failed to delete entry: ${String(cause)}`,
1050+
}),
1051+
),
1052+
);
1053+
return {};
1054+
}
1055+
10381056
case WS_METHODS.shellOpenInEditor: {
10391057
const body = stripRequestTag(request.body);
10401058
return yield* openInEditor(body);

apps/web/src/components/WorkspaceFileTree.tsx

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { type ProjectDirectoryEntry, type ProjectEntry } from "@okcode/contracts";
2-
import { useQuery } from "@tanstack/react-query";
2+
import { useQuery, useQueryClient } from "@tanstack/react-query";
33
import {
44
ChevronRightIcon,
55
FolderClosedIcon,
@@ -27,9 +27,9 @@ import { toastManager } from "./ui/toast";
2727

2828
const TREE_ROW_LEFT_PADDING = 8;
2929
const TREE_ROW_DEPTH_OFFSET = 14;
30-
type WorkspaceFileAction = "open" | "open-in-editor" | "reveal-in-finder" | "copy-path";
31-
type WorkspaceDirectoryAction = "expand" | "collapse" | "open-in-finder" | "copy-path";
32-
type WorkspaceSearchDirectoryAction = "reveal-in-tree" | "open-in-finder" | "copy-path";
30+
type WorkspaceFileAction = "open" | "open-in-editor" | "reveal-in-finder" | "copy-path" | "delete";
31+
type WorkspaceDirectoryAction = "expand" | "collapse" | "open-in-finder" | "copy-path" | "delete";
32+
type WorkspaceSearchDirectoryAction = "reveal-in-tree" | "open-in-finder" | "copy-path" | "delete";
3333

3434
export const WorkspaceFileTree = memo(function WorkspaceFileTree(props: {
3535
cwd: string;
@@ -205,6 +205,30 @@ export const WorkspaceFileTree = memo(function WorkspaceFileTree(props: {
205205
setFiltersOpen(false);
206206
}, []);
207207

208+
const queryClient = useQueryClient();
209+
const deleteEntry = useCallback(
210+
async (pathValue: string) => {
211+
const api = readNativeApi();
212+
if (!api) return;
213+
const name = basenameOfPath(pathValue);
214+
const confirmed = await api.dialog.confirm(`Are you sure you want to delete "${name}"?`);
215+
if (!confirmed) return;
216+
try {
217+
await api.projects.deleteEntry({ cwd: props.cwd, relativePath: pathValue });
218+
toastManager.add({ type: "success", title: `Deleted ${name}` });
219+
void queryClient.invalidateQueries({ queryKey: ["projectListDirectory"] });
220+
void queryClient.invalidateQueries({ queryKey: ["projectSearchEntries"] });
221+
} catch (error) {
222+
toastManager.add({
223+
type: "error",
224+
title: "Failed to delete",
225+
description: error instanceof Error ? error.message : "An error occurred.",
226+
});
227+
}
228+
},
229+
[props.cwd, queryClient],
230+
);
231+
208232
return (
209233
<div className={cn("space-y-2", props.className)}>
210234
<div className="space-y-1.5 px-2">
@@ -273,6 +297,7 @@ export const WorkspaceFileTree = memo(function WorkspaceFileTree(props: {
273297
isLoading={searchResultsQuery.isLoading}
274298
fileManagerName={fileManagerName}
275299
onCopyPath={copyWorkspacePath}
300+
onDeleteEntry={deleteEntry}
276301
onOpenDirectoryInFileManager={openDirectoryInFileManager}
277302
onOpenFileInEditor={openFileInNativeEditor}
278303
onOpenFile={openFile}
@@ -288,6 +313,7 @@ export const WorkspaceFileTree = memo(function WorkspaceFileTree(props: {
288313
expandedDirectories={expandedDirectories}
289314
fileManagerName={fileManagerName}
290315
onCopyPath={copyWorkspacePath}
316+
onDeleteEntry={deleteEntry}
291317
onOpenDirectoryInFileManager={openDirectoryInFileManager}
292318
onOpenFileInEditor={openFileInNativeEditor}
293319
onOpenFile={openFile}
@@ -310,6 +336,7 @@ const WorkspaceSearchResults = memo(function WorkspaceSearchResults(props: {
310336
truncated: boolean;
311337
resolvedTheme: "light" | "dark";
312338
onCopyPath: (pathValue: string) => void;
339+
onDeleteEntry: (pathValue: string) => void;
313340
onOpenDirectoryInFileManager: (pathValue: string) => void;
314341
onOpenFileInEditor: (pathValue: string) => void;
315342
onOpenFile: (pathValue: string, event?: { metaKey?: boolean; ctrlKey?: boolean }) => void;
@@ -344,6 +371,7 @@ const WorkspaceSearchResults = memo(function WorkspaceSearchResults(props: {
344371
entry={entry}
345372
fileManagerName={props.fileManagerName}
346373
onCopyPath={props.onCopyPath}
374+
onDeleteEntry={props.onDeleteEntry}
347375
onOpenDirectoryInFileManager={props.onOpenDirectoryInFileManager}
348376
onOpenFileInEditor={props.onOpenFileInEditor}
349377
onOpenFile={props.onOpenFile}
@@ -367,6 +395,7 @@ const WorkspaceSearchResultRow = memo(function WorkspaceSearchResultRow(props: {
367395
fileManagerName: string;
368396
resolvedTheme: "light" | "dark";
369397
onCopyPath: (pathValue: string) => void;
398+
onDeleteEntry: (pathValue: string) => void;
370399
onOpenDirectoryInFileManager: (pathValue: string) => void;
371400
onOpenFileInEditor: (pathValue: string) => void;
372401
onOpenFile: (pathValue: string, event?: { metaKey?: boolean; ctrlKey?: boolean }) => void;
@@ -387,6 +416,7 @@ const WorkspaceSearchResultRow = memo(function WorkspaceSearchResultRow(props: {
387416
{ id: "reveal-in-tree", label: "Reveal in tree" },
388417
{ id: "open-in-finder", label: `Open in ${props.fileManagerName}` },
389418
{ id: "copy-path", label: "Copy path" },
419+
{ id: "delete", label: "Delete", destructive: true },
390420
],
391421
{ x: event.clientX, y: event.clientY },
392422
);
@@ -396,6 +426,8 @@ const WorkspaceSearchResultRow = memo(function WorkspaceSearchResultRow(props: {
396426
props.onOpenDirectoryInFileManager(props.entry.path);
397427
} else if (clicked === "copy-path") {
398428
props.onCopyPath(props.entry.path);
429+
} else if (clicked === "delete") {
430+
props.onDeleteEntry(props.entry.path);
399431
}
400432
return;
401433
}
@@ -406,6 +438,7 @@ const WorkspaceSearchResultRow = memo(function WorkspaceSearchResultRow(props: {
406438
{ id: "open-in-editor", label: "Open in editor" },
407439
{ id: "reveal-in-finder", label: `Reveal in ${props.fileManagerName}` },
408440
{ id: "copy-path", label: "Copy path" },
441+
{ id: "delete", label: "Delete", destructive: true },
409442
],
410443
{ x: event.clientX, y: event.clientY },
411444
);
@@ -418,6 +451,8 @@ const WorkspaceSearchResultRow = memo(function WorkspaceSearchResultRow(props: {
418451
props.onRevealFileInFileManager(props.entry.path);
419452
} else if (clicked === "copy-path") {
420453
props.onCopyPath(props.entry.path);
454+
} else if (clicked === "delete") {
455+
props.onDeleteEntry(props.entry.path);
421456
}
422457
},
423458
[isDirectory, props],
@@ -468,6 +503,7 @@ const WorkspaceFileTreeDirectory = memo(function WorkspaceFileTreeDirectory(prop
468503
fileManagerName: string;
469504
resolvedTheme: "light" | "dark";
470505
onCopyPath: (pathValue: string) => void;
506+
onDeleteEntry: (pathValue: string) => void;
471507
onOpenDirectoryInFileManager: (pathValue: string) => void;
472508
onOpenFileInEditor: (pathValue: string) => void;
473509
onToggleDirectory: (pathValue: string) => void;
@@ -521,6 +557,7 @@ const WorkspaceFileTreeDirectory = memo(function WorkspaceFileTreeDirectory(prop
521557
fileManagerName={props.fileManagerName}
522558
isExpanded={isExpanded}
523559
onCopyPath={props.onCopyPath}
560+
onDeleteEntry={props.onDeleteEntry}
524561
onOpenDirectoryInFileManager={props.onOpenDirectoryInFileManager}
525562
onToggleDirectory={props.onToggleDirectory}
526563
/>
@@ -532,6 +569,7 @@ const WorkspaceFileTreeDirectory = memo(function WorkspaceFileTreeDirectory(prop
532569
expandedDirectories={props.expandedDirectories}
533570
fileManagerName={props.fileManagerName}
534571
onCopyPath={props.onCopyPath}
572+
onDeleteEntry={props.onDeleteEntry}
535573
onOpenDirectoryInFileManager={props.onOpenDirectoryInFileManager}
536574
onOpenFileInEditor={props.onOpenFileInEditor}
537575
onOpenFile={props.onOpenFile}
@@ -551,6 +589,7 @@ const WorkspaceFileTreeDirectory = memo(function WorkspaceFileTreeDirectory(prop
551589
entry={entry}
552590
fileManagerName={props.fileManagerName}
553591
onCopyPath={props.onCopyPath}
592+
onDeleteEntry={props.onDeleteEntry}
554593
onOpenFileInEditor={props.onOpenFileInEditor}
555594
onOpenFile={props.onOpenFile}
556595
onRevealFileInFileManager={props.onRevealFileInFileManager}
@@ -573,6 +612,7 @@ const WorkspaceDirectoryRow = memo(function WorkspaceDirectoryRow(props: {
573612
fileManagerName: string;
574613
isExpanded: boolean;
575614
onCopyPath: (pathValue: string) => void;
615+
onDeleteEntry: (pathValue: string) => void;
576616
onOpenDirectoryInFileManager: (pathValue: string) => void;
577617
onToggleDirectory: (pathValue: string) => void;
578618
}) {
@@ -590,6 +630,7 @@ const WorkspaceDirectoryRow = memo(function WorkspaceDirectoryRow(props: {
590630
},
591631
{ id: "open-in-finder", label: `Open in ${props.fileManagerName}` },
592632
{ id: "copy-path", label: "Copy path" },
633+
{ id: "delete", label: "Delete", destructive: true },
593634
],
594635
{ x: event.clientX, y: event.clientY },
595636
);
@@ -599,6 +640,8 @@ const WorkspaceDirectoryRow = memo(function WorkspaceDirectoryRow(props: {
599640
props.onOpenDirectoryInFileManager(props.entry.path);
600641
} else if (clicked === "copy-path") {
601642
props.onCopyPath(props.entry.path);
643+
} else if (clicked === "delete") {
644+
props.onDeleteEntry(props.entry.path);
602645
}
603646
},
604647
[props],
@@ -649,6 +692,7 @@ const WorkspaceFileRow = memo(function WorkspaceFileRow(props: {
649692
fileManagerName: string;
650693
resolvedTheme: "light" | "dark";
651694
onCopyPath: (pathValue: string) => void;
695+
onDeleteEntry: (pathValue: string) => void;
652696
onOpenFileInEditor: (pathValue: string) => void;
653697
onOpenFile: (pathValue: string, event?: { metaKey?: boolean; ctrlKey?: boolean }) => void;
654698
onRevealFileInFileManager: (pathValue: string) => void;
@@ -665,6 +709,7 @@ const WorkspaceFileRow = memo(function WorkspaceFileRow(props: {
665709
{ id: "open-in-editor", label: "Open in editor" },
666710
{ id: "reveal-in-finder", label: `Reveal in ${props.fileManagerName}` },
667711
{ id: "copy-path", label: "Copy path" },
712+
{ id: "delete", label: "Delete", destructive: true },
668713
],
669714
{ x: event.clientX, y: event.clientY },
670715
);
@@ -676,6 +721,8 @@ const WorkspaceFileRow = memo(function WorkspaceFileRow(props: {
676721
props.onRevealFileInFileManager(props.entry.path);
677722
} else if (clicked === "copy-path") {
678723
props.onCopyPath(props.entry.path);
724+
} else if (clicked === "delete") {
725+
props.onDeleteEntry(props.entry.path);
679726
}
680727
},
681728
[props],

apps/web/src/wsNativeApi.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ export function createWsNativeApi(): NativeApi {
193193
listDirectory: (input) => transport.request(WS_METHODS.projectsListDirectory, input),
194194
writeFile: (input) => transport.request(WS_METHODS.projectsWriteFile, input),
195195
readFile: (input) => transport.request(WS_METHODS.projectsReadFile, input),
196+
deleteEntry: (input) => transport.request(WS_METHODS.projectsDeleteEntry, input),
196197
},
197198
shell: {
198199
openInEditor: (cwd, editor) =>

packages/contracts/src/ipc.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import type {
2424
GitStatusResult,
2525
} from "./git";
2626
import type {
27+
ProjectDeleteEntryInput,
2728
ProjectListDirectoryInput,
2829
ProjectListDirectoryResult,
2930
ProjectReadFileInput,
@@ -313,6 +314,7 @@ export interface NativeApi {
313314
listDirectory: (input: ProjectListDirectoryInput) => Promise<ProjectListDirectoryResult>;
314315
writeFile: (input: ProjectWriteFileInput) => Promise<ProjectWriteFileResult>;
315316
readFile: (input: ProjectReadFileInput) => Promise<ProjectReadFileResult>;
317+
deleteEntry: (input: ProjectDeleteEntryInput) => Promise<void>;
316318
};
317319
shell: {
318320
openInEditor: (cwd: string, editor: EditorId) => Promise<void>;

packages/contracts/src/project.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,3 +83,9 @@ export const ProjectReadFileResult = Schema.Struct({
8383
imageDataUrl: Schema.optional(Schema.String),
8484
});
8585
export type ProjectReadFileResult = typeof ProjectReadFileResult.Type;
86+
87+
export const ProjectDeleteEntryInput = Schema.Struct({
88+
cwd: TrimmedNonEmptyString,
89+
relativePath: TrimmedNonEmptyString.check(Schema.isMaxLength(PROJECT_DIRECTORY_PATH_MAX_LENGTH)),
90+
});
91+
export type ProjectDeleteEntryInput = typeof ProjectDeleteEntryInput.Type;

packages/contracts/src/ws.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ import {
5858
SaveProjectEnvironmentVariablesInput,
5959
} from "./environment";
6060
import {
61+
ProjectDeleteEntryInput,
6162
ProjectListDirectoryInput,
6263
ProjectReadFileInput,
6364
ProjectSearchEntriesInput,
@@ -93,6 +94,7 @@ export const WS_METHODS = {
9394
projectsListDirectory: "projects.listDirectory",
9495
projectsWriteFile: "projects.writeFile",
9596
projectsReadFile: "projects.readFile",
97+
projectsDeleteEntry: "projects.deleteEntry",
9698

9799
// Shell methods
98100
shellOpenInEditor: "shell.openInEditor",
@@ -204,6 +206,7 @@ const WebSocketRequestBody = Schema.Union([
204206
tagRequestBody(WS_METHODS.projectsListDirectory, ProjectListDirectoryInput),
205207
tagRequestBody(WS_METHODS.projectsWriteFile, ProjectWriteFileInput),
206208
tagRequestBody(WS_METHODS.projectsReadFile, ProjectReadFileInput),
209+
tagRequestBody(WS_METHODS.projectsDeleteEntry, ProjectDeleteEntryInput),
207210

208211
// Shell methods
209212
tagRequestBody(WS_METHODS.shellOpenInEditor, OpenInEditorInput),

0 commit comments

Comments
 (0)