diff --git a/desktop/scripts/check-file-sizes.mjs b/desktop/scripts/check-file-sizes.mjs index 166b1dc77..ea0e42109 100644 --- a/desktop/scripts/check-file-sizes.mjs +++ b/desktop/scripts/check-file-sizes.mjs @@ -339,7 +339,10 @@ const overrides = new Map([ // +3: onLinkShortcutRef wiring (ref decl + editor option + assignment) for // the ⌘K link-editor shortcut, mirroring the existing onEditLinkRef // pattern. Queued to split with the rest of this list. - ["src/features/messages/ui/MessageComposer.tsx", 1036], + // +15 local-dictation integration: useLocalDictation wiring (mic toggle, + // transcript-append handler, recording/transcribing state, stop-on-send/edit + // guards). Load-bearing feature growth; queued to split with the rest. + ["src/features/messages/ui/MessageComposer.tsx", 1052], // global-agent-config: model-tuning section (BuzzAgentModelTuningFields via // EditAgentAdvancedFields) + providerValid gate + effectiveProvider derivation // + globalProvider threading into getPersonaProviderOptions. All load-bearing diff --git a/desktop/src/features/dictation/hooks/useLocalDictation.ts b/desktop/src/features/dictation/hooks/useLocalDictation.ts index f4e8efca7..0f8394551 100644 --- a/desktop/src/features/dictation/hooks/useLocalDictation.ts +++ b/desktop/src/features/dictation/hooks/useLocalDictation.ts @@ -84,6 +84,10 @@ export function useLocalDictation({ const workletRef = useRef(null); const batchTimerRef = useRef | null>(null); const audioBatchRef = useRef([]); + // Tail of the serialized flush chain. Each flush appends its IPC work to + // this promise so flushes run strictly one-after-another; awaiting the tail + // guarantees every already-started flush's chunks are enqueued native-side. + const flushChainRef = useRef>(Promise.resolve()); const unlistenTranscriptRef = useRef(null); const unlistenStateRef = useRef(null); const onRecordingStartRef = useRef(onRecordingStart); @@ -137,10 +141,17 @@ export function useLocalDictation({ }, []); /** Flush accumulated audio batch to the native STT engine. Returns a promise - * that resolves once the IPC call completes (or immediately if nothing to flush). */ + * that resolves once the IPC call completes (or immediately if nothing to flush). + * + * Flushes are serialized through `flushChainRef`: the batch is drained + * synchronously here (so each flush owns a distinct set of samples in FIFO + * order), but the actual IPC send is chained after any prior in-flight + * flush. This keeps chunks strictly ordered and lets `stopRecording`/ + * `cleanup` await the chain tail so an earlier timer-tick flush can't still + * be enqueuing chunks when `stop_dictation` fires. */ const flushAudioBatch = useCallback((): Promise => { const batch = audioBatchRef.current; - if (batch.length === 0) return Promise.resolve(); + if (batch.length === 0) return flushChainRef.current; // Tag this flush with the session that owns the buffered audio. Captured // once here so every chunk of this batch carries the same session; native @@ -148,7 +159,10 @@ export function useLocalDictation({ // flush from a just-stopped session can't leak into a newer session. const session = nativeSessionRef.current; - // Calculate total byte length and merge into a single buffer. + // Calculate total byte length and merge into a single buffer. Drain the + // batch synchronously so a concurrent timer tick can't grab the same + // samples — the merged buffer captured here is this flush's exclusive + // payload. let totalSamples = 0; for (const chunk of batch) { totalSamples += chunk.length; @@ -193,7 +207,12 @@ export function useLocalDictation({ await invokeRawBinary("push_dictation_audio", payload).catch(() => {}); } }; - return sendChunks(); + + // Chain this flush's IPC work after any prior in-flight flush so chunks + // stay strictly ordered and awaiting the tail drains everything. + const chained = flushChainRef.current.then(sendChunks); + flushChainRef.current = chained; + return chained; }, []); const cleanup = useCallback(() => { @@ -267,6 +286,9 @@ export function useLocalDictation({ // Reset any leftover audio buffer so a stale batch from a prior session // can't be flushed into this new session/draft. audioBatchRef.current = []; + // Reset the flush chain so this session's flushes don't chain behind a + // prior session's (already-settled) tail. + flushChainRef.current = Promise.resolve(); setIsStarting(true); onRecordingStartRef.current?.();