Triage
Verdict: Valid bug. Keep open.
The scroll handlers are not clearing the selection. The chat timeline is a virtualized LegendList, and a drag that scrolls unmounts and reorders the nodes the browser selection is attached to. That matches gaps, a full clear, and a highlight that lands on different messages while the button is still down.
Area is apps/web. The Linux desktop app is this web UI in Electron. The same timeline is what the hosted site renders. Native mobile chat is a different surface.
Checked on main at 371b52d9. The reported v0.0.42 (719a76ca) already mounts the timeline the same way, and nothing since then preserves a selection across scroll. The recording and screenshots are not attached on the issue, so the pixels were not rechecked. The code path is enough.
What happens
The timeline list does not opt into recycling and does not set a draw distance. @legendapp/list 3.3.5 then defaults recycleItems to false and drawDistance to 250.
| 1 | <LegendList<MessagesTimelineRow> |
| 2 | ref={listRef} |
| 3 | data={rows} |
| 4 | extraData={`${listIdentityKey}:${rows.length}`} |
| 5 | keyExtractor={keyExtractor} |
| 6 | getItemType={getItemType} |
| 7 | renderItem={renderItem} |
| 8 | estimatedItemSize={90} |
onScroll={handleScroll} only updates the scroll anchor, the at-end flag, and the minimap. It never touches window.getSelection().
Rows are absolutely positioned (top: 0 plus a per-row top). On web, LegendList keeps a container pool and sorts those containers into index order only 500ms after the last position change (useDOMOrder in 3.3.5). Until that timer fires, DOM order is not visual order. A native selection is a DOM range, so the highlight does not follow the pixels under the cursor. Visually intervening message bodies stay dark, and text that is only adjacent in the DOM lights up somewhere else.
Leaving the viewport plus the 250px buffer drops the container (containerItemKey becomes undefined and the slot renders nothing). Short “test N” rows are well under the 90px estimate, so a manual scroll from “test 12” unmounts that anchor quickly. Chromium then drops or relocates the range. The button is still down, so the next move starts a new range on whatever row is now under the pointer.
The citation toolbar observer does dismiss on scroll, and it does not call removeAllRanges(). That only hides the Cite button. removeAllRanges() in this path runs when the user actually cites.
| 1 | const onScroll = () => { |
| 2 | cancelPending(); |
| 3 | // Autoscroll during a drag must not cancel that gesture's eventual release. |
| 4 | if (!pointerDown) { |
| 5 | pointer = null; |
| 6 | dismissed = true; |
| 7 | } |
| 8 | onDismiss("cancel"); |
| 9 | }; |
user-select: none on tool-row headers, turn folds, and timestamps can also leave unhighlighted chrome between messages. That does not move a highlight onto a different message, and it does not explain a user message body such as “test 45” staying unselected while neighbors are highlighted.
Our LegendList patch reorders containers with Element.moveBefore so a reorder does not restart row state. Electron 44 has that API. The reorder still waits until scrolling has been idle for 500ms, and it does not keep an off-screen anchor mounted.
Not the same issue
- #7305, #6555, and #8541 are terminal selection and terminal drag-autoscroll.
- #9409 would auto-copy an existing chat or terminal selection. It does not keep the timeline selection intact while scrolling.
- #9175 is copying a code block without markdown.
- #6110, #8779, and #11297 are native iOS and Android text selection.
- #12935 is selecting the thread title while renaming.
No open issue or PR covers chat text selection breaking while the timeline scrolls.
Next step
For the life of a primary-button drag inside the timeline, keep every row from the anchor through the pointer mounted, and do not reorder list containers until mouseup. Scroll must not clear the selection. A short drag that never scrolls should keep working, including Cite on an assistant span.
Repro: a thread of many short alternating user and assistant messages, press on an early message, hold, and scroll with the wheel and the scrollbar toward the bottom. The highlight should stay a single continuous span of the messages the pointer passed through.