aboutsummaryrefslogtreecommitdiffstats

The sl0ppi CLI — design

Status: agreed, not yet implemented. This is the contract the MVP is built against. Decisions marked (deferred) are deliberately out of the MVP.

The problem

sl0ppij list-sessions on the development box returns 65 sessions, nearly all EXITED, named effulgent-magpie, chatty-ocelot, rusty-capsicum. Nothing maps a session to a project, so resuming work means remembering which animal was which repo.

The two halves of sl0ppi each solve part of this and do not talk to each other:

  • sl0ppij knows the terminal arrangement, and via purpose= can record what each pane and tab is for — but it deliberately never interprets that string.
  • pir knows the agent history for a directory — but nothing about the terminal.

The CLI's job is to own the vocabulary the fork refuses to own, and to make reattaching restore both halves at once.

Model

sl0ppij session  =  a ROOT dir     ~/dev            -> session "dev"
  tab            =  a PROJECT      ~/dev/foo        -> purpose "project:foo.a1b2c3"
    panes        =  .sl0ppi/layout.kdl              -> purposes "agent:main", "service:web", ...

One root, one session. One project, one tab. Panes come from the project.

Identity lives in the tab purpose

The tab name is a UI string a human renames; the tab purpose is the key. This is not a preference, it is what the fork guarantees (see sl0ppij/sl0p/workspace-hints-handover.md):

  • reported by sl0ppij action list-tabs --json
  • layout-declared purposes outrank in-band ones and cannot be overridden, so cat hostile.txt in a pane cannot relabel a project tab
  • purposes survive resurrection, because restored tabs come back through the layout — so identity is stable across a session restart

Slug format: project:<basename>.<hash8> where the hash is of the absolute path. Keeps identity unique when two roots both contain api, and stays well inside the 64-char limit. The sanitiser accepts [A-Za-z0-9_.:/-], so the format survives verbatim.

Commands

sl0ppi                     attach the root session. Inside a project? focus its tab
sl0ppi up [--root DIR]     create/attach, then ensure one tab per discovered project
sl0ppi ls                  projects x (tab? panes? agent alive?)
sl0ppi open <project>      ensure + focus a tab (creates on demand)
sl0ppi close <project>     close the tab, leave the session
sl0ppi init [dir]          scaffold .sl0ppi/layout.kdl
sl0ppi agent [project]     focus the agent:* pane; --resume picks up its pi history
sl0ppi doctor              verify install, plugins, config, versions

up is idempotent, and everything rests on that

up diffs the discovered projects against list-tabs --json and creates only what is missing. Running it fifty times produces the same session. If this property ever breaks, the tool is worse than useless, so it is the first thing the tests assert.

Decided

D1 — Discovery is depth-1. Direct children of the root containing a .sl0ppi/ directory. Recursive scanning of ~/dev is slow and surprising. sl0ppi open path/to/nested always works explicitly.

D2 — Directories without .sl0ppi/ are ignored. No implicit tabs. Use sl0ppi init to opt a project in.

D3 — Tab order is alphabetical. MVP. A root-level order file is (deferred).

D4 — Stale tabs auto-close. A tab whose project: purpose points at a missing directory is closed by up/ls. Rationale: the purpose encodes an absolute path, so "the directory is gone" is unambiguous. Caveat to watch: a temporarily unmounted path would look identical to a deleted one, so auto-close must skip closing when the root itself is missing (that is a mount problem, not a deleted project).

D5 — Every project declares an agent. The scaffolded layout includes a pane with purpose="agent:main" running pi.

D6 — Command panes are start_suspended by default. A root with twelve projects must not become twelve running servers and twelve agents on up. The tab and its arrangement exist; nothing executes until the pane is focused and gets a keypress. --eager opts in per run.

D7 — .sl0ppi/layout.kdl is a fragment: pane nodes only, no wrapping layout {} or tab {}. sl0ppi composes the tab around it:

tab name="foo" purpose="project:foo.a1b2c3" cwd="/home/user/dev/foo" {
    <fragment verbatim>
}

Pure string composition, no KDL parser, and sl0ppi keeps sole control of identity and cwd. sl0ppi print <project> emits the full layout for use with plain sl0ppij --layout.

D8 — Running sl0ppi inside the session focuses, it does not nest. Detected via $SL0PPIJ/$ZELLIJ.

D9 — sl0ppi is the daily entry point, and it fails open. Any error — bad layout, unreadable root, unexpected state — warns on stderr and execs plain sl0ppij with the original arguments. An orchestrator bug must never leave you without a terminal; when the environment breaks, the terminal is what you debug it with. sl0ppij stays fully usable on its own, and sl0ppi ignores sessions it did not create.

D10 — TypeScript on the bundled bun. We already ship bun and pi's ./client SDK, and pi-ext is TypeScript, so this adds no runtime. It is mostly JSON and process spawning; a Rust binary would share nothing with the extensions. It also gives pir a path off its undeclared python3 dependency (see below).

Non-goals

  • Not a session manager competing with zellij's; ad-hoc sl0ppij -s foo sessions are none of its business.
  • No daemon, no database. All state is derived from list-tabs --json, list-panes --json and the filesystem, so there is nothing to desync.
  • Never required. Deleting sl0ppi leaves sl0ppij and pi working exactly as now.
  • Does not auto-launch agents you did not ask for (hence D6).

Known problem: tab overflow

Twelve projects is twelve tabs in a status bar that shows maybe six before it truncates. At that scale the tab bar is a status display, not navigation — which is presumably why zj-pane-finder exists. So sl0ppi open should be a fuzzy picker rather than a name argument you must remember.

MVP: sl0ppi open <name> with prefix matching. A picker is the first thing to add after, and if tabs turn out to be the wrong container at 20+ projects, that is the design review trigger.

Known problem: pir is not self-contained

The shipped bin/pir is #!/usr/bin/env python3 and shells out to fzf. Neither is in the tarball; the clean-room test only passed because /usr/bin was on PATH. So the packaging claim "no system runtime needed" is currently true for sl0ppij and pi, and false for pir.

Absorbing pir into sl0ppi agent --resume (D10) removes the python3 dependency. fzf then becomes either a declared external dependency or an internal picker.

Fork changes this design required

Designing against the fork turned up one real gap, found by integration-testing rather than reading:

  • purposes were dropped by runtime new-tab (sl0p: carry pane and tab purposes over the wire). TiledPaneLayout/FloatingPaneLayout had no protobuf purpose field, so a layout sent as an Action lost every purpose it declared. Session-creation layouts kept theirs, which is why the feature looked fine until an orchestrator tried to add a tab. Both protobuf schemas now carry it, sanitised on ingest.

Also worth recording, because it costs an afternoon otherwise: KDL nodes on a single line need ; terminators. tab x { pane } fails to parse; the generator emits multi-line KDL, which sidesteps it entirely.

MVP scope

  1. discovery + slug + list-tabs --json diff (the idempotency core)
  2. up, ls, open, close
  3. init scaffolding a layout with agent:main + start_suspended
  4. fail-open wrapper (D9)
  5. doctor

Deferred: agent --resume (needs the pir port), fuzzy picker, root order file, eager/lazy per project, multi-root listing.