Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lua/opencode/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ M.defaults = {
tools = {
show_output = true,
},
always_scroll_to_bottom = false,
},
input = {
text = {
Expand Down
1 change: 1 addition & 0 deletions lua/opencode/types.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
41 changes: 41 additions & 0 deletions lua/opencode/ui/output_window.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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 })
Expand Down Expand Up @@ -177,13 +208,23 @@ 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()
M.set_lines({})
-- 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
25 changes: 15 additions & 10 deletions lua/opencode/ui/renderer.lua
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ function M.reset()
require('opencode.api').respond_to_permission('reject')
end
state.current_permission = nil

trigger_on_data_rendered()
end

Expand Down Expand Up @@ -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

Expand Down