Skip to content
Merged
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
104 changes: 55 additions & 49 deletions README.md

Large diffs are not rendered by default.

63 changes: 63 additions & 0 deletions lua/opencode/api.lua
Original file line number Diff line number Diff line change
Expand Up @@ -680,6 +680,63 @@ function M.undo(messageId)
end)
end

function M.timeline()
local user_messages = {}
for _, msg in ipairs(state.messages or {}) do
local parts = msg.parts or {}
local is_summary = #parts == 1 and parts[1].synthetic == true
if msg.info.role == 'user' and not is_summary then
table.insert(user_messages, msg)
end
end
if #user_messages == 0 then
vim.notify('No user messages in the current session', vim.log.levels.WARN)
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)
end

--- Forks the current session from a specific user message.
---@param message_id? string The ID of the user message to fork from. If not provided, uses the last user message.
function M.fork_session(message_id)
if not state.active_session then
vim.notify('No active session to fork', vim.log.levels.WARN)
return
end

local message_to_fork = message_id or state.last_user_message and state.last_user_message.info.id
if not message_to_fork then
vim.notify('No user message to fork from', vim.log.levels.WARN)
return
end

state.api_client
:fork_session(state.active_session.id, {
messageID = message_to_fork,
})
:and_then(function(response)
vim.schedule(function()
if response and response.id then
vim.notify('Session forked successfully. New session ID: ' .. response.id, vim.log.levels.INFO)
core.switch_session(response.id)
else
vim.notify('Session forked but no new session ID received', vim.log.levels.WARN)
end
end)
end)
:catch(function(err)
vim.schedule(function()
vim.notify('Failed to fork session: ' .. vim.inspect(err), vim.log.levels.ERROR)
end)
end)
end

-- Returns the ID of the next user message after the current undo point
-- This is a port of the opencode tui logic
-- https://github.com/sst/opencode/blob/dev/packages/tui/internal/components/chat/messages.go#L1199
Expand Down Expand Up @@ -1074,6 +1131,11 @@ M.commands = {
end
end,
},

timeline = {
desc = 'Open timeline picker to navigate/undo/redo/fork to message',
fn = M.timeline,
},
}

M.slash_commands_map = {
Expand All @@ -1089,6 +1151,7 @@ M.slash_commands_map = {
['/redo'] = { fn = M.redo, desc = 'Redo last action' },
['/sessions'] = { fn = M.select_session, desc = 'Select session' },
['/share'] = { fn = M.share, desc = 'Share current session' },
['/timeline'] = { fn = M.timeline, desc = 'Open timeline picker' },
['/undo'] = { fn = M.undo, desc = 'Undo last action' },
['/unshare'] = { fn = M.unshare, desc = 'Unshare current session' },
}
Expand Down
9 changes: 9 additions & 0 deletions lua/opencode/api_client.lua
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,15 @@ function OpencodeApiClient:summarize_session(id, summary_data, directory)
return self:_call('/session/' .. id .. '/summarize', 'POST', summary_data, { directory = directory })
end

--- Fork an existing session at a specific message
--- @param id string Session ID (required)
--- @param fork_data {messageID?: string}|nil Fork data
--- @param directory string|nil Directory path
--- @return Promise<Session>
function OpencodeApiClient:fork_session(id, fork_data, directory)
return self:_call('/session/' .. id .. '/fork', 'POST', fork_data, { directory = directory })
end

-- Message endpoints

--- List messages for a session
Expand Down
7 changes: 7 additions & 0 deletions lua/opencode/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ M.defaults = {
['<leader>oI'] = { 'open_input_new_session', desc = 'Open input (new session)' },
['<leader>oo'] = { 'open_output', desc = 'Open output window' },
['<leader>ot'] = { 'toggle_focus', desc = 'Toggle focus' },
['<leader>oT'] = { 'timeline', desc = 'Session timeline' },
['<leader>oq'] = { 'close', desc = 'Close Opencode window' },
['<leader>os'] = { 'select_session', desc = 'Select session' },
['<leader>op'] = { 'configure_provider', desc = 'Configure provider' },
Expand Down Expand Up @@ -75,12 +76,17 @@ M.defaults = {
delete_session = { '<C-d>' },
new_session = { '<C-n>' },
},
timeline_picker = {
undo = { '<C-u>', mode = { 'i', 'n' } },
fork = { '<C-f>', mode = { 'i', 'n' } },
},
},
ui = {
position = 'right',
input_position = 'bottom',
window_width = 0.40,
input_height = 0.15,
picker_width = 100,
display_model = true,
display_context_size = true,
display_cost = true,
Expand Down Expand Up @@ -172,6 +178,7 @@ M.defaults = {
debug = {
enabled = false,
capture_streamed_events = false,
show_ids = false,
},
prompt_guard = nil,
}
Expand Down
27 changes: 15 additions & 12 deletions lua/opencode/core.lua
Original file line number Diff line number Diff line change
Expand Up @@ -25,21 +25,24 @@ function M.select_session(parent_id)
end
return
end
-- clear the model so it can be set by the session. If it doesn't get set
-- then core.get_model() will reset it to the default
state.current_model = nil
state.active_session = selected_session
if state.windows then
state.restore_points = {}
-- Don't need to update either renderer because they subscribe to
-- session changes
ui.focus_input()
else
M.open()
end
M.switch_session(selected_session.id)
end)
end

function M.switch_session(session_id)
local selected_session = session.get_by_id(session_id)
-- clear the model so it can be set by the session. If it doesn't get set
-- then core.get_model() will reset it to the default
state.current_model = nil
state.active_session = selected_session
if state.windows then
state.restore_points = {}
ui.focus_input()
else
M.open()
end
end

---@param opts? OpenOpts
function M.open(opts)
opts = opts or { focus = 'input', new_session = false }
Expand Down
2 changes: 1 addition & 1 deletion lua/opencode/git_review.lua
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,7 @@ function M.with_restore_point(restore_point_id, fn)
item.files and #item.files or 0,
item.deleted_files and #item.deleted_files or 0,
item.id:sub(1, 8),
utils.time_ago(item.created_at) or 'unknown',
utils.format_time(item.created_at) or 'unknown',
item.from_snapshot_id and item.from_snapshot_id:sub(1, 8) or 'none'
)
end,
Expand Down
1 change: 1 addition & 0 deletions lua/opencode/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ function M.setup(opts)
local config = require('opencode.config')
config.setup(opts)

require('opencode.ui.highlight').setup()
require('opencode.core').setup()
require('opencode.api').setup()
require('opencode.keymap').setup(config.keymap)
Expand Down
7 changes: 7 additions & 0 deletions lua/opencode/types.lua
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,16 @@
---@field output_window OpencodeKeymapOutputWindow
---@field permission OpencodeKeymapPermission
---@field session_picker OpencodeSessionPickerKeymap
---@field timeline_picker OpencodeTimelinePickerKeymap

---@class OpencodeSessionPickerKeymap
---@field delete_session OpencodeKeymapEntry
---@field new_session OpencodeKeymapEntry

---@class OpencodeTimelinePickerKeymap
---@field undo OpencodeKeymapEntry
---@field fork OpencodeKeymapEntry

---@class OpencodeCompletionFileSourcesConfig
---@field enabled boolean
---@field preferred_cli_tool 'server'|'fd'|'fdfind'|'rg'|'git'
Expand All @@ -95,6 +100,7 @@
---@field input_position 'bottom'|'top' # Position of the input window (default: 'bottom')
---@field window_width number
---@field input_height number
---@field picker_width number|nil # Default width for all pickers (nil uses current window width)
---@field display_model boolean
---@field display_context_size boolean
---@field display_cost boolean
Expand Down Expand Up @@ -126,6 +132,7 @@
---@class OpencodeDebugConfig
---@field enabled boolean
---@field capture_streamed_events boolean
---@field show_ids boolean

--- @class OpencodeProviders
--- @field [string] string[]
Expand Down
Loading