From d37ac8c2bc9660883ea1fafee7bc31c1b7e71375 Mon Sep 17 00:00:00 2001 From: disrupted Date: Thu, 5 Feb 2026 23:43:15 +0100 Subject: [PATCH 1/5] fix(renderer): handle session title updates --- lua/opencode/ui/renderer.lua | 17 ++++++++++-- tests/replay/renderer_spec.lua | 47 ++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/lua/opencode/ui/renderer.lua b/lua/opencode/ui/renderer.lua index 6e16e010..a8f681b0 100644 --- a/lua/opencode/ui/renderer.lua +++ b/lua/opencode/ui/renderer.lua @@ -839,8 +839,21 @@ function M.on_session_updated(properties) if not properties or not properties.info or not state.active_session then return end - if not vim.deep_equal(state.active_session.revert, properties.info.revert) then - state.active_session.revert = properties.info.revert + + local updated_session = properties.info + if not updated_session.id or updated_session.id ~= state.active_session.id then + return + end + + local current_session = state.active_session + local revert_changed = not vim.deep_equal(current_session.revert, updated_session.revert) + + local merged_session = vim.tbl_deep_extend('force', vim.deepcopy(current_session), updated_session) + if not vim.deep_equal(current_session, merged_session) then + state.active_session = merged_session + end + + if revert_changed then M._render_full_session_data(state.messages) end end diff --git a/tests/replay/renderer_spec.lua b/tests/replay/renderer_spec.lua index 5764b1cd..20e0c2bd 100644 --- a/tests/replay/renderer_spec.lua +++ b/tests/replay/renderer_spec.lua @@ -3,6 +3,7 @@ local ui = require('opencode.ui.ui') local helpers = require('tests.helpers') local output_window = require('opencode.ui.output_window') local assert = require('luassert') +local stub = require('luassert.stub') local config = require('opencode.config') local function assert_output_matches(expected, actual, name) @@ -144,6 +145,52 @@ describe('renderer unit tests', function() ) end end) + + it('updates active session title from session.updated event', function() + local renderer = require('opencode.ui.renderer') + + state.active_session = { + id = 'ses_123', + title = 'New session - 2026-02-05T22:26:08.579Z', + time = { created = 1, updated = 1 }, + } + + renderer.on_session_updated({ + info = { + id = 'ses_123', + title = 'Branch review request', + time = { created = 1, updated = 2 }, + }, + }) + + assert.are.equal('Branch review request', state.active_session.title) + end) + + it('rerenders full session when revert changes', function() + local renderer = require('opencode.ui.renderer') + + state.messages = {} + state.active_session = { + id = 'ses_123', + title = 'Session', + time = { created = 1, updated = 1 }, + revert = { messageID = 'msg_1', snapshot = 'a', diff = '' }, + } + + local render_stub = stub(renderer, '_render_full_session_data') + + renderer.on_session_updated({ + info = { + id = 'ses_123', + title = 'Session', + time = { created = 1, updated = 2 }, + revert = { messageID = 'msg_2', snapshot = 'b', diff = '' }, + }, + }) + + assert.stub(render_stub).was_called_with(state.messages) + render_stub:revert() + end) end) describe('renderer functional tests', function() From d9de04a40d1a4851af0aa2b452ef7b670cbfebf2 Mon Sep 17 00:00:00 2001 From: disrupted Date: Thu, 5 Feb 2026 23:58:03 +0100 Subject: [PATCH 2/5] refactor(renderer): prevent flickering for rapid events --- lua/opencode/ui/renderer.lua | 9 ++++++++- tests/replay/renderer_spec.lua | 30 ++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/lua/opencode/ui/renderer.lua b/lua/opencode/ui/renderer.lua index a8f681b0..8e82d384 100644 --- a/lua/opencode/ui/renderer.lua +++ b/lua/opencode/ui/renderer.lua @@ -847,10 +847,17 @@ function M.on_session_updated(properties) local current_session = state.active_session local revert_changed = not vim.deep_equal(current_session.revert, updated_session.revert) + local previous_title = current_session.title local merged_session = vim.tbl_deep_extend('force', vim.deepcopy(current_session), updated_session) if not vim.deep_equal(current_session, merged_session) then - state.active_session = merged_session + for key, value in pairs(merged_session) do + current_session[key] = value + end + + if updated_session.title and updated_session.title ~= previous_title then + require('opencode.ui.topbar').render() + end end if revert_changed then diff --git a/tests/replay/renderer_spec.lua b/tests/replay/renderer_spec.lua index 20e0c2bd..2e58ed91 100644 --- a/tests/replay/renderer_spec.lua +++ b/tests/replay/renderer_spec.lua @@ -148,6 +148,7 @@ describe('renderer unit tests', function() it('updates active session title from session.updated event', function() local renderer = require('opencode.ui.renderer') + local topbar = require('opencode.ui.topbar') state.active_session = { id = 'ses_123', @@ -155,6 +156,9 @@ describe('renderer unit tests', function() time = { created = 1, updated = 1 }, } + local active_session_ref = state.active_session + local topbar_render_stub = stub(topbar, 'render') + renderer.on_session_updated({ info = { id = 'ses_123', @@ -163,7 +167,10 @@ describe('renderer unit tests', function() }, }) + assert.is_true(state.active_session == active_session_ref) assert.are.equal('Branch review request', state.active_session.title) + assert.stub(topbar_render_stub).was_called() + topbar_render_stub:revert() end) it('rerenders full session when revert changes', function() @@ -191,6 +198,29 @@ describe('renderer unit tests', function() assert.stub(render_stub).was_called_with(state.messages) render_stub:revert() end) + + it('ignores session.updated for non-active session IDs', function() + local renderer = require('opencode.ui.renderer') + + state.active_session = { + id = 'ses_123', + title = 'Session', + time = { created = 1, updated = 1 }, + } + + local render_stub = stub(renderer, '_render_full_session_data') + + renderer.on_session_updated({ + info = { + id = 'ses_999', + title = 'Should not apply', + }, + }) + + assert.are.equal('Session', state.active_session.title) + assert.stub(render_stub).was_not_called() + render_stub:revert() + end) end) describe('renderer functional tests', function() From 464f4f7f4b20c05686e3a182775e0a13645829db Mon Sep 17 00:00:00 2001 From: disrupted Date: Fri, 6 Feb 2026 00:24:23 +0100 Subject: [PATCH 3/5] test: update replay snapshots --- tests/data/redo-all.expected.json | 2 +- tests/helpers.lua | 21 ++++++++++++++++----- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/tests/data/redo-all.expected.json b/tests/data/redo-all.expected.json index e8f3e3e5..d2f2fd75 100644 --- a/tests/data/redo-all.expected.json +++ b/tests/data/redo-all.expected.json @@ -1 +1 @@ -{"extmarks":[[1,1,0,{"virt_text_hide":false,"virt_text_win_col":-3,"ns_id":3,"virt_text_pos":"win_col","virt_text_repeat_linebreak":false,"priority":10,"right_gravity":true,"virt_text":[["▌󰭻 ","OpencodeMessageRoleUser"],[" "],["USER","OpencodeMessageRoleUser"],["","OpencodeHint"],[" (2025-10-20 15:20:02)","OpencodeHint"],[" [msg_a0234c0b7001y2o9S1jMaNVZar]","OpencodeHint"]]}],[2,2,0,{"virt_text_hide":false,"virt_text_win_col":-3,"ns_id":3,"virt_text_pos":"win_col","virt_text_repeat_linebreak":true,"priority":4096,"right_gravity":true,"virt_text":[["▌","OpencodeMessageRoleUser"]]}],[3,3,0,{"virt_text_hide":false,"virt_text_win_col":-3,"ns_id":3,"virt_text_pos":"win_col","virt_text_repeat_linebreak":true,"priority":4096,"right_gravity":true,"virt_text":[["▌","OpencodeMessageRoleUser"]]}],[4,4,0,{"virt_text_hide":false,"virt_text_win_col":-3,"ns_id":3,"virt_text_pos":"win_col","virt_text_repeat_linebreak":true,"priority":4096,"right_gravity":true,"virt_text":[["▌","OpencodeMessageRoleUser"]]}],[5,5,0,{"virt_text_hide":false,"virt_text_win_col":-3,"ns_id":3,"virt_text_pos":"win_col","virt_text_repeat_linebreak":true,"priority":4096,"right_gravity":true,"virt_text":[["▌","OpencodeMessageRoleUser"]]}],[6,8,0,{"virt_text_hide":false,"virt_text_win_col":-3,"ns_id":3,"virt_text_pos":"win_col","virt_text_repeat_linebreak":false,"priority":10,"right_gravity":true,"virt_text":[[" ","OpencodeMessageRoleAssistant"],[" "],["BUILD","OpencodeMessageRoleAssistant"],[" gpt-5-mini","OpencodeHint"],[" (2025-10-20 15:20:04)","OpencodeHint"],[" [msg_a0234c7960011LTxTvD94hfWCi]","OpencodeHint"]]}],[7,12,0,{"virt_text_hide":false,"virt_text_win_col":-1,"ns_id":3,"virt_text_pos":"win_col","virt_text_repeat_linebreak":true,"priority":4096,"right_gravity":true,"virt_text":[["▌","OpencodeToolBorder"]]}],[8,13,0,{"virt_text_hide":false,"virt_text_win_col":-1,"ns_id":3,"virt_text_pos":"win_col","virt_text_repeat_linebreak":true,"priority":4096,"right_gravity":true,"virt_text":[["▌","OpencodeToolBorder"]]}],[9,14,0,{"virt_text_hide":false,"virt_text_win_col":-1,"ns_id":3,"virt_text_pos":"win_col","virt_text_repeat_linebreak":true,"priority":4096,"right_gravity":true,"virt_text":[["▌","OpencodeToolBorder"]]}],[10,15,0,{"hl_eol":true,"end_right_gravity":false,"ns_id":3,"virt_text_pos":"overlay","virt_text_repeat_linebreak":false,"priority":5000,"virt_text_hide":false,"virt_text":[["-","OpencodeDiffDelete"]],"hl_group":"OpencodeDiffDelete","end_col":0,"right_gravity":true,"end_row":16}],[11,15,0,{"virt_text_hide":false,"virt_text_win_col":-1,"ns_id":3,"virt_text_pos":"win_col","virt_text_repeat_linebreak":true,"priority":4096,"right_gravity":true,"virt_text":[["▌","OpencodeToolBorder"]]}],[12,16,0,{"hl_eol":true,"end_right_gravity":false,"ns_id":3,"virt_text_pos":"overlay","virt_text_repeat_linebreak":false,"priority":5000,"virt_text_hide":false,"virt_text":[["+","OpencodeDiffAdd"]],"hl_group":"OpencodeDiffAdd","end_col":0,"right_gravity":true,"end_row":17}],[13,16,0,{"virt_text_hide":false,"virt_text_win_col":-1,"ns_id":3,"virt_text_pos":"win_col","virt_text_repeat_linebreak":true,"priority":4096,"right_gravity":true,"virt_text":[["▌","OpencodeToolBorder"]]}],[14,17,0,{"virt_text_hide":false,"virt_text_win_col":-1,"ns_id":3,"virt_text_pos":"win_col","virt_text_repeat_linebreak":true,"priority":4096,"right_gravity":true,"virt_text":[["▌","OpencodeToolBorder"]]}],[15,18,0,{"virt_text_hide":false,"virt_text_win_col":-1,"ns_id":3,"virt_text_pos":"win_col","virt_text_repeat_linebreak":true,"priority":4096,"right_gravity":true,"virt_text":[["▌","OpencodeToolBorder"]]}],[16,19,0,{"virt_text_hide":false,"virt_text_win_col":-1,"ns_id":3,"virt_text_pos":"win_col","virt_text_repeat_linebreak":true,"priority":4096,"right_gravity":true,"virt_text":[["▌","OpencodeToolBorder"]]}],[17,20,0,{"virt_text_hide":false,"virt_text_win_col":-1,"ns_id":3,"virt_text_pos":"win_col","virt_text_repeat_linebreak":true,"priority":4096,"right_gravity":true,"virt_text":[["▌","OpencodeToolBorder"]]}],[18,25,0,{"virt_text_hide":false,"virt_text_win_col":-3,"ns_id":3,"virt_text_pos":"win_col","virt_text_repeat_linebreak":false,"priority":10,"right_gravity":true,"virt_text":[[" ","OpencodeMessageRoleAssistant"],[" "],["BUILD","OpencodeMessageRoleAssistant"],[" gpt-5-mini","OpencodeHint"],[" (2025-10-20 15:20:09)","OpencodeHint"],[" [msg_a0234d8fb001SXyngLjuKSuxOY]","OpencodeHint"]]}],[19,30,0,{"virt_text_hide":false,"virt_text_win_col":-3,"ns_id":3,"virt_text_pos":"win_col","virt_text_repeat_linebreak":false,"priority":10,"right_gravity":true,"virt_text":[["▌󰭻 ","OpencodeMessageRoleUser"],[" "],["USER","OpencodeMessageRoleUser"],["","OpencodeHint"],[" (2025-10-20 15:20:11)","OpencodeHint"],[" [msg_a0234e308001SKl5bQUibp5gtI]","OpencodeHint"]]}],[20,31,0,{"virt_text_hide":false,"virt_text_win_col":-3,"ns_id":3,"virt_text_pos":"win_col","virt_text_repeat_linebreak":true,"priority":4096,"right_gravity":true,"virt_text":[["▌","OpencodeMessageRoleUser"]]}],[21,32,0,{"virt_text_hide":false,"virt_text_win_col":-3,"ns_id":3,"virt_text_pos":"win_col","virt_text_repeat_linebreak":true,"priority":4096,"right_gravity":true,"virt_text":[["▌","OpencodeMessageRoleUser"]]}],[22,35,0,{"virt_text_hide":false,"virt_text_win_col":-3,"ns_id":3,"virt_text_pos":"win_col","virt_text_repeat_linebreak":false,"priority":10,"right_gravity":true,"virt_text":[[" ","OpencodeMessageRoleAssistant"],[" "],["BUILD","OpencodeMessageRoleAssistant"],[" gpt-5-mini","OpencodeHint"],[" (2025-10-20 15:20:11)","OpencodeHint"],[" [msg_a0234e31f001m4EsQdPmY3PTtS]","OpencodeHint"]]}],[23,42,0,{"virt_text_hide":false,"virt_text_win_col":-3,"ns_id":3,"virt_text_pos":"win_col","virt_text_repeat_linebreak":false,"priority":10,"right_gravity":true,"virt_text":[[" ","OpencodeMessageRoleAssistant"],[" "],["BUILD","OpencodeMessageRoleAssistant"],[" gpt-5-mini","OpencodeHint"],[" (2025-10-20 15:20:16)","OpencodeHint"],[" [msg_a0234f482001PQbMjWc6W8s0eF]","OpencodeHint"]]}],[24,46,0,{"virt_text_hide":false,"virt_text_win_col":-1,"ns_id":3,"virt_text_pos":"win_col","virt_text_repeat_linebreak":true,"priority":4096,"right_gravity":true,"virt_text":[["▌","OpencodeToolBorder"]]}],[25,47,0,{"virt_text_hide":false,"virt_text_win_col":-1,"ns_id":3,"virt_text_pos":"win_col","virt_text_repeat_linebreak":true,"priority":4096,"right_gravity":true,"virt_text":[["▌","OpencodeToolBorder"]]}],[26,48,0,{"virt_text_hide":false,"virt_text_win_col":-1,"ns_id":3,"virt_text_pos":"win_col","virt_text_repeat_linebreak":true,"priority":4096,"right_gravity":true,"virt_text":[["▌","OpencodeToolBorder"]]}],[27,49,0,{"hl_eol":true,"end_right_gravity":false,"ns_id":3,"virt_text_pos":"overlay","virt_text_repeat_linebreak":false,"priority":5000,"virt_text_hide":false,"virt_text":[["-","OpencodeDiffDelete"]],"hl_group":"OpencodeDiffDelete","end_col":0,"right_gravity":true,"end_row":50}],[28,49,0,{"virt_text_hide":false,"virt_text_win_col":-1,"ns_id":3,"virt_text_pos":"win_col","virt_text_repeat_linebreak":true,"priority":4096,"right_gravity":true,"virt_text":[["▌","OpencodeToolBorder"]]}],[29,50,0,{"hl_eol":true,"end_right_gravity":false,"ns_id":3,"virt_text_pos":"overlay","virt_text_repeat_linebreak":false,"priority":5000,"virt_text_hide":false,"virt_text":[["+","OpencodeDiffAdd"]],"hl_group":"OpencodeDiffAdd","end_col":0,"right_gravity":true,"end_row":51}],[30,50,0,{"virt_text_hide":false,"virt_text_win_col":-1,"ns_id":3,"virt_text_pos":"win_col","virt_text_repeat_linebreak":true,"priority":4096,"right_gravity":true,"virt_text":[["▌","OpencodeToolBorder"]]}],[31,51,0,{"virt_text_hide":false,"virt_text_win_col":-1,"ns_id":3,"virt_text_pos":"win_col","virt_text_repeat_linebreak":true,"priority":4096,"right_gravity":true,"virt_text":[["▌","OpencodeToolBorder"]]}],[32,52,0,{"virt_text_hide":false,"virt_text_win_col":-1,"ns_id":3,"virt_text_pos":"win_col","virt_text_repeat_linebreak":true,"priority":4096,"right_gravity":true,"virt_text":[["▌","OpencodeToolBorder"]]}],[33,53,0,{"virt_text_hide":false,"virt_text_win_col":-1,"ns_id":3,"virt_text_pos":"win_col","virt_text_repeat_linebreak":true,"priority":4096,"right_gravity":true,"virt_text":[["▌","OpencodeToolBorder"]]}],[34,54,0,{"virt_text_hide":false,"virt_text_win_col":-1,"ns_id":3,"virt_text_pos":"win_col","virt_text_repeat_linebreak":true,"priority":4096,"right_gravity":true,"virt_text":[["▌","OpencodeToolBorder"]]}],[35,59,0,{"virt_text_hide":false,"virt_text_win_col":-3,"ns_id":3,"virt_text_pos":"win_col","virt_text_repeat_linebreak":false,"priority":10,"right_gravity":true,"virt_text":[[" ","OpencodeMessageRoleAssistant"],[" "],["BUILD","OpencodeMessageRoleAssistant"],[" gpt-5-mini","OpencodeHint"],[" (2025-10-20 15:20:17)","OpencodeHint"],[" [msg_a0234f9c6001JCKYaca1HHwwx6]","OpencodeHint"]]}],[36,64,0,{"virt_text_hide":false,"virt_text_win_col":-3,"ns_id":3,"virt_text_pos":"win_col","virt_text_repeat_linebreak":false,"priority":10,"right_gravity":true,"virt_text":[["▌󰭻 ","OpencodeMessageRoleUser"],[" "],["USER","OpencodeMessageRoleUser"],["","OpencodeHint"],[" (2025-10-20 15:22:29)","OpencodeHint"],[" [msg_a0236fd1c001TlwqL8fwvq529i]","OpencodeHint"]]}],[37,65,0,{"virt_text_hide":false,"virt_text_win_col":-3,"ns_id":3,"virt_text_pos":"win_col","virt_text_repeat_linebreak":true,"priority":4096,"right_gravity":true,"virt_text":[["▌","OpencodeMessageRoleUser"]]}],[38,66,0,{"virt_text_hide":false,"virt_text_win_col":-3,"ns_id":3,"virt_text_pos":"win_col","virt_text_repeat_linebreak":true,"priority":4096,"right_gravity":true,"virt_text":[["▌","OpencodeMessageRoleUser"]]}],[39,69,0,{"virt_text_hide":false,"virt_text_win_col":-3,"ns_id":3,"virt_text_pos":"win_col","virt_text_repeat_linebreak":false,"priority":10,"right_gravity":true,"virt_text":[[" ","OpencodeMessageRoleAssistant"],[" "],["BUILD","OpencodeMessageRoleAssistant"],[" gpt-5-mini","OpencodeHint"],[" (2025-10-20 15:22:29)","OpencodeHint"],[" [msg_a0236fd57001pTnTjSBdFlleCb]","OpencodeHint"]]}],[40,76,0,{"virt_text_hide":false,"virt_text_win_col":-3,"ns_id":3,"virt_text_pos":"win_col","virt_text_repeat_linebreak":false,"priority":10,"right_gravity":true,"virt_text":[[" ","OpencodeMessageRoleAssistant"],[" "],["BUILD","OpencodeMessageRoleAssistant"],[" gpt-5-mini","OpencodeHint"],[" (2025-10-20 15:22:34)","OpencodeHint"],[" [msg_a02371241001PBQAsr8Oc9hqNI]","OpencodeHint"]]}],[41,80,0,{"virt_text_hide":false,"virt_text_win_col":-1,"ns_id":3,"virt_text_pos":"win_col","virt_text_repeat_linebreak":true,"priority":4096,"right_gravity":true,"virt_text":[["▌","OpencodeToolBorder"]]}],[42,81,0,{"virt_text_hide":false,"virt_text_win_col":-1,"ns_id":3,"virt_text_pos":"win_col","virt_text_repeat_linebreak":true,"priority":4096,"right_gravity":true,"virt_text":[["▌","OpencodeToolBorder"]]}],[43,82,0,{"virt_text_hide":false,"virt_text_win_col":-1,"ns_id":3,"virt_text_pos":"win_col","virt_text_repeat_linebreak":true,"priority":4096,"right_gravity":true,"virt_text":[["▌","OpencodeToolBorder"]]}],[44,83,0,{"hl_eol":true,"end_right_gravity":false,"ns_id":3,"virt_text_pos":"overlay","virt_text_repeat_linebreak":false,"priority":5000,"virt_text_hide":false,"virt_text":[["-","OpencodeDiffDelete"]],"hl_group":"OpencodeDiffDelete","end_col":0,"right_gravity":true,"end_row":84}],[45,83,0,{"virt_text_hide":false,"virt_text_win_col":-1,"ns_id":3,"virt_text_pos":"win_col","virt_text_repeat_linebreak":true,"priority":4096,"right_gravity":true,"virt_text":[["▌","OpencodeToolBorder"]]}],[46,84,0,{"hl_eol":true,"end_right_gravity":false,"ns_id":3,"virt_text_pos":"overlay","virt_text_repeat_linebreak":false,"priority":5000,"virt_text_hide":false,"virt_text":[["+","OpencodeDiffAdd"]],"hl_group":"OpencodeDiffAdd","end_col":0,"right_gravity":true,"end_row":85}],[47,84,0,{"virt_text_hide":false,"virt_text_win_col":-1,"ns_id":3,"virt_text_pos":"win_col","virt_text_repeat_linebreak":true,"priority":4096,"right_gravity":true,"virt_text":[["▌","OpencodeToolBorder"]]}],[48,85,0,{"virt_text_hide":false,"virt_text_win_col":-1,"ns_id":3,"virt_text_pos":"win_col","virt_text_repeat_linebreak":true,"priority":4096,"right_gravity":true,"virt_text":[["▌","OpencodeToolBorder"]]}],[49,86,0,{"virt_text_hide":false,"virt_text_win_col":-1,"ns_id":3,"virt_text_pos":"win_col","virt_text_repeat_linebreak":true,"priority":4096,"right_gravity":true,"virt_text":[["▌","OpencodeToolBorder"]]}],[50,87,0,{"virt_text_hide":false,"virt_text_win_col":-1,"ns_id":3,"virt_text_pos":"win_col","virt_text_repeat_linebreak":true,"priority":4096,"right_gravity":true,"virt_text":[["▌","OpencodeToolBorder"]]}],[51,88,0,{"virt_text_hide":false,"virt_text_win_col":-1,"ns_id":3,"virt_text_pos":"win_col","virt_text_repeat_linebreak":true,"priority":4096,"right_gravity":true,"virt_text":[["▌","OpencodeToolBorder"]]}],[52,93,0,{"virt_text_hide":false,"virt_text_win_col":-3,"ns_id":3,"virt_text_pos":"win_col","virt_text_repeat_linebreak":false,"priority":10,"right_gravity":true,"virt_text":[[" ","OpencodeMessageRoleAssistant"],[" "],["BUILD","OpencodeMessageRoleAssistant"],[" gpt-5-mini","OpencodeHint"],[" (2025-10-20 15:22:39)","OpencodeHint"],[" [msg_a023723d0001r87MaJThFssUw1]","OpencodeHint"]]}]],"timestamp":1769172213,"actions":[{"text":"[R]evert file","display_line":90,"args":["d988cc85565b99017d40ad8baea20225165be9d5"],"type":"diff_revert_selected_file","range":{"from":90,"to":90},"key":"R"},{"text":"Revert [A]ll","display_line":90,"args":["d988cc85565b99017d40ad8baea20225165be9d5"],"type":"diff_revert_all","range":{"from":90,"to":90},"key":"A"},{"text":"[D]iff","display_line":90,"args":["d988cc85565b99017d40ad8baea20225165be9d5"],"type":"diff_open","range":{"from":90,"to":90},"key":"D"},{"text":"[R]evert file","display_line":22,"args":["1b6ba655c6c0d899965adff278ac6320d5fc3b12"],"type":"diff_revert_selected_file","range":{"from":22,"to":22},"key":"R"},{"text":"Revert [A]ll","display_line":22,"args":["1b6ba655c6c0d899965adff278ac6320d5fc3b12"],"type":"diff_revert_all","range":{"from":22,"to":22},"key":"A"},{"text":"[D]iff","display_line":22,"args":["1b6ba655c6c0d899965adff278ac6320d5fc3b12"],"type":"diff_open","range":{"from":22,"to":22},"key":"D"},{"text":"[R]evert file","display_line":56,"args":["57d83f5596cb1f142fbc681d3d93b7184f7f73cd"],"type":"diff_revert_selected_file","range":{"from":56,"to":56},"key":"R"},{"text":"Revert [A]ll","display_line":56,"args":["57d83f5596cb1f142fbc681d3d93b7184f7f73cd"],"type":"diff_revert_all","range":{"from":56,"to":56},"key":"A"},{"text":"[D]iff","display_line":56,"args":["57d83f5596cb1f142fbc681d3d93b7184f7f73cd"],"type":"diff_open","range":{"from":56,"to":56},"key":"D"}],"lines":["----","","","add another word","","[`test.txt`](test.txt)","","----","","","I'll append a single word (\"again\") to the first line of `test.txt`. Applying a precise edit to the existing line now.","","** edit** `/home/francis/Projects/_nvim/opencode.nvim/test.txt`","","`````txt"," tangram quiver saffron nebula cobalt murmur plinth zephyr ember lattice cadenza another yet extra more"," tangram quiver saffron nebula cobalt murmur plinth zephyr ember lattice cadenza another yet extra more again"," "," ","","`````","","**󰻛 Created Snapshot** `1b6ba655`","","----","","","**Done:** added the word `again` to `test.txt`.","","----","","","add another word","","----","","","I'll read `test.txt` to get the current first-line content, then append one word (\"again2\") to that line. Proceeding to read the file.","","** read** `/home/francis/Projects/_nvim/opencode.nvim/test.txt`","","----","","","Now I'll append the word \"again2\" to the first line. I'll apply an exact in-place edit to update that line.","","** edit** `/home/francis/Projects/_nvim/opencode.nvim/test.txt`","","`````txt"," tangram quiver saffron nebula cobalt murmur plinth zephyr ember lattice cadenza another yet extra more again"," tangram quiver saffron nebula cobalt murmur plinth zephyr ember lattice cadenza another yet extra more again again2"," "," ","","`````","","**󰻛 Created Snapshot** `57d83f55`","","----","","","**Done:** appended the word `again2` to `test.txt`.","","----","","","add another word","","----","","","I'll read `test.txt` to get the current first-line content, then append the word `again3`. Proceeding to read the file.","","** read** `/home/francis/Projects/_nvim/opencode.nvim/test.txt`","","----","","","I'll append the word \"again3\" to the first line of `test.txt` with an exact in-place edit. Applying the change now.","","** edit** `/home/francis/Projects/_nvim/opencode.nvim/test.txt`","","`````txt"," tangram quiver saffron nebula cobalt murmur plinth zephyr ember lattice cadenza another yet extra more again again2"," tangram quiver saffron nebula cobalt murmur plinth zephyr ember lattice cadenza another yet extra more again again2 again3"," "," ","","`````","","**󰻛 Created Snapshot** `d988cc85`","","----","","","**Done:** appended the word `again3` to `test.txt`.","",""]} +{"timestamp":1770332852,"lines":["----","","","add another word","","[`test.txt`](test.txt)","","----","","","I'll append a single word (\"again\") to the first line of `test.txt`. Applying a precise edit to the existing line now.","","** edit** `/home/francis/Projects/_nvim/opencode.nvim/test.txt`","","`````txt"," tangram quiver saffron nebula cobalt murmur plinth zephyr ember lattice cadenza another yet extra more"," tangram quiver saffron nebula cobalt murmur plinth zephyr ember lattice cadenza another yet extra more again"," "," ","","`````","","**󰻛 Created Snapshot** `1b6ba655`","","----","","","**Done:** added the word `again` to `test.txt`.","","----","","","add another word","","----","","","I'll read `test.txt` to get the current first-line content, then append one word (\"again2\") to that line. Proceeding to read the file.","","** read** `/home/francis/Projects/_nvim/opencode.nvim/test.txt`","","----","","","Now I'll append the word \"again2\" to the first line. I'll apply an exact in-place edit to update that line.","","** edit** `/home/francis/Projects/_nvim/opencode.nvim/test.txt`","","`````txt"," tangram quiver saffron nebula cobalt murmur plinth zephyr ember lattice cadenza another yet extra more again"," tangram quiver saffron nebula cobalt murmur plinth zephyr ember lattice cadenza another yet extra more again again2"," "," ","","`````","","**󰻛 Created Snapshot** `57d83f55`","","----","","","**Done:** appended the word `again2` to `test.txt`.","","----","","> 1 message reverted, 2 tool calls reverted",">","> type `/redo` to restore.",""," test.txt: +1 -1",""],"extmarks":[[1,1,0,{"virt_text":[["▌󰭻 ","OpencodeMessageRoleUser"],[" "],["USER","OpencodeMessageRoleUser"],["","OpencodeHint"],[" (2025-10-20 15:20:02)","OpencodeHint"],[" [msg_a0234c0b7001y2o9S1jMaNVZar]","OpencodeHint"]],"virt_text_pos":"win_col","virt_text_win_col":-3,"right_gravity":true,"ns_id":3,"priority":10,"virt_text_repeat_linebreak":false,"virt_text_hide":false}],[2,2,0,{"virt_text":[["▌","OpencodeMessageRoleUser"]],"virt_text_pos":"win_col","virt_text_win_col":-3,"right_gravity":true,"ns_id":3,"priority":4096,"virt_text_repeat_linebreak":true,"virt_text_hide":false}],[3,3,0,{"virt_text":[["▌","OpencodeMessageRoleUser"]],"virt_text_pos":"win_col","virt_text_win_col":-3,"right_gravity":true,"ns_id":3,"priority":4096,"virt_text_repeat_linebreak":true,"virt_text_hide":false}],[4,4,0,{"virt_text":[["▌","OpencodeMessageRoleUser"]],"virt_text_pos":"win_col","virt_text_win_col":-3,"right_gravity":true,"ns_id":3,"priority":4096,"virt_text_repeat_linebreak":true,"virt_text_hide":false}],[5,5,0,{"virt_text":[["▌","OpencodeMessageRoleUser"]],"virt_text_pos":"win_col","virt_text_win_col":-3,"right_gravity":true,"ns_id":3,"priority":4096,"virt_text_repeat_linebreak":true,"virt_text_hide":false}],[6,8,0,{"virt_text":[[" ","OpencodeMessageRoleAssistant"],[" "],["BUILD","OpencodeMessageRoleAssistant"],[" gpt-5-mini","OpencodeHint"],[" (2025-10-20 15:20:04)","OpencodeHint"],[" [msg_a0234c7960011LTxTvD94hfWCi]","OpencodeHint"]],"virt_text_pos":"win_col","virt_text_win_col":-3,"right_gravity":true,"ns_id":3,"priority":10,"virt_text_repeat_linebreak":false,"virt_text_hide":false}],[7,12,0,{"virt_text":[["▌","OpencodeToolBorder"]],"virt_text_pos":"win_col","virt_text_win_col":-1,"right_gravity":true,"ns_id":3,"priority":4096,"virt_text_repeat_linebreak":true,"virt_text_hide":false}],[8,13,0,{"virt_text":[["▌","OpencodeToolBorder"]],"virt_text_pos":"win_col","virt_text_win_col":-1,"right_gravity":true,"ns_id":3,"priority":4096,"virt_text_repeat_linebreak":true,"virt_text_hide":false}],[9,14,0,{"virt_text":[["▌","OpencodeToolBorder"]],"virt_text_pos":"win_col","virt_text_win_col":-1,"right_gravity":true,"ns_id":3,"priority":4096,"virt_text_repeat_linebreak":true,"virt_text_hide":false}],[10,15,0,{"end_row":16,"hl_eol":true,"right_gravity":true,"ns_id":3,"virt_text_repeat_linebreak":false,"hl_group":"OpencodeDiffDelete","virt_text":[["-","OpencodeDiffDelete"]],"virt_text_pos":"overlay","priority":5000,"virt_text_hide":false,"end_right_gravity":false,"end_col":0}],[11,15,0,{"virt_text":[["▌","OpencodeToolBorder"]],"virt_text_pos":"win_col","virt_text_win_col":-1,"right_gravity":true,"ns_id":3,"priority":4096,"virt_text_repeat_linebreak":true,"virt_text_hide":false}],[12,16,0,{"end_row":17,"hl_eol":true,"right_gravity":true,"ns_id":3,"virt_text_repeat_linebreak":false,"hl_group":"OpencodeDiffAdd","virt_text":[["+","OpencodeDiffAdd"]],"virt_text_pos":"overlay","priority":5000,"virt_text_hide":false,"end_right_gravity":false,"end_col":0}],[13,16,0,{"virt_text":[["▌","OpencodeToolBorder"]],"virt_text_pos":"win_col","virt_text_win_col":-1,"right_gravity":true,"ns_id":3,"priority":4096,"virt_text_repeat_linebreak":true,"virt_text_hide":false}],[14,17,0,{"virt_text":[["▌","OpencodeToolBorder"]],"virt_text_pos":"win_col","virt_text_win_col":-1,"right_gravity":true,"ns_id":3,"priority":4096,"virt_text_repeat_linebreak":true,"virt_text_hide":false}],[15,18,0,{"virt_text":[["▌","OpencodeToolBorder"]],"virt_text_pos":"win_col","virt_text_win_col":-1,"right_gravity":true,"ns_id":3,"priority":4096,"virt_text_repeat_linebreak":true,"virt_text_hide":false}],[16,19,0,{"virt_text":[["▌","OpencodeToolBorder"]],"virt_text_pos":"win_col","virt_text_win_col":-1,"right_gravity":true,"ns_id":3,"priority":4096,"virt_text_repeat_linebreak":true,"virt_text_hide":false}],[17,20,0,{"virt_text":[["▌","OpencodeToolBorder"]],"virt_text_pos":"win_col","virt_text_win_col":-1,"right_gravity":true,"ns_id":3,"priority":4096,"virt_text_repeat_linebreak":true,"virt_text_hide":false}],[18,25,0,{"virt_text":[[" ","OpencodeMessageRoleAssistant"],[" "],["BUILD","OpencodeMessageRoleAssistant"],[" gpt-5-mini","OpencodeHint"],[" (2025-10-20 15:20:09)","OpencodeHint"],[" [msg_a0234d8fb001SXyngLjuKSuxOY]","OpencodeHint"]],"virt_text_pos":"win_col","virt_text_win_col":-3,"right_gravity":true,"ns_id":3,"priority":10,"virt_text_repeat_linebreak":false,"virt_text_hide":false}],[19,30,0,{"virt_text":[["▌󰭻 ","OpencodeMessageRoleUser"],[" "],["USER","OpencodeMessageRoleUser"],["","OpencodeHint"],[" (2025-10-20 15:20:11)","OpencodeHint"],[" [msg_a0234e308001SKl5bQUibp5gtI]","OpencodeHint"]],"virt_text_pos":"win_col","virt_text_win_col":-3,"right_gravity":true,"ns_id":3,"priority":10,"virt_text_repeat_linebreak":false,"virt_text_hide":false}],[20,31,0,{"virt_text":[["▌","OpencodeMessageRoleUser"]],"virt_text_pos":"win_col","virt_text_win_col":-3,"right_gravity":true,"ns_id":3,"priority":4096,"virt_text_repeat_linebreak":true,"virt_text_hide":false}],[21,32,0,{"virt_text":[["▌","OpencodeMessageRoleUser"]],"virt_text_pos":"win_col","virt_text_win_col":-3,"right_gravity":true,"ns_id":3,"priority":4096,"virt_text_repeat_linebreak":true,"virt_text_hide":false}],[22,35,0,{"virt_text":[[" ","OpencodeMessageRoleAssistant"],[" "],["BUILD","OpencodeMessageRoleAssistant"],[" gpt-5-mini","OpencodeHint"],[" (2025-10-20 15:20:11)","OpencodeHint"],[" [msg_a0234e31f001m4EsQdPmY3PTtS]","OpencodeHint"]],"virt_text_pos":"win_col","virt_text_win_col":-3,"right_gravity":true,"ns_id":3,"priority":10,"virt_text_repeat_linebreak":false,"virt_text_hide":false}],[23,42,0,{"virt_text":[[" ","OpencodeMessageRoleAssistant"],[" "],["BUILD","OpencodeMessageRoleAssistant"],[" gpt-5-mini","OpencodeHint"],[" (2025-10-20 15:20:16)","OpencodeHint"],[" [msg_a0234f482001PQbMjWc6W8s0eF]","OpencodeHint"]],"virt_text_pos":"win_col","virt_text_win_col":-3,"right_gravity":true,"ns_id":3,"priority":10,"virt_text_repeat_linebreak":false,"virt_text_hide":false}],[24,46,0,{"virt_text":[["▌","OpencodeToolBorder"]],"virt_text_pos":"win_col","virt_text_win_col":-1,"right_gravity":true,"ns_id":3,"priority":4096,"virt_text_repeat_linebreak":true,"virt_text_hide":false}],[25,47,0,{"virt_text":[["▌","OpencodeToolBorder"]],"virt_text_pos":"win_col","virt_text_win_col":-1,"right_gravity":true,"ns_id":3,"priority":4096,"virt_text_repeat_linebreak":true,"virt_text_hide":false}],[26,48,0,{"virt_text":[["▌","OpencodeToolBorder"]],"virt_text_pos":"win_col","virt_text_win_col":-1,"right_gravity":true,"ns_id":3,"priority":4096,"virt_text_repeat_linebreak":true,"virt_text_hide":false}],[27,49,0,{"end_row":50,"hl_eol":true,"right_gravity":true,"ns_id":3,"virt_text_repeat_linebreak":false,"hl_group":"OpencodeDiffDelete","virt_text":[["-","OpencodeDiffDelete"]],"virt_text_pos":"overlay","priority":5000,"virt_text_hide":false,"end_right_gravity":false,"end_col":0}],[28,49,0,{"virt_text":[["▌","OpencodeToolBorder"]],"virt_text_pos":"win_col","virt_text_win_col":-1,"right_gravity":true,"ns_id":3,"priority":4096,"virt_text_repeat_linebreak":true,"virt_text_hide":false}],[29,50,0,{"end_row":51,"hl_eol":true,"right_gravity":true,"ns_id":3,"virt_text_repeat_linebreak":false,"hl_group":"OpencodeDiffAdd","virt_text":[["+","OpencodeDiffAdd"]],"virt_text_pos":"overlay","priority":5000,"virt_text_hide":false,"end_right_gravity":false,"end_col":0}],[30,50,0,{"virt_text":[["▌","OpencodeToolBorder"]],"virt_text_pos":"win_col","virt_text_win_col":-1,"right_gravity":true,"ns_id":3,"priority":4096,"virt_text_repeat_linebreak":true,"virt_text_hide":false}],[31,51,0,{"virt_text":[["▌","OpencodeToolBorder"]],"virt_text_pos":"win_col","virt_text_win_col":-1,"right_gravity":true,"ns_id":3,"priority":4096,"virt_text_repeat_linebreak":true,"virt_text_hide":false}],[32,52,0,{"virt_text":[["▌","OpencodeToolBorder"]],"virt_text_pos":"win_col","virt_text_win_col":-1,"right_gravity":true,"ns_id":3,"priority":4096,"virt_text_repeat_linebreak":true,"virt_text_hide":false}],[33,53,0,{"virt_text":[["▌","OpencodeToolBorder"]],"virt_text_pos":"win_col","virt_text_win_col":-1,"right_gravity":true,"ns_id":3,"priority":4096,"virt_text_repeat_linebreak":true,"virt_text_hide":false}],[34,54,0,{"virt_text":[["▌","OpencodeToolBorder"]],"virt_text_pos":"win_col","virt_text_win_col":-1,"right_gravity":true,"ns_id":3,"priority":4096,"virt_text_repeat_linebreak":true,"virt_text_hide":false}],[35,59,0,{"virt_text":[[" ","OpencodeMessageRoleAssistant"],[" "],["BUILD","OpencodeMessageRoleAssistant"],[" gpt-5-mini","OpencodeHint"],[" (2025-10-20 15:20:17)","OpencodeHint"],[" [msg_a0234f9c6001JCKYaca1HHwwx6]","OpencodeHint"]],"virt_text_pos":"win_col","virt_text_win_col":-3,"right_gravity":true,"ns_id":3,"priority":10,"virt_text_repeat_linebreak":false,"virt_text_hide":false}],[36,69,0,{"virt_text":[["+1","OpencodeDiffAddText"]],"virt_text_pos":"win_col","virt_text_win_col":12,"right_gravity":true,"ns_id":3,"priority":1000,"virt_text_repeat_linebreak":false,"virt_text_hide":false}],[37,69,0,{"virt_text":[["-1","OpencodeDiffDeleteText"]],"virt_text_pos":"win_col","virt_text_win_col":15,"right_gravity":true,"ns_id":3,"priority":1000,"virt_text_repeat_linebreak":false,"virt_text_hide":false}]],"actions":[{"text":"[R]evert file","key":"R","type":"diff_revert_selected_file","range":{"to":56,"from":56},"args":["57d83f5596cb1f142fbc681d3d93b7184f7f73cd"],"display_line":56},{"text":"Revert [A]ll","key":"A","type":"diff_revert_all","range":{"to":56,"from":56},"args":["57d83f5596cb1f142fbc681d3d93b7184f7f73cd"],"display_line":56},{"text":"[D]iff","key":"D","type":"diff_open","range":{"to":56,"from":56},"args":["57d83f5596cb1f142fbc681d3d93b7184f7f73cd"],"display_line":56},{"text":"[R]evert file","key":"R","type":"diff_revert_selected_file","range":{"to":22,"from":22},"args":["1b6ba655c6c0d899965adff278ac6320d5fc3b12"],"display_line":22},{"text":"Revert [A]ll","key":"A","type":"diff_revert_all","range":{"to":22,"from":22},"args":["1b6ba655c6c0d899965adff278ac6320d5fc3b12"],"display_line":22},{"text":"[D]iff","key":"D","type":"diff_open","range":{"to":22,"from":22},"args":["1b6ba655c6c0d899965adff278ac6320d5fc3b12"],"display_line":22}]} \ No newline at end of file diff --git a/tests/helpers.lua b/tests/helpers.lua index 886f61c5..95f22ab1 100644 --- a/tests/helpers.lua +++ b/tests/helpers.lua @@ -224,15 +224,26 @@ end function M.get_session_from_events(events, with_session_updates) -- renderer needs a valid session id - -- find the last session.updated event + -- merge session.updated events and use the latest updated session if with_session_updates then - for i = #events, 1, -1 do - local event = events[i] - if event.type == 'session.updated' and event.properties.info and event.properties.info then - return event.properties.info + local sessions_by_id = {} + local last_session_id = nil + + for _, event in ipairs(events) do + if event.type == 'session.updated' and event.properties.info then + local info = event.properties.info + if info.id then + local existing = sessions_by_id[info.id] or {} + sessions_by_id[info.id] = vim.tbl_deep_extend('force', existing, vim.deepcopy(info)) + last_session_id = info.id + end end end + + if last_session_id then + return sessions_by_id[last_session_id] + end end for _, event in ipairs(events) do -- find the session id in a message or part event From 4c68d5a6573524a088bb4f83772347c289f7eef3 Mon Sep 17 00:00:00 2001 From: disrupted Date: Fri, 6 Feb 2026 15:16:36 +0100 Subject: [PATCH 4/5] docs: add comment --- lua/opencode/ui/renderer.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lua/opencode/ui/renderer.lua b/lua/opencode/ui/renderer.lua index 8e82d384..91c24337 100644 --- a/lua/opencode/ui/renderer.lua +++ b/lua/opencode/ui/renderer.lua @@ -850,6 +850,9 @@ function M.on_session_updated(properties) local previous_title = current_session.title local merged_session = vim.tbl_deep_extend('force', vim.deepcopy(current_session), updated_session) + + -- mutate existing `state.active_session` table in place + -- reassigning would cause UI flickering on frequent `session.updated` events since it triggers a full rerender if not vim.deep_equal(current_session, merged_session) then for key, value in pairs(merged_session) do current_session[key] = value From f0420624c813baf12494273d807a86f98fdbcce3 Mon Sep 17 00:00:00 2001 From: disrupted Date: Fri, 6 Feb 2026 15:18:04 +0100 Subject: [PATCH 5/5] docs: move comment to the correct place --- lua/opencode/ui/renderer.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lua/opencode/ui/renderer.lua b/lua/opencode/ui/renderer.lua index 91c24337..15d28051 100644 --- a/lua/opencode/ui/renderer.lua +++ b/lua/opencode/ui/renderer.lua @@ -851,9 +851,9 @@ function M.on_session_updated(properties) local merged_session = vim.tbl_deep_extend('force', vim.deepcopy(current_session), updated_session) - -- mutate existing `state.active_session` table in place - -- reassigning would cause UI flickering on frequent `session.updated` events since it triggers a full rerender if not vim.deep_equal(current_session, merged_session) then + -- mutate existing `state.active_session` table in place + -- reassigning would cause UI flickering on frequent `session.updated` events since it triggers a full rerender for key, value in pairs(merged_session) do current_session[key] = value end