aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoruser <user@clank>2026-08-12 16:42:40 +0200
committeruser <user@clank>2026-08-12 16:42:40 +0200
commit889180b6b6b7a52144ffb14fc50d9531203677e6 (patch)
treee41184df00ee6eb7a83112119346caa7bbd8c66b
parentsl0p: hot-reload status_bar_button_style (diff)
downloadsl0ppij-889180b6b6b7a52144ffb14fc50d9531203677e6.tar.gz
sl0ppij-889180b6b6b7a52144ffb14fc50d9531203677e6.tar.xz
sl0ppij-889180b6b6b7a52144ffb14fc50d9531203677e6.zip
sl0p: propose per-pane workspace hints
A design doc, not an implementation: lets a project ship a layout that also tells an agent what each pane is for. The load-bearing decision is the boundary -- zellij carries one opaque string per pane and never interprets it, pi owns the whole vocabulary. That keeps the fork change to a single field threaded along the existing status_bar path, and lets the vocabulary change without rebuilding zellij. Records two findings that were checked rather than assumed, since both would otherwise be discovered the expensive way: pane names cannot carry metadata (there is no stable name field at runtime, and title is clobbered by whatever the program sets), and the runtime query surface already exists (list-panes --json), so the work is only to get the field into it.
-rw-r--r--sl0p/workspace-hints-proposal.md176
1 files changed, 176 insertions, 0 deletions
diff --git a/sl0p/workspace-hints-proposal.md b/sl0p/workspace-hints-proposal.md
new file mode 100644
index 00000000..b81782ab
--- /dev/null
+++ b/sl0p/workspace-hints-proposal.md
@@ -0,0 +1,176 @@
+# 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.** Proposal. Nothing implemented.
+
+## 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.