diff --git a/lua/eca/commands.lua b/lua/eca/commands.lua index 6a245e6..24d26cc 100644 --- a/lua/eca/commands.lua +++ b/lua/eca/commands.lua @@ -343,6 +343,50 @@ function M.setup() desc = "Emergency fix for treesitter issues in ECA chat", }) + vim.api.nvim_create_user_command("EcaChatSelectModel", function() + local eca = require("eca") + + if not eca or not eca.current or not eca.current.sidebar then + Logger.notify("No active ECA sidebar found", vim.log.levels.WARN) + return + end + + local chat = eca.current.sidebar + local models = chat.mediator:models() + + vim.ui.select(models, { + prompt = "Select ECA Chat Model:", + }, function(choice) + if choice then + chat.mediator:update_selected_model(choice) + end + end) + end, { + desc = "Select current ECA Chat model", + }) + + vim.api.nvim_create_user_command("EcaChatSelectBehavior", function() + local eca = require("eca") + + if not eca or not eca.current or not eca.current.sidebar then + Logger.notify("No active ECA sidebar found", vim.log.levels.WARN) + return + end + + local chat = eca.current.sidebar + local behaviors = chat.mediator:behaviors() + + vim.ui.select(behaviors, { + prompt = "Select ECA Chat Behavior:", + }, function(choice) + if choice then + chat.mediator:update_selected_behavior(choice) + end + end) + end, { + desc = "Select current ECA Chat behavior", + }) + Logger.debug("ECA commands registered") end diff --git a/lua/eca/mediator.lua b/lua/eca/mediator.lua index 7bdc8e6..9298891 100644 --- a/lua/eca/mediator.lua +++ b/lua/eca/mediator.lua @@ -26,14 +26,30 @@ function mediator:send(method, params, callback) self.server:send_request(method, params, callback) end +function mediator:behaviors() + return self.state.config.behaviors.list +end + function mediator:selected_behavior() return self.state.config.behaviors.selected end +function mediator:update_selected_behavior(behavior) + self.state:update_selected_behavior(behavior) +end + +function mediator:models() + return self.state.config.models.list +end + function mediator:selected_model() return self.state.config.models.selected end +function mediator:update_selected_model(model) + self.state:update_selected_model(model) +end + function mediator:tokens_session() return self.state.usage.tokens.session end diff --git a/lua/eca/server.lua b/lua/eca/server.lua index 54b94e4..5ac3a23 100644 --- a/lua/eca/server.lua +++ b/lua/eca/server.lua @@ -98,7 +98,7 @@ end ---in testing ---@param opts? eca.ServerStartOpts function M:start(opts) - opts = opts or { initialize = true } + opts = vim.tbl_deep_extend("force", { initialize = true }, opts or {}) local this_file = debug.getinfo(1, "S").source:sub(2) local proj_root = vim.fn.fnamemodify(this_file, ":p:h:h:h") @@ -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", "-u", script_path, "-c", lua_cmd } + local cmd = { nvim_exe, "--headless", "--noplugin", (opts.clean and " --clean" or ""), "-u", script_path, "-c", lua_cmd } vim.system(cmd, { text = true }, function(out) if out.code ~= 0 then diff --git a/lua/eca/sidebar.lua b/lua/eca/sidebar.lua index e0c7d02..743c3e9 100644 --- a/lua/eca/sidebar.lua +++ b/lua/eca/sidebar.lua @@ -1279,6 +1279,8 @@ function M:_send_message(message) requestId = tostring(os.time()), message = message, contexts = contexts or {}, + model = self.mediator:selected_model(), + behavior = self.mediator:selected_behavior(), }, function(err, result) if err then print("err is " .. err) diff --git a/lua/eca/state.lua b/lua/eca/state.lua index 2da885c..89677bc 100644 --- a/lua/eca/state.lua +++ b/lua/eca/state.lua @@ -168,7 +168,7 @@ end function State:_update_usage(usage) self.usage = { tokens = { - limit = (usage.limit and usage.limit.output) or self.usage.tokens.limit, + limit = (usage.limit and usage.limit.context) or self.usage.tokens.limit, session = usage.sessionTokens or self.usage.tokens.session, }, costs = { @@ -198,4 +198,28 @@ function State:_update_tools(tool) end) end +function State:update_selected_model(model) + if not model or type(model) ~= "string" then + return + end + + self.config.models.selected = model + + vim.schedule(function() + require("eca.observer").notify({ type = "state/updated", content = { config = vim.deepcopy(self.config) } }) + end) +end + +function State:update_selected_behavior(behavior) + if not behavior or type(behavior) ~= "string" then + return + end + + self.config.behaviors.selected = behavior + + vim.schedule(function() + require("eca.observer").notify({ type = "state/updated", content = { config = vim.deepcopy(self.config) } }) + end) +end + return State diff --git a/scripts/server_path.lua b/scripts/server_path.lua index 8e0317b..f7b0570 100644 --- a/scripts/server_path.lua +++ b/scripts/server_path.lua @@ -3,9 +3,19 @@ local ServerPath = {} -- Setup if headless if #vim.api.nvim_list_uis() == 0 then _G.ServerPath = ServerPath + + -- hijack to make server tests work on CI using --clean mode + local eca_available = pcall(require, "eca") + if not eca_available then + vim.cmd([[let &rtp.=','.getcwd()]]) + vim.cmd('set rtp+=deps/nui.nvim') + vim.cmd('set rtp+=deps/eca-nvim') + end + vim.o.swapfile = false vim.o.backup = false vim.o.writebackup = false + require("eca").setup({}) end diff --git a/tests/test_select_commands.lua b/tests/test_select_commands.lua new file mode 100644 index 0000000..23c028d --- /dev/null +++ b/tests/test_select_commands.lua @@ -0,0 +1,175 @@ +local MiniTest = require("mini.test") +local eq = MiniTest.expect.equality +local child = MiniTest.new_child_neovim() + +local function setup_test_environment() + -- Setup commands + require('eca.commands').setup() + + -- 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) + _G.Eca = require('eca') + _G.Eca.current = { sidebar = _G.Sidebar } + + -- Mock vim.ui.select for testing + _G.selected_choice = nil + _G.shown_items = nil + _G.shown_prompt = nil + _G.original_select = vim.ui.select + + _G.mock_select = function(choice) + _G.selected_choice = choice + vim.ui.select = function(items, opts, on_choice) + _G.shown_items = items + _G.shown_prompt = opts.prompt + on_choice(choice) + end + end + + _G.restore_select = function() + vim.ui.select = _G.original_select + 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() + child.lua([[_G.restore_select()]]) + end, + post_once = child.stop, + }, +}) + +-- Test EcaChatSelectModel command +T["EcaChatSelectModel"] = MiniTest.new_set() + +T["EcaChatSelectModel"]["command is registered"] = function() + local commands = child.lua_get("vim.api.nvim_get_commands({})") + eq(type(commands.EcaChatSelectModel), "table") + eq(commands.EcaChatSelectModel.name, "EcaChatSelectModel") +end + +T["EcaChatSelectModel"]["updates state when model selected"] = function() + -- Setup initial state with models + child.lua([[ + _G.State.config.models.list = { "model1", "model2", "model3" } + _G.State.config.models.selected = "model1" + + -- Mock vim.ui.select to auto-select model2 + _G.mock_select("model2") + ]]) + + -- Execute command + child.cmd("EcaChatSelectModel") + + -- Check that state was updated + eq(child.lua_get("_G.State.config.models.selected"), "model2") +end + +T["EcaChatSelectModel"]["handles nil selection"] = function() + -- Setup initial state + child.lua([[ + _G.State.config.models.list = { "model1", "model2" } + _G.State.config.models.selected = "model1" + + -- Mock vim.ui.select to return nil (user cancelled) + _G.mock_select(nil) + ]]) + + -- Execute command + child.cmd("EcaChatSelectModel") + + -- Check that state was NOT updated (still model1) + eq(child.lua_get("_G.State.config.models.selected"), "model1") +end + +T["EcaChatSelectModel"]["displays all available models"] = function() + -- Setup models list + child.lua([[ + _G.State.config.models.list = { "gpt-4", "gpt-3.5-turbo", "claude-3" } + + -- Mock vim.ui.select to capture the items shown + _G.mock_select(nil) + ]]) + + -- Execute command + child.cmd("EcaChatSelectModel") + + -- Verify all models were shown + local shown_items = child.lua_get("_G.shown_items") + eq(shown_items[1], "gpt-4") + eq(shown_items[2], "gpt-3.5-turbo") + eq(shown_items[3], "claude-3") +end + +-- Test EcaChatSelectBehavior command +T["EcaChatSelectBehavior"] = MiniTest.new_set() + +T["EcaChatSelectBehavior"]["command is registered"] = function() + local commands = child.lua_get("vim.api.nvim_get_commands({})") + eq(type(commands.EcaChatSelectBehavior), "table") + eq(commands.EcaChatSelectBehavior.name, "EcaChatSelectBehavior") +end + +T["EcaChatSelectBehavior"]["updates state when behavior selected"] = function() + -- Setup initial state with behaviors + child.lua([[ + _G.State.config.behaviors.list = { "helpful", "creative", "concise" } + _G.State.config.behaviors.selected = "helpful" + + -- Mock vim.ui.select to auto-select creative + _G.mock_select("creative") + ]]) + + -- Execute command + child.cmd("EcaChatSelectBehavior") + + -- Check that state was updated + eq(child.lua_get("_G.State.config.behaviors.selected"), "creative") +end + +T["EcaChatSelectBehavior"]["handles nil selection"] = function() + -- Setup initial state + child.lua([[ + _G.State.config.behaviors.list = { "helpful", "creative" } + _G.State.config.behaviors.selected = "helpful" + + -- Mock vim.ui.select to return nil (user cancelled) + _G.mock_select(nil) + ]]) + + -- Execute command + child.cmd("EcaChatSelectBehavior") + + -- Check that state was NOT updated (still helpful) + eq(child.lua_get("_G.State.config.behaviors.selected"), "helpful") +end + +T["EcaChatSelectBehavior"]["displays all available behaviors"] = function() + -- Setup behaviors list + child.lua([[ + _G.State.config.behaviors.list = { "helpful", "creative", "concise", "technical" } + + -- Mock vim.ui.select to capture the items shown + _G.mock_select(nil) + ]]) + + -- Execute command + child.cmd("EcaChatSelectBehavior") + + -- Verify all behaviors were shown + local shown_items = child.lua_get("_G.shown_items") + eq(shown_items[1], "helpful") + eq(shown_items[2], "creative") + eq(shown_items[3], "concise") + eq(shown_items[4], "technical") +end + +return T diff --git a/tests/test_server_integration.lua b/tests/test_server_integration.lua index e9a73db..94d3e8d 100644 --- a/tests/test_server_integration.lua +++ b/tests/test_server_integration.lua @@ -40,7 +40,7 @@ end T["server"] = MiniTest.new_set() T["server"]["start"] = function() - child.lua("_G.server:start()") + child.lua("_G.server:start({ clean = true })") child.lua([[ _G.server_started = vim.wait(10000, function() return _G.server and _G.server:is_running() @@ -52,7 +52,7 @@ T["server"]["start"] = function() end T["server"]["start without initialize"] = function() - child.lua("_G.server:start({ initialize = false })") + child.lua("_G.server:start({ clean = true, initialize = false })") child.lua([[ _G.server_started = vim.wait(10000, function() return _G.server and _G.server:is_running() @@ -67,7 +67,7 @@ T["server"]["start with inexistent path"] = function() child.lua([[ Config = require("eca.config") Config.setup({ server_path = "non-existing-path" } ) - _G.server:start() + _G.server:start({ clean = true }) ]]) child.lua([[ _G.server_started = vim.wait(1000, function() diff --git a/tests/test_server_path.lua b/tests/test_server_path.lua index b0880f5..a285171 100644 --- a/tests/test_server_path.lua +++ b/tests/test_server_path.lua @@ -9,6 +9,7 @@ local function setup_test_environment() "nvim", "--headless", "--noplugin", + "--clean", "--cmd", [[lua package.preload["eca.path_finder"] = function() local M = {} diff --git a/tests/test_state.lua b/tests/test_state.lua index 66a6d11..9f89a78 100644 --- a/tests/test_state.lua +++ b/tests/test_state.lua @@ -97,7 +97,7 @@ T["updates via observer notifications"]["updates usage on usage content"] = func method = 'chat/contentReceived', params = { content = { type = 'usage', - limit = { output = 1024 }, + limit = { context = 1024 }, sessionTokens = 256, lastMessageCost = '0.42', sessionCost = '3.14', @@ -184,4 +184,72 @@ T["updates via observer notifications"]["updates tools on tool/serverUpdated"] = eq(#updates >= 1, true) end +T["update selected model and behavior"] = MiniTest.new_set() + +T["update selected model and behavior"]["update_selected_model updates config"] = function() + child.lua([[ + _G.State.config.models.list = { "model1", "model2" } + _G.State.config.models.selected = "model1" + + _G.State:update_selected_model("model2") + ]]) + + eq(child.lua_get("_G.State.config.models.selected"), "model2") +end + +T["update selected model and behavior"]["update_selected_model handles nil"] = function() + child.lua([[ + _G.State.config.models.selected = "model1" + + -- Should not update if nil is passed + _G.State:update_selected_model(nil) + ]]) + + eq(child.lua_get("_G.State.config.models.selected"), "model1") +end + +T["update selected model and behavior"]["update_selected_model handles non-string"] = function() + child.lua([[ + _G.State.config.models.selected = "model1" + + -- Should not update if non-string is passed + _G.State:update_selected_model(123) + ]]) + + eq(child.lua_get("_G.State.config.models.selected"), "model1") +end + +T["update selected model and behavior"]["update_selected_behavior updates config"] = function() + child.lua([[ + _G.State.config.behaviors.list = { "helpful", "creative" } + _G.State.config.behaviors.selected = "helpful" + + _G.State:update_selected_behavior("creative") + ]]) + + eq(child.lua_get("_G.State.config.behaviors.selected"), "creative") +end + +T["update selected model and behavior"]["update_selected_behavior handles nil"] = function() + child.lua([[ + _G.State.config.behaviors.selected = "helpful" + + -- Should not update if nil is passed + _G.State:update_selected_behavior(nil) + ]]) + + eq(child.lua_get("_G.State.config.behaviors.selected"), "helpful") +end + +T["update selected model and behavior"]["update_selected_behavior handles non-string"] = function() + child.lua([[ + _G.State.config.behaviors.selected = "helpful" + + -- Should not update if non-string is passed + _G.State:update_selected_behavior(123) + ]]) + + eq(child.lua_get("_G.State.config.behaviors.selected"), "helpful") +end + return T