Repository navigation
Conversation
Right-click a worktree in the sidebar and choose "Open in Split View" to show it beside the active one. Whichever side was clicked last becomes the active worktree, so the sidebar, shortcuts and Cmd+S follow it; the other side stays painted and live but does not own focus or shortcuts. - New worktree-split-view slice stores the left/right pair (with each side's execution host) and the divider ratio. Not persisted yet. - Surfaces stay siblings ordered with CSS `order` and a container CSS variable, so switching sides or dragging never remounts a workspace. - The partner is exempt from terminal parking, browser guest eviction, agent auto-ack and on-screen attention scans like the active one. - Removing, renaming or host-twin purging a paired worktree clears or re-points the pair. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Activating a worktree outside the pair now ends the split, so a stale pair no longer keeps two panes or hides "Open in Split View". Adds a close button on the divider and shows "Close Split View" only while the split is on screen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The side-by-side pair is now saved in the workspace session and restored in the same hydration update as the active worktree, so the first frame already shows both halves. A pair whose side no longer exists, or that no longer holds the active worktree, is dropped silently. The divider ratio is not saved; a restored split starts at 50:50. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A scoped session merge could move focus outside the pair while leaving the pair set, and a tabless side of a not-yet-listed SSH repo failed restore validation, dropping the split before the host connected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
📝 Walkthrough
Priority: ➖ Normal Severity of issue fixed: Medium Merge Risk: 🔵 Low · up to
|
There was a problem hiding this comment.
Actionable comments posted: 2
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Repository UI
- Review profile: CHILL
- Plan: Advanced
- Run ID:
51242b0f-93c9-4b5b-b2e5-08c91327ca6f
📒 Files selected for processing: 64
src/main/orca-profiles/profile-project-session-field-disposition.tssrc/main/persistence/restoring-sessions/session-worktree-ownership.tssrc/renderer/src/components/TerminalSplitWorkspaceSurfaces.tsxsrc/renderer/src/components/TerminalWorktreeSplitSurface.tsxsrc/renderer/src/components/WorkspacePaneOverlayLayers.tsxsrc/renderer/src/components/browser-pane/assemble-chrome/BrowserPaneOverlayLayer.tsxsrc/renderer/src/components/native-chat/StructuredAgentSessionPaneOverlayLayer.tsxsrc/renderer/src/components/sidebar/WorktreeContextMenuView.tsxsrc/renderer/src/components/sidebar/WorktreeSplitViewMenuItems.tsxsrc/renderer/src/components/tab-group/TabGroupSplitLayout.tsxsrc/renderer/src/components/tab-group/TabGroupSplitNodeTree.tsxsrc/renderer/src/components/terminal-cold-activation.tssrc/renderer/src/components/terminal-pane/TerminalPaneOverlayLayer.tsxsrc/renderer/src/components/terminal-pane/use-manual-terminal-worktree-parking.tssrc/renderer/src/components/terminal-parked-watcher-sync-entries.test.tsxsrc/renderer/src/components/terminal-parking-pass-candidates.tssrc/renderer/src/components/terminal/initial-terminal-admission-wait.test.tsxsrc/renderer/src/components/terminal/initial-terminal-structured-launch.test.tsxsrc/renderer/src/components/use-terminal-browser-retention.tssrc/renderer/src/components/use-terminal-parking-foundation.tssrc/renderer/src/components/use-terminal-parking-pass.tssrc/renderer/src/components/use-terminal-watcher-effects.tssrc/renderer/src/components/use-terminal-workspace-foundation.tssrc/renderer/src/components/worktree-split-view-divider.tsxsrc/renderer/src/hooks/agent-auto-ack-targets.test.tssrc/renderer/src/hooks/agent-auto-ack-targets.tssrc/renderer/src/hooks/on-screen-attention-scan.tssrc/renderer/src/i18n/locales/en.jsonsrc/renderer/src/i18n/locales/es.jsonsrc/renderer/src/i18n/locales/fr.jsonsrc/renderer/src/i18n/locales/ja.jsonsrc/renderer/src/i18n/locales/ko.jsonsrc/renderer/src/i18n/locales/zh.jsonsrc/renderer/src/lib/workspace-session-patch.tssrc/renderer/src/lib/workspace-session-relevant-fields.test.tssrc/renderer/src/lib/workspace-session-sanitized-tabs.tssrc/renderer/src/lib/workspace-session.test.tssrc/renderer/src/lib/workspace-session.tssrc/renderer/src/store/index.tssrc/renderer/src/store/slices/store-test-helpers.tssrc/renderer/src/store/slices/workspace-attention/workspace-unread-flag.tssrc/renderer/src/store/slices/worktree-split-view-session-restore.test.tssrc/renderer/src/store/slices/worktree-split-view.test.tssrc/renderer/src/store/slices/worktree-split-view.tssrc/renderer/src/store/slices/worktrees/session/set-active-worktree.tssrc/renderer/src/store/slices/worktrees/session/worktree-identity-rename-state.tssrc/renderer/src/store/slices/worktrees/teardown/host-twin-worktree-purge.tssrc/renderer/src/store/slices/worktrees/teardown/remove-worktree-store-cleanup.tssrc/renderer/src/store/slices/worktrees/teardown/worktree-purge-state.tssrc/renderer/src/store/terminals/workspace-terminal-hydration-patch.tssrc/renderer/src/store/terminals/workspace-terminal-hydration.tssrc/renderer/src/store/types.tssrc/shared/execution-host-id-schema.tssrc/shared/workspace-layout/workspace-layout-beside.tssrc/shared/workspace-layout/workspace-layout-profile.test-fixture.tssrc/shared/workspace-session-host-field-ownership.tssrc/shared/workspace-session-schema-field-coverage.test.tssrc/shared/workspace-session-schema.tssrc/shared/workspace-session-state-types.tssrc/shared/worktree-split-view-pair.test.tssrc/shared/worktree-split-view-pair.tssrc/shared/wsl-move/wsl-move-dispositions.tstests/e2e/worktree-split-view-restart.spec.tstests/e2e/worktree-split-view.spec.ts
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.
| // Why the pair too: a tabless side of a not-yet-listed SSH repo must still restore. | ||
| [ | ||
| ...collectPersistedWorktreeIdsForSessionHydration(session), | ||
| ...(session.worktreeSplitViewPair | ||
| ? [ | ||
| session.worktreeSplitViewPair.leftWorktreeId, | ||
| session.worktreeSplitViewPair.rightWorktreeId | ||
| ] | ||
| : []) | ||
| ] |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
sed -n '1,180p' src/renderer/src/store/terminals/workspace-terminal-ssh-placeholders.ts
sed -n '32,95p' src/shared/worktree/degraded-repo-worktree-validity.ts
sed -n '45,100p' src/renderer/src/store/terminals/workspace-terminal-hydration.ts
sed -n '40,82p' src/renderer/src/components/use-terminal-workspace-foundation.ts
sed -n '62,88p' src/renderer/src/store/slices/worktree-split-view-session-restore.test.tsRepository: stablyai/orca
Length of output: 11626
🏁 Script executed:
set -o pipefail
printf '%s\n' '--- PR diff for hydration and placeholder paths ---'
git diff --no-ext-diff --unified=35 23d68112df3d8dd1c7bc3707a793dac1d31d3072 ac726e5e18d72dcc4337284f4ad5efb38d23c90d -- \
src/renderer/src/store/terminals/workspace-terminal-hydration.ts \
src/renderer/src/store/terminals/workspace-terminal-ssh-placeholders.ts \
src/shared/worktree/degraded-repo-worktree-validity.ts \
src/shared/worktree-split-view-pair.ts
printf '%s\n' '--- bound symbols and call sites ---'
rg -n -F --glob '*.ts' -e 'addHydratedSshWorktreePlaceholders' -e 'buildValidWorktreeIdsForSessionHydration' -e 'restorableWorktreeSplitViewPair' -e 'detectedWorktreesByRepo' src || test "$?" -eq 1
printf '%s\n' '--- hydration owner and relevant tests ---'
sed -n '1,220p' src/renderer/src/store/terminals/workspace-terminal-hydration.ts
sed -n '1,180p' src/shared/worktree/degraded-repo-worktree-validity.ts
sed -n '1,140p' src/shared/worktree-split-view-pair.ts
sed -n '1,260p' src/renderer/src/store/slices/worktree-split-view-session-restore.test.tsRepository: stablyai/orca
Length of output: 41871
🏁 Script executed:
set -o pipefail
printf '%s\n' '--- split-pair lifecycle bindings ---'
rg -n -F --glob '*.ts' -e 'worktreeSplitViewPair' -e 'closeWorktreeSplitView' -e 'splitPartnerWorktreeId' src/renderer/src/store src/renderer/src/components src/shared | head -240
printf '%s\n' '--- detected-listing type and merge paths ---'
rg -n -F --glob '*.ts' -e 'export type DetectedWorktreeListResult' -e 'interface DetectedWorktreeListResult' -e 'authoritative:' -e 'worktreesByRepo:' src/shared src/renderer/src/store/slices/worktrees src/renderer/src/store/repos | head -240
sed -n '1,280p' src/renderer/src/store/slices/worktrees/listing/fetched-worktree-merge.ts
sed -n '1,180p' src/renderer/src/store/slices/worktrees/session/worktree-slice-initial-state.tsRepository: stablyai/orca
Length of output: 32276
🏁 Script executed:
set -o pipefail
printf '%s\n' '--- authoritative removal and split cleanup ---'
rg -n -F --glob '*.ts' -e 'splitViewPairAfterRemoval' -e 'buildWorktreePurgeState' -e 'getRemovedWorktreeIdsAfterAuthoritativeScan' -e 'hasIndexedDetectedWorktree' src/renderer/src/store/slices src/renderer/src/lib
printf '%s\n' '--- purge state and split-removal callers ---'
sed -n '1,220p' src/renderer/src/store/slices/worktrees/teardown/worktree-purge-state.ts
sed -n '1,190p' src/renderer/src/store/slices/worktree-split-view.ts
printf '%s\n' '--- remainder of fetched merge ---'
sed -n '230,340p' src/renderer/src/store/slices/worktrees/listing/fetched-worktree-merge.ts
printf '%s\n' '--- detected result contract ---'
sed -n '250,300p' src/shared/worktree/types.tsRepository: stablyai/orca
Length of output: 26616
Materialize tabless SSH pair sides as provisional placeholders.
buildValidWorktreeIdsForSessionHydration admits the pair IDs while the SSH listing is unavailable, but addHydratedSshWorktreePlaceholders only examines tabsByWorktree. A tabless side therefore remains in worktreeSplitViewPair but has no worktree surface, so the restored split does not render both sides.
Pass only pair IDs that survived validWorktreeIds into the placeholder helper. Do not pass every raw pair ID, because an authoritative listing can already prove that a side was deleted. Later authoritative removal already purges the placeholder and clears the pair.
Suggested fix
diff --git a/src/renderer/src/store/terminals/workspace-terminal-ssh-placeholders.ts b/src/renderer/src/store/terminals/workspace-terminal-ssh-placeholders.ts
--- a/src/renderer/src/store/terminals/workspace-terminal-ssh-placeholders.ts
+++ b/src/renderer/src/store/terminals/workspace-terminal-ssh-placeholders.ts
@@ -45,10 +45,15 @@ export function addHydratedSshWorktreePlaceholders(
repos: readonly Repo[],
sourceWorktreesByRepo: Record<string, Worktree[]>,
- tabsByWorktree: Record<string, TerminalTab[]>
+ tabsByWorktree: Record<string, TerminalTab[]>,
+ additionalWorktreeIds: Iterable<string> = []
): Record<string, Worktree[]> {
const sshRepoIds = new Set(repos.filter((repo) => repo.connectionId).map((repo) => repo.id))
// Why copy-on-write: hydration writes this map straight to the store; an unconditional copy
// rerendered every whole-map selector on every hydration with no data change.
let worktreesByRepo = sourceWorktreesByRepo
- for (const worktreeId of Object.keys(tabsByWorktree)) {
+ const hydratedWorktreeIds = new Set([
+ ...Object.keys(tabsByWorktree),
+ ...additionalWorktreeIds
+ ])
+ for (const worktreeId of hydratedWorktreeIds) {
diff --git a/src/renderer/src/store/terminals/workspace-terminal-hydration.ts b/src/renderer/src/store/terminals/workspace-terminal-hydration.ts
--- a/src/renderer/src/store/terminals/workspace-terminal-hydration.ts
+++ b/src/renderer/src/store/terminals/workspace-terminal-hydration.ts
@@ -128,10 +128,19 @@ export function createWorkspaceTerminalHydrationActions(
tabsByWorktree,
activeWorktreeId,
activeTabId
})
+ const splitPairWorktreeIds = session.worktreeSplitViewPair
+ ? [
+ session.worktreeSplitViewPair.leftWorktreeId,
+ session.worktreeSplitViewPair.rightWorktreeId
+ ].filter((worktreeId) => validWorktreeIds.has(worktreeId))
+ : []
const worktreesByRepo = addHydratedSshWorktreePlaceholders(
runtimeSessionPlaceholders.repos,
runtimeSessionPlaceholders.worktreesByRepo,
- tabsByWorktree
+ tabsByWorktree,
+ splitPairWorktreeIds
)| } finally { | ||
| await orcaPage | ||
| .evaluate(() => window.__store?.getState().closeWorktreeSplitView()) | ||
| .catch(() => undefined) | ||
| rmSync(tempRoot, { recursive: true, force: true }) | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Move the temporary-repository removal to after Electron shuts down.
The test adds secondRepositoryPath to the running app through addRepoPath, so Orca watches that folder. The finally block then calls rmSync(tempRoot, ...) while the electronApp fixture is still alive. On Windows, watched worktrees stay locked until Electron and its daemons exit. In that state, rmSync throws EBUSY or EPERM, and force: true does not suppress that error. The test then fails in cleanup even when every split-view assertion passed.
The orca-app fixture provides registerPostElectronShutdownCleanup for this case. Request that fixture and register the removal through it.
Proposed fix
-test('splits worktrees from two different repositories', async ({ orcaPage }) => {
+test('splits worktrees from two different repositories', async ({
+ orcaPage,
+ registerPostElectronShutdownCleanup
+}) => {
test.setTimeout(180_000)
...
const tempRoot = mkdtempSync(path.join(os.tmpdir(), 'orca-e2e-split-view-'))
+ registerPostElectronShutdownCleanup(async () => {
+ rmSync(tempRoot, { recursive: true, force: true })
+ })
...
} finally {
await orcaPage
.evaluate(() => window.__store?.getState().closeWorktreeSplitView())
.catch(() => undefined)
- rmSync(tempRoot, { recursive: true, force: true })
}
ELI5
You can now put two worktrees side by side in one Orca window, the way you'd put two documents next to each other. Both stay live; whichever side you click is the one you type into. If you quit Orca with the split open, it comes back that way.
What Changed
Before: only one worktree is on screen. Watching an agent in one worktree while working in another means flipping back and forth in the sidebar.
After:
Mechanism:
worktreeSplitViewPair: left/right worktree id + execution host id). Input ownership is unchanged: it still belongs toactiveWorktreeId, which is always one side of the pair.isWorktreeSurfaceShown, which also counts the split partner: terminal parking, the parked-watcher sync, browser retention, the on-screen attention scan and agent auto-ack. Without this the partner's terminals would be parked and its agents' notifications treated as unseen.worktreeSplitViewPair), validated on restore withrestorableWorktreeSplitViewPair, and set in the same store update asactiveWorktreeId. It is classified in every session field table (host ownershipglobal, not transferred between profiles, not moved by the WSL mover,viewfor layout-beside).Why
Users run agents in parallel worktrees and want to keep an eye on more than one at a time.
Alternatives considered:
The new session field is optional, so sessions written by older builds load unchanged and older peers ignore it.
Linked Issue
Fixes #31015
Visual Proof
Same seeded repo, captured from the Electron E2E harness (prompt shortened, first-run onboarding hints hidden).
Testing
Verified on macOS through the Electron E2E runs below (local worktrees, and two worktrees from different repositories), including a screenshot of the restored split after relaunch. Not run on Linux, Windows or a live SSH host; the SSH restore paths are covered by unit tests.
Automated:
worktree-split-view.test.ts(slice),worktree-split-view-pair.test.ts(restore validation),worktree-split-view-session-restore.test.ts(restore in the same update, missing side, old sessions, SSH repo not yet listed, scoped merge in/out of the pair, persistence, trip to settings), plus the session field-coverage tests.--project electron-headless):worktree-split-view.spec.ts(side by side + input follows the click; two repositories) andworktree-split-view-restart.spec.ts(quit and relaunch restores both sides and the focused side).pnpm lint,pnpm tc, the affected unit tests (1,268 files) and the desktop build (build:desktop, including the mobile web bundle) pass.build:native, where the macOS Swift helpers fail because the local Xcode Command Line Tools install is broken (swift-packageaborts with a missing symbol). This PR touches no native or Swift code.AI Disclosure
Implemented with Claude Code (Claude Opus 5.5), including the tests. An independent agent review was run on the branch; its two medium findings are fixed in the last commit.
Review
Agent skill upstream boundary
Notes
Ensure no issues in: Security, Cross-platoform support (Linux, Windows, Mac), Remote SSH, Mobile, general backwards compatibility, performance
Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred)🤖 Generated with Claude Code