@@ -7,9 +7,99 @@ local Promise = require('opencode.promise')
77local ctx = require (' opencode.ui.renderer.ctx' )
88local buf = require (' opencode.ui.renderer.buffer' )
99local events = require (' opencode.ui.renderer.events' )
10+ local session_view = require (' opencode.ui.renderer.session_view' )
11+ local virtualize = require (' opencode.ui.renderer.virtualize' )
12+ local backend = require (' opencode.ui.renderer.backend' )
13+ local overlays = require (' opencode.ui.renderer.components.overlays' )
14+ local scheduler = require (' opencode.ui.renderer.scheduler' )
1015
1116local M = {}
1217
18+ local DEFAULT_MAX_RENDERED_MESSAGES = 80
19+
20+ local function update_current_message_state (messages )
21+ local current_message = nil
22+ local last_user_message = nil
23+
24+ for _ , message in ipairs (messages or {}) do
25+ if message and message .info and message .info .id then
26+ current_message = message
27+ if message .info .role == ' user' then
28+ last_user_message = message
29+ end
30+ end
31+ end
32+
33+ state .renderer .set_current_message (current_message )
34+ state .renderer .set_last_user_message (last_user_message )
35+ end
36+
37+ local function find_revert_index (messages )
38+ if not state .active_session or not state .active_session .revert then
39+ return nil
40+ end
41+
42+ local revert_message_id = state .active_session .revert .messageID
43+ if not revert_message_id then
44+ return nil
45+ end
46+
47+ for i , message in ipairs (messages or {}) do
48+ if message and message .info and message .info .id == revert_message_id then
49+ return i
50+ end
51+ end
52+
53+ return nil
54+ end
55+
56+ local function build_full_session_output (messages )
57+ local revert_index = find_revert_index (messages )
58+ local max_messages = config .ui .output .max_rendered_messages or DEFAULT_MAX_RENDERED_MESSAGES
59+ local hidden_message_count = math.max (# (messages or {}) - max_messages , 0 )
60+ local base_blocks = session_view .build_blocks (messages , {
61+ max_messages = max_messages ,
62+ get_child_parts = function (session_id )
63+ return ctx .render_state :get_child_session_parts (session_id )
64+ end ,
65+ include_revert_overlay = revert_index ~= nil ,
66+ revert_index = revert_index ,
67+ revert_info = state .active_session and state .active_session .revert or nil ,
68+ })
69+
70+ local visible_render = virtualize .select_visible_blocks (base_blocks , {
71+ max_lines = config .ui .output .max_rendered_lines ,
72+ })
73+
74+ if hidden_message_count > 0 then
75+ local hidden_block = overlays .render_hidden_history ({
76+ hidden_count = hidden_message_count ,
77+ })
78+ if hidden_block then
79+ table.insert (visible_render .blocks , 1 , hidden_block )
80+ visible_render .visible_line_count = visible_render .visible_line_count + (hidden_block .line_count or 0 )
81+ end
82+ end
83+
84+ visible_render .hidden_message_count = hidden_message_count
85+
86+ return visible_render
87+ end
88+
89+ local function apply_visible_render (visible_render , should_scroll )
90+ backend .apply_full (visible_render )
91+
92+ local visible_keys = {}
93+ for _ , block in ipairs (visible_render .blocks or {}) do
94+ visible_keys [# visible_keys + 1 ] = block .key
95+ end
96+ ctx .prev_visible_keys = visible_keys
97+
98+ if should_scroll then
99+ M .scroll_to_bottom ()
100+ end
101+ end
102+
13103-- Expose event handlers on M so tests can call them directly and subscriptions
14104-- can be stubbed cleanly (e.g. stub(renderer, '_render_full_session_data'))
15105M .on_session_updated = events .on_session_updated
@@ -134,38 +224,42 @@ end
134224function M ._render_full_session_data (session_data )
135225 M .reset ()
136226
137- if not state .active_session or not state . messages then
227+ if not state .active_session then
138228 return
139229 end
140230
141- local revert_index = nil
142- local set_mode_from_messages = not state .current_model
143-
144- for i , msg in ipairs (session_data ) do
145- if state .active_session .revert and state .active_session .revert .messageID == msg .info .id then
146- revert_index = i
147- end
148- events .on_message_updated ({ info = msg .info }, revert_index )
149- for _ , part in ipairs (msg .parts or {}) do
150- events .on_part_updated ({ part = part }, revert_index )
151- end
231+ state .renderer .set_messages (session_data or {})
232+ if not state .messages then
233+ return
152234 end
153235
154- if revert_index then
155- buf .write_formatted_data (formatter ._format_revert_message (state .messages , revert_index ))
156- end
236+ local set_mode_from_messages = not state .current_model
237+
238+ update_current_message_state (state .messages )
239+ local visible_render = build_full_session_output (state .messages )
240+ apply_visible_render (visible_render , true )
157241
158242 if set_mode_from_messages then
159243 set_model_and_mode_from_messages ()
160244 end
161245
162- M .scroll_to_bottom (true )
163-
164246 if config .hooks and config .hooks .on_session_loaded then
165247 pcall (config .hooks .on_session_loaded , state .active_session )
166248 end
167249end
168250
251+ --- @param snapshot ? { full_render ?: boolean , dirty_messages ?: table<string , boolean> , dirty_parts ?: table<string , boolean> }
252+ function M .perform_scheduled_render (snapshot )
253+ if not output_window .mounted () or not state .active_session or not state .messages then
254+ return
255+ end
256+
257+ local should_scroll = snapshot == nil or snapshot .full_render == true or next (snapshot .dirty_messages or {}) ~= nil
258+ update_current_message_state (state .messages )
259+ local visible_render = build_full_session_output (state .messages )
260+ apply_visible_render (visible_render , should_scroll )
261+ end
262+
169263--- Fetch the active session from the server and render it
170264--- @return Promise<OpencodeMessage[]>
171265function M .render_full_session ()
259353
260354--- Scroll to bottom after all queued events have been processed
261355function M .on_emit_events_finished ()
356+ scheduler .flush ()
262357 M .scroll_to_bottom ()
263358end
264359
0 commit comments