Skip to content

Commit a252ab4

Browse files
committed
fix(files): fix two scroll logic bugs introduced in previous streaming scroll fix
The prior fix introduced a regression for the "user was at bottom" case and a false-disengagement bug from programmatic scroll events. Bug 1 — Effect ordering: all three effects fire on the same render when isStreamInteractionLocked flips true. Effect 2 called isAtBottom() AFTER Effect 1 had already called model.setValue(), which grew scrollHeight. The old "at bottom" scroll position was now 200px short of the new bottom, so isAtBottom() returned false, textareaStuckRef was set false, and Effect 3 never called revealLine. Users at the bottom stopped following the stream. Fix: measure isAtBottom() in Effect 1 BEFORE setValue, while scrollHeight is still accurate. Set textareaStuckRef = true only (never false here). Effect 2 no longer initializes the ref — only the listener disengages it. Bug 2 — onDidScrollChange fires during model.setValue: Monaco fires onDidScrollChange when scroll dimensions change, including when setValue grows the document. This caused the listener to disengage auto-scroll on every content update even with no user interaction. Fix: add suppressScrollListenerRef, set true before setValue/restoreViewState and false after. The listener exits early when suppressed, so only genuine user scroll events (wheel, trackpad, keyboard, scrollbar) can disengage. Both refs moved to the component's ref block for conventional placement.
1 parent 8cec4a3 commit a252ab4

1 file changed

Lines changed: 27 additions & 13 deletions

File tree

  • apps/sim/app/workspace/[workspaceId]/files/components/file-viewer

apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -799,6 +799,8 @@ function TextEditor({
799799
const lastSyncedContentRef = useRef('')
800800
const hasAutoFocusedRef = useRef(false)
801801
const contentRef = useRef('')
802+
const textareaStuckRef = useRef(false)
803+
const suppressScrollListenerRef = useRef(false)
802804

803805
const [splitPct, setSplitPct] = useState(SPLIT_DEFAULT_PCT)
804806
const [isResizing, setIsResizing] = useState(false)
@@ -852,37 +854,49 @@ function TextEditor({
852854
if (monacoValue === content) return
853855

854856
if (isStreamInteractionLocked || monacoValue === lastSyncedContentRef.current) {
855-
// Preserve the user's scroll position during streaming updates, unless they
856-
// are already at the bottom (in which case Effect 3 will scroll to the new bottom).
857+
if (isStreamInteractionLocked) {
858+
// Measure BEFORE setValue — scrollHeight hasn't grown yet, so the
859+
// "at bottom" check is accurate. Re-engage auto-scroll if at bottom;
860+
// never disengage here (user scroll events do that via Effect 2).
861+
const scrollTop = editor.getScrollTop()
862+
const scrollHeight = editor.getScrollHeight()
863+
const { height } = editor.getLayoutInfo()
864+
if (scrollHeight - scrollTop - height < 80) {
865+
textareaStuckRef.current = true
866+
}
867+
}
868+
// Preserve the user's scroll position when they've scrolled away from the
869+
// bottom. Suppress the onDidScrollChange listener so programmatic scroll
870+
// changes (setValue / restoreViewState) don't falsely disengage auto-scroll.
857871
const viewState =
858872
isStreamInteractionLocked && !textareaStuckRef.current ? editor.saveViewState() : null
873+
suppressScrollListenerRef.current = true
859874
model.setValue(content)
860875
if (viewState) editor.restoreViewState(viewState)
876+
suppressScrollListenerRef.current = false
861877
lastSyncedContentRef.current = content
862878
}
863879
}, [content, isStreamInteractionLocked])
864880

865-
const textareaStuckRef = useRef(true)
866881
useEffect(() => {
867882
const editor = monacoEditorRef.current
868883
if (!editor || !isStreamInteractionLocked || disableStreamingAutoScroll) {
869884
textareaStuckRef.current = false
870885
return
871886
}
872887

873-
const isAtBottom = () => {
888+
// Effect 1 re-engages auto-scroll (sets true) immediately before each setValue,
889+
// measuring scroll position while scrollHeight is still accurate. This listener
890+
// only needs to disengage when the user physically scrolls away from the bottom.
891+
// Suppressed during programmatic setValue/restoreViewState in Effect 1.
892+
const disposable = editor.onDidScrollChange(() => {
893+
if (suppressScrollListenerRef.current) return
874894
const scrollTop = editor.getScrollTop()
875895
const scrollHeight = editor.getScrollHeight()
876896
const { height } = editor.getLayoutInfo()
877-
return scrollHeight - scrollTop - height < 80
878-
}
879-
880-
// Initialize from actual position — only follow if already at the bottom.
881-
textareaStuckRef.current = isAtBottom()
882-
883-
// Use Monaco's scroll API so trackpad, scrollbar drag, and keyboard all update the flag.
884-
const disposable = editor.onDidScrollChange(() => {
885-
textareaStuckRef.current = isAtBottom()
897+
if (scrollHeight - scrollTop - height >= 80) {
898+
textareaStuckRef.current = false
899+
}
886900
})
887901

888902
return () => {

0 commit comments

Comments
 (0)