migrating the logs file

This commit is contained in:
2026-03-24 21:53:14 -04:00
parent 4416626acf
commit 9687b352d5
29 changed files with 452 additions and 374 deletions

View File

@@ -4,379 +4,31 @@
local M = {}
local params = require("codetyper.params.agents.logs")
---@class LogEntry
---@field timestamp string ISO timestamp
---@field level string "info" | "debug" | "request" | "response" | "tool" | "error"
---@field message string Log message
---@field data? table Optional structured data
---@class LogState
---@field entries LogEntry[] All log entries
---@field listeners table[] Functions to call when new entries are added
---@field total_prompt_tokens number Running total of prompt tokens
---@field total_response_tokens number Running total of response tokens
local state = {
entries = {},
listeners = {},
total_prompt_tokens = 0,
total_response_tokens = 0,
current_provider = nil,
current_model = nil,
}
--- Get current timestamp
---@return string
local function get_timestamp()
return os.date("%H:%M:%S")
end
--- Add a log entry
---@param level string Log level
---@param message string Log message
---@param data? table Optional data
function M.log(level, message, data)
local entry = {
timestamp = get_timestamp(),
level = level,
message = message,
data = data,
}
table.insert(state.entries, entry)
-- Notify all listeners
for _, listener in ipairs(state.listeners) do
pcall(listener, entry)
end
end
--- Log info message
---@param message string
---@param data? table
function M.info(message, data)
M.log("info", message, data)
end
--- Log debug message
---@param message string
---@param data? table
function M.debug(message, data)
M.log("debug", message, data)
end
--- Log API request
---@param provider string LLM provider
---@param model string Model name
---@param prompt_tokens? number Estimated prompt tokens
function M.request(provider, model, prompt_tokens)
state.current_provider = provider
state.current_model = model
local msg = string.format("[%s] %s", provider:upper(), model)
if prompt_tokens then
msg = msg .. string.format(" | Prompt: ~%d tokens", prompt_tokens)
end
M.log("request", msg, {
provider = provider,
model = model,
prompt_tokens = prompt_tokens,
})
end
--- Log API response with token usage
---@param prompt_tokens number Tokens used in prompt
---@param response_tokens number Tokens in response
---@param stop_reason? string Why the response stopped
function M.response(prompt_tokens, response_tokens, stop_reason)
state.total_prompt_tokens = state.total_prompt_tokens + prompt_tokens
state.total_response_tokens = state.total_response_tokens + response_tokens
local msg = string.format(
"Tokens: %d in / %d out | Total: %d in / %d out",
prompt_tokens,
response_tokens,
state.total_prompt_tokens,
state.total_response_tokens
)
if stop_reason then
msg = msg .. " | Stop: " .. stop_reason
end
M.log("response", msg, {
prompt_tokens = prompt_tokens,
response_tokens = response_tokens,
total_prompt = state.total_prompt_tokens,
total_response = state.total_response_tokens,
stop_reason = stop_reason,
})
end
--- Log tool execution
---@param tool_name string Name of the tool
---@param status string "start" | "success" | "error" | "approval"
---@param details? string Additional details
function M.tool(tool_name, status, details)
local icons = params.icons
local msg = string.format("[%s] %s", icons[status] or status, tool_name)
if details then
msg = msg .. ": " .. details
end
M.log("tool", msg, {
tool = tool_name,
status = status,
details = details,
})
end
--- Log error
---@param message string
---@param data? table
function M.error(message, data)
M.log("error", "ERROR: " .. message, data)
end
--- Log warning
---@param message string
---@param data? table
function M.warning(message, data)
M.log("warning", "WARN: " .. message, data)
end
--- Add log entry (compatibility function for scheduler)
--- Accepts {type = "info", message = "..."} format
---@param entry table Log entry with type and message
function M.add(entry)
if entry.type == "clear" then
M.clear()
return
end
M.log(entry.type or "info", entry.message or "", entry.data)
end
--- Log thinking/reasoning step (Claude Code style)
---@param step string Description of what's happening
function M.thinking(step)
M.log("thinking", step)
end
--- Log a reasoning/explanation message (shown prominently)
---@param message string The reasoning message
function M.reason(message)
M.log("reason", message)
end
--- Log file read operation
---@param filepath string Path of file being read
---@param lines? number Number of lines read
function M.read(filepath, lines)
local msg = string.format("Read(%s)", vim.fn.fnamemodify(filepath, ":~:."))
if lines then
msg = msg .. string.format("\n ⎿ Read %d lines", lines)
end
M.log("action", msg)
end
--- Log explore/search operation
---@param description string What we're exploring
function M.explore(description)
M.log("action", string.format("Explore(%s)", description))
end
--- Log explore done
---@param tool_uses number Number of tool uses
---@param tokens number Tokens used
---@param duration number Duration in seconds
function M.explore_done(tool_uses, tokens, duration)
M.log(
"result",
string.format(" ⎿ Done (%d tool uses · %.1fk tokens · %.1fs)", tool_uses, tokens / 1000, duration)
)
end
--- Log update/edit operation
---@param filepath string Path of file being edited
---@param added? number Lines added
---@param removed? number Lines removed
function M.update(filepath, added, removed)
local msg = string.format("Update(%s)", vim.fn.fnamemodify(filepath, ":~:."))
if added or removed then
local parts = {}
if added and added > 0 then
table.insert(parts, string.format("Added %d lines", added))
end
if removed and removed > 0 then
table.insert(parts, string.format("Removed %d lines", removed))
end
if #parts > 0 then
msg = msg .. "\n" .. table.concat(parts, ", ")
end
end
M.log("action", msg)
end
--- Log a task/step that's in progress
---@param task string Task name
---@param status string Status message (optional)
function M.task(task, status)
local msg = task
if status then
msg = msg .. " " .. status
end
M.log("task", msg)
end
--- Log task completion
---@param next_task? string Next task (optional)
function M.task_done(next_task)
local msg = " ⎿ Done"
if next_task then
msg = msg .. "\n" .. next_task
end
M.log("result", msg)
end
--- Register a listener for new log entries
---@param callback fun(entry: LogEntry)
---@return number Listener ID for removal
function M.add_listener(callback)
table.insert(state.listeners, callback)
return #state.listeners
end
--- Remove a listener
---@param id number Listener ID
function M.remove_listener(id)
if id > 0 and id <= #state.listeners then
table.remove(state.listeners, id)
end
end
--- Get all log entries
---@return LogEntry[]
function M.get_entries()
return state.entries
end
--- Get token totals
---@return number, number prompt_tokens, response_tokens
function M.get_token_totals()
return state.total_prompt_tokens, state.total_response_tokens
end
--- Get current provider info
---@return string?, string? provider, model
function M.get_provider_info()
return state.current_provider, state.current_model
end
--- Clear all logs and reset counters
function M.clear()
state.entries = {}
state.total_prompt_tokens = 0
state.total_response_tokens = 0
state.current_provider = nil
state.current_model = nil
-- Notify listeners of clear
for _, listener in ipairs(state.listeners) do
pcall(listener, { level = "clear" })
end
end
--- Format entry for display
---@param entry LogEntry
---@return string
function M.format_entry(entry)
-- Claude Code style formatting for thinking/action entries
local thinking_types = params.thinking_types
local is_thinking = vim.tbl_contains(thinking_types, entry.level)
if is_thinking then
local prefix = params.thinking_prefixes[entry.level] or ""
if prefix ~= "" then
return prefix .. " " .. entry.message
else
return entry.message
end
end
-- Traditional log format for other types
local level_prefix = params.level_icons[entry.level] or "?"
local base = string.format("[%s] %s %s", entry.timestamp, level_prefix, entry.message)
-- If this is a response entry with raw_response, append the full response
if entry.data and entry.data.raw_response then
local response = entry.data.raw_response
-- Add separator and the full response
base = base .. "\n" .. string.rep("-", 40) .. "\n" .. response .. "\n" .. string.rep("-", 40)
end
return base
end
--- Format entry for display in chat (compact Claude Code style)
---@param entry LogEntry
---@return string|nil Formatted string or nil to skip
function M.format_for_chat(entry)
-- Skip certain log types in chat view
local skip_types = { "debug", "queue", "patch" }
if vim.tbl_contains(skip_types, entry.level) then
return nil
end
-- Claude Code style formatting
local thinking_types = params.thinking_types
if vim.tbl_contains(thinking_types, entry.level) then
local prefix = params.thinking_prefixes[entry.level] or ""
if prefix ~= "" then
return prefix .. " " .. entry.message
else
return entry.message
end
end
-- Tool logs
if entry.level == "tool" then
return "" .. entry.message:gsub("^%[.-%] ", "")
end
-- Info/success
if entry.level == "info" or entry.level == "success" then
return "" .. entry.message
end
-- Errors
if entry.level == "error" then
return "" .. entry.message
end
-- Request/response (compact)
if entry.level == "request" then
return "" .. entry.message
end
if entry.level == "response" then
return "" .. entry.message
end
return nil
end
--- Estimate token count for a string (rough approximation)
---@param text string
---@return number
function M.estimate_tokens(text)
-- Rough estimate: ~4 characters per token for English text
return math.ceil(#text / 4)
end
M.log = require("codetyper.adapters.nvim.ui.logs.log")
M.clear = require("codetyper.adapters.nvim.ui.logs.clear")
M.info = require("codetyper.adapters.nvim.ui.logs.info")
M.debug = require("codetyper.adapters.nvim.ui.logs.debug")
M.error = require("codetyper.adapters.nvim.ui.logs.error")
M.warning = require("codetyper.adapters.nvim.ui.logs.warning")
M.thinking = require("codetyper.adapters.nvim.ui.logs.thinking")
M.reason = require("codetyper.adapters.nvim.ui.logs.reason")
M.request = require("codetyper.adapters.nvim.ui.logs.request")
M.response = require("codetyper.adapters.nvim.ui.logs.response")
M.tool = require("codetyper.adapters.nvim.ui.logs.tool")
M.add = require("codetyper.adapters.nvim.ui.logs.add")
M.read = require("codetyper.adapters.nvim.ui.logs.read")
M.explore = require("codetyper.adapters.nvim.ui.logs.explore")
M.explore_done = require("codetyper.adapters.nvim.ui.logs.explore_done")
M.update = require("codetyper.adapters.nvim.ui.logs.update")
M.task = require("codetyper.adapters.nvim.ui.logs.task")
M.task_done = require("codetyper.adapters.nvim.ui.logs.task_done")
M.add_listener = require("codetyper.adapters.nvim.ui.logs.add_listener")
M.remove_listener = require("codetyper.adapters.nvim.ui.logs.remove_listener")
M.get_entries = require("codetyper.adapters.nvim.ui.logs.get_entries")
M.get_token_totals = require("codetyper.adapters.nvim.ui.logs.get_token_totals")
M.get_provider_info = require("codetyper.adapters.nvim.ui.logs.get_provider_info")
M.format_entry = require("codetyper.adapters.nvim.ui.logs.format_entry")
M.format_for_chat = require("codetyper.adapters.nvim.ui.logs.format_for_chat")
M.estimate_tokens = require("codetyper.utils.estimate_tokens")
return M

View File

@@ -0,0 +1,15 @@
local log = require("codetyper.adapters.nvim.ui.logs.log")
local clear = require("codetyper.adapters.nvim.ui.logs.clear")
--- Add log entry (compatibility function for scheduler)
--- Accepts {type = "info", message = "..."} format
---@param entry table Log entry with type and message
local function add(entry)
if entry.type == "clear" then
clear()
return
end
log(entry.type or "info", entry.message or "", entry.data)
end
return add

View File

@@ -0,0 +1,11 @@
local state = require("codetyper.state.state")
--- Register a listener for new log entries
---@param callback fun(entry: LogEntry)
---@return number listener_id Listener ID for removal
local function add_listener(callback)
table.insert(state.listeners, callback)
return #state.listeners
end
return add_listener

View File

@@ -0,0 +1,16 @@
local state = require("codetyper.state.state")
--- Clear all logs and reset counters
local function clear()
state.entries = {}
state.total_prompt_tokens = 0
state.total_response_tokens = 0
state.current_provider = nil
state.current_model = nil
for _, listener in ipairs(state.listeners) do
pcall(listener, { level = "clear" })
end
end
return clear

View File

@@ -0,0 +1,10 @@
local log = require("codetyper.adapters.nvim.ui.logs.log")
--- Log debug message
---@param message string
---@param data? table
local function debug(message, data)
log("debug", message, data)
end
return debug

View File

@@ -0,0 +1,10 @@
local log = require("codetyper.adapters.nvim.ui.logs.log")
--- Log error message
---@param message string
---@param data? table
local function log_error(message, data)
log("error", "ERROR: " .. message, data)
end
return log_error

View File

@@ -0,0 +1,9 @@
local log = require("codetyper.adapters.nvim.ui.logs.log")
--- Log explore/search operation
---@param description string What we're exploring
local function explore(description)
log("action", string.format("Explore(%s)", description))
end
return explore

View File

@@ -0,0 +1,14 @@
local log = require("codetyper.adapters.nvim.ui.logs.log")
--- Log explore done with stats
---@param tool_uses number Number of tool uses
---@param tokens number Tokens used
---@param duration number Duration in seconds
local function explore_done(tool_uses, tokens, duration)
log(
"result",
string.format(" ⎿ Done (%d tool uses · %.1fk tokens · %.1fs)", tool_uses, tokens / 1000, duration)
)
end
return explore_done

View File

@@ -0,0 +1,30 @@
local params = require("codetyper.params.agents.logs")
--- Format a log entry for display
---@param entry LogEntry
---@return string
local function format_entry(entry)
local thinking_types = params.thinking_types
local is_thinking = vim.tbl_contains(thinking_types, entry.level)
if is_thinking then
local prefix = params.thinking_prefixes[entry.level] or ""
if prefix ~= "" then
return prefix .. " " .. entry.message
else
return entry.message
end
end
local level_prefix = params.level_icons[entry.level] or "?"
local base = string.format("[%s] %s %s", entry.timestamp, level_prefix, entry.message)
if entry.data and entry.data.raw_response then
local separator = string.rep("-", 40)
base = base .. "\n" .. separator .. "\n" .. entry.data.raw_response .. "\n" .. separator
end
return base
end
return format_entry

View File

@@ -0,0 +1,45 @@
local params = require("codetyper.params.agents.logs")
--- Format entry for display in chat (compact Claude Code style)
---@param entry LogEntry
---@return string|nil formatted Formatted string or nil to skip
local function format_for_chat(entry)
local skip_types = { "debug", "queue", "patch" }
if vim.tbl_contains(skip_types, entry.level) then
return nil
end
local thinking_types = params.thinking_types
if vim.tbl_contains(thinking_types, entry.level) then
local prefix = params.thinking_prefixes[entry.level] or ""
if prefix ~= "" then
return prefix .. " " .. entry.message
else
return entry.message
end
end
if entry.level == "tool" then
return "" .. entry.message:gsub("^%[.-%] ", "")
end
if entry.level == "info" or entry.level == "success" then
return "" .. entry.message
end
if entry.level == "error" then
return "" .. entry.message
end
if entry.level == "request" then
return "" .. entry.message
end
if entry.level == "response" then
return "" .. entry.message
end
return nil
end
return format_for_chat

View File

@@ -0,0 +1,9 @@
local state = require("codetyper.state.state")
--- Get all log entries
---@return LogEntry[]
local function get_entries()
return state.entries
end
return get_entries

View File

@@ -0,0 +1,10 @@
local state = require("codetyper.state.state")
--- Get current provider info
---@return string|nil provider
---@return string|nil model
local function get_provider_info()
return state.current_provider, state.current_model
end
return get_provider_info

View File

@@ -0,0 +1,10 @@
local state = require("codetyper.state.state")
--- Get token totals
---@return number prompt_tokens
---@return number response_tokens
local function get_token_totals()
return state.total_prompt_tokens, state.total_response_tokens
end
return get_token_totals

View File

@@ -0,0 +1,10 @@
local log = require("codetyper.adapters.nvim.ui.logs.log")
--- Log info message
---@param message string
---@param data? table
local function info(message, data)
log("info", message, data)
end
return info

View File

@@ -0,0 +1,23 @@
local state = require("codetyper.state.state")
local get_timestamp = require("codetyper.utils.get_timestamp")
--- Add a log entry and notify all listeners
---@param level string Log level
---@param message string Log message
---@param data? table Optional data
local function log(level, message, data)
local entry = {
timestamp = get_timestamp(),
level = level,
message = message,
data = data,
}
table.insert(state.entries, entry)
for _, listener in ipairs(state.listeners) do
pcall(listener, entry)
end
end
return log

View File

@@ -0,0 +1,14 @@
local log = require("codetyper.adapters.nvim.ui.logs.log")
--- Log file read operation
---@param filepath string Path of file being read
---@param lines? number Number of lines read
local function read(filepath, lines)
local message = string.format("Read(%s)", vim.fn.fnamemodify(filepath, ":~:."))
if lines then
message = message .. string.format("\n ⎿ Read %d lines", lines)
end
log("action", message)
end
return read

View File

@@ -0,0 +1,9 @@
local log = require("codetyper.adapters.nvim.ui.logs.log")
--- Log a reasoning/explanation message (shown prominently)
---@param message string The reasoning message
local function reason(message)
log("reason", message)
end
return reason

View File

@@ -0,0 +1,11 @@
local state = require("codetyper.state.state")
--- Remove a listener by ID
---@param listener_id number Listener ID
local function remove_listener(listener_id)
if listener_id > 0 and listener_id <= #state.listeners then
table.remove(state.listeners, listener_id)
end
end
return remove_listener

View File

@@ -0,0 +1,24 @@
local state = require("codetyper.state.state")
local log = require("codetyper.adapters.nvim.ui.logs.log")
--- Log API request
---@param provider string LLM provider
---@param model string Model name
---@param prompt_tokens? number Estimated prompt tokens
local function request(provider, model, prompt_tokens)
state.current_provider = provider
state.current_model = model
local message = string.format("[%s] %s", provider:upper(), model)
if prompt_tokens then
message = message .. string.format(" | Prompt: ~%d tokens", prompt_tokens)
end
log("request", message, {
provider = provider,
model = model,
prompt_tokens = prompt_tokens,
})
end
return request

View File

@@ -0,0 +1,33 @@
local state = require("codetyper.state.state")
local log = require("codetyper.adapters.nvim.ui.logs.log")
--- Log API response with token usage
---@param prompt_tokens number Tokens used in prompt
---@param response_tokens number Tokens in response
---@param stop_reason? string Why the response stopped
local function response(prompt_tokens, response_tokens, stop_reason)
state.total_prompt_tokens = state.total_prompt_tokens + prompt_tokens
state.total_response_tokens = state.total_response_tokens + response_tokens
local message = string.format(
"Tokens: %d in / %d out | Total: %d in / %d out",
prompt_tokens,
response_tokens,
state.total_prompt_tokens,
state.total_response_tokens
)
if stop_reason then
message = message .. " | Stop: " .. stop_reason
end
log("response", message, {
prompt_tokens = prompt_tokens,
response_tokens = response_tokens,
total_prompt = state.total_prompt_tokens,
total_response = state.total_response_tokens,
stop_reason = stop_reason,
})
end
return response

View File

@@ -0,0 +1,14 @@
local log = require("codetyper.adapters.nvim.ui.logs.log")
--- Log a task/step that's in progress
---@param task_name string Task name
---@param status string|nil Status message
local function task(task_name, status)
local message = task_name
if status then
message = message .. " " .. status
end
log("task", message)
end
return task

View File

@@ -0,0 +1,13 @@
local log = require("codetyper.adapters.nvim.ui.logs.log")
--- Log task completion
---@param next_task? string Next task
local function task_done(next_task)
local message = " ⎿ Done"
if next_task then
message = message .. "\n" .. next_task
end
log("result", message)
end
return task_done

View File

@@ -0,0 +1,9 @@
local log = require("codetyper.adapters.nvim.ui.logs.log")
--- Log thinking/reasoning step
---@param step string Description of what's happening
local function thinking(step)
log("thinking", step)
end
return thinking

View File

@@ -0,0 +1,23 @@
local log = require("codetyper.adapters.nvim.ui.logs.log")
local params = require("codetyper.params.agents.logs")
--- Log tool execution
---@param tool_name string Name of the tool
---@param status string "start" | "success" | "error" | "approval"
---@param details? string Additional details
local function tool(tool_name, status, details)
local icons = params.icons
local message = string.format("[%s] %s", icons[status] or status, tool_name)
if details then
message = message .. ": " .. details
end
log("tool", message, {
tool = tool_name,
status = status,
details = details,
})
end
return tool

View File

@@ -0,0 +1,24 @@
local log = require("codetyper.adapters.nvim.ui.logs.log")
--- Log update/edit operation
---@param filepath string Path of file being edited
---@param added? number Lines added
---@param removed? number Lines removed
local function update(filepath, added, removed)
local message = string.format("Update(%s)", vim.fn.fnamemodify(filepath, ":~:."))
if added or removed then
local parts = {}
if added and added > 0 then
table.insert(parts, string.format("Added %d lines", added))
end
if removed and removed > 0 then
table.insert(parts, string.format("Removed %d lines", removed))
end
if #parts > 0 then
message = message .. "\n" .. table.concat(parts, ", ")
end
end
log("action", message)
end
return update

View File

@@ -0,0 +1,10 @@
local log = require("codetyper.adapters.nvim.ui.logs.log")
--- Log warning message
---@param message string
---@param data? table
local function warning(message, data)
log("warning", "WARN: " .. message, data)
end
return warning

View File

@@ -12,6 +12,11 @@ local state = {
diff_buf = nil,
diff_win = nil,
is_open = false,
listeners = {},
total_prompt_tokens = 0,
total_response_tokens = 0,
current_provider = nil,
current_model = nil,
}
return state

View File

@@ -0,0 +1,8 @@
--- Estimate token count for a string (rough approximation ~4 chars per token)
---@param text string
---@return number
local function estimate_tokens(text)
return math.ceil(#text / 4)
end
return estimate_tokens

View File

@@ -0,0 +1,7 @@
--- Get current timestamp formatted as HH:MM:SS
---@return string
local function get_timestamp()
return os.date("%H:%M:%S")
end
return get_timestamp