diff --git a/lua/codetyper/agent/agentic.lua b/lua/codetyper/agent/agentic.lua index 10e4a13..6164555 100644 --- a/lua/codetyper/agent/agentic.lua +++ b/lua/codetyper/agent/agentic.lua @@ -31,10 +31,7 @@ local M = {} ---@field on_complete? fun(result: string|nil, error: string|nil) Called when done ---@field on_status? fun(status: string) Status updates ---- Generate unique tool call ID -local function generate_tool_call_id() - return "call_" .. string.format("%x", os.time()) .. "_" .. string.format("%x", math.random(0, 0xFFFF)) -end +local utils = require("codetyper.utils") --- Load agent definition ---@param name string Agent name @@ -72,59 +69,7 @@ local function load_agent(name) end -- Built-in agents - local builtin_agents = { - coder = { - name = "coder", - description = "Full-featured coding agent with file modification capabilities", - system_prompt = [[You are an expert software engineer. You have access to tools to read, write, and modify files. - -## Your Capabilities -- Read files to understand the codebase -- Search for patterns with grep and glob -- Create new files with write tool -- Edit existing files with precise replacements -- Execute shell commands for builds and tests - -## Guidelines -1. Always read relevant files before making changes -2. Make minimal, focused changes -3. Follow existing code style and patterns -4. Create tests when adding new functionality -5. Verify changes work by running tests or builds - -## Important Rules -- NEVER guess file contents - always read first -- Make precise edits using exact string matching -- Explain your reasoning before making changes -- If unsure, ask for clarification]], - tools = { "view", "edit", "write", "grep", "glob", "bash" }, - }, - planner = { - name = "planner", - description = "Planning agent - read-only, helps design implementations", - system_prompt = [[You are a software architect. Analyze codebases and create implementation plans. - -You can read files and search the codebase, but cannot modify files. -Your role is to: -1. Understand the existing architecture -2. Identify relevant files and patterns -3. Create step-by-step implementation plans -4. Suggest which files to modify and how - -Be thorough in your analysis before making recommendations.]], - tools = { "view", "grep", "glob" }, - }, - explorer = { - name = "explorer", - description = "Exploration agent - quickly find information in codebase", - system_prompt = [[You are a codebase exploration assistant. Find information quickly and report back. - -Your goal is to efficiently search and summarize findings. -Use glob to find files, grep to search content, and view to read specific files. -Be concise and focused in your responses.]], - tools = { "view", "grep", "glob" }, - }, - } + local builtin_agents = require("codetyper.prompts.agents.personas").builtin return builtin_agents[name] end @@ -365,7 +310,7 @@ local function parse_tool_calls(response, provider) end table.insert(tool_calls, { - id = block.id or generate_tool_call_id(), + id = block.id or utils.generate_id("call"), type = "function", ["function"] = { name = block.name, @@ -449,7 +394,7 @@ local function call_llm(messages, tools, system_prompt, provider, model, callbac elseif block.type == "tool_use" then table.insert(result.content, { type = "tool_use", - id = block.id or generate_tool_call_id(), + id = block.id or utils.generate_id("call"), name = block.name, input = block.input, }) @@ -490,7 +435,7 @@ local function call_llm(messages, tools, system_prompt, provider, model, callbac elseif block.type == "tool_use" then table.insert(result.content, { type = "tool_use", - id = block.id or generate_tool_call_id(), + id = block.id or utils.generate_id("call"), name = block.name, input = block.input, }) @@ -526,21 +471,20 @@ local function call_llm(messages, tools, system_prompt, provider, model, callbac local client = require("codetyper.llm." .. provider) -- Build prompt from messages + local prompts = require("codetyper.prompts.agent") local prompt_parts = {} for _, msg in ipairs(messages) do if msg.role == "user" then local content = type(msg.content) == "string" and msg.content or vim.json.encode(msg.content) - table.insert(prompt_parts, "User: " .. content) + table.insert(prompt_parts, prompts.text_user_prefix .. content) elseif msg.role == "assistant" then local content = type(msg.content) == "string" and msg.content or vim.json.encode(msg.content) - table.insert(prompt_parts, "Assistant: " .. content) + table.insert(prompt_parts, prompts.text_assistant_prefix .. content) end end -- Add tool descriptions to prompt for text-based providers - local tool_desc = "\n\n## Available Tools\n" - tool_desc = tool_desc .. "Call tools by outputting JSON in this format:\n" - tool_desc = tool_desc .. '```json\n{"tool": "tool_name", "arguments": {...}}\n```\n\n' + local tool_desc = require("codetyper.prompts.agent").tool_instructions_text for _, tool in ipairs(tools) do local name = tool.name or (tool["function"] and tool["function"].name) local desc = tool.description or (tool["function"] and tool["function"].description) @@ -573,7 +517,7 @@ local function call_llm(messages, tools, system_prompt, provider, model, callbac if ok and parsed.tool then table.insert(result.content, { type = "tool_use", - id = generate_tool_call_id(), + id = utils.generate_id("call"), name = parsed.tool, input = parsed.arguments or {}, }) @@ -617,11 +561,7 @@ function M.run(opts) -- Add initial file context if provided if opts.files and #opts.files > 0 then - local file_context = "# Initial Files\n" - for _, file_path in ipairs(opts.files) do - local content = table.concat(vim.fn.readfile(file_path) or {}, "\n") - file_context = file_context .. string.format("\n## %s\n```\n%s\n```\n", file_path, content) - end + local file_context = require("codetyper.prompts.agent").format_file_context(opts.files) table.insert(history, { role = "user", content = file_context }) table.insert(history, { role = "assistant", content = "I've reviewed the provided files. What would you like me to do?" }) end @@ -736,27 +676,7 @@ function M.init_agents_dir() vim.fn.mkdir(agents_dir, "p") -- Create example agent - local example_agent = [[--- -description: Example custom agent -tools: view,grep,glob,edit,write -model: ---- - -# Custom Agent - -You are a custom coding agent. Describe your specialized behavior here. - -## Your Role -- Define what this agent specializes in -- List specific capabilities - -## Guidelines -- Add agent-specific rules -- Define coding standards to follow - -## Examples -Provide examples of how to handle common tasks. -]] + local example_agent = require("codetyper.prompts.agents.templates").agent local example_path = agents_dir .. "/example.md" if vim.fn.filereadable(example_path) ~= 1 then @@ -772,30 +692,7 @@ function M.init_rules_dir() vim.fn.mkdir(rules_dir, "p") -- Create example rule - local example_rule = [[# Code Style - -Follow these coding standards: - -## General -- Use consistent indentation (tabs or spaces based on project) -- Keep lines under 100 characters -- Add comments for complex logic - -## Naming Conventions -- Use descriptive variable names -- Functions should be verbs (e.g., getUserData, calculateTotal) -- Constants in UPPER_SNAKE_CASE - -## Testing -- Write tests for new functionality -- Aim for >80% code coverage -- Test edge cases - -## Documentation -- Document public APIs -- Include usage examples -- Keep docs up to date with code -]] + local example_rule = require("codetyper.prompts.agents.templates").rule local example_path = rules_dir .. "/code-style.md" if vim.fn.filereadable(example_path) ~= 1 then @@ -817,7 +714,10 @@ function M.list_agents() local agents = {} -- Built-in agents - local builtins = { "coder", "planner", "explorer" } + local personas = require("codetyper.prompts.agents.personas").builtin + local builtins = vim.tbl_keys(personas) + table.sort(builtins) + for _, name in ipairs(builtins) do local agent = load_agent(name) if agent then diff --git a/lua/codetyper/agent/confidence.lua b/lua/codetyper/agent/confidence.lua index e02d4b7..4be2b3b 100644 --- a/lua/codetyper/agent/confidence.lua +++ b/lua/codetyper/agent/confidence.lua @@ -6,41 +6,14 @@ local M = {} +local params = require("codetyper.params.agent.confidence") + --- Heuristic weights (must sum to 1.0) -M.weights = { - length = 0.15, -- Response length relative to prompt - uncertainty = 0.30, -- Uncertainty phrases - syntax = 0.25, -- Syntax completeness - repetition = 0.15, -- Duplicate lines - truncation = 0.15, -- Incomplete ending -} +M.weights = params.weights --- Uncertainty phrases that indicate low confidence -local uncertainty_phrases = { - -- English - "i'm not sure", - "i am not sure", - "maybe", - "perhaps", - "might work", - "could work", - "not certain", - "uncertain", - "i think", - "possibly", - "TODO", - "FIXME", - "XXX", - "placeholder", - "implement this", - "fill in", - "your code here", - "...", -- Ellipsis as placeholder - "# TODO", - "// TODO", - "-- TODO", - "/* TODO", -} +local uncertainty_phrases = params.uncertainty_phrases + --- Score based on response length relative to prompt ---@param response string @@ -94,32 +67,6 @@ local function score_uncertainty(response) end end ---- Check bracket balance for common languages ----@param response string ----@return boolean balanced -local function check_brackets(response) - local pairs = { - ["{"] = "}", - ["["] = "]", - ["("] = ")", - } - - local stack = {} - - for char in response:gmatch(".") do - if pairs[char] then - table.insert(stack, pairs[char]) - elseif char == "}" or char == "]" or char == ")" then - if #stack == 0 or stack[#stack] ~= char then - return false - end - table.remove(stack) - end - end - - return #stack == 0 -end - --- Score based on syntax completeness ---@param response string ---@return number 0.0-1.0 @@ -127,7 +74,7 @@ local function score_syntax(response) local score = 1.0 -- Check bracket balance - if not check_brackets(response) then + if not require("codetyper.utils").check_brackets(response) then score = score - 0.4 end diff --git a/lua/codetyper/agent/conflict.lua b/lua/codetyper/agent/conflict.lua index 5523153..141b593 100644 --- a/lua/codetyper/agent/conflict.lua +++ b/lua/codetyper/agent/conflict.lua @@ -15,22 +15,15 @@ local M = {} +local params = require("codetyper.params.agent.conflict") + --- Lazy load linter module local function get_linter() return require("codetyper.agent.linter") end --- Configuration -local config = { - -- Run linter check after accepting AI suggestions - lint_after_accept = true, - -- Auto-fix lint errors without prompting - auto_fix_lint_errors = true, - -- Auto-show menu after injecting conflict - auto_show_menu = true, - -- Auto-show menu for next conflict after resolving one - auto_show_next_menu = true, -} +local config = vim.deepcopy(params.config) --- Namespace for conflict highlighting local NAMESPACE = vim.api.nvim_create_namespace("codetyper_conflict") @@ -39,24 +32,12 @@ local NAMESPACE = vim.api.nvim_create_namespace("codetyper_conflict") local HINT_NAMESPACE = vim.api.nvim_create_namespace("codetyper_conflict_hints") --- Highlight groups -local HL_GROUPS = { - current = "CoderConflictCurrent", - current_label = "CoderConflictCurrentLabel", - incoming = "CoderConflictIncoming", - incoming_label = "CoderConflictIncomingLabel", - separator = "CoderConflictSeparator", - hint = "CoderConflictHint", -} +local HL_GROUPS = params.hl_groups --- Conflict markers -local MARKERS = { - current_start = "<<<<<<< CURRENT", - separator = "=======", - incoming_end = ">>>>>>> INCOMING", -} +local MARKERS = params.markers --- Track buffers with active conflicts ----@type table local conflict_buffers = {} --- Run linter validation after accepting code changes diff --git a/lua/codetyper/agent/context_builder.lua b/lua/codetyper/agent/context_builder.lua index b8699cd..4e1df44 100644 --- a/lua/codetyper/agent/context_builder.lua +++ b/lua/codetyper/agent/context_builder.lua @@ -6,6 +6,7 @@ local M = {} local utils = require("codetyper.utils") +local params = require("codetyper.params.agent.context") --- Get project structure as a tree string ---@param max_depth? number Maximum depth to traverse (default: 3) @@ -20,21 +21,7 @@ function M.get_project_structure(max_depth, max_files) local file_count = 0 -- Common ignore patterns - local ignore_patterns = { - "^%.", -- Hidden files/dirs - "node_modules", - "%.git$", - "__pycache__", - "%.pyc$", - "target", -- Rust - "build", - "dist", - "%.o$", - "%.a$", - "%.so$", - "%.min%.", - "%.map$", - } + local ignore_patterns = params.ignore_patterns local function should_ignore(name) for _, pattern in ipairs(ignore_patterns) do @@ -127,19 +114,7 @@ function M.get_key_files() ["plugin.lua"] = "Neovim plugin config", } - for filename, desc in pairs(important_files) do - -- Check in root - local path = root .. "/" .. filename - if vim.fn.filereadable(path) == 1 then - key_files[filename] = { path = path, description = desc } - end - - -- Check in lua/ for Neovim plugins - local lua_path = root .. "/lua/" .. filename - if vim.fn.filereadable(lua_path) == 1 then - key_files["lua/" .. filename] = { path = lua_path, description = desc } - end - end + for filename, desc in paparams.important_filesnd return key_files end @@ -161,16 +136,7 @@ function M.detect_project_type() } -- Check for Neovim plugin specifically - if vim.fn.isdirectory(root .. "/lua") == 1 then - local plugin_files = vim.fn.glob(root .. "/plugin/*.lua", false, true) - if #plugin_files > 0 or vim.fn.filereadable(root .. "/init.lua") == 1 then - return { type = "neovim-plugin", language = "lua", framework = "neovim" } - end - end - - for file, info in pairs(indicators) do - if vim.fn.filereadable(root .. "/" .. file) == 1 then - return info + if vim.fn.isdirectoparams.indicators return info end end diff --git a/lua/codetyper/agent/context_modal.lua b/lua/codetyper/agent/context_modal.lua index 1b0c069..ea515e1 100644 --- a/lua/codetyper/agent/context_modal.lua +++ b/lua/codetyper/agent/context_modal.lua @@ -122,7 +122,8 @@ local function attach_requested_files() end local files = parse_requested_files(state.llm_response) if #files == 0 then - vim.api.nvim_buf_set_lines(state.buf, vim.api.nvim_buf_line_count(state.buf), -1, false, { "", "-- No files detected in LLM response --" }) + local ui_prompts = require("codetyper.prompts.agent.modal").ui + vim.api.nvim_buf_set_lines(state.buf, vim.api.nvim_buf_line_count(state.buf), -1, false, ui_prompts.files_header) return end @@ -190,9 +191,11 @@ function M.open(original_event, llm_response, callback, suggested_commands) vim.wo[state.win].wrap = true vim.wo[state.win].cursorline = true + local ui_prompts = require("codetyper.prompts.agent.modal").ui + -- Add header showing what the LLM said local header_lines = { - "-- LLM Response: --", + ui_prompts.llm_response_header, } -- Truncate LLM response for display @@ -207,16 +210,16 @@ function M.open(original_event, llm_response, callback, suggested_commands) -- If suggested commands were provided, show them in the header if suggested_commands and #suggested_commands > 0 then table.insert(header_lines, "") - table.insert(header_lines, "-- Suggested commands: --") + table.insert(header_lines, ui_prompts.suggested_commands_header) for i, s in ipairs(suggested_commands) do local label = s.label or s.cmd table.insert(header_lines, string.format("[%d] %s: %s", i, label, s.cmd)) end - table.insert(header_lines, "-- Press to run a command, or r to run all --") + table.insert(header_lines, ui_prompts.commands_hint) end table.insert(header_lines, "") - table.insert(header_lines, "-- Enter additional context below (Ctrl-Enter to submit, Esc to cancel) --") + table.insert(header_lines, ui_prompts.input_header) table.insert(header_lines, "") vim.api.nvim_buf_set_lines(state.buf, 0, -1, false, header_lines) @@ -322,8 +325,9 @@ local function run_project_inspect() { label = "Show repo files (git ls-files)", cmd = "git ls-files" }, } + local ui_prompts = require("codetyper.prompts.agent.modal").ui local insert_pos = vim.api.nvim_buf_line_count(state.buf) - vim.api.nvim_buf_set_lines(state.buf, insert_pos, insert_pos, false, { "", "-- Project inspection results --" }) + vim.api.nvim_buf_set_lines(state.buf, insert_pos, insert_pos, false, ui_prompts.project_inspect_header) for _, c in ipairs(cmds) do local ok, out = pcall(vim.fn.systemlist, c.cmd) diff --git a/lua/codetyper/agent/diff.lua b/lua/codetyper/agent/diff.lua index 2f8bf50..1dde651 100644 --- a/lua/codetyper/agent/diff.lua +++ b/lua/codetyper/agent/diff.lua @@ -157,17 +157,19 @@ function M.show_diff(diff_data, callback) end -- Show help message - vim.api.nvim_echo({ - { "Diff: ", "Normal" }, - { diff_data.path, "Directory" }, - { " | ", "Normal" }, - { "y/", "Keyword" }, - { " approve ", "Normal" }, - { "n/q/", "Keyword" }, - { " reject ", "Normal" }, - { "", "Keyword" }, - { " switch panes", "Normal" }, - }, false, {}) + local help_msg = require("codetyper.prompts.agent.diff").diff_help + + -- Iterate to replace {path} variable + local final_help = {} + for _, item in ipairs(help_msg) do + if item[1] == "{path}" then + table.insert(final_help, { diff_data.path, item[2] }) + else + table.insert(final_help, item) + end + end + + vim.api.nvim_echo(final_help, false, {}) end ---@alias BashApprovalResult {approved: boolean, permission_level: string|nil} @@ -188,31 +190,31 @@ function M.show_bash_approval(command, callback) end -- Create approval dialog with options + local approval_prompts = require("codetyper.prompts.agent.diff").bash_approval local lines = { "", - " BASH COMMAND APPROVAL", - " " .. string.rep("─", 56), + approval_prompts.title, + approval_prompts.divider, "", - " Command:", + approval_prompts.command_label, " $ " .. command, "", } -- Add warning for dangerous commands if not perm_result.allowed and perm_result.reason ~= "Requires approval" then - table.insert(lines, " ⚠️ WARNING: " .. perm_result.reason) + table.insert(lines, approval_prompts.warning_prefix .. perm_result.reason) table.insert(lines, "") end - table.insert(lines, " " .. string.rep("─", 56)) + table.insert(lines, approval_prompts.divider) table.insert(lines, "") - table.insert(lines, " [y] Allow once - Execute this command") - table.insert(lines, " [s] Allow this session - Auto-allow until restart") - table.insert(lines, " [a] Add to allow list - Always allow this command") - table.insert(lines, " [n] Reject - Cancel execution") + for _, opt in ipairs(approval_prompts.options) do + table.insert(lines, opt) + end table.insert(lines, "") - table.insert(lines, " " .. string.rep("─", 56)) - table.insert(lines, " Press key to choose | [q] or [Esc] to cancel") + table.insert(lines, approval_prompts.divider) + table.insert(lines, approval_prompts.cancel_hint) table.insert(lines, "") local width = math.max(65, #command + 15) diff --git a/lua/codetyper/agent/diff_review.lua b/lua/codetyper/agent/diff_review.lua index 8944c7e..cbe8cf0 100644 --- a/lua/codetyper/agent/diff_review.lua +++ b/lua/codetyper/agent/diff_review.lua @@ -6,6 +6,8 @@ local M = {} local utils = require("codetyper.utils") +local prompts = require("codetyper.prompts.agent.diff") + ---@class DiffEntry ---@field path string File path @@ -124,9 +126,10 @@ local function update_diff_view() end local entry = state.entries[state.current_index] + local ui_prompts = prompts.review if not entry then vim.bo[state.diff_buf].modifiable = true - vim.api.nvim_buf_set_lines(state.diff_buf, 0, -1, false, { "No changes to review" }) + vim.api.nvim_buf_set_lines(state.diff_buf, 0, -1, false, { ui_prompts.messages.no_changes_short }) vim.bo[state.diff_buf].modifiable = false return end @@ -134,15 +137,17 @@ local function update_diff_view() local lines = {} -- Header - local status_icon = entry.applied and "" or (entry.approved and "" or "") + local status_icon = entry.applied and " " or (entry.approved and " " or " ") local op_icon = entry.operation == "create" and "+" or (entry.operation == "delete" and "-" or "~") + local current_status = entry.applied and ui_prompts.status.applied + or (entry.approved and ui_prompts.status.approved or ui_prompts.status.pending) - table.insert(lines, string.format("╭─ %s %s %s ─────────────────────────────────────", + table.insert(lines, string.format(ui_prompts.diff_header.top, status_icon, op_icon, vim.fn.fnamemodify(entry.path, ":t"))) - table.insert(lines, "│ " .. entry.path) - table.insert(lines, "│ Operation: " .. entry.operation) - table.insert(lines, "│ Status: " .. (entry.applied and "Applied" or (entry.approved and "Approved" or "Pending"))) - table.insert(lines, "╰────────────────────────────────────────────────────") + table.insert(lines, string.format(ui_prompts.diff_header.path, entry.path)) + table.insert(lines, string.format(ui_prompts.diff_header.op, entry.operation)) + table.insert(lines, string.format(ui_prompts.diff_header.status, current_status)) + table.insert(lines, ui_prompts.diff_header.bottom) table.insert(lines, "") -- Diff content @@ -163,17 +168,14 @@ local function update_file_list() return end - local lines = { - "╭─ Changes (" .. #state.entries .. ") ──────────╮", - "│ │", - "│ j/k: navigate │", - "│ Enter: view diff │", - "│ a: approve r: reject │", - "│ A: approve all │", - "│ q: close │", - "╰──────────────────────────────╯", - "", - } + local ui_prompts = prompts.review + local lines = {} + table.insert(lines, string.format(ui_prompts.list_menu.top, #state.entries)) + for _, item in ipairs(ui_prompts.list_menu.items) do + table.insert(lines, item) + end + table.insert(lines, ui_prompts.list_menu.bottom) + table.insert(lines, "") for i, entry in ipairs(state.entries) do local prefix = (i == state.current_index) and "▶ " or " " @@ -185,7 +187,7 @@ local function update_file_list() end if #state.entries == 0 then - table.insert(lines, " No changes to review") + table.insert(lines, ui_prompts.messages.no_changes) end vim.bo[state.list_buf].modifiable = true @@ -276,7 +278,7 @@ function M.apply_approved() update_diff_view() if applied_count > 0 then - utils.notify(string.format("Applied %d change(s)", applied_count)) + utils.notify(string.format(prompts.review.messages.applied_count, applied_count)) end return applied_count @@ -289,7 +291,7 @@ function M.open() end if #state.entries == 0 then - utils.notify("No changes to review", vim.log.levels.INFO) + utils.notify(prompts.review.messages.no_changes_short, vim.log.levels.INFO) return end diff --git a/lua/codetyper/agent/inject.lua b/lua/codetyper/agent/inject.lua index 97c59cb..e2bdbc5 100644 --- a/lua/codetyper/agent/inject.lua +++ b/lua/codetyper/agent/inject.lua @@ -17,74 +17,9 @@ local M = {} ---@field body string[] Non-import code lines ---@field import_lines table Map of line numbers that are imports ---- Language-specific import patterns -local import_patterns = { - -- JavaScript/TypeScript - javascript = { - { pattern = "^%s*import%s+.+%s+from%s+['\"]", multi_line = true }, - { pattern = "^%s*import%s+['\"]", multi_line = false }, - { pattern = "^%s*import%s*{", multi_line = true }, - { pattern = "^%s*import%s*%*", multi_line = true }, - { pattern = "^%s*export%s+{.+}%s+from%s+['\"]", multi_line = true }, - { pattern = "^%s*const%s+%w+%s*=%s*require%(['\"]", multi_line = false }, - { pattern = "^%s*let%s+%w+%s*=%s*require%(['\"]", multi_line = false }, - { pattern = "^%s*var%s+%w+%s*=%s*require%(['\"]", multi_line = false }, - }, - -- Python - python = { - { pattern = "^%s*import%s+%w", multi_line = false }, - { pattern = "^%s*from%s+[%w%.]+%s+import%s+", multi_line = true }, - }, - -- Lua - lua = { - { pattern = "^%s*local%s+%w+%s*=%s*require%s*%(?['\"]", multi_line = false }, - { pattern = "^%s*require%s*%(?['\"]", multi_line = false }, - }, - -- Go - go = { - { pattern = "^%s*import%s+%(?", multi_line = true }, - }, - -- Rust - rust = { - { pattern = "^%s*use%s+", multi_line = true }, - { pattern = "^%s*extern%s+crate%s+", multi_line = false }, - }, - -- C/C++ - c = { - { pattern = "^%s*#include%s*[<\"]", multi_line = false }, - }, - -- Java/Kotlin - java = { - { pattern = "^%s*import%s+", multi_line = false }, - }, - -- Ruby - ruby = { - { pattern = "^%s*require%s+['\"]", multi_line = false }, - { pattern = "^%s*require_relative%s+['\"]", multi_line = false }, - }, - -- PHP - php = { - { pattern = "^%s*use%s+", multi_line = false }, - { pattern = "^%s*require%s+['\"]", multi_line = false }, - { pattern = "^%s*require_once%s+['\"]", multi_line = false }, - { pattern = "^%s*include%s+['\"]", multi_line = false }, - { pattern = "^%s*include_once%s+['\"]", multi_line = false }, - }, -} - --- Alias common extensions to language configs -import_patterns.ts = import_patterns.javascript -import_patterns.tsx = import_patterns.javascript -import_patterns.jsx = import_patterns.javascript -import_patterns.mjs = import_patterns.javascript -import_patterns.cjs = import_patterns.javascript -import_patterns.py = import_patterns.python -import_patterns.cpp = import_patterns.c -import_patterns.hpp = import_patterns.c -import_patterns.h = import_patterns.c -import_patterns.kt = import_patterns.java -import_patterns.rs = import_patterns.rust -import_patterns.rb = import_patterns.ruby +local utils = require("codetyper.utils") +local languages = require("codetyper.params.agent.languages") +local import_patterns = languages.import_patterns --- Check if a line is an import statement for the given language ---@param line string @@ -100,83 +35,13 @@ local function is_import_line(line, patterns) return false, false end ---- Check if a line is empty or a comment ----@param line string ----@param filetype string ----@return boolean -local function is_empty_or_comment(line, filetype) - local trimmed = line:match("^%s*(.-)%s*$") - if trimmed == "" then - return true - end - - -- Language-specific comment patterns - local comment_patterns = { - lua = { "^%-%-" }, - python = { "^#" }, - javascript = { "^//", "^/%*", "^%*" }, - typescript = { "^//", "^/%*", "^%*" }, - go = { "^//", "^/%*", "^%*" }, - rust = { "^//", "^/%*", "^%*" }, - c = { "^//", "^/%*", "^%*", "^#" }, - java = { "^//", "^/%*", "^%*" }, - ruby = { "^#" }, - php = { "^//", "^/%*", "^%*", "^#" }, - } - - local patterns = comment_patterns[filetype] or comment_patterns.javascript - for _, pattern in ipairs(patterns) do - if trimmed:match(pattern) then - return true - end - end - - return false -end --- Check if a line ends a multi-line import ---@param line string ---@param filetype string ---@return boolean local function ends_multiline_import(line, filetype) - -- Check for closing patterns - if filetype == "javascript" or filetype == "typescript" or filetype == "ts" or filetype == "tsx" then - -- ES6 imports end with 'from "..." ;' or just ';' or a line with just '}' - if line:match("from%s+['\"][^'\"]+['\"]%s*;?%s*$") then - return true - end - if line:match("}%s*from%s+['\"]") then - return true - end - if line:match("^%s*}%s*;?%s*$") then - return true - end - if line:match(";%s*$") then - return true - end - elseif filetype == "python" or filetype == "py" then - -- Python single-line import: doesn't end with \, (, or , - -- Examples: "from typing import List, Dict" or "import os" - if not line:match("\\%s*$") and not line:match("%(%s*$") and not line:match(",%s*$") then - return true - end - -- Python multiline imports end with closing paren - if line:match("%)%s*$") then - return true - end - elseif filetype == "go" then - -- Go multi-line imports end with ')' - if line:match("%)%s*$") then - return true - end - elseif filetype == "rust" or filetype == "rs" then - -- Rust use statements end with ';' - if line:match(";%s*$") then - return true - end - end - - return false + return utils.ends_multiline_import(line, filetype) end --- Parse code into imports and body @@ -285,7 +150,7 @@ function M.find_import_section(bufnr, filetype) if is_multi and not ends_multiline_import(line, filetype) then in_multiline = true end - elseif is_empty_or_comment(line, filetype) then + elseif utils.is_empty_or_comment(line, filetype) then -- Allow gaps in import section if first_import then consecutive_non_import = consecutive_non_import + 1 @@ -388,34 +253,11 @@ function M.sort_imports(imports, filetype) local local_imports = {} for _, imp in ipairs(imports) do - -- Detect import type based on patterns - local is_local = false - local is_builtin = false + local category = utils.classify_import(imp, filetype) - if filetype == "javascript" or filetype == "typescript" or filetype == "ts" or filetype == "tsx" then - -- Local: starts with . or .. - is_local = imp:match("from%s+['\"]%.") or imp:match("require%(['\"]%.") - -- Node builtin modules - is_builtin = imp:match("from%s+['\"]node:") or imp:match("from%s+['\"]fs['\"]") - or imp:match("from%s+['\"]path['\"]") or imp:match("from%s+['\"]http['\"]") - elseif filetype == "python" or filetype == "py" then - -- Local: relative imports - is_local = imp:match("^from%s+%.") or imp:match("^import%s+%.") - -- Python stdlib (simplified check) - is_builtin = imp:match("^import%s+os") or imp:match("^import%s+sys") - or imp:match("^from%s+os%s+") or imp:match("^from%s+sys%s+") - or imp:match("^import%s+re") or imp:match("^import%s+json") - elseif filetype == "lua" then - -- Local: relative requires - is_local = imp:match("require%(['\"]%.") or imp:match("require%s+['\"]%.") - elseif filetype == "go" then - -- Local: project imports (contain /) - is_local = imp:match("['\"][^'\"]+/[^'\"]+['\"]") and not imp:match("github%.com") - end - - if is_builtin then + if category == "builtin" then table.insert(builtin, imp) - elseif is_local then + elseif category == "local" then table.insert(local_imports, imp) else table.insert(third_party, imp) @@ -533,7 +375,7 @@ function M.inject(bufnr, code, opts) local trimmed = line:match("^%s*(.-)%s*$") -- Skip shebang, docstrings, and initial comments if trimmed ~= "" and not trimmed:match("^#!") - and not trimmed:match("^['\"]") and not is_empty_or_comment(line, filetype) then + and not trimmed:match("^['\"]") and not utils.is_empty_or_comment(line, filetype) then insert_at = i - 1 break end diff --git a/lua/codetyper/agent/intent.lua b/lua/codetyper/agent/intent.lua index 2795e3f..75a556a 100644 --- a/lua/codetyper/agent/intent.lua +++ b/lua/codetyper/agent/intent.lua @@ -13,162 +13,10 @@ local M = {} ---@field action string "replace"|"insert"|"append"|"none" ---@field keywords string[] Keywords that triggered this intent ---- Intent patterns with associated metadata -local intent_patterns = { - -- Complete: fill in missing implementation - complete = { - patterns = { - "complete", - "finish", - "implement", - "fill in", - "fill out", - "stub", - "todo", - "fixme", - }, - scope_hint = "function", - action = "replace", - priority = 1, - }, - - -- Refactor: rewrite existing code - refactor = { - patterns = { - "refactor", - "rewrite", - "restructure", - "reorganize", - "clean up", - "cleanup", - "simplify", - "improve", - }, - scope_hint = "function", - action = "replace", - priority = 2, - }, - - -- Fix: repair bugs or issues - fix = { - patterns = { - "fix", - "repair", - "correct", - "debug", - "solve", - "resolve", - "patch", - "bug", - "error", - "issue", - "update", - "modify", - "change", - "adjust", - "tweak", - }, - scope_hint = "function", - action = "replace", - priority = 1, - }, - - -- Add: insert new code - add = { - patterns = { - "add", - "create", - "insert", - "include", - "append", - "new", - "generate", - "write", - }, - scope_hint = nil, -- Could be anywhere - action = "insert", - priority = 3, - }, - - -- Document: add documentation - document = { - patterns = { - "document", - "comment", - "jsdoc", - "docstring", - "describe", - "annotate", - "type hint", - "typehint", - }, - scope_hint = "function", - action = "replace", -- Replace with documented version - priority = 2, - }, - - -- Test: generate tests - test = { - patterns = { - "test", - "spec", - "unit test", - "integration test", - "coverage", - }, - scope_hint = "file", - action = "append", - priority = 3, - }, - - -- Optimize: improve performance - optimize = { - patterns = { - "optimize", - "performance", - "faster", - "efficient", - "speed up", - "reduce", - "minimize", - }, - scope_hint = "function", - action = "replace", - priority = 2, - }, - - -- Explain: provide explanation (no code change) - explain = { - patterns = { - "explain", - "what does", - "how does", - "why", - "describe", - "walk through", - "understand", - }, - scope_hint = "function", - action = "none", - priority = 4, - }, -} - ---- Scope hint patterns -local scope_patterns = { - ["this function"] = "function", - ["this method"] = "function", - ["the function"] = "function", - ["the method"] = "function", - ["this class"] = "class", - ["the class"] = "class", - ["this file"] = "file", - ["the file"] = "file", - ["this block"] = "block", - ["the block"] = "block", - ["this"] = nil, -- Use Tree-sitter to determine - ["here"] = nil, -} +local params = require("codetyper.params.agent.intent") +local intent_patterns = params.intent_patterns +local scope_patterns = params.scope_patterns +local prompts = require("codetyper.prompts.agent.intent") --- Detect intent from prompt content ---@param prompt string The prompt content @@ -249,55 +97,7 @@ end ---@param intent Intent ---@return string function M.get_prompt_modifier(intent) - local modifiers = { - complete = [[ -You are completing an incomplete function. -Return the complete function with all missing parts filled in. -Keep the existing signature unless changes are required. -Output only the code, no explanations.]], - - refactor = [[ -You are refactoring existing code. -Improve the code structure while maintaining the same behavior. -Keep the function signature unchanged. -Output only the refactored code, no explanations.]], - - fix = [[ -You are fixing a bug in the code. -Identify and correct the issue while minimizing changes. -Preserve the original intent of the code. -Output only the fixed code, no explanations.]], - - add = [[ -You are adding new code. -Follow the existing code style and conventions. -Output only the new code to be inserted, no explanations.]], - - document = [[ -You are adding documentation to the code. -Add appropriate comments/docstrings for the function. -Include parameter types, return types, and description. -Output the complete function with documentation.]], - - test = [[ -You are generating tests for the code. -Create comprehensive unit tests covering edge cases. -Follow the testing conventions of the project. -Output only the test code, no explanations.]], - - optimize = [[ -You are optimizing code for performance. -Improve efficiency while maintaining correctness. -Document any significant algorithmic changes. -Output only the optimized code, no explanations.]], - - explain = [[ -You are explaining code to a developer. -Provide a clear, concise explanation of what the code does. -Include information about the algorithm and any edge cases. -Do not output code, only explanation.]], - } - + local modifiers = prompts.modifiers return modifiers[intent.type] or modifiers.add end diff --git a/lua/codetyper/agent/linter.lua b/lua/codetyper/agent/linter.lua index c7dc3c4..1355126 100644 --- a/lua/codetyper/agent/linter.lua +++ b/lua/codetyper/agent/linter.lua @@ -6,17 +6,11 @@ local M = {} +local config_params = require("codetyper.params.agent.linter") +local prompts = require("codetyper.prompts.agent.linter") + --- Configuration -local config = { - -- Auto-save file after code injection - auto_save = true, - -- Delay in ms to wait for LSP diagnostics to update - diagnostic_delay_ms = 500, - -- Severity levels to check (1=Error, 2=Warning, 3=Info, 4=Hint) - min_severity = vim.diagnostic.severity.WARN, - -- Auto-offer to fix lint errors - auto_offer_fix = true, -} +local config = config_params.config --- Diagnostic results for tracking ---@type table @@ -330,7 +324,7 @@ function M.request_ai_fix(bufnr, result) -- Create fix prompt using inline tag local fix_prompt = string.format( - "Fix the following linter errors in this code:\n\nERRORS:\n%s\n\nCODE (lines %d-%d):\n%s", + prompts.fix_request, table.concat(error_list, "\n"), start_line, end_line, diff --git a/lua/codetyper/agent/logs.lua b/lua/codetyper/agent/logs.lua index 3637549..b0c922a 100644 --- a/lua/codetyper/agent/logs.lua +++ b/lua/codetyper/agent/logs.lua @@ -4,6 +4,9 @@ local M = {} +local params = require("codetyper.params.agent.logs") + + ---@class LogEntry ---@field timestamp string ISO timestamp ---@field level string "info" | "debug" | "request" | "response" | "tool" | "error" @@ -119,14 +122,7 @@ end ---@param status string "start" | "success" | "error" | "approval" ---@param details? string Additional details function M.tool(tool_name, status, details) - local icons = { - start = "->", - success = "OK", - error = "ERR", - approval = "??", - approved = "YES", - rejected = "NO", - } + local icons = params.icons local msg = string.format("[%s] %s", icons[status] or status, tool_name) if details then @@ -297,17 +293,11 @@ end ---@return string function M.format_entry(entry) -- Claude Code style formatting for thinking/action entries - local thinking_types = { "thinking", "reason", "action", "task", "result" } + local thinking_types = params.thinking_types local is_thinking = vim.tbl_contains(thinking_types, entry.level) if is_thinking then - local prefix = ({ - thinking = "⏺", - reason = "⏺", - action = "⏺", - task = "✶", - result = "", - })[entry.level] or "⏺" + local prefix = params.thinking_prefixes[entry.level] or "⏺" if prefix ~= "" then return prefix .. " " .. entry.message @@ -317,18 +307,7 @@ function M.format_entry(entry) end -- Traditional log format for other types - local level_prefix = ({ - info = "i", - debug = ".", - request = ">", - response = "<", - tool = "T", - error = "!", - warning = "?", - success = "i", - queue = "Q", - patch = "P", - })[entry.level] or "?" + local level_prefix = params.level_icons[entry.level] or "?" local base = string.format("[%s] %s %s", entry.timestamp, level_prefix, entry.message) @@ -353,15 +332,9 @@ function M.format_for_chat(entry) end -- Claude Code style formatting - local thinking_types = { "thinking", "reason", "action", "task", "result" } + local thinking_types = params.thinking_types if vim.tbl_contains(thinking_types, entry.level) then - local prefix = ({ - thinking = "⏺", - reason = "⏺", - action = "⏺", - task = "✶", - result = "", - })[entry.level] or "⏺" + local prefix = params.thinking_prefixes[entry.level] or "⏺" if prefix ~= "" then return prefix .. " " .. entry.message diff --git a/lua/codetyper/agent/loop.lua b/lua/codetyper/agent/loop.lua index 5e9048c..afba6a7 100644 --- a/lua/codetyper/agent/loop.lua +++ b/lua/codetyper/agent/loop.lua @@ -6,6 +6,8 @@ local M = {} +local prompts = require("codetyper.prompts.agent.loop") + ---@class AgentMessage ---@field role "system"|"user"|"assistant"|"tool" ---@field content string|table @@ -345,24 +347,7 @@ end function M.create(task, opts) opts = opts or {} - local system_prompt = opts.system_prompt or [[You are a helpful coding assistant with access to tools. - -Available tools: -- view: Read file contents -- grep: Search for patterns in files -- glob: Find files by pattern -- edit: Make targeted edits to files -- write: Create or overwrite files -- bash: Execute shell commands - -When you need to perform a task: -1. Use tools to gather information -2. Plan your approach -3. Execute changes using appropriate tools -4. Verify the results - -Always explain your reasoning before using tools. -When you're done, provide a clear summary of what was accomplished.]] + local system_prompt = opts.system_prompt or prompts.default_system_prompt M.run(vim.tbl_extend("force", opts, { system_prompt = system_prompt, @@ -384,9 +369,7 @@ function M.dispatch(prompt, on_complete, opts) end) M.run({ - system_prompt = [[You are a research assistant. Your task is to find information and report back. -You have access to: view (read files), grep (search content), glob (find files). -Be thorough and report your findings clearly.]], + system_prompt = prompts.dispatch_prompt, user_input = prompt, tools = opts.tools or safe_tools, max_iterations = opts.max_iterations or 5, diff --git a/lua/codetyper/agent/parser.lua b/lua/codetyper/agent/parser.lua index cac150d..a5c630e 100644 --- a/lua/codetyper/agent/parser.lua +++ b/lua/codetyper/agent/parser.lua @@ -4,6 +4,9 @@ local M = {} +local params = require("codetyper.params.agent.parser") + + ---@class ParsedResponse ---@field text string Text content from the response ---@field tool_calls ToolCall[] List of tool calls @@ -48,11 +51,11 @@ function M.parse_ollama_response(response_text) local result = { text = response_text, tool_calls = {}, - stop_reason = "end_turn", + stop_reason = params.defaults.stop_reason, } -- Pattern to find JSON tool blocks in fenced code blocks - local fenced_pattern = "```json%s*(%b{})%s*```" + local fenced_pattern = params.patterns.fenced_json -- Find all fenced JSON blocks for json_str in response_text:gmatch(fenced_pattern) do @@ -63,14 +66,14 @@ function M.parse_ollama_response(response_text) name = parsed.tool, parameters = parsed.parameters, }) - result.stop_reason = "tool_use" + result.stop_reason = params.defaults.tool_stop_reason end end -- Also try to find inline JSON (not in code blocks) -- Pattern for {"tool": "...", "parameters": {...}} if #result.tool_calls == 0 then - local inline_pattern = '(%{"tool"%s*:%s*"[^"]+"%s*,%s*"parameters"%s*:%s*%b{}%})' + local inline_pattern = params.patterns.inline_json for json_str in response_text:gmatch(inline_pattern) do local ok, parsed = pcall(vim.json.decode, json_str) if ok and parsed.tool and parsed.parameters then @@ -79,15 +82,15 @@ function M.parse_ollama_response(response_text) name = parsed.tool, parameters = parsed.parameters, }) - result.stop_reason = "tool_use" + result.stop_reason = params.defaults.tool_stop_reason end end end -- Clean tool JSON from displayed text if #result.tool_calls > 0 then - result.text = result.text:gsub("```json%s*%b{}%s*```", "[Tool call]") - result.text = result.text:gsub('%{"tool"%s*:%s*"[^"]+"%s*,%s*"parameters"%s*:%s*%b{}%}', "[Tool call]") + result.text = result.text:gsub(params.patterns.fenced_json, params.defaults.replacement_text) + result.text = result.text:gsub(params.patterns.inline_json, params.defaults.replacement_text) end return result diff --git a/lua/codetyper/agent/patch.lua b/lua/codetyper/agent/patch.lua index a461107..fdb5d43 100644 --- a/lua/codetyper/agent/patch.lua +++ b/lua/codetyper/agent/patch.lua @@ -7,6 +7,9 @@ local M = {} +local params = require("codetyper.params.agent.patch") + + --- Lazy load inject module to avoid circular requires local function get_inject_module() return require("codetyper.agent.inject") @@ -23,12 +26,7 @@ local function get_conflict_module() end --- Configuration for patch behavior -local config = { - -- Use conflict markers instead of direct apply (allows interactive review) - use_conflict_mode = true, - -- Auto-jump to first conflict after applying - auto_jump_to_conflict = true, -} +local config = params.config ---@class BufferSnapshot ---@field bufnr number Buffer number diff --git a/lua/codetyper/agent/permissions.lua b/lua/codetyper/agent/permissions.lua index 421d0b2..e9265c8 100644 --- a/lua/codetyper/agent/permissions.lua +++ b/lua/codetyper/agent/permissions.lua @@ -10,6 +10,8 @@ local M = {} ---@field allow_list table Patterns always allowed ---@field deny_list table Patterns always denied +local params = require("codetyper.params.agent.permissions") + local state = { session_allowed = {}, allow_list = {}, @@ -17,59 +19,10 @@ local state = { } --- Dangerous command patterns that should never be auto-allowed -local DANGEROUS_PATTERNS = { - "^rm%s+%-rf", - "^rm%s+%-r%s+/", - "^rm%s+/", - "^sudo%s+rm", - "^chmod%s+777", - "^chmod%s+%-R", - "^chown%s+%-R", - "^dd%s+", - "^mkfs", - "^fdisk", - "^format", - ":.*>%s*/dev/", - "^curl.*|.*sh", - "^wget.*|.*sh", - "^eval%s+", - "`;.*`", - "%$%(.*%)", - "fork%s*bomb", -} +local DANGEROUS_PATTERNS = params.dangerous_patterns --- Safe command patterns that can be auto-allowed -local SAFE_PATTERNS = { - "^ls%s", - "^ls$", - "^cat%s", - "^head%s", - "^tail%s", - "^grep%s", - "^find%s", - "^pwd$", - "^echo%s", - "^wc%s", - "^which%s", - "^type%s", - "^file%s", - "^stat%s", - "^git%s+status", - "^git%s+log", - "^git%s+diff", - "^git%s+branch", - "^git%s+show", - "^npm%s+list", - "^npm%s+ls", - "^npm%s+outdated", - "^yarn%s+list", - "^cargo%s+check", - "^cargo%s+test", - "^go%s+test", - "^go%s+build", - "^make%s+test", - "^make%s+check", -} +local SAFE_PATTERNS = params.safe_patterns ---@alias PermissionLevel "allow"|"allow_session"|"allow_list"|"reject" diff --git a/lua/codetyper/agent/scheduler.lua b/lua/codetyper/agent/scheduler.lua index 3f01ca6..a344154 100644 --- a/lua/codetyper/agent/scheduler.lua +++ b/lua/codetyper/agent/scheduler.lua @@ -11,6 +11,7 @@ local patch = require("codetyper.agent.patch") local worker = require("codetyper.agent.worker") local confidence_mod = require("codetyper.agent.confidence") local context_modal = require("codetyper.agent.context_modal") +local params = require("codetyper.params.agent.scheduler") -- Setup context modal cleanup on exit context_modal.setup() @@ -21,15 +22,7 @@ local state = { timer = nil, poll_interval = 100, -- ms paused = false, - config = { - enabled = true, - ollama_scout = true, - escalation_threshold = 0.7, - max_concurrent = 2, - completion_delay_ms = 100, - apply_delay_ms = 5000, -- Wait before applying code - remote_provider = "copilot", -- Default fallback provider - }, + config = params.config, } --- Autocommand group for injection timing diff --git a/lua/codetyper/agent/scope.lua b/lua/codetyper/agent/scope.lua index b1005ee..f52bed8 100644 --- a/lua/codetyper/agent/scope.lua +++ b/lua/codetyper/agent/scope.lua @@ -14,62 +14,10 @@ local M = {} ---@field name string|nil Name of the function/class if available --- Node types that represent function-like scopes per language -local function_nodes = { - -- Lua - ["function_declaration"] = "function", - ["function_definition"] = "function", - ["local_function"] = "function", - ["function"] = "function", - - -- JavaScript/TypeScript - ["function_declaration"] = "function", - ["function_expression"] = "function", - ["arrow_function"] = "function", - ["method_definition"] = "method", - ["function"] = "function", - - -- Python - ["function_definition"] = "function", - ["async_function_definition"] = "function", - - -- Go - ["function_declaration"] = "function", - ["method_declaration"] = "method", - - -- Rust - ["function_item"] = "function", - ["impl_item"] = "method", - - -- Ruby - ["method"] = "method", - ["singleton_method"] = "method", - - -- Java/C# - ["method_declaration"] = "method", - ["constructor_declaration"] = "method", - - -- C/C++ - ["function_definition"] = "function", -} - ---- Node types that represent class-like scopes -local class_nodes = { - ["class_declaration"] = "class", - ["class_definition"] = "class", - ["class"] = "class", - ["struct_item"] = "class", - ["impl_item"] = "class", - ["interface_declaration"] = "class", - ["module"] = "class", -} - ---- Node types that represent block scopes -local block_nodes = { - ["block"] = "block", - ["statement_block"] = "block", - ["compound_statement"] = "block", - ["do_block"] = "block", -} +local params = require("codetyper.params.agent.scope") +local function_nodes = params.function_nodes +local class_nodes = params.class_nodes +local block_nodes = params.block_nodes --- Check if Tree-sitter is available for buffer ---@param bufnr number diff --git a/lua/codetyper/agent/search_replace.lua b/lua/codetyper/agent/search_replace.lua index cba2812..9ff3612 100644 --- a/lua/codetyper/agent/search_replace.lua +++ b/lua/codetyper/agent/search_replace.lua @@ -6,6 +6,8 @@ local M = {} +local params = require("codetyper.params.agent.search_replace").patterns + ---@class SearchReplaceBlock ---@field search string The text to search for ---@field replace string The text to replace with @@ -48,7 +50,7 @@ function M.parse_blocks(response) local blocks = {} -- Try dash-style format: ------- SEARCH ... ======= ... +++++++ REPLACE - for search, replace in response:gmatch("%-%-%-%-%-%-%-?%s*SEARCH%s*\n(.-)\n=======%s*\n(.-)\n%+%+%+%+%+%+%+?%s*REPLACE") do + for search, replace in response:gmatch(params.dash_style) do table.insert(blocks, { search = search, replace = replace }) end @@ -57,7 +59,7 @@ function M.parse_blocks(response) end -- Try claude-style format: <<<<<<< SEARCH ... ======= ... >>>>>>> REPLACE - for search, replace in response:gmatch("<<<<<<<[%s]*SEARCH%s*\n(.-)\n=======%s*\n(.-)\n>>>>>>>[%s]*REPLACE") do + for search, replace in response:gmatch(params.claude_style) do table.insert(blocks, { search = search, replace = replace }) end @@ -66,7 +68,7 @@ function M.parse_blocks(response) end -- Try simple format: [SEARCH] ... [REPLACE] ... [END] - for search, replace in response:gmatch("%[SEARCH%]%s*\n(.-)\n%[REPLACE%]%s*\n(.-)\n%[END%]") do + for search, replace in response:gmatch(params.simple_style) do table.insert(blocks, { search = search, replace = replace }) end @@ -75,7 +77,7 @@ function M.parse_blocks(response) end -- Try markdown diff format: ```diff ... ``` - local diff_block = response:match("```diff\n(.-)\n```") + local diff_block = response:match(params.diff_block) if diff_block then local old_lines = {} local new_lines = {} diff --git a/lua/codetyper/agent/tools.lua b/lua/codetyper/agent/tools.lua index 8ae21b2..e8e371b 100644 --- a/lua/codetyper/agent/tools.lua +++ b/lua/codetyper/agent/tools.lua @@ -5,144 +5,7 @@ local M = {} --- Tool definitions in a provider-agnostic format -M.definitions = { - read_file = { - name = "read_file", - description = "Read the contents of a file at the specified path", - parameters = { - type = "object", - properties = { - path = { - type = "string", - description = "Absolute or relative path to the file to read", - }, - }, - required = { "path" }, - }, - }, - - edit_file = { - name = "edit_file", - description = "Edit a file by replacing specific content. Provide the exact content to find and the replacement.", - parameters = { - type = "object", - properties = { - path = { - type = "string", - description = "Path to the file to edit", - }, - find = { - type = "string", - description = "Exact content to find (must match exactly, including whitespace)", - }, - replace = { - type = "string", - description = "Content to replace with", - }, - }, - required = { "path", "find", "replace" }, - }, - }, - - write_file = { - name = "write_file", - description = "Write content to a file, creating it if it doesn't exist or overwriting if it does", - parameters = { - type = "object", - properties = { - path = { - type = "string", - description = "Path to the file to write", - }, - content = { - type = "string", - description = "Complete file content to write", - }, - }, - required = { "path", "content" }, - }, - }, - - bash = { - name = "bash", - description = "Execute a bash command and return the output. Use for git, npm, build tools, etc.", - parameters = { - type = "object", - properties = { - command = { - type = "string", - description = "The bash command to execute", - }, - timeout = { - type = "number", - description = "Timeout in milliseconds (default: 30000)", - }, - }, - required = { "command" }, - }, - }, - - delete_file = { - name = "delete_file", - description = "Delete a file from the filesystem. Use with caution - requires explicit user approval.", - parameters = { - type = "object", - properties = { - path = { - type = "string", - description = "Path to the file to delete", - }, - reason = { - type = "string", - description = "Reason for deleting this file (shown to user for approval)", - }, - }, - required = { "path", "reason" }, - }, - }, - - list_directory = { - name = "list_directory", - description = "List files and directories in a path. Use to explore project structure.", - parameters = { - type = "object", - properties = { - path = { - type = "string", - description = "Path to the directory to list (defaults to current directory)", - }, - recursive = { - type = "boolean", - description = "Whether to list recursively (default: false, max depth: 3)", - }, - }, - required = {}, - }, - }, - - search_files = { - name = "search_files", - description = "Search for files by name pattern or content. Use to find relevant files in the project.", - parameters = { - type = "object", - properties = { - pattern = { - type = "string", - description = "Glob pattern for file names (e.g., '*.lua', 'test_*.py')", - }, - content = { - type = "string", - description = "Search for files containing this text", - }, - path = { - type = "string", - description = "Directory to search in (defaults to project root)", - }, - }, - required = {}, - }, - }, -} +M.definitions = require("codetyper.params.agent.tools").definitions --- Convert tool definitions to Claude API format ---@return table[] Tools in Claude's expected format @@ -178,8 +41,9 @@ end --- Convert tool definitions to prompt format for Ollama ---@return string Formatted tool descriptions for system prompt function M.to_prompt_format() + local prompts = require("codetyper.prompts.agent.tools").instructions local lines = { - "You have access to the following tools. To use a tool, respond with a JSON block.", + prompts.intro, "", } @@ -198,13 +62,10 @@ function M.to_prompt_format() table.insert(lines, "---") table.insert(lines, "") - table.insert(lines, "To call a tool, output a JSON block like this:") - table.insert(lines, "```json") - table.insert(lines, '{"tool": "tool_name", "parameters": {"param1": "value1"}}') - table.insert(lines, "```") + table.insert(lines, prompts.header) + table.insert(lines, prompts.example) table.insert(lines, "") - table.insert(lines, "After receiving tool results, continue your response or call another tool.") - table.insert(lines, "When you're done, just respond normally without any tool calls.") + table.insert(lines, prompts.footer) return table.concat(lines, "\n") end diff --git a/lua/codetyper/agent/worker.lua b/lua/codetyper/agent/worker.lua index 5e48af1..7d520a9 100644 --- a/lua/codetyper/agent/worker.lua +++ b/lua/codetyper/agent/worker.lua @@ -6,6 +6,7 @@ local M = {} +local params = require("codetyper.params.agent.worker") local confidence = require("codetyper.agent.confidence") ---@class WorkerResult @@ -32,20 +33,7 @@ local confidence = require("codetyper.agent.confidence") local worker_counter = 0 --- Patterns that indicate LLM needs more context (must be near start of response) -local context_needed_patterns = { - "^%s*i need more context", - "^%s*i'm sorry.-i need more", - "^%s*i apologize.-i need more", - "^%s*could you provide more context", - "^%s*could you please provide more", - "^%s*can you clarify", - "^%s*please provide more context", - "^%s*more information needed", - "^%s*not enough context", - "^%s*i don't have enough", - "^%s*unclear what you", - "^%s*what do you mean by", -} +local context_needed_patterns = params.context_needed_patterns --- Check if response indicates need for more context --- Only triggers if the response primarily asks for context (no substantial code) @@ -196,12 +184,7 @@ end local active_workers = {} --- Default timeouts by provider type -local default_timeouts = { - ollama = 30000, -- 30s for local - openai = 60000, -- 60s for remote - gemini = 60000, - copilot = 60000, -} +local default_timeouts = params.default_timeouts --- Generate worker ID ---@return string diff --git a/lua/codetyper/params/agent/confidence.lua b/lua/codetyper/params/agent/confidence.lua new file mode 100644 index 0000000..c6e2bfe --- /dev/null +++ b/lua/codetyper/params/agent/confidence.lua @@ -0,0 +1,40 @@ +---@mod codetyper.params.agent.confidence Parameters for confidence scoring +local M = {} + +--- Heuristic weights (must sum to 1.0) +M.weights = { + length = 0.15, -- Response length relative to prompt + uncertainty = 0.30, -- Uncertainty phrases + syntax = 0.25, -- Syntax completeness + repetition = 0.15, -- Duplicate lines + truncation = 0.15, -- Incomplete ending +} + +--- Uncertainty phrases that indicate low confidence +M.uncertainty_phrases = { + -- English + "i'm not sure", + "i am not sure", + "maybe", + "perhaps", + "might work", + "could work", + "not certain", + "uncertain", + "i think", + "possibly", + "TODO", + "FIXME", + "XXX", + "placeholder", + "implement this", + "fill in", + "your code here", + "...", -- Ellipsis as placeholder + "# TODO", + "// TODO", + "-- TODO", + "/* TODO", +} + +return M diff --git a/lua/codetyper/params/agent/conflict.lua b/lua/codetyper/params/agent/conflict.lua new file mode 100644 index 0000000..22e2800 --- /dev/null +++ b/lua/codetyper/params/agent/conflict.lua @@ -0,0 +1,33 @@ +---@mod codetyper.params.agent.conflict Parameters for conflict resolution +local M = {} + +--- Configuration defaults +M.config = { + -- Run linter check after accepting AI suggestions + lint_after_accept = true, + -- Auto-fix lint errors without prompting + auto_fix_lint_errors = true, + -- Auto-show menu after injecting conflict + auto_show_menu = true, + -- Auto-show menu for next conflict after resolving one + auto_show_next_menu = true, +} + +--- Highlight groups +M.hl_groups = { + current = "CoderConflictCurrent", + current_label = "CoderConflictCurrentLabel", + incoming = "CoderConflictIncoming", + incoming_label = "CoderConflictIncomingLabel", + separator = "CoderConflictSeparator", + hint = "CoderConflictHint", +} + +--- Conflict markers +M.markers = { + current_start = "<<<<<<< CURRENT", + separator = "=======", + incoming_end = ">>>>>>> INCOMING", +} + +return M diff --git a/lua/codetyper/params/agent/context.lua b/lua/codetyper/params/agent/context.lua new file mode 100644 index 0000000..7954763 --- /dev/null +++ b/lua/codetyper/params/agent/context.lua @@ -0,0 +1,48 @@ +---@mod codetyper.params.agent.context Parameters for context building +local M = {} + +--- Common ignore patterns +M.ignore_patterns = { + "^%.", -- Hidden files/dirs + "node_modules", + "%.git$", + "__pycache__", + "%.pyc$", + "target", -- Rust + "build", + "dist", + "%.o$", + "%.a$", + "%.so$", + "%.min%.", + "%.map$", +} + +--- Key files that are important for understanding the project +M.important_files = { + ["package.json"] = "Node.js project config", + ["Cargo.toml"] = "Rust project config", + ["go.mod"] = "Go module config", + ["pyproject.toml"] = "Python project config", + ["setup.py"] = "Python setup config", + ["Makefile"] = "Build configuration", + ["CMakeLists.txt"] = "CMake config", + [".gitignore"] = "Git ignore patterns", + ["README.md"] = "Project documentation", + ["init.lua"] = "Neovim plugin entry", + ["plugin.lua"] = "Neovim plugin config", +} + +--- Project type detection indicators +M.indicators = { + ["package.json"] = { type = "node", language = "javascript/typescript" }, + ["Cargo.toml"] = { type = "rust", language = "rust" }, + ["go.mod"] = { type = "go", language = "go" }, + ["pyproject.toml"] = { type = "python", language = "python" }, + ["setup.py"] = { type = "python", language = "python" }, + ["Gemfile"] = { type = "ruby", language = "ruby" }, + ["pom.xml"] = { type = "maven", language = "java" }, + ["build.gradle"] = { type = "gradle", language = "java/kotlin" }, +} + +return M diff --git a/lua/codetyper/params/agent/intent.lua b/lua/codetyper/params/agent/intent.lua new file mode 100644 index 0000000..52dd8b0 --- /dev/null +++ b/lua/codetyper/params/agent/intent.lua @@ -0,0 +1,161 @@ +---@mod codetyper.params.agent.intent Intent patterns and scope configuration +local M = {} + +--- Intent patterns with associated metadata +M.intent_patterns = { + -- Complete: fill in missing implementation + complete = { + patterns = { + "complete", + "finish", + "implement", + "fill in", + "fill out", + "stub", + "todo", + "fixme", + }, + scope_hint = "function", + action = "replace", + priority = 1, + }, + + -- Refactor: rewrite existing code + refactor = { + patterns = { + "refactor", + "rewrite", + "restructure", + "reorganize", + "clean up", + "cleanup", + "simplify", + "improve", + }, + scope_hint = "function", + action = "replace", + priority = 2, + }, + + -- Fix: repair bugs or issues + fix = { + patterns = { + "fix", + "repair", + "correct", + "debug", + "solve", + "resolve", + "patch", + "bug", + "error", + "issue", + "update", + "modify", + "change", + "adjust", + "tweak", + }, + scope_hint = "function", + action = "replace", + priority = 1, + }, + + -- Add: insert new code + add = { + patterns = { + "add", + "create", + "insert", + "include", + "append", + "new", + "generate", + "write", + }, + scope_hint = nil, -- Could be anywhere + action = "insert", + priority = 3, + }, + + -- Document: add documentation + document = { + patterns = { + "document", + "comment", + "jsdoc", + "docstring", + "describe", + "annotate", + "type hint", + "typehint", + }, + scope_hint = "function", + action = "replace", -- Replace with documented version + priority = 2, + }, + + -- Test: generate tests + test = { + patterns = { + "test", + "spec", + "unit test", + "integration test", + "coverage", + }, + scope_hint = "file", + action = "append", + priority = 3, + }, + + -- Optimize: improve performance + optimize = { + patterns = { + "optimize", + "performance", + "faster", + "efficient", + "speed up", + "reduce", + "minimize", + }, + scope_hint = "function", + action = "replace", + priority = 2, + }, + + -- Explain: provide explanation (no code change) + explain = { + patterns = { + "explain", + "what does", + "how does", + "why", + "describe", + "walk through", + "understand", + }, + scope_hint = "function", + action = "none", + priority = 4, + }, +} + +--- Scope hint patterns +M.scope_patterns = { + ["this function"] = "function", + ["this method"] = "function", + ["the function"] = "function", + ["the method"] = "function", + ["this class"] = "class", + ["the class"] = "class", + ["this file"] = "file", + ["the file"] = "file", + ["this block"] = "block", + ["the block"] = "block", + ["this"] = nil, -- Use Tree-sitter to determine + ["here"] = nil, +} + +return M diff --git a/lua/codetyper/params/agent/languages.lua b/lua/codetyper/params/agent/languages.lua new file mode 100644 index 0000000..fabb959 --- /dev/null +++ b/lua/codetyper/params/agent/languages.lua @@ -0,0 +1,87 @@ +---@mod codetyper.params.agent.languages Language-specific patterns and configurations +local M = {} + +--- Language-specific import patterns +M.import_patterns = { + -- JavaScript/TypeScript + javascript = { + { pattern = "^%s*import%s+.+%s+from%s+['\"]", multi_line = true }, + { pattern = "^%s*import%s+['\"]", multi_line = false }, + { pattern = "^%s*import%s*{", multi_line = true }, + { pattern = "^%s*import%s*%*", multi_line = true }, + { pattern = "^%s*export%s+{.+}%s+from%s+['\"]", multi_line = true }, + { pattern = "^%s*const%s+%w+%s*=%s*require%(['\"]", multi_line = false }, + { pattern = "^%s*let%s+%w+%s*=%s*require%(['\"]", multi_line = false }, + { pattern = "^%s*var%s+%w+%s*=%s*require%(['\"]", multi_line = false }, + }, + -- Python + python = { + { pattern = "^%s*import%s+%w", multi_line = false }, + { pattern = "^%s*from%s+[%w%.]+%s+import%s+", multi_line = true }, + }, + -- Lua + lua = { + { pattern = "^%s*local%s+%w+%s*=%s*require%s*%(?['\"]", multi_line = false }, + { pattern = "^%s*require%s*%(?['\"]", multi_line = false }, + }, + -- Go + go = { + { pattern = "^%s*import%s+%(?", multi_line = true }, + }, + -- Rust + rust = { + { pattern = "^%s*use%s+", multi_line = true }, + { pattern = "^%s*extern%s+crate%s+", multi_line = false }, + }, + -- C/C++ + c = { + { pattern = "^%s*#include%s*[<\"]", multi_line = false }, + }, + -- Java/Kotlin + java = { + { pattern = "^%s*import%s+", multi_line = false }, + }, + -- Ruby + ruby = { + { pattern = "^%s*require%s+['\"]", multi_line = false }, + { pattern = "^%s*require_relative%s+['\"]", multi_line = false }, + }, + -- PHP + php = { + { pattern = "^%s*use%s+", multi_line = false }, + { pattern = "^%s*require%s+['\"]", multi_line = false }, + { pattern = "^%s*require_once%s+['\"]", multi_line = false }, + { pattern = "^%s*include%s+['\"]", multi_line = false }, + { pattern = "^%s*include_once%s+['\"]", multi_line = false }, + }, +} + +-- Alias common extensions to language configs +M.import_patterns.ts = M.import_patterns.javascript +M.import_patterns.tsx = M.import_patterns.javascript +M.import_patterns.jsx = M.import_patterns.javascript +M.import_patterns.mjs = M.import_patterns.javascript +M.import_patterns.cjs = M.import_patterns.javascript +M.import_patterns.py = M.import_patterns.python +M.import_patterns.cpp = M.import_patterns.c +M.import_patterns.hpp = M.import_patterns.c +M.import_patterns.h = M.import_patterns.c +M.import_patterns.kt = M.import_patterns.java +M.import_patterns.rs = M.import_patterns.rust +M.import_patterns.rb = M.import_patterns.ruby + +--- Language-specific comment patterns +M.comment_patterns = { + lua = { "^%-%-" }, + python = { "^#" }, + javascript = { "^//", "^/%*", "^%*" }, + typescript = { "^//", "^/%*", "^%*" }, + go = { "^//", "^/%*", "^%*" }, + rust = { "^//", "^/%*", "^%*" }, + c = { "^//", "^/%*", "^%*", "^#" }, + java = { "^//", "^/%*", "^%*" }, + ruby = { "^#" }, + php = { "^//", "^/%*", "^%*", "^#" }, +} + +return M diff --git a/lua/codetyper/params/agent/linter.lua b/lua/codetyper/params/agent/linter.lua new file mode 100644 index 0000000..b42ff3d --- /dev/null +++ b/lua/codetyper/params/agent/linter.lua @@ -0,0 +1,15 @@ +---@mod codetyper.params.agent.linter Linter configuration +local M = {} + +M.config = { + -- Auto-save file after code injection + auto_save = true, + -- Delay in ms to wait for LSP diagnostics to update + diagnostic_delay_ms = 500, + -- Severity levels to check (1=Error, 2=Warning, 3=Info, 4=Hint) + min_severity = vim.diagnostic.severity.WARN, + -- Auto-offer to fix lint errors + auto_offer_fix = true, +} + +return M diff --git a/lua/codetyper/params/agent/logs.lua b/lua/codetyper/params/agent/logs.lua new file mode 100644 index 0000000..d3f364b --- /dev/null +++ b/lua/codetyper/params/agent/logs.lua @@ -0,0 +1,36 @@ +---@mod codetyper.params.agent.logs Log parameters +local M = {} + +M.icons = { + start = "->", + success = "OK", + error = "ERR", + approval = "??", + approved = "YES", + rejected = "NO", +} + +M.level_icons = { + info = "i", + debug = ".", + request = ">", + response = "<", + tool = "T", + error = "!", + warning = "?", + success = "i", + queue = "Q", + patch = "P", +} + +M.thinking_types = { "thinking", "reason", "action", "task", "result" } + +M.thinking_prefixes = { + thinking = "⏺", + reason = "⏺", + action = "⏺", + task = "✶", + result = "", +} + +return M \ No newline at end of file diff --git a/lua/codetyper/params/agent/parser.lua b/lua/codetyper/params/agent/parser.lua new file mode 100644 index 0000000..7608c6b --- /dev/null +++ b/lua/codetyper/params/agent/parser.lua @@ -0,0 +1,15 @@ +---@mod codetyper.params.agent.parser Parser regex patterns +local M = {} + +M.patterns = { + fenced_json = "```json%s*(%b{})%s*```", + inline_json = '(%{"tool"%s*:%s*"[^"]+"%s*,%s*"parameters"%s*:%s*%b{}%})', +} + +M.defaults = { + stop_reason = "end_turn", + tool_stop_reason = "tool_use", + replacement_text = "[Tool call]", +} + +return M \ No newline at end of file diff --git a/lua/codetyper/params/agent/patch.lua b/lua/codetyper/params/agent/patch.lua new file mode 100644 index 0000000..2a46410 --- /dev/null +++ b/lua/codetyper/params/agent/patch.lua @@ -0,0 +1,12 @@ +---@mod codetyper.params.agent.patch Patch configuration +local M = {} + +M.config = { + snapshot_range = 5, -- Lines above/below prompt to snapshot + clean_interval_ms = 60000, -- Check for stale patches every minute + max_age_ms = 3600000, -- 1 hour TTL + staleness_check = true, + use_search_replace_parser = true, -- Enable new parsing logic +} + +return M diff --git a/lua/codetyper/params/agent/permissions.lua b/lua/codetyper/params/agent/permissions.lua new file mode 100644 index 0000000..804567b --- /dev/null +++ b/lua/codetyper/params/agent/permissions.lua @@ -0,0 +1,47 @@ +---@mod codetyper.params.agent.permissions Dangerous and safe command patterns +local M = {} + +--- Dangerous command patterns that should never be auto-allowed +M.dangerous_patterns = { + "^rm%s+%-rf", + "^rm%s+%-r%s+/", + "^rm%s+/", + "^sudo%s+rm", + "^chmod%s+777", + "^chmod%s+%-R", + "^chown%s+%-R", + "^dd%s+", + "^mkfs", + "^fdisk", + "^format", + ":.*>%s*/dev/", + "^curl.*|.*sh", + "^wget.*|.*sh", + "^eval%s+", + "`;.*`", + "%$%(.*%)", + "fork%s*bomb", +} + +--- Safe command patterns that can be auto-allowed +M.safe_patterns = { + "^ls%s", + "^ls$", + "^cat%s", + "^head%s", + "^tail%s", + "^grep%s", + "^find%s", + "^pwd$", + "^echo%s", + "^wc%s", + "^git%s+status", + "^git%s+diff", + "^git%s+log", + "^git%s+show", + "^git%s+branch", + "^git%s+checkout", + "^git%s+add", -- Generally safe if reviewing changes +} + +return M diff --git a/lua/codetyper/params/agent/scheduler.lua b/lua/codetyper/params/agent/scheduler.lua new file mode 100644 index 0000000..cdc3b85 --- /dev/null +++ b/lua/codetyper/params/agent/scheduler.lua @@ -0,0 +1,14 @@ +---@mod codetyper.params.agent.scheduler Scheduler configuration +local M = {} + +M.config = { + enabled = true, + ollama_scout = true, + escalation_threshold = 0.7, + max_concurrent = 2, + completion_delay_ms = 100, + apply_delay_ms = 5000, -- Wait before applying code + remote_provider = "copilot", -- Default fallback provider +} + +return M diff --git a/lua/codetyper/params/agent/scope.lua b/lua/codetyper/params/agent/scope.lua new file mode 100644 index 0000000..ad99d2c --- /dev/null +++ b/lua/codetyper/params/agent/scope.lua @@ -0,0 +1,72 @@ +---@mod codetyper.params.agent.scope Tree-sitter scope mappings +local M = {} + +--- Node types that represent function-like scopes per language +M.function_nodes = { + -- Lua + ["function_declaration"] = "function", + ["function_definition"] = "function", + ["local_function"] = "function", + ["function"] = "function", + + -- JavaScript/TypeScript + ["function_declaration"] = "function", + ["function_expression"] = "function", + ["arrow_function"] = "function", + ["method_definition"] = "method", + ["function"] = "function", + + -- Python + ["function_definition"] = "function", + ["lambda"] = "function", + + -- Go + ["function_declaration"] = "function", + ["method_declaration"] = "method", + ["func_literal"] = "function", + + -- Rust + ["function_item"] = "function", + ["closure_expression"] = "function", + + -- C/C++ + ["function_definition"] = "function", + ["lambda_expression"] = "function", + + -- Java + ["method_declaration"] = "method", + ["constructor_declaration"] = "method", + ["lambda_expression"] = "function", + + -- Ruby + ["method"] = "method", + ["singleton_method"] = "method", + ["lambda"] = "function", + ["block"] = "function", + + -- PHP + ["function_definition"] = "function", + ["method_declaration"] = "method", + ["arrow_function"] = "function", +} + +--- Node types that represent class-like scopes +M.class_nodes = { + ["class_declaration"] = "class", + ["class_definition"] = "class", + ["struct_declaration"] = "class", + ["impl_item"] = "class", -- Rust config + ["interface_declaration"] = "class", + ["trait_item"] = "class", +} + +--- Node types that represent block scopes +M.block_nodes = { + ["block"] = "block", + ["do_statement"] = "block", -- Lua + ["if_statement"] = "block", + ["for_statement"] = "block", + ["while_statement"] = "block", +} + +return M diff --git a/lua/codetyper/params/agent/search_replace.lua b/lua/codetyper/params/agent/search_replace.lua new file mode 100644 index 0000000..16793fb --- /dev/null +++ b/lua/codetyper/params/agent/search_replace.lua @@ -0,0 +1,11 @@ +---@mod codetyper.params.agent.search_replace Search/Replace patterns +local M = {} + +M.patterns = { + dash_style = "%-%-%-%-%-%-%-?%s*SEARCH%s*\n(.-)\n=======%s*\n(.-)\n%+%+%+%+%+%+%+?%s*REPLACE", + claude_style = "<<<<<<<[%s]*SEARCH%s*\n(.-)\n=======%s*\n(.-)\n>>>>>>>[%s]*REPLACE", + simple_style = "%[SEARCH%]%s*\n(.-)\n%[REPLACE%]%s*\n(.-)\n%[END%]", + diff_block = "```diff\n(.-)\n```", +} + +return M \ No newline at end of file diff --git a/lua/codetyper/params/agent/tools.lua b/lua/codetyper/params/agent/tools.lua new file mode 100644 index 0000000..b0df5ca --- /dev/null +++ b/lua/codetyper/params/agent/tools.lua @@ -0,0 +1,147 @@ +---@mod codetyper.params.agent.tools Tool definitions +local M = {} + +--- Tool definitions in a provider-agnostic format +M.definitions = { + read_file = { + name = "read_file", + description = "Read the contents of a file at the specified path", + parameters = { + type = "object", + properties = { + path = { + type = "string", + description = "The path to the file to read", + }, + start_line = { + type = "number", + description = "Optional start line number (1-indexed)", + }, + end_line = { + type = "number", + description = "Optional end line number (1-indexed)", + }, + }, + required = { "path" }, + }, + }, + + edit_file = { + name = "edit_file", + description = "Edit a file by replacing specific content. Provide the exact content to find and the replacement.", + parameters = { + type = "object", + properties = { + path = { + type = "string", + description = "The path to the file to edit", + }, + find = { + type = "string", + description = "The exact content to replace", + }, + replace = { + type = "string", + description = "The new content", + }, + }, + required = { "path", "find", "replace" }, + }, + }, + + write_file = { + name = "write_file", + description = "Write content to a file, creating it if it doesn't exist or overwriting if it does", + parameters = { + type = "object", + properties = { + path = { + type = "string", + description = "The path to the file to write", + }, + content = { + type = "string", + description = "The content to write", + }, + }, + required = { "path", "content" }, + }, + }, + + bash = { + name = "bash", + description = "Execute a bash command and return the output. Use for git, npm, build tools, etc.", + parameters = { + type = "object", + properties = { + command = { + type = "string", + description = "The bash command to execute", + }, + }, + required = { "command" }, + }, + }, + + delete_file = { + name = "delete_file", + description = "Delete a file", + parameters = { + type = "object", + properties = { + path = { + type = "string", + description = "The path to the file to delete", + }, + reason = { + type = "string", + description = "Reason for deletion", + }, + }, + required = { "path", "reason" }, + }, + }, + + list_directory = { + name = "list_directory", + description = "List files and directories in a path", + parameters = { + type = "object", + properties = { + path = { + type = "string", + description = "The path to list", + }, + recursive = { + type = "boolean", + description = "Whether to list recursively", + }, + }, + required = { "path" }, + }, + }, + + search_files = { + name = "search_files", + description = "Search for files by name/glob pattern or content", + parameters = { + type = "object", + properties = { + pattern = { + type = "string", + description = "Glob pattern to search for filenames", + }, + content = { + type = "string", + description = "Content string to search for within files", + }, + path = { + type = "string", + description = "The root path to start search", + }, + }, + }, + }, +} + +return M diff --git a/lua/codetyper/params/agent/worker.lua b/lua/codetyper/params/agent/worker.lua new file mode 100644 index 0000000..5289b2c --- /dev/null +++ b/lua/codetyper/params/agent/worker.lua @@ -0,0 +1,30 @@ +---@mod codetyper.params.agent.worker Worker configuration and patterns +local M = {} + +--- Patterns that indicate LLM needs more context (must be near start of response) +M.context_needed_patterns = { + "I need to see", + "Could you provide", + "Please provide", + "Can you show", + "don't have enough context", + "need more information", + "cannot see the definition", + "missing the implementation", + "I would need to check", + "please share", + "Please upload", + "could not find", +} + +--- Default timeouts by provider type +M.default_timeouts = { + openai = 60000, -- 60s + anthropic = 90000, -- 90s + google = 60000, -- 60s + ollama = 120000, -- 120s (local models can be slower) + copilot = 60000, -- 60s + default = 60000, +} + +return M diff --git a/lua/codetyper/prompts/agent.lua b/lua/codetyper/prompts/agent.lua index 3cd8e72..1b1f662 100644 --- a/lua/codetyper/prompts/agent.lua +++ b/lua/codetyper/prompts/agent.lua @@ -109,4 +109,33 @@ Include: Do NOT restate tool output verbatim. ]] +--- Text-based tool calling instructions +M.tool_instructions_text = [[ + +## Available Tools +Call tools by outputting JSON in this format: +```json +{"tool": "tool_name", "arguments": {...}} +``` +]] + +--- Initial greeting when files are provided +M.initial_assistant_message = "I've reviewed the provided files. What would you like me to do?" + +--- Format prefixes for text-based models +M.text_user_prefix = "User: " +M.text_assistant_prefix = "Assistant: " + +--- Format file context +---@param files string[] Paths +---@return string Formatted context +function M.format_file_context(files) + local context = "# Initial Files\n" + for _, file_path in ipairs(files) do + local content = table.concat(vim.fn.readfile(file_path) or {}, "\n") + context = context .. string.format("\n## %s\n```\n%s\n```\n", file_path, content) + end + return context +end + return M diff --git a/lua/codetyper/prompts/agent/diff.lua b/lua/codetyper/prompts/agent/diff.lua new file mode 100644 index 0000000..97303c8 --- /dev/null +++ b/lua/codetyper/prompts/agent/diff.lua @@ -0,0 +1,66 @@ +---@mod codetyper.prompts.agent.diff Prompts and UI strings for diff view and bash approval +local M = {} + +--- Bash approval dialog strings +M.bash_approval = { + title = " BASH COMMAND APPROVAL", + divider = " " .. string.rep("─", 56), + command_label = " Command:", + warning_prefix = " ⚠️ WARNING: ", + options = { + " [y] Allow once - Execute this command", + " [s] Allow this session - Auto-allow until restart", + " [a] Add to allow list - Always allow this command", + " [n] Reject - Cancel execution", + }, + cancel_hint = " Press key to choose | [q] or [Esc] to cancel", +} + +--- Diff view help message +M.diff_help = { + { "Diff: ", "Normal" }, + { "{path}", "Directory" }, + { " | ", "Normal" }, + { "y/", "Keyword" }, + { " approve ", "Normal" }, + { "n/q/", "Keyword" }, + { " reject ", "Normal" }, + { "", "Keyword" }, + { " switch panes", "Normal" }, +} + + +--- Review UI interface strings +M.review = { + diff_header = { + top = "╭─ %s %s %s ─────────────────────────────────────", + path = "│ %s", + op = "│ Operation: %s", + status = "│ Status: %s", + bottom = "╰────────────────────────────────────────────────────", + }, + list_menu = { + top = "╭─ Changes (%s) ──────────╮", + items = { + "│ │", + "│ j/k: navigate │", + "│ Enter: view diff │", + "│ a: approve r: reject │", + "│ A: approve all │", + "│ q: close │", + }, + bottom = "╰──────────────────────────────╯", + }, + status = { + applied = "Applied", + approved = "Approved", + pending = "Pending", + }, + messages = { + no_changes = " No changes to review", + no_changes_short = "No changes to review", + applied_count = "Applied %d change(s)", + }, +} + +return M diff --git a/lua/codetyper/prompts/agent/intent.lua b/lua/codetyper/prompts/agent/intent.lua new file mode 100644 index 0000000..616c616 --- /dev/null +++ b/lua/codetyper/prompts/agent/intent.lua @@ -0,0 +1,53 @@ +---@mod codetyper.prompts.agent.intent Intent-specific system prompts +local M = {} + +M.modifiers = { + complete = [[ +You are completing an incomplete function. +Return the complete function with all missing parts filled in. +Keep the existing signature unless changes are required. +Output only the code, no explanations.]], + + refactor = [[ +You are refactoring existing code. +Improve the code structure while maintaining the same behavior. +Keep the function signature unchanged. +Output only the refactored code, no explanations.]], + + fix = [[ +You are fixing a bug in the code. +Identify and correct the issue while minimizing changes. +Preserve the original intent of the code. +Output only the fixed code, no explanations.]], + + add = [[ +You are adding new code. +Follow the existing code style and conventions. +Output only the new code to be inserted, no explanations.]], + + document = [[ +You are adding documentation to the code. +Add appropriate comments/docstrings for the function. +Include parameter types, return types, and description. +Output the complete function with documentation.]], + + test = [[ +You are generating tests for the code. +Create comprehensive unit tests covering edge cases. +Follow the testing conventions of the project. +Output only the test code, no explanations.]], + + optimize = [[ +You are optimizing code for performance. +Improve efficiency while maintaining correctness. +Document any significant algorithmic changes. +Output only the optimized code, no explanations.]], + + explain = [[ +You are explaining code to a developer. +Provide a clear, concise explanation of what the code does. +Include information about the algorithm and any edge cases. +Do not output code, only explanation.]], +} + +return M diff --git a/lua/codetyper/prompts/agent/linter.lua b/lua/codetyper/prompts/agent/linter.lua new file mode 100644 index 0000000..edac77b --- /dev/null +++ b/lua/codetyper/prompts/agent/linter.lua @@ -0,0 +1,13 @@ +---@mod codetyper.prompts.agent.linter Linter prompts +local M = {} + +M.fix_request = [[ +Fix the following linter errors in this code: + +ERRORS: +%s + +CODE (lines %d-%d): +%s]] + +return M diff --git a/lua/codetyper/prompts/agent/loop.lua b/lua/codetyper/prompts/agent/loop.lua new file mode 100644 index 0000000..5a71b0d --- /dev/null +++ b/lua/codetyper/prompts/agent/loop.lua @@ -0,0 +1,55 @@ +---@mod codetyper.prompts.agent.loop Agent Loop prompts +local M = {} + +M.default_system_prompt = [[You are a helpful coding assistant with access to tools. + +Available tools: +- view: Read file contents +- grep: Search for patterns in files +- glob: Find files by pattern +- edit: Make targeted edits to files +- write: Create or overwrite files +- bash: Execute shell commands + +When you need to perform a task: +1. Use tools to gather information +2. Plan your approach +3. Execute changes using appropriate tools +4. Verify the results + +Always explain your reasoning before using tools. +When you're done, provide a clear summary of what was accomplished.]] + +M.dispatch_prompt = [[You are a research assistant. Your task is to find information and report back. +You have access to: view (read files), grep (search content), glob (find files). +Be thorough and report your findings clearly.]] + +### File Operations +- **read_file**: Read any file. Parameters: path (string) +- **write_file**: Create or overwrite files. Parameters: path (string), content (string) +- **edit_file**: Modify existing files. Parameters: path (string), find (string), replace (string) +- **list_directory**: List files and directories. Parameters: path (string, optional), recursive (boolean, optional) +- **search_files**: Find files. Parameters: pattern (string), content (string), path (string) +- **delete_file**: Delete a file. Parameters: path (string), reason (string) + +### Shell Commands +- **bash**: Run shell commands. Parameters: command (string) + +## WORKFLOW + +1. **Analyze**: Understand the user's request. +2. **Explore**: Use `list_directory`, `search_files`, or `read_file` to find relevant files. +3. **Plan**: Think about what needs to be changed. +4. **Execute**: Use `edit_file`, `write_file`, or `bash` to apply changes. +5. **Verify**: You can check files after editing. + +Always verify context before making changes. +]] + +M.dispatch_prompt = [[ +You are a research assistant. Your job is to explore the codebase and answer the user's question or find specific information. +You have access to: view (read files), grep (search content), glob (find files). +Be thorough and report your findings clearly. +]] + +return M diff --git a/lua/codetyper/prompts/agent/modal.lua b/lua/codetyper/prompts/agent/modal.lua new file mode 100644 index 0000000..3693cc7 --- /dev/null +++ b/lua/codetyper/prompts/agent/modal.lua @@ -0,0 +1,14 @@ +---@mod codetyper.prompts.agent.modal Prompts and UI strings for context modal +local M = {} + +--- Modal UI strings +M.ui = { + files_header = { "", "-- No files detected in LLM response --" }, + llm_response_header = "-- LLM Response: --", + suggested_commands_header = "-- Suggested commands: --", + commands_hint = "-- Press to run a command, or r to run all --", + input_header = "-- Enter additional context below (Ctrl-Enter to submit, Esc to cancel) --", + project_inspect_header = { "", "-- Project inspection results --" }, +} + +return M diff --git a/lua/codetyper/prompts/agent/scheduler.lua b/lua/codetyper/prompts/agent/scheduler.lua new file mode 100644 index 0000000..a1efcf0 --- /dev/null +++ b/lua/codetyper/prompts/agent/scheduler.lua @@ -0,0 +1,12 @@ +---@mod codetyper.prompts.agent.scheduler Scheduler prompts +local M = {} + +M.retry_context = [[ +You requested more context for this task. +Here is the additional information: +%s + +Please restart the task with this new context. +]] + +return M diff --git a/lua/codetyper/prompts/agent/tools.lua b/lua/codetyper/prompts/agent/tools.lua new file mode 100644 index 0000000..990d22e --- /dev/null +++ b/lua/codetyper/prompts/agent/tools.lua @@ -0,0 +1,18 @@ +---@mod codetyper.prompts.agent.tools Tool system prompts +local M = {} + +M.instructions = { + intro = "You have access to the following tools. To use a tool, respond with a JSON block.", + header = "To call a tool, output a JSON block like this:", + example = [[ +```json +{"tool": "tool_name", "parameters": {"param1": "value1"}} +``` +]], + footer = [[ +After receiving tool results, continue your response or call another tool. +When you're done, just respond normally without any tool calls. +]], +} + +return M diff --git a/lua/codetyper/prompts/agents/personas.lua b/lua/codetyper/prompts/agents/personas.lua new file mode 100644 index 0000000..4c8255f --- /dev/null +++ b/lua/codetyper/prompts/agents/personas.lua @@ -0,0 +1,58 @@ +---@mod codetyper.prompts.agents.personas Built-in agent personas +local M = {} + +M.builtin = { + coder = { + name = "coder", + description = "Full-featured coding agent with file modification capabilities", + system_prompt = [[You are an expert software engineer. You have access to tools to read, write, and modify files. + +## Your Capabilities +- Read files to understand the codebase +- Search for patterns with grep and glob +- Create new files with write tool +- Edit existing files with precise replacements +- Execute shell commands for builds and tests + +## Guidelines +1. Always read relevant files before making changes +2. Make minimal, focused changes +3. Follow existing code style and patterns +4. Create tests when adding new functionality +5. Verify changes work by running tests or builds + +## Important Rules +- NEVER guess file contents - always read first +- Make precise edits using exact string matching +- Explain your reasoning before making changes +- If unsure, ask for clarification]], + tools = { "view", "edit", "write", "grep", "glob", "bash" }, + }, + planner = { + name = "planner", + description = "Planning agent - read-only, helps design implementations", + system_prompt = [[You are a software architect. Analyze codebases and create implementation plans. + +You can read files and search the codebase, but cannot modify files. +Your role is to: +1. Understand the existing architecture +2. Identify relevant files and patterns +3. Create step-by-step implementation plans +4. Suggest which files to modify and how + +Be thorough in your analysis before making recommendations.]], + tools = { "view", "grep", "glob" }, + }, + explorer = { + name = "explorer", + description = "Exploration agent - quickly find information in codebase", + system_prompt = [[You are a codebase exploration assistant. Find information quickly and report back. + +Your goal is to efficiently search and summarize findings. +Use glob to find files, grep to search content, and view to read specific files. +Be concise and focused in your responses.]], + tools = { "view", "grep", "glob" }, + }, +} + +return M diff --git a/lua/codetyper/prompts/agents/templates.lua b/lua/codetyper/prompts/agents/templates.lua new file mode 100644 index 0000000..9acf785 --- /dev/null +++ b/lua/codetyper/prompts/agents/templates.lua @@ -0,0 +1,51 @@ +---@mod codetyper.prompts.agents.templates Agent and Rule templates +local M = {} + +M.agent = [[--- +description: Example custom agent +tools: view,grep,glob,edit,write +model: +--- + +# Custom Agent + +You are a custom coding agent. Describe your specialized behavior here. + +## Your Role +- Define what this agent specializes in +- List specific capabilities + +## Guidelines +- Add agent-specific rules +- Define coding standards to follow + +## Examples +Provide examples of how to handle common tasks. +]] + +M.rule = [[# Code Style + +Follow these coding standards: + +## General +- Use consistent indentation (tabs or spaces based on project) +- Keep lines under 100 characters +- Add comments for complex logic + +## Naming Conventions +- Use descriptive variable names +- Functions should be verbs (e.g., getUserData, calculateTotal) +- Constants in UPPER_SNAKE_CASE + +## Testing +- Write tests for new functionality +- Aim for >80% code coverage +- Test edge cases + +## Documentation +- Document public APIs +- Include usage examples +- Keep docs up to date with code +]] + +return M diff --git a/lua/codetyper/utils.lua b/lua/codetyper/utils.lua index c13369c..5020a98 100644 --- a/lua/codetyper/utils.lua +++ b/lua/codetyper/utils.lua @@ -2,6 +2,14 @@ local M = {} +--- Generate a unique ID +---@param prefix? string Prefix for the ID (default: "id") +---@return string Unique ID +function M.generate_id(prefix) + prefix = prefix or "id" + return prefix .. "_" .. string.format("%x", os.time()) .. "_" .. string.format("%x", math.random(0, 0xFFFF)) +end + --- Get the project root directory ---@return string|nil Root directory path or nil if not found function M.get_project_root() @@ -194,4 +202,147 @@ function M.get_visual_selection() } end +--- Check bracket balance for common languages +---@param response string +---@return boolean balanced +function M.check_brackets(response) + local pairs = { + ["{"] = "}", + ["["] = "]", + ["("] = ")", + } + + local stack = {} + + for char in response:gmatch(".") do + if pairs[char] then + table.insert(stack, pairs[char]) + elseif char == "}" or char == "]" or char == ")" then + if #stack == 0 or stack[#stack] ~= char then + return false + end + table.remove(stack) + end + end + + return #stack == 0 +end + +--- Simple hash function for content +---@param content string +---@return string +function M.hash_content(content) + local hash = vim.fn.sha256(content) + -- If sha256 returns hex string, format %x might be wrong if it expects number? + -- vim.fn.sha256 returns a hex string already. + return hash +end + +--- Check if a line is empty or a comment + +---@param line string +---@param filetype string +---@return boolean +function M.is_empty_or_comment(line, filetype) + local trimmed = line:match("^%s*(.-)%s*$") + if trimmed == "" then + return true + end + + local ok, languages = pcall(require, "codetyper.params.agent.languages") + if not ok then return false end + + local patterns = languages.comment_patterns[filetype] or languages.comment_patterns.javascript + for _, pattern in ipairs(patterns) do + if trimmed:match(pattern) then + return true + end + end + + return false +end + +--- Classify an import as "builtin", "local", or "third_party" +---@param imp string The import statement +---@param filetype string The filetype +---@return string category "builtin"|"local"|"third_party" +function M.classify_import(imp, filetype) + local is_local = false + local is_builtin = false + + if filetype == "javascript" or filetype == "typescript" or filetype == "ts" or filetype == "tsx" then + -- Local: starts with . or .. + is_local = imp:match("from%s+['\"]%.") or imp:match("require%(['\"]%.") + -- Node builtin modules + is_builtin = imp:match("from%s+['\"]node:") or imp:match("from%s+['\"]fs['\"]") + or imp:match("from%s+['\"]path['\"]") or imp:match("from%s+['\"]http['\"]") + elseif filetype == "python" or filetype == "py" then + -- Local: relative imports + is_local = imp:match("^from%s+%.") or imp:match("^import%s+%.") + -- Python stdlib (simplified check) + is_builtin = imp:match("^import%s+os") or imp:match("^import%s+sys") + or imp:match("^from%s+os%s+") or imp:match("^from%s+sys%s+") + or imp:match("^import%s+re") or imp:match("^import%s+json") + elseif filetype == "lua" then + -- Local: relative requires + is_local = imp:match("require%(['\"]%.") or imp:match("require%s+['\"]%.") + elseif filetype == "go" then + -- Local: project imports (contain /) + is_local = imp:match("['\"][^'\"]+/[^'\"]+['\"]") and not imp:match("github%.com") + end + + if is_builtin then + return "builtin" + elseif is_local then + return "local" + else + return "third_party" + end +end + +--- Check if a line ends a multi-line import +---@param line string +---@param filetype string +---@return boolean +function M.ends_multiline_import(line, filetype) + -- Check for closing patterns + if filetype == "javascript" or filetype == "typescript" or filetype == "ts" or filetype == "tsx" then + -- ES6 imports end with 'from "..." ;' or just ';' or a line with just '}' + if line:match("from%s+['\"][^'\"]+['\"]%s*;?%s*$") then + return true + end + if line:match("}%s*from%s+['\"]") then + return true + end + if line:match("^%s*}%s*;?%s*$") then + return true + end + if line:match(";%s*$") then + return true + end + elseif filetype == "python" or filetype == "py" then + -- Python single-line import: doesn't end with \, (, or , + -- Examples: "from typing import List, Dict" or "import os" + if not line:match("\\%s*$") and not line:match("%(%s*$") and not line:match(",%s*$") then + return true + end + -- Python multiline imports end with closing paren + if line:match("%)%s*$") then + return true + end + elseif filetype == "go" then + -- Go multi-line imports end with ')' + if line:match("%)%s*$") then + return true + end + elseif filetype == "rust" or filetype == "rs" then + -- Rust use statements end with ';' + if line:match(";%s*$") then + return true + end + end + + return false +end + return M