aboutsummaryrefslogtreecommitdiffstats
path: root/cli/src (follow)
Commit message (Collapse)AuthorAge
* sl0p: keep a templated tab's purpose, so runtime tabs get a status baruser2026-08-13
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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.
* cli: carry pane templates into the generated layoutuser2026-08-13
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | `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.
* cli: keep the status bar, so other projects are visibleuser2026-08-13
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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.
* cli: create sessions by attaching, to dodge the phantom-client buguser2026-08-13
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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').
* cli: survive a dead session, and a flaky terminaluser2026-08-12
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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
* cli: init and doctor, and stop one bad project breaking a rootuser2026-08-12
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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.
* cli: up, ls, open, closeuser2026-08-12
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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.
* cli: the idempotency core (discovery, tab identity, plan diff)user2026-08-12
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.