Skip to content

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

Merged
H-Chris233 merged 3 commits into
Open-Less:betafrom
bigsongeth:feat/esc-exclusive-during-session
Aug 1, 2026
Merged

feat(hotkey): 会话激活期间 Esc 由 OpenLess 独占——不再透传宿主应用#855
H-Chris233 merged 3 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, Tests


Description

  • Add process-wide ESC_EXCLUSIVE flag, maintained by emit_capsule.

  • macOS tap and Windows hook consume Esc during active dictation.

  • Exclude QA sessions; Esc remains owned by focused floating panel.

  • Harden capsule idle scheduling and approval cancellation races.


Diagram Walkthrough

flowchart LR
  Esc["Esc keydown"] --> Listener["Platform listener (macOS tap / Windows hook)"]
  Listener --> Check{"esc_exclusive?"}
  Check -- "yes" --> Consume["Return null / LRESULT(1)"]
  Check -- "no" --> Host["Pass to host app"]
  emit_capsule["emit_capsule state change"] --> Flag["set_esc_exclusive"]
  Flag --> Check
Loading

File Walkthrough

Relevant files
Bug fix
coordinator.rs
Guard capsule idle timers and approval cancellation           

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

  • Add tests for stale capsule idle schedule and idle hide behavior.
  • Modify schedule_capsule_idle to skip stale timers when a newer state
    is emitted.
  • Force final outcome to Cancelled when session is cancelled after
    approval wait.
  • Treat cancellation during approval wait as Deny and clean up the
    approval registry.
+35/-0   
dictation.rs
Handle cancellation during approval wait                                 

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

  • Force final outcome to Cancelled when cancelled flag is set after
    approval.
  • Use tokio::select! to abort approval wait on processing cancel.
  • Clean up approval registry on session cancellation.
  • Add unit test for approval denied on session cancel.
+39/-4   
Enhancement
capsule_focus.rs
Maintain Esc exclusivity in capsule state emit                     

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

  • Add pure helper esc_exclusive_for_capsule for Esc exclusivity
    decisions.
  • Update ESC_EXCLUSIVE flag in emit_capsule based on capsule state and
    dictation phase.
  • Add table-driven tests covering active, terminal, and QA scenarios.
+65/-0   
hotkey.rs
Consume Esc in platform listeners when exclusive                 

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

  • Add process-wide ESC_EXCLUSIVE atomic flag and set_esc_exclusive
    helper.
  • macOS active tap returns null for Esc keydown when exclusive.
  • Windows low-level hook returns LRESULT(1) for Esc keydown when
    exclusive.
+26/-3   
Miscellaneous
hotkey.rs
Add mobile stub for Esc exclusivity                                           

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

  • Add no-op set_esc_exclusive stub for mobile platforms.
+3/-0     

@github-actions

github-actions Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

PR Reviewer Guide 🔍

(Review updated until commit cee2e66)

Here are some key observations to aid the review process:

🎫 Ticket compliance analysis ❌

853 - Not compliant

Non-compliant requirements:

  • 本 PR diff 未包含 HotkeyEvent::Cancelled 的移除
  • 未引入 esc_cancel_bridge_loop / spawn_esc_cancel_bridge
  • 未改动 macOS/Windows/Linux 监听器中的取消通道
  • 未对转写/润色取消链路做验证

77 - Partially compliant

Compliant requirements:

  • schedule_capsule_idle 增加旧定时器防护:到点若 last_capsule_state 已变化则跳过
  • 保持 dictation 与 QA 均 Idle 才 emit 的条件(对应 phase==Idle 要求)

Non-compliant requirements:

  • 本 PR diff 未显示 7 个终止态出口的调用点
  • 未显示 Done 700ms→2000ms 的统一调整

Requires further human verification:

  • 2 秒自动隐藏的实际 UI 行为需人工验证

161 - Not compliant

Non-compliant requirements:

  • 未实现 Esc 的 QA 路由
  • 未为 QA SSE 流注入取消信号
  • 未增加 on_delta 的 session_id 守卫
  • 未处理 loading 帧

Requires further human verification:

  • QA 浮窗取消/重开/流式输出的端到端 UI 表现需人工验证
⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
🧪 PR contains tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Possible Issue

maybe_request_approval 改用 tokio::select! 后,v = rx => v.unwrap_or(false) 分支在审批通道因 sender 被丢弃而返回 Err/None 时只返回 false,不再像旧代码的兜底分支那样从 less_computer_approvals 删除 token。只有超时和取消分支会清理注册表。若前端在等待审批时直接关闭(通道断开),token 会残留并导致后续同命令审批被误判为 pending 或内存泄漏。建议在 Err/None 时也执行清理。

v = rx => v.unwrap_or(false),

@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

@H-Chris233 H-Chris233 self-assigned this Jul 31, 2026
胶囊转圈时按 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 <[email protected]>
@H-Chris233
H-Chris233 force-pushed the feat/esc-exclusive-during-session branch from 5323d42 to 77cbe7c Compare July 31, 2026 13:57
@github-actions

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit 77cbe7c

- 提取 esc_exclusive_for_capsule(state, phase) 纯函数,emit_capsule 调用
- 表格测试覆盖:进行中胶囊非 Idle 独占、终止帧清除、QA(phase=Idle)不独占
@github-actions

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit 2cab0c0

- maybe_request_approval 增加取消感知:会话取消(Esc)时按 Deny 处理并清理
  审批注册表,避免审批挂起期间 Esc 被独占吞掉却毫无效果(PR Open-Less#855 场景)
- run_voice_agent_transcript 审批后若会话已取消,强制结果为 Cancelled,
  避免把第一轮拦截文本当 Done 收尾
- schedule_capsule_idle 记录触发时胶囊状态,到点时若期间有更新的 emit 则
  跳过隐藏(旧 schedule 不再覆盖新状态,消除取消路径双 emit 竞争)
- 补测试:审批取消感知、schedule 新旧状态竞争
@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit cee2e66

@H-Chris233
H-Chris233 merged commit 52e6ef4 into Open-Less:beta Aug 1, 2026
5 checks passed
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.

2 participants