feat(panel): select code to comment, keep several line comments open, and light a thread's lines on hover
C2 of the "Better comment box" stack, after #3792 (C1, merged). It makes three changes to Files changed in the Pull request tab: you can select code to comment on it, keep several line comments open at once, and hover a review thread to light the lines it is on. It is cut from main, so no restack was needed. It includes #3794, #3796, #3798 and #3799.
Summary
- Select to comment. Select code text in a file and a floating Comment button appears over the selection, as in stage-cli. Pressing it opens the line comment on the lines the selection covers.
- How lines are read (
diff-view/text-selection.ts). Once the drag lets go, the bundle reads the selection from that file's shadow root throughSelection.getComposedRanges. It maps each end to its Pierre row (data-line,data-line-type):- A removed row counts on the old side and an added row on the new side.
- An unchanged row counts on its column's side: the left column of a split diff is the old side, and every other column is the new side.
- A drag that ends at the very start of a row, as a triple-click does, ends on the row before it, even across a gap between hunks.
- A selection that ends in a note, or crosses into another file, maps to no lines.
- Where the button shows. The bundle passes the lines and a box around the selected text to the view through a new contract callback,
DiffListProps.onSelectText. The view only shows the button wherecommentLinesaccepts the range, meaning the hunk lines GitHub takes; expanded context lines get nothing. - What dismisses it. A press in the list, a press elsewhere in the window, or a scroll.
- The button keeps focus where it was, so the selection survives the press.
- How lines are read (
- Several drafts (
comment-drafts.ts). Line comments are no longer limited to one at a time: several can be open across lines and files.- They live above the virtual list, so a comment scrolled out of the window and back keeps its words.
- Each comment is keyed by its file and range, one per last line and side, as GitHub anchors a thread. Picking lines that end where an open comment ends moves that comment rather than opening a second.
- Words are kept by file, side and last line, so a comment cancelled unsent comes back with its words when opened there again. A comment GitHub accepted drops its words.
- The lines of the comment opened or typed in last stay lit.
- The Minimize inline comments setting keeps a line's threads expanded wherever any comment is being written on that line.
- New comments get the keyboard. Before this change, the comment's field tried to focus before Pierre had placed its note in the page, so it never got focus. That was invisible with one comment opened from the gutter, but typing after Comment went to the list's keys instead (for example, it folded the file). The field now focuses as soon as its note is in the page.
- The gutter "+" and lit rows. Both come from Pierre's own defaults, and
mainshows the same, so #3794 didn't cause them.- The gutter "+" hung past the line-number column, about a character short of a line's height, and covered the first characters of code. It now stays inside the number column, where it may cover the number.
- Lit lines also lit the notes under them, so a comment being written sat inside a coloured band. Those notes now keep their own background (
LOOK_CSS).
- Thread hover. Hovering a review thread on its line lights the lines it is on, and so does hovering the chip it's minimized to. The chip lights the widest of its threads' ranges, judged by the order the diff draws its rows. What the pointer rests on is tracked as a stack, so moving from a thread shown under the chip back to the chip lights the chip's lines again. Hover takes priority over a draft's lit lines and gives them back when the pointer leaves.
Desktop and panel only. No change to the service or to what is sent over the wire. Comments still go through memberPullRequestWrite.
Screenshots
Taken from the real AgentPullRequestTab with the real diff bundle and the built styles.css, using synthetic fixtures in headless Chromium driven by Playwright. Every step was a real interaction: a mouse drag to select, a press on Comment, typing, then hovering the thread.
| Light | Dark | |
|---|---|---|
| Code selected as text: the floating Comment press | ![]() | ![]() |
| Two comments open at once (lines 10–12, then old line 4, lit as the last opened) | ![]() | ![]() |
| The pointer on Charles's thread lights lines 5–6 | ![]() | ![]() |
Evidence
pnpm check:affected runs out of memory in this sandbox, so I ran the parts one at a time on the merged tree. All passed:
tscfor@luke/paneland@luke/desktop./scripts/oxlint.sh,biome check packages/panel,./scripts/repository-checks.sh,pnpm knip,node scripts/discarded-effect.mjs- vitest for
@luke/panel: 198 files, 2,130 tests cd apps/desktop && node scripts/build.mjs, within the bundle budget, which was not re-recorded
Tests (+11)
diff-view/text-selection.test.ts(5): only a press on the code starts a drag (a press on a header or a note doesn't, so a selection left behind isn't offered again); a unified selection from a removed row to an added row; an unchanged row in the old column of a split diff; a drag ending at a row's start, across a gap; a selection ending in a note.changes-model.test.ts(1):rowPlacefollows the order the diff draws its rows, so an added row drawn before a removed row comes first.agent-pull-request-tab.test.ts(5):- Several comments open at once, each with its own words, with the last opened one lit. Cancel closes one. Reopening brings its words back. Lines ending on the same line move the comment.
- Selected code over hunk lines offers "Comment on lines 12–13", which opens that comment. Lines outside the hunks get no offer, and a released selection removes it.
- A new comment takes the keyboard once its note is in the page.
- Hovering a review thread lights its lines, and leaving lets them go.
- The existing line-comment tests now find comments by their drawn element rather than the single
line-commentkey.
Invariants
-
./scripts/check.shgreen. (CI) - JSON Schema goldens unchanged.
- Every behavior change has, in this PR, the test that fails without it; no change-detector test added.
- Gateway envelope goldens unchanged.
- No new
astype assertion. - No
Effect.runPromise/runSync/runForkoutside the runtime edges. - No new
setTimeout/setInterval/new Promise/AbortController/fs.watch. The bundle waits onerequestAnimationFrameafter a drag lets go, and the field waits on aMutationObserver, which is disconnected once it fires or on unmount. - No Effect, Stream, or Layer written as a statement and dropped.
- Test count: +11, listed above.
- Nothing hand-rolled replaced.
- No
AGENTS.mdsentence names a changed part. -
PRIVACY.mdunchanged. The Comment button's word is ordinary text that session replay masks, and the tab staysph-no-capture. - Package deps unchanged.
- Barrel/door rule untouched.
Notes
- Out of scope, owned by other agents: the file header, the tree, the shortcuts sheet and the diff settings.
Made with Luke





