Skip to content

fix(app): give a sandbox its project so New session lands in it - #221

Merged
quickbeard merged 1 commit into
mainfrom
sandbox-project-seed
Aug 13, 2026
Merged

quickbeard merged 1 commit into
mainfrom
sandbox-project-seed

Conversation

@quickbeard

Copy link
Copy Markdown
Owner

Problem

New session on a sandbox silently starts the session on the local server. The sandbox looks selected in the sidebar while the agent runs on the host — pwd in a "sandbox" session prints a host path.

Confirmed against live state on a Linux machine. The guest's own database had run zero sessions:

project            1  ->  /workspace   (vcs=git)
project_directory  1  ->  /workspace
session            0
message            0

while the host sidecar's opencode.db held the sessions the user thought were sandboxed.

The app's persisted server store shows why:

scope 'local':               [{"worktree": "/home/.../codev-storage"}]
scope 'sandbox:codev-code':  [{"worktree": "/home/codev/.cache/codev"}]
scope 'sandbox:codev-work':  (absent)

A sandbox exposes exactly one directory — the project folder it was created with, at a fixed guest path (/workspace on vz and bwrap, the wslpath translation on WSL) — and the guest registers it as its own project at boot. But the app's per-server project list starts empty and nothing ever fills it: every projects.open call site is a user opening a folder.

With no project for that server:

  • home-model.openNewSession → if (!conn || !project) return — a silent no-op.
  • the titlebar's openNewTab fallback chain → walks past the sandbox to server.key, then to the first server with any project — the local one.

The sandbox:codev-code row above shows the manual escape hatch is a trap too: the directory picker opens on the server's home (/home/codev), not its worktree, so the user ended up adding /home/codev/.cache/codev as their "project".

Fix

Seed a sandbox's project list from the server itself, reading sync.data.path.directory — which every server ctx already fetches as part of its bootstrap, so it needs no new request and stays backend-agnostic (no hardcoded /workspace, which would be wrong on WSL). An explicit close is respected: re-opening it would make the sandbox's only project impossible to remove.

The logic is fork-only (packages/app/src/sandbox/projects.ts). context/global.tsx gains an import and one call — SandboxesProvider is mounted above GlobalProvider in app.tsx, so it cannot reach ensureServerCtx and the call has to live in the Global init. DIVERGENCES.md records that.

Verification

Six tests in test-browser/sandbox-projects.test.ts drive the real function under real Solid reactivity (the parameter type is structural precisely so this needs no ctx mock): seeds from directory, falls back to worktree, ignores non-sandbox servers, skips a sandbox that already has a project, respects a closed project, and seeds a sandbox that becomes ready later.

bun typecheck, bun run test:unit (657 pass), and bun run test:browser (51 pass) all clean in packages/app.

The sandbox runtime itself was verified healthy in the same investigation — /path reports worktree: /workspace, /project lists it, /config carries the brokered gateway, and egress is correctly limited to the broker (models.dev and registry.npmjs.org fail with EAI_AGAIN inside the guest). The gap was purely that the app never told the sandbox server which project to open.

Follow-up not in this PR

The directory picker opening on path.home rather than path.directory is what produced the /home/codev/.cache/codev project above. Seeding makes that path unnecessary for the common case, but the picker default is still worth revisiting — it affects any server whose home and worktree differ.

🤖 Generated with Claude Code

New session on a sandbox silently started the session on the local server:
the sandbox looked selected while the agent ran on the host.

A sandbox exposes exactly one directory — the project folder it was created
with, at a fixed guest path (/workspace on vz and bwrap, the wslpath
translation on WSL) — and the guest registers it as its own project at boot.
The app's per-server project list, though, starts empty and nothing ever
fills it: projects are only ever added by a user opening a folder. With no
project for that server, home-model.openNewSession returns early and the
titlebar's openNewTab fallback chain walks on to another server.

Seed it from the server itself, reading sync.data.path.directory, which
every server ctx already fetches, so it stays backend-agnostic. An explicit
close is respected — re-opening it would make the sandbox's only project
impossible to remove.

The logic is fork-only (src/sandbox/projects.ts); global.tsx gains an import
and one call, since SandboxesProvider is mounted above GlobalProvider and
cannot reach ensureServerCtx.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@quickbeard
quickbeard merged commit 0694b9c into main Aug 13, 2026
0 of 5 checks passed
@quickbeard
quickbeard deleted the sandbox-project-seed branch August 13, 2026 07:34
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