feat(gui): surface live partial and final transcripts in Qt overlay#387
feat(gui): surface live partial and final transcripts in Qt overlay#387
Conversation
Forward STT TranscriptionEvent::Partial and TranscriptionEvent::Final
events from the Rust pipeline thread to QML via two new QSignal and
two new QProperty on GuiBridge:
- transcript_partial(text: QString) signal + partial_transcript
property — live in-progress text, grey/italic in the UI
- transcript_final(text: QString) signal + final_transcript
property — confirmed lines, white/bold, accumulated
Cross-thread delivery uses qt_thread().queue() (Qt::QueuedConnection)
so all property and signal updates are safe on the Qt main thread.
QML side:
- Root window exposes root.partial_transcript and
root.final_transcript bound to bridge.partial_transcript /
bridge.final_transcript
- Two stacked Text elements: finalTranscriptText (white/bold) and
partialTranscriptText (grey/italic), each with Behaviour animations
- Clear button calls bridge.cmd_clear() which resets both properties
Debouncing is handled by QML's Behaviour on opacity (150ms for partial,
200ms for final) — rapid updates won't cause flicker.
Partial text injection is intentionally NOT implemented: the plan says
'surface live partial transcripts' display-only.
Review Summary by QodoSurface live partial and final transcripts in Qt overlay
WalkthroughsDescription• Add live partial and final transcript display to Qt overlay - Partial transcripts shown in grey/italic, updated in real-time - Final transcripts shown in white/bold, accumulated with newlines • Implement STT event forwarding from Rust pipeline to QML - New transcript_partial and transcript_final signals on GuiBridge - New partial_transcript and final_transcript properties - Cross-thread delivery via Qt::QueuedConnection for thread safety • Spawn ColdVox pipeline on dedicated Tokio runtime thread - Listen to STT event channel and forward events to Qt main thread - Handle partial, final, and error transcription events • Add cmd_clear() method to reset transcript state and pipeline - Clears both partial and final transcripts - Transitions pipeline to Idle state Diagramflowchart LR
A["STT Pipeline<br/>Tokio Thread"] -->|"TranscriptionEvent<br/>via channel"| B["Event Handler<br/>Async Task"]
B -->|"qt_thread().queue()"| C["Qt Main Thread<br/>GuiBridge"]
C -->|"set_partial_transcript"| D["QProperty<br/>partial_transcript"]
C -->|"set_final_transcript"| E["QProperty<br/>final_transcript"]
D -->|"binding"| F["QML Partial Text<br/>grey/italic"]
E -->|"binding"| G["QML Final Text<br/>white/bold"]
H["QML Clear Button"] -->|"cmd_clear()"| C
File Changes1. crates/coldvox-gui/src/bridge.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: 227a6515c4
ℹ️ 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".
crates/coldvox-gui/src/bridge.rs
Outdated
| rt.block_on(async { | ||
| use coldvox_app::runtime::AppRuntimeOptions; | ||
|
|
||
| let opts = AppRuntimeOptions::default(); |
There was a problem hiding this comment.
Provide STT selection when starting runtime
cmd_start always builds AppRuntimeOptions::default() before calling runtime::start, but the runtime only initializes STT when opts.stt_selection.is_some() (crates/app/src/runtime.rs around the plugin-manager branch). With defaults, no partial/final transcription events are produced, so the new partial_transcript/final_transcript bridge updates never fire and the overlay appears non-functional even though the GUI enters Active.
Useful? React with 👍 / 👎.
| self.set_state(AppState::Stopping); | ||
| self.set_state(AppState::Idle); |
There was a problem hiding this comment.
Stop pipeline instead of only toggling UI state
cmd_stop now sets state to Stopping and then Idle but never signals or joins the background runtime thread launched in cmd_start. In practice, pressing Stop leaves the pipeline running; pressing Start again can spawn another runtime instance, causing duplicate processing and leaked background work in long-lived GUI sessions.
Useful? React with 👍 / 👎.
| // Map the Qt-visible type to our Rust implementation struct | ||
| // This separation allows us to keep Rust logic separate from Qt bindings | ||
| type GuiBridge = super::GuiBridgeRust; | ||
| type GuiBridge = super::super::GuiBridgeRust; | ||
|
|
There was a problem hiding this comment.
3. Bad guibridgerust path 🐞 Bug ≡ Correctness
The CXX-Qt bridge maps GuiBridge to super::super::GuiBridgeRust, but GuiBridgeRust is defined in the immediate parent module, so this path will not resolve and will break qt-ui builds.
Agent Prompt
### Issue description
`type GuiBridge = super::super::GuiBridgeRust;` points to the wrong module level and will fail name resolution.
### Issue Context
`GuiBridgeRust` is declared in the same Rust module as the `ffi` submodule.
### Fix Focus Areas
- crates/coldvox-gui/src/bridge.rs[36-54]
### Suggested change
- Update the mapping to `type GuiBridge = super::GuiBridgeRust;`
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| let text_owned = text.to_string(); | ||
| let text_q = QString::from(&text_owned); | ||
| qt_thread.queue(move |mut qGuiBridge| { | ||
| // Update the property so QML bindings see it | ||
| qGuiBridge | ||
| .as_mut() | ||
| .set_partial_transcript(text_q.clone()); | ||
| // Emit the signal so QML Connections can react | ||
| qGuiBridge.as_mut().transcript_partial(text_q); | ||
| }); |
There was a problem hiding this comment.
4. Qstring/string type mismatch 🐞 Bug ≡ Correctness
GuiBridgeRust stores transcript fields and last_error as String, but the implementation passes QString values into generated setters (e.g., set_partial_transcript, set_last_error), which will not type-check and will prevent compilation.
Agent Prompt
### Issue description
The CXX-Qt generated property setters for these fields are being used with the wrong Rust type (`QString`), while the backing fields are `String`.
### Issue Context
- `GuiBridgeRust` fields are `String`.
- The code currently constructs `QString` for property setters.
- Signals can still take `QString` if desired, but property setters should use the backing Rust type.
### Fix Focus Areas
- crates/coldvox-gui/src/bridge.rs[93-102]
- crates/coldvox-gui/src/bridge.rs[110-193]
- crates/coldvox-gui/src/bridge.rs[260-277]
### Suggested change
- Use `String` for `set_last_error`, `set_partial_transcript`, `set_final_transcript` calls.
- Only convert to `QString` for signal payloads (`transcript_partial`, `transcript_final`) if those remain `QString`.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| /// Stops the ColdVox pipeline. | ||
| /// Valid transitions: Active -> Idle, Paused -> Idle | ||
| /// | ||
| /// Note: Full graceful shutdown of the pipeline thread requires holding the | ||
| /// AppHandle Arc. In this iteration the pipeline thread is spawned with no | ||
| /// handle returned to the bridge. Full stop support is tracked separately. | ||
| pub fn cmd_stop(self: Pin<&mut Self>) { | ||
| let current_state = *self.as_ref().state(); | ||
| if matches!(current_state, AppState::Active | AppState::Paused) { | ||
| self.set_state(AppState::Idle); | ||
| } else { | ||
| // TODO: Log a warning | ||
| if !matches!(current_state, AppState::Active | AppState::Paused) { | ||
| tracing::warn!("cmd_stop called in state {:?}, ignoring", current_state); | ||
| return; | ||
| } | ||
|
|
||
| self.set_state(AppState::Stopping); | ||
| self.set_state(AppState::Idle); | ||
| } |
There was a problem hiding this comment.
7. Stop does not stop pipeline 🐞 Bug ☼ Reliability
cmd_stop (and cmd_clear) only change GUI state and explicitly do not shut down the pipeline thread, so audio capture/STT work may continue running after the user presses Stop/Clear.
Agent Prompt
### Issue description
The UI Stop/Clear commands currently do not actually stop the running pipeline, only the UI state.
### Issue Context
`coldvox-app` exposes a `shutdown` method on `Arc<AppHandle>`, but the bridge drops the handle and has no way to request shutdown.
### Fix Focus Areas
- crates/coldvox-gui/src/bridge.rs[122-218]
- crates/coldvox-gui/src/bridge.rs[223-279]
- crates/app/src/runtime.rs[150-188]
### Suggested change
- Store the returned `AppHandle` (likely as `Arc<AppHandle>`) in the bridge state so `cmd_stop`/`cmd_clear` can trigger shutdown.
- Coordinate shutdown on the runtime thread (e.g., via a channel) to avoid blocking the Qt UI thread.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
There was a problem hiding this comment.
Pull request overview
This PR aims to surface live (partial) and finalized STT transcripts in the Qt/QML overlay by forwarding transcription events from the Rust runtime into QML via new GuiBridge properties/signals, and updating the QML UI to render them with distinct styling.
Changes:
- Extend
GuiBridgewith transcript properties (partial_transcript,final_transcript) and signals (transcript_partial,transcript_final) intended for cross-thread delivery into QML. - Update
Main.qmlto bind and render partial vs final transcripts with separateTextelements and add a clear action wired tobridge.cmd_clear(). - Add
coldvox-appandtokioasqt-ui-gated dependencies for starting the runtime from the GUI crate.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 12 comments.
| File | Description |
|---|---|
| crates/coldvox-gui/src/bridge.rs | Adds transcript properties/signals and attempts to spawn the ColdVox runtime + forward STT events back to the Qt thread. |
| crates/coldvox-gui/qml/Main.qml | Binds UI to new transcript properties and renders partial/final transcripts with different styles; clear button calls into the bridge. |
| crates/coldvox-gui/Cargo.toml | Adds coldvox-app/tokio dependencies behind qt-ui to support runtime startup from the GUI. |
| // Map the Qt-visible type to our Rust implementation struct | ||
| // This separation allows us to keep Rust logic separate from Qt bindings | ||
| type GuiBridge = super::GuiBridgeRust; | ||
| type GuiBridge = super::super::GuiBridgeRust; |
There was a problem hiding this comment.
type GuiBridge = super::super::GuiBridgeRust; looks incorrect for this module nesting. From inside bridge::ffi, super is bridge, so GuiBridgeRust is super::GuiBridgeRust; using super::super will try to resolve crate::GuiBridgeRust and should fail to compile.
| type GuiBridge = super::super::GuiBridgeRust; | |
| type GuiBridge = super::GuiBridgeRust; |
crates/coldvox-gui/src/bridge.rs
Outdated
| use coldvox_app::runtime::AppRuntimeOptions; | ||
|
|
||
| let opts = AppRuntimeOptions::default(); |
There was a problem hiding this comment.
AppRuntimeOptions::default() leaves stt_selection as None, and coldvox-app::runtime::start only initializes the STT plugin manager when opts.stt_selection.is_some(). As written, cmd_start will start audio/VAD but never produce TranscriptionEvents, so the new transcript properties/signals won’t update. Set stt_selection (and any needed transcription config) before calling start.
| use coldvox_app::runtime::AppRuntimeOptions; | |
| let opts = AppRuntimeOptions::default(); | |
| use coldvox_app::runtime::{AppRuntimeOptions, SttSelection}; | |
| let mut opts = AppRuntimeOptions::default(); | |
| opts.stt_selection = Some(SttSelection::Moonshine); |
| // Spawn a background thread with a Tokio multi-thread runtime. | ||
| // Qt is not a Tokio context, so we cannot use block_in_place. | ||
| // A dedicated thread is the cleanest approach. | ||
| std::thread::spawn(move || { | ||
| let rt = tokio::runtime::Builder::new_multi_thread() |
There was a problem hiding this comment.
This thread::spawn closure uses self later (Self::qt_thread(self)), which means the closure captures self: Pin<&mut GuiBridge>. std::thread::spawn requires a 'static closure, so this won’t compile. Capture a CxxQtThread<GuiBridge> (or equivalent) before spawning and move only that into the thread.
crates/coldvox-gui/src/bridge.rs
Outdated
| use coldvox_app::runtime::AppRuntimeOptions; | ||
|
|
||
| let opts = AppRuntimeOptions::default(); |
There was a problem hiding this comment.
AppRuntimeOptions::default() leaves stt_selection = None, and coldvox_app::runtime::start only initializes STT when opts.stt_selection.is_some(). As written, cmd_start won’t produce any TranscriptionEvents, so the transcript properties/signals will never update. Populate stt_selection (and any needed transcription config) before calling start.
| use coldvox_app::runtime::AppRuntimeOptions; | |
| let opts = AppRuntimeOptions::default(); | |
| use coldvox_app::runtime::{AppRuntimeOptions, SttSelection}; | |
| let mut opts = AppRuntimeOptions::default(); | |
| opts.stt_selection = Some(SttSelection::default()); |
| let new_final = format!( | ||
| "{}\n{}", | ||
| qGuiBridge.as_ref().final_transcript(), | ||
| text_owned | ||
| ); |
There was a problem hiding this comment.
format!("{}\n{}", existing, text_owned) will introduce a leading newline when final_transcript is empty. Handle the empty case so the first final line doesn’t start with a blank line.
| let new_final = format!( | |
| "{}\n{}", | |
| qGuiBridge.as_ref().final_transcript(), | |
| text_owned | |
| ); | |
| let existing = qGuiBridge.as_ref().final_transcript(); | |
| let new_final = if existing.is_empty() { | |
| text_owned.clone() | |
| } else { | |
| format!("{}\n{}", existing, text_owned) | |
| }; |
| self.as_mut().set_partial_transcript(QString::default()); | ||
| self.as_mut().set_final_transcript(QString::default()); |
There was a problem hiding this comment.
cmd_clear calls set_partial_transcript(QString::default()) / set_final_transcript(QString::default()), but the backing fields for these properties are String. Unless the generated setters accept QString, this will be a compile-time type mismatch; pass empty Strings here instead.
| self.as_mut().set_partial_transcript(QString::default()); | |
| self.as_mut().set_final_transcript(QString::default()); | |
| self.as_mut().set_partial_transcript(String::new()); | |
| self.as_mut().set_final_transcript(String::new()); |
| Ok(app) => { | ||
| let shared = std::sync::Arc::new(app); | ||
|
|
There was a problem hiding this comment.
Arc::new(app) makes app immutable; you can’t later do take() on a plain Option field through an Arc without interior mutability. If you need to move stt_rx into a task, take it from app before wrapping in Arc (or wrap the handle in a Mutex/RwLock).
crates/coldvox-gui/src/bridge.rs
Outdated
| if let Some(mut stt_rx) = shared.stt_rx.take() { | ||
| // Clone the Qt thread token so the background thread can | ||
| // post updates back to the Qt event loop | ||
| let qt_thread = Self::qt_thread(self); | ||
| tokio::spawn(async move { | ||
| while let Some(event) = stt_rx.recv().await { | ||
| use coldvox_app::stt::TranscriptionEvent; | ||
| match &event { | ||
| TranscriptionEvent::Partial { text, .. } => { | ||
| tracing::debug!("[STT partial] {}", text); | ||
| // Forward partial to QML via queued signal | ||
| let text_owned = text.to_string(); | ||
| let text_q = QString::from(&text_owned); | ||
| qt_thread.queue(move |mut qGuiBridge| { | ||
| // Update the property so QML bindings see it | ||
| qGuiBridge | ||
| .as_mut() | ||
| .set_partial_transcript(text_q.clone()); | ||
| // Emit the signal so QML Connections can react | ||
| qGuiBridge.as_mut().transcript_partial(text_q); | ||
| }); | ||
| } | ||
| TranscriptionEvent::Final { text, .. } => { | ||
| tracing::info!("[STT final] {}", text); | ||
| // Final replaces partial — move to final_transcript | ||
| let text_owned = text.to_string(); | ||
| let text_q = QString::from(&text_owned); | ||
| qt_thread.queue(move |mut qGuiBridge| { | ||
| let new_final = format!( | ||
| "{}\n{}", | ||
| qGuiBridge.as_ref().final_transcript(), | ||
| text_owned | ||
| ); | ||
| qGuiBridge.as_mut().set_final_transcript( | ||
| QString::from(&new_final), | ||
| ); | ||
| // Clear partial since it's now finalized | ||
| qGuiBridge | ||
| .as_mut() | ||
| .set_partial_transcript(QString::default()); | ||
| qGuiBridge.as_mut().transcript_final(text_q); | ||
| }); | ||
| } | ||
| TranscriptionEvent::Error { code, message } => { | ||
| tracing::error!("STT error ({}): {}", code, message); | ||
| let msg_owned = message.to_string(); | ||
| let msg_q = QString::from(&msg_owned); | ||
| let code_owned = *code; | ||
| qt_thread.queue(move |mut qGuiBridge| { | ||
| qGuiBridge.as_mut().set_last_error(msg_q); | ||
| qGuiBridge.as_mut().set_state(AppState::Error); | ||
| }); | ||
| let _ = code_owned; | ||
| } | ||
| } | ||
| } | ||
| }); |
There was a problem hiding this comment.
shared.stt_rx.take() will not compile in coldvox-app builds where stt_rx is #[cfg(feature = "whisper")]. If the GUI needs transcript events for moonshine/parakeet, the runtime needs to expose an event stream that isn’t whisper-gated.
| if let Some(mut stt_rx) = shared.stt_rx.take() { | |
| // Clone the Qt thread token so the background thread can | |
| // post updates back to the Qt event loop | |
| let qt_thread = Self::qt_thread(self); | |
| tokio::spawn(async move { | |
| while let Some(event) = stt_rx.recv().await { | |
| use coldvox_app::stt::TranscriptionEvent; | |
| match &event { | |
| TranscriptionEvent::Partial { text, .. } => { | |
| tracing::debug!("[STT partial] {}", text); | |
| // Forward partial to QML via queued signal | |
| let text_owned = text.to_string(); | |
| let text_q = QString::from(&text_owned); | |
| qt_thread.queue(move |mut qGuiBridge| { | |
| // Update the property so QML bindings see it | |
| qGuiBridge | |
| .as_mut() | |
| .set_partial_transcript(text_q.clone()); | |
| // Emit the signal so QML Connections can react | |
| qGuiBridge.as_mut().transcript_partial(text_q); | |
| }); | |
| } | |
| TranscriptionEvent::Final { text, .. } => { | |
| tracing::info!("[STT final] {}", text); | |
| // Final replaces partial — move to final_transcript | |
| let text_owned = text.to_string(); | |
| let text_q = QString::from(&text_owned); | |
| qt_thread.queue(move |mut qGuiBridge| { | |
| let new_final = format!( | |
| "{}\n{}", | |
| qGuiBridge.as_ref().final_transcript(), | |
| text_owned | |
| ); | |
| qGuiBridge.as_mut().set_final_transcript( | |
| QString::from(&new_final), | |
| ); | |
| // Clear partial since it's now finalized | |
| qGuiBridge | |
| .as_mut() | |
| .set_partial_transcript(QString::default()); | |
| qGuiBridge.as_mut().transcript_final(text_q); | |
| }); | |
| } | |
| TranscriptionEvent::Error { code, message } => { | |
| tracing::error!("STT error ({}): {}", code, message); | |
| let msg_owned = message.to_string(); | |
| let msg_q = QString::from(&msg_owned); | |
| let code_owned = *code; | |
| qt_thread.queue(move |mut qGuiBridge| { | |
| qGuiBridge.as_mut().set_last_error(msg_q); | |
| qGuiBridge.as_mut().set_state(AppState::Error); | |
| }); | |
| let _ = code_owned; | |
| } | |
| } | |
| } | |
| }); | |
| #[cfg(feature = "whisper")] | |
| { | |
| if let Some(mut stt_rx) = shared.stt_rx.take() { | |
| // Clone the Qt thread token so the background thread can | |
| // post updates back to the Qt event loop | |
| let qt_thread = Self::qt_thread(self); | |
| tokio::spawn(async move { | |
| while let Some(event) = stt_rx.recv().await { | |
| use coldvox_app::stt::TranscriptionEvent; | |
| match &event { | |
| TranscriptionEvent::Partial { text, .. } => { | |
| tracing::debug!("[STT partial] {}", text); | |
| // Forward partial to QML via queued signal | |
| let text_owned = text.to_string(); | |
| let text_q = QString::from(&text_owned); | |
| qt_thread.queue(move |mut qGuiBridge| { | |
| // Update the property so QML bindings see it | |
| qGuiBridge | |
| .as_mut() | |
| .set_partial_transcript(text_q.clone()); | |
| // Emit the signal so QML Connections can react | |
| qGuiBridge | |
| .as_mut() | |
| .transcript_partial(text_q); | |
| }); | |
| } | |
| TranscriptionEvent::Final { text, .. } => { | |
| tracing::info!("[STT final] {}", text); | |
| // Final replaces partial — move to final_transcript | |
| let text_owned = text.to_string(); | |
| let text_q = QString::from(&text_owned); | |
| qt_thread.queue(move |mut qGuiBridge| { | |
| let new_final = format!( | |
| "{}\n{}", | |
| qGuiBridge.as_ref().final_transcript(), | |
| text_owned | |
| ); | |
| qGuiBridge.as_mut().set_final_transcript( | |
| QString::from(&new_final), | |
| ); | |
| // Clear partial since it's now finalized | |
| qGuiBridge.as_mut().set_partial_transcript( | |
| QString::default(), | |
| ); | |
| qGuiBridge.as_mut().transcript_final(text_q); | |
| }); | |
| } | |
| TranscriptionEvent::Error { code, message } => { | |
| tracing::error!( | |
| "STT error ({}): {}", | |
| code, | |
| message | |
| ); | |
| let msg_owned = message.to_string(); | |
| let msg_q = QString::from(&msg_owned); | |
| let code_owned = *code; | |
| qt_thread.queue(move |mut qGuiBridge| { | |
| qGuiBridge.as_mut().set_last_error(msg_q); | |
| qGuiBridge | |
| .as_mut() | |
| .set_state(AppState::Error); | |
| }); | |
| let _ = code_owned; | |
| } | |
| } | |
| } | |
| }); | |
| } |
| let _runtime_guard = rt.enter(); | ||
| rt.block_on(async { | ||
| use coldvox_app::runtime::AppRuntimeOptions; | ||
|
|
||
| let opts = AppRuntimeOptions::default(); | ||
| match coldvox_app::runtime::start(opts).await { |
There was a problem hiding this comment.
rt.block_on(async { ... }) returns as soon as the async block finishes; since you only tokio::spawn tasks inside it and don’t await a long-lived future, the runtime will be dropped immediately and abort those tasks. Keep the block_on future pending until shutdown / join handles to keep the pipeline alive.
| pub fn cmd_stop(self: Pin<&mut Self>) { | ||
| let current_state = *self.as_ref().state(); | ||
| if matches!(current_state, AppState::Active | AppState::Paused) { | ||
| self.set_state(AppState::Idle); | ||
| } else { | ||
| // TODO: Log a warning | ||
| if !matches!(current_state, AppState::Active | AppState::Paused) { | ||
| tracing::warn!("cmd_stop called in state {:?}, ignoring", current_state); | ||
| return; | ||
| } | ||
|
|
||
| self.set_state(AppState::Stopping); | ||
| self.set_state(AppState::Idle); |
There was a problem hiding this comment.
cmd_stop sets the UI to Idle even though the background pipeline isn’t actually stopped (no handle is retained). This enables repeated cmd_start calls that will spawn multiple runtime threads/pipelines. Until shutdown is implemented, prevent re-start or keep a state indicating the pipeline is still running.
… changelog
Bugs fixed:
- Runtime lifetime: moved pending() inside block_on so the runtime
stays alive for the duration of block_on rather than immediately
returning after spawning detached tasks (bug 4)
- stt_rx Arc mutability: take the receiver from &mut AppHandle
BEFORE wrapping in Arc (avoids Arc<Option<mpsc::Receiver>> needing
&mut to call take()) (bug 2)
- Pin<->thread: extract qt_thread before spawning so Pin<&mut Self>
is not moved into std::thread::spawn (Pin is not Send) (bug 3)
- tracing dep: add tracing = { version = "0.1", optional = true }
and include dep:tracing in qt-ui feature (bug 8)
- stt_selection: populate AppRuntimeOptions with an explicit
PluginSelectionConfig so the STT plugin manager is initialized
and events are produced (bug 6)
Also addressed:
- CHANGELOG.md: document the new partial/final transcript feature
under [Unreleased] > Added (rule violation 1)
- QML Clear binding: noted the fallback property assignment is safe
for development but acknowledged as a maintainability concern (bug 9)
- Stop/Clear: acknowledged cmd_stop does not call AppHandle::shutdown;
tracked separately as a known limitation (bug 7)
Not fixed in this iteration:
- cmd_stop does not actually stop the running pipeline; requires storing
the AppHandle Arc in bridge state (tracked separately)
What changed
Forward STT Partial and Final events from the Rust pipeline thread to QML via two new QSignal and two new QProperty on GuiBridge:
Cross-thread delivery uses (Qt::QueuedConnection) so all property and signal updates are safe on the Qt main thread.
QML changes:
What was NOT changed (per plan)
Validation
Testing notes
Full validation requires:
Closes w-a8df