From 0cb8bbcf6223700ff73761ef853a0b38d8a34caa Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Mon, 29 Dec 2025 11:24:15 +0700 Subject: [PATCH] Improve dictation UX: transcribe-only mode with UI fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Dictation now appends transcribed text to input instead of auto-sending - Voice button stays visible even with attachments or existing text - Dictation works on create agent screen (not just agent chat) - Input auto-grows when dictation appends text - New agent screen input has solid background (no transparency) - Timer preserves duration after confirming (no scary reset to zero) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- packages/app/src/app/agent/new.tsx | 21 +-- .../app/src/components/agent-input-area.tsx | 125 +++++++++--------- packages/app/src/hooks/use-dictation.ts | 3 +- 3 files changed, 79 insertions(+), 70 deletions(-) diff --git a/packages/app/src/app/agent/new.tsx b/packages/app/src/app/agent/new.tsx index 9f08d48d1..be05705c1 100644 --- a/packages/app/src/app/agent/new.tsx +++ b/packages/app/src/app/agent/new.tsx @@ -972,14 +972,16 @@ export default function DraftAgentScreen() { ) : null} - + + + @@ -1024,6 +1026,9 @@ const styles = StyleSheet.create((theme) => ({ contentContainer: { flex: 1, }, + inputAreaWrapper: { + backgroundColor: theme.colors.background, + }, configScrollContent: { flexGrow: 1, }, diff --git a/packages/app/src/components/agent-input-area.tsx b/packages/app/src/components/agent-input-area.tsx index 14db31a34..c32d29247 100644 --- a/packages/app/src/components/agent-input-area.tsx +++ b/packages/app/src/components/agent-input-area.tsx @@ -171,10 +171,6 @@ export function AgentInputArea({ const agentIdRef = useRef(agentId); const sendAgentMessageRef = useRef(sendAgentMessage); const onSubmitMessageRef = useRef(onSubmitMessage); - const agentStatusRef = useRef(undefined); - const updateQueueRef = useRef< - ((updater: (current: QueuedMessage[]) => QueuedMessage[]) => void) | null - >(null); const submitMessage = useCallback( async (text: string, images?: Array<{ uri: string; mimeType: string }>) => { if (onSubmitMessageRef.current) { @@ -190,33 +186,12 @@ export function AgentInputArea({ if (!text) { return; } - const shouldQueue = agentStatusRef.current === "running"; - if (shouldQueue) { - updateQueueRef.current?.((current) => [ - ...current, - { - id: generateMessageId(), - text, - }, - ]); - return; - } - void (async () => { - try { - await submitMessage(text); - } catch (error) { - console.error("[AgentInput] Failed to send transcribed message:", error); - updateQueueRef.current?.((current) => [ - ...current, - { - id: generateMessageId(), - text, - }, - ]); - } - })(); + const current = userInput; + const shouldPad = current.length > 0 && !/\s$/.test(current); + const nextValue = `${current}${shouldPad ? " " : ""}${text}`; + setUserInput(nextValue); }, - [submitMessage] + [setUserInput, userInput] ); const handleDictationError = useCallback((error: Error) => { @@ -225,24 +200,24 @@ export function AgentInputArea({ const canStartDictation = useCallback(() => { const socketConnected = wsOrInert.getConnectionState ? wsOrInert.getConnectionState().isConnected : wsOrInert.isConnected; - const allowed = !isRealtimeMode && socketConnected && Boolean(agent); + const allowed = !isRealtimeMode && socketConnected && (Boolean(agent) || Boolean(onSubmitMessage)); console.log("[AgentInput] canStartDictation", { allowed, isRealtimeMode, wsConnected: socketConnected, }); return allowed; - }, [agent, isRealtimeMode, wsOrInert]); + }, [agent, isRealtimeMode, onSubmitMessage, wsOrInert]); const canConfirmDictation = useCallback(() => { const socketConnected = wsOrInert.getConnectionState ? wsOrInert.getConnectionState().isConnected : wsOrInert.isConnected; - const allowed = socketConnected && Boolean(agent); + const allowed = socketConnected && (Boolean(agent) || Boolean(onSubmitMessage)); console.log("[AgentInput] canConfirmDictation", { allowed, wsConnected: socketConnected, }); return allowed; - }, [agent, wsOrInert]); + }, [agent, onSubmitMessage, wsOrInert]); const { isRecording: isDictating, @@ -657,13 +632,22 @@ export function AgentInputArea({ }); } + const handleNativeSizerLayout = useCallback( + (event: { nativeEvent: { layout: { height: number } } }) => { + if (IS_WEB) { + return; + } + applyMeasuredHeight(event.nativeEvent.layout.height, "native-sizer"); + }, + [applyMeasuredHeight] + ); + const isAgentRunning = agent?.status === "running"; - agentStatusRef.current = agent?.status; const hasText = userInput.trim().length > 0; const hasImages = selectedImages.length > 0; const hasSendableContent = hasText || hasImages; const shouldShowSendButton = !isAgentRunning && hasSendableContent; - const shouldShowVoiceControls = !hasSendableContent; + const shouldShowVoiceControls = true; const shouldHandleDesktopSubmit = IS_WEB; const updateQueue = useCallback( @@ -676,11 +660,6 @@ export function AgentInputArea({ }, [agentId, serverId, setQueuedMessages], ); - useEffect(() => { - updateQueueRef.current = updateQueue; - }, [updateQueue]); - - useLayoutEffect(() => { if (!IS_WEB) { return; @@ -1003,6 +982,13 @@ export function AgentInputArea({ editable={!isDictating && wsOrInert.isConnected} onKeyPress={shouldHandleDesktopSubmit ? handleDesktopSubmitKeyPress : undefined} /> + {!IS_WEB && ( + + + {userInput.length > 0 ? userInput : " "} + + + )} {/* Button row below input */} @@ -1058,27 +1044,30 @@ export function AgentInputArea({ )} - ) : shouldShowSendButton ? ( - - {isSubmitLoading ? ( - - ) : ( - + ) : ( + <> + {shouldShowSendButton && ( + + {isSubmitLoading ? ( + + ) : ( + + )} + )} - - ) : shouldShowVoiceControls ? ( - <> - {voiceButton} - {realtimeButton} - - ) : null} + {shouldShowVoiceControls && voiceButton} + {realtimeButton} + + )} @@ -1197,6 +1186,20 @@ const styles = StyleSheet.create(((theme: any) => ({ } : {}), }, + textInputSizerContainer: { + position: "absolute", + left: 0, + right: 0, + opacity: 0, + zIndex: -1, + }, + textInputSizer: { + width: "100%", + paddingHorizontal: theme.spacing[4], + paddingVertical: theme.spacing[3], + fontSize: theme.fontSize.lg, + lineHeight: theme.fontSize.lg * 1.4, + }, buttonRow: { flexDirection: "row", alignItems: "center", diff --git a/packages/app/src/hooks/use-dictation.ts b/packages/app/src/hooks/use-dictation.ts index 794d54677..d1ae39241 100644 --- a/packages/app/src/hooks/use-dictation.ts +++ b/packages/app/src/hooks/use-dictation.ts @@ -320,6 +320,7 @@ export function useDictation(options: UseDictationOptions): UseDictationResult { pendingRequestIdRef.current = null; setPendingRequestId(null); setIsProcessing(false); + setDuration(0); setStatus("idle"); setRetryAttempt(0); setRetryInfo(null); @@ -493,7 +494,6 @@ export function useDictation(options: UseDictationOptions): UseDictationResult { setError(null); stopDurationTracking(); - setDuration(0); setIsProcessing(true); setRetryInfo(null); setRetryAttempt(0); @@ -566,6 +566,7 @@ export function useDictation(options: UseDictationOptions): UseDictationResult { pendingRequestIdRef.current = null; setPendingRequestId(null); setIsProcessing(false); + setDuration(0); setFailedRecording(null); setStatus("idle"); setRetryAttempt(0);