Skip to content

feat(slash-catalogs): slash command catalogs + insert-never-send composer menu - #684

Merged
danshapiro merged 14 commits into
mainfrom
the-usual/slash-command-catalogs
Aug 24, 2026
Merged

feat(slash-catalogs): slash command catalogs + insert-never-send composer menu#684
danshapiro merged 14 commits into
mainfrom
the-usual/slash-command-catalogs

Conversation

@danshapiro

Copy link
Copy Markdown
Owner

Slash-command catalogs

Fresh-agent panes now surface provider-advertised slash commands in the composer as a grouped menu alongside the static pane actions, with insert-never-send semantics (selecting a row inserts /name into the composer; only the user's own Enter sends — and for opencode, typed Enter on a catalog-matched slash routes to the command endpoint).

What changed

  • Contract (shared/fresh-agent-contract.ts): optional .strict() commands slot on the fresh-agent session snapshot — graceful absence for Rust/codex/offline providers.
  • Claude/kilroy (server/sdk-bridge.ts): create-time supportedCommands() probe (fire-and-forget, never awaited — no create-lock stall) joined with the lazy stream init frame; commands_changed pushes REPLACE the catalog with drop-stale-probe ordering; published rows are subtracted data-driven against the session's latest terminal_slash_commands list.
  • Opencode (server/fresh-agent/adapters/opencode/): catalog captured from the live shared sidecar's GET /command (no scratch spawn) with lazy one-shot re-capture + 60s backoff on fetch failure; catalog-matched composer sends dispatch via POST /session/:id/command (canonical casing, verbatim arguments) with a turn-scale timeout that cannot discard the shared sidecar (discardOnRequestTimeout: false); every failure mode degrades to a verbatim prompt send.
  • Composer (FreshAgentComposer.tsx): catalog merged as a second labelled menu group ("Agent session" beside "Pane actions"), cross-kind collisions displayed, within-kind case-insensitive dedupe, typed-Enter consults action rows only. A11y preserved: role="group" + aria-label, rows stay role="menuitem".

Review P3 fix (this PR's head commit)

normalizeSlashCommandRows previously rejected the entire create-time probe payload on the first invalid row, silently leaving claude/kilroy panes with no command menu until a mid-session push arrived. The probe path now drops invalid rows individually (debug-logging the drop count) — the per-row-drop convention this branch already established in the opencode catalog module. Push paths remain strict: an invalid commands_changed payload is still rejected whole, keeping the prior catalog.

Test evidence

  • 460 focused tests across shared schema/helpers, sdk-bridge, claude/opencode adapters, composer, and FreshAgentView wiring.
  • test/unit/server/sdk-bridge.test.ts: 304/304 after the P3 fix (includes the new probe per-row-drop test and the unchanged strict-push test).
  • Broad gate (npm test, coordinated, cloud vitest backend): exit 0 — cloud lanes 1295/1295 + 1398/1398; electron 350/350. Executions: cloud build 5aa73b17, freshell-vitest-7rgqw.
  • E2E (cloud backend, image e310c3710b83): freshopencode-model-picker.spec.ts 7/7 passed, including the 3 new slash-command tests (grouped menu rows, insert-never-send with no freshAgent.send frame over a bounded negative window, statics byte-identical without a catalog stub). The spec is explicitly cloud-legal (not in CLOUD_SKIP_SPECS). Execution: freshell-e2e-46hn7.

Known honest gaps (plan-sanctioned): no freshclaude-leg e2e (covered by unit + live smoke); no live-sidecar command-turn SSE probe (the adapter synthesizes the busy/idle bracket by design).

… + terminal subtract; opencode POST /session/:id/command dispatch lane; smoke needs first turn)
…Hint wording, terminal_slash_commands optional note, smoke phrasing)
…-time supportedCommands + commands_changed REPLACE, terminal-subtracted)
…ecar; retry catalog capture lazily; drop inert slashCommandNames

Delta-review round-5 server findings:

- F1: runCommand's turn-scale timeout flowed through json()'s catch into
  discardRunning('request_timeout'), killing the shared opencode serve
  sidecar for ALL freshopencode panes when a single long command turn
  overran. json() gains an opt-out (discardOnRequestTimeout?: boolean,
  default preserves the wedged-sidecar discard); runCommand passes false,
  so the send rejects with the timeout error while the sidecar survives —
  matching the prompt path's own turn-timeout behavior. The pinned timeout
  test now pins survival (+ the send error surfacing) instead of the kill.
- F2: a create-time /command catalog capture failure left session commands
  permanently absent, so every slash send leaked verbatim to the model.
  captureCommands now stamps commandsCaptureFailedAt on failure/malformed
  payloads, and the send path settles the catalog through
  settleCommandsBeforeSend: after the backoff window (60s) it fires the
  session's single lazy re-capture and awaits it before classifying.
  Create stays non-blocking (capture remains fire-and-forget there).
  Tests pin retry-once-after-failure, backoff observance, no re-fetch
  after success, once-per-session, and dispatch against the recovered
  catalog.
- F4: SdkSessionState.slashCommandNames was captured from the system/init
  frame and deep-cloned on state snapshots but consumed nowhere (grep
  confirms: no readers in server/, shared/, src/, test/, or logs). Removed
  the field, its cloneSessionState line, and its init-frame capture; the
  terminal_slash_commands capture and the publish subtract are untouched.
  No test pinned the field, so no pins were removed.
… for session rows)

Delta-review round-5 client nit (F3): the slash menu's static help line
claimed 'Enter runs' regardless of the highlighted row, but session rows
insert `/name ` text on Enter (they never dispatch or send). The hint now
tracks the highlighted row kind — 'Enter runs' for pane action rows,
'Enter inserts' for session rows — and is unchanged for the files menu
and whenever no session row is highlighted. Test pins the hint switching
as the highlight walks between the two groups.
…obe path

normalizeSlashCommandRows rejected the entire supportedCommands() probe
payload on the first invalid row, silently leaving claude/kilroy panes
with no command menu until a mid-session commands_changed push arrived.
The probe has no prior catalog to clobber, so it now drops invalid rows
individually (debug-logging the drop count), mirroring the opencode
catalog's per-row-drop convention. Push paths stay strict: an invalid
commands_changed payload is still rejected whole, keeping the prior
catalog.
@danshapiro
danshapiro merged commit e3f280b into main Aug 24, 2026
3 checks passed
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.

1 participant