Skip to content

fix: preserve live selections during pane output - #57

Merged
peinan merged 3 commits into
mainfrom
issue/54-live-selection
Oct 5, 2026
Merged

peinan merged 3 commits into
mainfrom
issue/54-live-selection

Conversation

@peinan

@peinan peinan commented Oct 5, 2026 •

Copy link
Copy Markdown
Owner

何が動いていなかったか

出力が続くペイン(スピナーやログが動いているシェル、scratch popup など、herdr が選択を処理するペイン)で、マウスのドラッグ選択がすぐ消え、離してもコピーされなかった。toast も出ないため、失敗に気付けない。

原因は 2 つある。

  • src/client/shell/state.rs の surface 更新時の判定(selection_content_changed)が、前後どちらかのフレームの content revision が奇数(PTY 書き込み途中のキャプチャ)であるか、選択したセルの文字が変わると、選択を破棄していた。popup 側の同等の判定(ed84d04b で入れたセル比較)も同じ。
  • 離したときのコピー(finish_pane_selection → request_selection_copy(outcome, false))が content revision 付きで送られていた。間に出力が 1 回でも挟まると、サーバが stale_content で拒否し、何も表示されずに失敗する。

upstream は 963f78f「fix: preserve live selections during pane output」(herdrdev#4193)で両方を直している。フォークのベースはそれより前なので入っていなかった。

修正

963f78f を cherry-pick し、フォーク独自の popup 選択と popup の copy mode にも同じ方針を適用した。

  • 明示的な選択(マウスのドラッグ、copy mode の v / Space / V)はバッファ上の範囲として保持する。破棄するのは、サイズ変更と normal/alternate 画面の切り替えのときだけにする。
  • 明示的な選択のコピーは、revision を付けずに今その範囲にあるテキストを読む。検索結果へのフォールバックとダブルクリックの単語コピーは、従来どおり revision を確認する。
  • サーバ側は変更していない。selection_text_for は revision なしを live 読みとして扱い、popup.selection.read もそのまま渡す。

衝突の解消

  • actions.rs:フォークの .target_content_revision(&pane_id)(popup にも対応)を残し、コメントだけ upstream の文言にした。
  • copy_mode.rs:target_content_state(id) -> Option<(u64, bool)> を足した。popup はメトリクスから、ペインは surface から revision と alternate 画面フラグを取る。target_content_revision はこれを使う形にし、enter_copy_mode は alternate_screen_active も記録する。exit_copy_mode は upstream のまま当たった。
  • mouse.rs:フォークは離したときの処理を finish_pane_selection(ペインと popup で共通)に切り出しているので、そこで request_selection_copy(outcome, true) にした。
  • state.rs:
    • upstream が消した pane_surface_row / selection_cells_unchanged に加え、使われなくなるフォーク独自の popup_surface_row / popup_selection_cells_unchanged も消した。
    • selection_content_changed を selection_invalidated に改名した。ペイン側は幅・高さ・alternate 画面だけを見る。upstream の word-gesture 条件は、フォークに a0474ae(word_selection_gesture)が無いので取り込んでいない。
    • popup 側は resized、screen_switched、「このフレームと対になるメトリクスが無ければ破棄」を残し、content_moved を消した。
    • copy mode の再同期は、popup とペインで共通のタプルに alternate_screen_active を足した。サイズか画面が変わったときだけ copy_mode.selection を消す。revision が変わっただけなら、revision・サイズ・画面フラグの更新と検索状態のクリアだけを行う。upstream 側の末尾(pane.alternate_screen_active を参照する行)は衝突マーカー無しで入るので、手で書き直した。
  • tests/mouse_selection.rs:衝突範囲は upstream の word-drag テスト(a0474ae7、フォークには無い)なので、フォーク側を残してテスト名の変更だけを取り込んだ。本文の変更はそのまま当たった(upstream の selection_repaint_deadline はフォークに無いので、assert!(drag.repaint) のまま)。
  • docs(docs/next/website/src/content/docs/{,ja/,zh-cn/}keyboard.mdx)はそのまま当たった。

popup の copy mode のサイズ(追加コミット c07d8e1)

レビューで、この PR が持ち込んだ popup だけの退行が見つかったので直した。

  • popup の copy mode は、入ったときのサイズを当たり判定の矩形(クライアントが計算した領域)から記録し、再同期では popup フレームの寸法と比べていた。以前は revision が変わるたびに copy mode の状態が消えていたので、この食い違いは表に出なかった。
  • フレームがクライアントの計算した領域に遅れて追いつくと(サイドバーの開閉、端末のサイズ変更、リモートの往復)、追いついたフレームがちょうど入ったときのサイズと同じなので、サイズ変更に見えない。古いフレーム上の copy mode の選択が残り、次のキーで復元され、y が折り返し直す前の範囲を revision なしで読んでいた。
  • enter_copy_mode は、popup については popup フレームの寸法を記録するようにした。再同期が比べるのと同じ値になる。ペインはこれまでどおりペインの内側の矩形を使う(再同期も同じ値と比べる)。
  • これで、copy mode に入った直後の最初のフレームで選択が消える問題もなくなった。
  • 当たり判定とフレームの寸法が食い違っている間(サイズ変更の途中、別クライアント向けの寸法)は、copy mode のカーソルとハイライトを入った直後から描かない。以前も、最初のフレームが届いた時点で同じ状態になっていた。

テスト

  • upstream のテスト(キーボード選択の 3 本と、既存テストの期待値の変更)を取り込んだ。
  • popup 向けに次を直した・足した。
    • popup_output_inside_the_selection_drops_it を popup_output_inside_the_selection_keeps_the_live_range に反転した。偶数・奇数の revision の両方で範囲が残り、離すと revision なしで読みに行くことを確かめる。
    • popup_selection_copies_through_endpoint_extraction に content_revision.is_none() を加えた。
    • upstream のキーボード選択 3 本の popup 版を足した(popup_keyboard_selections_survive_output_and_copy_live_ranges / popup_empty_keyboard_anchor_keeps_search_fallback_revision_guard / popup_keyboard_selection_does_not_return_after_resize_or_screen_switch)。
    • 「対になるメトリクスが無いフレームでは破棄する」分岐を確かめるテストが無かったので、a_popup_frame_without_its_report_drops_the_selection を足した。
    • popup の copy mode のテストは、既存の surface_with_popup() を使う。この fixture のフレーム(10 列)は、クライアントが計算する領域(枠 2 と予約列 1 を除いた 9 列)より 1 列広く、フレームが追いつく前の状態にあたる。
    • popup_copy_selection_drops_when_a_lagging_frame_catches_up:追いついたフレームで copy mode の選択も消え、l で復元されず、y が何も読まないことを確かめる。
  • alternate 画面で入った copy mode の選択が、同じ画面の出力で消えないことを確かめるテストを、ペインと popup に足した(keyboard_selection_on_the_alternate_screen_survives_output / popup_keyboard_selection_on_the_alternate_screen_survives_output)。
  • ダブルクリックの単語コピーが revision を付けて読むことを、client_double_click_selects_and_copies_endpoint_row_word で確かめるようにした。
  • popup_output_outside_the_selection_keeps_the_drag のコメントを、セル比較の説明から live 範囲の方針に直した。
  • 変更を 1 つずつ元に戻すと、対応するテストが落ちることを確かめた(popup の revision 変化で破棄、copy mode の画面切替の判定を外す、copy mode を revision 変化で破棄、離したときに revision 付きで読む、対のメトリクスが無くても保持、popup の copy mode のサイズを当たり判定から取る、入るときの画面フラグを記録しない、ダブルクリックの単語コピーを revision なしにする)。

検証

worktree の vendor/libghostty-vt/zig-out は main のビルド済みを複製し、ZIG=/usr/bin/true で cargo を回した(ローカルの zig/SDK が壊れているため)。

  • cargo fmt --check:OK
  • cargo clippy --all-targets --locked -- -D warnings:OK
  • cargo nextest run --locked --no-fail-fast:3354 件中 3353 件成功。失敗は live_handoff::live_handoff_preserves_pane_process_io の 1 件で、main でも落ちる既知の環境依存テスト。
  • uv run python -m unittest scripts.test_ui_hot_path_architecture scripts.test_docs_translation_parity:OK
  • cargo build --release --locked:OK
  • 追加コミット後(main の取り込み込み):fmt と clippy は OK。client::shell 327/327、client:: 568/568、全体は 3361 件中 3360 件成功(失敗は上と同じ live_handoff_preserves_pane_process_io)。リリースビルドでハーネスの copy --rounds 5 --text-mode fast-spinner --drag-step 0.04 は 15/15、popup --rounds 2 --drag --drag-step 0.03 --click-delay 0.2 --timeout 2 は bcdefghi をコピー、クリック失敗 0。
  • PTY ハーネス(隔離セッション、diag_mouse.py)。before は導入済みの 0.9.0-fork.9、after はこのブランチの最初のコミットのリリースビルド。
シナリオ before after
copy --rounds 5 --text-mode fast-spinner --drag-step 0.04 0/15 一致(コピー自体が発生しない) 15/15 一致
selhold --rounds 4 --text-mode fast-spinner ハイライト 0/15〜16 サンプル、コピー 0/4 ハイライト 全サンプル(16/16〜17/17)、コピー 4/4
copy --rounds 3 --text-mode static 9/9 9/9
copy-click --rounds 6 6/6 フォーカス(p50 14.3 ms / max 22.3 ms) 6/6(p50 12.3 ms / max 19.0 ms)
popup --rounds 2 --drag --drag-step 0.03 --click-delay 0.2 --timeout 2 bcdefghi をコピー、クリック失敗 0 同じ
consistency --rounds 3 --panes 3 --drag 古い表示 0 0
wide 差分行 0 0
(参考)copy --rounds 3 --text-mode append --drag-step 0.04 9/9 9/9

参考の append は、この行数では実行中にビューがスクロールしなかった。--rows 30 --rounds 2 でスクロールさせた回は before 5/6、after 6/6。before の 1 件は、ドラッグ中に 1 行スクロールしてハーネスの固定座標が次の行を指したもので、選択の消失ではない。どちらも 1 回だけの試行で、ドラッグとスクロールが重なるかはタイミング次第。

残課題・注意点

  • スクロールバックが上限に達した状態で出力が続くと、先頭の行が追い出された分だけ live 範囲がずれる。upstream が意図的に受け入れたトレードオフで、今回もそのまま。
  • ダブルクリックの単語コピーは従来どおり revision を確認する(upstream も同じ)。出力が続くペインでは、ダブルクリックのコピーが stale_content で失敗しうる。
  • この移植は、フォーク自身の ed84d04(popup のセル比較)を巻き戻す。
  • 次の upstream 同期では、ペイン側は upstream 版(word-gesture 条件を含む)を採り、popup 側はこのブランチの版を残す。
  • 以下はこの PR では直していない(別 issue の候補)。
    • 端へのドラッグによる自動スクロールは、始めたときの offset と最大値を持ち続ける(mouse.rs の selection_scroll_metrics と tick_selection_autoscroll)。出力でドラッグが打ち切られなくなったので、履歴を遡った状態で追記が続くペインでは、ビューが最新側へ飛ぶことがある。upstream(現在の master)も同じコードなので、upstream に報告する候補。
    • popup のフレームが対になるメトリクス無しで届くと、消えるのは self.selection だけで、copy mode の anchor は残る。その間に y を押すと、検索結果があればそれにフォールバックし、メトリクスが無いので target_content_revision が None になり、revision の確認なしに読む。既存の挙動で、起きるのはまれ。
    • プラグインのアクションは選択を Some(revision) 付きで送る(actions.rs の PluginAction)。copy mode の選択が出力で消えなくなったため、出力が続くペインで copy mode からプラグインのアクションを呼ぶと stale_content で拒否される。upstream から引き継いだ挙動。
    • pane_scroll_targets は offset がちょうど一致したときにしか消えない(state.rs の set_pane_surface)。履歴を遡った状態で追記が速いペインでは、copy mode の offset が同期しなくなり、残っている選択が古い投影として描かれなくなる。y ではその選択をコピーできる。既存の挙動。

refs #54

🤖 Generated with Claude Code

Port of upstream 963f78f (herdrdev#4193), adapted to the fork's
popup selection and popup copy mode: explicit selections are live buffer
ranges dropped only on resize or a screen switch, and mouse copies read
the live range instead of a revision the server rejects once output lands.

refs #54
(cherry picked from commit 963f78f)
@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: a6cb1e5a-bb4b-4101-afdd-b0b29c8b6f3b
📥 Commits

Reviewing files that changed from the base of the PR and between 0a4434d and c07d8e1.

📒 Files selected for processing (10)
  • docs/next/website/src/content/docs/ja/keyboard.mdx
  • docs/next/website/src/content/docs/keyboard.mdx
  • docs/next/website/src/content/docs/zh-cn/keyboard.mdx
  • src/client/shell/actions.rs
  • src/client/shell/copy_mode.rs
  • src/client/shell/mouse.rs
  • src/client/shell/state.rs
  • src/client/shell/tests/copy.rs
  • src/client/shell/tests/mouse_selection.rs
  • src/client/shell/tests/popup_focus_projection.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

コンテンツ更新後も、マウスやキーボードで行った選択を維持するよう変更しました。コピー時は現在の選択範囲を読み取ります。サイズ変更や通常画面・代替画面の切り替えでは選択を解除します。検索結果由来の選択には、取得時のリビジョンを使います。

Changes

コピー選択の動作

Layer / File(s) Summary
選択の保持と無効化条件
src/client/shell/state.rs, src/client/shell/tests/mouse_selection.rs, src/client/shell/tests/popup_focus_projection.rs
コンテンツリビジョンや選択セルの内容が変わっても選択を維持します。ジオメトリまたは画面状態が変わった場合は選択を解除します。ポップアップに対応するメトリクスがない場合も選択を解除します。
ライブ範囲を使うコピー要求
src/client/shell/copy_mode.rs, src/client/shell/mouse.rs, src/client/shell/actions.rs, src/client/shell/tests/copy.rs
明示的な選択ではライブ範囲をコピー要求に渡します。検索結果由来の選択では取得時のリビジョンを使います。ペインとポップアップの出力、リサイズ、画面切り替えに対する動作をテストします。
動作確認とキーボード文書
src/client/shell/tests/copy.rs, docs/next/website/src/content/docs/{keyboard,ja/keyboard,zh-cn/keyboard}.mdx
テストと3言語の文書に、出力後の選択保持、コピー時のライブ範囲の読み取り、および選択の解除条件を反映しました。

Priority: ➖ Normal

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

Change: Bug fix

Sequence Diagram(s)

sequenceDiagram
  actor User
  participant CopyMode as コピーモード
  participant CopyRequest as request_selection_copy
  participant Buffer as ペインまたはポップアップのバッファ
  User->>CopyMode: 選択を確定
  CopyMode->>CopyRequest: 選択種別を渡す
  CopyRequest->>Buffer: 明示選択の現在の範囲を読み取る
  CopyRequest->>Buffer: 検索結果を取得時のリビジョンで読み取る
Loading

Suggested reviewers: ogulcancelik, jjliebig

Merge Risk: ⚪ Minimal · up to c07d8

The selection and copy behavior appears ready to merge after normal checks.

Security Architecture Review

Security architecture risk: 🔵 Low · up to c07d8

The change affects which text is copied, rather than granting additional access. No introduced security defect was established, but late copy completion and restart behavior remain partly unverified.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — A process able to change the addressed terminal buffer can influence text copied from an explicit range after selection begins. The supported outcome is replacement of clipboard contents through the existing copy action, not automatic execution or demonstrated access to another target.

Trust Boundaries and Controls

  • observed — Revision-free copying relaxes content freshness, not target resolution. Missing pane runtimes and replaced popup identities still fail through their existing endpoint checks.

Resilience and Maintainability Implications

  • observed — Ordinary pane allocation uses an increasing global counter; public pane numbers advance rather than recycling on removal, and dead-pane aliases are removed. Successful asynchronous copy completion has no selection-generation check, a pre-existing limitation rather than a demonstrated new defect.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 84.38% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 32 functions across 7 files. (3 skipped: 3 …
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.
Title check ✅ Passed タイトルは、出力中も選択範囲を維持するという主な変更を簡潔に示しています。
Description check ✅ Passed 説明は、変更の背景、実装方針、popup 固有の修正、テスト結果と既知の制約を具体的に記載しています。変更内容と一致しています。
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Commit to this branch
  • Create a new PR
📝 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 added 2 commits October 5, 2026 13:22
Popup copy mode recorded the client's hit size on entry but resynced
against the popup frame size. Now that output keeps copy-mode selections,
a popup resized to exactly the entry hit size no longer looked like a
resize, so a pre-reflow selection survived and copied the wrong range.
Record the frame size on entry, which also stops the first frame after
entering from dropping a selection.

refs #54
@peinan
peinan merged commit a467b7a into main Oct 5, 2026
6 of 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/54-live-selection branch October 5, 2026 10:38
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