<feed xmlns='http://www.w3.org/2005/Atom'>
<title>sl0ppij.git, branch main</title>
<subtitle>a sl0p.foo repository</subtitle>
<id>https://git.sl0p.foo/sl0ppij.git/atom/?h=main</id>
<link rel='self' href='https://git.sl0p.foo/sl0ppij.git/atom/?h=main'/>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0ppij.git/'/>
<updated>2026-08-12T16:44:40Z</updated>
<entry>
<title>sl0p: document --purpose and tab purposes</title>
<updated>2026-08-12T16:44:40Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-12T16:44:40Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0ppij.git/commit/?id=1c93b2f8e4bde3e812f71c513bedbe98ce08dc05'/>
<id>urn:sha1:1c93b2f8e4bde3e812f71c513bedbe98ce08dc05</id>
<content type='text'>
</content>
</entry>
<entry>
<title>sl0p: --purpose flag and tab-level purpose</title>
<updated>2026-08-12T16:44:24Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-12T16:44:24Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0ppij.git/commit/?id=2c67579398835d51e625085ce3b22c22bc8f0cba'/>
<id>urn:sha1:2c67579398835d51e625085ce3b22c22bc8f0cba</id>
<content type='text'>
Completes the two pieces deferred from the first workspace-hints pass.

`zellij run --purpose X` / `zellij action new-pane --purpose X` tag a pane at
spawn time, which is what an agent wants for panes it creates itself; the OSC
wrapper still works for programs that declare their own role. Not supported with
--in-place, which replaces another pane rather than being a pane of its own: clap
rejects the combination rather than accepting a flag that would do nothing.

`tab name="work" purpose="agent-work"` gives a whole tab a role, reported by
`list-tabs --json`. It rides the tab's ROOT layout node, reusing the pane field
because a tab's root is the tab container and never itself a pane -- asserted by a
test for the tightest case (a tab holding exactly one pane), since a leak there
would silently mislabel a pane.

The interesting part is why the flag first shipped broken. There are TWO protobuf
boundaries -- the plugin API and the client-&gt;server IPC contract -- and a CLI flag
must cross the second one. Defaulting the field in the conversion (what "no
protoc" usually forces) compiles cleanly and then silently drops the value in
transit, so --purpose parsed, printed a pane id, and did nothing. Fixed by adding
the field to both contracts by hand, in the .proto AND the committed generated
prost, kept in lockstep so a future regeneration reproduces it: protoc is only
needed to regenerate, not to build. Both boundaries now have a round-trip test,
and the IPC one was confirmed to fail when the wire read is removed.

Sanitising moved to Grid::set_authoritative_purpose, the single choke point every
out-of-band source passes through, after the CLI path was observed delivering
`bad purpose; rm -rf /` intact into list-panes --json.
</content>
</entry>
<entry>
<title>sl0p: document workspace hints for consumers</title>
<updated>2026-08-12T15:06:29Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-12T15:06:29Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0ppij.git/commit/?id=82df0861b06529e881209e90a039dd86d1d7db20'/>
<id>urn:sha1:82df0861b06529e881209e90a039dd86d1d7db20</id>
<content type='text'>
</content>
</entry>
<entry>
<title>sl0p: per-pane workspace hints (purpose=)</title>
<updated>2026-08-12T15:05:34Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-12T15:05:34Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0ppij.git/commit/?id=df052db4608f4d835fe7b4ca76dbb4059e78943b'/>
<id>urn:sha1:df052db4608f4d835fe7b4ca76dbb4059e78943b</id>
<content type='text'>
Implements the zellij half of sl0p/workspace-hints-proposal.md: a pane can carry
an opaque agent-facing hint for what it is FOR, so a project can ship a layout
that an agent is able to navigate instead of guessing.

zellij never interprets a purpose. The consuming agent owns the entire vocabulary,
which is what keeps this to one string threaded along the existing status_bar path
rather than a policy engine, and lets the vocabulary change without a rebuild.

Two ways in, because declared panes and agent-created panes are different:
  layout:  pane name="server" purpose="dev-server"
  in-band: printf '\033]5577;1;purpose;agent:build\033\\'
The second exists because tagging at spawn time would mean threading a field
through 28 SpawnTerminal call sites, and a new action variant is impossible while
protobuf enums cannot be regenerated. Wrapping the command costs one printf.

A declared purpose outranks an in-band one and cannot be overridden or cleared by
it. OSC arrives from whatever the pane happens to be showing, so otherwise a
`cat hostile.txt` in the human's editor pane could relabel it as the agent's
scratch pane and invite an agent to run commands where the human is working. A
layout is written by a human in a file they can read; in-band data only fills a
blank.

Exposed on PaneListEntry (`list-panes --json`) rather than PaneInfo, which crosses
the plugin protobuf API we cannot regenerate; resolved in the screen thread where
panes live rather than via route.rs's per-pane PTY round-trips. Values are
sanitised to a bounded [A-Za-z0-9_.:/-] token at both entry points, because a
purpose is never rendered -- it is matched against and pasted into consumers'
shell and JSON pipelines.

Verified end to end rather than by inspection: purposes from a layout appear in
list-panes --json against the right pane ids, an agent-spawned pane tags itself,
a declared purpose survives an in-band override attempt, all of it comes back
after kill + resurrect, an unknown pane property is still a parse error, and a
layout with no purpose leaves dump-layout unchanged.
</content>
</entry>
<entry>
<title>sl0p: propose per-pane workspace hints</title>
<updated>2026-08-12T14:42:40Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-12T14:42:40Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0ppij.git/commit/?id=889180b6b6b7a52144ffb14fc50d9531203677e6'/>
<id>urn:sha1:889180b6b6b7a52144ffb14fc50d9531203677e6</id>
<content type='text'>
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.
</content>
</entry>
<entry>
<title>sl0p: hot-reload status_bar_button_style</title>
<updated>2026-08-12T14:21:04Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-12T14:21:04Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0ppij.git/commit/?id=78174b714461e510c6f8e703b29b796c443a35ee'/>
<id>urn:sha1:78174b714461e510c6f8e703b29b796c443a35ee</id>
<content type='text'>
The option was only read when a client connected, so changing it needed a new
session -- inconsistent with gap/padding/title_alignment, which apply live, and
the kind of inconsistency that reads as "my config change did nothing".

Threaded through update_frame_layout, the existing vehicle for the fork's
pane_frames options: Tab -&gt; tiled/floating containers -&gt; each pane's Style,
including suppressed panes so a hidden stack member is styled correctly when it
swaps back into view.

Verified by editing config.kdl against a running session and watching
1:Approve flip to [Approve] and back, then confirming a click still resolves
afterwards -- the button widths change with the style, so a stale layout would
have silently mis-targeted clicks.
</content>
</entry>
<entry>
<title>sl0p: add an indexed style for status bar buttons</title>
<updated>2026-08-12T14:15:19Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-12T14:15:19Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0ppij.git/commit/?id=c3764fca00c1d91c06612a33162b2354e9d42be0'/>
<id>urn:sha1:c3764fca00c1d91c06612a33162b2354e9d42be0</id>
<content type='text'>
`status_bar_button_style "indexed"` renders `1:Approve 2:Reject` instead of
`[Approve] [Reject]`, so the bar doubles as a key legend for a harness that binds
alt+1..9 to the same actions.

A config option rather than a protocol field: whether the numbers mean anything
depends on what the USER has bound, not on what the program knows. It needs no
wire change and so no version bump, and the click report still carries the id --
never the index -- so a program is unaffected by which style is in force.

The style is threaded into layout_status_bar, the one function the renderer draws
from and the hit-test resolves against. That is what makes this safe: changing a
button's rendered width necessarily changes its clickable span too, verified by a
test asserting span width equals the drawn width and that every drawn column
resolves back, for both styles.

Also fixes the web client's Style construction to read the option from config
rather than defaulting it -- my first pass claimed that code had no pane_frames
config available, which was wrong; it uses it two lines further down.
</content>
</entry>
<entry>
<title>sl0p: highlight the status bar button under the pointer</title>
<updated>2026-08-12T13:50:04Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-12T13:50:04Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0ppij.git/commit/?id=266147d1d5ecadafda6a4893a60019c80f3c2c85'/>
<id>urn:sha1:266147d1d5ecadafda6a4893a60019c80f3c2c85</id>
<content type='text'>
A click target with no hover feedback gives you no way to tell what you are about
to hit, which matters more here than elsewhere because the buttons sit in a frame
row that is only one cell tall.

The hovered button is inverted rather than merely recoloured: swapping
fore/background reads regardless of which colours a theme uses, and makes "the
pointer is on THIS one" unmistakable next to its unhovered neighbours.

Tracked per client, because hover belongs to a pointer rather than to a pane --
with two users attached, one pointer must not light up the other's button. Stored
on the pane keyed by client id, which the frame renderer already has.

Updated before the action dispatch rather than in the hover action, because
motion over the ACTIVE pane is dispatched as SendToTerminal, not UpdateHover --
and the pane you are about to click a button in is usually the one you are
already in. Every other pane's hover for that client is cleared on each motion,
so moving between panes cannot leave a button lit behind.
</content>
</entry>
<entry>
<title>sl0p: document the pane status bar protocol</title>
<updated>2026-08-12T13:30:11Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-12T13:30:11Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0ppij.git/commit/?id=f9376a5ab8708c95d6ba40f069c9c29dedf67421'/>
<id>urn:sha1:f9376a5ab8708c95d6ba40f069c9c29dedf67421</id>
<content type='text'>
Reference for programs wanting to drive the bar: the OSC 5577 verbs, escaping
rules, click reports, limits, rendering behaviour, persistence, and the security
model.

Both worked examples were run against a real session rather than written from
memory, which caught four bugs in my own samples:

- the Python reader kept a trailing buffer window, so a processed report was
  re-matched and every click fired twice
- it used tty.setraw, which also disables signals, leaving the sample unkillable
  with Ctrl-C
- it exited via traceback and left the bar behind; both samples now clear it on
  exit
- the shell reader echoed the report to the screen, and an `stty -echo` did not
  help because bash's `read -n` saves and restores terminal settings around each
  call. `read -s` is the fix.

Also verified and documented two behaviours I would otherwise have guessed at: a
BEL terminator works as well as ST, and the bar is not drawn when pane frames are
off (it lives in the frame row), reappearing when they return.
</content>
</entry>
<entry>
<title>sl0p: clickable action buttons in the pane status bar</title>
<updated>2026-08-12T13:18:33Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-12T13:18:33Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0ppij.git/commit/?id=549f052ae5451d57b58f5de22de57b2a586e6d23'/>
<id>urn:sha1:549f052ae5451d57b58f5de22de57b2a586e6d23</id>
<content type='text'>
Second slice of the status/action frame. A program declares buttons, and a click
is reported back to it:

    printf '\033]5577;1;buttons;approve:Approve;reject:Reject\033\\'
    -&gt; click -&gt; program's stdin receives  OSC 5577 ; 1 ; click ; approve  ST

The report is a FIXED format carrying only the id, never bytes the payload chose,
and ids are restricted to [A-Za-z0-9_-]. The sequence that creates a button is
in-band and untrusted -- `cat` of a hostile file emits it as readily as a
cooperating program -- so a click that could send arbitrary bytes would let
viewing a file inject into the stdin of whatever is running, a shell included.
Labels are '%'-escaped since the wire format is ';'-separated, and both the
button count and id length are capped because the list lives as long as the pane.

Positions come from one layout_status_bar function that the renderer draws from
and the hit-test resolves against. Computing that geometry twice is how the
stacked-pane picker once selected the wrong pane; a test asserts every drawn
column resolves back to the button drawn there, across 100 widths.

Two things the drive caught that unit tests could not:

- The first delivery path pushed onto grid.pending_messages_to_pty, which is
  only drained when the PANE produces output -- so a click on an idle program sat
  in the queue forever. The write now happens at the tab level, which owns the
  senders.
- The hit-test derived the frame row from the pane's raw geometry, but this fork
  draws the frame inset by gap_offset, so with `gap 1` every click missed by one
  row. It now derives from the CONTENT rect, which is by definition the inside of
  the drawn frame and therefore the same columns the renderer lays out.

Buttons serialise with the session like the status text, so a resurrected pane
comes back with its buttons intact.
</content>
</entry>
</feed>
