change-to-tools #2

Merged
dominik.polakovics merged 5 commits from change-to-tools into main 2025-01-31 13:38:30 +01:00
4 changed files with 63 additions and 202 deletions
Showing only changes of commit 0ff77954db - Show all commits

View File

@@ -50,12 +50,12 @@ function M.load()
improved_debug = false, improved_debug = false,
enable_chunking = false, enable_chunking = false,
enable_step_by_step = true, enable_step_by_step = true,
enable_debug_commands = false,
-- New table for tool auto-accept configuration -- Removed enable_debug_commands
tool_auto_accept = { tool_auto_accept = {
readFile = false, readFile = false,
editFile = false, editFile = false,
replace_in_file = false,
executeCommand = false, executeCommand = false,
} }
} }
@@ -106,9 +106,6 @@ function M.load()
if type(result.enable_step_by_step) == "boolean" then if type(result.enable_step_by_step) == "boolean" then
config.enable_step_by_step = result.enable_step_by_step config.enable_step_by_step = result.enable_step_by_step
end end
if type(result.enable_debug_commands) == "boolean" then
config.enable_debug_commands = result.enable_debug_commands
end
-- Load tool_auto_accept if present -- Load tool_auto_accept if present
if type(result.tool_auto_accept) == "table" then if type(result.tool_auto_accept) == "table" then
@@ -124,7 +121,7 @@ function M.load()
config.initial_prompt = "You are a coding assistant who receives a project's context and user instructions..." config.initial_prompt = "You are a coding assistant who receives a project's context and user instructions..."
end end
-- Merge the default prompt blocks with the config's initial prompt -- Merge default prompt blocks
if type(config.default_prompt_blocks) == "table" and #config.default_prompt_blocks > 0 then if type(config.default_prompt_blocks) == "table" and #config.default_prompt_blocks > 0 then
local merged_prompt = {} local merged_prompt = {}
for _, block_name in ipairs(config.default_prompt_blocks) do for _, block_name in ipairs(config.default_prompt_blocks) do

View File

@@ -67,7 +67,6 @@ end
-- PROMPT CONSTRUCTION -- PROMPT CONSTRUCTION
------------------------------------------------------------------------------ ------------------------------------------------------------------------------
local function build_tools_available_block() local function build_tools_available_block()
-- We'll list each tool from tools_module.available_tools
local lines = {} local lines = {}
lines[#lines+1] = "<tools_available>" lines[#lines+1] = "<tools_available>"
for _, t in ipairs(tools_module.available_tools) do for _, t in ipairs(tools_module.available_tools) do
@@ -94,7 +93,7 @@ local function build_prompt(user_input, dirs, conf)
-- 3) <task> -- 3) <task>
local task_lines = {} local task_lines = {}
table.insert(task_lines, "<task>") task_lines[#task_lines+1] = "<task>"
task_lines[#task_lines+1] = user_input task_lines[#task_lines+1] = user_input
for _, file_path in ipairs(initial_files) do for _, file_path in ipairs(initial_files) do
task_lines[#task_lines+1] = ("'%s' (see below for file content)"):format(file_path) task_lines[#task_lines+1] = ("'%s' (see below for file content)"):format(file_path)
@@ -154,7 +153,7 @@ local function handle_step_by_step_if_needed(prompt, conf)
end end
------------------------------------------------------------------------------ ------------------------------------------------------------------------------
-- :ChatGPT Command -- :ChatGPT
------------------------------------------------------------------------------ ------------------------------------------------------------------------------
local function run_chatgpt_command() local function run_chatgpt_command()
local conf = config.load() local conf = config.load()
@@ -221,7 +220,7 @@ local function run_chatgpt_command()
end end
------------------------------------------------------------------------------ ------------------------------------------------------------------------------
-- :ChatGPTPaste Command -- :ChatGPTPaste
------------------------------------------------------------------------------ ------------------------------------------------------------------------------
local function run_chatgpt_paste_command() local function run_chatgpt_paste_command()
local conf = config.load() local conf = config.load()
@@ -239,29 +238,21 @@ local function run_chatgpt_paste_command()
return return
end end
-- 1) Debug commands -- Check if we have tools
if data.commands and conf.enable_debug_commands then if data.tools then
local results = {} -- Must also verify project name
for _, cmd in ipairs(data.commands) do if not data.project_name or data.project_name ~= conf.project_name then
table.insert(results, require('chatgpt_nvim').execute_debug_command(cmd, conf)) vim.api.nvim_err_writeln("Project name mismatch or missing. Aborting tool usage.")
end
local output = table.concat(results, "\n\n")
copy_to_clipboard(output)
print("Debug command results copied to clipboard!")
return return
end end
-- 2) Tools (handle multiple calls in one request)
if data.tools then
local output_messages = tools_manager.handle_tool_calls(data.tools, conf, is_subpath, read_file) local output_messages = tools_manager.handle_tool_calls(data.tools, conf, is_subpath, read_file)
-- If the output is too large (over conf.prompt_char_limit?), we might respond with a special message.
copy_to_clipboard(output_messages) copy_to_clipboard(output_messages)
print("Tool call results have been processed and copied to clipboard.") print("Tool call results have been processed and copied to clipboard.")
return return
end end
-- 3) If we see project_name & files => final changes or file requests -- If we see project_name & files => older YAML style. We handle it but it's discouraged now.
-- (If the user is still using older YAML style, we handle it, but not recommended.)
if data.project_name and data.files then if data.project_name and data.files then
if data.project_name ~= conf.project_name then if data.project_name ~= conf.project_name then
vim.api.nvim_err_writeln("Project name mismatch. Aborting.") vim.api.nvim_err_writeln("Project name mismatch. Aborting.")
@@ -294,7 +285,6 @@ local function run_chatgpt_paste_command()
vim.api.nvim_err_writeln("Preview not closed in time. Aborting.") vim.api.nvim_err_writeln("Preview not closed in time. Aborting.")
return return
end end
end end
local final_files = data.files local final_files = data.files
@@ -354,7 +344,7 @@ local function run_chatgpt_paste_command()
"\n\nProject name: " .. (conf.project_name or ""), "\n\nProject name: " .. (conf.project_name or ""),
"\n\nBelow are the requested files from the project, each preceded by its filename in backticks and enclosed in triple backticks.\n", "\n\nBelow are the requested files from the project, each preceded by its filename in backticks and enclosed in triple backticks.\n",
table.concat(file_sections, "\n"), table.concat(file_sections, "\n"),
"\n\nIf you need more files, please respond again in YAML listing additional files. Or use the new 'tools' array approach to read/edit files. If you have all info, provide final changes or just proceed." "\n\nIf you need more files, please respond again in YAML listing additional files, or use the 'tools:' approach. If you have all info, provide final changes or continue your instructions."
} }
local prompt = table.concat(sections, "\n") local prompt = table.concat(sections, "\n")
@@ -375,7 +365,7 @@ local function run_chatgpt_paste_command()
print("Prompt (with requested files) copied to clipboard! Paste it into ChatGPT.") print("Prompt (with requested files) copied to clipboard! Paste it into ChatGPT.")
end end
else else
vim.api.nvim_err_writeln("Invalid response. Expected either 'tools' or 'project_name & files' in YAML.") vim.api.nvim_err_writeln("No tools or recognized file instructions found. Provide 'tools:' or older 'project_name & files'.")
end end
end end
@@ -425,94 +415,4 @@ M.run_chatgpt_command = run_chatgpt_command
M.run_chatgpt_paste_command = run_chatgpt_paste_command M.run_chatgpt_paste_command = run_chatgpt_paste_command
M.run_chatgpt_current_buffer_command = run_chatgpt_current_buffer_command M.run_chatgpt_current_buffer_command = run_chatgpt_current_buffer_command
-- Provide debug commands for "ls" and "grep"
M.execute_debug_command = function(cmd, conf)
if type(cmd) ~= "table" or not cmd.command then
return "Invalid command object."
end
local command = cmd.command
if command == "ls" then
local dir = cmd.dir or "."
local args = cmd.args or {}
local cmd_str = "ls"
if #args > 0 then
cmd_str = cmd_str .. " " .. table.concat(args, " ")
end
cmd_str = cmd_str .. " " .. dir
local handle = io.popen(cmd_str)
if not handle then
return "Failed to run ls command."
end
local result = handle:read("*a") or ""
handle:close()
return "Listing files in: " .. dir .. "\n" .. result
elseif command == "grep" then
local args = cmd.args or {}
if #args == 0 then
local pattern = cmd.pattern
local target = cmd.target
if not pattern or not target then
return "Usage for grep: {command='grep', args=['-r','pattern','target']} or {pattern='<text>', target='<path>'}"
end
local stat = vim.loop.fs_stat(target)
if not stat then
return "Cannot grep: target path does not exist"
end
local function do_grep(search_string, filepath)
local c = read_file(filepath)
if not c then
return "Could not read file: " .. filepath
end
local lines = {}
local line_num = 0
for line in c:gmatch("([^\n]*)\n?") do
line_num = line_num + 1
if line:find(search_string, 1, true) then
lines[#lines+1] = filepath .. ":" .. line_num .. ":" .. line
end
end
return (#lines == 0) and ("No matches in " .. filepath) or table.concat(lines, "\n")
end
if stat.type == "directory" then
local h = io.popen("ls -p " .. target .. " | grep -v /")
if not h then
return "Failed to read directory contents for grep."
end
local all_files = {}
for file in h:read("*a"):gmatch("[^\n]+") do
all_files[#all_files+1] = target .. "/" .. file
end
h:close()
local results = {}
for _, f in ipairs(all_files) do
local fstat = vim.loop.fs_stat(f)
if fstat and fstat.type == "file" then
results[#results+1] = do_grep(pattern, f)
end
end
return table.concat(results, "\n")
else
return do_grep(pattern, target)
end
else
-- new approach with flags/args
local cmd_str = "grep " .. table.concat(args, " ")
local handle = io.popen(cmd_str)
if not handle then
return "Failed to run grep command."
end
local result = handle:read("*a") or ""
handle:close()
return result
end
else
return "Unknown command: " .. command
end
end
return M return M

View File

@@ -256,54 +256,40 @@ local M = {
["basic-prompt"] = [[ ["basic-prompt"] = [[
### Basic Prompt ### Basic Prompt
1. **Analyze Required Files** You are assisting me in a coding workflow for a project (e.g., "my_project"). Whenever you need to inspect or modify files, or execute commands, you must provide both:
- Check the project structure (or any provided file list).
- If you identify a file reference or function you do not have, **produce a YAML snippet** requesting that files content in full. For example: 1. `project_name: "<actual_project_name>"` (matching the real project name)
2. A `tools:` array describing the operations you want to perform.
**Example** (substitute `<actual_project_name>` with the real name):
```yaml ```yaml
project_name: my_project project_name: "my_project"
files: tools:
- path: "relative/path/to/needed_file" - tool: "readFile"
``` path: "relative/path/to/file"
- Do not proceed with final changes if you lack the necessary file contents.
- Always provide the entire file content when you do include a file.
2. **Request File Contents** - tool: "replace_in_file"
- For every file you need but dont have, provide a short YAML snippet (like above) to retrieve it. path: "relative/path/to/file"
- Ask any clarifying questions outside the YAML code block. replacements:
- search: "old text"
replace: "new text"
3. **Provide Output YAML** - tool: "editFile"
- When you have all information, output the final YAML in this format: path: "relative/path/to/file"
```yaml
project_name: my_project
files:
- path: "src/main.py"
content: | content: |
# Updated main function # Full updated file content here
def main():
print("Hello, World!")
- path: "docs/README.md" - tool: "executeCommand"
delete: true command: "ls -la"
``` ```
- `project_name` must match the projects configured name.
- Each file item in `files` must have `path` and either `content` or `delete: true`.
4. **Explain Changes** **Key Points**:
- After the final YAML, add a brief explanation of the modifications (outside the YAML). - Always include `project_name: "<actual_project_name>"` in the same YAML as `tools`.
- If you only need one tool, include just one object in the `tools` array.
5. **Iterate as Needed** - If multiple tools are needed, list them sequentially under `tools`.
- If further context or changes are required, repeat steps to request files or clarifications. - Allways run at least one tool (e.g., `readFile`, `editFile`, `executeCommand`), exept you have finished.
- The plugin will verify the `project_name` is correct before running any tools.
6. **Important Notes** - If the response grows too large, I'll guide you to break it into smaller steps.
- Never modify or delete a file you havent explicitly requested or received.
- Use comments in code only to clarify code logic, not to explain your thought process.
- Explanations go outside the YAML.
7. **Best Practices**
- Keep file paths accurate to avoid mistakes during implementation.
- Maintain a clear, logical structure for your changes.
- Use the final YAML consistently for clarity.
]], ]],
["secure-coding"] = [[ ["secure-coding"] = [[
### Secure Coding Guidelines ### Secure Coding Guidelines
@@ -370,33 +356,10 @@ local M = {
["step-prompt"] = [[ ["step-prompt"] = [[
It appears this request might exceed the model's prompt character limit if done all at once. It appears this request might exceed the model's prompt character limit if done all at once.
Please break down the tasks into smaller steps and handle them one by one. Please break down the tasks into smaller steps and handle them one by one.
At each step, we'll provide relevant files or context if needed.
Thank you! Thank you!
]], ]],
["file-selection-instructions"] = [[ ["file-selection-instructions"] = [[
Delete lines for directories you do NOT want, then save & close (e.g. :wq, :x, or :bd) Delete lines for directories you do NOT want, then save & close (e.g. :wq, :x, or :bd)
]],
["debug-commands-info"] = [[
### Debugging Commands
If you need debugging commands, include them in your YAML response as follows:
```yaml
commands:
- command: "ls"
args: ["-l", "path/to/directory"]
- command: "grep"
args: ["-r", "searchString", "path/to/file/or/directory"]
```
The "ls" command uses the system's 'ls' command to list directory contents. You can pass flags or additional arguments in `args`.
The "grep" command searches for a given pattern in files or directories, again receiving flags or additional arguments in `args`.
If you omit `args` for grep, you can still use the older format with `pattern` and `target` for backward compatibility.
This are the only two commands supported at the moment.
When these commands are present and `enable_debug_commands` is true, I'll execute them and return the results in the clipboard.
]] ]]
} }

View File

@@ -1,8 +1,7 @@
local tools_module = require("chatgpt_nvim.tools") local tools_module = require("chatgpt_nvim.tools")
local uv = vim.loop
local M = {} local M = {}
-- Simple destructive command check
local function is_destructive_command(cmd) local function is_destructive_command(cmd)
if not cmd then return false end if not cmd then return false end
local destructive_list = { "rm", "sudo", "mv", "cp" } local destructive_list = { "rm", "sudo", "mv", "cp" }
@@ -14,7 +13,6 @@ local function is_destructive_command(cmd)
return false return false
end end
-- Prompt user if not auto-accepted or if command is destructive
local function prompt_user_tool_accept(tool_call, conf) local function prompt_user_tool_accept(tool_call, conf)
local function ask_user(msg) local function ask_user(msg)
vim.api.nvim_out_write(msg .. " [y/N]: ") vim.api.nvim_out_write(msg .. " [y/N]: ")
@@ -39,11 +37,9 @@ local function prompt_user_tool_accept(tool_call, conf)
end end
end end
-- We'll pass references to `read_file` from init.
local function handle_tool_calls(tools, conf, is_subpath_fn, read_file_fn) local function handle_tool_calls(tools, conf, is_subpath_fn, read_file_fn)
local messages = {} local messages = {}
for _, call in ipairs(tools) do for _, call in ipairs(tools) do
-- Prompt user acceptance
local accepted = prompt_user_tool_accept(call, conf) local accepted = prompt_user_tool_accept(call, conf)
if not accepted then if not accepted then
table.insert(messages, string.format("Tool [%s] was rejected by user.", call.tool or "nil")) table.insert(messages, string.format("Tool [%s] was rejected by user.", call.tool or "nil"))
@@ -58,7 +54,12 @@ local function handle_tool_calls(tools, conf, is_subpath_fn, read_file_fn)
end end
end end
return table.concat(messages, "\n\n") local combined = table.concat(messages, "\n\n")
local limit = conf.prompt_char_limit or 8000
if #combined > limit then
return "The combined tool output is too large ("..#combined.."). Please break down the operations into smaller steps."
end
return combined
end end
M.handle_tool_calls = handle_tool_calls M.handle_tool_calls = handle_tool_calls