irongit

My nvim configuration

feat(claude): telescope directory sessions

huncholanehuncholaneauthored
parent 6458fa5commit db74a46da105f723c957c00c34dafe514d2646d0Browse files

3 files changed, +192 -0

+1-0claude-nvim/lua/claude/init.lua
@@ -34,6 +34,7 @@ function M.setup(opts)
3434 vim.keymap.set("n", "<leader>ct", ui.toggle, { desc = "Claude: Toggle panel" })
3535 vim.keymap.set("n", "<leader>cn", ui.start_new, { desc = "Claude: New session" })
3636 vim.keymap.set("n", "<leader>cr", ui.resume_last, { desc = "Claude: Resume last session" })
37+ vim.keymap.set("n", "<leader>cs", pickers.sessions_picker, { desc = "Claude: Sessions picker" })
3738 vim.keymap.set("n", "<leader>cq", pickers.prompts_picker, { desc = "Claude: Prompts → quickfix" })
3839 vim.keymap.set("n", "<leader>cl", pickers.qf_last, { desc = "Claude: Last prompt → quickfix" })
3940 vim.keymap.set("n", "<leader>cp", ui.open_prompt_keep_focus, { desc = "Claude: Prompt (keep focus)" })
+122-0claude-nvim/lua/claude/pickers.lua
@@ -112,4 +112,126 @@ function M.prompts_picker()
112112 }):find()
113113 end
114114
115+local function projects_dir()
116+ local cwd = vim.fn.getcwd()
117+ local enc = cwd:gsub("[/%.]", "-")
118+ local home = vim.uv.os_homedir() or os.getenv("HOME") or ""
119+ return home .. "/.claude/projects/" .. enc
120+end
121+
122+local function first_user_text(path, max_chars)
123+ local f = io.open(path, "r")
124+ if not f then return nil end
125+ local result
126+ for line in f:lines() do
127+ local ok, evt = pcall(vim.json.decode, line)
128+ if ok and type(evt) == "table" and evt.message and evt.message.role == "user" then
129+ local c = evt.message.content
130+ local text
131+ if type(c) == "string" then
132+ text = c
133+ elseif type(c) == "table" then
134+ local parts = {}
135+ for _, b in ipairs(c) do
136+ if type(b) == "table" and b.type == "text" and b.text then
137+ table.insert(parts, b.text)
138+ end
139+ end
140+ text = table.concat(parts, "\n")
141+ end
142+ if text and text ~= "" and not text:match("^<") then
143+ result = text:sub(1, max_chars or 4000)
144+ break
145+ end
146+ end
147+ end
148+ f:close()
149+ return result
150+end
151+
152+local function list_sessions()
153+ local dir = projects_dir()
154+ local out = {}
155+ local handle = vim.uv.fs_scandir(dir)
156+ if not handle then return out end
157+ while true do
158+ local name, t = vim.uv.fs_scandir_next(handle)
159+ if not name then break end
160+ if t == "file" and name:sub(-6) == ".jsonl" then
161+ local full = dir .. "/" .. name
162+ local stat = vim.uv.fs_stat(full)
163+ table.insert(out, {
164+ claude_id = name:sub(1, -7),
165+ path = full,
166+ mtime = stat and stat.mtime.sec or 0,
167+ })
168+ end
169+ end
170+ table.sort(out, function(a, b) return a.mtime > b.mtime end)
171+ return out
172+end
173+
174+function M.sessions_picker()
175+ local ok_t, _ = pcall(require, "telescope")
176+ if not ok_t then
177+ vim.notify("claude: telescope.nvim not available", vim.log.levels.ERROR)
178+ return
179+ end
180+ local pickers = require("telescope.pickers")
181+ local finders = require("telescope.finders")
182+ local actions = require("telescope.actions")
183+ local action_state = require("telescope.actions.state")
184+ local previewers = require("telescope.previewers")
185+ local conf = require("telescope.config").values
186+
187+ local sessions = list_sessions()
188+ if #sessions == 0 then
189+ vim.notify("claude: no sessions found for " .. vim.fn.getcwd(), vim.log.levels.WARN)
190+ return
191+ end
192+
193+ pickers.new({}, {
194+ prompt_title = "Claude sessions (" .. #sessions .. ")",
195+ finder = finders.new_table({
196+ results = sessions,
197+ entry_maker = function(s)
198+ local first = first_user_text(s.path, 200) or "(no prompt)"
199+ local display = string.format(
200+ "%s %s %s",
201+ os.date("%Y-%m-%d %H:%M", s.mtime),
202+ s.claude_id:sub(1, 8),
203+ snippet(first, 80)
204+ )
205+ return {
206+ value = s,
207+ display = display,
208+ ordinal = display,
209+ }
210+ end,
211+ }),
212+ sorter = conf.generic_sorter({}),
213+ previewer = previewers.new_buffer_previewer({
214+ define_preview = function(self, entry)
215+ local text = first_user_text(entry.value.path, 4000) or "(no user message)"
216+ local lines = { "# " .. entry.value.claude_id, "" }
217+ for _, l in ipairs(vim.split(text, "\n", { plain = true })) do
218+ table.insert(lines, l)
219+ end
220+ vim.api.nvim_buf_set_lines(self.state.bufnr, 0, -1, false, lines)
221+ vim.bo[self.state.bufnr].filetype = "markdown"
222+ end,
223+ }),
224+ attach_mappings = function(prompt_bufnr)
225+ actions.select_default:replace(function()
226+ local entry = action_state.get_selected_entry()
227+ actions.close(prompt_bufnr)
228+ if entry and entry.value then
229+ require("claude.ui").resume_session(entry.value.claude_id)
230+ end
231+ end)
232+ return true
233+ end,
234+ }):find()
235+end
236+
115237 return M
+69-0claude-nvim/lua/claude/ui.lua
@@ -250,6 +250,45 @@ local function replay_history(claude_id)
250250 return turns > 0
251251 end
252252
253+function M.resume_session(claude_id)
254+ if not claude_id or claude_id == "" then
255+ vim.notify("claude: missing claude_id", vim.log.levels.WARN)
256+ return
257+ end
258+ local runner = require("claude.runner")
259+ local store = require("claude.store")
260+ -- find existing session.json with this claude_id, else build thin session
261+ local session
262+ for _, entry in ipairs(store.list_sessions()) do
263+ local s = store.load_path(entry.path)
264+ if s and s.claude_id == claude_id then session = s break end
265+ end
266+ if not session then
267+ session = {
268+ id = store.uuid(),
269+ started_at = os.time(),
270+ cwd = vim.fn.getcwd(),
271+ claude_id = claude_id,
272+ prompts = {},
273+ }
274+ end
275+ local sid = runner.attach_existing(session)
276+ panel.sid = sid
277+ store.write_pointer({ session_id = session.id, claude_id = claude_id, ts = os.time(), cwd = session.cwd })
278+ clear_buf()
279+ M.open_panel()
280+ append_lines({
281+ "# Claude — resumed",
282+ "_id: " .. sid:sub(1, 8) .. " ← " .. claude_id:sub(1, 8) .. "_",
283+ "",
284+ })
285+ local replayed = replay_history(claude_id)
286+ if not replayed then
287+ append_lines({ "_(no prior transcript found on disk)_", "" })
288+ end
289+ append_lines({ "Press `i` to send a prompt.", "" })
290+end
291+
253292 function M.resume_last()
254293 local runner = require("claude.runner")
255294 local sid = runner.resume_from_pointer()
@@ -274,6 +313,36 @@ function M.resume_last()
274313 append_lines({ "Press `i` to send a prompt.", "" })
275314 end
276315
316+function M.resume_session(claude_id)
317+ if not claude_id or claude_id == "" then
318+ vim.notify("claude: missing session id", vim.log.levels.WARN)
319+ return
320+ end
321+ local runner = require("claude.runner")
322+ local store = require("claude.store")
323+ local session = {
324+ id = store.uuid(),
325+ started_at = os.time(),
326+ cwd = vim.fn.getcwd(),
327+ claude_id = claude_id,
328+ prompts = {},
329+ }
330+ local sid = runner.attach_existing(session)
331+ panel.sid = sid
332+ clear_buf()
333+ M.open_panel()
334+ append_lines({
335+ "# Claude — resumed",
336+ "_id: " .. sid:sub(1, 8) .. " ← " .. claude_id:sub(1, 8) .. "_",
337+ "",
338+ })
339+ local replayed = replay_history(claude_id)
340+ if not replayed then
341+ append_lines({ "_(no prior transcript found on disk)_", "" })
342+ end
343+ append_lines({ "Press `i` to send a prompt.", "" })
344+end
345+
277346 function M.open_input(opts)
278347 opts = opts or {}
279348 if not panel.sid then