From 9c99944f26702c836b794883011d5a1989bb5c87 Mon Sep 17 00:00:00 2001 From: Carlos Gutierrez Date: Sun, 11 Jan 2026 20:12:13 -0500 Subject: [PATCH] updating files --- .gitignore | 6 +- lua/codetyper/ask.lua | 1264 ++++++++++++++++--------------- lua/codetyper/prompts/agent.lua | 46 ++ scripts/create_support_files.sh | 25 - 4 files changed, 689 insertions(+), 652 deletions(-) create mode 100644 lua/codetyper/prompts/agent.lua delete mode 100644 scripts/create_support_files.sh diff --git a/.gitignore b/.gitignore index 9aadf8d..1a3cacc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ - - # Codetyper.nvim - AI coding partner files *.coder.* .coder/ + +/@ +add gitignore for lua files +/@ diff --git a/lua/codetyper/ask.lua b/lua/codetyper/ask.lua index 16669b3..056633f 100644 --- a/lua/codetyper/ask.lua +++ b/lua/codetyper/ask.lua @@ -15,226 +15,226 @@ local utils = require("codetyper.utils") ---@type AskState local state = { - input_buf = nil, - input_win = nil, - output_buf = nil, - output_win = nil, - is_open = false, - history = {}, - referenced_files = {}, - target_width = nil, -- Store the target width to maintain it + input_buf = nil, + input_win = nil, + output_buf = nil, + output_win = nil, + is_open = false, + history = {}, + referenced_files = {}, + target_width = nil, -- Store the target width to maintain it + agent_mode = false, -- Whether agent mode is enabled (can make file changes) } --- Get the ask window configuration ---@return table Config local function get_config() - local ok, codetyper = pcall(require, "codetyper") - if ok and codetyper.is_initialized() then - return codetyper.get_config() - end - return { - window = { width = 0.4, border = "rounded" }, - } + local ok, codetyper = pcall(require, "codetyper") + if ok and codetyper.is_initialized() then + return codetyper.get_config() + end + return { + window = { width = 0.4, border = "rounded" }, + } end --- Create the output buffer (chat history) ---@return number Buffer number local function create_output_buffer() - local buf = vim.api.nvim_create_buf(false, true) + local buf = vim.api.nvim_create_buf(false, true) - vim.bo[buf].buftype = "nofile" - vim.bo[buf].bufhidden = "hide" - vim.bo[buf].swapfile = false - vim.bo[buf].filetype = "markdown" + vim.bo[buf].buftype = "nofile" + vim.bo[buf].bufhidden = "hide" + vim.bo[buf].swapfile = false + vim.bo[buf].filetype = "markdown" - -- Set initial content - local header = { - "╔═══════════════════════════════════╗", - "║ 🤖 CODETYPER ASK ║", - "╠═══════════════════════════════════╣", - "║ Ask about code or concepts ║", - "║ ║", - "║ 💡 Keymaps: ║", - "║ @ → attach file ║", - "║ C-Enter → send ║", - "║ C-n → new chat ║", - "║ C-f → add current file ║", - "║ C-h/j/k/l → navigate ║", - "║ q → close │ K/J → jump ║", - "╚═══════════════════════════════════╝", - "", - } - vim.api.nvim_buf_set_lines(buf, 0, -1, false, header) + -- Set initial content + local header = { + "╔═════════════════════════════╗", + "║ 🤖 CODETYPER ASK ║", + "╠═════════════════════════════╣", + "║ Ask about code or concepts ║", + "║ ║", + "║ 💡 Keymaps: ║", + "║ @ → attach file ║", + "║ C-Enter → send ║", + "║ C-n → new chat ║", + "║ C-f → add current file ║", + "║ C-h/j/k/l → navigate ║", + "║ q → close │ K/J → jump ║", + "╚═════════════════════════════╝", + "", + } + vim.api.nvim_buf_set_lines(buf, 0, -1, false, header) - return buf + return buf end --- Create the input buffer ---@return number Buffer number local function create_input_buffer() - local buf = vim.api.nvim_create_buf(false, true) + local buf = vim.api.nvim_create_buf(false, true) - vim.bo[buf].buftype = "nofile" - vim.bo[buf].bufhidden = "hide" - vim.bo[buf].swapfile = false - vim.bo[buf].filetype = "markdown" + vim.bo[buf].buftype = "nofile" + vim.bo[buf].bufhidden = "hide" + vim.bo[buf].swapfile = false + vim.bo[buf].filetype = "markdown" - -- Set placeholder text - local placeholder = { - "┌───────────────────────────────────┐", - "│ 💬 Type your question here... │", - "│ │", - "│ @ attach │ C-Enter send │ C-n new │", - "└───────────────────────────────────┘", - } - vim.api.nvim_buf_set_lines(buf, 0, -1, false, placeholder) + -- Set placeholder text + local placeholder = { + "┌───────────────────────────────────┐", + "│ 💬 Type your question here... │", + "│ │", + "│ @ attach │ C-Enter send │ C-n new │", + "└───────────────────────────────────┘", + } + vim.api.nvim_buf_set_lines(buf, 0, -1, false, placeholder) - return buf + return buf end --- Setup keymaps for the input buffer ---@param buf number Buffer number local function setup_input_keymaps(buf) - local opts = { buffer = buf, noremap = true, silent = true } + local opts = { buffer = buf, noremap = true, silent = true } - -- Submit with Ctrl+Enter - vim.keymap.set("i", "", function() - M.submit() - end, opts) + -- Submit with Ctrl+Enter + vim.keymap.set("i", "", function() + M.submit() + end, opts) - vim.keymap.set("n", "", function() - M.submit() - end, opts) + vim.keymap.set("n", "", function() + M.submit() + end, opts) - vim.keymap.set("n", "", function() - M.submit() - end, opts) + vim.keymap.set("n", "", function() + M.submit() + end, opts) - -- Include current file context with Ctrl+F - vim.keymap.set({ "n", "i" }, "", function() - M.include_file_context() - end, opts) + -- Include current file context with Ctrl+F + vim.keymap.set({ "n", "i" }, "", function() + M.include_file_context() + end, opts) - -- File picker with @ - vim.keymap.set("i", "@", function() - M.show_file_picker() - end, opts) + -- File picker with @ + vim.keymap.set("i", "@", function() + M.show_file_picker() + end, opts) - -- Close with q in normal mode - vim.keymap.set("n", "q", function() - M.close() - end, opts) + -- Close with q in normal mode + vim.keymap.set("n", "q", function() + M.close() + end, opts) - -- Clear input with Ctrl+c - vim.keymap.set("n", "", function() - M.clear_input() - end, opts) + -- Clear input with Ctrl+c + vim.keymap.set("n", "", function() + M.clear_input() + end, opts) - -- New chat with Ctrl+n (clears everything) - vim.keymap.set({ "n", "i" }, "", function() - M.new_chat() - end, opts) + -- New chat with Ctrl+n (clears everything) + vim.keymap.set({ "n", "i" }, "", function() + M.new_chat() + end, opts) - -- Window navigation (works in both normal and insert mode) - vim.keymap.set({ "n", "i" }, "", function() - vim.cmd("wincmd h") - end, opts) + -- Window navigation (works in both normal and insert mode) + vim.keymap.set({ "n", "i" }, "", function() + vim.cmd("wincmd h") + end, opts) - vim.keymap.set({ "n", "i" }, "", function() - vim.cmd("wincmd j") - end, opts) + vim.keymap.set({ "n", "i" }, "", function() + vim.cmd("wincmd j") + end, opts) - vim.keymap.set({ "n", "i" }, "", function() - vim.cmd("wincmd k") - end, opts) + vim.keymap.set({ "n", "i" }, "", function() + vim.cmd("wincmd k") + end, opts) - vim.keymap.set({ "n", "i" }, "", function() - vim.cmd("wincmd l") - end, opts) + vim.keymap.set({ "n", "i" }, "", function() + vim.cmd("wincmd l") + end, opts) - -- Jump to output window - vim.keymap.set("n", "K", function() - M.focus_output() - end, opts) + -- Jump to output window + vim.keymap.set("n", "K", function() + M.focus_output() + end, opts) - -- When entering insert mode, clear placeholder - vim.api.nvim_create_autocmd("InsertEnter", { - buffer = buf, - callback = function() - local lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false) - local content = table.concat(lines, "\n") - if content:match("Type your question here") then - vim.api.nvim_buf_set_lines(buf, 0, -1, false, { "" }) - end - end, - }) + -- When entering insert mode, clear placeholder + vim.api.nvim_create_autocmd("InsertEnter", { + buffer = buf, + callback = function() + local lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false) + local content = table.concat(lines, "\n") + if content:match("Type your question here") then + vim.api.nvim_buf_set_lines(buf, 0, -1, false, { "" }) + end + end, + }) end --- Setup keymaps for the output buffer ---@param buf number Buffer number local function setup_output_keymaps(buf) - local opts = { buffer = buf, noremap = true, silent = true } + local opts = { buffer = buf, noremap = true, silent = true } - -- Close with q - vim.keymap.set("n", "q", function() - M.close() - end, opts) + -- Close with q + vim.keymap.set("n", "q", function() + M.close() + end, opts) - -- Clear history with Ctrl+c - vim.keymap.set("n", "", function() - M.clear_history() - end, opts) + -- Clear history with Ctrl+c + vim.keymap.set("n", "", function() + M.clear_history() + end, opts) - -- New chat with Ctrl+n (clears everything) - vim.keymap.set("n", "", function() - M.new_chat() - end, opts) + -- New chat with Ctrl+n (clears everything) + vim.keymap.set("n", "", function() + M.new_chat() + end, opts) - -- Copy last response with Y - vim.keymap.set("n", "Y", function() - M.copy_last_response() - end, opts) + -- Copy last response with Y + vim.keymap.set("n", "Y", function() + M.copy_last_response() + end, opts) - -- Jump to input with i or J - vim.keymap.set("n", "i", function() - M.focus_input() - end, opts) + -- Jump to input with i or J + vim.keymap.set("n", "i", function() + M.focus_input() + end, opts) - vim.keymap.set("n", "J", function() - M.focus_input() - end, opts) + vim.keymap.set("n", "J", function() + M.focus_input() + end, opts) - -- Window navigation - vim.keymap.set("n", "", function() - vim.cmd("wincmd h") - end, opts) + -- Window navigation + vim.keymap.set("n", "", function() + vim.cmd("wincmd h") + end, opts) - vim.keymap.set("n", "", function() - vim.cmd("wincmd j") - end, opts) + vim.keymap.set("n", "", function() + vim.cmd("wincmd j") + end, opts) - vim.keymap.set("n", "", function() - vim.cmd("wincmd k") - end, opts) - - vim.keymap.set("n", "", function() - vim.cmd("wincmd l") - end, opts) + vim.keymap.set("n", "", function() + vim.cmd("wincmd k") + end, opts) + vim.keymap.set("n", "", function() + vim.cmd("wincmd l") + end, opts) end --- Calculate window dimensions (always 1/4 of screen) ---@return table Dimensions local function calculate_dimensions() - -- Always use 1/4 of the screen width - local width = math.floor(vim.o.columns * 0.25) + -- Always use 1/4 of the screen width + local width = math.floor(vim.o.columns * 0.25) - return { - width = math.max(width, 30), -- Minimum 30 columns - total_height = vim.o.lines - 4, - output_height = vim.o.lines - 14, - input_height = 8, - } + return { + width = math.max(width, 30), -- Minimum 30 columns + total_height = vim.o.lines - 4, + output_height = vim.o.lines - 14, + input_height = 8, + } end --- Autocmd group for maintaining width @@ -242,625 +242,639 @@ local ask_augroup = nil --- Setup autocmd to always maintain 1/4 window width local function setup_width_autocmd() - -- Clear previous autocmd group if exists - if ask_augroup then - pcall(vim.api.nvim_del_augroup_by_id, ask_augroup) - end + -- Clear previous autocmd group if exists + if ask_augroup then + pcall(vim.api.nvim_del_augroup_by_id, ask_augroup) + end - ask_augroup = vim.api.nvim_create_augroup("CodetypeAskWidth", { clear = true }) + ask_augroup = vim.api.nvim_create_augroup("CodetypeAskWidth", { clear = true }) - -- Always maintain 1/4 width on any window event - vim.api.nvim_create_autocmd({ "WinResized", "WinNew", "WinClosed", "VimResized" }, { - group = ask_augroup, - callback = function() - if not state.is_open or not state.output_win then return end - if not vim.api.nvim_win_is_valid(state.output_win) then return end + -- Always maintain 1/4 width on any window event + vim.api.nvim_create_autocmd({ "WinResized", "WinNew", "WinClosed", "VimResized" }, { + group = ask_augroup, + callback = function() + if not state.is_open or not state.output_win then + return + end + if not vim.api.nvim_win_is_valid(state.output_win) then + return + end - vim.schedule(function() - if state.output_win and vim.api.nvim_win_is_valid(state.output_win) then - -- Always calculate 1/4 of current screen width - local target_width = math.max(math.floor(vim.o.columns * 0.25), 30) - state.target_width = target_width - - local current_width = vim.api.nvim_win_get_width(state.output_win) - if current_width ~= target_width then - pcall(vim.api.nvim_win_set_width, state.output_win, target_width) - end - end - end) - end, - desc = "Maintain Ask panel at 1/4 window width", - }) + vim.schedule(function() + if state.output_win and vim.api.nvim_win_is_valid(state.output_win) then + -- Always calculate 1/4 of current screen width + local target_width = math.max(math.floor(vim.o.columns * 0.25), 30) + state.target_width = target_width + + local current_width = vim.api.nvim_win_get_width(state.output_win) + if current_width ~= target_width then + pcall(vim.api.nvim_win_set_width, state.output_win, target_width) + end + end + end) + end, + desc = "Maintain Ask panel at 1/4 window width", + }) end --- Open the ask panel function M.open() - -- Use the is_open() function which validates window state - if M.is_open() then - M.focus_input() - return - end + -- Use the is_open() function which validates window state + if M.is_open() then + M.focus_input() + return + end - local dims = calculate_dimensions() + local dims = calculate_dimensions() - -- Store the target width - state.target_width = dims.width + -- Store the target width + state.target_width = dims.width - -- Create buffers if they don't exist - if not state.output_buf or not vim.api.nvim_buf_is_valid(state.output_buf) then - state.output_buf = create_output_buffer() - setup_output_keymaps(state.output_buf) - end + -- Create buffers if they don't exist + if not state.output_buf or not vim.api.nvim_buf_is_valid(state.output_buf) then + state.output_buf = create_output_buffer() + setup_output_keymaps(state.output_buf) + end - if not state.input_buf or not vim.api.nvim_buf_is_valid(state.input_buf) then - state.input_buf = create_input_buffer() - setup_input_keymaps(state.input_buf) - end + if not state.input_buf or not vim.api.nvim_buf_is_valid(state.input_buf) then + state.input_buf = create_input_buffer() + setup_input_keymaps(state.input_buf) + end - -- Save current window to return to it later - local current_win = vim.api.nvim_get_current_win() + -- Save current window to return to it later + local current_win = vim.api.nvim_get_current_win() - -- Create output window (top-left) - vim.cmd("topleft vsplit") - state.output_win = vim.api.nvim_get_current_win() - vim.api.nvim_win_set_buf(state.output_win, state.output_buf) - vim.api.nvim_win_set_width(state.output_win, dims.width) + -- Create output window (top-left) + vim.cmd("topleft vsplit") + state.output_win = vim.api.nvim_get_current_win() + vim.api.nvim_win_set_buf(state.output_win, state.output_buf) + vim.api.nvim_win_set_width(state.output_win, dims.width) - -- Window options for output - vim.wo[state.output_win].number = false - vim.wo[state.output_win].relativenumber = false - vim.wo[state.output_win].signcolumn = "no" - vim.wo[state.output_win].wrap = true - vim.wo[state.output_win].linebreak = true - vim.wo[state.output_win].cursorline = false - vim.wo[state.output_win].winfixwidth = true + -- Window options for output + vim.wo[state.output_win].number = false + vim.wo[state.output_win].relativenumber = false + vim.wo[state.output_win].signcolumn = "no" + vim.wo[state.output_win].wrap = true + vim.wo[state.output_win].linebreak = true + vim.wo[state.output_win].cursorline = false + vim.wo[state.output_win].winfixwidth = true - -- Create input window (bottom of the left panel) - vim.cmd("belowright split") - state.input_win = vim.api.nvim_get_current_win() - vim.api.nvim_win_set_buf(state.input_win, state.input_buf) - vim.api.nvim_win_set_height(state.input_win, dims.input_height) + -- Create input window (bottom of the left panel) + vim.cmd("belowright split") + state.input_win = vim.api.nvim_get_current_win() + vim.api.nvim_win_set_buf(state.input_win, state.input_buf) + vim.api.nvim_win_set_height(state.input_win, dims.input_height) - -- Window options for input - vim.wo[state.input_win].number = false - vim.wo[state.input_win].relativenumber = false - vim.wo[state.input_win].signcolumn = "no" - vim.wo[state.input_win].wrap = true - vim.wo[state.input_win].linebreak = true - vim.wo[state.input_win].winfixheight = true - vim.wo[state.input_win].winfixwidth = true + -- Window options for input + vim.wo[state.input_win].number = false + vim.wo[state.input_win].relativenumber = false + vim.wo[state.input_win].signcolumn = "no" + vim.wo[state.input_win].wrap = true + vim.wo[state.input_win].linebreak = true + vim.wo[state.input_win].winfixheight = true + vim.wo[state.input_win].winfixwidth = true - state.is_open = true + state.is_open = true - -- Setup autocmd to maintain width - setup_width_autocmd() + -- Setup autocmd to maintain width + setup_width_autocmd() - -- Setup autocmd to close both windows when one is closed - local close_group = vim.api.nvim_create_augroup("CodetypeAskClose", { clear = true }) - - vim.api.nvim_create_autocmd("WinClosed", { - group = close_group, - callback = function(args) - local closed_win = tonumber(args.match) - -- Check if one of our windows was closed - if closed_win == state.output_win or closed_win == state.input_win then - -- Defer to avoid issues during window close - vim.schedule(function() - -- Close the other window if it's still open - if closed_win == state.output_win then - if state.input_win and vim.api.nvim_win_is_valid(state.input_win) then - pcall(vim.api.nvim_win_close, state.input_win, true) - end - elseif closed_win == state.input_win then - if state.output_win and vim.api.nvim_win_is_valid(state.output_win) then - pcall(vim.api.nvim_win_close, state.output_win, true) - end - end - - -- Reset state - state.input_win = nil - state.output_win = nil - state.is_open = false - state.target_width = nil - - -- Clean up autocmd groups - pcall(vim.api.nvim_del_augroup_by_id, close_group) - if ask_augroup then - pcall(vim.api.nvim_del_augroup_by_id, ask_augroup) - ask_augroup = nil - end - end) - end - end, - desc = "Close both Ask windows together", - }) + -- Setup autocmd to close both windows when one is closed + local close_group = vim.api.nvim_create_augroup("CodetypeAskClose", { clear = true }) - -- Focus the input window and start insert mode - vim.api.nvim_set_current_win(state.input_win) - vim.cmd("startinsert") + vim.api.nvim_create_autocmd("WinClosed", { + group = close_group, + callback = function(args) + local closed_win = tonumber(args.match) + -- Check if one of our windows was closed + if closed_win == state.output_win or closed_win == state.input_win then + -- Defer to avoid issues during window close + vim.schedule(function() + -- Close the other window if it's still open + if closed_win == state.output_win then + if state.input_win and vim.api.nvim_win_is_valid(state.input_win) then + pcall(vim.api.nvim_win_close, state.input_win, true) + end + elseif closed_win == state.input_win then + if state.output_win and vim.api.nvim_win_is_valid(state.output_win) then + pcall(vim.api.nvim_win_close, state.output_win, true) + end + end + + -- Reset state + state.input_win = nil + state.output_win = nil + state.is_open = false + state.target_width = nil + + -- Clean up autocmd groups + pcall(vim.api.nvim_del_augroup_by_id, close_group) + if ask_augroup then + pcall(vim.api.nvim_del_augroup_by_id, ask_augroup) + ask_augroup = nil + end + end) + end + end, + desc = "Close both Ask windows together", + }) + + -- Focus the input window and start insert mode + vim.api.nvim_set_current_win(state.input_win) + vim.cmd("startinsert") end --- Show file picker for @ mentions function M.show_file_picker() - -- Check if telescope is available - local has_telescope, telescope = pcall(require, "telescope.builtin") + -- Check if telescope is available + local has_telescope, telescope = pcall(require, "telescope.builtin") - if has_telescope then - telescope.find_files({ - prompt_title = "Select file to reference (@)", - attach_mappings = function(prompt_bufnr, map) - local actions = require("telescope.actions") - local action_state = require("telescope.actions.state") + if has_telescope then + telescope.find_files({ + prompt_title = "Select file to reference (@)", + attach_mappings = function(prompt_bufnr, map) + local actions = require("telescope.actions") + local action_state = require("telescope.actions.state") - actions.select_default:replace(function() - actions.close(prompt_bufnr) - local selection = action_state.get_selected_entry() - if selection then - local filepath = selection.path or selection[1] - local filename = vim.fn.fnamemodify(filepath, ":t") - M.add_file_reference(filepath, filename) - end - end) - return true - end, - }) - else - -- Fallback: simple input - vim.ui.input({ prompt = "Enter file path: " }, function(input) - if input and input ~= "" then - local filepath = vim.fn.fnamemodify(input, ":p") - local filename = vim.fn.fnamemodify(filepath, ":t") - M.add_file_reference(filepath, filename) - end - end) - end + actions.select_default:replace(function() + actions.close(prompt_bufnr) + local selection = action_state.get_selected_entry() + if selection then + local filepath = selection.path or selection[1] + local filename = vim.fn.fnamemodify(filepath, ":t") + M.add_file_reference(filepath, filename) + end + end) + return true + end, + }) + else + -- Fallback: simple input + vim.ui.input({ prompt = "Enter file path: " }, function(input) + if input and input ~= "" then + local filepath = vim.fn.fnamemodify(input, ":p") + local filename = vim.fn.fnamemodify(filepath, ":t") + M.add_file_reference(filepath, filename) + end + end) + end end --- Add a file reference to the input ---@param filepath string Full path to the file ---@param filename string Display name function M.add_file_reference(filepath, filename) - -- Normalize filepath - filepath = vim.fn.fnamemodify(filepath, ":p") - - -- Store the reference with full path - state.referenced_files[filename] = filepath + -- Normalize filepath + filepath = vim.fn.fnamemodify(filepath, ":p") - -- Read and validate file exists - local content = utils.read_file(filepath) - if not content then - utils.notify("Warning: Could not read file: " .. filename, vim.log.levels.WARN) - end + -- Store the reference with full path + state.referenced_files[filename] = filepath - -- Add to input buffer - if state.input_buf and vim.api.nvim_buf_is_valid(state.input_buf) then - local lines = vim.api.nvim_buf_get_lines(state.input_buf, 0, -1, false) - local text = table.concat(lines, "\n") + -- Read and validate file exists + local content = utils.read_file(filepath) + if not content then + utils.notify("Warning: Could not read file: " .. filename, vim.log.levels.WARN) + end - -- Clear placeholder if present - if text:match("Type your question here") then - text = "" - end + -- Add to input buffer + if state.input_buf and vim.api.nvim_buf_is_valid(state.input_buf) then + local lines = vim.api.nvim_buf_get_lines(state.input_buf, 0, -1, false) + local text = table.concat(lines, "\n") - -- Add file reference (with single @) - local reference = "[📎 " .. filename .. "] " - text = text .. reference + -- Clear placeholder if present + if text:match("Type your question here") then + text = "" + end - vim.api.nvim_buf_set_lines(state.input_buf, 0, -1, false, vim.split(text, "\n")) + -- Add file reference (with single @) + local reference = "[📎 " .. filename .. "] " + text = text .. reference - -- Move cursor to end - if state.input_win and vim.api.nvim_win_is_valid(state.input_win) then - vim.api.nvim_set_current_win(state.input_win) - local line_count = vim.api.nvim_buf_line_count(state.input_buf) - local last_line = vim.api.nvim_buf_get_lines(state.input_buf, line_count - 1, line_count, false)[1] or "" - vim.api.nvim_win_set_cursor(state.input_win, { line_count, #last_line }) - vim.cmd("startinsert!") - end - end + vim.api.nvim_buf_set_lines(state.input_buf, 0, -1, false, vim.split(text, "\n")) - utils.notify("Added file: " .. filename .. " (" .. (content and #content or 0) .. " bytes)") + -- Move cursor to end + if state.input_win and vim.api.nvim_win_is_valid(state.input_win) then + vim.api.nvim_set_current_win(state.input_win) + local line_count = vim.api.nvim_buf_line_count(state.input_buf) + local last_line = vim.api.nvim_buf_get_lines(state.input_buf, line_count - 1, line_count, false)[1] or "" + vim.api.nvim_win_set_cursor(state.input_win, { line_count, #last_line }) + vim.cmd("startinsert!") + end + end + + utils.notify("Added file: " .. filename .. " (" .. (content and #content or 0) .. " bytes)") end --- Close the ask panel function M.close() - -- Remove the width maintenance autocmd first - if ask_augroup then - pcall(vim.api.nvim_del_augroup_by_id, ask_augroup) - ask_augroup = nil - end + -- Remove the width maintenance autocmd first + if ask_augroup then + pcall(vim.api.nvim_del_augroup_by_id, ask_augroup) + ask_augroup = nil + end - -- Find a window to focus after closing (not the ask windows) - local target_win = nil - for _, win in ipairs(vim.api.nvim_list_wins()) do - local buf = vim.api.nvim_win_get_buf(win) - if win ~= state.input_win and win ~= state.output_win then - local buftype = vim.bo[buf].buftype - if buftype == "" or buftype == "acwrite" then - target_win = win - break - end - end - end + -- Find a window to focus after closing (not the ask windows) + local target_win = nil + for _, win in ipairs(vim.api.nvim_list_wins()) do + local buf = vim.api.nvim_win_get_buf(win) + if win ~= state.input_win and win ~= state.output_win then + local buftype = vim.bo[buf].buftype + if buftype == "" or buftype == "acwrite" then + target_win = win + break + end + end + end - -- Close input window - if state.input_win and vim.api.nvim_win_is_valid(state.input_win) then - pcall(vim.api.nvim_win_close, state.input_win, true) - end + -- Close input window + if state.input_win and vim.api.nvim_win_is_valid(state.input_win) then + pcall(vim.api.nvim_win_close, state.input_win, true) + end - -- Close output window - if state.output_win and vim.api.nvim_win_is_valid(state.output_win) then - pcall(vim.api.nvim_win_close, state.output_win, true) - end + -- Close output window + if state.output_win and vim.api.nvim_win_is_valid(state.output_win) then + pcall(vim.api.nvim_win_close, state.output_win, true) + end - -- Reset state - state.input_win = nil - state.output_win = nil - state.is_open = false - state.target_width = nil + -- Reset state + state.input_win = nil + state.output_win = nil + state.is_open = false + state.target_width = nil - -- Focus the target window if found, otherwise focus first available - if target_win and vim.api.nvim_win_is_valid(target_win) then - pcall(vim.api.nvim_set_current_win, target_win) - else - -- If no valid window, make sure we're not left with empty state - local wins = vim.api.nvim_list_wins() - if #wins > 0 then - pcall(vim.api.nvim_set_current_win, wins[1]) - end - end + -- Focus the target window if found, otherwise focus first available + if target_win and vim.api.nvim_win_is_valid(target_win) then + pcall(vim.api.nvim_set_current_win, target_win) + else + -- If no valid window, make sure we're not left with empty state + local wins = vim.api.nvim_list_wins() + if #wins > 0 then + pcall(vim.api.nvim_set_current_win, wins[1]) + end + end end --- Toggle the ask panel function M.toggle() - if M.is_open() then - M.close() - else - M.open() - end + if M.is_open() then + M.close() + else + M.open() + end end --- Focus the input window function M.focus_input() - if state.input_win and vim.api.nvim_win_is_valid(state.input_win) then - vim.api.nvim_set_current_win(state.input_win) - vim.cmd("startinsert") - end + if state.input_win and vim.api.nvim_win_is_valid(state.input_win) then + vim.api.nvim_set_current_win(state.input_win) + vim.cmd("startinsert") + end end --- Focus the output window function M.focus_output() - if state.output_win and vim.api.nvim_win_is_valid(state.output_win) then - vim.api.nvim_set_current_win(state.output_win) - end + if state.output_win and vim.api.nvim_win_is_valid(state.output_win) then + vim.api.nvim_set_current_win(state.output_win) + end end --- Get input text ---@return string Input text local function get_input_text() - if not state.input_buf or not vim.api.nvim_buf_is_valid(state.input_buf) then - return "" - end + if not state.input_buf or not vim.api.nvim_buf_is_valid(state.input_buf) then + return "" + end - local lines = vim.api.nvim_buf_get_lines(state.input_buf, 0, -1, false) - local content = table.concat(lines, "\n") + local lines = vim.api.nvim_buf_get_lines(state.input_buf, 0, -1, false) + local content = table.concat(lines, "\n") - -- Ignore placeholder - if content:match("Type your question here") then - return "" - end + -- Ignore placeholder + if content:match("Type your question here") then + return "" + end - return content + return content end --- Clear input buffer function M.clear_input() - if state.input_buf and vim.api.nvim_buf_is_valid(state.input_buf) then - vim.api.nvim_buf_set_lines(state.input_buf, 0, -1, false, { "" }) - end - state.referenced_files = {} + if state.input_buf and vim.api.nvim_buf_is_valid(state.input_buf) then + vim.api.nvim_buf_set_lines(state.input_buf, 0, -1, false, { "" }) + end + state.referenced_files = {} end --- Append text to output buffer ---@param text string Text to append ---@param is_user boolean Whether this is user message local function append_to_output(text, is_user) - if not state.output_buf or not vim.api.nvim_buf_is_valid(state.output_buf) then - return - end + if not state.output_buf or not vim.api.nvim_buf_is_valid(state.output_buf) then + return + end - vim.bo[state.output_buf].modifiable = true + vim.bo[state.output_buf].modifiable = true - local lines = vim.api.nvim_buf_get_lines(state.output_buf, 0, -1, false) + local lines = vim.api.nvim_buf_get_lines(state.output_buf, 0, -1, false) - local timestamp = os.date("%H:%M") - local header = is_user - and "┌─ 👤 You [" .. timestamp .. "] ────────" - or "┌─ 🤖 AI [" .. timestamp .. "] ──────────" + local timestamp = os.date("%H:%M") + local header = is_user and "┌─ 👤 You [" .. timestamp .. "] ────────" + or "┌─ 🤖 AI [" .. timestamp .. "] ──────────" - local new_lines = { "", header, "│" } + local new_lines = { "", header, "│" } - -- Add text lines with border - for _, line in ipairs(vim.split(text, "\n")) do - table.insert(new_lines, "│ " .. line) - end + -- Add text lines with border + for _, line in ipairs(vim.split(text, "\n")) do + table.insert(new_lines, "│ " .. line) + end - table.insert(new_lines, "└─────────────────────────────────") + table.insert( + new_lines, + "└─────────────────────────────────" + ) - for _, line in ipairs(new_lines) do - table.insert(lines, line) - end + for _, line in ipairs(new_lines) do + table.insert(lines, line) + end - vim.api.nvim_buf_set_lines(state.output_buf, 0, -1, false, lines) - vim.bo[state.output_buf].modifiable = false + vim.api.nvim_buf_set_lines(state.output_buf, 0, -1, false, lines) + vim.bo[state.output_buf].modifiable = false - -- Scroll to bottom - if state.output_win and vim.api.nvim_win_is_valid(state.output_win) then - local line_count = vim.api.nvim_buf_line_count(state.output_buf) - vim.api.nvim_win_set_cursor(state.output_win, { line_count, 0 }) - end + -- Scroll to bottom + if state.output_win and vim.api.nvim_win_is_valid(state.output_win) then + local line_count = vim.api.nvim_buf_line_count(state.output_buf) + vim.api.nvim_win_set_cursor(state.output_win, { line_count, 0 }) + end end --- Build context from referenced files ---@return string Context string, number File count local function build_file_context() - local context = "" - local file_count = 0 + local context = "" + local file_count = 0 - for filename, filepath in pairs(state.referenced_files) do - local content = utils.read_file(filepath) - if content and content ~= "" then - -- Detect language from extension - local ext = vim.fn.fnamemodify(filepath, ":e") - local lang = ext or "text" - - context = context .. "\n\n=== FILE: " .. filename .. " ===\n" - context = context .. "Path: " .. filepath .. "\n" - context = context .. "```" .. lang .. "\n" .. content .. "\n```\n" - file_count = file_count + 1 - end - end + for filename, filepath in pairs(state.referenced_files) do + local content = utils.read_file(filepath) + if content and content ~= "" then + -- Detect language from extension + local ext = vim.fn.fnamemodify(filepath, ":e") + local lang = ext or "text" - return context, file_count + context = context .. "\n\n=== FILE: " .. filename .. " ===\n" + context = context .. "Path: " .. filepath .. "\n" + context = context .. "```" .. lang .. "\n" .. content .. "\n```\n" + file_count = file_count + 1 + end + end + + return context, file_count end --- Build context for the question ---@return table Context object local function build_context() - local context = { - project_root = utils.get_project_root(), - current_file = nil, - current_content = nil, - language = nil, - referenced_files = state.referenced_files, - } + local context = { + project_root = utils.get_project_root(), + current_file = nil, + current_content = nil, + language = nil, + referenced_files = state.referenced_files, + } - -- Try to get current file context from the non-ask window - local wins = vim.api.nvim_list_wins() - for _, win in ipairs(wins) do - if win ~= state.input_win and win ~= state.output_win then - local buf = vim.api.nvim_win_get_buf(win) - local filepath = vim.api.nvim_buf_get_name(buf) + -- Try to get current file context from the non-ask window + local wins = vim.api.nvim_list_wins() + for _, win in ipairs(wins) do + if win ~= state.input_win and win ~= state.output_win then + local buf = vim.api.nvim_win_get_buf(win) + local filepath = vim.api.nvim_buf_get_name(buf) - if filepath and filepath ~= "" and not filepath:match("%.coder%.") then - context.current_file = filepath - context.current_content = table.concat(vim.api.nvim_buf_get_lines(buf, 0, -1, false), "\n") - context.language = vim.bo[buf].filetype - break - end - end - end + if filepath and filepath ~= "" and not filepath:match("%.coder%.") then + context.current_file = filepath + context.current_content = table.concat(vim.api.nvim_buf_get_lines(buf, 0, -1, false), "\n") + context.language = vim.bo[buf].filetype + break + end + end + end - return context + return context end --- Submit the question to LLM function M.submit() - local question = get_input_text() + local question = get_input_text() - if not question or question:match("^%s*$") then - utils.notify("Please enter a question", vim.log.levels.WARN) - M.focus_input() - return - end + if not question or question:match("^%s*$") then + utils.notify("Please enter a question", vim.log.levels.WARN) + M.focus_input() + return + end - -- Build context BEFORE clearing input (to preserve file references) - local context = build_context() - local file_context, file_count = build_file_context() + -- Build context BEFORE clearing input (to preserve file references) + local context = build_context() + local file_context, file_count = build_file_context() - -- Build display message (without full file contents) - local display_question = question - if file_count > 0 then - display_question = question .. "\n📎 " .. file_count .. " file(s) attached" - end + -- Build display message (without full file contents) + local display_question = question + if file_count > 0 then + display_question = question .. "\n📎 " .. file_count .. " file(s) attached" + end - -- Add user message to output - append_to_output(display_question, true) + -- Add user message to output + append_to_output(display_question, true) - -- Clear input and references AFTER building context - M.clear_input() + -- Clear input and references AFTER building context + M.clear_input() - -- Build system prompt for ask mode using prompts module - local prompts = require("codetyper.prompts") - local system_prompt = prompts.system.ask + -- Build system prompt for ask mode using prompts module + local prompts = require("codetyper.prompts") + local system_prompt = prompts.system.ask - if context.current_file then - system_prompt = system_prompt .. "\n\nCurrent open file: " .. context.current_file - system_prompt = system_prompt .. "\nLanguage: " .. (context.language or "unknown") - end + if context.current_file then + system_prompt = system_prompt .. "\n\nCurrent open file: " .. context.current_file + system_prompt = system_prompt .. "\nLanguage: " .. (context.language or "unknown") + end - -- Add to history - table.insert(state.history, { role = "user", content = question }) + -- Add to history + table.insert(state.history, { role = "user", content = question }) - -- Show loading indicator - append_to_output("⏳ Thinking...", false) + -- Show loading indicator + append_to_output("⏳ Thinking...", false) - -- Get LLM client and generate response - local ok, llm = pcall(require, "codetyper.llm") - if not ok then - append_to_output("❌ Error: LLM module not loaded", false) - return - end + -- Get LLM client and generate response + local ok, llm = pcall(require, "codetyper.llm") + if not ok then + append_to_output("❌ Error: LLM module not loaded", false) + return + end - local client = llm.get_client() + local client = llm.get_client() - -- Build full prompt WITH file contents - local full_prompt = question - if file_context ~= "" then - full_prompt = "USER QUESTION: " .. question .. "\n\n" .. - "ATTACHED FILE CONTENTS (please analyze these):" .. file_context - end + -- Build full prompt WITH file contents + local full_prompt = question + if file_context ~= "" then + full_prompt = "USER QUESTION: " + .. question + .. "\n\n" + .. "ATTACHED FILE CONTENTS (please analyze these):" + .. file_context + end - -- Also add current file if no files were explicitly attached - if file_count == 0 and context.current_content and context.current_content ~= "" then - full_prompt = "USER QUESTION: " .. question .. "\n\n" .. - "CURRENT FILE (" .. (context.current_file or "unknown") .. "):\n```\n" .. - context.current_content .. "\n```" - end + -- Also add current file if no files were explicitly attached + if file_count == 0 and context.current_content and context.current_content ~= "" then + full_prompt = "USER QUESTION: " + .. question + .. "\n\n" + .. "CURRENT FILE (" + .. (context.current_file or "unknown") + .. "):\n```\n" + .. context.current_content + .. "\n```" + end - local request_context = { - file_content = file_context ~= "" and file_context or context.current_content, - language = context.language, - prompt_type = "explain", - file_path = context.current_file, - } + local request_context = { + file_content = file_context ~= "" and file_context or context.current_content, + language = context.language, + prompt_type = "explain", + file_path = context.current_file, + } - client.generate(full_prompt, request_context, function(response, err) - -- Remove loading indicator - if state.output_buf and vim.api.nvim_buf_is_valid(state.output_buf) then - vim.bo[state.output_buf].modifiable = true - local lines = vim.api.nvim_buf_get_lines(state.output_buf, 0, -1, false) - -- Remove last few lines (the thinking message) - local to_remove = 0 - for i = #lines, 1, -1 do - if lines[i]:match("Thinking") or lines[i]:match("^[│└┌─]") then - to_remove = to_remove + 1 - if lines[i]:match("┌") then - break - end - else - break - end - end - for _ = 1, math.min(to_remove, 5) do - table.remove(lines) - end - vim.api.nvim_buf_set_lines(state.output_buf, 0, -1, false, lines) - vim.bo[state.output_buf].modifiable = false - end + client.generate(full_prompt, request_context, function(response, err) + -- Remove loading indicator + if state.output_buf and vim.api.nvim_buf_is_valid(state.output_buf) then + vim.bo[state.output_buf].modifiable = true + local lines = vim.api.nvim_buf_get_lines(state.output_buf, 0, -1, false) + -- Remove last few lines (the thinking message) + local to_remove = 0 + for i = #lines, 1, -1 do + if lines[i]:match("Thinking") or lines[i]:match("^[│└┌─]") then + to_remove = to_remove + 1 + if lines[i]:match("┌") then + break + end + else + break + end + end + for _ = 1, math.min(to_remove, 5) do + table.remove(lines) + end + vim.api.nvim_buf_set_lines(state.output_buf, 0, -1, false, lines) + vim.bo[state.output_buf].modifiable = false + end - if err then - append_to_output("❌ Error: " .. err, false) - return - end + if err then + append_to_output("❌ Error: " .. err, false) + return + end - if response then - -- Add to history - table.insert(state.history, { role = "assistant", content = response }) - -- Display response - append_to_output(response, false) - else - append_to_output("❌ No response received", false) - end + if response then + -- Add to history + table.insert(state.history, { role = "assistant", content = response }) + -- Display response + append_to_output(response, false) + else + append_to_output("❌ No response received", false) + end - -- Focus back to input - M.focus_input() - end) + -- Focus back to input + M.focus_input() + end) end --- Clear chat history function M.clear_history() - state.history = {} - state.referenced_files = {} + state.history = {} + state.referenced_files = {} - if state.output_buf and vim.api.nvim_buf_is_valid(state.output_buf) then - local header = { - "╔═══════════════════════════════════╗", - "║ 🤖 CODETYPER ASK ║", - "╠═══════════════════════════════════╣", - "║ Ask about code or concepts ║", - "║ ║", - "║ 💡 Keymaps: ║", - "║ @ → attach file ║", - "║ C-Enter → send ║", - "║ C-n → new chat ║", - "║ C-f → add current file ║", - "║ C-h/j/k/l → navigate ║", - "║ q → close │ K/J → jump ║", - "╚═══════════════════════════════════╝", - "", - } - vim.bo[state.output_buf].modifiable = true - vim.api.nvim_buf_set_lines(state.output_buf, 0, -1, false, header) - vim.bo[state.output_buf].modifiable = false - end + if state.output_buf and vim.api.nvim_buf_is_valid(state.output_buf) then + local header = { + "╔═══════════════════════════════════╗", + "║ 🤖 CODETYPER ASK ║", + "╠═══════════════════════════════════╣", + "║ Ask about code or concepts ║", + "║ ║", + "║ 💡 Keymaps: ║", + "║ @ → attach file ║", + "║ C-Enter → send ║", + "║ C-n → new chat ║", + "║ C-f → add current file ║", + "║ C-h/j/k/l → navigate ║", + "║ q → close │ K/J → jump ║", + "╚═══════════════════════════════════╝", + "", + } + vim.bo[state.output_buf].modifiable = true + vim.api.nvim_buf_set_lines(state.output_buf, 0, -1, false, header) + vim.bo[state.output_buf].modifiable = false + end - utils.notify("Chat history cleared") + utils.notify("Chat history cleared") end --- Start a new chat (clears history and input) function M.new_chat() - -- Clear the input - M.clear_input() - -- Clear the history - M.clear_history() - -- Focus the input - M.focus_input() - utils.notify("Started new chat") + -- Clear the input + M.clear_input() + -- Clear the history + M.clear_history() + -- Focus the input + M.focus_input() + utils.notify("Started new chat") end --- Include current file context in input function M.include_file_context() - local context = build_context() + local context = build_context() - if not context.current_file then - utils.notify("No file context available", vim.log.levels.WARN) - return - end + if not context.current_file then + utils.notify("No file context available", vim.log.levels.WARN) + return + end - local filename = vim.fn.fnamemodify(context.current_file, ":t") - M.add_file_reference(context.current_file, filename) + local filename = vim.fn.fnamemodify(context.current_file, ":t") + M.add_file_reference(context.current_file, filename) end --- Copy last assistant response to clipboard function M.copy_last_response() - for i = #state.history, 1, -1 do - if state.history[i].role == "assistant" then - vim.fn.setreg("+", state.history[i].content) - utils.notify("Response copied to clipboard") - return - end - end - utils.notify("No response to copy", vim.log.levels.WARN) + for i = #state.history, 1, -1 do + if state.history[i].role == "assistant" then + vim.fn.setreg("+", state.history[i].content) + utils.notify("Response copied to clipboard") + return + end + end + utils.notify("No response to copy", vim.log.levels.WARN) end --- Check if ask panel is open (validates window state) ---@return boolean function M.is_open() - -- Verify windows are actually valid, not just the flag - if state.is_open then - local output_valid = state.output_win and vim.api.nvim_win_is_valid(state.output_win) - local input_valid = state.input_win and vim.api.nvim_win_is_valid(state.input_win) - - -- If either window is invalid, reset the state - if not output_valid or not input_valid then - state.is_open = false - state.output_win = nil - state.input_win = nil - state.target_width = nil - -- Clean up autocmd - if ask_augroup then - pcall(vim.api.nvim_del_augroup_by_id, ask_augroup) - ask_augroup = nil - end - end - end - - return state.is_open + -- Verify windows are actually valid, not just the flag + if state.is_open then + local output_valid = state.output_win and vim.api.nvim_win_is_valid(state.output_win) + local input_valid = state.input_win and vim.api.nvim_win_is_valid(state.input_win) + + -- If either window is invalid, reset the state + if not output_valid or not input_valid then + state.is_open = false + state.output_win = nil + state.input_win = nil + state.target_width = nil + -- Clean up autocmd + if ask_augroup then + pcall(vim.api.nvim_del_augroup_by_id, ask_augroup) + ask_augroup = nil + end + end + end + + return state.is_open end --- Get chat history ---@return table History function M.get_history() - return state.history + return state.history end return M diff --git a/lua/codetyper/prompts/agent.lua b/lua/codetyper/prompts/agent.lua new file mode 100644 index 0000000..7b75344 --- /dev/null +++ b/lua/codetyper/prompts/agent.lua @@ -0,0 +1,46 @@ +---@mod codetyper.prompts.agent Agent prompts for Codetyper.nvim +--- +--- System prompts for the agentic mode with tool use. + +local M = {} + +--- System prompt for agent mode +M.system = [[You are an AI coding agent integrated into Neovim via Codetyper.nvim. +You can read files, edit code, write new files, and run bash commands to help the user. + +You have access to the following tools: +- read_file: Read file contents +- edit_file: Edit a file by finding and replacing specific content +- write_file: Write or create a file +- bash: Execute shell commands + +GUIDELINES: +1. Always read a file before editing it to understand its current state +2. Use edit_file for targeted changes (find and replace specific content) +3. Use write_file only for new files or complete rewrites +4. Be conservative with bash commands - only run what's necessary +5. After making changes, summarize what you did +6. If a task requires multiple steps, think through the plan first + +IMPORTANT: +- Be precise with edit_file - the "find" content must match exactly +- When editing, include enough context to make the match unique +- Never delete files without explicit user confirmation +- Always explain what you're doing and why +]] + +--- Tool usage instructions appended to system prompt +M.tool_instructions = [[ +When you need to use a tool, output the tool call in a JSON block. +After receiving the result, you can either call another tool or provide your final response. + +SAFETY RULES: +- Never run destructive bash commands (rm -rf, etc.) without confirmation +- Always preserve existing functionality when editing +- If unsure about a change, ask for clarification first +]] + +--- Prompt for when agent finishes +M.completion = [[Based on the tool results above, please provide a summary of what was done and any next steps the user should take.]] + +return M diff --git a/scripts/create_support_files.sh b/scripts/create_support_files.sh deleted file mode 100644 index 0ea966f..0000000 --- a/scripts/create_support_files.sh +++ /dev/null @@ -1,25 +0,0 @@ -# Create .coder/ folder if does not exist -mkdir -p .coder -# Create .coder/settings.json with default settings if it does not exist -if [ ! -f .coder/settings.json ]; then - cat < .coder/settings.json - { - "editor.fontSize": 14, - "editor.tabSize": 2, - "files.autoSave": "afterDelay", - "files.autoSaveDelay": 1000, - "terminal.integrated.fontSize": 14, - "workbench.colorTheme": "Default Dark+" - } -EOL -fi - -# Add the .coder/ folder to .gitignore if not already present -if ! grep -q "^.coder/$" .gitignore; then - echo ".coder/" >> .gitignore -fi - -# Add the ./**/*.coder.* files to .gitignore if not already present -if ! grep -q "^.*/\.coder/.*$" .gitignore; then - echo ".*/.coder/.*" >> .gitignore -fi