design phase 9 w15 commands - sinrimin/harness-nexus GitHub Wiki
Status: SHIPPED 2026-09-17 (branch
feat/p9-w15-commands, daemon0.20.0-p9w15). Ground truth:research-phase-9-w14-w15-plan-commands.md(adapter-source-verified) + the rig captures noted inside.
The ACP available_commands_update push carries the agent's slash-command
catalog โ claude-code (after new/load/resume; custom + MCP commands renamed
mcp:<name>), codex-acp (review / review-branch / review-commit /
init / compact / logout, with input.hint on the review family), and
opencode (its Command.Info list โ which includes platform-deployed
command/*.md custom commands). dsh/hermes never push one. Today the daemon
maps it to raw and the web drops it: the portal user has no way to
discover or invoke the agent's commands.
Invocation needs NO new protocol: a command runs as an ordinary
session/prompt whose text is /name rest-of-line (verified for all three
adapters in the research doc โ claude's CLI parses slash text, codex's
thread handles it, opencode's detectSlashCommand matches the pushed names
and routes to its internal session.command).
Additive stream kind (bounded view of the adapter push):
export const availableCommandViewSchema = z.object({
name: z.string().min(1).max(128), // VERBATIM adapter name (may be 'mcp:foo')
description: z.string().max(512), // clamped daemon-side
hint: z.string().max(256).optional(), // from input.hint (args placeholder)
});
// chatStreamEventSchema +=
z.object({ kind: z.literal('commands'), commands: z.array(availableCommandViewSchema).max(64) });Replace semantics (the push always carries the full catalog). Riding the
ordinary stream gives it the history ring + resync replay for free; the
claude wrapper pushes AFTER session/load replay (deliberately), so a
rejoined channel re-receives it live.
One new arm in mapAcpUpdate: case 'available_commands_update' โ
takeAvailableCommands(update.availableCommands) (โค64 rows, name โค128,
description clamped 512, input.hint clamped 256, malformed rows skipped)
โ {kind:'commands', commands}. Stateless, like plan. DAEMON_VERSION โ
0.20.0-p9w15.
-
fold.ts:FoldState.commands: CommandView[](starts[]; replace on event; history rebuild converges). -
Composer
/-palette (portal-reference behavior, Signal-styled):- Opens while the draft starts with
/, commands exist, the channel is ready and no turn is running. The FIRST word after/filters (name OR description substring); the palette stays open while typing args. - Rows:
/{name}(mono) + description + the argshint(muted). - Keyboard:
โ/โcycle (wrapping),Esccloses, bareEnterSELECTS the active row and fills/name(focus stays; the user reviews then sends),Enterwith a space after the first word sends as a normal prompt. Click selects.isComposingguards IME as everywhere else. - Positioned as a popover ABOVE the composer card (absolute; the card must
not clip it), width matching the card, capped height with scroll,
role="listbox"+aria-activedescendant.
- Opens while the draft starts with
- No fallback table. An agent that never pushed commands shows no palette (dsh, hermes) โ same data-driven honesty as the W9 selectors. The portal reference ships a hardcoded fallback list; we deliberately don't.
- The empty-filter state shows a muted "no matching commands" row (palette stays open so the user sees why).
- Structured command input (
input.hintis display-only โ ACP 1.4 has only the unstructured kind), command history/autocomplete beyond the filter, server-side command storage (the catalog is per-channel session state).
- shared: schema cases (accept, bad kind, cap).
- cli:
mapAcpUpdatecases (verbatim names incl.mcp:, hint extraction, clamp, malformed drop, empty catalog passes as cleared) + fixture round trip โ the fixture gains aFIXTURE_COMMANDS=1arm pushing a small catalog aftersession/new(one command with a hint, one without). - web: fold arm mirrors
plan's state-only merge (no component harness); palette interaction verified on the rig.
-
Catalogs, all four targets: codex 6 (
review+input.hint,review-branch,review-commit,init,compact,logout); claude-code 46 (incl.deep-researchAND the emitter-installedcc-test-kit:โฆplugin commands โ platform-deployed commands surface through the agent's own catalog;mcp:/plugin names ride verbatim); opencode 3 (init,review,customize-opencode); dsh NONE โ typing/shows no palette. -
Palette (browser): opens on
/; first-word filter (/ininarrows to exactly/init; a non-matching prefix shows the ๆ ๅน้ ๅฝไปค empty state); ArrowUp/ArrowDown cycle; bare Enter SELECTS and fills/name(trailing space verified); click selects. -
Execution E2E:
/reviewsent as a plain prompt โ codex ran its review command (1m17s turn, honest verdict โ the rig container has no bubblewrap so its sandbox can't launch; environment limit, not a pipeline issue; clean turn end, composer re-enabled).
pickCommand filled /name but never dismissed the palette โ and the
keyword-change effect reset paletteDismissed right back, so the list kept
showing after every commit until Escape. Now every commit (bare Enter, mouse
click, and NEW: Tab while the palette is open) marks the keyword it filled
(a ref, so the reopen-on-new-keyword rule survives), fills the Sender, and
closes the palette โ the filled text in the Sender is the review affordance.
Escape semantics unchanged; typing a new /-keyword reopens as before.