Skip to content

Commit a9f20e2

Browse files
totalolageclaudeThomasK33
authored
feat: add env configuration option and fix vim.notify scheduling (#21)
* feat: add env configuration option - Add env field to config for passing environment variables to Claude CLI - Update init.lua to pass env variables when spawning Claude terminal - Allows users to set custom environment like ANTHROPIC_API_KEY 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> * chore: remove trailing newlines and teminal cmd env var * fix: properly schedule vim.notify and nvim_echo calls * test: add missing env field to config test fixtures Change-Id: Icabc93ce10712b9bf37e6a43fdeabdecef2e1780 Signed-off-by: Thomas Kosiewski <[email protected]> --------- Signed-off-by: Thomas Kosiewski <[email protected]> Co-authored-by: Claude <[email protected]> Co-authored-by: Thomas Kosiewski <[email protected]>
1 parent a01b9dc commit a9f20e2

File tree

6 files changed

+54
-49
lines changed

6 files changed

+54
-49
lines changed

lua/claudecode/config.lua

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ M.defaults = {
66
port_range = { min = 10000, max = 65535 },
77
auto_start = true,
88
terminal_cmd = nil,
9+
env = {}, -- Custom environment variables for Claude terminal
910
log_level = "info",
1011
track_selection = true,
1112
visual_demotion_delay_ms = 50, -- Milliseconds to wait before demoting a visual selection
@@ -78,6 +79,13 @@ function M.validate(config)
7879
assert(type(config.diff_opts.vertical_split) == "boolean", "diff_opts.vertical_split must be a boolean")
7980
assert(type(config.diff_opts.open_in_current_tab) == "boolean", "diff_opts.open_in_current_tab must be a boolean")
8081

82+
-- Validate env
83+
assert(type(config.env) == "table", "env must be a table")
84+
for key, value in pairs(config.env) do
85+
assert(type(key) == "string", "env keys must be strings")
86+
assert(type(value) == "string", "env values must be strings")
87+
end
88+
8189
-- Validate models
8290
assert(type(config.models) == "table", "models must be a table")
8391
assert(#config.models > 0, "models must not be empty")
@@ -87,7 +95,6 @@ function M.validate(config)
8795
assert(type(model.name) == "string" and model.name ~= "", "models[" .. i .. "].name must be a non-empty string")
8896
assert(type(model.value) == "string" and model.value ~= "", "models[" .. i .. "].value must be a non-empty string")
8997
end
90-
9198
return true
9299
end
93100

lua/claudecode/init.lua

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ M.version = {
3636
--- @field port_range {min: integer, max: integer} Port range for WebSocket server.
3737
--- @field auto_start boolean Auto-start WebSocket server on Neovim startup.
3838
--- @field terminal_cmd string|nil Custom terminal command to use when launching Claude.
39+
--- @field env table<string,string> Custom environment variables for Claude terminal.
3940
--- @field log_level "trace"|"debug"|"info"|"warn"|"error" Log level.
4041
--- @field track_selection boolean Enable sending selection updates to Claude.
4142
--- @field visual_demotion_delay_ms number Milliseconds to wait before demoting a visual selection.
@@ -49,6 +50,7 @@ local default_config = {
4950
port_range = { min = 10000, max = 65535 },
5051
auto_start = true,
5152
terminal_cmd = nil,
53+
env = {},
5254
log_level = "info",
5355
track_selection = true,
5456
visual_demotion_delay_ms = 50, -- Reduced from 200ms for better responsiveness in tree navigation
@@ -306,14 +308,14 @@ function M.setup(opts)
306308

307309
logger.setup(M.state.config)
308310

309-
-- Setup terminal module: always try to call setup to pass terminal_cmd,
311+
-- Setup terminal module: always try to call setup to pass terminal_cmd and env,
310312
-- even if terminal_opts (for split_side etc.) are not provided.
311313
local terminal_setup_ok, terminal_module = pcall(require, "claudecode.terminal")
312314
if terminal_setup_ok then
313315
-- Guard in case tests or user replace the module with a minimal stub without `setup`.
314316
if type(terminal_module.setup) == "function" then
315317
-- terminal_opts might be nil, which the setup function should handle gracefully.
316-
terminal_module.setup(terminal_opts, M.state.config.terminal_cmd)
318+
terminal_module.setup(terminal_opts, M.state.config.terminal_cmd, M.state.config.env)
317319
end
318320
else
319321
logger.error("init", "Failed to load claudecode.terminal module for setup.")

lua/claudecode/logger.lua

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -68,22 +68,19 @@ local function log(level, component, message_parts)
6868
end
6969
end
7070

71-
if level == M.levels.ERROR then
72-
vim.schedule(function()
71+
-- Wrap all vim.notify and nvim_echo calls in vim.schedule to avoid
72+
-- "nvim_echo must not be called in a fast event context" errors
73+
vim.schedule(function()
74+
if level == M.levels.ERROR then
7375
vim.notify(prefix .. " " .. message, vim.log.levels.ERROR, { title = "ClaudeCode Error" })
74-
end)
75-
elseif level == M.levels.WARN then
76-
vim.schedule(function()
76+
elseif level == M.levels.WARN then
7777
vim.notify(prefix .. " " .. message, vim.log.levels.WARN, { title = "ClaudeCode Warning" })
78-
end)
79-
else
80-
-- For INFO, DEBUG, TRACE, use nvim_echo to avoid flooding notifications,
81-
-- to make them appear in :messages, and wrap in vim.schedule
82-
-- to avoid "nvim_echo must not be called in a fast event context".
83-
vim.schedule(function()
78+
else
79+
-- For INFO, DEBUG, TRACE, use nvim_echo to avoid flooding notifications,
80+
-- to make them appear in :messages
8481
vim.api.nvim_echo({ { prefix .. " " .. message, "Normal" } }, true, {})
85-
end)
86-
end
82+
end
83+
end)
8784
end
8885

8986
--- @param component string|nil Optional component/module name.

lua/claudecode/terminal.lua

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ local config = {
2424
show_native_term_exit_tip = true,
2525
terminal_cmd = nil,
2626
auto_close = true,
27+
env = {}, -- Custom environment variables for Claude terminal
2728
snacks_win_opts = {},
2829
}
2930

@@ -153,6 +154,11 @@ local function get_claude_command_and_env(cmd_args)
153154
env_table["CLAUDE_CODE_SSE_PORT"] = tostring(sse_port_value)
154155
end
155156

157+
-- Merge custom environment variables from config
158+
for key, value in pairs(config.env) do
159+
env_table[key] = value
160+
end
161+
156162
return cmd_string, env_table
157163
end
158164

@@ -186,7 +192,8 @@ end
186192
-- @field user_term_config.show_native_term_exit_tip boolean Show tip for exiting native terminal (default: true).
187193
-- @field user_term_config.snacks_win_opts table Opts to pass to `Snacks.terminal.open()` (default: {}).
188194
-- @param p_terminal_cmd string|nil The command to run in the terminal (from main config).
189-
function M.setup(user_term_config, p_terminal_cmd)
195+
-- @param p_env table|nil Custom environment variables to pass to the terminal (from main config).
196+
function M.setup(user_term_config, p_terminal_cmd, p_env)
190197
if user_term_config == nil then -- Allow nil, default to empty table silently
191198
user_term_config = {}
192199
elseif type(user_term_config) ~= "table" then -- Warn if it's not nil AND not a table
@@ -204,6 +211,16 @@ function M.setup(user_term_config, p_terminal_cmd)
204211
config.terminal_cmd = nil -- Fallback to default behavior
205212
end
206213

214+
if p_env == nil or type(p_env) == "table" then
215+
config.env = p_env or {}
216+
else
217+
vim.notify(
218+
"claudecode.terminal.setup: Invalid env provided: " .. tostring(p_env) .. ". Using empty table.",
219+
vim.log.levels.WARN
220+
)
221+
config.env = {}
222+
end
223+
207224
for k, v in pairs(user_term_config) do
208225
if config[k] ~= nil and k ~= "terminal_cmd" then -- terminal_cmd is handled above
209226
if k == "split_side" and (v == "left" or v == "right") then

tests/config_test.lua

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -175,34 +175,24 @@ describe("Config module", function()
175175
assert(type(config.defaults.track_selection) == "boolean")
176176
end)
177177

178-
it("should validate valid configuration", function()
179-
local valid_config = {
180-
port_range = { min = 10000, max = 65535 },
181-
auto_start = true,
178+
it("should apply and validate user configuration", function()
179+
local user_config = {
182180
terminal_cmd = "toggleterm",
183181
log_level = "debug",
184182
track_selection = false,
185-
visual_demotion_delay_ms = 50,
186-
connection_wait_delay = 200,
187-
connection_timeout = 10000,
188-
queue_timeout = 5000,
189-
diff_opts = {
190-
auto_close_on_accept = true,
191-
show_diff_stats = true,
192-
vertical_split = true,
193-
open_in_current_tab = true,
194-
},
195183
models = {
196184
{ name = "Claude Opus 4 (Latest)", value = "claude-opus-4-20250514" },
197185
{ name = "Claude Sonnet 4 (Latest)", value = "claude-sonnet-4-20250514" },
198186
},
199187
}
200188

201-
local success, _ = pcall(function()
202-
return config.validate(valid_config)
189+
local success, final_config = pcall(function()
190+
return config.apply(user_config)
203191
end)
204192

205193
assert(success == true)
194+
assert(final_config.env ~= nil) -- Should inherit default empty table
195+
assert(type(final_config.env) == "table")
206196
end)
207197

208198
it("should merge user config with defaults", function()

tests/unit/config_spec.lua

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -25,30 +25,22 @@ describe("Configuration", function()
2525
expect(config.defaults).to_have_key("models")
2626
end)
2727

28-
it("should validate valid configuration", function()
29-
local valid_config = {
30-
port_range = { min = 10000, max = 65535 },
31-
auto_start = true,
28+
it("should apply and validate user configuration", function()
29+
local user_config = {
3230
terminal_cmd = "toggleterm",
3331
log_level = "debug",
3432
track_selection = false,
35-
visual_demotion_delay_ms = 50,
36-
connection_wait_delay = 200,
37-
connection_timeout = 10000,
38-
queue_timeout = 5000,
39-
diff_opts = {
40-
auto_close_on_accept = true,
41-
show_diff_stats = true,
42-
vertical_split = true,
43-
open_in_current_tab = true,
44-
},
4533
models = {
4634
{ name = "Test Model", value = "test-model" },
4735
},
4836
}
4937

50-
local success = config.validate(valid_config)
51-
expect(success).to_be_true()
38+
local final_config = config.apply(user_config)
39+
expect(final_config).to_be_table()
40+
expect(final_config.terminal_cmd).to_be("toggleterm")
41+
expect(final_config.log_level).to_be("debug")
42+
expect(final_config.track_selection).to_be_false()
43+
expect(final_config.env).to_be_table() -- Should inherit default empty table
5244
end)
5345

5446
it("should reject invalid port range", function()

0 commit comments

Comments
 (0)