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
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ Refer to the [Quick Chat](#-quick-chat) section for more details.
- [User Commands](#user-commands)
- [Contextual Actions for Snapshots](#-contextual-actions-for-snapshots)
- [Prompt Guard](#-prompt-guard)
- [Custom user hooks](#-custom-user-hooks)
- [Server-Sent Events (SSE) autocmds](#-server-sent-events-sse-autocmds)
- [Quick Chat](#-quick-chat)
- [Setting up opencode](#-setting-up-opencode)

Expand Down Expand Up @@ -868,6 +870,42 @@ require('opencode').setup({
- **No parameters**: The guard function receives no parameters. Access vim state directly (e.g., `vim.fn.getcwd()`, `vim.bo.filetype`).
- **Error handling**: If the guard function throws an error or returns a non-boolean value, the prompt is denied with an appropriate error message.

## 📡 Server-Sent Events (SSE) autocmds

Opencode.nvim forwards all server-sent events as Neovim User autocmds, allowing you to react to events and automate workflows. All events are fired with the pattern `OpencodeEvent:<event.type>`.

### Event Data Structure

The autocmd receives event data in `args.data.event`:

```lua
{
type = "event.name", -- Opencode event type
properties = { ... } -- Event-specific properties
}
```

### Wildcard Patterns

You can use wildcards to match multiple event types:

- `OpencodeEvent:*` - All events
- `OpencodeEvent:session.*` - All session events
- `OpencodeEvent:permission.*` - All permission events

### Example

```lua
vim.api.nvim_create_autocmd('User', {
pattern = 'OpencodeEvent:permission.asked',
callback = function(args)
local event = args.data.event
vim.notify(vim.inspect(event))
-- trigger custom logic
end,
})
```

## Quick chat

Quick chat allows you to start a temporary opencode session with context from the current line or selection.
Expand Down
29 changes: 17 additions & 12 deletions lua/opencode/event_manager.lua
Original file line number Diff line number Diff line change
Expand Up @@ -100,10 +100,9 @@ local util = require('opencode.util')
--- @field properties {ide: string}

--- @class ServerStartingEvent
--- @field server_job table
--- @field url string

--- @class ServerReadyEvent
--- @field server_job table
--- @field url string

--- @class ServerStoppedEvent
Expand Down Expand Up @@ -303,26 +302,32 @@ end

--- Emit an event to all subscribers
--- @param event_name OpencodeEventName The event name
--- @param data any Data to pass to event listeners
--- @param data table Data to pass to event listeners
function EventManager:emit(event_name, data)
local listeners = self.events[event_name]
if not listeners then
return
end

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

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

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

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

vim.api.nvim_exec_autocmds('User', {
pattern = 'OpencodeEvent:' .. event_name,
data = {
event = event,
},
})
end

--- Start the event manager and begin listening to server events
Expand All @@ -340,10 +345,10 @@ function EventManager:start()
--- @param prev OpencodeServer|nil
function(key, current, prev)
if current and current:get_spawn_promise() then
self:emit('custom.server_starting', { server_job = current })
self:emit('custom.server_starting', { url = current.url })

current:get_spawn_promise():and_then(function(server)
self:emit('custom.server_ready', { server_job = server, url = server.url })
self:emit('custom.server_ready', { url = server.url })
vim.defer_fn(function()
self:_subscribe_to_server_events(server)
end, 200)
Expand Down
60 changes: 56 additions & 4 deletions tests/unit/event_manager_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -102,10 +102,10 @@ describe('EventManager', function()

it('should handle starting and stopping', function()
assert.is_false(event_manager.is_started)

event_manager:start()
assert.is_true(event_manager.is_started)

event_manager:stop()
assert.is_false(event_manager.is_started)
assert.are.same({}, event_manager.events)
Expand All @@ -114,8 +114,60 @@ describe('EventManager', function()
it('should not start multiple times', function()
event_manager:start()
local first_start = event_manager.is_started

event_manager:start() -- Should not do anything
assert.are.equal(first_start, event_manager.is_started)
end)
end)

describe('User autocmd events', function()
it('should fire User autocmd when emitting events', function()
local autocmd_called = false
local autocmd_data = nil

local autocmd_id = vim.api.nvim_create_autocmd('User', {
pattern = 'OpencodeEvent:test_event',
callback = function(args)
autocmd_called = true
autocmd_data = args.data
end,
})

event_manager:emit('test_event', { test = 'value' })

vim.wait(100, function()
return autocmd_called
end)

vim.api.nvim_del_autocmd(autocmd_id)

assert.is_true(autocmd_called)
assert.are.same({
event = {
type = 'test_event',
properties = { test = 'value' },
},
}, autocmd_data)
end)

it('should fire User autocmd even when no internal listeners exist', function()
local autocmd_called = false

local autocmd_id = vim.api.nvim_create_autocmd('User', {
pattern = 'OpencodeEvent:orphan_event',
callback = function(args)
autocmd_called = true
end,
})

event_manager:emit('orphan_event', { data = 'test' })

vim.wait(100, function()
return autocmd_called
end)

vim.api.nvim_del_autocmd(autocmd_id)

assert.is_true(autocmd_called)
end)
end)
end)