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 go 1.25.6
require gopkg.in/yaml.v3 v3.0.1 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 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 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: 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. - 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. - 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. - 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` + "`" + `. - 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 "", 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. // 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 - 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 - 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 - 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. - 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. - 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.`, - 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 { if len(line) > maxLineLen {
line = line[:maxLineLen] + "..." line = line[:maxLineLen] + "..."
} }
lines = append(lines, fmt.Sprintf("%05d| %s", lineNum, line)) lines = append(lines, line)
} }
lineNum++ lineNum++
if lineNum > off+lim { if lineNum > off+lim {
@ -97,7 +97,7 @@ func (s *Server) dispatchRead(_ context.Context, raw json.RawMessage) (string, e
} }
s.markRead(a.FilePath) 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. // 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 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 if err := os.WriteFile(a.FilePath, []byte(a.Content), 0644); err != nil { //nolint:gosec
return "", fmt.Errorf("writing file: %w", err) return "", fmt.Errorf("writing file: %w", err)
} }
s.markRead(a.FilePath) s.markRead(a.FilePath)
return "File written successfully", nil
if !exists {
return plusLines(a.Content), nil
}
return unifiedDiff(a.FilePath, oldContent, a.Content), nil
} }