tools/file: output diff format (context=bare, new=+, changes=unified diff)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Levi Neely 2026-04-12 20:39:42 +02:00
parent 5efcafa9a6
commit 496087a874
6 changed files with 44 additions and 6 deletions

2
go.mod
View File

@ -3,3 +3,5 @@ module ollie
go 1.25.6
require gopkg.in/yaml.v3 v3.0.1
require github.com/aymanbagabas/go-udiff v0.4.1

2
go.sum
View File

@ -1,3 +1,5 @@
github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o=
github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

25
pkg/tools/file/diff.go Normal file
View File

@ -0,0 +1,25 @@
package file
import (
"strings"
"github.com/aymanbagabas/go-udiff"
)
// unifiedDiff produces a unified diff of oldContent → newContent for display.
func unifiedDiff(path, oldContent, newContent string) string {
return udiff.Unified(path, path, oldContent, newContent)
}
// plusLines formats content as diff "added" lines, each prefixed with "+".
// Used by file_write for new files.
func plusLines(content string) string {
lines := strings.Split(strings.TrimRight(content, "\n"), "\n")
sb := strings.Builder{}
for _, l := range lines {
sb.WriteByte('+')
sb.WriteString(l)
sb.WriteByte('\n')
}
return sb.String()
}

View File

@ -16,7 +16,7 @@ var ToolEdit = tools.ToolInfo{
Usage:
- You must use your ` + "`" + `Read` + "`" + ` tool at least once in the conversation before editing. This tool will error if you attempt an edit without reading the file.
- When editing text from Read tool output, ensure you preserve the exact indentation (tabs/spaces) as it appears AFTER the line number prefix. The line number prefix format is: line number + "| ". Everything after that is the actual file content to match. Never include any part of the line number prefix in the old_string or new_string.
- When editing text from Read tool output, ensure you preserve the exact indentation (tabs/spaces) as it appears AFTER the leading "+" prefix. Never include the leading "+" in old_string or new_string.
- ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.
- Only use emojis if the user explicitly requests it. Avoid adding emojis to files unless asked.
- The edit will FAIL if ` + "`" + `old_string` + "`" + ` is not unique in the file. Either provide a larger string with more surrounding context to make it unique or use ` + "`" + `replace_all` + "`" + ` to change every instance of ` + "`" + `old_string` + "`" + `.
@ -67,7 +67,7 @@ func (s *Server) dispatchEdit(_ context.Context, raw json.RawMessage) (string, e
return "", fmt.Errorf("writing file: %w", err)
}
return "File edited successfully", nil
return unifiedDiff(a.FilePath, original, newContent), nil
}
// robustReplace tries exact → whitespace-normalized → indentation-flexible → trimmed-boundary.

View File

@ -22,7 +22,7 @@ Usage:
- By default, it reads up to 2000 lines starting from the beginning of the file
- You can optionally specify a line offset and limit (especially handy for long files), but it's recommended to read the whole file by not providing these parameters
- Any lines longer than 2000 characters will be truncated
- Results are returned with line numbers in the format: line number + "| " + content, starting at 1
- Results are returned in diff format: existing content lines have no prefix
- This tool can only read files, not directories. To read a directory, use an ls command via the Bash tool.
- You can call multiple tools in a single response. It is always better to speculatively read multiple potentially useful files in parallel.
- If you read a file that exists but has empty contents "(empty file)" will be returned.`,
@ -82,7 +82,7 @@ func (s *Server) dispatchRead(_ context.Context, raw json.RawMessage) (string, e
if len(line) > maxLineLen {
line = line[:maxLineLen] + "..."
}
lines = append(lines, fmt.Sprintf("%05d| %s", lineNum, line))
lines = append(lines, line)
}
lineNum++
if lineNum > off+lim {
@ -97,7 +97,7 @@ func (s *Server) dispatchRead(_ context.Context, raw json.RawMessage) (string, e
}
s.markRead(a.FilePath)
return strings.Join(lines, "\n"), nil
return strings.Join(lines, "\n") + "\n", nil
}
// openChecked validates and opens a regular file within size limits.

View File

@ -47,10 +47,19 @@ func (s *Server) dispatchWrite(_ context.Context, raw json.RawMessage) (string,
return errText("existing file must be read first. Use the Read tool to examine the file contents."), nil
}
var oldContent string
if exists {
oldContent, _ = readFileChecked(a.FilePath)
}
if err := os.WriteFile(a.FilePath, []byte(a.Content), 0644); err != nil { //nolint:gosec
return "", fmt.Errorf("writing file: %w", err)
}
s.markRead(a.FilePath)
return "File written successfully", nil
if !exists {
return plusLines(a.Content), nil
}
return unifiedDiff(a.FilePath, oldContent, a.Content), nil
}