Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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 thread
tamercuba marked this conversation as resolved.
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 thread
tamercuba marked this conversation as resolved.
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 thread
tamercuba marked this conversation as resolved.

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 thread
tamercuba marked this conversation as resolved.
Outdated
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 thread
tamercuba marked this conversation as resolved.

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 thread
tamercuba marked this conversation as resolved.
Outdated

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