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
Copy link
Copy Markdown
Collaborator

@appergb appergb commented May 9, 2026

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

github-actions Bot commented May 9, 2026

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 beta May 9, 2026
5 checks passed
pull Bot 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 free to 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