diff --git a/lua/opencode/config.lua b/lua/opencode/config.lua index a13d7c4b..3fb9bf8d 100644 --- a/lua/opencode/config.lua +++ b/lua/opencode/config.lua @@ -102,6 +102,7 @@ M.defaults = { tools = { show_output = true, }, + always_scroll_to_bottom = false, }, input = { text = { diff --git a/lua/opencode/types.lua b/lua/opencode/types.lua index 7e8f864d..81b2e62a 100644 --- a/lua/opencode/types.lua +++ b/lua/opencode/types.lua @@ -114,6 +114,7 @@ ---@class OpencodeUIOutputConfig ---@field tools { show_output: boolean } ---@field rendering OpencodeUIOutputRenderingConfig +---@field always_scroll_to_bottom boolean ---@class OpencodeContextConfig ---@field enabled boolean diff --git a/lua/opencode/ui/output_window.lua b/lua/opencode/ui/output_window.lua index c69b754f..696a612e 100644 --- a/lua/opencode/ui/output_window.lua +++ b/lua/opencode/ui/output_window.lua @@ -3,6 +3,7 @@ local config = require('opencode.config') local M = {} M.namespace = vim.api.nvim_create_namespace('opencode_output') +M.viewport_at_bottom = true function M.create_buf() local output_buf = vim.api.nvim_create_buf(false, true) @@ -27,6 +28,36 @@ function M.mounted(windows) return windows and windows.output_buf and windows.output_win and vim.api.nvim_win_is_valid(windows.output_win) end +---Check if the output window is currently at the bottom +---@param win? integer Window ID, defaults to state.windows.output_win +---@return boolean true if at bottom, false otherwise +function M.is_at_bottom(win) + if config.ui.output.always_scroll_to_bottom then + return true + end + + win = win or (state.windows and state.windows.output_win) + + if not win or not vim.api.nvim_win_is_valid(win) then + return true + end + + if not state.windows or not state.windows.output_buf then + return true + end + + local ok, line_count = pcall(vim.api.nvim_buf_line_count, state.windows.output_buf) + if not ok or not line_count or line_count == 0 then + return true + end + + local botline = vim.fn.line('w$', win) + + -- Consider at bottom if bottom visible line is at or near the end + -- Use -1 tolerance for wrapped lines + return botline >= line_count - 1 +end + function M.setup(windows) vim.api.nvim_set_option_value('winhighlight', config.ui.window_highlight, { win = windows.output_win }) vim.api.nvim_set_option_value('wrap', true, { win = windows.output_win }) @@ -177,6 +208,15 @@ function M.setup_autocmds(windows, group) state.subscribe('current_permission', function() require('opencode.keymap').toggle_permission_keymap(windows.output_buf) end) + + -- Track scroll position when window is scrolled + vim.api.nvim_create_autocmd('WinScrolled', { + group = group, + buffer = windows.output_buf, + callback = function() + M.viewport_at_bottom = M.is_at_bottom(windows.output_win) + end, + }) end function M.clear() @@ -184,6 +224,7 @@ function M.clear() -- clear extmarks in all namespaces as I've seen RenderMarkdown leave some -- extmarks behind M.clear_extmarks(0, -1, true) + M.viewport_at_bottom = true end return M diff --git a/lua/opencode/ui/renderer.lua b/lua/opencode/ui/renderer.lua index 2b1762cf..37a15a06 100644 --- a/lua/opencode/ui/renderer.lua +++ b/lua/opencode/ui/renderer.lua @@ -50,6 +50,7 @@ function M.reset() require('opencode.api').respond_to_permission('reject') end state.current_permission = nil + trigger_on_data_rendered() end @@ -224,26 +225,30 @@ function M.scroll_to_bottom() return end - local botline = vim.fn.line('w$', state.windows.output_win) - local cursor = vim.api.nvim_win_get_cursor(state.windows.output_win) - local cursor_row = cursor[1] or 0 - local is_focused = vim.api.nvim_get_current_win() == state.windows.output_win - local prev_line_count = M._prev_line_count or 0 ---@cast line_count integer M._prev_line_count = line_count - local was_at_bottom = (botline >= prev_line_count) or prev_line_count == 0 - trigger_on_data_rendered() - if is_focused and cursor_row < prev_line_count - 1 then - return + -- Determine if we should scroll to bottom + local should_scroll = false + + -- Always scroll on initial render + if prev_line_count == 0 then + should_scroll = true + -- Scroll if user is at bottom (respects manual scroll position) + elseif output_window.viewport_at_bottom then + should_scroll = true end - if was_at_bottom or not is_focused then + if should_scroll then vim.api.nvim_win_set_cursor(state.windows.output_win, { line_count, 0 }) + output_window.viewport_at_bottom = true + else + -- User has scrolled up, don't scroll + output_window.viewport_at_bottom = false end end