From f26de6c52f7442762973155c26743d3494fb5887 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 15 Jan 2026 12:48:54 -0600 Subject: [PATCH] feat(app): delete workspace --- packages/app/src/pages/layout.tsx | 229 +++++++++++++----- packages/opencode/src/project/project.ts | 15 ++ .../src/server/routes/experimental.ts | 26 ++ packages/opencode/src/worktree/index.ts | 66 +++++ packages/sdk/js/src/v2/gen/sdk.gen.ts | 38 +++ packages/sdk/js/src/v2/gen/types.gen.ts | 31 +++ 6 files changed, 345 insertions(+), 60 deletions(-) diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 56d6bfbf8..8d61f0510 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -32,6 +32,7 @@ import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu" import { Collapsible } from "@opencode-ai/ui/collapsible" import { DiffChanges } from "@opencode-ai/ui/diff-changes" import { Spinner } from "@opencode-ai/ui/spinner" +import { Dialog } from "@opencode-ai/ui/dialog" import { getFilename } from "@opencode-ai/util/path" import { Session } from "@opencode-ai/sdk/v2/client" import { usePlatform } from "@/context/platform" @@ -906,6 +907,99 @@ export default function Layout(props: ParentProps) { } } + const errorMessage = (err: unknown) => { + if (err && typeof err === "object" && "data" in err) { + const data = (err as { data?: { message?: string } }).data + if (data?.message) return data.message + } + if (err instanceof Error) return err.message + return "Request failed" + } + + const deleteWorkspace = async (directory: string) => { + const current = currentProject() + if (!current) return + if (directory === current.worktree) return + + const result = await globalSDK.client.worktree + .remove({ directory: current.worktree, worktreeRemoveInput: { directory } }) + .then((x) => x.data) + .catch((err) => { + showToast({ + title: "Failed to delete workspace", + description: errorMessage(err), + }) + return false + }) + + if (!result) return + + layout.projects.close(directory) + layout.projects.open(current.worktree) + + if (params.dir && base64Decode(params.dir) === directory) { + navigateToProject(current.worktree) + } + } + + function DialogDeleteWorkspace(props: { directory: string }) { + const name = createMemo(() => getFilename(props.directory)) + const [data, setData] = createStore({ + status: "loading" as "loading" | "ready" | "error", + dirty: false, + }) + + onMount(() => { + const current = currentProject() + if (!current) { + setData({ status: "error", dirty: false }) + return + } + + globalSDK.client.file + .status({ directory: props.directory }) + .then((x) => { + const files = x.data ?? [] + const dirty = files.length > 0 + setData({ status: "ready", dirty }) + }) + .catch(() => { + setData({ status: "error", dirty: false }) + }) + }) + + const handleDelete = async () => { + await deleteWorkspace(props.directory) + dialog.close() + } + + const description = () => { + if (data.status === "loading") return "Checking for unmerged changes..." + if (data.status === "error") return "Unable to verify git status." + if (!data.dirty) return "No unmerged changes detected." + return "Unmerged changes detected in this workspace." + } + + return ( + +
+
+ Delete workspace "{name()}"? + {description()} +
+
+ + +
+
+
+ ) + } + createEffect( on( () => ({ ready: pageReady(), dir: params.dir, id: params.id }), @@ -1205,6 +1299,7 @@ export default function Layout(props: ParentProps) { const SortableWorkspace = (props: { directory: string; project: LocalProject; mobile?: boolean }): JSX.Element => { const sortable = createSortable(props.directory) const [workspaceStore, setWorkspaceStore] = globalSync.child(props.directory) + const [menuOpen, setMenuOpen] = createSignal(false) const slug = createMemo(() => base64Encode(props.directory)) const sessions = createMemo(() => workspaceStore.session @@ -1239,62 +1334,85 @@ export default function Layout(props: ParentProps) {
-
- -
-
- -
- {local() ? "local" : "sandbox"} : - - {workspaceStore.vcs?.branch ?? getFilename(props.directory)} - - } - > - { - const trimmed = next.trim() - if (!trimmed) return - renameWorkspace(props.directory, trimmed) - setEditor("value", workspaceValue()) - }} - class="text-14-medium text-text-base min-w-0 truncate" - displayClass="text-14-medium text-text-base min-w-0 truncate" - editing={workspaceEditActive()} - stopPropagation={false} - openOnDblClick={false} +
+
+ +
+
+ +
+ {local() ? "local" : "sandbox"} : + + {workspaceStore.vcs?.branch ?? getFilename(props.directory)} + + } + > + { + const trimmed = next.trim() + if (!trimmed) return + renameWorkspace(props.directory, trimmed) + setEditor("value", workspaceValue()) + }} + class="text-14-medium text-text-base min-w-0 truncate" + displayClass="text-14-medium text-text-base min-w-0 truncate" + editing={workspaceEditActive()} + stopPropagation={false} + openOnDblClick={false} + /> + + - - -
-
-
+