aboutsummaryrefslogtreecommitdiffstats

Handover: pi side of workspace hints

The zellij half is done and on origin/main (sl0ppij, zellij --version → 0.45.0+sl0p1). This describes exactly what the terminal now gives you, what it deliberately does not, and the traps that will cost you an afternoon each.

Design rationale (and what was rejected): sl0p/workspace-hints-proposal.md.

The contract

A pane can carry one opaque string called its purpose. zellij never interprets it. You own the entire vocabulary. Nothing in the terminal knows or cares what dev-server means, and nothing enforces anything based on it.

That is deliberate: it means you can change the vocabulary whenever you like without touching or rebuilding zellij.

Reading purposes

$ zellij action list-panes --json
[
  { "id": 0, "title": "editor",   "tab_id": 0, "tab_name": "work", "purpose": "human-editor", ... },
  { "id": 1, "title": "server",   "tab_id": 0, "purpose": "dev-server", ... },
  { "id": 2, "title": "plain",    "tab_id": 0, ... },
  { "id": 3, "title": "buildjob", "tab_id": 0, "purpose": "agent:build", ... }
]
  • the key is omitted entirely when a pane has no purpose — do not expect null
  • id + is_plugin identify the pane; the CLI id form is terminal_<id> / plugin_<id>, which is what every -p/--pane-id flag wants
  • tab_id, tab_position, tab_name come along, so you can tell whether a pane is in the agent's own tab

Find a pane by purpose:

id=$(zellij action list-panes --json \
     | python3 -c 'import json,sys; print(next((f"terminal_{p[\"id\"]}" for p in json.load(sys.stdin) if p.get("purpose")=="dev-server"), ""))')

Writing purposes: two paths, and they are not equal

1. The workspace file (authoritative)

layout {
    tab name="work" {
        pane name="editor" purpose="human-editor"
        pane name="server" purpose="dev-server" command="npm" { args "run" "dev"; }
        pane name="logs"   purpose="logs"       command="tail" { args "-f" "log/dev.log"; }
        pane name="agent"  purpose="agent-scratch"
    }
}

Works on tiled and floating panes. Start it with:

zellij --layout .pi/workspace.kdl attach --create myproject

Not zellij -s NAME --layout FILE — that means "add this layout as a tab to existing session NAME" and fails if the session does not exist. This bites everyone once.

2. --purpose at spawn time (authoritative)

id=$(zellij run --no-focus --purpose agent:build --name job -- cargo build)
zellij action new-pane --no-focus --floating --purpose agent:scratch

Rejected with --in-place (that replaces another pane rather than being one), so you get an error rather than a flag that silently does nothing.

3. In-band, from inside the pane (fills blanks only)

printf '\033]5577;1;purpose;agent:build\033\\'

For a program that decides its own role as it runs. Prefer --purpose when you are the one spawning the pane.

zellij run --no-focus --name buildjob -- \
     bash -lc 'printf "\033]5577;1;purpose;agent:build\033\\"; exec cargo build'

Tab purpose

tab name="work" purpose="agent-work" { ... }

Reported by zellij action list-tabs --json as purpose. Panes carry tab_id, so join the two listings when you need "the pane in the agent's tab". Tabs have no in-band path -- a tab is not a thing a program runs inside.

The asymmetry you must not design against

A layout-declared or --purpose purpose outranks an in-band one and cannot be overridden or cleared by it. (--purpose counts as out-of-band: whoever can run the CLI can already do anything.)

Because OSC arrives in-band from whatever the pane is showing: without this rule, cat hostile.txt in the human's editor pane could relabel it agent-scratch and invite an agent to run commands where the human is working. A layout is written by a human in a file they can read; in-band data can only fill a blank.

Consequences for you:

  • a pane declared in the workspace keeps its purpose for life — do not try to re-tag it and silently succeed at nothing (the OSC is ignored, not an error)
  • an untagged pane can be tagged once by whatever runs in it
  • after resurrection everything is authoritative, because restored purposes come back through the layout. So a pane your agent tagged in-band cannot be re-tagged after a session restart. If a pane's role genuinely changes over its life, close it and spawn a fresh one — cheap, and it keeps the audit trail honest.

What is sanitised, and what that does not buy you

Values are reduced to a bounded token at both entry points: [A-Za-z0-9_.:/-], max 64 chars, and anything sanitising to nothing becomes absent. A purpose is never rendered — it is matched against and pasted into your shell/JSON pipelines, so whitespace, quotes and ; do not survive. rm -rf /; echo "x" arrives as rm-rf/echox.

This is not a trust boundary. Layout files already run arbitrary commands via command=, so a workspace file committed to a repo is already arbitrary code execution. Sanitising purpose changes nothing about that.

The real rule is yours: do not auto-open a workspace file from a repo nobody has read. Please say so plainly in the pi-side docs rather than letting the presence of a sanitiser imply safety.

Durability

Purposes survive detach/reattach and kill/resurrect. Verified: layout-declared and in-band ones both came back after kill-session + attach.

One timing detail that will confuse you while testing: the resurrection snapshot is written by a periodic serialiser (~1 min in this config), not on exit. Kill a session 20 seconds after creating it and there is nothing to resurrect and the session is simply gone. Wait for ~/.cache/zellij/contract_version_1/session_info/<name>/session-layout.kdl to exist before testing resurrection.

Suggested division of labour

Nothing below is enforced by zellij; it is convention, and it is yours to set.

purpose kind who creates it agent behaviour
human-* workspace file read with dump-screen -p, never write
dev-server, logs workspace file, self-starting commands read; restart only if asked
agent:* agent spawns and tags free to use, and close when done

The trap worth encoding: using a pane you did not create means writing keystrokes into whatever is currently running there, which types cargo build into a vim session the moment state drifts. Prefer spawning your own tagged pane over typing into someone else's. Declared panes are best for things that start themselves, or for the human.

Agent house rules that already exist

There is a skill at ~/.the assistant/agent/skills/zellij-panes/SKILL.md covering the terminal habits this builds on, all verified against the binary:

  • zellij run --no-focus puts a pane in your own tab without stealing focus — plain new-pane follows the human's focus and lands in whatever tab they are looking at
  • --no-focus resolves "your tab" from ZELLIJ_PANE_ID, and silently falls back to the human's focused tab if that variable is missing. Never strip it
  • read and close by id (dump-screen -p, close-pane -p); bare close-pane closes the focused pane, possibly the human's or the agent's own
  • --block-until-exit-success gives you the real exit code from a visible pane, so you never need to scrape output
  • never kill-all-sessions / delete-all-sessions; act only on sessions you created, by exact name

Not implemented, on purpose

Ask if you need any of these; each was left out for a stated reason, not forgotten.

  • --purpose with --in-place — an in-place pane replaces another rather than being one. Rejected by clap, not silently ignored.
  • purposes on plugin panes — plugin panes are zellij's own UI.
  • a structured meta bag — one string until it demonstrably is not enough. command/args/cwd already carry "what runs here".
  • plugin visibility — purpose is on PaneListEntry (CLI JSON), not PaneInfo, which crosses the plugin protobuf API we cannot regenerate. If a plugin needs it, that is a separate conversation.
  • any enforcement — zellij cannot enforce agent policy; an agent is just a process running commands. Purposes are advisory, permanently.

Where the code is

git log --oneline -1 → df052db4 sl0p: per-pane workspace hints (purpose=)

concern file
sanitiser, layout fields zellij-utils/src/input/layout.rs (sanitize_pane_purpose)
KDL parse + allowlist zellij-utils/src/kdl/kdl_layout_parser.rs
OSC verb, authority rule zellij-server/src/panes/grid.rs (declare_purpose_in_band)
list-panes exposure zellij-server/src/screen.rs (collect_pane_list), zellij-utils/src/data.rs (PaneListEntry)
serialise / resurrect zellij-utils/src/session_serialization.rs

Tests: cargo test -p zellij-server -p zellij-utils --lib (1499 + 464 green). Search sl0p_pane_purpose and sl0p_in_band_purpose for the behaviours above, including the override-attack case.