sloshDriving it

Scripting#

Three things make slosh easy to drive from other programs: a socket that does everything the keyboard does, an escape sequence a pane can use to draw its own chrome, and a headless mode that is the whole program without a terminal.

The control socket#

One JSON object per line, over the session's socket. A detached session answers exactly as an attached one does.

$ slosh -s work cmd '{"cmd":"new-tab","name":"api"}'
{"ok":true,"id":2}
$ slosh -s work cmd '{"cmd":"panes"}'
{"ok":true,"panes":[{"id":1,"title":"nvim","alive":true,...}]}

Panes and tabs are addressed by id, so a background tab is scriptable.

verb
panes tabswhat exists, with ids, rects, titles, purposes, state. A pane's tab_id is what move-pane and select-tab want; its tab is where that tab sits in the strip
snapshotthe composited screen, as JSON, format:"text", or format:"bytes" — the emitter's own output for this frame (a second call is the delta)
deadlinewhen the session wants its next frame, in ms, or -1
sendbytes as if typed, decoded like input ("data":"\\x01\\\\")
rawbytes straight into the focused pane's pty
resizecols rows, and optionally cell_w cell_h
split`dir:"cols"\"rows", id` for which pane to split
focusid
close rerunid, or 0 for the focused pane
clear-shadersid, or 0 for the focused pane; answers `cleared:0\1` — the way back from a pane that painted itself unreadable
new-tab select-tab close-tab move-tab set-nametabs, by id or index
set-name target="pane"a pane's name, which outranks the title its program sets
move-paneid of the pane, tab id to move it into (0 for a tab of its own, with an optional name), `dir:"cols"\"rows"`. The pane keeps running — same pty, same scrollback
floatbare: toggle a pane floating (id, or 0 for the focused one). With any of x y w h it places: floats first when tiled, re-places when floating, never un-floats; omitted fields keep their value
new-floata fresh floating shell over the current tab, centred, in the focused pane's directory; answers id
set-nametarget:"tab" (the default, because that is what this verb has always meant) or "pane", id, name. A pane accepts 0 for the focused one. A pane's name wins over the title the program sets, so this is how a program that keeps announcing something stale gets overruled; an empty name clears it and hands the label back. Refusals say no such pane or no such tab, so a mistyped target is visible in the reply
set-purpose`target:"pane"\"tab", id (or 0 for the focused pane, and the tab you are in), purpose. An empty purpose` clears the slot and unlocks it, handing the label back to the program — a lock held over an empty string was a state nothing could get out of
dump-layout apply-layoutsee layouts. dump-layout takes tab (0 for every tab), relative_to to write every cwd= under that directory instead of absolute, and suspend (as-is none commands all); it answers kdl panes suspended, and an unknown tab is an error rather than an empty document
workspacesthe projects on disk and which of them are open: roots says whether any are configured at all, and each entry has name path purpose layout (a file path, or "") mtime tab (0 when closed) — see workspaces
open-workspacename or path, and suspended; answers tab purpose path created tabs honoured. Already open means focused, with created:false
close-workspacename or purpose; answers closed, how many tabs went
save-workspacewrite this tab as the project's layout: tab (0 for the current one), path for a tab that is not a workspace yet, suspend, and force to overwrite a layout the project already has; answers path purpose panes suspended replaced
notifyput a line in the session's status area
graphicsthe kitty placements on screen, or the bytes sent for them
clipboardwhat the session has copied
reloadre-read the config; answers {"ok":true,"warning":...} if it had a complaint
splashreplay the attach greeting; fx and motion pick the colour effect and the assembly by index, for a deterministic one
edit-configopen the config in a pane
aliveis it running, and how many panes and tabs
quitend the session

Driving a project#

Everything a program needs in a project it has never seen — open it, ask what is in it, act on the purposes the project's own layout declared:

$ slosh -s work cmd '{"cmd":"open-workspace","name":"api"}'
{"ok":true,"tab":3,"purpose":"project:api.5c1f0a3b","path":"/home/you/dev/api","created":true,"tabs":1,"honoured":0}
$ slosh -s work cmd '{"cmd":"open-workspace","name":"api"}'
{"ok":true,"tab":3,"purpose":"project:api.5c1f0a3b","path":"/home/you/dev/api","created":false,"tabs":0,"honoured":0}

The second call focuses what is there and says created:false. Opening is idempotent, so a script drives it in a loop without asking first — and "have I opened this already" is the question a script gets wrong after a crash or a re-attach.

Then read the tab it handed back:

$ slosh -s work cmd '{"cmd":"panes"}' \
    | jq -c '.panes[] | select(.tab_id == 3) | {id, purpose, suspended}'
{"id":7,"purpose":"agent:main","suspended":false}
{"id":8,"purpose":"service:web","suspended":true}
$ slosh -s work cmd '{"cmd":"rerun","id":8}'    # start the dev server

The project's own layout file decided that service:web is the dev server and that it starts asleep; nothing in the session, the config or the calling program had to know that.

workspaces reports each project's layout file mtime, so a tool that kept the mtime it opened a workspace with can tell the file has moved on since — without the session storing a byte on its behalf. Re-applying the changed layout is deliberately not offered: the panes it would replace have processes in them.

A pane can draw its own chrome#

By printing an escape sequence — no plugin, no config:

printf '\033]5577;1;status;building 3/7\033\\'
printf '\033]5577;1;buttons;approve:Approve;cancel:Cancel\033\\'
# clicking [Approve] arrives on the program's stdin as:
#   \033]5577;1;click;approve\033\\

The status text appears in the pane's frame; the buttons are real targets in it. A program that wants to be asked something can ask in place rather than printing a prompt and hoping.

purpose is the other verb: printf '\033]5577;1;purpose;logs\033\\'. A purpose declared by a layout or the control API wins and cannot be overwritten this way.

shader is the third, and the only one the session can refuse: it sets the shader passes for the pane that asked -- a document in the config's own syntax, so an entry's where= decides which rect it lands on and the reply counts what went where. It needs in_band_shaders true because a program restyling your session is a hazard before it is a convenience. shader; with no rect named clears both of that pane's chains, which is never refused -- and clear-shaders above is the same thing from outside, for a program that will not do it itself. shader-load;<path> hands over a shaders { } file instead of a chain and answers with how much of it ran (ok;1 chrome, 0 content), so a script can apply a preset without knowing how to read one. See shaders.

Anything the session sends back to a program ends its verb in -reply -- hello-reply, shader-reply -- and no request verb may. A pane that echoes what it is sent (cat, a shell with echo on, a REPL waiting for a line) would otherwise be answered into a loop, which is exactly what hello used to do: 4 MB of hellos in a second and a half.

Driving it from an agent#

Everything above is what an agent needs, and none of it says which parts matter. .agents/skills/driving-slosh/SKILL.md is the same socket written as instructions: how a program in a pane finds out which session it is in (SLOSH_SESSION, SLOSH_BIN), why work belongs in a pane that was given a command rather than typed into somebody's shell, that alive and exit_code are how you wait rather than reading the screen for a marker your own echo matches, and that purpose is the handle to find things by because titles change underneath you.

It follows the .agents/skills/<name>/SKILL.md convention, so an agent working in a checkout picks it up without being told. To use it elsewhere, copy or symlink the directory into wherever your agent looks for skills. tests/test_skill.py checks every verb, variable and panes field it names against the program, because a stale skill is worse than a missing one: an agent acts on it without a human reading it first.

Headless#

slosh --script is the whole program without a terminal: commands on stdin, answers on stdout. It is how the test suite works — drive these events, assert this screen — which is also why the suite is 1,500-odd real end-to-end checks that finish in about thirteen seconds.

$ printf '%s\n' '{"cmd":"split","dir":"cols"}' '{"cmd":"snapshot","format":"text"}' \
    | slosh --script --cols 80 --rows 24 -- /bin/sh

The bare-verb form (snapshot text, send \x01\\, resize 100 30) is a human-friendly alias for the same code, so a script and a test cannot drift from what the API does.