Skip to content

feat(palette): COLLIE_COMMANDS β€” the Agent-commands palette becomes yours - #109

Closed
enieuwy wants to merge 1 commit into
AltanS:mainfrom
enieuwy:feat/operator-commands
Closed

feat(palette): COLLIE_COMMANDS β€” the Agent-commands palette becomes yours#109
enieuwy wants to merge 1 commit into
AltanS:mainfrom
enieuwy:feat/operator-commands

Conversation

@enieuwy

@enieuwy enieuwy commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

The gap

The shipped catalogs in web/src/lib/agent-commands.ts are limited, by their own sourcing rules, to commands every user of that harness has. A command registered by a plugin or by the user exists on one machine only, so it can never be vouched for there β€” and it's exactly the command worth one tap: omp's /fork-in-herdr, a Claude Code custom command, your own /deploy.

Today the only way to send one from Collie is to type it, or fight terminal pass-through.

The change

COLLIE_COMMANDS=omp:/fork-in-herdr=Fork this conversation into a new herdr tab

A pane your rows address shows your rows and nothing else. A pane none of them address keeps the shipped catalog. Declare nothing and every pane is exactly as shipped.

Replacing rather than merging is the point. This surface is a handful of one-thumb shortcuts, and the value of the shipped ten is that someone chose them β€” a list half-chosen by the operator and half-guessed for them is worse than either whole one.

Nothing is lost by replacing it. The agent's own / completion already renders in the mirrored pane, live and complete. I checked what the alternative would even be: the set is registered at runtime inside the agent process (omp has no commands --json, no on-disk list; fork-in gets its two by calling registerCommand at load), so there is nothing to enumerate from out here. Scraping the popup back through pane.read yields four wrapped visible rows and a scrollbar, per-agent grammar, in the user's live session. Discovery belongs to the harness. This pane is for the handful you reach for without looking.

Rules

Four, all chosen so adding a row narrows or clarifies, never widens:

  1. Your list is the palette, on the panes it addresses.
  2. A pane you did not address keeps its catalog. Scoping to omp: says nothing about your claude panes. (One unscoped row addresses everything, and .env.example says so.)
  3. Danger is inherited, not reset. Naming a shipped command takes over its description but keeps its confirm β€” re-wording a session wipe can't make it one-tap.
  4. Narrowest scope wins; one /name is one row. Exact beats family beats unscoped, so /deploy=Everywhere,omp:/deploy=On omp is one button whose text depends on the pane (two would also collide on the palette's key={c.command}). A family scope is only ever the catalog's own name for the family: claude: reaches a claude-code pane because CLAUDE's shipped rows do, but claude-local: addresses claude-local only β€” folding an arbitrary operator string through the lookup ladder would widen a scope written to be narrow.

Grammar reuses the existing comma-separated list style (envList), so a description can't contain a comma and an arg hint can't contain =. Both documented in .env.example and pinned by tests. An empty scope (:/wipe) is rejected rather than read as "every agent" β€” the operator was reaching for something narrower than that, so the failure should be the narrow one.

Shape

  • bridge/config.ts β€” parseOperatorCommands(), pure and unit-tested without process.env
  • bridge/server.ts β€” served on /api/config; the key is omitted entirely when unset, so an operator who never touched this ships the same payload as before
  • web/src/lib/operator-commands.ts β€” module-state store + useSyncExternalStore, the lib/server-build.ts idiom. One successful read cached per page load; a failed read isn't cached, and the kick lives in an effect rather than the render body so a refusal can't become a request per 1.5s tick
  • web/src/lib/agent-commands.ts β€” commandsFor(agent, mine) resolves the four rules

Tests

24 new, all green. Parser grammar (scope, arg hints, duplicates, empty scope, the = and comma costs), the store's caching/retry/concurrency contract, the four resolution rules, and the rendered palette (replacement, one-button dedupe, inherited confirm).

bunx tsc --noEmit clean both packages. Bridge suite 569/569. Web suite: 2117 pass / 48 fail β€” the same 48 fail on main (localStorage-less environment: theme, dash/display prefs, draft persistence), none in touched files.

Verified end-to-end on a live bridge: /api/config carries the rows, and the screenshot above is the real sheet on an omp pane β€” "Search 2 commands…", shipped catalog gone.

Versioning

Per CLAUDE.md, fork PRs leave the version files and CHANGELOG alone. Suggested entry:

  • COLLIE_COMMANDS makes the Agent-commands palette yours β€” your own slash commands, including plugin- and user-registered ones the shipped catalogs can't carry; a pane your rows address shows them instead of the catalog (#NN, 3021479)

Note

Built on top of #108 conceptually but textually independent β€” no overlapping files, either order merges clean.

…ours

The shipped catalogs in web/src/lib/agent-commands.ts are limited, by their
own sourcing rules, to commands every user of that harness has. A command
registered by a PLUGIN or by the user exists on one machine only, so it can
never be vouched for there β€” and it is exactly the command worth one tap:
omp's /fork-in-herdr, a Claude Code custom command, your own /deploy.

COLLIE_COMMANDS declares them, per pane or globally:

  COLLIE_COMMANDS=omp:/fork-in-herdr=Fork this conversation into a herdr tab

A pane your rows address shows YOUR rows and nothing else. A pane none of
them address keeps the shipped catalog; declare nothing and every pane is as
shipped. Replacing rather than merging is the point: this surface is a
handful of one-thumb shortcuts, and the value of the shipped ten is that
someone chose them β€” a list half-chosen by you and half-guessed for you is
worse than either whole one.

Nothing is lost by it. The agent's own `/` completion renders in the
mirrored pane, live and complete, which no list on this side could stay: the
set is registered at runtime inside the agent process, so there is nothing
to enumerate from out here. Discovery belongs to the harness; this pane is
for the handful you reach for without looking.

Four rules, all so that adding a row narrows or clarifies, never widens:

1. Your list is the palette, on the panes it addresses.
2. A pane you did not address keeps its catalog.
3. Danger is inherited, not reset β€” naming a shipped command takes over its
   description but keeps its confirm, so re-wording a session wipe cannot
   make it one-tap.
4. The narrowest scope wins and one /name is one row: exact beats family
   beats unscoped. A family scope is only ever the catalog's own name for
   the family, so `claude-local:` addresses claude-local and not the family
   the catalog lookup would fold it onto.

The grammar reuses the existing comma-separated list style, so a description
cannot contain a comma and an arg hint cannot contain `=`; both are
documented in .env.example and pinned by tests.

Per CLAUDE.md, a fork PR leaves the version files and CHANGELOG alone.
@AltanS

AltanS commented Aug 17, 2026

Copy link
Copy Markdown
Owner

Thanks @enieuwy β€” second one this week, and again the hard part arrived already done: the four resolution rules, the store, and the replace-not-merge argument all landed as you wrote them (your commit is cherry-picked into #112, authorship preserved). What changed on top: rows now live in a commands.toml next to .env instead of the env var β€” your own PR body documented the costs of the packed grammar (the comma and = bans), and a file dissolves them while giving the danger marker a home (confirm = true). Edits are picked up live, no restart. The replace-vs-merge reasoning you argued is now ADR 0018, so it outlives this thread. Ships with the next release.

@AltanS AltanS closed this Aug 17, 2026
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