Skip to content

Commit bce7144

Browse files
committed
fix(context): normalize mention path handling
Normalize mention paths to relative values for mention commands, and enforce trailing separators for directory mentions using path-style-aware separators.
1 parent af87a8b commit bce7144

6 files changed

Lines changed: 143 additions & 22 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,8 @@ require("codex").setup({
101101
- `:CodexClose` closes the active Codex terminal session
102102
- `:CodexClearInput` clears the active Codex terminal input line
103103
- `:CodexSend` sends selected lines with path and range context.
104-
- `:CodexMentionFile [path]` sends `/mention` for a file (or current buffer path).
105-
- `:CodexMentionDirectory [path]` sends `/mention` for a directory (or current buffer's directory).
104+
- `:CodexMentionFile [path]` sends `/mention` for a file path, normalized to cwd-relative.
105+
- `:CodexMentionDirectory [path]` sends `/mention` for a directory path, normalized to cwd-relative and forced to end with a path separator.
106106
- `:CodexResume[!]` resumes in-process or launches `codex resume` (`!` uses
107107
`--last` when launching).
108108
- `:CodexModel` sends `/model`

docs/command-interactions.md

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,24 +9,24 @@ provider collaborators.
99

1010
## Command Mapping
1111

12-
| User Command | Entry Function | Primary Path |
13-
| ------------------------------- | -------------------------------- | ----------------------------------------------------- |
14-
| `:Codex` | `codex.toggle()` | Toggle active terminal or open a focused session |
15-
| `:Codex!` | `codex.open(true)` | Force-open and focus terminal |
16-
| `:CodexFocus` | `codex.focus()` | Focus active session or open one |
17-
| `:CodexClose` | `codex.close()` | Close active session and reset queue |
18-
| `:CodexClearInput` | `codex.clear_input()` | Send `<C-c>` to active session |
19-
| `:CodexSend` | `codex.send_selection(opts)` | Collect selection, format, send via queue |
20-
| `:CodexMentionFile [path]` | `codex.mention_file(path)` | Build `/mention` payload for file and submit |
21-
| `:CodexMentionDirectory [path]` | `codex.mention_directory(path)` | Build `/mention` payload for directory and submit |
22-
| `:CodexResume` | `codex.resume({ last = false })` | In-process `/resume` or launch `codex resume` |
23-
| `:CodexResume!` | `codex.resume({ last = true })` | Launch `codex resume --last` when opening new process |
24-
| `:CodexModel` | `codex.set_model()` | Slash command wrapper (`/model`) |
25-
| `:CodexStatus` | `codex.show_status()` | Slash command wrapper (`/status`) |
26-
| `:CodexPermissions` | `codex.show_permissions()` | Slash command wrapper (`/permissions`) |
27-
| `:CodexCompact` | `codex.compact()` | Slash command wrapper (`/compact`) |
28-
| `:CodexReview [instructions]` | `codex.review(instructions)` | Slash command wrapper (`/review ...`) |
29-
| `:CodexDiff` | `codex.show_diff()` | Slash command wrapper (`/diff`) |
12+
| User Command | Entry Function | Primary Path |
13+
| ------------------------------- | -------------------------------- | ------------------------------------------------------------------------------------ |
14+
| `:Codex` | `codex.toggle()` | Toggle active terminal or open a focused session |
15+
| `:Codex!` | `codex.open(true)` | Force-open and focus terminal |
16+
| `:CodexFocus` | `codex.focus()` | Focus active session or open one |
17+
| `:CodexClose` | `codex.close()` | Close active session and reset queue |
18+
| `:CodexClearInput` | `codex.clear_input()` | Send `<C-c>` to active session |
19+
| `:CodexSend` | `codex.send_selection(opts)` | Collect selection, format, send via queue |
20+
| `:CodexMentionFile [path]` | `codex.mention_file(path)` | Build `/mention` payload for relative file and submit |
21+
| `:CodexMentionDirectory [path]` | `codex.mention_directory(path)` | Build `/mention` payload for relative directory (with trailing separator) and submit |
22+
| `:CodexResume` | `codex.resume({ last = false })` | In-process `/resume` or launch `codex resume` |
23+
| `:CodexResume!` | `codex.resume({ last = true })` | Launch `codex resume --last` when opening new process |
24+
| `:CodexModel` | `codex.set_model()` | Slash command wrapper (`/model`) |
25+
| `:CodexStatus` | `codex.show_status()` | Slash command wrapper (`/status`) |
26+
| `:CodexPermissions` | `codex.show_permissions()` | Slash command wrapper (`/permissions`) |
27+
| `:CodexCompact` | `codex.compact()` | Slash command wrapper (`/compact`) |
28+
| `:CodexReview [instructions]` | `codex.review(instructions)` | Slash command wrapper (`/review ...`) |
29+
| `:CodexDiff` | `codex.show_diff()` | Slash command wrapper (`/diff`) |
3030

3131
## Setup Registration Flow
3232

@@ -164,6 +164,7 @@ init.lua mention_file(path) / mention_directory(path) -> mention module
164164
|- resolve path (arg or current buffer path via %:p / %:p:h)
165165
|- [missing path] -> log + return false, "current buffer has no file/directory path"
166166
|- path.to_relative(...)
167+
|- [directory only] path.ensure_dir_trailing_separator(...)
167168
\- mention.dispatch(relative_path)
168169
|- formatter.format_mention(relative_path)
169170
|- [active + alive] provider.focus(handle) before prompt capture

lua/codex/context/mention.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,7 @@ function M.create(opts)
216216
end
217217

218218
resolved_path = deps.path.to_relative(deps.vim, resolved_path)
219+
resolved_path = deps.path.ensure_dir_trailing_separator(deps.vim, resolved_path)
219220
return dispatch_mention(resolved_path)
220221
end
221222

lua/codex/context/path.lua

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,58 @@
11
local M = {}
22

3+
---Best-effort host OS check for Windows to pick default path separators.
4+
---@param vim_api table|nil
5+
---@return boolean
6+
local function is_windows(vim_api)
7+
vim_api = vim_api or vim
8+
9+
local uv = vim_api.uv or vim_api.loop
10+
if uv and type(uv.os_uname) == "function" then
11+
local ok, uname = pcall(uv.os_uname)
12+
if ok and type(uname) == "table" and type(uname.sysname) == "string" then
13+
return uname.sysname:match("Windows") ~= nil
14+
end
15+
end
16+
17+
local fn = vim_api.fn
18+
if type(fn) == "table" and type(fn.has) == "function" then
19+
local ok_win32, has_win32 = pcall(fn.has, "win32")
20+
if ok_win32 and has_win32 == 1 then
21+
return true
22+
end
23+
local ok_win64, has_win64 = pcall(fn.has, "win64")
24+
if ok_win64 and has_win64 == 1 then
25+
return true
26+
end
27+
end
28+
29+
return false
30+
end
31+
32+
---Choose separator by path style first, then host OS as fallback.
33+
---@param vim_api table|nil
34+
---@param path string
35+
---@return string
36+
local function choose_separator(vim_api, path)
37+
if path:find("\\", 1, true) ~= nil then
38+
return "\\"
39+
end
40+
41+
if path:match("^%a:[/\\]?") or path:match("^\\\\") then
42+
return "\\"
43+
end
44+
45+
if path:find("/", 1, true) ~= nil then
46+
return "/"
47+
end
48+
49+
if is_windows(vim_api) then
50+
return "\\"
51+
end
52+
53+
return "/"
54+
end
55+
356
--- Convert an absolute file path to a cwd-relative path via fnamemodify.
457
---@param vim_api table|nil
558
---@param filepath string
@@ -18,4 +71,26 @@ function M.to_relative(vim_api, filepath)
1871
return filepath
1972
end
2073

74+
---Ensure directory paths end with one separator matching path style/OS.
75+
---@param vim_api table|nil
76+
---@param path string
77+
---@return string
78+
function M.ensure_dir_trailing_separator(vim_api, path)
79+
if path == "" then
80+
return path
81+
end
82+
83+
local separator = choose_separator(vim_api, path)
84+
if path:sub(-1) == separator then
85+
return path
86+
end
87+
88+
local normalized = path:gsub("[/\\]+$", "")
89+
if normalized == "" then
90+
return separator
91+
end
92+
93+
return normalized .. separator
94+
end
95+
2196
return M

tests/unit/init_spec.lua

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1535,14 +1535,32 @@ describe("codex.init public api", function()
15351535
assert.equals("<termcoded:<CR>>", env.fake_vim._feedkeys_calls[1].keys)
15361536
end)
15371537

1538+
it(
1539+
"mention_directory appends trailing separator when explicit directory path omits it",
1540+
function()
1541+
local env = setup_with_deps()
1542+
1543+
local ok = env.codex.mention_directory("/tmp")
1544+
local mention_payload_expected = "<termcoded:<C-e>><termcoded:<C-u>>/mention ../../tmp/"
1545+
1546+
assert.is_true(ok)
1547+
assert.equals("../../tmp/", env.formatter.mention_paths[1])
1548+
assert.equals(1, #env.provider.send_calls)
1549+
assert.equals(mention_payload_expected, env.provider.send_calls[1].text)
1550+
end
1551+
)
1552+
15381553
it("mention_directory resolves current buffer directory when argument is nil", function()
15391554
local env = setup_with_deps()
1555+
env.fake_vim.uv.os_uname = function()
1556+
return { sysname = "Linux" }
1557+
end
15401558

15411559
local ok = env.codex.mention_directory(nil)
1542-
local mention_payload_expected = "<termcoded:<C-e>><termcoded:<C-u>>/mention .."
1560+
local mention_payload_expected = "<termcoded:<C-e>><termcoded:<C-u>>/mention ../"
15431561

15441562
assert.is_true(ok)
1545-
assert.equals("..", env.formatter.mention_paths[1])
1563+
assert.equals("../", env.formatter.mention_paths[1])
15461564
assert.equals(1, #env.provider.send_calls)
15471565
assert.equals(mention_payload_expected, env.provider.send_calls[1].text)
15481566
end)

tests/unit/path_spec.lua

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,30 @@ describe("codex.context.path", function()
3131
local fake_vim = { fn = {} }
3232
assert.equals("", path.to_relative(fake_vim, ""))
3333
end)
34+
35+
describe("ensure_dir_trailing_separator", function()
36+
it("adds forward slash for unix-style relative paths", function()
37+
assert.equals("../", path.ensure_dir_trailing_separator(nil, ".."))
38+
end)
39+
40+
it("keeps existing unix trailing slash", function()
41+
assert.equals("../../tmp/", path.ensure_dir_trailing_separator(nil, "../../tmp/"))
42+
end)
43+
44+
it("adds backslash for windows-style paths", function()
45+
assert.equals("C:\\work\\repo\\", path.ensure_dir_trailing_separator(nil, "C:\\work\\repo"))
46+
end)
47+
48+
it("falls back to host OS separator when style is ambiguous", function()
49+
local fake_vim = {
50+
uv = {
51+
os_uname = function()
52+
return { sysname = "Windows_NT" }
53+
end,
54+
},
55+
}
56+
57+
assert.equals(".\\", path.ensure_dir_trailing_separator(fake_vim, "."))
58+
end)
59+
end)
3460
end)

0 commit comments

Comments
 (0)