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 858c9db34fad chore: camel-jbang - source editor fixes: go to node in 
Java and XML, reload after an AI write, edit title with errors, Simple function 
quick fix (#27197)
858c9db34fad is described below

commit 858c9db34fada4222860eee455d7cf36d5e41934
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu Oct 1 13:50:34 2026 +0200

    chore: camel-jbang - source editor fixes: go to node in Java and XML, 
reload after an AI write, edit title with errors, Simple function quick fix 
(#27197)
    
    Source editor fixes: Ctrl+G lists the nodes of Java and XML routes and the 
when/otherwise of a YAML choice, the view shows a file an AI tool wrote, the 
edit title keeps the file name when there are errors, and a Simple function the 
error names has a quick fix.
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
---
 .../modules/ROOT/pages/camel-jbang-tui.adoc        |   2 +
 .../dsl/jbang/core/commands/ai/QuickFixes.java     |  14 +-
 .../dsl/jbang/core/commands/ai/QuickFixesTest.java |  17 +++
 .../dsl/jbang/core/commands/tui/CamelMonitor.java  |  10 ++
 .../dsl/jbang/core/commands/tui/McpFacade.java     |  10 ++
 .../core/commands/tui/ModelRouteNodeScanner.java   | 153 +++++++++++++++++++++
 .../dsl/jbang/core/commands/tui/SourceTab.java     |  33 ++++-
 .../dsl/jbang/core/commands/tui/SourceViewer.java  |  24 +++-
 .../core/commands/tui/YamlRouteNodeScanner.java    |  78 ++++++++++-
 .../src/main/resources/tui/help/source.md          |   3 +-
 .../commands/tui/ModelRouteNodeScannerTest.java    | 105 ++++++++++++++
 .../core/commands/tui/SourceViewerReloadTest.java  |  82 +++++++++++
 .../commands/tui/YamlRouteNodeScannerTest.java     |  85 ++++++++++++
 13 files changed, 604 insertions(+), 12 deletions(-)

diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
index cee7fa671c4e..8dca367565c1 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
@@ -272,6 +272,8 @@ line instead: the file is saved as it is, and the AI panel 
opens with the questi
 the problem and its text), for you to send with *Enter* or change first. 
`camel_validate_source` gives the same fixes to
 an agent, as `fixes` with the text to find and its replacement.
 
+A Simple function the error names the right one of, such as `+${bdy}+` for 
`+${body}+`, has a quick fix too.
+
 Validation can be toggled off in the Settings (F2 > Settings).
 
 == Activity
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/QuickFixes.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/QuickFixes.java
index b8065931d337..2c8d330cde10 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/QuickFixes.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/QuickFixes.java
@@ -25,8 +25,9 @@ import java.util.regex.Pattern;
 /**
  * The fix of a problem the validation reports, when the problem says it: an 
option that is a typo of another (Did you
  * mean: [period]), an enum value of the wrong case, a to that needs toD, a 
Simple expression where a property
- * placeholder is meant. The fix is a replacement of text on the line of the 
problem, which an editor applies with one
- * key and an agent applies as an edit, whatever the DSL (YAML, Java, XML) the 
line is written in.
+ * placeholder is meant, a Simple function the error names the right one of 
(did you mean ${body}?). The fix is a
+ * replacement of text on the line of the problem, which an editor applies 
with one key and an agent applies as an edit,
+ * whatever the DSL (YAML, Java, XML) the line is written in.
  * <p/>
  * A fix is only given when it is certain: one suggestion, a value that 
matches but for its case or a letter or two.
  */
@@ -58,6 +59,9 @@ public final class QuickFixes {
     private static final Pattern SIMPLE_AS_PLACEHOLDER = Pattern.compile(
             "([\\w.-]+)=(\\$\\{[^}]*\\}) is a Simple expression, which an 
endpoint option is not evaluated as.*\\{\\{([^}]+)\\}\\}");
     private static final Pattern DYNAMIC_TO = Pattern.compile("holds an 
expression \\(\\$\\{");
+    private static final Pattern UNKNOWN_FUNCTION = Pattern.compile("Unknown 
function: (.+?) \\((?:the argument goes in"
+                                                                    + " 
parentheses: )?(?:did you mean |function names are"
+                                                                    + " case 
sensitive: )(\\$\\{.+\\})\\??\\)");
 
     private QuickFixes() {
     }
@@ -105,6 +109,12 @@ public final class QuickFixes {
             String placeholder = "{{" + m.group(3) + "}}";
             return new Fix(m.group(2) + " → " + placeholder, m.group(2), 
placeholder);
         }
+        m = UNKNOWN_FUNCTION.matcher(message);
+        if (m.find()) {
+            // ${bdy} (did you mean ${body}?): the function the hint names, in 
place of the one written
+            String written = "${" + m.group(1) + "}";
+            return new Fix(written + " → " + m.group(2), written, m.group(2));
+        }
         if (DYNAMIC_TO.matcher(message).find() && message.contains("toD")) {
             // the step itself: .to( in Java, <to in XML, to: in YAML
             for (String[] form : new String[][] { { ".to(", ".toD(" }, { "<to 
", "<toD " }, { "to:", "toD:" } }) {
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/QuickFixesTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/QuickFixesTest.java
index 7c73c78e2213..a037a4458122 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/QuickFixesTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/QuickFixesTest.java
@@ -81,6 +81,23 @@ class QuickFixesTest {
                 """)).isEqualTo("<from uri=\"timer:tick?period=1000\"/>");
     }
 
+    @Test
+    void aSimpleFunctionTheErrorNamesTheRightOneOf() {
+        assertThat(fixed("MyRoute.java", java(
+                "        from(\"timer:tick\").filter(simple(\"${bdy} > 
3\")).to(\"seda:out\");")))
+                .isEqualTo("from(\"timer:tick\").filter(simple(\"${body} > 
3\")).to(\"seda:out\");");
+        assertThat(fixed("MyRoute.java", java(
+                "        
from(\"timer:tick\").setBody(simple(\"${Body}\")).to(\"seda:out\");")))
+                
.isEqualTo("from(\"timer:tick\").setBody(simple(\"${body}\")).to(\"seda:out\");");
+    }
+
+    @Test
+    void aSimpleFunctionWithoutASuggestionHasNoFix() {
+        String error = "Line 6: Simple syntax error: Unknown function: zzzzqqq 
(the functions are documented on the"
+                       + " simple language page (functions))";
+        assertThat(QuickFixes.fixFor(error, "    
.setBody(simple(\"${zzzzqqq}\"))")).isNull();
+    }
+
     @Test
     void aTypoOfAYamlOption() {
         assertThat(fixed("route.camel.yaml", """
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
index 8e2e95da2f10..61bd5a2aac2d 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
@@ -193,6 +193,8 @@ public class CamelMonitor extends CamelCommand {
     private final EditReplay editReplay = new EditReplay();
     // a live write waiting to start on the UI thread, and the promise its 
tool thread waits on
     private volatile McpFacade.FileWrite pendingReplay;
+    /** The files AI tools wrote, for the source viewer to show again on the 
UI thread. */
+    private final Queue<Path> writtenFiles = new ConcurrentLinkedQueue<>();
     private volatile CompletableFuture<McpFacade.ReplayOutcome> 
pendingReplayOutcome;
     // both are read by the tool thread to decide whether a write can be 
replayed
     private volatile McpFacade.FileWrite activeReplay;
@@ -820,6 +822,11 @@ public class CamelMonitor extends CamelCommand {
                         return parked != null && activeReplayOutcome == null ? 
parked.file() : null;
                     }
 
+                    @Override
+                    public void fileWritten(Path file) {
+                        writtenFiles.add(file);
+                    }
+
                     @Override
                     public boolean confirmFileWrite(McpFacade.FileWrite 
request) {
                         CompletableFuture<Boolean> answer = new 
CompletableFuture<>();
@@ -1875,6 +1882,9 @@ public class CamelMonitor extends CamelCommand {
         drawOverlay.tick(now);
         captionOverlay.tick(now);
         tickEditReplay(now);
+        for (Path written = writtenFiles.poll(); written != null; written = 
writtenFiles.poll()) {
+            tabRegistry.sourceTab().reloadIfShowing(written);
+        }
         recordingManager.tickRecentKeys(now);
         boolean anyDiagramShowing = tabRegistry.routesTab().isShowDiagram()
                 || tabRegistry.diagramTab().isShowDiagram();
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 abb818bc1628..071a4624249b 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
@@ -116,6 +116,13 @@ class McpFacade {
         default String parkedReplayFile() {
             return null;
         }
+
+        /**
+         * Tells that a tool wrote a file, so a view of the file shows what is 
in it now. Called on the calling (tool)
+         * thread.
+         */
+        default void fileWritten(Path file) {
+        }
     }
 
     /** How file writes requested by tools are handled; set by the user with 
/write in the AI panel. */
@@ -1219,6 +1226,9 @@ class McpFacade {
         } catch (IOException e) {
             return writeError("Failed to write " + filePath + ": " + 
e.getMessage());
         }
+        if (bridge != null) {
+            bridge.fileWritten(filePath);
+        }
         JsonObject result = new JsonObject();
         result.put("status", exists ? "overwritten" : "created");
         result.put("file", file);
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ModelRouteNodeScanner.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ModelRouteNodeScanner.java
new file mode 100644
index 000000000000..9088cee8e209
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ModelRouteNodeScanner.java
@@ -0,0 +1,153 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Supplier;
+
+import org.apache.camel.LineNumberAware;
+import org.apache.camel.NamedNode;
+import org.apache.camel.catalog.CamelCatalog;
+import org.apache.camel.dsl.jbang.core.commands.ai.RouteModel;
+import 
org.apache.camel.dsl.jbang.core.commands.tui.YamlRouteNodeScanner.EntryKind;
+import 
org.apache.camel.dsl.jbang.core.commands.tui.YamlRouteNodeScanner.NodeEntry;
+import org.apache.camel.model.ChoiceDefinition;
+import org.apache.camel.model.OtherwiseDefinition;
+import org.apache.camel.model.ProcessorDefinition;
+import org.apache.camel.model.RouteDefinition;
+import org.apache.camel.model.TryDefinition;
+import org.apache.camel.model.WhenDefinition;
+
+/**
+ * The nodes of the Java and XML DSL routes of a file for Go to Node (Ctrl+G) 
of the Source tab, as the YAML scanner
+ * gives them for a YAML file. The file is read into the Camel model as the 
route checks read it: a Java source by the
+ * Java DSL parser, without compiling or running it.
+ */
+final class ModelRouteNodeScanner {
+
+    private static final int MAX_DEPTH = 50;
+
+    private ModelRouteNodeScanner() {
+    }
+
+    /**
+     * The nodes of the routes of a Java or XML DSL file; empty when the file 
is neither or cannot be read.
+     *
+     * @param javaSources the Java sources of the project by path, for the 
constants of other classes
+     */
+    static List<NodeEntry> scan(
+            String filePath, String fileName, String content, Map<String, 
Supplier<String>> javaSources,
+            CamelCatalog catalog) {
+        RouteModel model;
+        try {
+            model = RouteModel.read(fileName, content, catalog, javaSources);
+        } catch (RuntimeException e) {
+            return List.of();
+        }
+        if (model == null) {
+            return List.of();
+        }
+        List<NodeEntry> answer = new ArrayList<>();
+        for (RouteDefinition r : model.routes()) {
+            if (r.getInput() == null || r.getInput().getUri() == null) {
+                continue;
+            }
+            String fromUri = 
DiagramSupport.stripQueryParams(JavaRouteScanner.uri(r.getInput().getUri()));
+            int fromLine = line(r.getInput(), line(r, 0));
+            String routeId = 
YamlRouteNodeScanner.resolveRouteId(r.getRouteId(), fromUri);
+            answer.add(new NodeEntry(EntryKind.ROUTE, routeId, fromUri, 
"route", fromUri, filePath, fromLine, 0, fromLine));
+            Set<Object> seen = Collections.newSetFromMap(new 
IdentityHashMap<>());
+            for (ProcessorDefinition<?> p : r.getOutputs()) {
+                walk(p, routeId, filePath, fromLine, 1, answer, seen);
+            }
+        }
+        return answer;
+    }
+
+    private static void walk(
+            NamedNode p, String routeId, String filePath, int fromLine, int 
depth, List<NodeEntry> answer,
+            Set<Object> seen) {
+        if (p == null || depth > MAX_DEPTH || !seen.add(p)) {
+            return;
+        }
+        answer.add(new NodeEntry(
+                EntryKind.PROCESSOR, routeId, null, p.getShortName(), 
label(p), filePath, line(p, fromLine), depth,
+                fromLine));
+        for (NamedNode child : children(p)) {
+            walk(child, routeId, filePath, fromLine, depth + 1, answer, seen);
+        }
+    }
+
+    /** The nodes under a node: the when and otherwise of a choice are nodes 
of their own, as the diagram shows them. */
+    private static List<NamedNode> children(NamedNode p) {
+        List<NamedNode> answer = new ArrayList<>();
+        if (p instanceof ChoiceDefinition choice) {
+            answer.addAll(choice.getWhenClauses());
+            if (choice.getOtherwise() != null) {
+                answer.add(choice.getOtherwise());
+            }
+        } else if (p instanceof WhenDefinition when) {
+            answer.addAll(when.getOutputs());
+        } else if (p instanceof OtherwiseDefinition otherwise) {
+            answer.addAll(otherwise.getOutputs());
+        } else if (p instanceof ProcessorDefinition<?> pd) {
+            answer.addAll(pd.getOutputs());
+            if (pd instanceof TryDefinition t) {
+                answer.addAll(t.getCatchClauses());
+                if (t.getFinallyClause() != null) {
+                    answer.add(t.getFinallyClause());
+                }
+            }
+        }
+        return answer;
+    }
+
+    /** The label of a node without its EIP name: filter[simple{...}] gives 
simple{...}. */
+    static String label(NamedNode p) {
+        if (p instanceof OtherwiseDefinition || p instanceof ChoiceDefinition) 
{
+            // the when and otherwise of a choice are listed under it
+            return "";
+        }
+        String label;
+        try {
+            label = p.getLabel();
+        } catch (RuntimeException e) {
+            return "";
+        }
+        if (label == null) {
+            return "";
+        }
+        String name = p.getShortName();
+        if (label.startsWith(name + "[") && label.endsWith("]")) {
+            label = label.substring(name.length() + 1, label.length() - 1);
+        }
+        return label.replace('\n', ' ');
+    }
+
+    /** The line of a node from 0, or the fallback when the model does not 
know it. */
+    private static int line(Object node, int fallback) {
+        if (node instanceof LineNumberAware n && n.getLineNumber() > 0) {
+            return n.getLineNumber() - 1;
+        }
+        return fallback;
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
index 5bb40ab14646..244bf7f2e51a 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
@@ -1181,6 +1181,17 @@ class SourceTab extends AbstractTab {
         return answer;
     }
 
+    /** The Java sources among the files by path, for the constants a Java 
route takes from another class. */
+    private static Map<String, Supplier<String>> javaSources(List<Path> files) 
{
+        Map<String, Supplier<String>> answer = new LinkedHashMap<>();
+        for (Path f : files) {
+            if (f.getFileName().toString().endsWith(".java")) {
+                answer.put(f.toString(), () -> readQuietly(f));
+            }
+        }
+        return answer;
+    }
+
     private static boolean isRouteSourceName(String name) {
         String lower = name.toLowerCase();
         return lower.endsWith(".java") || lower.endsWith(".xml") || 
lower.endsWith(".yaml") || lower.endsWith(".yml");
@@ -1227,17 +1238,33 @@ class SourceTab extends AbstractTab {
         }
     }
 
+    /** Shows a file again when the viewer shows it and it is not being 
edited: a file an AI tool wrote. */
+    void reloadIfShowing(Path file) {
+        sourceViewer.reloadIfShowing(file);
+    }
+
     private List<YamlRouteNodeScanner.NodeEntry> buildSourceNodeIndex() {
         List<YamlRouteNodeScanner.NodeEntry> nodes = new ArrayList<>();
+        Map<String, Supplier<String>> javaSources = null;
         for (FilesBrowser.FileEntry entry : entries) {
             if (entry.directory()) {
                 continue;
             }
             Path path = Path.of(entry.path());
-            if (!isCamelSourceFile(path) || 
!SourceEditAssist.isYamlFile(path)) {
-                continue;
+            if (isJavaRouteFile(path)
+                    || isCamelSourceFile(path) && 
path.getFileName().toString().toLowerCase().endsWith(".xml")) {
+                String content = readQuietly(path);
+                if (content != null) {
+                    if (javaSources == null) {
+                        javaSources = javaSources(routeSources());
+                    }
+                    nodes.addAll(ModelRouteNodeScanner.scan(
+                            path.toString(), path.getFileName().toString(), 
content, javaSources,
+                            ArchitectureView.catalog()));
+                }
+            } else if (isCamelSourceFile(path) && 
SourceEditAssist.isYamlFile(path)) {
+                nodes.addAll(YamlRouteNodeScanner.scanFile(path));
             }
-            nodes.addAll(YamlRouteNodeScanner.scanFile(path));
         }
         return nodes;
     }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewer.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewer.java
index a9b82841230d..2ebc6e3d5ae1 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewer.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewer.java
@@ -562,6 +562,20 @@ class SourceViewer {
         return selectedLine;
     }
 
+    /**
+     * Shows the file again, at the same line, when it is the file shown and 
it is not being edited: a file an AI tool
+     * wrote while the viewer showed the old content. An edit in progress is 
left alone, as it holds the user's work.
+     */
+    void reloadIfShowing(Path file) {
+        if (editMode || editableFile == null || file == null
+                || 
!editableFile.toAbsolutePath().normalize().equals(file.toAbsolutePath().normalize()))
 {
+            return;
+        }
+        int line = selectedLine;
+        loadFile(editableFile);
+        goToLine(Math.min(line, Math.max(0, getLineCount() - 1)));
+    }
+
     int getLineCount() {
         if (editMode) {
             return editState.lineCount();
@@ -2179,6 +2193,11 @@ class SourceViewer {
                 }
             }
         }
+        if (!diffOverlay && !visibleErrors.isEmpty()) {
+            // a block has one title at the top: the error count goes on the 
line of the file name, not instead of it
+            Style errorStyle = 
Style.EMPTY.fg(dev.tamboui.style.Color.rgb(0xFF, 0x66, 0x66));
+            titleSpans.add(Span.styled(" errors: " + visibleErrors.size() + " 
", errorStyle));
+        }
         Title posTitle;
         if (diffOverlay) {
             posTitle = Title.from(
@@ -2198,11 +2217,6 @@ class SourceViewer {
             blockBuilder.borders(Borders.ALL)
                     .title(Title.from(Line.from(titleSpans)))
                     .titleBottom(posTitle);
-            if (!visibleErrors.isEmpty()) {
-                Style errorStyle = 
Style.EMPTY.fg(dev.tamboui.style.Color.rgb(0xFF, 0x66, 0x66));
-                blockBuilder.title(Title.from(Line.from(
-                        Span.styled(" errors: " + visibleErrors.size() + " ", 
errorStyle))).right());
-            }
         }
         if (borderStyle != null) {
             blockBuilder.borderStyle(borderStyle);
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/YamlRouteNodeScanner.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/YamlRouteNodeScanner.java
index 01ec6ec7abd5..97acfac224fe 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/YamlRouteNodeScanner.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/YamlRouteNodeScanner.java
@@ -20,7 +20,9 @@ import java.io.IOException;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.util.ArrayDeque;
 import java.util.ArrayList;
+import java.util.Deque;
 import java.util.List;
 import java.util.Set;
 
@@ -78,6 +80,8 @@ class YamlRouteNodeScanner {
         int activeRouteIndent = -1;
         String pendingEndpointEip = null;
         int pendingEndpointIndent = -1;
+        // the when: lists being read (a choice can be nested in a when), each 
as its indent and that of its items
+        Deque<int[]> whens = new ArrayDeque<>();
 
         for (int i = 0; i < lines.size(); i++) {
             String line = lines.get(i);
@@ -165,6 +169,33 @@ class YamlRouteNodeScanner {
                 continue;
             }
 
+            // the when and otherwise of a choice are nodes of their own, as 
the diagram (and Java and XML) show them
+            while (!whens.isEmpty() && indent <= whens.peek()[0]) {
+                whens.pop();
+            }
+            if ("when:".equals(trimmed) || "otherwise:".equals(trimmed)) {
+                if (trimmed.startsWith("when")) {
+                    whens.push(new int[] { indent, -1 });
+                } else {
+                    String routeId = resolveRouteId(currentRouteId, 
currentFromUri);
+                    result.add(new NodeEntry(
+                            EntryKind.PROCESSOR, routeId, null, "otherwise", 
"", filePath, i,
+                            Math.max(1, (indent - activeRouteIndent) / 2), 
activeRouteFromLine));
+                }
+                continue;
+            }
+            int[] when = whens.peek();
+            if (when != null && trimmed.startsWith("- ") && (when[1] < 0 || 
indent == when[1])) {
+                // an item of the when list (- expression: or - simple: ...): 
a when, at the indent of when: so it is a
+                // sibling of the otherwise
+                when[1] = indent;
+                String routeId = resolveRouteId(currentRouteId, 
currentFromUri);
+                result.add(new NodeEntry(
+                        EntryKind.PROCESSOR, routeId, null, "when", 
expressionLabel(lines, i), filePath, i,
+                        Math.max(1, (when[0] - activeRouteIndent) / 2), 
activeRouteFromLine));
+                continue;
+            }
+
             if (isBlockEndpointEipLine(trimmed)) {
                 String eip = extractNodeType(line);
                 if (eip != null && ENDPOINT_EIPS.contains(eip)) {
@@ -208,7 +239,7 @@ class YamlRouteNodeScanner {
                 EntryKind.ROUTE, resolvedId, fromUri, "route", fromUri, 
filePath, fromLine, 0, fromLine));
     }
 
-    private static String resolveRouteId(String routeId, String fromUri) {
+    static String resolveRouteId(String routeId, String fromUri) {
         if (routeId != null && !routeId.isEmpty()) {
             return routeId;
         }
@@ -332,6 +363,51 @@ class YamlRouteNodeScanner {
         return "";
     }
 
+    /**
+     * The expression of a when item as language{text}, as the model labels 
it: from {@code - simple: "${body} > 6"}, or
+     * {@code - expression:} with the language and its expression on the lines 
under it. Empty when not found.
+     */
+    static String expressionLabel(List<String> lines, int itemLine) {
+        int base = lineIndent(lines.get(itemLine));
+        String language = null;
+        for (int j = itemLine; j < lines.size(); j++) {
+            String next = lines.get(j);
+            if (next.isBlank() || next.trim().startsWith("#")) {
+                continue;
+            }
+            if (j > itemLine && lineIndent(next) <= base) {
+                break;
+            }
+            String content = next.trim();
+            if (content.startsWith("- ")) {
+                content = content.substring(2).trim();
+            }
+            int colon = content.indexOf(':');
+            if (colon <= 0) {
+                continue;
+            }
+            String key = content.substring(0, colon).trim();
+            String value = unquote(content.substring(colon + 1).trim());
+            if ("steps".equals(key)) {
+                break;
+            }
+            if (BOILERPLATE_KEYS.contains(key)) {
+                continue;
+            }
+            if ("expression".equals(key)) {
+                if (!value.isEmpty()) {
+                    return language != null ? language + "{" + value + "}" : 
value;
+                }
+                continue;
+            }
+            language = key;
+            if (!value.isEmpty()) {
+                return language + "{" + value + "}";
+            }
+        }
+        return "";
+    }
+
     private static String extractYamlValue(String trimmed, String key) {
         String prefix = key + ":";
         if (!trimmed.startsWith(prefix)) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/source.md 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/source.md
index 18a054497b65..c563f8f24b66 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/source.md
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/source.md
@@ -59,7 +59,8 @@ a heat map of the route: where messages go, where they fail.
   line, problem); press Enter to send it, or change it first
 - **Shift+F9** — apply the fix of the problem on the cursor line, when the 
problem
   says it (an option typo, an enum value a letter off, `to` that should be
-  `toD`, `${key}` where `{{key}}` is meant); the Error panel shows the fix
+  `toD`, `${key}` where `{{key}}` is meant, a Simple function the error names
+  the right one of, such as `${bdy}` → `${body}`); the Error panel shows the 
fix
 - Java and XML DSL routes are checked as you type, like YAML routes: endpoint
   options, simple expressions, and a `to` with `${...}` that should be a `toD`.
   The problems are marked on their lines; an XML file with problems is not 
saved
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ModelRouteNodeScannerTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ModelRouteNodeScannerTest.java
new file mode 100644
index 000000000000..9b16528aabdf
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ModelRouteNodeScannerTest.java
@@ -0,0 +1,105 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.util.List;
+import java.util.Map;
+
+import org.apache.camel.catalog.DefaultCamelCatalog;
+import 
org.apache.camel.dsl.jbang.core.commands.tui.YamlRouteNodeScanner.EntryKind;
+import 
org.apache.camel.dsl.jbang.core.commands.tui.YamlRouteNodeScanner.NodeEntry;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Go to Node (Ctrl+G) lists the routes of Java and XML files too, read into 
the Camel model.
+ */
+class ModelRouteNodeScannerTest {
+
+    /** Each node as depth, type[label] @ line (from 1). */
+    private static List<String> describe(List<NodeEntry> nodes) {
+        return nodes.stream()
+                .map(n -> n.indent() + " " + n.type() + "[" + n.label() + "] 
@" + (n.lineIndex() + 1))
+                .toList();
+    }
+
+    @Test
+    void theNodesOfAJavaRoute() {
+        String java = """
+                import org.apache.camel.builder.RouteBuilder;
+
+                public class OrderRoute extends RouteBuilder {
+                    @Override
+                    public void configure() throws Exception {
+                        from("timer:orders?period=500")
+                            .routeId("orders")
+                            .filter(simple("${body} > 3"))
+                                .to("log:big")
+                            .end()
+                            .choice()
+                                .when(simple("${body} == 9"))
+                                    .throwException(new 
IllegalStateException("Out of stock"))
+                                .otherwise()
+                                    .to("seda:shipping");
+                    }
+                }
+                """;
+        List<NodeEntry> nodes = ModelRouteNodeScanner.scan(
+                "/p/OrderRoute.java", "OrderRoute.java", java, Map.of(), new 
DefaultCamelCatalog());
+
+        assertThat(nodes.get(0).kind()).isEqualTo(EntryKind.ROUTE);
+        assertThat(nodes.get(0).routeId()).isEqualTo("orders");
+        assertThat(nodes.get(0).fromUri()).isEqualTo("timer:orders");
+        assertThat(nodes.get(0).lineIndex()).isEqualTo(5);
+        assertThat(nodes.get(0).filePath()).isEqualTo("/p/OrderRoute.java");
+        assertThat(describe(nodes.subList(1, nodes.size()))).containsExactly(
+                "1 filter[simple{${body} > 3}] @8",
+                "2 to[log:big] @9",
+                "1 choice[] @11",
+                "2 when[simple{${body} == 9}] @12",
+                "3 throwException[java.lang.IllegalStateException] @13",
+                "2 otherwise[] @14",
+                "3 to[seda:shipping] @15");
+    }
+
+    @Test
+    void theNodesOfAnXmlRoute() {
+        String xml = """
+                <routes xmlns="http://camel.apache.org/schema/xml-io";>
+                    <route id="shipping">
+                        <from uri="seda:shipping"/>
+                        <log message="Shipping ${body}"/>
+                        <to uri="log:shipped"/>
+                    </route>
+                </routes>
+                """;
+        List<NodeEntry> nodes = ModelRouteNodeScanner.scan(
+                "/p/routes.xml", "routes.xml", xml, Map.of(), new 
DefaultCamelCatalog());
+
+        assertThat(nodes.get(0).routeId()).isEqualTo("shipping");
+        assertThat(describe(nodes.subList(1, nodes.size()))).containsExactly(
+                "1 log[Shipping ${body}] @4",
+                "1 to[log:shipped] @5");
+    }
+
+    @Test
+    void aFileThatIsNotARouteHasNoNodes() {
+        assertThat(ModelRouteNodeScanner.scan(
+                "/p/Util.java", "Util.java", "public class Util {}", Map.of(), 
new DefaultCamelCatalog())).isEmpty();
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewerReloadTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewerReloadTest.java
new file mode 100644
index 000000000000..574dc4a158cf
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewerReloadTest.java
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * A file an AI tool wrote is shown again in the viewer, unless it is being 
edited.
+ */
+class SourceViewerReloadTest {
+
+    @TempDir
+    Path tempDir;
+
+    @Test
+    void theViewerShowsWhatTheAiWrote() throws IOException {
+        Path file = tempDir.resolve("route.camel.yaml");
+        Files.writeString(file, "line0\nline1 ${bdy}\nline2\n");
+        SourceViewer viewer = new SourceViewer();
+        viewer.loadFile(file);
+        viewer.goToLine(1);
+
+        Files.writeString(file, "line0\nline1 ${body}\nline2\n");
+        viewer.reloadIfShowing(file);
+
+        viewer.enterEditMode();
+        
assertThat(viewer.editState().text()).contains("${body}").doesNotContain("${bdy}");
+        assertThat(viewer.getSelectedLine()).isEqualTo(1);
+    }
+
+    @Test
+    void anEditInProgressIsLeftAlone() throws IOException {
+        Path file = tempDir.resolve("route.camel.yaml");
+        Files.writeString(file, "mine\n");
+        SourceViewer viewer = new SourceViewer();
+        viewer.loadFile(file);
+        viewer.enterEditMode();
+
+        Files.writeString(file, "theirs\n");
+        viewer.reloadIfShowing(file);
+
+        assertThat(viewer.isEditMode()).isTrue();
+        assertThat(viewer.editState().text()).contains("mine");
+    }
+
+    @Test
+    void anotherFileIsNotReloaded() throws IOException {
+        Path file = tempDir.resolve("a.camel.yaml");
+        Path other = tempDir.resolve("b.camel.yaml");
+        Files.writeString(file, "a\n");
+        Files.writeString(other, "b\n");
+        SourceViewer viewer = new SourceViewer();
+        viewer.loadFile(file);
+
+        Files.writeString(file, "changed\n");
+        viewer.reloadIfShowing(other);
+
+        viewer.enterEditMode();
+        
assertThat(viewer.editState().text()).contains("a").doesNotContain("changed");
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/YamlRouteNodeScannerTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/YamlRouteNodeScannerTest.java
index 0a1349aba336..e43c4a1f8364 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/YamlRouteNodeScannerTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/YamlRouteNodeScannerTest.java
@@ -211,6 +211,91 @@ class YamlRouteNodeScannerTest {
                 .hasSize(2);
     }
 
+    /** Each processor as indent type[label] @line (from 1). */
+    private static List<String> describe(List<YamlRouteNodeScanner.NodeEntry> 
entries) {
+        return entries.stream()
+                .filter(e -> e.kind() == 
YamlRouteNodeScanner.EntryKind.PROCESSOR)
+                .map(e -> e.indent() + " " + e.type() + "[" + e.label() + "] 
@" + (e.lineIndex() + 1))
+                .toList();
+    }
+
+    @Test
+    void theWhenAndOtherwiseOfAChoiceAreNodes() {
+        String yaml = String.join("\n",
+                "- route:",
+                "    id: shipping",
+                "    from:",
+                "      uri: seda:shipping",
+                "      steps:",
+                "        - choice:",
+                "            when:",
+                "              - expression:",
+                "                  simple:",
+                "                    expression: \"${body} > 6\"",
+                "                steps:",
+                "                  - setHeader:",
+                "                      name: carrier",
+                "              - simple: \"${body} > 3\"",
+                "                steps:",
+                "                  - log:",
+                "                      message: medium",
+                "            otherwise:",
+                "              steps:",
+                "                - setHeader:",
+                "                    name: carrier",
+                "        - log:",
+                "            message: shipped",
+                "");
+
+        List<YamlRouteNodeScanner.NodeEntry> entries = 
YamlRouteNodeScanner.scanLines(List.of(yaml.split("\n")), "f");
+
+        // when and otherwise are siblings under the choice, with their steps 
under them
+        assertThat(describe(entries)).containsExactly(
+                "2 choice[] @6",
+                "4 when[simple{${body} > 6}] @8",
+                "7 setHeader[carrier] @12",
+                "4 when[simple{${body} > 3}] @14",
+                "7 log[medium] @16",
+                "4 otherwise[] @18",
+                "6 setHeader[carrier] @20",
+                "2 log[shipped] @22");
+    }
+
+    @Test
+    void aChoiceInAWhenKeepsTheOuterWhens() {
+        String yaml = String.join("\n",
+                "- route:",
+                "    from:",
+                "      uri: direct:start",
+                "      steps:",
+                "        - choice:",
+                "            when:",
+                "              - simple: \"${body} > 6\"",
+                "                steps:",
+                "                  - choice:",
+                "                      when:",
+                "                        - simple: \"${body} > 8\"",
+                "                          steps:",
+                "                            - to:",
+                "                                uri: direct:big",
+                "              - simple: \"${body} > 3\"",
+                "                steps:",
+                "                  - to:",
+                "                      uri: direct:medium",
+                "");
+
+        List<YamlRouteNodeScanner.NodeEntry> entries = 
YamlRouteNodeScanner.scanLines(List.of(yaml.split("\n")), "f");
+
+        assertThat(describe(entries)).containsExactly(
+                "2 choice[] @5",
+                "4 when[simple{${body} > 6}] @7",
+                "7 choice[] @9",
+                "9 when[simple{${body} > 8}] @11",
+                "12 to[direct:big] @14",
+                "4 when[simple{${body} > 3}] @15",
+                "7 to[direct:medium] @18");
+    }
+
     @Test
     void scanEmptyFileReturnsEmptyList() throws IOException {
         Path file = tempDir.resolve("empty.camel.yaml");

Reply via email to