Skip to content

Shared project sandbox, dev containers, and tab renaming - #16

Merged
wess merged 5 commits into
mainfrom
sandbox
Jul 27, 2026
Merged

Shared project sandbox, dev containers, and tab renaming#16
wess merged 5 commits into
mainfrom
sandbox

Conversation

@wess

@wess wess commented Jul 27, 2026

Copy link
Copy Markdown
Owner

Shared project sandbox

A project's panes and its whole agent team can run inside one container
instead of on the host, turning relay-team-autonomy's permission bypass from a
promise into a boundary. Self-contained: docker or podman is the only
dependency — no VS Code, no devcontainer CLI, no image to pull.

Three decisions carry the rest:

  • One container per project, not per agent. Agents on a team need to see
    each other's edits and share a build cache; isolation between them stays in
    git worktrees inside the shared mount. Panes are refcounted, so the container
    outlives any one of them.
  • The project is identity-mounted. Git records absolute paths in a worktree,
    so under any other mapping a worktree made on one side is broken on the other.
    Equal paths keep the worktree verbs working from both sides and remove the
    need for a path translation layer anywhere.
  • Relay stays on the host. build::worker resolves the role, renders the
    harness, and writes the MCP config exactly as for a host launch; --sandbox
    changes only the final exec, rewriting the bus URL to the engine's gateway
    host and handing the agent its config at the path the container mounts it.

Project resolution walks up to the repository root so every pane in a checkout
shares one container, and refuses $HOME, /, and depth-1 paths: the sandbox
mounts what it is given, and mounting a home directory would hand an unattended
agent the whole machine.

Dev containers

A project's devcontainer.json is read when it has one and ignored when it does
not. Its image becomes the base Sinclair layers the agent CLI onto; mounts,
remoteUser, containerEnv/remoteEnv are applied; shutdownAction raises an
advisory, because closing an editor should not stop a container with a team
working in it. A container an editor already started is entered rather than
duplicated, and never stopped or removed — the user's editor is very likely
attached to it.

Tab renaming (#15)

Renaming was already an action, a View-menu item, a palette entry, and a
documented keybinding — and the dialog never painted: its host element had
no size, so the modal's absolutely-positioned backdrop, drawn in the deferred
pass, had nothing to resolve against. The feature looked missing because it was
invisible.

tab-title-show-host was dead the same way: parsed, written to settings.json,
a toggle in Settings and a promise in the help text, and no consumer. Tab labels
now honour it.

Right-clicking a tab opens a menu (rename, split, close, close others), and
every tab bar along the layout's top edge drags the window rather than only the
top-right one.

Verification

  • 1149 tests, cargo clippy --all-targets clean, release build clean.
  • The generated sandbox image was built and run against real Docker: identity
    mount, agent CLI, $HOME volume, and gateway host all confirmed inside the
    container.
  • Git worktrees round-trip both directions across the mount — created inside
    the container and read on the host, and the reverse.
  • Menus, the Containers panel, the tab context menu, tab titles, window
    dragging, and the rename dialog were all driven in the running app.

Note for review

vendor/guise moves to a commit on the sinclair-panegroup branch of
wess/guise, not main — guise's main has since retargeted to crates.io gpui
0.2.2, which this workspace does not use.

Wess Cope added 5 commits July 27, 2026 11:11
A team launched with relay-team-autonomy runs on the real machine, with the
user's credentials, with permission prompts bypassed. The sandbox puts the
project and its whole roster inside one container instead: one filesystem, one
toolchain, one blast radius.

Self-contained by design — docker or podman is the only dependency. No editor,
no devcontainer CLI, and no image to pull from a registry we would have to
publish and keep current. A project's devcontainer.json and a container an
editor already started are both used when present, and nothing degrades when
they are not.

Three decisions carry the rest:

- One container per project, not per agent. Agents on a team need to see each
  other's edits and share a build cache; isolation between them stays in git
  worktrees inside the shared mount. Panes are refcounted, so the container
  outlives any one of them.
- The project is identity-mounted. Git records absolute paths in a worktree, so
  under any other mapping a worktree made on one side is broken on the other.
  Equal paths keep the worktree verbs working from both sides and remove the
  need for a path translation layer anywhere.
- Relay stays on the host. build::worker resolves the role, renders the harness,
  and writes the MCP config exactly as it does for a host launch; --sandbox
  changes only the final exec, rewriting the bus URL to the engine's gateway
  host and handing the agent its config at the path the container mounts it.

container grows Sandbox, Recipe, Mount, adopt (label discovery plus ownership,
so a container Sinclair did not create is entered and never removed), and a
devcontainer.json reader on config's JSONC parser — all still pure argv
construction. app/sandbox resolves settings into a description and brings it up
off the render path; root/sandbox counts panes and retires the container.

Project resolution walks up to the repository root so every pane in a checkout
shares one container, and refuses $HOME, / and depth-1 paths: the sandbox mounts
what it is given, and mounting a home directory would hand an unattended agent
the whole machine.

UI: a Sandbox submenu under File and AI with a live status line, a Project
sandbox section in the Containers panel, fourteen sandbox-* settings, and six
bindable actions.
Three surfaces were missing, all of them the ones people actually reach for.

The command palette listed OS Tabs and Attach to Container but nothing for the
sandbox, so the feature was menu-only.

Progress and failures lived in a menu the user had to know to open. Bringing a
sandbox up the first time builds an image — minutes — and clicking the item
looked identical to a hang. Resolving now reveals the Containers drawer, where
the live status line and the resolve advisories already were.

A failure was worse than invisible: sandbox_fail wrote to stderr and a status
line nobody was looking at, so opening a shell from a pane with no repository
above it did nothing at all. It now surfaces the reason on screen.

show_sandbox_panel reveals rather than toggles — it is only ever called to
show something, and collapsing an open drawer would be the opposite of the
point.
Issue #15 asks for tab renaming. It was already there — `change_tab_title`, in
the View menu, in the command palette, documented — but two things stood
between the user and it.

The rename dialog never painted. Its host element had no size, so the modal's
absolutely-positioned backdrop, painted in the deferred pass, had nothing to
resolve against: the dialog was created and re-rendered every frame and was
never on screen. That is the whole of the reported problem — renaming looked
missing because it was invisible. Filling the window fixes it.

`tab-title-show-host` was dead. It parsed, it wrote to settings.json, it had a
toggle in Settings and a line in the help text promising "tabs show just the
path" — and nothing read it. The tab label now honours it, with a strip that
only fires on something genuinely shaped like `user@host:`, so
`https://example.com:8080` and `nvim: main.rs` are left alone. A title the user
typed is never rewritten. The flag rides in a cell the reload path updates, so
toggling it does not need a restart.

Right-click on a tab now opens a menu — rename, split, close, close others —
because the View menu is not where anyone looks with their hand already on a
tab. Handlers defer their action: running straight into the same window
re-enters an update in progress, which is refused, and the menu closes having
done nothing.

Window dragging comes from the guise side: every top-row tab bar drags now, not
just the corner one.
`mounts` and `remoteUser` were read out of a project's devcontainer.json and
then dropped on the floor. Both are now applied: mounts through a parser for
the engine's `type=bind,source=…,target=…` form, which is what the spec uses
and not the `-v` shorthand the settings key takes, and `remoteUser` as the
container's user unless `sandbox-user` overrides it. A project that names a
user has decided who its tooling runs as.

New tutorial, docs/devcontainers.html: opening a shell inside the environment a
repo already defines, exactly which fields are read and which are ignored,
sharing the container an editor started (and why Sinclair never stops one it
did not create), the two lines that make a devcontainer suit agents as well as
people, and what happens when there is no devcontainer.json at all. Linked from
the tutorials index, the docs overview, the nav manifest, and both directions
between it and the sandbox page.

The tab work is documented too: right-clicking a tab, dragging the window from
any top-row tab bar, and what `tab-title-show-host` actually does now that
something reads it — its reference entry promised the opposite of the old
behaviour.
@wess
wess merged commit 443f20a into main Jul 27, 2026
3 checks passed
@wess
wess deleted the sandbox branch July 27, 2026 18:02
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