Skip to content

Commit cdbe088

Browse files
feat(ui): Improve scroll at bottom to support scrolling without focus
1 parent aad6305 commit cdbe088

File tree

3 files changed

+56
-10
lines changed

3 files changed

+56
-10
lines changed

lua/opencode/state.lua

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
---@field last_input_window_position integer[]|nil
1515
---@field last_output_window_position integer[]|nil
1616
---@field last_code_win_before_opencode integer|nil
17+
---@field output_window_at_bottom boolean
1718
---@field current_code_buf number|nil
1819
---@field display_route any|nil
1920
---@field current_mode string
@@ -55,6 +56,7 @@ local _state = {
5556
last_focused_opencode_window = nil,
5657
last_input_window_position = nil,
5758
last_output_window_position = nil,
59+
output_window_at_bottom = true,
5860
last_code_win_before_opencode = nil,
5961
current_code_buf = nil,
6062
display_route = nil,

lua/opencode/ui/output_window.lua

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,32 @@ function M.mounted(windows)
2727
return windows and windows.output_buf and windows.output_win and vim.api.nvim_win_is_valid(windows.output_win)
2828
end
2929

30+
---Check if the output window is currently at the bottom
31+
---@param win? integer Window ID, defaults to state.windows.output_win
32+
---@return boolean true if at bottom, false otherwise
33+
function M.is_at_bottom(win)
34+
win = win or (state.windows and state.windows.output_win)
35+
36+
if not win or not vim.api.nvim_win_is_valid(win) then
37+
return true -- Assume at bottom if window invalid
38+
end
39+
40+
if not state.windows or not state.windows.output_buf then
41+
return true
42+
end
43+
44+
local ok, line_count = pcall(vim.api.nvim_buf_line_count, state.windows.output_buf)
45+
if not ok or not line_count or line_count == 0 then
46+
return true -- Empty buffer, consider at bottom
47+
end
48+
49+
local botline = vim.fn.line('w$', win)
50+
51+
-- Consider at bottom if bottom visible line is at or near the end
52+
-- Use -1 tolerance for wrapped lines
53+
return botline >= line_count - 1
54+
end
55+
3056
function M.setup(windows)
3157
vim.api.nvim_set_option_value('winhighlight', config.ui.window_highlight, { win = windows.output_win })
3258
vim.api.nvim_set_option_value('wrap', true, { win = windows.output_win })
@@ -177,6 +203,16 @@ function M.setup_autocmds(windows, group)
177203
state.subscribe('current_permission', function()
178204
require('opencode.keymap').toggle_permission_keymap(windows.output_buf)
179205
end)
206+
207+
-- Track scroll position when window is scrolled
208+
vim.api.nvim_create_autocmd('WinScrolled', {
209+
group = group,
210+
buffer = windows.output_buf,
211+
callback = function()
212+
-- Update state to track if user is at bottom
213+
state.output_window_at_bottom = M.is_at_bottom(windows.output_win)
214+
end,
215+
})
180216
end
181217

182218
function M.clear()

lua/opencode/ui/renderer.lua

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ function M.reset()
4646
state.messages = {}
4747
state.last_user_message = nil
4848
state.current_permission = nil
49+
50+
-- Reset scroll state when session changes
51+
state.output_window_at_bottom = true
52+
4953
trigger_on_data_rendered()
5054
end
5155

@@ -220,26 +224,30 @@ function M.scroll_to_bottom()
220224
return
221225
end
222226

223-
local botline = vim.fn.line('w$', state.windows.output_win)
224-
local cursor = vim.api.nvim_win_get_cursor(state.windows.output_win)
225-
local cursor_row = cursor[1] or 0
226-
local is_focused = vim.api.nvim_get_current_win() == state.windows.output_win
227-
228227
local prev_line_count = M._prev_line_count or 0
229228

230229
---@cast line_count integer
231230
M._prev_line_count = line_count
232231

233-
local was_at_bottom = (botline >= prev_line_count) or prev_line_count == 0
234-
235232
trigger_on_data_rendered()
236233

237-
if is_focused and cursor_row < prev_line_count - 1 then
238-
return
234+
-- Determine if we should scroll to bottom
235+
local should_scroll = false
236+
237+
-- Always scroll on initial render
238+
if prev_line_count == 0 then
239+
should_scroll = true
240+
-- Scroll if user is at bottom (respects manual scroll position)
241+
elseif state.output_window_at_bottom then
242+
should_scroll = true
239243
end
240244

241-
if was_at_bottom or not is_focused then
245+
if should_scroll then
242246
vim.api.nvim_win_set_cursor(state.windows.output_win, { line_count, 0 })
247+
state.output_window_at_bottom = true
248+
else
249+
-- User has scrolled up, don't scroll
250+
state.output_window_at_bottom = false
243251
end
244252
end
245253

0 commit comments

Comments
 (0)