diff --git a/apps/web/src/components/chat/ChatComposer.tsx b/apps/web/src/components/chat/ChatComposer.tsx index eb01b8242e2df..d459c531d81f2 100644 --- a/apps/web/src/components/chat/ChatComposer.tsx +++ b/apps/web/src/components/chat/ChatComposer.tsx @@ -1834,6 +1834,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) }, }); const { blocksSubmission: voiceInputActive, submitAfterDictation } = speechInput; + const voiceControlsOpen = speechInput.available && speechInput.state.phase !== "idle"; // A compact footer gives the dictation capsule the whole row beside the send button. const dictationTakesFooter = isComposerFooterCompact && speechInput.state.phase !== "idle"; const submissionTarget = activePendingProgress ? "pending-user-input" : "provider-turn"; @@ -3667,16 +3668,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()} - /> - + void speechInput.start()} + /> ) : null}