Skip to content

feat(desktop): add message read-aloud playback - #3239

Closed
tellaho wants to merge 1 commit into
tho/pr-2467-basefrom
tho/message-tts
Closed

feat(desktop): add message read-aloud playback#3239
tellaho wants to merge 1 commit into
tho/pr-2467-basefrom
tho/message-tts

Conversation

@tellaho

@tellaho tellaho commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Category: new-feature
User Impact: Users can play or stop a spoken reading of delivered agent messages from the desktop message action bar.

Problem: Ordinary chat messages have no click-to-play speech path; Buzz's existing local TTS is coupled to active huddles. Solution: Add a separate app-scoped Pocket TTS controller with deterministic message-keyed playback, then expose it through the existing message action surface while preventing message and huddle audio from competing.

Note

This PR is stacked on #2467. Review only the changes above that PR; this remains an experiment until the base lands.

File changes

desktop/scripts/check-file-sizes.mjs
Adds the message TTS implementation to the repository's explicit large-file allowance.

desktop/src-tauri/src/app_state.rs
Owns message playback independently from huddle state at the application level.

desktop/src-tauri/src/huddle/mod.rs
Stops message playback before a huddle starts or joins so both flows cannot compete for audio.

desktop/src-tauri/src/lib.rs
Registers the message TTS module and its desktop commands.

desktop/src-tauri/src/message_tts.rs
Implements local Pocket synthesis, message-keyed lifecycle events, app-wide superseding playback, disclosed UTF-8-safe truncation, model recovery, and huddle exclusion.

desktop/src-tauri/src/message_tts_tests.rs
Covers truncation, lifecycle payloads, concurrent stop behavior, and stale completion races.

desktop/src/app/AppShell.tsx
Mounts message playback state around routed message surfaces while retaining access to huddle state.

desktop/src/features/message-tts/lib/playbackReducer.d.mts
Describes reducer event and state contracts for TypeScript consumers.

desktop/src/features/message-tts/lib/playbackReducer.mjs
Keeps frontend playback state deterministic when backend events arrive out of order.

desktop/src/features/message-tts/lib/playbackReducer.test.mjs
Exercises supersede, detached error, and rapid play/stop event sequences.

desktop/src/features/message-tts/ui/MessageTtsProvider.tsx
Bridges Tauri commands and events to message actions, truncation notices, huddle gating, and failure toasts.

desktop/src/features/messages/ui/MessageActionBar.tsx
Adds an accessible Play/Stop control for delivered agent text using the existing action-bar system.

Reproduction steps

  1. Run the desktop app from this branch against a relay with delivered agent messages.
  2. Hover a delivered agent text message and select Read aloud; verify local speech begins and the control changes to Stop reading.
  3. Start another agent message while one is speaking; verify the newer message supersedes the older one and its control owns the active state.
  4. Stop playback, then exercise rapid play/stop and switching between messages; verify stale completions do not clear newer playback.
  5. Start or join a huddle; verify active message speech stops and read-aloud is unavailable while the huddle is active.
  6. Play a message longer than 2,000 characters; verify truncation is disclosed in the UI and spoken output.

Screenshot

Read-aloud button in the desktop message action bar

Verification

  • pnpm test: 3,414 passed
  • cargo test: 1,576 passed
  • TypeScript, Biome, and file-size checks: passed
  • Independent code and UI review: approved

Let a user explicitly play/stop a delivered agent response from the
message action bar, reusing the local Pocket TTS stack while keeping
read-aloud fully independent from huddle orchestration.

Backend (src-tauri):
- New app-scoped message_tts module with its own TtsPipeline instance
  and fresh active/cancel flags — zero STT/barge-in/huddle-phase
  coupling. Commands: speak_chat_message / stop_chat_message_speech.
- Message-keyed lifecycle (preparing/playing/completed/stopped/
  superseded/failed) emitted as message-tts-state-changed events.
  Generation tokens guarantee stale watcher completions can never
  overwrite a newer request; one playback app-wide, newer play
  supersedes older.
- Huddle exclusion both ways: speak refuses while huddle phase is not
  Idle, and start/join_huddle stop any message playback before taking
  the audio device.
- Long-message limit mirrors the huddle cap (2000 chars, char-counted)
  with a spoken suffix and a truncated flag so the cut is disclosed,
  not silent. Missing voice model kicks off the download and surfaces
  a retryable error. No persisted audio cache.

Frontend (desktop/src):
- MessageTtsProvider (mounted in AppShell inside HuddleProvider) folds
  backend events through a pure reducer (playbackReducer.mjs) so stale
  terminal events for superseded messages can't clear newer playback.
  Errors surface as toasts + per-message state; truncation is disclosed
  via toast at playback start.
- ReadAloudButton in MessageActionBar for delivered agent text only
  (isAgent, not pending, has body), with aria-label/aria-pressed,
  tooltip, and disabled-with-reason during active huddles.

Tests: 16 Rust unit tests (truncation UTF-8 safety, supersede/stop
races, concurrent stops, event payload shape) and 13 node:test reducer
tests (out-of-order supersede delivery, detached errors, rapid
play/stop). Full suites green: cargo test (1576), pnpm test (3414),
typecheck, biome + file-size checks, desktop build.

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
@tellaho tellaho closed this Jul 27, 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.

1 participant