Skip to content

Commit 65ce47f

Browse files
committed
feat(chat): add display.chat.window.pertab for tab-local chats
When enabled, chat buffers visible in another tab are not hidden when opening or cycling chats in the current tab. Cycling via { / } is scoped to chats that are either visible in the current tab or not currently visible in any tab, so chats opened in other tabs are never stolen. The Toggle command jumps to the existing tab when the chat is open elsewhere (with a notify) instead of moving the chat. The close keymap prefers a chat that's already in or available to the current tab when auto-opening a sibling chat after close. Chats survive tab closure and fall back into the hidden pool, where they remain cycle-eligible from any tab. pertab is mutually exclusive with sticky; if both are enabled, the sticky autocmd's callback no-ops (a warning is logged at setup) so the chat does not follow tab switches under pertab semantics. Includes the bufnr_available_to_current_tab helper to share the hidden-or-current-tab availability check between cycling and the close keymap, and extends registry.move with an optional filter so the chat keymaps can scope cycling to current-tab + hidden entries without changing CLI cycling behaviour. Adds tests for: - chat in another tab survives opening a new chat in the current tab - close_last_chat skips chats visible in other tabs - sticky+pertab logs a warning and chat does not follow tab switch - closing a tab leaves its chat in the hidden pool with registry intact
1 parent 2e33fbd commit 65ce47f

9 files changed

Lines changed: 274 additions & 40 deletions

File tree

doc/codecompanion.txt

Lines changed: 35 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
*codecompanion.txt* For NVIM v0.11 Last change: 2026 May 04
1+
*codecompanion.txt* For NVIM v0.11 Last change: 2026 May 05
22

33
==============================================================================
44
Table of Contents *codecompanion-table-of-contents*
@@ -221,7 +221,7 @@ and blink.cmp <https://github.com/Saghen/blink.cmp>. For the latter, on version
221221
},
222222
<
223223

224-
The plugin also supports |codecompanion-usage-chat-buffer-completion| and
224+
The plugin also supports |codecompanion-usage-chat-buffer--completion| and
225225
coc.nvim <https://github.com/neoclide/coc.nvim>.
226226

227227

@@ -314,10 +314,10 @@ SETUP *codecompanion-getting-started-setup*
314314
CHAT AND INLINE ~
315315

316316

317-
[!NOTE] The adapters that the plugin supports out of the box can be found here
317+
[!NOTE] The adapters that the plugin supports out of the box can be found in
318+
the built-in adapters directory
318319
<https://github.com/olimorris/codecompanion.nvim/tree/main/lua/codecompanion/adapters>.
319-
Or, see the user contributed adapters
320-
|codecompanion-configuration-adapters-http-community-adapters|.
320+
Or, see the |codecompanion-configuration-adapters-http-community-adapters|.
321321
|codecompanion-configuration-adapters-acp| are only supported for the chat
322322
interaction.
323323
The Chat Buffer is where you can converse with an LLM from within a Neovim
@@ -450,7 +450,7 @@ Commands` in the chat buffer.
450450
**Editor Context**
451451

452452
`Editor Context`, accessed via `#` (by default), contain data about the present
453-
state of Neovim. You can find a list of available editor context,
453+
state of Neovim. You can find a
454454
|codecompanion-usage-chat-buffer-editor-context|. The buffer editor context
455455
will automatically link a buffer to the chat buffer, by default, updating the
456456
LLM when the buffer changes.
@@ -469,15 +469,13 @@ You can use them in your prompts like:
469469
`<C-_>` in insert mode when in the chat buffer. Note: Slash commands should
470470
also work with coc.nvim.
471471
`Slash commands`, accessed via `/` (by default), run commands to insert
472-
additional context into the chat buffer. You can find a list of available
473-
commands as well as how to use them,
472+
additional context into the chat buffer. You can find a
474473
|codecompanion-usage-chat-buffer-slash-commands|.
475474

476475
**Tools**
477476

478477
`Tools`, accessed via `@` (by default), allow the LLM to function as an agent
479-
and leverage external tools. You can find a list of available tools as well as
480-
how to use them,
478+
and leverage external tools. You can find a
481479
|codecompanion-usage-chat-buffer-agents-tools-available-tools|.
482480

483481
You can use them in your prompts like:
@@ -621,8 +619,9 @@ IN THE CHAT BUFFER ~
621619
[!NOTE] CodeCompanion enables context management by default
622620
If you're using the `openai_responses` or `anthropic` adapters, then
623621
CodeCompanion will use their native server-side compaction capabilities. Please
624-
see their respective documentation here
625-
<https://developers.openai.com/api/docs/guides/compaction> and here
622+
see the OpenAI compaction documentation
623+
<https://developers.openai.com/api/docs/guides/compaction> and Anthropic
624+
compaction documentation
626625
<https://platform.claude.com/docs/en/build-with-claude/compaction> for more
627626
information.
628627

@@ -1415,11 +1414,10 @@ The example below uses the `gemini-api-key` method, pulling the API key from
14151414

14161415
SETUP: GOOSE CLI ~
14171416

1418-
To use Goose <https://block.github.io/goose/> in CodeCompanion, ensure you've
1419-
followed their documentation
1420-
<https://block.github.io/goose/docs/getting-started/installation/> to setup and
1421-
install Goose CLI. Then ensure that in your chat buffer you select the `goose`
1422-
adapter.
1417+
To use Goose <https://goose-docs.ai/> in CodeCompanion, ensure you've followed
1418+
their documentation <https://goose-docs.ai/docs/getting-started/installation/>
1419+
to setup and install Goose CLI. Then ensure that in your chat buffer you select
1420+
the `goose` adapter.
14231421

14241422

14251423
SETUP: KILO CODE ~
@@ -1600,7 +1598,7 @@ the adapter's URL, headers, parameters and other fields at runtime.
16001598

16011599

16021600
[!NOTE] In this `command` example, we're using the 1Password CLI to extract the
1603-
Gemini API Key. You could also use gpg as outlined here
1601+
Gemini API Key. You could also use gpg as outlined in this community discussion
16041602
<https://github.com/olimorris/codecompanion.nvim/discussions/601>
16051603
Supported `env` value types: - **Plain environment variable name (string)**: if
16061604
the value is the name of an environment variable that has already been set
@@ -1850,8 +1848,8 @@ Thanks to the community for building the following adapters:
18501848
- Venice.ai <https://github.com/olimorris/codecompanion.nvim/discussions/972>
18511849
- Vertex AI <https://github.com/viespejo/cc-adapter-vertex-ai.nvim>
18521850

1853-
The section of the discussion forums which is dedicated to user created
1854-
adapters can be found here
1851+
The section of the discussion forums dedicated to user-created adapters can be
1852+
found in the adapter discussions on GitHub
18551853
<https://github.com/olimorris/codecompanion.nvim/discussions?discussions_q=is%3Aopen+label%3A%22tip%3A+adapter%22>.
18561854
Use these individual threads as a place to raise issues and ask questions about
18571855
your specific adapters.
@@ -3589,6 +3587,14 @@ buffer.
35893587
When in a chat buffer, you can cycle between other chat buffers with `{` or
35903588
`}`.
35913589

3590+
By default, opening or cycling to a chat hides whichever chat is currently
3591+
visible. If you'd rather keep chats per tab — so a chat opened in tab A is
3592+
never closed or stolen by activity in tab B — set `display.chat.window.pertab
3593+
= true` in your config. With that enabled, `{` / `}` only cycles through chats
3594+
that are visible in the current tab or not currently visible anywhere, and
3595+
`:CodeCompanionChat Toggle` jumps to the existing tab when the chat lives
3596+
there.
3597+
35923598

35933599
ACTION PALETTE *codecompanion-usage-action-palette*
35943600

@@ -3878,7 +3884,8 @@ HOW THEY WORK ~
38783884

38793885
Tools make use of an LLM's function calling
38803886
<https://platform.openai.com/docs/guides/function-calling> ability. All tools
3881-
in CodeCompanion follow OpenAI's function calling specification, here
3887+
in CodeCompanion follow OpenAI's function calling specification for defining
3888+
functions
38823889
<https://platform.openai.com/docs/guides/function-calling#defining-functions>.
38833890

38843891
When a tool is added to the chat buffer, the LLM is instructured by the plugin
@@ -3889,8 +3896,8 @@ which sees tool's added to a queue and sequentially worked with their output
38893896
being shared back to the LLM via the chat buffer. Depending on the tool, flags
38903897
may be inserted on the chat buffer for later processing.
38913898

3892-
An outline of the architecture can be seen
3893-
|codecompanion-extending-tools-architecture|.
3899+
An outline of the |codecompanion-extending-tools-architecture| is available in
3900+
the extending section.
38943901

38953902

38963903
AGENTS / TOOL GROUPS ~
@@ -4283,7 +4290,7 @@ receive up to date information:
42834290

42844291
Currently, the tool uses tavily <https://www.tavily.com> and you'll need to
42854292
ensure that an API key has been set accordingly, as per the adapter
4286-
<https://github.com/olimorris/codecompanion.nvim/blob/main/lua/codecompanion/adapters/tavily.lua>.
4293+
<https://github.com/olimorris/codecompanion.nvim/blob/main/lua/codecompanion/adapters/http/tavily.lua>.
42874294

42884295

42894296
ADAPTER TOOLS ~
@@ -4874,7 +4881,8 @@ file to share with the LLM. This can be a useful way to minimize token
48744881
consumption whilst sharing the basic outline of a file. The plugin utilizes the
48754882
amazing work from **aerial.nvim** by using their Tree-sitter symbol queries as
48764883
the basis. The list of filetypes that the plugin currently supports can be
4877-
found here <https://github.com/olimorris/codecompanion.nvim/tree/main/queries>.
4884+
found in the Tree-sitter queries directory
4885+
<https://github.com/olimorris/codecompanion.nvim/tree/main/queries>.
48784886

48794887
The command has native, `Telescope`, `mini.pick`, `fzf.lua` and `snacks.nvim`
48804888
providers available. Also, multiple symbols can be selected and added to the
@@ -5519,7 +5527,7 @@ This guide is intended to serve as a reference for anyone who wishes to
55195527
contribute an adapter to the plugin or understand the inner workings of
55205528
existing adapters.
55215529

5522-
The plugin's in-built adapters can be found here
5530+
The plugin's in-built adapters can be found in the adapters source directory
55235531
<https://github.com/olimorris/codecompanion.nvim/tree/main/lua/codecompanion/adapters>.
55245532

55255533

@@ -6872,7 +6880,7 @@ response from the LLM, identifying the tool and duly executing it.
68726880
There are two types of tools that CodeCompanion can leverage:
68736881

68746882
1. **Command-based**: These tools can execute a series of commands in the background using `vim.system`. They're non-blocking, meaning you can carry out other activities in Neovim whilst they run. Useful for heavy/time-consuming tasks.
6875-
2. **Function-based**: These tools, like insert_edit_into_file <https://github.com/olimorris/codecompanion.nvim/blob/main/lua/codecompanion/interactions/chat/tools/builtin/insert_edit_into_file.lua>, execute Lua functions directly in Neovim within the main process, one after another. They can also be executed asynchronously.
6883+
2. **Function-based**: These tools, like insert_edit_into_file <https://github.com/olimorris/codecompanion.nvim/blob/main/lua/codecompanion/interactions/chat/tools/builtin/insert_edit_into_file/init.lua>, execute Lua functions directly in Neovim within the main process, one after another. They can also be executed asynchronously.
68766884

68776885
For the purposes of this section of the guide, we'll be building a simple
68786886
function-based calculator tool that an LLM can use to do basic maths.

doc/configuration/chat-buffer.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -867,7 +867,8 @@ require("codecompanion").setup({
867867
chat = {
868868
window = {
869869
buflisted = false, -- List the chat buffer in the buffer list?
870-
sticky = false, -- Chat window follows when switching tabs
870+
sticky = false, -- Chat window follows when switching tabs (ignored when `pertab` is true)
871+
pertab = false, -- Each tab has its own chat window? (mutually exclusive with `sticky`)
871872

872873
layout = "vertical", -- float|vertical|horizontal|tab|buffer
873874
full_height = true, -- for vertical layout

doc/usage/introduction.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,5 @@ The `:CodeCompanionChat Toggle` command will automatically create a chat buffer
2828

2929
When in a chat buffer, you can cycle between other chat buffers with `{` or `}`.
3030

31+
By default, opening or cycling to a chat hides whichever chat is currently visible. If you'd rather keep chats per tab — so a chat opened in tab A is never closed or stolen by activity in tab B — set `display.chat.window.pertab = true` in your config. With that enabled, `{` / `}` only cycles through chats that are visible in the current tab or not currently visible anywhere, and `:CodeCompanionChat Toggle` jumps to the existing tab when the chat lives there.
32+

lua/codecompanion/config.lua

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1095,7 +1095,11 @@ The user is working on a %s machine. Please respond with system specific command
10951095
-- Window options for the chat buffer
10961096
window = {
10971097
buflisted = false, -- List the chat buffer in the buffer list?
1098-
sticky = false, -- Chat window follows when switching tabs
1098+
sticky = false, -- Chat window follows when switching tabs (ignored when `pertab` is true)
1099+
pertab = false, -- Treat each tab as having its own chat window? Chats opened in
1100+
-- another tab won't be hidden when opening/cycling chats in the current tab,
1101+
-- and `{`/`}` cycling is scoped to chats that are either visible in the current
1102+
-- tab or not currently visible in any tab. Mutually exclusive with `sticky`.
10991103

11001104
layout = "vertical", -- float|vertical|horizontal|tab|buffer
11011105
full_height = true, -- for vertical layout

lua/codecompanion/init.lua

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -524,10 +524,20 @@ CodeCompanion.setup = function(opts)
524524
end
525525

526526
local window_config = config.display.chat.window
527-
if window_config.sticky and window_config.layout ~= "buffer" and window_config.layout ~= "tab" then
527+
if window_config.sticky and window_config.pertab then
528+
log:warn(
529+
"[CodeCompanion] `display.chat.window.sticky` and `display.chat.window.pertab` are mutually exclusive. "
530+
.. "Disabling `sticky` because `pertab` makes chats tab-local by design."
531+
)
532+
elseif window_config.sticky and window_config.layout ~= "buffer" and window_config.layout ~= "tab" then
528533
api.nvim_create_autocmd("TabEnter", {
529534
group = api.nvim_create_augroup("codecompanion.sticky_buffer", { clear = true }),
530535
callback = function(args)
536+
-- No-op if pertab has been enabled by a subsequent setup() call.
537+
-- pertab and sticky are mutually exclusive; pertab wins.
538+
if config.display.chat.window.pertab then
539+
return
540+
end
531541
local chat = CodeCompanion.last_chat()
532542
if chat and chat.ui:is_visible_non_curtab() then
533543
chat.buffer_context = get_context(args.buf)

lua/codecompanion/interactions/chat/init.lua

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2015,10 +2015,18 @@ function Chat.last_chat()
20152015
end
20162016

20172017
---Close the last chat buffer
2018+
---
2019+
---When `display.chat.window.pertab` is enabled, the last chat is left alone if
2020+
---it is currently visible in a tab other than the current one. This prevents
2021+
---opening or cycling a chat in tab A from hiding a chat that the user has
2022+
---explicitly placed in tab B.
20182023
---@return nil
20192024
function Chat.close_last_chat()
20202025
if last_chat and not vim.tbl_isempty(last_chat) then
20212026
if last_chat.ui:is_visible() then
2027+
if config.display.chat.window.pertab and last_chat.ui:is_visible_non_curtab() then
2028+
return
2029+
end
20222030
last_chat.ui:hide()
20232031
end
20242032
end
@@ -2064,9 +2072,17 @@ function Chat.toggle(args)
20642072

20652073
-- If the chat is visible in a different tab ...
20662074
if chat.ui:is_visible_non_curtab() then
2067-
if config.display.chat.window.layout == "tab" then
2068-
-- ... open it (go there) if chat opens in tabs
2069-
chat.ui:open()
2075+
if config.display.chat.window.layout == "tab" or config.display.chat.window.pertab then
2076+
-- ... jump to it (go there) if chat opens in tabs, or if pertab is enabled
2077+
-- (in pertab mode we never steal a chat from another tab).
2078+
local target_tab = api.nvim_win_get_tabpage(chat.ui.winnr)
2079+
if config.display.chat.window.pertab then
2080+
utils.notify(
2081+
fmt("Chat is open in tab %d. Switching tab.", api.nvim_tabpage_get_number(target_tab)),
2082+
vim.log.levels.INFO
2083+
)
2084+
end
2085+
api.nvim_set_current_tabpage(target_tab)
20702086
return
20712087
else
20722088
-- ... or close it so we can open it below

lua/codecompanion/interactions/chat/keymaps/init.lua

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,48 @@ M.regenerate = {
299299
end,
300300
}
301301

302+
---Whether the given buffer is "available" to the current tab \u2014 i.e. either
303+
---it is not currently displayed in any window, or it is displayed in a window
304+
---in the current tab. A buffer that is only visible in another tab is NOT
305+
---available to the current tab. Used in pertab mode to avoid stealing chats
306+
---from other tabs.
307+
---@param bufnr number
308+
---@return boolean
309+
local function bufnr_available_to_current_tab(bufnr)
310+
local wins = vim.fn.win_findbuf(bufnr)
311+
if #wins == 0 then
312+
return true
313+
end
314+
315+
local current_tab = api.nvim_get_current_tabpage()
316+
for _, w in ipairs(wins) do
317+
if api.nvim_win_get_tabpage(w) == current_tab then
318+
return true
319+
end
320+
end
321+
322+
return false
323+
end
324+
325+
---Build a registry filter for chat cycling.
326+
---When `display.chat.window.pertab` is enabled, only cycle through chats that
327+
---are either visible in the current tab or not currently visible in any tab.
328+
---Chats which are visible in another tab are skipped (and not stolen). Non-
329+
---chat entries (e.g. CLI buffers) are always allowed.
330+
---@return fun(entry: CodeCompanion.Registry.Entry): boolean | nil
331+
local function chat_cycle_filter()
332+
if not config.display.chat.window.pertab then
333+
return nil
334+
end
335+
336+
return function(entry)
337+
if entry.interaction ~= "chat" then
338+
return true
339+
end
340+
return bufnr_available_to_current_tab(entry.bufnr)
341+
end
342+
end
343+
302344
M.close = {
303345
callback = function(chat)
304346
chat:close()
@@ -309,7 +351,21 @@ M.close = {
309351
end
310352

311353
local window_opts = chat.ui.window_opts or { default = true }
312-
chats[1].chat.ui:open({ window_opts = window_opts })
354+
355+
local target = chats[1]
356+
357+
-- In pertab mode, prefer a chat that isn't already visible in another tab
358+
-- so closing one chat doesn't steal a sibling chat from another tab.
359+
if config.display.chat.window.pertab then
360+
for _, c in ipairs(chats) do
361+
if bufnr_available_to_current_tab(c.chat.bufnr) then
362+
target = c
363+
break
364+
end
365+
end
366+
end
367+
368+
target.chat.ui:open({ window_opts = window_opts })
313369
end,
314370
}
315371

@@ -475,14 +531,14 @@ M.buffer_sync_diff = {
475531
M.next_chat = {
476532
desc = "Move to the next chat",
477533
callback = function(chat)
478-
require("codecompanion.interactions.shared.registry").move(chat.bufnr, 1)
534+
require("codecompanion.interactions.shared.registry").move(chat.bufnr, 1, { filter = chat_cycle_filter() })
479535
end,
480536
}
481537

482538
M.previous_chat = {
483539
desc = "Move to the previous chat",
484540
callback = function(chat)
485-
require("codecompanion.interactions.shared.registry").move(chat.bufnr, -1)
541+
require("codecompanion.interactions.shared.registry").move(chat.bufnr, -1, { filter = chat_cycle_filter() })
486542
end,
487543
}
488544

0 commit comments

Comments
 (0)