Skip to content
44 changes: 44 additions & 0 deletions lua/eca/commands.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
16 changes: 16 additions & 0 deletions lua/eca/mediator.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions lua/eca/server.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions lua/eca/sidebar.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
26 changes: 25 additions & 1 deletion lua/eca/state.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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
10 changes: 10 additions & 0 deletions scripts/server_path.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
175 changes: 175 additions & 0 deletions tests/test_select_commands.lua
Original file line number Diff line number Diff line change
@@ -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
6 changes: 3 additions & 3 deletions tests/test_server_integration.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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()
Expand All @@ -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()
Expand Down
1 change: 1 addition & 0 deletions tests/test_server_path.lua
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ local function setup_test_environment()
"nvim",
"--headless",
"--noplugin",
"--clean",
"--cmd",
[[lua package.preload["eca.path_finder"] = function()
local M = {}
Expand Down
Loading