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

@@ -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

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

View 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