Skip to main content

zellij

Session, layout, and pane management for Zellij terminal multiplexer. Use when managing Zellij sessions, layouts, or automating terminal workflows.

Source facts

Repository
ryanmaclean/skills
Last source activity
April 15, 2026 at 03:58
Detected SKILL.md language
English
Stars
1
Forks
0

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
zellij
description
Session, layout, and pane management for Zellij terminal multiplexer. Use when managing Zellij sessions, layouts, or automating terminal workflows.
# Zellij — Session, Layout, and Pane Management ## Core rule: never hardcode the session name Zellij injects `$ZELLIJ_SESSION_NAME` into every process inside a session. Always read it — never write `"pmr"`, `"bop"`, or any literal name. ```nu # Nushell — canonical zellij_session function used in all dispatch.nu files def zellij_session [] { if "ZELLIJ_SESSION_NAME" in $env { $env.ZELLIJ_SESSION_NAME } else { # Running outside Zellij — auto-detect let sessions = (^zellij list-sessions --no-formatting 2>/dev/null | lines | where { |l| ($l | str trim) != "" }) if ($sessions | length) == 1 { $sessions | first | str trim } else if ($sessions | length) > 1 { error make { msg: $"Multiple sessions active: ($sessions | str join ', '). Set ZELLIJ_SESSION_NAME." } } else { error make { msg: "No Zellij session found. Start one with: zellij --session <name>" } } } } ``` ```bash # Bash equivalent SESSION=${ZELLIJ_SESSION_NAME:-$(zellij list-sessions --no-formatting | head -1 | awk '{print $1}')} ``` ## Sessions ```bash # Start or resume (canonical pattern — no hardcoded name) zellij attach --create bop # bop project zellij attach --create efi # efi/dub project zellij attach --create zam # zam project # List active sessions zellij list-sessions zellij list-sessions --no-formatting # machine-readable, no ANSI # Detach without killing Ctrl+p d # Kill a session zellij kill-session <name> ``` ## Layouts Each project has a `.kdl` layout file: | Project | Layout | Launch | |---------|--------|--------| | bop | `layouts/bop.kdl` | `zellij --session bop --layout layouts/bop.kdl` | | efi | `layouts/efi.kdl` | `zellij --session efi --layout layouts/efi.kdl` | | zam | `layouts/zam.kdl` | `zellij --session zam --layout layouts/zam.kdl` | ### bop.kdl structure Two tabs: `bop` (primary) and `dispatch` (engine runners). **bop tab** (7-pane card lifecycle layout): ``` ┌──────────┬──────────────────┬──────────────────┐ │ BOARD │ SPEC │ QA │ │ watch │ what to build │ did it work? │ │ status ├──────────────────┼──────────────────┤ │ │ STDOUT │ STDERR │ │ │ agent output │ agent errors │ ├──────────┴──────────────────┴──────────────────┤ │ INSPECTOR — bop inspect <id> │ ├─────────────────────────────────────────────────┤ │ SHELL — bop commands (focus/retry/approve) │ └─────────────────────────────────────────────────┘ ``` **dispatch tab**: dispatcher (75%) | merge-gate (75%) | dispatch-shell (25%) ### KDL quick reference ```kdl layout { default_tab_template { children pane size=2 borderless=true { plugin location="zellij:status-bar" } } tab name="main" focus=true { pane split_direction="horizontal" { pane name="top" size="70%" { command "nu" } pane name="bottom" size="30%" { command "nu" } } } tab name="logs" { pane name="log-tail" { command "nu" args "-c" "tail -f /tmp/bop.log" } } } ``` `split_direction="horizontal"` = children stacked top/bottom. `split_direction="vertical"` = children side by side. ## Spawning panes from scripts ```bash # Fire-and-forget — pane closes when command exits zellij --session $ZELLIJ_SESSION_NAME run \ --name my-card \ --close-on-exit \ -- sh -c "nu /path/to/adapter.nu ..." # Inspectable — pane stays open after command exits (for debugging) zellij --session $ZELLIJ_SESSION_NAME run \ --name my-card \ -- sh -c "nu /path/to/adapter.nu ..." # Open a new tab zellij --session $ZELLIJ_SESSION_NAME action new-tab --name "logs" # Navigate to an existing tab by name zellij --session $ZELLIJ_SESSION_NAME action go-to-tab-name "bop" ``` **Use `--close-on-exit` for**: dispatch worker panes (adapter runs). **Omit `--close-on-exit` for**: interactive panes, debugging, long-lived agents. ## PATH gotcha — critical for dispatch workers `nu` (Nushell) is **not** in PATH inside Zellij panes spawned via `zellij run`. Always use the full path: ```bash # WRONG — fails silently in detached pane context nu dispatch.nu mark-done 022 # CORRECT /opt/homebrew/bin/nu dispatch.nu mark-done 022 ``` Same applies to `bop` binary: use `$ROOT/target/debug/bop` or add to PATH explicitly: ```bash export PATH=$HOME/bop/target/debug:$PATH ``` ## Suspending TUI to drop to shell (`!` key in bop ui) **Do NOT use `Ctrl+O`** — that is Zellij's built-in Session mode key and will be intercepted before reaching any app running inside a pane. `bop ui` uses `!` (normal mode, no modifier) for the subshell drop, inspired by vim's `:!` convention. Zellij does not intercept bare printable keys in normal mode. ```rust // Triggered by KeyCode::Char('!') in the AppEvent::Key handler // Suspend TUI crossterm::terminal::disable_raw_mode()?; crossterm::execute!(stdout(), crossterm::terminal::LeaveAlternateScreen)?; // Run shell in card's worktree std::process::Command::new(std::env::var("SHELL").unwrap_or("sh".into())) .current_dir(&card_worktree) .status()?; // Restore TUI crossterm::execute!(stdout(), crossterm::terminal::EnterAlternateScreen)?; crossterm::terminal::enable_raw_mode()?; // trigger full redraw ``` This does NOT open a new Zellij pane — it suspends and restores within the existing pane. Works correctly inside Zellij because alternate screen is stacked (Zellij has its own alternate screen above it). ## Zellij keys already taken — do NOT use in TUI apps These are intercepted by Zellij before reaching pane content: | Key | Zellij action | |-----|---------------| | `Ctrl+O` | Session mode (session manager) | | `Ctrl+P` | Pane mode | | `Ctrl+T` | Tab mode | | `Ctrl+N` | New pane | | `Ctrl+H` | Move pane focus left | | `Ctrl+B` | Scroll up | | `Ctrl+F` | Scroll down | | `Ctrl+G` | Unlock (in locked mode) | **Safe to use in TUI apps**: bare printable keys (`!`, `e`, `n`, `r`, etc.), `Alt+<key>` (Zellij uses Alt+arrow but not most Alt+letter combos), function keys `F1`–`F12`. ## Key bindings (Zellij default — navigation) | Key | Action | |-----|--------| | `Alt + arrow` | Move focus between panes | | `Alt + n` | New pane | | `Alt + [` / `]` | Previous / next tab | | `Ctrl+p d` | Detach from session (session stays alive) | | `Ctrl+p z` | Toggle pane fullscreen | | `Ctrl+p x` | Close focused pane | | `Ctrl+p r` | Rename focused pane | ## Integration with dispatch.nu (all three projects) All three projects (bop, efi, zam) use the same `zellij_session` pattern: ``` dispatch.nu run --wave N --yes → zellij_session [] # reads $ZELLIJ_SESSION_NAME → zellij --session <name> run --name <spec-id> --close-on-exit -- sh -c <AC cmd> → pane runs Auto-Claude agent → on exit: mark-done / mark-failed called via /opt/homebrew/bin/nu ``` Lock file (`dispatch-lock.json`) records `session: (zellij_session)` so rescue operations can target the correct session. ## Nested panes — bop ui card tabs (spec 029 future) Each running card will optionally get a tab with nested panes: - Top pane: adapter stdout (live) - Bottom pane: log tail (`logs/stderr`) Tab naming convention: `card-<id>` (e.g. `card-feat-auth`). Created via `zellij action new-tab --name card-<id>`, then pane splits via `zellij action new-pane --direction down`. ## Projects and session names | Project | Canonical session | Layout | |---------|-------------------|--------| | bop | `bop` | `layouts/bop.kdl` | | efi (dub) | `efi` | `layouts/efi.kdl` | | zam | `zam` | `layouts/zam.kdl` | These are current names — they WILL change. Always use `$ZELLIJ_SESSION_NAME`. ## Quick reference ```bash # Get current session name (canonical method) SESSION=${ZELLIJ_SESSION_NAME:-$(zellij list-sessions --no-formatting | head -1 | awk '{print $1}')} # Start/attach to session zellij attach --create <session-name> # List sessions zellij list-sessions --no-formatting # Run command in new pane zellij --session $SESSION run --name pane-name --close-on-exit -- command # Detach without killing Ctrl+p d # Navigate between panes Alt + arrow keys ``` ## Common mistakes - **Hardcoding session names**: Never write literal names like "bop" or "efi" — always read `$ZELLIJ_SESSION_NAME` - **Using Ctrl+O in TUI apps**: Zellij intercepts this key for Session mode — use bare printable keys like `!` instead - **Not using full paths to binaries**: `nu` and other tools are not in PATH inside spawned panes — use full paths like `/opt/homebrew/bin/nu` - **Forgetting --close-on-exit**: Worker panes should close when done to avoid resource leaks - **Using Ctrl+O for subshell drop**: Use bare `!` key instead, as Ctrl+O is intercepted by Zellij
View on GitHub