Skip to content

feat(hotkey): 会话激活期间 Esc 由 OpenLess 独占——不再透传宿主应用#855

Open
bigsongeth wants to merge 2 commits into
Open-Less:betafrom
bigsongeth:feat/esc-exclusive-during-session
Open

feat(hotkey): 会话激活期间 Esc 由 OpenLess 独占——不再透传宿主应用#855
bigsongeth wants to merge 2 commits into
Open-Less:betafrom
bigsongeth:feat/esc-exclusive-during-session

Conversation

@bigsongeth

@bigsongeth bigsongeth commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

User description

依赖 #853,请先合并那个。 没有独立取消通道的话,Processing 期间吞掉的 Esc 无法真正触发取消,会退化成「吃掉按键但什么都不做」。

问题

胶囊转圈(录音/转写/润色)时按 Esc,OpenLess 取消会话的同时,宿主应用也收到了这个 Esc——一次按键双重生效。典型场景:在 Claude 里边对话边听写,按 Esc 想取消转写,结果顺带把 Claude 正在生成的回复也取消了。

交互模型

参照输入法:组合窗激活时按 Esc,取消的是候选词,宿主应用收不到这个键——因为此刻用户的意图对象显然是输入法。胶囊显示进行中的会话时完全同构:Esc 的语义就是「取消这个会话」,应当被独占消费。一次输入触发两个语义(且第二个是破坏性的)违反基本交互原则。

Windows 底层钩子本来就吞掉听写触发键(返回 LRESULT(1)),唯独 Esc 一直放行——本 PR 是把同一语义补齐到 Esc,并给 macOS 补上对应能力(active tap 返回 null 删除事件)。

实现

  • hotkey.rs:进程级 ESC_EXCLUSIVE 旗标 + set_esc_exclusive()。置位期间 macOS tap 对 Esc keydown 返回 null,Windows 钩子返回 LRESULT(1)。keyup 不吞(宿主应用几乎都在 keydown 响应 Esc,孤儿 keyup 无害)。
  • capsule_focus.rs:在 emit_capsule(所有会话状态变化的单一出口,含 fix(capsule): 终止态 2s 自动隐藏 — 覆盖 Done/Cancelled/Error 全路径 #77 审计保证的全部终止路径)维护旗标——胶囊为 Recording/Transcribing/Polishing 且 dictation phase 非 Idle 时置位,终止帧(Done/Cancelled/Error/Idle)自然清除。
  • phase 条件专门排除 QA 会话:QA 也走胶囊,但它的 Esc 由聚焦的浮窗窗口自行处理([qa] 划词追问浮窗多处取消 race:Esc 关不掉流 / 烧 token / 跨会话漏字 / loading 帧丢失 #161 的路由),全局吞键反而会把浮窗的 Esc 挡掉。QA 行为完全不变。
  • 卡死风险有界:万一 Processing 挂住,吞键窗口由转写全局超时兜底,不会永久吃掉全系统的 Esc;进程退出则 tap 一并消失。

验证

  • cargo check --tests 干净;cargo test 794 passed。
  • daily 构建 dogfood 实测通过(macOS):转写/润色中按 Esc → 会话取消且宿主应用无反应;空闲时 Esc 正常透传;QA 浮窗 Esc 行为不变。

🤖 Generated with Claude Code


PR Type

Bug fix, Enhancement


Description

  • Esc 取消改走独立通道,修复转写/润色期间按 Esc 无响应

  • 新增 esc-cancel-bridge 专用线程,即时处理 Esc 事件

  • 会话激活期间 Esc 由 OpenLess 独占,不透传宿主应用

  • Windows 钩子与 macOS tap 同步修改,Linux 适配器签名更新


Diagram Walkthrough

flowchart LR
  ESC["Esc 按键"] --> cancel_tx["独立 Sender<()>"]
  cancel_tx --> esc_cancel_bridge["esc-cancel-bridge 线程"]
  esc_cancel_bridge --> cancel_session["cancel_session (立即)"]
  cancel_session --> set_cancelled["置 cancelled 旗标"]
  set_cancelled --> select_cancel["end_session 的 select! 命中"]
  ESC --> esc_exclusive{"会话激活?<br/>exclusive?"}
  esc_exclusive -->|是| swallow["吞掉 keydown<br/>不透传宿主"]
Loading

File Walkthrough

Relevant files
Enhancement
coordinator.rs
Spawn esc cancel bridge                                                                   

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

  • 在 hotkey start 时调用 spawn_esc_cancel_bridge 并传递 cancel_tx
+2/-1     
capsule_focus.rs
Add esc exclusive logic                                                                   

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

  • 在 emit_capsule 中根据胶囊状态设置 esc_exclusive 旗标
  • 会话激活(录音/转写/润色)时 Esc 不透传宿主应用
+11/-0   
hotkey.rs
Rework Esc handling: independent channel and exclusive flag

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

  • 从 HotkeyEvent 枚举移除 Cancelled
  • 添加 cancel_tx 参数和 send_cancel_or_log 函数
  • 添加 ESC_EXCLUSIVE 静态变量及 set_esc_exclusive
  • macOS 和 Windows 平台回调:使用 cancel_tx 发送取消事件,并检查 esc_exclusive 吞键
  • 更新 start_listener_thread 和 start_adapter 签名
+74/-14 
Bug fix
hotkey_loops.rs
Add esc cancel bridge loop                                                             

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

  • 新增 esc_cancel_bridge_loop 和 spawn_esc_cancel_bridge
  • 从 hotkey_bridge_loop 和 less_computer_modifier_bridge_loop 移除 Cancelled
    处理
+34/-6   
Miscellaneous
hotkey.rs
Update mobile stub                                                                             

openless-all/app/src-tauri/src/mobile_stubs/hotkey.rs

  • 从 HotkeyEvent 移除 Cancelled
  • 添加 set_esc_exclusive 空实现
  • 更新 start 方法签名以接受 cancel_tx
+4/-1     

bigsongeth and others added 2 commits July 23, 2026 15:47
Open-Less#798 给 end_session 加了「转写 vs 取消」的 select! 赛跑,但取消信号本身
送不进来:hotkey bridge 线程为修 Open-Less#468/Open-Less#475 的 latch 竞态改成了串行
block_on,Hold 松手后 end_session 在 bridge 线程上同步跑完整段转写 +
润色,期间 Esc 的 HotkeyEvent::Cancelled 只能在同一条 channel 里排队。
流程跑完后才被取出,此时 phase 已回 Idle,cancel 变 no-op —— 用户按
Esc 毫无反应,必须干等转写完成。听写键为修饰键(默认 fn 等)时必现;
组合键用户不受影响(按键事件走 ComboHotkeyMonitor 的独立线程),这也是
Open-Less#798 验证时没暴露的原因。

修法:把 Cancelled 从 HotkeyEvent 枚举拆出,Esc 在 tap/hook 回调里改发
独立的 Sender<()>,由专用 esc-cancel-bridge 线程消费并调 cancel_session
(纯同步快路径,不 await)。cancelled 旗标即时置位,end_session 的
select! 赛跑与润色流的 should_cancel 立即命中。macOS CGEventTap 与
Windows WH_KEYBOARD_LL 同步修改;Less Computer 修饰键 monitor 的 Esc
转发同样迁移。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
胶囊转圈时按 Esc,取消会话的同时宿主应用也收到了这个键——一次按键双重
生效(如顺带取消了 Claude 正在生成的回复)。参照输入法的交互模型:组合窗
激活时 Esc 只取消候选词、宿主应用收不到;胶囊显示进行中的会话时同理,
Esc 的语义就是「取消它」,应当被独占消费。

实现:hotkey.rs 新增进程级 ESC_EXCLUSIVE 旗标,由 emit_capsule(所有会话
状态变化的单一出口)维护——胶囊为 Recording/Transcribing/Polishing 且
dictation phase 非 Idle 时置位,终止帧清除。phase 条件排除 QA 会话:QA
的 Esc 由聚焦的浮窗窗口处理,吞键反而会挡掉。置位期间 macOS active tap
对 Esc keydown 返回 null 删除事件,Windows 底层钩子返回 LRESULT(1)——
Windows 本就吞听写触发键,Esc 是补齐同一语义。keyup 不吞:宿主应用几乎
都在 keydown 响应 Esc,孤儿 keyup 无害。

依赖 Open-Less#853(独立取消通道):没有它,Processing 期间吞掉的 Esc 无法真正
触发取消,会变成「吃掉按键但什么都不做」。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

PR Reviewer Guide 🔍

(Review updated until commit 5323d42)

Here are some key observations to aid the review process:

🎫 Ticket compliance analysis 🔶

77 - Partially compliant

Compliant requirements:

  • (empty) – This PR does not implement the auto-hide feature; it only uses the existing emit_capsule single exit point.

Non-compliant requirements:

  • Add schedule_capsule_idle helper.
  • Call the helper from all terminal states.
  • Unify delay to 2000ms.
  • Guard idle emission with phase check.

Requires further human verification:

  • (empty)

853 - Partially compliant

Compliant requirements:

  • Esc cancel moved to independent channel.
  • Added esc_cancel_bridge_loop and spawn_esc_cancel_bridge.
  • macOS and Windows Esc handlers now send via cancel_tx.
  • Cancelled removed from all bridge loops.
  • send_cancel_or_log helper added.
  • mobile stub updated (Cancelled removed, cancel_tx parameter added).

Non-compliant requirements:

  • (empty)

Requires further human verification:

  • (empty)

161 - Partially compliant

Compliant requirements:

  • (empty) – This PR only excludes QA from the exclusive Esc flag to avoid breaking QA Esc, but does not implement any of the specific fixes.

Non-compliant requirements:

  • Fix QA Esc routing.
  • SSE cancel mechanism.
  • Session-id guard.
  • Loading frame handling.

Requires further human verification:

  • (empty)
⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
🧪 No relevant tests
🔒 No security concerns identified
⚡ No major issues detected

@bigsongeth
bigsongeth marked this pull request as ready for review July 24, 2026 05:02
@github-actions

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit 5323d42

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