Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,10 +206,6 @@ require('opencode').setup({
undo = { '<C-u>', mode = { 'i', 'n' } }, -- Undo to selected message in timeline picker
fork = { '<C-f>', mode = { 'i', 'n' } }, -- Fork from selected message in timeline picker
},
history_picker = {
delete_entry = { '<C-d>', mode = { 'i', 'n' } }, -- Delete selected entry in the history picker
clear_all = { '<C-X>', mode = { 'i', 'n' } }, -- Clear all entries in the history picker
},
model_picker = {
toggle_favorite = { '<C-f>', mode = { 'i', 'n' } },
},
Expand Down
46 changes: 34 additions & 12 deletions lua/opencode/commands/handlers/session.lua
Original file line number Diff line number Diff line change
Expand Up @@ -418,12 +418,33 @@ function M.actions.undo(messageId)
return
end

run_api_action_with_checktime(
state_obj.api_client:revert_message(state_obj.active_session.id, {
messageID = message_to_revert,
}),
'Failed to undo last message: '
)
-- Snapshot the message now; state may drop it once the revert lands.
local refill_message = nil
for _, msg in ipairs(state_obj.messages or {}) do
if msg.info and msg.info.id == message_to_revert then
refill_message = msg
break
end
end
if not refill_message and state_obj.last_user_message and state_obj.last_user_message.info.id == message_to_revert then
refill_message = state_obj.last_user_message
end

local revert_promise = state_obj.api_client:revert_message(state_obj.active_session.id, {
messageID = message_to_revert,
})

revert_promise:and_then(function()
schedule_checktime()
if refill_message then
local input_window = require('opencode.ui.input_window')
if input_window.refill_prompt_from_message(refill_message) then
input_window.focus_input()
end
end
end):catch(function(err)
notify_error('Failed to undo last message: ', err)
end)
end)
end

Expand Down Expand Up @@ -515,12 +536,13 @@ function M.actions.timeline()
return
end

local timeline_picker = require('opencode.ui.timeline_picker')
timeline_picker.pick(user_messages, function(selected_msg)
if selected_msg then
require('opencode.ui.navigation').goto_message_by_id(selected_msg.info.id)
end
end)
require('opencode.ui.timeline_picker').pick(user_messages, {
callback = function(selected_msg)
if selected_msg then
require('opencode.ui.navigation').goto_message_by_id(selected_msg.info.id)
end
end,
})
end

---@param message_id? string
Expand Down
113 changes: 75 additions & 38 deletions lua/opencode/commands/handlers/workflow.lua
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,6 @@ local function get_active_session_or_warn(message)
return active_session
end

---@param command string
---@param args string[]|nil
local function schedule_slash_history(command, args)
local joined_args = args and table.concat(args, ' ') or ''
vim.schedule(function()
history.write('/' .. command .. ' ' .. joined_args)
end)
end

---@param args string[]|nil
---@return string
local function join_args(args)
Expand Down Expand Up @@ -115,36 +106,90 @@ function M.actions.quick_chat(message, range)
quick_chat.quick_chat(prompt, { context_config = ctx }, range)
end

---Refill the input buffer from a user message returned by the timeline picker.
---Open the opencode UI first when the input window is not mounted yet.
---@param message OpencodeMessage
local function refill_input_from_message(message)
local windows = state.windows
if not input_window.mounted(windows) then
session_runtime.open({ focus = 'input' })
windows = state.windows
end
if not input_window.mounted(windows) then
return
end
---@cast windows { input_win: integer, input_buf: integer }
if input_window.refill_prompt_from_message(message) then
input_window.focus_input()
end
end

---@param entries OpencodeHistoryEntry[]
local function pick_history_with(entries)
local messages = vim.tbl_map(function(entry)
return entry.message
end, entries)
require('opencode.ui.timeline_picker').pick(messages, {
title = 'Select History Entry',
callback = function(selected_msg)
if selected_msg then
refill_input_from_message(selected_msg)
end
end,
})
end

function M.actions.select_history()
require('opencode.ui.history_picker').pick()
local entries = history.read()
if #entries == 0 then
vim.notify('No history entries found', vim.log.levels.INFO)
return false
end
return pick_history_with(entries)
end

---@param prompt string|nil
local function restore_prompt_history_entry(prompt)
if not prompt then
---Refill the input buffer from a history entry reconstructed from the active
---session's user messages
---@param entry OpencodeHistoryEntry
local function restore_prompt_history_entry(entry)
local windows = state.windows
if not windows or not windows.input_buf or not input_window.mounted(windows) then
return
end
if input_window.refill_prompt_from_message(entry.message) then
input_window.focus_input()
end
end

input_window.set_content(prompt)
require('opencode.ui.mention').restore_mentions(state.windows.input_buf)
---@param draft string[]
local function restore_prompt_draft(draft)
if input_window.replace_input(draft) then
input_window.focus_input()
end
end

function M.actions.prev_history()
if not state.ui.is_visible() then
return
end

local prev_prompt = history.prev()
restore_prompt_history_entry(prev_prompt)
local entry = history.prev()
if entry then
restore_prompt_history_entry(entry)
end
end

function M.actions.next_history()
if not state.ui.is_visible() then
return
end

local next_prompt = history.next()
restore_prompt_history_entry(next_prompt)
local result = history.next()
if result and result.message then
restore_prompt_history_entry(result)
elseif result then
restore_prompt_draft(result)
end
end

function M.actions.prev_prompt_history()
Expand Down Expand Up @@ -256,16 +301,12 @@ M.actions.run_user_command = Promise.async(function(name, args)
return
end

state.api_client
:send_command(active_session.id, {
command = name,
arguments = join_args(args),
model = model,
agent = agent,
})
:and_then(function()
schedule_slash_history(name, args)
end)
state.api_client:send_command(active_session.id, {
command = name,
arguments = join_args(args),
model = model,
agent = agent,
})
end) --[[@as Promise<void> ]]
end)

Expand Down Expand Up @@ -355,15 +396,11 @@ M.actions.review = Promise.async(function(args)

state.session.set_active(new_session)
window_handler.actions.open_input():await()
state.api_client
:send_command(state.active_session.id, {
command = 'review',
arguments = join_args(args),
model = state.current_model,
})
:and_then(function()
schedule_slash_history('review', args)
end)
state.api_client:send_command(state.active_session.id, {
command = 'review',
arguments = join_args(args),
model = state.current_model,
})
end)

M.actions.add_visual_selection = Promise.async(
Expand Down
4 changes: 0 additions & 4 deletions lua/opencode/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -120,10 +120,6 @@ M.defaults = {
undo = { '<C-u>', mode = { 'i', 'n' }, desc = 'Undo to selected message' },
fork = { '<C-f>', mode = { 'i', 'n' }, desc = 'Fork from selected message' },
},
history_picker = {
delete_entry = { '<C-d>', mode = { 'i', 'n' }, desc = 'Delete selected history entries' },
clear_all = { '<C-X>', mode = { 'i', 'n' }, desc = 'Clear all history entries' },
},
model_picker = {
toggle_favorite = { '<C-f>', mode = { 'i', 'n' }, desc = 'Toggle model favorite' },
},
Expand Down
Loading
Loading