Repository navigation
Agent chat: a drag from the margin beside the column selects text - #24326
Conversation
…xt (red on base) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a16030-bf28-7072-b1f6-b83bbd0beacc&host=cosmic-gray
A press in the empty gutter either side of the centered transcript column starts a selection at the caret beside it (x clamped into the column), and the drag extends it with setBaseAndExtent to the caret under the pointer. A plain click does nothing. Listeners live only for the drag (mouseup, blur, unmount); one caret hit test per animation frame. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a16030-bf28-7072-b1f6-b83bbd0beacc&host=cosmic-gray
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a16030-bf28-7072-b1f6-b83bbd0beacc&host=cosmic-gray
… selection; tests read as a document's margin A press left of a line starts at its start, right of it at its end. The selection follows a wheel scroll under a still pointer, and the edge scroll stops at the view's end. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a16030-bf28-7072-b1f6-b83bbd0beacc&host=cosmic-gray
|
cmux status (one comment, edited in place; each line is updated by the job that owns it) Review App of this head (app.zip, kept 1 day)
Gallery before/after for
States
Pick other states with a Passes: CI passes on CI passes on Written by red: cmux-next checks (god files, concurrency, crash safety, l10n); NEW: Tour of |
|
Owned by transcript-parity on cosmic-gray · session |
|
Found 8 test failures on Blacksmith runners: Failures
|
|
Bead |
…resses that only close a menu; edge scroll is the reader's Review fixes: a plain click in the margin blurs the composer as a press on the background always has (composerFocus.ts contract), a press that only closes a composer menu starts no selection (menuPressGuard swallowsPress), and an edge-scroll step tells the transcript it is the reader's scroll so follow-latest does not pull the view back. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a16030-bf28-7072-b1f6-b83bbd0beacc&host=cosmic-gray
|
Review (subagent, correctness first): SHIP at 1714a95 An independent Claude subagent read the full diff from f61b6a1 (read only). The first pass at c839704 returned BLOCK: a plain margin click had stopped blurring the composer, which changed the
Coverage gaps, non-blocking: no test of the edge-scroll step itself, and the follow test moves |
|
Review (codex xhigh, correctness first): BLOCK at 1714a95
Verification: |
|
Orphaned: no activity for 13 h, and no lane holds a lease on it. Adopted by the orphan sweep: merged |
|
|
|
|
|
|
…l it lets go Codex review: the transcript virtualizes rows, so a long margin drag that scrolled its start row out unmounted the anchor and collapsed the selection. The start row is held mounted from the drag's first move, and both end rows after the button lifts, until the selection is cleared or no longer starts there (one selectionchange listener, at most two rows). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a16030-bf28-7072-b1f6-b83bbd0beacc&host=cosmic-gray
…utter-drag-select
…e per chat and survive older history paging in Review fixes: lastPlaced spans only the scrolled rows (a held row far from the view made every streamed chunk's height search walk the rows between), a hold from another chat mounts nothing, and a held row is also found by its distance from the end when older history pages in above it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a16030-bf28-7072-b1f6-b83bbd0beacc&host=cosmic-gray
…t by placing every row again Review follow-up: a held row still streaming (a margin drag beside the live reply, then a scroll far up) changed height outside the search window, so each chunk placed every row from the first. Its index is now looked up directly (at most two rows). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a16030-bf28-7072-b1f6-b83bbd0beacc&host=cosmic-gray
|
Review (subagent, correctness first): SHIP at 41176d9 An independent Claude subagent read the code only and re-reviewed every head since c839704. All its findings are fixed:
The lookup costs a fixed amount, and its fallback after a prepend is still correct. Coverage gap, not blocking: no test of a held row that is still streaming; that path can only fall back to a full layout. |
Showing another chat releases the margin drag's controller (ends any drag, drops its selectionchange listener) and clears the held rows, so a hold made in one chat never pins rows when that chat is shown again. A held row that leaves the document also lets go. Browser test fails without the release. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a16030-bf28-7072-b1f6-b83bbd0beacc&host=cosmic-gray
|
Review (subagent, correctness first): SHIP at ef29982 The session-switch fix is correct. The sessionChanged branch ends any drag (window listeners, rAF, selectionchange listener) and clears heldRows. A held row that React unmounts releases on the next selectionchange. |
Bugbot needs on-demand usage enabledBugbot uses usage-based billing for this team and requires on-demand usage to be enabled. A team admin can enable on-demand usage in the Cursor dashboard. |
|
|
…test waits A held row that neither its index nor its distance from the end finds (rows paged in above and arrived below in one snapshot) is looked up by id. The render stays pure: an effect keyed on where the rows were found writes the corrected positions back (dropping a row that is gone), so later renders read them directly again. The browser test now waits for rows to mount before it reads the scroller (a WebKit CI race), covers the both-ends case (red without the lookup), and checks that a margin click away from any text still takes focus off the composer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a16030-bf28-7072-b1f6-b83bbd0beacc&host=cosmic-gray
…nto tp/gutter-drag-select
|
|
|
|
… redrawn The drag's anchor is a collapsed live Range instead of a raw node and offset, so a streaming reply that replaces or shortens the text under the press moves the start with the DOM rather than leaving it detached or out of range. If the start leaves the transcript entirely, the drag ends instead of edge-scrolling on with nothing to select. Browser test: the first message's markdown is redrawn mid-drag (red on the previous code in both engines). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a16030-bf28-7072-b1f6-b83bbd0beacc&host=cosmic-gray
The drag pins the row its press reads in. The start must stay connected inside that row: a row taken out moves a live Range's point to the thread, which would otherwise start the selection somewhere else. Browser test: the first message leaves the transcript mid-drag (red on the previous code in both engines). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a16030-bf28-7072-b1f6-b83bbd0beacc&host=cosmic-gray
A selection the margin drag left keeps both end rows. When either row leaves the transcript, the DOM moves that end to text the reader never chose, so the selection is cleared and the rows released. Browsers don't all send selectionchange for that move, so a MutationObserver on the held rows' parents (childList only, live only while rows are held) checks the two rows too. Browser tests: the start or the end message is removed after the drag (red on the previous code in both engines). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a16030-bf28-7072-b1f6-b83bbd0beacc&host=cosmic-gray
A selection the margin drag left whose end the reader moves to another row (a Shift-click) is theirs now: the held rows and the removal observer let go, as they already did when its start moved, so taking out the old end row no longer clears the reader's new selection. Browser test red on the previous code in both engines. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a16030-bf28-7072-b1f6-b83bbd0beacc&host=cosmic-gray
|
Review (codex xhigh, correctness first): SHIP at e3a8e52 Diff-only Codex review at xhigh (cmux16, Earlier Codex BLOCKs on this PR, all fixed with browser tests that fail without the fix in both engines: |
Conflict in VirtualTranscript's mounted ranges: base keeps a fold's anchor row mounted (foldAnchor.ts); the branch keeps the margin selection's end rows. Both now feed the held indices, which mount single rows and are measured by index. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a16030-bf28-7072-b1f6-b83bbd0beacc&host=cosmic-gray
|
Review (codex xhigh, correctness first): SHIP at 783aac8 Delta review of the hand-resolved base merge, on top of the SHIP at e3a8e52. Codex xhigh on cmux16, diff-only, At this head:
The reds at this head are base reds:
|
|
Shipped in:
|
![Fix with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/fix-with-codesmith-light-3x.png)
Leo: "also i want click and drag to highlight text even when im just clicking on the left or right of the main pane/out of the chat portion column... it's allegeldy prob something with the entier html and the cursor detecting... stuff"
What changes
In the agent pane, the transcript column is centered. A drag that started in the empty margin beside it selected nothing. Now the margins work like a document's:
composerFocus.ts). The click also lets go of an existing transcript selection, as a click in a document's margin does.menuPressGuard.tsnow exportsswallowsPress).reader), so follow-latest does not pull the view back during the drag.heldRows, at most two) until the selection is cleared or no longer starts there.Why CSS alone was not enough
The page is
user-select: none, and messages opt back in with.selectable(shared/desktop.css). Rows are absolutely placed by the virtualizer, so a press in the margin hits.acpmux-scrollor.acpmux-spacer, and the browser's own hit test finds no line beside it. Making the margins selectable would anchor the selection at the thread's start, not at the line beside the press. No overlay orpointer-eventslayer was in the way.So
conversation/gutterSelect.tstakes a press in the margin and reads the caret withcaretPositionFromPoint(falling back tocaretRangeFromPointfor WebKit), with x clamped one pixel inside the column's content box and y clamped above the bottom stack. It then callsSelection.setBaseAndExtent. It is installed with the transcript scroller's other input listeners (VirtualTranscript, App.tsx, +5 lines).Cost and stop conditions
mousemove/mouseup/blurlisteners and the scrollerscrolllistener are added on a margin press. They are removed on mouseup, on a move with the button up (released outside the page), on window blur, and when the transcript unmounts.requestAnimationFrameat a time. Each frame does one caret hit test and reads one row's box; there is no per-row work and nothing per mousemove beyond scheduling the frame.Range, so the DOM moves its start with a redrawn message. The drag pins the row its press reads in; if that row is taken out, the drag ends.selectionchangelistener keeps a finished drag's two end rows mounted. AMutationObserveron those rows' parents (childList only, no subtree) clears the selection if either row is taken out; it is disconnected with the listener. It is removed when the selection collapses or leaves its start row, on the next margin press, when the transcript shows another chat, when its row leaves the document, or on unmount. Holding a row reads its ownaria-posinset, with no walk of the rows.Size
About 1210 changed lines, over the usual 800. The product code is about 410 lines (
gutterSelect.ts302, App.tsx 82, menuPressGuard.ts 26). The rest is tests (529) and the gallery entry (93). Splitting the tests off would land the behaviour without them, so this stays one PR.Tests
webviews/test/transcript-gutter-select-browser.test.ts: real Chromium and WebKit through Playwright, on the transcript harness, with 16 cases per engine:conversation/gutterSelect.test.ts: the clamp, gutter, threshold and edge-scroll math.test/menu-press-guard.test.ts:swallowsPressis true only during a press that closes a menu.Red on base: commit 067d196 (the test only) with App.tsx at merge base f61b6a1. The margin drags selected
""in both engines. Green on head 783aac8 (after taking in base; the fold-anchor tests pass too): 32/32 browser, 15/15 unit (gutter and menu-press-guard).transcript-pinned-bottomalso passes at head (its WebKit "send while paging" case is flaky on base as well). Command:cd webviews && CMUX_BROWSER_TESTS=1 bun test ./test/transcript-gutter-select-browser.test.tsObserved / Not run
bun run check(green on our files; at this head its only failure is formatting insrc/pages/settings/components/HostSections.tsx, which comes from base), andgallery-coverage. The coverage test's two failures (chat-inspector/chatFolders.ts,DropOverlay) also fail at base.Before / after
A drag pressed in the left margin beside the first answer (left red dot) and dragged into the third (right red dot), on the transcript harness with synthetic text. Headless Chromium, captured mid-drag.
Gallery entry
agent-pane.gutter-select(synthetic text only):from-left-marginandfrom-right-marginplay a margin drag across messages. Before (merge base) shows no selection; after shows the selection across the answers.Compare: https://cmux-lawrences-mac-mini.tail137216.ts.net:18796/compare/tp-gutter-drag-select/?entry=agent-pane.gutter-select (tailnet)
Changelog
Overlap: none found (searched open and merged PRs for "selection", "gutter select" and "margin drag"). #24050 (tp/fold-animation) touches VirtualTranscript elsewhere; this adds one effect next to the existing input listeners and does not conflict.
Agent session: cosmic-gray
🤖 Generated with Claude Code