Skip to content

fix(popup): 跨端弹窗一致性 — capsule 四边 clamp + Windows 句柄重试(#470) + qa 焦点契约(#466) - #616

Merged
appergb merged 2 commits into
betafrom
fix/cross-platform-popup
Jun 8, 2026
Merged

fix(popup): 跨端弹窗一致性 — capsule 四边 clamp + Windows 句柄重试(#470) + qa 焦点契约(#466)#616
appergb merged 2 commits into
betafrom
fix/cross-platform-popup

Conversation

@appergb

@appergbappergb commented Jun 7, 2026

Copy link
Copy Markdown
Collaborator

User description

跨端弹窗一致性修复(task 1d 落地)

承接审计:Less Computer agent 弹窗设计上仅 macOS(trivially 一致);真正的跨端分歧在 capsuleqa(均为既有 #470/#466 历史问题)。本 PR 修复之。

修复 1 — capsule 定位四边 clamp(lib.rs

  • 抽出纯函数 clamp_to_monitor(x,y,w,h, area),保证整窗落在显示器内。
  • Windows position_capsule_bottom_center只夹顶边[Windows] [bug] 语音输入不显示录音胶囊 #470 飘屏根因之一)→ 改为四边都夹,优先夹到工作区 rcWork(避开任务栏)。ForegroundMonitor 加 work area 字段(复用 GetMonitorInfoW 已返回的 rcWork,无新 Win32 调用)。
  • 5 个新单测:屏内不动 / 右下越界回拉 / 负原点左副屏 / 工作区避任务栏 / 病态超宽输入。

修复 2 — Windows capsule 句柄重试(coordinator/ime_insertion.rs

修复 3 — qa 焦点契约(保守:仅文档化,不改运行时

验证

cargo check 0 error / cargo test --lib424 passed(419 基线 + 5 clamp)/ npm build 绿。

⚠️ 需 Windows 实测(mac 本机编不到)

修复 1 的 Windows 物理坐标分支、修复 2 的句柄重试整段——本机仅验证了纯函数 clamp_to_monitor(5 测)+ cargo check + cfg 完整;真实「多显示器+任务栏+WebView2」端到端表现需 CI + Windows 实测。

base = refactor/srp-split-dedup(stacked,合并顺序:#597#615 → 本 PR)。


PR Type

Bug fix, Enhancement


Description

  • Fix Windows capsule handle retry with bounded retries (up to 5 attempts, ~100ms max)

  • Fix capsule positioning: clamp all four edges to work area (avoid off-screen and taskbar)

  • Add work area to ForegroundMonitor and update capsule position calculation

  • Add comprehensive unit tests for clamp_to_monitor function

  • Document qa focus contract across platforms (no runtime change)


File Walkthrough

Relevant files
Bug fix
ime_insertion.rs
Add bounded retry for Windows capsule handle

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

  • Add bounded retry (max 5 attempts, ~18ms each) for Win32 handle in
    show_capsule_window_no_activate
  • If handle unavailable after retries, fall back to show() as before
  • Handle non-Win32 handle early failure
+40/-11
lib.rs
Add clamp_to_monitor function and work area usage

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

  • Extract clamp_to_monitor function to clamp window to rect (four edges)
  • Use work area (rcWork) for capsule positioning to avoid taskbar
  • Add work area fields to ForegroundMonitor struct
  • Update capsule position calculation to clamp both x and y
  • Add comprehensive unit tests for clamp_to_monitor
  • Document qa focus contract across platforms (no runtime change)
+129/-10

@appergb

Copy link
Copy Markdown
CollaboratorAuthor

@claude 复审这个跨端弹窗修复 PR。重点:

  1. capsule 四边 clamp:纯函数 clamp_to_monitor(lib.rs)逻辑是否正确?Windows position_capsule_bottom_center 改用工作区 rcWork 四边夹取,越界回拉数学有无问题?5 个单测覆盖是否充分?
  2. Windows 句柄重试show_capsule_window_no_activate(coordinator/ime_insertion.rs)≤5×18ms 重试再回落,在 run_on_main_thread 闭包里同步阻塞 <100ms 是否可接受?有无更好做法?
  3. qa 焦点契约(关键判断):我查证 set_focus()划词追问功能的弹窗X不掉 #466 为修 WebView2 ESC/X 失效有意加的,判定移除会回归 划词追问功能的弹窗X不掉 #466 故只文档化不改运行时——这个保守判断对吗?还是说有办法既不抢焦点又不回归 划词追问功能的弹窗X不掉 #466
  4. cfg 完整性(mac 本机只能 check macOS,Windows 分支靠 CI)。

已验证:cargo check 0 error / cargo test --lib 424 passed(+5 clamp)/ npm build 绿。是否可以合并?(base=refactor/srp-split-dedup)

@claude

claudeBot commented Jun 7, 2026

Copy link
Copy Markdown

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


PR #616 跨端弹窗修复 — 复审报告

  • 读取变更文件与调用上下文
  • 复审 1:clamp_to_monitor 逻辑 + 单测
  • 复审 2:show_capsule_window_no_activate 句柄重试
  • 复审 3:qa 焦点契约判断
  • 复审 4:cfg 完整性
  • 综合结论

复审 1 — clamp_to_monitor 四边 clamp(lib.rs

数学:正确。

let max_x = (area_right - w).max(area_left);let max_y = (area_bottom - h).max(area_top);let clamped_x = x.clamp(area_left, max_x);let clamped_y = y.clamp(area_top, max_y);
  • max_x = (area_right - w).max(area_left) 正确推出「整窗右边不超出 area_right」的最大左上角 x;area 宽于窗口时退化为 area_left(避免 .clamp 低高互换 panic)。
  • .clamp(area_left, max_x) 一次完成四边:左/右靠 x 轴,上/下靠 y 轴。

position_capsule_bottom_center 的 x 源(L1701):

let x = mon.left + ((mon_w - phys_w) / 2).max(0);

phys_w > mon_w.max(0) 把偏移夹为 0 → x = mon.left。随后 clamp_to_monitor 再把它推到 work_left。逻辑链完整,无漏洞。

单测覆盖:充分,5 个测出发点互补。

测试覆盖场景
leaves_on_screen_untouched屏内正常位置不动
pulls_back_off_screen_right_and_bottom右/下越界回拉
pushes_into_negative_origin_left_monitor负原点副屏 + y 负值上夹
respects_work_area_above_taskbar工作区避任务栏
degrades_gracefully_when_window_wider_than_area病态:窗口宽于 area

一个小缺口:没有单独的用例覆盖「x 在 [area_left, area_right] 内、但 x + w > area_right(右边溢出)」的常见真实场景(测试 2 的 x=2000 > area_right=1920 走的是同一代码路径,但测了更极端的情形)。建议补一个:

// x 在区域内但右边缘超出 → 应左移到 area_right - wlet(x, y) = clamp_to_monitor(1800,900,264,126,0,0,1920,1040);assert_eq!(x,1920 - 264);// 1656

不阻塞合并,但上线前补上更稳。

可能的溢出area_right - w 是 i32 减法,极端输入(area_right 很小且 w 很大)理论上会溢出;.max(area_left) 不能防止溢出本身。Rust debug build 会 panic(有助于暴露),release build 会静默环绕。物理像素范围内这不会发生,但如果想彻底规避可改为 area_right.saturating_sub(w).max(area_left)Fix this →


复审 2 — Windows 句柄重试(coordinator/ime_insertion.rs

重试逻辑:正确,有一处无害死代码。

循环 0..5attempt + 1 < 5 在第 4 次(末次)迭代时为 false,走 Err(e)return false。总睡眠 = 4 次 × 18ms = 72ms < 100ms。✓

letSome(hwnd) = hwnd else{returnfalse;// ← 理论上不可达};

这行 else 分支是不可达代码:循环内唯一的正常出口是 break(此时 hwnd 已 Some),失败出口全部提前 return false。HANDLE_RETRY_ATTEMPTS 若为 0 才会进这里,但它是常量 5。这不影响运行时行为,但 cargo clippy 可能报 unreachable_patterns 或 redundant 警告;建议加 #[allow(unreachable_code)] 或直接移除(直接用 hwnd.unwrap() 因为逻辑保证)。Fix this →

主线程阻塞 < 100ms:可接受,但值得记录。

调用栈是 run_on_main_thread 闭包 → show_capsule_window_for_recordingshow_capsule_window_no_activate(见 capsule.rs:223)。在 Tauri 主线程同步阻塞最多 72ms,会延迟该帧内其他主线程工作(e.g. window resize、message pump)。

PR 描述的权衡是正确的:否则那一帧本就要走 window.show() 抢焦点,代价更高。实际表现中,如果 webview 已 realize(多数情况),attempt=0 就成功,0ms 额外阻塞。只有首帧冷启动才会碰重试。

有无更好做法?

理论上可以把重试移到后台线程,成功后再 run_on_main_threadShowWindow。但:

  1. ShowWindow 需要在创建该 HWND 的线程(即主线程)调用;反向 marshal 会引入另一次 run_on_main_thread + 跨线程协调。
  2. 复杂度增加,且不能保证那一帧内完成——可能胶囊对用户短暂不可见。

结论:当前做法是合理工程权衡,可接受。 建议在 CI metrics 或 release note 里记录「首帧冷启动可能有 ≤72ms 额外 latency」,方便日后排查性能投诉。


复审 3 — qa 焦点契约保守判断(关键判断)

判断:正确,保守路线合适。

文档新增的三端契约对比清晰无误:

  • macOSorderFrontRegardless → 全程不抢焦点 ✓
  • Windowsshow() + set_focus() → 有意抢焦,WebView2 子窗口 focus 模型决定的,qa.rs focus-dance 补偿 ✓
  • Linux:tauri.conf.json focus:false → Tauri 建成非激活窗口,window.show() 不抢焦 ✓

「有办法既不抢焦点又不回归 #466」吗?

理论上有,但代价高:

  1. WM_SETFOCUS / SetForegroundWindow + 注册热键接管 WebView2 子窗口键盘路由
  2. 或通过 WebView2 ICoreWebView2Controller::MoveFocus 逻辑焦点而非 OS 焦点

两者均属 Windows 平台深水区,且 WebView2 focus model 在 Electron/WebView2 生态历史上不稳定(见 MS 多个 known issues)。当前「文档化、不动运行时」的保守判断正确:风险/收益不对等,且 focus-dance 已经把用户体感副作用控制住了。 注释里 别删。 的警告足够强,不需要 runtime guard。


复审 4 — cfg 完整性

符号cfg 标注评估
clamp_to_monitor#[cfg_attr(not(windows), allow(dead_code))]正确;非 Windows dead_code 警告压住,测试跨平台可跑 ✓
ForegroundMonitor + work_* 字段struct 整体 #[cfg(windows)]正确,字段只在 Windows 存在 ✓
foreground_window_monitor()#[cfg(windows)]正确 ✓
show_capsule_window_no_activate (Windows 版)#[cfg(windows)]正确,其他平台有 mac/linux 各自 cfg ✓
position_capsule_bottom_center Windows 路径#[cfg(target_os = "windows")] { ... } 块内正确 ✓

clamp_to_monitorcfg_attr 而非 #[cfg] 是好的:允许非 Windows CI 也编译并运行全部 5 个单测(纯函数无平台依赖)。


综合结论

可以合并(pending Windows CI 绿)。

所有核心逻辑正确,5 个新单测全部有效,qa 焦点保守判断合理,cfg 无遗漏。

合并前推荐修复(非阻塞,但建议):

  1. area_right.saturating_sub(w) 防溢出(纯防御,线上触发概率极低)
  2. 补右边缘溢出单测(x 在区域内但右缘溢出)
  3. 清理/说明 let Some(hwnd) else { return false } 死代码

Windows 实测 checklist(合并后或 CI 中验证):

  • 多显示器:胶囊跟随前台 App 所在屏,不飘到主屏
  • 任务栏在底部:胶囊整窗不压住任务栏(验证 work area clamp)
  • 冷启动首帧:胶囊正常显示,不抢焦点(验证重试路径)
  • 任务栏在侧边/顶部:验证 work_left/work_right/work_top 的影响

@github-actions

github-actionsBot commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

PR Reviewer Guide 🔍

(Review updated until commit 4824080)

Here are some key observations to aid the review process:

🎫 Ticket compliance analysis 🔶

597 - Partially compliant

Compliant requirements:

  • (无)本 PR 未实现 Less Computer 核心功能,仅修复了相关弹窗的跨端定位问题。

Non-compliant requirements:

  • 专用键 + 动作模式 + 流式聊天弹框
  • 全屏彩虹描边
  • 设置相关
  • 交互式审批启发式
  • 护栏 deny 清单完整性
  • 死代码清理
  • dangerouslySetInnerHTML 安全

Requires further human verification:

  • 本 PR 修复了弹窗跨端一致性问题,但 Less Computer 主功能仍需单独审查。此 ticket 的绝大部分需求未在本 PR 中实现,需等待后续提交。

470 - Partially compliant

Compliant requirements:

  • 添加了 clamp_to_monitor 四边 clamp 逻辑,保证整窗落在工作区内
  • show_capsule_window_no_activate 添加了有界重试(≤5×18ms)获取 Win32 句柄,减少句柄不可用时的回落路径
  • ForegroundMonitor 中添加工作区字段,胶囊优先夹到工作区内避开任务栏
  • 新增 5 个单测覆盖多场景

Non-compliant requirements:

  • (无)

Requires further human verification:

  • (无)

466 - Partially compliant

Compliant requirements:

  • (无)

Non-compliant requirements:

Requires further human verification:

  • 需要 Windows 真机验证:在当前行为下,用户能否正常使用 ESC 或 X 按钮关闭弹窗?焦点抢占是否有其他副作用?如果问题依然存在,需考虑其他修复方向。
⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
🧪 PR contains tests
🔒 No security concerns identified
⚡ No major issues detected

@appergb

Copy link
Copy Markdown
CollaboratorAuthor

已应用复审的 3 项非阻塞收口建议(commit 4824080):① clamp_to_monitor 改用 saturating_sub 防整数溢出;② 补「x 在屏内但右缘溢出」单测(现 425 passed);③ 给不可达的 hwnd guard 加注释说明(仅 HANDLE_RETRY_ATTEMPTS==0 防御兜底,clippy 不报、零运行时变化)。cargo check 0 error / cargo test --lib 425 passed / npm build 绿。Windows 端到端仍需 CI + 实测(见 PR 描述的 checklist)。感谢复审 🙏

@github-actions

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit 4824080

@appergb

Copy link
Copy Markdown
CollaboratorAuthor

@claude 合并前最终复审。本 PR = 跨端弹窗一致性:capsule 四边 clamp(saturating_sub 防溢出) + Windows 句柄重试(#470) + qa 焦点契约文档化(#466)。最新 4824080。自审核对:clamp 多屏负原点/超宽窗口正确、句柄重试有上界且失败安全降级。请确认坐标计算与平台一致性、可合并。

@claude

claudeBot commented Jun 8, 2026

Copy link
Copy Markdown

Claude finished @appergb's task in 2m 47s —— View job


PR #616 合并前最终复审

  • 读取变更文件(最新 4824080
  • 验证 clamp_to_monitor:saturating_sub + 6 个单测数学核对
  • 验证 Windows 句柄重试 + hwnd guard 注释
  • 坐标计算与平台一致性综合核对
  • 综合结论 / 合并判断

复审 1 — clamp_to_monitor 数学逐步验证

函数核心(含 saturating_sub,commit 4824080):

let max_x = area_right.saturating_sub(w).max(area_left);let max_y = area_bottom.saturating_sub(h).max(area_top);let clamped_x = x.clamp(area_left, max_x);let clamped_y = y.clamp(area_top, max_y);
  • saturating_sub 正确防御 area_right 极小(或 w 极大)时的减法溢出 ✓
  • .max(area_left) 保证 max_x >= area_left,使 .clamp(area_left, max_x) 永不 panic(low > high 会 panic)✓
  • 最终 clamped_x + w <= area_right 成立(当无饱和时);饱和时退化为 area_left,至少保证左上角可见 ✓

6 个单测手算验证:

测试参数 (x,y,w,h,区域)预期验算
屏内不动(800,900,264,126, 0,0,1920,1040)(800,900)max_x=1656,max_y=914; 800∈[0,1656], 900∈[0,914] ✓
右下越界(2000,1200,264,126, 0,0,1920,1040)(1656,914)2000>1656→夹到1656; 1200>914→夹到914 ✓
右缘溢出(新增)(1800,900,264,126, 0,0,1920,1040)x=16561800∈[0,1656]为否(>1656)→夹到1656; 1800+264=2064>1920被阻止 ✓
负原点副屏(-3000,-100,294,138, -2560,0,0,1440)(-2560,0)max_x=(0-294).max(-2560)=-294; -3000<-2560→夹到-2560; max_y=1302; -100<0→夹到0 ✓
工作区避任务栏(800,1030,264,126, 0,0,1920,1040)y=914max_y=1040-126=914; 1030>914→夹到914; y+126=1040<=1040 ✓
超宽病态(500,500,800,600, 0,0,400,300)(0,0)max_x=sat_sub(400,800)=-400→.max(0)=0; 500.clamp(0,0)=0; 同理y ✓

6 个测试全部数学正确,覆盖场景互补(前 5 个基础场景 + 新增右缘在区域内但整窗溢出的关键真实场景)。425 passed 数字吻合(419+6)


复审 2 — Windows 句柄重试终态核对

constHANDLE_RETRY_ATTEMPTS:u32 = 5;// 0..5 = 5 次constHANDLE_RETRY_INTERVAL_MS:u64 = 18;

时序分析:

  • attempt 0,1,2,3:Err(_) if attempt+1 < 5 → sleep 18ms(共 4 次 sleep)
  • attempt 4(末次):attempt+1=5 < 5 为 false → 走 Err(e)return false
  • 最坏阻塞:4 × 18ms = 72ms < 100ms

非 Win32 句柄:第一次 Ok(handle) 匹配到 _ 分支立即 return false,不重试(正确,句柄类型不随时间变化)✓

hwnd guard(commit 4824080 新增注释):

// 此 else 仅是 HANDLE_RETRY_ATTEMPTS == 0(循环体一次都不跑)// 时的防御性兜底,当前常量为 5 时不可达,保留以防后续把次数改 0。letSome(hwnd) = hwnd else{returnfalse};

注释清晰说明了「不可达但防御性保留」的意图,clippy 不报(代码路径在类型系统层面合法),可接受 ✓


复审 3 — 坐标系与平台一致性

x 初始计算与工作区夹取链:

let x = mon.left + ((mon_w - phys_w) / 2).max(0);// 在全屏宽居中// 随后 clamp_to_monitor 夹到 work 区域

x 以全屏宽居中,再被 clamp 到工作区。对最常见的底部任务栏(work 区 = [0, work_right]):水平方向工作区与全屏同宽,x 保持居中不受影响 ✓。侧边任务栏下,clamp 仅在胶囊实际压到任务栏区域时才偏移,符合预期 ✓。

工作区退回逻辑:

if mon.work_right > mon.work_left && mon.work_bottom > mon.work_top{ ...}else{(mon.left, mon.top, mon.right, mon.bottom)}

条件检查工作区非退化(非零面积),GetMonitorInfoW 返回的 rcWork 通常与 rcMonitor 同时填充,此 else 仅覆盖极端边缘情况 ✓

cfg 完整性确认:

符号cfg状态
clamp_to_monitorcfg_attr(not(windows), allow(dead_code))非 Windows 压住警告、测试跨平台可跑 ✓
ForegroundMonitor.work_*struct 整体 #[cfg(windows)]字段仅在 Windows 存在 ✓
foreground_window_monitor()#[cfg(windows)]
Windows capsule 路径#[cfg(target_os = "windows")] 块内
句柄重试整段#[cfg(windows)] 函数变体

综合结论

✅ 可以合并(pending Windows CI 绿)。

上一轮复审提出的 3 项非阻塞建议均已正确落地:

建议处理
saturating_sub 防整数溢出✅ 已应用
补右缘溢出单测(x 在区域内但 x+w 越出)✅ 新增 test 3,数学正确
hwnd guard 死代码说明✅ 加了清晰注释,clippy 不报

代码正确性、平台一致性、测试覆盖均到位。qa 焦点保守判断(仅文档化)维持原判。

Windows 实测 checklist(合并后验证,优先级排序):

  • 底部任务栏:胶囊整窗不压任务栏(核心 fix 验证)
  • 多显示器:胶囊跟随前台 App 所在屏,不飘到主屏
  • 冷启动首帧:胶囊正常显示,不抢焦点(验证重试路径,预期 72ms 内解决)
  • 侧边 / 顶部任务栏:验证 work_{left,right,top} 对应 clamp 生效

@appergb
appergb changed the base branch from refactor/srp-split-dedup to betaJune 8, 2026 05:27
@appergb
appergb merged commit b4038de into betaJun 8, 2026
1 check passed
@appergb
appergb deleted the fix/cross-platform-popup branch June 9, 2026 06:06
Sign up for freeto 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

@appergb