From a0696c31b6c9acdf268285f94f9ea2e191262dab Mon Sep 17 00:00:00 2001 From: Divit Kashyap <162712154+divitkashyap@users.noreply.github.com> Date: Sun, 24 May 2026 13:32:29 +0100 Subject: [PATCH] fix(tui): prevent scroll snap when reading history during LLM response Track when the user has scrolled away from the bottom and disable sticky scroll while that's true. Without this, every token streamed during an LLM response snaps the viewport back to the bottom, making it impossible to read history mid-response. Upward scroll commands (Page Up, Line Up, Half Page Up, First message) set the flag. Commands that return to bottom (Last message, toBottom, session sync) reset it. Closes #4196 --- .../src/cli/cmd/tui/routes/session/index.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index ae23d58bcc95..19f4db9bb8a4 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -225,6 +225,7 @@ export function Session() { const [diffWrapMode] = kv.signal<"word" | "none">("diff_wrap_mode", "word") const [_animationsEnabled, _setAnimationsEnabled] = kv.signal("animations_enabled", true) const [showGenericToolOutput, setShowGenericToolOutput] = kv.signal("generic_tool_output_visibility", false) + const [userScrolledUp, setUserScrolledUp] = createSignal(false) const wide = createMemo(() => dimensions().width > 120) const sidebarVisible = createMemo(() => { @@ -270,7 +271,10 @@ export function Session() { } editor.reconnect(result.data.directory) await sync.session.sync(sessionID) - if (route.sessionID === sessionID && scroll) scroll.scrollBy(100_000) + if (route.sessionID === sessionID && scroll) { + scroll.scrollBy(100_000) + setUserScrolledUp(false) + } })().catch((error) => { if (route.sessionID !== sessionID) return toast.show({ @@ -404,6 +408,7 @@ export function Session() { setTimeout(() => { if (!scroll || scroll.isDestroyed) return scroll.scrollTo(scroll.scrollHeight) + setUserScrolledUp(false) }, 50) } @@ -736,6 +741,7 @@ export function Session() { hidden: true, run: () => { scroll.scrollBy(-scroll.height / 2) + setUserScrolledUp(true) dialog.clear() }, }, @@ -756,6 +762,7 @@ export function Session() { hidden: true, run: () => { scroll.scrollBy(-1) + setUserScrolledUp(true) dialog.clear() }, }, @@ -776,6 +783,7 @@ export function Session() { hidden: true, run: () => { scroll.scrollBy(-scroll.height / 4) + setUserScrolledUp(true) dialog.clear() }, }, @@ -796,6 +804,7 @@ export function Session() { hidden: true, run: () => { scroll.scrollTo(0) + setUserScrolledUp(true) dialog.clear() }, }, @@ -806,6 +815,7 @@ export function Session() { hidden: true, run: () => { scroll.scrollTo(scroll.scrollHeight) + setUserScrolledUp(false) dialog.clear() }, }, @@ -1120,7 +1130,7 @@ export function Session() { foregroundColor: theme.border, }, }} - stickyScroll={true} + stickyScroll={!userScrolledUp()} stickyStart="bottom" flexGrow={1} scrollAcceleration={scrollAcceleration()}