Skip to content
30 changes: 30 additions & 0 deletions lua/eca/commands.lua
Original file line number Diff line number Diff line change
Expand Up @@ -346,4 +346,34 @@ function M.setup()
Logger.debug("ECA commands registered")
end

vim.api.nvim_create_user_command("EcaChatSelectModel", function()
local state = require("eca.state"):new()
local models = state.config.models.list

vim.ui.select(models, {
prompt = "Select ECA Chat Model:",
}, function(choice)
if choice then
state:update_selected_model(choice)
end
end)
end, {
desc = "Select Current ECA Chat model",

Copilot AI Oct 8, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent capitalization in command description. Should be 'Select current ECA Chat model' to match the pattern used in the behavior command description.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense

})

vim.api.nvim_create_user_command("EcaChatSelectBehavior", function()
local state = require("eca.state"):new()
local behaviors = state.config.behaviors.list

vim.ui.select(behaviors, {
prompt = "Select ECA Chat Behavior:",
}, function(choice)
if choice then
state:update_selected_behavior(choice)
end
end)
end, {
desc = "Select Current ECA Chat behavior",
})

Copilot AI Oct 8, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These command definitions are placed outside the M.setup() function, which means they will be executed immediately when the module is loaded rather than when setup() is called. This creates inconsistent behavior with other commands in the module and could lead to registration issues if the module is required multiple times.

Suggested change
Logger.debug("ECA commands registered")
end
vim.api.nvim_create_user_command("EcaChatSelectModel", function()
local state = require("eca.state"):new()
local models = state.config.models.list
vim.ui.select(models, {
prompt = "Select ECA Chat Model:",
}, function(choice)
if choice then
state:update_selected_model(choice)
end
end)
end, {
desc = "Select Current ECA Chat model",
})
vim.api.nvim_create_user_command("EcaChatSelectBehavior", function()
local state = require("eca.state"):new()
local behaviors = state.config.behaviors.list
vim.ui.select(behaviors, {
prompt = "Select ECA Chat Behavior:",
}, function(choice)
if choice then
state:update_selected_behavior(choice)
end
end)
end, {
desc = "Select Current ECA Chat behavior",
})
vim.api.nvim_create_user_command("EcaChatSelectModel", function()
local state = require("eca.state"):new()
local models = state.config.models.list
vim.ui.select(models, {
prompt = "Select ECA Chat Model:",
}, function(choice)
if choice then
state:update_selected_model(choice)
end
end)
end, {
desc = "Select Current ECA Chat model",
})
vim.api.nvim_create_user_command("EcaChatSelectBehavior", function()
local state = require("eca.state"):new()
local behaviors = state.config.behaviors.list
vim.ui.select(behaviors, {
prompt = "Select ECA Chat Behavior:",
}, function(choice)
if choice then
state:update_selected_behavior(choice)
end
end)
end, {
desc = "Select Current ECA Chat behavior",
})
Logger.debug("ECA commands registered")
end

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch


return M
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
168 changes: 168 additions & 0 deletions tests/test_select_commands.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
local MiniTest = require("mini.test")
local eq = MiniTest.expect.equality
local child = MiniTest.new_child_neovim()

local T = MiniTest.new_set({
hooks = {
pre_case = function()
child.restart({ "-u", "scripts/minimal_init.lua" })
child.lua([[
-- Setup commands
require('eca.commands').setup()

-- Instantiate state singleton
_G.State = require('eca.state').new()

-- 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,
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
70 changes: 69 additions & 1 deletion tests/test_state.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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