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
103 changes: 84 additions & 19 deletions lua/opencode/api.lua
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
local core = require('opencode.core')
local util = require('opencode.util')
local session = require('opencode.session')
local input_window = require('opencode.ui.input_window')
local config_file = require('opencode.config_file')
local state = require('opencode.state')

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 = {}

Expand Down Expand Up @@ -274,7 +274,7 @@ function M.submit_input_prompt()
-- we're displaying /help or something similar, need to clear that and refresh
-- the session data before sending the command
state.display_route = nil
ui.render_output()
ui.render_output(true)
end

input_window.handle_submit()
Expand Down Expand Up @@ -348,7 +348,7 @@ function M.debug_session()
end

function M.initialize()
ui.render_output(true)
local id = require('opencode.id')

local new_session = core.create_new_session('AGENTS.md Initialization')
if not new_session then
Expand Down Expand Up @@ -382,7 +382,7 @@ function M.agent_build()
end

function M.select_agent()
local modes = require('opencode.config_file').get_opencode_agents()
local modes = config_file.get_opencode_agents()
vim.ui.select(modes, {
prompt = 'Select mode:',
}, function(selection)
Expand All @@ -395,7 +395,7 @@ function M.select_agent()
end

function M.switch_mode()
local modes = require('opencode.config_file').get_opencode_agents()
local modes = config_file.get_opencode_agents() --[[@as string[] ]]

local current_index = util.index_of(modes, state.current_mode)

Expand Down Expand Up @@ -449,15 +449,15 @@ function M.help()
'',
'### Subcommands',
'',
'| Command | Description |',
'|--------------|-------------------------------------------------------|',
'| Command | Description |',
'|--------------|-------------|',
}, false)

if not state.windows or not state.windows.output_win then
return
end

local max_desc_length = math.floor((vim.api.nvim_win_get_width(state.windows.output_win) / 1.3) - 5)
local max_desc_length = vim.api.nvim_win_get_width(state.windows.output_win) - 22

local sorted_commands = vim.tbl_keys(M.commands)
table.sort(sorted_commands)
Expand All @@ -468,7 +468,7 @@ function M.help()
if #desc > max_desc_length then
desc = desc:sub(1, max_desc_length - 3) .. '...'
end
table.insert(msg, string.format('| %-12s | %-53s |', name, desc))
table.insert(msg, string.format('| %-12s | %-' .. max_desc_length .. 's |', name, desc))
end

table.insert(msg, '')
Expand All @@ -478,10 +478,9 @@ function M.help()
end

function M.mcp()
local info = require('opencode.config_file')
local mcp = info.get_mcp_servers()
local mcp = config_file.get_mcp_servers()
if not mcp then
ui.notify('No MCP configuration found. Please check your opencode config file.', 'warn')
vim.notify('No MCP configuration found. Please check your opencode config file.', vim.log.levels.WARN)
return
end

Expand All @@ -491,19 +490,26 @@ function M.mcp()
local msg = M.with_header({
'### Available MCP servers',
'',
'| Name | Type | cmd |',
'|--------|------|-----|',
'| Name | Type | cmd/url |',
'|--------|------|---------|',
})

for name, def in pairs(mcp) do
local cmd_or_url
if def.type == 'local' then
cmd_or_url = def.command and table.concat(def.command, ' ')
elseif def.type == 'remote' then
cmd_or_url = def.url
end

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, ' ')
cmd_or_url
)
)
end
Expand All @@ -512,13 +518,38 @@ function M.mcp()
ui.render_lines(msg)
end

function M.commands_list()
local commands = config_file.get_user_commands()
if not commands then
vim.notify('No user commands found. Please check your opencode config file.', vim.log.levels.WARN)
return
end

state.display_route = '/commands'
M.open_input()

local msg = M.with_header({
'### Available User Commands',
'',
'| Name | Description |Arguments|',
'|------|-------------|---------|',
})

for name, def in pairs(commands) do
local desc = def.description or ''
table.insert(msg, string.format('| %s | %s | %s |', name, desc, tostring(config_file.command_takes_arguments(def))))
end

table.insert(msg, '')
ui.render_lines(msg)
end

--- Runs a user-defined command by name.
--- @param name string The name of the user command to run.
--- @param args? string[] Additional arguments to pass to the command.
function M.run_user_command(name, args)
M.open_input()

ui.render_output(true)
if not state.active_session then
vim.notify('No active session', vim.log.levels.WARN)
return
Expand Down Expand Up @@ -988,6 +1019,15 @@ M.commands = {

command = {
desc = 'Run user-defined command',
completions = function()
local user_commands = config_file.get_user_commands()
if not user_commands then
return {}
end
local names = vim.tbl_keys(user_commands)
table.sort(names)
return names
end,
fn = function(args)
local name = args[1]
if not name or name == '' then
Expand All @@ -1008,6 +1048,11 @@ M.commands = {
fn = M.mcp,
},

commands_list = {
desc = 'Show user-defined commands',
fn = M.commands_list,
},

permission = {
desc = 'Respond to permissions (accept/accept_all/deny)',
completions = { 'accept', 'accept_all', 'deny' },
Expand All @@ -1032,6 +1077,7 @@ M.slash_commands_map = {
['/agent'] = { fn = M.select_agent, desc = 'Select agent mode' },
['/agents_init'] = { fn = M.initialize, desc = 'Initialize AGENTS.md session' },
['/child-sessions'] = { fn = M.select_child_session, desc = 'Select child session' },
['/command-list'] = { fn = M.commands_list, desc = 'Show user-defined commands' },
['/compact'] = { fn = M.compact_session, desc = 'Compact current session' },
['/mcp'] = { fn = M.mcp, desc = 'Show MCP server configuration' },
['/models'] = { fn = M.configure_provider, desc = 'Switch provider/model' },
Expand Down Expand Up @@ -1126,9 +1172,13 @@ function M.complete_command(arg_lead, cmd_line, cursor_pos)
end

if num_parts <= 3 and subcmd_def.completions then
local completions = subcmd_def.completions
if type(completions) == 'function' then
completions = completions() --[[@as string[] ]]
end
return vim.tbl_filter(function(opt)
return vim.startswith(opt, arg_lead)
end, subcmd_def.completions)
end, completions)
end

if num_parts <= 4 and subcmd_def.sub_completions then
Expand Down Expand Up @@ -1169,6 +1219,21 @@ function M.get_slash_commands()
fn = def.fn,
})
end

local user_commands = config_file.get_user_commands()
if user_commands then
for name, def in pairs(user_commands) do
table.insert(result, {
slash_cmd = '/' .. name,
desc = def.description or 'User command',
fn = function(...)
M.run_user_command(name, ...)
end,
args = config_file.command_takes_arguments(def),
})
end
end

return result
end

Expand Down
7 changes: 7 additions & 0 deletions lua/opencode/config_file.lua
Original file line number Diff line number Diff line change
Expand Up @@ -126,4 +126,11 @@ function M.get_mcp_servers()
return cfg and cfg.mcp or nil
end

---Does this opencode user command take arguments?
---@param command OpencodeCommand
---@return boolean
function M.command_takes_arguments(command)
return command.template and command.template:find('$ARGUMENTS') ~= nil or false
end

return M
4 changes: 2 additions & 2 deletions lua/opencode/core.lua
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ function M.open(opts)
-- and the windows were closed so we need to do a full refresh. This mostly happens
-- when opening the window after having closed it since we're not currently clearing
-- the session on api.close()
ui.render_output(false)
ui.render_output()
end
end
end
Expand Down Expand Up @@ -277,7 +277,7 @@ local function on_opencode_server()
end

--- Switches the current mode to the specified agent.
--- @param mode string The agent/mode to switch to
--- @param mode string|nil The agent/mode to switch to
--- @return boolean success Returns true if the mode was switched successfully, false otherwise
function M.switch_to_mode(mode)
if not mode or mode == '' then
Expand Down
9 changes: 7 additions & 2 deletions lua/opencode/event_manager.lua
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
local state = require('opencode.state')
local config = require('opencode.config')
local ThrottlingEmitter = require('opencode.throttling_emitter')
local util = require('opencode.util')

--- @class EventInstallationUpdated
--- @field type "installation.updated"
Expand Down Expand Up @@ -278,12 +279,16 @@ function EventManager:emit(event_name, data)

local event = { type = event_name, properties = data }

if require('opencode.config').debug.capture_streamed_events then
if config.debug.capture_streamed_events then
table.insert(self.captured_events, vim.deepcopy(event))
end

for _, callback in ipairs(listeners) do
pcall(callback, data)
local ok, result = util.pcall_trace(callback, data)

if not ok then
vim.notify('Error calling ' .. event_name .. ' listener: ' .. result, vim.log.levels.ERROR)
end
end
end

Expand Down
39 changes: 16 additions & 23 deletions lua/opencode/state.lua
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ local config = require('opencode.config')
---@field unsubscribe fun( key:string|nil, cb:fun(key:string, new_val:any, old_val:any))
---@field is_running fun():boolean

local M = {}

-- Internal raw state table
local _state = {
-- ui
Expand Down Expand Up @@ -96,10 +98,10 @@ local _listeners = {}
---@usage
--- state.subscribe('foo', function(key, new, old) ... end)
--- state.subscribe('*', function(key, new, old) ... end)
local function subscribe(key, cb)
function M.subscribe(key, cb)
if type(key) == 'table' then
for _, k in ipairs(key) do
subscribe(k, cb)
M.subscribe(k, cb)
end
return
end
Expand All @@ -113,7 +115,7 @@ end
--- Unsubscribe a callback for a key (or all keys)
---@param key string|nil
---@param cb fun(key:string, new_val:any, old_val:any)
local function unsubscribe(key, cb)
function M.unsubscribe(key, cb)
key = key or '*'
local list = _listeners[key]
if not list then
Expand Down Expand Up @@ -148,7 +150,7 @@ local function _notify(key, new_val, old_val)
end)
end

local function append(key, value)
function M.append(key, value)
if type(value) ~= 'table' then
error('Value must be a table to append')
end
Expand All @@ -164,7 +166,7 @@ local function append(key, value)
_notify(key, _state[key], old)
end

local function remove(key, idx)
function M.remove(key, idx)
if not _state[key] then
return
end
Expand All @@ -177,16 +179,21 @@ local function remove(key, idx)
_notify(key, _state[key], old)
end

---
--- Returns true if any job (run or server) is running
---
function M.is_running()
return M.job_count > 0
end

--- Observable state proxy. All reads/writes go through this table.
--- Use `state.subscribe(key, cb)` to listen for changes.
--- Use `state.unsubscribe(key, cb)` to remove listeners.
---
--- Example:
--- state.subscribe('foo', function(key, new, old) print(key, new, old) end)
--- state.foo = 42 -- triggers callback
---@type OpencodeState
local M = {}
setmetatable(M, {
return setmetatable(M, {
__index = function(_, k)
return _state[k]
end,
Expand All @@ -203,18 +210,4 @@ setmetatable(M, {
__ipairs = function()
return ipairs(_state)
end,
})

M.append = append
M.remove = remove
M.subscribe = subscribe
M.unsubscribe = unsubscribe

---
--- Returns true if any job (run or server) is running
---
function M.is_running()
return M.job_count > 0
end

return M
}) --[[@as OpencodeState]]
Loading