Files
2026-06-30 10:38:52 +08:00

492 lines
14 KiB
Go

package forwarder
import (
"fmt"
"strings"
"unicode/utf8"
"github.com/sergi/go-diff/diffmatchpatch"
"google.golang.org/protobuf/encoding/protojson"
"cursor/gen/agentv1"
runtimecore "cursor/internal/backend/agent/core"
)
const editReadBinaryContentLimit = 32 * 1024
type editComputation struct {
BeforeContent string
AfterContent string
DiffString string
LinesAdded int32
LinesRemoved int32
Message string
}
type editResultPayload struct {
BeforeContent string
AfterContent string
DiffString string
LinesAdded int32
LinesRemoved int32
Message string
}
type editMatch struct {
Start int
End int
}
type normalizedEditContent struct {
Text string
OriginalOffsets []int
}
func computeEditDiff(beforeContent string, afterContent string) (string, int32, int32) {
dmp := diffmatchpatch.New()
charsBefore, charsAfter, lineArray := dmp.DiffLinesToChars(beforeContent, afterContent)
diffs := dmp.DiffMain(charsBefore, charsAfter, false)
diffs = dmp.DiffCharsToLines(diffs, lineArray)
linesAdded := int32(0)
linesRemoved := int32(0)
for _, diff := range diffs {
switch diff.Type {
case diffmatchpatch.DiffInsert:
linesAdded += countChangedLines(diff.Text)
case diffmatchpatch.DiffDelete:
linesRemoved += countChangedLines(diff.Text)
}
}
return dmp.PatchToText(dmp.PatchMake(beforeContent, diffs)), linesAdded, linesRemoved
}
func normalizeEditContentLineEndings(content string) normalizedEditContent {
var builder strings.Builder
builder.Grow(len(content))
offsets := make([]int, 1, len(content)+1)
offsets[0] = 0
for index := 0; index < len(content); {
if content[index] == '\r' && index+1 < len(content) && content[index+1] == '\n' {
builder.WriteByte('\n')
index += 2
offsets = append(offsets, index)
continue
}
if content[index] == '\r' {
builder.WriteByte('\n')
index++
offsets = append(offsets, index)
continue
}
builder.WriteByte(content[index])
index++
offsets = append(offsets, index)
}
return normalizedEditContent{
Text: builder.String(),
OriginalOffsets: offsets,
}
}
func normalizeLineEndingsToLF(value string) string {
if !strings.ContainsAny(value, "\r\n") {
return value
}
normalized := strings.ReplaceAll(value, "\r\n", "\n")
return strings.ReplaceAll(normalized, "\r", "\n")
}
func detectPreferredFileLineEnding(content string) string {
return dominantLineEnding(content, "\n")
}
func preferredMatchLineEnding(content string, match editMatch, defaultLineEnding string) string {
return dominantLineEnding(content[match.Start:match.End], defaultLineEnding)
}
func normalizeReplacementLineEndings(newString string, lineEnding string) string {
if !strings.ContainsAny(newString, "\r\n") {
return newString
}
normalized := normalizeLineEndingsToLF(newString)
switch lineEnding {
case "\r\n":
return strings.ReplaceAll(normalized, "\n", "\r\n")
case "\r":
return strings.ReplaceAll(normalized, "\n", "\r")
default:
return normalized
}
}
func dominantLineEnding(content string, fallback string) string {
if fallback == "" {
fallback = "\n"
}
counts := map[string]int{
"\n": 0,
"\r\n": 0,
"\r": 0,
}
first := ""
for index := 0; index < len(content); {
switch {
case content[index] == '\r' && index+1 < len(content) && content[index+1] == '\n':
counts["\r\n"]++
if first == "" {
first = "\r\n"
}
index += 2
case content[index] == '\r':
counts["\r"]++
if first == "" {
first = "\r"
}
index++
case content[index] == '\n':
counts["\n"]++
if first == "" {
first = "\n"
}
index++
default:
index++
}
}
best := ""
bestCount := 0
for _, candidate := range []string{"\r\n", "\n", "\r"} {
if counts[candidate] > bestCount {
best = candidate
bestCount = counts[candidate]
}
}
if bestCount == 0 {
return fallback
}
for _, candidate := range []string{"\r\n", "\n", "\r"} {
if counts[candidate] == bestCount && candidate == first {
return candidate
}
}
return best
}
func countChangedLines(text string) int32 {
if text == "" {
return 0
}
count := int32(strings.Count(text, "\n"))
if !strings.HasSuffix(text, "\n") {
count++
}
return count
}
func buildCompletedEditToolCall(path string, result *agentv1.EditResult) *agentv1.ToolCall {
args := &agentv1.EditArgs{Path: strings.TrimSpace(path)}
return &agentv1.ToolCall{
Tool: &agentv1.ToolCall_EditToolCall{
EditToolCall: &agentv1.EditToolCall{
Args: args,
Result: result,
},
},
}
}
func buildSuccessfulEditResult(path string, beforeContent string, afterContent string, diffString string, linesAdded int32, linesRemoved int32, message string) *agentv1.EditResult {
success := &agentv1.EditSuccess{
Path: strings.TrimSpace(path),
AfterFullFileContent: afterContent,
}
if beforeContent != "" {
success.BeforeFullFileContent = stringPtr(beforeContent)
}
if diffString != "" {
success.DiffString = stringPtr(diffString)
}
if linesAdded != 0 {
success.LinesAdded = int32Ptr(linesAdded)
}
if linesRemoved != 0 {
success.LinesRemoved = int32Ptr(linesRemoved)
}
if message != "" {
success.Message = stringPtr(message)
}
return &agentv1.EditResult{
Result: &agentv1.EditResult_Success{Success: success},
}
}
func buildFinalEditSuccessResult(path string, afterContent string, payload editResultPayload) *agentv1.EditResult {
diffString := payload.DiffString
linesAdded := payload.LinesAdded
linesRemoved := payload.LinesRemoved
if afterContent != payload.AfterContent {
diffString, linesAdded, linesRemoved = computeEditDiff(payload.BeforeContent, afterContent)
}
return buildSuccessfulEditResult(path, payload.BeforeContent, afterContent, diffString, linesAdded, linesRemoved, payload.Message)
}
func buildEditErrorResult(path string, errorMessage string) *agentv1.EditResult {
trimmedPath := strings.TrimSpace(path)
trimmedError := strings.TrimSpace(errorMessage)
return &agentv1.EditResult{
Result: &agentv1.EditResult_Error{
Error: &agentv1.EditError{
Path: trimmedPath,
Error: trimmedError,
ModelVisibleError: stringPtr(trimmedError),
},
},
}
}
func decodeReadDataAsEditableText(data []byte) (string, bool, string) {
if len(data) > editReadBinaryContentLimit {
return "", false, fmt.Sprintf("Read returned binary data larger than %d bytes", editReadBinaryContentLimit)
}
if !utf8.Valid(data) {
return "", false, "Read returned binary data that is not valid UTF-8 text"
}
return string(data), true, ""
}
func buildEditResultFromReadResult(path string, result *agentv1.ReadResult) *agentv1.EditResult {
if result == nil {
return buildEditErrorResult(path, "read result missing")
}
switch item := result.GetResult().(type) {
case *agentv1.ReadResult_FileNotFound:
return &agentv1.EditResult{
Result: &agentv1.EditResult_FileNotFound{
FileNotFound: &agentv1.EditFileNotFound{Path: firstNonEmpty(item.FileNotFound.GetPath(), path)},
},
}
case *agentv1.ReadResult_PermissionDenied:
return &agentv1.EditResult{
Result: &agentv1.EditResult_ReadPermissionDenied{
ReadPermissionDenied: &agentv1.EditReadPermissionDenied{Path: firstNonEmpty(item.PermissionDenied.GetPath(), path)},
},
}
case *agentv1.ReadResult_Rejected:
return &agentv1.EditResult{
Result: &agentv1.EditResult_Rejected{
Rejected: &agentv1.EditRejected{
Path: firstNonEmpty(item.Rejected.GetPath(), path),
Reason: item.Rejected.GetReason(),
},
},
}
case *agentv1.ReadResult_InvalidFile:
return buildEditErrorResult(firstNonEmpty(item.InvalidFile.GetPath(), path), item.InvalidFile.GetReason())
case *agentv1.ReadResult_Error:
return buildEditErrorResult(firstNonEmpty(item.Error.GetPath(), path), item.Error.GetError())
case *agentv1.ReadResult_Success:
return buildEditErrorResult(firstNonEmpty(item.Success.GetPath(), path), "read result did not include editable text content")
default:
return buildEditErrorResult(path, "unknown read result")
}
}
func buildEditResultFromWriteResult(path string, result *agentv1.WriteResult) *agentv1.EditResult {
if result == nil {
return buildEditErrorResult(path, "write result missing")
}
switch item := result.GetResult().(type) {
case *agentv1.WriteResult_PermissionDenied:
return &agentv1.EditResult{
Result: &agentv1.EditResult_WritePermissionDenied{
WritePermissionDenied: &agentv1.EditWritePermissionDenied{
Path: firstNonEmpty(item.PermissionDenied.GetPath(), path),
Error: item.PermissionDenied.GetError(),
},
},
}
case *agentv1.WriteResult_Rejected:
return &agentv1.EditResult{
Result: &agentv1.EditResult_Rejected{
Rejected: &agentv1.EditRejected{
Path: firstNonEmpty(item.Rejected.GetPath(), path),
Reason: item.Rejected.GetReason(),
},
},
}
case *agentv1.WriteResult_NoSpace:
return buildEditErrorResult(firstNonEmpty(item.NoSpace.GetPath(), path), "no space left")
case *agentv1.WriteResult_Error:
return buildEditErrorResult(firstNonEmpty(item.Error.GetPath(), path), item.Error.GetError())
case *agentv1.WriteResult_Success:
afterContent := item.Success.GetFileContentAfterWrite()
return buildSuccessfulEditResult(firstNonEmpty(item.Success.GetPath(), path), "", afterContent, "", 0, 0, "")
default:
return buildEditErrorResult(path, "unknown write result")
}
}
func extractReadContentForEdit(result *agentv1.ReadResult) (string, bool) {
success := result.GetSuccess()
if success == nil {
return "", false
}
switch output := success.GetOutput().(type) {
case *agentv1.ReadSuccess_Content:
return normalizeLineEndingsToLF(output.Content), true
case *agentv1.ReadSuccess_Data:
text, ok, _ := decodeReadDataAsEditableText(output.Data)
return normalizeLineEndingsToLF(text), ok
}
if success.GetOutputBlobId() != nil {
return "", false
}
return "", true
}
func summarizeEditResult(result *agentv1.EditResult) string {
if result == nil {
return "edit result missing"
}
switch item := result.GetResult().(type) {
case *agentv1.EditResult_Success:
return item.Success.GetAfterFullFileContent()
case *agentv1.EditResult_FileNotFound:
return fmt.Sprintf("file not found: %s", item.FileNotFound.GetPath())
case *agentv1.EditResult_ReadPermissionDenied:
return fmt.Sprintf("permission denied: %s", item.ReadPermissionDenied.GetPath())
case *agentv1.EditResult_WritePermissionDenied:
return item.WritePermissionDenied.GetError()
case *agentv1.EditResult_Rejected:
return item.Rejected.GetReason()
case *agentv1.EditResult_Error:
return item.Error.GetError()
default:
return "unknown edit result"
}
}
func summarizeEditResultWithoutPath(result *agentv1.EditResult) string {
if result == nil {
return "edit result missing"
}
switch item := result.GetResult().(type) {
case *agentv1.EditResult_Success:
return item.Success.GetAfterFullFileContent()
case *agentv1.EditResult_FileNotFound:
return "file not found"
case *agentv1.EditResult_ReadPermissionDenied:
return "permission denied"
case *agentv1.EditResult_WritePermissionDenied:
return item.WritePermissionDenied.GetError()
case *agentv1.EditResult_Rejected:
return item.Rejected.GetReason()
case *agentv1.EditResult_Error:
return item.Error.GetError()
default:
return "unknown edit result"
}
}
func editResultWithoutPath(result *agentv1.EditResult) *agentv1.EditResult {
if result == nil {
return nil
}
switch item := result.GetResult().(type) {
case *agentv1.EditResult_Success:
return &agentv1.EditResult{Result: &agentv1.EditResult_Success{Success: &agentv1.EditSuccess{
AfterFullFileContent: item.Success.GetAfterFullFileContent(),
BeforeFullFileContent: item.Success.BeforeFullFileContent,
DiffString: item.Success.DiffString,
LinesAdded: item.Success.LinesAdded,
LinesRemoved: item.Success.LinesRemoved,
Message: item.Success.Message,
}}}
case *agentv1.EditResult_FileNotFound:
return &agentv1.EditResult{Result: &agentv1.EditResult_FileNotFound{FileNotFound: &agentv1.EditFileNotFound{}}}
case *agentv1.EditResult_ReadPermissionDenied:
return &agentv1.EditResult{Result: &agentv1.EditResult_ReadPermissionDenied{ReadPermissionDenied: &agentv1.EditReadPermissionDenied{}}}
case *agentv1.EditResult_WritePermissionDenied:
return &agentv1.EditResult{Result: &agentv1.EditResult_WritePermissionDenied{WritePermissionDenied: &agentv1.EditWritePermissionDenied{Error: item.WritePermissionDenied.GetError()}}}
case *agentv1.EditResult_Rejected:
return &agentv1.EditResult{Result: &agentv1.EditResult_Rejected{Rejected: &agentv1.EditRejected{Reason: item.Rejected.GetReason()}}}
case *agentv1.EditResult_Error:
return &agentv1.EditResult{Result: &agentv1.EditResult_Error{Error: &agentv1.EditError{
Error: item.Error.GetError(),
ModelVisibleError: item.Error.ModelVisibleError,
}}}
default:
return result
}
}
func summarizeStructuredEditResult(result *agentv1.EditResult) string {
if result == nil {
return "edit result missing"
}
if _, ok := result.GetResult().(*agentv1.EditResult_Success); ok {
encoded, err := protojson.Marshal(result)
if err == nil {
return string(encoded)
}
}
return summarizeEditResult(result)
}
func hiddenWriteControlError(message *agentv1.ExecClientControlMessage) string {
if message == nil {
return "write operation failed"
}
switch item := message.GetMessage().(type) {
case *agentv1.ExecClientControlMessage_Throw:
return firstNonEmpty(strings.TrimSpace(item.Throw.GetError()), "write operation failed")
case *agentv1.ExecClientControlMessage_StreamClose:
return "write operation closed unexpectedly"
default:
return "write operation failed"
}
}
func readJSONStringAny(args map[string]any, keys ...string) (string, bool, bool) {
for _, key := range keys {
value, ok := args[key]
if !ok {
continue
}
typed, ok := value.(string)
if !ok {
return "", true, false
}
return typed, true, true
}
return "", false, false
}
func readJSONIntAny(args map[string]any, keys ...string) (int, bool, bool) {
value, found, err := runtimecore.ReadIntArg(args, keys...)
if err != nil {
return 0, true, false
}
return value, found, found
}
func readJSONBoolAny(args map[string]any, keys ...string) (bool, bool, bool) {
for _, key := range keys {
value, ok := args[key]
if !ok {
continue
}
typed, ok := value.(bool)
if !ok {
return false, true, false
}
return typed, true, true
}
return false, false, false
}
func int32Ptr(value int32) *int32 {
return &value
}