Skip to content

Commit 8001af2

Browse files
fix(web): fix inaccurate scroll position when selecting chat references (#1035)
* fix(web): fix inaccurate scroll position when selecting chat references (SOU-724) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: update CHANGELOG for #1035 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent d910b8d commit 8001af2

File tree

3 files changed

+35
-21
lines changed

3 files changed

+35
-21
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2727
- Fixed homepage scrolling issue in the ask UI. [#1014](https://github.com/sourcebot-dev/sourcebot/pull/1014)
2828
- Fixed UI freeze when the `grep` tool returns a large number of results with `groupByRepo=true`. [#1032](https://github.com/sourcebot-dev/sourcebot/pull/1032)
2929
- Fixed issue where the search scope selection persisted after a new thread is created. [#1033](https://github.com/sourcebot-dev/sourcebot/pull/1033)
30+
- Fixed inaccurate scroll position when selecting a chat reference in the ask UI. [#1035](https://github.com/sourcebot-dev/sourcebot/pull/1035)
3031

3132
## [4.15.11] - 2026-03-20
3233

packages/web/src/features/chat/components/chatThread/referencedFileSourceListItem.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,13 +214,18 @@ const ReferencedFileSourceListItemComponent = ({
214214
return isExpanded ? ChevronDown : ChevronRight;
215215
}, [isExpanded]);
216216

217+
const isSelectedWithoutRange = useMemo(() => {
218+
return references.some(r => r.id === selectedReference?.id && !selectedReference?.range);
219+
}, [references, selectedReference?.id, selectedReference?.range]);
220+
217221
return (
218222
<div className="relative" id={id}>
219223
{/* Sentinel element to scroll to when collapsing a file */}
220224
<div id={`${id}-start`} />
221225
{/* Sticky header outside the bordered container */}
222226
<div className={cn("sticky top-0 z-10 flex flex-row items-center bg-accent py-1 px-3 gap-1.5 border-l border-r border-t rounded-t-md", {
223227
'rounded-b-md border-b': !isExpanded,
228+
'border-chat-reference-selected-border border-b': isSelectedWithoutRange,
224229
})}>
225230
<ExpandCollapseIcon className={`h-3 w-3 cursor-pointer mt-0.5`} onClick={() => onExpandedChanged(!isExpanded)} />
226231
<PathHeader

packages/web/src/features/chat/components/chatThread/referencedSourcesListView.tsx

Lines changed: 29 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -117,34 +117,42 @@ const ReferencedSourcesListViewComponent = ({
117117
const view = editorRef.view;
118118
const lineNumber = selectedReference.range.startLine;
119119

120-
// Get the line's position within the CodeMirror document
121120
const pos = view.state.doc.line(lineNumber).from;
122-
const blockInfo = view.lineBlockAt(pos);
123-
const lineTopInCodeMirror = blockInfo.top;
124-
125-
// Get the bounds of both elements
126-
const viewportRect = scrollAreaViewport.getBoundingClientRect();
127-
const codeMirrorRect = view.dom.getBoundingClientRect();
128-
129-
// Calculate the line's position relative to the ScrollArea content
130-
const lineTopRelativeToScrollArea = lineTopInCodeMirror + (codeMirrorRect.top - viewportRect.top) + scrollAreaViewport.scrollTop;
131-
132-
// Get the height of the visible ScrollArea
133-
const scrollAreaHeight = scrollAreaViewport.clientHeight;
134-
135-
// Calculate the target scroll position to center the line
136-
const targetScrollTop = lineTopRelativeToScrollArea - (scrollAreaHeight / 3);
137121

138122
// Expand the file if it's collapsed.
139123
setCollapsedFileIds((collapsedFileIds) => collapsedFileIds.filter((id) => id !== fileId));
140124

141-
// Scroll to the calculated position
142-
// @NOTE: Using requestAnimationFrame is a bit of a hack to ensure
143-
// that the collapsed file ids state has updated before scrolling.
125+
// @hack: CodeMirror 6 virtualizes line rendering — it only renders lines near the
126+
// browser viewport and uses estimated heights for everything else. This means
127+
// coordsAtPos() returns inaccurate positions for lines that are off-screen,
128+
// causing the scroll to land at the wrong position on the first click.
129+
//
130+
// To work around this, we use a two-step scroll:
131+
// Step 1: Instantly bring the file element into the browser viewport. This
132+
// forces CodeMirror to render and measure the target lines.
133+
// Step 2: In the next frame (after CodeMirror has measured), coordsAtPos()
134+
// returns accurate screen coordinates which we use to scroll precisely
135+
// to the target line.
136+
scrollIntoView(fileSourceElement, {
137+
scrollMode: 'if-needed',
138+
block: 'start',
139+
behavior: 'instant',
140+
});
141+
144142
requestAnimationFrame(() => {
143+
const coords = view.coordsAtPos(pos);
144+
if (!coords) {
145+
return;
146+
}
147+
148+
const viewportRect = scrollAreaViewport.getBoundingClientRect();
149+
const lineTopRelativeToScrollArea = coords.top - viewportRect.top + scrollAreaViewport.scrollTop;
150+
const scrollAreaHeight = scrollAreaViewport.clientHeight;
151+
const targetScrollTop = lineTopRelativeToScrollArea - (scrollAreaHeight / 3);
152+
145153
scrollAreaViewport.scrollTo({
146154
top: Math.max(0, targetScrollTop),
147-
behavior: 'smooth',
155+
behavior: 'instant',
148156
});
149157
});
150158
}
@@ -154,7 +162,7 @@ const ReferencedSourcesListViewComponent = ({
154162
scrollIntoView(fileSourceElement, {
155163
scrollMode: 'if-needed',
156164
block: 'start',
157-
behavior: 'smooth',
165+
behavior: 'instant',
158166
});
159167
}
160168
}, [getFileId, sources, selectedReference]);

0 commit comments

Comments
 (0)