From 241da9595a8faddd81de08ebecd85970e22c0139 Mon Sep 17 00:00:00 2001 From: Garrett Anderson Date: Wed, 25 Mar 2026 02:29:27 -0700 Subject: [PATCH 1/3] feat(ai): add OpenCode.nvim integration --- lua/lazyvim/plugins/extras/ai/opencode.lua | 324 +++++++++++++++++++++ 1 file changed, 324 insertions(+) create mode 100644 lua/lazyvim/plugins/extras/ai/opencode.lua diff --git a/lua/lazyvim/plugins/extras/ai/opencode.lua b/lua/lazyvim/plugins/extras/ai/opencode.lua new file mode 100644 index 00000000..4b808c4d --- /dev/null +++ b/lua/lazyvim/plugins/extras/ai/opencode.lua @@ -0,0 +1,324 @@ +local function safe_ask(prompt, opts) + if not vim.fn.executable("opencode") then + vim.notify("opencode: binary not installed. See opencode.ai", vim.log.levels.ERROR) + return + end + require("opencode").ask(prompt, opts) +end + +local function focus_opencode() + vim.schedule(function() + for _, win in ipairs(vim.api.nvim_list_wins()) do + local buf = vim.api.nvim_win_get_buf(win) + if vim.api.nvim_buf_get_name(buf):match("opencode %-%-port") then + vim.api.nvim_set_current_win(win) + end + end + end) +end + +return { + recommended = true, + + -- which-key + { + "folke/which-key.nvim", + optional = true, + opts = function(_, opts) + opts.spec = opts.spec or {} + table.insert(opts.spec, { "a", group = "OpenCode" }) + table.insert(opts.spec, { "ag", group = "git" }) + table.insert(opts.spec, { "ax", group = "diagnostics/quickfix" }) + if LazyVim.has("grapple.nvim") then + table.insert(opts.spec, { "af", group = "grapple" }) + end + table.insert(opts.spec, { "ap", group = "prompt" }) + end, + }, + + { + "folke/snacks.nvim", + }, + + { + "Nickvandyke/opencode.nvim", + version = "*", + cmd = "OpenCode", + keys = function() + local keys = { + { "gO", false }, + { + "aa", + function() + require("opencode").toggle() + focus_opencode() + end, + mode = { "n", "t" }, + desc = "Toggle OpenCode", + }, + { + "aq", + function() + require("opencode").stop() + end, + mode = { "n", "t" }, + desc = "Stop/Close OpenCode", + }, + { + "as", + function() + require("opencode").select() + end, + mode = { "n", "x" }, + desc = "Select action", + }, + { + "ai", + function() + safe_ask("", { submit = true, focus = false }) + end, + mode = { "n", "x" }, + desc = "Ask (empty)", + }, + { + "aI", + function() + safe_ask("@this: ", { submit = true, focus = false }) + end, + mode = { "n", "x" }, + desc = "Ask with context", + }, + { + "ab", + function() + safe_ask("@buffer ", { submit = true, focus = false }) + end, + mode = { "n", "x" }, + desc = "Ask about buffer", + }, + { + "agd", + function() + safe_ask("@diff ", { submit = true, focus = false }) + end, + mode = { "n", "x" }, + desc = "Git diff", + }, + { + "ax", + group = "diagnostics/quickfix", + }, + { + "axx", + function() + safe_ask("@quickfix ", { submit = true, focus = false }) + end, + mode = { "n", "x" }, + desc = "Ask with quickfix", + }, + { + "axd", + function() + safe_ask("@diagnostics ", { submit = true, focus = false }) + end, + mode = { "n", "x" }, + desc = "Ask with diagnostics", + }, + { + "av", + function() + safe_ask("@visible ", { submit = true, focus = false }) + end, + mode = { "n", "x" }, + desc = "Ask with visible text", + }, + { + "aB", + function() + safe_ask("@buffers ", { submit = true, focus = false }) + end, + mode = { "n", "x" }, + desc = "Ask with all buffers", + }, + { + "am", + function() + safe_ask("@marks ", { submit = true, focus = false }) + end, + mode = { "n", "x" }, + desc = "Ask with marks", + }, + { + "ape", + function() + safe_ask("@this explain: ", { submit = true, focus = false }) + end, + mode = { "n", "x" }, + desc = "Explain", + }, + { + "apf", + function() + safe_ask("@this fix: ", { submit = true, focus = false }) + end, + mode = { "n", "x" }, + desc = "Fix", + }, + { + "apd", + function() + safe_ask("@this diagnose: ", { submit = true, focus = false }) + end, + mode = { "n", "x" }, + desc = "Diagnose", + }, + { + "apr", + function() + safe_ask("@this review: ", { submit = true, focus = false }) + end, + mode = { "n", "x" }, + desc = "Review", + }, + { + "apt", + function() + safe_ask("@this test: ", { submit = true, focus = false }) + end, + mode = { "n", "x" }, + desc = "Test", + }, + { + "apo", + function() + safe_ask("@this optimize: ", { submit = true, focus = false }) + end, + mode = { "n", "x" }, + desc = "Optimize", + }, + { + "an", + function() + require("opencode").command("session.new") + end, + mode = { "n" }, + desc = "New session", + }, + { + "ac", + function() + require("opencode").command("session.close") + end, + mode = { "n" }, + desc = "Close session", + }, + { + "go", + function() + return require("opencode").operator("@this ") + end, + expr = true, + mode = { "n", "x" }, + desc = "Add range to OpenCode", + }, + { + "goo", + function() + return require("opencode").operator("@this ") .. "_" + end, + expr = true, + mode = { "n" }, + desc = "Add line to OpenCode", + }, + } + + if LazyVim.has("grapple.nvim") then + table.insert(keys, { + "af", + function() + safe_ask("@grapple ", { submit = true, focus = false }) + end, + mode = { "n", "x" }, + desc = "Ask with grapple", + }) + end + + return keys + end, + config = function() + vim.g.opencode_opts = {} + + -- Fix for marksman crashing on opencode buffers + vim.api.nvim_create_autocmd({ "BufAdd", "BufWinEnter", "TermOpen" }, { + callback = function(event) + if vim.api.nvim_buf_get_name(event.buf):match("opencode") then + vim.bo[event.buf].filetype = "opencode" + vim.b[event.buf].lsp_ignore = true + -- Detach any clients that might have already attached + local clients = vim.lsp.get_clients({ bufnr = event.buf }) + for _, client in ipairs(clients) do + vim.lsp.buf_detach_client(event.buf, client.id) + end + end + end, + }) + + vim.api.nvim_create_autocmd({ "BufEnter" }, { + pattern = "*:opencode --port*", + callback = function() + vim.cmd("startinsert") + end, + }) + + vim.api.nvim_create_autocmd({ "TermOpen" }, { + group = vim.api.nvim_create_augroup("opencode_integrated", { clear = true }), + pattern = "*:opencode --port*", + callback = function(event) + vim.bo[event.buf].buflisted = false + vim.bo[event.buf].filetype = "opencode" + + vim.keymap.set({ "t", "n" }, "", "h", { buffer = event.buf, desc = "Go to Left Window" }) + vim.keymap.set( + { "t", "n" }, + "", + "j", + { buffer = event.buf, desc = "Go to Lower Window" } + ) + vim.keymap.set( + { "t", "n" }, + "", + "k", + { buffer = event.buf, desc = "Go to Upper Window" } + ) + vim.keymap.set( + { "t", "n" }, + "", + "l", + { buffer = event.buf, desc = "Go to Right Window" } + ) + vim.keymap.set("t", "", function() + require("opencode").command("session.half.page.up") + end, { buffer = event.buf, desc = "Half scroll back" }) + vim.keymap.set("t", "", function() + require("opencode").command("session.half.page.down") + end, { buffer = event.buf, desc = "Half scroll forward" }) + vim.keymap.set("t", "", function() + require("opencode").command("session.page.up") + end, { buffer = event.buf, desc = "Scroll backward" }) + vim.keymap.set("t", "", function() + require("opencode").command("session.page.down") + end, { buffer = event.buf, desc = "Scroll forward" }) + end, + }) + + vim.api.nvim_create_autocmd("VimLeavePre", { + callback = function() + if vim.fn.has("unix") == 1 then + local pid = vim.fn.getpid() + vim.fn.system({ "pkill", "-P", tostring(pid), "-f", "opencode" }) + end + end, + }) + end, + }, +} From 75c475fc3cd7d5cd1197620c6e19ebe965930739 Mon Sep 17 00:00:00 2001 From: G Date: Thu, 14 May 2026 17:12:06 -0700 Subject: [PATCH 2/3] fix pause in opencode window --- lua/lazyvim/plugins/extras/ai/opencode.lua | 464 +++++++++++---------- 1 file changed, 237 insertions(+), 227 deletions(-) diff --git a/lua/lazyvim/plugins/extras/ai/opencode.lua b/lua/lazyvim/plugins/extras/ai/opencode.lua index 4b808c4d..77f066b6 100644 --- a/lua/lazyvim/plugins/extras/ai/opencode.lua +++ b/lua/lazyvim/plugins/extras/ai/opencode.lua @@ -28,242 +28,218 @@ return { opts.spec = opts.spec or {} table.insert(opts.spec, { "a", group = "OpenCode" }) table.insert(opts.spec, { "ag", group = "git" }) - table.insert(opts.spec, { "ax", group = "diagnostics/quickfix" }) - if LazyVim.has("grapple.nvim") then - table.insert(opts.spec, { "af", group = "grapple" }) - end table.insert(opts.spec, { "ap", group = "prompt" }) end, }, - { - "folke/snacks.nvim", - }, - { "Nickvandyke/opencode.nvim", version = "*", - cmd = "OpenCode", - keys = function() - local keys = { - { "gO", false }, - { - "aa", - function() - require("opencode").toggle() - focus_opencode() - end, - mode = { "n", "t" }, - desc = "Toggle OpenCode", - }, - { - "aq", - function() - require("opencode").stop() - end, - mode = { "n", "t" }, - desc = "Stop/Close OpenCode", - }, - { - "as", - function() - require("opencode").select() - end, - mode = { "n", "x" }, - desc = "Select action", - }, - { - "ai", - function() - safe_ask("", { submit = true, focus = false }) - end, - mode = { "n", "x" }, - desc = "Ask (empty)", - }, - { - "aI", - function() - safe_ask("@this: ", { submit = true, focus = false }) - end, - mode = { "n", "x" }, - desc = "Ask with context", - }, - { - "ab", - function() - safe_ask("@buffer ", { submit = true, focus = false }) - end, - mode = { "n", "x" }, - desc = "Ask about buffer", - }, - { - "agd", - function() - safe_ask("@diff ", { submit = true, focus = false }) - end, - mode = { "n", "x" }, - desc = "Git diff", - }, - { - "ax", - group = "diagnostics/quickfix", - }, - { - "axx", - function() - safe_ask("@quickfix ", { submit = true, focus = false }) - end, - mode = { "n", "x" }, - desc = "Ask with quickfix", - }, - { - "axd", - function() - safe_ask("@diagnostics ", { submit = true, focus = false }) - end, - mode = { "n", "x" }, - desc = "Ask with diagnostics", - }, - { - "av", - function() - safe_ask("@visible ", { submit = true, focus = false }) - end, - mode = { "n", "x" }, - desc = "Ask with visible text", - }, - { - "aB", - function() - safe_ask("@buffers ", { submit = true, focus = false }) - end, - mode = { "n", "x" }, - desc = "Ask with all buffers", - }, - { - "am", - function() - safe_ask("@marks ", { submit = true, focus = false }) - end, - mode = { "n", "x" }, - desc = "Ask with marks", - }, - { - "ape", - function() - safe_ask("@this explain: ", { submit = true, focus = false }) - end, - mode = { "n", "x" }, - desc = "Explain", - }, - { - "apf", - function() - safe_ask("@this fix: ", { submit = true, focus = false }) - end, - mode = { "n", "x" }, - desc = "Fix", - }, - { - "apd", - function() - safe_ask("@this diagnose: ", { submit = true, focus = false }) - end, - mode = { "n", "x" }, - desc = "Diagnose", - }, - { - "apr", - function() - safe_ask("@this review: ", { submit = true, focus = false }) - end, - mode = { "n", "x" }, - desc = "Review", - }, - { - "apt", - function() - safe_ask("@this test: ", { submit = true, focus = false }) - end, - mode = { "n", "x" }, - desc = "Test", - }, - { - "apo", - function() - safe_ask("@this optimize: ", { submit = true, focus = false }) - end, - mode = { "n", "x" }, - desc = "Optimize", - }, - { - "an", - function() - require("opencode").command("session.new") - end, - mode = { "n" }, - desc = "New session", - }, - { - "ac", - function() - require("opencode").command("session.close") - end, - mode = { "n" }, - desc = "Close session", - }, - { - "go", - function() - return require("opencode").operator("@this ") - end, - expr = true, - mode = { "n", "x" }, - desc = "Add range to OpenCode", - }, - { - "goo", - function() - return require("opencode").operator("@this ") .. "_" - end, - expr = true, - mode = { "n" }, - desc = "Add line to OpenCode", - }, - } + dependencies = { + "nvim-lua/plenary.nvim", + }, + cmd = { "OpenCode", "OpenCodeToggle" }, + keys = { + { "gO", false }, - if LazyVim.has("grapple.nvim") then - table.insert(keys, { - "af", - function() - safe_ask("@grapple ", { submit = true, focus = false }) - end, - mode = { "n", "x" }, - desc = "Ask with grapple", - }) - end - - return keys - end, - config = function() - vim.g.opencode_opts = {} - - -- Fix for marksman crashing on opencode buffers - vim.api.nvim_create_autocmd({ "BufAdd", "BufWinEnter", "TermOpen" }, { - callback = function(event) - if vim.api.nvim_buf_get_name(event.buf):match("opencode") then - vim.bo[event.buf].filetype = "opencode" - vim.b[event.buf].lsp_ignore = true - -- Detach any clients that might have already attached - local clients = vim.lsp.get_clients({ bufnr = event.buf }) - for _, client in ipairs(clients) do - vim.lsp.buf_detach_client(event.buf, client.id) - end - end + { + "aa", + function() + require("opencode").toggle() + focus_opencode() end, - }) + mode = { "n", "t" }, + desc = "Toggle OpenCode", + }, + + { + "aq", + function() + require("opencode").stop() + end, + mode = { "n", "t" }, + desc = "Stop/Close OpenCode", + }, + + { + "as", + function() + require("opencode").select() + end, + mode = { "n", "x" }, + desc = "Select action", + }, + + { + "ai", + function() + safe_ask("", { submit = true, focus = false }) + end, + mode = { "n", "x" }, + desc = "Ask (empty)", + }, + + { + "aI", + function() + safe_ask("@this: ", { submit = true, focus = false }) + end, + mode = { "n", "x" }, + desc = "Ask with context", + }, + + { + "ab", + function() + safe_ask("@buffer ", { submit = true, focus = false }) + end, + mode = { "n", "x" }, + desc = "Ask about buffer", + }, + + { + "agd", + function() + safe_ask("@diff ", { submit = true, focus = false }) + end, + mode = { "n", "x" }, + desc = "Git diff", + }, + + { + "ax", + function() + safe_ask("@quickfix ", { submit = true, focus = false }) + end, + mode = { "n", "x" }, + desc = "Ask with quickfix", + }, + + { + "av", + function() + safe_ask("@visible ", { submit = true, focus = false }) + end, + mode = { "n", "x" }, + desc = "Ask with visible text", + }, + + { + "aB", + function() + safe_ask("@buffers ", { submit = true, focus = false }) + end, + mode = { "n", "x" }, + desc = "Ask with all buffers", + }, + + { + "am", + function() + safe_ask("@marks ", { submit = true, focus = false }) + end, + mode = { "n", "x" }, + desc = "Ask with marks", + }, + + { + "ape", + function() + safe_ask("@this explain: ", { submit = true, focus = false }) + end, + mode = { "n", "x" }, + desc = "Explain", + }, + + { + "apf", + function() + safe_ask("@this fix: ", { submit = true, focus = false }) + end, + mode = { "n", "x" }, + desc = "Fix", + }, + + { + "apd", + function() + safe_ask("@this diagnose: ", { submit = true, focus = false }) + end, + mode = { "n", "x" }, + desc = "Diagnose", + }, + + { + "apr", + function() + safe_ask("@this review: ", { submit = true, focus = false }) + end, + mode = { "n", "x" }, + desc = "Review", + }, + + { + "apt", + function() + safe_ask("@this test: ", { submit = true, focus = false }) + end, + mode = { "n", "x" }, + desc = "Test", + }, + + { + "apo", + function() + safe_ask("@this optimize: ", { submit = true, focus = false }) + end, + mode = { "n", "x" }, + desc = "Optimize", + }, + + { + "an", + function() + require("opencode").command("session.new") + end, + mode = { "n" }, + desc = "New session", + }, + + { + "ac", + function() + require("opencode").command("session.close") + end, + mode = { "n" }, + desc = "Close session", + }, + + { + "go", + function() + return require("opencode").operator("@this ") + end, + expr = true, + mode = { "n", "x" }, + desc = "Add range to OpenCode", + }, + + { + "goo", + function() + return require("opencode").operator("@this ") .. "_" + end, + expr = true, + mode = { "n" }, + desc = "Add line to OpenCode", + }, + }, + opts = function() + -- 1. Setup autogroups/autocmds for terminal optimizations + local opencode_augroup = vim.api.nvim_create_augroup("opencode_integrated", { clear = true }) vim.api.nvim_create_autocmd({ "BufEnter" }, { + group = opencode_augroup, pattern = "*:opencode --port*", callback = function() vim.cmd("startinsert") @@ -271,11 +247,15 @@ return { }) vim.api.nvim_create_autocmd({ "TermOpen" }, { - group = vim.api.nvim_create_augroup("opencode_integrated", { clear = true }), + -- group = vim.api.nvim_create_augroup("opencode_integrated", { clear = true }), + group = opencode_augroup, pattern = "*:opencode --port*", callback = function(event) vim.bo[event.buf].buflisted = false - vim.bo[event.buf].filetype = "opencode" + + -- Override space layout globally inside this exact buffer window + vim.keymap.set("t", "", "", { buffer = event.buf, nowait = true, noremap = true }) + vim.b[event.buf].which_key_ignore = true vim.keymap.set({ "t", "n" }, "", "h", { buffer = event.buf, desc = "Go to Left Window" }) vim.keymap.set( @@ -319,6 +299,36 @@ return { end end, }) + -- Essential: Return an empty table container to stop lazy.nvim fallback calls + return {} + end, + }, + + -- 2. Modify which-key v3 auto-triggers to fully drop terminal mode input tracking + { + "folke/which-key.nvim", + optional = true, + opts = function(_, opts) + opts.triggers = opts.triggers or {} + + -- Check if default auto triggers are present + local has_auto = false + for _, trigger in ipairs(opts.triggers) do + -- Standard v3 layout tables wrap the auto hook directive as an array string value + if type(trigger) == "table" and trigger == "" then + has_auto = true + -- Strip terminal mode ("t") and command mode ("c") from automatic hooking loops + trigger.mode = "nixso" + elseif trigger == "" then + -- Fallback mutation path if declared as a raw flat string pattern + has_auto = true + end + end + + -- If LazyVim default configs haven't initialized it, enforce a clean non-terminal rule + if not has_auto then + table.insert(opts.triggers, { "", mode = "nixso" }) + end end, }, } From 0c65a87781e5f2f92e5bedfc9f08caa8af4378b5 Mon Sep 17 00:00:00 2001 From: G Date: Tue, 19 May 2026 18:21:51 -0700 Subject: [PATCH 3/3] git screw up. add changes from https://github.com/g-lok/LazyVim/pull/2 --- lua/lazyvim/plugins/extras/ai/opencode.lua | 172 ++++++++------------- 1 file changed, 67 insertions(+), 105 deletions(-) diff --git a/lua/lazyvim/plugins/extras/ai/opencode.lua b/lua/lazyvim/plugins/extras/ai/opencode.lua index 77f066b6..bfa37c19 100644 --- a/lua/lazyvim/plugins/extras/ai/opencode.lua +++ b/lua/lazyvim/plugins/extras/ai/opencode.lua @@ -1,4 +1,9 @@ -local function safe_ask(prompt, opts) +if not vim.fn.executable("opencode") then + vim.notify("opencode: binary not installed. See opencode.ai", vim.log.levels.ERROR) + return {} +end + +local function ask(prompt, opts) if not vim.fn.executable("opencode") then vim.notify("opencode: binary not installed. See opencode.ai", vim.log.levels.ERROR) return @@ -6,20 +11,7 @@ local function safe_ask(prompt, opts) require("opencode").ask(prompt, opts) end -local function focus_opencode() - vim.schedule(function() - for _, win in ipairs(vim.api.nvim_list_wins()) do - local buf = vim.api.nvim_win_get_buf(win) - if vim.api.nvim_buf_get_name(buf):match("opencode %-%-port") then - vim.api.nvim_set_current_win(win) - end - end - end) -end - return { - recommended = true, - -- which-key { "folke/which-key.nvim", @@ -46,9 +38,8 @@ return { "aa", function() require("opencode").toggle() - focus_opencode() end, - mode = { "n", "t" }, + mode = { "n" }, desc = "Toggle OpenCode", }, @@ -57,7 +48,7 @@ return { function() require("opencode").stop() end, - mode = { "n", "t" }, + mode = { "n" }, desc = "Stop/Close OpenCode", }, @@ -73,7 +64,7 @@ return { { "ai", function() - safe_ask("", { submit = true, focus = false }) + ask("", { submit = true, focus = false }) end, mode = { "n", "x" }, desc = "Ask (empty)", @@ -82,7 +73,7 @@ return { { "aI", function() - safe_ask("@this: ", { submit = true, focus = false }) + ask("@this: ", { submit = true, focus = false }) end, mode = { "n", "x" }, desc = "Ask with context", @@ -91,7 +82,7 @@ return { { "ab", function() - safe_ask("@buffer ", { submit = true, focus = false }) + ask("@buffer ", { submit = true, focus = false }) end, mode = { "n", "x" }, desc = "Ask about buffer", @@ -100,7 +91,7 @@ return { { "agd", function() - safe_ask("@diff ", { submit = true, focus = false }) + ask("@diff ", { submit = true, focus = false }) end, mode = { "n", "x" }, desc = "Git diff", @@ -109,7 +100,7 @@ return { { "ax", function() - safe_ask("@quickfix ", { submit = true, focus = false }) + ask("@quickfix ", { submit = true, focus = false }) end, mode = { "n", "x" }, desc = "Ask with quickfix", @@ -118,7 +109,7 @@ return { { "av", function() - safe_ask("@visible ", { submit = true, focus = false }) + ask("@visible ", { submit = true, focus = false }) end, mode = { "n", "x" }, desc = "Ask with visible text", @@ -127,7 +118,7 @@ return { { "aB", function() - safe_ask("@buffers ", { submit = true, focus = false }) + ask("@buffers ", { submit = true, focus = false }) end, mode = { "n", "x" }, desc = "Ask with all buffers", @@ -136,7 +127,7 @@ return { { "am", function() - safe_ask("@marks ", { submit = true, focus = false }) + ask("@marks ", { submit = true, focus = false }) end, mode = { "n", "x" }, desc = "Ask with marks", @@ -145,7 +136,7 @@ return { { "ape", function() - safe_ask("@this explain: ", { submit = true, focus = false }) + ask("@this explain: ", { submit = true, focus = false }) end, mode = { "n", "x" }, desc = "Explain", @@ -154,7 +145,7 @@ return { { "apf", function() - safe_ask("@this fix: ", { submit = true, focus = false }) + ask("@this fix: ", { submit = true, focus = false }) end, mode = { "n", "x" }, desc = "Fix", @@ -163,7 +154,7 @@ return { { "apd", function() - safe_ask("@this diagnose: ", { submit = true, focus = false }) + ask("@this diagnose: ", { submit = true, focus = false }) end, mode = { "n", "x" }, desc = "Diagnose", @@ -172,7 +163,7 @@ return { { "apr", function() - safe_ask("@this review: ", { submit = true, focus = false }) + ask("@this review: ", { submit = true, focus = false }) end, mode = { "n", "x" }, desc = "Review", @@ -181,7 +172,7 @@ return { { "apt", function() - safe_ask("@this test: ", { submit = true, focus = false }) + ask("@this test: ", { submit = true, focus = false }) end, mode = { "n", "x" }, desc = "Test", @@ -190,7 +181,7 @@ return { { "apo", function() - safe_ask("@this optimize: ", { submit = true, focus = false }) + ask("@this optimize: ", { submit = true, focus = false }) end, mode = { "n", "x" }, desc = "Optimize", @@ -234,48 +225,58 @@ return { desc = "Add line to OpenCode", }, }, - opts = function() - -- 1. Setup autogroups/autocmds for terminal optimizations - local opencode_augroup = vim.api.nvim_create_augroup("opencode_integrated", { clear = true }) + config = function(_, opts) + local config = require("opencode.config") + local opencode_cmd = "opencode --port" + local snacks_terminal_opts = { + win = { + position = "right", + on_win = function(win) + -- Set up keymaps and cleanup for an arbitrary terminal + require("opencode.terminal").setup(win.win) + end, + enter = false, + }, + -- Otherwise, we get error `Terminal exited with code` when closing Opencode because + -- the `stop` command already closes the terminal window, but the Snacks.terminal `TermClose` + -- autocmd kicks in and tries to close it again, causing the error. + auto_close = false, + } - vim.api.nvim_create_autocmd({ "BufEnter" }, { - group = opencode_augroup, - pattern = "*:opencode --port*", - callback = function() - vim.cmd("startinsert") - end, - }) + config.opts = vim.tbl_deep_extend( + "force", + config.opts, + { + server = { + start = function() + require("snacks.terminal").open(opencode_cmd, snacks_terminal_opts) + end, + stop = function() + local win, created = require("snacks.terminal").get(opencode_cmd, snacks_terminal_opts) + if win and created then + win:close() + vim.cmd.stopinsert() + elseif win and not created then + win:close() + end + end, + toggle = function() + require("snacks.terminal").toggle(opencode_cmd, snacks_terminal_opts) + end, + }, + }, + -- We merge here also `opts` so we can bypass the following limitation with `vim.g` https://github.com/nickjvandyke/opencode.nvim/issues/36 + opts or {}, + vim.g.opencode_opts or {} + ) vim.api.nvim_create_autocmd({ "TermOpen" }, { - -- group = vim.api.nvim_create_augroup("opencode_integrated", { clear = true }), - group = opencode_augroup, + group = vim.api.nvim_create_augroup("opencode_integrated", { clear = true }), pattern = "*:opencode --port*", callback = function(event) vim.bo[event.buf].buflisted = false + vim.bo[event.buf].filetype = "opencode" - -- Override space layout globally inside this exact buffer window - vim.keymap.set("t", "", "", { buffer = event.buf, nowait = true, noremap = true }) - vim.b[event.buf].which_key_ignore = true - - vim.keymap.set({ "t", "n" }, "", "h", { buffer = event.buf, desc = "Go to Left Window" }) - vim.keymap.set( - { "t", "n" }, - "", - "j", - { buffer = event.buf, desc = "Go to Lower Window" } - ) - vim.keymap.set( - { "t", "n" }, - "", - "k", - { buffer = event.buf, desc = "Go to Upper Window" } - ) - vim.keymap.set( - { "t", "n" }, - "", - "l", - { buffer = event.buf, desc = "Go to Right Window" } - ) vim.keymap.set("t", "", function() require("opencode").command("session.half.page.up") end, { buffer = event.buf, desc = "Half scroll back" }) @@ -290,45 +291,6 @@ return { end, { buffer = event.buf, desc = "Scroll forward" }) end, }) - - vim.api.nvim_create_autocmd("VimLeavePre", { - callback = function() - if vim.fn.has("unix") == 1 then - local pid = vim.fn.getpid() - vim.fn.system({ "pkill", "-P", tostring(pid), "-f", "opencode" }) - end - end, - }) - -- Essential: Return an empty table container to stop lazy.nvim fallback calls - return {} - end, - }, - - -- 2. Modify which-key v3 auto-triggers to fully drop terminal mode input tracking - { - "folke/which-key.nvim", - optional = true, - opts = function(_, opts) - opts.triggers = opts.triggers or {} - - -- Check if default auto triggers are present - local has_auto = false - for _, trigger in ipairs(opts.triggers) do - -- Standard v3 layout tables wrap the auto hook directive as an array string value - if type(trigger) == "table" and trigger == "" then - has_auto = true - -- Strip terminal mode ("t") and command mode ("c") from automatic hooking loops - trigger.mode = "nixso" - elseif trigger == "" then - -- Fallback mutation path if declared as a raw flat string pattern - has_auto = true - end - end - - -- If LazyVim default configs haven't initialized it, enforce a clean non-terminal rule - if not has_auto then - table.insert(opts.triggers, { "", mode = "nixso" }) - end end, }, }