1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
|
# 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.
```console
$ 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"
}
```
2. **`--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)
```
3. **Surface it in `list-panes --json`** as `"purpose": "dev-server"` (and `null`
when unset).
4. **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.
|