@@ -9,6 +9,222 @@ local flush = require('opencode.ui.renderer.flush')
99local scroll = require (' opencode.ui.renderer.scroll' )
1010
1111local M = {}
12+ local HIDDEN_MESSAGES_NOTICE_MESSAGE_ID = ' __opencode_hidden_messages_notice__'
13+ local HIDDEN_MESSAGES_NOTICE_PART_ID = ' __opencode_hidden_messages_notice_part__'
14+
15+ --- @return integer | nil
16+ local function get_max_rendered_messages ()
17+ local limit = config .ui and config .ui .output and config .ui .output .max_messages
18+ if type (limit ) ~= ' number' or limit <= 0 then
19+ return nil
20+ end
21+ return math.floor (limit )
22+ end
23+
24+ --- @param message OpencodeMessage | nil
25+ --- @return boolean
26+ local function is_renderer_synthetic_message (message )
27+ local message_id = message and message .info and message .info .id
28+ return message_id == ' __opencode_revert_message__' or message_id == HIDDEN_MESSAGES_NOTICE_MESSAGE_ID
29+ end
30+
31+ --- @param message OpencodeMessage | nil
32+ --- @return boolean
33+ local function is_active_session_message (message )
34+ local session_id = message and message .info and message .info .sessionID
35+ return session_id ~= nil and state .active_session and state .active_session .id == session_id
36+ end
37+
38+ --- @param messages OpencodeMessage[] | nil
39+ --- @return OpencodeMessage[]
40+ local function get_real_session_messages (messages )
41+ return vim .tbl_filter (function (message )
42+ return is_active_session_message (message ) and not is_renderer_synthetic_message (message )
43+ end , messages or {})
44+ end
45+
46+ --- @param messages OpencodeMessage[] | nil
47+ --- @return integer | nil
48+ local function get_revert_index (messages )
49+ local revert = state .active_session and state .active_session .revert
50+ local revert_message_id = revert and revert .messageID
51+ if not revert_message_id then
52+ return nil
53+ end
54+
55+ local real_messages = get_real_session_messages (messages )
56+ for i , message in ipairs (real_messages ) do
57+ if message .info and message .info .id == revert_message_id then
58+ return i
59+ end
60+ end
61+
62+ return nil
63+ end
64+
65+ --- @param messages OpencodeMessage[] | nil
66+ --- @return OpencodeMessage[] visible_messages
67+ --- @return integer hidden_count
68+ local function get_visible_session_messages (messages )
69+ local real_messages = get_real_session_messages (messages )
70+ local revert_index = get_revert_index (messages )
71+ if revert_index then
72+ real_messages = vim .list_slice (real_messages , 1 , revert_index - 1 )
73+ end
74+
75+ local limit = get_max_rendered_messages ()
76+ if not limit or # real_messages <= limit then
77+ return real_messages , 0
78+ end
79+
80+ local start_index = # real_messages - limit + 1
81+ return vim .list_slice (real_messages , start_index , # real_messages ), start_index - 1
82+ end
83+
84+ --- @param hidden_count integer
85+ --- @return OpencodeMessage
86+ local function build_hidden_messages_notice (hidden_count )
87+ local session_id = state .active_session and state .active_session .id or ' '
88+ return {
89+ info = {
90+ id = HIDDEN_MESSAGES_NOTICE_MESSAGE_ID ,
91+ sessionID = session_id ,
92+ role = ' system' ,
93+ },
94+ parts = {
95+ {
96+ id = HIDDEN_MESSAGES_NOTICE_PART_ID ,
97+ messageID = HIDDEN_MESSAGES_NOTICE_MESSAGE_ID ,
98+ sessionID = session_id ,
99+ type = ' hidden-messages-display' ,
100+ state = {
101+ hidden_count = hidden_count ,
102+ },
103+ },
104+ },
105+ }
106+ end
107+
108+ --- @param message_id string
109+ --- @return OpencodeMessage | nil
110+ local function find_message_in_state (message_id )
111+ for _ , message in ipairs (state .messages or {}) do
112+ if message .info and message .info .id == message_id then
113+ return message
114+ end
115+ end
116+ return nil
117+ end
118+
119+ --- @param message OpencodeMessage
120+ local function ensure_message_rendered (message )
121+ local message_id = message .info and message .info .id
122+ if not message_id or ctx .render_state :get_message (message_id ) then
123+ return
124+ end
125+
126+ ctx .render_state :set_message (message )
127+ flush .mark_message_dirty (message_id )
128+
129+ for _ , part in ipairs (message .parts or {}) do
130+ if part .id and part .type ~= ' step-start' and part .type ~= ' step-finish' then
131+ ctx .render_state :set_part (part )
132+ flush .mark_part_dirty (part .id , message_id )
133+ end
134+ end
135+ end
136+
137+ --- @param hidden_count integer
138+ local function upsert_hidden_messages_notice (hidden_count )
139+ local existing_message = ctx .render_state :get_message (HIDDEN_MESSAGES_NOTICE_MESSAGE_ID )
140+ local notice_message = build_hidden_messages_notice (hidden_count )
141+
142+ if not existing_message then
143+ ensure_message_rendered (notice_message )
144+ return
145+ end
146+
147+ local existing_part = ctx .render_state :get_part (HIDDEN_MESSAGES_NOTICE_PART_ID )
148+ if not existing_part or not existing_part .part then
149+ hide_rendered_message (HIDDEN_MESSAGES_NOTICE_MESSAGE_ID )
150+ ensure_message_rendered (notice_message )
151+ return
152+ end
153+
154+ ctx .render_state :set_message (notice_message , existing_message .line_start , existing_message .line_end )
155+ ctx .render_state :set_part (notice_message .parts [1 ], existing_part .line_start , existing_part .line_end )
156+ flush .mark_part_dirty (HIDDEN_MESSAGES_NOTICE_PART_ID , HIDDEN_MESSAGES_NOTICE_MESSAGE_ID )
157+ end
158+
159+ --- @param message_id string
160+ local function hide_rendered_message (message_id )
161+ local rendered_message = ctx .render_state :get_message (message_id )
162+ local message = rendered_message and rendered_message .message or find_message_in_state (message_id )
163+ if not message then
164+ return
165+ end
166+
167+ ctx .render_state :clear_orphan_parts (message_id )
168+ for _ , part in ipairs (message .parts or {}) do
169+ if part .id then
170+ flush .queue_part_removal (part .id )
171+ end
172+ end
173+ flush .queue_message_removal (message_id )
174+ end
175+
176+ local function reconcile_rendered_message_limit ()
177+ if not state .active_session or not state .messages then
178+ return
179+ end
180+
181+ local limit = get_max_rendered_messages ()
182+ if not limit then
183+ if ctx .render_state :get_message (HIDDEN_MESSAGES_NOTICE_MESSAGE_ID ) then
184+ hide_rendered_message (HIDDEN_MESSAGES_NOTICE_MESSAGE_ID )
185+ end
186+ return
187+ end
188+
189+ local visible_messages , hidden_count = get_visible_session_messages (state .messages )
190+ local visible_ids = {}
191+ for _ , message in ipairs (visible_messages ) do
192+ local message_id = message .info and message .info .id
193+ if message_id then
194+ visible_ids [message_id ] = true
195+ ensure_message_rendered (message )
196+ end
197+ end
198+
199+ for _ , message in ipairs (get_real_session_messages (state .messages )) do
200+ local message_id = message .info and message .info .id
201+ if message_id and not visible_ids [message_id ] and ctx .render_state :get_message (message_id ) then
202+ hide_rendered_message (message_id )
203+ end
204+ end
205+
206+ if hidden_count > 0 then
207+ upsert_hidden_messages_notice (hidden_count )
208+ elseif ctx .render_state :get_message (HIDDEN_MESSAGES_NOTICE_MESSAGE_ID ) then
209+ hide_rendered_message (HIDDEN_MESSAGES_NOTICE_MESSAGE_ID )
210+ end
211+ end
212+
213+ --- @param message_id string | nil
214+ --- @return boolean
215+ local function is_message_visible (message_id )
216+ if not message_id then
217+ return false
218+ end
219+
220+ for _ , message in ipairs (select (1 , get_visible_session_messages (state .messages ))) do
221+ if message .info and message .info .id == message_id then
222+ return true
223+ end
224+ end
225+
226+ return false
227+ end
12228
13229-- Expose event handlers on M so tests can call them directly and subscriptions
14230-- can be stubbed cleanly (e.g. stub(renderer, '_render_full_session_data'))
@@ -100,22 +316,35 @@ end
100316function M ._render_full_session_data (session_data , opts )
101317 opts = opts or {}
102318 M .reset ()
319+ state .renderer .set_messages (vim .deepcopy (session_data or {}))
103320
104321 if not state .active_session or not state .messages then
105322 return
106323 end
107324
108- local revert_index = nil
325+ local visible_messages , hidden_count = get_visible_session_messages (state .messages )
326+ local revert_index = get_revert_index (state .messages )
109327
110328 flush .begin_bulk_mode ()
111329
112- for i , msg in ipairs (session_data ) do
113- if state .active_session .revert and state .active_session .revert .messageID == msg .info .id then
114- revert_index = i
115- end
116- events .on_message_updated ({ info = msg .info }, revert_index )
330+ if hidden_count > 0 then
331+ local hidden_notice = build_hidden_messages_notice (hidden_count )
332+ events .on_message_updated (hidden_notice )
333+ events .on_part_updated ({ part = hidden_notice .parts [1 ] })
334+ end
335+
336+ for _ , msg in ipairs (visible_messages ) do
337+ events .on_message_updated ({ info = msg .info })
117338 for _ , part in ipairs (msg .parts or {}) do
118- events .on_part_updated ({ part = part }, revert_index )
339+ events .on_part_updated ({ part = part })
340+ end
341+ end
342+
343+ for _ , msg in ipairs (state .messages ) do
344+ if msg .info and msg .info .sessionID ~= state .active_session .id then
345+ for _ , part in ipairs (msg .parts or {}) do
346+ events .on_part_updated ({ part = part })
347+ end
119348 end
120349 end
121350
@@ -234,6 +463,9 @@ function M.on_session_changed(_, new, old)
234463 end
235464end
236465
466+ M .reconcile_rendered_message_limit = reconcile_rendered_message_limit
467+ M .is_message_visible = is_message_visible
468+
237469--- Scroll to bottom after all queued events have been processed
238470function M .on_emit_events_finished ()
239471 M .scroll_to_bottom ()
0 commit comments