Skip to content

fix(selection-polish): 選區潤色預覽確認按鈕偶發無反應 - #1045

Closed
leonoxo wants to merge 3 commits into
Open-Less:betafrom
leonoxo:fix/selection-polish-preview-confirm-beta
Closed

leonoxo wants to merge 3 commits into
Open-Less:betafrom
leonoxo:fix/selection-polish-preview-confirm-beta

Conversation

@leonoxo

@leonoxo leonoxo commented Sep 9, 2026

Copy link
Copy Markdown

問題

選區潤色(Selection Polish)預覽窗的「確認替換」按鈕有兩類偶發失敗:

  1. 第一擊被吞:確認窗是 Nonactivating NSPanel,點「確認替換」的第一下只讓面板取得 key,不會觸發按鈕;要點第二下才生效。
  2. 誤判選區改變(SelectionChanged)而不替換:confirm 時原 app 被扶回前台,但我們自己的預覽窗仍持有 keyboard focus;校驗用的模擬 Cmd+C 打進我們自己的預覽窗(沒有選區)→ 剪貼簿為空 → 誤判「選區已改變」,替換被拒。

根因(build22/23 實機 log 坐實)

  • first-click:Web view 不支援 acceptFirstMouse,正解是點擊派發前把面板扶成 key。
  • no-op:log 中失敗 run 的 focused_pid=<self:preview-window>front=Hermes 已過——reactivate() 只查 NSWorkspace 前台 pid,不對預覽窗 resignKey,Cmd+C 的 HID 事件打給「握鍵盤焦點的視窗」。
  • 伴隨崩潰:first-click monitor 的 closure 漏 move,以引用捕捉區域 panel → 函數返回後 dangling stack reference → ObjC foreign exception 越 FFI → Tao runloop abort(SIGABRT)。

修法(本 PR 13 檔)

1. First-click guard(src-tauri/src/lib.rs,僅 selection-polish 預覽窗)

  • 本地 left-mouse-down monitor:點擊派發前,若事件目標是預覽窗,先 makeKeyWindow 再原樣放行事件(additive、不合成/重播點擊)。
  • 目標視窗以 windowNumber + 固定視窗標題雙重驗證(不抓 NSPanel 裸指標);每次預覽窗 show 都重試/刷新註冊——早期註冊失敗可復甦、stale window number 不會誤扶其他視窗。
  • monitor callback 內每個 AppKit 調用雙層邊界:catch_unwind(Rust panic)+ objc2::exception::catch(ObjC exception);closure 以 move 捕捉全部狀態(修崩潰)。

2. Apply 前同步 resignKey(core_adapters.rs + lib.rs

  • confirm 時先把預覽窗 resignKeyWindow(log apply: preview resigned key before reactivate was_key=true),再 reactivate() 原 app、校驗、貼上。
  • 主執行緒直執行 fast path;worker 線程才 dispatch 回主執行緒等待。無 timeout——AppKit 操作一旦排入無法取消,timeout 只會造成「回報失敗但操作仍晚到執行」的 race。

3. 貼上前最後防線(selection.rs + core_adapters.rs

  • validate_selection_insertion_target 的模擬 Cmd+C 兜底含 200ms 重試,期間焦點可能跳走;而「另一個視窗恰好暴露相同選區文本」時文本比對會放行。因此在 insert() 前一刻重讀前台 app 的 pid + name,與捕獲時不符即拒絕(selectionPolishTargetChanged)。純增量,不改既有校驗。

4. 選區潤色預覽窗標題改用繁體(src-tauri/src/lib.rs,commit 4f1772ce

選區潤色預覽窗標題原先是簡體「OpenLess 选区润色预览」,與預設 zh-TW UI 不一致。改為繁體「OpenLess 選區潤色預覽」,並同步更新 first-click guard 的視窗標題身份常數(guard 靠 windowNumber + 標題雙重驗證,兩者必須一致)。

5. 腳手架回顯剝離保守化(crates/openless-core/src/streaming_insert.rs

  • 舊版見「活標籤 + 已知導語子串」就裁切,會誤傷模型合法產出的 XML(範例/指令情境)。
  • 新版要求完整 canonical 模板(直接由 prompts.rsuser_prompt/selection_user_prompt 衍生,含 OpenCC 繁體變體,不會與 prompt 漂移)整段吻合、且 close 後只允許已知結尾,才剝離;任何額外前後文字、多信封、合法 XML 一律原樣保留。9 項測試含「合法 XML 不被觸碰」。

驗證

靜態

  • openless-core 全套:763 passed / 1 ignored(含 9 項 scaffolding 契約、selection-envelope prompt 契約)
  • Frontend:71 項全過;tsc --noEmit 通過
  • src-tauri cargo check 通過;git diff --check 通過
  • 五輪獨立 fail-closed code review,第五輪 passed=true(前四輪 12 項問題全數修復:monitor 錯置/佔用 flag/stale pointer/主執行緒死鎖/timeout race/panic 邊界/session 跨取消 race/導語錯字與歧義裁切 等)

實機(macOS,build22/23/25)

  • build22(resign 修法):9/9 confirm 成功,0 SelectionChanged;伴隨 1 次崩潰(first-click closure 漏 move,已修)
  • build23(+move):5/5 全第一擊 confirm,0 新崩潰
  • build25(本 PR 最終候選):≥8 次 confirm 全成功:每次 confirm 前命中 first-click guard: panel made key before first mouse-down(第一擊生效)、resigned key was_key=truepost_copy: post_ok=true;0 SelectionChanged / 0 TargetChanged / 0 ClipboardEmpty / 0 崩潰

已知限制(reviewer 4 項 non-blocking 建議,留待後續)

  • selection_target_still_front 通過到實際貼上 key stroke 之間仍有極小 check-then-act 窗口(遠小於舊 200ms 窗口,且新增 pid 比對嚴於舊版)。
  • resign_selection_polish_preview_key_for_applyrx.recv() 無 timeout:今日安全(isMainThread fast-path 避免自死鎖,apply 確認在 tokio worker 線程),但可加防禦性 timeout。
  • first-click monitor 常驻生命週期(無 removeMonitor):對 menu-bar app 量極低且 fail-safe(事件一律放行),可考慮 preview 銷毀時拆除。
  • assemble_polish_system_prompt(settings 頁診斷)硬編 UserEnvelope::RawTranscript:現行無影響,若未來加 selection 型 style-pack 預覽需同步。

圈選潤色的「確認並替換」偶爾沒反應,根因鏈有四個,分別處理:

1. 预览窗 show 抢焦点:show_selection_polish_preview 走 window.show() +
   set_focus()(tao 层 makeKeyAndOrderFront + NSApp.activate),把 OpenLess
   推成 frontmost;原 app 丢前台后编辑器的选区被清,之后 confirm 的
   reactivate/validate 就「有时」失败。预览窗改成与胶囊/QA 同手法的
   Nonactivating NSPanel(to_panel + styleMask 先读再 OR NonactivatingPanel
   位——set_style_mask 是全量替换;level 3 + FullScreenAuxiliary/CanJoinAllSpaces
   叠全屏 app),show 走主线程 orderFrontRegardless,不 makeKey、不激活 app。
2. 失败即杀 session:confirm 任何 Err 都 fail_if_active → Failed + 隐藏预览,
   用户刚编辑的内容直接丢,只剩「点确认没反应」。分流:session 失效
   (Cancelled/InvalidState/InvalidArgument/Busy)才结算失败并隐藏;
   瞬时平台错误(焦点恢复/目标复核抖动)回退 Preview 保持可重试。
   fail_if_active 加相位保护:只结算 Capturing/Preview/Applying,已
   Completed 的不再被误标 Failed(complete 与 fail 之间的窄窗口)。
3. reactivate 固定 120ms:部分 app(Electron、自绘窗口)恢复 key window
   需要更久,固定 sleep 一次就核 pid 会偶发误判「恢复失败」。改成
   80ms x 4 轮询,每轮补一次 activate(幂等),pid 稳定立刻返回。
4. 前端 busy 卡死:预览窗复用(hide 不卸载组件),上一轮 confirm/cancel
   后 busy 停留 true,下一轮两个按钮全 disabled。「shown」事件到达时
   复位 busy/error。

验证:cargo check/test openless-core(含契约测试
transient_platform_failure_keeps_the_preview_retryable)全绿;
src-tauri 本机因缺 Metal 工具链(qwen3-asr-rs 需要完整 Xcode)无法整树
check,新窗口代码已用同版本依赖(tauri 2.11.5 / tauri-nspanel v2 /
objc2 0.5.2)的独立 crate 编译验证通过;前端 tsc + 71 个前端测试全绿。
macOS 实机行为(预览不抢焦点、confirm 重试)需要 CI(submodules: recursive)
或本机装完整 Xcode 后验证。
@appergb

appergb commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

抱歉,我们写得很清楚:目前正在进行 2.0 的优化,所以暂不接受任何 PR。如果你希望 PR 能继续推进并成功完成,可能需要等我们完全迁移到 2.0 的新后端、新前端之后,再针对这个问题进行解决。

@H-Chris233

Copy link
Copy Markdown
Collaborator

抱歉,我们写得很清楚:目前正在进行 2.0 的优化,所以暂不接受任何 PR。如果你希望 PR 能继续推进并成功完成,可能需要等我们完全迁移到 2.0 的新后端、新前端之后,再针对这个问题进行解决。

我先合你的,再合ta的,就这么简单

@Open-Less Open-Less deleted a comment from chatgpt-codex-connector Bot Sep 9, 2026
@leonoxo
leonoxo marked this pull request as draft September 10, 2026 06:29
- First-click guard for the non-activating selection-polish preview
  NSPanel: a local left-mouse-down monitor makes the panel key before
  the first click lands, so the confirm button responds on the first
  press instead of swallowing it.
  - Targets the preview via windowNumber + fixed window title
    (never a raw panel pointer), retargeted on every preview show so
    registration retries survive an early failure and stale window
    numbers cannot redirect the make-key to another window.
  - Every AppKit call in the monitor callback is double-wrapped
    (catch_unwind + objc2::exception::catch); the closure moves all
    captured state (fixes a dangling stack reference that crashed
    the main runloop under Tao).
- Apply path: resign the preview panel key synchronously before
  reactivating the original target, so the simulated Cmd+C used by
  selection revalidation hits the original app's focused window
  instead of our own preview panel (root cause of false
  SelectionChanged rejections / empty-clipboard misses).
  Main-thread fast path, dispatch only from worker threads; no
  timeout (a timed-out AppKit call could still fire late).
- Re-validate the captured frontmost app (pid + name) immediately
  before the insert keystroke; the 200 ms simulate-copy retry inside
  validation could otherwise let focus move to a window exposing the
  same selected text and paste into the wrong target.
- Scaffolding echo stripping now requires the complete canonical
  prompt template (Simplified + OpenCC Traditional variants, derived
  from prompts.rs so it cannot drift) to match before any content is
  removed; any extra prose or legitimate XML output is returned
  unchanged (9 tests, including legitimate-XML-not-touched).
@leonoxo
leonoxo marked this pull request as ready for review September 13, 2026 00:05
The window title (and the first-click guard identity constant that must
match it) was Simplified while the app UI defaults to zh-TW. Title and
guard constant are changed together to keep identity validation intact.
@leonoxo

leonoxo commented Sep 13, 2026

Copy link
Copy Markdown
Author

Superseded by #1073 (rebased to UI 2.0 base, Seg-1 only). Seg-2 UE chain remains FROZEN.

@leonoxo leonoxo closed this Sep 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants