Skip to content

Commit 8cec4a3

Browse files
committed
fix(files): preserve scroll position during Mothership streaming edits
Two fixes to the Monaco auto-scroll logic: 1. At streaming start, initialize textareaStuckRef from the editor's actual scroll position (isAtBottom check) instead of unconditionally setting true. Previously every streaming session jumped the viewport to the last line on the very first content update, even when the user was reading mid-file. 2. Replace the wheel-only DOM listener with editor.onDidScrollChange(), the proper Monaco API. This covers trackpad, scrollbar drag, and keyboard scroll — not just mouse wheel. As a bonus, scrolling back to the bottom during streaming now re-engages follow mode (matching iTerm2/xterm.js behavior). 3. Save and restore view state around model.setValue() during streaming when the user has scrolled away from the bottom. This prevents Monaco from resetting the viewport on each content replacement. When the user is at the bottom, view state is not saved so Effect 3 can scroll to the new bottom.
1 parent 14f77b3 commit 8cec4a3

1 file changed

Lines changed: 18 additions & 10 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: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -852,7 +852,12 @@ function TextEditor({
852852
if (monacoValue === content) return
853853

854854
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+
const viewState =
858+
isStreamInteractionLocked && !textareaStuckRef.current ? editor.saveViewState() : null
855859
model.setValue(content)
860+
if (viewState) editor.restoreViewState(viewState)
856861
lastSyncedContentRef.current = content
857862
}
858863
}, [content, isStreamInteractionLocked])
@@ -865,20 +870,23 @@ function TextEditor({
865870
return
866871
}
867872

868-
textareaStuckRef.current = true
869-
const domNode = editor.getDomNode()
870-
if (!domNode) return
873+
const isAtBottom = () => {
874+
const scrollTop = editor.getScrollTop()
875+
const scrollHeight = editor.getScrollHeight()
876+
const { height } = editor.getLayoutInfo()
877+
return scrollHeight - scrollTop - height < 80
878+
}
871879

872-
const scrollable = domNode.querySelector('.monaco-scrollable-element') as HTMLElement | null
873-
if (!scrollable) return
880+
// Initialize from actual position — only follow if already at the bottom.
881+
textareaStuckRef.current = isAtBottom()
874882

875-
const onWheel = (e: Event) => {
876-
if ((e as WheelEvent).deltaY < 0) textareaStuckRef.current = false
877-
}
878-
scrollable.addEventListener('wheel', onWheel, { passive: true })
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()
886+
})
879887

880888
return () => {
881-
scrollable.removeEventListener('wheel', onWheel)
889+
disposable.dispose()
882890
}
883891
}, [isStreamInteractionLocked, disableStreamingAutoScroll])
884892

0 commit comments

Comments
 (0)