@@ -12,6 +12,29 @@ local M = {}
1212local HIDDEN_MESSAGES_NOTICE_MESSAGE_ID = ' __opencode_hidden_messages_notice__'
1313local HIDDEN_MESSAGES_NOTICE_PART_ID = ' __opencode_hidden_messages_notice_part__'
1414
15+ -- Internal lazy-render: only render enough messages to fill the viewport
16+ -- on initial load, load more on scroll-to-top.
17+ -- Estimated lines per message (header + body). Used to calculate how many
18+ -- messages to render initially without formatting the entire session.
19+ local EST_LINES_PER_MSG = 5
20+ -- Extra buffer ratio on top of the viewport estimate
21+ local VIEWPORT_BUFFER = 1.5
22+
23+ --- Calculate how many messages to render initially based on window height.
24+ --- @return integer
25+ local function get_initial_render_count ()
26+ local win = state .windows and state .windows .output_win
27+ if not win or not vim .api .nvim_win_is_valid (win ) then
28+ return math.huge -- no window: render all (tests, headless)
29+ end
30+ local ok , height = pcall (vim .api .nvim_win_get_height , win )
31+ if not ok or not height or height <= 0 then
32+ return math.huge
33+ end
34+ -- ceiling division: how many messages to fill the viewport + buffer
35+ return math.ceil (height / EST_LINES_PER_MSG * VIEWPORT_BUFFER )
36+ end
37+
1538--- @return integer | nil
1639local function get_max_rendered_messages ()
1740 local limit = config .ui and config .ui .output and config .ui .output .max_messages
319342--- @param opts ? { restore_model_from_messages ?: boolean }
320343function M ._render_full_session_data (session_data , opts )
321344 opts = opts or {}
345+ -- Read lazy_render_count BEFORE reset() clears it, so load_more_messages
346+ -- can incrementally increase the rendered count across re-renders.
347+ local lazy_limit = ctx .lazy_render_count
348+ local t_start = vim .uv .hrtime ()
322349 M .reset ()
323350 state .renderer .set_messages (session_data or {})
324351
@@ -329,6 +356,25 @@ function M._render_full_session_data(session_data, opts)
329356 local visible_messages , hidden_count = get_visible_session_messages (state .messages )
330357 local revert_index = get_revert_index (state .messages )
331358
359+ -- Apply lazy-render limit: only render enough messages to fill the viewport.
360+ -- User can scroll to top to load more. Only active when explicitly requested
361+ -- (lazy_render_count is set by load_more_messages) or when rendering a full
362+ -- session for the first time (opts.lazy = true).
363+ if lazy_limit == nil and opts .lazy then
364+ local initial = get_initial_render_count ()
365+ if # visible_messages > initial then
366+ lazy_limit = initial
367+ end
368+ end
369+ -- Persist lazy_limit back to ctx so subsequent load_more_messages can
370+ -- read it before the next M.reset() call.
371+ ctx .lazy_render_count = lazy_limit
372+ if lazy_limit and # visible_messages > lazy_limit then
373+ hidden_count = hidden_count + (# visible_messages - lazy_limit )
374+ visible_messages = vim .list_slice (visible_messages , # visible_messages - lazy_limit + 1 )
375+ end
376+
377+ local t_format_start = vim .uv .hrtime ()
332378 flush .begin_bulk_mode ()
333379
334380 if hidden_count > 0 then
@@ -376,8 +422,10 @@ function M._render_full_session_data(session_data, opts)
376422 events .on_part_updated ({ part = revert_message .parts [1 ] })
377423 end
378424
425+ local t_format_end = vim .uv .hrtime ()
379426 flush .flush ()
380427 flush .end_bulk_mode ()
428+ local t_flush_end = vim .uv .hrtime ()
381429
382430 if opts .restore_model_from_messages then
383431 require (' opencode.services.agent_model' ).initialize_current_model ({ restore_from_messages = true })
@@ -399,6 +447,7 @@ function M.render_from_cache(session_data)
399447 end
400448 M ._render_full_session_data (session_data , {
401449 restore_model_from_messages = true ,
450+ lazy = ctx .lazy_render_count ~= nil ,
402451 })
403452 local active_session = state .active_session
404453 if active_session and active_session .id then
@@ -407,6 +456,28 @@ function M.render_from_cache(session_data)
407456 end
408457end
409458
459+ --- Load more older messages into the output buffer.
460+ --- Called when user scrolls to the top of the output window.
461+ --- @return boolean Whether more messages were loaded
462+ function M .load_more_messages ()
463+ if not state .messages then
464+ return false
465+ end
466+ local total = # get_visible_session_messages (state .messages )
467+ if total == 0 then
468+ return false
469+ end
470+ local current = ctx .lazy_render_count or math.min (get_initial_render_count (), total )
471+ if current >= total then
472+ return false
473+ end
474+
475+ -- Load another viewport's worth of messages
476+ ctx .lazy_render_count = math.min (current + get_initial_render_count (), total )
477+ M .render_from_cache (state .messages )
478+ return true
479+ end
480+
410481--- Fetch the active session from the server and render it
411482--- @return Promise<OpencodeMessage[]>
412483function M .render_full_session ()
@@ -416,6 +487,7 @@ function M.render_full_session()
416487 return fetch_session ():and_then (function (session_data )
417488 M ._render_full_session_data (session_data , {
418489 restore_model_from_messages = true ,
490+ lazy = true ,
419491 })
420492 local active_session = state .active_session
421493 if active_session and active_session .id then
0 commit comments