Skip to content

feat(worktree): docker isolation toggle and image build flow#1350

Draft
mvanhorn wants to merge 4 commits intostablyai:mainfrom
mvanhorn:feat/docker-worktree-toggle
Draft

feat(worktree): docker isolation toggle and image build flow#1350
mvanhorn wants to merge 4 commits intostablyai:mainfrom
mvanhorn:feat/docker-worktree-toggle

Conversation

@mvanhorn
Copy link
Copy Markdown
Contributor

@mvanhorn mvanhorn commented May 2, 2026

Summary

Adds an opt-in "Isolate" toggle per worktree row plus a per-repo "Default isolation" setting. When enabled, Orca builds (or reuses cached) a Docker image via the foundation in #1349 and persists isolation: 'docker' on the worktree. Default off, worktree-native stays the zero-friction default.

This is PR 2 of 3 in the docker isolation series. PR 1 (#1349) shipped the provider foundation. PR 3 (TBD) wires the dispatch so the persisted isolation actually routes execution through the Docker provider triple.

Why this lands as its own PR: keeping persistence + UI separate from dispatch routing keeps each review surface narrow. Maintainers can review the worktree shape change and the toggle UX here without also reviewing changes to provider-dispatch / orca-runtime.

Screenshots

UI changes are limited to:

  • A small "Isolate" icon toggle on each worktree row in the sidebar (idle / building / on / disabled-no-docker / disabled-ssh-repo states)
  • A new "Default isolation for new worktrees" toggle in Repository Settings

Screenshots will be added before merge once the dispatch wiring (PR 3) lands so the toggle does something visible end-to-end. PR 2 in isolation produces a build spinner and persists the choice; the visible payoff is in PR 3.

Testing

  • pnpm lint
  • pnpm typecheck
  • pnpm test --run src/renderer/src/components/sidebar/IsolateToggleButton.test.tsx src/main/ipc/docker.test.ts src/renderer/src/store/slices/worktrees.test.ts src/renderer/src/components/settings/RepositoryPane.test.tsx
  • pnpm build
  • Added 12 new tests (component states, IPC handlers, store slice reducer, persistence). The component test covers off / building / on / disabled-no-docker / disabled-ssh-repo states. The IPC test covers engine status caching, build progress emission, SSH repo rejection, and setWorktreeIsolation persistence.

AI Review Report

A code review pass was run before submission. Risks checked and findings:

  • Cross-platform compatibility: macOS / Linux / Windows toggle visibility (Docker engine detection from feat(providers): docker provider foundation (pty / filesystem / git) #1349 governs the disabled state). Tooltip text avoids platform-specific symbols.
  • Persistence shape: isolation: 'host' | 'docker' defaults to 'host' for existing worktrees that don't carry the field. Repo-level defaultIsolation?: RepoIsolationDefault is optional so existing repo records stay forward-compatible.
  • IPC handler hygiene: each handler validates inputs, returns errors as {error: string} rather than throwing, and uses mainWindow.webContents.send for progress events. The 30s engine-status cache is module-scoped with a deterministic guard.
  • File naming: concrete domain names (IsolateToggleButton, docker.ts). No helpers / utils introduced.
  • Project types in .ts, not .d.ts.

Two findings surfaced. One was fixed in this PR; one is intentionally deferred to PR 3:

  • Fixed: SSH repos (those with repo.connectionId) cannot run a local Docker build against a remote path. Added a guard in src/main/ipc/docker.ts that rejects the build with a clear error, and the toggle disables itself for SSH-mounted repos with the tooltip "Docker isolation isn't available for SSH-mounted repos.". SSH-repo container isolation is a separate problem (would route the build through the SSH provider) and is out of scope for this series.
  • Deferred to PR 3: the toggle persists isolation: 'docker' but no terminal/agent spawn path reads worktree.isolation yet, so today the metadata is set without affecting execution. This is intentional series progression — PR 3 wires provider-dispatch.ts and orca-runtime.ts so the persisted choice actually routes pty / filesystem / git operations through the Docker provider triple from feat(providers): docker provider foundation (pty / filesystem / git) #1349. Splitting persistence from routing keeps each PR's review surface narrow.

Security Audit

Input validation: setWorktreeIsolation accepts only 'host' or 'docker'; any other value rejects with an error. buildImage rejects SSH repos before any docker shell-out.

Command execution: the only docker shell-out goes through the engine client introduced in #1349, which builds argv arrays for child_process.spawn (no shell). User strings (repo path, Dockerfile path) flow as separate argv entries.

Path handling: Dockerfile resolution uses path.join. Bind-mount paths route through the helper from #1349 with cross-platform tests.

Auth, secrets, IPC surface: three new IPC handlers (docker:engine-status, docker:build-image, docker:set-worktree-isolation) plus a docker:build-progress event. No new outbound network surface from Orca itself. No secrets or credentials touched. No auth flow changes.

Persistence: settings written to the existing local store via the cardProps pattern from PR #1293. No new disk paths.

Follow-up: PR 3's dispatch wiring will need its own audit covering routing decisions, container lifecycle (spawn / hibernate / terminate), and the Images panel.

Notes

This PR builds on #1349. Because that PR isn't merged yet, this PR's diff against main includes PR 1's commits as well; the net diff vs main is cumulative. After #1349 merges, this branch will rebase to drop the duplicate commits.

Series:

Open as draft to make the dependency obvious. Will mark ready for review once #1349 merges.

X: @mvanhorn

AI was used for assistance.

mvanhorn added 4 commits May 1, 2026 23:36
Adds Docker as a fourth provider triple alongside local and SSH.
Pure plumbing: pty / filesystem / git providers + docker engine
detection + image build + container lifecycle + bind-mount path
resolution. No user-facing UI yet, no dispatch wiring.

Mirrors the SSH provider triple's API. Cross-platform: macOS
(Docker Desktop / Colima), Linux (Docker Engine, rootless), Windows
(Docker Desktop WSL2 named pipe).

Tests cover happy and error paths through a docker-engine-fake.ts
recorder, so no real Docker daemon is required.

Foundation PR for the docker isolation series. Follow-ups will add
the user-facing toggle and dispatch wiring.
- Allocate TTY for Docker PTY sessions via tty flag on spawnExec, so
  isatty() returns true for shells, vim, and ncurses programs.
- Return originalContent + modifiedContent for working-tree diffs
  instead of returning a unified patch as modifiedContent.
- Return blob contents for branch diffs and honor options.oldPath for
  renames, matching the local provider's GitDiffResult shape.
- Preserve the new path as path and the old path as oldPath when
  parsing R100 rename entries (was reversed).
- Surface git porcelain v2 u records as unresolved conflict entries
  so docker worktrees in conflict show their conflicted files.
- Resolve repo default branch via origin/HEAD before browseFile,
  instead of hardcoding 'main' for remote file URLs.
Adds an opt-in "Isolate" toggle per worktree row. When enabled, Orca
builds (or reuses cached) a Docker image via the foundation in the
docker provider series and persists the choice. Default off.

Repo-level default toggle in Repository Settings: "Isolate worktrees
from this repo by default."

Implementation:
- 3 new IPC handlers in src/main/ipc/docker.ts (engine-status,
  build-image, set-worktree-isolation) with build-progress events
- IsolateToggleButton.tsx covers off / building / on / disabled-no-docker
  states with vitest coverage
- RepositoryPane gains the per-repo defaultIsolation toggle
- Worktree shape extends with isolation field; defaultIsolation flows
  through worktree-remote so new worktrees inherit the repo default
- vitest.config.ts updated so .test.tsx files are discovered for the
  new component tests
Building a local Docker image against an SSH repo's remote path would
fail or target the wrong machine. Add a guard in both the main process
and the renderer:

- buildImage IPC returns an explicit error when repo.connectionId is set
- IsolateToggleButton disables itself for SSH repos with a clear tooltip

Local-only Docker isolation is the v1 surface. SSH-repo container
isolation is a separate problem (would require routing the build
through the SSH provider) and is out of scope for this series.
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