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 (
+
+ )
+ }
+
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}
+ />
+
+
-
-
-
-
-
-
-
+
+
- navigate(`/${slug()}/session`)}
- />
-
+
+
+
+
+
+
+ navigate(`/${slug()}/session`)}>
+ New session
+
+ dialog.show(() => )}
+ >
+ Delete workspace
+
+
+
+
+
+ navigate(`/${slug()}/session`)}
+ />
+
+
+