Skip to content

Commit bd8db5e

Browse files
authored
feat/jump to user message (#420)
* feat(output_window): add jump-to-user-message action with [u/]u keymap Add navigation.goto_next_user_message / goto_prev_user_message and the underlying renderer.get_next_user_message / get_prev_user_message, which filter state.messages by role == 'user' so the cursor only lands on user turns (not the assistant response between them). Wire them through workflow (action + command_def), expose them in opencode.api, and bind [u / ]u in the default output_window keymap next to the existing ]] / [[ section-movement pair. Tests cover role filtering, jump behavior, and the no-target notify case in tests/unit/navigation_user_message_spec.lua. * fix(navigation): load all messages before user-message jump under lazy render Under lazy render only the most recent N messages have a line_start in render_state, so goto_prev/next_user_message would silently no-op when the target user message had not yet been rendered, falling through to the 'No previous/next user message' notify. Mirror the 'gg' binding in output_window.setup_keymaps and force a full render via renderer.load_all_messages() before resolving the target. Three new tests cover the lazy render interaction.
1 parent d07db45 commit bd8db5e

6 files changed

Lines changed: 433 additions & 0 deletions

File tree

lua/opencode/api.lua

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ local action_groups = {
8080
next_prompt_history = workflow.next_prompt_history,
8181
next_message = workflow.next_message,
8282
prev_message = workflow.prev_message,
83+
next_user_message = workflow.next_user_message,
84+
prev_user_message = workflow.prev_user_message,
8385
mention_file = workflow.mention_file,
8486
mention = workflow.mention,
8587
context_items = workflow.context_items,

lua/opencode/commands/handlers/workflow.lua

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,14 @@ function M.actions.prev_message()
277277
require('opencode.ui.navigation').goto_prev_message()
278278
end
279279

280+
function M.actions.next_user_message()
281+
require('opencode.ui.navigation').goto_next_user_message()
282+
end
283+
284+
function M.actions.prev_user_message()
285+
require('opencode.ui.navigation').goto_prev_user_message()
286+
end
287+
280288
function M.actions.clear_selections()
281289
require('opencode.context').clear_selections()
282290
vim.notify('Selections cleared', vim.log.levels.INFO)
@@ -472,6 +480,14 @@ M.command_defs = {
472480
desc = 'Navigate to previous message in output window',
473481
execute = M.actions.prev_message,
474482
},
483+
next_user_message = {
484+
desc = 'Navigate to next user message in output window',
485+
execute = M.actions.next_user_message,
486+
},
487+
prev_user_message = {
488+
desc = 'Navigate to previous user message in output window',
489+
execute = M.actions.prev_user_message,
490+
},
475491
clear_selections = {
476492
desc = 'Clear only selections from context',
477493
execute = M.actions.clear_selections,

lua/opencode/config.lua

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ M.defaults = {
6969
['<C-c>'] = { 'cancel', desc = 'Cancel running request' },
7070
[']]'] = { 'next_message', desc = 'Go to next message' },
7171
['[['] = { 'prev_message', desc = 'Go to previous message' },
72+
[']u'] = { 'next_user_message', desc = 'Go to next user message' },
73+
['[u'] = { 'prev_user_message', desc = 'Go to previous user message' },
7274
['<tab>'] = { 'toggle_pane', mode = { 'n' }, desc = 'Toggle input/output panes' },
7375
['i'] = { 'focus_input', desc = 'Focus input window' },
7476
['gr'] = { 'references', desc = 'Browse code references' },

lua/opencode/ui/navigation.lua

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,52 @@ function M.goto_prev_message()
6262
vim.api.nvim_win_set_cursor(win, { 1, 0 })
6363
end
6464

65+
function M.goto_next_user_message()
66+
require('opencode.ui.ui').focus_output()
67+
local windows = state.windows or {}
68+
local win = windows.output_win
69+
local buf = windows.output_buf
70+
71+
if not win or not buf then
72+
return
73+
end
74+
75+
-- Mirror `gg` in output_window.setup_keymaps: under lazy render the target
76+
-- message may not yet have a line_start, so force a full render first.
77+
renderer.load_all_messages()
78+
79+
local current_line = vim.api.nvim_win_get_cursor(win)[1]
80+
local next_message = renderer.get_next_user_message(current_line)
81+
if next_message and next_message.line_start then
82+
vim.api.nvim_win_set_cursor(win, { next_message.line_start + 1, 0 })
83+
return
84+
end
85+
86+
vim.notify('No next user message', vim.log.levels.INFO)
87+
end
88+
89+
function M.goto_prev_user_message()
90+
require('opencode.ui.ui').focus_output()
91+
local windows = state.windows or {}
92+
local win = windows.output_win
93+
local buf = windows.output_buf
94+
95+
if not win or not buf then
96+
return
97+
end
98+
99+
renderer.load_all_messages()
100+
101+
local current_line = vim.api.nvim_win_get_cursor(win)[1]
102+
local previous_message = renderer.get_prev_user_message(current_line)
103+
if previous_message and previous_message.line_start then
104+
vim.api.nvim_win_set_cursor(win, { previous_message.line_start + 1, 0 })
105+
return
106+
end
107+
108+
vim.notify('No previous user message', vim.log.levels.INFO)
109+
end
110+
65111
---@param raw string
66112
local function resolve_path(raw)
67113
if vim.uv.fs_stat(raw) then

lua/opencode/ui/renderer.lua

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -625,4 +625,35 @@ function M.get_prev_rendered_message(current_line)
625625
return nil
626626
end
627627

628+
---@param current_line integer
629+
---@return RenderedMessage|nil
630+
function M.get_next_user_message(current_line)
631+
for _, message in ipairs(state.messages or {}) do
632+
if message.info and message.info.role == 'user' then
633+
local rendered = message.info.id and ctx.render_state:get_message(message.info.id) or nil
634+
if rendered and rendered.line_start and rendered.line_start + 1 > current_line then
635+
return rendered
636+
end
637+
end
638+
end
639+
640+
return nil
641+
end
642+
643+
---@param current_line integer
644+
---@return RenderedMessage|nil
645+
function M.get_prev_user_message(current_line)
646+
for i = #(state.messages or {}), 1, -1 do
647+
local message = state.messages[i]
648+
if message and message.info and message.info.role == 'user' then
649+
local rendered = message.info.id and ctx.render_state:get_message(message.info.id)
650+
if rendered and rendered.line_start and rendered.line_start + 1 < current_line then
651+
return rendered
652+
end
653+
end
654+
end
655+
656+
return nil
657+
end
658+
628659
return M

0 commit comments

Comments
 (0)