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
157
158
159
160
161
162
163
164
|
# 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.
```bash
$ 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` `tabs` | what 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 |
| `snapshot` | the composited screen, as JSON, `format:"text"`, or `format:"bytes"`, the emitter's own output for this frame (a second call is the delta) |
| `deadline` | when the session wants its next frame, in ms, or -1 |
| `send` | bytes as if typed, decoded like input (`"data":"\\x01\\\\"`) |
| `raw` | bytes straight into the focused pane's pty |
| `resize` | `cols` `rows`, and optionally `cell_w` `cell_h` |
| `split` | `dir:"cols"\|"rows"`, `id` for which pane to split |
| `focus` | `id` |
| `close` `rerun` | `id`, or 0 for the focused pane |
| `clear-shaders` | `id`, 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` | tabs, by `id` or `index` |
| `move-pane` | `id` 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 |
| `float` | bare: toggle a pane [floating](panes.md#floating-a-pane) (`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-float` | a fresh floating shell over the current tab, centred, in the focused pane's directory; answers `id` |
| `set-name` | `target:"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 |
| `dump-layout` `apply-layout` | see [layouts](layouts.md). `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 |
| `workspaces` | the 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](workspaces.md) |
| `open-workspace` | `name` or `path`, and `suspended`; answers `tab` `purpose` `path` `created` `tabs` `honoured`. Already open means focused, with `created:false` |
| `close-workspace` | `name` or `purpose`; answers `closed`, how many tabs went |
| `save-workspace` | write 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` |
| `notify` | put a line in the session's status area |
| `graphics` | the kitty placements on screen, or the bytes sent for them. The bytes are rendered for *you*, not the client, so anything stateful in them (image transmissions, deletions) is re-sent to the client on its next frame |
| `clipboard` | what the session has copied |
| `reload` | re-read the config; answers `{"ok":true,"warning":...}` if it had a complaint |
| `splash` | replay the attach greeting; `fx` and `motion` pick the colour effect and the assembly by index, for a deterministic one |
| `edit-config` | open the config in a pane |
| `alive` | is it running, and how many panes and tabs |
| `quit` | end 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:
```bash
$ 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. "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:
```bash
$ 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:
```bash
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.
With the tab bar [on a side](panes.md#the-tab-bar), the status is also a row
under the pane's tab — visible from every other tab, and clicking it jumps to
the pane that said it. A status is not decoration on your own frame; it is how
a pane reports progress to somebody working elsewhere, which is a reason to
keep it current and `clear` it when the work is done.
`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, as 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; `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](shaders.md#prototyping-in-a-pane).
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.
```bash
$ 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.
|