@@ -23,6 +23,16 @@ local util = require('opencode.util')
2323--- @field type " message.part.updated"
2424--- @field properties { part : OpencodeMessagePart }
2525
26+ --- @class EventMessagePartDelta
27+ --- @field type " message.part.delta"
28+ --- @field properties {
29+ --- sessionID: string,
30+ --- messageID: string,
31+ --- partID: string,
32+ --- field: string,
33+ --- delta: string
34+ --- }
35+
2636--- @class EventMessagePartRemoved
2737--- @field type " message.part.removed"
2838--- @field properties { sessionID : string , messageID : string , partID : string }
@@ -128,6 +138,7 @@ local util = require('opencode.util')
128138--- | "message.updated"
129139--- | "message.removed"
130140--- | "message.part.updated"
141+ --- | "message.part.delta"
131142--- | "message.part.removed"
132143--- | "session.compacted"
133144--- | "session.idle"
@@ -170,6 +181,7 @@ function EventManager.new()
170181 state_server_listener = nil ,
171182 is_started = false ,
172183 captured_events = {},
184+ _parts_by_id = {},
173185 }, EventManager )
174186
175187 local throttle_ms = config .ui .output .rendering .event_throttle_ms
186198--- @overload fun ( self : EventManager , event_name : " message.updated" , callback : fun ( data : EventMessageUpdated[ ' properties' ] ): nil )
187199--- @overload fun ( self : EventManager , event_name : " message.removed" , callback : fun ( data : EventMessageRemoved[ ' properties' ] ): nil )
188200--- @overload fun ( self : EventManager , event_name : " message.part.updated" , callback : fun ( data : EventMessagePartUpdated[ ' properties' ] ): nil )
201+ --- @overload fun ( self : EventManager , event_name : " message.part.delta" , callback : fun ( data : EventMessagePartDelta[ ' properties' ] ): nil )
189202--- @overload fun ( self : EventManager , event_name : " message.part.removed" , callback : fun ( data : EventMessagePartRemoved[ ' properties' ] ): nil )
190203--- @overload fun ( self : EventManager , event_name : " session.compacted" , callback : fun ( data : EventSessionCompacted[ ' properties' ] ): nil )
191204--- @overload fun ( self : EventManager , event_name : " session.idle" , callback : fun ( data : EventSessionIdle[ ' properties' ] ): nil )
226239--- @overload fun ( self : EventManager , event_name : " message.updated" , callback : fun ( data : EventMessageUpdated[ ' properties' ] ): nil )
227240--- @overload fun ( self : EventManager , event_name : " message.removed" , callback : fun ( data : EventMessageRemoved[ ' properties' ] ): nil )
228241--- @overload fun ( self : EventManager , event_name : " message.part.updated" , callback : fun ( data : EventMessagePartUpdated[ ' properties' ] ): nil )
242+ --- @overload fun ( self : EventManager , event_name : " message.part.delta" , callback : fun ( data : EventMessagePartDelta[ ' properties' ] ): nil )
229243--- @overload fun ( self : EventManager , event_name : " message.part.removed" , callback : fun ( data : EventMessagePartRemoved[ ' properties' ] ): nil )
230244--- @overload fun ( self : EventManager , event_name : " session.compacted" , callback : fun ( data : EventSessionCompacted[ ' properties' ] ): nil )
231245--- @overload fun ( self : EventManager , event_name : " session.idle" , callback : fun ( data : EventSessionIdle[ ' properties' ] ): nil )
@@ -260,15 +274,93 @@ function EventManager:unsubscribe(event_name, callback)
260274 end
261275end
262276
277+ --- Normalize message.part.delta events into message.part.updated events so
278+ --- consumers can continue rendering full part payloads.
279+ --- @param event table
280+ --- @return table | nil
281+ function EventManager :_normalize_stream_event (event )
282+ if not event or not event .type then
283+ return nil
284+ end
285+
286+ local properties = event .properties or {}
287+
288+ if event .type == ' message.part.updated' and properties .part and properties .part .id then
289+ self ._parts_by_id [properties .part .id ] = vim .deepcopy (properties .part )
290+ return event
291+ end
292+
293+ if event .type == ' message.part.removed' and properties .partID then
294+ self ._parts_by_id [properties .partID ] = nil
295+ return event
296+ end
297+
298+ if event .type ~= ' message.part.delta' then
299+ return event
300+ end
301+
302+ local part_id = properties .partID
303+ local message_id = properties .messageID
304+ local session_id = properties .sessionID
305+ local field = properties .field
306+
307+ if not part_id or not message_id or not session_id or not field then
308+ return nil
309+ end
310+
311+ local part = vim .deepcopy (self ._parts_by_id [part_id ])
312+ if not part then
313+ part = {
314+ id = part_id ,
315+ messageID = message_id ,
316+ sessionID = session_id ,
317+ }
318+
319+ if field == ' text' then
320+ part .type = ' text'
321+ part .text = ' '
322+ end
323+ end
324+
325+ local delta = properties .delta
326+ local current = part [field ]
327+ if type (delta ) == ' string' then
328+ if type (current ) == ' string' then
329+ part [field ] = current .. delta
330+ else
331+ part [field ] = delta
332+ end
333+ else
334+ part [field ] = delta
335+ end
336+
337+ self ._parts_by_id [part_id ] = part
338+
339+ return {
340+ type = ' message.part.updated' ,
341+ properties = {
342+ part = part ,
343+ },
344+ }
345+ end
346+
263347--- Callback from ThrottlingEmitter when the events are now ready to be processed.
264348--- Collapses parts that are duplicated, making sure to replace earlier parts with later
265349--- ones (but keeping the earlier position)
266350--- @param events any
267351function EventManager :_on_drained_events (events )
268352 self :emit (' custom.emit_events.started' , {})
269353
354+ local normalized_events = {}
355+ for _ , event in ipairs (events ) do
356+ local normalized_event = self :_normalize_stream_event (event )
357+ if normalized_event then
358+ table.insert (normalized_events , normalized_event )
359+ end
360+ end
361+
270362 if not config .ui .output .rendering .event_collapsing then
271- for _ , event in ipairs (events ) do
363+ for _ , event in ipairs (normalized_events ) do
272364 self :emit (event .type , event .properties )
273365 end
274366 self :emit (' custom.emit_events.finished' , {})
@@ -278,7 +370,7 @@ function EventManager:_on_drained_events(events)
278370 local collapsed_events = {}
279371 local part_update_indices = {}
280372
281- for i , event in ipairs (events ) do
373+ for i , event in ipairs (normalized_events ) do
282374 if event .type == ' message.part.updated' and event .properties .part then
283375 local part_id = event .properties .part .id
284376 if part_update_indices [part_id ] then
@@ -289,7 +381,10 @@ function EventManager:_on_drained_events(events)
289381 -- permission.updated/permission.asked sits between the two updates.
290382 local has_intervening_permission_event = false
291383 for j = previous_index + 1 , i - 1 do
292- if events [j ] and (events [j ].type == ' permission.updated' or events [j ].type == ' permission.asked' ) then
384+ if normalized_events [j ] and (
385+ normalized_events [j ].type == ' permission.updated'
386+ or normalized_events [j ].type == ' permission.asked'
387+ ) then
293388 has_intervening_permission_event = true
294389 break
295390 end
@@ -312,7 +407,7 @@ function EventManager:_on_drained_events(events)
312407 end
313408 end
314409
315- for i = 1 , # events do
410+ for i = 1 , # normalized_events do
316411 local event = collapsed_events [i ]
317412 if event then
318413 self :emit (event .type , event .properties )
@@ -404,6 +499,7 @@ function EventManager:stop()
404499 self :_cleanup_server_subscription ()
405500
406501 self .throttling_emitter :clear ()
502+ self ._parts_by_id = {}
407503 self .events = {}
408504end
409505
0 commit comments