diff --git a/README.md b/README.md index 1d1884a..317473c 100644 --- a/README.md +++ b/README.md @@ -391,6 +391,19 @@ _See [config.lua#L9](./lua/avante/config.lua) for the full config_ -- auto_approve_tool_permissions = true, -- Auto-approve all tools (no prompts) -- auto_approve_tool_permissions = {"bash", "replace_in_file"}, -- Auto-approve specific tools only }, + prompt_logger = { -- logs prompts to disk (timestamped, for replay/debugging) + enabled = true, -- toggle logging entirely + log_dir = vim.fn.stdpath("cache") .. "/avante_prompts", -- directory where logs are saved + fortune_cookie_on_success = false, -- shows a random fortune after each logged prompt (requires `fortune` installed) + next_prompt = { + normal = "", -- load the next (newer) prompt log in normal mode + insert = "", + }, + prev_prompt = { + normal = "", -- load the previous (older) prompt log in normal mode + insert = "", + }, + }, mappings = { --- @class AvanteConflictMappings diff = { diff --git a/lua/avante/config.lua b/lua/avante/config.lua index 57d42a1..3c44b96 100644 --- a/lua/avante/config.lua +++ b/lua/avante/config.lua @@ -431,6 +431,19 @@ M._defaults = { auto_approve_tool_permissions = false, -- Default: show permission prompts for all tools auto_check_diagnostics = true, }, + prompt_logger = { -- logs prompts to disk (timestamped, for replay/debugging) + enabled = true, -- toggle logging entirely + log_dir = Utils.join_paths(vim.fn.stdpath("cache"), "avante_prompts"), -- directory where logs are saved + fortune_cookie_on_success = false, -- shows a random fortune after each logged prompt (requires `fortune` installed) + next_prompt = { + normal = "", -- load the next (newer) prompt log in normal mode + insert = "", + }, + prev_prompt = { + normal = "", -- load the previous (older) prompt log in normal mode + insert = "", + }, + }, history = { max_tokens = 4096, carried_entry_count = nil, diff --git a/lua/avante/sidebar.lua b/lua/avante/sidebar.lua index 3a49446..9347057 100644 --- a/lua/avante/sidebar.lua +++ b/lua/avante/sidebar.lua @@ -11,6 +11,7 @@ local Config = require("avante.config") local Diff = require("avante.diff") local Llm = require("avante.llm") local Utils = require("avante.utils") +local PromptLogger = require("avante.utils.promptLogger") local Highlights = require("avante.highlights") local RepoMap = require("avante.repo_map") local FileSelector = require("avante.file_selector") @@ -2562,6 +2563,8 @@ function Sidebar:create_input_container() ---@param request string local function handle_submit(request) + if Config.prompt_logger.enabled then PromptLogger.log_prompt(request) end + if self.is_generating then self:add_history_messages({ HistoryMessage:new({ role = "user", content = request }), @@ -2845,6 +2848,10 @@ function Sidebar:create_input_container() self.input_container:map("n", Config.mappings.submit.normal, on_submit) self.input_container:map("i", Config.mappings.submit.insert, on_submit) + self.input_container:map("n", Config.prompt_logger.next_prompt.normal, PromptLogger.on_log_retrieve(-1)) + self.input_container:map("i", Config.prompt_logger.next_prompt.insert, PromptLogger.on_log_retrieve(-1)) + self.input_container:map("n", Config.prompt_logger.prev_prompt.normal, PromptLogger.on_log_retrieve(1)) + self.input_container:map("i", Config.prompt_logger.prev_prompt.insert, PromptLogger.on_log_retrieve(1)) if Config.mappings.sidebar.close_from_input ~= nil then if Config.mappings.sidebar.close_from_input.normal ~= nil then diff --git a/lua/avante/utils/init.lua b/lua/avante/utils/init.lua index 95987e1..d5ce98e 100644 --- a/lua/avante/utils/init.lua +++ b/lua/avante/utils/init.lua @@ -9,6 +9,7 @@ local lsp = vim.lsp ---@field path avante.utils.path ---@field environment avante.utils.environment ---@field lsp avante.utils.lsp +---@field logger avante.utils.promptLogger local M = {} setmetatable(M, { diff --git a/lua/avante/utils/promptLogger.lua b/lua/avante/utils/promptLogger.lua new file mode 100644 index 0000000..813d448 --- /dev/null +++ b/lua/avante/utils/promptLogger.lua @@ -0,0 +1,99 @@ +local Config = require("avante.config") +local Utils = require("avante.utils") + +---@class avante.utils.promptLogger +local M = {} + +function M.log_prompt(request) + local log_dir = Config.prompt_logger.log_dir + local log_file = Utils.join_paths(log_dir, "avante_prompt_" .. os.date("%Y%m%d_%H%M%S") .. ".log") + + if vim.fn.isdirectory(log_dir) == 0 then vim.fn.mkdir(log_dir, "p") end + + local file = io.open(log_file, "w") + if file then + file:write(request) + file:close() + if Config.prompt_logger and Config.prompt_logger.fortune_cookie_on_success then + local handle = io.popen("fortune -s -n 100") + if handle then + local fortune_msg = handle:read("*a") + handle:close() + if fortune_msg and #fortune_msg > 0 then vim.notify(fortune_msg, vim.log.levels.INFO, { title = "" }) end + end + end + else + vim.notify("Failed to log prompt", vim.log.levels.ERROR) + end +end + +-- Cache + helper +local logs, idx = {}, 0 + +local function refresh_logs() + local dir = Config.prompt_logger.log_dir + logs = vim.fn.glob(Utils.join_paths(dir, "avante_prompt_*.log"), false, true) + table.sort(logs, function(a, b) -- newest first + return a > b + end) +end + +---@param step integer 0 = keep | 1 = newer | -1 = older +local function load_log(step) + if #logs == 0 then refresh_logs() end + if #logs == 0 then + vim.notify("No prompt logs found 🤷", vim.log.levels.WARN) + return + end + idx = (idx + step) -- turn wheel + if idx < 1 then idx = #logs end -- wrap around + if idx > #logs then idx = 1 end + + local fp = io.open(logs[idx], "r") + if not fp then + vim.notify("Could not open " .. logs[idx], vim.log.levels.ERROR) + return + end + local content = fp:read("*a") + fp:close() + + local buf = vim.api.nvim_get_current_buf() + vim.api.nvim_buf_set_lines(buf, 0, -1, false, vim.split(content, "\n", { plain = true })) + vim.bo[buf].modifiable = true + vim.b[buf].avante_logpath = logs[idx] +end + +function M.next_log() load_log(1) end + +function M.prev_log() load_log(-1) end + +local function _read_log(delta) + if #logs == 0 then refresh_logs() end + if #logs == 0 then return nil end + + local target = idx + delta + if target < 1 then target = 1 end + if target > #logs then target = #logs end + + idx = target + + local fp = io.open(logs[idx], "r") + if not fp then return nil end + local txt = fp:read("*a") + fp:close() + return { txt = txt, path = logs[idx] } +end + +function M.on_log_retrieve(delta) + return function() + local res = _read_log(delta) + if not res then + vim.notify("No logs available", vim.log.levels.WARN) + return + end + vim.api.nvim_buf_set_lines(0, 0, -1, false, vim.split(res.txt, "\n", { plain = true })) + vim.api.nvim_win_set_cursor(0, { 1, 0 }) + end +end + +return M