-
Notifications
You must be signed in to change notification settings - Fork 233
Edit tool confirmation uses vscode to show diff when cagent is run within vscode #1441
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,12 @@ | ||
| package editfile | ||
|
|
||
| import ( | ||
| "crypto/sha256" | ||
| "encoding/json" | ||
| "fmt" | ||
| "log/slog" | ||
| "os" | ||
| "os/exec" | ||
| "path/filepath" | ||
| "strings" | ||
| "sync" | ||
|
|
@@ -44,12 +47,13 @@ type linePair struct { | |
| newLineNum int | ||
| } | ||
|
|
||
| func renderEditFile(toolCall tools.ToolCall, width int, splitView bool, toolStatus types.ToolStatus) string { | ||
| var args builtin.EditFileArgs | ||
| if err := json.Unmarshal([]byte(toolCall.Function.Arguments), &args); err != nil { | ||
| return "" | ||
| } | ||
| type editFileRenderer interface { | ||
| Render(args builtin.EditFileArgs, width int, splitView bool, toolStatus types.ToolStatus) string | ||
| } | ||
|
|
||
| type builtinEditFileRenderer struct{} | ||
|
|
||
| func (r *builtinEditFileRenderer) Render(args builtin.EditFileArgs, width int, splitView bool, toolStatus types.ToolStatus) string { | ||
| var output strings.Builder | ||
| for i, edit := range args.Edits { | ||
| if i > 0 { | ||
|
|
@@ -71,6 +75,88 @@ func renderEditFile(toolCall tools.ToolCall, width int, splitView bool, toolStat | |
| return output.String() | ||
| } | ||
|
|
||
| type vscodeLikeRenderer struct { | ||
| externalTool string | ||
| } | ||
|
|
||
| func externalDiffSignature(externalTool, path string, oldContent, newContent []byte) string { | ||
| sha := sha256.New() | ||
| sha.Write([]byte(externalTool)) | ||
| sha.Write([]byte(path)) | ||
| sha.Write(oldContent) | ||
| sha.Write(newContent) | ||
| return fmt.Sprintf("%x", sha.Sum(nil)) | ||
| } | ||
|
|
||
| var activeDiffs sync.Map | ||
|
|
||
| func ensureExternalDiffShownActive(externalTool, path, diffSignature string, newContent []byte) { | ||
| do := func() { | ||
| defer activeDiffs.Delete(diffSignature) | ||
| tempDir, err := os.MkdirTemp("", "cagent-editfile-*") | ||
| if err != nil { | ||
| slog.Warn("Error creating temp dir for external diff", "error", err) | ||
| return | ||
| } | ||
| defer os.RemoveAll(tempDir) | ||
|
|
||
| tempFilePath := filepath.Join(tempDir, filepath.Base(path)) | ||
|
|
||
| if err := os.WriteFile(tempFilePath, newContent, 0o644); err != nil { | ||
| slog.Warn("Error writing temp file for external diff", "error", err) | ||
| return | ||
| } | ||
| // wd is for diff + wait for editor to close. This makes sure we delete the temp dir only after editing is done. | ||
| if err := exec.Command(externalTool, "-wd", path, tempFilePath).Run(); err != nil { | ||
| slog.Warn("Error running external diff tool", "error", err) | ||
| } | ||
| } | ||
| todo, loaded := activeDiffs.LoadOrStore(diffSignature, do) | ||
| // if already loaded, do nothing else, start the diff process | ||
| if !loaded { | ||
| go todo.(func())() | ||
| } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 MEDIUM: Goroutine leak - no cancellation mechanism The goroutine
Recommended fix: Pass a context that can be cancelled on application shutdown. Store running goroutines and cancel them during cleanup. |
||
| } | ||
|
|
||
| func (r *vscodeLikeRenderer) Render(args builtin.EditFileArgs, width int, splitView bool, toolStatus types.ToolStatus) string { | ||
| // assumes we are printing confirmation | ||
|
|
||
| // generate a file with all edits applied | ||
| originalContent, err := os.ReadFile(args.Path) | ||
| if err != nil { | ||
| return styles.CenterStyle.Render(fmt.Sprintf("Error reading original file: %v", err)) | ||
| } | ||
| modifiedContent := string(originalContent) | ||
| for _, edit := range args.Edits { | ||
| modifiedContent = strings.Replace(modifiedContent, edit.OldText, edit.NewText, 1) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 HIGH: Incorrect edit application logic Applying edits sequentially using
This will cause the VSCode diff viewer to show incorrect changes to the user. Recommended fix: Apply edits based on their original positions in the file, not sequentially. Consider using a proper diff/patch algorithm or applying edits from end-to-beginning to avoid position shifts. |
||
| } | ||
|
|
||
| diffSignature := externalDiffSignature(r.externalTool, args.Path, originalContent, []byte(modifiedContent)) | ||
|
|
||
| ensureExternalDiffShownActive(r.externalTool, args.Path, diffSignature, []byte(modifiedContent)) | ||
|
|
||
| return styles.CenterStyle.Render("Please review changes in your code editor") | ||
| } | ||
|
|
||
| func resolveEditFileRenderer(toolStatus types.ToolStatus, isInConfirmationDialog bool) editFileRenderer { | ||
| if toolStatus == types.ToolStatusConfirmation && isInConfirmationDialog && os.Getenv("TERM_PROGRAM") == "vscode" { | ||
| return &vscodeLikeRenderer{ | ||
| externalTool: "code", | ||
| } | ||
| } | ||
| return &builtinEditFileRenderer{} | ||
| } | ||
|
|
||
| func renderEditFile(toolCall tools.ToolCall, width int, splitView bool, toolStatus types.ToolStatus, isInConfirmationDialog bool) string { | ||
| var args builtin.EditFileArgs | ||
| if err := json.Unmarshal([]byte(toolCall.Function.Arguments), &args); err != nil { | ||
| return "" | ||
| } | ||
|
|
||
| renderer := resolveEditFileRenderer(toolStatus, isInConfirmationDialog) | ||
| return renderer.Render(args, width, splitView, toolStatus) | ||
| } | ||
|
|
||
| func computeDiff(path, oldText, newText string, toolStatus types.ToolStatus) []*udiff.Hunk { | ||
| currentContent, err := os.ReadFile(path) | ||
| if err != nil { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 MEDIUM: No context for command execution
exec.Command(externalTool, "-wd", path, tempFilePath).Run()runs without a context for cancellation. If the editor hangs or the user wants to exit the application, there's no way to terminate this command.Recommended fix: Use
exec.CommandContext(ctx, externalTool, "-wd", path, tempFilePath).Run()with a cancellable context.