<feed xmlns='http://www.w3.org/2005/Atom'>
<title>sl0ppij.git/zellij-server/src/session_layout_metadata.rs, branch main</title>
<subtitle>a sl0p.foo repository</subtitle>
<id>https://git.sl0p.foo/sl0ppij.git/atom/zellij-server/src/session_layout_metadata.rs?h=main</id>
<link rel='self' href='https://git.sl0p.foo/sl0ppij.git/atom/zellij-server/src/session_layout_metadata.rs?h=main'/>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0ppij.git/'/>
<updated>2026-08-12T16:44:24Z</updated>
<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: 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: 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>
<entry>
<title>sl0p: serialise the pane status bar for resurrection</title>
<updated>2026-08-12T12:52:43Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-12T12:52:13Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0ppij.git/commit/?id=598e77563b8c6cace224c763e00d2fdb4157a869'/>
<id>urn:sha1:598e77563b8c6cace224c763e00d2fdb4157a869</id>
<content type='text'>
The status bar survived detach/reattach (it lives on Grid) but not a
serialise/restore cycle, so a resurrected pane came back blank.

It now travels the whole per-pane serialisation pipeline, mirroring the fork's
existing default_fg/default_bg: pane -&gt; PaneLayoutMetadata -&gt;
PaneLayoutManifest -&gt; TiledPaneLayout -&gt; a status_bar property in the serialised
KDL, then back through the normal layout parser and applied by LayoutApplier
when the pane is recreated.

The sanitiser moved from grid.rs into zellij-utils so both entry points share
one definition. That matters: a layout file on disk is no more trustworthy than
the escape sequence that wrote it -- it may have been hand-edited, or copied from
somewhere else -- so restored text is stripped and length-capped on the way in
just like the OSC payload was.

Verified end to end rather than by unit test alone: set the bar, waited for a
serialisation pass, confirmed status_bar="..." in session-layout.kdl, killed the
session until list-sessions reported EXITED, then attached and watched it come
back from disk. The earlier attempt only proved detach/reattach because Quit had
left the session alive.
</content>
</entry>
<entry>
<title>Warning and duplication cleanups (#4857)</title>
<updated>2026-03-16T09:37:45Z</updated>
<author>
<name>Aram Drevekenin</name>
<email>aram@poor.dev</email>
</author>
<published>2026-03-16T09:37:45Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0ppij.git/commit/?id=91e7297b6397fd52d4ff995bdff1fa124a5acf81'/>
<id>urn:sha1:91e7297b6397fd52d4ff995bdff1fa124a5acf81</id>
<content type='text'>
* cleanup compilation warnings

* consolidate EditScrollbackRaw into EditScrollback</content>
</entry>
<entry>
<title>feat: add per-pane default foreground/background color support (#4737)</title>
<updated>2026-03-04T13:26:26Z</updated>
<author>
<name>Karl Bunch</name>
<email>karlbunch@users.noreply.github.com</email>
</author>
<published>2026-03-04T13:26:26Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0ppij.git/commit/?id=f77cdec298576164f34baf0456e4b986bde269e6'/>
<id>urn:sha1:f77cdec298576164f34baf0456e4b986bde269e6</id>
<content type='text'>
* feat: add per-pane default foreground/background color support

Add per-pane default foreground and background colors, configurable
through four surfaces:

- OSC 10/11 escape sequences (set/query) and OSC 110/111 (reset)
- CLI action: zellij action set-pane-color --pane-id &lt;id&gt; --fg/--bg
- Layout config: default_fg/default_bg properties on pane nodes
- Plugin API: set_pane_color() SDK function and PaneInfo read-side

Colors are applied at render time as a fallback when cells have no
explicit foreground/background, without mutating stored grid data.

Closes #4712

* refactor: address review feedback for pane color support

- Extract adjust_styles_for_custom_bg_fg in output/mod.rs
- Move color parsing (xparse_color) from tab into Grid
- Move color serialization (ansi_code_to_color_string) into Grid
- Pane trait now uses Option&lt;String&gt; for color getters/setters
- Add colors to session serialization (dump-layout)
- Add default_fg/default_bg to protobuf IPC schema to fix
  layout-in-session color loss
- Update example layout with tab-bar and status-bar

* fix stack overflow and ordering

* rustfmt

* update e2e snapshots

---------

Co-authored-by: Karl Bunch &lt;karl@karlbunch.com&gt;
Co-authored-by: Aram Drevekenin &lt;aram@poor.dev&gt;</content>
</entry>
<entry>
<title>fix: properly detect editor panes when serializing sessions (#4717)</title>
<updated>2026-02-18T08:09:06Z</updated>
<author>
<name>Aram Drevekenin</name>
<email>aram@poor.dev</email>
</author>
<published>2026-02-18T08:09:06Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0ppij.git/commit/?id=0731d71a670312d2a574d338bfe762685c346710'/>
<id>urn:sha1:0731d71a670312d2a574d338bfe762685c346710</id>
<content type='text'>
* fix: properly detect editor panes when serializing sessions

* rustfmt</content>
</entry>
<entry>
<title>Layout manager (#4601)</title>
<updated>2026-01-13T10:27:07Z</updated>
<author>
<name>Aram Drevekenin</name>
<email>aram@poor.dev</email>
</author>
<published>2026-01-13T10:27:07Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0ppij.git/commit/?id=4a27ceea0d99b036ca607e05714cef15689d7c26'/>
<id>urn:sha1:4a27ceea0d99b036ca607e05714cef15689d7c26</id>
<content type='text'>
* ad plugin override_layout command

* watch available layouts and report to plugin thread

* Event::AvailableLayoutInfo

* save layout command

* refactor save_layout

* fix api and permissions

* delete_layout API command

* edit layout in place API

* edit layout response through api

* propagate layout parsing errors

* generate random name plugin api

* dump stringified layout API

* synchronicity dump_session_layout and include LayoutMetadata in sync response

* parse_layout plugin api

* fix: default to default layout dir in plugin api

* rename_layout plugin command API

* initial implementation of multi-tab override

* apply to multiple tabs

* allow applying to only the active tab

* add tab/pane names to layout parsing metadata

* indicate is_builtin_plugin in PaneMetadata

* get_layout_dir plugin API command

* consolidate (and fix) is_builtin_plugin logic

* fix bad ret

* allow dumping the layout for just a specific tab in the session

* get focused pane and tab sync api

* color last substring variants

* add plugin

* update assets

* fix tests

* add e2e tests for new plugin apis

* update ui with new shortcut

* rustfmt

* docs(changelog): add PR</content>
</entry>
<entry>
<title>fix(serialization): don't serialize when only UI elements present and provide post command discovery hook (#4276)</title>
<updated>2025-07-08T20:50:08Z</updated>
<author>
<name>Aram Drevekenin</name>
<email>aram@poor.dev</email>
</author>
<published>2025-07-08T20:50:08Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0ppij.git/commit/?id=da9cf4ffeb1659669b067165ba6718556d665f2b'/>
<id>urn:sha1:da9cf4ffeb1659669b067165ba6718556d665f2b</id>
<content type='text'>
* do not serialize when only UI elements are present

* start work on a post serialization hook

* add post_command_discovery_hook

* fix tests

* style(fmt): rustfmt

* some cleanups

* moar formatting

* docs(changelog): add PR</content>
</entry>
<entry>
<title>feat(plugins): add API to list clients, their focused panes and running commands/plugins (#3687)</title>
<updated>2024-10-22T13:27:40Z</updated>
<author>
<name>Aram Drevekenin</name>
<email>aram@poor.dev</email>
</author>
<published>2024-10-22T13:27:40Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0ppij.git/commit/?id=d671ab650e231d353a05a2808c0984e952de38c4'/>
<id>urn:sha1:d671ab650e231d353a05a2808c0984e952de38c4</id>
<content type='text'>
* fix(list-clients): properly show client info after a tab was closed

* feat(plugins): add API to list clients, their focused panes and running commands/plugins

* style(fmt): rustfmt</content>
</entry>
</feed>
