Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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 @@ -865,6 +867,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
26 changes: 18 additions & 8 deletions lua/opencode/event_manager.lua
Original file line number Diff line number Diff line change
Expand Up @@ -303,26 +303,36 @@ 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)
-- Dispatch to internal listeners
Comment thread
disrupted marked this conversation as resolved.
Outdated
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

-- Fire User autocmd for external subscribers
if not vim.startswith(event_name, 'custom.') then
Comment thread
sudo-tee marked this conversation as resolved.
Outdated
vim.api.nvim_exec_autocmds('User', {
pattern = 'OpencodeEvent:' .. event_name,
data = {
event = event,
},
})
end
end

--- Start the event manager and begin listening to server events
Expand Down
69 changes: 65 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,69 @@ 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

-- Set up autocmd listener
Comment thread
disrupted marked this conversation as resolved.
Outdated
local autocmd_id = vim.api.nvim_create_autocmd('User', {
pattern = 'OpencodeEvent:test_event',
callback = function(args)
autocmd_called = true
autocmd_data = args.data
end,
})

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

-- Wait for autocmd to fire
vim.wait(100, function()
return autocmd_called
end)

-- Clean up
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

-- Set up autocmd listener (no internal listener subscribed)
local autocmd_id = vim.api.nvim_create_autocmd('User', {
pattern = 'OpencodeEvent:orphan_event',
callback = function(args)
autocmd_called = true
end,
})

-- Emit event without internal listeners
event_manager:emit('orphan_event', { data = 'test' })

-- Wait for autocmd to fire
vim.wait(100, function()
return autocmd_called
end)

-- Clean up
vim.api.nvim_del_autocmd(autocmd_id)

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