Skip to content

feat: support batched edits with optional replaceAll in edit_file #273

@LeeCheneler

Description

@LeeCheneler

User Story

As an LLM using Tomo,
I want to apply multiple edits to a file in a single tool call and optionally replace all occurrences of a string,
so that I can complete refactors and renames in fewer round-trips with a single confirmation prompt.

Acceptance Criteria

  • edit_file accepts an edits array instead of a single oldString/newString pair
  • Each entry in edits has oldString, newString, and optional replaceAll (default false)
  • When replaceAll is false, oldString must match exactly once in the current file content (existing behaviour)
  • When replaceAll is true, every occurrence of oldString is replaced; fails only if there are zero matches
  • Edits are applied sequentially: each edit operates on the result of the previous edit, not the original file content
  • The whole call is atomic per file: if any edit fails, the file is not modified and the tool returns an error identifying which edit failed
  • A single combined unified diff is generated from the original content vs. the final content after all edits
  • Permission check and confirmation prompt happen once for the combined diff, not per edit
  • formatCall displays the path along with the number of edits (e.g. path/to/file.ts (3 edits))
  • Tool description is rewritten to explain the new contract: prefer batched edits over multiple calls, explain sequential application, explain replaceAll semantics
  • Tests cover: single edit, multiple sequential edits, replaceAll true/false, atomic failure when one edit in the batch fails, identical oldString/newString rejection, zero-match and multi-match failures

Additional Context

Today edit_file accepts a single oldString/newString pair and requires the match to be unique. A refactor that touches N spots in a file becomes N tool calls, N round-trips, and N confirmation prompts when the write permission isn't pre-granted. This is slow and noisy.

Batching solves the round-trip and prompt cost. replaceAll removes the friction of having to add surrounding context to disambiguate when the LLM legitimately wants to rename every occurrence (e.g. variable rename, import path change). The unique-match guardrail stays as the default — replaceAll is opt-in per edit so accidental sweeps still require an explicit choice.

Sequential application (each edit sees the previous edit's output) is important: it means the LLM doesn't have to mentally model non-overlapping windows in the original file, which is a common failure mode. Atomicity per file means there's no partial-write state for the model to recover from on failure.

Existing implementation: src/tools/edit-file.ts. The unified diff helper at src/tools/diff.ts already handles (original, final) so the combined-diff requirement is essentially free.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions