From 1b3d94ce153536a5bed757ae84906d7e93b6b4b4 Mon Sep 17 00:00:00 2001 From: Ethan Callanan Date: Thu, 26 Feb 2026 11:30:56 -0500 Subject: [PATCH 1/4] feat(ask): add popup buffer input option --- README.md | 2 + lua/opencode/config.lua | 19 ++++ lua/opencode/ui/ask/init.lua | 197 ++++++++++++++++++++++++++++++++++- 3 files changed, 216 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4a114a9..efa9393 100644 --- a/README.md +++ b/README.md @@ -185,6 +185,8 @@ Input a prompt for `opencode`. - Press `` to browse recent asks. - Highlights and completes contexts and `opencode` subagents. - Press `` to trigger built-in completion. +- Set `opts.ask.capture = "buffer"` for a larger centered floating buffer. + - Configure via `opts.ask.buffer` (for example: `submit_keys`, `cancel_keys`, `submit_on_write`, `linewrap`). - End the prompt with `\n` to append instead of submit. - Additionally, when using `snacks.input`: - Press `` to append instead of submit. diff --git a/lua/opencode/config.lua b/lua/opencode/config.lua index 6035a23..7b407b9 100644 --- a/lua/opencode/config.lua +++ b/lua/opencode/config.lua @@ -79,8 +79,27 @@ local defaults = { test = { prompt = "Add tests for @this", submit = true }, }, ask = { + capture = "input", prompt = "Ask opencode: ", completion = "customlist,v:lua.opencode_completion", + buffer = { + width_ratio = 0.7, + height_ratio = 0.3, + min_width = 60, + min_height = 8, + border = "rounded", + title_pos = "center", + linewrap = false, + submit_on_write = false, + start_insert = true, + submit_keys = { + n = { "" }, + i = { "" }, + }, + cancel_keys = { + n = { "q", "" }, + }, + }, snacks = { icon = "󰚩 ", win = { diff --git a/lua/opencode/ui/ask/init.lua b/lua/opencode/ui/ask/init.lua index a7edafc..344ec06 100644 --- a/lua/opencode/ui/ask/init.lua +++ b/lua/opencode/ui/ask/init.lua @@ -7,9 +7,192 @@ local M = {} ---Text of the prompt. ---@field prompt? string --- +---Where to capture ask input. +---`"input"` uses `vim.ui.input`. +---`"buffer"` uses a centered floating multi-line buffer. +---@field capture? "input"|"buffer" +--- +---Options for buffer capture mode. +---@field buffer? opencode.ask.BufferOpts +--- ---Options for [`snacks.input`](https://github.com/folke/snacks.nvim/blob/main/docs/input.md). ---@field snacks? snacks.input.Opts +---@class opencode.ask.BufferOpts +---@field width_ratio? number +---@field height_ratio? number +---@field min_width? number +---@field min_height? number +---@field border? string +---@field title_pos? "left"|"center"|"right" +---@field linewrap? boolean +---@field submit_on_write? boolean +---@field start_insert? boolean +---@field submit_keys? table +---@field cancel_keys? table + +---@param keys string|string[]|nil +---@return string[] +local function normalize_keys(keys) + if type(keys) == "string" then + return { keys } + end + if vim.islist(keys) then + return keys + end + return {} +end + +---@param buf number +---@param mode_keys table|nil +---@param callback function +local function set_mode_keymaps(buf, mode_keys, callback) + if not mode_keys then + return + end + for mode, keys in pairs(mode_keys) do + for _, lhs in ipairs(normalize_keys(keys)) do + vim.keymap.set(mode, lhs, callback, { buffer = buf, nowait = true, silent = true }) + end + end +end + +---@param buf number +---@param context opencode.Context +---@param ns number +---@param agents opencode.cli.client.Agent[] +local function highlight_buffer(buf, context, ns, agents) + vim.api.nvim_buf_clear_namespace(buf, ns, 0, -1) + local lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false) + local text = table.concat(lines, "\n") + local rendered = context:render(text, agents) + local extmarks = context.extmarks(rendered.input) + + for _, extmark in ipairs(extmarks) do + vim.api.nvim_buf_set_extmark(buf, ns, (extmark.row or 1) - 1, extmark.col, { + end_col = extmark.end_col, + hl_group = extmark.hl_group, + }) + end +end + +---@param default? string +---@param context opencode.Context +---@param ask_opts opencode.ask.Opts +---@param server opencode.cli.server.Server +---@return Promise +local function buffer_input(default, context, ask_opts, server) + local Promise = require("opencode.promise") + local buffer_opts = ask_opts.buffer or {} + + return Promise.new(function(resolve, reject) + local width = math.max(buffer_opts.min_width or 60, math.floor(vim.o.columns * (buffer_opts.width_ratio or 0.7))) + local height = math.max(buffer_opts.min_height or 8, math.floor(vim.o.lines * (buffer_opts.height_ratio or 0.3))) + local row = math.max(1, math.floor((vim.o.lines - height) / 2) - 1) + local col = math.max(0, math.floor((vim.o.columns - width) / 2)) + + local buf = vim.api.nvim_create_buf(false, true) + local win = vim.api.nvim_open_win(buf, true, { + relative = "editor", + width = width, + height = height, + row = row, + col = col, + style = "minimal", + border = buffer_opts.border or "rounded", + title = " " .. (ask_opts.prompt or "Ask opencode: ") .. " ", + title_pos = buffer_opts.title_pos or "center", + }) + + vim.bo[buf].bufhidden = "wipe" + vim.bo[buf].filetype = "opencode_ask" + vim.bo[buf].buftype = "" + vim.bo[buf].swapfile = false + vim.wo[win].wrap = buffer_opts.linewrap == true + vim.wo[win].linebreak = buffer_opts.linewrap == true + + if buffer_opts.linewrap == true then + vim.keymap.set("n", "j", "gj", { buffer = buf, nowait = true, silent = true }) + vim.keymap.set("n", "k", "gk", { buffer = buf, nowait = true, silent = true }) + vim.keymap.set("n", "0", "g0", { buffer = buf, nowait = true, silent = true }) + vim.keymap.set("n", "^", "g^", { buffer = buf, nowait = true, silent = true }) + vim.keymap.set("n", "$", "g$", { buffer = buf, nowait = true, silent = true }) + end + + local initial = default and vim.split(default, "\n", { plain = true, trimempty = false }) or { "" } + if #initial == 0 then + initial = { "" } + end + if default and default ~= "" and initial[#initial] ~= "" then + table.insert(initial, "") + end + vim.api.nvim_buf_set_lines(buf, 0, -1, false, initial) + vim.api.nvim_win_set_cursor(win, { #initial, 0 }) + + local ns = vim.api.nvim_create_namespace("opencode_ask_highlight") + highlight_buffer(buf, context, ns, server.subagents) + + local done = false + local function finish_submit() + if done then + return + end + done = true + local lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false) + local value = table.concat(lines, "\n") + if vim.api.nvim_win_is_valid(win) then + vim.api.nvim_win_close(win, true) + end + if value == "" then + reject() + else + resolve(value) + end + end + local function finish_cancel() + if done then + return + end + done = true + if vim.api.nvim_win_is_valid(win) then + vim.api.nvim_win_close(win, true) + end + reject() + end + + vim.api.nvim_create_autocmd({ "TextChanged", "TextChangedI" }, { + buffer = buf, + callback = function() + highlight_buffer(buf, context, ns, server.subagents) + end, + }) + + vim.api.nvim_create_autocmd("WinClosed", { + once = true, + pattern = tostring(win), + callback = finish_cancel, + }) + + if buffer_opts.submit_on_write == true then + vim.api.nvim_create_autocmd("BufWriteCmd", { + buffer = buf, + callback = finish_submit, + }) + end + + set_mode_keymaps(buf, buffer_opts.submit_keys or { n = { "" }, i = { "" } }, finish_submit) + set_mode_keymaps(buf, buffer_opts.cancel_keys or { n = { "q", "" } }, finish_cancel) + + vim.lsp.start(require("opencode.ui.ask.cmp"), { + bufnr = buf, + }) + + if buffer_opts.start_insert ~= false then + vim.cmd("startinsert") + end + end) +end + ---Prompt for input with `vim.ui.input`, with context- and server-aware completion. --- ---@param default? string Text to pre-fill the input with. @@ -21,6 +204,11 @@ function M.ask(default, context) return require("opencode.cli.server") .get() :next(function(server) ---@param server opencode.cli.server.Server + local ask_opts = require("opencode.config").opts.ask or {} + if ask_opts.capture == "buffer" then + return buffer_input(default, context, ask_opts, server) + end + ---@type snacks.input.Opts local input_opts = { default = default, @@ -29,10 +217,15 @@ function M.ask(default, context) return context.input_highlight(rendered.input) end, } + + local input_ask_opts = vim.deepcopy(ask_opts) + input_ask_opts.capture = nil + input_ask_opts.buffer = nil + -- Nest `snacks.input` options under `opts.ask.snacks` for consistency with other `snacks`-exclusive config, -- and to keep its fields optional. Double-merge is kinda ugly but seems like the lesser evil. - input_opts = vim.tbl_deep_extend("force", input_opts, require("opencode.config").opts.ask) - input_opts = vim.tbl_deep_extend("force", input_opts, require("opencode.config").opts.ask.snacks) + input_opts = vim.tbl_deep_extend("force", input_opts, input_ask_opts) + input_opts = vim.tbl_deep_extend("force", input_opts, ask_opts.snacks or {}) return Promise.input(input_opts) end) From e365c58e70fe50f44cb9b1735a7f09eec177add1 Mon Sep 17 00:00:00 2001 From: Ethan Callanan Date: Thu, 26 Feb 2026 13:44:32 -0500 Subject: [PATCH 2/4] Re-order --- lua/opencode/config.lua | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/lua/opencode/config.lua b/lua/opencode/config.lua index 7b407b9..9028e4d 100644 --- a/lua/opencode/config.lua +++ b/lua/opencode/config.lua @@ -82,24 +82,6 @@ local defaults = { capture = "input", prompt = "Ask opencode: ", completion = "customlist,v:lua.opencode_completion", - buffer = { - width_ratio = 0.7, - height_ratio = 0.3, - min_width = 60, - min_height = 8, - border = "rounded", - title_pos = "center", - linewrap = false, - submit_on_write = false, - start_insert = true, - submit_keys = { - n = { "" }, - i = { "" }, - }, - cancel_keys = { - n = { "q", "" }, - }, - }, snacks = { icon = "󰚩 ", win = { @@ -139,6 +121,25 @@ local defaults = { end, }, }, + buffer = { + width_ratio = 0.7, + height_ratio = 0.3, + min_width = 60, + min_height = 8, + border = "rounded", + title_pos = "center", + linewrap = false, + submit_on_write = false, + start_insert = true, + submit_keys = { + n = { "" }, + i = { "" }, + }, + cancel_keys = { + n = { "q", "" }, + i = { "" }, + }, + }, }, select = { prompt = "opencode: ", From 5ca22458fef95538af21e193c46d67dd344cdca8 Mon Sep 17 00:00:00 2001 From: Ethan Callanan Date: Thu, 26 Feb 2026 14:32:24 -0500 Subject: [PATCH 3/4] Fix submit on write --- lua/opencode/ui/ask/init.lua | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/lua/opencode/ui/ask/init.lua b/lua/opencode/ui/ask/init.lua index 344ec06..bbd8043 100644 --- a/lua/opencode/ui/ask/init.lua +++ b/lua/opencode/ui/ask/init.lua @@ -91,7 +91,10 @@ local function buffer_input(default, context, ask_opts, server) local row = math.max(1, math.floor((vim.o.lines - height) / 2) - 1) local col = math.max(0, math.floor((vim.o.columns - width) / 2)) - local buf = vim.api.nvim_create_buf(false, true) + local submit_on_write = buffer_opts.submit_on_write == true + local temp_file = submit_on_write and vim.fn.tempname() or nil + + local buf = vim.api.nvim_create_buf(false, not submit_on_write) local win = vim.api.nvim_open_win(buf, true, { relative = "editor", width = width, @@ -108,6 +111,10 @@ local function buffer_input(default, context, ask_opts, server) vim.bo[buf].filetype = "opencode_ask" vim.bo[buf].buftype = "" vim.bo[buf].swapfile = false + + if temp_file then + vim.api.nvim_buf_set_name(buf, temp_file) + end vim.wo[win].wrap = buffer_opts.linewrap == true vim.wo[win].linebreak = buffer_opts.linewrap == true @@ -143,6 +150,9 @@ local function buffer_input(default, context, ask_opts, server) if vim.api.nvim_win_is_valid(win) then vim.api.nvim_win_close(win, true) end + if temp_file then + pcall(vim.fn.delete, temp_file) + end if value == "" then reject() else @@ -157,6 +167,9 @@ local function buffer_input(default, context, ask_opts, server) if vim.api.nvim_win_is_valid(win) then vim.api.nvim_win_close(win, true) end + if temp_file then + pcall(vim.fn.delete, temp_file) + end reject() end @@ -173,8 +186,8 @@ local function buffer_input(default, context, ask_opts, server) callback = finish_cancel, }) - if buffer_opts.submit_on_write == true then - vim.api.nvim_create_autocmd("BufWriteCmd", { + if submit_on_write then + vim.api.nvim_create_autocmd("BufWritePost", { buffer = buf, callback = finish_submit, }) From f443b2665631eeea8c3779691c6268eb50af35fd Mon Sep 17 00:00:00 2001 From: Ethan Callanan Date: Thu, 26 Feb 2026 14:34:46 -0500 Subject: [PATCH 4/4] Fix lsp error --- lua/opencode/ui/ask/init.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/opencode/ui/ask/init.lua b/lua/opencode/ui/ask/init.lua index bbd8043..35b520e 100644 --- a/lua/opencode/ui/ask/init.lua +++ b/lua/opencode/ui/ask/init.lua @@ -37,7 +37,7 @@ local function normalize_keys(keys) if type(keys) == "string" then return { keys } end - if vim.islist(keys) then + if type(keys) == "table" and vim.islist(keys) then return keys end return {}