Skip to content

fix(windows): 修复 #466 / #468 / #469 / #470 四个 Windows bug#471

Merged
appergb merged 10 commits into
mainfrom
fix/windows-bugs-468-469-470-466
May 18, 2026
Merged

fix(windows): 修复 #466 / #468 / #469 / #470 四个 Windows bug#471
appergb merged 10 commits into
mainfrom
fix/windows-bugs-468-469-470-466

Conversation

@appergb
Copy link
Copy Markdown
Collaborator

@appergb appergb commented May 18, 2026

User description

Summary

集中修复 4 个 Windows 端 bug,每个 issue 独立 commit、独立 closes:

Test plan

风险 / 已知 trade-off


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

flowchart LR
  A["Open QA panel"] -- "store focus target" --> B["Capture selection"]
  B -- "restore focus" --> C["Refocus QA window"]
  D["IME snapshot restore"] -- "legacy + modern API" --> E["Original input method restored"]
  F["Single-instance relaunch"] -- "respect start_minimized" --> G["Skip main window"]
  H["Capsule visible state"] -- "one-time logs" --> I["Better Windows diagnostics"]
Loading

File Walkthrough

Relevant files
Bug fix
coordinator.rs
Add QA focus handoff and capsule logs                                       

openless-all/app/src-tauri/src/coordinator.rs

  • Adds Windows-only capture_external_focus_target to refresh the saved
    QA focus target.
  • Temporarily restores focus to the user app before selection capture,
    then refocuses QA afterward.
  • Adds one-time capsule logs for the first visible show and for
    suppression by show_capsule=false.
  • Introduces capsule_state_log_name for compact, safe diagnostics.
+97/-1   
qa.rs
Track QA focus target across sessions                                       

openless-all/app/src-tauri/src/coordinator/qa.rs

  • Adds qa_focus_target to QaSessionState for Windows focus-dance
    handling.
  • Captures the initial focus target when opening the QA panel.
  • Clears the stored focus target when closing the QA panel.
  • Updates imports to use the new focus-target capture helper.
+11/-1   
lib.rs
Respect minimized relaunch and refocus QA                               

openless-all/app/src-tauri/src/lib.rs

  • Skips showing the main window on single-instance relaunch when
    start_minimized is enabled.
  • Preserves silent startup behavior on Win11 and other
    relaunch-triggered paths.
  • Adds refocus_qa_window for Windows to show and focus the QA webview.
  • Reworks QA window activation so keyboard input reaches the webview
    reliably.
+42/-14 
windows_ime_profile.rs
Restore IME state with legacy symmetry                                     

openless-all/app/src-tauri/src/windows_ime_profile.rs

  • Restores legacy TSF state before the modern profile manager call.
  • Splits TextService and KeyboardLayout restore paths to match
    activation behavior.
  • Logs modern ActivateProfile failures as warnings when legacy restore
    already succeeded.
  • Fixes Windows input-method restoration so the user returns to the
    original IME.
+40/-6   

baiqing added 4 commits May 18, 2026 09:34
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
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 18, 2026

PR Reviewer Guide 🔍

(Review updated until commit 6c06b2f)

Here are some key observations to aid the review process:

🎫 Ticket compliance analysis 🔶

469 - Partially compliant

Compliant requirements:

  • Restore path now updates both legacy TSF state and modern profile state.

Non-compliant requirements:

  • None identified from code review.

Requires further human verification:

  • Verify on Windows 10/11 with a third-party IME after voice input.

466 - Partially compliant

Compliant requirements:

  • QA now receives keyboard focus so ESC can reach the webview.
  • QA now gets a real foreground/focus path so the close button is no longer expected to be eaten by the OS first-click behavior.

Non-compliant requirements:

  • None identified from code review.

Requires further human verification:

  • Verify the close behavior on a real Windows desktop session.

470 - Partially compliant

Compliant requirements:

  • None.

Non-compliant requirements:

  • The PR does not implement the capsule display fix; it only adds diagnostic logging.

Requires further human verification:

  • Root-cause verification and UI testing on Windows are still needed.
⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
🧪 No relevant tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Focus Regression

On Windows this now uses window.show() plus set_focus(), which means reopening the QA window will steal foreground focus instead of staying as a no-activate overlay. That is a user-visible regression for any workflow where QA is opened while another app is active, because typing or copy actions in the source app can be interrupted by the focus grab.

fn show_qa_window_no_activate<R: tauri::Runtime>(window: &tauri::WebviewWindow<R>) -> bool {
    // 函数名沿用历史命名,实际行为已切到「show + focus」—— 让 QA webview 真正拿到键盘
    // 焦点,ESC 才能到 React 监听、X 按钮 first-click 才不会被 OS 当作激活点击吃掉。
    //
    // 走 Tauri 的 show() / set_focus() 而不是 Win32 SetForegroundWindow + SetFocus
    // 的原因(pr_agent 关注点二轮回应):
    //   - 直接 SetFocus(host_hwnd) 不保证 WebView2 child 收键盘事件,WebView2 子窗口
    //     有自己的 focus 模型。Tauri 内部走 webview 专用路径,能把焦点真正送到 webview。
    //   - SetForegroundWindow 在 Win11 focus-stealing prevention 下可能被拒。Tauri
    //     2.x 在跨平台 abstraction 里做了兜底(按 SPI 临时调整 / attach input queue)。
    //
    // 对 issue #164 "QA 浮窗不抢前台 app 焦点"的取舍:浮窗出现时会短暂成为前台,
    // 但 begin_qa_session 抓选区前 focus-dance 会把焦点临时还给用户原 app(见
    // coordinator.rs 同 issue 注释),抓完再 refocus_qa_window 收回 —— 选区路径
    // 仍能正常工作,issue #164 在「QA 出现的那一帧」短暂被违背是 #466 修复的代价。
    if window.show().is_err() {
        return false;
    }
    let _ = window.set_focus();
    true

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
@github-actions
Copy link
Copy Markdown

Persistent review updated to latest commit 9c6c42d

pr_agent 提的 forward-looking 隐患:诊断日志现在用 `state:?`,CapsuleState
本身是纯枚举不含字段,但哪天加上 `Error(String)` 之类的话 `:?` 会把
ASR / polish / error 文本意外塞进 log 文件。

加 capsule_state_log_name(state) → &'static str,显式枚举到一个固定短名
("idle" / "recording" / ... / "error")。两处 #470 诊断 log 都改成
这个 helper。逻辑零变化,纯收紧日志输出语义。

refs #470
@github-actions
Copy link
Copy Markdown

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
@github-actions
Copy link
Copy Markdown

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
@github-actions
Copy link
Copy Markdown

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
@github-actions
Copy link
Copy Markdown

Persistent review updated to latest commit 205b219

@appergb
Copy link
Copy Markdown
Collaborator Author

appergb commented May 18, 2026

@codex 请审查这个 PR。

简短说明:

主要风险点(请重点看):

  1. windows_ime_profile.rs::restore_profile 的「先 legacy 再 modern」调用序列,以及 modern 失败降级为 warn 的合理性
  2. coordinator.rs::begin_qa_session 的 focus-dance + 多轮 stale-focus 刷新逻辑(GetWindowThreadProcessId 判本进程)
  3. lib.rs single-instance 回调里读 start_minimized 的位置和 try_state 解锁正确性
  4. lib.rs::show_qa_window_no_activate 从 Win32 直连切到 Tauri window.set_focus(),对 WebView2 child focus 是否真的够稳

辛苦。

@appergb
Copy link
Copy Markdown
Collaborator Author

appergb commented May 18, 2026

@claude 请审查这个 PR。

简短说明:

4 个 Windows-only bug 一起修:#469 (IME 切不回) / #468 (Win11 静默启动失效) / #466 (QA 弹窗 X/ESC 关不掉) / #470 (录音胶囊不显示)
#470 是诊断日志 only,不是 closes(refs https://github.com/Open-Less/openless/issues/470)—— 缺真机日志,先加 log 让下次能定位
pr_agent 已五轮迭代到 clean(#466 #468 #469 PR Code Verified、No major issues detected、无安全风险)
三平台 CI 全绿
主要风险点(请重点看):

windows_ime_profile.rs::restore_profile 的「先 legacy 再 modern」调用序列,以及 modern 失败降级为 warn 的合理性
coordinator.rs::begin_qa_session 的 focus-dance + 多轮 stale-focus 刷新逻辑(GetWindowThreadProcessId 判本进程)
lib.rs single-instance 回调里读 start_minimized 的位置和 try_state 解锁正确性
lib.rs::show_qa_window_no_activate 从 Win32 直连切到 Tauri window.set_focus(),对 WebView2 child focus 是否真的够稳
辛苦。

@claude
Copy link
Copy Markdown

claude Bot commented May 18, 2026

Claude finished @appergb's task in 3m 24s —— View job


PR #471 代码审查

  • 初始化任务列表
  • 读取变更文件
  • 审查 windows_ime_profile.rs::restore_profile — legacy→modern 调用序列
  • 审查 coordinator.rs::begin_qa_session — focus-dance + stale-focus 刷新逻辑
  • 审查 lib.rs — single-instance 回调 start_minimized 读取位置与 try_state 解锁
  • 审查 lib.rs::show_qa_window_no_activate — Tauri set_focus() 对 WebView2 child focus 稳定性
  • 汇总审查意见

审查结论

整体代码质量较高,逻辑链条清晰,注释与 pr_agent 往来交代充分。以下按四个风险点逐一说明,并附一个真正值得在合并前修复的细节 bug。


1. windows_ime_profile.rs::restore_profile — legacy→modern 调用序列 ✅ 合理

结论:逻辑正确,降级策略可接受。

激活路径(activate_openless_profile):

EnableLanguageProfile → ChangeCurrentLanguage → ActivateLanguageProfile  (legacy)
ActivateProfile                                                            (modern)

恢复路径(restore_profile TextService 分支):

ChangeCurrentLanguage → ActivateLanguageProfile  (legacy, ? → error)
ActivateProfile                                   (modern, warn on failure)

对称性:EnableLanguageProfile 不在恢复路径中——正确,用户原 IME 在它成为当前 profile 之前就已经 enabled,不需要重新 enable。PROFILE_RESTORE_FLAGS 去掉了 TF_IPPMF_ENABLEPROFILE,与逻辑一致。

KeyboardLayout 分支:只调 ChangeCurrentLanguage,不调 ActivateLanguageProfile——正确,键盘布局没有 CLSID/profile GUID,ActivateProfileGUID_NULL 是 Windows API 约定行为。

modern_result warn 降级:注释解释已很充分。ChangeCurrentLanguage + ActivateLanguageProfile 完成后,OS 视觉层(语言指示器、路由)已切回;现代 API 失败是内部 bookkeeping 不同步,不影响用户感知。warn 而非 error 合理。

轻微提示(非阻塞):restore_profile TextService 分支内,with_input_processor_profileswith_profile_manager 分别各自做一次 CoInitializeEx + CoUninitialize(两个独立 ComApartment)。顺序调用,不会有问题,但略微冗余。现阶段不必改,知悉即可。


2. coordinator.rs::begin_qa_session focus-dance — 发现一个可修复的细节 ⚠️

结论:逻辑正确,但两次独立 lock 之间存在 TOCTOU 窗口,建议合并为一次。

当前代码(coordinator.rs 约 2582–2588 行):

#[cfg(target_os = "windows")]
{
    if let Some(current_external) = capture_external_focus_target() {
        inner.qa_state.lock().qa_focus_target = Some(current_external);  // lock #1 释放
    }
    let saved_target = inner.qa_state.lock().qa_focus_target;            // lock #2 重新获取
    let _ = restore_focus_target_if_possible(saved_target);
}

两次 lock 之间,close_qa_panel(在另一个线程上)可能将 qa_focus_target 清成 None。虽然用户同时按关闭和 Option 的概率极低,但在逻辑上属于可修正的竞态。建议合并为单次加锁:

#[cfg(target_os = "windows")]
{
    let saved_target = {
        let mut state = inner.qa_state.lock();
        if let Some(current_external) = capture_external_focus_target() {
            state.qa_focus_target = Some(current_external);
        }
        state.qa_focus_target
    };
    let _ = restore_focus_target_if_possible(saved_target);
}

capture_external_focus_target() 只调 GetForegroundWindow + GetCurrentProcessId(无锁,纯查询),放在持 qa_state 锁期间调用不会死锁。

Fix this →

其余 focus-dance 逻辑均正确:

  • capture_external_focus_targetGetCurrentProcessId 过滤自身进程窗口,避免把焦点还给 QA 本身——逻辑无懈可击。
  • restore_focus_target_if_possible 中的 std::thread::sleep(60ms) 阻塞了 tokio worker thread,但这是旧代码,本 PR 不引入,知悉即可。
  • refocus_qa_window 在 capture_selection 后调 show_qa_window_no_activate,正确完成 focus-dance 下半场。

3. lib.rsstart_minimized 读取位置与 try_state 解锁 ✅ 正确

结论:实现正确,无锁泄漏。

if let Some(coordinator) = app
    .try_state::<Arc<coordinator::Coordinator>>()
    .map(|s| Arc::clone(&*s))          // State<> 临时引用在此 drop
{
    if coordinator.prefs().get().start_minimized {
        return;
    }
}
  • try_state() 返回 Option<State<'_, T>>State 只是 Arc<T> 的共享引用,不持锁。Arc::cloneState<> guard 立即释放,不存在"try_state 不解锁"问题。
  • prefs().get().start_minimizedArc<Coordinator> 上,prefs 本身有独立 RwLockget() 拿读锁,读完即释放。无死锁风险。
  • 位置正确性:CLI intent 分支先 return,不会走到 start_minimized 检查;只有普通的「用户再次点图标/Win11 登录时重新打开」才会到达该检查。符合 [windows] 启动时静默运行功能在 Win11 失效 #468 的语义:只拦截"无意图的二次启动",有意图的 CLI dispatch 不受影响。

4. lib.rs::show_qa_window_no_activateset_focus() 对 WebView2 的稳定性 ✅ 合理,但函数名已成误导

结论:Tauri 抽象比直连 Win32 更稳,函数名遗留问题已在注释中说明。

fn show_qa_window_no_activate<R: tauri::Runtime>(window: &tauri::WebviewWindow<R>) -> bool {
    // 函数名沿用历史命名,实际行为已切到「show + focus」
    if window.show().is_err() {
        return false;
    }
    let _ = window.set_focus();
    true
}

为什么 window.set_focus()SetFocus(host_hwnd) 更可靠:直接 SetFocus 作用在 WebView2 宿主窗口(hwnd)上,不保证 WebView2 的 CoreWebView2Controller 把焦点传递到内嵌 Chromium 进程。Tauri 2.x 的 set_focus() 内部会调 CoreWebView2Controller::MoveFocus 并传 COREWEBVIEW2_MOVE_FOCUS_REASON_PROGRAMMATIC,能真正让 webview 接收键盘事件——这正是 ESC/X 能响应的前提。

Win11 focus-stealing preventionwindow.show() 触发 ShowWindow(SW_SHOW) + 内部 SetForegroundWindow,Tauri 已在抽象层做了 SPI (SystemParametersInfo) 临时调权或 input queue attach,比裸 SetForegroundWindow 更能在 Win11 严格策略下成功。

名字遗留问题show_qa_window_no_activate 已经名不副实——它现在会 activate。注释里写了原因和后续补救路径,可以接受。若想消除歧义,可在 follow-up PR 中重命名为 show_and_focus_qa_window,但不属于本次合并的阻塞项。

refocus_qa_window 复用此函数:正确——它就是要 show + focus,让 QA 收回键盘。


汇总

风险点 结论 阻塞合并?
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
@github-actions
Copy link
Copy Markdown

Persistent review updated to latest commit 6c06b2f

@appergb appergb merged commit e0e6453 into main May 18, 2026
4 checks passed
appergb added a commit that referenced this pull request May 18, 2026
* 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>
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.

1 participant