Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
970c2f8
chore(ci): removed old branch ref
johnseth97 Jun 19, 2025
7b64b2f
feat(config): added ability to define model and fixed borders
johnseth97 Jun 19, 2025
3fd52f1
chore(tests): moved files to avoid running entire plenary suite
johnseth97 Jun 19, 2025
0f69238
chore(readme): Updated README.MD
johnseth97 Jun 19, 2025
f87782f
fix(ui)!: fixed many issues with opening/closing the buffer
johnseth97 Jun 19, 2025
72f377f
feat(tests): added tests for model option
johnseth97 Jun 19, 2025
50c049b
chore(README): Updated README.MD with links to codex config docs
johnseth97 Jun 19, 2025
c44ddf3
fix(syntax): proper list bracing
johnseth97 Jun 19, 2025
7be5d67
chore(cleanup): removed debug statements
johnseth97 Jun 19, 2025
ef31635
fix(ci): plenary headache
johnseth97 Jun 19, 2025
e0d7dc9
fix(cov): safe exit
johnseth97 Jun 19, 2025
1c46573
fif(workflow): install yarn and pnmp globally
johnseth97 Jun 19, 2025
7856da4
syntax
johnseth97 Jun 19, 2025
0475046
whoops
johnseth97 Jun 19, 2025
9c4422b
feat(makefile): refactor
johnseth97 Jun 19, 2025
b515f0e
syntax
johnseth97 Jun 19, 2025
d368594
missing run step
johnseth97 Jun 19, 2025
e605ad1
fix(installer_spec): updated tests
johnseth97 Jun 19, 2025
2af135e
split installer spec
johnseth97 Jun 19, 2025
22de19e
missed fake job ID
johnseth97 Jun 19, 2025
4e79683
wrong termopen
johnseth97 Jun 19, 2025
998683f
skipping path checks
johnseth97 Jun 19, 2025
53e73d8
allowing success for all PMs
johnseth97 Jun 19, 2025
eb59605
flipped success logic
johnseth97 Jun 19, 2025
4b301dd
found that found notice was out of place
johnseth97 Jun 19, 2025
3bcb1d4
finally found the missing piece
johnseth97 Jun 19, 2025
7194efd
Wrapping in pcall
johnseth97 Jun 19, 2025
133d290
shhhh
johnseth97 Jun 19, 2025
d8e2a67
cquit
johnseth97 Jun 19, 2025
f5dfc30
graceful
johnseth97 Jun 19, 2025
38d3fa9
bun and deno....
johnseth97 Jun 19, 2025
01b4b9e
minimal run cov
johnseth97 Jun 19, 2025
f886345
WD
johnseth97 Jun 19, 2025
423e997
maybe
johnseth97 Jun 19, 2025
57d3c00
feat(ci): makefile
johnseth97 Jun 20, 2025
83792b4
fix(syntax)
johnseth97 Jun 20, 2025
6985d8f
makefile updates
johnseth97 Jun 20, 2025
cd48637
header
johnseth97 Jun 20, 2025
69ec315
plenary tests shouldn't run
johnseth97 Jun 20, 2025
7206387
syntax
johnseth97 Jun 20, 2025
623b4fd
removed test spec to test specs
johnseth97 Jun 20, 2025
646d485
loud
johnseth97 Jun 20, 2025
a48e924
skipping in Ci
johnseth97 Jun 20, 2025
bb65e3f
better output
johnseth97 Jun 20, 2025
7984024
push
johnseth97 Jun 20, 2025
4051e6e
explict cov output
johnseth97 Jun 20, 2025
0eced20
whoops
johnseth97 Jun 20, 2025
2392335
lcov
johnseth97 Jun 20, 2025
21588a7
rc
johnseth97 Jun 20, 2025
e0da2bc
debug
johnseth97 Jun 20, 2025
4917d6b
indent
johnseth97 Jun 20, 2025
2a1aff1
pathing
johnseth97 Jun 20, 2025
a68093a
t
johnseth97 Jun 20, 2025
080215b
pathing
johnseth97 Jun 20, 2025
d947135
final
johnseth97 Jun 20, 2025
83b91bf
enable semver
johnseth97 Jun 20, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 32 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: CI

on:
push:
branches: [main, codecov-dev]
branches: [main]
pull_request:
branches: [main]

Expand Down Expand Up @@ -66,7 +66,7 @@ jobs:
eval "$(luarocks --lua-version=5.1 path)"
luarocks --lua-version=5.1 list # debug
nvim --headless -u tests/minimal_init.lua -c "luafile tests/run_cov.lua"

luacov -r lcov > lcov.info
sed -i 's|SF:.*/codex.nvim/codex.nvim/|SF:|g' lcov.info
head -n 10 lcov.info # debug
Expand All @@ -77,10 +77,39 @@ jobs:

echo "=== all source-file entries ==="
grep '^SF:' lcov.info | sed -e 's/^SF://g' | sort | uniq | head -n 10

- name: Upload code coverage
uses: codecov/codecov-action@v4
with:
files: lcov.info # <-- new file
disable_search: true
token: ${{ secrets.CODECOV_TOKEN }}

release:
name: Semantic Release
runs-on: ubuntu-latest
needs: test
if: github.ref == 'refs/heads/main' && github.event_name == 'push'

steps:
- name: Checkout repo
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'

- name: Install semantic-release and plugins
run: |
npm install --no-save \
semantic-release \
@semantic-release/commit-analyzer \
@semantic-release/release-notes-generator \
@semantic-release/changelog \
@semantic-release/github

- name: Run semantic-release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: npx semantic-release
26 changes: 18 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
## A Neovim plugin integrating the open-sourced Codex CLI (`codex`).
> Latest version: ![GitHub tag (latest SemVer)](https://img.shields.io/github/v/tag/johnseth97/codex.nvim?sort=semver)

Note: As of v1.0.0, <Esc> no longer closes the Codex window. Press q to close, and <Esc><Esc> to safely interrupt model generation without resetting context.

### Features:
- ✅ Toggle Codex floating window with `:CodexToggle`
- ✅ Optional keymap mapping via `setup` call
Expand Down Expand Up @@ -31,27 +33,35 @@ export OPENAI_API_KEY=your_api_key
return {
'johnseth97/codex.nvim',
lazy = true,
cmd = { 'Codex', 'CodexToggle' }, -- Optional: Load only on command execution
keys = {
{
'<leader>cc',
'<leader>cc', -- Change this to your preferred keybinding
function() require('codex').toggle() end,
desc = 'Toggle Codex popup',
},
},
opts = {
keymaps = {}, -- disable internal mapping
border = 'rounded', -- or 'double'
width = 0.8,
height = 0.8,
autoinstall = true,
keymaps = {}, -- Disable internal default keymap (<leader>cc -> :CodexToggle)
border = 'rounded', -- Options: 'single', 'double', or 'rounded'
width = 0.8, -- Width of the floating window (0.0 to 1.0)
height = 0.8, -- Height of the floating window (0.0 to 1.0)
model = nil, -- Optional: pass a string to use a specific model (e.g., 'o3-mini')
autoinstall = true, -- Automatically install the Codex CLI if not found
},
}
```
}```

### Usage:
- Call `:Codex` (or `:CodexToggle`) to open or close the Codex popup.
-- Map your own keybindings via the `keymaps.toggle` setting.
- Add the following code to show backgrounded Codex window in lualine:

```lua
require('codex').status() -- drop in to your lualine sections
```

### Configuration:
- All plugin configurations can be seen in the `opts` table of the plugin setup, as shown in the installation section.

- **For deeper customization, please refer to the [Codex CLI documentation](https://github.com/openai/codex?tab=readme-ov-file#full-configuration-example) full configuration example. These features change quickly as Codex CLI is in active beta development.*

46 changes: 31 additions & 15 deletions lua/codex/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ local config = {
width = 0.8,
height = 0.8,
cmd = 'codex',
model = nil, -- Default to the latest model
autoinstall = true,
}

Expand Down Expand Up @@ -37,13 +38,13 @@ local function open_window()

local styles = {
single = {
{ '', 'FloatBorder' },
{ '', 'FloatBorder' },
{ '─', 'FloatBorder' },
{ '', 'FloatBorder' },
{ '', 'FloatBorder' },
{ '│', 'FloatBorder' },
{ '', 'FloatBorder' },
{ '', 'FloatBorder' },
{ '─', 'FloatBorder' },
{ '', 'FloatBorder' },
{ '', 'FloatBorder' },
{ '│', 'FloatBorder' },
},
double = {
Expand Down Expand Up @@ -83,6 +84,16 @@ local function open_window()
end

function M.open()
local function create_clean_buf()
local buf = vim.api.nvim_create_buf(false, false)
vim.api.nvim_buf_set_option(buf, 'bufhidden', 'hide')
vim.api.nvim_buf_set_option(buf, 'swapfile', false)
vim.api.nvim_buf_set_option(buf, 'filetype', 'codex')
vim.api.nvim_buf_set_keymap(buf, 't', 'q', [[<C-\><C-n><cmd>lua require('codex').close()<CR>]], { noremap = true, silent = true })
vim.api.nvim_buf_set_keymap(buf, 'n', 'q', [[<cmd>lua require('codex').close()<CR>]], { noremap = true, silent = true })
return buf
end

if state.win and vim.api.nvim_win_is_valid(state.win) then
vim.api.nvim_set_current_win(state.win)
return
Expand All @@ -98,7 +109,7 @@ function M.open()
else
-- Show failure message *after* buffer is created
if not state.buf or not vim.api.nvim_buf_is_valid(state.buf) then
state.buf = vim.api.nvim_create_buf(false, false)
state.buf = create_clean_buf()
end
vim.api.nvim_buf_set_lines(state.buf, 0, -1, false, {
'Autoinstall cancelled or failed.',
Expand Down Expand Up @@ -128,32 +139,37 @@ function M.open()
end
end

-- At this point, CLI is available: safe to setup buffer and window
if not state.buf or not vim.api.nvim_buf_is_valid(state.buf) then
state.buf = vim.api.nvim_create_buf(false, false)
vim.api.nvim_buf_set_option(state.buf, 'bufhidden', 'hide')
vim.api.nvim_buf_set_option(state.buf, 'swapfile', false)
vim.api.nvim_buf_set_option(state.buf, 'filetype', 'codex')
vim.api.nvim_buf_set_keymap(state.buf, 't', '<Esc>', [[<C-\><C-n><cmd>lua require('codex').close()<CR>]], { noremap = true, silent = true })
vim.api.nvim_buf_set_keymap(state.buf, 'n', '<Esc>', [[<cmd>lua require('codex').close()<CR>]], { noremap = true, silent = true })
local function is_buf_reusable(buf)
return type(buf) == 'number' and vim.api.nvim_buf_is_valid(buf)
end

if not is_buf_reusable(state.buf) then
state.buf = create_clean_buf()
end

open_window()

if not state.job then
state.job = vim.fn.termopen(config.cmd, {
local cmd_args = type(config.cmd) == 'string' and { config.cmd } or vim.deepcopy(config.cmd)
if config.model then
table.insert(cmd_args, '-m')
table.insert(cmd_args, config.model)
end

state.job = vim.fn.termopen(cmd_args, {
cwd = vim.loop.cwd(),
on_exit = function()
state.job = nil
end,
})
end
end

function M.close()
if state.win and vim.api.nvim_win_is_valid(state.win) then
vim.api.nvim_win_close(state.win, true)
state.win = nil
end
state.win = nil
end

function M.toggle()
Expand Down
2 changes: 1 addition & 1 deletion makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
LUAROCKS_ENV = eval "$(luarocks --lua-version=5.1 path)"

# Headless Neovim test runner
NVIM_TEST_CMD = nvim --headless -u tests/minimal_init.lua -c "PlenaryBustedDirectory tests/"
NVIM_TEST_CMD = nvim --headless -u tests/minimal_init.lua -c "PlenaryBustedDirectory tests/specs/"

.PHONY: test coverage clean

Expand Down
62 changes: 57 additions & 5 deletions tests/codex_spec.lua → tests/specs/codex_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ describe('codex.nvim', function()
end)

it('opens a floating terminal window', function()
require('codex').setup { cmd = "echo 'test'" }
require('codex').setup { cmd = { 'echo', 'test' } }
require('codex').open()

local win = vim.api.nvim_get_current_win()
Expand All @@ -39,19 +39,27 @@ describe('codex.nvim', function()
end)

it('toggles the window', function()
require('codex').setup { cmd = "echo 'test'" }
require('codex').setup { cmd = { 'echo', 'test' } }

require('codex').toggle()
local win1 = vim.api.nvim_get_current_win()
local buf = vim.api.nvim_win_get_buf(win1)

assert(vim.api.nvim_win_is_valid(win1), 'Codex window should be open')

print('🔍 buffer modified before toggle:', vim.api.nvim_buf_get_option(buf, 'modified'))

-- Optional: manually mark it clean
vim.api.nvim_buf_set_option(buf, 'modified', false)

require('codex').toggle()
local still_valid = pcall(vim.api.nvim_win_get_buf, win1)
assert(not still_valid, 'Codex window should be closed')

local ok, _ = pcall(vim.api.nvim_win_get_buf, win1)
assert(not ok, 'Codex window should be closed')
end)

it('shows statusline only when job is active but window is not', function()
require('codex').setup { cmd = 'sleep 1000' }
require('codex').setup { cmd = 'sleep', '1000' }
require('codex').open()

vim.defer_fn(function()
Expand All @@ -60,4 +68,48 @@ describe('codex.nvim', function()
eq(status, '[Codex]')
end, 100)
end)

it('passes -m <model> to termopen when configured', function()
local original_fn = vim.fn
local termopen_called = false
local received_cmd = {}

-- Mock vim.fn with proxy
vim.fn = setmetatable({
termopen = function(cmd, opts)
termopen_called = true
received_cmd = cmd
if type(opts.on_exit) == 'function' then
vim.defer_fn(function()
opts.on_exit(0)
end, 10)
end
return 123
end,
}, { __index = original_fn })

-- Reload module fresh
package.loaded['codex'] = nil
package.loaded['codex.state'] = nil
local codex = require 'codex'

codex.setup {
cmd = 'codex',
model = 'o3-mini',
}

codex.open()

vim.wait(500, function()
return termopen_called
end, 10)

assert(termopen_called, 'termopen should be called')
assert(type(received_cmd) == 'table', 'cmd should be passed as a list')
assert(vim.tbl_contains(received_cmd, '-m'), 'should include -m flag')
assert(vim.tbl_contains(received_cmd, 'o3-mini'), 'should include specified model name')

-- Restore original
vim.fn = original_fn
end)
end)
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Loading