Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions lua/eca/commands.lua
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,15 @@ function M.setup()
desc = "Display ECA server tools (yank preview on confirm)",
})

vim.api.nvim_create_user_command("EcaChatClear", function()
local sidebar = require("eca").get()
if sidebar then
sidebar:clear_chat()
end
Comment on lines +550 to +554
end, {
desc = "Clear ECA chat buffer",
})

Logger.debug("ECA commands registered")
end

Expand Down
1 change: 1 addition & 0 deletions lua/eca/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ M._defaults = {
auto_start_server = false, -- Automatically start server on setup
auto_download = true, -- Automatically download server if not found
show_status_updates = true, -- Show status updates in notifications
preserve_chat_history = false, -- When true, chat history is preserved across sidebar open/close cycles
},
context = {
auto_repo_map = true, -- Automatically add repoMap context when starting new chat
Expand Down
46 changes: 43 additions & 3 deletions lua/eca/sidebar.lua
Original file line number Diff line number Diff line change
Expand Up @@ -157,11 +157,19 @@ function M:close()
end

function M:_close_windows_only()
local preserve = Config.behavior and Config.behavior.preserve_chat_history

for name, container in pairs(self.containers) do
if container and container.winid and vim.api.nvim_win_is_valid(container.winid) then
container:unmount()
-- Keep the container reference but mark window as invalid
container.winid = nil
if preserve and name == "chat" then
-- Close only the window, keep the buffer alive
pcall(vim.api.nvim_win_close, container.winid, true)
container.winid = nil
else
container:unmount()
-- Keep the container reference but mark window as invalid
container.winid = nil
end
Comment on lines +160 to +172
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great catch, this one is actually important. The current implementation goes for a simpler approach (nil out the bufnr, then unmount) rather than fully rethinking the Split lifecycle.

Im not looking to do a bigger refactor here unless more people get involved or theres at least a discussion thread around it (happy to revisit if that happens btw).

end
end
Logger.debug("ECA sidebar windows closed")
Expand Down Expand Up @@ -245,6 +253,15 @@ function M:reset()
end
end

function M:clear_chat()
local chat = self.containers and self.containers.chat
if chat and chat.bufnr and vim.api.nvim_buf_is_valid(chat.bufnr) then
self._welcome_message_applied = true
self._force_welcome = false
vim.api.nvim_buf_set_lines(chat.bufnr, 0, -1, false, {})
end
end
Comment on lines +256 to +263

function M:new_chat()
self:reset()
self._force_welcome = true
Expand Down Expand Up @@ -318,6 +335,22 @@ function M:_create_containers()
winfixwidth = false,
}

local preserve = Config.behavior and Config.behavior.preserve_chat_history
local existing_chat_bufnr = preserve
and self.containers.chat
and self.containers.chat.bufnr
and vim.api.nvim_buf_is_valid(self.containers.chat.bufnr)
and self.containers.chat.bufnr
or nil

-- Clean up the old chat Split's autocmds before creating a new one.
-- Detach the buffer first so that unmount() does not delete it.
if existing_chat_bufnr then
local old_chat = self.containers.chat
old_chat.bufnr = nil
Comment on lines +347 to +350
pcall(old_chat.unmount, old_chat)
end

-- Create and mount main chat container first
self.containers.chat = Split({
relative = "editor",
Expand All @@ -332,6 +365,13 @@ function M:_create_containers()
}),
win_options = base_win_options,
})

if existing_chat_bufnr then
pcall(vim.api.nvim_buf_delete, self.containers.chat.bufnr, { force = true })
self.containers.chat.bufnr = existing_chat_bufnr
Logger.debug("Reusing existing chat buffer: " .. existing_chat_bufnr)
end
Comment on lines 338 to +373

self.containers.chat:mount()
self:_setup_container_events(self.containers.chat, "chat")

Expand Down
238 changes: 238 additions & 0 deletions tests/test_chat_clear.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
local MiniTest = require("mini.test")
local eq = MiniTest.expect.equality
local child = MiniTest.new_child_neovim()

local function flush(ms)
vim.uv.sleep(ms or 120)
child.api.nvim_eval("1")
end

local function setup_helpers()
_G.fill_chat = function()
local sidebar = require("eca").get()
local chat = sidebar.containers.chat
vim.api.nvim_buf_set_lines(chat.bufnr, 0, -1, false, { "hello", "world", "foo" })
end

_G.get_chat_lines = function()
local sidebar = require("eca").get()
if not sidebar then
return nil
end
local chat = sidebar.containers and sidebar.containers.chat
if not chat or not vim.api.nvim_buf_is_valid(chat.bufnr) then
return nil
end
return vim.api.nvim_buf_get_lines(chat.bufnr, 0, -1, false)
end

_G.chat_has_old_content = function()
for _, line in ipairs(_G.get_chat_lines() or {}) do
if line == "hello" or line == "world" or line == "foo" then
return true
end
end
return false
end

_G.get_sidebar_flags = function()
local sidebar = require("eca").get()
if not sidebar then
return nil
end
return {
welcome_message_applied = sidebar._welcome_message_applied,
force_welcome = sidebar._force_welcome,
}
end
end

local function setup_env(preserve_chat_history)
child.lua(
[[
local Eca = require("eca")
Eca.setup({
behavior = {
auto_start_server = false,
auto_set_keymaps = false,
preserve_chat_history = ...,
},
})
local tab = vim.api.nvim_get_current_tabpage()
Eca._init(tab)
Eca.open_sidebar({})
]],
{ preserve_chat_history }
)
child.lua_func(setup_helpers)
end

local T = MiniTest.new_set({
hooks = {
pre_case = function()
child.restart({ "-u", "scripts/minimal_init.lua" })
end,
post_once = child.stop,
},
})

-- EcaChatClear ---------------------------------------------------------------

T["EcaChatClear"] = MiniTest.new_set()

T["EcaChatClear"]["command is registered"] = function()
setup_env(false)
local commands = child.lua_get("vim.api.nvim_get_commands({})")
eq(type(commands.EcaChatClear), "table")
eq(commands.EcaChatClear.name, "EcaChatClear")
end

T["EcaChatClear"]["clears chat buffer when sidebar is open"] = function()
setup_env(false)
flush(200)

child.lua("_G.fill_chat()")
eq(#child.lua_get("_G.get_chat_lines()"), 3)

child.cmd("EcaChatClear")

eq(child.lua_get("_G.get_chat_lines()"), { "" })
end

T["EcaChatClear"]["works without error when buffer is already empty"] = function()
setup_env(false)
flush(200)

child.lua([[
local sidebar = require("eca").get()
vim.api.nvim_buf_set_lines(sidebar.containers.chat.bufnr, 0, -1, false, {})
]])

child.cmd("EcaChatClear")

eq(child.lua_get("_G.get_chat_lines()"), { "" })
end

T["EcaChatClear"]["clears hidden buffer when sidebar is closed with preserve=true"] = function()
setup_env(true)
flush(200)

child.lua("_G.fill_chat()")
child.lua([[require("eca").close_sidebar()]])
flush(100)

child.cmd("EcaChatClear")

eq(child.lua_get("_G.get_chat_lines()"), { "" })
end

T["EcaChatClear"]["buffer stays cleared on reopen with preserve=true"] = function()
setup_env(true)
flush(200)

child.lua("_G.fill_chat()")
child.lua([[require("eca").close_sidebar()]])
flush(100)

child.cmd("EcaChatClear")

eq(child.lua_get("_G.get_chat_lines()"), { "" })

child.lua([[require("eca").open_sidebar({})]])
flush(200)

eq(child.lua_get("_G.chat_has_old_content()"), false)
end

T["EcaChatClear"]["is a no-op when sidebar is closed and buffer was destroyed (preserve=false)"] = function()
-- With preserve=false, closing the sidebar destroys the buffer, so there is
-- nothing for EcaChatClear to clear. The important guarantee is that the
-- command does not raise an error in this state.
setup_env(false)
flush(200)

child.lua("_G.fill_chat()")
child.lua([[require("eca").close_sidebar()]])
flush(100)

local ok = child.lua_get("pcall(vim.cmd, 'EcaChatClear')")
eq(ok, true)
end

T["EcaChatClear"]["resets _welcome_message_applied and _force_welcome"] = function()
setup_env(false)
flush(200)

child.lua("_G.fill_chat()")
child.cmd("EcaChatClear")

local flags = child.lua_get("_G.get_sidebar_flags()")
eq(flags.welcome_message_applied, true)
eq(flags.force_welcome, false)
end
Comment on lines +162 to +172

T["EcaChatClear"]["is idempotent when called twice"] = function()
setup_env(false)
flush(200)

child.lua("_G.fill_chat()")
child.cmd("EcaChatClear")
child.cmd("EcaChatClear")

eq(child.lua_get("_G.get_chat_lines()"), { "" })
end

-- preserve_chat_history toggle cycle -----------------------------------------

T["preserve_chat_history"] = MiniTest.new_set()

T["preserve_chat_history"]["reuses same bufnr and keeps content across close/open"] = function()
setup_env(true)
flush(200)

child.lua("_G.fill_chat()")
local bufnr_before = child.lua_get("require('eca').get().containers.chat.bufnr")

child.lua([[require("eca").close_sidebar()]])
flush(100)
child.lua([[require("eca").open_sidebar({})]])
flush(200)

local bufnr_after = child.lua_get("require('eca').get().containers.chat.bufnr")
eq(bufnr_before, bufnr_after)
eq(child.lua_get("_G.chat_has_old_content()"), true)
end

T["preserve_chat_history"]["does not leak buffers across repeated toggles"] = function()
setup_env(true)
flush(200)

local buf_count_before = child.lua_get("#vim.api.nvim_list_bufs()")

for _ = 1, 5 do
child.lua([[require("eca").close_sidebar()]])
flush(100)
child.lua([[require("eca").open_sidebar({})]])
flush(200)
end

local buf_count_after = child.lua_get("#vim.api.nvim_list_bufs()")
-- Allow at most 1 extra buffer (nui internals), but definitely not 5+
eq(buf_count_after - buf_count_before <= 1, true)
end

T["preserve_chat_history"]["content is lost when preserve is disabled"] = function()
setup_env(false)
flush(200)

child.lua("_G.fill_chat()")

child.lua([[require("eca").close_sidebar()]])
flush(100)
child.lua([[require("eca").open_sidebar({})]])
flush(200)

eq(child.lua_get("_G.chat_has_old_content()"), false)
end

return T
Loading