fix(next): select text from transcript gaps

Merged
TE
10d ago
+1085-49

Dragging from the blank padding between transcript rows now starts or restarts text selection at the nearest visible caret. A line above contributes its end and a line below its start. Plain gap clicks clear an old selection; text and controls keep their native pointer behavior.

Leo's request:

There's these dead zones in between, like, the padding between tool calls and, like, prompts and agent messages and stuff. So we need to be able to, like, just have that not be a dead zone. Just have that also be able to, like, go into the mouse down and re-highlight, like, the text thing.

The existing transcript handler resolves mounted text once on pointerdown and extends a native range during the drag. Public fixtures cover all nine tool/prompt/agent row pairs, both side gutters, selection restart and clearing, control padding, and invisible code handoff text. Separate midpoint cases add a 32px gap only in the integration fixture, measure the actual upper/lower article bounds, and verify that each press lands outside both rows and their controls; production spacing is unchanged.

The pointerdown path now checks event.composedPath() before classifying transcript padding. Presses whose path enters an open shadow root or shadow host stay with native selection; the handler never synthesizes a range inside that boundary. The browser test server supplies Pierre's real portable workers on HTTP so HighlightedCode and EditDiffHunks render their actual diffs-container text. Regressions drag inside each component's own shadow root, assert native pointer ownership and code-only selection, then unmount React roots and terminate the test workers. Product scheme policy is unchanged.

Changelog

Fixed: Text selection can start in the gaps and padding between transcript messages and tool calls.

Observed / Not run

  • Final source head: 093474ad1e141146b2008496254cbcac71981420; whole tree ea7f132314bd5dd41ede7aa0f6a82f3dfe312947.
  • Fleet browser run 40801742241: 95 passed, 0 failed, across Chromium and WebKit. This includes all nine row-type pairs, actual inter-row midpoints, RTL, sender chips, native video play/seek ownership, real highlighted code and diff shadow roots, side gutters, selection clearing/restart and virtualization. The workflow then stopped on formatting in two changed files.
  • Formatting and full web-check recovery 40802306645: SUCCESS. Applied the formatter output for those two files; the tested formatted tree equals the final source tree above. No behavior changed after the 95-test run. Scoped pre-push gate and whitespace check pass.
  • The handler now owns only explicit transcript gap targets (scroller/thread surfaces, the composer strip, and the row's transparent padding marker). Every content descendant keeps native pointer handling. The row marker inherits the real padding, including zero-padding tool rows; no control denylist is needed.
  • The nearest mounted text is resolved only on pointerdown. Above/below anchors use the text endpoint and computed direction, while drags keep the existing bounded animation-frame and virtualization path. Pointerup, cancel, lost-button movement, blur and unmount clean up the listeners/frame.
  • Earlier red/green run 40646866503 proved the original gap drag fails on base and selects text after the fix. The final capture below reruns the same public fixture and drag on the final tested tree.
  • Final gallery and full web-check run 40805502807: SUCCESS, all 26 light/dark gallery cases healthy with zero newly broken states. The tool-to-tool gallery arm now supplies an actual 32px fixture gap, matching the existing midpoint regression; production adjacent-tool spacing remains unchanged. Snapshot 26f252bb3c2fa64c543a375614febc57078a57fb and final source share the whole tree above.
  • Native macOS build and signed app execution were not run; this change is confined to web selection behavior.

Review (independent Codex gpt-6-astra high, review_gap_final_26704): SHIP at 093474ad1e141146b2008496254cbcac71981420

The reviewer re-inspected the whole final diff, surrounding lifecycle, and final gallery-only repair from the bare mirror. Primary inspection agrees. This is a different-model read-only subagent review; fleet runs are separate evidence.

The transcript spacing follows the private Claude tool-row and Codex expanded-command references; those reference images are not published here. The gap-behavior captures below use the public agent-pane.gutter-select/gaps-ready gallery fixture, Chromium 151, 1280×560, English, Apple System Colors, and the same drag coordinates.

Before: gap drag selects nothingAfter: gap drag selects text
Before gap selectionAfter gap selection

Final checks: all 80 checks pass apart from Package dependency lists sorted in cmux-next. Exact base replay 40808066907 reproduces the identical CmuxNextHomeEngineTests: imports CmuxNextHome but does not declare it in its dependencies on the tested merge base fe31ba94c5d1eeee3c81dc1768a8a910872e9b8c. This PR changes no Swift, manifest or dependency-checker input. All four own web shards and gallery health pass; no pending checks remain.

Agent session: agent session