From fcc78c71a53825dba498536c593fbdf50c7727db Mon Sep 17 00:00:00 2001 From: disrupted Date: Fri, 13 Feb 2026 19:54:09 +0100 Subject: [PATCH 1/3] fix(ui): avoid forced scroll-to-bottom while reading older output --- lua/opencode/ui/renderer.lua | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lua/opencode/ui/renderer.lua b/lua/opencode/ui/renderer.lua index 833a349a..255fffbc 100644 --- a/lua/opencode/ui/renderer.lua +++ b/lua/opencode/ui/renderer.lua @@ -323,11 +323,17 @@ function M.scroll_to_bottom(force) local should_scroll = force == true if not should_scroll then + local ok_cursor, cursor = pcall(vim.api.nvim_win_get_cursor, state.windows.output_win) + local cursor_row = ok_cursor and cursor[1] or 1 + -- Always scroll on initial render if prev_line_count == 0 then should_scroll = true + -- Respect explicit config to always follow output + elseif config.ui.output.always_scroll_to_bottom then + should_scroll = true -- Scroll if user is at bottom (respects manual scroll position) - elseif output_window.viewport_at_bottom then + elseif cursor_row >= prev_line_count then should_scroll = true end end @@ -338,10 +344,6 @@ function M.scroll_to_bottom(force) vim.api.nvim_win_call(state.windows.output_win, function() vim.cmd('normal! zb') end) - output_window.viewport_at_bottom = true - else - -- User has scrolled up, don't scroll - output_window.viewport_at_bottom = false end end From ad0e3d3dc6bc4e848c8f428cfbdcab12c8acacdf Mon Sep 17 00:00:00 2001 From: disrupted Date: Fri, 13 Feb 2026 20:02:34 +0100 Subject: [PATCH 2/3] refactor(ui): only retrieve cursor position if necessary --- lua/opencode/ui/renderer.lua | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/lua/opencode/ui/renderer.lua b/lua/opencode/ui/renderer.lua index 255fffbc..f16f78dc 100644 --- a/lua/opencode/ui/renderer.lua +++ b/lua/opencode/ui/renderer.lua @@ -323,9 +323,6 @@ function M.scroll_to_bottom(force) local should_scroll = force == true if not should_scroll then - local ok_cursor, cursor = pcall(vim.api.nvim_win_get_cursor, state.windows.output_win) - local cursor_row = ok_cursor and cursor[1] or 1 - -- Always scroll on initial render if prev_line_count == 0 then should_scroll = true @@ -333,8 +330,10 @@ function M.scroll_to_bottom(force) elseif config.ui.output.always_scroll_to_bottom then should_scroll = true -- Scroll if user is at bottom (respects manual scroll position) - elseif cursor_row >= prev_line_count then - should_scroll = true + else + local ok_cursor, cursor = pcall(vim.api.nvim_win_get_cursor, state.windows.output_win) + local cursor_row = ok_cursor and cursor[1] or 1 + should_scroll = cursor_row >= prev_line_count end end From 677d437db8d4b38a2865ed08d68dd91bdb99756d Mon Sep 17 00:00:00 2001 From: disrupted Date: Fri, 13 Feb 2026 20:06:21 +0100 Subject: [PATCH 3/3] test: add unit tests for scroll_to_bottom behavior --- tests/unit/cursor_tracking_spec.lua | 54 +++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/tests/unit/cursor_tracking_spec.lua b/tests/unit/cursor_tracking_spec.lua index 5a63a91a..00fa8c15 100644 --- a/tests/unit/cursor_tracking_spec.lua +++ b/tests/unit/cursor_tracking_spec.lua @@ -185,3 +185,57 @@ describe('output_window.is_at_bottom', function() assert.is_true(output_window.is_at_bottom(win)) end) end) + +describe('renderer.scroll_to_bottom', function() + local renderer = require('opencode.ui.renderer') + local output_window = require('opencode.ui.output_window') + local buf, win + + before_each(function() + config.setup({}) + buf = vim.api.nvim_create_buf(false, true) + local lines = {} + for i = 1, 50 do + lines[i] = 'line ' .. i + end + vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines) + + win = vim.api.nvim_open_win(buf, true, { + relative = 'editor', width = 80, height = 10, row = 0, col = 0, + }) + + state.windows = { output_win = win, output_buf = buf } + renderer._prev_line_count = 50 + end) + + after_each(function() + pcall(vim.api.nvim_win_close, win, true) + pcall(vim.api.nvim_buf_delete, buf, { force = true }) + state.windows = nil + renderer._prev_line_count = 0 + output_window.viewport_at_bottom = nil + end) + + it('does not force-scroll when user cursor is above previous bottom', function() + vim.api.nvim_win_set_cursor(win, { 10, 0 }) + output_window.viewport_at_bottom = true + + vim.api.nvim_buf_set_lines(buf, -1, -1, false, { 'line 51' }) + renderer.scroll_to_bottom() + + local cursor = vim.api.nvim_win_get_cursor(win) + assert.equals(10, cursor[1]) + end) + + it('still scrolls when always_scroll_to_bottom is enabled', function() + config.values.ui.output.always_scroll_to_bottom = true + vim.api.nvim_win_set_cursor(win, { 10, 0 }) + + vim.api.nvim_buf_set_lines(buf, -1, -1, false, { 'line 51' }) + renderer.scroll_to_bottom() + + local cursor = vim.api.nvim_win_get_cursor(win) + assert.equals(51, cursor[1]) + config.values.ui.output.always_scroll_to_bottom = false + end) +end)