aboutsummaryrefslogtreecommitdiffstats

Proposal: workspace hints (per-pane purpose)

Goal. Let a project ship a zellij layout that also tells an agent what each pane is for, so "open this project" produces a terminal arrangement the agent can navigate instead of guessing.

Status. Implemented in df052db4, except the deliberate omissions listed at the bottom. Consumer-facing docs: sl0p/workspace-hints-handover.md.

The boundary that keeps this small

zellij carries an opaque string per pane. It never interprets it. pi owns the entire vocabulary.

zellij must not learn what dev-server means, must not enforce policy, must not special-case a value. Every semantic lives in pi. Consequences:

  • the fork change is one string threaded through an existing path — no new enums, no new subsystems, no protobuf enum variants (which we cannot regenerate anyway)
  • the vocabulary can change daily without rebuilding zellij
  • zellij cannot be blamed for, or asked to enforce, agent policy

Anything in a proposal that requires zellij to understand a purpose is out of scope by construction.

Why not just use pane names (the zero-work alternative)

Tempting: pane name="agent:server" needs no fork change. It does not work, and this was checked rather than assumed:

  • list-panes --json exposes title, and no separate stable name field
  • title is clobbered by the running program. In a live session right now a pane declared in a layout reports title="pi ⚙ working…" — written by a zsh title hook, not by the layout

So a name is a UI string a program owns, not a data channel. Metadata stored there is silently destroyed the moment a shell sets its own title. Hence a dedicated field.

The half that is load-bearing

A layout is a template consumed at session creation. A hint is worthless unless it survives into the running session and an agent can query it.

Good news: that surface already exists and is already rich.

$ zellij action list-panes --json
[{ "id": 1, "title": "…", "tab_id": 0, "tab_name": "Tab #1", "is_focused": false, … }]

So the work is: get purpose into that JSON. No new query command is needed.

v1 scope

  1. purpose="…" as a pane property in layout KDL, for tiled and floating panes.

kdl layout { pane split_direction="vertical" { pane name="editor" purpose="human-editor" pane { 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" }

  1. --purpose <STRING> on new-pane / run, so an agent can tag panes it creates itself. Without this only layout-declared panes have semantics and an agent cannot find the server pane it started five minutes ago. Same field, trivial addition, closes the loop.

bash id=$(zellij run --no-focus --purpose dev-server --name server -- npm run dev)

  1. Surface it in list-panes --json as "purpose": "dev-server" (and null when unset).

  2. Round-trip it in dump-layout and session serialization, so a resurrected session keeps its semantics. A workspace that forgets what its panes were for after a restart is not a workspace.

That is the whole feature.

Template to clone

The fork's status_bar pane attribute is the same shape — string, declared in KDL, lives on the pane, round-trips through dump-layout and resurrection. Follow that path exactly:

step file
allowlist the property (tiled and floating) zellij-utils/src/kdl/kdl_layout_parser.rs (is_a_valid_pane_property, is_a_valid_floating_pane_property)
parse + sanitize same file (kdl_get_string_property_or_child_value_with_error!)
layout structs zellij-utils/src/input/layout.rs (TiledPaneLayout, FloatingPaneLayout)
apply to the runtime pane zellij-server/src/tab/layout_applier.rs
store on the pane zellij-server/src/panes/terminal_pane.rs, plugin_pane.rs
expose to list-panes zellij-server/src/session_layout_metadata.rs, PaneInfo in zellij-utils/src/data.rs
serialize / resurrect zellij-utils/src/session_serialization.rs
CLI flag zellij-utils/src/cli.rs, zellij-utils/src/input/actions.rs, zellij-server/src/route.rs

grep -rln status_bar --include=*.rs zellij-utils/src zellij-server/src lists the full precedent if a step above is unclear.

Constraints

  • No protoc. New protobuf fields are fine if defaulted at both boundaries (plugin_api/, ipc/protobuf_conversion.rs); new enum variants are impossible. purpose must therefore be a plain Option<String>, never an enum. If exposing it to plugins costs protobuf churn, defer the plugin path — CLI JSON is what pi consumes.
  • Absent must be a total no-op. A layout with no purpose anywhere must produce byte-identical behaviour and an unchanged dump-layout, so existing layouts and the snapshot suite are untouched.
  • Sanitize like sanitize_status_bar_text does: bounded length, no control characters. Reuse that function's home in input/layout.rs so KDL and CLI paths share one sanitizer.
  • Keep the allowlist strict. Unknown pane properties must still error; do not relax is_a_valid_pane_property into "accept anything".

Design note: declared panes vs agent-spawned panes

There is a trap in "the workspace declares a pane and the agent uses it". Using someone else's pane means writing keystrokes into whatever is currently running in it — which types cargo build into a vim session if the state ever drifts.

Recommended split, and pi's convention should encode it:

  • declared panes are for things that start themselves (servers, tail -f, watch) or that belong to the human (editor). The agent reads them (dump-screen -p) and otherwise leaves them alone.
  • the agent's own work goes in panes the agent spawns and tags (zellij run --no-focus --purpose …), and closes when done.

So purpose is mostly used two ways: "this is the human's, don't touch" and "this is the log/server I should read", plus "this is mine, I made it".

Trust

Layout files already execute arbitrary commands (command="rm"). A workspace file committed to a repo is therefore pre-existing arbitrary code execution, and sanitizing purpose does not change that one bit.

The real rule is pi's, not zellij's: do not auto-open a workspace file from a repo you have not read. Sanitization here only prevents a hostile purpose string from corrupting terminal output or pi's parsing — it is not a trust boundary. Say so plainly in the pi-side docs so nobody infers safety that is not there.

Verification checklist

Prove each empirically, not by inspection:

  1. layout with purpose → list-panes --json reports it for the right pane ids
  2. floating pane with purpose → same
  3. zellij run --purpose X → reported for the new pane
  4. kill and resurrect the session → purpose still reported
  5. dump-layout of a session built from a purpose layout → re-running that output reproduces the purposes
  6. an unknown property (pane porpoise="x") → still a parse error
  7. a layout with no purpose → unchanged dump-layout, full suite green (cargo test -p zellij-server -p zellij-utils --lib)

Deliberately out of scope

  • tab-level purpose — plausibly wanted ("this tab is agent scratch"), but pane-level answers the actual question first. Add later if it earns it.
  • a general meta key/value bag — one string is enough until it demonstrably is not. command/args/cwd already carry the "what runs here" data.
  • zellij enforcing anything (read-only panes, agent permissions). It cannot: the agent is just a process running CLI commands. Advisory only, forever.
  • a new query command — list-panes --json already exists.