Skip to content

feat(core): worktree-based workspace switching with stash-based warp#23

Closed
CreatorGhost wants to merge 1 commit into
devfrom
worktree-workspace-switch
Closed

feat(core): worktree-based workspace switching with stash-based warp#23
CreatorGhost wants to merge 1 commit into
devfrom
worktree-workspace-switch

Conversation

@CreatorGhost

@CreatorGhost CreatorGhost commented Jul 11, 2026

Copy link
Copy Markdown
Owner

Port of upstream anomalyco/opencode#36052 (owner-approved feature addition).

What's ported

  • worktree CLI command group (create/list/remove/reset) with stash-based warp between worktrees.
  • Control-plane worktree adapter + workspace adapter runtime wiring.
  • Server HttpApi workspace/instance route additions and TUI workspace-create dialog integration.

Fork adaptations

  • Resolved an import conflict in packages/tui/src/component/prompt/index.tsx with our paste-summary changes (kept both; verified shouldSummarizePaste intact).
  • Fixed an error-channel leak in resolveWorktreeName (svc.list() now maps adapter errors through fail like every other call site) that broke typecheck against our newer dev.
  • Regenerated the SDK via bun run generate from packages/client per repo policy — output matches the PR's bundled gen files byte-for-byte.

Verified: control-plane + vcs tests (49 pass), TUI dialog-workspace-create tests (2 pass); typecheck clean in packages/opencode, tui, and sdk/js.

Summary by CodeRabbit

  • New Features
    • Added CLI commands to create, list, remove, and reset linked workspaces.
    • Workspace creation now supports optional names, branches, and improved directory details.
    • Added Git stash and restore operations through the API.
    • Workspace switching preserves session state and safely handles uncommitted changes.
  • Improvements
    • Workspace management is available without the experimental feature flag.
    • Workspace lists support JSON and table output formats.
    • Updated workspace labels, directory displays, and selection dialogs.
  • Bug Fixes
    • Improved workspace synchronization, routing, and session directory handling.

@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@CreatorGhost, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 58 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1b9e4be7-8b8c-4019-988b-4d18aac54458

📥 Commits

Reviewing files that changed from the base of the PR and between 6ef9236 and f4a9985.

⛔ Files ignored due to path filters (2)
  • packages/sdk/js/src/v2/gen/sdk.gen.ts is excluded by !**/gen/**
  • packages/sdk/js/src/v2/gen/types.gen.ts is excluded by !**/gen/**
📒 Files selected for processing (24)
  • packages/opencode/src/cli/cmd/worktree.ts
  • packages/opencode/src/control-plane/adapters/worktree.ts
  • packages/opencode/src/control-plane/workspace-adapter-runtime.ts
  • packages/opencode/src/control-plane/workspace.ts
  • packages/opencode/src/index.ts
  • packages/opencode/src/project/vcs.ts
  • packages/opencode/src/server/routes/instance/httpapi/groups/instance.ts
  • packages/opencode/src/server/routes/instance/httpapi/groups/workspace.ts
  • packages/opencode/src/server/routes/instance/httpapi/handlers/instance.ts
  • packages/opencode/src/server/routes/instance/httpapi/handlers/workspace.ts
  • packages/opencode/src/session/session.ts
  • packages/tui/src/app.tsx
  • packages/tui/src/component/dialog-workspace-create.tsx
  • packages/tui/src/component/prompt/index.tsx
  • packages/tui/src/component/prompt/workspace.tsx
  • packages/tui/src/context/directory.ts
  • packages/tui/src/context/project.tsx
  • packages/tui/src/context/sdk.tsx
  • packages/tui/src/context/sync.tsx
  • packages/tui/src/feature-plugins/home/footer.tsx
  • packages/tui/src/feature-plugins/sidebar/footer.tsx
  • packages/tui/src/routes/session/index.tsx
  • packages/tui/src/ui/dialog-select.tsx
  • packages/tui/test/cli/cmd/tui/dialog-workspace-create.test.ts
📝 Walkthrough

Walkthrough

Adds linked workspace and worktree management across the CLI, control plane, VCS APIs, HTTP routes, session state, and TUI. Workspace selection, synchronization, directory display, and warp flows now support named workspaces, directory propagation, stash preservation, and linked workspace metadata.

Changes

Workspace and Worktree Flow

Layer / File(s) Summary
Worktree commands and linked workspace creation
packages/opencode/src/cli/cmd/worktree.ts, packages/opencode/src/control-plane/..., packages/opencode/src/index.ts
Adds create, list, remove, and reset worktree commands, registers them in the CLI, and supports linked workspace naming and runtime IDs.
VCS stash service and HTTP API
packages/opencode/src/project/vcs.ts, packages/opencode/src/server/routes/instance/httpapi/...
Adds Git stash and stash-pop operations with corresponding HTTP endpoints and handlers.
Warp and session directory propagation
packages/opencode/src/control-plane/workspace.ts, packages/opencode/src/session/session.ts, packages/opencode/src/server/routes/instance/httpapi/groups/workspace.ts, packages/opencode/src/server/routes/instance/httpapi/handlers/workspace.ts
Passes optional directories through workspace warp requests and conditionally stores them on sessions.
TUI workspace selection, creation, deletion, and warp behavior
packages/tui/src/component/dialog-workspace-create.tsx, packages/tui/src/component/prompt/..., packages/tui/src/app.tsx, packages/tui/src/ui/dialog-select.tsx
Reworks workspace selection, adds optional names and deletion, removes the workspace list palette entry, and stashes changes before warp with restoration afterward.
TUI synchronization and directory display
packages/tui/src/context/..., packages/tui/src/feature-plugins/..., packages/tui/src/routes/session/index.tsx, packages/tui/test/...
Loads VCS data during bootstrap and session changes, preserves selected workspaces during sync, updates directory rendering, and tests workspace detail generation.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant DialogWorkspaceSelect
  participant VcsService
  participant WorkspaceHttpApi
  participant Session
  User->>DialogWorkspaceSelect: Select workspace
  DialogWorkspaceSelect->>VcsService: Check status and stash changes
  DialogWorkspaceSelect->>WorkspaceHttpApi: Warp session
  WorkspaceHttpApi->>Session: Set workspace and directory
  DialogWorkspaceSelect->>VcsService: Restore matching stash
Loading
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is missing required template sections like issue reference, type of change, screenshots, and checklist. Add the missing template sections: issue reference, change type, detailed implementation notes, verification steps, screenshots/recordings if applicable, and the checklist.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: worktree-based workspace switching with stash-based warp.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch worktree-workspace-switch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/tui/src/component/prompt/workspace.tsx (1)

102-123: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Stashed changes are never restored when the warp itself fails.

hasStashed/stashMsg are only popped inside if (warped) { ... }. If warpWorkspaceSession returns false (e.g. network error, VcsApplyError, timeout), the user's uncommitted changes remain stashed on sourceWorkspaceID with no restore attempt and no toast telling them their working directory changed — their files appear to have silently vanished until they manually run git stash pop.

🐛 Proposed fix
     const warped = await warpWorkspaceSession({ ... })
-    if (warped) {
-      showNotice(workspace.name)
-      if (hasStashed && stashMsg) {
-        const popped = await sdk.client.vcs.stashPop({ message: stashMsg, workspace: sourceWorkspaceID }).catch(() => undefined)
-        if (!popped?.data) {
-          toast.show({ title: "Warp", message: "Failed to restore stashed changes", variant: "error" })
-        } else {
-          toast.show({ title: "Warp", message: "Uncommitted changes restored", variant: "success" })
-        }
-      }
-    }
+    if (warped) showNotice(workspace.name)
+    if (hasStashed && stashMsg) {
+      const popped = await sdk.client.vcs.stashPop({ message: stashMsg, workspace: sourceWorkspaceID }).catch(() => undefined)
+      if (!popped?.data) {
+        toast.show({ title: "Warp", message: "Failed to restore stashed changes", variant: "error" })
+      } else if (!warped) {
+        toast.show({ title: "Warp", message: "Warp failed; uncommitted changes restored", variant: "info" })
+      } else {
+        toast.show({ title: "Warp", message: "Uncommitted changes restored", variant: "success" })
+      }
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/tui/src/component/prompt/workspace.tsx` around lines 102 - 123, Move
the stashed-change restoration block using hasStashed and stashMsg outside the
if (warped) condition so it runs regardless of whether warpWorkspaceSession
succeeds. Preserve showNotice(workspace.name) only for successful warps, and
retain the existing stashPop error and success toasts while ensuring restoration
uses sourceWorkspaceID.
packages/tui/src/app.tsx (1)

609-610: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Remove the stale workspace.list entry from appBindingCommands. The app command list no longer registers that command, so the default app keybinds can dispatch a missing command.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/tui/src/app.tsx` around lines 609 - 610, Remove the stale
workspace.list entry from the appBindingCommands definition in app.tsx, while
preserving the remaining default app keybind entries and ordering.
🧹 Nitpick comments (2)
packages/opencode/src/server/routes/instance/httpapi/groups/instance.ts (1)

141-162: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Missing error response type for stash/stash-pop endpoints.

The vcsApply endpoint defines error: ApiVcsApplyError, but neither vcsStash nor vcsStashPop declare an error type. If git stash fails (non-git project, git not installed, merge conflict on pop), the error will surface as an unstructured 500. Consider adding a dedicated error schema (e.g., ApiVcsStashError) or reusing ApiVcsApplyError for consistency, especially since stashPop can fail with conflicts.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/opencode/src/server/routes/instance/httpapi/groups/instance.ts`
around lines 141 - 162, Add an explicit VCS error response schema to both the
`vcsStash` and `vcsStashPop` `HttpApiEndpoint.post` definitions, reusing the
existing `ApiVcsApplyError` or an appropriate dedicated `ApiVcsStashError`.
Ensure failures such as unavailable Git, non-Git projects, and stash-pop
conflicts are represented through the declared structured error response.
packages/opencode/src/cli/cmd/worktree.ts (1)

121-126: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

resolveWorktreeName returns the input name as a directory fallback, which may be misleading.

When no worktree matches name, the function returns name as-is, which is then passed as directory to svc.remove/svc.reset. The CLI help text says "worktree name or directory", so this is intentional — it allows users to pass a directory path directly. However, the function name resolveWorktreeName implies it always resolves a name, not passes through a directory. Consider renaming to resolveWorktreeTarget for clarity, or adding a brief comment explaining the fallback behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/opencode/src/cli/cmd/worktree.ts` around lines 121 - 126, Clarify
the intentional passthrough behavior in resolveWorktreeName: when no matching
worktree is found, the input may be a directory path used by svc.remove or
svc.reset. Prefer renaming the function to resolveWorktreeTarget and update all
call sites, or add a brief comment documenting this fallback.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/opencode/src/project/vcs.ts`:
- Around line 429-441: Update stashPop to avoid substring matching when
selecting a stash: parse each git stash list entry and compare input.message
exactly against the full stash message after the identifying delimiter, while
preserving the existing ref extraction and return behavior.

In `@packages/tui/src/component/dialog-workspace-create.tsx`:
- Around line 215-248: Update the remove function’s project.workspace.sync()
call to handle rejected synchronization like the existing
project.sync().catch(() => undefined) calls, ensuring execution reaches
setRemoving(undefined) even when synchronization fails.

In `@packages/tui/src/context/sdk.tsx`:
- Around line 95-97: Bound both awaited sdk.sync.start() calls in the relevant
initialization and props.events mount flows so a stalled sync cannot block SSE
startup or reconnection; use the existing timeout mechanism if available, or
make the sync kick-off fire-and-forget while preserving its error suppression.

In `@packages/tui/src/routes/session/index.tsx`:
- Around line 311-317: Update the session bootstrap flow around the workspace
lookup and vcs.get call to track the active session request generation or
cancellation state, and validate it before every state mutation. Prevent stale
requests from committing workspace, VCS, bootstrap, or session-sync updates
after a session switch, while preserving updates from the currently active
request.

---

Outside diff comments:
In `@packages/tui/src/app.tsx`:
- Around line 609-610: Remove the stale workspace.list entry from the
appBindingCommands definition in app.tsx, while preserving the remaining default
app keybind entries and ordering.

In `@packages/tui/src/component/prompt/workspace.tsx`:
- Around line 102-123: Move the stashed-change restoration block using
hasStashed and stashMsg outside the if (warped) condition so it runs regardless
of whether warpWorkspaceSession succeeds. Preserve showNotice(workspace.name)
only for successful warps, and retain the existing stashPop error and success
toasts while ensuring restoration uses sourceWorkspaceID.

---

Nitpick comments:
In `@packages/opencode/src/cli/cmd/worktree.ts`:
- Around line 121-126: Clarify the intentional passthrough behavior in
resolveWorktreeName: when no matching worktree is found, the input may be a
directory path used by svc.remove or svc.reset. Prefer renaming the function to
resolveWorktreeTarget and update all call sites, or add a brief comment
documenting this fallback.

In `@packages/opencode/src/server/routes/instance/httpapi/groups/instance.ts`:
- Around line 141-162: Add an explicit VCS error response schema to both the
`vcsStash` and `vcsStashPop` `HttpApiEndpoint.post` definitions, reusing the
existing `ApiVcsApplyError` or an appropriate dedicated `ApiVcsStashError`.
Ensure failures such as unavailable Git, non-Git projects, and stash-pop
conflicts are represented through the declared structured error response.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 69fcd12b-5408-4a17-b5e9-d4d32aed6d43

📥 Commits

Reviewing files that changed from the base of the PR and between 67e2bd4 and 6ef9236.

⛔ Files ignored due to path filters (2)
  • packages/sdk/js/src/v2/gen/sdk.gen.ts is excluded by !**/gen/**
  • packages/sdk/js/src/v2/gen/types.gen.ts is excluded by !**/gen/**
📒 Files selected for processing (24)
  • packages/opencode/src/cli/cmd/worktree.ts
  • packages/opencode/src/control-plane/adapters/worktree.ts
  • packages/opencode/src/control-plane/workspace-adapter-runtime.ts
  • packages/opencode/src/control-plane/workspace.ts
  • packages/opencode/src/index.ts
  • packages/opencode/src/project/vcs.ts
  • packages/opencode/src/server/routes/instance/httpapi/groups/instance.ts
  • packages/opencode/src/server/routes/instance/httpapi/groups/workspace.ts
  • packages/opencode/src/server/routes/instance/httpapi/handlers/instance.ts
  • packages/opencode/src/server/routes/instance/httpapi/handlers/workspace.ts
  • packages/opencode/src/session/session.ts
  • packages/tui/src/app.tsx
  • packages/tui/src/component/dialog-workspace-create.tsx
  • packages/tui/src/component/prompt/index.tsx
  • packages/tui/src/component/prompt/workspace.tsx
  • packages/tui/src/context/directory.ts
  • packages/tui/src/context/project.tsx
  • packages/tui/src/context/sdk.tsx
  • packages/tui/src/context/sync.tsx
  • packages/tui/src/feature-plugins/home/footer.tsx
  • packages/tui/src/feature-plugins/sidebar/footer.tsx
  • packages/tui/src/routes/session/index.tsx
  • packages/tui/src/ui/dialog-select.tsx
  • packages/tui/test/cli/cmd/tui/dialog-workspace-create.test.ts

Comment on lines +429 to +441
stashPop: Effect.fn("Vcs.stashPop")(function* (input: StashInput) {
const ctx = yield* InstanceState.context
if (ctx.project.vcs !== "git") return false
const list = yield* git.run(["stash", "list"], { cwd: ctx.directory })
const ref = list
.text()
.split(/\r?\n/)
.find((line) => line.includes(input.message))
?.split(":")[0]
if (!ref) return false
yield* git.run(["stash", "pop", ref], { cwd: ctx.directory })
return true
}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

stashPop substring match can pop the wrong stash

line.includes(input.message) is a substring search across git stash list output. If multiple stash entries contain the same substring (e.g., message "fix" matches "fix" and "fix-bug"), find returns the first match — which may not be the stash created by the corresponding stash call. Popping the wrong stash overwrites the working tree with unintended content, risking data loss.

Use an exact message match or a more precise delimiter-based extraction to avoid ambiguity.

🛡️ Proposed fix: match full stash message after last colon
       stashPop: Effect.fn("Vcs.stashPop")(function* (input: StashInput) {
         const ctx = yield* InstanceState.context
         if (ctx.project.vcs !== "git") return false
         const list = yield* git.run(["stash", "list"], { cwd: ctx.directory })
-        const ref = list
-          .text()
-          .split(/\r?\n/)
-          .find((line) => line.includes(input.message))
-          ?.split(":")[0]
+        const entry = list
+          .text()
+          .split(/\r?\n/)
+          .find((line) => {
+            // git stash list format: stash@{N}: WIP on branch: hash: message
+            // Extract message as everything after the 3rd colon (or 2nd for non-WIP)
+            const parts = line.split(": ")
+            const message = parts.slice(2).join(": ")
+            return message === input.message
+          })
+        const ref = entry?.split(":")[0]
         if (!ref) return false
         yield* git.run(["stash", "pop", ref], { cwd: ctx.directory })
         return true
       }),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
stashPop: Effect.fn("Vcs.stashPop")(function* (input: StashInput) {
const ctx = yield* InstanceState.context
if (ctx.project.vcs !== "git") return false
const list = yield* git.run(["stash", "list"], { cwd: ctx.directory })
const ref = list
.text()
.split(/\r?\n/)
.find((line) => line.includes(input.message))
?.split(":")[0]
if (!ref) return false
yield* git.run(["stash", "pop", ref], { cwd: ctx.directory })
return true
}),
stashPop: Effect.fn("Vcs.stashPop")(function* (input: StashInput) {
const ctx = yield* InstanceState.context
if (ctx.project.vcs !== "git") return false
const list = yield* git.run(["stash", "list"], { cwd: ctx.directory })
const entry = list
.text()
.split(/\r?\n/)
.find((line) => {
// git stash list format: stash@{N}: WIP on branch: hash: message
// Extract message as everything after the 3rd colon (or 2nd for non-WIP)
const parts = line.split(": ")
const message = parts.slice(2).join(": ")
return message === input.message
})
const ref = entry?.split(":")[0]
if (!ref) return false
yield* git.run(["stash", "pop", ref], { cwd: ctx.directory })
return true
}),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/opencode/src/project/vcs.ts` around lines 429 - 441, Update stashPop
to avoid substring matching when selecting a stash: parse each git stash list
entry and compare input.message exactly against the full stash message after the
identifying delimiter, while preserving the existing ref extraction and return
behavior.

Comment on lines +215 to +248
async function remove(workspace: Workspace) {
if (removing()) return

const confirmed = await DialogConfirm.show(
dialog,
"Delete workspace",
`Are you sure you want to delete "${workspace.name}"?`,
"delete",
)
if (confirmed !== true) return

setRemoving(workspace.id)
const result = await sdk.client.experimental.workspace.remove({ id: workspace.id }).catch((err) => ({
error: err,
}))
if (result?.error) {
setRemoving(undefined)
toast.show({
variant: "error",
title: "Failed to delete workspace",
message: errorMessage(result.error),
})
return
}

if (project.workspace.current() === workspace.id) {
project.workspace.set(undefined)
await project.sync().catch(() => undefined)
route.navigate({ type: "home" })
}
await project.workspace.sync()
await project.sync().catch(() => undefined)
setRemoving(undefined)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Missing error handling can leave removing stuck.

Unlike the sibling project.sync().catch(() => undefined) calls, await project.workspace.sync() on line 245 has no .catch(). If it rejects, the function throws before reaching setRemoving(undefined) on line 247, leaving the option permanently showing "Deleting..." with no toast/recovery.

🐛 Proposed fix
     if (project.workspace.current() === workspace.id) {
       project.workspace.set(undefined)
       await project.sync().catch(() => undefined)
       route.navigate({ type: "home" })
     }
-    await project.workspace.sync()
+    await project.workspace.sync().catch(() => undefined)
     await project.sync().catch(() => undefined)
     setRemoving(undefined)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async function remove(workspace: Workspace) {
if (removing()) return
const confirmed = await DialogConfirm.show(
dialog,
"Delete workspace",
`Are you sure you want to delete "${workspace.name}"?`,
"delete",
)
if (confirmed !== true) return
setRemoving(workspace.id)
const result = await sdk.client.experimental.workspace.remove({ id: workspace.id }).catch((err) => ({
error: err,
}))
if (result?.error) {
setRemoving(undefined)
toast.show({
variant: "error",
title: "Failed to delete workspace",
message: errorMessage(result.error),
})
return
}
if (project.workspace.current() === workspace.id) {
project.workspace.set(undefined)
await project.sync().catch(() => undefined)
route.navigate({ type: "home" })
}
await project.workspace.sync()
await project.sync().catch(() => undefined)
setRemoving(undefined)
}
async function remove(workspace: Workspace) {
if (removing()) return
const confirmed = await DialogConfirm.show(
dialog,
"Delete workspace",
`Are you sure you want to delete "${workspace.name}"?`,
"delete",
)
if (confirmed !== true) return
setRemoving(workspace.id)
const result = await sdk.client.experimental.workspace.remove({ id: workspace.id }).catch((err) => ({
error: err,
}))
if (result?.error) {
setRemoving(undefined)
toast.show({
variant: "error",
title: "Failed to delete workspace",
message: errorMessage(result.error),
})
return
}
if (project.workspace.current() === workspace.id) {
project.workspace.set(undefined)
await project.sync().catch(() => undefined)
route.navigate({ type: "home" })
}
await project.workspace.sync().catch(() => undefined)
await project.sync().catch(() => undefined)
setRemoving(undefined)
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/tui/src/component/dialog-workspace-create.tsx` around lines 215 -
248, Update the remove function’s project.workspace.sync() call to handle
rejected synchronization like the existing project.sync().catch(() => undefined)
calls, ensuring execution reaches setRemoving(undefined) even when
synchronization fails.

Comment on lines +95 to +97
// Start syncing workspaces, it's important to do this after
// we've started listening to events
await sdk.sync.start().catch(() => {})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n 'sync' packages/opencode/src/control-plane/workspace-adapter-runtime.ts | head -50
rg -rn 'sync\.start\(' packages/sdk packages/opencode 2>/dev/null

Repository: CreatorGhost/TheCode

Length of output: 333


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the relevant files and inspect the surrounding code.
git ls-files | rg '^packages/(tui|sdk|opencode)/'
echo '--- sdk.tsx excerpt ---'
sed -n '60,150p' packages/tui/src/context/sdk.tsx
echo '--- workspace-adapter-runtime references ---'
rg -n 'sync\.start|Flag\.OPENCODE_EXPERIMENTAL_WORKSPACES|events\.stream|onMount' packages/tui/src/context/sdk.tsx packages/opencode/src/control-plane/workspace-adapter-runtime.ts packages/sdk -S

Repository: CreatorGhost/TheCode

Length of output: 50376


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- packages/tui/src/context/sdk.tsx around sync.start calls ---'
nl -ba packages/tui/src/context/sdk.tsx | sed -n '85,135p'

echo '--- sync.start references in repo ---'
rg -n 'sync\.start\(' packages/tui packages/opencode packages/sdk -S

echo '--- sync implementation candidates ---'
rg -n 'class .*Sync|function .*sync|const .*sync|start\(\)' packages/opencode/src/sync packages/opencode/src/control-plane packages/sdk/js/src -S | head -200

Repository: CreatorGhost/TheCode

Length of output: 263


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- packages/tui/src/context/sdk.tsx excerpt ---'
sed -n '85,135p' packages/tui/src/context/sdk.tsx

echo '--- sync.start references ---'
rg -n 'sync\.start\(' packages/tui packages/opencode packages/sdk -S

echo '--- sync-related implementation ---'
rg -n 'export .*start|start\(|sync' packages/opencode/src/sync packages/opencode/src/control-plane packages/sdk/js/src -S | head -200

Repository: CreatorGhost/TheCode

Length of output: 25789


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- sync start handler ---'
sed -n '1,220p' packages/opencode/src/server/routes/instance/httpapi/handlers/sync.ts

echo '--- generated client request path for sync.start ---'
sed -n '4538,4575p' packages/sdk/js/src/v2/gen/sdk.gen.ts

echo '--- client timeout / abort plumbing ---'
sed -n '1,320p' packages/sdk/js/src/v2/gen/client/client.gen.ts

Repository: CreatorGhost/TheCode

Length of output: 12179


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- workspace sync start implementation ---'
sed -n '430,900p' packages/opencode/src/control-plane/workspace.ts

echo '--- client timeout-related config/search ---'
rg -n 'timeout|AbortSignal|signal:|requestTimeout|fetch\(' packages/sdk/js/src packages/tui/src packages/opencode/src -S | head -250

Repository: CreatorGhost/TheCode

Length of output: 42171


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- packages/sdk/js/src/v2/client.ts ---'
sed -n '1,220p' packages/sdk/js/src/v2/client.ts

echo '--- packages/sdk/js/src/v2/index.ts ---'
sed -n '1,120p' packages/sdk/js/src/v2/index.ts

echo '--- packages/tui/src/context/sdk.tsx imports ---'
sed -n '1,60p' packages/tui/src/context/sdk.tsx

Repository: CreatorGhost/TheCode

Length of output: 5344


Bound the awaited sync kick-off

Both sdk.sync.start() calls are awaited without any timeout. If the request stalls, it can block SSE startup/reconnect and the props.events mount path. A small timeout or fire-and-forget wrapper would keep event consumption moving.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/tui/src/context/sdk.tsx` around lines 95 - 97, Bound both awaited
sdk.sync.start() calls in the relevant initialization and props.events mount
flows so a stalled sync cannot block SSE startup or reconnection; use the
existing timeout mechanism if available, or make the sync kick-off
fire-and-forget while preserving its error suppression.

Comment on lines +311 to +317
const targetWorkspace = result.data.workspaceID
? project.workspace.get(result.data.workspaceID)
: undefined
if (targetWorkspace?.branch) {
sync.set("vcs", { branch: targetWorkspace.branch })
}
await sdk.client.vcs.get({ workspace: result.data.workspaceID ?? undefined }).then((x) => sync.set("vcs", x.data)).catch(() => undefined)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the target file around the reported lines.
sed -n '260,360p' packages/tui/src/routes/session/index.tsx

# Find nearby session-switch / sync state logic for context.
rg -n "route\.sessionID|sync\.set\\(\"vcs\"|workspaceID|bootstrap" packages/tui/src/routes/session/index.tsx

Repository: CreatorGhost/TheCode

Length of output: 6566


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the file structure to locate the relevant component / effect.
ast-grep outline packages/tui/src/routes/session/index.tsx --view expanded

Repository: CreatorGhost/TheCode

Length of output: 3462


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect all occurrences of this effect pattern in the repo to see if cancellation is handled elsewhere.
rg -n "await sdk\.client\.vcs\.get|sync\.set\\(\"vcs\"" packages/tui/src -g '*.tsx' -g '*.ts'

Repository: CreatorGhost/TheCode

Length of output: 781


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the full relevant section with line numbers for precise reasoning.
cat -n packages/tui/src/routes/session/index.tsx | sed -n '280,340p'

Repository: CreatorGhost/TheCode

Length of output: 2850


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
p = Path('packages/tui/src/routes/session/index.tsx')
text = p.read_text()
for needle in ['route.sessionID', 'sdk.client.vcs.get', 'sync.set("vcs"', 'AbortController', 'generation', 'requestId', 'useEffect']:
    print(f"\n=== {needle} ===")
    for i, line in enumerate(text.splitlines(), 1):
        if needle in line:
            start = max(1, i-8)
            end = min(len(text.splitlines()), i+12)
            for j in range(start, end+1):
                print(f"{j:4d}: {text.splitlines()[j-1]}")
            break
PY

Repository: CreatorGhost/TheCode

Length of output: 3362


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the full useEffect / async transition around the reported lines.
python3 - <<'PY'
from pathlib import Path
text = Path('packages/tui/src/routes/session/index.tsx').read_text().splitlines()
targets = ['sdk.client.vcs.get', 'sync.set("vcs"', 'route.sessionID', 'useEffect(']
seen = set()
for needle in targets:
    for idx, line in enumerate(text, 1):
        if needle in line and idx not in seen:
            seen.add(idx)
            start = max(1, idx - 25)
            end = min(len(text), idx + 25)
            print(f"\n### around line {idx} ({needle})")
            for i in range(start, end + 1):
                print(f"{i:4d}: {text[i-1]}")
            break
PY

Repository: CreatorGhost/TheCode

Length of output: 6746


Guard the session bootstrap against stale async writes. A fast session switch can let an earlier request finish later and overwrite the active session’s workspace/VCS state; add a request-generation or abort check before committing workspace, VCS, bootstrap, and session-sync updates.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/tui/src/routes/session/index.tsx` around lines 311 - 317, Update the
session bootstrap flow around the workspace lookup and vcs.get call to track the
active session request generation or cancellation state, and validate it before
every state mutation. Prevent stale requests from committing workspace, VCS,
bootstrap, or session-sync updates after a session switch, while preserving
updates from the currently active request.

@CreatorGhost
CreatorGhost force-pushed the worktree-workspace-switch branch from 6ef9236 to f4a9985 Compare July 12, 2026 16:00
@CreatorGhost

Copy link
Copy Markdown
Owner Author

Deferring this port. An Opus code review found real data-loss risks in the newly HTTP-exposed stash primitives that must be resolved before merge:

  • stash/stashPop ignore git exit codes (fail-open) — a failed stash push reports success, so a later destructive step (worktree reset --hard/checkout) can lose uncommitted changes.
  • stashPop returns true even when git stash pop hits a merge conflict (leaves conflict markers + a dangling stash), and selects the stash by substring match (can pop the wrong stash).
  • /vcs/stash + /vcs/stash-pop are not orchestrated with the actual warp (which uses patch-copy via diffRaw→apply), so a failure between stash and pop strands the user's uncommitted changes with no restore.
  • stash/stashPop run against ctx.directory while other VCS methods use ctx.worktree; in a worktree/warp context these can differ.

To revive: make stash/stashPop fail-closed (check exit codes, tagged error channel), handle pop conflicts, use an exact stash-message match, standardize the tree, and either wire stash into sessionWarp with restore-on-failure or drop the unwired stash endpoints. Closing for now; branch worktree-workspace-switch is retained.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant