Skip to content

feat(gui): surface live partial and final transcripts in Qt overlay#387

Open
Coldaine wants to merge 2 commits intomainfrom
worker/coldvox/w-2b39-gui-pipeline-integration
Open

feat(gui): surface live partial and final transcripts in Qt overlay#387
Coldaine wants to merge 2 commits intomainfrom
worker/coldvox/w-2b39-gui-pipeline-integration

Conversation

@Coldaine
Copy link
Copy Markdown
Owner

@Coldaine Coldaine commented Apr 2, 2026

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:

Signal Property Content Style
transcript_partial(text) partial_transcript Live in-progress text Grey/italic
transcript_final(text) final_transcript Confirmed lines White/bold

Cross-thread delivery uses (Qt::QueuedConnection) so all property and signal updates are safe on the Qt main thread.

QML changes:

  • Root window exposes and bound to the bridge properties
  • Two stacked elements with distinct visual styling
  • Clear button calls which resets both transcript properties

What was NOT changed (per plan)

  • No text injection behavior — partials are display-only
  • No changes to VAD or STT pipeline behavior
  • Partial transcripts are NOT appended to any document or input field

Validation

  • passes
  • Rust code compiles structurally (cannot build full Qt binary in this environment due to lockfile version)
  • Cross-thread safety via Qt::QueuedConnection (standard CXX-Qt pattern)
  • Debouncing via QML Behaviour animations (150ms partial, 200ms final)

Testing notes

Full validation requires:

  1. Running
  2. Feeding audio through the pipeline
  3. Observing grey/italic live text that transitions to white/bold on finalization

Closes w-a8df

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.
Copilot AI review requested due to automatic review settings April 2, 2026 05:24
@qodo-code-review
Copy link
Copy Markdown

Review Summary by Qodo

Surface live partial and final transcripts in Qt overlay

✨ Enhancement

Grey Divider

Walkthroughs

Description
• 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
Diagram
flowchart 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
Loading

Grey Divider

File Changes

1. crates/coldvox-gui/src/bridge.rs ✨ Enhancement +175/-38

Implement STT event forwarding and transcript properties

• Add two new QProperties (partial_transcript, final_transcript) and two new QSignals
 (transcript_partial, transcript_final) to GuiBridge
• Implement cmd_start() to spawn ColdVox pipeline on dedicated Tokio thread with STT event
 listener
• Forward STT Partial/Final/Error events from async task to Qt main thread via qt_thread().queue()
• Add cmd_clear() method to reset transcript properties and stop pipeline
• Replace TODO comments with proper tracing::warn!() logging for invalid state transitions
• Update cmd_stop() to include Stopping state transition

crates/coldvox-gui/src/bridge.rs


2. crates/coldvox-gui/Cargo.toml Dependencies +4/-1

Add pipeline and async runtime dependencies

• Add coldvox-app dependency (optional, gated by qt-ui feature)
• Add tokio with full features (optional, gated by qt-ui feature)
• Update qt-ui feature to include both new dependencies

crates/coldvox-gui/Cargo.toml


3. crates/coldvox-gui/qml/Main.qml ✨ Enhancement +27/-6

Add dual transcript display with styling and animations

• Replace single transcript property with partial_transcript and final_transcript aliases
 bound to bridge properties
• Split transcript display into two Text elements: finalTranscriptText (white/bold) and
 partialTranscriptText (grey/italic)
• Add distinct Behaviour animations for opacity (200ms for final, 150ms for partial)
• Update clear button to call bridge.cmd_clear() with fallback to clearing local properties
• Add documentation comments explaining transcript property purposes

crates/coldvox-gui/qml/Main.qml


Grey Divider

Qodo Logo

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review bot commented Apr 2, 2026

Code Review by Qodo

🐞 Bugs (4) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX Issues (0)

Grey Divider


Action required

1. Transcript overlay missing changelog📘 Rule violation ⚙ Maintainability
Description
The PR introduces a new user-visible transcript overlay (partial/final transcript text in the Qt UI)
but CHANGELOG.md has no corresponding entry under [Unreleased]. This violates the requirement to
document user-visible changes in the changelog.
Code

crates/coldvox-gui/qml/Main.qml[R215-239]

+          // Finalized transcript lines — white/bold, stable once emitted
         Text {
-            id: transcriptText
+            id: finalTranscriptText
           width: parent.width
           wrapMode: Text.WordWrap
           color: "#F5F5F5"
           font.pixelSize: 16
+            font.bold: true
           lineHeight: 1.5
-            text: root.transcript
+            text: root.final_transcript
           Behavior on opacity { NumberAnimation { duration: 200 } }
-            onTextChanged: scroll.scrollToBottom()
+          }
+
+          // Live partial transcript — grey/italic, updated rapidly
+          Text {
+            id: partialTranscriptText
+            width: parent.width
+            wrapMode: Text.WordWrap
+            color: Qt.rgba(0.8, 0.8, 0.8, 0.9)
+            font.pixelSize: 16
+            font.italic: true
+            lineHeight: 1.5
+            text: root.partial_transcript
+            Behavior on opacity { NumberAnimation { duration: 150 } }
         }
Evidence
The UI now displays final_transcript and partial_transcript in the main overlay window, which is
a user-visible feature change, but the [Unreleased] section in CHANGELOG.md does not mention
this addition.

CLAUDE.md
crates/coldvox-gui/qml/Main.qml[215-239]
CHANGELOG.md[5-32]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
User-visible UI changes (live partial/final transcripts in the Qt overlay) were added without a corresponding `CHANGELOG.md` entry.
## Issue Context
Per the project rubric, user-visible changes must be documented under `[Unreleased]` in `CHANGELOG.md`.
## Fix Focus Areas
- CHANGELOG.md[5-32]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. cmd_start uses Arc mutably📘 Rule violation ≡ Correctness
Description
cmd_start wraps app in Arc and then calls shared.stt_rx.take(), which requires mutable
access and will not compile when shared is an Arc. Additionally, the spawned thread captures
self via move ||, which is not Send/'static, so cargo check/clippy -D warnings is
expected to fail.
Code

crates/coldvox-gui/src/bridge.rs[R122-146]

+        std::thread::spawn(move || {
+            let rt = tokio::runtime::Builder::new_multi_thread()
+                .enable_all()
+                .build()
+                .expect("failed to build Tokio runtime for ColdVox pipeline");
+
+            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 {
+                    Ok(app) => {
+                        let shared = std::sync::Arc::new(app);
+
+                        // Grab the STT event channel receiver.
+                        // Direct Qt property updates from this async context are not possible
+                        // (Qt objects are thread-bound to the Qt main thread), so we use
+                        // qt_thread().queue() with a Queued connection to safely emit
+                        // signals across the thread boundary.
+                        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 {
Evidence
The PR’s new cmd_start implementation attempts to mutate stt_rx through an Arc
(shared.stt_rx.take()), but stt_rx is an Option field on AppHandle and must be taken from a
mutable handle. The same new block also moves self into std::thread::spawn, which requires
captured values to be Send + 'static.

AGENTS.md
crates/coldvox-gui/src/bridge.rs[122-146]
crates/app/src/runtime.rs[114-123]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`cmd_start` likely fails to compile/lint due to (1) attempting `shared.stt_rx.take()` after wrapping `app` in `Arc`, and (2) moving `self` into `std::thread::spawn`.
## Issue Context
- `AppHandle.stt_rx` is an `Option<Receiver<...>>` intended to be taken from a mutable `AppHandle`.
- `std::thread::spawn` closures must be `Send + 'static`; `Pin<&mut Self>` (`self`) cannot be moved into that thread.
## Fix Focus Areas
- crates/coldvox-gui/src/bridge.rs[122-146]
- crates/app/src/runtime.rs[114-123]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Bad GuiBridgeRust path 🐞 Bug ≡ Correctness
Description
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.
Code

crates/coldvox-gui/src/bridge.rs[R51-54]

       // 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;
+
Evidence
Inside the ffi module, super refers to the outer bridge module where GuiBridgeRust is
defined; using super::super points above the module that actually defines the struct.

crates/coldvox-gui/src/bridge.rs[36-54]
crates/coldvox-gui/src/bridge.rs[91-102]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## 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


View more (5)
4. QString/String type mismatch 🐞 Bug ≡ Correctness
Description
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.
Code

crates/coldvox-gui/src/bridge.rs[R153-162]

+                                            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);
+                                            });
Evidence
The backing Rust struct fields are String, and existing unit tests call set_last_error with a
Rust String, but the new code passes QString into these setters in multiple places.

crates/coldvox-gui/src/bridge.rs[93-102]
crates/coldvox-gui/src/bridge.rs[110-193]
crates/coldvox-gui/src/bridge.rs[223-277]
crates/coldvox-gui/src/bridge.rs[339-349]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## 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


5. Tokio runtime drops tasks🐞 Bug ☼ Reliability
Description
The background thread’s rt.block_on(async { ... }) completes immediately after spawning tasks, so
the Tokio runtime is dropped and all spawned tasks (including the STT receive loop) are aborted
before producing UI updates.
Code

crates/coldvox-gui/src/bridge.rs[R122-217]

+        std::thread::spawn(move || {
+            let rt = tokio::runtime::Builder::new_multi_thread()
+                .enable_all()
+                .build()
+                .expect("failed to build Tokio runtime for ColdVox pipeline");
+
+            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 {
+                    Ok(app) => {
+                        let shared = std::sync::Arc::new(app);
+
+                        // Grab the STT event channel receiver.
+                        // Direct Qt property updates from this async context are not possible
+                        // (Qt objects are thread-bound to the Qt main thread), so we use
+                        // qt_thread().queue() with a Queued connection to safely emit
+                        // signals across the thread boundary.
+                        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;
+                                        }
+                                    }
+                                }
+                            });
+                        }
+
+                        tracing::info!("ColdVox pipeline started successfully");
+
+                        // Hold the pipeline open. The Arc is dropped when:
+                        // - The GUI process exits (normal shutdown)
+                        // - A future implementation calls shutdown explicitly
+                        let keep_alive = shared.clone();
+                        tokio::spawn(async move {
+                            std::future::pending::<()>().await;
+                            let _ = keep_alive;
+                        });
+                    }
+                    Err(e) => {
+                        tracing::error!("Failed to start ColdVox pipeline: {}", e);
+                    }
+                }
+            });
+        });
Evidence
rt.block_on awaits only the setup future; the long-lived work is placed into tokio::spawn tasks
that are not awaited. When block_on returns, the runtime is dropped, which terminates all tasks on
that runtime.

crates/coldvox-gui/src/bridge.rs[122-217]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The Tokio runtime thread exits right after setup, dropping the runtime and cancelling all background tasks.
### Issue Context
Spawning a `pending()` future inside `tokio::spawn` does not keep the runtime alive if the main `block_on` future completes.
### Fix Focus Areas
- crates/coldvox-gui/src/bridge.rs[122-217]
### Suggested change
- Keep the runtime thread alive by awaiting a never-ending future *in the `block_on` future*, e.g.:
- after starting tasks: `std::future::pending::<()>().await;`
- Or store/await `JoinHandle`s instead of detaching them, so `block_on` does not return until shutdown.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. STT never starts by default🐞 Bug ≡ Correctness
Description
cmd_start uses AppRuntimeOptions::default(), whose stt_selection defaults to None; the
runtime only initializes the STT plugin manager and STT processing when stt_selection.is_some(),
so no transcription events will be produced for the GUI.
Code

crates/coldvox-gui/src/bridge.rs[R130-134]

+                use coldvox_app::runtime::AppRuntimeOptions;
+
+                let opts = AppRuntimeOptions::default();
+                match coldvox_app::runtime::start(opts).await {
+                    Ok(app) => {
Evidence
The GUI starts the runtime with default options, and the runtime explicitly gates STT plugin manager
initialization (and thus STT processing) on opts.stt_selection.is_some(). The default
implementation sets stt_selection: None, so STT won’t run and the transcript channel will be idle.

crates/coldvox-gui/src/bridge.rs[130-134]
crates/app/src/runtime.rs[94-103]
crates/app/src/runtime.rs[480-505]
crates/app/src/runtime.rs[519-524]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The runtime is started with defaults that disable STT, so the GUI will never receive partial/final transcript events.
### Issue Context
`coldvox_app::runtime::start` only initializes the STT plugin manager when `AppRuntimeOptions.stt_selection` is set.
### Fix Focus Areas
- crates/coldvox-gui/src/bridge.rs[130-134]
- crates/app/src/runtime.rs[94-103]
- crates/app/src/runtime.rs[480-505]
### Suggested change
- Populate `AppRuntimeOptions.stt_selection` with an appropriate `PluginSelectionConfig` (from settings/config), rather than using `default()` as-is.
- Alternatively, change `coldvox-app` defaults/behavior if STT should be enabled by default for GUI mode.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. Stop does not stop pipeline 🐞 Bug ☼ Reliability
Description
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.
Code

crates/coldvox-gui/src/bridge.rs[R223-238]

+    /// 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);
   }
Evidence
The bridge itself documents that it does not hold an AppHandle to perform graceful shutdown, and
the implementation only sets state fields. The runtime provides AppHandle::shutdown(Arc) as the
intended shutdown path, but the bridge never stores/uses the handle.

crates/coldvox-gui/src/bridge.rs[223-238]
crates/app/src/runtime.rs[150-188]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## 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


8. Missing tracing dependency🐞 Bug ≡ Correctness
Description
bridge.rs now uses tracing::warn/debug/info/error, but coldvox-gui does not declare a
tracing dependency, so qt-ui builds will fail with an unresolved crate error.
Code

crates/coldvox-gui/Cargo.toml[R15-21]

cxx = "1"
cxx-qt = { version = "0.7", optional = true }
cxx-qt-lib = { version = "0.7", features = ["qt_qml", "qt_gui"], optional = true }
+# Real pipeline bindings — only needed when Qt UI is enabled
+coldvox-app = { path = "../app", optional = true }
+tokio = { version = "1", features = ["full"], optional = true }
Evidence
The Rust bridge imports and calls tracing::* macros in multiple places, while Cargo.toml lists
no tracing dependency for coldvox-gui (transitive deps cannot be used directly).

crates/coldvox-gui/src/bridge.rs[110-114]
crates/coldvox-gui/Cargo.toml[13-21]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`coldvox-gui` uses `tracing::...` but does not depend on `tracing`.
### Issue Context
Rust crates must declare direct dependencies for crates they reference.
### Fix Focus Areas
- crates/coldvox-gui/Cargo.toml[13-28]
- crates/coldvox-gui/src/bridge.rs[110-114]
### Suggested change
- Add `tracing = "0.1"` under `[dependencies]` (optionally gated behind `qt-ui` if desired).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

9. Clear overwrites QML bindings 🐞 Bug ⚙ Maintainability
Description
The Clear button’s fallback branch assigns to root.partial_transcript/root.final_transcript,
which will overwrite the property bindings to bridge.* and can leave the UI no longer tracking
bridge updates if that branch is ever executed.
Code

crates/coldvox-gui/qml/Main.qml[R267-268]

+        // Clear — resets transcript state via bridge cmd_clear, falls back to clearing local aliases
+        ControlButton { label: "🗑"; onClicked: { if (typeof bridge !== 'undefined' && bridge.cmd_clear) { bridge.cmd_clear() } else { root.partial_transcript = ""; root.final_transcript = "" } } }
Evidence
partial_transcript and final_transcript are defined via expressions referencing bridge.*, but
the Clear fallback assigns new literal values to those same properties.

crates/coldvox-gui/qml/Main.qml[26-31]
crates/coldvox-gui/qml/Main.qml[267-268]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The Clear fallback assigns to properties that are intended to stay bound to bridge properties.
### Issue Context
If you need a fallback without a bridge, use separate local properties or explicitly re-bind after assignment.
### Fix Focus Areas
- crates/coldvox-gui/qml/Main.qml[26-31]
- crates/coldvox-gui/qml/Main.qml[267-268]
### Suggested change
- Remove the assignment fallback (if `bridge` is always present in real builds), or
- Use separate local-only properties for the no-bridge mode instead of overwriting the bound ones.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

rt.block_on(async {
use coldvox_app::runtime::AppRuntimeOptions;

let opts = AppRuntimeOptions::default();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment on lines +236 to +237
self.set_state(AppState::Stopping);
self.set_state(AppState::Idle);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment on lines 51 to +54
// 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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

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

Comment on lines +153 to +162
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);
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

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

Comment on lines +223 to 238
/// 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);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

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

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 GuiBridge with transcript properties (partial_transcript, final_transcript) and signals (transcript_partial, transcript_final) intended for cross-thread delivery into QML.
  • Update Main.qml to bind and render partial vs final transcripts with separate Text elements and add a clear action wired to bridge.cmd_clear().
  • Add coldvox-app and tokio as qt-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;
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
type GuiBridge = super::super::GuiBridgeRust;
type GuiBridge = super::GuiBridgeRust;

Copilot uses AI. Check for mistakes.
Comment on lines +130 to +132
use coldvox_app::runtime::AppRuntimeOptions;

let opts = AppRuntimeOptions::default();
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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);

Copilot uses AI. Check for mistakes.
Comment on lines +119 to +123
// 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()
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +130 to +132
use coldvox_app::runtime::AppRuntimeOptions;

let opts = AppRuntimeOptions::default();
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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());

Copilot uses AI. Check for mistakes.
Comment on lines +170 to +174
let new_final = format!(
"{}\n{}",
qGuiBridge.as_ref().final_transcript(),
text_owned
);
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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)
};

Copilot uses AI. Check for mistakes.
Comment on lines +270 to +271
self.as_mut().set_partial_transcript(QString::default());
self.as_mut().set_final_transcript(QString::default());
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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());

Copilot uses AI. Check for mistakes.
Comment on lines +134 to +136
Ok(app) => {
let shared = std::sync::Arc::new(app);

Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines +142 to +198
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;
}
}
}
});
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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;
}
}
}
});
}

Copilot uses AI. Check for mistakes.
Comment on lines +128 to +133
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 {
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 229 to +237
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);
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
… 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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants