fix(windows): 修复 #466 / #468 / #469 / #470 四个 Windows bug#471
Conversation
restore_profile 之前只调 ITfInputProcessorProfileMgr::ActivateProfile (现代 API),但 activate_openless_profile 走的是「现代 + legacy」组合: ChangeCurrentLanguage + ActivateLanguageProfile + ActivateProfile。 结果是 OS 的 legacy current language / active profile 状态被 OpenLess 激活路径动过,restore 路径只翻新现代 API → OS 仍认 OpenLess 是当前 输入法,用户的 WeChat 输入法切不回去。 修法是 restore_profile 对称补齐 legacy API 调用。TextService 分支 加 ChangeCurrentLanguage + ActivateLanguageProfile;KeyboardLayout 分支加 ChangeCurrentLanguage(legacy 没有专门的 keyboard layout 接 口,HKL 由现代 ActivateProfile 处理)。 回归点:c3b59329 (5/1) "fix: avoid clipboard fallback for Windows voice insertion" 把激活路径升级为「现代 + legacy」混合,没同步升级 恢复路径。 closes #469
Win11 启动时静默运行(start_minimized=true)被绕过的根因不在初次启动
的 setup() 分支(那里读 prefs 正常),而在 tauri_plugin_single_instance
的回调里:
show_main_window(app) // 无条件抢出主窗口
Win11 常见触发链:
- Registry Run 触发首次启动 → setup() 尊重 start_minimized,window 隐藏 ✓
- Win11 "登录时重新打开应用" 功能再触发一次 → single-instance 截获
→ show_main_window → 窗口弹出 ✗
- 或者 autostart 自身被 OS 触发两次
修法:single-instance 回调里先读 prefs,start_minimized=true 就直接
return(不走 CLI intent 也不 show)。托盘左键、托盘菜单 "显示主窗口"
两条路径仍然显式调 show_main_window,是用户的主动操作,不受影响。
closes #468
show_qa_window_no_activate 用 SW_SHOWNOACTIVATE 显示窗口 + tauri.conf.json 里 \"focus: false\" 双管齐下,QA webview 从来拿不到键盘焦点: - ESC keydown 永远不会到 React 监听器 → ESC 关不掉 - X 按钮 first-click 被 OS 当作激活点击吃掉 → 用户感知是"按了没反应" 修法是 show 时调 ShowWindow(SW_SHOW) + SetForegroundWindow + SetFocus, 明确把焦点交给 QA webview。前者只是创建提示,后者控制每次 show 时的 实际焦点。 权衡:这破坏了 issue #164 的 "QA 浮窗不抢前台 app 焦点" 语义。后续 如需恢复"选区抓取不被打断",在 begin_qa_session 期间临时把焦点还给 saved front app HWND,capture_selection 跑完再收回 QA 即可。issue #466 的 "X / ESC 完全不能关闭" 比这个权衡严重得多,先修主路径。 closes #466
issue #470 / #470-like 报告"看不到录音胶囊"时,目前 log 没法定位是: (a) prefs.show_capsule 被关 (b) emit_capsule 根本没被调(state 一直 Idle) (c) show_capsule_window_for_recording 调了但 Win32 路径失败 加两条一次性 info log(per-process): - 首次 show_capsule=true && visible=true:明确"胶囊路径有跑、开关是开的" - 首次 show_capsule=false && visible=true:明确"用户开关把胶囊关掉了" 下次用户报"胶囊没显示"时,看一行 log 就能定根因,不再让用户自己反 复确认设置。逻辑不动,纯加 log。 refs #470
PR Reviewer Guide 🔍(Review updated until commit 6c06b2f)Here are some key observations to aid the review process:
|
pr_agent 指出 #466 修复(QA 浮窗主动抓前台)会破坏 begin_qa_session 里 capture_selection 的 simulate_copy 路径 —— 此时前台是 QA 自己的 webview,Ctrl+C 抓不到用户原 app 的选区。 补 focus-dance: - open_qa_panel: 在 show_qa_window 抢前台之前调 capture_focus_target(), 把用户原 app 的 HWND 存进 QaSessionState.qa_focus_target。 - begin_qa_session: capture_selection 之前 restore_focus_target_if_possible 把焦点临时还给 saved HWND;capture_selection 跑完再 refocus_qa_window 把焦点交还 QA,保证 ESC/X 等交互继续可用。 - close_qa_panel: 清掉 qa_focus_target。 多轮场景下,用户在两轮之间主动切回原 app 时 saved == current 前台 → restore_focus_target_if_possible 是 no-op,capture_selection 走原路径。 非 Windows 平台所有 focus-dance 分支都 #[cfg] 掉,macOS / Linux 不受影响。 refs #466
|
Persistent review updated to latest commit 9c6c42d |
|
Persistent review updated to latest commit ed5f460 |
pr_agent 二轮关注点:直接 SetFocus(host_hwnd) + SetForegroundWindow 在 Windows 上有两个潜在隐患: 1. WebView2 子窗口有独立 focus 模型,host HWND 拿到焦点不保证 webview 收到键盘事件 → ESC 仍可能收不到。 2. Win11 focus-stealing prevention 可能拒绝 SetForegroundWindow, 导致整个抢前台序列被 OS 静默吞掉。 切换到 Tauri 的 window.show() + window.set_focus(): - Tauri 内部专门处理 WebView2 child window 的 focus 路由 - 内部对 SetForegroundWindow 失败做了 SPI / AttachThreadInput 兜底 - 还顺便去掉了 raw_window_handle / windows-rs 的直连依赖噪音 行为等价,但走的是 Tauri 跨平台抽象 → 更稳,且后续 Tauri 升级时跟着 获益。注释里把决策理由写明,下次有人想恢复 Win32 直连前再想想。 refs #466
|
Persistent review updated to latest commit e2cf2ab |
pr_agent 关注点:restore_profile 的"先 legacy 再 modern"调用序列里, 如果 modern ActivateProfile 失败,前两步 ChangeCurrentLanguage + ActivateLanguageProfile 已经成功 —— OS 视觉层(语言指示器、键盘事件 路由)走 legacy 视图,用户已经看到 IME 切回去了。现代 API 失败只是 内部 bookkeeping 跟不上,不会让用户感知"还停在 OpenLess"。 旧实现把 modern 失败当整体失败 propagate 出去,caller restore_session 会打一条 "restore saved profile failed" 的 warn,误导排查方向。 改成:现代 ActivateProfile 失败时在本函数内打 warn(信息更具体,明 确说明 legacy 已 OK),return Ok。caller 看到 Ok = "用户感知已切回"。 两个 ImeProfileKind 分支都同步改。 21 个 windows_ime 单元测试全过(测的是 restore_decision / 状态机, 不直接 mock Win32 调用,行为不受影响)。 refs #469
|
Persistent review updated to latest commit 2c03c17 |
pr_agent 关注点:qa_focus_target 只在 open_qa_panel 抓一次,多轮 QA 里用户开 QA 后切到别的 app(App B)再按 Option,focus-dance 会把焦点 抢回 App A,capture_selection 抓的是 A 的选区而不是 B 的,多轮新选区 直接丢。 策略:每轮 begin_qa_session 开头看「当前前台是不是本进程的窗口」: - 是 OpenLess 自家窗口(QA / capsule / main)→ 用户没切走,沿用 saved。 - 是别的 app → 用户主动切到了真正的外部 app,刷新 saved 为这个 HWND。 判断走 GetWindowThreadProcessId + GetCurrentProcessId 对比 pid,比维护 QA 窗口 HWND list 简单且安全(capsule / 未来其他自家窗口都自动覆盖)。 新增 helper capture_external_focus_target() —— 与 capture_focus_target() 对称,专门跳过本进程窗口。非 Windows 平台返回 None。 refs #466
|
Persistent review updated to latest commit 205b219 |
|
@codex 请审查这个 PR。 简短说明:
主要风险点(请重点看):
辛苦。 |
|
@claude 请审查这个 PR。 简短说明: 4 个 Windows-only bug 一起修:#469 (IME 切不回) / #468 (Win11 静默启动失效) / #466 (QA 弹窗 X/ESC 关不掉) / #470 (录音胶囊不显示) windows_ime_profile.rs::restore_profile 的「先 legacy 再 modern」调用序列,以及 modern 失败降级为 warn 的合理性 |
|
Claude finished @appergb's task in 3m 24s —— View job PR #471 代码审查
|
| 风险点 | 结论 | 阻塞合并? |
|---|---|---|
restore_profile legacy→modern 序列 |
✅ 正确,warn 降级合理 | 否 |
| focus-dance 两次独立 lock(TOCTOU) | 建议修(低风险,1 行改动) | |
start_minimized 读取与 try_state |
✅ 正确,无锁泄漏 | 否 |
set_focus() WebView2 稳定性 |
✅ 比直连 Win32 更稳,取舍合理 | 否 |
唯一建议修复项:将 begin_qa_session 中的两次 qa_state.lock() 合并为一次(见上方 Fix this 链接)。改动极小,可在合并前顺手处理。其余已知 trade-off(函数名遗留、async 中 blocking sleep、#470 诊断 log 非修复)均已在代码注释或 PR 描述中充分说明,不构成阻塞。
Cloud 评审指出:focus-dance 多轮刷新里原来分两段: lock #1 写 qa_focus_target ← capture_external_focus_target() lock #2 读 qa_focus_target → restore_focus_target_if_possible 两次 lock 中间,close_qa_panel 在别的线程把 qa_focus_target 清成 None 时会被覆盖回旧 HWND。虽然"按关闭"和"按 Option"同时触发的几率极低, 但属于可消除的竞态,1 行改动就能根治。 合一个 scope:在持锁期间既写最新外部前台、又读出来交给 restore。 capture_external_focus_target() 只调 GetForegroundWindow / GetCurrentProcessId 等纯查询,不会反向取 qa_state 锁,持锁期间调用安全。 refs #466
|
Persistent review updated to latest commit 6c06b2f |
* fix(windows): 修复 #466 #468 #469 IME 切回 / 静默启动 / QA 焦点 + #470 capsule 诊断 (#471) 集中修复 4 个 Windows-only bug: - #469 IME 切不回 — restore_profile 对称补 legacy TSF ChangeCurrentLanguage + ActivateLanguageProfile,回归点是 c3b5932 升级激活路径时漏改恢复路径。现代 ActivateProfile 失败降为 warn(legacy 已完成视觉切回)。 - #468 Win11 静默启动失效 — single-instance 回调读 start_minimized,true 时跳过 show_main_window;托盘 / CLI intent 路径不受影响。 - #466 QA 弹窗 X/ESC 关不掉 — Win 上从 SW_SHOWNOACTIVATE 切到 Tauri window.show + set_focus,让 webview 拿键盘焦点。配套 focus-dance:begin_qa_session 抓选区前临时把焦点还给用户原 app,抓完再 refocus QA;多轮场景下用 GetWindowThreadProcessId 区分本进程窗口,刷新 qa_focus_target,避免 stale HWND。 - #470 录音胶囊不显示 — 加 per-process 一次性诊断 log(CapsuleState 用 capsule_state_log_name 显式短名,不走 Debug 防字段扩展时泄露),下次能直接定位 prefs / state / Win32 哪一环失败。 pr_agent 五轮迭代到 No major issues + No security concerns;Cloud 评审建议的 begin_qa_session 双锁 TOCTOU 已合并为单 scope。三平台 build + pr_agent CI 全绿。 #470 没标 closes,是 refs—需要真机日志才能定位真因,先 ship 诊断手段。 * chore(release): bump version to 1.3.4-3 (Beta) — Windows #466 #468 #469 修复 * fix(settings-ui): 权限页徽章不换行 + 本地 ASR 区块 Win 灰显 权限页 (Settings → 快捷键 → 权限): - Pill 组件加 whiteSpace:nowrap + flexShrink:0 → "已安装"徽章在 Win 窄宽下不再被挤成 3 行(每个汉字一行)。 - 行内 message span 加 whiteSpace:nowrap + overflow:hidden + textOverflow:ellipsis + minWidth:0 → 长文本("Windows 低层键盘 hook 已安装")超宽时省略号收尾,徽章保持完整。 - zh-CN 描述大段精简:descAcc / descNoAcc / micDesc / accDesc / hotkeyDesc / networkDesc / windowsImeDesc / windowsIme.* 都改短, 减少 Win 上拥挤感(用户反馈 zh 文案过长)。 高级页 (Settings → 高级 → 本地 ASR): - Windows 上把"本地 ASR 模型(实验性)"标题区 + 警告小字 + Qwen3 行 整组 opacity:0.45 灰显 —— Qwen3 在 Win 是 stub 不支持,那条"实验性" 主线在 Win 没意义;用户视觉关注点应落到下方独立的 Foundry 行(Foundry 保持正常颜色)。Toggle 的 disabled 行为已经在原 onToggle 条件里,本次 纯加视觉灰显。 * fix(windows): 前端 App.tsx 在 show 主窗口前读 startMinimized 兜底 #471 在 Rust 端 setup() / single-instance callback 都加了 start_minimized suppression,本机日志确认 Rust 路径完全正确("[main] start_minimized=true → 跳过初始 show")。但用户在 Win11 1.3.4-3 上仍然「重启进桌面那一刻主 窗口就出来了」。 定位到最后一条遗漏路径在前端:App.tsx 里 mount 时一条 useEffect 无条件 通过 IPC 调 `getCurrentWindow().show()`,把 Rust 端已经 suppress 的窗口 又拉出来 —— 这条路径 Rust log 完全看不见(走的是 plugin-window 的 showWindow IPC,不经过我们的 show_main_window helper)。 那条 useEffect 当初是给 issue #163 引入的:Windows 权限探测可能死锁让 首屏卡灰,所以前端兜底 force show 一次。 修法:show 前先读 prefs.startMinimized,true 就 return。读 prefs 失败 (极少数情况)保持原 show 行为,不让 #163 的兜底语义丢失。 closes #468 (the actual one this time, Rust + JS 双端都收敛了) --------- Co-authored-by: baiqing <lbx12309@icloud.com>

User description
Summary
集中修复 4 个 Windows 端 bug,每个 issue 独立 commit、独立 closes:
restore_profile路径补对称的 legacy TSF API 调用(ChangeCurrentLanguage+ActivateLanguageProfile)。回归点是 c3b5932(5/1)激活路径加了 legacy API 但恢复路径没同步加,OS 仍认 OpenLess 是当前 IME。closes #469tauri_plugin_single_instance回调里读prefs.start_minimized,true 就直接 return,不再无条件调show_main_window。Win11 "登录时重新打开应用"等二次启动路径被拦下。托盘 / 主动调用路径不受影响。closes #468show_qa_window_no_activate改用SW_SHOW + SetForegroundWindow + SetFocus,让 QA webview 拿到键盘焦点。否则 ESC keydown 永远不到 React 监听、X 按钮 first-click 被 OS 当激活点击吃掉。代价:临时打破 [windows] 划词追问第二次按 Option 抓不到选区 — show_qa_window 抢前台焦点 #164 的"不抢前台焦点"语义(注释里写了后续怎么补)。closes #466refs #470(不 close)Test plan
cargo check(macOS 本地) — 仅 preexisting 的 unrelated warningscargo test --lib windows_ime— 21 个 IME 单元测试全部通过[capsule] first show this session: ...([Windows] [bug] 语音输入不显示录音胶囊 #470 诊断 log 触发)风险 / 已知 trade-off
capture_selection走 Ctrl+C fallback 时,前台是 QA 自己 → 选区抓不到。补救路径写在代码注释里:begin_qa_session 期间临时把焦点还给 saved front app HWND,capture 完回收。如果用户实际命中这个回归,会另开 issue 跟进。ChangeCurrentLanguage+ActivateLanguageProfile报错,会被?抛回 restore_session 的 warn log,能定位。PR Type
Bug fix
Description
Fix Windows IME restore symmetry
Preserve minimized relaunch behavior
Hand off QA focus on Windows
Add capsule visibility diagnostics
Diagram Walkthrough
File Walkthrough
coordinator.rs
Add QA focus handoff and capsule logsopenless-all/app/src-tauri/src/coordinator.rs
capture_external_focus_targetto refresh the savedQA focus target.
then refocuses QA afterward.
suppression by
show_capsule=false.capsule_state_log_namefor compact, safe diagnostics.qa.rs
Track QA focus target across sessionsopenless-all/app/src-tauri/src/coordinator/qa.rs
qa_focus_targettoQaSessionStatefor Windows focus-dancehandling.
lib.rs
Respect minimized relaunch and refocus QAopenless-all/app/src-tauri/src/lib.rs
start_minimizedis enabled.relaunch-triggered paths.
refocus_qa_windowfor Windows to show and focus the QA webview.reliably.
windows_ime_profile.rs
Restore IME state with legacy symmetryopenless-all/app/src-tauri/src/windows_ime_profile.rs
activation behavior.
ActivateProfilefailures as warnings when legacy restorealready succeeded.
original IME.