From 84ee3d96d59ff4e0ce938ab9c0924fb562f36624 Mon Sep 17 00:00:00 2001 From: baiqing Date: Sun, 10 May 2026 08:08:07 +0800 Subject: [PATCH] fix(coordinator): read show_capsule pref inside main-thread closure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses pr_agent "Stale State" review on PR #389. show_capsule was read on the audio callback thread and snapshotted into the run_on_main_thread closure. If the user toggled the preference between the audio-thread emit and the main-thread closure firing (one or two frames later), the queued window show/hide could apply the old preference and briefly flash the wrong UI state. Move that read inside the closure so the latest user pref always wins. Why visible / translation stay snapshotted: they're the payload of THIS specific capsule:state event — the whole point is to tell the JS frontend "this is the state at the moment emit_capsule was called". Re-reading them inside the closure would mean the OS-level window state and the JS-side payload disagree (the closure lands one frame later than the emit_to that already shipped). No behavior change in the steady state; eliminates a 16–33 ms stale- pref window when the user changes show_capsule mid-dictation. --- openless-all/app/src-tauri/src/coordinator.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/openless-all/app/src-tauri/src/coordinator.rs b/openless-all/app/src-tauri/src/coordinator.rs index 8a26fadd..aefd6397 100644 --- a/openless-all/app/src-tauri/src/coordinator.rs +++ b/openless-all/app/src-tauri/src/coordinator.rs @@ -3666,19 +3666,26 @@ fn emit_capsule( translation, }; - let show_capsule = inner.prefs.get().show_capsule; + // visible / translation 是「这一帧 capsule:state event 的 payload」内容 —— + // 必须在 call-site(即音频线程触发 emit_capsule 时)就算定,否则 main thread + // 闭包里读到的将是「下一帧」的 state,跟实际下发给 JS 的 payload 不一致。 let visible = !matches!(state, CapsuleState::Idle); // emit_capsule 会被 cpal process_callback(音频回调线程)调用 ~30 Hz —— 在该 // 线程上调用 NSWindow / HWND API 会撞 macOS dispatch_assert_queue_fail SIGTRAP // 或者 Win32 SendMessage 死锁。把 window.show/hide + 位置调整 marshal 到主线程; // app.emit_to 走 Tauri 内部事件总线,本身线程安全,保留同步调用。详见 audit 3.2.2。 + // + // show_capsule(用户偏好)在主线程执行时再读 —— 用户可以在录音过程中改设置, + // 闭包入队到真正跑之间窗口上限是一两帧(~16-33ms),用最新值消除 stale-pref + // 闪烁。pr_agent 关注点 — 见 audit follow-up。 let inner_for_main = Arc::clone(inner); let app_for_main = app.clone(); let _ = app.run_on_main_thread(move || { let Some(window) = app_for_main.get_webview_window("capsule") else { return; }; + let show_capsule = inner_for_main.prefs.get().show_capsule; // 三平台统一:Done / Cancelled / Error 状态保留 ~1.5s toast // (schedule_capsule_idle 之后会回 Idle 隐藏)。 // Windows 上 linger 的真实问题(截图选中 / 死区 / 拖拽卡顿)由 #140 加的