Say /new to her: the command surface in 文 compose #85

Open
opened 2026-08-01 17:18:04 +00:00 by aiko · 2 comments
Owner

The client half of denpatou#30, split out because the acceptance there spans both repos and neither half can be branched or reviewed cleanly as one diff.

Blocked on the go-between half: the catalogue frame has to exist and be named in hermes_plugin/denpa/protocol.py before this can read it. Nothing here invents a frame name.

What this is

Everything the operator touches. Today 文 compose sends whatever is typed as speech, so there is no way to say /new — everything goes to the agent as prose and hopes it lands as a tool call.

  • Text beginning with / in 文 compose is sent as a command, not as speech.
  • Typing / opens autocomplete over the catalogue the go-between sends: filters as you type, completes on selection.
  • The catalogue refreshes without restarting the Receiver — it is sent on connect and re-sent when the set changes, so a Receiver open across a Hermes restart is not stale.
  • A command's reply is legible in the log as a command's answer, not as her speech.
  • An unknown or refused command says so on the device.

Decided already, do not relitigate

  • Every command is offered, including ones _check_slash_access will refuse (decided 2026-08-01 on denpatou#30). One operator on their own hardware: a refusal is nearly always a misconfiguration, and a command missing from the menu is indistinguishable from Hermes not having it. The refusal speaks instead. Commands filtered out upstream by _is_gateway_available never reach the catalogue at all and are a different case — those genuinely cannot run here.
  • The compose line is the whole affordance. No menu, no buttons. Buttons are a later pass for whatever turns out to be worth reaching without typing.
  • No hand-kept command list may exist in this repo. The test is that adding a command to Hermes makes it appear in Denpa's autocomplete with no change here.

Design first

docs/design/denpa-receiver/ has nothing for a command surface. The autocomplete's shape at both dock and pal widths wants deciding before it is built — denpatou#30's original open questions named exactly this, and the compose surface itself (denpa#53) is recent enough that its treatment is the thing to build on.

Acceptance

  • /-prefixed text in 文 compose is sent as a command rather than as speech
  • Typing / opens autocomplete over the catalogue frame; it filters as you type and completes on selection
  • The catalogue refreshes without a restart
  • A command's reply is drawn as an answer, distinct from her speech
  • An unknown or refused command is visible on the device
  • No hand-maintained command list in this repo
  • The surface is designed in docs/design/denpa-receiver/ before it is built
  • /stop typed mid-turn actually interrupts her — the end-to-end proof, and the go-between's busy_policy pass-through is what makes it possible

denpatou#30 (the wire, and every decision behind this), denpa#53 (the compose surface this extends), denpatou#32 (mismatch 6: slash-confirm reaches no consumer, so an autocompleted command that needs confirming stops silently)

The client half of [denpatou#30](https://git.aiko.works/aiko/denpatou/issues/30), split out because the acceptance there spans both repos and neither half can be branched or reviewed cleanly as one diff. **Blocked on the go-between half**: the catalogue frame has to exist and be named in `hermes_plugin/denpa/protocol.py` before this can read it. Nothing here invents a frame name. ## What this is Everything the operator touches. Today 文 compose sends whatever is typed as speech, so there is no way to say `/new` — everything goes to the agent as prose and hopes it lands as a tool call. - Text beginning with `/` in 文 compose is sent as a **command**, not as speech. - Typing `/` opens autocomplete over the catalogue the go-between sends: filters as you type, completes on selection. - The catalogue refreshes without restarting the Receiver — it is sent on connect and re-sent when the set changes, so a Receiver open across a Hermes restart is not stale. - A command's reply is legible in the log **as a command's answer, not as her speech**. - An unknown or refused command says so on the device. ## Decided already, do not relitigate - **Every command is offered, including ones `_check_slash_access` will refuse** (decided 2026-08-01 on `denpatou#30`). One operator on their own hardware: a refusal is nearly always a misconfiguration, and a command missing from the menu is indistinguishable from Hermes not having it. The refusal speaks instead. Commands filtered out upstream by `_is_gateway_available` never reach the catalogue at all and are a different case — those genuinely cannot run here. - **The compose line is the whole affordance.** No menu, no buttons. Buttons are a later pass for whatever turns out to be worth reaching without typing. - **No hand-kept command list may exist in this repo.** The test is that adding a command to Hermes makes it appear in Denpa's autocomplete with no change here. ## Design first `docs/design/denpa-receiver/` has nothing for a command surface. The autocomplete's shape at both dock and pal widths wants deciding before it is built — `denpatou#30`'s original open questions named exactly this, and the compose surface itself (`denpa#53`) is recent enough that its treatment is the thing to build on. ## Acceptance - [ ] `/`-prefixed text in 文 compose is sent as a command rather than as speech - [ ] Typing `/` opens autocomplete over the catalogue frame; it filters as you type and completes on selection - [ ] The catalogue refreshes without a restart - [ ] A command's reply is drawn as an answer, distinct from her speech - [ ] An unknown or refused command is visible on the device - [ ] No hand-maintained command list in this repo - [ ] The surface is designed in `docs/design/denpa-receiver/` before it is built - [ ] `/stop` typed mid-turn actually interrupts her — the end-to-end proof, and the go-between's `busy_policy` pass-through is what makes it possible ## Related `denpatou#30` (the wire, and every decision behind this), `denpa#53` (the compose surface this extends), `denpatou#32` (mismatch 6: `slash-confirm` reaches no consumer, so an autocompleted command that needs confirming stops silently)
Author
Owner

This was generated by AI during triage.

Constraint added by the operator, 2026-08-01: only typed text may launch a command. Never speech.

Everything above talks about "text beginning with /", which is ambiguous in exactly the wrong place. A transcript is also text arriving from a device — Hermes transcribes voice and the result comes back down the same route. So the rule is on the input's provenance, not on its first character:

  • The command path is reachable only from the typed compose surface (文), and never from the voice path.
  • A transcription that happens to begin with /, or a spoken "slash" that STT renders as one, is speech and is handled exactly as it is today.
  • If the two paths converge before they can be told apart, the frame carrying typed text has to say it is typed. Not inferred, and no heuristic on the text.

Why it matters: a spoken /stop that really stops her is a microphone that can be triggered by a podcast, a housemate, or her own audio bleeding into an open mic. Commands are deliberate acts, and typing is what makes them deliberate. This is the same family as ADR-0004's asks suspending a turn — a thing that changes state should not be reachable by accident.

Added to the acceptance:

  • A voice-in whose transcript begins with / is spoken about, not executed — pinned by a test
> *This was generated by AI during triage.* **Constraint added by the operator, 2026-08-01: only typed text may launch a command. Never speech.** Everything above talks about "text beginning with `/`", which is ambiguous in exactly the wrong place. A transcript is also text arriving from a device — Hermes transcribes voice and the result comes back down the same route. So the rule is on the input's **provenance**, not on its first character: - The command path is reachable only from the typed compose surface (文), and never from the voice path. - A transcription that happens to begin with `/`, or a spoken "slash" that STT renders as one, is **speech** and is handled exactly as it is today. - If the two paths converge before they can be told apart, the frame carrying typed text has to say it is typed. Not inferred, and no heuristic on the text. Why it matters: a spoken `/stop` that really stops her is a microphone that can be triggered by a podcast, a housemate, or her own audio bleeding into an open mic. Commands are deliberate acts, and typing is what makes them deliberate. This is the same family as ADR-0004's asks suspending a turn — a thing that changes state should not be reachable by accident. Added to the acceptance: - [ ] A voice-in whose transcript begins with `/` is spoken about, not executed — pinned by a test
Author
Owner

This was generated by AI during triage.

The wire landed (denpatou#30, merged locally as 341e43b), and it added a requirement to this ticket.

FRAME_TEXT_IN now carries typed, and the command gate reads that rather than the frame type. The reason is on denpatou#30: the frame type was evidence of a keyboard only by accident, since conversation_handler.py mints one for any text-input, live/bilibili_live.py mints one from a stranger's chat message, and denpa#55 would mint one from a transcript.

So: 文 compose must set typed: true on the text-input it sends, or no command will ever fire. Absent or false is speech, deliberately — a client that has not learned to say it loses commands rather than gaining a microphone that can run them.

Added to the acceptance:

  • 文 compose sets typed on the text-input it sends, and nothing else in the client does
> *This was generated by AI during triage.* **The wire landed (`denpatou#30`, merged locally as `341e43b`), and it added a requirement to this ticket.** `FRAME_TEXT_IN` now carries `typed`, and the command gate reads that rather than the frame type. The reason is on `denpatou#30`: the frame type was evidence of a keyboard only by accident, since `conversation_handler.py` mints one for any `text-input`, `live/bilibili_live.py` mints one from a stranger's chat message, and `denpa#55` would mint one from a transcript. So: **文 compose must set `typed: true` on the `text-input` it sends, or no command will ever fire.** Absent or false is speech, deliberately — a client that has not learned to say it loses commands rather than gaining a microphone that can run them. Added to the acceptance: - [ ] 文 compose sets `typed` on the text-input it sends, and nothing else in the client does
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
aiko/denpa#85
No description provided.