Skip to content
Open
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
30 changes: 19 additions & 11 deletions lua/opencode/ui/output_window.lua
Original file line number Diff line number Diff line change
Expand Up @@ -404,31 +404,28 @@ function M.set_folds(fold_ranges)
end

local was_open = M.get_open_fold_starts(win, buf)

vim.api.nvim_buf_set_var(buf, 'opencode_folds', folds)

vim.api.nvim_win_call(win, function()
local view = vim.fn.winsaveview()
vim.cmd('silent! normal! zx')
local prev_starts = {}
for _, start_line in ipairs(prev_folds.starts) do
prev_starts[start_line] = true
end

-- manual avoids foldexpr recalculation on each fold operation
vim.api.nvim_set_option_value('foldmethod', 'manual', { win = 0 })

local line_count = vim.api.nvim_buf_line_count(buf)
for _, range in ipairs(folds.ranges) do
if not prev_starts[range.from] then
vim.fn.cursor(range.from, 1)
vim.cmd('silent! normal! zc')
if range.from <= line_count and range.to <= line_count then
vim.cmd(range.from .. ',' .. range.to .. 'fold')
end
end

for _, range in ipairs(folds.ranges) do
if was_open[range.from] then
vim.fn.cursor(range.from, 1)
vim.cmd('silent! normal! zo')
vim.cmd(range.from .. ',' .. range.to .. 'foldopen!')
end
end

-- stay manual; switching to expr re-evaluates foldexpr per-line
vim.fn.winrestview(view)
end)
end
Expand Down Expand Up @@ -692,11 +689,22 @@ function M.setup_autocmds(windows, group)
end,
})

-- Lazy-render: load more messages on scroll-to-top (debounced)
local debounced_load_more = require('opencode.util').debounce(function()
local renderer = require('opencode.ui.renderer')
if renderer.load_more_messages() then
pcall(vim.api.nvim_win_set_cursor, windows.output_win, { 3, 0 })
end
end, 150)
vim.api.nvim_create_autocmd('WinScrolled', {
group = group,
buffer = windows.output_buf,
callback = function()
M.sync_cursor_with_viewport(windows.output_win)
local ok, cursor = pcall(vim.api.nvim_win_get_cursor, windows.output_win)
if ok and cursor and cursor[1] <= 3 then
debounced_load_more()
end
end,
})
end
Expand Down
64 changes: 64 additions & 0 deletions lua/opencode/ui/renderer.lua
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,25 @@ local M = {}
local HIDDEN_MESSAGES_NOTICE_MESSAGE_ID = '__opencode_hidden_messages_notice__'
local HIDDEN_MESSAGES_NOTICE_PART_ID = '__opencode_hidden_messages_notice_part__'

-- Lazy-render: render only viewport-sized message count on initial load,
-- load more on scroll-to-top.
local EST_LINES_PER_MSG = 5
local VIEWPORT_BUFFER = 1.5

---Calculate how many messages to render initially based on window height.
---@return integer
local function get_initial_render_count()
local win = state.windows and state.windows.output_win
if not win or not vim.api.nvim_win_is_valid(win) then
return math.huge -- no window: render all (tests, headless)
end
local ok, height = pcall(vim.api.nvim_win_get_height, win)
if not ok or not height or height <= 0 then
return math.huge
end
return math.ceil(height / EST_LINES_PER_MSG * VIEWPORT_BUFFER)
end

---@return integer|nil
local function get_max_rendered_messages()
local limit = config.ui and config.ui.output and config.ui.output.max_messages
Expand Down Expand Up @@ -319,6 +338,9 @@ end
---@param opts? { restore_model_from_messages?: boolean }
function M._render_full_session_data(session_data, opts)
opts = opts or {}
-- Read before reset() clears it
local lazy_limit = ctx.lazy_render_count
local t_start = vim.uv.hrtime()
M.reset()
state.renderer.set_messages(session_data or {})

Expand All @@ -329,6 +351,22 @@ function M._render_full_session_data(session_data, opts)
local visible_messages, hidden_count = get_visible_session_messages(state.messages)
local revert_index = get_revert_index(state.messages)

-- Lazy-render: only render viewport-sized message count.
-- Active when ctx.lazy_render_count is set or opts.lazy=true.
if lazy_limit == nil and opts.lazy then
local initial = get_initial_render_count()
if #visible_messages > initial then
lazy_limit = initial
end
end
-- Persist back to ctx so it survives the next M.reset()
ctx.lazy_render_count = lazy_limit
if lazy_limit and #visible_messages > lazy_limit then
hidden_count = hidden_count + (#visible_messages - lazy_limit)
visible_messages = vim.list_slice(visible_messages, #visible_messages - lazy_limit + 1)
end

local t_format_start = vim.uv.hrtime()
flush.begin_bulk_mode()

if hidden_count > 0 then
Expand Down Expand Up @@ -376,8 +414,10 @@ function M._render_full_session_data(session_data, opts)
events.on_part_updated({ part = revert_message.parts[1] })
end

local t_format_end = vim.uv.hrtime()
flush.flush()
flush.end_bulk_mode()
local t_flush_end = vim.uv.hrtime()

if opts.restore_model_from_messages then
require('opencode.services.agent_model').initialize_current_model({ restore_from_messages = true })
Expand All @@ -399,6 +439,7 @@ function M.render_from_cache(session_data)
end
M._render_full_session_data(session_data, {
restore_model_from_messages = true,
lazy = ctx.lazy_render_count ~= nil,
})
local active_session = state.active_session
if active_session and active_session.id then
Expand All @@ -407,6 +448,28 @@ function M.render_from_cache(session_data)
end
end

---Load more older messages into the output buffer.
---Called when user scrolls to the top of the output window.
---@return boolean Whether more messages were loaded
function M.load_more_messages()
if not state.messages then
return false
end
local total = #get_visible_session_messages(state.messages)
if total == 0 then
return false
end
local current = ctx.lazy_render_count or math.min(get_initial_render_count(), total)
if current >= total then
return false
end

-- Load another viewport's worth
ctx.lazy_render_count = math.min(current + get_initial_render_count(), total)
M.render_from_cache(state.messages)
return true
end

---Fetch the active session from the server and render it
---@return Promise<OpencodeMessage[]>
function M.render_full_session()
Expand All @@ -416,6 +479,7 @@ function M.render_full_session()
return fetch_session():and_then(function(session_data)
M._render_full_session_data(session_data, {
restore_model_from_messages = true,
lazy = true,
})
local active_session = state.active_session
if active_session and active_session.id then
Expand Down
3 changes: 3 additions & 0 deletions lua/opencode/ui/renderer/ctx.lua
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ local ctx = {
global_folds = {},
---@type table<string, {from: number, to: number}[]>
part_folds = {},
---@type integer|nil Number of messages to render from the end (nil = all)
lazy_render_count = nil,
}

---Reset all renderer caches and pending state.
Expand All @@ -56,6 +58,7 @@ function ctx:reset()
self.markdown_render_scheduled = false
self.global_folds = {}
self.part_folds = {}
self.lazy_render_count = nil
self:bulk_reset()
end

Expand Down
Loading
Loading