diff --git a/README.md b/README.md index 47339a4..d263718 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ - ✅ Optional keymap mapping via `setup` call - ✅ Background running when window hidden - ✅ Statusline integration via `require('codex').status()` +- ✅ Browse Codex chat history with `:CodexHistory` ### Installation: @@ -31,7 +32,7 @@ export OPENAI_API_KEY=your_api_key return { 'kkrampis/codex.nvim', lazy = true, - cmd = { 'Codex', 'CodexToggle' }, -- Optional: Load only on command execution + cmd = { 'Codex', 'CodexToggle', 'CodexHistory', 'CodexHistoryToggle', 'CodexLast', 'CodexPin', 'CodexPinned', 'CodexClearSessions' }, -- Optional: Load only on command execution keys = { { 'cc', -- Change this to your preferred keybinding @@ -44,6 +45,11 @@ return { keymaps = { toggle = nil, -- Keybind to toggle Codex window (Disabled by default, watch out for conflicts) quit = '', -- Keybind to close the Codex window (default: Ctrl + q) + history = 'ch', -- Keybind to toggle Codex history + term_normal = '', -- Enter terminal-normal mode + last = 'cl', -- Resume last Codex session + pin = 'cp', -- Pin current Codex session + pinned = 'cP', -- Resume pinned Codex session }, -- Disable internal default keymap (cc -> :CodexToggle) border = 'rounded', -- Options: 'single', 'double', or 'rounded' width = 0.8, -- Width of the floating window (0.0 to 1.0) @@ -52,11 +58,27 @@ return { autoinstall = true, -- Automatically install the Codex CLI if not found panel = false, -- Open Codex in a side-panel (vertical split) instead of floating window use_buffer = false, -- Capture Codex stdout into a normal buffer instead of a terminal buffer + auto_insert = true, -- Enter terminal mode on open/focus + history = { + max_entries = 200, -- Limit entries in history list + max_files = 1000, -- Limit session files scanned for history (perf) + auto_close_active = true, -- Close active session when resuming from history + ui = 'buffer', -- 'buffer' or 'telescope' (requires telescope.nvim) + persist_pin = true, -- Persist pinned session across restarts + persist_last = true, -- Persist last session across restarts + }, }, }``` ### Usage: - Call `:Codex` (or `:CodexToggle`) to open or close the Codex popup or side-panel. +- Call `:CodexHistory` to browse past Codex sessions and resume them. +- Call `:CodexHistoryToggle` to switch between the live Codex session and history in the same window. +- Call `:CodexLast` to resume the most recent Codex session. +- Call `:CodexPin` to pin the current resumed session, and `:CodexPinned` to jump back to it. +- Call `:CodexClearSessions` to clear pinned and last sessions. +- Use `Tab` to toggle between Codex and history when the Codex window is focused. +- If `history.ui = 'telescope'`, the toggle command opens the Telescope picker instead of swapping the Codex window. - Map your own keybindings via the `keymaps.toggle` setting. - To choose floating popup vs side-panel, set `panel = false` (popup) or `panel = true` (panel) in your setup options. - To capture Codex output in an editable buffer instead of a terminal, set `use_buffer = true` (or `false` to keep terminal) in your setup options. diff --git a/lua/codex/history.lua b/lua/codex/history.lua new file mode 100644 index 0000000..f5849ba --- /dev/null +++ b/lua/codex/history.lua @@ -0,0 +1,254 @@ +local M = {} + +local function decode_json(line) + local ok, data = pcall(vim.json.decode, line) + if ok then return data end + ok, data = pcall(vim.fn.json_decode, line) + if ok then return data end + return nil +end + +local function codex_home() + return os.getenv('CODEX_HOME') or vim.fn.expand('~/.codex') +end + +local function session_files() + local dir = codex_home() .. '/sessions' + return vim.fn.globpath(dir, '**/*.jsonl', true, true) +end + +local function parse_session_meta(file) + local lines = vim.fn.readfile(file, '', 1) + if not lines or #lines == 0 then return nil end + local data = decode_json(lines[1]) + if not data or data.type ~= 'session_meta' then return nil end + + local payload = data.payload or {} + local git = payload.git or {} + + return { + id = payload.id, + timestamp = payload.timestamp, + cwd = payload.cwd, + source = payload.source, + originator = payload.originator, + model_provider = payload.model_provider, + branch = git.branch, + repository_url = git.repository_url, + file = file, + } +end + +local function short_time(iso) + if not iso or iso == '' then return '' end + local t = iso:gsub('T', ' '):gsub('Z', '') + return t:sub(1, 16) +end + +local function display_line(entry) + local time = short_time(entry.timestamp) + local id = entry.id or 'unknown' + local cwd = entry.cwd or '' + local branch = entry.branch or '' + local source = entry.source or '' + + if source ~= '' then + source = '[' .. source .. ']' + end + + return string.format('%s %s %s %s %s', time, id, cwd, branch, source) +end + +local function load_entries(max_entries) + local config = require('codex').get_config() + local max_files = (config.history and config.history.max_files) or 1000 + local files = session_files() + + if max_files and #files > max_files then + table.sort(files, function(a, b) + local sa = vim.loop.fs_stat(a) + local sb = vim.loop.fs_stat(b) + local ma = sa and sa.mtime and sa.mtime.sec or 0 + local mb = sb and sb.mtime and sb.mtime.sec or 0 + return ma > mb + end) + local trimmed = {} + for i = 1, max_files do + trimmed[i] = files[i] + end + files = trimmed + end + + local entries = {} + for _, file in ipairs(files) do + local entry = parse_session_meta(file) + if entry and entry.id and entry.timestamp then + table.insert(entries, entry) + end + end + + table.sort(entries, function(a, b) + return (a.timestamp or '') > (b.timestamp or '') + end) + + if max_entries and #entries > max_entries then + local trimmed = {} + for i = 1, max_entries do + trimmed[i] = entries[i] + end + entries = trimmed + end + + return entries +end + +function M.latest_session_id() + local config = require('codex').get_config() + local list = load_entries(config.history and config.history.max_entries or 200) + if #list == 0 then return nil end + return list[1].id +end + +local function open_telescope(entries) + local ok_telescope = pcall(require, 'telescope') + if not ok_telescope then + return false + end + local ok, pickers = pcall(require, 'telescope.pickers') + if not ok then + return false + end + local finders = require('telescope.finders') + local conf = require('telescope.config').values + local actions = require('telescope.actions') + local action_state = require('telescope.actions.state') + + pickers.new({}, { + prompt_title = 'Codex History', + finder = finders.new_table({ + results = entries, + entry_maker = function(entry) + return { + value = entry, + display = display_line(entry), + ordinal = (entry.timestamp or '') .. ' ' .. (entry.cwd or '') .. ' ' .. (entry.id or ''), + } + end, + }), + sorter = conf.generic_sorter({}), + attach_mappings = function(prompt_bufnr, map) + local function resume_selected() + local selection = action_state.get_selected_entry() + if not selection or not selection.value then + return + end + actions.close(prompt_bufnr) + require('codex').resume(selection.value.id) + end + map('i', '', resume_selected) + map('n', '', resume_selected) + return true + end, + }):find() + + return true +end + +local function close_window(win) + if win and vim.api.nvim_win_is_valid(win) then + vim.api.nvim_win_close(win, true) + end +end + +function M.build_buffer(entries) + local config = require('codex').get_config() + local list = entries or load_entries(config.history and config.history.max_entries or 200) + + local buf = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_option(buf, 'bufhidden', 'wipe') + vim.api.nvim_buf_set_option(buf, 'buftype', 'nofile') + vim.api.nvim_buf_set_option(buf, 'swapfile', false) + vim.api.nvim_buf_set_option(buf, 'filetype', 'codex-history') + + local header = { + 'Codex History', + 'Enter: resume q: close /: search Tab: toggle', + '', + } + + local lines = {} + for _, line in ipairs(header) do + table.insert(lines, line) + end + + for _, entry in ipairs(list) do + table.insert(lines, display_line(entry)) + end + + if #list == 0 then + table.insert(lines, 'No Codex sessions found in ' .. codex_home() .. '/sessions') + end + + vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines) + vim.api.nvim_buf_set_option(buf, 'modifiable', false) + + vim.b[buf].codex_history_entries = list + vim.b[buf].codex_history_header_len = #header + + vim.keymap.set('n', 'q', function() + close_window(vim.api.nvim_get_current_win()) + end, { buffer = buf, silent = true }) + + local config = require('codex').get_config() + if config.keymaps and config.keymaps.quit then + vim.keymap.set('n', config.keymaps.quit, function() + require('codex').close() + end, { buffer = buf, silent = true }) + end + + if config.keymaps and config.keymaps.history then + vim.keymap.set('n', config.keymaps.history, function() + require('codex').toggle_history() + end, { buffer = buf, silent = true }) + end + + vim.keymap.set('n', '', function() + local win = vim.api.nvim_get_current_win() + local line = vim.api.nvim_win_get_cursor(win)[1] + local idx = line - (vim.b[buf].codex_history_header_len or 0) + local entry = (vim.b[buf].codex_history_entries or {})[idx] + if not entry then + return + end + require('codex').resume(entry.id) + end, { buffer = buf, silent = true }) + + vim.keymap.set('n', '', function() + require('codex').toggle_history() + end, { buffer = buf, silent = true }) + + return buf +end + +function M.open_split(entries) + local buf = M.build_buffer(entries) + vim.cmd('botright split') + local win = vim.api.nvim_get_current_win() + vim.api.nvim_win_set_buf(win, buf) + vim.api.nvim_win_set_height(win, math.min(15, vim.o.lines - 4)) + return buf, win +end + +function M.open(entries) + local config = require('codex').get_config() + local list = entries or load_entries(config.history and config.history.max_entries or 200) + if config.history and config.history.ui == 'telescope' then + if open_telescope(list) then + return + end + vim.notify('[codex.nvim] Telescope not available; falling back to buffer history view', vim.log.levels.WARN) + end + return M.open_split(list) +end + +return M diff --git a/lua/codex/init.lua b/lua/codex/init.lua index 4378b46..104dbff 100644 --- a/lua/codex/init.lua +++ b/lua/codex/init.lua @@ -4,10 +4,29 @@ local state = require 'codex.state' local M = {} +local function enter_terminal_mode() + vim.schedule(function() + if + config.auto_insert + and state.win and vim.api.nvim_win_is_valid(state.win) + and state.buf and vim.api.nvim_buf_is_valid(state.buf) + and vim.bo[state.buf].buftype == 'terminal' + then + vim.api.nvim_set_current_win(state.win) + vim.cmd('startinsert') + end + end) +end + local config = { keymaps = { toggle = nil, quit = '', -- Default: Ctrl+q to quit + history = 'ch', + term_normal = '', -- Enter terminal-normal mode + last = 'cl', + pin = 'cp', + pinned = 'cP', }, border = 'single', width = 0.8, @@ -17,11 +36,34 @@ local config = { autoinstall = true, panel = false, -- if true, open Codex in a side-panel instead of floating window use_buffer = false, -- if true, capture Codex stdout into a normal buffer instead of a terminal + auto_insert = true, -- if true, enter terminal mode on open/focus + history = { + max_entries = 200, + max_files = 1000, + auto_close_active = true, + ui = 'buffer', -- 'buffer' or 'telescope' (requires telescope.nvim) + persist_pin = true, + persist_last = true, + }, } function M.setup(user_config) config = vim.tbl_deep_extend('force', config, user_config or {}) + if config.history and config.history.persist_pin then + local ok, pinned = pcall(vim.fn.readfile, state.pinned_session_file or '') + if ok and pinned and pinned[1] and pinned[1] ~= '' then + state.pinned_session_id = pinned[1] + end + end + + if config.history and config.history.persist_last then + local ok, last = pcall(vim.fn.readfile, state.last_session_file or '') + if ok and last and last[1] and last[1] ~= '' then + state.last_session_id = last[1] + end + end + vim.api.nvim_create_user_command('Codex', function() M.toggle() end, { desc = 'Toggle Codex popup' }) @@ -30,12 +72,101 @@ function M.setup(user_config) M.toggle() end, { desc = 'Toggle Codex popup (alias)' }) + vim.api.nvim_create_user_command('CodexHistory', function() + M.open_history(false) + end, { desc = 'Browse Codex chat history' }) + + vim.api.nvim_create_user_command('CodexHistoryToggle', function() + M.toggle_history() + end, { desc = 'Toggle Codex history view' }) + + vim.api.nvim_create_user_command('CodexLast', function() + M.open_last() + end, { desc = 'Resume last Codex session' }) + + vim.api.nvim_create_user_command('CodexPin', function() + M.pin_current() + end, { desc = 'Pin current Codex session' }) + + vim.api.nvim_create_user_command('CodexPinned', function() + M.open_pinned() + end, { desc = 'Resume pinned Codex session' }) + + vim.api.nvim_create_user_command('CodexClearSessions', function() + M.clear_sessions() + end, { desc = 'Clear pinned/last Codex sessions' }) + if config.keymaps.toggle then vim.api.nvim_set_keymap('n', config.keymaps.toggle, 'CodexToggle', { noremap = true, silent = true }) end + + if config.keymaps.history then + vim.api.nvim_set_keymap('n', config.keymaps.history, 'CodexHistoryToggle', { noremap = true, silent = true }) + vim.api.nvim_set_keymap('t', config.keymaps.history, [[CodexHistoryToggle]], { noremap = true, silent = true }) + end + + if config.keymaps.last then + vim.api.nvim_set_keymap('n', config.keymaps.last, 'CodexLast', { noremap = true, silent = true }) + end + + if config.keymaps.pin then + vim.api.nvim_set_keymap('n', config.keymaps.pin, 'CodexPin', { noremap = true, silent = true }) + end + + if config.keymaps.pinned then + vim.api.nvim_set_keymap('n', config.keymaps.pinned, 'CodexPinned', { noremap = true, silent = true }) + end + + -- Toggle history from the live Codex terminal + local group = vim.api.nvim_create_augroup('CodexKeymaps', { clear = true }) + vim.api.nvim_create_autocmd('FileType', { + group = group, + pattern = 'codex', + callback = function(args) + local buf = args.buf + vim.keymap.set('n', '', function() + require('codex').toggle_history() + end, { buffer = buf, silent = true }) + vim.keymap.set('t', '', function() + require('codex').toggle_history() + end, { buffer = buf, silent = true }) + + if config.keymaps.term_normal then + vim.keymap.set('t', config.keymaps.term_normal, [[]], { buffer = buf, silent = true }) + end + + if config.auto_insert then + vim.schedule(function() + if vim.api.nvim_buf_is_valid(buf) and vim.bo[buf].buftype == 'terminal' then + vim.cmd('startinsert') + end + end) + end + end, + }) + if config.auto_insert then + local auto_group = vim.api.nvim_create_augroup('CodexAutoInsert', { clear = true }) + vim.api.nvim_create_autocmd({ 'BufEnter', 'WinEnter', 'TermOpen', 'TermEnter' }, { + group = auto_group, + pattern = '*', + callback = function(args) + local buf = args.buf + if vim.bo[buf].filetype ~= 'codex' or vim.bo[buf].buftype ~= 'terminal' then + return + end + vim.schedule(function() + if vim.api.nvim_buf_is_valid(buf) then + vim.api.nvim_set_current_buf(buf) + vim.cmd('startinsert') + end + end) + end, + }) + end end -local function open_window() +local function open_window(buf) + local target_buf = buf or state.buf local width = math.floor(vim.o.columns * config.width) local height = math.floor(vim.o.lines * config.height) local row = math.floor((vim.o.lines - height) / 2) @@ -77,7 +208,7 @@ local function open_window() local border = type(config.border) == 'string' and styles[config.border] or config.border - state.win = vim.api.nvim_open_win(state.buf, true, { + state.win = vim.api.nvim_open_win(target_buf, true, { relative = 'editor', width = width, height = height, @@ -89,18 +220,41 @@ local function open_window() end --- Open Codex in a side-panel (vertical split) instead of floating window -local function open_panel() +local function open_panel(buf) -- Create a vertical split on the right and show the buffer vim.cmd('vertical rightbelow vsplit') local win = vim.api.nvim_get_current_win() - vim.api.nvim_win_set_buf(win, state.buf) + vim.api.nvim_win_set_buf(win, buf or state.buf) -- Adjust width according to config (percentage of total columns) local width = math.floor(vim.o.columns * config.width) vim.api.nvim_win_set_width(win, width) state.win = win end -function M.open() +local function update_winbar(win) + if not win or not vim.api.nvim_win_is_valid(win) then return end + local buf = vim.api.nvim_win_get_buf(win) + local ft = vim.bo[buf].filetype + if ft == 'codex-history' then + vim.api.nvim_win_set_option(win, 'winbar', ' Codex History | Tab: Codex ') + elseif ft == 'codex' then + vim.api.nvim_win_set_option(win, 'winbar', ' Codex | Tab: History ') + else + vim.api.nvim_win_set_option(win, 'winbar', '') + end +end + +local function resolve_check_cmd(cmd) + if type(cmd) == 'table' then + return cmd[1] + end + if type(cmd) == 'string' then + return cmd:match('^%S+') + end + return nil +end + +function M.open(cmd_args) local function create_clean_buf() local buf = vim.api.nvim_create_buf(false, false) @@ -121,16 +275,46 @@ function M.open() if state.win and vim.api.nvim_win_is_valid(state.win) then vim.api.nvim_set_current_win(state.win) - return + local win_buf = vim.api.nvim_win_get_buf(state.win) + if win_buf == state.history_buf or vim.bo[win_buf].filetype == 'codex-history' then + if not state.buf or not vim.api.nvim_buf_is_valid(state.buf) then + state.buf = create_clean_buf() + end + vim.api.nvim_win_set_buf(state.win, state.buf) + update_winbar(state.win) + else + enter_terminal_mode() + return + end end - local check_cmd = type(config.cmd) == 'string' and not config.cmd:find '%s' and config.cmd or (type(config.cmd) == 'table' and config.cmd[1]) or nil + local cmd_to_run + if cmd_args then + cmd_to_run = cmd_args + else + if type(config.cmd) == 'string' then + if config.cmd:find '%s' then + cmd_to_run = config.cmd + else + cmd_to_run = { config.cmd } + end + else + cmd_to_run = vim.deepcopy(config.cmd) + end + + if type(cmd_to_run) == 'table' and config.model then + table.insert(cmd_to_run, '-m') + table.insert(cmd_to_run, config.model) + end + end + + local check_cmd = resolve_check_cmd(cmd_to_run) if check_cmd and vim.fn.executable(check_cmd) == 0 then if config.autoinstall then installer.prompt_autoinstall(function(success) if success then - M.open() -- Try again after installing + M.open(cmd_args) -- Try again after installing else -- Show failure message *after* buffer is created if not state.buf or not vim.api.nvim_buf_is_valid(state.buf) then @@ -173,18 +357,18 @@ function M.open() end if config.panel then open_panel() else open_window() end + update_winbar(state.win) - if not state.job then - -- assemble command - 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 + -- Ensure terminal buffer is clean before starting job + if vim.api.nvim_buf_is_valid(state.buf) then + vim.bo[state.buf].modified = false + vim.api.nvim_set_current_buf(state.buf) + end + if not state.job then if config.use_buffer then -- capture stdout/stderr into normal buffer - state.job = vim.fn.jobstart(cmd_args, { + state.job = vim.fn.jobstart(cmd_to_run, { cwd = vim.loop.cwd(), stdout_buffered = true, on_stdout = function(_, data) @@ -212,14 +396,158 @@ function M.open() }) else -- use a terminal buffer - state.job = vim.fn.termopen(cmd_args, { + state.job = vim.fn.termopen(cmd_to_run, { cwd = vim.loop.cwd(), on_exit = function() state.job = nil end, }) + enter_terminal_mode() + end + else + enter_terminal_mode() + end +end + +function M.open_history(reuse_win) + local history = require('codex.history') + if config.history and config.history.ui == 'telescope' then + history.open() + return + end + + local buf = history.build_buffer() + state.history_buf = buf + + if reuse_win and state.win and vim.api.nvim_win_is_valid(state.win) then + vim.api.nvim_set_current_win(state.win) + vim.api.nvim_win_set_buf(state.win, buf) + update_winbar(state.win) + return + end + + if config.panel then + open_panel(buf) + else + open_window(buf) + end + update_winbar(state.win) +end + +function M.toggle_history() + if config.history and config.history.ui == 'telescope' then + require('codex.history').open() + return + end + + if state.win and vim.api.nvim_win_is_valid(state.win) then + local win_buf = vim.api.nvim_win_get_buf(state.win) + if win_buf == state.history_buf or vim.bo[win_buf].filetype == 'codex-history' then + if state.buf and vim.api.nvim_buf_is_valid(state.buf) then + vim.api.nvim_win_set_buf(state.win, state.buf) + update_winbar(state.win) + if config.auto_insert and vim.bo[state.buf].buftype == 'terminal' then + vim.cmd('startinsert') + end + return + end + M.open(nil) + return + end + M.open_history(true) + return + end + + M.open_history(false) +end + +function M.resume(session_id) + if not session_id or session_id == '' then + vim.notify('[codex.nvim] Missing session id for resume', vim.log.levels.ERROR) + return + end + + if state.job then + if config.history and config.history.auto_close_active then + pcall(vim.fn.jobstop, state.job) + pcall(vim.fn.chanclose, state.job) + M.close() + state.job = nil + else + vim.notify('[codex.nvim] Close the active Codex session before resuming another', vim.log.levels.WARN) + return + end + end + + local cmd + if type(config.cmd) == 'table' then + cmd = vim.deepcopy(config.cmd) + table.insert(cmd, 'resume') + table.insert(cmd, session_id) + elseif type(config.cmd) == 'string' then + if config.cmd:find '%s' then + vim.notify('[codex.nvim] config.cmd contains spaces; using "codex" for resume', vim.log.levels.WARN) + cmd = { 'codex', 'resume', session_id } + else + cmd = { config.cmd, 'resume', session_id } end + else + cmd = { 'codex', 'resume', session_id } end + + state.last_session_id = session_id + if config.history and config.history.persist_last then + local dir = vim.fn.stdpath('data') .. '/codex.nvim' + vim.fn.mkdir(dir, 'p') + vim.fn.writefile({ session_id }, state.last_session_file) + end + M.open(cmd) +end + +function M.open_last() + local history = require('codex.history') + local id = state.last_session_id or history.latest_session_id() + if not id then + vim.notify('[codex.nvim] No Codex sessions found', vim.log.levels.WARN) + return + end + M.resume(id) +end + +function M.pin_current() + local id = state.last_session_id + if not id then + vim.notify('[codex.nvim] No active session to pin. Resume a session first.', vim.log.levels.WARN) + return + end + state.pinned_session_id = id + if config.history and config.history.persist_pin then + local dir = vim.fn.stdpath('data') .. '/codex.nvim' + vim.fn.mkdir(dir, 'p') + vim.fn.writefile({ id }, state.pinned_session_file) + end + vim.notify('[codex.nvim] Pinned session: ' .. id, vim.log.levels.INFO) +end + +function M.open_pinned() + local id = state.pinned_session_id + if not id or id == '' then + vim.notify('[codex.nvim] No pinned session. Use :CodexPin first.', vim.log.levels.WARN) + return + end + M.resume(id) +end + +function M.clear_sessions() + state.last_session_id = nil + state.pinned_session_id = nil + if state.last_session_file then + pcall(vim.fn.delete, state.last_session_file) + end + if state.pinned_session_file then + pcall(vim.fn.delete, state.pinned_session_file) + end + vim.notify('[codex.nvim] Cleared pinned and last sessions', vim.log.levels.INFO) end function M.close() @@ -233,10 +561,18 @@ function M.toggle() if state.win and vim.api.nvim_win_is_valid(state.win) then M.close() else - M.open() + M.open(nil) end end +function M.get_config() + return config +end + +function M.get_state() + return state +end + function M.statusline() if state.job and not (state.win and vim.api.nvim_win_is_valid(state.win)) then return '[Codex]' diff --git a/lua/codex/state.lua b/lua/codex/state.lua index be06ca3..5bf1476 100644 --- a/lua/codex/state.lua +++ b/lua/codex/state.lua @@ -4,6 +4,14 @@ local M = { buf = nil, win = nil, job = nil, + history_buf = nil, + last_session_id = nil, + pinned_session_id = nil, + pinned_session_file = nil, + last_session_file = nil, } +M.pinned_session_file = vim.fn.stdpath('data') .. '/codex.nvim/pinned_session' +M.last_session_file = vim.fn.stdpath('data') .. '/codex.nvim/last_session' + return M diff --git a/tests/specs/codex_spec.lua b/tests/specs/codex_spec.lua index 3b10db9..d6d500c 100644 --- a/tests/specs/codex_spec.lua +++ b/tests/specs/codex_spec.lua @@ -110,4 +110,30 @@ describe('codex.nvim', function() -- Restore original vim.fn = original_fn end) + + it('returns most recent session id from history', function() + local original_fn = vim.fn + + vim.fn = setmetatable({ + globpath = function() + return { '/tmp/a.jsonl', '/tmp/b.jsonl' } + end, + readfile = function(path, _, __) + if path == '/tmp/a.jsonl' then + return { '{"type":"session_meta","payload":{"id":"a","timestamp":"2025-01-01T00:00:00Z"}}' } + end + return { '{"type":"session_meta","payload":{"id":"b","timestamp":"2025-02-01T00:00:00Z"}}' } + end, + expand = function() + return '/tmp' + end, + }, { __index = original_fn }) + + package.loaded['codex.history'] = nil + local history = require 'codex.history' + local id = history.latest_session_id() + eq(id, 'b') + + vim.fn = original_fn + end) end)