| Commit message (Collapse) | Author | Age |
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The tarball shipped bun's default x64 build, which requires AVX2 -- so sl0ppi
installed cleanly on a plain KVM guest and then died with "Illegal instruction"
the first time anything ran pi. Known since 0.1.7 and carried as an open item;
not something to ship a third time.
packaging/fetch-vendor.sh now fetches bun-linux-x64-baseline.zip. Verified
without needing such a machine, on an emulated CPU that has none:
qemu-x86_64 -cpu Nehalem <baseline>/bun --version -> 1.3.14
qemu-x86_64 -cpu Nehalem <default>/bun --version -> Illegal instruction
Also drops the installer's closing note about the status bar asking for
permissions on first launch: shipped plugins are granted implicitly now, so the
prompt it told people to expect no longer appears. (That text is baked into the
0.1.9 tarball; it is wrong but harmless, and corrected for the next one.)
And gitignores *.core -- qemu writes one where you stand when it kills a target
on an illegal instruction, and a 100MB core dump nearly went into this commit.
0.1.9 contains everything since 0.1.8: the split button actually visible and
clickable where it is drawn, including on stacked panes; shipped plugins not
asking for permission; the status bar as a framed box in the pane style with a
new-tab button and margins matching the pane gap, collapsing to a single line
in the mobile layout; and runtime tabs keeping a status bar without losing the
purpose the CLI uses for identity.
|
| |
|
|
|
|
|
|
| |
`sl0ppij -s NAME kill-session` parses, exits 0, and leaves the session running;
the top-level `sl0ppij kill-session NAME` is the one that works. The checks used
the first form, so cleanup was resting entirely on the delete-session that
follows it -- which is how a test session was still listed and its server still
running right after a check "cleaned up" on the test box.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Clicking the bar's `+` made a tab with no status bar. The button only exposed
it: pressing `n` in TAB mode did the same, in any `sl0ppi up` session.
The chain: a generated layout had no default_tab_template, so a tab created at
runtime had nothing to take a bar from. It had none because
parse_tab_node_with_template read a tab node's name, cwd and focus but never
its `purpose` -- so templating erased the purpose that IS identity here
(`sl0ppi ls` finds project tabs by it), and the CLI worked around that by
inlining the template into every tab instead of emitting it.
Fixed at the root: a templated tab keeps its own purpose, and the CLI emits
`default_tab_template` again. The generated layout also shrinks -- one copy of
the bar's configuration instead of one per tab.
The two properties trade off against each other, so sl0ppi-up-live.py now
asserts both at once: `sl0ppi ls` still sees a tab per project, and every tab
including one created at runtime has exactly one status bar. Confirmed by
reverting each half separately: without the parser fix the tabs lose their
purpose and `ls` shows "alpha —"; without the CLI change the runtime tab has
zero bars, which is the reported bug.
Two measurement traps cost an hour here and are now written down: counting bars
by grepping dump-layout also counts the template and every swap layout ("4 bars
for 3 tabs"), and list-panes stops reporting borderless plugin panes for a tab
once a client has rendered it.
Also in here, from the same session:
- the split button check found buttons by searching for " + " on any row with a
frame glyph, which matches the stack list's "+N more" once two panes sit side
by side, and then reports the product broken. It now requires the frame
corner the button is drawn against.
- the checks killed their sessions but did not delete them, so each run left an
"EXITED - attach to resurrect" entry: 35 had quietly filled the session
picker. They now delete-session by name.
- and the trap that cost the user their running client: I killed a bare
`sl0ppij` because "there are no user sessions on this box". That was a guess,
not evidence, and wrong. A check's processes always carry that run's own
socket dir or `attach sl0ptest-...`; a bare `sl0ppij` with a cwd in the repo
is a human's. Written into the handover next to the `pkill -f` trap, which is
the same mistake in a different costume.
|
| | |
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
`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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Given three rows the bar now draws the same rounded box the panes do, so the
bottom of the screen reads as one more pane instead of a strip of text under
them. Given one row it stays the line it always was, which is what a terminal
narrow enough to collapse the layout should spend on chrome.
The height is the LAYOUT's decision, because a plugin cannot resize its own
pane:
- default.kdl defines the bar as a pane_template `sl0pbar` and places it at
size=3;
- default.swap.kdl defines tab_template "mobile" placing the same template at
size=1, and the `stacked` swap layout -- what responsive_layout collapses to
below responsive_min_cols -- uses it.
That reuse works because zellij parses a layout and its .swap.kdl with the same
parser, on purpose, "so that we can reuse assets (eg. pane and tab templates)".
Otherwise the collapsed template would have to duplicate the whole 100-line
zjstatus configuration, and the two would drift within a week.
ModuleConfig::viewport is the one definition of where the content line lives:
three rows or more and it is framed, fewer and it is not, plus the column the
content starts at. Rendering and click routing both read it, so the frame moves
the drawing and the hit-testing together -- a click on the new border hits
nothing rather than the widget it sits above. Clicks now check the LINE too,
which a single-line bar never had to.
status-bar-live.py grew the other half of the feature: the borders must span
exactly the columns the pane frames span with rounded corners at their ends, a
click on the border must not make a tab, and after shrinking the terminal below
the responsive threshold the bar must be one line with no box -- with its button
still working. The collapse is real: the bar lands on the last row, which only
happens if the swap layout actually switched.
One test bug worth naming: locating the button with str::find gave a BYTE
offset, and the box glyphs are three bytes each, so the click landed two columns
right of the thing it found. Same trap the pane frame tests document.
|
| |
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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: <config dir>/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.
|
| |
|
|
|
|
|
|
|
|
| |
kill-session does not always take the server with it: 38 of them had piled up
on this box, one per run, and a leftover test session is exactly what produced
a "MY FOCUS" bug report from the user once already.
Matched on `comm` plus the run's own socket dir, never `pkill -f` -- that
matches the checking process's own command line, and has killed real clients
here before. The temp dir goes with it.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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/.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
| |
Where the split-button work stopped, what is open, the traps that cost time, and
how to verify pane/layout/mouse work on a real session rather than in the test
harness.
|
| |
|
|
|
|
| |
A right-aligned title no longer runs flush to the corner: the split button now
occupies SPLIT_BUTTON_WIDTH columns between them. Adjusts the expectation rather
than the layout, since that gap is the intended design.
|
| | |
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The hover-gated version did not work. `mouse_is_hovering_over_pane` never became
true in a live session, so the `+` was invisible while its hit-test -- which is
geometric and never consulted hover -- kept working. Clickable but invisible is
the worst of both, and it is exactly the failure the shared
`split_button_columns()` was supposed to prevent: that guarantees render and
hit-test agree on WHERE, not on WHETHER.
Reported as "I hover over panes but don't see a +". My end-to-end test had
proved the click, not the button, and the passing snapshots only proved the
renderer works when the flag is set.
Why the flag is false live is still open and is written into the code comment:
the plumbing from `mouse_hover_pane_id` through `FrameParams` reads correctly,
and the config that reproduces it sets both `advanced_mouse_actions` and
`mouse_hover_effects`. Re-gate on hover only once that is understood AND shown
on a real session rather than in a snapshot.
265 snapshots updated: every pane frame now carries the button. 233 are
mechanically provable as button-only (the ` + ` replaces three horizontal glyphs
before a corner); the remaining 32 differ only by a header line offset in the
comparison, with the same substitution underneath.
|
| | |
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Switching panes, switching tabs, renaming, reordering and resizing all have a
mouse path in this fork. Spawning a pane did not: it was keyboard-only.
A `+` now appears at the right end of a pane's title line while the pointer is
over that pane, and clicking it splits the pane. Hover-driven rather than always
drawn, for the same reason the resize hint and the status bar button highlight
are: a permanent `+` on every frame is noise, and the pointer being there is
exactly when the button is useful.
render ui/pane_boundaries_frame.rs::render_split_button
hit tab/mouse_handler.rs::intercept_split_button_click
shared split_button_columns(), so what is clickable is what is visible
The button sits immediately left of the top-right corner and the existing scroll
and pin indications get the room that is left. That ordering is deliberate: a
fixed position relative to the pane's right edge is one the mouse handler can
hit-test directly, instead of re-deriving the whole title layout the way
`status_bar_button_at` has to.
The clicked pane is focused first, then the split is requested exactly as a
keybinding would (`PtyInstruction::SpawnTerminal` with `NoPreference`), so mouse
and keyboard produce the same arrangement and the split lands on the pane whose
button was clicked rather than whichever pane happened to be focused.
Not drawn on floating panes: their PIN control owns that corner and is
hit-tested by its drawn position, so anything to its right silently moves it out
from under the pointer. Caught by
test_ctrl_click_on_floating_pin_button_toggles_pin, which stopped pinning.
Verified on the test box by injecting real SGR mouse events into an attached
client's pty at 160x45:
click on the button 1 -> 2 panes
click on the title 1 -> 1
click mid-frame 1 -> 1
Two hover snapshots updated, and only those two: the `+` on the hovered pane
(including an unfocused one -- splitting a pane you are not in is useful).
zellij-server 1502 pass, zellij-utils 473 pass.
|
| |
|
|
|
|
| |
The tab harness cannot reproduce this (no rendering means panes_to_hide is
always empty), so the verification is a scripted pty that resizes mid-session.
Written down so it can be re-run rather than re-derived.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A pane created after a responsive collapse/expand cycle was absorbed into a
stack, even at full width. Reported as "a new pane starts collapsed even though
the terminal is big enough", and it appeared to fix itself on resize.
The expand was never really happening. `SwapLayouts::tiled_layout_at` checked
whether a candidate layout fits by positioning it against
`tiled_panes.visible_panes_count()` -- and a stack hides every member but one.
So while collapsed, the check ran with a pane count of 1, and a layout with no
room for the hidden panes passed as feasible. The tab reported
`sl0p:responsive-expand` while every pane stayed in the single rect it had while
collapsed; the next pane added simply joined that stack.
Feasibility is now checked against every tiled pane, hidden or not, which is the
count the layout actually has to accommodate. Only the responsive collapse/expand
resolves layouts through this function -- ordinary swap-layout cycling has its
own body in `swap_tiled_panes` -- so the change is contained to the path that was
wrong.
Verified on the test box (responsive_min_cols 90) with a scripted pty that
resizes mid-session, geometry read back from `list-panes --json`:
before narrow(80) 2 panes -> one rect, both 80x23 (correct collapse)
widen to 160 -> STILL one rect, 160x44 (expand did nothing)
add a pane -> new pane joins the stack
after narrow(80) 2 panes -> one rect (collapse unchanged)
widen to 160 -> x=0 and x=80, side by side (really expanded)
add three panes -> 2+1, then 2x2, then 3+2 grids, no shared rects
wide from the start -> unaffected
narrow again (70) -> collapses, as before
No unit test ships with this, deliberately. Three attempts could not make one
discriminate: the tab harness does no rendering, so nothing populates
`panes_to_hide` and both counts are equal; hiding panes by hand does not help
because the harness's expand takes the BASE path; and asserting on the resolver
directly bypasses the count, since `tiled_layout_at` uses it only for the
feasibility check and returns the raw layout. A test that passes with the fix
reverted is worse than none, so the scripted live reproduction above is the
verification, and it is written down in docs/roadmap.md to be re-run.
zellij-server 1502 pass, zellij-utils 473 pass.
|
| |
|
|
|
|
|
|
|
|
|
|
| |
A pane added after a narrow->wide responsive cycle is absorbed into a stack,
because the tab stays pinned to the synthesized sl0p:responsive-expand layout,
which was generated for the pane count at synthesis time. Creating the session
wide and adding a pane is correct; creating it narrow, widening, then adding is
not. apply_responsive_layout only runs on resize, which is why resizing appears
to fix it.
Includes the instrumented step table and the geometry that proves the stack, so
the next session can start from the mechanism rather than the symptom.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Reported: only one tab for proj_a, where was proj_b? Both tabs existed --
list-tabs showed proj_b at position 1 the whole time -- but there was nothing on
screen to say so, because a generated session had NO tab bar and NO status bar.
sl0ppi passes its own `--layout` when creating a session, which replaces the
user's default layout, and the shipped config gets its chrome from
`layouts/default.kdl`'s `default_tab_template` (the zjstatus pane, which is what
draws the tab names). So a root with five projects looked exactly like a root
with one.
The obvious fix is to emit that `default_tab_template` into the generated
layout. It does not work: a templated tab comes back with `purpose=None`,
because the fork hangs a tab's purpose on the tab's root layout and templating
replaces it. Identity is load-bearing here -- with no purposes `sl0ppi ls` sees
no project tabs and `up` would happily create a second set of them. Caught by
checking list-tabs after the change rather than trusting the tab bar appearing.
So the template is INLINED instead: its body is taken, the project's panes are
substituted for its `children` node, and the result becomes the tab's contents.
The `tab` node keeps its own name/purpose/cwd, and the status bar comes along.
A template with no `children` node is skipped rather than mangled.
The extractor brace-matches by hand but respects strings and comments: a real
zjstatus template is full of `{index}` and `{name}` inside quoted format
strings, and counting those would truncate the block and emit a layout that does
not parse. It reads the live config, so a user's own default.kdl is honoured.
Verified on the test box: tab bar reads "1 proj_a 2 proj_b", tab purposes are
project:proj_a / project:proj_b, `sl0ppi ls` sees both, `up` still says "up to
date", and no phantom-client marker.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Reported symptom: a pane titled "MY FOCUS AND:" in a fresh sl0ppi session.
That is zellij's multi-user indicator. A session created with
`attach --create-background` keeps one client in its connected set forever, so
`connected_clients.len() > 1` and every pane frame renders its multi-user form
with one anonymous colour block. sl0ppi created every session that way, so every
sl0ppi session looked like someone else was in it.
Isolated by comparison rather than by reading:
created with attach --create-background marker on the FIRST attach, and every one after
created by an ordinary attach that dies no marker, ever
Ruled out by measurement: not the CLI-action clients (15 actions with no attach
leaves nothing behind), and not the creating client exiting too early (making it
wait for a server ack -- 94ms, server answered -- changed nothing; that
experiment was reverted rather than shipped with a comment claiming a fix it did
not deliver). The client-id change did not cause this either: it only made it
visible, because recycled ids used to hand the phantom's id straight to the next
client that attached.
So: when `sl0ppi` has a terminal it now creates the session by ATTACHING to it
(`--layout X attach --create NAME`) instead of creating it in the background and
attaching second. That is the path with no leak, and it is one process instead
of two. Headless `up` keeps the background path, where no one is looking at a
pane frame.
This is a workaround. The fork bug is recorded in docs/roadmap.md with what has
been ruled out and where to instrument next, and it still affects anyone calling
`zellij attach -b` directly.
Verified on the test box under a real pty: no marker on creation or on any later
attach, both project tabs present, agents still staged
(purpose='agent:main' held=true cmd='pi').
|
| |
|
|
|
| |
Both causes are now understood and closed, so the known-issue section becomes a
record of what it was and how it was proven fixed.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
`zellij action` failed ~20% of the time against a live session: exit 0, empty
stdout, empty stderr, and nothing in the server log. `sl0ppi ls` inherited it,
and attached clients were dropped with "lost connection to the zellij server".
The chain, which took measuring rather than reading to find:
1. every action first probes liveness (sessions::assert_socket): connect,
send ConnStatus, disconnect. The probe is a full client and takes an id.
2. new_client() handed out the LOWEST FREE id, so the action connection that
follows microseconds later gets the id the probe just released.
3. the main server thread then processes the queued ConnStatus(id), whose
handler replies and calls remove_client!(id) -- removing the *action*
client, which now owns that id.
4. the action's reply goes through send_to_client, which returns Ok(()) for an
unknown client. The payload is dropped in silence; the client sees only the
disconnect and exits 0 with nothing to print.
Fix: hand out ids from an advancing cursor instead of reusing the lowest free
one, so a stale removal cannot land on a fresh connection. ClientId is a u16 and
every CLI invocation burns two, so the cursor wraps and skips ids that are still
in use rather than assuming they are free.
Measured on the 2-vCPU Debian box that surfaced this:
before after
list-tabs ~20-25% 0/50 failed
list-panes ~20-25% 0/50 failed
list-tabs (attached) ~25% 0/30 failed
sl0ppi ls most runs 0/15 failed
5 attach cycles dropped 5/5 stable
Three regression tests: ids are not reused immediately, they advance and skip
live ones, and they wrap without colliding or hanging. zellij-server: 1502 pass.
The silent-drop in send_to_client is left as-is but is worth revisiting: an
unknown client id is a bug in the caller, and swallowing it is what made this
cost a day instead of a minute.
|
| |
|
|
|
|
|
| |
Records the exact function and the two defects in it (judging liveness on the
first message received, and deleting a live socket on a transient
ConnectionRefused), plus what the resize fix did and did not solve. The
handshake change is deliberately left unmade rather than rushed.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Attaching to a session of `start_suspended` panes could drop the client with
"your zellij client lost connection to the zellij server", and `zellij action`
against that live session failed most of the time.
Cause: `set_terminal_size` treats "no fd for this terminal id" as an error, but a
reserved-and-not-yet-started terminal is exactly what `start_suspended` (and any
held pane) is. Resizes arrive in cached bursts (ResizeCache), so every attach and
every layout change logged one formatted, contexted, file-written error PER
suspended pane, on the server's hot path.
The map distinguishes the two cases already -- `Some(None)` is reserved-without-fd
(reserve_terminal_id), `None` is genuinely unknown -- so the fix is to match them
separately and stay quiet about the former.
Measured on a 2-vCPU Debian box, `action list-tabs` against a live session:
before 2 suspended panes 5/12 failed 2 plain panes 1/12 failed
after 2 suspended panes 6/30 failed 2 plain panes 9/30 failed
resize errors logged: 126+ -> 0
ie. the suspended-pane penalty is gone; what remains is a separate baseline
failure rate that affects plain sessions equally (see the known issue in
docs/roadmap.md).
zellij-server: 1499 tests still pass.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Three bugs found on a fresh Debian box, all of which made sl0ppi look broken
when the terminal underneath it was the problem.
1. JSON crash on startup. `action list-panes --json` against a session that has
died exits 0 with EMPTY stdout, and JSON.parse("") threw "JSON Parse error:
Unexpected EOF" at the user. Empty output is now "nothing", and genuinely
malformed output is reported with the command name and the offending text.
2. `up` silently resurrected. A dead-but-resurrectable session is not an empty
slot: `attach --create-background NAME` restores it from the serialised copy
and IGNORES --layout. The result was a root full of projects coming back as
plain shells with no agents and no purposes -- and the resurrected background
session then exited again within seconds. sl0ppi now refuses to guess and
explains the two real options (`up --fresh` to rebuild, `sl0ppij attach` to
restore it properly in a terminal). Verified on a clean slate that a fresh
`up` produces exactly the right panes: purpose='agent:main' held=true cmd='pi'.
3. The terminal's CLI actions are racy. Measured against a LIVE session on a
2-vCPU box: list-tabs and list-panes each fail ~60% of the time while the
session is detached, ~25% with a client attached, either with empty output or
"There is no active session!" (the action client consults
get_active_session() before it looks at -s). That is a defect in sl0ppij --
filed as a known issue -- but these are idempotent reads, so sl0ppi retries
them with backoff and distinguishes "the session is gone" from "the terminal
would not answer". `sl0ppi ls` went from failing most runs to 8/8.
Also:
- session creation returns before the server is registered, so `up` now waits
for readiness instead of racing its own next command
- UserError: actionable answers ("no project foo", "that session is a saved
dead copy") print and exit 1 instead of falling through to sl0ppij, which
buried the explanation and started a session nobody asked for
- `sl0ppi print <project>` implemented; it was referenced by the init template
and the design doc but did not exist
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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 -> 0.45.0+sl0p1
pi --version -> 0.84.1
sl0ppi doctor -> all good
The vhost itself is committed in the sl0pday repo, where that box's nginx config
lives.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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 -> 0.45.0+sl0p1 (static musl, no libc dependency)
pi --version -> 0.84.1 (bundled bun)
sl0ppi doctor -> all good
sl0ppi init x2 -> two projects scaffolded
sl0ppi up -> session "dev", 2 tabs, agents suspended
sl0ppi ls -> agent:suspended, 1 pane each
sl0ppi up (again) -> "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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Slices 3 and 5 of docs/cli-design.md.
`init` writes .sl0ppi/layout.kdl. The template is mostly comments, because the
purpose vocabulary and the start_suspended habit are the two things nobody can
guess, and the point of use is the cheapest place to explain them. A test
asserts the template passes doctor's own fragment check -- shipping people a
starting point that fails our validation would be embarrassing.
`doctor` splits into gathering facts (IO) and judging them (pure), so the rules
are testable. It is honest about its limits: sl0ppij has no parse-only mode, so
fragments get a structural check (empty, unbalanced braces, a whole `layout {}`
pasted in, a `tab` node sl0ppi owns) rather than a claim of validity. It also
reports the known packaging hole -- pir needs python3 and fzf, neither of which
is in the tarball.
Driving them for real turned up a worse bug than either command:
a single broken fragment took down the entire root
On session creation every tab shares one layout file, so one unbalanced brace
made the whole `up` fail and no project opened at all. Fragments are now checked
before use on BOTH paths, broken ones are skipped with the reason, and the rest
open. `up` also registers the root even when some projects failed, since
forgetting it would orphan the tabs that did open.
Two smaller fixes from the same session:
- falling open with no tty used to spawn a bare sl0ppij, which silently
CREATES a session; that is where a handful of randomly-named junk sessions
came from during testing. It now reports and exits.
- a rename leftover: `setup --check` still described "the configuration of
zellij".
Test isolation lesson worth recording: sessions live in the runtime dir
(/run/user/1000/sl0ppij), not TMPDIR, so setting TMPDIR alone does not isolate
them -- an earlier test session called `root` was silently reused by a later
test. SL0PPIJ_SOCKET_DIR is what actually isolates.
88 tests green.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Slice 2 of docs/cli-design.md. The commands read the world (discovery +
list-tabs), compute a plan, then apply it -- never mutating while walking, which
is how `up` stays idempotent.
sl0ppij.ts process wrapper; every piece of state comes back out of
list-tabs/list-panes, so there is nothing to desync
layout.ts composes the tab node around a project's fragment
root.ts root resolution, session naming, the root registry
workspace.ts up / ls / open / close
index.ts dispatch + the fail-open wrapper
Four things this turned up that reading the code would not have:
- `attach --create-background` fails with "There is no active session!" when
the caller is itself inside a session -- which sl0ppi almost always is,
being run from the user's daily-driver terminal. Session creation now runs
with the SL0PPIJ*/ZELLIJ* markers stripped: we are creating a new detached
session, not nesting.
- falling open by passing our own argv to sl0ppij just produces "unrecognized
subcommand: up" and still leaves the user without a terminal. Our verbs and
flags are stripped first, so what is handed over is something sl0ppij
actually understands.
- attaching panics in the client when stdin is not a tty, so `up` from a
script or CI step died. It now reports the session is ready and exits.
- a start_suspended pane reports `is_held`, not `exited`. Conflating them made
`ls` claim every agent was running when none had started. Plugin panes (the
status bar, the link handler) also inflated pane counts and are now excluded
as chrome.
Index conventions were verified against a live session rather than inferred,
because guessing closes the wrong tab: list-tabs `position` and
`close-tab --tab-id` are 0-based, `go-to-tab` is 1-based, and closing renumbers
every tab after it, so batched closes run in descending position order.
Verified end to end against a real session (three projects, one shipping a
custom fragment): up creates the session with all tabs and their purposes; up
again reports "up to date" with tabs unchanged; ls reports panes and
agent:suspended correctly; open creates-and-focuses and resolves unique
prefixes; close removes a tab; deleting a project auto-closes its tab; and with
the root itself missing, nothing is closed at all.
64 tests green.
|
| |
|
|
|
|
| |
Runtime new-tab dropped purposes until 5873374; the design depends on it, so
the dependency is written down rather than assumed. Also notes the single-line
KDL terminator trap, which cost a debugging round.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
`new-tab --layout` / `--layout-string` created the tab and silently dropped
every purpose the layout declared -- the tab's own (which rides the tab's root
layout) and each pane's. Layouts applied at session creation kept theirs, so
this only broke tabs added at runtime, which is exactly what an orchestrator
does. Found by integration-testing the sl0ppi CLI against a real session rather
than trusting that the documented feature worked on this path.
Cause: TiledPaneLayout and FloatingPaneLayout had no protobuf `purpose` field,
so anything travelling as an Action lost it. The original workspace-hints commit
says as much in a comment -- "adding a proto field would force regenerating the
committed assets" -- because generated prost output is checked in and protoc was
not available. It is now (a user-local protoc 25.1, no sudo, no system package),
so the deferral can be paid off properly instead of worked around.
There are two protobuf schemas and the distinction matters: plugin_api/ is what
plugins speak, client_server_contract/ is what the CLI speaks. The first attempt
fixed only plugin_api and changed nothing observable; both now carry the field.
plugin_api/action.proto TiledPaneLayout.purpose = 14
FloatingPaneLayout.purpose = 13
client_server_contract/common_types TiledPaneLayout.purpose = 16
FloatingPaneLayout.purpose = 15
Purposes are sanitised on ingest, so a value arriving over IPC obeys the same
invariant as one parsed from a layout file (bounded token, <=64 chars) whoever
constructed the message. Verified live: a tab declaring `rm -rf /; echo "x"` is
stored as `rm-rf/echox`, and a 200-char pane purpose arrives capped at 64.
Tests: two new wire round-trips (a NewTab layout carrying tab, pane and floating
purposes; and the sanitiser). zellij-utils 473 passed, zellij-server 1499
passed. Live: `new-tab --layout-string` now reports
purpose='project:added/nested' with panes 'agent:main' and 'logs:app'.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Slice 1 of docs/cli-design.md: the only part with logic worth testing hard.
Pure functions, no subprocesses, so the tests hit them directly.
purpose.ts tab identity as a zellij tab purpose carrying the project's
root-relative path
discover.ts depth-1 scan for .sl0ppi/ markers
plan.ts discovered projects vs list-tabs -> {create, close, keep, foreign}
Identity carries the *path* because `list-panes --json` exposes no cwd (checked
against a live session, not assumed), so there is otherwise no way back from a
tab to a directory on disk. Slashes survive the fork's sanitiser, so nested
paths encode fine.
Two safety properties, both fuzzed over 5000 random names:
- a purpose never exceeds 64 chars and always survives re-sanitising, so what
we generate is what the terminal stores
- projectRelPath() reconstructs the original path exactly or returns null --
never a different path. Wrong answers close the wrong tab; refusing only
leaves a stale one.
The fuzz caught a real bug in the second property. The first implementation
detected lossy encodings by re-encoding and comparing, which silently failed:
`project:<47 a's>.<hash8>` is itself 64 legal characters, so it re-encodes to
itself, the check passed, and a bogus path was "reconstructed" -- which would
have closed a live tab. Lossy purposes now carry a structural marker (a leading
dot, which no discovered path can have) instead of relying on a comparison.
Plus the mount guard from the design: when the ROOT itself is missing, close
nothing. An unmounted filesystem is indistinguishable from mass deletion, and
one of those two readings eats your whole session.
31 unit tests + 2 fuzz properties, 10178 assertions, green.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
One root dir = one sl0ppij session, one project = one tab, panes from the
project's .sl0ppi/layout.kdl fragment.
Verified against the fork before designing on top of it, rather than assuming:
- `action new-tab --layout|--layout-string --name --cwd` exists and returns
the new tab id
- `tab name= purpose=` is reported by `action list-tabs --json`
- purposes accept [A-Za-z0-9_.:/-] up to 64 chars, so "project:foo.a1b2c3"
survives the sanitiser verbatim
- layout-declared purposes outrank in-band ones and survive resurrection, so
tab identity cannot be spoofed from inside a pane and is stable across a
session restart
- `start_suspended` exists, which is what keeps `up` on a twelve-project root
from becoming twelve running servers
Load-bearing property: `up` diffs discovered projects against list-tabs and
creates only what is missing, so running it repeatedly is a no-op. First thing
the tests will assert.
Entry point fails open: any error warns and execs plain sl0ppij with the
original args. Today's incident is the argument -- when the environment breaks,
the terminal is what you debug it with, so the orchestrator must never stand
between the user and their multiplexer.
Also records two problems found while designing: tab overflow (twelve projects
vs a status bar showing ~six, so `open` wants to be a picker) and that the
shipped `pir` needs python3 + fzf, neither of which is in the tarball.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The gnu release build required GLIBC_2.39 (Jan 2024), so the "install it on
another machine" artifact would have failed on Debian 12, Ubuntu 22.04 and
RHEL 9. That is the whole premise of this repo, so it is a defect, not a nit.
make build -> cargo zigbuild --target x86_64-unknown-linux-musl
result -> ELF, statically linked, 0 GLIBC_ symbols, 51M
make build-gnu kept for fast local use
A musl build needs a musl C toolchain: isahc/curl pulls libz-sys and
libnghttp2-sys unconditionally (confirmed: --no-default-features does not drop
them). cargo-zigbuild + the ziglang python module were already installed here,
so this needs no sudo, no docker daemon and no system packages.
stage.sh now refuses to be quiet about it: it reports "statically linked" or
warns loudly that the artifact inherits the build box's libc.
Re-verified from the tarball in a throwaway HOME with node/npm/bun/cargo
replaced by poison stubs: static binary, pi 0.84.1 on the bundled bun, both wasm
plugins installed, session starts under a pty with no errors.
Remaining libc floor is bun's own glibc 2.17 (2012); bun publishes a musl build
if Alpine ever matters.
|
| |
|
|
|
|
|
| |
The disk risk in the roadmap described a situation that no longer exists (12G
free, no monorepo build yet). Replaced with the real standing risk: sl0ppij/target
is a symlink into the old ~/lol/zellij checkout, so deleting that checkout breaks
builds until 'rm sl0ppij/target && make build plugins'.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Phase 3: `make package` now produces a tarball that provably works on a machine
with no node, npm, bun or rust.
Two bugs the clean-room test found, neither visible until a session actually
started:
- the default layout builds its status bar out of zjstatus, which we were not
shipping at all (vendored as sl0pstatus/ in the preceding commits)
- install.sh installed a hardcoded plugin list, so zjstatus.wasm would not
have reached the config dir even once staged
Also: layouts/*.kdl still pointed at ~/.config/zellij, missed by the rename pass
because that pass only rewrote config.kdl.
Verified end to end, from the tarball, in a throwaway HOME with node/npm/bun/
cargo shadowed by stubs that fail loudly if invoked:
sl0ppij --version -> sl0ppij 0.45.0+sl0p1
pi --version -> 0.84.1 (on the bundled bun)
pir --help -> ok
sl0ppij -s realtest -> session starts under a pty, config "Well defined",
fork pane titles render, both wasm plugins load,
no errors beyond the first-run permission cache
Artifact: sl0ppi-0.1.0-x86_64-linux.tar.gz, 83M, + .sha256.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Double-clicking a tab in the bar switches to it and drops into RenameTab mode,
matching the double-click-to-rename we added for panes in the sl0ppij zellij
fork. Enter commits, Esc cancels, exactly as the rename keybind does.
Widgets are held behind &self and cannot keep click state, so double-click
detection lives centrally in ModuleConfig::handle_mouse_action and is passed
to Widget::process_click as a flag.
THE TRAP, and the reason this is not a two-line change:
handle_mouse_action also runs for Mouse::Release. A single click therefore
arrives as LeftClick followed by Release at the SAME position. Counting
releases would make every single click register as a double click, and
resetting on release would mean a real double click never registers at all,
since the release falls between the two clicks. Only LeftClick participates;
Release neither counts nor resets.
A completed pair is consumed, so a triple click does not fire a second rename.
switch_tab_to() is called before switch_to_input_mode(), because RenameTab acts
on the ACTIVE tab - renaming without switching first would rename the wrong one.
Threshold is 400ms, the same one zellij uses for text-selection double clicks,
so the bar feels consistent with the panes.
Detection is extracted as register_click_for_double_click() so it can be tested
without constructing a ModuleConfig. Tests cover: a real double click, the
release-in-between regression, too-slow clicks, clicks at different positions,
and the triple-click case.
Upstream tests still pass: 25 total (20 upstream + 5 new).
Note: this repo's .cargo/config.toml targets wasm32-wasip1 and runs tests under
wasmtime, which is not installed here. Tests are run with
`cargo test --target x86_64-unknown-linux-gnu` instead.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Squashed import of dj95/zjstatus at 74f36b4 ("chore: bump version to v0.24.0"),
the merge-base our fork was developed against.
Found by the phase-3 smoke test: installing the tarball into a throwaway HOME
and actually starting a session logged
failed to load plugin from disk:
'~/.config/zellij/plugins/zjstatus.wasm' (No such file or directory)
config/layouts/default.kdl replaces zellij's stock tab-bar + status-bar with a
zjstatus bar, so the status bar is load-bearing for the default layout -- and
the .wasm in the live config dir turned out to be a *custom build*, not the
upstream release. Dropping it as "a third-party blob no longer referenced" (see
the config snapshot commit) was wrong: it was referenced from the layouts, which
that commit did not check.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
config/config.kdl plugin path -> ~/.config/sl0ppij/plugins, builtin plugins
-> the sl0ppij: URL scheme. Verified to load under the
renamed binary ("[CONFIG FILE]: Well defined.").
install-config.sh syncs to ~/.config/sl0ppij
install.sh a stock zellij on the target is no longer a warning, it is
just a fact: the two coexist
stage.sh drops the "renaming at package time is a fig leaf" note
Makefile builds `-p zellij` (package) and ships `sl0ppij` (binary);
`make check` now also gates the root package, using the
prebuilt asset plugins so it does not need debug wasm
docs/ rename.md records what was done, what was deliberately
left (plugin-internal strings live in prebuilt wasm), and
the verification actually performed
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Makes the fork installable and runnable beside a stock zellij: separate binary
name, config dir, cache dir, socket dir, log file and plugin URL scheme.
Everything user-visible is defined once in zellij-utils/src/branding.rs and
referenced from the ~15 places that used to hold a literal, rather than sed-ing
the tree -- upstream has 3300 commits of future rebases, and each touched
upstream line is a conflict forever. A conflict in branding.rs itself always
resolves the same way: keep our constant, take their code.
binary zellij -> sl0ppij
config dir ~/.config/zellij -> ~/.config/sl0ppij
system cfg /etc/zellij -> /etc/sl0ppij
cache/data ProjectDirs(Zellij) -> ProjectDirs(foo.sl0p.sl0ppij)
socket/tmp /tmp/zellij-$UID -> /tmp/sl0ppij-$UID
log zellij-log/zellij.log -> sl0ppij-log/sl0ppij.log
plugin urls zellij:tab-bar -> sl0ppij:tab-bar (zellij: still parses)
Two things deliberately NOT renamed:
- The cargo package stays `zellij`; only [[bin]] is renamed. Renaming the
package would ripple through every intra-workspace dep, the plugin API and
the 0.45.0+sl0p1 version trick for a name nobody sees.
- The $ZELLIJ* environment variables stay, because the ecosystem (shell
prompts, "am I in a multiplexer" checks, our own pi-ext) looks for them. We
now export $SL0PPIJ* alongside them, and those take precedence on read.
branding::import_compat_env() folds an inherited ZELLIJ_CONFIG_DIR into its
SL0PPIJ_ spelling at startup, before clap reads the environment -- clap can
only be told one variable name per argument.
Snapshots: 14 updated, all of them the plugin URL scheme or a session message.
The insta churn (assertion_line metadata, trailing newlines) that a bulk
--force-update introduces across 340+ other snapshots was reverted, so the diff
here is only what actually changed.
Tests: zellij-utils 471 passed, zellij-server 1499 passed. branding.rs carries
its own tests, including one asserting no shipped default path contains the
string "zellij".
Verified live: sl0ppij creates /tmp/sl0ppij-1000 next to an existing
/tmp/zellij-1000, and `sl0ppij list-sessions` reports none while a stock zellij
reports three running sessions.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Adds the layer that makes this a distributable product rather than three
directories:
VERSION one sl0ppi version for the whole monorepo
FORKS.md what each component is based on, and how to rebase the fork
Makefile build / vendor / stage / package, plus dev-box helpers
packaging/ fetch pinned payloads, assemble an install tree, install it
docs/ architecture, the rename plan, the roadmap
The self-contained runtime question is settled and verified: pi upstream builds
its own release binary with `bun build --compile`, and a vendored bun runs the
vendored pi dist unmodified, so the tarball carries bun + pi and needs no system
node, npm or rust on the target machine.
packaging/install.sh does a per-user install under ~/.local, never clobbers an
existing config (writes .new alongside, or backs up under --force-config), and
warns loudly when a stock zellij is present -- because until docs/rename.md is
implemented the binary is still zellij underneath and would collide with it.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The most opinionated part of sl0ppi was never in any repo: it lived in
~/.config/zellij as dotfiles, versioned by ~20 hand-made `config.kdl.pre-*`
backups. Imported verbatim so the packaging work has something to install.
Deliberately not imported:
- config.kdl.pre-* / layouts/*.pre-* / themes/*.pre-* backups (that is what
git is for now)
- plugins/pane-finder.wasm (a build output of sl0ppij/sl0p/zj-pane-finder)
- plugins/zjstatus.wasm (a third-party blob no longer referenced by
config.kdl -- dropped as dead weight)
The snapshot is faithful, so it still hardcodes development-machine paths;
config/README.md lists what the rename work has to parameterise.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The repo already treated extensions as a deliberate snapshot: copy every
*.ts into ~/.pi/agent/extensions, skip files already identical. Standalone
helpers in scripts/ had no such path, so pir had to be copied by hand and
then drifted from the checkout.
CLI_TOOLS is a "<source in scripts/>:<installed name>" list, so the
installed command name can differ from the filename (pi-resume -> pir).
Same copy-not-symlink policy as the extensions, for the same reason: the
global set should not track in-progress edits. Copies are chmod +x, the
destination is PI_BIN_DIR-overridable like PI_EXT_DIR, a missing source is
reported and skipped rather than aborting the whole install, and the entries
participate in the existing --dry-run output and the "nothing changed"
tally.
Also records why answer-picker.ts is absent from the list: it needed a
helper binary in its socket+separate-pane form (c88fe0d), and does not any
more now that zellij draws and hit-tests the buttons itself and reports
clicks on pi's stdin. That implementation is parked in attic/, which this
script does not glob -- top-level *.ts only -- so nothing there installs.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
`pi -c` blindly grabs the newest session, and the alternative is digging
through ~/.pi/agent/sessions/--home-user-dev-foo--/ for a UUID by hand.
This lists every session for the cwd in an fzf picker with a live transcript
preview and execs `pi --session <file>` on the pick.
pir pick a session for this dir
pir -a pick across every project dir
pir <query...> start fzf with an initial query
pir -g TEXT only sessions whose full transcript contains TEXT
The -g pass exists because the fzf query matches the list line -- title and
id -- not deep history, so "the session where I fixed the mouse bug" is
unfindable without it. It shells out to rg when available and falls back to
grep.
In the picker: enter resumes, ctrl-f forks instead (execs --fork, leaving the
original untouched), ctrl-o prints the path and exits without launching,
ctrl-a toggles current-dir vs all-dirs, ctrl-s toggles recent vs longest.
The toggles are fzf --bind=reload of this same script, which is what --list
and --preview are for -- they are internal modes invoked by fzf, not user
entry points.
Session directory resolution mirrors pi's own getDefaultSessionDirPath()
encoding, and honours PI_APP_NAME / <APP>_CODING_AGENT_DIR, so a renamed or
relocated agent dir still resolves. Forked sessions are detected from the
header's parentSession and marked in the list.
Requires fzf; checked up front with a clear error rather than failing inside
a pipeline.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Puts the follow-ups you retype constantly ("continue", "yes", "run the
tests") into the bottom frame row of pi's own pane, using the sl0p zellij
fork's OSC 5577 pane-status-bar protocol. Click one or press alt+N and it is
sent as a real user message.
╰ alt+N · ready ──[continue]─[yes]─[no]─[explain]───╯
WHY THE THIRD DESIGN. v1 drew its own button row and enabled terminal mouse
reporting inside pi. It worked and cost too much: mouse reporting is
per-terminal, not per-region, the wheel is just another button in that
protocol (SGR bit 64), and the DECSET modes only select how much MOTION is
reported -- there is no bit that selects which buttons. So pi's pane traded
native wheel scrolling and click-drag selection for clickable buttons, and
because pi renders inline into the main screen it cannot scroll the
emulator's scrollback back to compensate. v2 moved the buttons to a second
pane to confine the tracking; that restored scrolling but cost a pane, a
picker process and a unix socket. OSC 5577 removes the tradeoff instead of
relocating it: zellij draws the buttons in the frame it already owns,
hit-tests them itself, and reports clicks on our stdin. No mouse reporting
anywhere, no extra pane, and the frame row does not shrink the content area
so nothing resizes the PTY. v2 is parked in attic/ (not globbed by
install.sh) as the only non-zellij path, should one be wanted.
ANSWERS come from DEFAULTS until <cwd>/.pi-answers.md exists, after which
the file is authoritative. It is SEEDED with the defaults on first write,
which makes "the file wins" and "you still get the defaults" one rule
instead of a merge order to remember -- and it means deleting a line
genuinely removes a default, which an additive model could not express.
/answers add|rm persists in one step; hand-edit + /answers reload also works.
Labels are backtick-delimited in the markdown so a body may contain the
em-dash separator (as "no -- don't do that" does).
/answers list renders a table via appendEntry + registerEntryRenderer, so it
is scrollable and copyable in the transcript but never enters LLM context.
BUTTON IDS ARE DERIVED FROM THE LABEL (ans-ship), not positional. With a
mutable answer list, positional ids are a live bug: change the answers and
an in-flight click for a vanished button resolves to whatever now occupies
that index and fires the wrong prompt. Label-derived ids fail the allow-list
instead.
BUTTONS EXIST ONLY BETWEEN TURNS. While a run is in flight they are
withdrawn -- so a click cannot queue a second answer behind the one already
working -- and the status text stays up to advertise that alt+N still does.
Withdrawing sends `buttons` with an empty payload, NOT `clear`: the fork's
buttons verb assigns parse_status_bar_buttons(payload), which filter_maps
away every field lacking an id:label, while `clear` is the one verb that
would also wipe the status text. Restoring keys off agent_settled as much as
agent_end, since agent_end never fires on the abort/error paths -- an escape
cancel or a provider blowup would otherwise leave the frame permanently bare.
SECURITY. These are in-band escapes, so anything that writes to the pane (a
cat of a hostile file) can declare buttons and draw a misleading bar. A
click report is a hint that a user pressed something, not proof we put it
there: ids are validated against what we actually declared, and the action
is deliberately non-destructive -- it sends a prompt, it does not deploy.
Click reports arrive interleaved with ordinary stdin and are stripped, or
they would be typed into the editor; pi-tui's StdinBuffer holds an ESC ]
sequence until its ST/BEL terminator, so we always see whole reports.
Caps match the fork exactly (8 buttons, 512-char status, 32-char ids), so we
never rely on zellij silently dropping our excess. Answers past the 8th stay
on alt+N and the status line says so.
|