Skip to content

Commit 66e1e7a

Browse files
committed
refactor(renderer): add batched flush, append and scroll subsystems
Introduce a renderer.flush scheduler to batch dirty/removed message
1 parent 281e026 commit 66e1e7a

8 files changed

Lines changed: 707 additions & 348 deletions

File tree

lua/opencode/ui/formatter.lua

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,12 @@ function M.format_part(part, message, is_last_part, get_child_parts)
522522
local question_window = require('opencode.ui.question_window')
523523
question_window.format_display(output)
524524
content_added = true
525+
elseif part.type == 'revert-display' then
526+
local revert_index = part.state and part.state.revert_index
527+
if revert_index then
528+
output = M._format_revert_message(state.messages or {}, revert_index)
529+
content_added = output:get_line_count() > 0
530+
end
525531
end
526532
end
527533

lua/opencode/ui/renderer.lua

Lines changed: 32 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,18 @@
11
local state = require('opencode.state')
22
local config = require('opencode.config')
3-
local formatter = require('opencode.ui.formatter')
43
local output_window = require('opencode.ui.output_window')
54
local permission_window = require('opencode.ui.permission_window')
65
local Promise = require('opencode.promise')
76
local ctx = require('opencode.ui.renderer.ctx')
8-
local buf = require('opencode.ui.renderer.buffer')
97
local events = require('opencode.ui.renderer.events')
8+
local flush = require('opencode.ui.renderer.flush')
109

1110
local M = {}
1211

1312
-- Expose event handlers on M so tests can call them directly and subscriptions
1413
-- can be stubbed cleanly (e.g. stub(renderer, '_render_full_session_data'))
1514
M.on_session_updated = events.on_session_updated
1615

17-
local trigger_on_data_rendered = require('opencode.util').debounce(function()
18-
local cb_type = type(config.ui.output.rendering.on_data_rendered)
19-
if cb_type == 'boolean' then
20-
return
21-
end
22-
if not state.windows or not state.windows.output_buf or not state.windows.output_win then
23-
return
24-
end
25-
if cb_type == 'function' then
26-
pcall(config.ui.output.rendering.on_data_rendered, state.windows.output_buf, state.windows.output_win)
27-
elseif vim.fn.exists(':RenderMarkdown') > 0 then
28-
vim.cmd(':RenderMarkdown')
29-
elseif vim.fn.exists(':Markview') > 0 then
30-
vim.cmd(':Markview render ' .. state.windows.output_buf)
31-
end
32-
end, config.ui.output.rendering.markdown_debounce_ms or 250)
33-
3416
---Reset all renderer state and clear the output buffer
3517
function M.reset()
3618
ctx:reset()
@@ -45,7 +27,7 @@ function M.reset()
4527
permission_window.clear_all()
4628
state.renderer.reset()
4729

48-
trigger_on_data_rendered()
30+
flush.trigger_on_data_rendered()
4931
end
5032

5133
---Unsubscribe from all events and reset
@@ -152,9 +134,32 @@ function M._render_full_session_data(session_data)
152134
end
153135

154136
if revert_index then
155-
buf.write_formatted_data(formatter._format_revert_message(state.messages, revert_index))
137+
local revert_message = {
138+
info = {
139+
id = '__opencode_revert_message__',
140+
sessionID = state.active_session.id,
141+
role = 'system',
142+
},
143+
parts = {
144+
{
145+
id = '__opencode_revert_part__',
146+
messageID = '__opencode_revert_message__',
147+
sessionID = state.active_session.id,
148+
type = 'revert-display',
149+
state = {
150+
revert_index = revert_index,
151+
},
152+
},
153+
},
154+
}
155+
156+
table.insert(state.messages, revert_message)
157+
events.on_message_updated(revert_message)
158+
events.on_part_updated({ part = revert_message.parts[1] })
156159
end
157160

161+
flush.flush()
162+
158163
if set_mode_from_messages then
159164
set_model_and_mode_from_messages()
160165
end
@@ -192,6 +197,7 @@ function M.render_output(output_data)
192197
output_window.set_lines(output_data.lines or {})
193198
output_window.clear_extmarks()
194199
output_window.set_extmarks(output_data.extmarks)
200+
flush.trigger_on_data_rendered()
195201
M.scroll_to_bottom()
196202
end
197203

@@ -218,15 +224,13 @@ function M.scroll_to_bottom(force)
218224
local prev_line_count = ctx.prev_line_count
219225
ctx.prev_line_count = line_count
220226

221-
trigger_on_data_rendered()
227+
local ok_cursor, cursor = pcall(vim.api.nvim_win_get_cursor, output_win)
222228

223229
local should_scroll = force
224230
or prev_line_count == 0
225231
or config.ui.output.always_scroll_to_bottom
226-
or (function()
227-
local ok_cursor, cursor = pcall(vim.api.nvim_win_get_cursor, output_win)
228-
return ok_cursor and cursor and (cursor[1] >= prev_line_count or cursor[1] >= line_count)
229-
end)()
232+
or (ok_cursor and cursor and cursor[1] >= prev_line_count)
233+
or output_window.is_at_bottom(output_win)
230234

231235
if should_scroll then
232236
local last_line = vim.api.nvim_buf_get_lines(output_buf, line_count - 1, line_count, false)[1] or ''
@@ -242,8 +246,8 @@ function M.on_focus_changed()
242246
if not permission_window.get_all_permissions()[1] then
243247
return
244248
end
245-
buf.rerender_part('permission-display-part')
246-
trigger_on_data_rendered()
249+
flush.mark_part_dirty('permission-display-part', 'permission-display-message')
250+
flush.flush()
247251
end
248252

249253
---Re-render when the active session changes
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
local M = {}
2+
3+
---@param old_lines string[]
4+
---@param new_lines string[]
5+
---@return boolean
6+
function M.is_append_only(old_lines, new_lines)
7+
local old_count = #old_lines
8+
if #new_lines <= old_count then
9+
return false
10+
end
11+
12+
for i = old_count, 1, -1 do
13+
if old_lines[i] ~= new_lines[i] then
14+
return false
15+
end
16+
end
17+
18+
return true
19+
end
20+
21+
---@param old_lines string[]
22+
---@param new_lines string[]
23+
---@return string[]
24+
function M.tail_lines(old_lines, new_lines)
25+
return vim.list_slice(new_lines, #old_lines + 1, #new_lines)
26+
end
27+
28+
---@param row_offset integer
29+
---@param extmarks table<number, OutputExtmark[]>|nil
30+
---@return table<number, OutputExtmark[]>
31+
function M.tail_extmarks(row_offset, extmarks)
32+
local tail = {}
33+
34+
for line_idx, marks in pairs(extmarks or {}) do
35+
if line_idx >= row_offset then
36+
tail[line_idx - row_offset] = vim.deepcopy(marks)
37+
end
38+
end
39+
40+
return tail
41+
end
42+
43+
return M

0 commit comments

Comments
 (0)