fix(coordinator): don't route hotkey edges to QA while dictation runs
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.
This commit is contained in:
1 parent
b1c387265b
commit
f46adce317
2 files changed
+23
-6
No files matched your search
@@ -12,8 +12,14 @@ pub(super) async fn handle_pressed_edge(inner: &Arc<Inner>) {
|
||||
let was_held = inner.hotkey_trigger_held.swap(true, Ordering::SeqCst);
|
||||
if !was_held {
|
||||
// 路由:QA 浮窗可见时,rightOption 边沿走 QA;否则走主听写。详见 issue #118 v2。
|
||||
// 例外:dictation session 已经在跑(Starting / Listening / Processing / Inserting),
|
||||
// 即使 QA 浮窗被打开了,这条边沿也必须先走 dictation。否则 begin_qa_session 会
|
||||
// 第二次抢同一个麦克风 device —— 在 Linux/PipeWire 上甚至会成功打开两路捕获,
|
||||
// dictation 的 recorder 没人停;在 macOS/Windows 上 cpal 会拒绝第二次 build_input_stream
|
||||
// 但 dictation session 仍在跑、用户找不到从 QA 面板停掉它的入口。审计 3.3.1。
|
||||
let dictation_active = !matches!(inner.state.lock().phase, SessionPhase::Idle);
|
||||
let panel_visible = inner.qa_state.lock().panel_visible;
|
||||
if panel_visible {
|
||||
if panel_visible && !dictation_active {
|
||||
handle_qa_option_edge(inner).await;
|
||||
} else {
|
||||
handle_pressed(inner).await;
|
||||
@@ -48,8 +54,12 @@ pub(super) async fn handle_released_edge(inner: &Arc<Inner>) {
|
||||
let was_held = inner.hotkey_trigger_held.swap(false, Ordering::SeqCst);
|
||||
if was_held {
|
||||
// QA 浮窗可见时,Option 行为是 press-toggle(不分 hold/release),release 边沿忽略。
|
||||
// 与 handle_pressed_edge 的路由对称:dictation session 在跑时 Pressed 已经被路由到
|
||||
// dictation,那 Released 必须也路由到 dictation —— 否则 Hold 模式松开热键时
|
||||
// end_session 不会触发,dictation 永远停不下来。审计 3.3.1。
|
||||
let dictation_active = !matches!(inner.state.lock().phase, SessionPhase::Idle);
|
||||
let panel_visible = inner.qa_state.lock().panel_visible;
|
||||
if panel_visible {
|
||||
if panel_visible && !dictation_active {
|
||||
return;
|
||||
}
|
||||
handle_released(inner).await;
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::sync::Arc;
|
||||
|
||||
use tauri::Emitter;
|
||||
|
||||
use crate::coordinator_state::{initial_session_id, SessionId};
|
||||
use crate::coordinator_state::{initial_session_id, SessionId, SessionPhase};
|
||||
use crate::selection::SelectionContext;
|
||||
use crate::types::CapsuleState;
|
||||
|
||||
@@ -86,9 +86,16 @@ pub(super) fn open_qa_panel(inner: &Arc<Inner>) {
|
||||
state.selection = None;
|
||||
state.front_app = capture_frontmost_app();
|
||||
}
|
||||
// 先把胶囊清干净,避免主听写上一次 Done 状态残留的 message/insertedChars
|
||||
// 在 QA Done 阶段被 capsule UI 错误复用("已之一粘贴这个 0" 那种)。
|
||||
emit_capsule(inner, CapsuleState::Idle, 0.0, 0, None, None);
|
||||
// 主听写 phase 是 Idle 才需要 sweep capsule —— 这里的语义是清掉「上一次 dictation
|
||||
// Done 状态残留」的 message / insertedChars,让 QA 自己的 capsule 状态从干净起跑
|
||||
// (否则 capsule UI 会出现 "已粘贴这个 0" 之类把上一次 inserted_chars 错误复用的
|
||||
// 显示)。但如果 dictation 当前正处于 Recording / Polishing / Inserting / Done toast
|
||||
// 显示中,强行 emit Idle 会把用户没看完的反馈抹掉、或者把 Polishing 中的进度条
|
||||
// 卡死。审计 3.3.4。
|
||||
let dictation_idle = matches!(inner.state.lock().phase, SessionPhase::Idle);
|
||||
if dictation_idle {
|
||||
emit_capsule(inner, CapsuleState::Idle, 0.0, 0, None, None);
|
||||
}
|
||||
if let Some(app) = inner.app.lock().clone() {
|
||||
crate::show_qa_window(&app, "idle");
|
||||
let _ = app.emit_to(
|
||||
|
||||
Reference in new issue
Block a user