From cc8aaf4136b6165a6bdbba1e290f09208ab973e0 Mon Sep 17 00:00:00 2001 From: joaopluigi Date: Fri, 3 Oct 2025 09:15:29 -0300 Subject: [PATCH 01/17] fix auto server start config and make it false by default --- lua/eca/config.lua | 2 +- lua/eca/init.lua | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/lua/eca/config.lua b/lua/eca/config.lua index 139e1ec..ec7126a 100644 --- a/lua/eca/config.lua +++ b/lua/eca/config.lua @@ -23,7 +23,7 @@ M._defaults = { behaviour = { auto_set_keymaps = true, auto_focus_sidebar = true, - auto_start_server = true, -- Automatically start server on setup + 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 }, diff --git a/lua/eca/init.lua b/lua/eca/init.lua index 5baed4e..e438b88 100644 --- a/lua/eca/init.lua +++ b/lua/eca/init.lua @@ -245,10 +245,12 @@ function M.setup(opts) M.state = require("eca.state").new() M.server = Server.new() M.mediator = require("eca.mediator").new(M.server, M.state) - -- Start server automatically in background - vim.defer_fn(function() - M.server:start() - end, 100) -- Small delay to ensure everything is loaded + + if Config.behaviour and Config.behaviour.auto_start_server then + vim.defer_fn(function() + M.server:start() + end, 100) -- Small delay to ensure everything is loaded + end M.did_setup = true end From e879e400fbf7403ed6fcfadc083f577f6c8e3b33 Mon Sep 17 00:00:00 2001 From: joaopluigi Date: Fri, 3 Oct 2025 09:17:23 -0300 Subject: [PATCH 02/17] make server start flow async --- lua/eca/path_finder.lua | 34 ++++---- lua/eca/server.lua | 134 +++++++++++++++++------------- tests/test_server_integration.lua | 16 +++- 3 files changed, 108 insertions(+), 76 deletions(-) diff --git a/lua/eca/path_finder.lua b/lua/eca/path_finder.lua index 733fa07..6e9cc94 100644 --- a/lua/eca/path_finder.lua +++ b/lua/eca/path_finder.lua @@ -100,7 +100,7 @@ function M:_write_version_file(version) file:write(version) file:close() else - Logger.notify("Could not write version file: " .. self._version_file, vim.log.levels.WARN) + Logger.warn("Could not write version file: " .. self._version_file) end end @@ -136,7 +136,7 @@ function M:_download_latest_server(server_path, version) local download_path = self._cache_dir .. "/" .. artifact_name - Logger.notify("Downloading latest ECA server version from: " .. download_url, vim.log.levels.INFO) + Logger.debug("Downloading latest ECA server version from: " .. download_url) -- Ensure cache directory exists vim.fn.mkdir(self._cache_dir, "p") @@ -150,8 +150,7 @@ function M:_download_latest_server(server_path, version) local download_result = os.execute(download_cmd) if download_result ~= 0 then - Logger.notify("Failed to download ECA server from: " .. download_url, vim.log.levels.ERROR) - return false + error("Failed to download ECA server from: " .. download_url) end -- Extract if it's a zip file @@ -161,8 +160,7 @@ function M:_download_latest_server(server_path, version) local extract_result = os.execute(extract_cmd) if extract_result ~= 0 then - Logger.notify("Failed to extract ECA server", vim.log.levels.ERROR) - return false + error("Failed to extract ECA server") end -- Remove the zip file after extraction @@ -170,26 +168,25 @@ function M:_download_latest_server(server_path, version) end -- Make executable (if not Windows) - if not vim.loop.os_uname().sysname:lower():match("windows") then + if not uv.os_uname().sysname:lower():match("windows") then os.execute("chmod +x " .. vim.fn.shellescape(server_path)) end if not Utils.file_exists(server_path) then - Logger.notify("ECA server binary not found after download and extraction", vim.log.levels.ERROR) - return false + error("ECA server binary not found after download and extraction") end -- Write version file self:_write_version_file(version) - Logger.notify("ECA server downloaded successfully", vim.log.levels.INFO) + Logger.debug("ECA server downloaded successfully") + return true end ---@return string -function M:find() +function M:find(custom_path) -- Check for custom server path first - local custom_path = Config.server_path if custom_path and custom_path:gsub("%s+", "") ~= "" then if Utils.file_exists(custom_path) then Logger.debug("Using custom server path: " .. custom_path) @@ -215,13 +212,18 @@ function M:find() -- Download if server doesn't exist or version is outdated if not server_exists or (latest_version and current_version ~= latest_version) then if not latest_version then - Logger.notify("Could not check for latest version, using existing server", vim.log.levels.WARN) + Logger.warn("Could not check for latest version, using existing server") return server_path end - local success = self:_download_latest_server(server_path, latest_version) - if not success then - error("Failed to download ECA server") + local success + + local ok, err = pcall(function() + success = self:_download_latest_server(server_path, latest_version) + end) + + if not ok or not success then + error((err and tostring(err)) or "Failed to download ECA server") end end diff --git a/lua/eca/server.lua b/lua/eca/server.lua index babe87e..745bef6 100644 --- a/lua/eca/server.lua +++ b/lua/eca/server.lua @@ -1,6 +1,5 @@ local Utils = require("eca.utils") local Config = require("eca.config") -local PathFinder = require("eca.path_finder") local Logger = require("eca.logger") ---@class eca.Server @@ -13,8 +12,9 @@ local Logger = require("eca.logger") ---@field on_stop function Callback when the server stops ---Called when a notification is received(message without an ID) ---@field on_notification fun(server: eca.Server, message: table) ----@field private path_finder eca.PathFinder Server path finder ---@field pending_requests {id: fun(err, data)} -- outgoing requests with callbacks +---@field cwd string Current working directory for the server process +---@field workspace_folders {name: string, uri: string}[] Workspace folders to send on initialize local M = {} ---@param opts? table @@ -22,7 +22,7 @@ local M = {} function M.new(opts) opts = vim.tbl_extend("keep", opts or {}, { on_start = function(pid) - require("eca.logger").notify("Started server with pid " .. pid, vim.log.levels.INFO) + require("eca.logger").debug("Started server with pid " .. pid) end, on_initialize = function() require("eca.logger").notify("Server ready to receive messages", vim.log.levels.INFO) @@ -37,7 +37,13 @@ function M.new(opts) require("eca.observer").notify(message) end) end, - path_finder = PathFinder:new(), + cwd = vim.fn.getcwd(), + workspace_folders = { + { + name = vim.fn.fnamemodify(Utils.get_project_root(), ":t"), + uri = "file://" .. Utils.get_project_root(), + }, + }, }) return setmetatable({ @@ -46,11 +52,12 @@ function M.new(opts) on_initialize = opts.on_initialize, on_stop = opts.on_stop, on_notification = opts.on_notification, - path_finder = opts.path_finder, messages = {}, pending_requests = {}, initialized = false, next_id = 0, + cwd = opts.cwd, + workspace_folders = opts.workspace_folders, }, { __index = M }) end @@ -93,70 +100,79 @@ end function M:start(opts) opts = opts or { initialize = true } - local server_path - local ok, path_finder_error = pcall(function() - server_path = self.path_finder:find() - end) + local custom_path = Config.server_path or "" - if not ok or not server_path then - Logger.notify("Could not find or download ECA server" .. tostring(path_finder_error), vim.log.levels.ERROR) - return - end + local this_file = debug.getinfo(1, "S").source:sub(2) + local proj_root = vim.fn.fnamemodify(this_file, ":p:h:h:h") + local script_path = proj_root .. "/scripts/server_path.lua" - Logger.debug("Starting ECA server: " .. server_path) + local nvim_exe = vim.fn.exepath("nvim") - local args = { server_path, "server" } - if Config.server_args and Config.server_args ~= "" then - vim.list_extend(args, vim.split(Config.server_args, " ")) + if not nvim_exe or nvim_exe == "" then + nvim_exe = "nvim" end - opts = vim.tbl_deep_extend("keep", opts, { - cmd = args, - text = true, - cwd = vim.fn.getcwd(), - stdin = true, - stdout = on_stdout(self), - stderr = on_stderr, - ---@param out vim.SystemCompleted - on_exit = function(out) - if out.code ~= 0 then - require("eca.logger").notify(string.format("Server exited with status code %d", out.code), vim.log.levels.ERROR) - end - end, - }) + local cmd = { nvim_exe, "-l", script_path, custom_path } - local started, process_or_err = pcall(vim.system, opts.cmd, { - cwd = opts.cwd, - text = opts.text, - stdin = opts.stdin, - stdout = opts.stdout, - stderr = opts.stderr, - }, opts.on_exit) - - if not started then - self.process = nil - Logger.notify(vim.inspect(process_or_err), vim.log.levels.ERROR) - return - end + vim.system(cmd, { text = true }, function(out) + if out.code ~= 0 then + Logger.notify(out.stderr, vim.log.levels.ERROR) + return + end - self.process = process_or_err - if self.on_start then - self.on_start(process_or_err.pid) - end + local stdout_lines = Utils.split_lines(out.stdout) + local server_path = stdout_lines[#stdout_lines] - if opts.initialize then - self:initialize() - end + Logger.debug("Starting ECA server: " .. server_path) + + local args = { server_path, "server" } + + if Config.server_args and Config.server_args ~= "" then + vim.list_extend(args, vim.split(Config.server_args, " ")) + end + + opts = vim.tbl_deep_extend("keep", opts, { + cmd = args, + text = true, + cwd = self.cwd, + stdin = true, + stdout = on_stdout(self), + stderr = on_stderr, + ---@param output vim.SystemCompleted + on_exit = function(output) + if output.code ~= 0 then + require("eca.logger").notify(string.format("Server exited with status code %d", output.code), vim.log.levels + .ERROR) + end + end, + }) + + local started, process_or_err = pcall(vim.system, opts.cmd, { + cwd = opts.cwd, + text = opts.text, + stdin = opts.stdin, + stdout = opts.stdout, + stderr = opts.stderr, + }, opts.on_exit) + + if not started then + self.process = nil + Logger.notify(vim.inspect(process_or_err), vim.log.levels.ERROR) + return + end + + self.process = process_or_err + if self.on_start then + self.on_start(process_or_err.pid) + end + + if opts.initialize then + self:initialize() + end + end) end function M:initialize() - local workspace_folders = { - { - name = vim.fn.fnamemodify(Utils.get_project_root(), ":t"), - uri = "file://" .. Utils.get_project_root(), - }, - } - self:send_request("initialize", { processId = vim.fn.getpid(), clientInfo = { @@ -168,7 +184,7 @@ function M:initialize() chat = true, }, }, - workspaceFolders = workspace_folders, + workspaceFolders = vim.deepcopy(self.workspace_folders), }, function(err, _) if err then Logger.notify("Could not initialize server: " .. err, vim.log.levels.ERROR) diff --git a/tests/test_server_integration.lua b/tests/test_server_integration.lua index 368438b..f2e646b 100644 --- a/tests/test_server_integration.lua +++ b/tests/test_server_integration.lua @@ -41,7 +41,13 @@ T["server"] = MiniTest.new_set() T["server"]["start"] = function() child.lua("_G.server.start({cmd = {'fake'}})") eq(child.lua_get("_G.server.process"), vim.NIL) - child.lua("_G.server:start()") + child.lua([[ + _G.server:start() + _G.server_started = vim.wait(5000, function() + return _G.server and _G.server:is_running() + end, 50) + ]]) + eq(child.lua_get("_G.server_started"), true) eq(child.lua_get("_G.server:is_running()"), true) end @@ -80,6 +86,14 @@ T["server"]["initialize"] = function() end child.lua("_G.method = " .. vim.inspect(test_case.method)) child.lua("_G.server:start({initialize = false})") + -- Wait for async server start + child.lua([[ + _G.server_started = vim.wait(5000, function() + return _G.server and _G.server:is_running() + end, 50) + ]]) + eq(child.lua_get("_G.server_started"), true) + child.lua_func(function() _G.server:send_request(_G.method, {}, function(err, result) _G.err = err From 23acc024047d99f18962c0adfbb5fe8031b1792c Mon Sep 17 00:00:00 2001 From: joaopluigi Date: Fri, 3 Oct 2025 09:18:23 -0300 Subject: [PATCH 03/17] add missing server path script --- scripts/server_path.lua | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 scripts/server_path.lua diff --git a/scripts/server_path.lua b/scripts/server_path.lua new file mode 100644 index 0000000..e8dc6f3 --- /dev/null +++ b/scripts/server_path.lua @@ -0,0 +1,16 @@ +local PathFinder = require("eca.path_finder") + +local custom_path = _G.arg[1] or "" +local path_finder = PathFinder:new() +local path + +local ok, err = pcall(function() + path = path_finder:find(custom_path) +end) + +if not ok then + io.stderr:write(tostring(err)) + os.exit(1) +end + +io.stdout:write(path) From 1118ad95d2464436ccdf732f316aaef50c921eaa Mon Sep 17 00:00:00 2001 From: joaopluigi Date: Fri, 3 Oct 2025 13:47:28 -0300 Subject: [PATCH 04/17] make server path call as lua command --- lua/eca/server.lua | 6 +++--- scripts/server_path.lua | 29 ++++++++++++++++++----------- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/lua/eca/server.lua b/lua/eca/server.lua index 745bef6..3c95724 100644 --- a/lua/eca/server.lua +++ b/lua/eca/server.lua @@ -100,8 +100,6 @@ end function M:start(opts) opts = opts or { initialize = true } - local custom_path = Config.server_path or "" - local this_file = debug.getinfo(1, "S").source:sub(2) local proj_root = vim.fn.fnamemodify(this_file, ":p:h:h:h") local script_path = proj_root .. "/scripts/server_path.lua" @@ -112,7 +110,9 @@ function M:start(opts) nvim_exe = "nvim" end - local cmd = { nvim_exe, "-l", script_path, custom_path } + local lua_cmd = string.format("lua ServerPath.run(%s)", Config.server_path or "") + + local cmd = { nvim_exe, "--headless", "--noplugin", "-u", script_path, "-c", lua_cmd } vim.system(cmd, { text = true }, function(out) if out.code ~= 0 then diff --git a/scripts/server_path.lua b/scripts/server_path.lua index e8dc6f3..8ab0562 100644 --- a/scripts/server_path.lua +++ b/scripts/server_path.lua @@ -1,16 +1,23 @@ -local PathFinder = require("eca.path_finder") +local ServerPath = {} -local custom_path = _G.arg[1] or "" -local path_finder = PathFinder:new() -local path +-- Export module +_G.ServerPath = ServerPath -local ok, err = pcall(function() - path = path_finder:find(custom_path) -end) +ServerPath.run = function(custom_path) + local path_finder = require("eca.path_finder"):new() + local path -if not ok then - io.stderr:write(tostring(err)) - os.exit(1) + local ok, err = pcall(function() + path = path_finder:find(custom_path) + end) + + if not ok then + io.stderr:write(tostring(err)) + os.exit(1) + end + + io.stdout:write(tostring(path)) + os.exit(0) end -io.stdout:write(path) +return ServerPath From 47423e0cbc032ed82bdf6d52331940b5a988d8fc Mon Sep 17 00:00:00 2001 From: joaopluigi Date: Mon, 6 Oct 2025 14:34:11 -0300 Subject: [PATCH 05/17] fix server integration test assertion --- tests/test_server_integration.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_server_integration.lua b/tests/test_server_integration.lua index f2e646b..6abdcd2 100644 --- a/tests/test_server_integration.lua +++ b/tests/test_server_integration.lua @@ -48,7 +48,6 @@ T["server"]["start"] = function() end, 50) ]]) eq(child.lua_get("_G.server_started"), true) - eq(child.lua_get("_G.server:is_running()"), true) end T["server"]["initialize"] = function() From 46e331a88c306d3777abdb14ecd34de7f51037cd Mon Sep 17 00:00:00 2001 From: joaopluigi Date: Mon, 6 Oct 2025 14:49:22 -0300 Subject: [PATCH 06/17] make path finder do nothing on server integration test --- tests/test_server_integration.lua | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/tests/test_server_integration.lua b/tests/test_server_integration.lua index 6abdcd2..98a1474 100644 --- a/tests/test_server_integration.lua +++ b/tests/test_server_integration.lua @@ -13,6 +13,7 @@ local function setup_test_environment() table.insert(_G.notifications, { message = message, level = level, opts = opts }) end _G.server = require("eca.server").new() + end local T = MiniTest.new_set({ @@ -20,6 +21,21 @@ local T = MiniTest.new_set({ pre_case = function() child.restart({ "-u", "scripts/minimal_init.lua" }) child.lua_func(setup_test_environment) + child.lua([[ + package.preload["eca.path_finder"] = function() + local M = {} + + function M.new() + return setmetatable({}, { __index = M }) + end + + function M.find(arg) + return ":" -- do nothing + end + + return M + end + ]]) end, post_case = function() child.lua("if _G.server and _G.server.process then _G.server.process:kill() end") @@ -39,7 +55,7 @@ end T["server"] = MiniTest.new_set() T["server"]["start"] = function() - child.lua("_G.server.start({cmd = {'fake'}})") + child.lua("_G.server.start()") eq(child.lua_get("_G.server.process"), vim.NIL) child.lua([[ _G.server:start() From 895b67458ed938861b653cf0c83e220e5625e19f Mon Sep 17 00:00:00 2001 From: joaopluigi Date: Mon, 6 Oct 2025 18:23:35 -0300 Subject: [PATCH 07/17] make server test integration more reasonable --- lua/eca/server.lua | 2 +- lua/eca/utils.lua | 7 +++ tests/test_server_integration.lua | 91 ++++++------------------------- 3 files changed, 25 insertions(+), 75 deletions(-) diff --git a/lua/eca/server.lua b/lua/eca/server.lua index 3c95724..7b6d58b 100644 --- a/lua/eca/server.lua +++ b/lua/eca/server.lua @@ -110,7 +110,7 @@ function M:start(opts) nvim_exe = "nvim" end - local lua_cmd = string.format("lua ServerPath.run(%s)", Config.server_path or "") + local lua_cmd = string.format("lua ServerPath.run(%s)", Utils.lua_quote(Config.options.server_path) or "") local cmd = { nvim_exe, "--headless", "--noplugin", "-u", script_path, "-c", lua_cmd } diff --git a/lua/eca/utils.lua b/lua/eca/utils.lua index 7a46ac8..42b78bd 100644 --- a/lua/eca/utils.lua +++ b/lua/eca/utils.lua @@ -118,4 +118,11 @@ function M.constants() return CONSTANTS end +---@param str string +---@return string +function M.lua_quote(str) + -- Escape backslashes and double quotes for Lua string literal + return '"' .. tostring(str):gsub('\\', '\\\\'):gsub('"', '\\"') .. '"' +end + return M diff --git a/tests/test_server_integration.lua b/tests/test_server_integration.lua index 98a1474..202761c 100644 --- a/tests/test_server_integration.lua +++ b/tests/test_server_integration.lua @@ -13,7 +13,6 @@ local function setup_test_environment() table.insert(_G.notifications, { message = message, level = level, opts = opts }) end _G.server = require("eca.server").new() - end local T = MiniTest.new_set({ @@ -21,21 +20,6 @@ local T = MiniTest.new_set({ pre_case = function() child.restart({ "-u", "scripts/minimal_init.lua" }) child.lua_func(setup_test_environment) - child.lua([[ - package.preload["eca.path_finder"] = function() - local M = {} - - function M.new() - return setmetatable({}, { __index = M }) - end - - function M.find(arg) - return ":" -- do nothing - end - - return M - end - ]]) end, post_case = function() child.lua("if _G.server and _G.server.process then _G.server.process:kill() end") @@ -54,70 +38,29 @@ local function sleep(ms) end T["server"] = MiniTest.new_set() + T["server"]["start"] = function() - child.lua("_G.server.start()") - eq(child.lua_get("_G.server.process"), vim.NIL) - child.lua([[ - _G.server:start() - _G.server_started = vim.wait(5000, function() + child.lua("_G.server:start()") + child.lua([[ + _G.server_started = vim.wait(10000, function() return _G.server and _G.server:is_running() - end, 50) + end, 100) ]]) eq(child.lua_get("_G.server_started"), true) + sleep(1000) + eq(child.lua_get("_G.server.initialized"), true) end -T["server"]["initialize"] = function() - local test_cases = { - { - method = "initialize", - want = { - chatBehaviors = { "agent", "plan" }, - chatDefaultBehavior = "agent", - chatDefaultModel = "anthropic/claude-sonnet-4-20250514", - chatWelcomeMessage = "Welcome to ECA!\n\nType '/' for commands\n\n", - models = { - "anthropic/claude-3-5-haiku-20241022", - "anthropic/claude-opus-4-1-20250805", - "anthropic/claude-opus-4-20250514", - "anthropic/claude-sonnet-4-20250514", - "github-copilot/claude-sonnet-4", - "github-copilot/gpt-4.1", - "github-copilot/gpt-5", - "github-copilot/gpt-5-mini", - "openai/gpt-4.1", - "openai/gpt-5", - "openai/gpt-5-mini", - "openai/gpt-5-nano", - "openai/o3", - "openai/o4-mini", - }, - }, - }, - { method = "not a command", want = vim.NIL }, - } - for i, test_case in ipairs(test_cases) do - if i == 1 then - test_case = test_cases[i + 1] - end - child.lua("_G.method = " .. vim.inspect(test_case.method)) - child.lua("_G.server:start({initialize = false})") - -- Wait for async server start - child.lua([[ - _G.server_started = vim.wait(5000, function() - return _G.server and _G.server:is_running() - end, 50) - ]]) - eq(child.lua_get("_G.server_started"), true) - - child.lua_func(function() - _G.server:send_request(_G.method, {}, function(err, result) - _G.err = err - _G.result = result - end) - end) - sleep(1500) - eq(child.lua_get("_G.result"), test_case.want) - end +T["server"]["start without initialize"] = function() + child.lua("_G.server:start({ initialize = false })") + child.lua([[ + _G.server_started = vim.wait(10000, function() + return _G.server and _G.server:is_running() + end, 100) + ]]) + eq(child.lua_get("_G.server_started"), true) + sleep(1000) + eq(child.lua_get("_G.server.initialized"), false) end return T From 1d46629b4c450766abe10221db73d9f26d41a0da Mon Sep 17 00:00:00 2001 From: joaopluigi Date: Mon, 6 Oct 2025 18:26:55 -0300 Subject: [PATCH 08/17] small adjustment --- lua/eca/server.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/eca/server.lua b/lua/eca/server.lua index 7b6d58b..99401e8 100644 --- a/lua/eca/server.lua +++ b/lua/eca/server.lua @@ -110,7 +110,7 @@ function M:start(opts) nvim_exe = "nvim" end - local lua_cmd = string.format("lua ServerPath.run(%s)", Utils.lua_quote(Config.options.server_path) or "") + local lua_cmd = string.format("lua ServerPath.run(%s)", Utils.lua_quote(Config.server_path) or "") local cmd = { nvim_exe, "--headless", "--noplugin", "-u", script_path, "-c", lua_cmd } From d837c4a633843f1cdf85feeac2c30a2088378501 Mon Sep 17 00:00:00 2001 From: joaopluigi Date: Tue, 7 Oct 2025 10:03:46 -0300 Subject: [PATCH 09/17] debug log in ci --- tests/test_server_integration.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_server_integration.lua b/tests/test_server_integration.lua index 202761c..3ed5963 100644 --- a/tests/test_server_integration.lua +++ b/tests/test_server_integration.lua @@ -46,6 +46,7 @@ T["server"]["start"] = function() return _G.server and _G.server:is_running() end, 100) ]]) + eq(child.lua_get("_G.log"), true) eq(child.lua_get("_G.server_started"), true) sleep(1000) eq(child.lua_get("_G.server.initialized"), true) From 0356e450d4ebbfb9fc4094b70e24ee0702814994 Mon Sep 17 00:00:00 2001 From: joaopluigi Date: Tue, 7 Oct 2025 10:04:57 -0300 Subject: [PATCH 10/17] more debug log in ci --- tests/test_server_integration.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_server_integration.lua b/tests/test_server_integration.lua index 3ed5963..d6a8163 100644 --- a/tests/test_server_integration.lua +++ b/tests/test_server_integration.lua @@ -59,6 +59,7 @@ T["server"]["start without initialize"] = function() return _G.server and _G.server:is_running() end, 100) ]]) + eq(child.lua_get("_G.notifications"), true) eq(child.lua_get("_G.server_started"), true) sleep(1000) eq(child.lua_get("_G.server.initialized"), false) From 4c59602ba32329e81e06df1434629a887e25941e Mon Sep 17 00:00:00 2001 From: joaopluigi Date: Tue, 7 Oct 2025 10:21:28 -0300 Subject: [PATCH 11/17] try passing all deps to script execution --- lua/eca/path_finder.lua | 1 - lua/eca/server.lua | 4 +++- tests/test_server_integration.lua | 2 -- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/lua/eca/path_finder.lua b/lua/eca/path_finder.lua index 6e9cc94..7f967fd 100644 --- a/lua/eca/path_finder.lua +++ b/lua/eca/path_finder.lua @@ -1,6 +1,5 @@ local uv = vim.uv or vim.loop local Utils = require("eca.utils") -local Config = require("eca.config") local Logger = require("eca.logger") ---@class eca.PathFinder diff --git a/lua/eca/server.lua b/lua/eca/server.lua index 99401e8..77f26f8 100644 --- a/lua/eca/server.lua +++ b/lua/eca/server.lua @@ -103,6 +103,8 @@ function M:start(opts) local this_file = debug.getinfo(1, "S").source:sub(2) local proj_root = vim.fn.fnamemodify(this_file, ":p:h:h:h") local script_path = proj_root .. "/scripts/server_path.lua" + local logger_path = proj_root .. "/lua/eca/logger.lua" + local utils_path = proj_root .. "/lua/eca/utils.lua" local nvim_exe = vim.fn.exepath("nvim") @@ -112,7 +114,7 @@ function M:start(opts) local lua_cmd = string.format("lua ServerPath.run(%s)", Utils.lua_quote(Config.server_path) or "") - local cmd = { nvim_exe, "--headless", "--noplugin", "-u", script_path, "-c", lua_cmd } + local cmd = { nvim_exe, "--headless", "--noplugin", "-u", utils_path, "-u", logger_path, "-u", script_path, "-c", lua_cmd } vim.system(cmd, { text = true }, function(out) if out.code ~= 0 then diff --git a/tests/test_server_integration.lua b/tests/test_server_integration.lua index d6a8163..202761c 100644 --- a/tests/test_server_integration.lua +++ b/tests/test_server_integration.lua @@ -46,7 +46,6 @@ T["server"]["start"] = function() return _G.server and _G.server:is_running() end, 100) ]]) - eq(child.lua_get("_G.log"), true) eq(child.lua_get("_G.server_started"), true) sleep(1000) eq(child.lua_get("_G.server.initialized"), true) @@ -59,7 +58,6 @@ T["server"]["start without initialize"] = function() return _G.server and _G.server:is_running() end, 100) ]]) - eq(child.lua_get("_G.notifications"), true) eq(child.lua_get("_G.server_started"), true) sleep(1000) eq(child.lua_get("_G.server.initialized"), false) From 3ae59404d220b3d1ea6963cae8d0f4ef4de05eeb Mon Sep 17 00:00:00 2001 From: joaopluigi Date: Tue, 7 Oct 2025 10:50:56 -0300 Subject: [PATCH 12/17] make server_path work headless and in clean mode --- lua/eca/api.lua | 5 +++++ lua/eca/server.lua | 4 +--- scripts/server_path.lua | 13 +++++++++++-- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/lua/eca/api.lua b/lua/eca/api.lua index 8ebc6f0..851a102 100644 --- a/lua/eca/api.lua +++ b/lua/eca/api.lua @@ -11,6 +11,11 @@ local M = {} function M.chat(opts) opts = opts or {} local eca = require("eca") + + if not M.is_server_running() then + M.start_server() + end + eca.open_sidebar(opts) end diff --git a/lua/eca/server.lua b/lua/eca/server.lua index 77f26f8..4d8b8c1 100644 --- a/lua/eca/server.lua +++ b/lua/eca/server.lua @@ -103,8 +103,6 @@ function M:start(opts) local this_file = debug.getinfo(1, "S").source:sub(2) local proj_root = vim.fn.fnamemodify(this_file, ":p:h:h:h") local script_path = proj_root .. "/scripts/server_path.lua" - local logger_path = proj_root .. "/lua/eca/logger.lua" - local utils_path = proj_root .. "/lua/eca/utils.lua" local nvim_exe = vim.fn.exepath("nvim") @@ -114,7 +112,7 @@ function M:start(opts) local lua_cmd = string.format("lua ServerPath.run(%s)", Utils.lua_quote(Config.server_path) or "") - local cmd = { nvim_exe, "--headless", "--noplugin", "-u", utils_path, "-u", logger_path, "-u", script_path, "-c", lua_cmd } + local cmd = { nvim_exe, "--headless", "--noplugin", "--clean", "-u", script_path, "-c", lua_cmd } vim.system(cmd, { text = true }, function(out) if out.code ~= 0 then diff --git a/scripts/server_path.lua b/scripts/server_path.lua index 8ab0562..d1ac13b 100644 --- a/scripts/server_path.lua +++ b/scripts/server_path.lua @@ -1,7 +1,16 @@ local ServerPath = {} --- Export module -_G.ServerPath = ServerPath +-- Setup if headless +if #vim.api.nvim_list_uis() == 0 then + _G.ServerPath = ServerPath + vim.cmd([[let &rtp.=','.getcwd()]]) + vim.cmd('set rtp+=deps/nui.nvim') + vim.cmd('set rtp+=deps/eca-nvim') + vim.o.swapfile = false + vim.o.backup = false + vim.o.writebackup = false + require("eca").setup({}) +end ServerPath.run = function(custom_path) local path_finder = require("eca.path_finder"):new() From 7be97829f623bd80345258ec20ccf3753cc67f7d Mon Sep 17 00:00:00 2001 From: joaopluigi Date: Tue, 7 Oct 2025 11:32:55 -0300 Subject: [PATCH 13/17] add another test case for inexistent server path and other small fixes --- lua/eca/path_finder.lua | 8 +++----- lua/eca/server.lua | 17 ++++++++++------- tests/test_server_integration.lua | 16 ++++++++++++++++ 3 files changed, 29 insertions(+), 12 deletions(-) diff --git a/lua/eca/path_finder.lua b/lua/eca/path_finder.lua index 7f967fd..3557f6e 100644 --- a/lua/eca/path_finder.lua +++ b/lua/eca/path_finder.lua @@ -187,12 +187,10 @@ end function M:find(custom_path) -- Check for custom server path first if custom_path and custom_path:gsub("%s+", "") ~= "" then - if Utils.file_exists(custom_path) then - Logger.debug("Using custom server path: " .. custom_path) - return custom_path - else - Logger.notify("Custom server path does not exist: " .. custom_path, vim.log.levels.WARN) + if not Utils.file_exists(custom_path) then + error("Custom server path does not exist: " .. custom_path) end + return custom_path end local server_path = self:_get_extension_server_path() diff --git a/lua/eca/server.lua b/lua/eca/server.lua index 4d8b8c1..c647494 100644 --- a/lua/eca/server.lua +++ b/lua/eca/server.lua @@ -110,7 +110,7 @@ function M:start(opts) nvim_exe = "nvim" end - local lua_cmd = string.format("lua ServerPath.run(%s)", Utils.lua_quote(Config.server_path) or "") + local lua_cmd = string.format("lua ServerPath.run(%s)", Utils.lua_quote(Config.server_path or "")) local cmd = { nvim_exe, "--headless", "--noplugin", "--clean", "-u", script_path, "-c", lua_cmd } @@ -253,12 +253,6 @@ end ---@param params table ---@param callback? function function M:send_request(method, params, callback) - if not self:is_running() then - Logger.error("ECA server is not running") - if callback then - callback("Server not running", nil) - end - end local id = self:get_next_id() local message = { jsonrpc = "2.0", @@ -272,6 +266,15 @@ function M:send_request(method, params, callback) local json = vim.json.encode(message) table.insert(self.messages, { content = json, content_length = #json }) + + if not self:is_running() then + Logger.error("ECA server is not running") + if callback then + callback("Server not running", nil) + end + return + end + local content = string.format("Content-Length: %d\r\n\r\n%s", #json, json) self.process:write(content) end diff --git a/tests/test_server_integration.lua b/tests/test_server_integration.lua index 202761c..e9a73db 100644 --- a/tests/test_server_integration.lua +++ b/tests/test_server_integration.lua @@ -63,4 +63,20 @@ T["server"]["start without initialize"] = function() eq(child.lua_get("_G.server.initialized"), false) end +T["server"]["start with inexistent path"] = function() + child.lua([[ + Config = require("eca.config") + Config.setup({ server_path = "non-existing-path" } ) + _G.server:start() + ]]) + child.lua([[ + _G.server_started = vim.wait(1000, function() + return _G.server and _G.server:is_running() + end, 100) + ]]) + eq(string.find(child.lua_get("_G.notifications[1].message"), "non-existing-path", 1 , true) ~= nil, true) + eq(child.lua_get("_G.server_started"), false) + eq(child.lua_get("_G.server.initialized"), false) +end + return T From ccca242d6bc20a1cb03116a1252f8ae12d4a3095 Mon Sep 17 00:00:00 2001 From: joaopluigi Date: Tue, 7 Oct 2025 11:36:26 -0300 Subject: [PATCH 14/17] rename behaviour -> behavior --- docs/configuration.md | 10 +++++----- docs/installation.md | 2 +- docs/troubleshooting.md | 4 ++-- lua/eca/config.lua | 2 +- lua/eca/init.lua | 4 ++-- plugin-spec.lua | 2 +- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index db96b5d..2ec9f58 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -21,7 +21,7 @@ require("eca").setup({ usage_string_format = "{messageCost} / {sessionCost}", -- === BEHAVIOR === - behaviour = { + behavior = { -- Set keymaps automatically auto_set_keymaps = true, @@ -114,7 +114,7 @@ require("eca").setup({ ### Minimalist ```lua require("eca").setup({ - behaviour = { show_status_updates = false }, + behavior = { show_status_updates = false }, windows = { width = 30 }, chat = { headers = { @@ -128,7 +128,7 @@ require("eca").setup({ ### Visual/UX focused ```lua require("eca").setup({ - behaviour = { auto_focus_sidebar = true }, + behavior = { auto_focus_sidebar = true }, windows = { width = 50, wrap = true, @@ -149,7 +149,7 @@ require("eca").setup({ require("eca").setup({ debug = true, server_args = "--log-level debug", - behaviour = { + behavior = { auto_start_server = true, show_status_updates = true, }, @@ -164,7 +164,7 @@ require("eca").setup({ ### Performance-oriented ```lua require("eca").setup({ - behaviour = { + behavior = { auto_focus_sidebar = false, show_status_updates = false, }, diff --git a/docs/installation.md b/docs/installation.md index c5df383..7167653 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -53,7 +53,7 @@ Advanced setup example: opts = { debug = false, server_path = "", - behaviour = { + behavior = { auto_set_keymaps = true, auto_focus_sidebar = true, }, diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 92df5c0..d37e5fc 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -36,7 +36,7 @@ Solutions: Symptoms: `ec` doesn't open chat Solutions: -- Ensure `behaviour.auto_set_keymaps = true` +- Ensure `behavior.auto_set_keymaps = true` - Confirm your `` key (default: `\`) - Configure shortcuts manually: @@ -61,5 +61,5 @@ Symptoms: Lag when typing, slow responses Solutions: - Reduce window width: `windows.width = 25` -- Disable visual updates: `behaviour.show_status_updates = false` +- Disable visual updates: `behavior.show_status_updates = false` - Use the minimalist configuration preset diff --git a/lua/eca/config.lua b/lua/eca/config.lua index ec7126a..a7f8604 100644 --- a/lua/eca/config.lua +++ b/lua/eca/config.lua @@ -20,7 +20,7 @@ M._defaults = { file = "", max_file_size_mb = 10, -- Maximum log file size in MB before warning }, - behaviour = { + behavior = { auto_set_keymaps = true, auto_focus_sidebar = true, auto_start_server = false, -- Automatically start server on setup diff --git a/lua/eca/init.lua b/lua/eca/init.lua index e438b88..612bbcf 100644 --- a/lua/eca/init.lua +++ b/lua/eca/init.lua @@ -32,7 +32,7 @@ function H.keymaps() require("eca.api").focus() end, { noremap = true }) - if Config.behaviour.auto_set_keymaps then + if Config.behavior.auto_set_keymaps then Utils.safe_keymap_set({ "n", "v" }, Config.mappings.chat, function() require("eca.api").chat() end, { desc = "eca: open chat" }) @@ -246,7 +246,7 @@ function M.setup(opts) M.server = Server.new() M.mediator = require("eca.mediator").new(M.server, M.state) - if Config.behaviour and Config.behaviour.auto_start_server then + if Config.behavior and Config.behavior.auto_start_server then vim.defer_fn(function() M.server:start() end, 100) -- Small delay to ensure everything is loaded diff --git a/plugin-spec.lua b/plugin-spec.lua index a441d89..68a4b71 100644 --- a/plugin-spec.lua +++ b/plugin-spec.lua @@ -17,7 +17,7 @@ return { file = "", -- Empty string uses default path max_file_size_mb = 10, }, - behaviour = { + behavior = { auto_set_keymaps = true, auto_focus_sidebar = true, }, From 6a3cdad8da44bbaa417f77f9a364a1e05b7b4a17 Mon Sep 17 00:00:00 2001 From: joaopluigi Date: Tue, 7 Oct 2025 13:15:44 -0300 Subject: [PATCH 15/17] add tests for server path script --- tests/test_server_path.lua | 70 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 tests/test_server_path.lua diff --git a/tests/test_server_path.lua b/tests/test_server_path.lua new file mode 100644 index 0000000..a285171 --- /dev/null +++ b/tests/test_server_path.lua @@ -0,0 +1,70 @@ +local MiniTest = require("mini.test") +local eq = MiniTest.expect.equality +local child = MiniTest.new_child_neovim() + +local function setup_test_environment() + Utils = require("eca.utils") + _G.cmd = function(custom_path) + return { + "nvim", + "--headless", + "--noplugin", + "--clean", + "--cmd", + [[lua package.preload["eca.path_finder"] = function() + local M = {} + function M.new() + return setmetatable({}, { __index = M }) + end + function M:find(custom_path) + if custom_path == "error" then + error("custom-server-path-error") + end + return (custom_path ~= "" and custom_path) or "no-custom-server-path" + end + return M + end]], + "-u", + "scripts/server_path.lua", + "-c", + string.format("lua ServerPath.run(%s)", Utils.lua_quote(custom_path or "")), + } + end +end + +local T = MiniTest.new_set({ + hooks = { + pre_case = function() + child.restart({ "-u", "scripts/minimal_init.lua" }) + child.lua_func(setup_test_environment) + end, + post_case = function() + end, + post_once = child.stop, + }, +}) + +T["server_path"] = MiniTest.new_set() + +T["server_path"]["run without custom path should print to stdout"] = function() + child.lua("_G.result = vim.system(_G.cmd(), { text = true }):wait()") + eq(child.lua_get("_G.result.code"), 0) + eq(child.lua_get("_G.result.stdout"), "no-custom-server-path") + eq(child.lua_get("_G.result.stderr"), "") +end + +T["server_path"]["run with custom path should print to stdout"] = function() + child.lua("_G.result = vim.system(_G.cmd('custom-server-path'), { text = true }):wait()") + eq(child.lua_get("_G.result.code"), 0) + eq(child.lua_get("_G.result.stdout"), "custom-server-path") + eq(child.lua_get("_G.result.stderr"), "") +end + +T["server_path"]["run with error should print to stderr"] = function() + child.lua("_G.result = vim.system(_G.cmd('error'), { text = true }):wait()") + eq(child.lua_get("_G.result.code"), 1) + eq(child.lua_get("_G.result.stdout"), "") + eq(string.find(child.lua_get("_G.result.stderr"), "custom-server-path-error", 1 , true) ~= nil, true) +end + +return T From 59bf0f24467c40934ec1d8f07694d8ccacfc93b1 Mon Sep 17 00:00:00 2001 From: joaopluigi Date: Tue, 7 Oct 2025 13:26:46 -0300 Subject: [PATCH 16/17] tiny adjustments --- docs/configuration.md | 2 +- lua/eca/init.lua | 2 +- lua/eca/sidebar.lua | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 2ec9f58..95be3b9 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -29,7 +29,7 @@ require("eca").setup({ auto_focus_sidebar = true, -- Start server automatically - auto_start_server = true, + auto_start_server = false, -- Download server automatically if not found auto_download = true, diff --git a/lua/eca/init.lua b/lua/eca/init.lua index 612bbcf..558cf78 100644 --- a/lua/eca/init.lua +++ b/lua/eca/init.lua @@ -32,7 +32,7 @@ function H.keymaps() require("eca.api").focus() end, { noremap = true }) - if Config.behavior.auto_set_keymaps then + if Config.behavior and Config.behavior.auto_set_keymaps then Utils.safe_keymap_set({ "n", "v" }, Config.mappings.chat, function() require("eca.api").chat() end, { desc = "eca: open chat" }) diff --git a/lua/eca/sidebar.lua b/lua/eca/sidebar.lua index 5786c45..e0c7d02 100644 --- a/lua/eca/sidebar.lua +++ b/lua/eca/sidebar.lua @@ -1223,7 +1223,7 @@ function M:_update_welcome_content() local cfg_msg = (cfg.message and cfg.message ~= "" and cfg.message) or nil local welcome_message = cfg_msg or (self.mediator and self.mediator:welcome_message() or nil) - local lines = { "Waiting for welcome message from ECA server..." } + local lines = { "Waiting for server to start..." } if welcome_message and welcome_message ~= "" then lines = Utils.split_lines(welcome_message) From abffd15addcd62cad6bd7548f3c640656e720683 Mon Sep 17 00:00:00 2001 From: joaopluigi Date: Tue, 7 Oct 2025 14:51:34 -0300 Subject: [PATCH 17/17] rm --clean option since it does not allow to run script outside the eca-nvim dir --- lua/eca/server.lua | 2 +- scripts/server_path.lua | 3 --- tests/test_server_path.lua | 1 - 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/lua/eca/server.lua b/lua/eca/server.lua index c647494..54b94e4 100644 --- a/lua/eca/server.lua +++ b/lua/eca/server.lua @@ -112,7 +112,7 @@ function M:start(opts) local lua_cmd = string.format("lua ServerPath.run(%s)", Utils.lua_quote(Config.server_path or "")) - local cmd = { nvim_exe, "--headless", "--noplugin", "--clean", "-u", script_path, "-c", lua_cmd } + local cmd = { nvim_exe, "--headless", "--noplugin", "-u", script_path, "-c", lua_cmd } vim.system(cmd, { text = true }, function(out) if out.code ~= 0 then diff --git a/scripts/server_path.lua b/scripts/server_path.lua index d1ac13b..8e0317b 100644 --- a/scripts/server_path.lua +++ b/scripts/server_path.lua @@ -3,9 +3,6 @@ local ServerPath = {} -- Setup if headless if #vim.api.nvim_list_uis() == 0 then _G.ServerPath = ServerPath - vim.cmd([[let &rtp.=','.getcwd()]]) - vim.cmd('set rtp+=deps/nui.nvim') - vim.cmd('set rtp+=deps/eca-nvim') vim.o.swapfile = false vim.o.backup = false vim.o.writebackup = false diff --git a/tests/test_server_path.lua b/tests/test_server_path.lua index a285171..b0880f5 100644 --- a/tests/test_server_path.lua +++ b/tests/test_server_path.lua @@ -9,7 +9,6 @@ local function setup_test_environment() "nvim", "--headless", "--noplugin", - "--clean", "--cmd", [[lua package.preload["eca.path_finder"] = function() local M = {}