A per-tab pane-switcher sidebar for zellij, built for stacked-pane workflows where many terminal agents (Claude Code, codex, …) run side by side. It answers one question at a glance: what is running in this tab and what does it want from me?
┌ sidebar ──────────────┐
│▾ PANES ⇄ │
│● codex literature │ ← blocked/working agent state marker
│ blocked . codex │ ← state, agent, and latest prompt/status
│✓ claude planner │ ← idle known agent
│ idle . claude │
│ clkao@mac:~/git/x │
│ unknown . unknown │
└───────────────────────┘
- Lists the current tab's terminal panes; click a row to focus that pane
- The sidebar is unfocusable (tab-bar mechanism): clicks are delivered without focusing it, so it never steals your keyboard
- Per-pane status line: detected state, agent kind, and latest prompt/status, refreshed every 2s
- Plugin-only agent awareness for Claude, Codex, and Pi panes using zellij's
running-command and scrollback APIs; shell panes remain visible as
unknown . unknown - Blocked prompts outrank working prompts, with state markers in the first line and details in the dimmed second line
- Keyboard navigation mode:
j/k/arrows move a highlight,Enterjumps,Escreturns focus where it was Alt /(or the⇄header) toggles the docked 28-col rail down to a 1-col sliver and back by cycling the tab's swap layouts — panes are rearranged in place, never spawned or hidden, and a manually re-split tab keeps its arrangement (the swap set is regenerated from the live layout)- Per-tab instances that toggle independently
- Tab-bound session rows:
scripts/zellij-new-tab.shcreates a fresh managed tab, verifies its resident rail, and privately starts the checkout-local subscriber. It projects AgentsView session state (blocked / working / idle / done) into that rail only; a click focuses one exact cwd-bound terminal and leaves zero or multiple matches unbound. - Gate rows: the rail can display pending decisions and float
subspace-tuion a gate artifact with--logpointed at its decision log. Gate delivery is separate from the first tab-bound session subscriber. Floating the review tool requires theRunCommandspermission (prompted once).
Requires a Rust toolchain with the wasm32-wasip1 target
(rustup target add wasm32-wasip1).
./build.sh
# → target/wasm32-wasip1/release/zellij-sidebar.wasm
# → target/zaphodbuild.sh pins rustup's rustc explicitly because a homebrew Rust earlier
on PATH lacks the wasm std and fails with can't find crate for core.
The second artifact is an internal native sidecar; it is not an installed
command or a second launcher.
Build and install only from the primary checkout:
./build.sh
./install.shThe installer derives the primary checkout from Git's common directory and
refuses linked worktrees, even when ZELLIJ_CONFIG_DIR points to a writable
destination. It also requires every remaining Zaphod MessagePlugin in the
effective config.kdl to use the primary checkout's canonical WASM URL and
rail "1"; on a mismatch, it reports each offending URL and writes nothing.
It never rewrites keybinds. Configure persistent Alt / as NoOp before
running it.
For the one-time cleanup, build the primary checkout and point the Zaphod
navigation route at
file:<PRIMARY_CHECKOUT>/target/wasm32-wasip1/release/zellij-sidebar.wasm.
Each message block must include rail "1"; leave Alt / fail-closed. This is
the reference shape, not a worktree-install recipe:
keybinds {
shared {
bind "Alt /" { NoOp; }
bind "Alt ." {
MessagePlugin "file:<PRIMARY_CHECKOUT>/target/wasm32-wasip1/release/zellij-sidebar.wasm" {
name "navigate"
floating true
skip_cache true
rail "1"
}
}
bind "Alt Shift z" {
NewTab { layout "<ABSOLUTE_ZELLIJ_CONFIG_ROOT>/layouts/zaphod.kdl"; }
}
}
}After cleanup, rerun ./install.sh. The installer renders and parses the
layout in a disposable Zellij 0.44.3 session, renames it atomically, and
rechecks identity. A failed postflight restores the previous layout bytes.
Use live state—not the rendered file—as the final oracle:
zellij --session <session> action list-panes --json -a -g -t
zellij --session <session> action dump-layoutOn an initialized Zaphod tab, exactly one sidebar should remain before and
after Alt /; every sidebar URL in the dump must name the primary checkout
artifact.
To activate this checkout and create a fresh tab in an existing session, run:
./scripts/zellij-new-tab.sh --session WORKThe command builds this checkout, renders its WASM URL into an inline layout,
and creates exactly one new tab. It atomically updates only existing Zaphod
keybind scopes in the selected config root: Alt Shift z natively creates
that root's stored layout by absolute path, and persistent Alt / is NoOp.
It never changes an existing tab. The absolute path matters: Zellij resolves
the named layout "zaphod" form from its standing default config root, even
when the session was launched with an isolated config root.
This direct command is also the current session-row entry point. After
new-tab returns, it waits for native list-panes state to show exactly one
tiled, non-suppressed Zaphod rail with the returned stable tab ID and this
checkout's canonical WASM URL. Only then does it start one private
target/zaphod subscribe process with the same Zellij profile and session.
The sidecar reads AgentsView from http://127.0.0.1:8080 by default; pass
--agentsview-url URL or set ZAPHOD_AGENTSVIEW_URL to use another endpoint.
Do not run the sidecar yourself.
If that exact rail never appears, the command reports
sidecar-target-unready, starts no sidecar, and preserves the newly created
tab for inspection. After it starts, target-tab loss, a delivered SIGINT or
SIGTERM, source EOF, or a source failure ends the sidecar. It does not
restart, retarget, or clean up AgentsView, Zellij sessions, tabs, panes, or
plugins.
Alt Shift z remains a tab-only shortcut. It cannot safely start the
subscriber because a native Zellij Run keybind materializes a helper pane.
Named pipes remain session-wide broadcasts, so the rail accepts a session
event only after a fresh PaneUpdate then TabUpdate maps its display
position to the exact stable recipient-tab-id. CWD is used only after that
check to focus a pane in the accepted rail.
When a tiled Zaphod rail is visible, approve its Reconfigure permission.
The rail requests a temporary runtime Alt / route to its own already-running
plugin; the persistent binding remains NoOp. reconfigure() has no
acknowledgement, so only a received literal keybind pipe at the active tiled
rail is allowed to toggle the docked/sliver layout. Sprint 1 proves this
ordinary journey for one attached client; second-client delivery is a named
follow-up. The separately tracked v3 hardening task must still prove that a
tiled sidebar-shaped unmanaged resident cannot qualify for that route; visual
shape or a URL substring is not managed ownership.
Use ZELLIJ_CONFIG_DIR, ZELLIJ_CONFIG_FILE, and ZELLIJ_DATA_DIR to run it
against an isolated profile. The current invocation creates its tab at once;
restart the Zellij server before relying on a newly written native keybind.
Run the real-key boundary with:
./tests/zellij-tmux-smoke-test.sh
./tests/zellij-two-rail-recipient-smoke-test.shThey use the isolated tmux smoke harness. The second test proves that a stable-tab recipient reaches only its target rail even when a bystander rail has the same terminal CWD.
scripts/zellij-worktree-test-profile.sh is parked experimental evidence. It
is not the candidate test path and does not define Alt / safety. Use the
isolated tmux smoke harness for every
real-key candidate check.
On first normal launch, the pane shows a permission prompt
(ReadApplicationState, ChangeApplicationState, ReadPaneContents,
Reconfigure, RunCommands) — focus it and approve once. Zellij's grant
cache is keyed by the raw WASM path; the smoke harness redirects HOME to a
temporary root and uses a deliberately pre-granted fixture, so it never
writes the operator's cache or fakes consent with keystrokes. Reconfigure
changes only runtime keybinds; Zaphod does not save that route to disk.
RunCommands is required only when a gate row floats subspace-tui.
Working prototype (zellij 0.44.3): per-tab toggle, click/keyboard switching,
plugin-local agent awareness, tab-bound session rows from the direct script,
state/status lines, and docked/sliver toggle. Create a rail with
scripts/zellij-new-tab.sh or the initialized Alt Shift z binding; use the
direct script when session rows are wanted. Alt / never creates or
retrofits a tab.
SPEC.md records the shipped prototype and its numbered Zellij plugin landmines, including the historical rebuild guidance. For the evergreen product direction—one managed tab or window, portable dock, workspace hub, and multiplexer drivers—see docs/zaphod-workspace-architecture.md.
cargo test— pure-function tests (row building, click mapping, toggle decisions); a host-side stub satisfies the wasm import at link time- The zellij server log is the oracle:
$TMPDIR/zellij-<uid>/zellij-log/zellij.log(permission denials, real compiles vs cache hits, wasm crashes) - Headless bench:
zellij attach bench --create-background, then drive it withzellij --session bench action …