<feed xmlns='http://www.w3.org/2005/Atom'>
<title>slosh.git/.agents, branch master</title>
<subtitle>an opinionated terminal multiplexer in C with programmable pane chrome</subtitle>
<id>https://git.sl0p.foo/slosh.git/atom/.agents?h=master</id>
<link rel='self' href='https://git.sl0p.foo/slosh.git/atom/.agents?h=master'/>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/'/>
<updated>2026-10-09T10:31:53Z</updated>
<entry>
<title>osc 7501: say what you are doing in somebody else's protocol</title>
<updated>2026-10-09T10:31:53Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-10-09T10:24:48Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=fcf305085343bcb5c5240ed642468461f247f1ed'/>
<id>urn:sha1:fcf305085343bcb5c5240ed642468461f247f1ed</id>
<content type='text'>
OSC 5577 only works for programs that know slosh exists. The program
status protocol is the same idea written down for everybody, by ghostty's
author and implemented by ghostty and Rex, so a program that already
speaks it needs to know nothing about us:

  printf '\033]7501;state=working:app=cargo:msg=YnVpbGRpbmc=\033\\'

Implemented *as well as* 5577, not instead of it. 5577 is a two-way
control channel -- buttons answer on the program's stdin, shaders are
refused with a reason -- and 7501 deliberately is not, so neither absorbs
the other. What 7501 buys is the programs we do not control.

The scanner becomes one pass for four things: 5577, 7501, OSC 133 ; A and
RIS. The last two are here because 7501 ties record lifetime to them -- a
new prompt or a full reset is how a stale `working` dies without a
heartbeat -- and because a pane should pay for that scan once. osc5577_fn
gives way to osc_scan_cbs_t.

Reports are all-or-nothing, which is the spec's rule: no state, an unknown
state, a broken limit, base64 that will not decode, a control character in
decoded text or a malformed id discards the whole report rather than
writing a partial one. A malformed *pair* is skipped and the rest parsed.

A pane keeps up to PANE_PS_MAX records with LRU eviction, and composes the
one line it has from them by precedence -- blocked, error, done, working,
idle -- resolving `app` from the nearest ancestor that has one, the way
the hierarchy is allowed to mean something. It lands in the slot 5577's
`status` already had, because every reader of a pane wants "what is this
doing", not "which protocol said so"; `panes` reports program_state,
program_kind, program_app and progress beside it, with -1 for a progress
nobody reported, since a bar drawn at 0% says something false.

The feature-detection reply is rate-limited here. Note for the next
re-vendor: upstream lib-vt implements 7501 too, newer than our pin, and
answers the support query itself through write_pty without rate-limiting
it. See "What upstream did" under D1 in DESIGN.md before swapping this
half of the file for GHOSTTY_TERMINAL_OPT_PROGRAM_STATUS.
</content>
</entry>
<entry>
<title>skill: which pane am I, and how not to land in somebody else's tab</title>
<updated>2026-09-29T12:26:49Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-09-29T12:26:49Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=84e19fad4810defd9f0fa6d3190197c559f3768f'/>
<id>urn:sha1:84e19fad4810defd9f0fa6d3190197c559f3768f</id>
<content type='text'>
The skill described the API and left the one question an agent actually
gets wrong unanswered. It now leads with it: your pane is $SLOSH_PANE,
`focused` is the human's, and the fallback when the environment is scrubbed
is `pid` -- never focus.

The rest follows the verbs that shipped with it: `capture` for reading a
pane in any tab, `raw id` for typing into one, `apply-layout focus:false`
and `move-pane beside` for building work beside yourself without dragging
the view along.
</content>
</entry>
<entry>
<title>skill: the verbs that shipped since, and the bare form's other shape</title>
<updated>2026-09-29T10:15:09Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-09-29T10:15:09Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=9a979f1a21f065be06cf97dceba9c578d58994bf'/>
<id>urn:sha1:9a979f1a21f065be06cf97dceba9c578d58994bf</id>
<content type='text'>
</content>
</entry>
<entry>
<title>osc 5577: `busy`, and a sidebar that shows it (busy_mark)</title>
<updated>2026-09-22T13:12:02Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-09-22T13:12:02Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=720aa48e249c0c962e8f73372ceded6092414a6e'/>
<id>urn:sha1:720aa48e249c0c962e8f73372ceded6092414a6e</id>
<content type='text'>
A status is a line of text, and a line of text cannot say whether it is still
true. "building 3/7" reads exactly the same while it builds and ten minutes
after it stopped, which is the one thing somebody reading it from another tab
wants to know -- and the only workaround available to a program was to write
the answer into the words and keep rewriting them.

So `busy`:

    printf '\033]5577;1;status;make test\033\\'
    printf '\033]5577;1;busy;1\033\\'
    ...
    printf '\033]5577;1;busy;0\033\\'

A flag of its own rather than a field of `status`, because a status is the
*whole* payload after the verb -- `status;a;b;c` has always been the text
"a;b;c" -- so anything added to that line would change what every existing
sender means, including the five pi extensions D1 promises byte compatibility
to. Orthogonal, so a program can leave one line up and toggle the other;
anything but 0/false/no/off is on; `clear` takes both; and a pane whose program
has gone is never busy, whatever it last claimed, because a spinner turning
under a dead tab is a lie you keep looking at.

In a sidebar the row turns `tab_status_busy` -- the theme's accent, derived
like the other status colours, so no theme needs a line -- and takes a spinner
in the column `tab_bar_padding` was already holding blank, so nothing reflows
when a program starts or stops working. `busy_mark` is the frames, separated by
spaces: one frame is a static mark and costs nothing, several make it turn, and
only then does the session keep a frame clock -- via the same `animating` flag
an animated shader uses, so the cost lands only while such a row is actually
composed, and `""` drops the mark entirely and leaves the colour to say it.
The frame is a pure function of the clock, like every other pulse here, so two
clients attached to one session show the same one.

`panes` now reports `status` and `busy`, which is the scriptable half of the
same fact: what a pane says about itself, without reading its screen.
</content>
</entry>
<entry>
<title>docs: the tab bar's new shapes, written down</title>
<updated>2026-09-10T13:51:23Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-09-10T13:51:23Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=fa64070de57ba8cb916ae4273df5df9a3f47e212'/>
<id>urn:sha1:fa64070de57ba8cb916ae4273df5df9a3f47e212</id>
<content type='text'>
A 'The tab bar' section on the panes page: the sidebar, its four knobs,
the status rows under each tab, and the borrowed directory name for
unnamed tabs. The config page's knob table learns the tab_bar_* keys.

scripting.md and the driving-slosh skill stop underselling the status
verb: it was documented back when the pane's own frame was the only
place a status could appear, which made it decoration. With the
sidebar it is how a pane reports progress to somebody working in
another tab -- so say that, and say to keep it current and clear it
when the work is done.
</content>
</entry>
<entry>
<title>slosh: rename the project from sl0ppty</title>
<updated>2026-08-18T19:26:00Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-18T19:26:00Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=b9e53cace4eb75263077df8623b011e66f5f2241'/>
<id>urn:sha1:b9e53cace4eb75263077df8623b011e66f5f2241</id>
<content type='text'>
sl0p + pty was error-prone to type and the zero made it worse; slosh
(slop + sh) is one syllable, impossible to typo, and slosh.foo is ours.

Mechanical rename everywhere it appears: binary, env vars (SLOSH,
SLOSH_SESSION, SLOSH_BIN, SLOSH_CONFIG), socket dir (/tmp/slosh-&lt;uid&gt;,
$XDG_RUNTIME_DIR/slosh), config path (~/.config/slosh), header guards,
src/sl0ppty.h -&gt; src/slosh.h, contrib/sl0ppty-dev -&gt; contrib/slosh-dev,
the driving-slosh skill, docs and tests. The sl0ppi fork references and
the sl0p theme stay: one is history, the other is the brand.

The rename surfaced a latent bug in test_osc5577: the hello-reply
answer always appeared on screen twice (ECHOCTL echo plus cat -v), but
a seven-character implementation name happened to wrap the second copy
across the 60-column boundary, so counting substrings in wrapped text
found one and passed for the wrong reason. A five-character name moved
the wrap and exposed it. The loop guard itself is correct -- with echo
off before the request there is exactly one reply -- so the test now
counts on de-wrapped text and tolerates the pane's own echo.
</content>
</entry>
<entry>
<title>sl0ppty: a --script pane is told no session rather than somebody else's</title>
<updated>2026-08-17T11:38:15Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-17T11:38:15Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=ea39e09a52240ce1d5166e9137e39b7caad499fd'/>
<id>urn:sha1:ea39e09a52240ce1d5166e9137e39b7caad499fd</id>
<content type='text'>
`server.c` set `SL0PPTY_SESSION` for its panes and carried a comment saying the
value was "deliberately unset under `--script`". Nothing unset it. A value never
set is a value inherited, so a script driver started from inside a pane handed its
own panes the *outer* session's name -- and the recipe the skill documents,
`-s ${SL0PPTY_SESSION:-main} cmd`, then sent its commands to a session it was not
in. Not a fallback to a guess: a confident wrong answer, because the name it read
was a real session that really answers.

Both variables now go through one `session_env()`, called by the server with its
name and by the headless driver with NULL, because the two modes have to agree
about what a pane is told and a comment in one of them is not agreement.
`SL0PPTY_BIN` is set there too rather than inherited, so it names the binary that
actually made the pane instead of one that made some pane somewhere -- which is
the whole reason that variable exists.

Found while explaining a stale pane title, from a check in `test_skill.py` that
failed under `make test` and passed on its own. It was not flaky: the suite runs
from inside a pane, where these are set, and the tool I ran it from by hand had an
environment where they were not. A test whose answer depends on where you run it
looks exactly like a race.

So it now passes the lie in deliberately: the session under test is started with
`SL0PPTY_SESSION=somebody-elses-session`, which makes the difference between
"never set" and "cleared" visible from any shell. The mirror check is next to it --
a real session tells its panes its *own* name, even when started from inside
another one -- so the obvious wrong fix, unsetting it in both modes, fails. Both
were verified by making both wrong versions and watching each test bite.

The two title checks now wait for the line instead of sampling after 120ms. That
was not the bug (a shell prints in about 12ms here, even under disk churn) and it
is one assumption fewer.
</content>
</entry>
<entry>
<title>sl0ppty: a skill for driving it, and the two variables that made one possible</title>
<updated>2026-08-17T11:08:06Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-17T11:08:06Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=244068e86c03b6c1bf457fac7e576bf5f68a43b7'/>
<id>urn:sha1:244068e86c03b6c1bf457fac7e576bf5f68a43b7</id>
<content type='text'>
`.agents/skills/driving-sl0ppty/SKILL.md`. The convention is
`.agents/skills/&lt;name&gt;/SKILL.md` with YAML frontmatter -- already in this tree
under `vendor/libghostty-vt`, so it is not an invention -- and it means an agent
working in a checkout picks the skill up without being told, while a directory per
skill leaves room for adjuncts later. `contrib/README.md` and
`docs/scripting.md` say where it is and how to use it elsewhere: copy or symlink
the directory into wherever your agent keeps skills.

**Writing it turned up the reason there was no skill worth writing.** A pane is
told `SL0PPTY=1` and nothing else, so a program inside one knows it is in a
session and cannot find out *which* -- `ls` lists them all and none of them is
labelled "the one you are in". So the whole most valuable case, an agent driving
the session it is running in, was guesswork. The server now sets
`SL0PPTY_SESSION` and `SL0PPTY_BIN` in its own environment, which every pane
inherits through fork: one place, no signatures threaded, and unset under
`--script`, which has no socket to name. `SL0PPTY_BIN` is read from
`/proc/self/exe` rather than `argv[0]`, and exists because the failure it prevents
is the one I hit writing the examples -- a session started from a build tree, and
`sl0ppty` not on the pane's `PATH`, so the skill's own commands did not run.

What the skill actually teaches, none of which `docs/scripting.md` says because a
reference is not allowed to have opinions:

- Work belongs in a pane that was **given** a command, not typed into somebody's
  shell. Then `alive` and `exit_code` off `panes` are how you wait, and the output
  survives the program (D14) so it is still there to read.
- There is **no wait primitive over the socket**: `settle` is headless-only, and
  `deadline` is about repaints and says nothing about program output. Poll.
- `purpose` is the handle. Titles change underneath you; a declared purpose cannot
  be relabelled by the program in the pane, which is what makes it safe to address.
- `snapshot` is the whole composited session, and only what is on screen. Cut a
  pane out with its `content_*` rect; do not expect scrollback.
- **The echoed command line matches your own marker.** `echo DONE` puts `DONE` on
  screen twice. Count, or use a marker you did not send.
- Write `\e`, never `\033`: the unescaper takes `\0` first, so `\033` arrives as
  NUL followed by `33` and nothing complains. That bug was live in this repo's own
  test suite until last week.
- A program in a pane can draw its own status and real buttons with OSC 5577 and
  read the click off its stdin, which is how it asks a question instead of printing
  a prompt and hoping.
- Close what you made; do not `quit` a session you did not start.

`tests/test_skill.py` is the part that keeps it true. A skill is documentation an
agent acts on without a human reading it first, so a stale one is worse than a
missing one: a wrong verb is a failed tool call somebody else has to debug. Every
verb the file names is sent to a live session and must not answer `unknown cmd`;
every `panes` field its central recipe polls must exist; the environment it
promises is read out of a real pane; the recipe itself is run end to end and must
report exit status 3 with its output still on screen. It also greps the file for
paths out of a home directory, because "plug and play" is a property somebody has
to keep.

That test found `close-pane` in the draft, which is the *keybinding* action name --
the verb is `close`. Exactly the class of error the test exists for, found before
anybody's agent ran it.

Verified: 1795 checks, all green, 43 of them in `tests/test_skill.py`. Then the
skill's recipe run verbatim against a real session -- tagged pane, polled to
`alive:false`, `exit status: 3`, `compiling`/`built`/`exited: status 3` still
readable -- and from inside a pane, `session=agentdemo bin=/…/sl0ppty` and a
`building 3/7` status drawn in its own frame.
</content>
</entry>
</feed>
