自発送信 break-in UI, against an injected trigger #24

Closed
opened 2026-07-29 17:41:13 +00:00 by aiko · 1 comment
Owner

What to build

The client half of the unprompted-broadcast design, built and tested without waiting for Hermes to be able to push one. Split out of #22 so the UI work is not idle behind an unscoped server task.

A cron-initiated turn is an unprompted broadcast that breaks in and names what caused it — a message appearing on its own with no attribution is exactly the thing being avoided.

  • ProactiveSpeakContext carries the trigger (source + job id) so every surface can state it.
  • Mobile: an acid band slides in from the top edge naming the trigger. Any user input dismisses it.
  • Desktop pal: no band — the invocation line changes to 自発 · trigger and the bubble appears.
  • Parked Android tab: an unprompted turn pulses the tab rather than forcing itself open. A broadcast can wait; only a suspended turn is allowed to grow the tab (see #19, #23).
  • The reconnect toast turns acid when broadcasts fired while the client was dark, offering to read them — never replaying them as audio.

Drive all of it from a dev-only injected message matching the shape agreed in aiko/ollvt-hermes-bridge#17, so the UI is demoable and testable now. #22 is then only the real integration.

Design reference: docs/design/denpa-receiver/README.md.

Acceptance criteria

  • A dev affordance can inject an unprompted turn with an arbitrary trigger name
  • An unprompted turn is visually distinct from a user-initiated one on every surface
  • The trigger is named on screen, not implied
  • Any user input dismisses the break-in band
  • Parked tab pulses rather than expanding
  • Reconnect toast offers to read missed broadcasts and never plays them
  • Tests cover trigger attribution and the dismiss path, driven by the injected message

Blocked by

## What to build The client half of the unprompted-broadcast design, built and tested **without** waiting for Hermes to be able to push one. Split out of #22 so the UI work is not idle behind an unscoped server task. A cron-initiated turn is an **unprompted broadcast** that breaks in and **names what caused it** — a message appearing on its own with no attribution is exactly the thing being avoided. - `ProactiveSpeakContext` carries the trigger (source + job id) so every surface can state it. - **Mobile:** an acid band slides in from the top edge naming the trigger. Any user input dismisses it. - **Desktop pal:** no band — the invocation line changes to 自発 · trigger and the bubble appears. - **Parked Android tab:** an unprompted turn **pulses the tab** rather than forcing itself open. A broadcast can wait; only a suspended turn is allowed to grow the tab (see #19, #23). - The reconnect toast turns acid when broadcasts fired while the client was dark, offering to read them — **never replaying them as audio**. Drive all of it from a **dev-only injected message** matching the shape agreed in aiko/ollvt-hermes-bridge#17, so the UI is demoable and testable now. #22 is then only the real integration. Design reference: `docs/design/denpa-receiver/README.md`. ## Acceptance criteria - [ ] A dev affordance can inject an unprompted turn with an arbitrary trigger name - [ ] An unprompted turn is visually distinct from a user-initiated one on every surface - [ ] The trigger is named on screen, not implied - [ ] Any user input dismisses the break-in band - [ ] Parked tab pulses rather than expanding - [ ] Reconnect toast offers to read missed broadcasts and never plays them - [ ] Tests cover trigger attribution and the dismiss path, driven by the injected message ## Blocked by - #12
Author
Owner

Reality audit

Governing rule: build to reality, Hermes leads. Not doing something is acceptable if it does not break core functionality. Breaking the design is acceptable. Building UI that does nothing is not.

Audited against the actual wire surface:

  • Hermes (hermes-agent 0.19.0, verified running): /health, /v1/models, /v1/runs, /v1/runs/{id}, /v1/runs/{id}/events, /v1/runs/{id}/stop, /v1/runs/{id}/approval
  • Run events: message.delta, tool.started, tool.completed, run.completed, run.cancelled, run.failed/run.errored, reasoning.available
  • Go-between → client: full-text, audio, control, error, set-model-and-conf, config-switched, config-files, background-files, history-list, history-data, new-history-created, history-deleted, user-input-transcription, backend-synth-complete, force-new-message, tool_call_status, interrupt-signal, heartbeat-ack, group-update
  • Client → go-between: text-input, mic-audio-data, mic-audio-end, raw-audio-data, ai-speak-signal, interrupt-signal, fetch-history-list, fetch-and-set-history, create-new-history, delete-history, fetch-configs, switch-config, fetch-backgrounds, audio-play-start, request-init-config, heartbeat

Folded back into #22

I split this out so the break-in UI could be built against an injected message while Hermes'' push path was designed. Under the build-to-reality rule that split is wrong: nothing can trigger an unprompted turn today, so this would ship a band, an invocation line and a pulse that only ever fire from a dev affordance.

That is precisely UI that does nothing.

Folded back into #22. Both halves land together when aiko/ollvt-hermes-bridge#16 makes a Hermes-initiated turn possible. The trigger-attribution design in this issue is preserved there.

## Reality audit Governing rule: **build to reality, Hermes leads. Not doing something is acceptable if it does not break core functionality. Breaking the design is acceptable. Building UI that does nothing is not.** Audited against the actual wire surface: - **Hermes** (`hermes-agent 0.19.0`, verified running): `/health`, `/v1/models`, `/v1/runs`, `/v1/runs/{id}`, `/v1/runs/{id}/events`, `/v1/runs/{id}/stop`, `/v1/runs/{id}/approval` - **Run events**: `message.delta`, `tool.started`, `tool.completed`, `run.completed`, `run.cancelled`, `run.failed`/`run.errored`, `reasoning.available` - **Go-between → client**: `full-text`, `audio`, `control`, `error`, `set-model-and-conf`, `config-switched`, `config-files`, `background-files`, `history-list`, `history-data`, `new-history-created`, `history-deleted`, `user-input-transcription`, `backend-synth-complete`, `force-new-message`, `tool_call_status`, `interrupt-signal`, `heartbeat-ack`, `group-update` - **Client → go-between**: `text-input`, `mic-audio-data`, `mic-audio-end`, `raw-audio-data`, `ai-speak-signal`, `interrupt-signal`, `fetch-history-list`, `fetch-and-set-history`, `create-new-history`, `delete-history`, `fetch-configs`, `switch-config`, `fetch-backgrounds`, `audio-play-start`, `request-init-config`, `heartbeat` ### Folded back into #22 I split this out so the break-in UI could be built against an injected message while Hermes'' push path was designed. Under the build-to-reality rule that split is wrong: **nothing can trigger an unprompted turn today**, so this would ship a band, an invocation line and a pulse that only ever fire from a dev affordance. That is precisely UI that does nothing. Folded back into #22. Both halves land together when aiko/ollvt-hermes-bridge#16 makes a Hermes-initiated turn possible. The trigger-attribution design in this issue is preserved there.
aiko closed this issue 2026-07-29 18:11:15 +00:00
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#24
No description provided.