Skip to content

Agent chat: a drag from the margin beside the column selects text - #24326

Merged
teamleaderleo merged 23 commits into
feat-cmux-nextfrom
tp/gutter-drag-select
Oct 23, 2026
Merged

teamleaderleo merged 23 commits into
feat-cmux-nextfrom
tp/gutter-drag-select

Conversation

@teamleaderleo

@teamleaderleo teamleaderleo commented Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

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:

  • A press in the left or right margin, followed by a drag, starts a selection at the caret beside the press. Left of a line, that is the line's start; right of a line, its end. The drag extends the selection to the caret under the pointer, into the column or down the margin.
  • The selection follows the view as it scrolls under the pointer (the wheel, or the pointer held above or below the view, which scrolls it).
  • A plain click in the margin, with no drag, selects nothing. Focus behaves as on base: a press on the background takes focus off the composer (Leo 2026-10-20, composerFocus.ts). The click also lets go of an existing transcript selection, as a click in a document's margin does.
  • A press that only closes an open composer menu does nothing else (menuPressGuard.ts now exports swallowsPress).
  • An edge-scroll step is the reader's scroll (VirtualTranscript's reader), so follow-latest does not pull the view back during the drag.
  • The transcript virtualizes rows, so a long drag could unmount the row the selection starts in and collapse it. That row now stays mounted from the drag's first move. After the drag, both end rows stay mounted (VirtualTranscript heldRows, at most two) until the selection is cleared or no longer starts there.
  • These keep their own behaviour, because the handler only takes presses whose target is the scroller, its spacer or the thread's own box: the minimap and scroll rail, the turn strip, Find's bar, Jump to latest, rows and their links and buttons, the composer, and the scrollbar lane (the right 16 px). A drag inside the column selects natively as before.

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-scroll or .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 or pointer-events layer was in the way.

So conversation/gutterSelect.ts takes a press in the margin and reads the caret with caretPositionFromPoint (falling back to caretRangeFromPoint for WebKit), with x clamped one pixel inside the column's content box and y clamped above the bottom stack. It then calls Selection.setBaseAndExtent. It is installed with the transcript scroller's other input listeners (VirtualTranscript, App.tsx, +5 lines).

Cost and stop conditions

  • Listeners. The window mousemove/mouseup/blur listeners and the scroller scroll listener 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.
  • Frames. One requestAnimationFrame at 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.
  • Anchor. The press is kept as a collapsed live 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.
  • Edge scroll. It repeats only while the pointer stays above or below the view during the drag, and stops at the view's end. The pending frame is cancelled with the listeners.
  • Held rows. One selectionchange listener keeps a finished drag's two end rows mounted. A MutationObserver on 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 own aria-posinset, with no walk of the rows.
  • A held row is found by its index or its distance from the end; only when rows arrive at both ends at once is it looked up by id, and an effect writes the found position back so the next render reads it directly. Held rows never widen the drawn-height search, which still spans only the scrolled rows; a held row whose height changes is looked up by its index. A hold belongs to its chat: showing another chat ends any drag and clears the held rows, so returning to the first chat pins nothing. A hold survives older history paging in above it.

Size

About 1210 changed lines, over the usual 800. The product code is about 410 lines (gutterSelect.ts 302, 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:
    • a drag from the left margin beside message 1 down into message 3 selects text from all three;
    • a drag that stays in the margin selects whole lines;
    • the selection keeps its start when the message under the press is redrawn mid-drag, as a streaming reply does (red on the raw-node anchor);
    • a drag whose message is taken out mid-drag ends, and selects nothing from the next message;
    • a finished margin selection is cleared when its start or its end message is taken out;
    • once the reader moves the selection's end to another message (Shift-click), the old end row taken out leaves it alone;
    • a drag from the right margin up starts at the end of the line beside the press;
    • a margin click selects nothing and takes focus off a field, as on base;
    • a margin click above the first row or below the last, away from any text, also takes focus off a field;
    • a margin click lets go of a selection;
    • the selection follows a scroll under a still pointer;
    • over 120 rows, a drag that scrolls 4800 px keeps its start, and the row is released once the selection is cleared (red without the hold in both engines);
    • a held row stays mounted when 30 older rows page in during the drag (red without the end-distance match), and when rows arrive at both ends in one snapshot (red without the lookup by id);
    • showing another chat and coming back lets the held row go (red without the release in both engines);
    • a drag inside the column is unchanged.
  • conversation/gutterSelect.test.ts: the clamp, gutter, threshold and edge-scroll math.
  • test/menu-press-guard.test.ts: swallowsPress is 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-bottom also 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.ts

Observed / Not run

  • Ran on huge-blue (Linux, headless Chromium 1234 and WebKit 2336): the browser file red at base and green at head, the unit file, bun run check (green on our files; at this head its only failure is formatting in src/pages/settings/components/HostSections.tsx, which comes from base), and gallery-coverage. The coverage test's two failures (chat-inspector / chatFolders.ts, DropOverlay) also fail at base.
  • Not run: the macOS WKWebView in the real app. The WebKit lane is Playwright's WebKit, not WKWebView.

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.

Before (base 0cb8ff2) After (41176d9)
gutter drag before gutter drag after

Gallery entry agent-pane.gutter-select (synthetic text only): from-left-margin and from-right-margin play 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

  • Fixed: In agent chats, dragging from the empty margin beside the conversation now selects text, as in a document.

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

teamleaderleo and others added 4 commits October 22, 2026 00:09
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
… 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
@teamleaderleo teamleaderleo added the risk:normal Risk tier (docs/ci/risk-tiers.md) label Oct 22, 2026
@github-actions

github-actions Bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

cmux status (one comment, edited in place; each line is updated by the job that owns it)

Review 783aac8: HQ feed card · gallery before/after runs · checks

App of this head (app.zip, kept 1 day)

gh workflow run reload-build.yml -R manaflow-ai/cmux --ref feat-cmux-next -f tag=pr24326 -f ref=tp/gutter-drag-select -f platform=macos -f backend_mode=local

Gallery before/after for 783aac8: 16 states rendered, 4 images differ, 822 more touched states not rendered. Images are in the run's artifact, not on this public PR.

gh run download 39946139936 -R manaflow-ai/cmux -n gallery-advisory-24326-783aac847b282373410fe507daf9112660a329dd (then read gallery-review.json)

States
State Theme Status Diff Picked by
agent-pane.chat-width/standard Apple System Colors Light unchanged 0% own App.tsx
agent-pane.chat-width/standard Apple System Colors unchanged 0% own App.tsx
agent-pane.chat-width/wide Apple System Colors Light unchanged 0% own App.tsx
agent-pane.chat-width/wide Apple System Colors unchanged 0% own App.tsx
agent-pane.chat-width/wide-blocks Apple System Colors Light unchanged 0% own App.tsx
agent-pane.chat-width/wide-blocks Apple System Colors unchanged 0% own App.tsx
agent-pane.composer-floats/scrolled-to-end Apple System Colors Light unchanged 0% own App.tsx
agent-pane.composer-floats/scrolled-to-end Apple System Colors unchanged 0% own App.tsx
agent-pane.composer-floats/composer-grown Apple System Colors Light unchanged 0% own App.tsx
agent-pane.composer-floats/composer-grown Apple System Colors unchanged 0% own App.tsx
agent-pane.docked-chat/new-chat Apple System Colors Light unchanged 0% own App.tsx
agent-pane.docked-chat/new-chat Apple System Colors unchanged 0% own App.tsx
agent-pane.docked-chat/after-turn Apple System Colors Light unchanged 0% own App.tsx
agent-pane.docked-chat/after-turn Apple System Colors unchanged 0% own App.tsx
agent-pane.docked-chat/working Apple System Colors Light unchanged 0% own App.tsx
agent-pane.docked-chat/working Apple System Colors unchanged 0% own App.tsx
agent-pane.docked-chat/typing-in-dock Apple System Colors Light unchanged 0% own App.tsx
agent-pane.docked-chat/typing-in-dock Apple System Colors unchanged 0% own App.tsx
agent-pane.docked-chat/long-title Apple System Colors Light unchanged 0% own App.tsx
agent-pane.docked-chat/long-title Apple System Colors unchanged 0% own App.tsx
agent-pane.gutter-select/from-left-margin Apple System Colors Light new 0% own App.tsx
agent-pane.gutter-select/from-left-margin Apple System Colors new 0% own App.tsx
agent-pane.gutter-select/from-right-margin Apple System Colors Light new 0% own App.tsx
agent-pane.gutter-select/from-right-margin Apple System Colors new 0% own App.tsx
agent-pane.orchestration-lab/local-queue Apple System Colors Light unchanged 0% own App.tsx
agent-pane.orchestration-lab/local-queue Apple System Colors unchanged 0% own App.tsx
agent-pane.orchestration-lab/build-box Apple System Colors Light unchanged 0% own App.tsx
agent-pane.orchestration-lab/build-box Apple System Colors unchanged 0% own App.tsx
agent-pane.orchestration-lab/cloud-fair-share Apple System Colors Light unchanged 0% own App.tsx
agent-pane.orchestration-lab/cloud-fair-share Apple System Colors unchanged 0% own App.tsx
agent-pane.streaming-follow-motion/stream Apple System Colors Light unchanged 0% own App.tsx
agent-pane.streaming-follow-motion/stream Apple System Colors unchanged 0% own App.tsx

Pick other states with a Gallery: entry.id, entry.id/variant line in the PR body.

Passes: CI passes on 783aac847b.

CI passes on 783aac847b (run 39946140052 attempt 1).

Written by scripts/ci/classify_failures.py (ci-failure-attribution.yml); signatures are its SIGNATURES table. A machine verdict is the runner's fault, not this PR's; yours means the failing file is one this PR changes, also red on main that main's latest full suite fails the same way, seen on other PRs that it failed on another pull request's run lately.

red: cmux-next checks (god files, concurrency, crash safety, l10n); NEW: red check: Checks job structure (+2 more) is not red on the base e7f31b7; do not merge until green; see lane-rules

Tour of 783aac847b (run): no media. The tour did not run: only webview code changed (8 path(s)), which the gallery advisory run checks in Linux Chromium on the pull request (its artifact holds any captures); add the media-tour-mac label, then re-add media-tour, to tour it on a Mac.

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Owned by transcript-parity on cosmic-gray · session

@blacksmith-sh

blacksmith-sh Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Found 8 test failures on Blacksmith runners:

Failures

Test View Logs
/tmp/tmp5wcyq7o1/summary-fail.json (live):/summary-fail.json (live): View Logs
/tmp/tmpeylubyib/summary-missing.json (live):/summary-missing.json (live): View Logs
[tabs] debug.tab_switch timed 0 of 40 switches/
[tabs] debug.tab_switch timed 0 of 40 switches
View Logs
[tabs] debug.tab_switch timed 0 of 40 switches/
[tabs] debug.tab_switch timed 0 of 40 switches
View Logs
[tabs] the window draws no frames/[tabs] the window draws no frames View Logs
[tabs] the window draws no frames/[tabs] the window draws no frames View Logs
test_base_passes (main.NoticesPairTests.test_base_passes)/
test_base_passes (main.NoticesPairTests.test_base_passes)
View Logs
test_targets_outside_the_app_go_to_the_mac_canary (main.SwiftCanaryFleetCoverage.te
st_targets_outside_the_app_go_to_the_mac_canary)/
test_targets_outside_the_app_go_to_the_mac_canary (main.SwiftCanaryFleetCoverage.te
st_targets_outside_the_app_go_to_the_mac_canary)
View Logs

Fix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Bead cx-cbcjb created for this PR by the cmux coordinator.

…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
@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

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 composerFocus.ts contract from Leo on 2026-10-20. Two non-blocking findings came with it: an open composer menu's press guard didn't stop the margin drag, and the edge scroll fought follow-latest. Head 1714a95 fixes all three, and the re-review found no new problems:

  • Focus: a margin press blurs the focused element, as base did.
  • Menu guard: swallowsPress(doc) is current at mousedown, because the guard decides in the window's capture-phase pointerdown.
  • Edge scroll: each step calls the transcript's reader() first, so it counts as the reader's scroll and follow-latest no longer pulls the view back.
  • Cleanup: removeGutter() runs with the scroller's other input listeners.

Coverage gaps, non-blocking: no test of the edge-scroll step itself, and the follow test moves scrollTop instead of sending a real wheel.

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Review (codex xhigh, correctness first): BLOCK at 1714a95

  • webviews/src/agent-session/acpmux/conversation/gutterSelect.ts:130-134 keeps the drag anchor as a live DOM Node and returns as soon as anchor.node.isConnected becomes false. VirtualTranscript deliberately unmounts rows outside its overscan window. A long margin drag (or edge-scroll) can therefore unmount the row where the drag began; WebKit/Chromium clear the native range when that node is removed, and subsequent moves cannot extend it. I reproduced this with 200 assistant rows: start in the left gutter beside row 0, scroll far enough for row 0 to unmount, and the selection becomes empty while the drag is still held. Preserve the anchor logically (row identity plus text offset, then resolve a live caret as rows mount) or pin the anchor row for the gesture, and add a browser regression that crosses the virtualization boundary.

Verification: cd webviews && bun test ./src/agent-session/acpmux/conversation/gutterSelect.test.ts (7 pass), bun test ./test/menu-press-guard.test.ts (7 pass), and ulimit -n 65536; CMUX_BROWSER_TESTS=1 bun test ./test/transcript-gutter-select-browser.test.ts (14 pass across Chromium and WebKit). git diff --check f61b6a137535..HEAD also passes.

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Orphaned: no activity for 13 h, and no lane holds a lease on it. Adopted by the orphan sweep: merged feat-cmux-next in at 41555f4. A later pass queues it for the merge hub if it is reviewed, small and red only on base reds.

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

cmux-next guards / nightly-guards / workflow-guard-tests / release-notary is red on 17 of 17 fresh feat-cmux-next PR heads in the last 2:00:00, so it is most likely a base red, not this PR's. The base-reds lane has it; a base merge picks up the fix once it lands.

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

cmux-next / cmux-next checks (god files, concurrency, crash safety, l10n) › Checks job structure is red on 17 of 17 fresh feat-cmux-next PR heads in the last 2:00:00, so it is most likely a base red, not this PR's. The base-reds lane has it; a base merge picks up the fix once it lands.

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

cmux-next / cmux-next checks (god files, concurrency, crash safety, l10n) › No fleet host names in gallery fixtures is red on 15 of 17 fresh feat-cmux-next PR heads in the last 2:00:00, so it is most likely a base red, not this PR's. The base-reds lane has it; a base merge picks up the fix once it lands.

teamleaderleo and others added 7 commits October 22, 2026 17:01
…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
…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
@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

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:

  • Focus: a margin press blurs the composer, as base did.
  • Menu guard: a press that only closes an open composer menu starts no selection (swallowsPress).
  • Edge scroll: each step counts as the reader's scroll, so follow-latest doesn't pull the view back.
  • Drawn-height search: held rows never widen it. lastPlaced spans the scrolled rows, and held indices are looked up directly, at most two.
  • Held rows: a hold belongs to its chat, and it survives older history paging in above it, found by its distance from the end.

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
@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

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. remove still detaches mousedown and then ends the drag. The new browser test fails without the release, because the old {session-1} hold matches again on return. The different-model review at this head is running separately.

@cursor

cursor Bot commented Oct 23, 2026

Copy link
Copy Markdown

Bugbot needs on-demand usage enabled

Bugbot 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.

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

cmux-next / cmux-next checks (god files, concurrency, crash safety, l10n) › No new SwiftUI (macOS UI is AppKit) is red on 13 of 14 fresh feat-cmux-next PR heads in the last 2:00:00, so it is most likely a base red, not this PR's. The base-reds lane has it; a base merge picks up the fix once it lands.

teamleaderleo and others added 2 commits October 23, 2026 01:43
…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
@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

rust workspaces / plan (changed Cargo workspaces) is red on 16 of 17 fresh feat-cmux-next PR heads in the last 2:00:00, so it is most likely a base red, not this PR's. The base-reds lane has it; a base merge picks up the fix once it lands.

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

rust workspaces / rust workspaces is red on 15 of 15 fresh feat-cmux-next PR heads in the last 2:00:00, so it is most likely a base red, not this PR's. The base-reds lane has it; a base merge picks up the fix once it lands.

teamleaderleo and others added 4 commits October 23, 2026 02:22
… 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
@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Review (codex xhigh, correctness first): SHIP at e3a8e52

Diff-only Codex review at xhigh (cmux16, codex exec --sandbox read-only). No further correctness or regression findings. The focus check releases stale holds after a Shift-click moves the selection's end, which keeps the valid selection when the old end row is removed.

Earlier Codex BLOCKs on this PR, all fixed with browser tests that fail without the fix in both engines:

  • session change releases the holds (ef29982);
  • held row found when rows arrive at both ends (478015b);
  • live-Range anchor survives a redraw (529e14c);
  • anchor row removed mid-drag (d0782cf);
  • held end row removed after the drag (0db9a10);
  • Shift-click moves the end (e3a8e52).

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
@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

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, --sandbox read-only. Base's fold-anchor held row now goes into the same held indices as the margin selection's rows: it is mounted as a single-row range and the drawn-height search checks it by index. No blocking findings.

At this head:

  • fold-anchor browser 9/9 and unit tests pass;
  • gutter browser tests 32/32 (Chromium and WebKit);
  • pinned-bottom 6/6;
  • the CI web run is green.

The reds at this head are base reds:

  • cmux-next guards (release-notary, Guard status);
  • cmux-next checks (test_cmux_next_perf_gate_unlit.py);
  • lint TS errors in thread-minimap-stream.test.tsx (4 on base).

@teamleaderleo
teamleaderleo merged commit 36febae into feat-cmux-next Oct 23, 2026
135 of 138 checks passed
@teamleaderleo
teamleaderleo deleted the tp/gutter-drag-select branch October 23, 2026 11:59
@github-actions

github-actions Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk:normal Risk tier (docs/ci/risk-tiers.md)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant