Skip to content

Commit 49a9ea4

Browse files
committed
feat: add ClaudeCodeSelectModel command with argument support
Change-Id: Ie982b04dc197c1abedc2f99e6d7a84974c9ee6b2 Signed-off-by: Thomas Kosiewski <[email protected]>
1 parent 404c420 commit 49a9ea4

File tree

6 files changed

+177
-14
lines changed

6 files changed

+177
-14
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ When Anthropic released Claude Code, they only supported VS Code and JetBrains.
3838
{ "<leader>af", "<cmd>ClaudeCodeFocus<cr>", desc = "Focus Claude" },
3939
{ "<leader>ar", "<cmd>ClaudeCode --resume<cr>", desc = "Resume Claude" },
4040
{ "<leader>aC", "<cmd>ClaudeCode --continue<cr>", desc = "Continue Claude" },
41+
{ "<leader>am", "<cmd>ClaudeCodeSelectModel<cr>", desc = "Select Claude model" },
4142
{ "<leader>ab", "<cmd>ClaudeCodeAdd %<cr>", desc = "Add current buffer" },
4243
{ "<leader>as", "<cmd>ClaudeCodeSend<cr>", mode = "v", desc = "Send to Claude" },
4344
{
@@ -91,6 +92,7 @@ That's it! The plugin will auto-configure everything else.
9192

9293
- `:ClaudeCode` - Toggle the Claude Code terminal window
9394
- `:ClaudeCodeFocus` - Smart focus/toggle Claude terminal
95+
- `:ClaudeCodeSelectModel` - Select Claude model and open terminal with optional arguments
9496
- `:ClaudeCodeSend` - Send current visual selection to Claude
9597
- `:ClaudeCodeAdd <file-path> [start-line] [end-line]` - Add specific file to Claude context with optional line range
9698
- `:ClaudeCodeDiffAccept` - Accept diff changes

dev-config.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ return {
1515
{ "<leader>af", "<cmd>ClaudeCodeFocus<cr>", desc = "Focus Claude" },
1616
{ "<leader>ar", "<cmd>ClaudeCode --resume<cr>", desc = "Resume Claude" },
1717
{ "<leader>aC", "<cmd>ClaudeCode --continue<cr>", desc = "Continue Claude" },
18+
{ "<leader>am", "<cmd>ClaudeCodeSelectModel<cr>", desc = "Select Claude model" },
1819

1920
-- Context sending
2021
{ "<leader>ab", "<cmd>ClaudeCodeAdd %<cr>", desc = "Add current buffer" },

lua/claudecode/config.lua

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,8 @@ M.defaults = {
1919
open_in_current_tab = true, -- Use current tab instead of creating new tab
2020
},
2121
models = {
22-
{ name = "Claude Opus 4 (Latest)", value = "claude-opus-4-20250514" },
23-
{ name = "Claude Sonnet 4 (Latest)", value = "claude-sonnet-4-20250514" },
24-
{ name = "Claude 3.7 Sonnet (Latest)", value = "claude-3-7-sonnet-latest" },
25-
{ name = "Claude 3.5 Haiku (Latest)", value = "claude-3-5-haiku-latest" },
22+
{ name = "Claude Opus 4 (Latest)", value = "opus" },
23+
{ name = "Claude Sonnet 4 (Latest)", value = "sonnet" },
2624
},
2725
}
2826

@@ -83,7 +81,7 @@ function M.validate(config)
8381
-- Validate models
8482
assert(type(config.models) == "table", "models must be a table")
8583
assert(#config.models > 0, "models must not be empty")
86-
84+
8785
for i, model in ipairs(config.models) do
8886
assert(type(model) == "table", "models[" .. i .. "] must be a table")
8987
assert(type(model.name) == "string" and model.name ~= "", "models[" .. i .. "].name must be a non-empty string")

lua/claudecode/init.lua

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -889,7 +889,8 @@ function M._create_commands()
889889
if current_mode == "v" or current_mode == "V" or current_mode == "\22" then
890890
vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("<Esc>", true, false, true), "n", false)
891891
end
892-
terminal.toggle({}, nil) -- `opts.fargs` can be used for future enhancements.
892+
local cmd_args = opts.args and opts.args ~= "" and opts.args or nil
893+
terminal.simple_toggle({}, cmd_args)
893894
end, {
894895
nargs = "*",
895896
desc = "Toggle the Claude Code terminal window (simple show/hide) with optional arguments",
@@ -942,16 +943,23 @@ function M._create_commands()
942943
desc = "Deny/reject the current diff changes",
943944
})
944945

945-
vim.api.nvim_create_user_command("ClaudeSelectModel", function()
946-
M.open_with_model()
946+
vim.api.nvim_create_user_command("ClaudeCodeSelectModel", function(opts)
947+
local cmd_args = opts.args and opts.args ~= "" and opts.args or nil
948+
M.open_with_model(cmd_args)
947949
end, {
948-
desc = "Select and open Claude terminal with chosen model",
950+
nargs = "*",
951+
desc = "Select and open Claude terminal with chosen model and optional arguments",
949952
})
950953
end
951954

952-
M.open_with_model = function()
955+
M.open_with_model = function(additional_args)
953956
local models = M.state.config.models
954957

958+
if not models or #models == 0 then
959+
logger.error("command", "No models configured for selection")
960+
return
961+
end
962+
955963
vim.ui.select(models, {
956964
prompt = "Select Claude model:",
957965
format_item = function(item)
@@ -962,13 +970,14 @@ M.open_with_model = function()
962970
return -- User cancelled
963971
end
964972

965-
local terminal_ok, terminal = pcall(require, "claudecode.terminal")
966-
if not terminal_ok then
967-
vim.notify("Terminal module not available", vim.log.levels.ERROR)
973+
if not choice.value or type(choice.value) ~= "string" then
974+
logger.error("command", "Invalid model value selected")
968975
return
969976
end
970977

971-
terminal.toggle({}, "--model " .. choice.value)
978+
local model_arg = "--model " .. choice.value
979+
local final_args = additional_args and (model_arg .. " " .. additional_args) or model_arg
980+
vim.cmd("ClaudeCode " .. final_args)
972981
end)
973982
end
974983

tests/config_test.lua

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,10 @@ describe("Config module", function()
192192
vertical_split = true,
193193
open_in_current_tab = true,
194194
},
195+
models = {
196+
{ name = "Claude Opus 4 (Latest)", value = "claude-opus-4-20250514" },
197+
{ name = "Claude Sonnet 4 (Latest)", value = "claude-sonnet-4-20250514" },
198+
},
195199
}
196200

197201
local success, _ = pcall(function()

tests/unit/init_spec.lua

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -462,4 +462,153 @@ describe("claudecode.init", function()
462462
assert.is_nil(call_args[2], "Second argument should be nil when no args provided")
463463
end)
464464
end)
465+
466+
describe("ClaudeCodeSelectModel command with arguments", function()
467+
local mock_terminal
468+
local mock_ui_select
469+
local mock_vim_cmd
470+
471+
before_each(function()
472+
mock_terminal = {
473+
toggle = spy.new(function() end),
474+
simple_toggle = spy.new(function() end),
475+
focus_toggle = spy.new(function() end),
476+
open = spy.new(function() end),
477+
close = spy.new(function() end),
478+
}
479+
480+
-- Mock vim.ui.select to automatically select the first model
481+
mock_ui_select = spy.new(function(models, opts, callback)
482+
-- Simulate user selecting the first model
483+
callback(models[1])
484+
end)
485+
486+
-- Mock vim.cmd to capture command execution
487+
mock_vim_cmd = spy.new(function(cmd) end)
488+
489+
vim.ui = vim.ui or {}
490+
vim.ui.select = mock_ui_select
491+
vim.cmd = mock_vim_cmd
492+
493+
local original_require = _G.require
494+
_G.require = function(mod)
495+
if mod == "claudecode.terminal" then
496+
return mock_terminal
497+
elseif mod == "claudecode.server.init" then
498+
return mock_server
499+
elseif mod == "claudecode.lockfile" then
500+
return mock_lockfile
501+
elseif mod == "claudecode.selection" then
502+
return mock_selection
503+
else
504+
return original_require(mod)
505+
end
506+
end
507+
end)
508+
509+
it("should register ClaudeCodeSelectModel command with correct configuration", function()
510+
local claudecode = require("claudecode")
511+
claudecode.setup({ auto_start = false })
512+
513+
local command_found = false
514+
for _, call in ipairs(vim.api.nvim_create_user_command.calls) do
515+
if call.vals[1] == "ClaudeCodeSelectModel" then
516+
command_found = true
517+
local config = call.vals[3]
518+
assert.is_equal("*", config.nargs)
519+
assert.is_true(
520+
string.find(config.desc, "model.*arguments") ~= nil,
521+
"Description should mention model and arguments"
522+
)
523+
break
524+
end
525+
end
526+
assert.is_true(command_found, "ClaudeCodeSelectModel command was not registered")
527+
end)
528+
529+
it("should call ClaudeCode command with model arg when no additional args provided", function()
530+
local claudecode = require("claudecode")
531+
claudecode.setup({ auto_start = false })
532+
533+
-- Find and call the ClaudeCodeSelectModel command handler
534+
local command_handler
535+
for _, call in ipairs(vim.api.nvim_create_user_command.calls) do
536+
if call.vals[1] == "ClaudeCodeSelectModel" then
537+
command_handler = call.vals[2]
538+
break
539+
end
540+
end
541+
542+
assert.is_function(command_handler, "Command handler should be a function")
543+
544+
command_handler({ args = "" })
545+
546+
-- Verify vim.ui.select was called
547+
assert(#mock_ui_select.calls > 0, "vim.ui.select was not called")
548+
549+
-- Verify vim.cmd was called with the correct ClaudeCode command
550+
assert(#mock_vim_cmd.calls > 0, "vim.cmd was not called")
551+
local cmd_arg = mock_vim_cmd.calls[1].vals[1]
552+
assert.is_equal("ClaudeCode --model opus", cmd_arg, "Should call ClaudeCode with model arg")
553+
end)
554+
555+
it("should call ClaudeCode command with model and additional args", function()
556+
local claudecode = require("claudecode")
557+
claudecode.setup({ auto_start = false })
558+
559+
-- Find and call the ClaudeCodeSelectModel command handler
560+
local command_handler
561+
for _, call in ipairs(vim.api.nvim_create_user_command.calls) do
562+
if call.vals[1] == "ClaudeCodeSelectModel" then
563+
command_handler = call.vals[2]
564+
break
565+
end
566+
end
567+
568+
assert.is_function(command_handler, "Command handler should be a function")
569+
570+
command_handler({ args = "--resume --verbose" })
571+
572+
-- Verify vim.ui.select was called
573+
assert(#mock_ui_select.calls > 0, "vim.ui.select was not called")
574+
575+
-- Verify vim.cmd was called with the correct ClaudeCode command including additional args
576+
assert(#mock_vim_cmd.calls > 0, "vim.cmd was not called")
577+
local cmd_arg = mock_vim_cmd.calls[1].vals[1]
578+
assert.is_equal(
579+
"ClaudeCode --model opus --resume --verbose",
580+
cmd_arg,
581+
"Should call ClaudeCode with model and additional args"
582+
)
583+
end)
584+
585+
it("should handle user cancellation gracefully", function()
586+
local claudecode = require("claudecode")
587+
claudecode.setup({ auto_start = false })
588+
589+
-- Mock vim.ui.select to simulate user cancellation
590+
vim.ui.select = spy.new(function(models, opts, callback)
591+
callback(nil) -- User cancelled
592+
end)
593+
594+
-- Find and call the ClaudeCodeSelectModel command handler
595+
local command_handler
596+
for _, call in ipairs(vim.api.nvim_create_user_command.calls) do
597+
if call.vals[1] == "ClaudeCodeSelectModel" then
598+
command_handler = call.vals[2]
599+
break
600+
end
601+
end
602+
603+
assert.is_function(command_handler, "Command handler should be a function")
604+
605+
command_handler({ args = "--resume" })
606+
607+
-- Verify vim.ui.select was called
608+
assert(#vim.ui.select.calls > 0, "vim.ui.select was not called")
609+
610+
-- Verify vim.cmd was NOT called due to user cancellation
611+
assert.is_equal(0, #mock_vim_cmd.calls, "vim.cmd should not be called when user cancels")
612+
end)
613+
end)
465614
end)

0 commit comments

Comments
 (0)