diff --git a/environment/filesystem.go b/environment/filesystem.go index a322c616..0847856a 100644 --- a/environment/filesystem.go +++ b/environment/filesystem.go @@ -63,14 +63,20 @@ type FileEditRequest struct { MatchID string } -func (env *Environment) FileEdit(ctx context.Context, req FileEditRequest) error { +// FileEditResult contains information about a successful file edit +type FileEditResult struct { + // Snippet is a short context snippet around the replaced text + Snippet string +} + +func (env *Environment) FileEdit(ctx context.Context, req FileEditRequest) (*FileEditResult, error) { if err := env.validateNotSubmoduleFile(req.TargetFile); err != nil { - return err + return nil, err } contents, err := env.container().File(req.TargetFile).Contents(ctx) if err != nil { - return err + return nil, err } matches := []int{} @@ -86,7 +92,7 @@ func (env *Environment) FileEdit(ctx context.Context, req FileEditRequest) error } if len(matches) == 0 { - return fmt.Errorf("search text not found in file %s", req.TargetFile) + return nil, fmt.Errorf("search text not found in file %s", req.TargetFile) } if len(matches) > 1 && req.MatchID == "" { @@ -97,7 +103,7 @@ func (env *Environment) FileEdit(ctx context.Context, req FileEditRequest) error matchDescriptions = append(matchDescriptions, fmt.Sprintf("Match %d (ID: %s):\n%s", i+1, id, context)) } - return fmt.Errorf("multiple matches found for search text in %s. Please specify which_match parameter with one of the following IDs:\n\n%s", + return nil, fmt.Errorf("multiple matches found for search text in %s. Please specify which_match parameter with one of the following IDs:\n\n%s", req.TargetFile, strings.Join(matchDescriptions, "\n\n")) } @@ -115,7 +121,7 @@ func (env *Environment) FileEdit(ctx context.Context, req FileEditRequest) error } } if !found { - return fmt.Errorf("match ID %s not found", req.MatchID) + return nil, fmt.Errorf("match ID %s not found", req.MatchID) } } @@ -125,10 +131,28 @@ func (env *Environment) FileEdit(ctx context.Context, req FileEditRequest) error ctr := env.container() err = env.apply(ctx, ctr.WithDirectory(".", ctr.Directory(".").WithPatch(patch))) if err != nil { - return fmt.Errorf("failed applying file edit, skipping git propagation: %w", err) + return nil, fmt.Errorf("failed applying file edit, skipping git propagation: %w", err) + } + + // Verify the edit was actually applied by reading the file back + verifiedContents, err := env.container().File(req.TargetFile).Contents(ctx) + if err != nil { + return nil, fmt.Errorf("failed to verify edit was applied: %w", err) } + + if verifiedContents != newContents { + return nil, fmt.Errorf("edit verification failed: file contents of %s after applying the patch do not match expected result", req.TargetFile) + } + + // Generate a context snippet around the replacement for confirmation + replaceIndex := targetMatchIndex + if replaceIndex >= len(verifiedContents) { + replaceIndex = max(0, len(verifiedContents)-1) + } + snippet := getMatchContext(verifiedContents, replaceIndex) + env.Notes.Add("Edit %s", req.TargetFile) - return nil + return &FileEditResult{Snippet: snippet}, nil } func (env *Environment) FileDelete(ctx context.Context, explanation, targetFile string) error { diff --git a/mcpserver/tools.go b/mcpserver/tools.go index ba73195d..42691103 100644 --- a/mcpserver/tools.go +++ b/mcpserver/tools.go @@ -788,21 +788,22 @@ func createEnvironmentFileEditTool(singleTenant bool) *Tool { return nil, err } - if err := env.FileEdit(ctx, environment.FileEditRequest{ + result, err := env.FileEdit(ctx, environment.FileEditRequest{ Explanation: request.GetString("explanation", ""), TargetFile: targetFile, Search: search, Replace: replace, MatchID: request.GetString("which_match", ""), - }); err != nil { - return mcp.NewToolResultErrorFromErr("failed to write file", err), nil + }) + if err != nil { + return mcp.NewToolResultErrorFromErr("failed to edit file", err), nil } if err := repo.UpdateFile(ctx, env, targetFile, request.GetString("explanation", "")); err != nil { return mcp.NewToolResultErrorFromErr("unable to update the environment", err), nil } - return mcp.NewToolResultText(fmt.Sprintf("file %s edited successfully and committed to container-use/%s remote ref", targetFile, env.ID)), nil + return mcp.NewToolResultText(fmt.Sprintf("file %s edited successfully and committed to container-use/%s remote ref\n\nVerified edit - snippet around replacement:\n%s", targetFile, env.ID, result.Snippet)), nil }, } }