Skip to content

fix(coordinator): block QA edge routing while dictation runs [高] - #390

Merged
H-Chris233 merged 1 commit into
betafrom
fix/audit-qa-dictation-routing
May 9, 2026
Merged

fix(coordinator): block QA edge routing while dictation runs [高]#390
H-Chris233 merged 1 commit into
betafrom
fix/audit-qa-dictation-routing

Conversation

@appergb

@appergbappergb commented May 9, 2026

Copy link
Copy Markdown
Collaborator

User description

Summary

Two related state-machine race fixes between dictation and the Q&A panel.

3.3.1 — Hotkey edge routing race

`handle_pressed_edge` routed the dictation hotkey edge to the QA panel whenever `qa_state.panel_visible` was true, regardless of whether a main dictation session was already running. If the user opened the QA panel mid-dictation, the next dictation-hotkey edge was routed into `begin_qa_session`, which calls `Recorder::start` a second time on the same mic device.

  • macOS / Windows: cpal usually rejects the second `build_input_stream`, but the original dictation session keeps running with no UX path to stop it from the QA panel.
  • Linux / PipeWire: the second open can succeed; two concurrent capture streams compete for the device.

Symmetrically, `handle_released_edge` swallowed Released whenever the panel was visible — fine when QA owned the press, but if dictation owned the press, Hold-mode dictation could never end because the Released edge was eaten.

Fix: read `inner.state.phase` alongside `panel_visible`. If a dictation session is non-Idle, both edges go to dictation regardless of `panel_visible`. QA panel stays open, just doesn't capture this hotkey.

3.3.4 — open_qa_panel clobbers in-flight capsule

`open_qa_panel` always emitted `CapsuleState::Idle` to sweep stale "已粘贴 N 字" Done residue. But if dictation was mid-flight (Recording bar / Polishing progress / Done toast still visible inside the ~1.5s auto-hide window), the sweep clobbered live UI.

Fix: guard the sweep on dictation phase == Idle. The original Done-residue sweep semantic still applies the common case (dictation finished, capsule auto-hid, user opens QA later) since by then phase is Idle.

Why one PR

Both fixes are about the same logical seam — "QA panel ↔ dictation session interaction" — and touch the two adjacent files (`coordinator/dictation.rs` + `coordinator/qa.rs`). Splitting them across PRs would just create a merge conflict on the same code review boundary.

Audit linkage

Audit IDs 3.3.1 + 3.3.4 (both CONFIRMED, 高 + 中). See `docs/audit-2026-05-10-validated.md` (local).

Test plan

  • `cargo test --lib` — 183/183 pass.
  • CI build — to be verified.
  • Manual verification: (a) open QA panel during dictation, press dictation hotkey → should stop dictation, not begin QA recording; (b) Hold-mode dictation with QA panel open → release should still end the session; (c) finish dictation, see Done toast, immediately open QA panel → toast should not be wiped. To be done after merge.

PR Type

Bug fix


Description

  • Prevent QA hotkey routing during active dictation

  • Fix Hold-mode stuck by proper release edge routing

  • Conditionally clear capsule to protect in-flight UI


Diagram Walkthrough

flowchart LR
A["Hotkey Edge"] --> B{"dictation active?"}
B -- "Yes" --> C["Route to Dictation"]
B -- "No, QA visible?" --> D["Route to QA"]
E["open_qa_panel"] --> F{"dictation idle?"}
F -- "Yes" --> G["Emit Idle Capsule"]
F -- "No" --> H["Keep Current Capsule"]
Loading

File Walkthrough

Relevant files
Bug fix
dictation.rs
Guard hotkey routing with dictation phase check

openless-all/app/src-tauri/src/coordinator/dictation.rs

  • Added dictation_active guard in handle_pressed_edge to route to
    dictation when session is non-Idle, even if QA panel is visible.
  • Added same guard in handle_released_edge to allow release to stop
    dictation when session is active.
  • Updated inline comments documenting the race condition and fix.
+12/-2
qa.rs
Conditional capsule reset on QA panel open

openless-all/app/src-tauri/src/coordinator/qa.rs

  • In open_qa_panel, emit CapsuleState::Idle only when dictation is Idle,
    preventing capsule overwrite of in-progress UI.
  • Imported SessionPhase to perform the state check.
  • Updated comment explaining the conditional capsule sweep logic.
+11/-4

handle_pressed_edge previously routed the dictation hotkey to the QA
panel whenever qa_state.panel_visible was true, regardless of whether a
main dictation session was already active. If the user opened the QA
panel mid-dictation (or while polishing/inserting), the next dictation-
hotkey edge was routed into begin_qa_session, which calls
Recorder::start a second time on the same mic device. cpal usually
rejects the second build_input_stream on macOS/Windows but the
dictation session keeps running with no UX path to stop it from the QA
panel; on Linux/PipeWire the second open can succeed and you get two
concurrent capture streams competing for the audio device.
Symmetrically, handle_released_edge swallowed Released entirely when
the panel was visible — fine when QA owned the press, but if dictation
owned the press, Hold-mode dictation could never end because the
Released edge was eaten.
Fix: read inner.state.phase alongside panel_visible. If a dictation
session is non-Idle (Starting / Listening / Processing / Inserting),
both edges go to dictation regardless of panel_visible. The QA panel
stays open, just doesn't capture this hotkey.
3.3.4 (same PR, same files): open_qa_panel always emit'd CapsuleState::Idle
to sweep stale Done residue, but if dictation was mid-flight that
clobbered the in-flight capsule (Recording bar, Polishing progress, the
brief Done toast). Guard the sweep on dictation phase == Idle.
Audit IDs 3.3.1 + 3.3.4 (CONFIRMED 高 + 中).
Test: 183/183 lib tests pass. Manual verification (open QA panel during
dictation, press dictation hotkey — should stop dictation, not begin QA;
also Hold-mode dictation while panel visible should still stop on
release) requires the running app, to be done after merge.
@github-actions

Copy link
Copy Markdown
Contributor

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 2 🔵🔵⚪⚪⚪
🧪 No relevant tests
🔒 No security concerns identified
⚡ No major issues detected

@H-Chris233
H-Chris233 merged commit 033e572 into betaMay 9, 2026
5 checks passed
@appergbappergb mentioned this pull request May 10, 2026
5 tasks
pullBot pushed a commit to yimmy23/openless that referenced this pull request May 10, 2026
10 PRs landed on beta this cycle:
- Open-Less#377 paste shortcut configurable (issue Open-Less#360)
- Open-Less#386 TS UserPreferences updateChannel alignment
- Open-Less#387 focus_target leak on Processing-phase cancel
- Open-Less#388 [严重] MacHotkeyAdapter::shutdown stops CFRunLoop + tap
- Open-Less#389 emit_capsule window.show/hide off audio thread
- Open-Less#390 QA / dictation hotkey routing race
- Open-Less#391 audio-mute spawn_blocking (async hygiene)
- Open-Less#392 hotkey supervisor + global dispatcher exit signal
- Open-Less#393 post-audit logic-review hotfixes (QA mute .await + focus_target Processing branch)
- Open-Less#394 in-process credentials cache (kills repeated Keychain prompts)
Bump 4 files: package.json, tauri.conf.json, Cargo.toml, Cargo.lock.
@appergb
appergb deleted the fix/audit-qa-dictation-routing branch May 10, 2026 10:14
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@appergb@H-Chris233