From 0e13d9b12fe5a5c5f1ef064439ebf6f922040b25 Mon Sep 17 00:00:00 2001 From: Codeman maintainer Date: Mon, 9 Nov 2026 18:19:05 +0100 Subject: [PATCH] fix(voice): edits from the #705 review - docs/api-reference.md and the voice-stream.ts overview now say a transcript frame is the current utterance so far and a final frame ends it, as the relay's own comment and the invariants doc do. - VoiceInput.cleanup() hides the Send button before stop(true), so the button stays up for the words stop(true) salvaged and inserted. Co-Authored-By: Claude Opus 5.5 --- docs/api-reference.md | 3 ++- src/web/public/voice-input.js | 4 +++- src/web/voice-stream.ts | 7 ++++--- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/docs/api-reference.md b/docs/api-reference.md index 4e9cb4b2..d6808e5f 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -1842,7 +1842,8 @@ same speech-to-text service the CLI's own `/voice` mode uses. Gated on the synce little-endian PCM, 16 kHz mono (<= 64 KB per frame), plus JSON control frames `{"t":"finalize"}` (ask for the final transcript) and `{"t":"stop"}`. Server sends `{"t":"ready"}`, `{"t":"transcript","text","final"}` (each frame is the - WHOLE running transcript, not a delta), `{"t":"error","message"}` and + current utterance so far, not a delta, and a `final: true` frame ends that + utterance), `{"t":"error","message"}` and `{"t":"closed"}`. Close codes: `4003` disallowed Host/Origin, `4004` unavailable (reason in the close reason), `4008` too many concurrent streams. Streams are capped in count and length (`src/config/voice.ts`). diff --git a/src/web/public/voice-input.js b/src/web/public/voice-input.js index 517b3b7f..97e5dbe8 100644 --- a/src/web/public/voice-input.js +++ b/src/web/public/voice-input.js @@ -1415,11 +1415,13 @@ const VoiceInput = { /** Cleanup on SSE reconnect or page unload */ cleanup() { + // Hide the Send button first: stop(true) below may insert the words it + // salvaged and show the button again for them. + this._hideVoiceSendBtn(); if (this.isRecording) { this.stop(true); try { this.recognition?.abort(); } catch (_e) { /* already ended */ } } - this._hideVoiceSendBtn(); DeepgramProvider._cleanup(); ClaudeVoiceProvider._cleanup(); this.recognition = null; diff --git a/src/web/voice-stream.ts b/src/web/voice-stream.ts index d0991941..05b15d19 100644 --- a/src/web/voice-stream.ts +++ b/src/web/voice-stream.ts @@ -13,9 +13,10 @@ * - `{"type":"KeepAlive"}` on open and every 8s, or upstream drops the socket * between utterances. * - Audio frames go up as raw binary. - * - Downstream, `TranscriptText`/`TranscriptInterim` carry the RUNNING transcript - * (each frame supersedes the previous one — they are not deltas to concatenate), - * and `TranscriptEndpoint` promotes the pending interim to final. + * - Downstream, `TranscriptText`/`TranscriptInterim` carry the current utterance + * so far (each frame supersedes the previous one; they are not deltas to + * concatenate), and `TranscriptEndpoint` promotes the pending interim to final, + * which ends that utterance. * - `{"type":"CloseStream"}` finalizes; the endpoint frame that follows is the * last transcript, so `finalize()` waits briefly for it rather than closing. *