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 e2c03ba

Browse files
feat: add working directory control for Claude terminal
Change-Id: I0cc3cf3815bc5634a6c01f4d708e0ccda8e53404 Signed-off-by: Thomas Kosiewski <tk@coder.com>
1 parent f756826 commit e2c03ba

File tree

8 files changed

+354
-16
lines changed

8 files changed

+354
-16
lines changed

‎README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,39 @@ For deep technical details, see [ARCHITECTURE.md](./ARCHITECTURE.md).
276276
}
277277
```
278278

279+
### Working Directory Control
280+
281+
You can fix the Claude terminal's working directory regardless of `autochdir` and buffer-local cwd changes. Options (precedence order):
282+
283+
- `cwd_provider(ctx)`: function that returns a directory string. Receives `{ file, file_dir, cwd }`.
284+
- `cwd`: static path to use as working directory.
285+
- `git_repo_cwd = true`: resolves git root from the current file directory (or cwd if no file).
286+
287+
Examples:
288+
289+
```lua
290+
require("claudecode").setup({
291+
-- Top-level aliases are supported and forwarded to terminal config
292+
git_repo_cwd = true,
293+
})
294+
295+
require("claudecode").setup({
296+
terminal = {
297+
cwd = vim.fn.expand("~/projects/my-app"),
298+
},
299+
})
300+
301+
require("claudecode").setup({
302+
terminal = {
303+
cwd_provider = function(ctx)
304+
-- Prefer repo root; fallback to file's directory
305+
local cwd = require("claudecode.cwd").git_root(ctx.file_dir or ctx.cwd) or ctx.file_dir or ctx.cwd
306+
return cwd
307+
end,
308+
},
309+
})
310+
```
311+
279312
## Floating Window Configuration
280313

281314
The `snacks_win_opts` configuration allows you to create floating Claude Code terminals with custom positioning, sizing, and key bindings. Here are several practical examples:

‎lua/claudecode/cwd.lua

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
--- Working directory resolution helpers for ClaudeCode.nvim
2+
---@module 'claudecode.cwd'
3+
4+
local M = {}
5+
6+
---Normalize and validate a directory path
7+
---@param dir string|nil
8+
---@return string|nil
9+
local function normalize_dir(dir)
10+
if type(dir) ~= "string" or dir == "" then
11+
return nil
12+
end
13+
-- Expand ~ and similar
14+
local expanded = vim.fn.expand(dir)
15+
local isdir = 1
16+
if vim.fn.isdirectory then
17+
isdir = vim.fn.isdirectory(expanded)
18+
end
19+
if isdir == 1 then
20+
return expanded
21+
end
22+
return nil
23+
end
24+
25+
---Find the git repository root starting from a directory
26+
---@param start_dir string|nil
27+
---@return string|nil
28+
function M.git_root(start_dir)
29+
start_dir = normalize_dir(start_dir)
30+
if not start_dir then
31+
return nil
32+
end
33+
34+
-- Prefer running without shell by passing a list
35+
local result
36+
if vim.fn.systemlist then
37+
local ok, _ = pcall(function()
38+
local _ = vim.fn.systemlist({ "git", "-C", start_dir, "rev-parse", "--show-toplevel" })
39+
end)
40+
if ok then
41+
result = vim.fn.systemlist({ "git", "-C", start_dir, "rev-parse", "--show-toplevel" })
42+
else
43+
-- Fallback to string command if needed
44+
local cmd = "git -C " .. vim.fn.shellescape(start_dir) .. " rev-parse --show-toplevel"
45+
result = vim.fn.systemlist(cmd)
46+
end
47+
end
48+
49+
if vim.v.shell_error == 0 and result and #result > 0 then
50+
local root = normalize_dir(result[1])
51+
if root then
52+
return root
53+
end
54+
end
55+
56+
-- Fallback: search for .git directory upward
57+
if vim.fn.finddir then
58+
local git_dir = vim.fn.finddir(".git", start_dir .. ";")
59+
if type(git_dir) == "string" and git_dir ~= "" then
60+
local parent = vim.fn.fnamemodify(git_dir, ":h")
61+
return normalize_dir(parent)
62+
end
63+
end
64+
65+
return nil
66+
end
67+
68+
---Resolve the effective working directory based on terminal config and context
69+
---@param term_cfg ClaudeCodeTerminalConfig
70+
---@param ctx ClaudeCodeCwdContext
71+
---@return string|nil
72+
function M.resolve(term_cfg, ctx)
73+
if type(term_cfg) ~= "table" then
74+
return nil
75+
end
76+
77+
-- 1) Custom provider takes precedence
78+
local provider = term_cfg.cwd_provider
79+
local provider_type = type(provider)
80+
if provider_type == "function" then
81+
local ok, res = pcall(provider, ctx)
82+
if ok then
83+
local p = normalize_dir(res)
84+
if p then
85+
return p
86+
end
87+
end
88+
end
89+
90+
-- 2) Static cwd
91+
local static_cwd = normalize_dir(term_cfg.cwd)
92+
if static_cwd then
93+
return static_cwd
94+
end
95+
96+
-- 3) Git repository root
97+
if term_cfg.git_repo_cwd then
98+
local start_dir = ctx and (ctx.file_dir or ctx.cwd) or vim.fn.getcwd()
99+
local root = M.git_root(start_dir)
100+
if root then
101+
return root
102+
end
103+
end
104+
105+
-- 4) No override
106+
return nil
107+
end
108+
109+
return M

‎lua/claudecode/init.lua

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,27 @@ function M.setup(opts)
300300

301301
-- Setup terminal module: always try to call setup to pass terminal_cmd and env,
302302
-- even if terminal_opts (for split_side etc.) are not provided.
303+
-- Map top-level cwd-related aliases into terminal config for convenience
304+
do
305+
local t = opts.terminal or {}
306+
local had_alias = false
307+
if opts.git_repo_cwd ~= nil then
308+
t.git_repo_cwd = opts.git_repo_cwd
309+
had_alias = true
310+
end
311+
if opts.cwd ~= nil then
312+
t.cwd = opts.cwd
313+
had_alias = true
314+
end
315+
if opts.cwd_provider ~= nil then
316+
t.cwd_provider = opts.cwd_provider
317+
had_alias = true
318+
end
319+
if had_alias then
320+
opts.terminal = t
321+
end
322+
end
323+
303324
local terminal_setup_ok, terminal_module = pcall(require, "claudecode.terminal")
304325
if terminal_setup_ok then
305326
-- Guard in case tests or user replace the module with a minimal stub without `setup`.

‎lua/claudecode/terminal.lua

Lines changed: 129 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ local defaults = {
1616
auto_close = true,
1717
env = {},
1818
snacks_win_opts = {},
19+
-- Working directory control
20+
cwd = nil, -- static cwd override
21+
git_repo_cwd = false, -- resolve to git root when spawning
22+
cwd_provider = nil, -- function(ctx) -> cwd string
1923
}
2024

2125
M.defaults = defaults
@@ -172,18 +176,67 @@ local function build_config(opts_override)
172176
snacks_win_opts = function(val)
173177
return type(val) == "table"
174178
end,
179+
cwd = function(val)
180+
return val == nil or type(val) == "string"
181+
end,
182+
git_repo_cwd = function(val)
183+
return type(val) == "boolean"
184+
end,
185+
cwd_provider = function(val)
186+
local t = type(val)
187+
if t == "function" then
188+
return true
189+
end
190+
if t == "table" then
191+
local mt = getmetatable(val)
192+
return mt and mt.__call ~= nil
193+
end
194+
return false
195+
end,
175196
}
176197
for key, val in pairs(opts_override) do
177198
if effective_config[key] ~= nil and validators[key] and validators[key](val) then
178199
effective_config[key] = val
179200
end
180201
end
181202
end
203+
-- Resolve cwd at config-build time so providers receive it directly
204+
local cwd_ctx = {
205+
file = (function()
206+
local path = vim.fn.expand("%:p")
207+
if type(path) == "string" and path ~= "" then
208+
return path
209+
end
210+
return nil
211+
end)(),
212+
cwd = vim.fn.getcwd(),
213+
}
214+
cwd_ctx.file_dir = cwd_ctx.file and vim.fn.fnamemodify(cwd_ctx.file, ":h") or nil
215+
216+
local resolved_cwd = nil
217+
-- Prefer provider function, then static cwd, then git root via resolver
218+
if effective_config.cwd_provider then
219+
local ok_p, res = pcall(effective_config.cwd_provider, cwd_ctx)
220+
if ok_p and type(res) == "string" and res ~= "" then
221+
resolved_cwd = vim.fn.expand(res)
222+
end
223+
end
224+
if not resolved_cwd and type(effective_config.cwd) == "string" and effective_config.cwd ~= "" then
225+
resolved_cwd = vim.fn.expand(effective_config.cwd)
226+
end
227+
if not resolved_cwd and effective_config.git_repo_cwd then
228+
local ok_r, cwd_mod = pcall(require, "claudecode.cwd")
229+
if ok_r and cwd_mod and type(cwd_mod.git_root) == "function" then
230+
resolved_cwd = cwd_mod.git_root(cwd_ctx.file_dir or cwd_ctx.cwd)
231+
end
232+
end
233+
182234
return {
183235
split_side = effective_config.split_side,
184236
split_width_percentage = effective_config.split_width_percentage,
185237
auto_close = effective_config.auto_close,
186238
snacks_win_opts = effective_config.snacks_win_opts,
239+
cwd = resolved_cwd,
187240
}
188241
end
189242

@@ -300,24 +353,84 @@ function M.setup(user_term_config, p_terminal_cmd, p_env)
300353
end
301354

302355
for k, v in pairs(user_term_config) do
303-
if defaults[k] ~= nil and k ~= "terminal_cmd" then -- terminal_cmd is handled above
304-
if k == "split_side" and (v == "left" or v == "right") then
305-
defaults[k] = v
306-
elseif k == "split_width_percentage" and type(v) == "number" and v > 0 and v < 1 then
307-
defaults[k] = v
308-
elseif k == "provider" and (v == "snacks" or v == "native" or v == "auto" or type(v) == "table") then
309-
defaults[k] = v
310-
elseif k == "show_native_term_exit_tip" and type(v) == "boolean" then
311-
defaults[k] = v
312-
elseif k == "auto_close" and type(v) == "boolean" then
313-
defaults[k] = v
314-
elseif k == "snacks_win_opts" and type(v) == "table" then
315-
defaults[k] = v
356+
if k == "split_side" then
357+
if v == "left" or v == "right" then
358+
defaults.split_side = v
359+
else
360+
vim.notify("claudecode.terminal.setup: Invalid value for split_side: " .. tostring(v), vim.log.levels.WARN)
361+
end
362+
elseif k == "split_width_percentage" then
363+
if type(v) == "number" and v > 0 and v < 1 then
364+
defaults.split_width_percentage = v
365+
else
366+
vim.notify(
367+
"claudecode.terminal.setup: Invalid value for split_width_percentage: " .. tostring(v),
368+
vim.log.levels.WARN
369+
)
370+
end
371+
elseif k == "provider" then
372+
if type(v) == "table" or v == "snacks" or v == "native" or v == "auto" then
373+
defaults.provider = v
316374
else
317-
vim.notify("claudecode.terminal.setup: Invalid value for " .. k .. ": " .. tostring(v), vim.log.levels.WARN)
375+
vim.notify(
376+
"claudecode.terminal.setup: Invalid value for provider: " .. tostring(v) .. ". Defaulting to 'native'.",
377+
vim.log.levels.WARN
378+
)
379+
end
380+
elseif k == "show_native_term_exit_tip" then
381+
if type(v) == "boolean" then
382+
defaults.show_native_term_exit_tip = v
383+
else
384+
vim.notify(
385+
"claudecode.terminal.setup: Invalid value for show_native_term_exit_tip: " .. tostring(v),
386+
vim.log.levels.WARN
387+
)
388+
end
389+
elseif k == "auto_close" then
390+
if type(v) == "boolean" then
391+
defaults.auto_close = v
392+
else
393+
vim.notify("claudecode.terminal.setup: Invalid value for auto_close: " .. tostring(v), vim.log.levels.WARN)
394+
end
395+
elseif k == "snacks_win_opts" then
396+
if type(v) == "table" then
397+
defaults.snacks_win_opts = v
398+
else
399+
vim.notify("claudecode.terminal.setup: Invalid value for snacks_win_opts", vim.log.levels.WARN)
400+
end
401+
elseif k == "cwd" then
402+
if v == nil or type(v) == "string" then
403+
defaults.cwd = v
404+
else
405+
vim.notify("claudecode.terminal.setup: Invalid value for cwd: " .. tostring(v), vim.log.levels.WARN)
406+
end
407+
elseif k == "git_repo_cwd" then
408+
if type(v) == "boolean" then
409+
defaults.git_repo_cwd = v
410+
else
411+
vim.notify("claudecode.terminal.setup: Invalid value for git_repo_cwd: " .. tostring(v), vim.log.levels.WARN)
412+
end
413+
elseif k == "cwd_provider" then
414+
local t = type(v)
415+
if t == "function" then
416+
defaults.cwd_provider = v
417+
elseif t == "table" then
418+
local mt = getmetatable(v)
419+
if mt and mt.__call then
420+
defaults.cwd_provider = v
421+
else
422+
vim.notify(
423+
"claudecode.terminal.setup: cwd_provider table is not callable (missing __call)",
424+
vim.log.levels.WARN
425+
)
426+
end
427+
else
428+
vim.notify("claudecode.terminal.setup: Invalid cwd_provider type: " .. tostring(t), vim.log.levels.WARN)
429+
end
430+
else
431+
if k ~= "terminal_cmd" then
432+
vim.notify("claudecode.terminal.setup: Unknown configuration key: " .. k, vim.log.levels.WARN)
318433
end
319-
elseif k ~= "terminal_cmd" then -- Avoid warning for terminal_cmd if passed in user_term_config
320-
vim.notify("claudecode.terminal.setup: Unknown configuration key: " .. k, vim.log.levels.WARN)
321434
end
322435
end
323436

‎lua/claudecode/terminal/native.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ local function open_terminal(cmd_string, env_table, effective_config, focus)
8888

8989
jobid = vim.fn.termopen(term_cmd_arg, {
9090
env = env_table,
91+
cwd = effective_config.cwd,
9192
on_exit = function(job_id, _, _)
9293
vim.schedule(function()
9394
if job_id == jobid then

‎lua/claudecode/terminal/snacks.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ local function build_opts(config, env_table, focus)
5050
focus = utils.normalize_focus(focus)
5151
return {
5252
env = env_table,
53+
cwd = config.cwd,
5354
start_insert = focus,
5455
auto_insert = focus,
5556
auto_close = false,

‎lua/claudecode/types.lua

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,14 @@
4040
-- In-tree terminal provider names
4141
---@alias ClaudeCodeTerminalProviderName "auto"|"snacks"|"native"
4242

43+
-- Working directory resolution context and provider
44+
---@class ClaudeCodeCwdContext
45+
---@field file string|nil -- absolute path of current buffer file (if any)
46+
---@field file_dir string|nil -- directory of current buffer file (if any)
47+
---@field cwd string -- current Neovim working directory
48+
49+
---@alias ClaudeCodeCwdProvider fun(ctx: ClaudeCodeCwdContext): string|nil
50+
4351
-- @ mention queued for Claude Code
4452
---@class ClaudeCodeMention
4553
---@field file_path string The absolute file path to mention
@@ -70,6 +78,9 @@
7078
---@field auto_close boolean
7179
---@field env table<string, string>
7280
---@field snacks_win_opts snacks.win.Config
81+
---@field cwd string|nil -- static working directory for Claude terminal
82+
---@field git_repo_cwd boolean|nil -- use git root of current file/cwd as working directory
83+
---@field cwd_provider? ClaudeCodeCwdProvider -- custom function to compute working directory
7384

7485
-- Port range configuration
7586
---@class ClaudeCodePortRange

0 commit comments

Comments
(0)

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