fix: better sidebar (#1603)

* fix: better sidebar

* feat: better msg history

* fix: tests
This commit is contained in:
yetone
2025-03-17 01:40:05 +08:00
committed by GitHub
parent f60f150a21
commit 6e77da83c1
17 changed files with 870 additions and 319 deletions

View File

@@ -17,9 +17,23 @@ local function get_abs_path(rel_path)
return p
end
function M.confirm(msg)
local ok = vim.fn.confirm(msg, "&Yes\n&No", 2)
return ok == 1
function M.confirm(message, callback)
local UI = require("avante.ui")
UI.confirm(message, callback)
end
---@param abs_path string
---@return boolean
local function is_ignored(abs_path)
local project_root = Utils.get_project_root()
local gitignore_path = project_root .. "/.gitignore"
local gitignore_patterns, gitignore_negate_patterns = Utils.parse_gitignore(gitignore_path)
-- The checker should only take care of the path inside the project root
-- Specifically, it should not check the project root itself
-- Otherwise if the binary is named the same as the project root (such as Go binary), any paths
-- insde the project root will be ignored
local rel_path = Utils.make_relative_path(abs_path, project_root)
return Utils.is_ignored(rel_path, gitignore_patterns, gitignore_negate_patterns)
end
---@param abs_path string
@@ -28,14 +42,7 @@ local function has_permission_to_access(abs_path)
if not Path:new(abs_path):is_absolute() then return false end
local project_root = Utils.get_project_root()
if abs_path:sub(1, #project_root) ~= project_root then return false end
local gitignore_path = project_root .. "/.gitignore"
local gitignore_patterns, gitignore_negate_patterns = Utils.parse_gitignore(gitignore_path)
-- The checker should only take care of the path inside the project root
-- Specifically, it should not check the project root itself
-- Otherwise if the binary is named the same as the project root (such as Go binary), any paths
-- insde the project root will be ignored
local rel_path = Utils.make_relative_path(abs_path, project_root)
return not Utils.is_ignored(rel_path, gitignore_patterns, gitignore_negate_patterns)
return not is_ignored(abs_path)
end
---@type AvanteLLMToolFunc<{ rel_path: string, pattern: string }>
@@ -164,6 +171,41 @@ function M.read_file(opts, on_log)
return content, nil
end
---@type AvanteLLMToolFunc<{ abs_path: string }>
function M.read_global_file(opts, on_log)
local abs_path = get_abs_path(opts.abs_path)
if is_ignored(abs_path) then return "", "This file is ignored: " .. abs_path end
if on_log then on_log("path: " .. abs_path) end
local file = io.open(abs_path, "r")
if not file then return "", "file not found: " .. abs_path end
local content = file:read("*a")
file:close()
return content, nil
end
---@type AvanteLLMToolFunc<{ abs_path: string, content: string }>
function M.write_global_file(opts, on_log, on_complete)
local abs_path = get_abs_path(opts.abs_path)
if is_ignored(abs_path) then return false, "This file is ignored: " .. abs_path end
if on_log then on_log("path: " .. abs_path) end
if on_log then on_log("content: " .. opts.content) end
if not on_complete then return false, "on_complete not provided" end
M.confirm("Are you sure you want to write to the file: " .. abs_path, function(ok)
if not ok then
on_complete(false, "User canceled")
return
end
local file = io.open(abs_path, "w")
if not file then
on_complete(false, "file not found: " .. abs_path)
return
end
file:write(opts.content)
file:close()
on_complete(true, nil)
end)
end
---@type AvanteLLMToolFunc<{ rel_path: string }>
function M.create_file(opts, on_log)
local abs_path = get_abs_path(opts.rel_path)
@@ -183,7 +225,7 @@ function M.create_file(opts, on_log)
end
---@type AvanteLLMToolFunc<{ rel_path: string, new_rel_path: string }>
function M.rename_file(opts, on_log)
function M.rename_file(opts, on_log, on_complete)
local abs_path = get_abs_path(opts.rel_path)
if not has_permission_to_access(abs_path) then return false, "No permission to access path: " .. abs_path end
if not Path:new(abs_path):exists() then return false, "File not found: " .. abs_path end
@@ -192,11 +234,15 @@ function M.rename_file(opts, on_log)
if on_log then on_log(abs_path .. " -> " .. new_abs_path) end
if not has_permission_to_access(new_abs_path) then return false, "No permission to access path: " .. new_abs_path end
if Path:new(new_abs_path):exists() then return false, "File already exists: " .. new_abs_path end
if not M.confirm("Are you sure you want to rename the file: " .. abs_path .. " to: " .. new_abs_path) then
return false, "User canceled"
end
os.rename(abs_path, new_abs_path)
return true, nil
if not on_complete then return false, "on_complete not provided" end
M.confirm("Are you sure you want to rename the file: " .. abs_path .. " to: " .. new_abs_path, function(ok)
if not ok then
on_complete(false, "User canceled")
return
end
os.rename(abs_path, new_abs_path)
on_complete(true, nil)
end)
end
---@type AvanteLLMToolFunc<{ rel_path: string, new_rel_path: string }>
@@ -214,32 +260,42 @@ function M.copy_file(opts, on_log)
end
---@type AvanteLLMToolFunc<{ rel_path: string }>
function M.delete_file(opts, on_log)
function M.delete_file(opts, on_log, on_complete)
local abs_path = get_abs_path(opts.rel_path)
if not has_permission_to_access(abs_path) then return false, "No permission to access path: " .. abs_path end
if not Path:new(abs_path):exists() then return false, "File not found: " .. abs_path end
if not Path:new(abs_path):is_file() then return false, "Path is not a file: " .. abs_path end
if not M.confirm("Are you sure you want to delete the file: " .. abs_path) then return false, "User canceled" end
if on_log then on_log("Deleting file: " .. abs_path) end
os.remove(abs_path)
return true, nil
if not on_complete then return false, "on_complete not provided" end
M.confirm("Are you sure you want to delete the file: " .. abs_path, function(ok)
if not ok then
on_complete(false, "User canceled")
return
end
if on_log then on_log("Deleting file: " .. abs_path) end
os.remove(abs_path)
on_complete(true, nil)
end)
end
---@type AvanteLLMToolFunc<{ rel_path: string }>
function M.create_dir(opts, on_log)
function M.create_dir(opts, on_log, on_complete)
local abs_path = get_abs_path(opts.rel_path)
if not has_permission_to_access(abs_path) then return false, "No permission to access path: " .. abs_path end
if Path:new(abs_path):exists() then return false, "Directory already exists: " .. abs_path end
if not M.confirm("Are you sure you want to create the directory: " .. abs_path) then
return false, "User canceled"
end
if on_log then on_log("Creating directory: " .. abs_path) end
Path:new(abs_path):mkdir({ parents = true })
return true, nil
if not on_complete then return false, "on_complete not provided" end
M.confirm("Are you sure you want to create the directory: " .. abs_path, function(ok)
if not ok then
on_complete(false, "User canceled")
return
end
if on_log then on_log("Creating directory: " .. abs_path) end
Path:new(abs_path):mkdir({ parents = true })
on_complete(true, nil)
end)
end
---@type AvanteLLMToolFunc<{ rel_path: string, new_rel_path: string }>
function M.rename_dir(opts, on_log)
function M.rename_dir(opts, on_log, on_complete)
local abs_path = get_abs_path(opts.rel_path)
if not has_permission_to_access(abs_path) then return false, "No permission to access path: " .. abs_path end
if not Path:new(abs_path):exists() then return false, "Directory not found: " .. abs_path end
@@ -247,26 +303,34 @@ function M.rename_dir(opts, on_log)
local new_abs_path = get_abs_path(opts.new_rel_path)
if not has_permission_to_access(new_abs_path) then return false, "No permission to access path: " .. new_abs_path end
if Path:new(new_abs_path):exists() then return false, "Directory already exists: " .. new_abs_path end
if not M.confirm("Are you sure you want to rename directory " .. abs_path .. " to " .. new_abs_path .. "?") then
return false, "User canceled"
end
if on_log then on_log("Renaming directory: " .. abs_path .. " to " .. new_abs_path) end
os.rename(abs_path, new_abs_path)
return true, nil
if not on_complete then return false, "on_complete not provided" end
M.confirm("Are you sure you want to rename directory " .. abs_path .. " to " .. new_abs_path .. "?", function(ok)
if not ok then
on_complete(false, "User canceled")
return
end
if on_log then on_log("Renaming directory: " .. abs_path .. " to " .. new_abs_path) end
os.rename(abs_path, new_abs_path)
on_complete(true, nil)
end)
end
---@type AvanteLLMToolFunc<{ rel_path: string }>
function M.delete_dir(opts, on_log)
function M.delete_dir(opts, on_log, on_complete)
local abs_path = get_abs_path(opts.rel_path)
if not has_permission_to_access(abs_path) then return false, "No permission to access path: " .. abs_path end
if not Path:new(abs_path):exists() then return false, "Directory not found: " .. abs_path end
if not Path:new(abs_path):is_dir() then return false, "Path is not a directory: " .. abs_path end
if not M.confirm("Are you sure you want to delete the directory: " .. abs_path) then
return false, "User canceled"
end
if on_log then on_log("Deleting directory: " .. abs_path) end
os.remove(abs_path)
return true, nil
if not on_complete then return false, "on_complete not provided" end
M.confirm("Are you sure you want to delete the directory: " .. abs_path, function(ok)
if not ok then
on_complete(false, "User canceled")
return
end
if on_log then on_log("Deleting directory: " .. abs_path) end
os.remove(abs_path)
on_complete(true, nil)
end)
end
---@type AvanteLLMToolFunc<{ rel_path: string, command: string }>
@@ -275,11 +339,6 @@ function M.bash(opts, on_log, on_complete)
if not has_permission_to_access(abs_path) then return false, "No permission to access path: " .. abs_path end
if not Path:new(abs_path):exists() then return false, "Path not found: " .. abs_path end
if on_log then on_log("command: " .. opts.command) end
if
not M.confirm("Are you sure you want to run the command: `" .. opts.command .. "` in the directory: " .. abs_path)
then
return false, "User canceled"
end
---change cwd to abs_path
---@param output string
---@param exit_code integer
@@ -292,18 +351,20 @@ function M.bash(opts, on_log, on_complete)
end
return output, nil
end
if on_complete then
Utils.shell_run_async(opts.command, "bash -c", function(output, exit_code)
local result, err = handle_result(output, exit_code)
on_complete(result, err)
end, abs_path)
return nil, nil
end
local old_cwd = vim.fn.getcwd()
vim.fn.chdir(abs_path)
local res = Utils.shell_run(opts.command, "bash -c")
vim.fn.chdir(old_cwd)
return handle_result(res.stdout, res.code)
if not on_complete then return false, "on_complete not provided" end
M.confirm(
"Are you sure you want to run the command: `" .. opts.command .. "` in the directory: " .. abs_path,
function(ok)
if not ok then
on_complete(false, "User canceled")
return
end
Utils.shell_run_async(opts.command, "bash -c", function(output, exit_code)
local result, err = handle_result(output, exit_code)
on_complete(result, err)
end, abs_path)
end
)
end
---@type AvanteLLMToolFunc<{ query: string }>
@@ -464,7 +525,7 @@ function M.git_diff(opts, on_log)
end
---@type AvanteLLMToolFunc<{ message: string, scope?: string }>
function M.git_commit(opts, on_log)
function M.git_commit(opts, on_log, on_complete)
local git_cmd = vim.fn.exepath("git")
if git_cmd == "" then return false, "Git command not found" end
local project_root = Utils.get_project_root()
@@ -518,36 +579,46 @@ function M.git_commit(opts, on_log)
-- Construct full commit message for confirmation
local full_commit_msg = table.concat(commit_msg_lines, "\n")
if not on_complete then return false, "on_complete not provided" end
-- Confirm with user
if not M.confirm("Are you sure you want to commit with message:\n" .. full_commit_msg) then
return false, "User canceled"
end
M.confirm("Are you sure you want to commit with message:\n" .. full_commit_msg, function(ok)
if not ok then
on_complete(false, "User canceled")
return
end
-- Stage changes if scope is provided
if opts.scope then
local stage_cmd = string.format("git add %s", opts.scope)
if on_log then on_log("Staging files: " .. stage_cmd) end
local stage_result = vim.fn.system(stage_cmd)
if vim.v.shell_error ~= 0 then
on_complete(false, "Failed to stage files: " .. stage_result)
return
end
end
-- Stage changes if scope is provided
if opts.scope then
local stage_cmd = string.format("git add %s", opts.scope)
if on_log then on_log("Staging files: " .. stage_cmd) end
local stage_result = vim.fn.system(stage_cmd)
if vim.v.shell_error ~= 0 then return false, "Failed to stage files: " .. stage_result end
end
-- Construct git commit command
local cmd_parts = { "git", "commit" }
-- Only add -S flag if GPG is available
if has_gpg then table.insert(cmd_parts, "-S") end
for _, line in ipairs(commit_msg_lines) do
table.insert(cmd_parts, "-m")
table.insert(cmd_parts, '"' .. line .. '"')
end
local cmd = table.concat(cmd_parts, " ")
-- Construct git commit command
local cmd_parts = { "git", "commit" }
-- Only add -S flag if GPG is available
if has_gpg then table.insert(cmd_parts, "-S") end
for _, line in ipairs(commit_msg_lines) do
table.insert(cmd_parts, "-m")
table.insert(cmd_parts, '"' .. line .. '"')
end
local cmd = table.concat(cmd_parts, " ")
-- Execute git commit
if on_log then on_log("Running command: " .. cmd) end
local result = vim.fn.system(cmd)
-- Execute git commit
if on_log then on_log("Running command: " .. cmd) end
local result = vim.fn.system(cmd)
if vim.v.shell_error ~= 0 then
on_complete(false, "Failed to commit: " .. result)
return
end
if vim.v.shell_error ~= 0 then return false, "Failed to commit: " .. result end
return true, nil
on_complete(true, nil)
end)
end
---@type AvanteLLMToolFunc<{ query: string }>
@@ -571,57 +642,62 @@ function M.python(opts, on_log, on_complete)
if on_log then on_log("cwd: " .. abs_path) end
if on_log then on_log("code:\n" .. opts.code) end
local container_image = opts.container_image or "python:3.11-slim-bookworm"
if
not M.confirm(
"Are you sure you want to run the following python code in the `"
.. container_image
.. "` container, in the directory: `"
.. abs_path
.. "`?\n"
.. opts.code
)
then
return nil, "User canceled"
end
if vim.fn.executable("docker") == 0 then return nil, "Python tool is not available to execute any code" end
if not on_complete then return nil, "on_complete not provided" end
M.confirm(
"Are you sure you want to run the following python code in the `"
.. container_image
.. "` container, in the directory: `"
.. abs_path
.. "`?\n"
.. opts.code,
function(ok)
if not ok then
on_complete(nil, "User canceled")
return
end
if vim.fn.executable("docker") == 0 then
on_complete(nil, "Python tool is not available to execute any code")
return
end
local function handle_result(result) ---@param result vim.SystemCompleted
if result.code ~= 0 then return nil, "Error: " .. (result.stderr or "Unknown error") end
local function handle_result(result) ---@param result vim.SystemCompleted
if result.code ~= 0 then return nil, "Error: " .. (result.stderr or "Unknown error") end
Utils.debug("output", result.stdout)
return result.stdout, nil
end
local job = vim.system(
{
"docker",
"run",
"--rm",
"-v",
abs_path .. ":" .. abs_path,
"-w",
abs_path,
container_image,
"python",
"-c",
opts.code,
},
{
text = true,
cwd = abs_path,
},
vim.schedule_wrap(function(result)
if not on_complete then return end
local output, err = handle_result(result)
on_complete(output, err)
end)
Utils.debug("output", result.stdout)
return result.stdout, nil
end
vim.system(
{
"docker",
"run",
"--rm",
"-v",
abs_path .. ":" .. abs_path,
"-w",
abs_path,
container_image,
"python",
"-c",
opts.code,
},
{
text = true,
cwd = abs_path,
},
vim.schedule_wrap(function(result)
if not on_complete then return end
local output, err = handle_result(result)
on_complete(output, err)
end)
)
end
)
if on_complete then return end
local result = job:wait()
return handle_result(result)
end
---@param user_input string
---@param history_messages AvanteLLMMessage[]
---@return AvanteLLMTool[]
function M.get_tools()
function M.get_tools(user_input, history_messages)
local custom_tools = Config.custom_tools
if type(custom_tools) == "function" then custom_tools = custom_tools() end
---@type AvanteLLMTool[]
@@ -634,7 +710,7 @@ function M.get_tools()
if tool.enabled == nil then
return true
else
return tool.enabled()
return tool.enabled({ user_input = user_input, history_messages = history_messages })
end
end)
:totable()
@@ -644,7 +720,7 @@ end
M._tools = {
{
name = "glob",
description = 'Fast file pattern matching using glob patterns like "**/*.js"',
description = 'Fast file pattern matching using glob patterns like "**/*.js", in current project scope',
param = {
type = "table",
fields = {
@@ -655,7 +731,7 @@ M._tools = {
},
{
name = "rel_path",
description = "Relative path to the directory, as cwd",
description = "Relative path to the project directory, as cwd",
type = "string",
},
},
@@ -704,7 +780,7 @@ M._tools = {
},
{
name = "python",
description = "Run python code. Can't use it to read files or modify files.",
description = "Run python code in current project scope. Can't use it to read files or modify files.",
param = {
type = "table",
fields = {
@@ -715,7 +791,7 @@ M._tools = {
},
{
name = "rel_path",
description = "Relative path to the directory, as cwd",
description = "Relative path to the project directory, as cwd",
type = "string",
},
},
@@ -736,7 +812,7 @@ M._tools = {
},
{
name = "git_diff",
description = "Get git diff for generating commit message",
description = "Get git diff for generating commit message in current project scope",
param = {
type = "table",
fields = {
@@ -763,7 +839,7 @@ M._tools = {
},
{
name = "git_commit",
description = "Commit changes with the given commit message",
description = "Commit changes with the given commit message in current project scope",
param = {
type = "table",
fields = {
@@ -796,13 +872,13 @@ M._tools = {
},
{
name = "list_files",
description = "List files in a directory",
description = "List files in current project scope",
param = {
type = "table",
fields = {
{
name = "rel_path",
description = "Relative path to the directory",
description = "Relative path to the project directory",
type = "string",
},
{
@@ -815,7 +891,7 @@ M._tools = {
returns = {
{
name = "files",
description = "List of files in the directory",
description = "List of filepaths in the directory",
type = "string[]",
},
{
@@ -828,13 +904,13 @@ M._tools = {
},
{
name = "search_files",
description = "Search for files in a directory",
description = "Search for files in current project scope",
param = {
type = "table",
fields = {
{
name = "rel_path",
description = "Relative path to the directory",
description = "Relative path to the project directory",
type = "string",
},
{
@@ -847,7 +923,7 @@ M._tools = {
returns = {
{
name = "files",
description = "List of files that match the keyword",
description = "List of filepaths that match the keyword",
type = "string",
},
{
@@ -860,13 +936,13 @@ M._tools = {
},
{
name = "grep_search",
description = "Search for a keyword in a directory using grep",
description = "Search for a keyword in a directory using grep in current project scope",
param = {
type = "table",
fields = {
{
name = "rel_path",
description = "Relative path to the directory",
description = "Relative path to the project directory",
type = "string",
},
{
@@ -911,13 +987,13 @@ M._tools = {
},
{
name = "read_file_toplevel_symbols",
description = "Read the top-level symbols of a file",
description = "Read the top-level symbols of a file in current project scope",
param = {
type = "table",
fields = {
{
name = "rel_path",
description = "Relative path to the file",
description = "Relative path to the file in current project scope",
type = "string",
},
},
@@ -938,13 +1014,28 @@ M._tools = {
},
{
name = "read_file",
description = "Read the contents of a file. If the file content is already in the context, do not use this tool.",
description = "Read the contents of a file in current project scope. If the file content is already in the context, do not use this tool.",
enabled = function(opts)
if opts.user_input:match("@read_global_file") then return false end
for _, message in ipairs(opts.history_messages) do
if message.role == "user" then
local content = message.content
if type(content) == "string" and content:match("@read_global_file") then return false end
if type(content) == "table" then
for _, item in ipairs(content) do
if type(item) == "string" and item:match("@read_global_file") then return false end
end
end
end
end
return true
end,
param = {
type = "table",
fields = {
{
name = "rel_path",
description = "Relative path to the file",
description = "Relative path to the file in current project scope",
type = "string",
},
},
@@ -963,15 +1054,104 @@ M._tools = {
},
},
},
{
name = "read_global_file",
description = "Read the contents of a file in the global scope. If the file content is already in the context, do not use this tool.",
enabled = function(opts)
if opts.user_input:match("@read_global_file") then return true end
for _, message in ipairs(opts.history_messages) do
if message.role == "user" then
local content = message.content
if type(content) == "string" and content:match("@read_global_file") then return true end
if type(content) == "table" then
for _, item in ipairs(content) do
if type(item) == "string" and item:match("@read_global_file") then return true end
end
end
end
end
return false
end,
param = {
type = "table",
fields = {
{
name = "abs_path",
description = "Absolute path to the file in global scope",
type = "string",
},
},
},
returns = {
{
name = "content",
description = "Contents of the file",
type = "string",
},
{
name = "error",
description = "Error message if the file was not read successfully",
type = "string",
optional = true,
},
},
},
{
name = "write_global_file",
description = "Write to a file in the global scope",
enabled = function(opts)
if opts.user_input:match("@write_global_file") then return true end
for _, message in ipairs(opts.history_messages) do
if message.role == "user" then
local content = message.content
if type(content) == "string" and content:match("@write_global_file") then return true end
if type(content) == "table" then
for _, item in ipairs(content) do
if type(item) == "string" and item:match("@write_global_file") then return true end
end
end
end
end
return false
end,
param = {
type = "table",
fields = {
{
name = "abs_path",
description = "Absolute path to the file in global scope",
type = "string",
},
{
name = "content",
description = "Content to write to the file",
type = "string",
},
},
},
returns = {
{
name = "success",
description = "True if the file was written successfully, false otherwise",
type = "boolean",
},
{
name = "error",
description = "Error message if the file was not written successfully",
type = "string",
optional = true,
},
},
},
{
name = "create_file",
description = "Create a new file",
description = "Create a new file in current project scope",
param = {
type = "table",
fields = {
{
name = "rel_path",
description = "Relative path to the file",
description = "Relative path to the file in current project scope",
type = "string",
},
},
@@ -992,13 +1172,13 @@ M._tools = {
},
{
name = "rename_file",
description = "Rename a file",
description = "Rename a file in current project scope",
param = {
type = "table",
fields = {
{
name = "rel_path",
description = "Relative path to the file",
description = "Relative path to the file in current project scope",
type = "string",
},
{
@@ -1024,13 +1204,13 @@ M._tools = {
},
{
name = "delete_file",
description = "Delete a file",
description = "Delete a file in current project scope",
param = {
type = "table",
fields = {
{
name = "rel_path",
description = "Relative path to the file",
description = "Relative path to the file in current project scope",
type = "string",
},
},
@@ -1051,13 +1231,13 @@ M._tools = {
},
{
name = "create_dir",
description = "Create a new directory",
description = "Create a new directory in current project scope",
param = {
type = "table",
fields = {
{
name = "rel_path",
description = "Relative path to the directory",
description = "Relative path to the project directory",
type = "string",
},
},
@@ -1078,13 +1258,13 @@ M._tools = {
},
{
name = "rename_dir",
description = "Rename a directory",
description = "Rename a directory in current project scope",
param = {
type = "table",
fields = {
{
name = "rel_path",
description = "Relative path to the directory",
description = "Relative path to the project directory",
type = "string",
},
{
@@ -1110,13 +1290,13 @@ M._tools = {
},
{
name = "delete_dir",
description = "Delete a directory",
description = "Delete a directory in current project scope",
param = {
type = "table",
fields = {
{
name = "rel_path",
description = "Relative path to the directory",
description = "Relative path to the project directory",
type = "string",
},
},
@@ -1137,13 +1317,13 @@ M._tools = {
},
{
name = "bash",
description = "Run a bash command in a directory. Can't use search commands like find/grep or read tools like cat/ls. Can't use it to read files or modify files.",
description = "Run a bash command in current project scope. Can't use search commands like find/grep or read tools like cat/ls. Can't use it to read files or modify files.",
param = {
type = "table",
fields = {
{
name = "rel_path",
description = "Relative path to the directory",
description = "Relative path to the project directory, as cwd",
type = "string",
},
{