Adding autocomplete and copilot suggestions

This commit is contained in:
2026-01-14 21:43:56 -05:00
parent 5493a5ec38
commit 84c8bcf92c
55 changed files with 11823 additions and 550 deletions

View File

@@ -9,7 +9,20 @@ local M = {}
---@param callback fun(approved: boolean) Called with user decision
function M.show_diff(diff_data, callback)
local original_lines = vim.split(diff_data.original, "\n", { plain = true })
local modified_lines = vim.split(diff_data.modified, "\n", { plain = true })
local modified_lines
-- For delete operations, show a clear message
if diff_data.operation == "delete" then
modified_lines = {
"",
" FILE WILL BE DELETED",
"",
" Reason: " .. (diff_data.reason or "No reason provided"),
"",
}
else
modified_lines = vim.split(diff_data.modified, "\n", { plain = true })
end
-- Calculate window dimensions
local width = math.floor(vim.o.columns * 0.8)
@@ -59,7 +72,7 @@ function M.show_diff(diff_data, callback)
col = col + half_width + 1,
style = "minimal",
border = "rounded",
title = " MODIFIED [" .. diff_data.operation .. "] ",
title = diff_data.operation == "delete" and " ⚠️ DELETE " or (" MODIFIED [" .. diff_data.operation .. "] "),
title_pos = "center",
})
@@ -157,26 +170,52 @@ function M.show_diff(diff_data, callback)
}, false, {})
end
--- Show approval dialog for bash commands
---@alias BashApprovalResult {approved: boolean, permission_level: string|nil}
--- Show approval dialog for bash commands with permission levels
---@param command string The bash command to approve
---@param callback fun(approved: boolean) Called with user decision
---@param callback fun(result: BashApprovalResult) Called with user decision
function M.show_bash_approval(command, callback)
-- Create a simple floating window for bash approval
local permissions = require("codetyper.agent.permissions")
-- Check if command is auto-approved
local perm_result = permissions.check_bash_permission(command)
if perm_result.auto and perm_result.allowed then
vim.schedule(function()
callback({ approved = true, permission_level = "auto" })
end)
return
end
-- Create approval dialog with options
local lines = {
"",
" BASH COMMAND APPROVAL",
" " .. string.rep("-", 50),
" " .. string.rep("", 56),
"",
" Command:",
" $ " .. command,
"",
" " .. string.rep("-", 50),
" Press [y] or [Enter] to execute",
" Press [n], [q], or [Esc] to cancel",
"",
}
local width = math.max(60, #command + 10)
-- 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, "")
end
table.insert(lines, " " .. string.rep("", 56))
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")
table.insert(lines, "")
table.insert(lines, " " .. string.rep("", 56))
table.insert(lines, " Press key to choose | [q] or [Esc] to cancel")
table.insert(lines, "")
local width = math.max(65, #command + 15)
local height = #lines
local buf = vim.api.nvim_create_buf(false, true)
@@ -196,45 +235,84 @@ function M.show_bash_approval(command, callback)
title_pos = "center",
})
-- Apply some highlighting
-- Apply highlighting
vim.api.nvim_buf_add_highlight(buf, -1, "Title", 1, 0, -1)
vim.api.nvim_buf_add_highlight(buf, -1, "String", 5, 0, -1)
-- Highlight options
for i, line in ipairs(lines) do
if line:match("^%s+%[y%]") then
vim.api.nvim_buf_add_highlight(buf, -1, "DiagnosticOk", i - 1, 0, -1)
elseif line:match("^%s+%[s%]") then
vim.api.nvim_buf_add_highlight(buf, -1, "DiagnosticInfo", i - 1, 0, -1)
elseif line:match("^%s+%[a%]") then
vim.api.nvim_buf_add_highlight(buf, -1, "DiagnosticHint", i - 1, 0, -1)
elseif line:match("^%s+%[n%]") then
vim.api.nvim_buf_add_highlight(buf, -1, "DiagnosticError", i - 1, 0, -1)
elseif line:match("⚠️") then
vim.api.nvim_buf_add_highlight(buf, -1, "DiagnosticWarn", i - 1, 0, -1)
end
end
local callback_called = false
local function close_and_respond(approved)
local function close_and_respond(approved, permission_level)
if callback_called then
return
end
callback_called = true
-- Grant permission if approved with session or list level
if approved and permission_level then
permissions.grant_permission(command, permission_level)
end
pcall(vim.api.nvim_win_close, win, true)
vim.schedule(function()
callback(approved)
callback({ approved = approved, permission_level = permission_level })
end)
end
local keymap_opts = { buffer = buf, noremap = true, silent = true, nowait = true }
-- Approve
-- Allow once
vim.keymap.set("n", "y", function()
close_and_respond(true)
close_and_respond(true, "allow")
end, keymap_opts)
vim.keymap.set("n", "<CR>", function()
close_and_respond(true)
close_and_respond(true, "allow")
end, keymap_opts)
-- Allow this session
vim.keymap.set("n", "s", function()
close_and_respond(true, "allow_session")
end, keymap_opts)
-- Add to allow list
vim.keymap.set("n", "a", function()
close_and_respond(true, "allow_list")
end, keymap_opts)
-- Reject
vim.keymap.set("n", "n", function()
close_and_respond(false)
close_and_respond(false, nil)
end, keymap_opts)
vim.keymap.set("n", "q", function()
close_and_respond(false)
close_and_respond(false, nil)
end, keymap_opts)
vim.keymap.set("n", "<Esc>", function()
close_and_respond(false)
close_and_respond(false, nil)
end, keymap_opts)
end
--- Show approval dialog for bash commands (simple version for backward compatibility)
---@param command string The bash command to approve
---@param callback fun(approved: boolean) Called with user decision
function M.show_bash_approval_simple(command, callback)
M.show_bash_approval(command, function(result)
callback(result.approved)
end)
end
return M

View File

@@ -27,6 +27,9 @@ function M.execute(tool_name, parameters, callback)
edit_file = M.handle_edit_file,
write_file = M.handle_write_file,
bash = M.handle_bash,
delete_file = M.handle_delete_file,
list_directory = M.handle_list_directory,
search_files = M.handle_search_files,
}
local handler = handlers[tool_name]
@@ -156,6 +159,165 @@ function M.handle_bash(params, callback)
})
end
--- Handle delete_file tool
---@param params table { path: string, reason: string }
---@param callback fun(result: ExecutionResult)
function M.handle_delete_file(params, callback)
local path = M.resolve_path(params.path)
local reason = params.reason or "No reason provided"
-- Check if file exists
if not utils.file_exists(path) then
callback({
success = false,
result = "File not found: " .. path,
requires_approval = false,
})
return
end
-- Read content for showing in diff (so user knows what they're deleting)
local content = utils.read_file(path) or "[Could not read file]"
callback({
success = true,
result = "Delete: " .. path .. " (" .. reason .. ")",
requires_approval = true,
diff_data = {
path = path,
original = content,
modified = "", -- Empty = deletion
operation = "delete",
reason = reason,
},
})
end
--- Handle list_directory tool
---@param params table { path?: string, recursive?: boolean }
---@param callback fun(result: ExecutionResult)
function M.handle_list_directory(params, callback)
local path = params.path and M.resolve_path(params.path) or (utils.get_project_root() or vim.fn.getcwd())
local recursive = params.recursive or false
-- Use vim.fn.readdir or glob for directory listing
local entries = {}
local function list_dir(dir, depth)
if depth > 3 then
return
end
local ok, files = pcall(vim.fn.readdir, dir)
if not ok or not files then
return
end
for _, name in ipairs(files) do
if name ~= "." and name ~= ".." and not name:match("^%.git$") and not name:match("^node_modules$") then
local full_path = dir .. "/" .. name
local stat = vim.loop.fs_stat(full_path)
if stat then
local prefix = string.rep(" ", depth)
local type_indicator = stat.type == "directory" and "/" or ""
table.insert(entries, prefix .. name .. type_indicator)
if recursive and stat.type == "directory" then
list_dir(full_path, depth + 1)
end
end
end
end
end
list_dir(path, 0)
local result = "Directory: " .. path .. "\n\n" .. table.concat(entries, "\n")
callback({
success = true,
result = result,
requires_approval = false,
})
end
--- Handle search_files tool
---@param params table { pattern?: string, content?: string, path?: string }
---@param callback fun(result: ExecutionResult)
function M.handle_search_files(params, callback)
local search_path = params.path and M.resolve_path(params.path) or (utils.get_project_root() or vim.fn.getcwd())
local pattern = params.pattern
local content_search = params.content
local results = {}
if pattern then
-- Search by file name pattern using glob
local glob_pattern = search_path .. "/**/" .. pattern
local files = vim.fn.glob(glob_pattern, false, true)
for _, file in ipairs(files) do
-- Skip common ignore patterns
if not file:match("node_modules") and not file:match("%.git/") then
table.insert(results, file:gsub(search_path .. "/", ""))
end
end
end
if content_search then
-- Search by content using grep
local grep_results = {}
local grep_cmd = string.format("grep -rl '%s' '%s' 2>/dev/null | head -20", content_search:gsub("'", "\\'"), search_path)
local handle = io.popen(grep_cmd)
if handle then
for line in handle:lines() do
if not line:match("node_modules") and not line:match("%.git/") then
table.insert(grep_results, line:gsub(search_path .. "/", ""))
end
end
handle:close()
end
-- Merge with pattern results or use as primary results
if #results == 0 then
results = grep_results
else
-- Intersection of pattern and content results
local pattern_set = {}
for _, f in ipairs(results) do
pattern_set[f] = true
end
results = {}
for _, f in ipairs(grep_results) do
if pattern_set[f] then
table.insert(results, f)
end
end
end
end
local result_text = "Search results"
if pattern then
result_text = result_text .. " (pattern: " .. pattern .. ")"
end
if content_search then
result_text = result_text .. " (content: " .. content_search .. ")"
end
result_text = result_text .. ":\n\n"
if #results == 0 then
result_text = result_text .. "No files found."
else
result_text = result_text .. table.concat(results, "\n")
end
callback({
success = true,
result = result_text,
requires_approval = false,
})
end
--- Actually apply an approved change
---@param diff_data DiffData The diff data to apply
---@param callback fun(result: ExecutionResult)
@@ -164,6 +326,24 @@ function M.apply_change(diff_data, callback)
-- Extract command from modified (remove "$ " prefix)
local command = diff_data.modified:gsub("^%$ ", "")
M.execute_bash_command(command, 30000, callback)
elseif diff_data.operation == "delete" then
-- Delete file
local ok, err = os.remove(diff_data.path)
if ok then
-- Close buffer if it's open
M.close_buffer_if_open(diff_data.path)
callback({
success = true,
result = "Deleted: " .. diff_data.path,
requires_approval = false,
})
else
callback({
success = false,
result = "Failed to delete: " .. diff_data.path .. " (" .. (err or "unknown error") .. ")",
requires_approval = false,
})
end
else
-- Write file
local success = utils.write_file(diff_data.path, diff_data.modified)
@@ -275,6 +455,22 @@ function M.reload_buffer_if_open(filepath)
end
end
--- Close a buffer if it's currently open (for deleted files)
---@param filepath string Path to the file
function M.close_buffer_if_open(filepath)
local full_path = vim.fn.fnamemodify(filepath, ":p")
for _, buf in ipairs(vim.api.nvim_list_bufs()) do
if vim.api.nvim_buf_is_loaded(buf) then
local buf_name = vim.api.nvim_buf_get_name(buf)
if buf_name == full_path then
-- Force close the buffer
pcall(vim.api.nvim_buf_delete, buf, { force = true })
break
end
end
end
end
--- Resolve a path (expand ~ and make absolute if needed)
---@param path string Path to resolve
---@return string Resolved path

View File

@@ -123,12 +123,14 @@ function M.agent_loop(context, callbacks)
local config = codetyper.get_config()
local parsed
if config.llm.provider == "claude" then
-- Copilot uses Claude-like response format
if config.llm.provider == "copilot" then
parsed = parser.parse_claude_response(response)
-- For Claude, preserve the original content array for proper tool_use handling
table.insert(state.conversation, {
role = "assistant",
content = response.content, -- Keep original content blocks for Claude API
content = parsed.text or "",
tool_calls = parsed.tool_calls,
_raw_content = response.content,
})
else
-- For Ollama, response is the text directly
@@ -200,9 +202,22 @@ function M.process_tool_calls(tool_calls, index, context, callbacks)
show_fn = diff.show_diff
end
show_fn(result.diff_data, function(approved)
show_fn(result.diff_data, function(approval_result)
-- Handle both old (boolean) and new (table) approval result formats
local approved = type(approval_result) == "table" and approval_result.approved or approval_result
local permission_level = type(approval_result) == "table" and approval_result.permission_level or nil
if approved then
logs.tool(tool_call.name, "approved", "User approved")
local log_msg = "User approved"
if permission_level == "allow_session" then
log_msg = "Allowed for session"
elseif permission_level == "allow_list" then
log_msg = "Added to allow list"
elseif permission_level == "auto" then
log_msg = "Auto-approved"
end
logs.tool(tool_call.name, "approved", log_msg)
-- Apply the change
executor.apply_change(result.diff_data, function(apply_result)
-- Store result for sending back to LLM
@@ -261,8 +276,9 @@ function M.continue_with_results(context, callbacks)
local codetyper = require("codetyper")
local config = codetyper.get_config()
if config.llm.provider == "claude" then
-- Claude format: tool_result blocks
-- Copilot uses Claude-like format for tool results
if config.llm.provider == "copilot" then
-- Claude-style tool_result blocks
local content = {}
for _, result in ipairs(state.pending_tool_results) do
table.insert(content, {

View File

@@ -0,0 +1,229 @@
---@mod codetyper.agent.permissions Permission manager for agent actions
---
--- Manages permissions for bash commands and file operations with
--- allow, allow-session, allow-list, and reject options.
local M = {}
---@class PermissionState
---@field session_allowed table<string, boolean> Commands allowed for this session
---@field allow_list table<string, boolean> Patterns always allowed
---@field deny_list table<string, boolean> Patterns always denied
local state = {
session_allowed = {},
allow_list = {},
deny_list = {},
}
--- 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",
}
--- 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",
}
---@alias PermissionLevel "allow"|"allow_session"|"allow_list"|"reject"
---@class PermissionResult
---@field allowed boolean Whether action is allowed
---@field reason string Reason for the decision
---@field auto boolean Whether this was an automatic decision
--- Check if a command matches a pattern
---@param command string The command to check
---@param pattern string The pattern to match
---@return boolean
local function matches_pattern(command, pattern)
return command:match(pattern) ~= nil
end
--- Check if command is dangerous
---@param command string The command to check
---@return boolean, string|nil dangerous, reason
local function is_dangerous(command)
for _, pattern in ipairs(DANGEROUS_PATTERNS) do
if matches_pattern(command, pattern) then
return true, "Matches dangerous pattern: " .. pattern
end
end
return false, nil
end
--- Check if command is safe
---@param command string The command to check
---@return boolean
local function is_safe(command)
for _, pattern in ipairs(SAFE_PATTERNS) do
if matches_pattern(command, pattern) then
return true
end
end
return false
end
--- Normalize command for comparison (trim, lowercase first word)
---@param command string
---@return string
local function normalize_command(command)
return vim.trim(command)
end
--- Check permission for a bash command
---@param command string The command to check
---@return PermissionResult
function M.check_bash_permission(command)
local normalized = normalize_command(command)
-- Check deny list first
for pattern, _ in pairs(state.deny_list) do
if matches_pattern(normalized, pattern) then
return {
allowed = false,
reason = "Command in deny list",
auto = true,
}
end
end
-- Check if command is dangerous
local dangerous, reason = is_dangerous(normalized)
if dangerous then
return {
allowed = false,
reason = reason,
auto = false, -- Require explicit approval for dangerous commands
}
end
-- Check session allowed
if state.session_allowed[normalized] then
return {
allowed = true,
reason = "Allowed for this session",
auto = true,
}
end
-- Check allow list patterns
for pattern, _ in pairs(state.allow_list) do
if matches_pattern(normalized, pattern) then
return {
allowed = true,
reason = "Matches allow list pattern",
auto = true,
}
end
end
-- Check if command is inherently safe
if is_safe(normalized) then
return {
allowed = true,
reason = "Safe read-only command",
auto = true,
}
end
-- Otherwise, require explicit permission
return {
allowed = false,
reason = "Requires approval",
auto = false,
}
end
--- Grant permission for a command
---@param command string The command
---@param level PermissionLevel The permission level
function M.grant_permission(command, level)
local normalized = normalize_command(command)
if level == "allow_session" then
state.session_allowed[normalized] = true
elseif level == "allow_list" then
-- Add as pattern (escape special chars for exact match)
local pattern = "^" .. vim.pesc(normalized) .. "$"
state.allow_list[pattern] = true
end
end
--- Add a pattern to the allow list
---@param pattern string Lua pattern to allow
function M.add_to_allow_list(pattern)
state.allow_list[pattern] = true
end
--- Add a pattern to the deny list
---@param pattern string Lua pattern to deny
function M.add_to_deny_list(pattern)
state.deny_list[pattern] = true
end
--- Clear session permissions
function M.clear_session()
state.session_allowed = {}
end
--- Reset all permissions
function M.reset()
state.session_allowed = {}
state.allow_list = {}
state.deny_list = {}
end
--- Get current permission state (for debugging)
---@return PermissionState
function M.get_state()
return vim.deepcopy(state)
end
return M

View File

@@ -23,7 +23,7 @@ local M = {}
---@field priority number Priority (1=high, 2=normal, 3=low)
---@field status string "pending"|"processing"|"completed"|"escalated"|"cancelled"|"needs_context"|"failed"
---@field attempt_count number Number of processing attempts
---@field worker_type string|nil LLM provider used ("ollama"|"claude"|etc)
---@field worker_type string|nil LLM provider used ("ollama"|"openai"|"gemini"|"copilot")
---@field created_at number System time when created
---@field intent Intent|nil Detected intent from prompt
---@field scope ScopeInfo|nil Resolved scope (function/class/file)

View File

@@ -28,7 +28,7 @@ local state = {
max_concurrent = 2,
completion_delay_ms = 100,
apply_delay_ms = 5000, -- Wait before applying code
remote_provider = "claude", -- Default fallback provider
remote_provider = "copilot", -- Default fallback provider
},
}
@@ -90,9 +90,7 @@ local function get_remote_provider()
-- If current provider is ollama, use configured remote
if config.llm.provider == "ollama" then
-- Check which remote provider is configured
if config.llm.claude and config.llm.claude.api_key then
return "claude"
elseif config.llm.openai and config.llm.openai.api_key then
if config.llm.openai and config.llm.openai.api_key then
return "openai"
elseif config.llm.gemini and config.llm.gemini.api_key then
return "gemini"
@@ -120,7 +118,7 @@ local function get_primary_provider()
return config.llm.provider
end
end
return "claude"
return "ollama"
end
--- Retry event with additional context

View File

@@ -81,6 +81,67 @@ M.definitions = {
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 = {},
},
},
}
--- Convert tool definitions to Claude API format

View File

@@ -35,14 +35,62 @@ local state = {
local ns_chat = vim.api.nvim_create_namespace("codetyper_agent_chat")
local ns_logs = vim.api.nvim_create_namespace("codetyper_agent_logs")
--- Fixed widths
local CHAT_WIDTH = 300
local LOGS_WIDTH = 50
--- Fixed heights
local INPUT_HEIGHT = 5
local LOGS_WIDTH = 50
--- Calculate dynamic width (1/4 of screen, minimum 30)
---@return number
local function get_panel_width()
return math.max(math.floor(vim.o.columns * 0.25), 30)
end
--- Autocmd group
local agent_augroup = nil
--- Autocmd group for width maintenance
local width_augroup = nil
--- Store target width
local target_width = nil
--- Setup autocmd to always maintain 1/4 window width
local function setup_width_autocmd()
-- Clear previous autocmd group if exists
if width_augroup then
pcall(vim.api.nvim_del_augroup_by_id, width_augroup)
end
width_augroup = vim.api.nvim_create_augroup("CodetypeAgentWidth", { clear = true })
-- Always maintain 1/4 width on any window event
vim.api.nvim_create_autocmd({ "WinResized", "WinNew", "WinClosed", "VimResized" }, {
group = width_augroup,
callback = function()
if not state.is_open or not state.chat_win then
return
end
if not vim.api.nvim_win_is_valid(state.chat_win) then
return
end
vim.schedule(function()
if state.chat_win and vim.api.nvim_win_is_valid(state.chat_win) then
-- Always calculate 1/4 of current screen width
local new_target = math.max(math.floor(vim.o.columns * 0.25), 30)
target_width = new_target
local current_width = vim.api.nvim_win_get_width(state.chat_win)
if current_width ~= target_width then
pcall(vim.api.nvim_win_set_width, state.chat_win, target_width)
end
end
end)
end,
desc = "Maintain Agent panel at 1/4 window width",
})
end
--- Add a log entry to the logs buffer
---@param entry table Log entry
local function add_log_entry(entry)
@@ -479,7 +527,7 @@ function M.open()
vim.cmd("topleft vsplit")
state.chat_win = vim.api.nvim_get_current_win()
vim.api.nvim_win_set_buf(state.chat_win, state.chat_buf)
vim.api.nvim_win_set_width(state.chat_win, CHAT_WIDTH)
vim.api.nvim_win_set_width(state.chat_win, get_panel_width())
-- Window options for chat
vim.wo[state.chat_win].number = false
@@ -592,6 +640,10 @@ function M.open()
end,
})
-- Setup autocmd to maintain 1/4 width
target_width = get_panel_width()
setup_width_autocmd()
state.is_open = true
-- Focus input and log startup
@@ -603,7 +655,16 @@ function M.open()
if ok then
local config = codetyper.get_config()
local provider = config.llm.provider
local model = provider == "claude" and config.llm.claude.model or config.llm.ollama.model
local model = "unknown"
if provider == "ollama" then
model = config.llm.ollama.model
elseif provider == "openai" then
model = config.llm.openai.model
elseif provider == "gemini" then
model = config.llm.gemini.model
elseif provider == "copilot" then
model = config.llm.copilot.model
end
logs.info(string.format("%s (%s)", provider, model))
end
end

View File

@@ -178,8 +178,7 @@ local active_workers = {}
--- Default timeouts by provider type
local default_timeouts = {
ollama = 30000, -- 30s for local
claude = 60000, -- 60s for remote
openai = 60000,
openai = 60000, -- 60s for remote
gemini = 60000,
copilot = 60000,
}
@@ -225,6 +224,54 @@ local function format_attached_files(attached_files)
return table.concat(parts, "")
end
--- Format indexed project context for inclusion in prompt
---@param indexed_context table|nil
---@return string
local function format_indexed_context(indexed_context)
if not indexed_context then
return ""
end
local parts = {}
-- Project type
if indexed_context.project_type and indexed_context.project_type ~= "unknown" then
table.insert(parts, "Project type: " .. indexed_context.project_type)
end
-- Relevant symbols
if indexed_context.relevant_symbols then
local symbol_list = {}
for symbol, files in pairs(indexed_context.relevant_symbols) do
if #files > 0 then
table.insert(symbol_list, symbol .. " (in " .. files[1] .. ")")
end
end
if #symbol_list > 0 then
table.insert(parts, "Relevant symbols: " .. table.concat(symbol_list, ", "))
end
end
-- Learned patterns
if indexed_context.patterns and #indexed_context.patterns > 0 then
local pattern_list = {}
for i, p in ipairs(indexed_context.patterns) do
if i <= 3 then
table.insert(pattern_list, p.content or "")
end
end
if #pattern_list > 0 then
table.insert(parts, "Project conventions: " .. table.concat(pattern_list, "; "))
end
end
if #parts == 0 then
return ""
end
return "\n\n--- Project Context ---\n" .. table.concat(parts, "\n")
end
--- Build prompt for code generation
---@param event table PromptEvent
---@return string prompt
@@ -245,9 +292,26 @@ local function build_prompt(event)
local filetype = vim.fn.fnamemodify(event.target_path or "", ":e")
-- Get indexed project context
local indexed_context = nil
local indexed_content = ""
pcall(function()
local indexer = require("codetyper.indexer")
indexed_context = indexer.get_context_for({
file = event.target_path,
intent = event.intent,
prompt = event.prompt_content,
scope = event.scope_text,
})
indexed_content = format_indexed_context(indexed_context)
end)
-- Format attached files
local attached_content = format_attached_files(event.attached_files)
-- Combine attached files and indexed context
local extra_context = attached_content .. indexed_content
-- Build context with scope information
local context = {
target_path = event.target_path,
@@ -258,6 +322,7 @@ local function build_prompt(event)
scope_range = event.scope_range,
intent = event.intent,
attached_files = event.attached_files,
indexed_context = indexed_context,
}
-- Build the actual prompt based on intent and scope
@@ -296,7 +361,7 @@ Return ONLY the complete %s with implementation. No explanations, no duplicates.
scope_type,
filetype,
event.scope_text,
attached_content,
extra_context,
event.prompt_content,
scope_type
)
@@ -317,7 +382,7 @@ Return the complete transformed %s. Output only code, no explanations.]],
filetype,
filetype,
event.scope_text,
attached_content,
extra_context,
event.prompt_content,
scope_type
)
@@ -337,7 +402,7 @@ Output only the code to insert, no explanations.]],
scope_name,
filetype,
event.scope_text,
attached_content,
extra_context,
event.prompt_content
)
end
@@ -357,7 +422,7 @@ Output only code, no explanations.]],
filetype,
filetype,
target_content:sub(1, 4000), -- Limit context size
attached_content,
extra_context,
event.prompt_content
)
end