From 5e85a241f99f19ecc9800c487af669fa74dbfe3f Mon Sep 17 00:00:00 2001 From: John Tennant Date: Fri, 31 Jul 2026 15:02:32 -0400 Subject: [PATCH] fix(desktop): scope dictation to composer context Signed-off-by: John Tennant --- .../features/dictation/hooks/useDictation.ts | 21 ++------- .../dictation/lib/voiceInput.test.mjs | 46 ++++--------------- .../src/features/dictation/lib/voiceInput.ts | 32 ++----------- .../features/messages/ui/MessageComposer.tsx | 2 +- 4 files changed, 16 insertions(+), 85 deletions(-) diff --git a/desktop/src/features/dictation/hooks/useDictation.ts b/desktop/src/features/dictation/hooks/useDictation.ts index 34a8bf245..a3ca1b75a 100644 --- a/desktop/src/features/dictation/hooks/useDictation.ts +++ b/desktop/src/features/dictation/hooks/useDictation.ts @@ -1,5 +1,5 @@ -import { useCallback, useRef } from "react"; -import { replaceTrailingTranscribedText } from "../lib/voiceInput"; +import { useCallback } from "react"; +import { appendTranscribedText } from "../lib/voiceInput"; import { useLocalDictation } from "./useLocalDictation"; interface UseDictationOptions { @@ -16,30 +16,15 @@ export function useDictation({ getText, setText, }: UseDictationOptions) { - const lastTranscriptRef = useRef(""); - const handleTranscript = useCallback( (transcript: string) => { - const previous = lastTranscriptRef.current; - const latest = getText(); - const merged = replaceTrailingTranscribedText( - latest, - previous, - transcript, - ); - setText(merged); - // Each native flush is an independent segment, so the next transcript - // appends instead of replacing this one. - lastTranscriptRef.current = ""; + setText(appendTranscribedText(getText(), transcript)); }, [getText, setText], ); const dictation = useLocalDictation({ disabled, - onRecordingStart: () => { - lastTranscriptRef.current = ""; - }, onTranscriptText: handleTranscript, }); diff --git a/desktop/src/features/dictation/lib/voiceInput.test.mjs b/desktop/src/features/dictation/lib/voiceInput.test.mjs index 3360d9ab0..2ae61b338 100644 --- a/desktop/src/features/dictation/lib/voiceInput.test.mjs +++ b/desktop/src/features/dictation/lib/voiceInput.test.mjs @@ -2,8 +2,8 @@ import assert from "node:assert/strict"; import test from "node:test"; import { + appendTranscribedText, getDictationSendDecision, - replaceTrailingTranscribedText, shouldAutoSubmitDictation, } from "./voiceInput.ts"; @@ -71,46 +71,16 @@ test("dictation auto-submit waits until capture and transcription settle", () => ); }); -// ── replaceTrailingTranscribedText ────────────────────────────────────────── +// ── appendTranscribedText ─────────────────────────────────────────────────── -test("replaceTrailingTranscribedText_appendsWhenNoPrevious", () => { - assert.equal( - replaceTrailingTranscribedText("Hello", "", "world"), - "Hello world", - ); +test("appendTranscribedText_appendsToExistingText", () => { + assert.equal(appendTranscribedText("Hello", "world"), "Hello world"); }); -test("replaceTrailingTranscribedText_appendsToEmptyBase", () => { - assert.equal(replaceTrailingTranscribedText("", "", "hello"), "hello"); +test("appendTranscribedText_appendsToEmptyBase", () => { + assert.equal(appendTranscribedText("", "hello"), "hello"); }); -test("replaceTrailingTranscribedText_replacesTrailingInterim", () => { - // Interim "hello wor" is refined to "hello world". - assert.equal( - replaceTrailingTranscribedText("hello wor", "hello wor", "hello world"), - "hello world", - ); -}); - -test("replaceTrailingTranscribedText_preservesTextTypedBeforeDictation", () => { - // User typed "Note: " then dictated; the manual prefix must survive. - assert.equal( - replaceTrailingTranscribedText("Note: hi", "hi", "hi there"), - "Note: hi there", - ); -}); - -test("replaceTrailingTranscribedText_appendsWhenPreviousNoLongerMatches", () => { - // If the previous transcript isn't the trailing text anymore, append. - assert.equal( - replaceTrailingTranscribedText("edited text", "old", "new"), - "edited text new", - ); -}); - -test("replaceTrailingTranscribedText_noDoubleSpaceBeforePunctuation", () => { - assert.equal( - replaceTrailingTranscribedText("Hello", "", ", world"), - "Hello, world", - ); +test("appendTranscribedText_avoidsSpaceBeforePunctuation", () => { + assert.equal(appendTranscribedText("Hello", ", world"), "Hello, world"); }); diff --git a/desktop/src/features/dictation/lib/voiceInput.ts b/desktop/src/features/dictation/lib/voiceInput.ts index da92bdf7f..7bb10c3a9 100644 --- a/desktop/src/features/dictation/lib/voiceInput.ts +++ b/desktop/src/features/dictation/lib/voiceInput.ts @@ -1,4 +1,7 @@ -function appendTranscribedText(baseText: string, fragment: string): string { +export function appendTranscribedText( + baseText: string, + fragment: string, +): string { const normalizedFragment = fragment.replace(/\s+/g, " ").trim(); if (!normalizedFragment) return baseText; if (!baseText.trim()) return normalizedFragment; @@ -37,30 +40,3 @@ export function shouldAutoSubmitDictation({ }): boolean { return requested && !isRecording && !isStarting && !isTranscribing; } - -export function replaceTrailingTranscribedText( - fullText: string, - previousTranscribedText: string, - nextTranscribedText: string, -): string { - if (!previousTranscribedText) { - return appendTranscribedText(fullText, nextTranscribedText); - } - - if (fullText.endsWith(previousTranscribedText)) { - return appendTranscribedText( - fullText.slice(0, -previousTranscribedText.length), - nextTranscribedText, - ); - } - - const trimmedPreviousText = previousTranscribedText.trim(); - if (trimmedPreviousText && fullText.endsWith(trimmedPreviousText)) { - return appendTranscribedText( - fullText.slice(0, -trimmedPreviousText.length), - nextTranscribedText, - ); - } - - return appendTranscribedText(fullText, nextTranscribedText); -} diff --git a/desktop/src/features/messages/ui/MessageComposer.tsx b/desktop/src/features/messages/ui/MessageComposer.tsx index c60976de5..ce9b6f8ee 100644 --- a/desktop/src/features/messages/ui/MessageComposer.tsx +++ b/desktop/src/features/messages/ui/MessageComposer.tsx @@ -274,7 +274,7 @@ function MessageComposerImpl({ isUploadingRef, setComposerContent, setEditorContent: richText.setContent, - draftKey: effectiveDraftKey, + draftKey: `${effectiveDraftKey}\0${editTarget?.id ?? ""}\0${replyTarget?.id ?? ""}`, composerRef: composerScrollRef, submitMessageRef, });