From ea9eb3dfaa3aa59cb627f3760887275ce9d04d81 Mon Sep 17 00:00:00 2001 From: Dmitriy Kovalenko Date: Wed, 28 Jan 2026 10:55:21 -0800 Subject: [PATCH 1/7] feat: Normal buffer keymaps --- README.md | 54 ++++++++++++++++++++++++------ lua/opencode/keymaps.lua | 29 ++++++++++++++++ lua/opencode/provider/snacks.lua | 2 +- lua/opencode/provider/terminal.lua | 2 ++ plugin/keymaps.lua | 8 +++++ 5 files changed, 83 insertions(+), 12 deletions(-) create mode 100644 lua/opencode/keymaps.lua create mode 100644 plugin/keymaps.lua diff --git a/README.md b/README.md index 95921586..39ed33ac 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ Integrate the [opencode](https://github.com/sst/opencode) AI assistant with Neov vim.keymap.set({ "n", "x" }, "go", function() return require("opencode").operator("@this ") end, { desc = "Add range to opencode", expr = true }) vim.keymap.set("n", "goo", function() return require("opencode").operator("@this ") .. "_" end, { desc = "Add line to opencode", expr = true }) + -- The default / keymaps will work in the normal mode but it is possible to scroll opencode from any buffer: vim.keymap.set("n", "", function() require("opencode").command("session.half.page.up") end, { desc = "Scroll opencode up" }) vim.keymap.set("n", "", function() require("opencode").command("session.half.page.down") end, { desc = "Scroll opencode down" }) @@ -79,17 +80,17 @@ programs.nixvim = { `opencode.nvim` replaces placeholders in prompts with the corresponding context: -| Placeholder | Context | -| -------------- | ------------------------------------------------------------- | -| `@this` | Operator range or visual selection if any, else cursor position | -| `@buffer` | Current buffer | -| `@buffers` | Open buffers | -| `@visible` | Visible text | -| `@diagnostics` | Current buffer diagnostics | -| `@quickfix` | Quickfix list | -| `@diff` | Git diff | -| `@marks` | Global marks | -| `@grapple` | [grapple.nvim](https://github.com/cbochs/grapple.nvim) tags | +| Placeholder | Context | +| -------------- | --------------------------------------------------------------- | +| `@this` | Operator range or visual selection if any, else cursor position | +| `@buffer` | Current buffer | +| `@buffers` | Open buffers | +| `@visible` | Visible text | +| `@diagnostics` | Current buffer diagnostics | +| `@quickfix` | Quickfix list | +| `@diff` | Git diff | +| `@marks` | Global marks | +| `@grapple` | [grapple.nvim](https://github.com/cbochs/grapple.nvim) tags | ### Prompts @@ -107,6 +108,37 @@ Select or reference prompts to review, explain, and improve your code: | `review` | Review `@this` for correctness and readability | | `test` | Add tests for `@this` | +### Keymaps + +`opencode.nvim` sets these buffer-local keymaps in opencode terminal buffers by default: + +| Keymap | Command | Description | +| ------- | ------------------------ | ---------------------------- | +| `` | `session.half.page.up` | Scroll up half page | +| `` | `session.half.page.down` | Scroll down half page | +| `` | `session.interrupt` | Interrup (same as esc press) | +| `gg` | `session.first` | Go to first message | +| `G` | `session.last` | Go to last message | + +You can customize or disable these keymaps: + +```lua +vim.g.opencode_opts = { + -- Customize keymaps + keymaps = { + n = { + [""] = { "session.half.page.up", desc = "Scroll up" }, + [""] = { "session.half.page.down", desc = "Scroll down" }, + ["gg"] = false, -- Disable this keymap + -- Add custom keymaps + [""] = { "session.new", desc = "New session" }, + }, + }, + -- Or disable all default keymaps + -- keymaps = false, +} +``` + ### Provider You can manually run `opencode` inside Neovim's CWD however you like and `opencode.nvim` will find it! diff --git a/lua/opencode/keymaps.lua b/lua/opencode/keymaps.lua new file mode 100644 index 00000000..e3971528 --- /dev/null +++ b/lua/opencode/keymaps.lua @@ -0,0 +1,29 @@ +local M = {} + +---Apply buffer-local keymaps to the given buffer. +---@param bufnr integer The buffer number to apply keymaps to. +function M.apply(bufnr) + local opts = { buffer = bufnr } + + vim.keymap.set("n", "", function() + require("opencode.api.command").command("session.half.page.up") + end, vim.tbl_extend("force", opts, { desc = "Scroll up half page" })) + + vim.keymap.set("n", "", function() + require("opencode.api.command").command("session.half.page.down") + end, vim.tbl_extend("force", opts, { desc = "Scroll down half page" })) + + vim.keymap.set("n", "gg", function() + require("opencode.api.command").command("session.first") + end, vim.tbl_extend("force", opts, { desc = "Go to first message" })) + + vim.keymap.set("n", "G", function() + require("opencode.api.command").command("session.last") + end, vim.tbl_extend("force", opts, { desc = "Go to last message" })) + + vim.keymap.set("n", "", function() + require("opencode.api.command").command("session.interrupt") + end, vim.tbl_extend("force", opts, { desc = "Interrupt current session (esc)" })) +end + +return M diff --git a/lua/opencode/provider/snacks.lua b/lua/opencode/provider/snacks.lua index af356a52..f8ab8e66 100644 --- a/lua/opencode/provider/snacks.lua +++ b/lua/opencode/provider/snacks.lua @@ -25,7 +25,7 @@ function Snacks.health() return "`snacks.nvim` is not available.", { "Install `snacks.nvim` and enable `snacks.terminal.`", } - elseif not snacks.config.get("terminal", {}).enabled then + elseif not snacks and snacks.config.get("terminal", {}).enabled then return "`snacks.terminal` is not enabled.", { "Enable `snacks.terminal` in your `snacks.nvim` configuration.", diff --git a/lua/opencode/provider/terminal.lua b/lua/opencode/provider/terminal.lua index dab7acdd..f0a0809b 100644 --- a/lua/opencode/provider/terminal.lua +++ b/lua/opencode/provider/terminal.lua @@ -49,6 +49,8 @@ function Terminal:start() self.bufnr = vim.api.nvim_create_buf(true, false) self.winid = vim.api.nvim_open_win(self.bufnr, true, self.opts) + require("opencode.keymaps").apply(self.bufnr) + -- Redraw terminal buffer on initial render. -- Fixes empty columns on the right side. local auid diff --git a/plugin/keymaps.lua b/plugin/keymaps.lua new file mode 100644 index 00000000..d5b551a4 --- /dev/null +++ b/plugin/keymaps.lua @@ -0,0 +1,8 @@ +-- Apply buffer-local keymaps to opencode terminal buffers. +-- This handles the snacks provider (and any other provider using `opencode_terminal` filetype). +vim.api.nvim_create_autocmd("FileType", { + pattern = "opencode_terminal", + callback = function(ev) + require("opencode.keymaps").apply(ev.buf) + end, +}) From d1ee6eb110fdd526e2207e789c2507ce50482603 Mon Sep 17 00:00:00 2001 From: Nick van Dyke Date: Mon, 2 Feb 2026 10:11:20 -0700 Subject: [PATCH 2/7] move in readme --- README.md | 45 +++++++++++++-------------------------------- 1 file changed, 13 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 39ed33ac..d7d4f2b1 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,6 @@ Integrate the [opencode](https://github.com/sst/opencode) AI assistant with Neov vim.keymap.set({ "n", "x" }, "go", function() return require("opencode").operator("@this ") end, { desc = "Add range to opencode", expr = true }) vim.keymap.set("n", "goo", function() return require("opencode").operator("@this ") .. "_" end, { desc = "Add line to opencode", expr = true }) - -- The default / keymaps will work in the normal mode but it is possible to scroll opencode from any buffer: vim.keymap.set("n", "", function() require("opencode").command("session.half.page.up") end, { desc = "Scroll opencode up" }) vim.keymap.set("n", "", function() require("opencode").command("session.half.page.down") end, { desc = "Scroll opencode down" }) @@ -108,37 +107,6 @@ Select or reference prompts to review, explain, and improve your code: | `review` | Review `@this` for correctness and readability | | `test` | Add tests for `@this` | -### Keymaps - -`opencode.nvim` sets these buffer-local keymaps in opencode terminal buffers by default: - -| Keymap | Command | Description | -| ------- | ------------------------ | ---------------------------- | -| `` | `session.half.page.up` | Scroll up half page | -| `` | `session.half.page.down` | Scroll down half page | -| `` | `session.interrupt` | Interrup (same as esc press) | -| `gg` | `session.first` | Go to first message | -| `G` | `session.last` | Go to last message | - -You can customize or disable these keymaps: - -```lua -vim.g.opencode_opts = { - -- Customize keymaps - keymaps = { - n = { - [""] = { "session.half.page.up", desc = "Scroll up" }, - [""] = { "session.half.page.down", desc = "Scroll down" }, - ["gg"] = false, -- Disable this keymap - -- Add custom keymaps - [""] = { "session.new", desc = "New session" }, - }, - }, - -- Or disable all default keymaps - -- keymaps = false, -} -``` - ### Provider You can manually run `opencode` inside Neovim's CWD however you like and `opencode.nvim` will find it! @@ -276,6 +244,19 @@ vim.g.opencode_opts = { Please submit PRs adding new providers! 🙂 +#### Keymaps + +`opencode.nvim` sets these buffer-local keymaps in provider terminals for Neovim-like message navigation: + +| Keymap | Command | Description | +| ------- | ------------------------ | ---------------------------- | +| `` | `session.half.page.up` | Scroll up half page | +| `` | `session.half.page.down` | Scroll down half page | +| `` | `session.interrupt` | Interrup (same as esc press) | +| `gg` | `session.first` | Go to first message | +| `G` | `session.last` | Go to last message | + + ## 🚀 Usage ### ✍️ Ask — `require("opencode").ask()` From 017313f050487281ad93bee71977b833fbb8e6cd Mon Sep 17 00:00:00 2001 From: Nick van Dyke Date: Wed, 4 Feb 2026 20:22:00 -0700 Subject: [PATCH 3/7] fix typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d7d4f2b1..966205a5 100644 --- a/README.md +++ b/README.md @@ -252,7 +252,7 @@ Please submit PRs adding new providers! 🙂 | ------- | ------------------------ | ---------------------------- | | `` | `session.half.page.up` | Scroll up half page | | `` | `session.half.page.down` | Scroll down half page | -| `` | `session.interrupt` | Interrup (same as esc press) | +| `` | `session.interrupt` | Interrupt | | `gg` | `session.first` | Go to first message | | `G` | `session.last` | Go to last message | From ae4a3ce4fbac1d7e602f2355468133f787556609 Mon Sep 17 00:00:00 2001 From: Nick van Dyke Date: Wed, 4 Feb 2026 20:33:53 -0700 Subject: [PATCH 4/7] trigger filetype autocmd manually to apply keymaps to terminal provider --- lua/opencode/keymaps.lua | 29 ----------------------------- lua/opencode/provider/terminal.lua | 6 ++++-- plugin/keymaps.lua | 26 ++++++++++++++++++++++---- 3 files changed, 26 insertions(+), 35 deletions(-) delete mode 100644 lua/opencode/keymaps.lua diff --git a/lua/opencode/keymaps.lua b/lua/opencode/keymaps.lua deleted file mode 100644 index e3971528..00000000 --- a/lua/opencode/keymaps.lua +++ /dev/null @@ -1,29 +0,0 @@ -local M = {} - ----Apply buffer-local keymaps to the given buffer. ----@param bufnr integer The buffer number to apply keymaps to. -function M.apply(bufnr) - local opts = { buffer = bufnr } - - vim.keymap.set("n", "", function() - require("opencode.api.command").command("session.half.page.up") - end, vim.tbl_extend("force", opts, { desc = "Scroll up half page" })) - - vim.keymap.set("n", "", function() - require("opencode.api.command").command("session.half.page.down") - end, vim.tbl_extend("force", opts, { desc = "Scroll down half page" })) - - vim.keymap.set("n", "gg", function() - require("opencode.api.command").command("session.first") - end, vim.tbl_extend("force", opts, { desc = "Go to first message" })) - - vim.keymap.set("n", "G", function() - require("opencode.api.command").command("session.last") - end, vim.tbl_extend("force", opts, { desc = "Go to last message" })) - - vim.keymap.set("n", "", function() - require("opencode.api.command").command("session.interrupt") - end, vim.tbl_extend("force", opts, { desc = "Interrupt current session (esc)" })) -end - -return M diff --git a/lua/opencode/provider/terminal.lua b/lua/opencode/provider/terminal.lua index f0a0809b..ead61fb3 100644 --- a/lua/opencode/provider/terminal.lua +++ b/lua/opencode/provider/terminal.lua @@ -47,9 +47,11 @@ function Terminal:start() local previous_win = vim.api.nvim_get_current_win() self.bufnr = vim.api.nvim_create_buf(true, false) - self.winid = vim.api.nvim_open_win(self.bufnr, true, self.opts) + vim.api.nvim_set_option_value("filetype", "opencode_terminal", { buf = self.bufnr }) + -- Neovim doesn't automatically trigger FileType autocommands for terminal buffers + vim.api.nvim_exec_autocmds("FileType", { pattern = "opencode_terminal" }) - require("opencode.keymaps").apply(self.bufnr) + self.winid = vim.api.nvim_open_win(self.bufnr, true, self.opts) -- Redraw terminal buffer on initial render. -- Fixes empty columns on the right side. diff --git a/plugin/keymaps.lua b/plugin/keymaps.lua index d5b551a4..90a95c04 100644 --- a/plugin/keymaps.lua +++ b/plugin/keymaps.lua @@ -1,8 +1,26 @@ --- Apply buffer-local keymaps to opencode terminal buffers. --- This handles the snacks provider (and any other provider using `opencode_terminal` filetype). vim.api.nvim_create_autocmd("FileType", { pattern = "opencode_terminal", - callback = function(ev) - require("opencode.keymaps").apply(ev.buf) + callback = function(event) + local opts = { buffer = event.buf } + + vim.keymap.set("n", "", function() + require("opencode.api.command").command("session.half.page.up") + end, vim.tbl_extend("force", opts, { desc = "Scroll up half page" })) + + vim.keymap.set("n", "", function() + require("opencode.api.command").command("session.half.page.down") + end, vim.tbl_extend("force", opts, { desc = "Scroll down half page" })) + + vim.keymap.set("n", "gg", function() + require("opencode.api.command").command("session.first") + end, vim.tbl_extend("force", opts, { desc = "Go to first message" })) + + vim.keymap.set("n", "G", function() + require("opencode.api.command").command("session.last") + end, vim.tbl_extend("force", opts, { desc = "Go to last message" })) + + vim.keymap.set("n", "", function() + require("opencode.api.command").command("session.interrupt") + end, vim.tbl_extend("force", opts, { desc = "Interrupt current session (esc)" })) end, }) From 49b22187b09e8d4a975eaf8a1debfcc630480617 Mon Sep 17 00:00:00 2001 From: Dmitriy Kovalenko Date: Wed, 4 Feb 2026 21:01:28 -0800 Subject: [PATCH 5/7] fix keymap application --- lua/opencode/keymaps.lua | 29 +++++++++++++++++++++++++++++ lua/opencode/provider/terminal.lua | 16 ++++++++++++---- plugin/keymaps.lua | 26 -------------------------- 3 files changed, 41 insertions(+), 30 deletions(-) create mode 100644 lua/opencode/keymaps.lua delete mode 100644 plugin/keymaps.lua diff --git a/lua/opencode/keymaps.lua b/lua/opencode/keymaps.lua new file mode 100644 index 00000000..e3971528 --- /dev/null +++ b/lua/opencode/keymaps.lua @@ -0,0 +1,29 @@ +local M = {} + +---Apply buffer-local keymaps to the given buffer. +---@param bufnr integer The buffer number to apply keymaps to. +function M.apply(bufnr) + local opts = { buffer = bufnr } + + vim.keymap.set("n", "", function() + require("opencode.api.command").command("session.half.page.up") + end, vim.tbl_extend("force", opts, { desc = "Scroll up half page" })) + + vim.keymap.set("n", "", function() + require("opencode.api.command").command("session.half.page.down") + end, vim.tbl_extend("force", opts, { desc = "Scroll down half page" })) + + vim.keymap.set("n", "gg", function() + require("opencode.api.command").command("session.first") + end, vim.tbl_extend("force", opts, { desc = "Go to first message" })) + + vim.keymap.set("n", "G", function() + require("opencode.api.command").command("session.last") + end, vim.tbl_extend("force", opts, { desc = "Go to last message" })) + + vim.keymap.set("n", "", function() + require("opencode.api.command").command("session.interrupt") + end, vim.tbl_extend("force", opts, { desc = "Interrupt current session (esc)" })) +end + +return M diff --git a/lua/opencode/provider/terminal.lua b/lua/opencode/provider/terminal.lua index ead61fb3..6bb4f0a3 100644 --- a/lua/opencode/provider/terminal.lua +++ b/lua/opencode/provider/terminal.lua @@ -47,10 +47,6 @@ function Terminal:start() local previous_win = vim.api.nvim_get_current_win() self.bufnr = vim.api.nvim_create_buf(true, false) - vim.api.nvim_set_option_value("filetype", "opencode_terminal", { buf = self.bufnr }) - -- Neovim doesn't automatically trigger FileType autocommands for terminal buffers - vim.api.nvim_exec_autocmds("FileType", { pattern = "opencode_terminal" }) - self.winid = vim.api.nvim_open_win(self.bufnr, true, self.opts) -- Redraw terminal buffer on initial render. @@ -67,6 +63,18 @@ function Terminal:start() end, }) + -- because jobsttart runs with term=true neovim converts the created buffer + -- into a terminal buffer which resets the keymap so we have to wait until the buffer + -- will become a terminal to apply our local keymaps + local bufnr = self.bufnr + vim.api.nvim_create_autocmd("TermOpen", { + buffer = bufnr, + once = true, + callback = function() + require("opencode.keymaps").apply(bufnr) + end, + }) + vim.fn.jobstart(self.cmd, { term = true, on_exit = function() diff --git a/plugin/keymaps.lua b/plugin/keymaps.lua deleted file mode 100644 index 90a95c04..00000000 --- a/plugin/keymaps.lua +++ /dev/null @@ -1,26 +0,0 @@ -vim.api.nvim_create_autocmd("FileType", { - pattern = "opencode_terminal", - callback = function(event) - local opts = { buffer = event.buf } - - vim.keymap.set("n", "", function() - require("opencode.api.command").command("session.half.page.up") - end, vim.tbl_extend("force", opts, { desc = "Scroll up half page" })) - - vim.keymap.set("n", "", function() - require("opencode.api.command").command("session.half.page.down") - end, vim.tbl_extend("force", opts, { desc = "Scroll down half page" })) - - vim.keymap.set("n", "gg", function() - require("opencode.api.command").command("session.first") - end, vim.tbl_extend("force", opts, { desc = "Go to first message" })) - - vim.keymap.set("n", "G", function() - require("opencode.api.command").command("session.last") - end, vim.tbl_extend("force", opts, { desc = "Go to last message" })) - - vim.keymap.set("n", "", function() - require("opencode.api.command").command("session.interrupt") - end, vim.tbl_extend("force", opts, { desc = "Interrupt current session (esc)" })) - end, -}) From dcf117690f12562b7a60722953f01f36ffc3dcfb Mon Sep 17 00:00:00 2001 From: Nick van Dyke Date: Thu, 5 Feb 2026 09:28:05 -0700 Subject: [PATCH 6/7] apply to snacks.terminal --- lua/opencode/config.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lua/opencode/config.lua b/lua/opencode/config.lua index 2b1a3d78..58569704 100644 --- a/lua/opencode/config.lua +++ b/lua/opencode/config.lua @@ -134,6 +134,9 @@ local defaults = { win = { position = "right", enter = false, -- Stay in the editor after opening the terminal + on_buf = function(win) + require("opencode.keymaps").apply(win.buf) + end, wo = { winbar = "", -- Title is unnecessary - `opencode` TUI has its own footer }, From 00f8e9974bfb1c7d1dc2c86ad5fd12b1825537e8 Mon Sep 17 00:00:00 2001 From: Nick van Dyke Date: Thu, 5 Feb 2026 10:11:36 -0700 Subject: [PATCH 7/7] fix lint --- lua/opencode/provider/terminal.lua | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lua/opencode/provider/terminal.lua b/lua/opencode/provider/terminal.lua index 6bb4f0a3..8b1b61c6 100644 --- a/lua/opencode/provider/terminal.lua +++ b/lua/opencode/provider/terminal.lua @@ -66,12 +66,11 @@ function Terminal:start() -- because jobsttart runs with term=true neovim converts the created buffer -- into a terminal buffer which resets the keymap so we have to wait until the buffer -- will become a terminal to apply our local keymaps - local bufnr = self.bufnr vim.api.nvim_create_autocmd("TermOpen", { - buffer = bufnr, + buffer = self.bufnr, once = true, - callback = function() - require("opencode.keymaps").apply(bufnr) + callback = function(event) + require("opencode.keymaps").apply(event.buf) end, })