<feed xmlns='http://www.w3.org/2005/Atom'>
<title>sl0ppi.git/cli/src/commands, branch main</title>
<subtitle>a sl0p.foo repository</subtitle>
<id>https://git.sl0p.foo/sl0ppi.git/atom/cli/src/commands?h=main</id>
<link rel='self' href='https://git.sl0p.foo/sl0ppi.git/atom/cli/src/commands?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>cli: keep the status bar, so other projects are visible</title>
<updated>2026-08-13T11:06:44Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-13T11:06:44Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0ppi.git/commit/?id=6d9e46274c0d7613441338e10fea6972e4d335dd'/>
<id>urn:sha1:6d9e46274c0d7613441338e10fea6972e4d335dd</id>
<content type='text'>
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.
</content>
</entry>
<entry>
<title>cli: create sessions by attaching, to dodge the phantom-client bug</title>
<updated>2026-08-13T10:52:55Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-13T10:52:55Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0ppi.git/commit/?id=f5712cda26448eff37f00cd3e25852ef37a85eee'/>
<id>urn:sha1:f5712cda26448eff37f00cd3e25852ef37a85eee</id>
<content type='text'>
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() &gt; 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').
</content>
</entry>
<entry>
<title>cli: survive a dead session, and a flaky terminal</title>
<updated>2026-08-12T21:49:19Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-12T21:49:19Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0ppi.git/commit/?id=acf61609ee7b99138251cea879d593d5f1689392'/>
<id>urn:sha1:acf61609ee7b99138251cea879d593d5f1689392</id>
<content type='text'>
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 &lt;project&gt;` implemented; it was referenced by the init template
    and the design doc but did not exist
</content>
</entry>
<entry>
<title>cli: init and doctor, and stop one bad project breaking a root</title>
<updated>2026-08-12T20:35:05Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-12T20:35:05Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0ppi.git/commit/?id=04a851c5aa543c362968ea562335cde64e4bc6b4'/>
<id>urn:sha1:04a851c5aa543c362968ea562335cde64e4bc6b4</id>
<content type='text'>
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.
</content>
</entry>
<entry>
<title>cli: up, ls, open, close</title>
<updated>2026-08-12T20:24:55Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-12T20:24:55Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/sl0ppi.git/commit/?id=2cd247828d863cdc779c7178c988152835096ca2'/>
<id>urn:sha1:2cd247828d863cdc779c7178c988152835096ca2</id>
<content type='text'>
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.
</content>
</entry>
</feed>
