Conversation
Phase 3B: Replace demo seam with real runtime bindings. - Add 5 new Tauri commands: update_partial_transcript, update_final_transcript, set_overlay_processing, set_overlay_listening, stop_overlay_capture - State methods: apply_partial_transcript, apply_final_transcript, apply_processing_state, apply_listening_state, stop_capture - React bridge helpers with 80ms debounce on partials - Unit tests for all new state transitions - Demo driver preserved for UX shell testing
Review Summary by QodoWire STT pipeline commands to overlay shell with debounced partial updates
WalkthroughsDescription• Add 5 new Tauri commands for STT pipeline integration (partial/final transcript, processing/listening state transitions, capture stop) • Implement corresponding state methods with full transcript lifecycle management • Add 80ms debounce mechanism in React hook to prevent flooding UI with rapid partial updates • Include comprehensive unit tests for all new state transitions and cleanup logic Diagramflowchart LR
STT["STT Pipeline"] -->|"update_partial_transcript"| Partial["Partial Transcript<br/>Listening State"]
STT -->|"update_final_transcript"| Final["Final Transcript<br/>Ready State"]
STT -->|"set_overlay_processing"| Processing["Processing State"]
STT -->|"set_overlay_listening"| Listening["Listening State"]
STT -->|"stop_overlay_capture"| Idle["Idle State<br/>Cleared"]
Partial -->|"80ms debounce"| Hook["useOverlayShell Hook"]
Hook -->|"Bridge"| React["React UI"]
File Changes1. crates/coldvox-gui/src-tauri/src/lib.rs
|
Code Review by Qodo
1.
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 61107a982e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // Pipeline wiring — for real STT integration. | ||
| // queuePartialTranscript debounces rapid partials; flushPartial sends immediately. | ||
| queuePartialTranscript, | ||
| updateFinalTranscript: (text: string) => runCommand(() => updateFinalTranscript(text)), |
There was a problem hiding this comment.
Cancel pending partial flush before finalizing transcript
queuePartialTranscript schedules a delayed updatePartialTranscript call, but updateFinalTranscript is invoked directly without clearing that timer. If STT emits a final result within 80ms of the last partial (a common path), the queued partial is sent after the final and can overwrite the snapshot back to listening with stale partial text. This creates out-of-order state transitions and can hide the committed final transcript.
Useful? React with 👍 / 👎.
| self.snapshot.status = OverlayStatus::Listening; | ||
| self.snapshot.partial_transcript.clear(); | ||
| self.snapshot.final_transcript.clear(); |
There was a problem hiding this comment.
Clear error message when transitioning to listening
apply_listening_state updates status/text but leaves error_message untouched, so a previous command failure can persist as an error badge even after the overlay has successfully re-entered listening. Because the UI renders errorMessage whenever it is non-null, this produces a misleading mixed state (listening + old error) until another method explicitly clears it.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Pull request overview
Wires the GUI overlay shell to a new “real STT pipeline” command seam by adding Tauri commands + frontend bridge helpers, and introduces debounced partial transcript updates to reduce UI churn during rapid STT output.
Changes:
- Added 5 new Tauri commands and corresponding
OverlayModelstate transitions for partial/final transcript + lifecycle state changes. - Added frontend bridge helpers to invoke the new commands from React.
- Updated
useOverlayShellwith an 80ms debounce/flush path for partial transcripts and unmount cleanup.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| crates/coldvox-gui/src/lib/overlayBridge.ts | Adds new invoke helpers for partial/final transcript and overlay state transitions. |
| crates/coldvox-gui/src/hooks/useOverlayShell.ts | Adds debounced partial transcript queuing and exposes new pipeline wiring helpers. |
| crates/coldvox-gui/src/hooks/useOverlayShell.test.tsx | Extends bridge mocks for new pipeline wiring functions. |
| crates/coldvox-gui/src-tauri/src/state.rs | Adds OverlayModel methods for partial/final transcript updates and state transitions + some unit tests. |
| crates/coldvox-gui/src-tauri/src/lib.rs | Registers new Tauri commands and maps them to the new OverlayModel methods. |
| /// Feed a live partial transcript update from the STT pipeline. | ||
| export function updatePartialTranscript(text: string): Promise<OverlaySnapshot> { | ||
| return invoke<OverlaySnapshot>("update_partial_transcript", { text }); | ||
| } | ||
|
|
||
| /// Feed a final transcript from the STT pipeline. | ||
| export function updateFinalTranscript(text: string): Promise<OverlaySnapshot> { | ||
| return invoke<OverlaySnapshot>("update_final_transcript", { text }); | ||
| } | ||
|
|
||
| /// Transition the overlay to Processing state. | ||
| export function setOverlayProcessing(): Promise<OverlaySnapshot> { | ||
| return invoke<OverlaySnapshot>("set_overlay_processing"); | ||
| } | ||
|
|
||
| /// Transition the overlay to Listening state. | ||
| export function setOverlayListening(): Promise<OverlaySnapshot> { | ||
| return invoke<OverlaySnapshot>("set_overlay_listening"); | ||
| } | ||
|
|
||
| /// Stop real capture and return to Idle. |
There was a problem hiding this comment.
TypeScript /// lines here are triple-slash-style comments, which are typically reserved for TS reference directives and are not used elsewhere in this codebase. Please switch these to normal // comments (or remove them) to avoid tooling/lint confusion and keep style consistent.
| /// Feed a live partial transcript update from the STT pipeline. | |
| export function updatePartialTranscript(text: string): Promise<OverlaySnapshot> { | |
| return invoke<OverlaySnapshot>("update_partial_transcript", { text }); | |
| } | |
| /// Feed a final transcript from the STT pipeline. | |
| export function updateFinalTranscript(text: string): Promise<OverlaySnapshot> { | |
| return invoke<OverlaySnapshot>("update_final_transcript", { text }); | |
| } | |
| /// Transition the overlay to Processing state. | |
| export function setOverlayProcessing(): Promise<OverlaySnapshot> { | |
| return invoke<OverlaySnapshot>("set_overlay_processing"); | |
| } | |
| /// Transition the overlay to Listening state. | |
| export function setOverlayListening(): Promise<OverlaySnapshot> { | |
| return invoke<OverlaySnapshot>("set_overlay_listening"); | |
| } | |
| /// Stop real capture and return to Idle. | |
| // Feed a live partial transcript update from the STT pipeline. | |
| export function updatePartialTranscript(text: string): Promise<OverlaySnapshot> { | |
| return invoke<OverlaySnapshot>("update_partial_transcript", { text }); | |
| } | |
| // Feed a final transcript from the STT pipeline. | |
| export function updateFinalTranscript(text: string): Promise<OverlaySnapshot> { | |
| return invoke<OverlaySnapshot>("update_final_transcript", { text }); | |
| } | |
| // Transition the overlay to Processing state. | |
| export function setOverlayProcessing(): Promise<OverlaySnapshot> { | |
| return invoke<OverlaySnapshot>("set_overlay_processing"); | |
| } | |
| // Transition the overlay to Listening state. | |
| export function setOverlayListening(): Promise<OverlaySnapshot> { | |
| return invoke<OverlaySnapshot>("set_overlay_listening"); | |
| } | |
| // Stop real capture and return to Idle. |
| // Pipeline wiring — for real STT integration. | ||
| // queuePartialTranscript debounces rapid partials; flushPartial sends immediately. | ||
| queuePartialTranscript, | ||
| updateFinalTranscript: (text: string) => runCommand(() => updateFinalTranscript(text)), | ||
| setOverlayProcessing: () => runCommand(setOverlayProcessing), | ||
| setOverlayListening: () => runCommand(setOverlayListening), | ||
| stopOverlayCapture: () => runCommand(stopOverlayCapture), |
There was a problem hiding this comment.
The return block comment says “flushPartial sends immediately”, but flushPartial isn’t returned from the hook, so callers can’t actually flush immediately. Either export flushPartial (and/or a cancel function) alongside queuePartialTranscript, or adjust the comment/API so they match.
| // Debounce-flush partial transcript updates to avoid flooding the shell on rapid STT output. | ||
| const pendingPartialRef = useRef<string | null>(null); | ||
| const flushTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null); | ||
|
|
||
| const flushPartial = useCallback(() => { | ||
| const text = pendingPartialRef.current; | ||
| if (text === null) return; | ||
| pendingPartialRef.current = null; | ||
| if (flushTimerRef.current !== null) { | ||
| clearTimeout(flushTimerRef.current); | ||
| flushTimerRef.current = null; | ||
| } | ||
| void runCommand(() => updatePartialTranscript(text)); | ||
| }, [runCommand]); | ||
|
|
||
| const queuePartialTranscript = useCallback( | ||
| (text: string) => { | ||
| pendingPartialRef.current = text; | ||
| if (flushTimerRef.current !== null) { | ||
| clearTimeout(flushTimerRef.current); | ||
| } | ||
| // Flush after 80 ms of no new partials — balances latency vs. reduce repaints. | ||
| flushTimerRef.current = setTimeout(flushPartial, 80); | ||
| }, | ||
| [flushPartial], | ||
| ); |
There was a problem hiding this comment.
The new debounce/flush behavior for partial transcripts isn’t covered by tests in this file. Please add a test that verifies rapid queuePartialTranscript calls only invoke updatePartialTranscript once after ~80ms (with the latest text), and that unmount cleanup cancels a pending flush.
| if (flushTimerRef.current !== null) { | ||
| clearTimeout(flushTimerRef.current); | ||
| } | ||
| // Flush after 80 ms of no new partials — balances latency vs. reduce repaints. |
There was a problem hiding this comment.
Minor grammar nit: “balances latency vs. reduce repaints” reads ungrammatical. Consider rewording (e.g., “balancing latency vs. reducing repaints”) to keep inline docs clear.
| // Flush after 80 ms of no new partials — balances latency vs. reduce repaints. | |
| // Flush after 80 ms of no new partials — balancing latency vs. reducing repaints. |
| self.snapshot.status = OverlayStatus::Processing; | ||
| if let Some(detail) = status_detail { | ||
| self.snapshot.status_detail = detail.to_string(); | ||
| } else { | ||
| self.snapshot.status_detail = | ||
| "Processing the utterance into a committed transcript.".to_string(); | ||
| } |
There was a problem hiding this comment.
apply_processing_state sets status/status_detail but doesn’t clear error_message or reset paused. If the model was previously in Error or paused demo state, the UI can incorrectly keep showing a stale error/paused flag after transitioning to Processing. Recommend clearing error_message and forcing paused = false in this transition (similar to other state updates).
| self.snapshot.status = OverlayStatus::Processing; | |
| if let Some(detail) = status_detail { | |
| self.snapshot.status_detail = detail.to_string(); | |
| } else { | |
| self.snapshot.status_detail = | |
| "Processing the utterance into a committed transcript.".to_string(); | |
| } | |
| self.snapshot.status = OverlayStatus::Processing; | |
| self.snapshot.paused = false; | |
| if let Some(detail) = status_detail { | |
| self.snapshot.status_detail = detail.to_string(); | |
| } else { | |
| self.snapshot.status_detail = | |
| "Processing the utterance into a committed transcript.".to_string(); | |
| } | |
| self.snapshot.error_message = None; |
| /// Transition to Listening state (new utterance started). | ||
| pub fn apply_listening_state(&mut self, status_detail: Option<&str>) -> OverlaySnapshot { | ||
| self.snapshot.status = OverlayStatus::Listening; | ||
| self.snapshot.partial_transcript.clear(); | ||
| self.snapshot.final_transcript.clear(); | ||
| if let Some(detail) = status_detail { | ||
| self.snapshot.status_detail = detail.to_string(); | ||
| } else { | ||
| self.snapshot.status_detail = "Listening for speech.".to_string(); | ||
| } | ||
| self.snapshot.expanded = true; | ||
| self.snapshot() |
There was a problem hiding this comment.
apply_listening_state clears transcripts but doesn’t clear error_message or reset paused. That can leave the overlay showing an old error/paused state even though it’s listening again. Consider setting error_message = None and paused = false when entering Listening via the real pipeline.
| #[test] | ||
| fn apply_partial_transcript_updates_text_and_keeps_listening() { | ||
| let mut model = OverlayModel::default(); | ||
| model.apply_listening_state(None); | ||
|
|
||
| let snap1 = model.apply_partial_transcript("hello", None); | ||
| assert_eq!(snap1.partial_transcript, "hello"); | ||
| assert_eq!(snap1.status, OverlayStatus::Listening); | ||
| assert!(snap1.final_transcript.is_empty()); | ||
|
|
||
| let snap2 = model.apply_partial_transcript("hello world", None); | ||
| assert_eq!(snap2.partial_transcript, "hello world"); | ||
| assert_eq!(snap2.status, OverlayStatus::Listening); | ||
| } | ||
|
|
||
| #[test] | ||
| fn apply_final_transcript_moves_partial_to_final_and_transitions_to_ready() { | ||
| let mut model = OverlayModel::default(); | ||
| model.apply_listening_state(None); | ||
| model.apply_partial_transcript("hello world", None); | ||
|
|
||
| let snap = model.apply_final_transcript("hello world", None); | ||
| assert!(snap.partial_transcript.is_empty()); | ||
| assert_eq!(snap.final_transcript, "hello world"); | ||
| assert_eq!(snap.status, OverlayStatus::Ready); | ||
| } | ||
|
|
||
| #[test] | ||
| fn stop_capture_clears_all_transcript_state() { | ||
| let mut model = OverlayModel::default(); | ||
| model.apply_listening_state(None); | ||
| model.apply_partial_transcript("partial text", None); | ||
|
|
||
| let snap = model.stop_capture(); | ||
| assert_eq!(snap.status, OverlayStatus::Idle); | ||
| assert!(snap.partial_transcript.is_empty()); | ||
| assert!(snap.final_transcript.is_empty()); | ||
| assert!(snap.error_message.is_none()); | ||
| } |
There was a problem hiding this comment.
PR description says the five new OverlayModel pipeline methods “all have unit tests”, but the added tests only cover partial/final/stop_capture; there’s no unit test for apply_processing_state or apply_listening_state. Either add coverage for those transitions or update the PR description to match reality.
…ions) Bugs fixed: - stop_capture now increments demo_token so an in-flight demo driver thread exits its loop instead of overwriting the Idle snapshot - Pipeline state transitions (apply_partial/final/processing/listening) reset the paused flag so demo pause state cannot leak into real capture sessions - queuePartialTranscript timer is cancelled before any non-partial transition (updateFinalTranscript, setOverlayProcessing, setOverlayListening, stopOverlayCapture) to prevent a delayed 80ms flush from overwriting a status change Rule violations addressed: - Added CHANGELOG.md entry for the Phase 3C STT pipeline command wiring - Added two unit tests: stop_capture_bumps_demo_token_and_stops_demo_driver and pipeline_transitions_reset_paused_flag
CI Feedback 🧐(Feedback updated until commit 4ce8476)A test triggered by this PR failed. Here is an AI-generated analysis of the failure:
|
…-3c-partial-transcript
What Changed
Phase 3B: Replace the demo seam with real runtime bindings from the existing Rust pipeline crates.
Rust backend (lib.rs + state.rs)
Added 5 new Tauri commands that map directly onto the STT pipeline lifecycle:
Corresponding methods added: , , , , . All have unit tests.
React bridge (overlayBridge.ts)
Exported 5 new bridge helpers mirroring the Tauri commands. is the hot path called on every STT partial.
Hook (useOverlayShell.ts)
— debounces rapid partials with an 80ms flush timer to avoid flooding the shell on fast STT output. Returns a for cleanup on unmount.
Returns the new pipeline wiring functions alongside the existing demo commands:
Validation
Context
Follows the plan in Phase 3B action items:
Next step: Connect the actual coldvox-audio / coldvox-stt event streams to these commands from the Rust host shell (not in this tranche — that requires deeper pipeline integration).
Task: w-2b39