Skip to content

Commit 1c136a0

Browse files
committed
fix(context-actions): fix off by one context actions
There was 2 off by one errors when adding contextual actions When adding them in snapshots When refreshing them after a part update A couple of test needed to be regenerated for it to work I added the possibility to regenerate a test snapshot by it's name or file-name
1 parent e31ce17 commit 1c136a0

11 files changed

+112
-78
lines changed

lua/opencode/ui/formatter.lua

Lines changed: 27 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -154,66 +154,45 @@ function M._format_revert_message(session_data, start_idx)
154154
return output
155155
end
156156

157+
local function add_action(output, text, action_type, args, key, line)
158+
line = (line or output:get_line_count()) - 1
159+
output:add_action({
160+
text = text,
161+
type = action_type,
162+
args = args,
163+
key = key,
164+
display_line = line,
165+
range = { from = line, to = line },
166+
})
167+
end
168+
157169
---@param output Output Output object to write to
158170
---@param part OpencodeMessagePart
159171
function M._format_patch(output, part)
172+
if not part.hash then
173+
return
174+
end
175+
160176
local restore_points = snapshot.get_restore_points_by_parent(part.hash) or {}
161177
M._format_action(output, icons.get('snapshot') .. ' Created Snapshot', vim.trim(part.hash:sub(1, 8)))
162-
local snapshot_header_line = output:get_line_count()
163178

164179
-- Anchor all snapshot-level actions to the snapshot header line
165-
output:add_action({
166-
text = '[R]evert file',
167-
type = 'diff_revert_selected_file',
168-
args = { part.hash },
169-
key = 'R',
170-
display_line = snapshot_header_line,
171-
range = { from = snapshot_header_line, to = snapshot_header_line },
172-
})
173-
output:add_action({
174-
text = 'Revert [A]ll',
175-
type = 'diff_revert_all',
176-
args = { part.hash },
177-
key = 'A',
178-
display_line = snapshot_header_line,
179-
range = { from = snapshot_header_line, to = snapshot_header_line },
180-
})
181-
output:add_action({
182-
text = '[D]iff',
183-
type = 'diff_open',
184-
args = { part.hash },
185-
key = 'D',
186-
display_line = snapshot_header_line,
187-
range = { from = snapshot_header_line, to = snapshot_header_line },
188-
})
180+
add_action(output, '[R]evert file', 'diff_revert_selected_file', { part.hash }, 'R')
181+
add_action(output, 'Revert [A]ll', 'diff_revert_all', { part.hash }, 'A')
182+
add_action(output, '[D]iff', 'diff_open', { part.hash }, 'D')
189183

190184
if #restore_points > 0 then
191185
for _, restore_point in ipairs(restore_points) do
192186
output:add_line(
193187
string.format(
194-
' %s Restore point `%s` - %s',
188+
' %s Restore point `%s` - %s ',
195189
icons.get('restore_point'),
196-
restore_point.id:sub(1, 8),
190+
vim.trim(restore_point.id:sub(1, 8)),
197191
util.format_time(restore_point.created_at)
198192
)
199193
)
200-
local restore_line = output:get_line_count()
201-
output:add_action({
202-
text = 'Restore [A]ll',
203-
type = 'diff_restore_snapshot_all',
204-
args = { restore_point.id },
205-
key = 'A',
206-
display_line = restore_line,
207-
range = { from = restore_line, to = restore_line },
208-
})
209-
output:add_action({
210-
text = '[R]estore file',
211-
type = 'diff_restore_snapshot_file',
212-
args = { restore_point.id },
213-
key = 'R',
214-
display_line = restore_line,
215-
range = { from = restore_line, to = restore_line },
216-
})
194+
add_action(output, 'Restore [A]ll', 'diff_restore_snapshot_all', { restore_point.id }, 'A')
195+
add_action(output, '[R]estore file', 'diff_restore_snapshot_file', { restore_point.id }, 'R')
217196
end
218197
end
219198
end
@@ -282,10 +261,10 @@ function M.format_message_header(message)
282261
and (not message.parts or #message.parts == 0)
283262
then
284263
local error = message.info.error
285-
local error_messgage = error.data and error.data.message or vim.inspect(error)
264+
local error_message = error.data and error.data.message or vim.inspect(error)
286265

287266
output:add_line('')
288-
M._format_callout(output, 'ERROR', error_messgage)
267+
M._format_callout(output, 'ERROR', error_message)
289268
end
290269

291270
output:add_line('')
@@ -797,8 +776,8 @@ function M.format_part(part, message, is_last_part)
797776

798777
if is_last_part and role == 'assistant' and message.info.error and message.info.error ~= '' then
799778
local error = message.info.error
800-
local error_messgage = error.data and error.data.message or vim.inspect(error)
801-
M._format_callout(output, 'ERROR', error_messgage)
779+
local error_message = error.data and error.data.message or vim.inspect(error)
780+
M._format_callout(output, 'ERROR', error_message)
802781
output:add_empty_line()
803782
end
804783

lua/opencode/ui/render_state.lua

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,8 @@ end
7777
---@param snapshot_id string Call ID
7878
---@return OpencodeMessagePart? part Part if found
7979
function RenderState:get_part_by_snapshot_id(snapshot_id)
80-
for _, rendered_message in pairs(self._messages) do
81-
for _, part in ipairs(rendered_message.message.parts) do
80+
for _, rendered_message in pairs(self._messages or {}) do
81+
for _, part in ipairs(rendered_message.message.parts or {}) do
8282
if part.type == 'patch' and part.hash == snapshot_id then
8383
return part
8484
end

lua/opencode/ui/renderer.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -413,7 +413,7 @@ function M._replace_part_in_buffer(part_id, formatted_data)
413413
output_window.set_extmarks(formatted_data.extmarks, cached.line_start)
414414

415415
if formatted_data.actions then
416-
M._render_state:add_actions(part_id, formatted_data.actions, cached.line_start)
416+
M._render_state:add_actions(part_id, formatted_data.actions, cached.line_start + 1)
417417
end
418418

419419
M._render_state:update_part_lines(part_id, cached.line_start, new_line_end)

tests/data/diagnostics.expected.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

tests/data/diff.expected.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"extmarks":[[1,1,0,{"virt_text_pos":"win_col","virt_text_hide":false,"virt_text_win_col":-3,"ns_id":3,"virt_text_repeat_linebreak":false,"priority":10,"right_gravity":true,"virt_text":[["▌󰭻 ","OpencodeMessageRoleUser"],[" "],["USER","OpencodeMessageRoleUser"],["","OpencodeHint"],[" (2025-10-12 06:42:56)","OpencodeHint"],[" [msg_9d7287269001C5gRusYfX7A1w1]","OpencodeHint"]]}],[2,2,0,{"virt_text_pos":"win_col","virt_text_hide":false,"virt_text_win_col":-3,"ns_id":3,"virt_text_repeat_linebreak":true,"priority":4096,"right_gravity":true,"virt_text":[["▌","OpencodeMessageRoleUser"]]}],[3,3,0,{"virt_text_pos":"win_col","virt_text_hide":false,"virt_text_win_col":-3,"ns_id":3,"virt_text_repeat_linebreak":true,"priority":4096,"right_gravity":true,"virt_text":[["▌","OpencodeMessageRoleUser"]]}],[4,3,39,{"right_gravity":true,"end_row":3,"end_col":53,"hl_group":"OpencodeMention","ns_id":3,"priority":1000,"hl_eol":false,"end_right_gravity":false}],[5,4,0,{"virt_text_pos":"win_col","virt_text_hide":false,"virt_text_win_col":-3,"ns_id":3,"virt_text_repeat_linebreak":true,"priority":4096,"right_gravity":true,"virt_text":[["▌","OpencodeMessageRoleUser"]]}],[6,5,0,{"virt_text_pos":"win_col","virt_text_hide":false,"virt_text_win_col":-3,"ns_id":3,"virt_text_repeat_linebreak":true,"priority":4096,"right_gravity":true,"virt_text":[["▌","OpencodeMessageRoleUser"]]}],[7,8,0,{"virt_text_pos":"win_col","virt_text_hide":false,"virt_text_win_col":-3,"ns_id":3,"virt_text_repeat_linebreak":false,"priority":10,"right_gravity":true,"virt_text":[[" ","OpencodeMessageRoleAssistant"],[" "],["BUILD","OpencodeMessageRoleAssistant"],[" claude-sonnet-4.5","OpencodeHint"],[" (2025-10-12 06:42:56)","OpencodeHint"],[" [msg_9d7287287001HVwpPaH7WkRVdN]","OpencodeHint"]]}],[8,10,0,{"virt_text_pos":"win_col","virt_text_hide":false,"virt_text_win_col":-1,"ns_id":3,"virt_text_repeat_linebreak":true,"priority":4096,"right_gravity":true,"virt_text":[["▌","OpencodeToolBorder"]]}],[9,11,0,{"virt_text_pos":"win_col","virt_text_hide":false,"virt_text_win_col":-1,"ns_id":3,"virt_text_repeat_linebreak":true,"priority":4096,"right_gravity":true,"virt_text":[["▌","OpencodeToolBorder"]]}],[10,12,0,{"virt_text_pos":"win_col","virt_text_hide":false,"virt_text_win_col":-1,"ns_id":3,"virt_text_repeat_linebreak":true,"priority":4096,"right_gravity":true,"virt_text":[["▌","OpencodeToolBorder"]]}],[11,13,0,{"virt_text_pos":"overlay","virt_text_hide":false,"end_col":0,"ns_id":3,"end_right_gravity":false,"virt_text_repeat_linebreak":false,"end_row":14,"hl_group":"OpencodeDiffDelete","right_gravity":true,"priority":5000,"hl_eol":true,"virt_text":[["-","OpencodeDiffDelete"]]}],[12,13,0,{"virt_text_pos":"win_col","virt_text_hide":false,"virt_text_win_col":-1,"ns_id":3,"virt_text_repeat_linebreak":true,"priority":4096,"right_gravity":true,"virt_text":[["▌","OpencodeToolBorder"]]}],[13,14,0,{"virt_text_pos":"overlay","virt_text_hide":false,"end_col":0,"ns_id":3,"end_right_gravity":false,"virt_text_repeat_linebreak":false,"end_row":15,"hl_group":"OpencodeDiffAdd","right_gravity":true,"priority":5000,"hl_eol":true,"virt_text":[["+","OpencodeDiffAdd"]]}],[14,14,0,{"virt_text_pos":"win_col","virt_text_hide":false,"virt_text_win_col":-1,"ns_id":3,"virt_text_repeat_linebreak":true,"priority":4096,"right_gravity":true,"virt_text":[["▌","OpencodeToolBorder"]]}],[15,15,0,{"virt_text_pos":"win_col","virt_text_hide":false,"virt_text_win_col":-1,"ns_id":3,"virt_text_repeat_linebreak":true,"priority":4096,"right_gravity":true,"virt_text":[["▌","OpencodeToolBorder"]]}],[16,16,0,{"virt_text_pos":"win_col","virt_text_hide":false,"virt_text_win_col":-1,"ns_id":3,"virt_text_repeat_linebreak":true,"priority":4096,"right_gravity":true,"virt_text":[["▌","OpencodeToolBorder"]]}],[17,21,0,{"virt_text_pos":"win_col","virt_text_hide":false,"virt_text_win_col":-3,"ns_id":3,"virt_text_repeat_linebreak":false,"priority":10,"right_gravity":true,"virt_text":[[" ","OpencodeMessageRoleAssistant"],[" "],["BUILD","OpencodeMessageRoleAssistant"],[" claude-sonnet-4.5","OpencodeHint"],[" (2025-10-12 06:43:03)","OpencodeHint"],[" [msg_9d7288f2f001hW6NqqhtBc72UU]","OpencodeHint"]]}]],"timestamp":1762908306,"lines":["----","","","can you add \"great\" before \"string\" in @diff-test.txt?","","[diff-test.txt](diff-test.txt)","","----","","","** edit** `diff-test.txt`","","`````txt"," this is a string"," this is a great string","","`````","","**󰻛 Created Snapshot** `1f593f7e`","","----","","",""],"actions":[{"key":"R","text":"[R]evert file","display_line":20,"args":["1f593f7ed419c95d3995f8ef4b98d4e571c3a492"],"range":{"from":20,"to":20},"type":"diff_revert_selected_file"},{"key":"A","text":"Revert [A]ll","display_line":20,"args":["1f593f7ed419c95d3995f8ef4b98d4e571c3a492"],"range":{"from":20,"to":20},"type":"diff_revert_all"},{"key":"D","text":"[D]iff","display_line":20,"args":["1f593f7ed419c95d3995f8ef4b98d4e571c3a492"],"range":{"from":20,"to":20},"type":"diff_open"}]}
1+
{"actions":[{"range":{"from":19,"to":19},"args":["1f593f7ed419c95d3995f8ef4b98d4e571c3a492"],"type":"diff_revert_selected_file","text":"[R]evert file","display_line":19,"key":"R"},{"range":{"from":19,"to":19},"args":["1f593f7ed419c95d3995f8ef4b98d4e571c3a492"],"type":"diff_revert_all","text":"Revert [A]ll","display_line":19,"key":"A"},{"range":{"from":19,"to":19},"args":["1f593f7ed419c95d3995f8ef4b98d4e571c3a492"],"type":"diff_open","text":"[D]iff","display_line":19,"key":"D"}],"timestamp":1763482495,"lines":["----","","","can you add \"great\" before \"string\" in @diff-test.txt?","","[diff-test.txt](diff-test.txt)","","----","","","** edit** `diff-test.txt`","","`````txt"," this is a string"," this is a great string","","`````","","**󰻛 Created Snapshot** `1f593f7e`","","----","","",""],"extmarks":[[1,1,0,{"ns_id":3,"right_gravity":true,"virt_text_pos":"win_col","virt_text_hide":false,"priority":10,"virt_text_repeat_linebreak":false,"virt_text":[["▌󰭻 ","OpencodeMessageRoleUser"],[" "],["USER","OpencodeMessageRoleUser"],["","OpencodeHint"],[" (2025-10-12 06:42:56)","OpencodeHint"],[" [msg_9d7287269001C5gRusYfX7A1w1]","OpencodeHint"]],"virt_text_win_col":-3}],[2,2,0,{"ns_id":3,"right_gravity":true,"virt_text_pos":"win_col","virt_text_hide":false,"priority":4096,"virt_text_repeat_linebreak":true,"virt_text":[["▌","OpencodeMessageRoleUser"]],"virt_text_win_col":-3}],[3,3,0,{"ns_id":3,"right_gravity":true,"virt_text_pos":"win_col","virt_text_hide":false,"priority":4096,"virt_text_repeat_linebreak":true,"virt_text":[["▌","OpencodeMessageRoleUser"]],"virt_text_win_col":-3}],[4,3,39,{"ns_id":3,"right_gravity":true,"end_col":53,"end_row":3,"priority":1000,"hl_eol":false,"end_right_gravity":false,"hl_group":"OpencodeMention"}],[5,4,0,{"ns_id":3,"right_gravity":true,"virt_text_pos":"win_col","virt_text_hide":false,"priority":4096,"virt_text_repeat_linebreak":true,"virt_text":[["▌","OpencodeMessageRoleUser"]],"virt_text_win_col":-3}],[6,5,0,{"ns_id":3,"right_gravity":true,"virt_text_pos":"win_col","virt_text_hide":false,"priority":4096,"virt_text_repeat_linebreak":true,"virt_text":[["▌","OpencodeMessageRoleUser"]],"virt_text_win_col":-3}],[7,8,0,{"ns_id":3,"right_gravity":true,"virt_text_pos":"win_col","virt_text_hide":false,"priority":10,"virt_text_repeat_linebreak":false,"virt_text":[[" ","OpencodeMessageRoleAssistant"],[" "],["BUILD","OpencodeMessageRoleAssistant"],[" claude-sonnet-4.5","OpencodeHint"],[" (2025-10-12 06:42:56)","OpencodeHint"],[" [msg_9d7287287001HVwpPaH7WkRVdN]","OpencodeHint"]],"virt_text_win_col":-3}],[8,10,0,{"ns_id":3,"right_gravity":true,"virt_text_pos":"win_col","virt_text_hide":false,"priority":4096,"virt_text_repeat_linebreak":true,"virt_text":[["▌","OpencodeToolBorder"]],"virt_text_win_col":-1}],[9,11,0,{"ns_id":3,"right_gravity":true,"virt_text_pos":"win_col","virt_text_hide":false,"priority":4096,"virt_text_repeat_linebreak":true,"virt_text":[["▌","OpencodeToolBorder"]],"virt_text_win_col":-1}],[10,12,0,{"ns_id":3,"right_gravity":true,"virt_text_pos":"win_col","virt_text_hide":false,"priority":4096,"virt_text_repeat_linebreak":true,"virt_text":[["▌","OpencodeToolBorder"]],"virt_text_win_col":-1}],[11,13,0,{"hl_eol":true,"right_gravity":true,"end_col":0,"end_row":14,"virt_text_repeat_linebreak":false,"hl_group":"OpencodeDiffDelete","ns_id":3,"virt_text":[["-","OpencodeDiffDelete"]],"virt_text_pos":"overlay","priority":5000,"end_right_gravity":false,"virt_text_hide":false}],[12,13,0,{"ns_id":3,"right_gravity":true,"virt_text_pos":"win_col","virt_text_hide":false,"priority":4096,"virt_text_repeat_linebreak":true,"virt_text":[["▌","OpencodeToolBorder"]],"virt_text_win_col":-1}],[13,14,0,{"hl_eol":true,"right_gravity":true,"end_col":0,"end_row":15,"virt_text_repeat_linebreak":false,"hl_group":"OpencodeDiffAdd","ns_id":3,"virt_text":[["+","OpencodeDiffAdd"]],"virt_text_pos":"overlay","priority":5000,"end_right_gravity":false,"virt_text_hide":false}],[14,14,0,{"ns_id":3,"right_gravity":true,"virt_text_pos":"win_col","virt_text_hide":false,"priority":4096,"virt_text_repeat_linebreak":true,"virt_text":[["▌","OpencodeToolBorder"]],"virt_text_win_col":-1}],[15,15,0,{"ns_id":3,"right_gravity":true,"virt_text_pos":"win_col","virt_text_hide":false,"priority":4096,"virt_text_repeat_linebreak":true,"virt_text":[["▌","OpencodeToolBorder"]],"virt_text_win_col":-1}],[16,16,0,{"ns_id":3,"right_gravity":true,"virt_text_pos":"win_col","virt_text_hide":false,"priority":4096,"virt_text_repeat_linebreak":true,"virt_text":[["▌","OpencodeToolBorder"]],"virt_text_win_col":-1}],[17,21,0,{"ns_id":3,"right_gravity":true,"virt_text_pos":"win_col","virt_text_hide":false,"priority":10,"virt_text_repeat_linebreak":false,"virt_text":[[" ","OpencodeMessageRoleAssistant"],[" "],["BUILD","OpencodeMessageRoleAssistant"],[" claude-sonnet-4.5","OpencodeHint"],[" (2025-10-12 06:43:03)","OpencodeHint"],[" [msg_9d7288f2f001hW6NqqhtBc72UU]","OpencodeHint"]],"virt_text_win_col":-3}]]}

0 commit comments

Comments
 (0)