A Neovim plugin for running a local opencode attach-mode TUI inside a snacks.nvim terminal, with Neovim-side session bridging and local integrations.
- Launch a local
opencode attachTUI against a configured backend server - Bridge the active attached TUI session back into Neovim
- Share the active Neovim file or visual selection with OpenCode's native editor-context integration
- Snacks terminal integration for an embedded OpenCode TUI pane
- Send prompts with context expansion (
@this,@buffer,@diagnostics) - Send direct review comments for the current line or visual range to the active session
- Auto-reload buffers when OpenCode edits files
- Expose
Userautocmds for notifications, statusline/tmux hooks, and other local integrations
{
"krmcbride/opencode.nvim",
dependencies = {
{ "folke/snacks.nvim", opts = { terminal = { enabled = true } } },
},
opts = {
server = {
url = "http://127.0.0.1:4096",
},
terminal = {
width = 0.43,
env = {
-- Extra environment for the child `opencode attach` process
SOME_CHILD_PROCESS_FLAG = "1",
-- Disables OpenTUI's Kitty graphics probe so it does not leak raw
-- probe text into the embedded Snacks terminal buffer.
OPENTUI_GRAPHICS = "0",
},
},
},
init = function()
-- Required for auto-reload when opencode edits files
vim.o.autoread = true
end,
keys = {
{ "<leader>ac", function() require("opencode").start({ focus = true, continue = true }) end, mode = { "n", "t" }, desc = "Continue opencode" },
{ "<leader>an", function() require("opencode").start({ focus = true, continue = false }) end, mode = { "n", "t" }, desc = "New opencode session" },
{ "<leader>aa", function() require("opencode").mention_selection({ focus = true }) end, mode = { "n", "x" }, desc = "Native mention selection" },
{ "<leader>aA", function() require("opencode").prompt("@this", { focus = true }) end, mode = { "n", "x" }, desc = "Append selection to prompt" },
{ "<leader>ab", function() require("opencode").prompt("@buffer", { focus = true }) end, desc = "Add buffer to prompt" },
{ "<leader>ad", function() require("opencode").prompt("@diagnostics", { focus = true }) end, desc = "Add diagnostics to prompt" },
{ "<leader>as", function() require("opencode").attach_session_prompt() end, desc = "Attach session ID" },
{ "<leader>av", function() require("opencode").review_selection() end, mode = "n", desc = "Review line" },
{ "<leader>av", function() require("opencode").review_visual_selection() end, mode = "x", desc = "Review selection" },
},
}If you are not using lazy.nvim, call require("opencode").setup({ ... }) yourself before using the plugin API.
All options with their defaults:
require("opencode").setup({
server = {
url = "http://127.0.0.1:4096", -- Backend server URL
},
auto_reload = true, -- Reload matching buffers on OpenCode edit events
editor_context = {
enabled = true, -- Share active Neovim file/selection with the embedded OpenCode TUI
},
terminal = {
cmd = nil, -- Optional custom attach command
dir = ".", -- Directory passed to `opencode attach`
continue = true, -- Default launch behavior; `start` can override per call
width = 0.35,
env = nil,
},
})
terminal.envnote:opts.terminal.envis only passed to the childopencode attachprocess. Backend/server feature flags usually need to be configured on the backend server process itself, not here. Editor context note:editor_context.enabled = truestarts a localhost WebSocket server lazily when the embedded TUI starts and injectsOPENCODE_EDITOR_SSE_PORTinto that TUI process. The long-running OpenCode backend server is not involved. Ifterminal.env.OPENCODE_EDITOR_SSE_PORTis already set, that explicit value is left untouched. To keep OpenCode's native integration from accidentally using an inherited Claude Code bridge,CLAUDE_CODE_SSE_PORTis cleared in the child TUI env and a warning is shown if a non-empty value is detected. Embedded terminal note:opencode.nvimruns the OpenCode TUI in a Snacks terminal. Neovim's embedded terminal does not support Kitty graphics, so settingOPENTUI_GRAPHICS = "0"underopts.terminal.envis recommended to avoid stray raw text likeGi=31337,s=1,v=1,a=q,t=d,f=24;AAAAappearing in the terminal buffer. Auto-reload note:auto_reload = truestill depends on Neovimautoread; setvim.o.autoread = truein your config. External non-OpenCode edits only surface throughOpencodeEvent:file.watcher.updatedwhen the backend server file watcher is enabled. Auth note: backend auth is read from Neovim'sOPENCODE_SERVER_PASSWORDand optionalOPENCODE_SERVER_USERNAMEenvironment variables. If you source credentials from a file or secret manager, populatevim.envbefore callingrequire("opencode").setup(...). Width: Set terminal width withopts.terminal.width. Other terminal behavior uses plugin defaults.
To track the active attached TUI session, OpenCode also needs the bundled TUI bridge plugin.
Add it to your OpenCode tui.json plugin list, not opencode.json:
{
"plugin": [
"file:///path/to/opencode.nvim/opencode-plugin"
]
}The bridge plugin is inert unless opencode.nvim launches the TUI with its bridge environment variables.
require("opencode").start() -- Start opencode if not running
require("opencode").start({ focus = true }) -- Start and focus if a new terminal is opened
require("opencode").start({ continue = true, focus = true }) -- Open with `--continue`, no-op if already open
require("opencode").start({ continue = false }) -- Open without `--continue`, no-op if already open
require("opencode").start({ continue = false, focus = true }) -- Open without `--continue`, no-op if already open
require("opencode").attach_session("ses_...") -- Attach directly to a specific session id
require("opencode").attach_session_prompt() -- Prompt for a session id, then attach
require("opencode").status() -- Show terminal, backend, bridge, and SSE status-- Insert the current line or selection through OpenCode's native editor integration.
require("opencode").mention_selection({ focus = true })
-- Add context through prompt appending (build up context, then submit in TUI).
require("opencode").prompt("@this") -- Current line or selection, legacy append path
require("opencode").prompt("@buffer") -- Current file
require("opencode").prompt("@diagnostics") -- LSP diagnostics
-- Focus the terminal after adding context
require("opencode").prompt("@this", { focus = true })
-- Or submit immediately
require("opencode").prompt("Fix @diagnostics", { submit = true })
require("opencode").prompt("Explain this", { clear = true, submit = true })Prompt Options:
| Option | Type | Description |
|---|---|---|
clear |
boolean | Clear the TUI input before appending |
submit |
boolean | Submit the TUI input after appending |
focus |
boolean | Focus the terminal after append; also enters Terminal mode and moves the cursor to EOL (see note below) |
focusbehavior: OpenCode’s@picker expects the cursor at the end of an@pathfragment. Withfocus = true, the plugin focuses the snacks terminal, switches to Terminal mode, then jumps to the end of the prompt so appended refs match that expectation.
Native Mentions vs Prompt Appending:
mention_selection() uses OpenCode's native editor-context WebSocket and sends an at_mentioned notification directly to the embedded TUI. This is the preferred path for current-line and visual-range mentions because it creates the TUI file mention without writing text through the terminal PTY or relying on autocomplete focus behavior.
prompt("@this"), prompt("@buffer"), and prompt("@diagnostics") use opencode.nvim's older prompt-append path. Keep this path for whole-buffer mentions, diagnostics, directory/file refs from pickers, and compatibility with older OpenCode versions. mention_selection() falls back to prompt("@this") if the embedded TUI has not connected to the editor-context WebSocket yet, unless called with { fallback = false }.
Context Placeholders:
These placeholders are defined by opencode.nvim, not by OpenCode itself. The plugin expands them into plain prompt text and native OpenCode-style file references before sending the prompt to the attached TUI/backend.
They only work when prompt text flows through opencode.nvim APIs like require("opencode").prompt(...) or mappings built on top of those APIs. Typing @this, @buffer, or @diagnostics directly into the OpenCode TUI does not trigger any special expansion.
| Placeholder | Expands To | Description |
|---|---|---|
@this |
@file.lua#21, @file.lua#21-30, or columns 8-15 in @file.lua#21 |
Current line, line range, or single-line char selection (columns as text; @…# last for TUI autocomplete) |
@buffer |
@file.lua |
Current buffer path |
@diagnostics |
Prompt text with a formatted diagnostic list and trailing @file ref |
LSP diagnostics for current buffer |
Tip:
@thisexpands to a native OpenCode file reference like@file.lua#21or@file.lua#21-30. Withfocus = true,opencode.nvimleaves the TUI cursor at end-of-line so the attached TUI can continue native@completion from that ref. Add a trailing space only if you explicitly want to dismiss the picker; the space is sent literally.
-- Review the current line in the active attached TUI session.
require("opencode").review_selection()
-- Review the current visual range in the active attached TUI session.
require("opencode").review_visual_selection()Reviews are sent directly through POST /session/<sessionID>/prompt_async using:
- one text part for your comment
- one ranged file attachment using
file://...?...start=&end=
The review popup is a small cursor-anchored editor float:
Ctrl-Ssubmits in normal or insert modeCtrl-Ccancels in insert modeqcancels in normal modeEnterinserts a newline
Direct review sends reuse the last persisted user message's agent, model, and variant when available, so they generally match the active session's existing model choice without requiring OpenCode core changes.
| Command | Description |
|---|---|
:Opencode status |
Show terminal, backend, bridge, and SSE status |
status includes the backend URL, SSE directory, bridge URL, bridged TUI route, and active session so you can verify where events and direct reviews are going.
opencode.nvim exposes three useful integration surfaces:
OpencodeEvent:*for backend SSE events in the currently subscribed backend directoryOpencodeActiveEvent:*for local embedded-TUI events scoped to the currently attached sessionOpencodeSessionChangedfor coarse route/session/cwd changes reported by the embedded TUI
That gives you enough surface to build your own notifications, tmux/workmux or window-status hooks, statusline components, per-session UI state, or any other local automation without hard-coding those integrations into the plugin.
Register these from your normal Neovim config with vim.api.nvim_create_autocmd("User", ...) after loading the plugin.
OpencodeEvent:* comes from the backend SSE stream. This is the right surface for backend file/edit lifecycle events and server connection state.
vim.api.nvim_create_autocmd("User", {
pattern = "OpencodeEvent:*",
callback = function(args)
local event = args.data.event
if event.type == "session.idle" then
vim.notify("opencode finished responding")
end
end,
})For OpencodeEvent:*, args.data includes:
event: the backend SSE event objecturl: the backend base URL that produced the event
When the bundled TUI bridge plugin is installed, opencode.nvim also forwards the active embedded session's local OpenCode events as autocmds.
This is the most useful surface for integrations that care about the embedded TUI the user is actually looking at, for example:
- desktop notifications when the active session goes idle or errors
- tmux/workmux or window-title status updates while the agent is busy or waiting on a question/permission prompt
- local UI reactions to
question.asked/permission.askedwithout watching every backend event globally
vim.api.nvim_create_autocmd("User", {
pattern = "OpencodeActiveEvent:*",
callback = function(args)
local event = args.data.event
if event.type == "session.idle" then
vim.notify("active embedded OpenCode session is idle")
end
end,
})OpencodeActiveEvent:* comes from the embedded TUI bridge plugin and is scoped to the currently attached session, which makes it suitable for local integrations like notifications, statusline widgets, or tmux/workmux hooks.
Currently forwarded event types:
session.statussession.idlesession.errormessage.updatedpermission.askedpermission.repliedquestion.askedquestion.replied
For OpencodeActiveEvent:*, args.data includes:
event: the forwarded OpenCode event objectroute: the local TUI route when the event was observedsession_id: the local attached session id when availableinstance_id: the Neovim bridge instance idcwd: the TUI working directory snapshot
OpencodeSessionChanged fires when the local bridge reports that the active embedded TUI route, session id, or cwd changed.
This is the right surface for integrations that want coarse session-aware state rather than every individual lifecycle event, for example:
- updating a statusline or winbar with the current attached session id
- mirroring the active OpenCode cwd into tmux/window metadata
- maintaining per-session caches keyed by
(instance_id, session_id)
vim.api.nvim_create_autocmd("User", {
pattern = "OpencodeSessionChanged",
callback = function(args)
local data = args.data
vim.notify(("route=%s session=%s cwd=%s"):format(data.route, data.session_id or "none", data.cwd or "none"))
end,
})For OpencodeSessionChanged, args.data includes:
route: the local TUI route when the event was observedsession_id: the active embedded OpenCode session id when availableinstance_id: the Neovim bridge instance idcwd: the TUI working directory snapshot
- Originally inspired by NickvanDyke/opencode.nvim
