Skip to content

fix(ui): release a stale pane mouse gesture when its button is pressed again - #56

Merged
peinan merged 1 commit into
mainfrom
issue/53-lost-mouse-release
Oct 5, 2026
Merged

peinan merged 1 commit into
mainfrom
issue/53-lost-mouse-release

Conversation

@peinan

@peinan peinan commented Oct 5, 2026

Copy link
Copy Markdown
Owner

マウスを捕捉するペインで「離した」が届かなかったとき、残ったジェスチャーが次のクリックを飲み込む不具合を直す。

何が動いていなかったか

マウスを捕捉するペイン(Claude Code の全画面、lazygit など)で押下(Down)が届き、離す(Up)が届かないと pane_mouse_gesture が残り続けていた。handle_mouse のジェスチャー処理は、握っているボタンの Drag/Up だけを元のペインへ転送し、それ以外の Down/Drag/Up はすべて捨てる。そのため次のことが起きていた。

  • 次のクリックの Down が丸ごと捨てられ、Up だけが元のペインに届いてジェスチャーが閉じる。結果として「1 回目のクリックが無視され、2 回目で効く」。
  • ドラッグは元のペインへ、元のペインの座標系で転送される。アプリ側の選択が古い押下位置から伸びて誤った範囲がコピーされたり、別のペインで操作したのに元のペインが反応したりする。

修正

  • src/client/shell/mouse.rs:handle_mouse のジェスチャー処理の直前に、「ジェスチャーが握っているボタンの Down が来たら、まず残ったジェスチャーを解放する」判定を足した。既存のジェスチャー処理は変えていない。
    • 同じボタンの Down は「離した」が欠けた後にしか来ない。パーサは押したまま動けば Drag を出し、Windows の変換もボタンが離れた状態からしか Down を出さない。
    • 解放後の Down は通常どおり処理されるので、選択開始・ジェスチャー開始・PaneFocus がいつもどおり走る。
    • リンクの Ctrl+クリックで遅れて再生されるイベント(replaying_url_click)は対象外にした。今押している本物のジェスチャーを解放しないためである。
    • 別のボタンの Down は従来どおり捨てる。和音操作の保護のためで、既存テスト resize_invalidation_drops_stale_hits_but_preserves_gesture_release がこれを要求している。
  • 解放処理は、フォーカス喪失時の Up 合成(src/client/shell/input.rs の release_input_leases)をそのまま release_pane_mouse_gesture として mouse.rs に移し、両方から呼ぶ。保存済みの hit と last_position(最後に見えた位置)に Up を送り、ピクセル座標と popup の扱いも同じ。フォーカス喪失時の順序(Up → focus-lost 通知)は変わらない。
  • 追加の処理はマウスイベントごとの O(1) の判定だけで、描画やペイン数に比例するループには触れていない。

検証

単体テスト(src/client/shell/tests/mouse_selection.rs に 4 件追加)

  • pressing_the_held_button_on_another_pane_releases_the_stale_gesture_first:pane_1 で Down→Drag の後、Up なしで pane_2 に Down。requests は [PaneInput{pane_1, [Up(Left)@(2,1)]}, PaneInput{pane_2, [Down(Left)@(1,0)]}]、actions に PaneFocus(pane_2)、続く Up は pane_2 に届く。
  • pressing_the_held_button_again_in_the_same_pane_releases_then_presses:同じペインでは [Up@旧位置, Down@新位置] が 1 メッセージにまとまる。
  • pressing_the_held_button_again_in_the_popup_releases_then_presses:popup でも PopupInput{[Up, Down]} になる。
  • a_replayed_link_click_press_leaves_the_held_gesture_alone:再生された Down では何も送らず、ジェスチャーも変わらない。

修正を外すと前 3 件が失敗し(押下が捨てられて requests == [])、!replaying_url_click の条件を外すと 4 件目が失敗することを確認した。

全体

  • cargo fmt --check:OK
  • cargo clippy --all-targets --locked -- -D warnings:OK
  • cargo nextest run --locked --no-fail-fast:3351 件中 3349 件成功、2 件失敗。失敗は live_handoff::live_handoff_preserves_pane_process_io と live_handoff::live_handoff_keeps_unmanaged_agent_name_bound_to_saved_session で、どちらも docs/fork/FORK_WORKFLOW.md §0 の環境依存の既知失敗。
  • uv run python -m unittest scripts.test_ui_hot_path_architecture:6 件 OK

PTY ハーネス

隔離した herdr セッションに SGR マウス報告を流して確かめた。修正前は 0.9.0-fork.9、修正後はこのブランチの release ビルドである。

シナリオ 修正前 修正後
orphan --rounds 4(「離した」が欠けた押下の後、別ペインの 1 回目のクリックでフォーカスが移るか) 0/4(毎回 2 回目で移る) 4/4
orphan-drag(「離した」が欠けた押下の後のドラッグ選択で、正しい範囲がコピーされるか) 0/3 3/3
click --rounds 12 --app-in all --human --pre-motion 10 --timeout 1.5 12/12、表示 p50 98.1 ms 12/12、表示 p50 98.4 ms
real --rounds 6 --panes 3 --app-fps 10 --app-any-motion --app-render-on-mouse --drag-step 0.012 --click-delay 0.15 --timeout 1.5 失敗 0、フォーカス表示 p50 16 ms / max 39 ms 失敗 0、p50 20 ms / max 28 ms
popup --rounds 2 --drag --drag-step 0.03 --click-delay 0.2 --timeout 2 失敗 0、コピー bcdefghi 失敗 0、コピー bcdefghi

orphan-drag で、修正後にアプリが受け取ったイベント列は次のとおり。

  • 同じペイン:release@(2,1) → press@(6,3) → drag … → release@(20,4)。残ったジェスチャーを最後に見えた位置で閉じてから押下が届く。
  • 別のペイン:元のペインには release@(2,1) の 1 件だけが届き、新しいペインには press@(6,3) … release@(20,4) が届く。

修正前は、同じペインでは押下が捨てられて選択が古い押下位置 (2,1) から伸びていた。別のペインでは新しいペインに何も届かず、元のペインに新しいペインの座標のまま release@(61,4) が届いていた。

なお、ハーネスの orphan-drag の判定は「ドラッグしたペインの最初のイベントが press であること」だった。同じペインのケースでは先頭に release@(2,1) が来るため、コピー内容が正しくても WRONG と出る。表の orphan-drag の数字は、同じペインのときに限り先頭にその release を許すよう判定だけを直したハーネスの写しで取った。

残課題(今回は直さない)

  • 別のボタンのジェスチャーで「離した」が欠けた場合(右・中ボタンのパススルー)は、そのボタンをもう一度押すかフォーカスが外れるまでジェスチャーが残り、その間の左クリックは捨てられる。
  • SGR ピクセルモード中は、ウィンドウ外で離した報告が捨てられる(src/client/shell/input.rs の handle_pixel_mouse で、セルに写せない座標は return する)。このときもジェスチャーは残るが、同じボタンを押し直せば今回の修正で解放される。

refs #53

🤖 Generated with Claude Code

…d again

When the host terminal never delivers a release, the pane mouse gesture
stays open: the next press is swallowed and its drag and release go to the
old pane. A press of the button the gesture holds can only follow a lost
release, so end the stale gesture where it was last seen, as focus loss
already does, and handle the press normally. Presses of other buttons stay
swallowed so chords keep working; replayed link-click events never count.

refs #53
@coderabbitai

coderabbitai Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: e3374e23-4690-41b2-b45d-1ab0d04ba002
📥 Commits

Reviewing files that changed from the base of the PR and between acfeb2d and 08aa896.

📒 Files selected for processing (3)
  • src/client/shell/input.rs
  • src/client/shell/mouse.rs
  • src/client/shell/tests/mouse_selection.rs

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Summary

Summary by CodeRabbit

  • 不具合修正
    • マウスボタンを押したまま同じボタンの新たな押下を受け取った場合、前の押下を最後の位置で解放してから、新しい押下を処理するようになりました。
    • 別のペインへの押下時も、元のペインに解放イベントが送られるようになりました。

Walkthrough

同じボタンの再押下時に、保持中のマウスジェスチャーを最後の位置で解放してから新しい押下を処理します。ジェスチャーの解放処理を共通化し、ペイン、ポップアップ、URLクリック再生に関するテストを追加します。

Changes

マウスジェスチャーの解放

Layer / File(s) Summary
解放処理の共通化
src/client/shell/mouse.rs, src/client/shell/input.rs
ジェスチャーの最後の位置と修飾キーを使って解放イベントを作成し、対象のペインまたはポップアップへ送ります。release_input_leases は共通関数を呼び出します。
同じボタンの再押下
src/client/shell/mouse.rs, src/client/shell/tests/mouse_selection.rs
URLクリックの再生中でない場合、前のジェスチャーを解放してから新しい押下を処理します。テストは別ペイン、同一ペイン、ポップアップでのイベント順序と、URLクリック再生中にジェスチャーが維持されることを確認します。

Priority: ⬇️ Low

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Bug fix

Suggested reviewers: ogulcancelik

Merge Risk: ⚪ Minimal · up to 08aa8

The change releases a stale mouse gesture when the same button is pressed again, which fixes the swallowed press after a lost release. No merge-blocking risk was identified.

Security Architecture Review

Security architecture risk: ⚪ Minimal · up to 08aa8

The change releases an abandoned mouse interaction before accepting another click, preserving the original destination and event order. No new privileges, broader input access, or material security regression were identified.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The incremental effect is bounded to the held gesture's original target and whatever target normal click routing subsequently chooses. The inspected change adds no target-addressing capability or authority beyond existing mouse input.

Trust Boundaries and Controls

  • observed — The cleanup requires an existing gesture and a Down of its held button. URL-click replay is explicitly excluded, while unmatched button events retain the existing swallowing behavior. The replay regression test asserts that a replayed press leaves the live gesture unchanged.

Resilience and Maintainability Implications

  • observed — The helper removes the gesture before enqueueing its release, making repeated cleanup without a newly established gesture a no-op. This is local at-most-once cleanup, not an atomic or acknowledged delivery guarantee across targets.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 10 functions across 3 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed タイトルは、ボタンの再押下時に古いペインマウスジェスチャーを解放するという主な変更を明確に示しています。
Description check ✅ Passed 説明は、問題の原因、修正内容、テスト結果、および未対応のケースを記載しており、変更内容に沿っています。
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@peinan
peinan merged commit 0a4434d into main Oct 5, 2026
7 checks passed
peinan added a commit that referenced this pull request Oct 5, 2026
Log #56, #57 and #58 under a new fork.10 generation, and note in the
workflow that the 15.4 SDK is gone, with the prebuilt libghostty-vt
workaround that keeps builds and tests running until the next upstream
sync.
@peinan
peinan deleted the issue/53-lost-mouse-release branch October 5, 2026 10:38
peinan added a commit that referenced this pull request Oct 5, 2026
After fork.10, selections still followed the pointer after release in
Ghostty, but the same herdr behaved in Terminal.app and a Ghostty
restart cleared it. Note under #56 that the lost releases come from
Ghostty 1.3.1 on macOS (ghostty-org/ghostty#10974), why herdr cannot
tell them from a real drag, and how to recover when it recurs.
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.

1 participant