Skip to content

Commit 01b353c

Browse files
committed
fix(streaming_renderer): lifecycle improvements
Trying to clean up the lifecycle management of streaming_renderer and other objects by leverage the state listeners more. Also move the improved autoscrolling code from output_renderer. Note, there is an issue with permission replacement that leaves the cursor up a few lines which stops scrolling so I'm still looking at that Looked up the session compacted event and realized we don't need to reset there but it would be nice to render something for the user.
1 parent 8b1f490 commit 01b353c

File tree

7 files changed

+112
-143
lines changed

7 files changed

+112
-143
lines changed

lua/opencode/core.lua

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,9 @@ function M.select_session(parent_id)
2727
state.active_session = selected_session
2828
if state.windows then
2929
state.restore_points = {}
30-
require('opencode.ui.streaming_renderer').reset()
31-
ui.render_output(true)
30+
-- Don't need to update either renderer because they subscribe to
31+
-- session changes
3232
ui.focus_input()
33-
ui.scroll_to_bottom()
3433
else
3534
M.open()
3635
end
@@ -178,8 +177,6 @@ function M.stop()
178177
state.api_client:abort_session(state.active_session.id):wait()
179178
end
180179
require('opencode.ui.footer').clear()
181-
-- ui.stop_render_output()
182-
-- require('opencode.ui.streaming_renderer').reset_and_render()
183180
input_window.set_content('')
184181
require('opencode.history').index = nil
185182
ui.focus_input()

lua/opencode/event_manager.lua

Lines changed: 0 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -320,51 +320,6 @@ end
320320
function EventManager.setup()
321321
state.event_manager = EventManager.new()
322322
state.event_manager:start()
323-
324-
local streaming_renderer = require('opencode.ui.streaming_renderer')
325-
326-
state.event_manager:subscribe('message.updated', function(event_data)
327-
-- state.last_output = os.time()
328-
-- vim.notify(vim.inspect(event_data) .. ',')
329-
streaming_renderer.handle_message_updated(event_data)
330-
end)
331-
332-
state.event_manager:subscribe('message.part.updated', function(event_data)
333-
-- state.last_output = os.time()
334-
-- vim.notify(vim.inspect(event_data) .. ',')
335-
streaming_renderer.handle_part_updated(event_data)
336-
end)
337-
338-
state.event_manager:subscribe('message.removed', function(event_data)
339-
-- state.last_output = os.time()
340-
-- vim.notify(vim.inspect(event_data) .. ',')
341-
streaming_renderer.handle_message_removed(event_data)
342-
end)
343-
344-
state.event_manager:subscribe('message.part.removed', function(event_data)
345-
-- state.last_output = os.time()
346-
-- vim.notify(vim.inspect(event_data) .. ',')
347-
streaming_renderer.handle_part_removed(event_data)
348-
end)
349-
350-
state.event_manager:subscribe('session.compacted', function(event_data)
351-
-- state.last_output = os.time()
352-
-- vim.notify(vim.inspect(event_data) .. ',')
353-
streaming_renderer.handle_session_compacted()
354-
end)
355-
356-
state.event_manager:subscribe('session.error', function(event_data)
357-
-- state.last_output = os.time()
358-
streaming_renderer.handle_session_error(event_data)
359-
end)
360-
361-
state.event_manager:subscribe('permission.updated', function(event_data)
362-
streaming_renderer.handle_permission_updated(event_data)
363-
end)
364-
365-
state.event_manager:subscribe('permission.replied', function(event_data)
366-
streaming_renderer.handle_permission_replied(event_data)
367-
end)
368323
end
369324

370325
return EventManager

lua/opencode/types.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,7 @@
211211
---@field snapshot string|nil Snapshot commit hash
212212
---@field sessionID string|nil Session identifier
213213
---@field messageID string|nil Message identifier
214+
---@field callID string|nil Call identifier (used for tools)
214215
---@field hash string|nil Hash identifier for patch parts
215216
---@field files string[]|nil List of file paths for patch parts
216217
---@field synthetic boolean|nil Whether the message was generated synthetically

lua/opencode/ui/output_renderer.lua

Lines changed: 14 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,6 @@ local output_window = require('opencode.ui.output_window')
77
local util = require('opencode.util')
88
local Promise = require('opencode.promise')
99

10-
M._cache = {
11-
prev_line_count = 0,
12-
}
13-
1410
M._subscriptions = {}
1511
M._ns_id = vim.api.nvim_create_namespace('opencode_output')
1612
M._debounce_ms = 50
@@ -42,15 +38,13 @@ M.render = vim.schedule_wrap(function(windows, force)
4238
return
4339
end
4440

45-
local changed = M.write_output(windows, lines)
41+
M.write_output(windows, lines)
4642

47-
if changed or force then
48-
vim.schedule(function()
49-
-- M.render_markdown()
50-
M.handle_auto_scroll(windows)
51-
require('opencode.ui.topbar').render()
52-
end)
53-
end
43+
vim.schedule(function()
44+
-- M.render_markdown()
45+
M.handle_auto_scroll(windows)
46+
require('opencode.ui.topbar').render()
47+
end)
5448

5549
pcall(function()
5650
vim.schedule(function()
@@ -93,39 +87,18 @@ function M.teardown()
9387
end
9488

9589
function M.stop()
90+
-- FIXME: the footer should probably own this... and it may
91+
-- not even be necessary
9692
loading_animation.stop()
97-
M._cache.prev_line_count = 0
9893
end
9994

10095
function M.write_output(windows, output_lines)
10196
if not output_window.mounted(windows) then
10297
return false
10398
end
10499

105-
local current_line_count = #output_lines
106-
local prev_line_count = M._cache.prev_line_count
107-
local changed = false
108-
109-
if prev_line_count == 0 then
110-
output_window.set_content(output_lines)
111-
changed = true
112-
elseif current_line_count > prev_line_count then
113-
local new_lines = vim.list_slice(output_lines, prev_line_count + 1)
114-
if #new_lines > 0 then
115-
output_window.append_content(new_lines, prev_line_count)
116-
changed = true
117-
end
118-
else
119-
output_window.set_content(output_lines)
120-
changed = true
121-
end
122-
123-
if changed then
124-
M._cache.prev_line_count = current_line_count
125-
M.apply_output_extmarks(windows)
126-
end
127-
128-
return changed
100+
output_window.set_content(output_lines)
101+
M.apply_output_extmarks(windows)
129102
end
130103

131104
function M.apply_output_extmarks(windows)
@@ -153,28 +126,10 @@ function M.apply_output_extmarks(windows)
153126
end
154127

155128
function M.handle_auto_scroll(windows)
156-
local ok, line_count = pcall(vim.api.nvim_buf_line_count, windows.output_buf)
157-
if not ok then
158-
return
159-
end
160-
161-
local botline = vim.fn.line('w$', windows.output_win)
162-
local cursor = vim.api.nvim_win_get_cursor(windows.output_win)
163-
local cursor_row = cursor[1] or 0
164-
local is_focused = vim.api.nvim_get_current_win() == windows.output_win
165-
166-
local prev_line_count = M._cache.prev_line_count or 0
167-
M._cache.prev_line_count = line_count
168-
169-
local was_at_bottom = (botline >= prev_line_count) or prev_line_count == 0
170-
171-
if is_focused and cursor_row < prev_line_count - 1 then
172-
return
173-
end
174-
175-
if was_at_bottom or not is_focused then
176-
require('opencode.ui.ui').scroll_to_bottom()
177-
end
129+
-- NOTE: logic moved to stream renderer
130+
-- output_render currently only used for loading whole sessions
131+
-- so just scroll to the bottom when that happens
132+
require('opencode.ui.ui').scroll_to_bottom()
178133
end
179134

180135
return M

lua/opencode/ui/streaming_renderer.lua

Lines changed: 88 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,69 @@ local state = require('opencode.state')
22

33
local M = {}
44

5+
M._subscriptions = {}
56
M._part_cache = {}
67
M._message_cache = {}
7-
M._session_id = nil
88
M._namespace = vim.api.nvim_create_namespace('opencode_stream')
9+
M._prev_line_count = 0
910

1011
function M.reset()
1112
M._part_cache = {}
1213
M._message_cache = {}
13-
M._session_id = nil
14+
M._prev_line_count = 0
15+
16+
-- FIXME: this prolly isn't the right place for state.messages to be
17+
-- cleared. It would probably be better to have streaming_renderer
18+
-- subscribe to state changes and if state.messages is cleared, it
19+
-- should clear it's state too
1420
state.messages = {}
1521
end
1622

23+
---Set up all subscriptions
24+
function M.setup_subscriptions(_)
25+
M._subscriptions.active_session = function(_, _, old)
26+
if not old then
27+
return
28+
end
29+
M.reset()
30+
end
31+
state.subscribe('active_session', M._subscriptions.active_session)
32+
M._setup_event_subscriptions()
33+
end
34+
35+
---Set up server event subscriptions
36+
---@param subscribe? boolean false to unsubscribe
37+
function M._setup_event_subscriptions(subscribe)
38+
if not state.event_manager then
39+
return
40+
end
41+
42+
local method = (subscribe == false) and 'unsubscribe' or 'subscribe'
43+
44+
state.event_manager[method](state.event_manager, 'message.updated', M.on_message_updated)
45+
state.event_manager[method](state.event_manager, 'message.part.updated', M.on_part_updated)
46+
state.event_manager[method](state.event_manager, 'message.removed', M.on_message_removed)
47+
state.event_manager[method](state.event_manager, 'message.part.removed', M.on_part_removed)
48+
state.event_manager[method](state.event_manager, 'session.compacted', M.on_session_compacted)
49+
state.event_manager[method](state.event_manager, 'session.error', M.on_session_error)
50+
state.event_manager[method](state.event_manager, 'permission.updated', M.on_permission_updated)
51+
state.event_manager[method](state.event_manager, 'permission.replied', M.on_permission_replied)
52+
end
53+
54+
---Unsubscribe from local state and server subscriptions
55+
function M._cleanup_subscriptions()
56+
M._setup_event_subscriptions(false)
57+
for key, cb in pairs(M._subscriptions) do
58+
state.unsubscribe(key, cb)
59+
end
60+
M._subscriptions = {}
61+
end
62+
63+
function M.teardown()
64+
M._cleanup_subscriptions()
65+
M.reset()
66+
end
67+
1768
function M._get_buffer_line_count()
1869
if not state.windows or not state.windows.output_buf then
1970
return 0
@@ -87,11 +138,29 @@ function M._text_to_lines(text)
87138
end
88139

89140
function M._scroll_to_bottom()
90-
local debounced_scroll = require('opencode.util').debounce(function()
91-
require('opencode.ui.ui').scroll_to_bottom()
92-
end, 50)
141+
local ok, line_count = pcall(vim.api.nvim_buf_line_count, state.windows.output_buf)
142+
if not ok then
143+
return
144+
end
93145

94-
debounced_scroll()
146+
local botline = vim.fn.line('w$', state.windows.output_win)
147+
local cursor = vim.api.nvim_win_get_cursor(state.windows.output_win)
148+
local cursor_row = cursor[1] or 0
149+
local is_focused = vim.api.nvim_get_current_win() == state.windows.output_win
150+
151+
local prev_line_count = M._prev_line_count or 0
152+
M._prev_line_count = line_count
153+
154+
local was_at_bottom = (botline >= prev_line_count) or prev_line_count == 0
155+
156+
if is_focused and cursor_row < prev_line_count - 1 then
157+
return
158+
end
159+
160+
if was_at_bottom or not is_focused then
161+
-- vim.notify('was_at_bottom: ' .. tostring(was_at_bottom) .. ' is_focused: ' .. tostring(is_focused))
162+
require('opencode.ui.ui').scroll_to_bottom()
163+
end
95164
end
96165

97166
function M._write_formatted_data(formatted_data)
@@ -206,7 +275,7 @@ function M._remove_part_from_buffer(part_id)
206275
M._part_cache[part_id] = nil
207276
end
208277

209-
function M.handle_message_updated(event)
278+
function M.on_message_updated(event)
210279
if not event or not event.properties or not event.properties.info then
211280
return
212281
end
@@ -216,13 +285,10 @@ function M.handle_message_updated(event)
216285
return
217286
end
218287

219-
if M._session_id and M._session_id ~= message.sessionID then
220-
-- TODO: there's probably more we need to do here
221-
M.reset()
288+
if state.active_session.id ~= message.sessionID then
289+
vim.notify('Session id does not match, discarding part: ' .. vim.inspect(message), vim.log.levels.WARN)
222290
end
223291

224-
M._session_id = message.sessionID
225-
226292
local found_idx = nil
227293
for i = #state.messages, math.max(1, #state.messages - 2), -1 do
228294
if state.messages[i].info.id == message.id then
@@ -252,7 +318,7 @@ function M.handle_message_updated(event)
252318
M._scroll_to_bottom()
253319
end
254320

255-
function M.handle_part_updated(event)
321+
function M.on_part_updated(event)
256322
if not event or not event.properties or not event.properties.part then
257323
return
258324
end
@@ -262,7 +328,7 @@ function M.handle_part_updated(event)
262328
return
263329
end
264330

265-
if M._session_id and M._session_id ~= part.sessionID then
331+
if state.active_session.id ~= part.sessionID then
266332
vim.notify('Session id does not match, discarding part: ' .. vim.inspect(part), vim.log.levels.WARN)
267333
return
268334
end
@@ -342,7 +408,7 @@ function M.handle_part_updated(event)
342408
M._scroll_to_bottom()
343409
end
344410

345-
function M.handle_part_removed(event)
411+
function M.on_part_removed(event)
346412
-- XXX: I don't have any sessions that remove parts so this code is
347413
-- currently untested
348414
if not event or not event.properties then
@@ -376,7 +442,7 @@ function M.handle_part_removed(event)
376442
M._remove_part_from_buffer(part_id)
377443
end
378444

379-
function M.handle_message_removed(event)
445+
function M.on_message_removed(event)
380446
-- XXX: I don't have any sessions that remove messages so this code is
381447
-- currently untested
382448
if not event or not event.properties then
@@ -416,10 +482,9 @@ function M.handle_message_removed(event)
416482
end
417483
end
418484

419-
function M.handle_session_compacted()
420-
M.reset()
421-
vim.notify('handle_session_compacted')
422-
require('opencode.ui.output_renderer').render(state.windows, true)
485+
function M.on_session_compacted()
486+
vim.notify('on_session_compacted')
487+
-- TODO: render a note that the session was compacted
423488
end
424489

425490
function M.reset_and_render()
@@ -428,7 +493,7 @@ function M.reset_and_render()
428493
require('opencode.ui.output_renderer').render(state.windows, true)
429494
end
430495

431-
function M.handle_session_error(event)
496+
function M.on_session_error(event)
432497
if not event or not event.properties or not event.properties.error then
433498
return
434499
end
@@ -443,7 +508,7 @@ function M.handle_session_error(event)
443508
M._scroll_to_bottom()
444509
end
445510

446-
function M.handle_permission_updated(event)
511+
function M.on_permission_updated(event)
447512
if not event or not event.properties then
448513
return
449514
end
@@ -461,7 +526,7 @@ function M.handle_permission_updated(event)
461526
end
462527
end
463528

464-
function M.handle_permission_replied(event)
529+
function M.on_permission_replied(event)
465530
if not event or not event.properties then
466531
return
467532
end

0 commit comments

Comments
 (0)