1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
|
# Handover
Written at the end of a long session. Start here.
## Pick this up first: the `+` split button does not render
**Feature:** a `+` at the right end of every pane's title line; clicking it
splits that pane. Shipped in 0.1.7 (hover-gated) and 0.1.8 (always drawn).
**Status: the click works, the button is invisible on a live session.**
- clicking those 3 columns splits the pane — verified by injecting real SGR
mouse events into an attached client's pty (`1 -> 2 panes`; clicking the title
or mid-frame correctly does nothing)
- the button renders in **265 unit snapshots**
- it has never once been seen on a real session
Two wrong diagnoses so far, both mine:
1. "It's hover-gated and hover is broken" → removed the gate in 0.1.8, still
invisible. (`mouse_is_hovering_over_pane` does appear to be false live, but
that was not the only problem.)
2. "The click test proves the feature" → it does not. The hit-test is purely
geometric and never consults the renderer, so it kept working while nothing
was drawn.
**The strong lead, untested:** every snapshot uses zellij's DEFAULT frame
config. The live config does not:
```kdl
pane_frames { rounded_corners true; title_alignment "center"; gap 1; padding 0 0 0 0 }
```
`render_title_right_side` is called from **three** different title builders
(`zellij-server/src/ui/pane_boundaries_frame.rs`, lines ~889, ~915, ~983) with
different `max_length` budgets. The snapshots only ever exercised the
left-aligned path; `title_alignment "center"` takes another one.
**Ten-second check before writing any code:** set `title_alignment "left"` in
`~/.config/sl0ppij/config.kdl` on the test box, re-attach, look for the `+`. If
it appears, that is the bug confirmed.
**Then:** add a frame unit test using the live config's alignment/rounded/gap
settings — the reproduction that was missing all along — and fix whichever
builder drops the button.
Code involved:
| what | where |
|---|---|
| render + geometry | `ui/pane_boundaries_frame.rs`: `render_split_button`, `split_button_columns`, `SPLIT_BUTTON_*` |
| click | `tab/mouse_handler.rs`: `intercept_split_button_click` |
Not drawn on floating panes on purpose: the PIN control owns that corner and is
hit-tested by its drawn position, so anything to its right moves it out from
under the pointer (`test_ctrl_click_on_floating_pin_button_toggles_pin`).
## Where the project is
Everything is committed and pushed to `ssh://git@git.sl0p.foo:39327/srv/git/sl0ppi.git`.
Released artifacts are at <https://sl0ppi.sl0p.foo> (currently 0.1.8).
| phase | state |
|---|---|
| monorepo, 3 vendored forks with history | done |
| rename (sl0ppij coexists with zellij) | done |
| portable config | done |
| tarball + installer, verified in a clean room | done |
| `sl0ppi` CLI MVP (`up`/`ls`/`open`/`close`/`init`/`print`/`doctor`) | done |
| site + release scripts (no CI, by choice) | done |
`make` for targets, `make test` for both suites, `make release && make publish`
to cut and upload a version, `packaging/publish.sh` verifies against the live
URL afterwards.
## Open, in the order I would do them
1. **The `+` rendering** (above).
2. **Phantom client on headless `sl0ppi up`.** `attach --create-background`
leaves one client in the session forever, so every pane frame shows
`MY FOCUS AND:`. Worked around by creating the session *by attaching* when
there is a tty; the headless path still leaks. Ruled out: the CLI-action
clients, and the creating client exiting too early (making it wait for a
server ack changed nothing). `screen.remove_client()` does clear
`connected_clients` with no early return, so instrument
`ScreenInstruction::RemoveClient` on a debug build to find where it is lost.
Cheap alternative: give the headless path its own pty (`script -qec`) and
kill the client afterwards — killed clients demonstrably do not leak.
3. **bun is the AVX2 build.** Caused `Illegal instruction` on a plain KVM guest.
One line in `packaging/fetch-vendor.sh`: `bun-linux-x64-baseline.zip`.
4. **`pir` needs python3 + fzf**, neither shipped. The fix is folding it into
`sl0ppi agent --resume` (already designed in `docs/cli-design.md`).
5. **Tab overflow.** `sl0ppi open <name>` wants to be a fuzzy picker once a root
has a dozen projects.
6. Button polish: shift/right-click for the other split direction; reuse
`status_bar_button_style` so it looks pressable.
Fixed this session, for context: client ids were recycled (silently ate ~25% of
`zellij action` output), suspended panes spammed resize errors, responsive
layouts resolved against `visible_panes_count()` and never really expanded,
runtime `new-tab` dropped pane/tab purposes, and generated sessions had no tab
bar. See `docs/roadmap.md` for each with its measurements.
## Traps worth knowing (all cost me time here)
- **`pkill -f` / `pgrep -f` match your own shell's command line.** I killed my
own script three times, and twice killed the user's live clients. Match on
`comm` instead: `ps -eo pid,comm,args | awk '$2=="sl0ppij"'`.
- **Test sessions live in the runtime dir**, not `TMPDIR`. Only
`SL0PPIJ_SOCKET_DIR` isolates them. A stale session called `dev` was silently
reused across tests.
- **`INSTA_FORCE_UPDATE=1` churns hundreds of snapshots** (strips
`assertion_line`, trailing newlines). Update per-test with
`INSTA_UPDATE=always cargo test ... <test_name>` and verify the diff contains
only what you expect.
- **git paths in loops**: `git diff --name-only` prints repo-root-relative
paths; running `git checkout --` on them from inside `sl0ppij/` silently
fails.
- **Single-line KDL needs `;` between nodes.** `tab x { pane }` will not parse;
emit multi-line.
- **A tab template destroys tab purposes** — identity is a tab `purpose`, and
templating replaces the tab's root layout. That is why `sl0ppi` inlines the
template into each tab instead of emitting `default_tab_template`.
- **The tab test harness does no rendering**, so `panes_to_hide` is always empty
and stack-related bugs cannot be reproduced in it. Use a scripted pty.
- **Clean up test sessions.** A background-created session I left behind is what
produced a "MY FOCUS" bug report from the user.
## Access
- test box: `ssh root@10.10.10.130` (Debian, 2 vCPU; where every live check ran)
- prod: `ssh root@sl0p.foo`; nginx config lives in `~/dev/sl0pday/nginx/sl0p.conf`,
deployed with `./deploy.sh nginx/sl0p.conf` (that repo has **no** git remote)
- `sl0ppi.sl0p.foo` currently answers **401 to everyone except 87.209.3.148**
(our egress). Lift by deleting the `geo` block and the `if` in the vhost.
- new git repos: `ssh root@sl0p.foo git-new-repo <name>`
- releases need `cargo-zigbuild` + the `ziglang` python module (both installed);
`protoc` 25.1 is in `~/.local/bin` for `cargo xtask proto`.
## How to verify things here
Unit tests are necessary and have repeatedly been insufficient — three of this
session's bugs passed them. For anything touching panes, layouts or the mouse,
drive a real session:
```python
# pty at a chosen size; resize or inject SGR mouse events mid-session
pid, fd = pty.fork()
if pid == 0: os.execvp("sl0ppi", ["sl0ppi"])
fcntl.ioctl(fd, termios.TIOCSWINSZ, struct.pack("HHHH", rows, cols, 0, 0))
os.write(fd, b"\x1b[<0;COL;ROWM") # left press, 1-based coords
```
then read the truth back out of `sl0ppij -s <session> action list-panes --json`
(x, y, cols, rows per pane — two panes sharing a rect means a stack) and strip
ANSI from the pty output before grepping for glyphs.
|