feat(core): worktree-based workspace switching with stash-based warp#23
feat(core): worktree-based workspace switching with stash-based warp#23CreatorGhost wants to merge 1 commit into
Conversation
|
Warning Review limit reached
Next review available in: 58 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the 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 configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (2)
📒 Files selected for processing (24)
📝 WalkthroughWalkthroughAdds 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. ChangesWorkspace and Worktree Flow
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
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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 winStashed changes are never restored when the warp itself fails.
hasStashed/stashMsgare only popped insideif (warped) { ... }. IfwarpWorkspaceSessionreturnsfalse(e.g. network error,VcsApplyError, timeout), the user's uncommitted changes remain stashed onsourceWorkspaceIDwith no restore attempt and no toast telling them their working directory changed — their files appear to have silently vanished until they manually rungit 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 winRemove the stale
workspace.listentry fromappBindingCommands. 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 winMissing error response type for stash/stash-pop endpoints.
The
vcsApplyendpoint defineserror: ApiVcsApplyError, but neithervcsStashnorvcsStashPopdeclare an error type. Ifgit stashfails (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 reusingApiVcsApplyErrorfor consistency, especially sincestashPopcan 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
resolveWorktreeNamereturns the input name as a directory fallback, which may be misleading.When no worktree matches
name, the function returnsnameas-is, which is then passed asdirectorytosvc.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 nameresolveWorktreeNameimplies it always resolves a name, not passes through a directory. Consider renaming toresolveWorktreeTargetfor 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
⛔ Files ignored due to path filters (2)
packages/sdk/js/src/v2/gen/sdk.gen.tsis excluded by!**/gen/**packages/sdk/js/src/v2/gen/types.gen.tsis excluded by!**/gen/**
📒 Files selected for processing (24)
packages/opencode/src/cli/cmd/worktree.tspackages/opencode/src/control-plane/adapters/worktree.tspackages/opencode/src/control-plane/workspace-adapter-runtime.tspackages/opencode/src/control-plane/workspace.tspackages/opencode/src/index.tspackages/opencode/src/project/vcs.tspackages/opencode/src/server/routes/instance/httpapi/groups/instance.tspackages/opencode/src/server/routes/instance/httpapi/groups/workspace.tspackages/opencode/src/server/routes/instance/httpapi/handlers/instance.tspackages/opencode/src/server/routes/instance/httpapi/handlers/workspace.tspackages/opencode/src/session/session.tspackages/tui/src/app.tsxpackages/tui/src/component/dialog-workspace-create.tsxpackages/tui/src/component/prompt/index.tsxpackages/tui/src/component/prompt/workspace.tsxpackages/tui/src/context/directory.tspackages/tui/src/context/project.tsxpackages/tui/src/context/sdk.tsxpackages/tui/src/context/sync.tsxpackages/tui/src/feature-plugins/home/footer.tsxpackages/tui/src/feature-plugins/sidebar/footer.tsxpackages/tui/src/routes/session/index.tsxpackages/tui/src/ui/dialog-select.tsxpackages/tui/test/cli/cmd/tui/dialog-workspace-create.test.ts
| 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 | ||
| }), |
There was a problem hiding this comment.
🗄️ 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.
| 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.
| 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) | ||
| } |
There was a problem hiding this comment.
🩺 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.
| 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.
| // Start syncing workspaces, it's important to do this after | ||
| // we've started listening to events | ||
| await sdk.sync.start().catch(() => {}) |
There was a problem hiding this comment.
🩺 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/nullRepository: 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 -SRepository: 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 -200Repository: 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 -200Repository: 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.tsRepository: 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 -250Repository: 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.tsxRepository: 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.
| 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) |
There was a problem hiding this comment.
🩺 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.tsxRepository: 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 expandedRepository: 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
PYRepository: 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
PYRepository: 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.
Ported from upstream anomalyco#36052.
6ef9236 to
f4a9985
Compare
|
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:
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 |
Port of upstream anomalyco/opencode#36052 (owner-approved feature addition).
What's ported
worktreeCLI command group (create/list/remove/reset) with stash-based warp between worktrees.Fork adaptations
packages/tui/src/component/prompt/index.tsxwith our paste-summary changes (kept both; verifiedshouldSummarizePasteintact).resolveWorktreeName(svc.list()now maps adapter errors throughfaillike every other call site) that broke typecheck against our newer dev.bun run generatefrom 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