local core = require('opencode.core')
local util = require('opencode.util')
local session = require('opencode.session')
local input_window = require('opencode.ui.input_window')
local ui = require('opencode.ui.ui')
local icons = require('opencode.ui.icons')
local state = require('opencode.state')
local git_review = require('opencode.git_review')
local history = require('opencode.history')
local id = require('opencode.id')
local M = {}
function M.swap_position()
require('opencode.ui.ui').swap_position()
end
function M.open_input()
core.open({ new_session = false, focus = 'input' })
vim.cmd('startinsert')
end
function M.open_input_new_session()
core.open({ new_session = true, focus = 'input' })
vim.cmd('startinsert')
end
function M.open_output()
core.open({ new_session = false, focus = 'output' })
end
function M.close()
if state.display_route then
state.display_route = nil
ui.clear_output()
ui.render_output()
ui.scroll_to_bottom()
return
end
ui.close_windows(state.windows)
end
function M.toggle(new_session)
if state.windows == nil then
local focus = state.last_focused_opencode_window or 'input'
core.open({ new_session = new_session == true, focus = focus })
else
M.close()
end
end
function M.toggle_focus(new_session)
if not ui.is_opencode_focused() then
local focus = state.last_focused_opencode_window or 'input'
core.open({ new_session = new_session == true, focus = focus })
else
ui.return_to_last_code_win()
end
end
function M.configure_provider()
core.configure_provider()
end
function M.stop()
core.stop()
end
function M.run(prompt)
core.run(prompt, {
ensure_ui = true,
new_session = false,
focus = 'output',
})
end
function M.run_new_session(prompt)
core.run(prompt, {
ensure_ui = true,
new_session = true,
focus = 'output',
})
end
function M.select_session()
core.select_session()
end
function M.toggle_pane()
if not state.windows then
core.open({ new_session = false, focus = 'output' })
return
end
ui.toggle_pane()
end
---@param from_snapshot_id? string
---@param to_snapshot_id? string|number
function M.diff_open(from_snapshot_id, to_snapshot_id)
if not state.messages or not state.active_session then
core.open({ new_session = false, focus = 'output' })
end
git_review.review(from_snapshot_id)
end
function M.diff_next()
if not state.windows then
core.open({ new_session = false, focus = 'output' })
end
git_review.next_diff()
end
function M.diff_prev()
if not state.windows then
core.open({ new_session = false, focus = 'output' })
end
git_review.prev_diff()
end
function M.diff_close()
if not state.windows then
core.open({ new_session = false, focus = 'output' })
end
git_review.close_diff()
end
---@param from_snapshot_id? string
function M.diff_revert_all(from_snapshot_id)
if not state.windows then
core.open({ new_session = false, focus = 'output' })
end
git_review.revert_all(from_snapshot_id)
end
---@param from_snapshot_id? string
---@param to_snapshot_id? string
function M.diff_revert_selected_file(from_snapshot_id, to_snapshot_id)
if not state.windows then
core.open({ new_session = false, focus = 'output' })
end
git_review.revert_selected_file(from_snapshot_id)
end
---@param restore_point_id? string
function M.diff_restore_snapshot_file(restore_point_id)
if not state.windows then
core.open({ new_session = false, focus = 'output' })
end
git_review.restore_snapshot_file(restore_point_id)
end
---@param restore_point_id? string
function M.diff_restore_snapshot_all(restore_point_id)
if not state.windows then
core.open({ new_session = false, focus = 'output' })
end
git_review.restore_snapshot_all(restore_point_id)
end
function M.diff_revert_all_last_prompt()
if not state.windows then
core.open({ new_session = false, focus = 'output' })
end
local snapshots = session.get_message_snapshot_ids(state.current_message)
local snapshot_id = snapshots and snapshots[1]
if not snapshot_id then
vim.notify('No snapshots found for the current message', vim.log.levels.WARN)
return
end
git_review.revert_all(snapshot_id)
end
function M.diff_revert_this(snapshot_id)
if not state.windows then
core.open({ new_session = false, focus = 'output' })
end
git_review.revert_current(snapshot_id)
end
function M.diff_revert_this_last_prompt()
if not state.windows then
core.open({ new_session = false, focus = 'output' })
end
local snapshots = session.get_message_snapshot_ids(state.current_message)
local snapshot_id = snapshots and snapshots[1]
if not snapshot_id then
vim.notify('No snapshots found for the current message', vim.log.levels.WARN)
return
end
end
function M.set_review_breakpoint()
vim.notify('Setting review breakpoint is not implemented yet', vim.log.levels.WARN)
git_review.create_snapshot()
end
function M.prev_history()
local prev_prompt = history.prev()
if prev_prompt then
input_window.set_content(prev_prompt)
end
end
function M.next_history()
local next_prompt = history.next()
if next_prompt then
input_window.set_content(next_prompt)
end
end
---@param title string
---@param cb fun(session: Session)?
function M.create_new_session(title, cb)
core.run_server_api('/session', 'POST', { title = title }, {
on_error = function(err)
vim.notify(err, vim.log.levels.ERROR)
end,
on_done = function(data)
if data and data.id then
local new_session = session.get_by_name(data.id)
if new_session and cb then
cb(new_session)
return
end
else
vim.notify('Failed to create new session: Invalid response from server', vim.log.levels.ERROR)
end
end,
})
end
function M.initialize()
M.create_new_session('AGENTS.md Initialization', function(new_session)
local providerId, modelId = state.current_model:match('^(.-)/(.+)$')
if not providerId or not modelId then
vim.notify('Invalid model format: ' .. tostring(state.current_model), vim.log.levels.ERROR)
return
end
state.active_session = new_session
M.open_input()
core.run_server_api('/session/' .. state.active_session.name .. '/init', 'POST', {
providerID = providerId,
modelID = modelId,
messageID = id.ascending('message'),
})
end)
end
function M.open_configuration_file()
local config_path = require('opencode.config_file').config_file
if vim.fn.filereadable(config_path) == 1 then
if ui.is_opencode_focused() then
vim.api.nvim_set_current_win(state.last_code_win_before_opencode)
end
vim.cmd('edit ' .. config_path)
else
vim.notify('Configuration file not found: ' .. config_path, 'error')
end
end
function M.agent_plan()
state.current_mode = 'plan'
require('opencode.ui.topbar').render()
end
function M.agent_build()
state.current_mode = 'build'
require('opencode.ui.topbar').render()
end
function M.select_agent()
local modes = require('opencode.config_file').get_opencode_agents()
vim.ui.select(modes, {
prompt = 'Select mode:',
}, function(selection)
if not selection then
return
end
state.current_mode = selection
require('opencode.ui.topbar').render()
end)
end
function M.switch_to_next_mode()
local modes = require('opencode.config_file').get_opencode_agents()
local current_index = util.index_of(modes, state.current_mode)
if current_index == -1 then
current_index = 0
end
-- Calculate next index, wrapping around if necessary
local next_index = (current_index % #modes) + 1
state.current_mode = modes[next_index]
require('opencode.ui.topbar').render()
end
function M.with_header(lines, show_welcome)
show_welcome = show_welcome or show_welcome
state.display_route = '/header'
local msg = {
'## Opencode.nvim',
'',
' ââââ ââââ âââ ââââ âââ ââââ ââââ âââ',
' ââââ ââââ âââ ââââ âââ ââââ ââââ âââ',
' ââââ ââââ âââ â â âââ ââââ âââ âââ',
'',
}
if show_welcome then
table.insert(
msg,
'Welcome to Opencode.nvim! This plugin allows you to interact with AI models directly from Neovim.'
)
table.insert(msg, '')
end
for _, line in ipairs(lines) do
table.insert(msg, line)
end
return msg
end
function M.help()
state.display_route = '/help'
M.open_input()
local msg = M.with_header({
'### Available Commands',
'',
'| Command | Description |',
'|-----------|---------------------|',
}, false)
local max_desc_length = (vim.api.nvim_win_get_width(state.windows.output_win) / 2) - 5
for _, def in pairs(M.commands) do
local desc = def.desc or ''
if #desc > max_desc_length then
desc = desc:sub(1, max_desc_length - 3) .. '...'
end
table.insert(msg, string.format('| %-10s | %s |', def.name, desc))
end
table.insert(msg, '')
ui.render_lines(msg)
end
function M.mcp()
local info = require('opencode.config_file')
local mcp = info.get_mcp_servers()
if not mcp then
ui.notify('No MCP configuration found. Please check your opencode config file.', 'warn')
return
end
state.display_route = '/mcp'
M.open_input()
local msg = M.with_header({
'### Available MCP servers',
'',
'| Name | Type | cmd |',
'|--------|------|-----|',
})
for name, def in pairs(mcp) do
table.insert(
msg,
string.format(
'| %s %-10s | %s | %s |',
(def.enabled and icons.get('status_on') or icons.get('status_off')),
name,
def.type,
table.concat(def.command, ' ')
)
)
end
table.insert(msg, '')
ui.render_lines(msg)
end
function M.run_user_command(name)
M.open_input()
core.run_server_api('/session/' .. state.active_session.name .. '/command', 'POST', {
command = name,
arguments = '',
})
end
-- Command definitions that call the API functions
M.commands = {
swap_position = {
name = 'OpencodeSwapPosition',
desc = 'Swap Opencode pane left/right',
fn = function()
M.swap_position()
end,
},
toggle = {
name = 'Opencode',
desc = 'Open opencode. Close if opened',
fn = function()
M.toggle()
end,
},
toggle_focus = {
name = 'OpencodeToggleFocus',
desc = 'Toggle focus between opencode and last window',
fn = function()
M.toggle_focus()
end,
},
open_input = {
name = 'OpencodeOpenInput',
desc = 'Opens and focuses on input window on insert mode',
fn = function()
M.open_input()
end,
},
open_input_new_session = {
name = 'OpencodeOpenInputNewSession',
slash_cmd = '/new',
desc = 'Opens and focuses on input window on insert mode. Creates a new session',
fn = function()
M.open_input_new_session()
end,
},
open_output = {
name = 'OpencodeOpenOutput',
desc = 'Opens and focuses on output window',
fn = function()
M.open_output()
end,
},
create_new_session = {
name = 'OpencodeCreateNewSession',
desc = 'Create a new opencode session',
fn = function(opts)
local title = opts.args and opts.args:match('^%s*(.+)')
if title and title ~= '' then
M.create_new_session(title, function(new_session)
state.active_session = new_session
M.open_input()
end)
else
vim.notify('Session title cannot be empty', vim.log.levels.ERROR)
end
end,
args = true,
},
close = {
name = 'OpencodeClose',
desc = 'Close UI windows',
fn = function()
M.close()
end,
},
stop = {
name = 'OpencodeStop',
desc = 'Stop opencode while it is running',
fn = function()
M.stop()
end,
},
select_session = {
name = 'OpencodeSelectSession',
slash_cmd = '/sessions',
desc = 'Select and load a opencode session',
fn = function()
M.select_session()
end,
},
toggle_pane = {
name = 'OpencodeTogglePane',
desc = 'Toggle between input and output panes',
fn = function()
M.toggle_pane()
end,
},
configure_provider = {
name = 'OpencodeConfigureProvider',
slash_cmd = '/models',
desc = 'Quick provider and model switch from predefined list',
fn = function()
M.configure_provider()
end,
},
run = {
name = 'OpencodeRun',
desc = 'Run opencode with a prompt (continue last session)',
fn = function(opts)
M.run(opts.args)
end,
args = true,
},
run_new_session = {
name = 'OpencodeRunNewSession',
desc = 'Run opencode with a prompt (new session)',
fn = function(opts)
M.run_new_session(opts.args)
end,
args = true,
},
diff_open = {
name = 'OpencodeDiff',
desc = 'Opens a diff tab of a modified file since the last opencode prompt',
fn = function()
M.diff_open()
end,
},
diff_next = {
name = 'OpencodeDiffNext',
desc = 'Navigate to next file diff',
fn = function()
M.diff_next()
end,
},
diff_prev = {
name = 'OpencodeDiffPrev',
desc = 'Navigate to previous file diff',
fn = function()
M.diff_prev()
end,
},
diff_close = {
name = 'OpencodeDiffClose',
desc = 'Close diff view tab and return to normal editing',
fn = function()
M.diff_close()
end,
},
diff_revert_all_last_prompt = {
name = 'OpencodeRevertAllLastPrompt',
desc = 'Revert all file changes since the last opencode prompt',
fn = function()
M.diff_revert_all_last_prompt()
end,
},
diff_revert_this_last_prompt = {
name = 'OpencodeRevertThisLastPrompt',
desc = 'Revert current file changes since the last opencode prompt',
fn = function()
M.diff_revert_this_last_prompt()
end,
},
diff_revert_all_session = {
name = 'OpencodeRevertAllSession',
desc = 'Revert all file changes since the last session',
fn = function()
M.diff_revert_all_session()
end,
},
diff_revert_this_session = {
name = 'OpencodeRevertThisSession',
desc = 'Revert current file changes since the last session',
fn = function()
M.diff_revert_this_session()
end,
},
diff_revert_all_to_snapshot = {
name = 'OpencodeRevertAllToSnapshot',
desc = 'Revert all file changes to a specific snapshot',
fn = function(snapshot)
if not snapshot then
vim.notify('Snapshot ID is required', vim.log.levels.ERROR)
return
end
M.diff_revert_all(snapshot)
end,
args = true,
},
diff_revert_this_to_snapshot = {
name = 'OpencodeRevertThisToSnapshot',
desc = 'Revert all file changes to a specific snapshot',
fn = function(snapshot)
if not snapshot then
vim.notify('Snapshot ID is required', vim.log.levels.ERROR)
return
end
M.diff_revert_this(snapshot)
end,
args = true,
},
diff_restore_snapshot_file = {
name = 'OpencodeRestoreSnapshotFile',
desc = 'Restore a file to a specific restore point',
fn = function(snapshot)
if not snapshot then
vim.notify('Snapshot ID is required', vim.log.levels.ERROR)
return
end
M.diff_restore_snapshot_file(snapshot)
end,
args = true,
},
diff_restore_snapshot_all = {
name = 'OpencodeRestoreSnapshotAll',
desc = 'Restore all files to a specific restore point',
fn = function(snapshot)
if not snapshot then
vim.notify('Snapshot ID is required', vim.log.levels.ERROR)
return
end
M.diff_restore_snapshot_all(snapshot)
end,
args = true,
},
set_review_breakpoint = {
name = 'OpencodeSetReviewBreakpoint',
desc = 'Set a review breakpoint to track changes',
fn = function()
M.set_review_breakpoint()
end,
},
init = {
name = 'OpencodeInit',
slash_cmd = '/init',
desc = 'Initialize/Update AGENTS.md file',
fn = function()
M.initialize()
end,
},
help = {
name = 'OpencodeHelp',
slash_cmd = '/help',
desc = 'Display help message',
fn = function()
M.help()
end,
},
mcp = {
name = 'OpencodeMCP',
slash_cmd = '/mcp',
desc = 'Display list of mcp servers',
fn = function()
M.mcp()
end,
},
open_configuration_file = {
name = 'OpencodeConfigFile',
desc = 'Open opencode configuration file',
fn = function()
M.open_configuration_file()
end,
},
opencode_mode_plan = {
name = 'OpencodeAgentPlan',
desc = 'Set opencode agent to `plan`. (Tool calling disabled. No editor context besides selections)',
fn = function()
M.agent_plan()
end,
},
opencode_mode_build = {
name = 'OpencodeAgentBuild',
desc = 'Set opencode agent to `build`. (Default with full agent capabilities)',
fn = function()
M.agent_build()
end,
},
open_code_select_mode = {
name = 'OpencodeAgentSelect',
slash_cmd = '/agent',
desc = 'Select opencode agent',
fn = function()
M.select_agent()
end,
},
run_user_command = {
name = 'OpencodeRunUserCommand',
desc = 'Run a user-defined Opencode command by name',
fn = function(opts)
local name = opts.args and opts.args:match('^%s*(%S+)')
if not name or name == '' then
vim.notify('User command name required. Usage: :OpencodeRunUserCommand