Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
43 changes: 43 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,37 @@ 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 always_scroll_to_bottom is enabled, always return true
Comment thread
guillaumeboehm marked this conversation as resolved.
Outdated
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 -- Assume at bottom if window invalid
Comment thread
guillaumeboehm marked this conversation as resolved.
Outdated
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 -- Empty buffer, consider at bottom
Comment thread
guillaumeboehm marked this conversation as resolved.
Outdated
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 +209,24 @@ 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()
-- Update state to track if user is at bottom
Comment thread
guillaumeboehm marked this conversation as resolved.
Outdated
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