diff --git a/openless-all/app/src-tauri/src/coordinator.rs b/openless-all/app/src-tauri/src/coordinator.rs index 72ce6dce..3dad786a 100644 --- a/openless-all/app/src-tauri/src/coordinator.rs +++ b/openless-all/app/src-tauri/src/coordinator.rs @@ -100,6 +100,8 @@ fn capsule_show_strategy_for_platform() -> CapsuleShowStrategy { static CAPSULE_NO_ACTIVATE_FALLBACK_WARNED: AtomicBool = AtomicBool::new(false); static CAPSULE_SUPPRESSED_BY_TOGGLE_LOGGED: AtomicBool = AtomicBool::new(false); static CAPSULE_FIRST_SHOW_LOGGED: AtomicBool = AtomicBool::new(false); +// #470 诊断 v2:capsule webview 句柄取不到时的一次性门,区分「窗口压根没创建」(A0)。 +static CAPSULE_WINDOW_MISSING_LOGGED: AtomicBool = AtomicBool::new(false); /// 给 #470 诊断日志用的 capsule 状态短名。显式枚举每个变体到 &'static str, /// 不走 `Debug` —— 哪天 CapsuleState 加了 `String` 字段,`:?` 会把 ASR / polish @@ -4989,9 +4991,13 @@ fn show_capsule_window_no_activate( }; let Ok(handle) = window.window_handle() else { + // #470 诊断 v2:Win32 show 路径最可能的暗点之一。此前静默 return, + // 无法观测「胶囊完全不显示」是否卡在这里。 + log::warn!("[capsule] no_activate failed: window_handle() unavailable — Win32 show skipped"); return false; }; let RawWindowHandle::Win32(raw) = handle.as_raw() else { + log::warn!("[capsule] no_activate failed: non-Win32 RawWindowHandle — Win32 show skipped"); return false; }; let hwnd = HWND(raw.hwnd.get() as *mut _); @@ -5226,6 +5232,14 @@ fn emit_capsule( let app_for_main = app.clone(); let _ = app.run_on_main_thread(move || { let Some(window) = app_for_main.get_webview_window("capsule") else { + // #470 诊断 v2:比 A/B/C 更靠前的暗点 A0 —— capsule webview 句柄取不到 + // (窗口未创建/已销毁)。此前静默 return,无法观测。一次性 warn。 + if !CAPSULE_WINDOW_MISSING_LOGGED.swap(true, Ordering::SeqCst) { + log::warn!( + "[capsule] capsule webview window not found — emit_capsule show path skipped (state={})", + capsule_state_log_name(state) + ); + } return; }; let show_capsule = inner_for_main.prefs.get().show_capsule; diff --git a/openless-all/app/src-tauri/src/lib.rs b/openless-all/app/src-tauri/src/lib.rs index 076e6c8e..4b65b3d8 100644 --- a/openless-all/app/src-tauri/src/lib.rs +++ b/openless-all/app/src-tauri/src/lib.rs @@ -1376,7 +1376,15 @@ pub(crate) fn position_capsule_bottom_center( let offset_from_bottom = (capsule_visual_height(translation_active) + 80.0 + bounds.bottom_inset) * scale; let y = ((mon.bottom as f64) - offset_from_bottom).round() as i32; - window.set_position(PhysicalPosition::new(x, y.max(mon.top)))?; + let clamped_y = y.max(mon.top); + // #470 诊断 v2:当前只夹了上边(.max(mon.top)),未夹下/左/右。多显示器、 + // 负坐标或异常 DPI 下胶囊可能被算到屏幕外却无任何观测。记录显示器几何与 + // 最终落点,用于证伪/证实「胶囊定位到屏幕外」(C 子嫌疑)。 + log::debug!( + "[capsule] win position: mon=({},{})..({},{}) scale={:.2} size=({}x{}) -> x={} y={} clamped_y={}", + mon.left, mon.top, mon.right, mon.bottom, scale, phys_w, phys_h, x, y, clamped_y + ); + window.set_position(PhysicalPosition::new(x, clamped_y))?; return Ok(()); } // 仅当 Win32 取不到前台显示器时,落回下面的 current_monitor 逻辑。 diff --git a/openless-all/app/src/components/Capsule.tsx b/openless-all/app/src/components/Capsule.tsx index e7ddb5d3..48065a57 100644 --- a/openless-all/app/src/components/Capsule.tsx +++ b/openless-all/app/src/components/Capsule.tsx @@ -286,6 +286,9 @@ function Pill({ os, state, level, insertedChars, message, onCancel, onConfirm }: // 动画结束就 unmount → 用户看到半截动画被截断。 // v1.3.1-6: 从 240ms 加到 360ms 让用户看清退出动画(240ms 太快感知不到)。 const EXIT_ANIM_MS = 360; +// #470 诊断 v2:模块级一次性门,只在 webview 收到第一个 capsule:state 事件时打 log。 +let capsuleStateFirstLogged = false; + // 初始可见 state:Tauri 内运行从 idle 开始(等后端 capsule:state 事件), // 浏览器 dev 模式从 recording 开始以便直接看到胶囊。 const INITIAL_VISIBLE_STATE: CapsuleState = isTauri ? 'idle' : 'recording'; @@ -321,6 +324,12 @@ export function Capsule() { const { listen } = await import('@tauri-apps/api/event'); const handle = await listen('capsule:state', event => { const p = event.payload; + if (!capsuleStateFirstLogged) { + capsuleStateFirstLogged = true; + // #470 诊断 v2:确认 capsule webview 确实收到了后端事件 —— 区分「后端没 + // emit」与「emit 了但窗口没显示/没渲染」。配合后端 [capsule] 日志定位根因。 + console.info('[capsule] first capsule:state received in webview, state=', p.state); + } setState(p.state); setLevel(p.level ?? 0); setMessage(p.message ?? undefined);