Skip to content

Commit 83f6168

Browse files
cameronrsudo-tee
authored andcommitted
fix(renderer): handle inserting out of order parts
1 parent 8d7b550 commit 83f6168

File tree

6 files changed

+363
-15
lines changed

6 files changed

+363
-15
lines changed

lua/opencode/core.lua

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,8 +165,6 @@ function M.before_run(opts)
165165
local is_new_session = opts and opts.new_session or not state.active_session
166166
opts = opts or {}
167167

168-
M.cancel()
169-
-- ui.clear_output()
170168

171169
M.open({
172170
new_session = is_new_session,

lua/opencode/ui/renderer.lua

Lines changed: 96 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -218,14 +218,16 @@ end
218218
---Write data to output_buf, including normal text and extmarks
219219
---@param formatted_data Output Formatted data as Output object
220220
---@param part_id? string Optional part ID to store actions
221+
---@param start_line? integer Optional line to insert at (shifts content down). If nil, appends to end of buffer.
221222
---@return {line_start: integer, line_end: integer}? Range where data was written
222-
function M._write_formatted_data(formatted_data, part_id)
223+
function M._write_formatted_data(formatted_data, part_id, start_line)
223224
if not state.windows or not state.windows.output_buf then
224225
return
225226
end
226227

227228
local buf = state.windows.output_buf
228-
local start_line = output_window.get_buf_line_count()
229+
local is_insertion = start_line ~= nil
230+
local target_line = start_line or output_window.get_buf_line_count()
229231
local new_lines = formatted_data.lines
230232
local extmarks = formatted_data.extmarks
231233

@@ -234,19 +236,23 @@ function M._write_formatted_data(formatted_data, part_id)
234236
end
235237

236238
if part_id and formatted_data.actions then
237-
M._render_state:add_actions(part_id, formatted_data.actions, start_line)
239+
M._render_state:add_actions(part_id, formatted_data.actions, target_line)
238240
end
239241

240-
output_window.set_lines(new_lines, start_line)
241-
output_window.set_extmarks(extmarks, start_line)
242+
if is_insertion then
243+
output_window.set_lines(new_lines, target_line, target_line)
244+
else
245+
output_window.set_lines(new_lines, target_line)
246+
end
247+
output_window.set_extmarks(extmarks, target_line)
242248

243249
return {
244-
line_start = start_line,
245-
line_end = start_line + #new_lines - 1,
250+
line_start = target_line,
251+
line_end = target_line + #new_lines - 1,
246252
}
247253
end
248254

249-
---Insert new part at end of buffer
255+
---Insert new part, either at end of buffer or in the middle for out-of-order parts
250256
---@param part_id string Part ID
251257
---@param formatted_data Output Formatted data as Output object
252258
---@return boolean Success status
@@ -260,14 +266,42 @@ function M._insert_part_to_buffer(part_id, formatted_data)
260266
return true
261267
end
262268

263-
local range = M._write_formatted_data(formatted_data, part_id)
269+
local is_current_message = state.current_message
270+
and state.current_message.info
271+
and state.current_message.info.id == cached.message_id
272+
273+
if is_current_message then
274+
-- NOTE: we're inserting a part for the current message, just add it to the end
275+
276+
local range = M._write_formatted_data(formatted_data, part_id)
277+
if not range then
278+
return false
279+
end
280+
281+
M._render_state:set_part(cached.part, range.line_start, range.line_end)
282+
283+
M._last_part_formatted = { part_id = part_id, formatted_data = formatted_data }
284+
285+
return true
286+
end
287+
288+
-- NOTE: We're inserting a part for the first time for a previous message. We need to find
289+
-- the insertion line (after the last part of this message or after the message header if
290+
-- no parts).
291+
local insertion_line = M._get_insertion_point_for_part(part_id, cached.message_id)
292+
if not insertion_line then
293+
return false
294+
end
295+
296+
local range = M._write_formatted_data(formatted_data, part_id, insertion_line)
264297
if not range then
265298
return false
266299
end
267300

268-
M._render_state:set_part(cached.part, range.line_start, range.line_end)
301+
local line_count = #formatted_data.lines
302+
M._render_state:shift_all(insertion_line, line_count)
269303

270-
M._last_part_formatted = { part_id = part_id, formatted_data = formatted_data }
304+
M._render_state:set_part(cached.part, range.line_start, range.line_end)
271305

272306
return true
273307
end
@@ -280,13 +314,11 @@ end
280314
function M._replace_part_in_buffer(part_id, formatted_data)
281315
local cached = M._render_state:get_part(part_id)
282316
if not cached or not cached.line_start or not cached.line_end then
283-
-- return M._insert_part_to_buffer(part_id, formatted_data)
284317
return false
285318
end
286319

287320
local new_lines = formatted_data.lines
288321
local new_line_count = #new_lines
289-
-- local old_line_count = cached.line_end - cached.line_start + 1
290322

291323
local old_formatted = M._last_part_formatted
292324
local can_optimize = old_formatted
@@ -298,10 +330,14 @@ function M._replace_part_in_buffer(part_id, formatted_data)
298330
local write_start_line = cached.line_start
299331

300332
if can_optimize then
333+
-- NOTE: This is an optimization to only replace the lines that are different
334+
-- if we're replacing the most recently formatted part.
335+
301336
---@cast old_formatted { formatted_data: { lines: string[] } }
302337
local old_lines = old_formatted.formatted_data.lines
303338
local first_diff_line = nil
304339

340+
-- Find the first line that's different
305341
for i = 1, math.min(#old_lines, new_line_count) do
306342
if old_lines[i] ~= new_lines[i] then
307343
first_diff_line = i
@@ -310,13 +346,15 @@ function M._replace_part_in_buffer(part_id, formatted_data)
310346
end
311347

312348
if not first_diff_line and new_line_count > #old_lines then
349+
-- The old lines all matched but maybe there are more new lines
313350
first_diff_line = #old_lines + 1
314351
end
315352

316353
if first_diff_line then
317354
lines_to_write = vim.list_slice(new_lines, first_diff_line, new_line_count)
318355
write_start_line = cached.line_start + first_diff_line - 1
319356
elseif new_line_count == #old_lines then
357+
-- Nothing was different, so we're done
320358
M._last_part_formatted = { part_id = part_id, formatted_data = formatted_data }
321359
return true
322360
end
@@ -790,6 +828,51 @@ function M._get_last_part_for_message(message)
790828
return nil
791829
end
792830

831+
---Get insertion point for an out-of-order part
832+
---@param part_id string The part ID to insert
833+
---@param message_id string The message ID the part belongs to
834+
---@return integer? insertion_line The line to insert at (1-indexed), or nil on error
835+
function M._get_insertion_point_for_part(part_id, message_id)
836+
local rendered_message = M._render_state:get_message(message_id)
837+
if not rendered_message or not rendered_message.message then
838+
return nil
839+
end
840+
841+
local message = rendered_message.message
842+
843+
local insertion_line = rendered_message.line_end and (rendered_message.line_end + 1)
844+
if not insertion_line then
845+
return nil
846+
end
847+
848+
local current_part_index = nil
849+
if message.parts then
850+
for i, part in ipairs(message.parts) do
851+
if part.id == part_id then
852+
current_part_index = i
853+
break
854+
end
855+
end
856+
end
857+
858+
if not current_part_index then
859+
return insertion_line
860+
end
861+
862+
for i = current_part_index - 1, 1, -1 do
863+
local prev_part = message.parts[i]
864+
if prev_part and prev_part.id then
865+
local prev_rendered = M._render_state:get_part(prev_part.id)
866+
867+
if prev_rendered and prev_rendered.line_end then
868+
return prev_rendered.line_end + 1
869+
end
870+
end
871+
end
872+
873+
return insertion_line
874+
end
875+
793876
---Re-render existing part with current state
794877
---Used for permission updates and other dynamic changes
795878
---@param part_id string Part ID to re-render
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"lines":["","----","","","Message 1","","----","","","Response 2 - Part 1","","Response 2 - Part 2","","Response 2 - Part 3","","Response 2 - Part 4 (late arrival)","","----","","","Message 3","","----","","","Response 4 - Part 1","","Response 4 - Part 2","","Response 4 - Part 3","","Response 4 - Part 4 (late arrival)","","----","","","Message 5","","----","","","Response 6 - Part 1","","Response 6 - Part 2","","Response 6 - Part 3",""],"actions":[],"extmarks":[[1,2,0,{"virt_text_hide":false,"virt_text_repeat_linebreak":false,"ns_id":3,"virt_text_win_col":-3,"right_gravity":true,"priority":10,"virt_text":[["▌󰭻 ","OpencodeMessageRoleUser"],[" "],["USER","OpencodeMessageRoleUser"],["","OpencodeHint"],[" (2001-09-09 01:46:41)","OpencodeHint"],[" [msg_001]","OpencodeHint"]],"virt_text_pos":"win_col"}],[2,3,0,{"virt_text_hide":false,"virt_text_repeat_linebreak":true,"ns_id":3,"virt_text_win_col":-3,"right_gravity":true,"priority":4096,"virt_text":[["▌","OpencodeMessageRoleUser"]],"virt_text_pos":"win_col"}],[3,4,0,{"virt_text_hide":false,"virt_text_repeat_linebreak":true,"ns_id":3,"virt_text_win_col":-3,"right_gravity":true,"priority":4096,"virt_text":[["▌","OpencodeMessageRoleUser"]],"virt_text_pos":"win_col"}],[4,7,0,{"virt_text_hide":false,"virt_text_repeat_linebreak":false,"ns_id":3,"virt_text_win_col":-3,"right_gravity":true,"priority":10,"virt_text":[[" ","OpencodeMessageRoleAssistant"],[" "],["BUILD","OpencodeMessageRoleAssistant"],["","OpencodeHint"],[" (2001-09-09 01:46:42)","OpencodeHint"],[" [msg_002]","OpencodeHint"]],"virt_text_pos":"win_col"}],[5,18,0,{"virt_text_hide":false,"virt_text_repeat_linebreak":false,"ns_id":3,"virt_text_win_col":-3,"right_gravity":true,"priority":10,"virt_text":[["▌󰭻 ","OpencodeMessageRoleUser"],[" "],["USER","OpencodeMessageRoleUser"],["","OpencodeHint"],[" (2001-09-09 01:46:43)","OpencodeHint"],[" [msg_003]","OpencodeHint"]],"virt_text_pos":"win_col"}],[6,19,0,{"virt_text_hide":false,"virt_text_repeat_linebreak":true,"ns_id":3,"virt_text_win_col":-3,"right_gravity":true,"priority":4096,"virt_text":[["▌","OpencodeMessageRoleUser"]],"virt_text_pos":"win_col"}],[7,20,0,{"virt_text_hide":false,"virt_text_repeat_linebreak":true,"ns_id":3,"virt_text_win_col":-3,"right_gravity":true,"priority":4096,"virt_text":[["▌","OpencodeMessageRoleUser"]],"virt_text_pos":"win_col"}],[8,23,0,{"virt_text_hide":false,"virt_text_repeat_linebreak":false,"ns_id":3,"virt_text_win_col":-3,"right_gravity":true,"priority":10,"virt_text":[[" ","OpencodeMessageRoleAssistant"],[" "],["BUILD","OpencodeMessageRoleAssistant"],["","OpencodeHint"],[" (2001-09-09 01:46:44)","OpencodeHint"],[" [msg_004]","OpencodeHint"]],"virt_text_pos":"win_col"}],[9,34,0,{"virt_text_hide":false,"virt_text_repeat_linebreak":false,"ns_id":3,"virt_text_win_col":-3,"right_gravity":true,"priority":10,"virt_text":[["▌󰭻 ","OpencodeMessageRoleUser"],[" "],["USER","OpencodeMessageRoleUser"],["","OpencodeHint"],[" (2001-09-09 01:46:45)","OpencodeHint"],[" [msg_005]","OpencodeHint"]],"virt_text_pos":"win_col"}],[10,35,0,{"virt_text_hide":false,"virt_text_repeat_linebreak":true,"ns_id":3,"virt_text_win_col":-3,"right_gravity":true,"priority":4096,"virt_text":[["▌","OpencodeMessageRoleUser"]],"virt_text_pos":"win_col"}],[11,36,0,{"virt_text_hide":false,"virt_text_repeat_linebreak":true,"ns_id":3,"virt_text_win_col":-3,"right_gravity":true,"priority":4096,"virt_text":[["▌","OpencodeMessageRoleUser"]],"virt_text_pos":"win_col"}],[12,39,0,{"virt_text_hide":false,"virt_text_repeat_linebreak":false,"ns_id":3,"virt_text_win_col":-3,"right_gravity":true,"priority":10,"virt_text":[[" ","OpencodeMessageRoleAssistant"],[" "],["BUILD","OpencodeMessageRoleAssistant"],["","OpencodeHint"],[" (2001-09-09 01:46:46)","OpencodeHint"],[" [msg_006]","OpencodeHint"]],"virt_text_pos":"win_col"}]],"timestamp":1761967800}

0 commit comments

Comments
 (0)