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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -655,6 +655,12 @@ Example keymap for silent add:
['<leader>oY'] = { 'add_visual_selection', { open_input = false }, mode = {'v'} }
```

Example keymap for toggling a specific context using a parameterized API action:

```lua
['<leader>ocf'] = { 'toggle_context#current_file', desc = 'Toggle current file context' }
```

### Run opts

You can pass additional options when running a prompt via command or API:
Expand Down
6 changes: 6 additions & 0 deletions lua/opencode/api.lua
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,12 @@ function M.quick_chat(message, range)
quick_chat.quick_chat(prompt, { context_config = ctx }, range)
end

---@param context_key OpencodeToggleableContextKey
---@return boolean|nil enabled
function M.toggle_context(context_key)
return require('opencode.context').toggle_context(context_key)
end

function M.toggle_pane()
ui.toggle_pane()
end
Expand Down
15 changes: 15 additions & 0 deletions lua/opencode/event_manager.lua
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,18 @@ local log = require('opencode.log')
--- @field type "session.error"
--- @field properties {sessionID: string, error: table}

--- @class EventSessionStatus
--- @field type "session.status"
--- @field properties {
--- sessionID: string,
--- status: {
--- type: string,
--- message?: string,
--- attempt?: number,
--- next?: number
--- }
--- }

--- @class OpencodePermission
--- @field id string
--- @field type string
Expand Down Expand Up @@ -146,6 +158,7 @@ local log = require('opencode.log')
--- | "session.updated"
--- | "session.deleted"
--- | "session.error"
--- | "session.status"
--- | "permission.updated"
--- | "permission.asked"
--- | "permission.replied"
Expand Down Expand Up @@ -208,6 +221,7 @@ end
--- @overload fun(self: EventManager, event_name: "session.updated", callback: fun(data: EventSessionUpdated['properties']): nil)
--- @overload fun(self: EventManager, event_name: "session.deleted", callback: fun(data: EventSessionDeleted['properties']): nil)
--- @overload fun(self: EventManager, event_name: "session.error", callback: fun(data: EventSessionError['properties']): nil)
--- @overload fun(self: EventManager, event_name: "session.status", callback: fun(data: EventSessionStatus['properties']): nil)
--- @overload fun(self: EventManager, event_name: "permission.updated", callback: fun(data: EventPermissionUpdated['properties']): nil)
--- @overload fun(self: EventManager, event_name: "permission.replied", callback: fun(data: EventPermissionReplied['properties']): nil)
--- @overload fun(self: EventManager, event_name: "file.edited", callback: fun(data: EventFileEdited['properties']): nil)
Expand Down Expand Up @@ -249,6 +263,7 @@ end
--- @overload fun(self: EventManager, event_name: "session.updated", callback: fun(data: EventSessionUpdated['properties']): nil)
--- @overload fun(self: EventManager, event_name: "session.deleted", callback: fun(data: EventSessionDeleted['properties']): nil)
--- @overload fun(self: EventManager, event_name: "session.error", callback: fun(data: EventSessionError['properties']): nil)
--- @overload fun(self: EventManager, event_name: "session.status", callback: fun(data: EventSessionStatus['properties']): nil)
--- @overload fun(self: EventManager, event_name: "permission.updated", callback: fun(data: EventPermissionUpdated['properties']): nil)
--- @overload fun(self: EventManager, event_name: "permission.replied", callback: fun(data: EventPermissionReplied['properties']): nil)
--- @overload fun(self: EventManager, event_name: "file.edited", callback: fun(data: EventFileEdited['properties']): nil)
Expand Down
12 changes: 9 additions & 3 deletions lua/opencode/keymap.lua
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,14 @@ local function process_keymap_entry(keymap_config, default_modes, base_opts, def
-- Skip keymap if explicitly set to false (disabled)
elseif config_entry then
local func_name = config_entry[1]
local func_args = config_entry[2]
local raw_callback = type(func_name) == 'function' and func_name or api[func_name]
local resolved_func_name = func_name
local inline_arg = nil
if type(func_name) == 'string' then
resolved_func_name, inline_arg = func_name:match('^([^#]+)#(.+)$')
resolved_func_name = resolved_func_name or func_name
end
local func_args = config_entry[2] or inline_arg
local raw_callback = type(func_name) == 'function' and func_name or api[resolved_func_name]
local callback = raw_callback

if raw_callback and func_args then
Expand All @@ -38,7 +44,7 @@ local function process_keymap_entry(keymap_config, default_modes, base_opts, def

local modes = config_entry.mode or default_modes
local opts = vim.tbl_deep_extend('force', {}, base_opts)
opts.desc = config_entry.desc or cmds[func_name] and cmds[func_name].desc
opts.desc = config_entry.desc or cmds[resolved_func_name] and cmds[resolved_func_name].desc

if callback then
if defer_to_completion then
Expand Down
2 changes: 1 addition & 1 deletion lua/opencode/types.lua
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@
---@field share? SessionShareInfo

---@class OpencodeKeymapEntry
---@field [1] string # Function name
---@field [1] string # Function name, optionally suffixed with #<arg> to pass one string arg
---@field mode? string|string[] # Mode(s) for the keymap
---@field desc? string # Keymap description

Expand Down
2 changes: 1 addition & 1 deletion lua/opencode/ui/formatter/utils.lua
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ end

local function build_diff_gutter(line_numbers, width)
local line_number = line_numbers.new or line_numbers.old
return string.format('%-' .. width .. 's', line_number and tostring(line_number) or '')
return string.format('%' .. width .. 's', line_number and tostring(line_number) or '')
end

local function add_diff_line(output, line, line_numbers, width)
Expand Down
112 changes: 111 additions & 1 deletion lua/opencode/ui/loading_animation.lua
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,115 @@ local M = {}
M._animation = {
frames = nil,
text = 'Thinking... ',
status_data = nil,
current_frame = 1,
timer = nil,
fps = 10,
extmark_id = nil,
ns_id = vim.api.nvim_create_namespace('opencode_loading_animation'),
status_event_manager = nil,
}

---@param status table|nil
---@return string|nil
function M._format_status_text(status)
if type(status) ~= 'table' then
return nil
end

local status_type = status.type

if status_type == 'busy' then
return M._animation.text
end

if status_type == 'idle' then
return nil
end

if status_type == 'retry' then
local message = status.message or 'Retrying request'
local details = {}

if type(status.attempt) == 'number' then
table.insert(details, 'retry ' .. status.attempt)
end

if type(status.next) == 'number' then
local now_ms = os.time() * 1000
local seconds = math.max(0, math.ceil((status.next - now_ms) / 1000))
table.insert(details, 'in ' .. seconds .. 's')
end

if #details > 0 then
return string.format('%s (%s)... ', message, table.concat(details, ', '))
end

return message .. '... '
end

if type(status.message) == 'string' and status.message ~= '' then
return status.message .. '... '
end

return M._animation.text
end

local function unsubscribe_session_status_event(manager)
if manager and M._animation.status_event_manager == manager then
manager:unsubscribe('session.status', M.on_session_status)
M._animation.status_event_manager = nil
end
end

local function subscribe_session_status_event(manager)
if not manager then
return
end

if M._animation.status_event_manager and M._animation.status_event_manager ~= manager then
unsubscribe_session_status_event(M._animation.status_event_manager)
end

if M._animation.status_event_manager == manager then
return
end

manager:subscribe('session.status', M.on_session_status)
M._animation.status_event_manager = manager
end

function M.on_session_status(properties)
if not properties or type(properties) ~= 'table' then
return
end

local active_session = state.active_session
if active_session and active_session.id and properties.sessionID ~= active_session.id then
return
end

M._animation.status_data = properties.status
M.render(state.windows)
end

local function on_active_session_change(_, new_session, old_session)
local new_id = new_session and new_session.id
local old_id = old_session and old_session.id
if new_id ~= old_id then
M._animation.status_data = nil
end
end

local function on_event_manager_change(_, new_manager, old_manager)
unsubscribe_session_status_event(old_manager)
subscribe_session_status_event(new_manager)
end

function M._get_display_text()
return M._format_status_text(M._animation.status_data) or M._animation.text
end

function M._get_frames()
if M._animation.frames then
return M._animation.frames
Expand Down Expand Up @@ -41,7 +143,7 @@ M.render = vim.schedule_wrap(function(windows)
return false
end

local loading_text = M._animation.text .. M._get_frames()[M._animation.current_frame]
local loading_text = M._get_display_text() .. M._get_frames()[M._animation.current_frame]

M._animation.extmark_id = vim.api.nvim_buf_set_extmark(windows.footer_buf, M._animation.ns_id, 0, 0, {
id = M._animation.extmark_id or nil,
Expand Down Expand Up @@ -97,6 +199,7 @@ end
function M.stop()
M._clear_animation_timer()
M._animation.current_frame = 1
M._animation.status_data = nil
if state.windows and state.windows.footer_buf and vim.api.nvim_buf_is_valid(state.windows.footer_buf) then
pcall(vim.api.nvim_buf_clear_namespace, state.windows.footer_buf, M._animation.ns_id, 0, -1)
end
Expand All @@ -120,10 +223,17 @@ end

function M.setup()
state.subscribe('job_count', on_running_change)
state.subscribe('active_session', on_active_session_change)
state.subscribe('event_manager', on_event_manager_change)
subscribe_session_status_event(state.event_manager)
end

function M.teardown()
state.unsubscribe('job_count', on_running_change)
state.unsubscribe('active_session', on_active_session_change)
state.unsubscribe('event_manager', on_event_manager_change)
unsubscribe_session_status_event(M._animation.status_event_manager)
M._animation.status_data = nil
end

return M
Loading