aboutsummaryrefslogtreecommitdiffstats
path: root/sl0p/workspace-hints-proposal.md
blob: 19e08f3c1d0adeee5fb2ef4dedd46d404ea44552 (plain) (blame)
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.