feat: add real-time logs panel for /@ @/ code generation

- Add logs_panel.lua module for standalone logs display
- Add logging to generate() functions in claude.lua and ollama.lua
- Show logs panel automatically when running transform commands
- Log request/response with token counting for both providers
- Add :CoderLogs command to toggle logs panel manually
- Clean up duplicate generate_with_tools function in claude.lua

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-13 20:54:26 -05:00
parent 2989fb5f14
commit 73c56d2f6d
4 changed files with 496 additions and 186 deletions

View File

@@ -48,11 +48,11 @@ end
--- Make HTTP request to Claude API
---@param body table Request body
---@param callback fun(response: string|nil, error: string|nil) Callback function
---@param callback fun(response: string|nil, error: string|nil, usage: table|nil) Callback function
local function make_request(body, callback)
local api_key = get_api_key()
if not api_key then
callback(nil, "Claude API key not configured")
callback(nil, "Claude API key not configured", nil)
return
end
@@ -87,40 +87,43 @@ local function make_request(body, callback)
if not ok then
vim.schedule(function()
callback(nil, "Failed to parse Claude response")
callback(nil, "Failed to parse Claude response", nil)
end)
return
end
if response.error then
vim.schedule(function()
callback(nil, response.error.message or "Claude API error")
callback(nil, response.error.message or "Claude API error", nil)
end)
return
end
-- Extract usage info
local usage = response.usage or {}
if response.content and response.content[1] and response.content[1].text then
local code = llm.extract_code(response.content[1].text)
vim.schedule(function()
callback(code, nil)
callback(code, nil, usage)
end)
else
vim.schedule(function()
callback(nil, "No content in Claude response")
callback(nil, "No content in Claude response", nil)
end)
end
end,
on_stderr = function(_, data)
if data and #data > 0 and data[1] ~= "" then
vim.schedule(function()
callback(nil, "Claude API request failed: " .. table.concat(data, "\n"))
callback(nil, "Claude API request failed: " .. table.concat(data, "\n"), nil)
end)
end
end,
on_exit = function(_, code)
if code ~= 0 then
vim.schedule(function()
callback(nil, "Claude API request failed with code: " .. code)
callback(nil, "Claude API request failed with code: " .. code, nil)
end)
end
end,
@@ -132,14 +135,38 @@ end
---@param context table Context information
---@param callback fun(response: string|nil, error: string|nil) Callback function
function M.generate(prompt, context, callback)
utils.notify("Sending request to Claude...", vim.log.levels.INFO)
local logs = require("codetyper.agent.logs")
local model = get_model()
-- Log the request
logs.request("claude", model)
logs.thinking("Building request body...")
local body = build_request_body(prompt, context)
make_request(body, function(response, err)
-- Estimate prompt tokens
local prompt_estimate = logs.estimate_tokens(vim.json.encode(body))
logs.debug(string.format("Estimated prompt: ~%d tokens", prompt_estimate))
logs.thinking("Sending to Claude API...")
utils.notify("Sending request to Claude...", vim.log.levels.INFO)
make_request(body, function(response, err, usage)
if err then
logs.error(err)
utils.notify(err, vim.log.levels.ERROR)
callback(nil, err)
else
-- Log token usage
if usage then
logs.response(
usage.input_tokens or 0,
usage.output_tokens or 0,
"end_turn"
)
end
logs.thinking("Response received, extracting code...")
logs.info("Code generated successfully")
utils.notify("Code generated successfully", vim.log.levels.INFO)
callback(response, nil)
end
@@ -156,172 +183,185 @@ function M.validate()
return true
end
--- Build request body for Claude API with tools
---@param messages table[] Conversation messages
---@param context table Context information
---@param tools table Tool definitions
---@return table Request body
local function build_tools_request_body(messages, context, tools)
local agent_prompts = require("codetyper.prompts.agent")
local tools_module = require("codetyper.agent.tools")
-- Build system prompt for agent mode
local system_prompt = agent_prompts.system
-- Add context about current file if available
if context.file_path then
system_prompt = system_prompt .. "\n\nCurrent working context:\n"
system_prompt = system_prompt .. "- File: " .. context.file_path .. "\n"
if context.language then
system_prompt = system_prompt .. "- Language: " .. context.language .. "\n"
end
end
-- Add project root info
local utils = require("codetyper.utils")
local root = utils.get_project_root()
if root then
system_prompt = system_prompt .. "- Project root: " .. root .. "\n"
end
return {
model = get_model(),
max_tokens = 4096,
system = system_prompt,
messages = messages,
tools = tools_module.to_claude_format(),
}
end
--- Make HTTP request to Claude API for tool use
---@param body table Request body
---@param callback fun(response: table|nil, error: string|nil) Callback function
local function make_tools_request(body, callback)
local api_key = get_api_key()
if not api_key then
callback(nil, "Claude API key not configured")
return
end
local json_body = vim.json.encode(body)
local cmd = {
"curl",
"-s",
"-X",
"POST",
API_URL,
"-H",
"Content-Type: application/json",
"-H",
"x-api-key: " .. api_key,
"-H",
"anthropic-version: 2023-06-01",
"-d",
json_body,
}
vim.fn.jobstart(cmd, {
stdout_buffered = true,
on_stdout = function(_, data)
if not data or #data == 0 or (data[1] == "" and #data == 1) then
return
end
local response_text = table.concat(data, "\n")
local ok, response = pcall(vim.json.decode, response_text)
if not ok then
vim.schedule(function()
callback(nil, "Failed to parse Claude response")
end)
return
end
if response.error then
vim.schedule(function()
callback(nil, response.error.message or "Claude API error")
end)
return
end
-- Return the full response for tool parsing
vim.schedule(function()
callback(response, nil)
end)
end,
on_stderr = function(_, data)
if data and #data > 0 and data[1] ~= "" then
vim.schedule(function()
callback(nil, "Claude API request failed: " .. table.concat(data, "\n"))
end)
end
end,
on_exit = function(_, code)
if code ~= 0 then
-- Only report if no response was received
-- (curl may return non-zero even with successful response in some cases)
end
end,
})
end
--- Generate response with tools using Claude API
--- Generate with tool use support for agentic mode
---@param messages table[] Conversation history
---@param context table Context information
---@param tools table Tool definitions (ignored, we use our own)
---@param callback fun(response: table|nil, error: string|nil) Callback function
function M.generate_with_tools(messages, context, tools, callback)
local logs = require("codetyper.agent.logs")
---@param tool_definitions table Tool definitions
---@param callback fun(response: table|nil, error: string|nil) Callback with raw response
function M.generate_with_tools(messages, context, tool_definitions, callback)
local logs = require("codetyper.agent.logs")
local model = get_model()
-- Log the request
local model = get_model()
logs.request("claude", model)
logs.thinking("Preparing API request...")
-- Log the request
logs.request("claude", model)
logs.thinking("Preparing agent request...")
local body = build_tools_request_body(messages, context, tools)
local api_key = get_api_key()
if not api_key then
logs.error("Claude API key not configured")
callback(nil, "Claude API key not configured")
return
end
-- Estimate prompt tokens
local prompt_estimate = logs.estimate_tokens(vim.json.encode(body))
logs.debug(string.format("Estimated prompt: ~%d tokens", prompt_estimate))
local tools_module = require("codetyper.agent.tools")
local agent_prompts = require("codetyper.prompts.agent")
make_tools_request(body, function(response, err)
if err then
logs.error(err)
callback(nil, err)
else
-- Log token usage from response
if response and response.usage then
logs.response(
response.usage.input_tokens or 0,
response.usage.output_tokens or 0,
response.stop_reason
)
end
-- Build system prompt with agent instructions
local system_prompt = llm.build_system_prompt(context)
system_prompt = system_prompt .. "\n\n" .. agent_prompts.system
system_prompt = system_prompt .. "\n\n" .. agent_prompts.tool_instructions
-- Log what's in the response
if response and response.content then
local has_text = false
local has_tools = false
for _, block in ipairs(response.content) do
if block.type == "text" then
has_text = true
elseif block.type == "tool_use" then
has_tools = true
logs.thinking("Tool call: " .. block.name)
end
end
if has_text then
logs.thinking("Response contains text")
end
if has_tools then
logs.thinking("Response contains tool calls")
end
end
-- Build request body with tools
local body = {
model = get_model(),
max_tokens = 4096,
system = system_prompt,
messages = M.format_messages_for_claude(messages),
tools = tools_module.to_claude_format(),
}
callback(response, nil)
end
end)
local json_body = vim.json.encode(body)
-- Estimate prompt tokens
local prompt_estimate = logs.estimate_tokens(json_body)
logs.debug(string.format("Estimated prompt: ~%d tokens", prompt_estimate))
logs.thinking("Sending to Claude API...")
local cmd = {
"curl",
"-s",
"-X", "POST",
API_URL,
"-H", "Content-Type: application/json",
"-H", "x-api-key: " .. api_key,
"-H", "anthropic-version: 2023-06-01",
"-d", json_body,
}
vim.fn.jobstart(cmd, {
stdout_buffered = true,
on_stdout = function(_, data)
if not data or #data == 0 or (data[1] == "" and #data == 1) then
return
end
local response_text = table.concat(data, "\n")
local ok, response = pcall(vim.json.decode, response_text)
if not ok then
vim.schedule(function()
logs.error("Failed to parse Claude response")
callback(nil, "Failed to parse Claude response")
end)
return
end
if response.error then
vim.schedule(function()
logs.error(response.error.message or "Claude API error")
callback(nil, response.error.message or "Claude API error")
end)
return
end
-- Log token usage from response
if response.usage then
logs.response(
response.usage.input_tokens or 0,
response.usage.output_tokens or 0,
response.stop_reason
)
end
-- Log what's in the response
if response.content then
for _, block in ipairs(response.content) do
if block.type == "text" then
logs.thinking("Response contains text")
elseif block.type == "tool_use" then
logs.thinking("Tool call: " .. block.name)
end
end
end
-- Return raw response for parser to handle
vim.schedule(function()
callback(response, nil)
end)
end,
on_stderr = function(_, data)
if data and #data > 0 and data[1] ~= "" then
vim.schedule(function()
logs.error("Claude API request failed: " .. table.concat(data, "\n"))
callback(nil, "Claude API request failed: " .. table.concat(data, "\n"))
end)
end
end,
on_exit = function(_, code)
if code ~= 0 then
vim.schedule(function()
logs.error("Claude API request failed with code: " .. code)
callback(nil, "Claude API request failed with code: " .. code)
end)
end
end,
})
end
--- Format messages for Claude API
---@param messages table[] Internal message format
---@return table[] Claude API message format
function M.format_messages_for_claude(messages)
local formatted = {}
for _, msg in ipairs(messages) do
if msg.role == "user" then
if type(msg.content) == "table" then
-- Tool results
table.insert(formatted, {
role = "user",
content = msg.content,
})
else
table.insert(formatted, {
role = "user",
content = msg.content,
})
end
elseif msg.role == "assistant" then
-- Build content array for assistant messages
local content = {}
-- Add text if present
if msg.content and msg.content ~= "" then
table.insert(content, {
type = "text",
text = msg.content,
})
end
-- Add tool uses if present
if msg.tool_calls then
for _, tool_call in ipairs(msg.tool_calls) do
table.insert(content, {
type = "tool_use",
id = tool_call.id,
name = tool_call.name,
input = tool_call.parameters,
})
end
end
if #content > 0 then
table.insert(formatted, {
role = "assistant",
content = content,
})
end
end
end
return formatted
end
return M

View File

@@ -44,7 +44,7 @@ end
--- Make HTTP request to Ollama API
---@param body table Request body
---@param callback fun(response: string|nil, error: string|nil) Callback function
---@param callback fun(response: string|nil, error: string|nil, usage: table|nil) Callback function
local function make_request(body, callback)
local host = get_host()
local url = host .. "/api/generate"
@@ -71,40 +71,46 @@ local function make_request(body, callback)
if not ok then
vim.schedule(function()
callback(nil, "Failed to parse Ollama response")
callback(nil, "Failed to parse Ollama response", nil)
end)
return
end
if response.error then
vim.schedule(function()
callback(nil, response.error or "Ollama API error")
callback(nil, response.error or "Ollama API error", nil)
end)
return
end
-- Extract usage info
local usage = {
prompt_tokens = response.prompt_eval_count or 0,
response_tokens = response.eval_count or 0,
}
if response.response then
local code = llm.extract_code(response.response)
vim.schedule(function()
callback(code, nil)
callback(code, nil, usage)
end)
else
vim.schedule(function()
callback(nil, "No response from Ollama")
callback(nil, "No response from Ollama", nil)
end)
end
end,
on_stderr = function(_, data)
if data and #data > 0 and data[1] ~= "" then
vim.schedule(function()
callback(nil, "Ollama API request failed: " .. table.concat(data, "\n"))
callback(nil, "Ollama API request failed: " .. table.concat(data, "\n"), nil)
end)
end
end,
on_exit = function(_, code)
if code ~= 0 then
vim.schedule(function()
callback(nil, "Ollama API request failed with code: " .. code)
callback(nil, "Ollama API request failed with code: " .. code, nil)
end)
end
end,
@@ -116,14 +122,38 @@ end
---@param context table Context information
---@param callback fun(response: string|nil, error: string|nil) Callback function
function M.generate(prompt, context, callback)
utils.notify("Sending request to Ollama...", vim.log.levels.INFO)
local logs = require("codetyper.agent.logs")
local model = get_model()
-- Log the request
logs.request("ollama", model)
logs.thinking("Building request body...")
local body = build_request_body(prompt, context)
make_request(body, function(response, err)
-- Estimate prompt tokens
local prompt_estimate = logs.estimate_tokens(vim.json.encode(body))
logs.debug(string.format("Estimated prompt: ~%d tokens", prompt_estimate))
logs.thinking("Sending to Ollama API...")
utils.notify("Sending request to Ollama...", vim.log.levels.INFO)
make_request(body, function(response, err, usage)
if err then
logs.error(err)
utils.notify(err, vim.log.levels.ERROR)
callback(nil, err)
else
-- Log token usage
if usage then
logs.response(
usage.prompt_tokens or 0,
usage.response_tokens or 0,
"end_turn"
)
end
logs.thinking("Response received, extracting code...")
logs.info("Code generated successfully")
utils.notify("Code generated successfully", vim.log.levels.INFO)
callback(response, nil)
end