Skip to content

A terminal beside the agent: zellij in every container, generically (no dotfiles, any image) #242

Closed

Description

@blooop

Graduated from map blooop/dotfiles#13's terminal-beside-the-agent item after a live investigation (2026-08-16). The earlier shape (dotfiles#14) put this in chezmoi; this ticket replaces that approach with a devlaunch-owned one that works in any image, including third-party devcontainers we do not control.

What this buys

A terminal opened from inside the agent session, rather than by attaching a second one from elsewhere. Proven live: with zellij present in the container, a completely non-interactive command opens a working pane.

Measured facts the design rests on

The capability works with no TTY anywhere. Through real devpod ssh --command (verified [ -t 0 ] → no) against mcr.microsoft.com/devcontainers/base:ubuntu:

  1. zellij attach -b agentsess — creates a detached session from a non-TTY command, exit 0.
  2. A separate non-TTY command: zellij list-sessions → the session is there.
  3. A separate non-TTY command: zellij -s agentsess action new-pane -- sh -c "touch /tmp/PANE_PROOF; sleep 30" → printed terminal_1, exit 0, and /tmp/PANE_PROOF existed with sleep 30 running.

ZELLIJ_SESSION_NAME does not need setting — -s <name> is sufficient and is the robust form. Bare zellij action new-pane also works by falling back to the sole active session, which is ambiguous once there are two; do not depend on it.

The one wrap point. dl <spec> -- <cmd> funnels through a single payload = f"bash -lc {shlex.quote(command)}" in workspace_ssh. Wrapping there covers wayfinder and aid without either changing, and because it is a login shell the PATH prepend that tool provisioning writes is already in effect. A bare dl <ws> attach passes command=None down the interactive branch and needs a second small edit in the same function.

Where zellij comes from — two routes, both measured.

Container-side pixi install (recommended). tools.py already provisions tools into any container, dotfiles-independent by design, and its fallback already bootstraps pixi (_pixi_bootstrap). zellij is a conda-forge package, and package downloads now hit the shared cache from #232/#240, so a second container onward pays extraction only. This is a third row in an existing table rather than new machinery, and it adds no new upstream dependency.

Host-side static binary mount (recorded alternative). The upstream zellij-x86_64-unknown-linux-musl release is a genuine single static file (52MB, static-pie). Verified: it runs unchanged in glibc (ubuntu:24.04) and musl (alpine) images as non-root uid 1000; devpod accepts a file-level --mount and passes it through verbatim; the binary lands executable at /usr/local/bin/zellij, which is on PATH in every image tested. Faster (no per-container work) but it needs a host-side download of a GitHub release, which is a new upstream dependency for a cost measured in seconds.

Do not try to lend the host's pixi copy. ~/.pixi/bin/zellij is a pixi trampoline, and the real binary behind it is dynamically linked against libssl/libcrypto inside its own env prefix, so it does not run when copied out. _gh_source already resolves trampolines for gh; that machinery does not rescue zellij. Only the musl release is single-file portable.

Traps already paid for elsewhere

Done when

  1. Every container devlaunch launches has a working zellij on PATH, with no dependence on the user's dotfiles and no edit to any repo's devcontainer.json. Take the container-side pixi route unless measurement contradicts the falsification condition below; record which and why.
  2. Provisioning zellij must not be able to fail a launch — the same "cost the feature, not the launch" rule the launch lock and the pixi cache already follow. A container that ends up without zellij still opens, and the wrap in (3) degrades to running the command directly.
  3. An opt-in wrap, default off: with it enabled, the command dl <spec> -- <cmd> runs inside a zellij session (zellij attach -c <session> -- <cmd> or equivalent), so zellij action new-pane works from within the agent. Default off means no existing invocation changes meaning. A bare dl <ws> attach should behave consistently with whatever is decided for the command form; say what it does either way.
  4. README documents the capability, how to enable it, the -s <name> form for opening a pane from inside a session, and that existing workspaces need dl <workspace> recreate.
  5. CHANGELOG under ## [Unreleased].

Falsification condition

If a warm container-side pixi global install zellij costs more than a few seconds, the cost stops being negligible and the static-binary mount becomes the better trade — every fact needed to switch is recorded above. Measure it and put the number in the PR; do not choose on taste.

Explicitly out of scope

Session persistence across dl <ws> recreate (zellij's serialization lives in the container and dies with it), and any host-side zellij integration — a container-side session is the only architecture that works, since an agent inside the container cannot reach a host socket.

This ticket was generated by AI during investigation follow-through; every measurement above was taken live on 2026-08-16.

Activity

  1. self-assigned this
    on Aug 16, 2026
  2. blooop commented on Aug 16, 2026

    @blooop
    OwnerAuthor

    breadcrumb: Building this as: (a) zellij provisioned container-side by pixi, (b) an opt-in, default-off wrap on the one command payload.

    The falsification measurement is in, and it does not fire. A warm pixi global install zellij inside devcontainers/base:ubuntu against the shared package cache, measured over three fresh containers: 0.56s, 0.23s, 0.23s. Cold (empty cache) it is 3.0s, and the cache it fills is 167MB. That is comfortably inside "a few seconds", so the container-side pixi route is taken and the static-musl-binary mount stays a recorded alternative. No new upstream dependency, no host-side download.

    Seams I will test at, all three pre-named by the ticket:

    1. The provisioning script text and the setup pass — that zellij is asked for, that asking is skipped when it is already there, and that a zellij install failing cannot change what the pass reports about the tools the launch actually needs. This is not a third row in the required-tools table: that table is also what the probe asks about and what the host lends, so a container holding gh and a real claude but no zellij would start reporting "tools missing" and pay a ~300MB claude lend on every launch, and would then never install zellij anyway because the lend short-circuits ahead of the install trip. zellij rides the setup pass as its own stage instead, where a failure is already contained and named by construction.
    2. The command payload in the one wrap point — the exact argv devpod is handed, with the switch off and on, in the idiom of the existing payload pins.
    3. Spawn counts — that the feature adds no devpod round trip on either the warm or the cold path.

    Open question I am deciding rather than parking, and flagging for review: putting the agent inside a zellij pane would take its stdout and its exit status away from dl, which every scripted dl <ws> -- cmd depends on. Since the ticket's own measurement shows zellij -s <name> action new-pane works from a command that is not in any session, the wrap ensures the named session exists and then runs the command beside it, stdio and exit status untouched. The capability the ticket is for is delivered exactly; the blast radius is nil. Details and what a bare attach does will be in the PR.

  3. blooop commented on Aug 16, 2026

    @blooop
    OwnerAuthor

    breadcrumb: Built and open as #243, all checks green.

    The slice: zellij is provisioned container-side by pixi as a stage of the setup pass every devpod up already pays, so it adds no round trip and its failure is contained and named rather than able to fail a launch; DEVLAUNCH_ZELLIJ=1 (default off) then makes dl <spec> -- <cmd> ensure the session exists before running the command.

    Route: container-side pixi. Warm install measured at 0.56s / 0.23s / 0.23s over three fresh containers, so the falsification condition does not fire and the static-musl mount stays the recorded alternative.

    Three things a reviewer should look at, all argued in the PR body:

    1. Beside the session, not inside a pane. The deliberate reading of "or equivalent". A pane would take the command's stdin, stdout and exit status away from dl, and the capability does not need it -- -s <name> action new-pane already works from a command in no session at all.
    2. Restart, not recreate. Because the measured route uses no bind mount, an existing workspace picks zellij up on dl <workspace> restart. The done-when said recreate, which was written for the mount route. The README documents the true, weaker requirement.
    3. zellij is not in REQUIRED_TOOLS, and there is a test whose whole job is to keep it out: that tuple drives the probe and the lend, so a row there would put every already-good container on the ~300MB lending path forever and still never install zellij.

    A bare dl <ws> attach does nothing different, switched on or off -- it sends no command to wrap, which is exactly what earns it a pty from devpod. The session is reachable by hand from the shell it hands over.

    Also worth recording for whoever touches this next: zellij attach -b <name> exits 1 when the session already exists, which is the common path, so the || true in the payload is load-bearing rather than defensive.

  4. blooop commented on Aug 16, 2026

    @blooop
    OwnerAuthor

    Two-axis review (Standards + Spec) on PR #243: Approve — both axes independently, residues resolved on the branch at c18ff8a.

    The falsification condition did not fire. An axis that did not write the code re-measured a warm pixi global install zellij on its own fresh containers: 0.43s / 0.20s / 0.83s (cold 2.27s, filling 167MB), against the PR's 0.56/0.23/0.23 and 3.0s — same order of magnitude. So the container-side pixi route stands on measurement, and the static-musl-binary mount stays the recorded alternative.

    One correction to this ticket's suggested mechanism, made by the builder and upheld by review: zellij is not a third row in REQUIRED_TOOLS. That tuple also drives probe_script and the lend path, so a row there would make every container that already has gh and a real claude report "tools missing", pay a ~300MB lend on every launch, and still never install zellij — a successful lend returns before the install trip. It rides setup_stages instead, where failure containment already exists structurally.

    Also learned here: zellij attach -b <name> exits 1 when the session already exists, which is the common path, so the || true in the wrap is load-bearing rather than defensive.

  5. added a commit that references this issue on Aug 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions