<feed xmlns='http://www.w3.org/2005/Atom'>
<title>sl0ppi.git/Makefile, branch main</title>
<subtitle>a sl0p.foo repository</subtitle>
<id>https://git.sl0p.foo/sl0ppi.git/atom/Makefile?h=main</id>
<link rel='self' href='https://git.sl0p.foo/sl0ppi.git/atom/Makefile?h=main'/>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0ppi.git/'/>
<updated>2026-08-13T19:19:22Z</updated>
<entry>
<title>cli: carry pane templates into the generated layout</title>
<updated>2026-08-13T19:19:22Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-13T19:19:22Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0ppi.git/commit/?id=94a1fd85e524bbe079b6ce3c8bb17295b706834c'/>
<id>urn:sha1:94a1fd85e524bbe079b6ce3c8bb17295b706834c</id>
<content type='text'>
`sl0ppi up` died on every session:

    × Failed to parse Zellij configuration
      43 │         sl0pbar size=3
         ·                ╰── Invalid tab property: sl0pbar

The CLI writes its own layout file and inlines the tab template into each tab
-- a default_tab_template would destroy the tab purposes that are the CLI's
notion of identity -- so the generated file is standalone. The moment the
status bar became a pane_template, the inlined body referred to a name only
layouts/default.kdl knew about.

extractPaneTemplates pulls those definitions out with the same brace scanner
that already had to respect strings and comments, and both generated layouts
emit them once at the top. The result is smaller than before, too: one copy of
the bar's configuration per session instead of one per tab.

packaging/checks/sl0ppi-up-live.py runs the real thing -- CLI, binary, session,
attached client -- because the CLI's own tests are string assertions and were
all green while this was broken. It fails with exactly the user's error on the
parent commit.

Not done, deliberately, and the check says why in a comment: copying the user's
default.swap.kdl next to the generated layout to make the bar collapse on a
narrow terminal. It appears to work, and then auto_layout swaps the tab to one
of the other swap layouts, whose bare `tab` nodes expect a default_tab_template
that a generated layout does not have -- and the bar disappears entirely.
Measured on screen both ways, and the check now pins "the bar survives a
rearrangement" so nobody re-derives it from JSON.

Which is the other lesson: `action list-panes` stops reporting the borderless
plugin pane once a client is attached. It had me chasing a bar that was on
screen the whole time. dump-layout and the screen agree; list-panes does not.
</content>
</entry>
<entry>
<title>test: run the status bar's tests in `make test`</title>
<updated>2026-08-13T18:38:15Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-13T18:38:15Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0ppi.git/commit/?id=2582fe3e78b0cac8155b9c8e9e1bf7aa36b18f51'/>
<id>urn:sha1:2582fe3e78b0cac8155b9c8e9e1bf7aa36b18f51</id>
<content type='text'>
sl0pstatus builds for wasm32-wasip1 and .cargo/config.toml points the test
runner at wasmtime, which is not installed here -- so `cargo test` in that crate
fails to launch and its 31 tests, including everything the bar's click routing
and margins rely on, were gated by nothing. Run them against the host target
instead, where they pass in 0.00s.
</content>
</entry>
<entry>
<title>sl0p(bar): a new-tab button, and margins that match the pane gap</title>
<updated>2026-08-13T18:36:45Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-13T18:36:45Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0ppi.git/commit/?id=91f40dccd9def02d147ea41615f7bc159cb306a6'/>
<id>urn:sha1:91f40dccd9def02d147ea41615f7bc159cb306a6</id>
<content type='text'>
First pass of the status bar overhaul, in the zjstatus fork plus the layout
that configures it.

margin_left / margin_right: the bar is a full-width pane while every pane above
it is inset by the pane gap, so its ends stuck out past every frame on screen.
The margins are applied by rendering into an inset viewport (inset_state),
which is the same thing click positions are translated by -- so what is drawn
and what is clickable come from one definition. That is the split button's
lesson applied before shipping rather than after.

{new_tab}: a clickable button that opens a tab and focuses it, with no name and
no cwd, i.e. exactly what the `n` binding in TAB mode does. Clicking a tab
already switches to it and double-clicking renames it; making one was the hole
in the strip. Rendering and hit-testing are the same measured string here, so
the button covers exactly the columns it draws.

Both keys are inert when unset, so an unmodified zjstatus config renders
identically.

packaging/checks/status-bar-live.py reads the bar off a real session and
asserts it spans exactly the columns the pane frames span, clicks the + and
expects one more tab, then clicks beside it and expects none. Confirmed by
setting the margins to 0 and watching the span assertion fail: (0, 99) against
the frames' (2, 97).

The check counts painted cells rather than ink, because a bar segment begins
with a padding space inside its coloured pill -- strip the colour and the left
edge reads one column off, which is half the margin being measured.
</content>
</entry>
<entry>
<title>sl0p: the plugins we ship do not ask for permission</title>
<updated>2026-08-13T18:08:50Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-13T18:08:50Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0ppi.git/commit/?id=fb7076efd6d3ab5c1d36b6e554045d87b43c15b6'/>
<id>urn:sha1:fb7076efd6d3ab5c1d36b6e554045d87b43c15b6</id>
<content type='text'>
pane-finder requests three permissions as it loads, so the first press of a
keybinding we ship opened a consent screen instead of a fuzzy finder. Prompting
is right for a wasm you found on the internet and wrong for the ones that come
in our own tarball.

plugin_is_shipped_with_sl0ppi (zellij-utils/src/input/permission.rs) grants
those implicitly, through the same instruction a human "y" produces, so the
plugin's permissions are set and recorded exactly as before. "Ours" is narrow
on purpose:

- builtins, asked via the existing PluginConfig::is_builtin -- the same
  predicate check_command_permission already uses to exempt them, so the two
  cannot drift;
- files inside a plugin directory we install into: the config dir's plugins/
  and the data dir's plugins/;
- nothing else. Remote plugins are never trusted, paths are normalised
  lexically first so plugins/../../evil.wasm is not "inside" the dir, and the
  directories are derived from the environment and default locations -- so
  getting them wrong costs a prompt and can never grant one elsewhere.

The trade, documented in config/README.md: &lt;config dir&gt;/plugins/ is now a
trusted directory. Anything able to write there can already bind arbitrary
commands to a key in config.kdl, so a consent screen for the wasm beside it was
not buying anything -- but it does mean plugins/ is not a download folder.

packaging/checks/plugin-permissions-live.py drives both halves on a real
session: the shipped plugin must run without the prompt, and the SAME wasm
loaded from outside the plugin dir must still ask. Confirmed by disabling the
rule and watching the first half fail with the exact "asks permission to ...
Allow? (y/n)" screen it was written against.
</content>
</entry>
<entry>
<title>sl0p: make the split button work on stacked panes too</title>
<updated>2026-08-13T17:39:18Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-13T17:39:18Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0ppi.git/commit/?id=ae726b5bb3ff8e9e8f2bef0e9fbc9d4dcb62a79e'/>
<id>urn:sha1:ae726b5bb3ff8e9e8f2bef0e9fbc9d4dcb62a79e</id>
<content type='text'>
Reported still dead on a live session after the previous fix. Stacks move the
frame twice more, and neither move was in the hit-test:

- a stack list is drawn over the top rows of its visible member, pushing that
  pane's frame down. The renderer applies this as a frame geom override
  (reserved_top_rows); the click side did not, so every pane in a stack was
  clickable one row above its own title. frame_rect_with_top_reserved() now
  applies the same shift, before the gap inset, in the same order as render;
- a stack's collapsed members drew a button on their one-row title. Nothing
  could honour it -- that row is the stack's picker, and "split the pane that
  is not on screen" is not a thing to ask anyway. They no longer draw one, and
  the click side mirrors the condition. 120 snapshot lines change, every one
  of them a one-liner losing its button and nothing else (same width, same
  content otherwise; audited mechanically).

Both confirmed by reintroducing each bug and watching the check fail. A third
candidate fix -- resolving a click to the stack's visible member, on the theory
that get_pane_at could return a collapsed sibling -- turned out to be
unnecessary: collapsed members have one-row geoms, so position lookup already
lands on the right pane. Removed rather than kept "just in case".

The check itself was wrong twice before it was right, in the direction that
matters most (reporting failures that were its own): it now reads until the pty
goes quiet instead of for a fixed time, and parses only a freshly forced
repaint, so a half-arrived screen cannot look like a button drawn where it is
not. It clicks every button it can see, and runs against config/config.kdl as
well, whose stacks the minimal config never had.

When the two disagree, the server's own log settles it: log::info! in
intercept_split_button_click, read /tmp/sl0ppij-$UID/sl0ppij-log/.
</content>
</entry>
<entry>
<title>build: cut the edit loop from 52s to 5s with more codegen units</title>
<updated>2026-08-13T16:59:59Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-13T16:59:59Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0ppi.git/commit/?id=92ffceaa5a4c39eab113b94272aa3cd13f634dcb'/>
<id>urn:sha1:92ffceaa5a4c39eab113b94272aa3cd13f634dcb</id>
<content type='text'>
zellij-server is one enormous crate and `fastdev` was building it in 16
codegen units, which is the granularity the incremental cache can skip work
at: a one-line change re-optimised a sixteenth of the crate. Splitting the
crates we actually edit into 256 units makes that a sliver instead.

Measured on the same one-line change to pane_boundaries_frame.rs, `git
checkout` of the file to a linked binary:

  codegen-units = 16    52s
  codegen-units = 256    6s, 4s on a second run
  + opt-level = 1        5s   (rejected: not worth a slower binary)

Ruled out on the way: linking the 69MB binary is ~2s, and rust-lld did not
beat the default linker on it, so the linker was never the problem. Root
crate codegen plus link is ~2s of the total.

Costs one 80s rebuild when switching profiles, and the fastdev binary keeps
opt-level 3. Verified with the full server/utils suites and make check-live.
</content>
</entry>
<entry>
<title>sl0p: make the split button visible, and clickable where it is drawn</title>
<updated>2026-08-13T16:46:58Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-13T16:46:58Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0ppi.git/commit/?id=2f0750285b1b174a552113c4aed9288f44985ae3'/>
<id>urn:sha1:2f0750285b1b174a552113c4aed9288f44985ae3</id>
<content type='text'>
The `+` shipped in 0.1.7 and 0.1.8 without ever appearing on a live session,
while rendering in 265 snapshots. Four faults, each enough on its own:

- title_line_without_middle computed the right side only inside the left
  side's and_then, so a pane with no title (hide_default_pane_names, the live
  default) got none at all;
- title_line_with_middle discarded the right side whenever the left was
  absent -- that is every frame in a session with a second client, which the
  live box always had, and "MY FOCUS AND:" on every frame was saying so;
- the title was budgeted before the button, so a title longer than the frame
  (any shell showing a path) consumed its columns. The button is anchored and
  hit-tested by pure geometry, so it cannot be the part that loses;
- the click hit-tested position_and_size() while the frame is drawn inset by
  the pane gap, putting the clickable columns two right of the glyph and a row
  above it at the top of the screen. Hence "the click works" and "nothing is
  drawn" both being true, and that click test proving nothing.

Adds Pane::frame_rect(), the rect the frame is really drawn in, which the
status bar buttons had already had to work around by hand.

Also fixes render_my_focus building its short form from separators that
Vec::append had already drained: 4 cells wide while reporting 6, a title line
two cells short of its pane. Found by asserting the width in the new test.

Verification, since unit tests have now been insufficient here four times:
split_button_is_drawn_where_it_is_hit_tested covers the combinations the live
config produces, and packaging/checks/split-button-live.py (make check-live)
reads the glyph off a real session, attaches a second client to force the
middle builder, and clicks the columns it found the glyph in. Both confirmed
to fail on the parent commit.
</content>
</entry>
<entry>
<title>release: 0.1.1, and stop the CLI carrying a second version</title>
<updated>2026-08-12T21:51:34Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-12T21:51:34Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0ppi.git/commit/?id=8f5dc3904093c402c16eca835528f59fbb9851b3'/>
<id>urn:sha1:8f5dc3904093c402c16eca835528f59fbb9851b3</id>
<content type='text'>
Published 0.1.1 with the dead-session and flaky-read fixes.

Also removes the `sl0ppi-cli` line from MANIFEST and the version field from
cli/package.json: the CLI ships as part of the product, so a version of its own
only drifts -- a 0.1.1 release shipped a MANIFEST claiming sl0ppi-cli 0.1.0.

Records the terminal-level flakiness as a known issue with the measurements, so
it is a tracked defect rather than folklore: list-tabs/list-panes fail ~60% of
the time against a detached live session on a 2-vCPU box, ~25% with a client
attached, and the action client blames "no active session" for what is really a
transient enumeration failure.
</content>
</entry>
<entry>
<title>release: publish sl0ppi.sl0p.foo, and a script to keep it current</title>
<updated>2026-08-12T21:12:23Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-12T21:12:23Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0ppi.git/commit/?id=4e647e2173a9817a2250666f8298b3b05d13ece5'/>
<id>urn:sha1:4e647e2173a9817a2250666f8298b3b05d13ece5</id>
<content type='text'>
Phase 5, settled as build scripts rather than CI:

  make release   tests, then a fresh static-musl tarball
  make publish   render the site, upload, and verify against the live URL

publish.sh is deliberately dumb -- rsync, then ask the live site whether it
agrees (status codes plus a re-downloaded checksum) -- but it has two guard
rails that matter:

  - it refuses to republish a version whose bytes differ from what is already
    live. /releases/ is cached for a year, so the same URL serving two different
    tarballs would be undebuggable. Bump VERSION instead.
  - artifacts upload before latest.json, so the pointer never names a file that
    is not downloadable yet.

site/install.sh is POSIX sh and short on purpose, because people are being asked
to pipe it into a shell. It reads latest.json, downloads the versioned artifact,
VERIFIES ITS SHA256, and only then unpacks and runs the bundled installer.
Refusal was tested for real by serving a doctored latest.json: it stops with a
checksum mismatch and installs nothing.

Verified end to end from the public URL, in a throwaway HOME with node, npm,
bun, cargo and rustc replaced by poison stubs:

  curl -fsSL https://sl0ppi.sl0p.foo/install.sh | sh
  sl0ppij --version -&gt; 0.45.0+sl0p1
  pi --version      -&gt; 0.84.1
  sl0ppi doctor     -&gt; all good

The vhost itself is committed in the sl0pday repo, where that box's nginx config
lives.
</content>
</entry>
<entry>
<title>build: ship the sl0ppi CLI in the tarball</title>
<updated>2026-08-12T20:51:23Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-12T20:51:23Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0ppi.git/commit/?id=f8f035713f29676348602246036aba840468cc5f'/>
<id>urn:sha1:f8f035713f29676348602246036aba840468cc5f</id>
<content type='text'>
The artifact was stale twice over: it predated the protobuf fix and did not
contain the CLI at all. Both fixed, and the whole chain re-verified from the
tarball rather than assumed.

The CLI ships as a single 31K file (`bun build --target=bun`) run by the bundled
bun, rather than a source tree or a second `--compile`d binary that would embed
another 90M copy of bun. `install.sh` generates the wrapper and pins
SL0PPI_BIN to the sl0ppij it just installed, so the orchestrator can never end
up driving some other zellij that happens to come first on PATH.

Verified in a throwaway HOME with node, npm, bun, cargo, rustc and python3 all
replaced by poison stubs that fail loudly if invoked:

  sl0ppij --version   -&gt; 0.45.0+sl0p1        (static musl, no libc dependency)
  pi --version        -&gt; 0.84.1              (bundled bun)
  sl0ppi doctor       -&gt; all good
  sl0ppi init x2      -&gt; two projects scaffolded
  sl0ppi up           -&gt; session "dev", 2 tabs, agents suspended
  sl0ppi ls           -&gt; agent:suspended, 1 pane each
  sl0ppi up (again)   -&gt; "up to date"

And the promise the whole thing exists for, end to end: attach a client, press
Enter in the suspended agent pane, and pi starts on the bundled runtime -- while
the other project's agent stays suspended, because only the focused pane got the
key.

`make test` now runs the CLI suite alongside the Rust ones, so the two halves
cannot drift apart unnoticed.
</content>
</entry>
</feed>
