diff --git a/AGENTS.md b/AGENTS.md index f99d9158..d798d55b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,7 +6,7 @@ A Neovim Lua plugin that bridges Neovim and the `opencode` CLI (external binary) ## Entrypoints -- **Public API**: `lua/opencode.lua` — exports `ask()`, `select()`, `prompt()`, `command()`, `operator()`, `format()`, `statusline` +- **Public API**: `lua/opencode.lua` — exports `ask()`, `select()`, `prompt()`, `command()`, `operator()`, `rename_session()`, `format()`, `statusline` - **Config**: `vim.g.opencode_opts` global (not a `setup()` call); merged with defaults from `lua/opencode/config.lua` - **Plugin files**: `plugin/highlights.lua` sets highlight groups; `plugin/events/` registers four autocmd groups (`OpencodeReload`, `OpencodeStatus`, `OpencodePermissions`, `OpencodeEdits`) that listen for `OpencodeEvent:*` User events to reload edited buffers, update statusline, display permission requests, and diff edit proposals diff --git a/README.md b/README.md index 256084b4..ce006e89 100644 --- a/README.md +++ b/README.md @@ -296,6 +296,13 @@ Prompt OpenCode. Wraps Prompt as an operator, supporting ranges and dot-repeat. +### Rename Session — `require("opencode").rename_session()` + +Rename the current session. + +- Prompts for a new title, pre-filled with the current one. +- Supports [snacks.input](https://github.com/folke/snacks.nvim/blob/main/docs/input.md). + ### Command — `require("opencode").command()` Command OpenCode: @@ -314,11 +321,14 @@ Command OpenCode: | `session.new` | Start new session | | `session.page.up` | Scroll messages up one page | | `session.page.down` | Scroll messages down one page | +| `session.rename` | Rename session | | `session.select` | Select session | | `session.share` | Share current session | | `session.redo` | Redo last undone action in current session | | `session.undo` | Undo last action in current session | +`session.rename` and `session.select` aren't OpenCode TUI commands — they're handled by opencode.nvim itself, opening the rename input and session picker respectively. + ## 👀 Events opencode.nvim forwards the connected OpenCode's Server-Sent-Events as an `OpencodeEvent` autocmd: diff --git a/lua/opencode.lua b/lua/opencode.lua index 8cef9bb0..42fba8b1 100644 --- a/lua/opencode.lua +++ b/lua/opencode.lua @@ -108,6 +108,20 @@ function M.operator(prompt) return "g@" end +---Rename the current session. +--- +--- - Prompts for a new title pre-filled with the current one. +--- +---@param opts? opencode.rename_session.Opts Override configured options for this call. +function M.rename_session(opts) + require("opencode.server.discovery") + .get() + :next(function(server) + return require("opencode.ui.rename_session").rename_session(server, opts) + end) + :catch(on_error) +end + M.format = require("opencode.context").format return M diff --git a/lua/opencode/api/command.lua b/lua/opencode/api/command.lua index 05552619..9571da12 100644 --- a/lua/opencode/api/command.lua +++ b/lua/opencode/api/command.lua @@ -4,6 +4,13 @@ local M = {} ---@param server opencode.server.Server ---@return Promise function M.command(command, server) + if command == "session.rename" then + return require("opencode.ui.rename_session").rename_session(server) + elseif command == "session.select" then + return require("opencode.ui.select_session").select_session(server):next(function(session) + return server:select_session(session.id) + end) + end return server:tui_execute_command(command):next(function() if command == "session.interrupt" then -- Evidently OpenCode only uses this command for their "double-tap Esc to interrupt" user keybind. diff --git a/lua/opencode/config.lua b/lua/opencode/config.lua index ab6f80a8..3327bc4b 100644 --- a/lua/opencode/config.lua +++ b/lua/opencode/config.lua @@ -2,6 +2,7 @@ ---@field server? opencode.server.Opts OpenCode server connection options. ---@field contexts? table Context placeholders and their builders. ---@field ask? opencode.ask.Opts Options for `ask()`. Supports [snacks.input](https://github.com/folke/snacks.nvim/blob/main/docs/input.md). +---@field rename_session? opencode.rename_session.Opts Options for `rename_session()`. Supports [snacks.input](https://github.com/folke/snacks.nvim/blob/main/docs/input.md). ---@field select? opencode.select.Opts Options and items for `select()`. Supports [snacks.picker](https://github.com/folke/snacks.nvim/blob/main/docs/picker.md). ---@field events? opencode.events.Opts Options for handling OpenCode events. @@ -88,6 +89,7 @@ local defaults = { ["session.interrupt"] = "Interrupt current session", ["session.new"] = "Start new session", ["session.redo"] = "Redo last undone action in current session", + ["session.rename"] = "Rename session", ["session.select"] = "Select session", ["session.undo"] = "Undo last action in current session", }, diff --git a/lua/opencode/server/init.lua b/lua/opencode/server/init.lua index e51f7b53..2e265b88 100644 --- a/lua/opencode/server/init.lua +++ b/lua/opencode/server/init.lua @@ -14,6 +14,7 @@ ---@field url string ---@field cwd string ---@field title string +---@field session_id? string The session currently active in the TUI. ---@field subagents opencode.server.Agent[] ---@field subscription_job_id? number ---@field heartbeat_timer? uv.uv_timer_t @@ -61,6 +62,7 @@ Server.__index = Server ---| { type: "server.instance.disposed" } ---| { type: "session.status", properties: { status: { type: "idle" | "busy" | "error" } } } ---| { type: "tui.command.execute", properties: { command: string } } +---| { type: "tui.session.select", properties: { sessionID: string } } ---| { type: string, properties: table } ---Attempt to connect to an OpenCode server and fetch its health and details. @@ -92,6 +94,7 @@ function Server.new(url) function(results) ---@param results { [1]: { directory: string, worktree: string }, [2]: opencode.server.Session[], [3]: opencode.server.Agent } self.cwd = results[1].directory or results[1].worktree self.title = results[2][1] and results[2][1].title or "" + self.session_id = results[2][1] and results[2][1].id self.subagents = vim.tbl_filter(function(agent) ---@param agent opencode.server.Agent return agent.mode == "subagent" end, results[3]) @@ -110,7 +113,7 @@ function Server:display_name() end ---@param path string ----@param method "GET" | "POST" +---@param method "GET" | "POST" | "PATCH" ---@param body table? ---@param on_success fun(response: table) ---@param on_error fun(msg: string, code: number, status: number?) @@ -293,11 +296,21 @@ end ---@param session_id string ---@return Promise function Server:select_session(session_id) + self.session_id = session_id return require("opencode.promise").new(function(resolve, reject) self:curl("/tui/select-session", "POST", { sessionID = session_id }, resolve, reject) end) end +---@param session_id string +---@param title string New title for the session. +---@return Promise +function Server:update_session(session_id, title) + return require("opencode.promise").new(function(resolve, reject) + self:curl("/session/" .. session_id, "PATCH", { title = title }, resolve, reject) + end) +end + ---@return Promise<{ directory: string, worktree: string }> function Server:get_path() return require("opencode.promise").new(function(resolve, reject) @@ -352,6 +365,8 @@ function Server:connect() resolve(self) elseif response.type == "server.instance.disposed" then self:disconnect() + elseif response.type == "tui.session.select" then + self.session_id = response.properties.sessionID end require("opencode.events").emit(response, self) diff --git a/lua/opencode/ui/rename_session.lua b/lua/opencode/ui/rename_session.lua new file mode 100644 index 00000000..b8169434 --- /dev/null +++ b/lua/opencode/ui/rename_session.lua @@ -0,0 +1,48 @@ +---@class opencode.rename_session.Opts +---@field prompt? string Text of the prompt. +---@field snacks? snacks.input.Opts Options for [snacks.input](https://github.com/folke/snacks.nvim/blob/main/docs/input.md). + +local M = {} + +---@param server opencode.server.Server +---@param opts? opencode.rename_session.Opts Override configured options for this call. +---@return Promise +function M.rename_session(server, opts) + local config = require("opencode.config") + ---@type snacks.input.Opts + local input_opts = { + prompt = "Rename session to: ", + } + input_opts = vim.tbl_deep_extend("keep", config.opts.rename_session or {}, input_opts) + input_opts = vim.tbl_deep_extend("force", input_opts, opts or {}) + + local snacks_ok, snacks = pcall(require, "snacks") + if snacks_ok and snacks.config.get("input", {}).enabled then + input_opts = vim.tbl_deep_extend("keep", input_opts, input_opts.snacks or {}) + end + input_opts.snacks = nil + + return server:get_sessions():next(function(sessions) + local current + for _, session in ipairs(sessions) do + if session.id == server.session_id then + current = session + break + end + end + current = current or sessions[1] + if not current then + return require("opencode.promise").resolve(nil) + end + + input_opts.default = current.title + return require("opencode.promise.ui").input(input_opts):next(function(title) + if not title or title == "" then + return require("opencode.promise").resolve(nil) + end + return server:update_session(current.id, title) + end) + end) +end + +return M diff --git a/lua/opencode/ui/select.lua b/lua/opencode/ui/select.lua index b96510af..fa1d82ce 100644 --- a/lua/opencode/ui/select.lua +++ b/lua/opencode/ui/select.lua @@ -149,13 +149,7 @@ function M.select(context, opts) if choice.__type == "prompt" then return require("opencode.api.prompt").prompt(choice.text, context) elseif choice.__type == "command" then - if choice.name == "session.select" then - return require("opencode.ui.select_session").select_session(context.server):next(function(session) - return context.server:select_session(session.id) - end) - else - return require("opencode.api.command").command(choice.name, context.server) - end + return require("opencode.api.command").command(choice.name, context.server) elseif choice.__type == "server" then if choice.name == "server.connect" then return require("opencode.server.discovery")