This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new ab68fe253780 CAMEL-25501: camel-jbang-mcp - camel_edit_file can apply 
several edits to one file in one call (#27633)
ab68fe253780 is described below

commit ab68fe253780f03f70b4f12ba5ccab67177a49f8
Author: Claus Ibsen <[email protected]>
AuthorDate: Sat Oct 10 00:10:57 2026 +0200

    CAMEL-25501: camel-jbang-mcp - camel_edit_file can apply several edits to 
one file in one call (#27633)
    
    * CAMEL-25501: camel-jbang-mcp - camel_edit_file can apply several edits to 
one file in one call
    
    camel_edit_file replaced one snippet per call, so a change to two places of 
a file (send to
    direct:shipment, and add the route that consumes it) was written in two 
calls, and dev mode
    reloaded the half-done file in between: exchanges waited for the route not 
there yet and were
    cut off by the next reload.
    
    - edits: an optional list of find/replace, applied in order to the content, 
validated, written
      and reloaded once; all or nothing, the answer says which edit missed
    - a write or edit whose YAML route sends to a direct: endpoint no route 
consumes yet answers
      with a note, so the route goes in the same call
    - array parameters with an items schema in the shared tool descriptor; 
camel ask passes list
      arguments as JSON
    - the camel-jbang monitor applies the edits as one confirmed change, notes 
an unconsumed
      direct: endpoint, and its AI panel prompt points to camel_edit_file (full 
prefix budget
      raised to 10500 for the edits schema)
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
    
    * CAMEL-25501: the note's summary and text come from one place
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
    
    * CAMEL-25501: say why a null edit is kept
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
    
    ---------
    
    Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
---
 .../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc    |   3 +
 .../modules/ROOT/pages/camel-jbang-mcp.adoc        |   5 +
 .../ROOT/pages/camel-jbang-tui-ai-agents.adoc      |   2 +-
 .../camel/dsl/jbang/core/commands/AskTools.java    |  12 +-
 .../dsl/jbang/core/commands/ai/AuthoringTools.java | 180 +++++++++++++++++++--
 .../dsl/jbang/core/commands/ai/ToolDescriptor.java |  34 +++-
 .../core/commands/ai/AuthoringToolsEditTest.java   | 112 +++++++++++++
 .../jbang/core/commands/mcp/AuthoringTools.java    |  40 ++++-
 .../core/commands/mcp/AuthoringToolsTest.java      |  18 +++
 .../camel/dsl/jbang/core/commands/tui/AiPanel.java |   7 +-
 .../dsl/jbang/core/commands/tui/McpFacade.java     |  16 +-
 .../jbang/core/commands/tui/TuiToolRegistry.java   |   7 +-
 .../jbang/core/commands/tui/AiPanelAcpTest.java    |   2 +-
 .../core/commands/tui/AiPanelPromptBudgetTest.java |   4 +-
 .../core/commands/tui/McpFacadeWriteFileTest.java  |  26 +++
 15 files changed, 440 insertions(+), 28 deletions(-)

diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
index f022c9640d33..dafc371e0f27 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
@@ -2209,6 +2209,9 @@ The authoring set has a new tool, `camel_edit_file`: it 
replaces one snippet of
 and what to put there, and validates and reloads the result as a write does. A 
model that rewrites a whole file to
 change one step corrupts the lines it did not mean to touch, so this is the 
tool for a change to an existing file;
 `camel_write_file` writes a new one.
+Several places of one file are changed in one call with its `edits` argument 
(a list of find and replace), so the
+file is written and reloaded once, not half done in between. A write or an 
edit whose route sends to a `direct:`
+endpoint no route consumes yet answers with a note saying so.
 
 `camel_eval_expression` evaluates any language, not only the ones on the 
server's own classpath: the component of
 a language such as `jsonpath`, `jq` or `xpath` is downloaded on first use, as 
`camel run` downloads what a route
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
index 7b51b46e0c83..3caabf9a0d22 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
@@ -354,6 +354,11 @@ project `directory` as an argument, the runtime tools take 
the integration `name
   Validated and reloaded as a write is, and the answer says at which line it 
changed. A model that rewrites a whole
   file to add one step corrupts the lines it did not mean to touch, so this is 
the tool for a change to an existing
   file; `camel_write_file` writes a new one.
+  A change to several places of one file goes in one call, as `edits` (a list 
of find and replace, applied in
+  order): the file is validated, written and reloaded once, never half done, 
and when one of them does not match
+  the file is not changed and the answer says which. A route that sends to a 
`direct:` endpoint no route consumes
+  yet gets a note in the answer: in dev mode the messages sent there wait for 
the route, and the reload that adds it
+  cuts them off, so the route belongs in the same call.
 
 | `camel_run`
 | Starts an integration from a project directory with `camel run --source-dir` 
in a separate process, in dev
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-ai-agents.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-ai-agents.adoc
index af88d7ee0468..3ba4c8e8cba2 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-ai-agents.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-ai-agents.adoc
@@ -249,7 +249,7 @@ told what became of it with your next question.
 
 An edit (`camel_edit_file`, which the agent uses to change a file without 
rewriting it) goes the same way: the
 snippet is replaced in the file's content and the result is confirmed or 
replayed like any write, so what you see
-in the editor is the finished file either way.
+in the editor is the finished file either way. An edit with several changes 
(`edits`) is one change to confirm.
 
 While the replay pauses you can also ask the agent about the change it just 
made: *F8* opens the AI panel
 with the question prefilled (`About edit 2 of 3:`), in a compact panel that 
leaves the edit in view (*Shift+F8*
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/AskTools.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/AskTools.java
index cf20eaa21f45..655ca21a45e4 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/AskTools.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/AskTools.java
@@ -85,7 +85,12 @@ public class AskTools {
             } else {
                 Map<String, JsonObject> props = new LinkedHashMap<>();
                 for (ToolDescriptor.Param p : td.params()) {
-                    props.put(p.name(), stringProp(p.description()));
+                    JsonObject prop = stringProp(p.description());
+                    if (p.items() != null) {
+                        prop.put("type", "array");
+                        prop.put("items", p.items());
+                    }
+                    props.put(p.name(), prop);
                 }
                 params = objectParams(props);
             }
@@ -143,7 +148,10 @@ public class AskTools {
                 if (args != null) {
                     for (String key : args.keySet()) {
                         Object val = args.get(key);
-                        if (val != null) {
+                        if (val instanceof Map<?, ?> || val instanceof 
List<?>) {
+                            // e.g. the edits of camel_edit_file: the shared 
tools read JSON
+                            argMap.put(key, Jsoner.serialize(val));
+                        } else if (val != null) {
                             argMap.put(key, val.toString());
                         }
                     }
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java
index 6a51293e425e..453aed7c17bc 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java
@@ -30,13 +30,16 @@ import java.util.Collection;
 import java.util.Comparator;
 import java.util.EnumSet;
 import java.util.HashMap;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
 import java.util.function.Consumer;
+import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
+import org.apache.camel.catalog.CamelCatalog;
 import org.apache.camel.dsl.jbang.core.common.RuntimeHelper;
 import org.apache.camel.util.json.JsonArray;
 import org.apache.camel.util.json.JsonObject;
@@ -61,6 +64,10 @@ public final class AuthoringTools {
 
     static final String NAME_DESC = "Integration name or pid (default: the 
selected one, or the only one running)";
     static final String VERSION_DESC = "Camel version to answer for (default: 
the CLI's own, or the selected integration's)";
+    public static final String EDITS_DESC
+            = "Several changes to the file in one call, applied in order and 
written once, so it is never reloaded"
+              + " half done, e.g. a to: direct:x and the route from direct:x:"
+              + " [{\"find\": \"...\", \"replace\": \"...\"}, {\"find\": 
\"...\", \"replace\": \"...\"}]";
     static final String DIRECTORY_DESC = "Project directory with the source 
files (default: the selected integration's)";
 
     /** Files listed and read by the file tools; more than that and a 
directory is not an integration's sources. */
@@ -239,23 +246,26 @@ public final class AuthoringTools {
                 }));
 
         registry.accept(tool("camel_edit_file",
-                "Changes a file by replacing one snippet: the exact text to 
find (it must occur once) and what to "
+                "Changes a file by replacing a snippet: the exact text to find 
(it must occur once) and what to "
                                                 + "put there. Validated and 
reloaded as a write is. Use it to change an existing file, "
-                                                + "camel_write_file for a new 
one.")
+                                                + "camel_write_file for a new 
one. A change to several places of the file is given "
+                                                + "at once in edits, so the 
file is written and reloaded once, not half done.")
                 .param("directory", "string", DIRECTORY_DESC, false)
                 .param("file", "string", FILE_PATH_DESC, true)
                 .param("find", "string", "The lines to replace as they stand 
in the file; other indentation is fine "
-                                         + "when the lines name one place",
-                        true)
-                .param("replace", "string", "The text to put there; empty 
removes it", true)
+                                         + "when the lines name one place (or 
use edits)",
+                        false)
+                .param("replace", "string", "The text to put there; empty 
removes it", false)
+                .arrayParam("edits", EDITS_DESC, false, "find", "The lines to 
replace as they stand in the file",
+                        "replace", "The text to put there; empty removes it")
                 .param("camelVersion", "string", VERSION_DESC, false)
                 .readOnly(false)
                 .core(true)
                 .executor((ctx, args) -> {
                     applyVersion(ctx, args);
                     Path dir = ctx.resolveDirectory(args.get("directory"));
-                    return editFile(ctx, dir, required(args, "file"), 
required(args, "find"),
-                            args.get("replace") == null ? "" : 
args.get("replace")).toJson();
+                    return editFile(ctx, dir, required(args, "file"), 
args.get("find"),
+                            args.get("replace") == null ? "" : 
args.get("replace"), args.get("edits")).toJson();
                 }));
 
         registry.accept(tool("camel_run",
@@ -607,7 +617,12 @@ public final class AuthoringTools {
     private static final int EDIT_WINDOW_LINES = 20;
 
     public static JsonObject editFile(ToolContext ctx, Path dir, String file, 
String find, String replace) {
-        JsonObject edit = editedContent(dir, file, find, replace);
+        return editFile(ctx, dir, file, find, replace, null);
+    }
+
+    /** As {@link #editFile(ToolContext, Path, String, String, String)} with 
several edits, written once. */
+    public static JsonObject editFile(ToolContext ctx, Path dir, String file, 
String find, String replace, String edits) {
+        JsonObject edit = editedContent(dir, file, find, replace, edits);
         String content = edit.getString("content");
         if (content == null) {
             return edit; // not-found or ambiguous: the answer says what to do 
instead
@@ -617,6 +632,10 @@ public final class AuthoringTools {
             result.put("status", "edited");
             result.put("editedAtLine", edit.getInteger("editedAtLine"));
             result.put("replacedLines", edit.getInteger("replacedLines"));
+            if (edit.get("edits") != null) {
+                result.put("edits", edit.get("edits"));
+                result.put("editedAtLines", edit.get("editedAtLines"));
+            }
         } else {
             result.put("message", "The file was not changed: the result has 
validation errors. Fix them and call"
                                   + " camel_edit_file again.");
@@ -630,19 +649,106 @@ public final class AuthoringTools {
      * TUI writes that content itself, so an edit is confirmed and replayed in 
the editor like a write.
      */
     public static JsonObject editedContent(Path dir, String file, String find, 
String replace) {
+        return editedContent(dir, file, find, replace, null);
+    }
+
+    /**
+     * As {@link #editedContent(Path, String, String, String)} for several 
snippets of one file: find/replace (when
+     * given) and then each edit of {@code edits} (a JSON list of find and 
replace) are applied in order to the content,
+     * so the file is written, and an integration in dev mode reloaded, once. 
A change made in two calls is reloaded
+     * half done in between: a route that sends to a direct endpoint whose 
route the next call adds waits for it, and is
+     * cut off by that reload (CAMEL-25501). All or nothing: the answer of the 
first edit that misses, saying which one,
+     * and the file is not changed.
+     */
+    public static JsonObject editedContent(Path dir, String file, String find, 
String replace, String edits) {
         Path path = resolveFile(dir, file);
         if (!Files.isRegularFile(path)) {
             throw new ToolExecutionException(file + " does not exist: write 
the whole file with camel_write_file");
         }
+        List<String[]> all = new ArrayList<>();
+        if (find != null && !find.isEmpty()) {
+            all.add(new String[] { find, replace == null ? "" : replace });
+        }
+        all.addAll(parseEdits(edits));
+        if (all.isEmpty()) {
+            throw new ToolExecutionException(
+                    "find is required: the text to replace, as it stands in 
the file (or"
+                                             + " edits, a list of find and 
replace for several places)");
+        }
         String content;
         try {
             content = Files.readString(path, StandardCharsets.UTF_8);
         } catch (IOException e) {
             throw new ToolExecutionException("Failed to read " + path + ": " + 
e.getMessage());
         }
-        if (find == null || find.isEmpty()) {
-            throw new ToolExecutionException("find is required: the text to 
replace, as it stands in the file");
+        JsonObject result = null;
+        JsonArray lines = new JsonArray();
+        int replaced = 0;
+        for (int i = 0; i < all.size(); i++) {
+            JsonObject one = editedSnippet(file, content, all.get(i)[0], 
all.get(i)[1]);
+            if (one.getString("content") == null) {
+                if (all.size() > 1) {
+                    one.put("edit", i + 1);
+                    one.put("edits", all.size());
+                    one.put("message", "Edit " + (i + 1) + " of " + all.size() 
+ ": " + one.getString("message")
+                                       + (i > 0 ? " (the file as the edits 
before it leave it)" : "")
+                                       + ". None of the edits was made: the 
file is unchanged.");
+                }
+                return one;
+            }
+            content = one.getString("content");
+            lines.add(one.getInteger("editedAtLine"));
+            replaced += one.getInteger("replacedLines");
+            if (result == null) {
+                result = one;
+            }
+        }
+        result.put("content", content);
+        result.put("replacedLines", replaced);
+        if (all.size() > 1) {
+            result.put("edits", all.size());
+            result.put("editedAtLines", lines);
+        }
+        return result;
+    }
+
+    /** The edits argument: a JSON list of objects with find and replace (or 
that list as a JSON string). */
+    static List<String[]> parseEdits(String edits) {
+        List<String[]> answer = new ArrayList<>();
+        if (edits == null || edits.isBlank()) {
+            return answer;
+        }
+        Object parsed;
+        try {
+            parsed = Jsoner.deserialize(edits);
+            if (parsed instanceof String text) {
+                // the list sent as a string holding JSON
+                parsed = Jsoner.deserialize(text);
+            }
+        } catch (Exception e) {
+            parsed = null;
+        }
+        if (parsed instanceof Map<?, ?> single) {
+            parsed = List.of(single);
+        }
+        if (!(parsed instanceof List<?> list)) {
+            throw new ToolExecutionException(EDITS_SHAPE);
         }
+        for (Object item : list) {
+            if (!(item instanceof Map<?, ?> m) || !(m.get("find") instanceof 
String f) || f.isEmpty()) {
+                throw new ToolExecutionException(EDITS_SHAPE);
+            }
+            answer.add(new String[] { f, m.get("replace") instanceof String r 
? r : "" });
+        }
+        return answer;
+    }
+
+    private static final String EDITS_SHAPE
+            = "edits must be a list of {\"find\": \"the text as it stands in 
the file\", \"replace\": \"the text to put"
+              + " there\"}";
+
+    /** One snippet replaced in the content: the {@link #editedContent(Path, 
String, String, String)} answer. */
+    private static JsonObject editedSnippet(String file, String content, 
String find, String replace) {
         String wanted = find;
         String put = replace;
         boolean trimmedMatch = false;
@@ -1009,8 +1115,15 @@ public final class AuthoringTools {
             // written with problems the file already had: said, so they are 
not taken for fixed
             result.put("existingProblems", new JsonArray(problemsBefore));
         }
+        UnconsumedDirect waiting = null;
         if (validate) {
             putKameletNotes(result, file, content);
+            waiting = unconsumedDirect(dir, file, content, ctx.catalog());
+            if (waiting != null) {
+                JsonArray notes = result.get("notes") instanceof JsonArray 
existing ? existing : new JsonArray();
+                notes.add(waiting.note());
+                result.put("notes", notes);
+            }
         }
         if (watch) {
             JsonObject reload = ReloadOutcome.await(ctx.pid(), processName, 
sinceKey, RELOAD_WAIT_MILLIS);
@@ -1027,9 +1140,56 @@ public final class AuthoringTools {
             result.put("message", "An integration running the file in dev mode 
reloads it now; otherwise restart the"
                                   + " integration for the change to take 
effect.");
         }
+        if (waiting != null) {
+            result.put("message", result.getString("message") + " Note: " + 
waiting.summary() + " (see notes).");
+        }
         return result;
     }
 
+    private static final Pattern UNCONSUMED_DIRECT = Pattern.compile("sends to 
(direct:[^,\\s]+), and no route consumes it");
+
+    /**
+     * The direct: endpoints a YAML route file just written sends to and no 
route consumes yet, as a note: not an error,
+     * the route is often a file or an edit still to come (CAMEL-24955), but 
until it is there the exchanges sent to it
+     * wait for it, and the reload that adds it cuts them off. Said at the 
write, the next change goes in one call with
+     * camel_edit_file edits (CAMEL-25501). Null when there is none.
+     */
+    public static UnconsumedDirect unconsumedDirect(Path dir, String file, 
String content, CamelCatalog catalog) {
+        String name = file != null ? file.toLowerCase(Locale.ROOT) : "";
+        if (!(name.endsWith(".yaml") || name.endsWith(".yml")) || 
name.endsWith(".kamelet.yaml")
+                || name.endsWith(".kamelet.yml")) {
+            return null;
+        }
+        Set<String> endpoints = new LinkedHashSet<>();
+        try {
+            for (String msg : 
EndpointConsumerChecks.validateYamlConsumers(content, dir, file, catalog)) {
+                Matcher m = UNCONSUMED_DIRECT.matcher(msg);
+                if (m.find()) {
+                    endpoints.add(m.group(1));
+                }
+            }
+        } catch (RuntimeException e) {
+            // a note only: a content the check cannot read has no note
+            return null;
+        }
+        if (endpoints.isEmpty()) {
+            return null;
+        }
+        String summary = String.join(", ", endpoints) + (endpoints.size() == 1 
? " has" : " have")
+                         + " no route consuming " + (endpoints.size() == 1 ? 
"it" : "them") + " yet";
+        return new UnconsumedDirect(
+                summary, summary + " (not in this file, nor in the other route 
files):"
+                         + " in dev mode the messages sent there wait for one, 
and are cut off when a later save reloads this"
+                         + " route. If the route goes in this file, add it 
together with the change that sends to it, in one"
+                         + " camel_edit_file call with edits; if it goes in 
another file, write that file next.");
+    }
+
+    /**
+     * The direct: endpoints no route consumes yet: the short summary for the 
message, and the whole note.
+     */
+    public record UnconsumedDirect(String summary, String note) {
+    }
+
     /**
      * The files of a project directory, subdirectories included, as {@code 
camel_get_files} lists them. A human answers
      * "which file has the route" with one {@code ls -R}; this gives a model 
the same in one call: the layout
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolDescriptor.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolDescriptor.java
index 08b7881473ff..8aa4ff60ff48 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolDescriptor.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolDescriptor.java
@@ -42,7 +42,14 @@ public class ToolDescriptor {
     private String[] deterministicUnless = new String[0];
     private String repeatHint;
 
-    public record Param(String name, String type, String description, boolean 
required) {
+    /**
+     * A parameter of the tool; {@code items} is the schema of the elements of 
an array parameter (else null).
+     */
+    public record Param(String name, String type, String description, boolean 
required, JsonObject items) {
+
+        public Param(String name, String type, String description, boolean 
required) {
+            this(name, type, description, required, null);
+        }
     }
 
     @FunctionalInterface
@@ -67,6 +74,28 @@ public class ToolDescriptor {
         return this;
     }
 
+    /**
+     * An array parameter whose elements are objects with the given string 
properties (name and description), all
+     * required. A client sends it as a JSON array; the executor gets it as 
JSON text.
+     */
+    public ToolDescriptor arrayParam(String name, String description, boolean 
required, String... properties) {
+        JsonObject props = new JsonObject();
+        JsonArray names = new JsonArray();
+        for (int i = 0; i + 1 < properties.length; i += 2) {
+            JsonObject prop = new JsonObject();
+            prop.put("type", "string");
+            prop.put("description", properties[i + 1]);
+            props.put(properties[i], prop);
+            names.add(properties[i]);
+        }
+        JsonObject items = new JsonObject();
+        items.put("type", "object");
+        items.put("properties", props);
+        items.put("required", names);
+        params.add(new Param(name, "array", description, required, items));
+        return this;
+    }
+
     public ToolDescriptor readOnly(boolean v) {
         readOnly = v;
         return this;
@@ -193,6 +222,9 @@ public class ToolDescriptor {
             JsonObject prop = new JsonObject();
             prop.put("type", p.type() != null ? p.type() : "string");
             prop.put("description", p.description());
+            if (p.items() != null) {
+                prop.put("items", p.items());
+            }
             properties.put(p.name(), prop);
             if (p.required()) {
                 required.add(p.name());
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringToolsEditTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringToolsEditTest.java
index dd0b4a3209f7..3ba478e272f4 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringToolsEditTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringToolsEditTest.java
@@ -20,7 +20,9 @@ import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
 
+import org.apache.camel.util.json.JsonArray;
 import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.io.TempDir;
 
@@ -229,4 +231,114 @@ class AuthoringToolsEditTest {
                 .isInstanceOf(ToolExecutionException.class)
                 .hasMessageContaining("camel_write_file");
     }
+
+    private static JsonObject edits(Path dir, String edits) {
+        return AuthoringTools.editFile(new ToolContext(), dir, 
"demo.camel.yaml", null, null, edits);
+    }
+
+    /** CAMEL-25501: two places of the file changed in one call, written once, 
so it is never reloaded half done. */
+    @Test
+    void severalEditsAreAppliedInOrderAndWrittenOnce(@TempDir Path dir) throws 
IOException {
+        Files.writeString(dir.resolve("demo.camel.yaml"), ROUTE);
+
+        JsonArray list = new JsonArray();
+        list.add(change("message: \"one\"",
+                "message: \"one\"\n        - to:\n            uri: 
direct:check"));
+        list.add(change("message: \"two\"", "message: \"checked ${body}\""));
+        JsonObject result = edits(dir, list.toJson());
+
+        
assertThat(result.getString("status")).as(result.toJson()).isEqualTo("edited");
+        assertThat(result.getInteger("edits")).isEqualTo(2);
+        assertThat(result.getInteger("editedAtLine")).isEqualTo(11);
+        assertThat(((JsonArray) 
result.get("editedAtLines"))).containsExactly(11, 21);
+        String after = Files.readString(dir.resolve("demo.camel.yaml"));
+        assertThat(after).contains("uri: direct:check\n").contains("message: 
\"checked ${body}\"")
+                .doesNotContain("message: \"two\"");
+    }
+
+    @Test
+    void anEditThatMissesLeavesTheFileUnchangedAndSaysWhichOne(@TempDir Path 
dir) throws IOException {
+        Files.writeString(dir.resolve("demo.camel.yaml"), ROUTE);
+
+        JsonArray list = new JsonArray();
+        list.add(change("message: \"one\"", "message: \"uno\""));
+        list.add(change("message: \"three\"", "message: \"tres\""));
+        JsonObject result = edits(dir, list.toJson());
+
+        assertThat(result.getString("status")).isEqualTo("not-found");
+        assertThat(result.getInteger("edit")).isEqualTo(2);
+        assertThat(result.getString("message")).startsWith("Edit 2 of 2: ")
+                .endsWith("None of the edits was made: the file is 
unchanged.");
+        
assertThat(Files.readString(dir.resolve("demo.camel.yaml"))).isEqualTo(ROUTE);
+    }
+
+    @Test
+    void editsSentAsAStringHoldingJsonAreRead(@TempDir Path dir) throws 
IOException {
+        Files.writeString(dir.resolve("demo.camel.yaml"), ROUTE);
+
+        JsonArray list = new JsonArray();
+        list.add(change("message: \"two\"", "message: \"deux\""));
+        // a model that builds the list as a JSON string sends it encoded once 
more
+        JsonObject result = edits(dir, Jsoner.serialize(list.toJson()));
+
+        assertThat(result.getString("status")).isEqualTo("edited");
+        
assertThat(Files.readString(dir.resolve("demo.camel.yaml"))).contains("message: 
\"deux\"");
+    }
+
+    @Test
+    void editsOfAnotherShapeSayWhatIsExpected(@TempDir Path dir) throws 
IOException {
+        Files.writeString(dir.resolve("demo.camel.yaml"), ROUTE);
+
+        assertThatThrownBy(() -> edits(dir, "[\"message: two\"]"))
+                .isInstanceOf(ToolExecutionException.class)
+                .hasMessageContaining("edits must be a list of {\"find\"");
+        assertThatThrownBy(() -> AuthoringTools.editFile(new ToolContext(), 
dir, "demo.camel.yaml", null, null, null))
+                .isInstanceOf(ToolExecutionException.class)
+                .hasMessageContaining("find is required");
+    }
+
+    @Test
+    void theToolListsEditsAsAnArrayOfFindAndReplace() {
+        JsonObject edits = (JsonObject) ((JsonObject) 
ToolRegistry.findTool("camel_edit_file").inputSchema()
+                .get("properties")).get("edits");
+
+        assertThat(edits.getString("type")).isEqualTo("array");
+        JsonObject items = (JsonObject) edits.get("items");
+        assertThat(((JsonObject) 
items.get("properties")).keySet()).containsExactly("find", "replace");
+        assertThat(ToolRegistry.findTool("camel_edit_file").params())
+                .noneMatch(p -> p.required() && (p.name().equals("find") || 
p.name().equals("replace")));
+    }
+
+    /** CAMEL-25501: the half-done edit is told, so the next change goes in 
one call. */
+    @Test
+    void sendingToADirectEndpointNoRouteConsumesGetsANote(@TempDir Path dir) 
throws IOException {
+        Files.writeString(dir.resolve("demo.camel.yaml"), ROUTE);
+
+        JsonObject half = edit(dir, "message: \"two\"", "message: \"two\"\n    
    - to:\n            uri: direct:ship");
+
+        assertThat(half.getString("status")).isEqualTo("edited");
+        JsonArray notes = (JsonArray) half.get("notes");
+        assertThat(notes).hasSize(1);
+        assertThat(notes.get(0).toString())
+                .startsWith("direct:ship has no route consuming it 
yet").contains("camel_edit_file call with edits");
+        assertThat(half.getString("message")).endsWith("Note: direct:ship has 
no route consuming it yet (see notes).");
+
+        JsonArray list = new JsonArray();
+        list.add(change("uri: direct:ship", "uri: direct:shipment"));
+        list.add(change("message: \"one\"\n",
+                "message: \"one\"\n\n- route:\n    id: shipment\n    from:\n   
   uri: direct:shipment\n"
+                                              + "      steps:\n        - 
log:\n            message: shipped\n"));
+        JsonObject whole = edits(dir, list.toJson());
+
+        
assertThat(whole.getString("status")).as(whole.toJson()).isEqualTo("edited");
+        assertThat(whole.get("notes")).isNull();
+        assertThat(whole.getString("message")).doesNotContain("Note:");
+    }
+
+    private static JsonObject change(String find, String replace) {
+        JsonObject jo = new JsonObject();
+        jo.put("find", find);
+        jo.put("replace", replace);
+        return jo;
+    }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringTools.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringTools.java
index 80327185eb75..e854a23641ca 100644
--- 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringTools.java
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringTools.java
@@ -17,6 +17,7 @@
 package org.apache.camel.dsl.jbang.core.commands.mcp;
 
 import java.util.LinkedHashMap;
+import java.util.List;
 import java.util.Map;
 
 import jakarta.enterprise.context.ApplicationScoped;
@@ -30,6 +31,7 @@ import io.quarkiverse.mcp.server.ToolCallException;
 import org.apache.camel.dsl.jbang.core.commands.ai.ToolContext;
 import org.apache.camel.dsl.jbang.core.commands.ai.ToolExecutionException;
 import org.apache.camel.dsl.jbang.core.commands.ai.ToolRegistry;
+import org.apache.camel.util.json.JsonArray;
 import org.apache.camel.util.json.JsonObject;
 
 /**
@@ -171,19 +173,45 @@ public class AuthoringTools {
     }
 
     @Tool(annotations = @Tool.Annotations(readOnlyHint = false, 
destructiveHint = false, openWorldHint = false),
-          description = "Changes a file by replacing one snippet: the exact 
text to find (it must occur once) and "
+          description = "Changes a file by replacing a snippet: the exact text 
to find (it must occur once) and "
                         + "what to put there. Validated and reloaded as a 
write is. Use it to change an existing file, "
-                        + "camel_write_file for a new one.")
+                        + "camel_write_file for a new one. A change to several 
places of the file is given at once in "
+                        + "edits, so the file is written and reloaded once, 
not half done.")
     public JsonObject camel_edit_file(
             @ToolArg(description = DIRECTORY_DESC, required = false) String 
directory,
             @ToolArg(description = "File path relative to the directory", 
required = true) String file,
             @ToolArg(description = "The lines to replace as they stand in the 
file; other indentation is fine when "
-                                   + "the lines name one place",
-                     required = true) String find,
-            @ToolArg(description = "The text to put there; empty removes it", 
required = true) String replace,
+                                   + "the lines name one place (or use edits)",
+                     required = false) String find,
+            @ToolArg(description = "The text to put there; empty removes it", 
required = false) String replace,
+            @ToolArg(description = 
org.apache.camel.dsl.jbang.core.commands.ai.AuthoringTools.EDITS_DESC,
+                     required = false) List<FileEdit> edits,
             @ToolArg(description = VERSION_DESC, required = false) String 
camelVersion) {
         return call("camel_edit_file", args("directory", directory, "file", 
file, "find", find, "replace", replace,
-                "camelVersion", camelVersion));
+                "edits", editsJson(edits), "camelVersion", camelVersion));
+    }
+
+    /** One change of camel_edit_file's edits: the text to find as it stands 
in the file, and what to put there. */
+    public record FileEdit(String find, String replace) {
+    }
+
+    /** The edits as the JSON list the shared tool reads, or null when there 
are none. */
+    static String editsJson(List<FileEdit> edits) {
+        if (edits == null || edits.isEmpty()) {
+            return null;
+        }
+        JsonArray list = new JsonArray();
+        for (FileEdit edit : edits) {
+            // a null element stays in the list as {}, on purpose: the shared 
tool then answers with the shape edits
+            // must have, so a malformed call is told rather than half applied
+            JsonObject jo = new JsonObject();
+            if (edit != null) {
+                jo.put("find", edit.find());
+                jo.put("replace", edit.replace());
+            }
+            list.add(jo);
+        }
+        return list.toJson();
     }
 
     @Tool(annotations = @Tool.Annotations(readOnlyHint = false, 
destructiveHint = false, openWorldHint = true),
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringToolsTest.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringToolsTest.java
index a552525e3b22..3c4250eb3845 100644
--- 
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringToolsTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringToolsTest.java
@@ -109,6 +109,24 @@ class AuthoringToolsTest {
                 
.isInstanceOf(ToolCallException.class).hasMessageContaining("directory is 
required");
     }
 
+    /** CAMEL-25501: several places of one file in one call, written once. */
+    @Test
+    void severalEditsOfOneFileAreWrittenOnce(@TempDir Path dir) throws 
Exception {
+        String route = "- route:\n    from:\n      uri: timer:tick\n      
steps:\n        - log:\n            message: hi\n"
+                       + "        - log:\n            message: bye\n";
+        Files.writeString(dir.resolve("demo.camel.yaml"), route, 
StandardCharsets.UTF_8);
+
+        JsonObject edited = tools.camel_edit_file(dir.toString(), 
"demo.camel.yaml", null, null,
+                List.of(new AuthoringTools.FileEdit("message: hi", "message: 
hello"),
+                        new AuthoringTools.FileEdit("message: bye", "message: 
goodbye")),
+                null);
+
+        assertThat(edited.getString("status")).isEqualTo("edited");
+        assertThat(edited.getInteger("edits")).isEqualTo(2);
+        assertThat(Files.readString(dir.resolve("demo.camel.yaml"), 
StandardCharsets.UTF_8))
+                .contains("message: hello").contains("message: goodbye");
+    }
+
     @Test
     void anUnknownIntegrationNameIsAnError() {
         assertThatThrownBy(() -> tools.camel_eval_expression("${body}", null, 
"camel", "no-such-app-xyz-1"))
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java
index b2e967781909..123292168428 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java
@@ -3297,7 +3297,7 @@ class AiPanel {
     private String acpPreamble() {
         String prompt = buildSystemPrompt();
         return (prompt.endsWith("\n") ? prompt : prompt + "\n")
-               + "- You run inside the Camel TUI: edit the integration's 
source files only with camel_write_file, "
+               + "- You run inside the Camel TUI: edit the integration's 
source files only with camel_edit_file or camel_write_file, "
                + "never with your own file tools, so the user sees the diff 
and confirms it, or watches the edit "
                + "being typed in the Source editor (/write live)\n";
     }
@@ -3332,11 +3332,12 @@ class AiPanel {
         sb.append("say what failed and what to try\n");
         sb.append("- To feed a route that consumes from a broker (MQTT, Kafka, 
JMS), tui_send_message can publish ");
         sb.append("to the broker with the route's own component and 
options\n");
-        sb.append("- To edit: camel_get_files, then camel_write_file with the 
complete file; the user confirms, never ");
+        sb.append("- To edit: camel_get_files, then camel_edit_file (several 
places at once with edits), camel_write_file ");
+        sb.append("for a new file; the user confirms, never ");
         sb.append(
                 "retry a rejected write. Invalid YAML/properties is refused 
with errors: fix them (camel_catalog_doc has the ");
         sb.append("option names)\n");
-        sb.append("- Write files only with camel_write_file; never paste file 
contents in the answer\n");
+        sb.append("- Write files only with these tools; never paste file 
contents in the answer\n");
         sb.append("- YAML DSL shape: a step is `- log: {message: ...}`, `- to: 
{uri: ...}`, an expression goes under ");
         sb.append("expression: (`- setBody: {expression: {simple: ...}}`); the 
shorthand forms are deprecated\n");
         sb.append("- 'log at WARN' in a route is the log step's loggingLevel 
in the source");
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
index c2859f6e976c..6b7bbd6a734a 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
@@ -1388,6 +1388,11 @@ class McpFacade {
         describeSourceDirectory(target, dir, result);
         result.put("lines", lines);
         result.put("bytes", content.getBytes(StandardCharsets.UTF_8).length);
+        // a route sending to a direct: endpoint no route consumes yet: its 
route goes in the same call (CAMEL-25501)
+        AuthoringTools.UnconsumedDirect waiting = 
AuthoringTools.unconsumedDirect(dir, file, content, null);
+        if (waiting != null) {
+            result.put("notes", new JsonArray(List.of(waiting.note())));
+        }
         return result;
     }
 
@@ -1397,6 +1402,11 @@ class McpFacade {
      * write (CAMEL-24909). The reading and the matching are the shared 
tool's, only the writing is the TUI's.
      */
     JsonObject editFile(String name, String file, String find, String replace, 
boolean confirm) {
+        return editFile(name, file, find, replace, null, confirm);
+    }
+
+    /** As {@link #editFile(String, String, String, String, boolean)} with 
several edits, written once (CAMEL-25501). */
+    JsonObject editFile(String name, String file, String find, String replace, 
String edits, boolean confirm) {
         IntegrationInfo target = findIntegration(name);
         if (target == null) {
             return writeError(name != null && !name.isEmpty()
@@ -1406,7 +1416,7 @@ class McpFacade {
         if (dir == null || !Files.isDirectory(dir)) {
             return writeError("No source directory found for the integration");
         }
-        JsonObject edit = AuthoringTools.editedContent(dir, file, find, 
replace);
+        JsonObject edit = AuthoringTools.editedContent(dir, file, find, 
replace, edits);
         String content = edit.getString("content");
         if (content == null) {
             return edit; // not-found, ambiguous or an error: the shared 
answer says what to do
@@ -1416,6 +1426,10 @@ class McpFacade {
             result.put("status", "edited");
             result.put("editedAtLine", edit.getInteger("editedAtLine"));
             result.put("replacedLines", edit.getInteger("replacedLines"));
+            if (edit.get("edits") != null) {
+                result.put("edits", edit.get("edits"));
+                result.put("editedAtLines", edit.get("editedAtLines"));
+            }
         }
         return result;
     }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java
index 4061f84e11e1..4fe764360ee7 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java
@@ -1490,9 +1490,12 @@ class TuiToolRegistry {
         String file = args.get("file") instanceof String s ? s : null;
         String find = args.get("find") instanceof String s ? s : null;
         String replace = args.get("replace") instanceof String s ? s : "";
+        Object list = args.get("edits");
+        String edits = list instanceof Map<?, ?> || list instanceof List<?> ? 
Jsoner.serialize(list)
+                : list instanceof String text ? text : null;
         boolean confirm = !Boolean.FALSE.equals(args.get("confirm"));
-        // an edit is confirmed and replayed in the editor like a write 
(CAMEL-24909)
-        return Jsoner.serialize(facade.editFile(name, file, find, replace, 
confirm));
+        // an edit is confirmed and replayed in the editor like a write 
(CAMEL-24909), several edits as one (CAMEL-25501)
+        return Jsoner.serialize(facade.editFile(name, file, find, replace, 
edits, confirm));
     }
 
     private String callValidateSource(Map<String, Object> args) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelAcpTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelAcpTest.java
index 4902bf9cd52c..6681b0ee5793 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelAcpTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelAcpTest.java
@@ -603,7 +603,7 @@ class AiPanelAcpTest {
         ask(panel, "hi");
         awaitIdle(panel);
         String prompt = promptText(agent.received("session/prompt").get(0));
-        assertTrue(prompt.contains("only with camel_write_file"), prompt);
+        assertTrue(prompt.contains("only with camel_edit_file or 
camel_write_file"), prompt);
         assertTrue(prompt.contains("/write live"), prompt);
     }
 
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelPromptBudgetTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelPromptBudgetTest.java
index 1237a0b52474..7bd7969a8983 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelPromptBudgetTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelPromptBudgetTest.java
@@ -60,7 +60,9 @@ class AiPanelPromptBudgetTest {
     // full mode only (hosted models), and the panel's own /overview sends no 
tools at all
     // raised from 9850 for tui_http_endpoints and tui_http_request 
(CAMEL-25307), measured ~10070: a model that can
     // see what the integration serves and call it is worth the ~250 tokens
-    static final int FULL_BUDGET_TOKENS = 10_250;
+    // raised from 10250 for the edits argument of camel_edit_file 
(CAMEL-25501), measured ~10400: several places of a
+    // file changed in one call, so dev mode never reloads a file half done
+    static final int FULL_BUDGET_TOKENS = 10_500;
     /** Measured ~5.5k tokens for 28 tools: the core set (~4.7k) plus every 
tool group (CAMEL-24834). */
     // the SQL group adds tui_execute_sql and tui_update_row (~385 tokens), 
each group one guidance line in the prompt
     // (~130 for all three); an integration rarely has all of them, and the 
groups only load for the integration that
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacadeWriteFileTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacadeWriteFileTest.java
index 5422c4dddf7c..cffa670ff52d 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacadeWriteFileTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacadeWriteFileTest.java
@@ -30,6 +30,7 @@ import org.junit.jupiter.api.io.TempDir;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 class McpFacadeWriteFileTest {
@@ -203,6 +204,31 @@ class McpFacadeWriteFileTest {
         assertEquals(1, bridge.asked, "nothing to confirm when the snippet is 
not there");
     }
 
+    /** CAMEL-25501: several edits are one change to confirm; a send to a 
direct: no route consumes yet is noted. */
+    @Test
+    void 
severalEditsAreOneConfirmedChangeAndAnUnconsumedDirectIsNoted(@TempDir Path 
dir) throws IOException {
+        Files.writeString(dir.resolve("demo.camel.yaml"), "- route:\n    id: 
x\n    from:\n      uri: direct:x\n"
+                                                          + "      steps:\n    
    - log:\n            message: hi\n");
+        ConfirmingBridge bridge = new ConfirmingBridge(true);
+        McpFacade facade = facade(dir, true, bridge);
+
+        JsonObject half = facade.editFile("demo", "demo.camel.yaml", "message: 
hi",
+                "message: hi\n        - to:\n            uri: direct:ship", 
true);
+        assertEquals("edited", half.getString("status"));
+        assertTrue(half.get("notes").toString().contains("direct:ship has no 
route consuming it yet"), half.toJson());
+
+        String edits = "[{\"find\": \"id: x\", \"replace\": \"id: picked\"},"
+                       + " {\"find\": \"uri: direct:ship\\n\", \"replace\": 
\"uri: direct:ship\\n\\n- route:\\n    id: ship\\n"
+                       + "    from:\\n      uri: direct:ship\\n      steps:\\n 
       - log:\\n            message: shipped\\n\"}]";
+        JsonObject whole = facade.editFile("demo", "demo.camel.yaml", null, 
null, edits, true);
+        assertEquals("edited", whole.getString("status"), whole.toJson());
+        assertEquals(2, whole.getInteger("edits"));
+        assertEquals(2, bridge.asked, "the two edits are confirmed as one 
change");
+        assertNull(whole.get("notes"), whole.toJson());
+        String after = Files.readString(dir.resolve("demo.camel.yaml"), 
StandardCharsets.UTF_8);
+        assertTrue(after.contains("id: picked") && after.contains("id: ship"), 
after);
+    }
+
     @Test
     void invalidContentIsNeverWritten(@TempDir Path dir) throws IOException {
         Files.writeString(dir.resolve("demo.camel.yaml"), "- route: {}\n");

Reply via email to