Skip to content

Prototype: operator-declared launchers in launchers.toml (one tap β†’ throwaway Space) - #125

Draft
enieuwy wants to merge 1 commit into
AltanS:mainfrom
enieuwy:feat/launcher-menu-upstream
Draft

Prototype: operator-declared launchers in launchers.toml (one tap β†’ throwaway Space)#125
enieuwy wants to merge 1 commit into
AltanS:mainfrom
enieuwy:feat/launcher-menu-upstream

Conversation

@enieuwy

@enieuwy enieuwy commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

This is a prototype more than a proposal β€” I built it for my own phone, it has been running on my install for a day, and I am posting it mostly to ask whether the shape is right. Happy for it to be closed, or to be told the whole idea belongs in a fork.

What I wanted

On the desktop I have a herdr popup bound to a key that runs a small TUI, I glance at it, and I quit. From the phone I could not have that. As far as I can tell a popup is invisible to Collie by construction β€” it has no pane id and is not in herdr's pane API, so pane.list/pane.read cannot see one. A pane is the only surface the phone can read, so the nearest equivalent I could think of was a pane that closes itself: herdr already drops a tab whose last pane closes and a Space whose last tab closes, so a command that closes its own pane leaves nothing behind.

What this adds

launchers.toml, next to commands.toml and keys.toml:

[[launchers]]
command = "showy-quota-peek"   # required; typed verbatim
label = "Quota bars"           # optional; defaults to the command's first token
cwd = "~/dev/collie"           # optional; defaults to home, leading ~ expanded

I deliberately did not reach for a new env var β€” #109 β†’ #112 settled where operator rows live, and this reuses that machinery rather than reopening it: same operator-file.ts reader, same mtime-checked live reload, same hold-the-last-good-rows posture, same drop-the-row-never-the-file validation.

A tap creates a Space labelled from the row, types the command, sends Enter, and navigates into the fresh pane with the existing freshPane flow.

POST /api/launch takes a command string and runs it only on exact equality with a configured row. My reasoning for a route rather than doing it client-side: /api/workspace plus a reply into the new pane would put the command line on the phone and leave nothing to match against, whereas this way the client can only name a row. Same write guard, same session scoping, same audit posture as the other structural creates β€” command is not added to METADATA_KEYS, so it redacts under COLLIE_AUDIT_CONTENT=none like any other content-bearing detail.

Enter is sent literally rather than COLLIE_SUBMIT_KEYS, since that is the submit sequence for an agent's composer and this is a bare shell prompt. A failed send rolls the Space back.

Screenshots

Rows are rumen-peek, showy-quota-peek, and a lazygit row that exists only to show cwd.

Dashboard section Folded The same rows as a sheet

The pane header at 390 px β€” Find, the launcher, the status badge:

One tap later, and then the part I actually care about: while it runs it is an ordinary Space, and after quitting it is gone.

Launched SPACES (7) while running SPACES (6) after quitting

(Images live on an orphan assets/launcher-screenshots branch in my fork, so they are not in this diff. Say the word and I will re-upload them as attachments instead.)

Where I am least sure

  • Two surfaces may be one too many. The dashboard section is the one-tap path; the sheet exists because from inside a pane the section costs Home β†’ tap β†’ Back. I would happily drop either.
  • A separate file, or a row kind in commands.toml? I chose separate because a command row addresses an existing pane and has a scope, while a launcher creates the pane, so scope would be meaningless for it β€” keys.toml felt like the precedent. Easy to fold in if you disagree.
  • Whether /api/launch should exist at all, per the reasoning above. This is the decision I would most like checked.
  • No confirm = true for launcher rows, though command rows have it. Trivial to add if you want the symmetry.
  • No ADR, since I do not think this closes an option anyone argued for. If you think the "own file" call needs one, I will write it.
  • I have only run this on macOS, against one herdr session. The screenshots and the geometry checks are headless Chromium at phone sizes; the day of real use is my own phone, so consider anything about other platforms untested.

Checks

bun test ./bridge ./scripts β†’ 666 pass. Web suite β†’ 114 files / 2393 tests pass. Both typechecks clean. New tests cover the grammar (defaults, ~ expansion, each drop case, later-row-wins, a non-array launchers, a non-table row among good ones), the route (unlisted command rejected without creating anything; a listed row creates a Space with that label and cwd and types the command plus Enter; a send failure closes the created pane), and the two UI surfaces.

Per CLAUDE.md I have left the version files and CHANGELOG.md alone.

The gesture this gives a phone is the one a desktop gets from a herdr popup, and
Collie can never show a popup: it has no pane id and is absent from herdr's pane
API, so `pane.list`/`pane.read` β€” everything Collie renders β€” cannot see it. A
pane is the only surface a phone can read, so the ephemerality has to be the
pane's own lifecycle. Herdr already drops a tab whose last pane closes and a
Space whose last tab closes, so a command that closes its own pane takes the
whole Space with it: tap, look, quit, nothing left running.

Rows live in `launchers.toml`, the third sibling of `commands.toml` and
`keys.toml`, sharing their reader, their mtime-checked live reload and their
hold-the-last-good-rows failure posture. A row is `command` plus an optional
`label` (defaults to the command's first token) and `cwd` (defaults to home, `~`
expanded). A control character in `command` drops the row rather than being
stripped: the line is typed verbatim through `pane.send_text`, so a newline
would submit a second line the operator never reviewed, and silently rewriting
what runs is worse than refusing it.

The configured rows are the allowlist. `POST /api/launch` takes a command STRING
and runs it only on exact equality with a row, so the client names a row and
never supplies a command line; the bridge interpolates nothing. That is why the
route exists rather than the client calling /api/workspace and then typing into
the new pane itself β€” doing it there would move the command line to the phone
and leave no allowlist to match against. It carries the same write guard, the
same session scoping and the same audit posture as the other structural creates,
including the fail-closed redaction default: `command` is not added to
METADATA_KEYS, and the line still answers who launched what, where, when.

Enter is sent literally rather than COLLIE_SUBMIT_KEYS: that setting is the
agent-dependent submit sequence for a TUI composer, and this is a bare shell
prompt where Enter is the only key that means "run it". A send that fails rolls
the Space back, so a launch that did not start cannot leave an empty shell.

Two surfaces, because the rows answer two different questions. The dashboard
carries a Launch section, folding on the same terms as Spaces and Recent β€” it is
the one section whose height a config file decides, and `flex-wrap` fits two
labels per row, so six launchers would put three rows of buttons between the herd
you came to read and the navigator below it. Folded, its header still carries the
count, which is the reason to unfold.

The Space and pane headers carry a rocket that opens the same rows as a sheet.
That is the case the dashboard strip cannot serve: you are reading an agent and
want a glance at something else, which otherwise costs Home, tap, Back. A sheet
row is a full screen width, so it shows the command under the label β€” the
difference between trusting a button and wondering what it runs. Both surfaces
render nothing at all when no rows are declared, so an operator who declares
none keeps the dashboard and the headers they already had.
@AltanS

AltanS commented Aug 23, 2026

Copy link
Copy Markdown
Owner

Thanks this is a cool addition!! I'm going to check the exact proposal and its shape in the upcoming days and get back at you.

@enieuwy

enieuwy commented Aug 23, 2026

Copy link
Copy Markdown
Contributor Author

Happy to contribute to collie! You've built something great πŸ‘

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants