1111--- @field actions table[] Actions associated with this part
1212--- @field has_extmarks boolean ? Whether the part currently has extmarks applied
1313
14+ --- @class RenderedBlock
15+ --- @field block RenderBlock Direct reference to a cached /rendered block
16+ --- @field key string Stable render block key
17+ --- @field kind string Block kind (message /part /overlay )
18+ --- @field message_id string ? Parent message ID when applicable
19+ --- @field part_id string ? Parent part ID when applicable
20+ --- @field line_start integer ? Line where the block starts when mounted
21+ --- @field line_end integer ? Line where the block ends when mounted
22+ --- @field line_count integer ? Cached block line count
23+
1424--- @class RenderState
1525--- @field _messages table<string , RenderedMessage> Message ID -> rendered message
1626--- @field _parts table<string , RenderedPart> Part ID -> rendered part
27+ --- @field _blocks table<string , RenderedBlock> Block key -> rendered block
28+ --- @field _message_blocks table<string , string> Message ID -> primary block key
29+ --- @field _part_blocks table<string , string> Part ID -> primary block key
1730--- @field _part_ranges { [1] : integer , [2] : integer , [3] : string } [] Sorted [line_start , line_end , part_id] for binary search
1831--- @field _message_ranges { [1] : integer , [2] : integer , [3] : string } [] Sorted [line_start , line_end , message_id] for binary search
1932--- @field _ranges_valid boolean Whether range arrays are sorted and up-to-date
3245function RenderState :reset ()
3346 self ._messages = {}
3447 self ._parts = {}
48+ self ._blocks = {}
49+ self ._message_blocks = {}
50+ self ._part_blocks = {}
3551 self ._part_ranges = {}
3652 self ._message_ranges = {}
3753 self ._ranges_valid = false
@@ -44,6 +60,92 @@ function RenderState:reset()
4460 self ._snapshot_id_index = {} -- snapshot_id -> OpencodeMessagePart
4561end
4662
63+ --- @param mapping table<string , string>
64+ --- @param id string ?
65+ --- @param key string ?
66+ local function clear_block_reference (mapping , id , key )
67+ if not id or not key then
68+ return
69+ end
70+
71+ if mapping [id ] == key then
72+ mapping [id ] = nil
73+ end
74+ end
75+
76+ --- @param block RenderBlock ?
77+ --- @return string ?
78+ local function get_block_key (block )
79+ if not block or not block .key or block .key == ' ' then
80+ return nil
81+ end
82+ return block .key
83+ end
84+
85+ --- @param message string | OpencodeMessage | nil
86+ --- @return string ?
87+ local function resolve_message_id (message )
88+ if type (message ) == ' string' then
89+ return message ~= ' ' and message or nil
90+ end
91+
92+ return message and message .info and message .info .id or nil
93+ end
94+
95+ --- @param part string | OpencodeMessagePart | nil
96+ --- @return string ?
97+ local function resolve_part_id (part )
98+ if type (part ) == ' string' then
99+ return part ~= ' ' and part or nil
100+ end
101+
102+ return part and part .id or nil
103+ end
104+
105+ --- @param block_key string
106+ --- @param line_start integer ?
107+ --- @param line_end integer ?
108+ --- @param message_id string ?
109+ --- @param part_id string ?
110+ --- @return RenderedBlock ?
111+ function RenderState :_upsert_block_entry (block_key , line_start , line_end , message_id , part_id )
112+ local entry = self ._blocks [block_key ]
113+ if not entry then
114+ return nil
115+ end
116+
117+ local previous_message_id = entry .message_id
118+ local previous_part_id = entry .part_id
119+
120+ entry .message_id = message_id or entry .message_id
121+ entry .part_id = part_id or entry .part_id
122+ entry .line_count = entry .block and entry .block .line_count or entry .line_count
123+ entry .kind = entry .block and entry .block .kind or entry .kind
124+
125+ if line_start ~= nil then
126+ entry .line_start = line_start
127+ end
128+ if line_end ~= nil then
129+ entry .line_end = line_end
130+ end
131+
132+ if previous_message_id ~= entry .message_id then
133+ clear_block_reference (self ._message_blocks , previous_message_id , block_key )
134+ end
135+ if previous_part_id ~= entry .part_id then
136+ clear_block_reference (self ._part_blocks , previous_part_id , block_key )
137+ end
138+
139+ if entry .kind == ' message' and entry .message_id then
140+ self ._message_blocks [entry .message_id ] = block_key
141+ end
142+ if entry .kind == ' part' and entry .part_id then
143+ self ._part_blocks [entry .part_id ] = block_key
144+ end
145+
146+ return entry
147+ end
148+
47149function RenderState :_recompute_max_line_end ()
48150 local max_line_end = 0
49151
@@ -59,6 +161,12 @@ function RenderState:_recompute_max_line_end()
59161 end
60162 end
61163
164+ for _ , block_data in pairs (self ._blocks ) do
165+ if block_data .line_end and block_data .line_end > max_line_end then
166+ max_line_end = block_data .line_end
167+ end
168+ end
169+
62170 self ._max_line_end = max_line_end
63171 self ._max_line_end_valid = true
64172 return max_line_end
@@ -205,6 +313,44 @@ function RenderState:get_message(message_id)
205313 return self ._messages [message_id ]
206314end
207315
316+ --- @param block_key string
317+ --- @return RenderedBlock ?
318+ function RenderState :get_block (block_key )
319+ return self ._blocks [block_key ]
320+ end
321+
322+ --- @param message string | OpencodeMessage
323+ --- @return RenderedBlock ?
324+ function RenderState :get_message_block (message )
325+ local message_id = resolve_message_id (message )
326+ if not message_id then
327+ return nil
328+ end
329+
330+ local block_key = self ._message_blocks [message_id ]
331+ if not block_key then
332+ block_key = ' message:' .. message_id
333+ end
334+
335+ return self ._blocks [block_key ]
336+ end
337+
338+ --- @param part string | OpencodeMessagePart
339+ --- @return RenderedBlock ?
340+ function RenderState :get_part_block (part )
341+ local part_id = resolve_part_id (part )
342+ if not part_id then
343+ return nil
344+ end
345+
346+ local block_key = self ._part_blocks [part_id ]
347+ if not block_key then
348+ block_key = ' part:' .. part_id
349+ end
350+
351+ return self ._blocks [block_key ]
352+ end
353+
208354--- @param line integer 1-indexed
209355--- @return RenderedMessage ?
210356function RenderState :get_message_at_line (line )
@@ -347,6 +493,58 @@ function RenderState:set_message(message, line_start, line_end)
347493 self ._max_line_end = line_end
348494 end
349495 end
496+
497+ local block_key = self ._message_blocks [message_id ] or (' message:' .. message_id )
498+ if self ._blocks [block_key ] then
499+ self :_upsert_block_entry (block_key , line_start , line_end , message_id , nil )
500+ end
501+ end
502+
503+ --- @param block RenderBlock
504+ --- @param line_start integer ?
505+ --- @param line_end integer ?
506+ --- @return RenderedBlock ?
507+ function RenderState :set_block (block , line_start , line_end )
508+ local block_key = get_block_key (block )
509+ if not block_key then
510+ return nil
511+ end
512+
513+ local existing = self ._blocks [block_key ]
514+ if not existing then
515+ self ._blocks [block_key ] = {
516+ block = block ,
517+ key = block_key ,
518+ kind = block .kind ,
519+ message_id = block .message_id ,
520+ part_id = block .part_id ,
521+ line_start = line_start ,
522+ line_end = line_end ,
523+ line_count = block .line_count ,
524+ }
525+ else
526+ existing .block = block
527+ existing .key = block_key
528+ existing .kind = block .kind
529+ existing .line_count = block .line_count
530+ end
531+
532+ return self :_upsert_block_entry (block_key , line_start , line_end , block .message_id , block .part_id )
533+ end
534+
535+ --- @param message string | OpencodeMessage
536+ --- @param block RenderBlock
537+ --- @param line_start integer ?
538+ --- @param line_end integer ?
539+ --- @return RenderedBlock ?
540+ function RenderState :set_message_block (message , block , line_start , line_end )
541+ local message_id = resolve_message_id (message )
542+ local entry = self :set_block (block , line_start , line_end )
543+ if not entry then
544+ return nil
545+ end
546+
547+ return self :_upsert_block_entry (entry .key , line_start , line_end , message_id , entry .part_id )
350548end
351549
352550--- @param part OpencodeMessagePart
@@ -394,6 +592,26 @@ function RenderState:set_part(part, line_start, line_end)
394592 end
395593
396594 self :_index_task_part_child_session (part_id , part )
595+
596+ local block_key = self ._part_blocks [part_id ] or (' part:' .. part_id )
597+ if self ._blocks [block_key ] then
598+ self :_upsert_block_entry (block_key , line_start , line_end , message_id , part_id )
599+ end
600+ end
601+
602+ --- @param part string | OpencodeMessagePart
603+ --- @param block RenderBlock
604+ --- @param line_start integer ?
605+ --- @param line_end integer ?
606+ --- @return RenderedBlock ?
607+ function RenderState :set_part_block (part , block , line_start , line_end )
608+ local part_id = resolve_part_id (part )
609+ local entry = self :set_block (block , line_start , line_end )
610+ if not entry then
611+ return nil
612+ end
613+
614+ return self :_upsert_block_entry (entry .key , line_start , line_end , entry .message_id , part_id )
397615end
398616
399617--- @param part_id string
@@ -419,6 +637,11 @@ function RenderState:update_part_lines(part_id, new_line_start, new_line_end)
419637 part_data .line_end = new_line_end
420638 self ._ranges_valid = false
421639
640+ local block_key = self ._part_blocks [part_id ] or (' part:' .. part_id )
641+ if self ._blocks [block_key ] then
642+ self :_upsert_block_entry (block_key , new_line_start , new_line_end , part_data .message_id , part_id )
643+ end
644+
422645 if self ._max_line_end_valid then
423646 if old_line_end == self ._max_line_end and new_line_end < old_line_end then
424647 self ._max_line_end_valid = false
@@ -462,6 +685,8 @@ function RenderState:remove_part(part_id)
462685 return false
463686 end
464687
688+ self :remove_block (self ._part_blocks [part_id ] or (' part:' .. part_id ))
689+
465690 if part_data .part and part_data .part .type == ' patch' and part_data .part .hash then
466691 self ._snapshot_id_index [part_data .part .hash ] = nil
467692 end
@@ -486,6 +711,20 @@ function RenderState:remove_part(part_id)
486711 return true
487712end
488713
714+ --- @param block_key string
715+ --- @return boolean
716+ function RenderState :remove_block (block_key )
717+ local entry = self ._blocks [block_key ]
718+ if not entry then
719+ return false
720+ end
721+
722+ clear_block_reference (self ._message_blocks , entry .message_id , block_key )
723+ clear_block_reference (self ._part_blocks , entry .part_id , block_key )
724+ self ._blocks [block_key ] = nil
725+ return true
726+ end
727+
489728--- @param message_id string
490729--- @return boolean
491730function RenderState :remove_message (message_id )
@@ -494,6 +733,8 @@ function RenderState:remove_message(message_id)
494733 return false
495734 end
496735
736+ self :remove_block (self ._message_blocks [message_id ] or (' message:' .. message_id ))
737+
497738 local line_count = msg_data .line_end - msg_data .line_start + 1
498739 local shift_from = msg_data .line_end + 1
499740
@@ -577,6 +818,14 @@ function RenderState:shift_all(from_line, delta)
577818 end
578819 end
579820
821+ for _ , block_data in pairs (self ._blocks ) do
822+ if block_data .line_start and block_data .line_end and block_data .line_start >= from_line then
823+ block_data .line_start = block_data .line_start + delta
824+ block_data .line_end = block_data .line_end + delta
825+ shifted = true
826+ end
827+ end
828+
580829 if shifted then
581830 self ._ranges_valid = false
582831 if self ._max_line_end_valid then
0 commit comments