Compare commits

...

3 Commits

9 changed files with 295 additions and 218 deletions

View File

@@ -16,6 +16,7 @@ enable_debug_commands: true
prompt_char_limit: 300000 prompt_char_limit: 300000
enable_chunking: false enable_chunking: false
enable_step_by_step: true enable_step_by_step: true
auto_lint: true
# New tool auto-accept config # New tool auto-accept config
tool_auto_accept: tool_auto_accept:

View File

@@ -83,3 +83,18 @@ commands:
The **list** command now uses the Linux `ls` command to list directory contents. The **grep** command searches for a given pattern in a file or all files in a directory. The **list** command now uses the Linux `ls` command to list directory contents. The **grep** command searches for a given pattern in a file or all files in a directory.
Enjoy your improved, more flexible ChatGPT Neovim plugin with step-by-step support! Enjoy your improved, more flexible ChatGPT Neovim plugin with step-by-step support!
## Test when developing
add new plugin to runtimepath
```vim
:set rtp^=~/temp_plugins/chatgpt.vim
```
Clear Lua module cache
```vim
:lua for k in pairs(package.loaded) do if k:match("^chatgpt_nvim") then package.loaded[k]=nil end end
```
Load new plugin code
```vim
:runtime plugin/chatgpt.vim
```

View File

@@ -50,12 +50,14 @@ 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 -- If auto_lint is true, we only do LSP-based checks.
auto_lint = false,
tool_auto_accept = { tool_auto_accept = {
readFile = false, readFile = false,
editFile = false, editFile = false,
replace_in_file = false,
executeCommand = false, executeCommand = false,
} }
} }
@@ -106,11 +108,12 @@ 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 -- auto_lint controls whether we do LSP-based checks
if type(result.auto_lint) == "boolean" then
config.auto_lint = result.auto_lint
end end
-- Load tool_auto_accept if present
if type(result.tool_auto_accept) == "table" then if type(result.tool_auto_accept) == "table" then
for k, v in pairs(result.tool_auto_accept) do for k, v in pairs(result.tool_auto_accept) do
if config.tool_auto_accept[k] ~= nil and type(v) == "boolean" then if config.tool_auto_accept[k] ~= nil and type(v) == "boolean" then
@@ -124,7 +127,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,4 +1,6 @@
local handler = require("chatgpt_nvim.handler") local handler = require("chatgpt_nvim.handler")
local robust_lsp = require("chatgpt_nvim.tools.lsp_robust_diagnostics")
local M = {} local M = {}
M.run = function(tool_call, conf, prompt_user_tool_accept, is_subpath, read_file) M.run = function(tool_call, conf, prompt_user_tool_accept, is_subpath, read_file)
@@ -14,13 +16,21 @@ M.run = function(tool_call, conf, prompt_user_tool_accept, is_subpath, read_file
return string.format("Tool [edit_file for '%s'] REJECTED. Path outside project root.", path) return string.format("Tool [edit_file for '%s'] REJECTED. Path outside project root.", path)
end end
-- 1) Write the new content
handler.write_file(path, new_content, conf) handler.write_file(path, new_content, conf)
local msg = {} local msg = {}
msg[#msg+1] = string.format("Tool [edit_file for '%s'] Result:\nThe content was successfully saved to %s.", path, path) msg[#msg+1] = string.format("Tool [edit_file for '%s'] Result:\nThe content was successfully saved to %s.", path, path)
msg[#msg+1] = "\nHere is the full, updated content of the file that was saved:\n" msg[#msg+1] = "\nHere is the full, updated content of the file that was saved:\n"
msg[#msg+1] = string.format("<final_file_content path=\"%s\">\n%s\n</final_file_content>", path, new_content) msg[#msg+1] = string.format("<final_file_content path=\"%s\">\n%s\n</final_file_content>", path, new_content)
msg[#msg+1] = "\nIMPORTANT: For any future changes to this file, use the final_file_content shown above as your reference.\n" msg[#msg+1] = "\nIMPORTANT: For any future changes to this file, use the final_file_content shown above as your reference.\n"
-- 2) If auto_lint => run robust LSP approach
if conf.auto_lint then
local diag_str = robust_lsp.lsp_check_file_content(path, new_content, 3000) -- 3s wait
msg[#msg+1] = "\n" .. diag_str
end
return table.concat(msg, "") return table.concat(msg, "")
end end

View File

@@ -0,0 +1,167 @@
local api = vim.api
local lsp = vim.lsp
local M = {}
local function guess_filetype(path)
local ext = path:match("^.+%.([^./\\]+)$")
if not ext then return nil end
ext = ext:lower()
local map = {
lua = "lua",
go = "go",
rs = "rust",
js = "javascript",
jsx = "javascriptreact",
ts = "typescript",
tsx = "typescriptreact",
php = "php",
}
return map[ext]
end
local function create_scratch_buffer(path, content)
local bufnr = api.nvim_create_buf(false, true)
if bufnr == 0 then
return nil
end
api.nvim_buf_set_name(bufnr, path)
local lines = {}
for line in (content.."\n"):gmatch("(.-)\n") do
table.insert(lines, line)
end
api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
api.nvim_buf_set_option(bufnr, "bufhidden", "wipe")
api.nvim_buf_set_option(bufnr, "swapfile", false)
return bufnr
end
local function attach_existing_lsp_client(bufnr, filetype)
local clients = lsp.get_active_clients()
if not clients or #clients == 0 then
return nil, "No active LSP clients"
end
for _, client in ipairs(clients) do
local ft_conf = client.config.filetypes or {}
if vim.tbl_contains(ft_conf, filetype) then
if not client.attached_buffers[bufnr] then
vim.lsp.buf_attach_client(bufnr, client.id)
end
return client.id
end
end
return nil, ("No active LSP client supports filetype '%s'"):format(filetype)
end
local function send_did_open(bufnr, client_id, path, filetype)
local client = lsp.get_client_by_id(client_id)
if not client then
return "Invalid client ID."
end
local text = table.concat(api.nvim_buf_get_lines(bufnr, 0, -1, false), "\n")
local uri = vim.uri_from_fname(path)
local didOpenParams = {
textDocument = {
uri = uri,
languageId = filetype,
version = 0,
text = text,
}
}
client.rpc.notify("textDocument/didOpen", didOpenParams)
return nil
end
local function send_did_change(bufnr, client_id)
local client = lsp.get_client_by_id(client_id)
if not client then return end
local text = table.concat(api.nvim_buf_get_lines(bufnr, 0, -1, false), "\n")
local uri = vim.uri_from_bufnr(bufnr)
local version = 1
client.rpc.notify("textDocument/didChange", {
textDocument = {
uri = uri,
version = version,
},
contentChanges = {
{ text = text }
}
})
end
local function wait_for_diagnostics(bufnr, timeout_ms)
local done = false
local result_diags = {}
local augrp = api.nvim_create_augroup("chatgpt_lsp_diag_"..bufnr, { clear = true })
api.nvim_create_autocmd("DiagnosticChanged", {
group = augrp,
callback = function(args)
if args.buf == bufnr then
local diags = vim.diagnostic.get(bufnr)
result_diags = diags
done = true
end
end
})
local waited = 0
local interval = 50
while not done and waited < timeout_ms do
vim.cmd(("sleep %d m"):format(interval))
waited = waited + interval
end
pcall(api.nvim_del_augroup_by_id, augrp)
return result_diags
end
local function diagnostics_to_string(diags)
if #diags == 0 then
return "No LSP diagnostics reported."
end
local lines = { "--- LSP Diagnostics ---" }
for _, d in ipairs(diags) do
local sev = vim.diagnostic.severity[d.severity] or d.severity
lines[#lines+1] = string.format("Line %d [%s]: %s", d.lnum + 1, sev, d.message)
end
return table.concat(lines, "\n")
end
function M.lsp_check_file_content(path, new_content, timeout_ms)
local filetype = guess_filetype(path) or "plaintext"
local bufnr = create_scratch_buffer(path, new_content)
if not bufnr then
return "(LSP) Could not create scratch buffer."
end
local client_id, err = attach_existing_lsp_client(bufnr, filetype)
if not client_id then
vim.api.nvim_buf_delete(bufnr, { force = true })
return "(LSP) " .. (err or "No suitable LSP client.")
end
local err2 = send_did_open(bufnr, client_id, path, filetype)
if err2 then
vim.api.nvim_buf_delete(bufnr, { force = true })
return "(LSP) " .. err2
end
send_did_change(bufnr, client_id)
local diags = wait_for_diagnostics(bufnr, timeout_ms or 2000)
local diag_str = diagnostics_to_string(diags)
vim.api.nvim_buf_delete(bufnr, { force = true })
return diag_str
end
return M

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,51 +13,66 @@ 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)
vim.api.nvim_out_write(msg .. " [y/N]: ")
local ans = vim.fn.input("")
if ans:lower() == "y" then
return true
end
return false
end
local auto_accept = conf.tool_auto_accept[tool_call.tool] local auto_accept = conf.tool_auto_accept[tool_call.tool]
-- If this is an executeCommand and we see it's destructive, force a user prompt
if tool_call.tool == "executeCommand" and auto_accept then if tool_call.tool == "executeCommand" and auto_accept then
if is_destructive_command(tool_call.command) then if is_destructive_command(tool_call.command) then
auto_accept = false auto_accept = false
end end
end end
if not auto_accept then if auto_accept then
return ask_user(("Tool request: %s -> Accept?"):format(tool_call.tool or "unknown")) -- If auto-accepted and not destructive, no prompt needed
else
return true return true
else
-- Build some context about the tool request
local msg = ("Tool request: %s\n"):format(tool_call.tool or "unknown")
if tool_call.path then
msg = msg .. ("Path: %s\n"):format(tool_call.path)
end
if tool_call.command then
msg = msg .. ("Command: %s\n"):format(tool_call.command)
end
if tool_call.replacements then
msg = msg .. ("Replacements: %s\n"):format(vim.inspect(tool_call.replacements))
end
msg = msg .. "Accept this tool request? [y/N]: "
-- Force a screen redraw so the user sees the prompt properly
vim.cmd("redraw")
local ans = vim.fn.input(msg)
return ans:lower() == "y"
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, ("Tool [%s] was rejected by user."):format(call.tool or "nil"))
else else
local tool_impl = tools_module.tools_by_name[call.tool] local tool_impl = tools_module.tools_by_name[call.tool]
if tool_impl then if tool_impl then
local msg = tool_impl.run(call, conf, prompt_user_tool_accept, is_subpath_fn, read_file_fn) local msg = tool_impl.run(call, conf, prompt_user_tool_accept, is_subpath_fn, read_file_fn)
table.insert(messages, msg) table.insert(messages, msg)
else else
table.insert(messages, string.format("Unknown tool type: '%s'", call.tool or "nil")) table.insert(messages, ("Unknown tool type: '%s'"):format(call.tool or "nil"))
end end
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 (%d chars). Please break down the operations into smaller steps."):format(#combined)
end
return combined
end end
M.handle_tool_calls = handle_tool_calls M.handle_tool_calls = handle_tool_calls

View File

@@ -1,14 +1,13 @@
local handler = require("chatgpt_nvim.handler") local handler = require("chatgpt_nvim.handler")
local robust_lsp = require("chatgpt_nvim.tools.lsp_robust_diagnostics")
local M = {} local M = {}
local function search_and_replace(original, replacements) local function search_and_replace(original, replacements)
-- Basic approach: do a global plain text replace for each entry
local updated = original local updated = original
for _, r in ipairs(replacements) do for _, r in ipairs(replacements) do
local search_str = r.search or "" local search_str = r.search or ""
local replace_str = r.replace or "" local replace_str = r.replace or ""
-- Here we do a global plain text replacement
updated = updated:gsub(search_str, replace_str) updated = updated:gsub(search_str, replace_str)
end end
return updated return updated
@@ -41,6 +40,11 @@ M.run = function(tool_call, conf, prompt_user_tool_accept, is_subpath, read_file
msg[#msg+1] = string.format("<final_file_content path=\"%s\">\n%s\n</final_file_content>", path, updated_data) msg[#msg+1] = string.format("<final_file_content path=\"%s\">\n%s\n</final_file_content>", path, updated_data)
msg[#msg+1] = "\nIMPORTANT: For any future changes to this file, use the final_file_content shown above as your reference.\n" msg[#msg+1] = "\nIMPORTANT: For any future changes to this file, use the final_file_content shown above as your reference.\n"
if conf.auto_lint then
local diag_str = robust_lsp.lsp_check_file_content(path, updated_data, 3000)
msg[#msg+1] = "\n" .. diag_str
end
return table.concat(msg, "") return table.concat(msg, "")
end end