diff --git a/lua/sidekick/cli/init.lua b/lua/sidekick/cli/init.lua index 9c0e9b5..8011ca8 100644 --- a/lua/sidekick/cli/init.lua +++ b/lua/sidekick/cli/init.lua @@ -12,6 +12,8 @@ local M = {} ---@field prompt? string ---@field text? sidekick.Text[] +---@alias sidekick.cli.Mode "new"|"continue"|"resume" + ---@class sidekick.cli.Config ---@field cmd string[] Command to run the CLI tool ---@field env? table Environment variables to set when running the command @@ -21,12 +23,15 @@ local M = {} ---@field mux_focus? boolean wether the tool needs to be focused in order to receive input ---@field format? fun(text:sidekick.Text[], str:string):string? ---@field native_scroll? boolean whether the tool handles scrolling natively +---@field continue? string[] Extra args appended to `cmd` to continue the most recent session +---@field resume? string[] Extra args appended to `cmd` to resume a session (typically via the tool's own picker) ---@class sidekick.cli.Show ---@field name? string ---@field focus? boolean ---@field filter? sidekick.cli.Filter ---@field all? boolean +---@field mode? sidekick.cli.Mode|false mode used when starting a new session (bypasses the picker) ---@class sidekick.cli.Hide ---@field name? string @@ -91,6 +96,7 @@ function M.show(opts) attach = true, filter = opts.filter, focus = opts.focus, + mode = opts.mode, show = true, }) end @@ -112,6 +118,7 @@ function M.toggle(opts) end, { attach = true, filter = opts.filter, + mode = opts.mode, }) end diff --git a/lua/sidekick/cli/session/init.lua b/lua/sidekick/cli/session/init.lua index 1c59892..b4c3dd4 100644 --- a/lua/sidekick/cli/session/init.lua +++ b/lua/sidekick/cli/session/init.lua @@ -75,10 +75,18 @@ function B.sessions() error("Backend:sessions() not implemented") end ----@param state sidekick.cli.session.Opts +---@param state sidekick.cli.session.Opts|{mode?:sidekick.cli.Mode} function M.new(state) local tool = state.tool tool = type(tool) == "string" and Config.get_tool(tool) or tool --[[@as sidekick.cli.Tool]] + local mode = state.mode --[[@as sidekick.cli.Mode?]] + if mode and mode ~= "new" then + local extra = tool.config[mode] + if extra and #extra > 0 then + local cmd = vim.list_extend(vim.deepcopy(tool.cmd), extra) + tool = tool:clone({ cmd = cmd }) + end + end local backend = state.backend or (Config.cli.mux.enabled and Config.cli.mux.backend or "terminal") local super = assert(M.backends[backend], "unknown backend: " .. backend) local meta = getmetatable(state) diff --git a/lua/sidekick/cli/state.lua b/lua/sidekick/cli/state.lua index 01f1f3e..1428e3b 100644 --- a/lua/sidekick/cli/state.lua +++ b/lua/sidekick/cli/state.lua @@ -13,6 +13,7 @@ local M = {} ---@field session? sidekick.cli.Session ---@field started? boolean ---@field terminal? sidekick.cli.Terminal +---@field mode? sidekick.cli.Mode ---@class sidekick.cli.Filter ---@field attached? boolean @@ -30,6 +31,7 @@ local M = {} ---@field focus? boolean ---@field attach? boolean ---@field all? boolean +---@field mode? sidekick.cli.Mode|false ---@param t sidekick.cli.State ---@param filter? sidekick.cli.Filter @@ -137,6 +139,45 @@ function M.get(filter) return ret end +---@param tool sidekick.cli.Tool +---@param cb fun(mode?: sidekick.cli.Mode) +---@param forced? sidekick.cli.Mode|false +local function pick_mode(tool, cb, forced) + local has_continue = type(tool.config.continue) == "table" and #tool.config.continue > 0 + local has_resume = type(tool.config.resume) == "table" and #tool.config.resume > 0 + if not (has_continue or has_resume) then + return cb(nil) + end + local default = forced == nil and Config.cli.resume or forced + if default == false or default == "new" then + return cb(nil) + elseif default == "continue" and has_continue then + return cb("continue") + elseif default == "resume" and has_resume then + return cb("resume") + end + local choices = { "new" } ---@type sidekick.cli.Mode[] + if has_continue then + choices[#choices + 1] = "continue" + end + if has_resume then + choices[#choices + 1] = "resume" + end + local labels = { + new = "new session", + continue = "continue most recent session", + resume = "resume a session (pick in tool)", + } + vim.ui.select(choices, { + prompt = ("Start `%s`:"):format(tool.name), + format_item = function(m) + return labels[m] + end, + }, function(choice) + cb(choice) + end) +end + --- Executes a callback with one or more attached sessions. ---@param cb fun(state: sidekick.cli.State, attached?: boolean):any? ---@param opts? sidekick.cli.With @@ -149,8 +190,18 @@ function M.with(cb, opts) if not state then return end - local ret, attached = M.attach(state, { show = opts.show, focus = opts.focus }) - cb(ret, attached) + local function proceed() + local ret, attached = M.attach(state, { show = opts.show, focus = opts.focus }) + cb(ret, attached) + end + if state.session == nil then + pick_mode(state.tool, function(mode) + state.mode = mode + proceed() + end, opts.mode) + else + proceed() + end end) local filter_attached = Util.merge(opts.filter, { attached = true }) @@ -182,7 +233,7 @@ function M.attach(state, opts) local tool = state.tool -- if the session is already attached, the below is a no-op - local session = state.session or Session.new({ tool = tool.name }) + local session = state.session or Session.new({ tool = tool.name, mode = state.mode }) session = Session.attach(session) state = M.get_state(session) -- update state diff --git a/lua/sidekick/config.lua b/lua/sidekick/config.lua index 564617e..fd953d6 100644 --- a/lua/sidekick/config.lua +++ b/lua/sidekick/config.lua @@ -33,6 +33,10 @@ local defaults = { -- Work with AI cli tools directly from within Neovim cli = { watch = true, -- notify Neovim of file changes done by AI CLI tools + -- When starting a tool that supports resume/continue, pick how to start it: + -- "ask" (default): show a picker when the tool has `continue` or `resume` defined + -- "new" / "continue" / "resume": always start with that mode, no picker + resume = "ask", ---@type "ask"|"new"|"continue"|"resume" ---@class sidekick.win.Opts win = { --- This is run when a new terminal is created, before starting it.