From 972b3e1859fcfe47a55a1095824dc7dafebeb7bb Mon Sep 17 00:00:00 2001 From: Noah Bauer Date: Thu, 23 Jul 2026 12:54:33 +0200 Subject: [PATCH 1/3] feat: add split before option --- README.md | 1 + lua/sidekick/cli/session/tmux.lua | 3 +++ lua/sidekick/config.lua | 1 + 3 files changed, 5 insertions(+) diff --git a/README.md b/README.md index 675ee135..cef78ecd 100644 --- a/README.md +++ b/README.md @@ -326,6 +326,7 @@ local defaults = { create = "terminal", ---@type "terminal"|"window"|"split" split = { vertical = true, -- vertical or horizontal split + before = false, -- place the split before the current pane (left when vertical, above when horizontal) size = 0.5, -- size of the split (0-1 for percentage) }, }, diff --git a/lua/sidekick/cli/session/tmux.lua b/lua/sidekick/cli/session/tmux.lua index 95dbbb48..9c98ca38 100644 --- a/lua/sidekick/cli/session/tmux.lua +++ b/lua/sidekick/cli/session/tmux.lua @@ -44,6 +44,9 @@ function M:start() elseif Config.cli.mux.create == "split" then local cmd = { "tmux", "split-window", "-dP", "-c", self.cwd, "-F", PANE_FORMAT } cmd[#cmd + 1] = Config.cli.mux.split.vertical and "-h" or "-v" + if Config.cli.mux.split.before then + cmd[#cmd + 1] = "-b" + end local size = Config.cli.mux.split.size vim.list_extend(cmd, { "-l", tostring(size <= 1 and ((size * 100) .. "%") or size) }) self:add_cmd(cmd) diff --git a/lua/sidekick/config.lua b/lua/sidekick/config.lua index d30e92e8..c6a95c8f 100644 --- a/lua/sidekick/config.lua +++ b/lua/sidekick/config.lua @@ -93,6 +93,7 @@ local defaults = { create = "terminal", ---@type "terminal"|"window"|"split" split = { vertical = true, -- vertical or horizontal split + before = false, -- place the split before the current pane (left when vertical, above when horizontal) size = 0.5, -- size of the split (0-1 for percentage) }, -- max lines to capture when dumping a multiplexer pane for scrollback support From 23164f7c743156781fa09c9bbad3754baf094092 Mon Sep 17 00:00:00 2001 From: Noah Bauer Date: Thu, 23 Jul 2026 13:04:26 +0200 Subject: [PATCH 2/3] feat: add close_on_exit option --- README.md | 1 + lua/sidekick/cli/session/tmux.lua | 17 +++++++++++++++++ lua/sidekick/config.lua | 1 + 3 files changed, 19 insertions(+) diff --git a/README.md b/README.md index cef78ecd..05ce50c6 100644 --- a/README.md +++ b/README.md @@ -328,6 +328,7 @@ local defaults = { vertical = true, -- vertical or horizontal split before = false, -- place the split before the current pane (left when vertical, above when horizontal) size = 0.5, -- size of the split (0-1 for percentage) + close_on_exit = false, -- close the split when Neovim exits }, }, --- Actual cli tool config is loaded from the runtime path `sk/cli/{tool}.lua` and merged with the config below. diff --git a/lua/sidekick/cli/session/tmux.lua b/lua/sidekick/cli/session/tmux.lua index 9c98ca38..d20c5272 100644 --- a/lua/sidekick/cli/session/tmux.lua +++ b/lua/sidekick/cli/session/tmux.lua @@ -51,10 +51,27 @@ function M:start() vim.list_extend(cmd, { "-l", tostring(size <= 1 and ((size * 100) .. "%") or size) }) self:add_cmd(cmd) self:spawn(cmd) + if Config.cli.mux.split.close_on_exit then + self:close_on_exit() + end Util.info(("Started **%s** in a new tmux split"):format(self.tool.name)) end end +--- Kill the tmux pane when Neovim exits. +function M:close_on_exit() + local pane_id = self.tmux_pane_id + if not pane_id then + return + end + vim.api.nvim_create_autocmd("VimLeavePre", { + once = true, + callback = function() + pcall(Util.exec, { "tmux", "kill-pane", "-t", pane_id }, { notify = false }) + end, + }) +end + --- Execute the given tmux command and update the session info, --- based on the first pane returned. ---@param cmd string[] diff --git a/lua/sidekick/config.lua b/lua/sidekick/config.lua index c6a95c8f..a6f1bfb7 100644 --- a/lua/sidekick/config.lua +++ b/lua/sidekick/config.lua @@ -95,6 +95,7 @@ local defaults = { vertical = true, -- vertical or horizontal split before = false, -- place the split before the current pane (left when vertical, above when horizontal) size = 0.5, -- size of the split (0-1 for percentage) + close_on_exit = false, -- close the split when Neovim exits }, -- max lines to capture when dumping a multiplexer pane for scrollback support -- more lines means slower loading of the scrollback From 26fbb62405350a286ce8ba35686cd72358d9c458 Mon Sep 17 00:00:00 2001 From: Noah Bauer Date: Thu, 23 Jul 2026 14:26:07 +0200 Subject: [PATCH 3/3] feat: toggle now also toggles tmux splits and also closes them on calling the close method (detaching) --- lua/sidekick/cli/init.lua | 34 +++++++++++----- lua/sidekick/cli/session/init.lua | 16 ++++++++ lua/sidekick/cli/session/tmux.lua | 64 +++++++++++++++++++++++++++++++ lua/sidekick/cli/state.lua | 1 + 4 files changed, 106 insertions(+), 9 deletions(-) diff --git a/lua/sidekick/cli/init.lua b/lua/sidekick/cli/init.lua index 9c0e9b50..60bf8253 100644 --- a/lua/sidekick/cli/init.lua +++ b/lua/sidekick/cli/init.lua @@ -86,7 +86,11 @@ end ---@overload fun(name: string) function M.show(opts) opts = filter_opts(opts) - State.with(function() end, { + State.with(function(state) + if not state.terminal and state.session and not state.session:is_open() then + state.session:show() + end + end, { all = opts.all, attach = true, filter = opts.filter, @@ -100,14 +104,22 @@ end function M.toggle(opts) opts = filter_opts(opts) State.with(function(state, attached) - if not state.terminal then + if state.terminal then + if not attached then + state.terminal:toggle() + end + if state.terminal:is_open() and opts.focus ~= false then + state.terminal:focus() + end return end - if not attached then - state.terminal:toggle() - end - if state.terminal:is_open() and opts.focus ~= false then - state.terminal:focus() + if state.session then + local open = state.session:is_open() + if open and not attached then + state.session:hide() + elseif not open then + state.session:show() + end end end, { attach = true, @@ -142,10 +154,14 @@ end function M.hide(opts) opts = filter_opts(opts) State.with(function(state) - return state.terminal and state.terminal:hide() + if state.terminal then + return state.terminal:hide() + elseif state.session and state.session:is_open() then + return state.session:hide() + end end, { all = opts.all, - filter = Util.merge(opts.filter, { terminal = true }), + filter = opts.filter, }) end diff --git a/lua/sidekick/cli/session/init.lua b/lua/sidekick/cli/session/init.lua index 1c598924..ebff9e9d 100644 --- a/lua/sidekick/cli/session/init.lua +++ b/lua/sidekick/cli/session/init.lua @@ -52,6 +52,22 @@ function B:attach() end --- Detach from an existing session function B:detach() end +--- Whether the session is currently visible. +--- Backends without a visibility concept are always considered open. +---@return boolean +function B:is_open() + return true +end + +--- Hide the session from view without stopping it (optional hook) +function B:hide() end + +--- Stop the session and remove its pane/window (optional hook) +function B:close() end + +--- Bring a hidden session back into view (optional hook) +function B:show() end + --- Start a new session --- If the backend returns a Cmd, a new terminal session will be spawned ---@return sidekick.cli.terminal.Cmd? diff --git a/lua/sidekick/cli/session/tmux.lua b/lua/sidekick/cli/session/tmux.lua index d20c5272..966f9e71 100644 --- a/lua/sidekick/cli/session/tmux.lua +++ b/lua/sidekick/cli/session/tmux.lua @@ -58,6 +58,70 @@ function M:start() end end +--- Whether the split pane currently lives in Neovim's tmux window. +--- Hidden panes are broken out into a separate (background) window. +---@return boolean +function M:is_open() + if Config.cli.mux.create ~= "split" then + return true + end + local pane = self:pane_id() + local nvim_pane = vim.env.TMUX_PANE + if not pane or not nvim_pane then + return true + end + local windows = {} ---@type table + local lines = Util.exec({ "tmux", "list-panes", "-a", "-F", "#{pane_id} #{window_id}" }, { notify = false }) + for _, line in ipairs(lines or {}) do + local p, w = line:match("^(%%%d+) (@%d+)$") + if p then + windows[p] = w + end + end + return windows[pane] ~= nil and windows[pane] == windows[nvim_pane] +end + +--- Hide the split by breaking its pane into a detached background window. +function M:hide() + if Config.cli.mux.create ~= "split" then + return + end + local pane = self:pane_id() + if pane then + Util.exec({ "tmux", "break-pane", "-d", "-s", pane }, { notify = false }) + end +end + +--- Show the split by joining its pane back next to Neovim. +function M:show() + if Config.cli.mux.create ~= "split" then + return + end + local pane = self:pane_id() + local nvim_pane = vim.env.TMUX_PANE + if not pane or not nvim_pane then + return + end + local cmd = { "tmux", "join-pane", "-s", pane, "-t", nvim_pane } + cmd[#cmd + 1] = Config.cli.mux.split.vertical and "-h" or "-v" + if Config.cli.mux.split.before then + cmd[#cmd + 1] = "-b" + end + local size = Config.cli.mux.split.size + vim.list_extend(cmd, { "-l", tostring(size <= 1 and ((size * 100) .. "%") or size) }) + Util.exec(cmd, { notify = false }) + -- keep focus on Neovim, matching the detached (`-d`) behavior of a fresh split + Util.exec({ "tmux", "select-pane", "-t", nvim_pane }, { notify = false }) +end + +--- Close the split/window by killing its tmux pane. +function M:close() + local pane = self.tmux_pane_id + if pane then + Util.exec({ "tmux", "kill-pane", "-t", pane }, { notify = false }) + end +end + --- Kill the tmux pane when Neovim exits. function M:close_on_exit() local pane_id = self.tmux_pane_id diff --git a/lua/sidekick/cli/state.lua b/lua/sidekick/cli/state.lua index 01f1f3e3..1480a13e 100644 --- a/lua/sidekick/cli/state.lua +++ b/lua/sidekick/cli/state.lua @@ -206,6 +206,7 @@ function M.detach(state) if state.terminal then state.terminal:close() else + state.session:close() Session.detach(state.session) Util.info("Detached from `" .. state.tool.name .. "`") end