From 73c56d2f6dffbcce555c577c01e8fe33f456ddc1 Mon Sep 17 00:00:00 2001 From: Carlos Gutierrez Date: Tue, 13 Jan 2026 20:54:26 -0500 Subject: [PATCH] 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 --- lua/codetyper/commands.lua | 53 ++++- lua/codetyper/llm/claude.lua | 376 +++++++++++++++++++---------------- lua/codetyper/llm/ollama.lua | 48 ++++- lua/codetyper/logs_panel.lua | 205 +++++++++++++++++++ 4 files changed, 496 insertions(+), 186 deletions(-) create mode 100644 lua/codetyper/logs_panel.lua diff --git a/lua/codetyper/commands.lua b/lua/codetyper/commands.lua index e62767c..0331882 100644 --- a/lua/codetyper/commands.lua +++ b/lua/codetyper/commands.lua @@ -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 diff --git a/lua/codetyper/llm/claude.lua b/lua/codetyper/llm/claude.lua index d899d68..707a536 100644 --- a/lua/codetyper/llm/claude.lua +++ b/lua/codetyper/llm/claude.lua @@ -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 diff --git a/lua/codetyper/llm/ollama.lua b/lua/codetyper/llm/ollama.lua index 76ec6d0..b0defdd 100644 --- a/lua/codetyper/llm/ollama.lua +++ b/lua/codetyper/llm/ollama.lua @@ -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 diff --git a/lua/codetyper/logs_panel.lua b/lua/codetyper/logs_panel.lua new file mode 100644 index 0000000..501eca4 --- /dev/null +++ b/lua/codetyper/logs_panel.lua @@ -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", "", 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