From 9d25965adf0e0f132d10a0c9fbc545d595b8a31a Mon Sep 17 00:00:00 2001 From: Seong Yong-ju Date: Mon, 20 Apr 2026 14:04:26 +0900 Subject: [PATCH] fix(cli): apply focus to tmux pane for external sessions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds `Session:focus()`, `Session:is_focused()`, and `Session:blur()` backend hooks with tmux implementations, and unifies the UI layer to dispatch through `state.session:*`. `Terminal` is already a `Session` backend so `state.terminal === state.session` for terminal-backed sessions — the same calls cover both paths. tmux: - focus: select-window then select-pane (window switch handles create="window", select-pane handles the pane within the window) - is_focused: compare client_active_pane with the pane id - blur: switch back to the editor pane via $TMUX_PANE Also fixes the default-focus asymmetry in `M.attach`: external focus now fires on `opts.show and opts.focus ~= false` to match terminal, so `send()` and `toggle()` (no args) focus external panes the same way they focus terminals. Closes folke/sidekick.nvim#179. Co-Authored-By: Claude Opus 4.7 (1M context) --- lua/sidekick/cli/init.lua | 21 +++++++-------- lua/sidekick/cli/session/init.lua | 12 +++++++++ lua/sidekick/cli/session/tmux.lua | 43 +++++++++++++++++++++++++++++++ lua/sidekick/cli/state.lua | 16 ++++++------ 4 files changed, 72 insertions(+), 20 deletions(-) diff --git a/lua/sidekick/cli/init.lua b/lua/sidekick/cli/init.lua index 9c0e9b50..5cfa62cd 100644 --- a/lua/sidekick/cli/init.lua +++ b/lua/sidekick/cli/init.lua @@ -100,14 +100,14 @@ end function M.toggle(opts) opts = filter_opts(opts) State.with(function(state, attached) - if not state.terminal then - return - end - if not attached then + if state.terminal and not attached then state.terminal:toggle() end - if state.terminal:is_open() and opts.focus ~= false then - state.terminal:focus() + if opts.focus == false then + return + end + if state.external or (state.terminal and state.terminal:is_open()) then + state.session:focus() end end, { attach = true, @@ -121,13 +121,10 @@ end function M.focus(opts) opts = filter_opts(opts) State.with(function(state) - if not state.terminal then - return - end - if state.terminal:is_focused() then - state.terminal:blur() + if state.session:is_focused() then + state.session:blur() else - state.terminal:focus() + state.session:focus() end end, { attach = true, diff --git a/lua/sidekick/cli/session/init.lua b/lua/sidekick/cli/session/init.lua index 1c598924..78701748 100644 --- a/lua/sidekick/cli/session/init.lua +++ b/lua/sidekick/cli/session/init.lua @@ -52,6 +52,18 @@ function B:attach() end --- Detach from an existing session function B:detach() end +--- Focus the session +function B:focus() end + +--- Whether the session currently has user focus +---@return boolean +function B:is_focused() + return false +end + +--- Move focus away from the session +function B:blur() 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 942ed7d1..788f4472 100644 --- a/lua/sidekick/cli/session/tmux.lua +++ b/lua/sidekick/cli/session/tmux.lua @@ -195,4 +195,47 @@ function M:dump() return ret end +---Focus the tmux pane +function M:focus() + local pane_id = self:pane_id() + if not pane_id then + return + end + -- select-window first so create="window" switches the client to the pane's window; + -- no-op for create="split" where the pane is already in the current window. + Util.exec({ "tmux", "select-window", "-t", pane_id }) + Util.exec({ "tmux", "select-pane", "-t", pane_id }) +end + +---Whether this tmux pane is the active pane of the calling client +---@return boolean +function M:is_focused() + local pane_id = self:pane_id() + if not pane_id then + return false + end + local lines = Util.exec({ + "tmux", + "display-message", + "-p", + "-t", + pane_id, + "#{==:#{client_active_pane},#{pane_id}}", + }, { notify = false }) + return lines ~= nil and lines[1] == "1" +end + +---Send focus back to the editor pane (identified via $TMUX_PANE) +function M:blur() + if not self:is_focused() then + return + end + local editor_pane = vim.env.TMUX_PANE + if not editor_pane then + return + end + Util.exec({ "tmux", "select-window", "-t", editor_pane }, { notify = false }) + Util.exec({ "tmux", "select-pane", "-t", editor_pane }, { notify = false }) +end + return M diff --git a/lua/sidekick/cli/state.lua b/lua/sidekick/cli/state.lua index 01f1f3e3..3c996d04 100644 --- a/lua/sidekick/cli/state.lua +++ b/lua/sidekick/cli/state.lua @@ -187,14 +187,14 @@ function M.attach(state, opts) state = M.get_state(session) -- update state local terminal = state.terminal - if terminal then - if opts.show then - terminal:show() - if opts.focus ~= false and terminal:is_running() then - terminal:focus() - end - end - elseif attached then + if terminal and opts.show then + terminal:show() + end + local visible = state.external or (terminal and terminal:is_running()) + if opts.show and opts.focus ~= false and visible then + session:focus() + end + if not terminal and attached then Util.info("Attached to `" .. state.tool.name .. "`") end return state, attached