From 7a7310dac0eb0b8fb3340ab6411f350295d59a03 Mon Sep 17 00:00:00 2001 From: joaopluigi Date: Fri, 10 Oct 2025 16:43:30 -0300 Subject: [PATCH 1/3] use a new chat-id when re-opening nvim --- lua/eca/mediator.lua | 4 ++++ lua/eca/sidebar.lua | 2 +- lua/eca/state.lua | 6 ++++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/lua/eca/mediator.lua b/lua/eca/mediator.lua index 9298891..9e081d5 100644 --- a/lua/eca/mediator.lua +++ b/lua/eca/mediator.lua @@ -86,4 +86,8 @@ function mediator:mcps() return mcps end +function mediator:id() + return (self.state and self.state.id) +end + return mediator diff --git a/lua/eca/sidebar.lua b/lua/eca/sidebar.lua index 743c3e9..1fb5b43 100644 --- a/lua/eca/sidebar.lua +++ b/lua/eca/sidebar.lua @@ -1275,7 +1275,7 @@ function M:_send_message(message) local contexts = self:get_contexts() self.mediator:send("chat/prompt", { - chatId = self.id, + chatId = self.mediator:id(), requestId = tostring(os.time()), message = message, contexts = contexts or {}, diff --git a/lua/eca/state.lua b/lua/eca/state.lua index 89677bc..9fa05f3 100644 --- a/lua/eca/state.lua +++ b/lua/eca/state.lua @@ -17,6 +17,7 @@ ---@field status string --- ---@class eca.State +---@field id string? ---@field status eca.StateStatus ---@field config eca.StateConfig ---@field usage eca.StateUsage @@ -26,6 +27,7 @@ local State = {} ---@return eca.State function State._new() local instance = setmetatable({ + id = nil, status = { state = "idle", text = "Idle", @@ -99,6 +101,10 @@ function State:_chat_content_received(message) return end + if message.params.chatId and message.params.chatId ~= self.id then + self.id = message.params.chatId + end + local content = message.params.content if content.type == "progress" then From 49b6dfbb3b908750415e9c0202a478ae334bbc51 Mon Sep 17 00:00:00 2001 From: joaopluigi Date: Fri, 10 Oct 2025 16:51:47 -0300 Subject: [PATCH 2/3] Add tests for chat-id lifecycle and reopen behavior MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a focused test suite validating that: - mediator/state track and expose the current chat-id - chat/prompt requests omit chatId before one is assigned and include it afterwards - reopening Neovim starts with no chat-id and uses a new one once provided by the server This safeguards the change to use a new chat-id when reopening Neovim and prevents regressions. 🤖 Generated with [eca](https://eca.dev) Co-Authored-By: eca --- tests/test_chat_id.lua | 118 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 tests/test_chat_id.lua diff --git a/tests/test_chat_id.lua b/tests/test_chat_id.lua new file mode 100644 index 0000000..49d135b --- /dev/null +++ b/tests/test_chat_id.lua @@ -0,0 +1,118 @@ +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 50) + child.api.nvim_eval("1") +end + +local function setup_env() + -- Initialize everything + _G.Server = require('eca.server').new() + _G.State = require('eca.state').new() + _G.Mediator = require('eca.mediator').new(_G.Server, _G.State) + _G.Sidebar = require('eca.sidebar').new(1, _G.Mediator) +end + +local T = MiniTest.new_set({ + hooks = { + pre_case = function() + child.restart({ "-u", "scripts/minimal_init.lua" }) + child.lua([[require('eca.commands').setup()]]) + child.lua_func(setup_env) + end, + post_case = function() + child.lua("if _G.Server and _G.Server.process then _G.Server.process:kill() end") + end, + post_once = child.stop, + }, +}) + +T["chat id lifecycle"] = MiniTest.new_set() + +T["chat id lifecycle"]["mediator id follows state id updates"] = function() + -- Initially there is no chat id + eq(child.lua_get("_G.State.id"), vim.NIL) + eq(child.lua_get("_G.Mediator:id()"), vim.NIL) + + -- Simulate a content notification with a chatId + child.lua([[require('eca.observer').notify({ + method = 'chat/contentReceived', + params = { chatId = 'chat-123', content = { type = 'progress', state = 'responding', text = '...' } }, + })]]) + flush() + + eq(child.lua_get("_G.State.id"), "chat-123") + eq(child.lua_get("_G.Mediator:id()"), "chat-123") +end + +T["chat id lifecycle"]["send request uses current chat id when available"] = function() + -- Ensure no chat id initially + eq(child.lua_get("_G.State.id"), vim.NIL) + + -- Send a message before any chat id exists + child.lua([[_G.Sidebar:_send_message('hello')]]) + + -- Inspect the last message sent to the server + local last_json = child.lua_get("_G.Server.messages[#_G.Server.messages].content") + local parsed = child.lua_get("vim.json.decode(...)", { last_json }) + eq(parsed.method, "chat/prompt") + -- When id is nil, chatId should be absent in params + eq(parsed.params.chatId == nil, true) + + -- Now simulate receiving a new chat id + child.lua([[require('eca.observer').notify({ + method = 'chat/contentReceived', + params = { chatId = 'chat-new', content = { type = 'progress', state = 'responding', text = '...' } }, + })]]) + flush() + eq(child.lua_get("_G.State.id"), "chat-new") + + -- Send another message; it should include the new chat id + child.lua([[_G.Sidebar:_send_message('again')]]) + local last_json2 = child.lua_get("_G.Server.messages[#_G.Server.messages].content") + local parsed2 = child.lua_get("vim.json.decode(...)", { last_json2 }) + eq(parsed2.method, "chat/prompt") + eq(parsed2.params.chatId, "chat-new") +end + +T["chat id lifecycle"]["reopening neovim uses a new chat id"] = function() + -- First session: set a chat id via notification + child.lua([[require('eca.observer').notify({ + method = 'chat/contentReceived', + params = { chatId = 'chat-first', content = { type = 'progress', state = 'responding', text = '...' } }, + })]]) + flush() + eq(child.lua_get("_G.State.id"), "chat-first") + + -- Restart Neovim to simulate reopening + child.restart({ "-u", "scripts/minimal_init.lua" }) + child.lua([[require('eca.commands').setup()]]) + child.lua_func(setup_env) + + -- New session should not carry over the previous chat id + eq(child.lua_get("_G.State.id"), vim.NIL) + + -- First message should not include a chatId + child.lua([[_G.Sidebar:_send_message('hello after reopen')]]) + local last_json3 = child.lua_get("_G.Server.messages[#_G.Server.messages].content") + local parsed = child.lua_get("vim.json.decode(...)", { last_json3 }) + eq(parsed.method, "chat/prompt") + eq(parsed.params.chatId == nil, true) + + -- After receiving content, a new chat id should be used + child.lua([[require('eca.observer').notify({ + method = 'chat/contentReceived', + params = { chatId = 'chat-second', content = { type = 'progress', state = 'responding', text = '...' } }, + })]]) + flush() + eq(child.lua_get("_G.State.id"), "chat-second") + + child.lua([[_G.Sidebar:_send_message('use new id')]]) + local last_json4 = child.lua_get("_G.Server.messages[#_G.Server.messages].content") + local parsed2 = child.lua_get("vim.json.decode(...)", { last_json4 }) + eq(parsed2.params.chatId, "chat-second") +end + +return T From 52f8181b80bdd15668e7ab5976b028351372ba3c Mon Sep 17 00:00:00 2001 From: joaopluigi Date: Fri, 10 Oct 2025 16:57:18 -0300 Subject: [PATCH 3/3] address code review changes --- lua/eca/mediator.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/eca/mediator.lua b/lua/eca/mediator.lua index 9e081d5..e70fd1d 100644 --- a/lua/eca/mediator.lua +++ b/lua/eca/mediator.lua @@ -87,7 +87,7 @@ function mediator:mcps() end function mediator:id() - return (self.state and self.state.id) + return self.state and self.state.id end return mediator