Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit e21a837

Browse files
feat(terminal/external): add cwd support and stricter placeholder parsing; set jobstart cwd; update docs/tests
Change-Id: If71a96214bb10d361fccaaeb5415080a5df3125c Signed-off-by: Thomas Kosiewski <tk@coder.com>
1 parent e737c52 commit e21a837

File tree

3 files changed

+81
-8
lines changed

3 files changed

+81
-8
lines changed

‎README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -264,8 +264,9 @@ For deep technical details, see [ARCHITECTURE.md](./ARCHITECTURE.md).
264264
-- Provider-specific options
265265
provider_opts = {
266266
-- Command for external terminal provider. Can be:
267-
-- 1. String with %s placeholder: "alacritty -e %s"
268-
-- 2. Function returning command: function(cmd, env) return "alacritty -e " .. cmd end
267+
-- 1. String with %s placeholder: "alacritty -e %s" (backward compatible)
268+
-- 2. String with two %s placeholders: "alacritty --working-directory %s -e %s" (cwd, command)
269+
-- 3. Function returning command: function(cmd, env) return "alacritty -e " .. cmd end
269270
external_terminal_cmd = nil,
270271
},
271272
},
@@ -463,6 +464,7 @@ Run Claude Code in a separate terminal application outside of Neovim:
463464
provider = "external",
464465
provider_opts = {
465466
external_terminal_cmd = "alacritty -e %s", -- %s is replaced with claude command
467+
-- Or with working directory: "alacritty --working-directory %s -e %s" (first %s = cwd, second %s = command)
466468
},
467469
},
468470
},
@@ -603,6 +605,8 @@ require("claudecode").setup({
603605

604606
The custom provider will automatically fall back to the native provider if validation fails or `is_available()` returns false.
605607

608+
Note: If your command or working directory may contain spaces or special characters, prefer returning a table of args from a function (e.g., `{ "alacritty", "--working-directory", cwd, "-e", "claude", "--help" }`) to avoid shell-quoting issues.
609+
606610
## Community Extensions
607611

608612
The following are third-party community extensions that complement claudecode.nvim. **These extensions are not affiliated with Coder and are maintained independently by community members.** We do not ensure that these extensions work correctly or provide support for them.

‎lua/claudecode/terminal/external.lua

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ function M.open(cmd_string, env_table)
4949

5050
local cmd_parts
5151
local full_command
52+
local cwd_for_jobstart = nil
5253

5354
-- Handle both string and function types
5455
if type(external_cmd) == "function" then
@@ -81,24 +82,47 @@ function M.open(cmd_string, env_table)
8182
return
8283
end
8384

84-
-- Replace %s in the template with the Claude command
85-
if not external_cmd:find("%%s") then
86-
vim.notify("external_terminal_cmd must contain '%s' placeholder for the Claude command.", vim.log.levels.ERROR)
85+
-- Count the number of %s placeholders and format accordingly
86+
-- 1 placeholder: backward compatible, just command ("alacritty -e %s")
87+
-- 2 placeholders: cwd and command ("alacritty --working-directory %s -e %s")
88+
local _, placeholder_count = external_cmd:gsub("%%s", "")
89+
90+
if placeholder_count == 0 then
91+
vim.notify("external_terminal_cmd must contain '%s' placeholder(s) for the command.", vim.log.levels.ERROR)
92+
return
93+
elseif placeholder_count == 1 then
94+
-- Backward compatible: just the command
95+
full_command = string.format(external_cmd, cmd_string)
96+
elseif placeholder_count == 2 then
97+
-- New feature: cwd and command
98+
local cwd = vim.fn.getcwd()
99+
cwd_for_jobstart = cwd
100+
full_command = string.format(external_cmd, cwd, cmd_string)
101+
else
102+
vim.notify(
103+
string.format(
104+
"external_terminal_cmd must use 1 '%%s' (command) or 2 '%%s' placeholders (cwd, command); got %d",
105+
placeholder_count
106+
),
107+
vim.log.levels.ERROR
108+
)
87109
return
88110
end
89111

90-
-- Build command by replacing %s with Claude command and splitting
91-
full_command = string.format(external_cmd, cmd_string)
92112
cmd_parts = vim.split(full_command, " ")
93113
else
94114
vim.notify("external_terminal_cmd must be a string or function, got: " .. type(external_cmd), vim.log.levels.ERROR)
95115
return
96116
end
97117

98118
-- Start the external terminal as a detached process
119+
-- Set cwd for jobstart when available to improve robustness even if the terminal ignores it
120+
cwd_for_jobstart = cwd_for_jobstart or (vim.fn.getcwd and vim.fn.getcwd() or nil)
121+
99122
jobid = vim.fn.jobstart(cmd_parts, {
100123
detach = true,
101124
env = env_table,
125+
cwd = cwd_for_jobstart,
102126
on_exit = function(job_id, exit_code, _)
103127
vim.schedule(function()
104128
if job_id == jobid then

‎tests/unit/terminal/external_spec.lua

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ describe("claudecode.terminal.external", function()
1818
return 123
1919
end), -- Return valid job id
2020
jobstop = spy.new(function() end),
21+
getcwd = spy.new(function()
22+
return "/cwd"
23+
end),
2124
},
2225
notify = spy.new(function() end),
2326
log = {
@@ -91,6 +94,7 @@ describe("claudecode.terminal.external", function()
9194
local call_args = mock_vim.fn.jobstart.calls[1].vals
9295
assert.are.same({ "alacritty", "-e", "claude", "--help" }, call_args[1])
9396
assert.are.same({ ENABLE_IDE_INTEGRATION = "true" }, call_args[2].env)
97+
assert.are.equal("/cwd", call_args[2].cwd)
9498
end)
9599

96100
it("should error if string command missing %s placeholder", function()
@@ -105,7 +109,7 @@ describe("claudecode.terminal.external", function()
105109

106110
assert
107111
.spy(mock_vim.notify)
108-
.was_called_with("external_terminal_cmd must contain '%s' placeholder for the Claude command.", mock_vim.log.levels.ERROR)
112+
.was_called_with("external_terminal_cmd must contain '%s' placeholder(s) for the command.", mock_vim.log.levels.ERROR)
109113
assert.spy(mock_vim.fn.jobstart).was_not_called()
110114
end)
111115

@@ -122,6 +126,45 @@ describe("claudecode.terminal.external", function()
122126
assert.spy(mock_vim.notify).was_called()
123127
assert.spy(mock_vim.fn.jobstart).was_not_called()
124128
end)
129+
130+
it("should handle string with two placeholders (cwd and command)", function()
131+
-- Mock vim.fn.getcwd to return a known directory
132+
mock_vim.fn.getcwd = spy.new(function()
133+
return "/test/project"
134+
end)
135+
136+
local config = {
137+
provider_opts = {
138+
external_terminal_cmd = "alacritty --working-directory %s -e %s",
139+
},
140+
}
141+
external_provider.setup(config)
142+
143+
external_provider.open("claude --help", { ENABLE_IDE_INTEGRATION = "true" })
144+
145+
assert.spy(mock_vim.fn.jobstart).was_called(1)
146+
local call_args = mock_vim.fn.jobstart.calls[1].vals
147+
assert.are.same({ "alacritty", "--working-directory", "/test/project", "-e", "claude", "--help" }, call_args[1])
148+
assert.are.same({ ENABLE_IDE_INTEGRATION = "true" }, call_args[2].env)
149+
assert.are.equal("/test/project", call_args[2].cwd)
150+
end)
151+
152+
it("should error if string has more than two placeholders", function()
153+
local config = {
154+
provider_opts = {
155+
external_terminal_cmd = "alacritty --working-directory %s -e %s --title %s",
156+
},
157+
}
158+
external_provider.setup(config)
159+
160+
external_provider.open("claude --help", {})
161+
162+
assert.spy(mock_vim.notify).was_called_with(
163+
"external_terminal_cmd must use 1 '%s' (command) or 2 '%s' placeholders (cwd, command); got 3",
164+
mock_vim.log.levels.ERROR
165+
)
166+
assert.spy(mock_vim.fn.jobstart).was_not_called()
167+
end)
125168
end)
126169

127170
describe("open with function command", function()
@@ -141,6 +184,7 @@ describe("claudecode.terminal.external", function()
141184
local call_args = mock_vim.fn.jobstart.calls[1].vals
142185
assert.are.same({ "kitty", "claude", "--help" }, call_args[1])
143186
assert.are.same({ ENABLE_IDE_INTEGRATION = "true" }, call_args[2].env)
187+
assert.are.equal("/cwd", call_args[2].cwd)
144188
end)
145189

146190
it("should handle function returning table", function()
@@ -158,6 +202,7 @@ describe("claudecode.terminal.external", function()
158202
assert.spy(mock_vim.fn.jobstart).was_called(1)
159203
local call_args = mock_vim.fn.jobstart.calls[1].vals
160204
assert.are.same({ "osascript", "-e", 'tell app "Terminal" to do script "claude"' }, call_args[1])
205+
assert.are.equal("/cwd", call_args[2].cwd)
161206
end)
162207

163208
it("should pass cmd and env to function", function()

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /