--- Reads Pi's on-disk session store to enumerate prior sessions for a cwd. --- Pi offers no session-listing RPC, so a cwd picker reads the filesystem. --- Sessions are bucketed per project; the bucket directory name encodes the --- cwd but is lossy, so the authoritative cwd is each session file's header. --- The directory Pi stores sessions in. --- `PI_CODING_AGENT_SESSION_DIR` wins; otherwise Pi's agent directory. local directory = function() local explicit = vim.env.PI_CODING_AGENT_SESSION_DIR if explicit and explicit ~= '' then return vim.fs.normalize( explicit ) end local agent = vim.env.PI_CODING_AGENT_DIR if not agent or agent == '' then agent = ( vim.env.HOME or vim.uv.os_homedir() ) .. '/.pi/agent' end return vim.fs.normalize( agent .. '/sessions' ) end --- Pulls the id and timestamp out of a `_.jsonl` session filename. local parse_name = function( name ) local timestamp, id = name:match '^(.-)_(.+)%.jsonl$' if not id then return nil end return { id = id, timestamp = timestamp, } end --- The `cwd` a session file was recorded for, from its header line. local header = function( path ) if type( path ) ~= 'string' or path == '' then return nil end local handle = io.open( path, 'r' ) if not handle then return nil end local first = handle:read( 'l' ) handle:close() if not first then return nil end local ok, record = pcall( vim.json.decode, first ) if not ok or type( record ) ~= 'table' or record.type ~= 'session' then return nil end return record end local valid_file = function( path, id ) local record = header( path ) return record ~= nil and record.id == id end --- The session files directly inside one bucket directory. local bucket_files = function( bucket ) local found = {} local handle = vim.uv.fs_scandir( bucket ) if not handle then return found end while true do local name, kind = vim.uv.fs_scandir_next( handle ) if not name then break end if kind == 'file' and name:match '%.jsonl$' then table.insert( found, name ) end end return found end --- Prior sessions recorded for a working directory, most recent first. --- Each entry is `{ id, timestamp, file }`. Every header determines its cwd; --- default bucket storage and explicit flat directories are both supported. --- @param cwd string working directory to match --- @param dir string? session directory override, for tests local sessions = function( cwd, dir ) if type( cwd ) ~= 'string' or cwd == '' then return {} end local root = dir and vim.fs.normalize( dir ) or directory() local target = vim.fs.normalize( cwd ) local scan = vim.uv.fs_scandir( root ) if not scan then return {} end local matches = {} local add = function( file ) local record = header( file ) local recorded = record and record.cwd if type( recorded ) ~= 'string' then return end recorded = vim.fs.normalize( recorded ) local same = recorded == target if vim.fn.has 'win32' == 1 then same = recorded:lower() == target:lower() end local parsed = parse_name( vim.fs.basename( file ) ) if same and parsed then table.insert( matches, { id = parsed.id, timestamp = parsed.timestamp, file = file, } ) end end while true do local name, kind = vim.uv.fs_scandir_next( scan ) if not name then break end if kind == 'file' and name:match '%.jsonl$' then add( root .. '/' .. name ) elseif kind == 'directory' then local bucket = root .. '/' .. name for _, file in ipairs( bucket_files( bucket ) ) do add( bucket .. '/' .. file ) end end end table.sort( matches, function( a, b ) return a.timestamp > b.timestamp end ) return matches end return { valid_file = valid_file, directory = directory, parse_name = parse_name, sessions = sessions, }