From 4021694be66b09dd16ad2ed5041852cc1d04330c Mon Sep 17 00:00:00 2001 From: npub13fn4ahfnvaa2qwylvegdgeajqs0mph6v4qsw4jcqnw4mjh3hzh2quuucm5 <8a675edd33677aa0389f6650d467b2041fb0df4ca820eacb009babb95e3715d4@sprout-oss.stage.blox.sqprod.co> Date: Thu, 9 Jul 2026 16:22:40 +0100 Subject: [PATCH] =?UTF-8?q?fix(dictation):=20gate=20=E2=8C=98D=20on=20focu?= =?UTF-8?q?s=20staying=20inside=20composer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The focus-in handler registers a composer as the active dictation target but never clears it on focus-out, so it stays active until unmount. Since AppShell dispatches the primary-D shortcut globally (outside settings), pressing ⌘D while focus is in quick search, a create-channel dialog, or any other mounted input could start mic capture and append transcript into the background composer's draft. Gate the shortcut on focus still being contained in the composer element at keydown time. --- .../features/dictation/hooks/useComposerDictation.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/desktop/src/features/dictation/hooks/useComposerDictation.ts b/desktop/src/features/dictation/hooks/useComposerDictation.ts index fc613b949..348f00ea7 100644 --- a/desktop/src/features/dictation/hooks/useComposerDictation.ts +++ b/desktop/src/features/dictation/hooks/useComposerDictation.ts @@ -121,13 +121,20 @@ export function useComposerDictation({ // ⌘D push-to-talk — hold to record, release to stop. // Dispatched from AppShell's keydown/keyup handlers. - // Only the active (most recently focused) composer responds, and only when - // not disabled/send-blocked. + // Only the active (most recently focused) composer responds, and only while + // focus remains inside it and it is not disabled/send-blocked. // biome-ignore lint/correctness/useExhaustiveDependencies: disabledRef/isSendBlockedRef are stable refs read at call time useEffect(() => { function handleKeyDown() { // Only respond if this is the active composer instance. if (!isActiveDictationComposer(instanceId)) return; + // Only respond if focus is still inside this composer. The active-composer + // registration persists after focusout (until unmount), so without this + // check, focusing a composer and then moving to another mounted input or + // dialog (quick search, create-channel, etc.) would still let ⌘D start + // microphone capture and append transcript into the background draft. + const el = composerRef?.current; + if (el && !el.contains(document.activeElement)) return; // Don't start dictation in disabled/blocked composers. if (disabledRef.current || isSendBlockedRef.current) return; if (!dictation.isRecording && !dictation.isStarting) { @@ -147,6 +154,7 @@ export function useComposerDictation({ }; }, [ instanceId, + composerRef, dictation.isRecording, dictation.isStarting, dictation.startRecording,