From 34fce21d048a9bbbb37b2442e5929ece226ca4da Mon Sep 17 00:00:00 2001 From: phanium <91544758+phanen@users.noreply.github.com> Date: Sat, 11 Jul 2026 19:07:48 +0800 Subject: [PATCH] fix(renderer): skip hidden-messages notice in message navigation Problem: when max_messages truncates older messages, the synthetic hidden-messages notice (appended to state.messages via on_message_updated) is matched by get_prev_rendered_message, so [[ yanks the cursor to the buffer top. Solution: skip entries where is_renderer_synthetic_message is true in both get_prev_rendered_message and get_next_rendered_message. --- lua/opencode/ui/renderer.lua | 18 +++++++---- tests/unit/navigation_spec.lua | 58 ++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 7 deletions(-) diff --git a/lua/opencode/ui/renderer.lua b/lua/opencode/ui/renderer.lua index 25b1a837..52fce068 100644 --- a/lua/opencode/ui/renderer.lua +++ b/lua/opencode/ui/renderer.lua @@ -619,10 +619,12 @@ function M.get_next_rendered_message(current_line) local next_message = nil for _, message in ipairs(state.messages or {}) do - local rendered = message.info and message.info.id and ctx.render_state:get_message(message.info.id) or nil - if rendered and rendered.line_start and rendered.line_start + 1 > current_line then - next_message = rendered - break + if not is_renderer_synthetic_message(message) then + local rendered = message.info and message.info.id and ctx.render_state:get_message(message.info.id) or nil + if rendered and rendered.line_start and rendered.line_start + 1 > current_line then + next_message = rendered + break + end end end @@ -634,9 +636,11 @@ end function M.get_prev_rendered_message(current_line) for i = #(state.messages or {}), 1, -1 do local message = state.messages[i] - local rendered = message and message.info and message.info.id and ctx.render_state:get_message(message.info.id) - if rendered and rendered.line_start and rendered.line_start + 1 < current_line then - return rendered + if message and not is_renderer_synthetic_message(message) then + local rendered = message.info and message.info.id and ctx.render_state:get_message(message.info.id) + if rendered and rendered.line_start and rendered.line_start + 1 < current_line then + return rendered + end end end diff --git a/tests/unit/navigation_spec.lua b/tests/unit/navigation_spec.lua index f6d472da..0e5d858f 100644 --- a/tests/unit/navigation_spec.lua +++ b/tests/unit/navigation_spec.lua @@ -656,3 +656,61 @@ describe('navigation jumplist preservation', function() assert.equals(0, mark[2]) end) end) + +describe('navigation hidden-messages-notice handling', function() + local output_buf, output_win + local original_windows + + before_each(function() + original_windows = state.store.get('windows') + state.ui.clear_hidden_window_state() + + output_buf = vim.api.nvim_create_buf(false, true) + output_win = vim.api.nvim_open_win(output_buf, true, { + relative = 'editor', + width = 80, + height = 20, + row = 0, + col = 0, + }) + state.ui.set_windows({ output_buf = output_buf, output_win = output_win }) + end) + + after_each(function() + state.ui.clear_hidden_window_state() + pcall(vim.api.nvim_win_close, output_win, true) + pcall(vim.api.nvim_buf_delete, output_buf, { force = true }) + + if original_windows ~= nil then + state.ui.set_windows(original_windows) + else + state.ui.clear_windows() + end + end) + + it('does not jump [[ to the hidden-messages notice when max_messages truncates', function() + local ctx = require('opencode.ui.renderer.ctx') + -- Simulate `on_message_updated` appending the hidden notice to `state.messages` after a `max_messages` truncation. + state.renderer.set_messages({ + { info = { id = 'real_old', role = 'assistant', sessionID = 's1' } }, + { info = { id = 'real_mid', role = 'user', sessionID = 's1' } }, + { info = { id = '__opencode_hidden_messages_notice__', role = 'system', sessionID = 's1' } }, + }) + ctx.render_state:set_message( + { info = { id = '__opencode_hidden_messages_notice__', role = 'system', sessionID = 's1' } }, + 1, + 2 + ) + ctx.render_state:set_message({ info = { id = 'real_old', role = 'assistant', sessionID = 's1' } }, 4, 8) + ctx.render_state:set_message({ info = { id = 'real_mid', role = 'user', sessionID = 's1' } }, 10, 18) + + vim.api.nvim_buf_set_lines(output_buf, 0, -1, false, vim.fn['repeat']({ 'line' }, 25)) + -- Without the fix, [[ from line 11 would match the notice (line 1) instead of `real_old` (line 4). + vim.api.nvim_win_set_cursor(output_win, { 11, 0 }) + + navigation.goto_prev_message() + + local cursor = vim.api.nvim_win_get_cursor(output_win) + assert.equals(5, cursor[1]) + end) +end)