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 --jsonexposestitle, and no separate stablenamefieldtitleis clobbered by the running program. In a live session right now a pane declared in a layout reportstitle="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
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"
}
--purpose <STRING>onnew-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)
-
Surface it in
list-panes --jsonas"purpose": "dev-server"(andnullwhen unset). -
Round-trip it in
dump-layoutand 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.purposemust therefore be a plainOption<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
purposeanywhere must produce byte-identical behaviour and an unchangeddump-layout, so existing layouts and the snapshot suite are untouched. - Sanitize like
sanitize_status_bar_textdoes: bounded length, no control characters. Reuse that function's home ininput/layout.rsso KDL and CLI paths share one sanitizer. - Keep the allowlist strict. Unknown pane properties must still error; do not
relax
is_a_valid_pane_propertyinto "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:
- layout with
purpose→list-panes --jsonreports it for the right pane ids - floating pane with
purpose→ same zellij run --purpose X→ reported for the new pane- kill and resurrect the session →
purposestill reported dump-layoutof a session built from apurposelayout → re-running that output reproduces the purposes- an unknown property (
pane porpoise="x") → still a parse error - a layout with no
purpose→ unchangeddump-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
metakey/value bag — one string is enough until it demonstrably is not.command/args/cwdalready 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 --jsonalready exists.
