From b3e5f078492af038ae1cceff54bc90954454c7d6 Mon Sep 17 00:00:00 2001 From: Koushik Date: Fri, 6 Nov 2026 16:07:11 +0530 Subject: [PATCH 1/2] fix(web): voice input no longer shifts the composer's model controls Recording now swaps the composer's right-side controls for the voice controls in place, instead of pushing the model picker left. The waveform is 12 compact bars driven by the mic level. --- apps/web/src/components/chat/ChatComposer.tsx | 67 +++++++++++-------- .../components/chat/ComposerSpeechButton.tsx | 13 ++-- .../src/components/chat/VoiceInputPill.tsx | 18 +++-- .../components/settings/MicrophoneTest.tsx | 2 +- 4 files changed, 53 insertions(+), 47 deletions(-) diff --git a/apps/web/src/components/chat/ChatComposer.tsx b/apps/web/src/components/chat/ChatComposer.tsx index 69022a93fbeea..a308c953f62ff 100644 --- a/apps/web/src/components/chat/ChatComposer.tsx +++ b/apps/web/src/components/chat/ChatComposer.tsx @@ -1879,6 +1879,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) }, }); const { blocksSubmission: voiceInputActive, submitAfterDictation } = speechInput; + const voiceControlsOpen = speechInput.available && speechInput.state.phase !== "idle"; const voiceSendDisabledReason = sendDisabledReason ?? (speechInput.preparing ? "Preparing voice input" : null); const dictationShortcutLabel = useDictationShortcut({ @@ -3612,13 +3613,35 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) } className="flex min-w-0 flex-1 flex-nowrap items-center justify-end gap-2" > -
- {composerControls} +
+
+ {composerControls} +
+ {/* Outside the measured controls so the timer's text changes never re-measure them. */} + {voiceControlsOpen ? ( +
+ void speechInput.start()} + onStop={() => void speechInput.stop()} + onCancel={() => speechInput.cancel()} + onSkipPostProcessing={() => speechInput.skipPostProcessing()} + /> +
+ ) : null}
{speechInput.available ? ( - <> - void speechInput.start()} - onStop={() => void speechInput.stop()} - onCancel={() => speechInput.cancel()} - onSkipPostProcessing={() => speechInput.skipPostProcessing()} - /> - void speechInput.start()} - onCancel={() => speechInput.cancel()} - /> - + void speechInput.start()} + onCancel={() => speechInput.cancel()} + /> ) : null} - {formatElapsed(elapsedSeconds)} {props.recordingLimitSeconds !== undefined @@ -120,7 +119,7 @@ function ComposerSpeechStatus(props: { return (
- + {props.state.phase === "post-processing" ? (