olimorris / olimorris/codecompanion.nvim
[Bug]: /fork renders the Context block twice in the forked chat
Nobody has claimed this yet.
- Dominant language
- Lua
- Stars
- 6.9k
- Forks
- 457
- Avg merge
- 1d 9h
- Merged PRs (30d)
- 46
Description
Pre-submission checklist
- I have read the documentation
- I have updated the plugin and all of its dependencies to the latest versions
- I have searched for existing issues and discussions
- If ACP related, I have confirmed that the issue is a CodeCompanion one and not an issue in the adapter itself
- My issue is not a minor or cosmetic quirk (e.g. formatting, spacing, or other non-functional details)
- I have reproduced this from a clean
minimal.luaand included everything needed for someone else to hit the same bug
Environment
Neovim: 0.12.5
OS: Ubuntu 20.04.6 LTS
CodeCompanion: main @ 070285d5 (fresh .repro clone)
Adapter and model: copilot - no request is ever sent, the bug happens while the fork is being built
What happened, and what did you expect?
Forking a chat with /fork renders the > Context: block twice in the forked buffer, once from the source chat's context and once from the context the new chat loads for itself:
## Me
> Context:
> - <group>demo</group>
> Context:
> - <group>demo</group>
I expected one block.
SlashCommand:output (slash_commands/builtin/fork.lua) builds the fork with Chat.new, which runs load_tools (interactions/chat/init.lua:507) and adds config.interactions.chat.tools.opts.default_tools. Each tool_registry:add calls context:add, which writes its row into the buffer and adds the tool's system prompt message - that is the second block. The fork then discards that bookkeeping: forked.context_items = context_items (fork.lua:67-69) overwrites the freshly loaded items with a copy of the source's, tool_registry.groups/in_use/schemas are overwritten likewise, and forked.context:render() inserts a whole new block at header_line + 1 - the first block. stop_context_insertion = true is already passed, but that only suppresses the visual selection, not the default tools.
Two consequences beyond the cosmetic duplication:
- The payload carries each default tool or group system prompt twice. In the repro below, the
demogroup's system prompt is inchat.messagestwice - once from the copied history, once from the fresh load.Chat:add_messagedoes not deduplicate. - Revoking a duplicated context row silently does nothing.
Context:get_from_chatreturns the id twice, so after deleting one rowChat:check_contextstill finds it in the buffer and keeps the tool granted. The repro printsread_file still in use=trueafter deleting one of the two<group>demo</group>rows.
Which behaviour do you consider correct for a fork - inherit the source's context and load no defaults, or start from a fresh default context and copy nothing? I have a fix for either and am happy to PR it (configurable under slash_commands.fork.opts if you want both). In the fresh-context case there is a follow-up decision: the copied history still contains the source's context-linked messages (rules, tool prompts, /file output) that the new Context block no longer lists, so those would either have to be stripped or left invisible.
minimal.lua
---@diagnostic disable: missing-fields
--[[
NOTE: Set the config path to enable the copilot adapter to work.
It will search the following paths for a token:
- "$CODECOMPANION_TOKEN_PATH/github-copilot/hosts.json"
- "$CODECOMPANION_TOKEN_PATH/github-copilot/apps.json"
--]]
vim.env["CODECOMPANION_TOKEN_PATH"] = vim.fn.expand("~/.config")
vim.env.LAZY_STDPATH = ".repro"
load(vim.fn.system("curl -s https://raw.githubusercontent.com/folke/lazy.nvim/main/bootstrap.lua"))()
-- Your CodeCompanion setup
local plugins = {
{
"olimorris/codecompanion.nvim",
dependencies = {
{ "nvim-lua/plenary.nvim" },
{
"nvim-treesitter/nvim-treesitter",
lazy = false,
build = ":TSUpdate",
},
},
opts = {
interactions = {
chat = {
adapter = "copilot",
tools = {
groups = {
-- A group with a system prompt, so the duplication is visible in
-- the message history as well as in the chat buffer
["demo"] = {
description = "Demo group",
system_prompt = "Demo group system prompt",
tools = { "read_file" },
},
},
opts = {
-- Any context a fresh chat loads by itself is enough to show the bug
default_tools = { "demo" },
},
},
},
inline = { adapter = "copilot" },
},
opts = {
log_level = "DEBUG",
},
},
},
}
require("lazy.minit").repro({ spec = plugins })
-- CONFIGURE PLUGINS HERE -----------------------------------------------------
-- Setup Tree-sitter
-- NOTE: Please restart Neovim to ensure parsers are loaded correctly
require("nvim-treesitter")
.install({
"lua",
"markdown",
"markdown_inline",
"yaml",
}, { summary = true, max_jobs = 10 })
:wait(1800000)
Steps to reproduce
nvim -u minimal.lua, quit once the Tree-sitter parsers have installed, then start it again (as the file notes)- Open a chat with
:CodeCompanionChat. It shows one> Context:block containing> - <group>demo</group> - Type
/forkin the chat buffer and accept it from the completion menu (<C-x><C-o>works with this minimal config) - Press Enter at the
Fork Titleprompt to accept the default title - The forked chat buffer shows two identical
> Context:blocks. No message has been sent to an LLM at any point - Delete one of the two
> - <group>demo</group>rows to revoke the group. The group stays granted, because the id is still present in the other block
Deterministic alternative, which is how I reproduced it: save the drive.lua below next to minimal.lua and run
nvim --headless -u minimal.lua -c "luafile drive.lua" -c "qa!"
which prints
source: context blocks=1 group system prompts=1
--- forked buffer ---
## Me
> Context:
> - <group>demo</group>
> Context:
> - <group>demo</group>
--- context blocks: 2 (expected 1) ---
forked: ids in buffer={ "<group>demo</group>", "<group>demo</group>" }
forked: group system prompts=2 (expected 1)
after deleting one row: read_file still in use=true
Example files
No source file is involved in this bug. The file below is the headless driver referenced above.
drive.lua
-- Headless driver: answers the fork title prompt, then reports the forked chat's state.
vim.ui.input = function(_, on_confirm)
on_confirm("Fork of chat")
end
local Chat = require("codecompanion.interactions.chat")
local config = require("codecompanion.config")
local function count_lines_matching(bufnr, pattern)
local count = 0
for _, line in ipairs(vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)) do
if line:match(pattern) then
count = count + 1
end
end
return count
end
local function count_group_prompts(chat)
local count = 0
for _, message in ipairs(chat.messages) do
if message.content == "Demo group system prompt" then
count = count + 1
end
end
return count
end
vim.cmd("CodeCompanionChat")
vim.wait(3000, function()
return Chat.last_chat() ~= nil
end)
local source = Chat.last_chat()
print(
"source: context blocks="
.. count_lines_matching(source.bufnr, "^> Context:")
.. " group system prompts="
.. count_group_prompts(source)
)
require("codecompanion.interactions.chat.slash_commands.builtin.fork")
.new({
Chat = source,
config = config.interactions.chat.slash_commands["fork"],
context = source.buffer_context,
})
:execute()
vim.wait(3000)
local forked = Chat.last_chat()
print("--- forked buffer ---")
print(table.concat(vim.api.nvim_buf_get_lines(forked.bufnr, 0, -1, false), "\n"))
print("--- context blocks: " .. count_lines_matching(forked.bufnr, "^> Context:") .. " (expected 1) ---")
print("forked: ids in buffer=" .. vim.inspect(forked.context:get_from_chat()))
print("forked: group system prompts=" .. count_group_prompts(forked) .. " (expected 1)")
-- Now delete one context row, as a user revoking the group would
forked.ui:unlock_buf()
for i, line in ipairs(vim.api.nvim_buf_get_lines(forked.bufnr, 0, -1, false)) do
if line == "> - <group>demo</group>" then
vim.api.nvim_buf_set_lines(forked.bufnr, i - 1, i, false, {})
break
end
end
forked:check_context()
print("after deleting one row: read_file still in use=" .. tostring(forked.tool_registry.in_use["read_file"] == true))
Log output (optional)
No response
Screenshots or recordings (optional)
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with slash_commands/builtin/fork.lua, interactions/chat/init.lua around load_tools, and the Context and tool_registry methods described in the report. Run the supplied minimal.lua and drive.lua reproduction to inspect the duplicated buffer rows and system prompts. Done means the chosen fork semantics produce one context block and prompt per item, with revoking a row correctly removing the tool.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- lua
- Domain
- tooling
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100