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:
@@ -287,6 +287,12 @@ local function cmd_type_toggle()
|
||||
switcher.show()
|
||||
end
|
||||
|
||||
--- Toggle logs panel
|
||||
local function cmd_logs_toggle()
|
||||
local logs_panel = require("codetyper.logs_panel")
|
||||
logs_panel.toggle()
|
||||
end
|
||||
|
||||
--- Switch focus between coder and target windows
|
||||
local function cmd_focus()
|
||||
if not window.is_open() then
|
||||
@@ -307,6 +313,8 @@ end
|
||||
local function cmd_transform()
|
||||
local parser = require("codetyper.parser")
|
||||
local llm = require("codetyper.llm")
|
||||
local logs_panel = require("codetyper.logs_panel")
|
||||
local logs = require("codetyper.agent.logs")
|
||||
|
||||
local bufnr = vim.api.nvim_get_current_buf()
|
||||
local filepath = vim.fn.expand("%:p")
|
||||
@@ -324,6 +332,10 @@ local function cmd_transform()
|
||||
return
|
||||
end
|
||||
|
||||
-- Open the logs panel to show generation progress
|
||||
logs_panel.open()
|
||||
logs.info("Transform started: " .. #prompts .. " prompt(s)")
|
||||
|
||||
utils.notify("Found " .. #prompts .. " prompt(s) to transform...", vim.log.levels.INFO)
|
||||
|
||||
-- Build context for this file
|
||||
@@ -355,11 +367,13 @@ local function cmd_transform()
|
||||
enhanced_prompt = enhanced_prompt .. "- Match the coding style of the existing file exactly\n"
|
||||
enhanced_prompt = enhanced_prompt .. "- Output must be ready to insert directly into the file\n"
|
||||
|
||||
logs.info("Processing: " .. clean_prompt:sub(1, 40) .. "...")
|
||||
utils.notify("Processing: " .. clean_prompt:sub(1, 40) .. "...", vim.log.levels.INFO)
|
||||
|
||||
-- Generate code for this prompt
|
||||
llm.generate(enhanced_prompt, context, function(response, err)
|
||||
if err then
|
||||
logs.error("Failed: " .. err)
|
||||
utils.notify("Failed: " .. err, vim.log.levels.ERROR)
|
||||
errors = errors + 1
|
||||
elseif response then
|
||||
@@ -411,10 +425,9 @@ local function cmd_transform()
|
||||
|
||||
completed = completed + 1
|
||||
if completed + errors >= pending then
|
||||
utils.notify(
|
||||
"Transform complete: " .. completed .. " succeeded, " .. errors .. " failed",
|
||||
errors > 0 and vim.log.levels.WARN or vim.log.levels.INFO
|
||||
)
|
||||
local msg = "Transform complete: " .. completed .. " succeeded, " .. errors .. " failed"
|
||||
logs.info(msg)
|
||||
utils.notify(msg, errors > 0 and vim.log.levels.WARN or vim.log.levels.INFO)
|
||||
end
|
||||
end)
|
||||
end
|
||||
@@ -428,6 +441,8 @@ end
|
||||
local function cmd_transform_range(start_line, end_line)
|
||||
local parser = require("codetyper.parser")
|
||||
local llm = require("codetyper.llm")
|
||||
local logs_panel = require("codetyper.logs_panel")
|
||||
local logs = require("codetyper.agent.logs")
|
||||
|
||||
local bufnr = vim.api.nvim_get_current_buf()
|
||||
local filepath = vim.fn.expand("%:p")
|
||||
@@ -453,6 +468,10 @@ local function cmd_transform_range(start_line, end_line)
|
||||
return
|
||||
end
|
||||
|
||||
-- Open the logs panel to show generation progress
|
||||
logs_panel.open()
|
||||
logs.info("Transform selection: " .. #prompts .. " prompt(s)")
|
||||
|
||||
utils.notify("Found " .. #prompts .. " prompt(s) in selection to transform...", vim.log.levels.INFO)
|
||||
|
||||
-- Build context for this file
|
||||
@@ -479,10 +498,12 @@ local function cmd_transform_range(start_line, end_line)
|
||||
enhanced_prompt = enhanced_prompt .. "- Match the coding style of the existing file exactly\n"
|
||||
enhanced_prompt = enhanced_prompt .. "- Output must be ready to insert directly into the file\n"
|
||||
|
||||
logs.info("Processing: " .. clean_prompt:sub(1, 40) .. "...")
|
||||
utils.notify("Processing: " .. clean_prompt:sub(1, 40) .. "...", vim.log.levels.INFO)
|
||||
|
||||
llm.generate(enhanced_prompt, context, function(response, err)
|
||||
if err then
|
||||
logs.error("Failed: " .. err)
|
||||
utils.notify("Failed: " .. err, vim.log.levels.ERROR)
|
||||
errors = errors + 1
|
||||
elseif response then
|
||||
@@ -525,10 +546,9 @@ local function cmd_transform_range(start_line, end_line)
|
||||
|
||||
completed = completed + 1
|
||||
if completed + errors >= pending then
|
||||
utils.notify(
|
||||
"Transform complete: " .. completed .. " succeeded, " .. errors .. " failed",
|
||||
errors > 0 and vim.log.levels.WARN or vim.log.levels.INFO
|
||||
)
|
||||
local msg = "Transform complete: " .. completed .. " succeeded, " .. errors .. " failed"
|
||||
logs.info(msg)
|
||||
utils.notify(msg, errors > 0 and vim.log.levels.WARN or vim.log.levels.INFO)
|
||||
end
|
||||
end)
|
||||
end
|
||||
@@ -548,6 +568,8 @@ end
|
||||
local function cmd_transform_at_cursor()
|
||||
local parser = require("codetyper.parser")
|
||||
local llm = require("codetyper.llm")
|
||||
local logs_panel = require("codetyper.logs_panel")
|
||||
local logs = require("codetyper.agent.logs")
|
||||
|
||||
local bufnr = vim.api.nvim_get_current_buf()
|
||||
local filepath = vim.fn.expand("%:p")
|
||||
@@ -565,9 +587,14 @@ local function cmd_transform_at_cursor()
|
||||
return
|
||||
end
|
||||
|
||||
-- Open the logs panel to show generation progress
|
||||
logs_panel.open()
|
||||
|
||||
local clean_prompt = parser.clean_prompt(prompt.content)
|
||||
local context = llm.build_context(filepath, "code_generation")
|
||||
|
||||
logs.info("Transform cursor: " .. clean_prompt:sub(1, 40) .. "...")
|
||||
|
||||
-- Build enhanced user prompt
|
||||
local enhanced_prompt = "TASK: " .. clean_prompt .. "\n\n"
|
||||
enhanced_prompt = enhanced_prompt .. "REQUIREMENTS:\n"
|
||||
@@ -581,6 +608,7 @@ local function cmd_transform_at_cursor()
|
||||
|
||||
llm.generate(enhanced_prompt, context, function(response, err)
|
||||
if err then
|
||||
logs.error("Transform failed: " .. err)
|
||||
utils.notify("Transform failed: " .. err, vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
@@ -622,6 +650,7 @@ local function cmd_transform_at_cursor()
|
||||
end
|
||||
|
||||
vim.api.nvim_buf_set_lines(bufnr, start_line - 1, end_line, false, replacement_lines)
|
||||
logs.info("Transform complete!")
|
||||
utils.notify("Transform complete!", vim.log.levels.INFO)
|
||||
end)
|
||||
end
|
||||
@@ -655,6 +684,7 @@ local function coder_cmd(args)
|
||||
["agent-toggle"] = cmd_agent_toggle,
|
||||
["agent-stop"] = cmd_agent_stop,
|
||||
["type-toggle"] = cmd_type_toggle,
|
||||
["logs-toggle"] = cmd_logs_toggle,
|
||||
}
|
||||
|
||||
local cmd_fn = commands[subcommand]
|
||||
@@ -676,7 +706,7 @@ function M.setup()
|
||||
"ask", "ask-close", "ask-toggle", "ask-clear",
|
||||
"transform", "transform-cursor",
|
||||
"agent", "agent-close", "agent-toggle", "agent-stop",
|
||||
"type-toggle",
|
||||
"type-toggle", "logs-toggle",
|
||||
}
|
||||
end,
|
||||
desc = "Codetyper.nvim commands",
|
||||
@@ -753,6 +783,11 @@ function M.setup()
|
||||
cmd_type_toggle()
|
||||
end, { desc = "Show Ask/Agent mode switcher" })
|
||||
|
||||
-- Logs panel command
|
||||
vim.api.nvim_create_user_command("CoderLogs", function()
|
||||
cmd_logs_toggle()
|
||||
end, { desc = "Toggle logs panel" })
|
||||
|
||||
-- Setup default keymaps
|
||||
M.setup_keymaps()
|
||||
end
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
205
lua/codetyper/logs_panel.lua
Normal file
205
lua/codetyper/logs_panel.lua
Normal file
@@ -0,0 +1,205 @@
|
||||
---@mod codetyper.logs_panel Standalone logs panel for code generation
|
||||
---
|
||||
--- Shows real-time logs when generating code via /@ @/ prompts.
|
||||
|
||||
local M = {}
|
||||
|
||||
local logs = require("codetyper.agent.logs")
|
||||
|
||||
---@class LogsPanelState
|
||||
---@field buf number|nil Buffer
|
||||
---@field win number|nil Window
|
||||
---@field is_open boolean Whether the panel is open
|
||||
---@field listener_id number|nil Listener ID for logs
|
||||
|
||||
local state = {
|
||||
buf = nil,
|
||||
win = nil,
|
||||
is_open = false,
|
||||
listener_id = nil,
|
||||
}
|
||||
|
||||
--- Namespace for highlights
|
||||
local ns_logs = vim.api.nvim_create_namespace("codetyper_logs_panel")
|
||||
|
||||
--- Fixed width
|
||||
local LOGS_WIDTH = 60
|
||||
|
||||
--- Add a log entry to the buffer
|
||||
---@param entry table Log entry
|
||||
local function add_log_entry(entry)
|
||||
if not state.buf or not vim.api.nvim_buf_is_valid(state.buf) then
|
||||
return
|
||||
end
|
||||
|
||||
vim.schedule(function()
|
||||
if not state.buf or not vim.api.nvim_buf_is_valid(state.buf) then
|
||||
return
|
||||
end
|
||||
|
||||
-- Handle clear event
|
||||
if entry.level == "clear" then
|
||||
vim.bo[state.buf].modifiable = true
|
||||
vim.api.nvim_buf_set_lines(state.buf, 0, -1, false, {
|
||||
"Generation Logs",
|
||||
string.rep("─", LOGS_WIDTH - 2),
|
||||
"",
|
||||
})
|
||||
vim.bo[state.buf].modifiable = false
|
||||
return
|
||||
end
|
||||
|
||||
vim.bo[state.buf].modifiable = true
|
||||
|
||||
local formatted = logs.format_entry(entry)
|
||||
local lines = vim.api.nvim_buf_get_lines(state.buf, 0, -1, false)
|
||||
local line_num = #lines
|
||||
|
||||
vim.api.nvim_buf_set_lines(state.buf, -1, -1, false, { formatted })
|
||||
|
||||
-- Apply highlighting based on level
|
||||
local hl_map = {
|
||||
info = "DiagnosticInfo",
|
||||
debug = "Comment",
|
||||
request = "DiagnosticWarn",
|
||||
response = "DiagnosticOk",
|
||||
tool = "DiagnosticHint",
|
||||
error = "DiagnosticError",
|
||||
}
|
||||
|
||||
local hl = hl_map[entry.level] or "Normal"
|
||||
vim.api.nvim_buf_add_highlight(state.buf, ns_logs, hl, line_num, 0, -1)
|
||||
|
||||
vim.bo[state.buf].modifiable = false
|
||||
|
||||
-- Auto-scroll logs
|
||||
if state.win and vim.api.nvim_win_is_valid(state.win) then
|
||||
local new_count = vim.api.nvim_buf_line_count(state.buf)
|
||||
pcall(vim.api.nvim_win_set_cursor, state.win, { new_count, 0 })
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
--- Update the title with token counts
|
||||
local function update_title()
|
||||
if not state.win or not vim.api.nvim_win_is_valid(state.win) then
|
||||
return
|
||||
end
|
||||
|
||||
local prompt_tokens, response_tokens = logs.get_token_totals()
|
||||
local provider, model = logs.get_provider_info()
|
||||
|
||||
if provider and state.buf and vim.api.nvim_buf_is_valid(state.buf) then
|
||||
vim.bo[state.buf].modifiable = true
|
||||
local title = string.format("%s | %d/%d tokens", (provider or ""):upper(), prompt_tokens, response_tokens)
|
||||
vim.api.nvim_buf_set_lines(state.buf, 0, 1, false, { title })
|
||||
vim.bo[state.buf].modifiable = false
|
||||
end
|
||||
end
|
||||
|
||||
--- Open the logs panel
|
||||
function M.open()
|
||||
if state.is_open then
|
||||
return
|
||||
end
|
||||
|
||||
-- Clear previous logs
|
||||
logs.clear()
|
||||
|
||||
-- Create buffer
|
||||
state.buf = vim.api.nvim_create_buf(false, true)
|
||||
vim.bo[state.buf].buftype = "nofile"
|
||||
vim.bo[state.buf].bufhidden = "hide"
|
||||
vim.bo[state.buf].swapfile = false
|
||||
|
||||
-- Create window on the right
|
||||
vim.cmd("botright vsplit")
|
||||
state.win = vim.api.nvim_get_current_win()
|
||||
vim.api.nvim_win_set_buf(state.win, state.buf)
|
||||
vim.api.nvim_win_set_width(state.win, LOGS_WIDTH)
|
||||
|
||||
-- Window options
|
||||
vim.wo[state.win].number = false
|
||||
vim.wo[state.win].relativenumber = false
|
||||
vim.wo[state.win].signcolumn = "no"
|
||||
vim.wo[state.win].wrap = true
|
||||
vim.wo[state.win].linebreak = true
|
||||
vim.wo[state.win].winfixwidth = true
|
||||
vim.wo[state.win].cursorline = false
|
||||
|
||||
-- Set initial content
|
||||
vim.bo[state.buf].modifiable = true
|
||||
vim.api.nvim_buf_set_lines(state.buf, 0, -1, false, {
|
||||
"Generation Logs",
|
||||
string.rep("─", LOGS_WIDTH - 2),
|
||||
"",
|
||||
})
|
||||
vim.bo[state.buf].modifiable = false
|
||||
|
||||
-- Setup keymaps
|
||||
local opts = { buffer = state.buf, noremap = true, silent = true }
|
||||
vim.keymap.set("n", "q", M.close, opts)
|
||||
vim.keymap.set("n", "<Esc>", M.close, opts)
|
||||
|
||||
-- Register log listener
|
||||
state.listener_id = logs.add_listener(function(entry)
|
||||
add_log_entry(entry)
|
||||
if entry.level == "response" then
|
||||
vim.schedule(update_title)
|
||||
end
|
||||
end)
|
||||
|
||||
state.is_open = true
|
||||
|
||||
-- Return focus to previous window
|
||||
vim.cmd("wincmd p")
|
||||
|
||||
logs.info("Logs panel opened")
|
||||
end
|
||||
|
||||
--- Close the logs panel
|
||||
function M.close()
|
||||
if not state.is_open then
|
||||
return
|
||||
end
|
||||
|
||||
-- Remove log listener
|
||||
if state.listener_id then
|
||||
logs.remove_listener(state.listener_id)
|
||||
state.listener_id = nil
|
||||
end
|
||||
|
||||
-- Close window
|
||||
if state.win and vim.api.nvim_win_is_valid(state.win) then
|
||||
pcall(vim.api.nvim_win_close, state.win, true)
|
||||
end
|
||||
|
||||
-- Reset state
|
||||
state.buf = nil
|
||||
state.win = nil
|
||||
state.is_open = false
|
||||
end
|
||||
|
||||
--- Toggle the logs panel
|
||||
function M.toggle()
|
||||
if state.is_open then
|
||||
M.close()
|
||||
else
|
||||
M.open()
|
||||
end
|
||||
end
|
||||
|
||||
--- Check if panel is open
|
||||
---@return boolean
|
||||
function M.is_open()
|
||||
return state.is_open
|
||||
end
|
||||
|
||||
--- Ensure panel is open (call before starting generation)
|
||||
function M.ensure_open()
|
||||
if not state.is_open then
|
||||
M.open()
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
Reference in New Issue
Block a user