From facd99ab13d1d09682b0335d672a128ec6baf48e Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Tue, 20 Jan 2026 13:09:22 +0700 Subject: [PATCH] Fix dictation lifecycle and add real STT e2e --- packages/app/e2e/dictation-web.spec.ts | 128 ++++++++++++---- packages/app/e2e/fixtures/recording.wav | Bin 0 -> 132990 bytes packages/app/e2e/fixtures/recording.webm | Bin 0 -> 31811 bytes .../app/src/components/agent-input-area.tsx | 3 + packages/app/src/components/message-input.tsx | 13 +- .../src/hooks/use-audio-recorder.native.ts | 140 +++++++++--------- .../app/src/hooks/use-audio-recorder.web.ts | 60 ++++++-- packages/app/src/hooks/use-dictation.ts | 38 ++++- packages/app/src/utils/attempt-guard.ts | 36 +++++ 9 files changed, 306 insertions(+), 112 deletions(-) create mode 100644 packages/app/e2e/fixtures/recording.wav create mode 100644 packages/app/e2e/fixtures/recording.webm create mode 100644 packages/app/src/utils/attempt-guard.ts diff --git a/packages/app/e2e/dictation-web.spec.ts b/packages/app/e2e/dictation-web.spec.ts index 4d04e263d..ae1af9600 100644 --- a/packages/app/e2e/dictation-web.spec.ts +++ b/packages/app/e2e/dictation-web.spec.ts @@ -2,9 +2,15 @@ import { test, expect } from './fixtures'; import { createAgent, ensureHostSelected, gotoHome, setWorkingDirectory } from './helpers/app'; import { createTempGitRepo } from './helpers/workspace'; import type { Page } from '@playwright/test'; +import { readFile } from 'node:fs/promises'; +import path from 'node:path'; -function addFakeMicrophone(page: Page) { - return page.addInitScript(() => { +async function addFakeMicrophone(page: Page) { + const fixturePath = path.resolve(__dirname, 'fixtures', 'recording.webm'); + const base64Audio = (await readFile(fixturePath)).toString('base64'); + const mimeType = 'audio/webm;codecs=opus'; + + return page.addInitScript(({ base64Audio, mimeType }) => { const mic = { active: 0, getUserMediaCalls: 0, @@ -13,29 +19,34 @@ function addFakeMicrophone(page: Page) { }; (window as any).__mic = mic; - const ensureMediaDevices = () => { - const nav = navigator as any; - if (!nav.mediaDevices) { - nav.mediaDevices = {}; - } - if (typeof nav.mediaDevices.getUserMedia !== 'function') { - nav.mediaDevices.getUserMedia = async () => { - mic.getUserMediaCalls += 1; - mic.active += 1; - const track = { - stop: () => { - mic.stopCalls += 1; - mic.active = Math.max(0, mic.active - 1); - }, - }; - return { - getTracks: () => [track], - }; - }; - } + (window as any).isSecureContext = true; + + const nav = navigator as any; + if (!nav.mediaDevices) { + nav.mediaDevices = {}; + } + nav.mediaDevices.getUserMedia = async () => { + mic.getUserMediaCalls += 1; + mic.active += 1; + const track = { + stop: () => { + mic.stopCalls += 1; + mic.active = Math.max(0, mic.active - 1); + }, + }; + return { + getTracks: () => [track], + }; }; - ensureMediaDevices(); + const blobFromBase64 = (base64: string, mimeType: string): Blob => { + const binaryString = atob(base64); + const bytes = new Uint8Array(binaryString.length); + for (let i = 0; i < binaryString.length; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + return new Blob([bytes], { type: mimeType }); + }; class FakeMediaRecorder extends EventTarget { public static isTypeSupported() { @@ -64,7 +75,7 @@ function addFakeMicrophone(page: Page) { this.state = 'inactive'; try { this.ondataavailable?.({ - data: new Blob(['paseo-e2e-audio'], { type: this.mimeType }), + data: blobFromBase64(base64Audio, mimeType), }); } catch (err) { this.onerror?.(err); @@ -74,7 +85,7 @@ function addFakeMicrophone(page: Page) { } (window as any).MediaRecorder = FakeMediaRecorder; - }); + }, { base64Audio, mimeType }); } test('dictation hotkeys do not trigger on background screens', async ({ page }) => { @@ -88,6 +99,7 @@ test('dictation hotkeys do not trigger on background screens', async ({ page }) await createAgent(page, 'Respond with exactly: Hello'); await expect(page).toHaveURL(/\/agent\//); + await expect(page.getByRole('textbox', { name: 'Message agent...' })).toBeEditable(); await page.keyboard.press('Control+d'); await page.waitForTimeout(200); @@ -107,6 +119,33 @@ test('dictation hotkeys do not trigger on background screens', async ({ page }) } }); +test('dictation transcribes fixture via real STT', async ({ page }) => { + await addFakeMicrophone(page); + + const repo = await createTempGitRepo(); + try { + await gotoHome(page); + await setWorkingDirectory(page, repo.path); + await ensureHostSelected(page); + await createAgent(page, 'Respond with exactly: Hello'); + + await expect(page).toHaveURL(/\/agent\//); + await expect(page.getByRole('textbox', { name: 'Message agent...' })).toBeEditable(); + + await page.keyboard.press('Control+d'); + await expect + .poll(async () => page.evaluate(() => (window as any).__mic.active as number)) + .toBe(1); + + await page.keyboard.press('Control+d'); + + const transcriptLocator = page.getByText(/voice note/i); + await expect(transcriptLocator).toBeVisible({ timeout: 60_000 }); + } finally { + await repo.cleanup(); + } +}); + test('cancel stops mic even if recorder is already inactive', async ({ page }) => { await addFakeMicrophone(page); @@ -118,6 +157,7 @@ test('cancel stops mic even if recorder is already inactive', async ({ page }) = await createAgent(page, 'Respond with exactly: Hello'); await expect(page).toHaveURL(/\/agent\//); + await expect(page.getByRole('textbox', { name: 'Message agent...' })).toBeEditable(); await page.keyboard.press('Control+d'); await expect @@ -139,3 +179,41 @@ test('cancel stops mic even if recorder is already inactive', async ({ page }) = await repo.cleanup(); } }); + +test('dictation confirm+send does not dispatch after navigating away', async ({ page }) => { + await addFakeMicrophone(page); + + const repo = await createTempGitRepo(); + try { + await gotoHome(page); + await setWorkingDirectory(page, repo.path); + await ensureHostSelected(page); + await createAgent(page, 'Respond with exactly: Hello'); + + await expect(page).toHaveURL(/\/agent\//); + await expect(page.getByRole('textbox', { name: 'Message agent...' })).toBeEditable(); + + await page.keyboard.press('Control+d'); + await expect + .poll(async () => page.evaluate(() => (window as any).__mic.active as number)) + .toBe(1); + + await page.keyboard.press('Control+d'); + + const newAgentButton = page.getByText('New Agent', { exact: true }).first(); + await expect(newAgentButton).toBeVisible(); + await newAgentButton.click(); + await expect(page).toHaveURL(/\/$/); + + await page.waitForTimeout(10_000); + + const agentEntry = page.getByText(repo.path).first(); + await expect(agentEntry).toBeVisible(); + await agentEntry.click(); + await expect(page).toHaveURL(/\/agent\//); + + await expect(page.getByText(/voice note/i)).not.toBeVisible(); + } finally { + await repo.cleanup(); + } +}); diff --git a/packages/app/e2e/fixtures/recording.wav b/packages/app/e2e/fixtures/recording.wav new file mode 100644 index 0000000000000000000000000000000000000000..8a26fca6783ad25ea958bfce409852b10f7a9e41 GIT binary patch literal 132990 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 literal 0 HcmV?d00001 diff --git a/packages/app/e2e/fixtures/recording.webm b/packages/app/e2e/fixtures/recording.webm new file mode 100644 index 0000000000000000000000000000000000000000..a3329e3e87696d22bc02540e297cc2864ff33997 GIT binary patch literal 31811 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 literal 0 HcmV?d00001 diff --git a/packages/app/src/components/agent-input-area.tsx b/packages/app/src/components/agent-input-area.tsx index 529b6bfc3..993ebac96 100644 --- a/packages/app/src/components/agent-input-area.tsx +++ b/packages/app/src/components/agent-input-area.tsx @@ -16,6 +16,7 @@ import Animated, { import { useReanimatedKeyboardAnimation } from "react-native-keyboard-controller"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useRealtime } from "@/contexts/realtime-context"; +import { useIsFocused } from "@react-navigation/native"; import { FOOTER_HEIGHT, MAX_CONTENT_WIDTH } from "@/constants/layout"; import { generateMessageId } from "@/types/stream"; import { AgentStatusBar } from "./agent-status-bar"; @@ -77,6 +78,7 @@ export function AgentInputArea({ const { theme } = useUnistyles(); const insets = useSafeAreaInsets(); const { height: keyboardHeight } = useReanimatedKeyboardAnimation(); + const isScreenFocused = useIsFocused(); const client = useSessionStore( (state) => state.sessions[serverId]?.client ?? null @@ -568,6 +570,7 @@ export function AgentInputArea({ placeholder="Message agent..." autoFocus={autoFocus} disabled={isRealtimeMode} + isScreenFocused={isScreenFocused} leftContent={leftContent} rightContent={rightContent} isAgentRunning={isAgentRunning} diff --git a/packages/app/src/components/message-input.tsx b/packages/app/src/components/message-input.tsx index 90f2a47d3..750fd072e 100644 --- a/packages/app/src/components/message-input.tsx +++ b/packages/app/src/components/message-input.tsx @@ -54,6 +54,8 @@ export interface MessageInputProps { placeholder?: string; autoFocus?: boolean; disabled?: boolean; + /** True when the containing screen is focused (React Navigation). Used to disable global hotkeys and cancel dictation when unfocused. */ + isScreenFocused?: boolean; /** Content to render on the left side of the button row (e.g., AgentStatusBar) */ leftContent?: React.ReactNode; /** Content to render on the right side after voice button (e.g., realtime button, cancel button) */ @@ -105,6 +107,7 @@ export const MessageInput = forwardRef( placeholder = "Message...", autoFocus = false, disabled = false, + isScreenFocused = true, leftContent, rightContent, isAgentRunning = false, @@ -202,12 +205,20 @@ export const MessageInput = forwardRef( onError: handleDictationError, canStart: canStartDictation, canConfirm: canConfirmDictation, + autoStopWhenHidden: { isVisible: isScreenFocused }, enableDuration: true, }); + useEffect(() => { + if (isDictating || isDictationProcessing || dictationStatus === "retrying") { + return; + } + sendAfterTranscriptRef.current = false; + }, [dictationStatus, isDictating, isDictationProcessing]); + // Cmd+D to start/submit dictation, Escape to cancel useEffect(() => { - if (!IS_WEB) return; + if (!IS_WEB || !isScreenFocused) return; function handleKeyDown(event: KeyboardEvent) { // Cmd+D: start dictation or submit if already dictating if ((event.metaKey || event.ctrlKey) && event.key === "d") { diff --git a/packages/app/src/hooks/use-audio-recorder.native.ts b/packages/app/src/hooks/use-audio-recorder.native.ts index 428a38c9c..14b7bd4c8 100644 --- a/packages/app/src/hooks/use-audio-recorder.native.ts +++ b/packages/app/src/hooks/use-audio-recorder.native.ts @@ -1,6 +1,7 @@ import { useAudioRecorder as useExpoAudioRecorder, useAudioRecorderState, RecordingOptions, RecordingPresets, requestRecordingPermissionsAsync, setAudioModeAsync } from 'expo-audio'; import { Paths, File, Directory, FileInfo } from 'expo-file-system'; import { useState, useEffect, useRef, useMemo, useCallback } from 'react'; +import { AttemptCancelledError, AttemptGuard } from '@/utils/attempt-guard'; export interface AudioCaptureConfig { sampleRate?: number; @@ -127,8 +128,8 @@ async function uriToBlob(uri: string): Promise { */ export function useAudioRecorder(config?: AudioCaptureConfig) { const [recordingStartTime, setRecordingStartTime] = useState(null); - const recordingIntentRef = useRef(false); - const stopRequestedRef = useRef(false); + const attemptGuardRef = useRef(new AttemptGuard()); + const startStopMutexRef = useRef | null>(null); // Store config callbacks in refs so they can update without recreating the recorder const configRef = useRef(config); @@ -203,19 +204,13 @@ export function useAudioRecorder(config?: AudioCaptureConfig) { } try { - recordingIntentRef.current = true; - const ensureNotCancelled = () => { - if (stopRequestedRef.current) { - throw new Error('Recording cancelled'); - } - }; - - ensureNotCancelled(); + const attemptId = attemptGuardRef.current.next(); + attemptGuardRef.current.assertCurrent(attemptId); // Request microphone permissions console.log('[AudioRecorder] Requesting recording permissions...'); const permissionResponse = await requestRecordingPermissionsAsync(); - ensureNotCancelled(); + attemptGuardRef.current.assertCurrent(attemptId); if (!permissionResponse.granted) { throw new Error('Microphone permission denied. Please enable microphone access in your device settings.'); @@ -227,7 +222,7 @@ export function useAudioRecorder(config?: AudioCaptureConfig) { playsInSilentMode: true, allowsRecording: true, }); - ensureNotCancelled(); + attemptGuardRef.current.assertCurrent(attemptId); console.log('[AudioRecorder] Starting recording with options:', { sampleRate: recordingOptions.sampleRate, @@ -237,25 +232,26 @@ export function useAudioRecorder(config?: AudioCaptureConfig) { const startTime = new Date(); setRecordingStartTime(startTime); - ensureNotCancelled(); + attemptGuardRef.current.assertCurrent(attemptId); // Prepare the recorder before recording (required step) console.log('[AudioRecorder] Preparing recorder...'); await recorder.prepareToRecordAsync(); - ensureNotCancelled(); + attemptGuardRef.current.assertCurrent(attemptId); console.log('[AudioRecorder] Starting recording...'); await recorder.record(); - ensureNotCancelled(); + attemptGuardRef.current.assertCurrent(attemptId); console.log('[AudioRecorder] Recording started at:', startTime.toISOString()); console.log('[AudioRecorder] Recorder isRecording:', recorder.isRecording); } catch (error: any) { - recordingIntentRef.current = false; setRecordingStartTime(null); - if (error?.message === 'Recording cancelled') { + if (error instanceof AttemptCancelledError) { console.log('[AudioRecorder] Recording start cancelled.'); - } else { + return; + } + if (error?.message !== 'Recording cancelled') { console.error('[AudioRecorder] Failed to start recording:', error); } throw new Error(`Failed to start audio recording: ${error.message}`); @@ -265,76 +261,75 @@ export function useAudioRecorder(config?: AudioCaptureConfig) { const stop = useCallback(async (): Promise => { const recorder = recorderRef.current; - // Guard against duplicate stop calls - if (!recorder.isRecording && !recordingIntentRef.current) { - console.error('[AudioRecorder] Attempted to stop when not recording. Recorder state:', { - isRecording: recorder.isRecording, - hasRecordingIntent: recordingIntentRef.current, - hasRecordingStartTime: !!recordingStartTime, - }); - throw new Error('Not recording'); + // Cancel any in-flight start attempt and serialize stop/cleanup. + attemptGuardRef.current.cancel(); + if (startStopMutexRef.current) { + await startStopMutexRef.current.catch(() => undefined); } - stopRequestedRef.current = true; - try { - // Stop recording - if (recorder.isRecording) { - await recorder.stop(); - } else { - console.warn('[AudioRecorder] Recorder already stopped before stop() call, continuing cleanup.'); - } - - // Get URI from recorder - let uri = recorder.uri; - console.log('[AudioRecorder] Initial URI from recorder:', uri); - - // Workaround for Expo SDK 54 Android bug - find actual recording file - if (recordingStartTime && (!uri || uri === '')) { - console.log('[AudioRecorder] Using workaround to find actual recording file...'); - const actualUri = await getActualRecordingUri(recordingStartTime); - if (actualUri) { - uri = actualUri; - console.log('[AudioRecorder] Found actual recording URI:', uri); + const stopPromise = (async () => { + // Stop recording + if (recorder.isRecording) { + await recorder.stop(); + } else { + console.warn('[AudioRecorder] Recorder already stopped before stop() call, continuing cleanup.'); } - } - if (!uri || uri === '') { - throw new Error('No recording URI found'); - } + // Get URI from recorder + let uri = recorder.uri; + console.log('[AudioRecorder] Initial URI from recorder:', uri); - // Get file info - const file = new File(uri); - const exists = file.exists; - console.log('[AudioRecorder] File exists:', exists); + // Workaround for Expo SDK 54 Android bug - find actual recording file + if (recordingStartTime && (!uri || uri === '')) { + console.log('[AudioRecorder] Using workaround to find actual recording file...'); + const actualUri = await getActualRecordingUri(recordingStartTime); + if (actualUri) { + uri = actualUri; + console.log('[AudioRecorder] Found actual recording URI:', uri); + } + } - if (!exists) { - throw new Error('Recording file does not exist'); - } + if (!uri || uri === '') { + // Cancellation / early stop: return an empty blob, but guarantee cleanup. + setRecordingStartTime(null); + return new Blob([], { type: 'audio/m4a' }); + } - // Convert URI to Blob - const audioBlob = await uriToBlob(uri); + // Get file info + const file = new File(uri); + const exists = file.exists; + console.log('[AudioRecorder] File exists:', exists); - console.log('[AudioRecorder] Recording converted to blob:', { - size: audioBlob.size, - type: audioBlob.type, - }); + if (!exists) { + setRecordingStartTime(null); + return new Blob([], { type: 'audio/m4a' }); + } - // Clean up the temporary file - file.delete(); + // Convert URI to Blob + const audioBlob = await uriToBlob(uri); - // Reset start time and clear recording intent - setRecordingStartTime(null); - recordingIntentRef.current = false; + console.log('[AudioRecorder] Recording converted to blob:', { + size: audioBlob.size, + type: audioBlob.type, + }); - return audioBlob; + // Clean up the temporary file + file.delete(); + + // Reset start time + setRecordingStartTime(null); + + return audioBlob; + })(); + startStopMutexRef.current = stopPromise; + return await stopPromise; } catch (error: any) { - recordingIntentRef.current = false; setRecordingStartTime(null); console.error('[AudioRecorder] Failed to stop recording:', error); throw new Error(`Failed to stop audio recording: ${error.message}`); } finally { - stopRequestedRef.current = false; + startStopMutexRef.current = null; } }, [recordingStartTime]); @@ -345,8 +340,7 @@ export function useAudioRecorder(config?: AudioCaptureConfig) { }, []); const isRecording = useCallback(() => { - // Treat local intent as backup so callers can detect pending sessions - return Boolean(recorderState.isRecording) || recordingIntentRef.current; + return Boolean(recorderState.isRecording); }, [recorderState.isRecording]); // Return stable object using useMemo diff --git a/packages/app/src/hooks/use-audio-recorder.web.ts b/packages/app/src/hooks/use-audio-recorder.web.ts index 63e0a09dc..a458bfc25 100644 --- a/packages/app/src/hooks/use-audio-recorder.web.ts +++ b/packages/app/src/hooks/use-audio-recorder.web.ts @@ -1,4 +1,5 @@ import { useCallback, useEffect, useRef, useState } from "react"; +import { AttemptCancelledError, AttemptGuard } from "@/utils/attempt-guard"; export interface AudioCaptureConfig { sampleRate?: number; @@ -21,6 +22,7 @@ export function useAudioRecorder(config?: AudioCaptureConfig) { const mediaRecorderRef = useRef(null); const mediaStreamRef = useRef(null); + const attemptGuardRef = useRef(new AttemptGuard()); const chunksRef = useRef([]); const supportedMimeTypeRef = useRef(undefined); const analyserRef = useRef(null); @@ -79,6 +81,14 @@ export function useAudioRecorder(config?: AudioCaptureConfig) { } }, []); + const hardReset = useCallback(() => { + stopMetering(); + cleanupStream(); + mediaRecorderRef.current = null; + chunksRef.current = []; + setIsRecording(false); + }, [cleanupStream, stopMetering]); + const startMetering = useCallback((stream: MediaStream) => { const onAudioLevel = configRef.current?.onAudioLevel; if (!onAudioLevel) { @@ -129,6 +139,8 @@ export function useAudioRecorder(config?: AudioCaptureConfig) { }, []); const start = useCallback(async () => { + const attemptId = attemptGuardRef.current.next(); + if (mediaRecorderRef.current && mediaRecorderRef.current.state === "recording") { throw new Error("Already recording"); } @@ -173,6 +185,22 @@ export function useAudioRecorder(config?: AudioCaptureConfig) { throw new Error(`Failed to access microphone: ${error?.message ?? error}`); } + try { + attemptGuardRef.current.assertCurrent(attemptId); + } catch (err) { + if (err instanceof AttemptCancelledError) { + stream.getTracks().forEach((track) => { + try { + track.stop(); + } catch { + // Ignore track stop errors. + } + }); + return; + } + throw err; + } + mediaStreamRef.current = stream; const recorderOptions: MediaRecorderOptions = {}; @@ -222,11 +250,25 @@ export function useAudioRecorder(config?: AudioCaptureConfig) { }, [cleanupStream, detectSupportedMimeType, startMetering]); const stop = useCallback(async (): Promise => { + attemptGuardRef.current.cancel(); + const recorder = mediaRecorderRef.current; - if (!recorder || recorder.state === "inactive") { + if (!recorder) { throw new Error("Not recording"); } + if (recorder.state === "inactive") { + stopMetering(); + cleanupStream(); + mediaRecorderRef.current = null; + setIsRecording(false); + + const mimeType = recorder.mimeType || detectSupportedMimeType() || "audio/webm"; + const blob = new Blob(chunksRef.current, { type: mimeType }); + chunksRef.current = []; + return blob; + } + return await new Promise((resolve, reject) => { const finalize = () => { recorder.removeEventListener("stop", finalize); @@ -275,17 +317,15 @@ export function useAudioRecorder(config?: AudioCaptureConfig) { useEffect(() => { return () => { - if (mediaRecorderRef.current && mediaRecorderRef.current.state === "recording") { - try { - mediaRecorderRef.current.stop(); - } catch { - // Ignore stop during unmount. - } + attemptGuardRef.current.cancel(); + try { + void mediaRecorderRef.current?.stop(); + } catch { + // Ignore stop during unmount. } - stopMetering(); - cleanupStream(); + hardReset(); }; - }, [cleanupStream, stopMetering]); + }, [hardReset]); const getSupportedMimeType = useCallback(() => detectSupportedMimeType(), [ detectSupportedMimeType, diff --git a/packages/app/src/hooks/use-dictation.ts b/packages/app/src/hooks/use-dictation.ts index 6ce2696ed..e4d59c906 100644 --- a/packages/app/src/hooks/use-dictation.ts +++ b/packages/app/src/hooks/use-dictation.ts @@ -5,6 +5,7 @@ import { useAudioRecorder } from "@/hooks/use-audio-recorder"; import type { DaemonClientV2 } from "@server/client/daemon-client-v2"; import type { TranscriptionResultMessage } from "@server/shared/messages"; import { generateMessageId } from "@/types/stream"; +import { AttemptGuard } from "@/utils/attempt-guard"; export type DictationStatus = "idle" | "recording" | "uploading" | "retrying" | "failed"; @@ -310,9 +311,15 @@ export function useDictation(options: UseDictationOptions): UseDictationResult { isRecordingRef.current = isRecording; }, [isRecording]); + const isProcessingRef = useRef(isProcessing); + useEffect(() => { + isProcessingRef.current = isProcessing; + }, [isProcessing]); + const pendingRequestIdRef = useRef(null); const activeStopPromiseRef = useRef | null>(null); const durationIntervalRef = useRef | null>(null); + const attemptGuardRef = useRef(new AttemptGuard()); const stopDurationTracking = useCallback(() => { if (durationIntervalRef.current) { @@ -510,6 +517,7 @@ export function useDictation(options: UseDictationOptions): UseDictationResult { isRecording: isRecordingRef.current, hasActiveStop: Boolean(activeStopPromiseRef.current), }); + attemptGuardRef.current.cancel(); if (!isRecordingRef.current && !activeStopPromiseRef.current) { console.log("[useDictation] cancelDictation ignored: nothing to cancel"); return; @@ -561,8 +569,11 @@ export function useDictation(options: UseDictationOptions): UseDictationResult { setRetryAttempt(0); setLastOutcome(null); + const attemptId = attemptGuardRef.current.next(); + try { const audioData = await stopRecorder(); + attemptGuardRef.current.assertCurrent(attemptId); const recordedDurationSeconds = durationRef.current; pendingAudioRef.current = buildCapturedAudioPayload(audioData, recordedDurationSeconds); setStatus("uploading"); @@ -571,6 +582,7 @@ export function useDictation(options: UseDictationOptions): UseDictationResult { setVolume(0); const transcription = await transmitDictation(); + attemptGuardRef.current.assertCurrent(attemptId); handleTranscriptionSuccess(transcription); } catch (err) { resetTranscriptionMutation(); @@ -661,14 +673,34 @@ export function useDictation(options: UseDictationOptions): UseDictationResult { const prevVisible = visibilityRef.current; visibilityRef.current = nextVisible; - if (prevVisible === true && nextVisible === false && isRecordingRef.current) { - cancelRef.current?.(); + if (prevVisible === true && nextVisible === false) { + attemptGuardRef.current.cancel(); + + if (isRecordingRef.current) { + cancelRef.current?.(); + return; + } + + if (isProcessingRef.current) { + stopDurationTracking(); + setDuration(0); + setIsProcessing(false); + setVolume(0); + setError(null); + setStatus("idle"); + setRetryAttempt(0); + setRetryInfo(null); + pendingAudioRef.current = null; + setFailedRecording(null); + setLastOutcome(null); + } } - }, [autoStopWhenHidden?.isVisible]); + }, [autoStopWhenHidden?.isVisible, stopDurationTracking]); useEffect(() => { return () => { + attemptGuardRef.current.cancel(); stopDurationTracking(); const activeStop = activeStopPromiseRef.current; if (activeStop) { diff --git a/packages/app/src/utils/attempt-guard.ts b/packages/app/src/utils/attempt-guard.ts new file mode 100644 index 000000000..020346a5f --- /dev/null +++ b/packages/app/src/utils/attempt-guard.ts @@ -0,0 +1,36 @@ +export class AttemptCancelledError extends Error { + constructor(message = "Attempt cancelled") { + super(message); + this.name = "AttemptCancelledError"; + } +} + +/** + * Tiny helper for async lifecycles: + * - Call `next()` to start a new attempt and invalidate previous ones + * - Call `cancel()` to invalidate any in-flight attempt + * - Call `assertCurrent(id)` after awaits to enforce a single active attempt + */ +export class AttemptGuard { + private attemptId = 0; + + public next(): number { + this.attemptId += 1; + return this.attemptId; + } + + public cancel(): void { + this.attemptId += 1; + } + + public isCurrent(id: number): boolean { + return id === this.attemptId; + } + + public assertCurrent(id: number): void { + if (!this.isCurrent(id)) { + throw new AttemptCancelledError(); + } + } +} +