irongit

My nvim configuration

feat(claude): undo and redo claude changes

huncholanehuncholaneauthored
parent 4725f3fcommit 67d01e3011030362a3b4be5fae6e4ee586937e38Browse files

3 files changed, +119 -1

+3-0claude-nvim/lua/claude/init.lua
@@ -5,6 +5,7 @@ local runner = require("claude.runner")
55 local highlights = require("claude.highlights")
66 local pickers = require("claude.pickers")
77 local edit = require("claude.edit")
8+local timetravel = require("claude.timetravel")
89
910 ---@class ClaudeConfig
1011 local defaults = {
@@ -33,6 +34,8 @@ function M.setup(opts)
3334
3435 vim.keymap.set("n", "<leader>cc", ui.cancel_current, { desc = "Claude: Cancel current prompt" })
3536 vim.keymap.set("n", "<leader>ci", edit.prompt_insert, { desc = "Claude: Insert at cursor with prompt" })
37+ vim.keymap.set("n", "<leader>cu", timetravel.undo, { desc = "Claude: Undo last claude change" })
38+ vim.keymap.set("n", "<leader>cU", timetravel.redo, { desc = "Claude: Redo claude change" })
3639 vim.keymap.set("n", "<leader>ct", ui.toggle, { desc = "Claude: Toggle panel" })
3740 vim.keymap.set("n", "<leader>cn", ui.start_new, { desc = "Claude: New session" })
3841 vim.keymap.set("n", "<leader>cr", ui.resume_last, { desc = "Claude: Resume last session" })
+9-1claude-nvim/lua/claude/runner.lua
@@ -79,7 +79,13 @@ local function flush_prompt(sid)
7979 local after = read_file(path)
8080 local hunks, unified = compute_change(before, after)
8181 if hunks then
82- table.insert(files, { path = path, hunks = hunks, diff = unified })
82+ table.insert(files, {
83+ path = path,
84+ hunks = hunks,
85+ diff = unified,
86+ before_content = before,
87+ after_content = after,
88+ })
8389 end
8490 end
8591 st.current_prompt = nil
@@ -92,6 +98,8 @@ local function flush_prompt(sid)
9298 vim.schedule(function()
9399 local ok_h, hl = pcall(require, "claude.highlights")
94100 if ok_h then hl.refresh_all() end
101+ local ok_t, tt = pcall(require, "claude.timetravel")
102+ if ok_t then tt.bump() end
95103 end)
96104 end
97105
+107-0claude-nvim/lua/claude/timetravel.lua
@@ -0,0 +1,107 @@
1+local M = {}
2+
3+local store = require("claude.store")
4+
5+local cursor = nil
6+
7+local function all_asc()
8+ local desc = store.all_prompts()
9+ local out = {}
10+ for i = #desc, 1, -1 do table.insert(out, desc[i]) end
11+ return out
12+end
13+
14+local function ensure_cursor()
15+ if cursor == nil then
16+ cursor = #all_asc()
17+ end
18+end
19+
20+local function write_file(path, content)
21+ local f = io.open(path, "wb")
22+ if not f then return false end
23+ f:write(content or "")
24+ f:close()
25+ return true
26+end
27+
28+local function reload_buffers(path)
29+ local abs = vim.fn.fnamemodify(path, ":p")
30+ for _, b in ipairs(vim.api.nvim_list_bufs()) do
31+ if vim.api.nvim_buf_is_loaded(b) then
32+ local n = vim.api.nvim_buf_get_name(b)
33+ if n ~= "" and vim.fn.fnamemodify(n, ":p") == abs then
34+ if not vim.bo[b].modified then
35+ pcall(vim.api.nvim_buf_call, b, function() vim.cmd("edit!") end)
36+ else
37+ vim.notify("claude: buffer " .. n .. " has unsaved edits — not reloaded", vim.log.levels.WARN)
38+ end
39+ end
40+ end
41+ end
42+end
43+
44+local function preview(p)
45+ return ((p.text or ""):gsub("%s+", " ")):sub(1, 50)
46+end
47+
48+function M.undo()
49+ ensure_cursor()
50+ local prompts = all_asc()
51+ if cursor <= 0 then
52+ vim.notify("claude: nothing to undo", vim.log.levels.WARN)
53+ return
54+ end
55+ local p = prompts[cursor]
56+ local applied = 0
57+ for _, f in ipairs(p.files or {}) do
58+ if f.before_content ~= nil then
59+ if write_file(f.path, f.before_content) then
60+ applied = applied + 1
61+ reload_buffers(f.path)
62+ end
63+ end
64+ end
65+ cursor = cursor - 1
66+ if applied == 0 then
67+ vim.notify("claude: prompt has no before-state (older schema)", vim.log.levels.WARN)
68+ else
69+ vim.notify("claude: undid '" .. preview(p) .. "' (" .. applied .. " files)")
70+ end
71+end
72+
73+function M.redo()
74+ ensure_cursor()
75+ local prompts = all_asc()
76+ if cursor >= #prompts then
77+ vim.notify("claude: nothing to redo", vim.log.levels.WARN)
78+ return
79+ end
80+ local p = prompts[cursor + 1]
81+ local applied = 0
82+ for _, f in ipairs(p.files or {}) do
83+ if f.after_content ~= nil then
84+ if write_file(f.path, f.after_content) then
85+ applied = applied + 1
86+ reload_buffers(f.path)
87+ end
88+ end
89+ end
90+ cursor = cursor + 1
91+ if applied == 0 then
92+ vim.notify("claude: prompt has no after-state (older schema)", vim.log.levels.WARN)
93+ else
94+ vim.notify("claude: redid '" .. preview(p) .. "' (" .. applied .. " files)")
95+ end
96+end
97+
98+function M.bump()
99+ ensure_cursor()
100+ cursor = (cursor or 0) + 1
101+end
102+
103+function M.reset()
104+ cursor = nil
105+end
106+
107+return M