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 89929a305815 CAMEL-26823: the Diagram and the source editor of the 
Camel CLI show whether a route waits for the answer of the route it sends to 
(#29616)
89929a305815 is described below

commit 89929a305815373d42e2f9a513098a4af3d11ea8
Author: Claus Ibsen <[email protected]>
AuthorDate: Wed Nov 11 13:17:33 2026 +0100

    CAMEL-26823: the Diagram and the source editor of the Camel CLI show 
whether a route waits for the answer of the route it sends to (#29616)
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../org/apache/camel/diagram/TopologyHelper.java   |   2 +
 .../apache/camel/diagram/TopologyLayoutEngine.java |  15 ++-
 .../camel/diagram/TopologyHelperErrorPathTest.java |  18 ++++
 .../ROOT/pages/camel-jbang-tui-diagram.adoc        |   4 +
 .../ROOT/pages/camel-jbang-tui-source-editor.adoc  |   6 +-
 .../jbang/core/commands/tui/DiagramTopology.java   |  10 ++
 .../dsl/jbang/core/commands/tui/JumpLink.java      |  43 ++++++++
 .../camel/dsl/jbang/core/commands/tui/SendHow.java | 120 +++++++++++++++++++++
 .../dsl/jbang/core/commands/tui/SourceTab.java     |  62 +++++++----
 .../dsl/jbang/core/commands/tui/SourceViewer.java  |  13 +--
 .../core/commands/tui/TopologyGroupFolds.java      |  10 +-
 .../core/commands/tui/diagram/DiagramColors.java   |   2 +
 .../tui/diagram/TopologyDiagramWidget.java         |   6 +-
 .../src/main/resources/tui/help/diagram.md         |   6 ++
 .../src/main/resources/tui/help/source.md          |   5 +
 .../commands/tui/SourceEditorProjectInfoTest.java  |   2 +-
 .../jbang/core/commands/tui/SourceSizeTest.java    |   2 +-
 .../core/commands/tui/SourceTabJumpLinksTest.java  | 119 ++++++++++++++++++--
 .../core/commands/tui/TopologyGroupFoldsTest.java  |  15 +++
 .../tui/diagram/TopologyReplyArrowTest.java        |  89 +++++++++++++++
 20 files changed, 501 insertions(+), 48 deletions(-)

diff --git 
a/components/camel-diagram/src/main/java/org/apache/camel/diagram/TopologyHelper.java
 
b/components/camel-diagram/src/main/java/org/apache/camel/diagram/TopologyHelper.java
index dc5f6ab09319..1b6a790d3b14 100644
--- 
a/components/camel-diagram/src/main/java/org/apache/camel/diagram/TopologyHelper.java
+++ 
b/components/camel-diagram/src/main/java/org/apache/camel/diagram/TopologyHelper.java
@@ -88,6 +88,8 @@ public final class TopologyHelper {
             edge.kind = eo.getStringOrDefault("kind", "call");
             edge.via = eo.getString("via");
             edge.handling = eo.getString("handling");
+            // an integration before 4.24 does not say it: its edges are drawn 
as before
+            edge.reply = Boolean.TRUE.equals(eo.get("reply"));
             if (includeErrorPaths || !edge.isErrorPath()) {
                 edges.add(edge);
             }
diff --git 
a/components/camel-diagram/src/main/java/org/apache/camel/diagram/TopologyLayoutEngine.java
 
b/components/camel-diagram/src/main/java/org/apache/camel/diagram/TopologyLayoutEngine.java
index 1e5d657ead90..40f8bceb3adf 100644
--- 
a/components/camel-diagram/src/main/java/org/apache/camel/diagram/TopologyLayoutEngine.java
+++ 
b/components/camel-diagram/src/main/java/org/apache/camel/diagram/TopologyLayoutEngine.java
@@ -155,7 +155,7 @@ public class TopologyLayoutEngine {
                 boolean backEdge = layers.getOrDefault(e.fromRouteId, 0) >= 
layers.getOrDefault(e.toRouteId, 0)
                         && !e.fromRouteId.equals(e.toRouteId);
                 boolean selfLoop = e.fromRouteId.equals(e.toRouteId);
-                layoutEdges.add(new TopologyLayoutEdge(from, to, e.endpoint, 
e.connectionType, backEdge, selfLoop));
+                layoutEdges.add(new TopologyLayoutEdge(from, to, e.endpoint, 
e.connectionType, backEdge, selfLoop, e.reply));
             }
         }
 
@@ -338,6 +338,9 @@ public class TopologyLayoutEngine {
         public String via;
         // for an error path: handled, continued or notHandled (null when a 
predicate decides)
         public String handling;
+        // whether the source route waits for the answer of the target route 
(a direct call, a request-reply over a
+        // broker) rather than handing the message off
+        public boolean reply;
 
         public boolean isErrorPath() {
             return "error".equals(kind);
@@ -382,17 +385,27 @@ public class TopologyLayoutEngine {
         public final String connectionType;
         public final boolean backEdge;
         public final boolean selfLoop;
+        // whether the source route waits for the answer of the target route
+        public final boolean reply;
 
         public TopologyLayoutEdge(
                                   TopologyLayoutNode from, TopologyLayoutNode 
to,
                                   String endpoint, String connectionType,
                                   boolean backEdge, boolean selfLoop) {
+            this(from, to, endpoint, connectionType, backEdge, selfLoop, 
false);
+        }
+
+        public TopologyLayoutEdge(
+                                  TopologyLayoutNode from, TopologyLayoutNode 
to,
+                                  String endpoint, String connectionType,
+                                  boolean backEdge, boolean selfLoop, boolean 
reply) {
             this.from = from;
             this.to = to;
             this.endpoint = endpoint;
             this.connectionType = connectionType;
             this.backEdge = backEdge;
             this.selfLoop = selfLoop;
+            this.reply = reply;
         }
     }
 
diff --git 
a/components/camel-diagram/src/test/java/org/apache/camel/diagram/TopologyHelperErrorPathTest.java
 
b/components/camel-diagram/src/test/java/org/apache/camel/diagram/TopologyHelperErrorPathTest.java
index d3e6f50d7ea2..5a06f235a196 100644
--- 
a/components/camel-diagram/src/test/java/org/apache/camel/diagram/TopologyHelperErrorPathTest.java
+++ 
b/components/camel-diagram/src/test/java/org/apache/camel/diagram/TopologyHelperErrorPathTest.java
@@ -52,6 +52,24 @@ class TopologyHelperErrorPathTest {
                 .containsExactly("errorHandler/handled", 
"onException/notHandled");
     }
 
+    @Test
+    void anEdgeSaysWhetherTheRouteWaitsForTheAnswer() {
+        // CAMEL-26823: reply is on every edge of a 4.24 integration; an older 
one does not say it
+        JsonObject root = new JsonObject();
+        JsonArray edges = new JsonArray();
+        JsonObject stock = edge("reserve-order", "stock-service", "call", 
null, null);
+        stock.put("reply", true);
+        edges.add(stock);
+        JsonObject shipping = edge("reserve-order", "shipping", "call", null, 
null);
+        shipping.put("reply", false);
+        edges.add(shipping);
+        edges.add(edge("legacy", "parked", null, null, null));
+        root.put("edges", edges);
+
+        assertThat(TopologyHelper.parseEdges(root)).extracting(e -> 
e.toRouteId + (e.reply ? " waits" : ""))
+                .containsExactly("stock-service waits", "shipping", "parked");
+    }
+
     private static JsonObject edge(String from, String to, String kind, String 
via, String handling) {
         JsonObject e = new JsonObject();
         e.put("fromRouteId", from);
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-diagram.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-diagram.adoc
index 0a08066a53bb..493061bef0cd 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-diagram.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-diagram.adoc
@@ -31,6 +31,10 @@ The topology shows all routes as boxes, connected by arrows:
 
 * *Trigger routes* (timer, cron, etc.) appear at the top
 * *Downstream routes* appear below, connected by directed arrows
+* The head of an arrow says whether the route waits for the answer of the 
route it sends to: `⇅` it does (a call to a
+  `direct` route, or a request-reply over a broker such as 
`exchangePattern=InOut` on a JMS queue), `▼` it hands the
+  message off (a queue, a topic, a `wireTap`). An integration before Camel 
4.24 does not say it, and all its arrows end
+  in `▼`
 * Route boxes show the route ID, the `from` endpoint, and metrics when enabled
 
 Navigate between route boxes with arrow keys. When a route is selected, an 
*Info panel* appears
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-source-editor.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-source-editor.adoc
index 050c0e96b304..13fe2996ee62 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-source-editor.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-source-editor.adoc
@@ -315,7 +315,11 @@ image::jbang/camel-tui-source-xml-quick-doc.png[The 
documentation of the logging
 
 * *Jump links* -- a line that sends to another route, such as 
`to("seda:shipping")`, shows `↵ shipping`. Press *Enter*
   on the line to go to that route, also when it is in another file or another 
DSL. A `from` shows which route calls
-  it; when more than one does, it shows `↵ 2 callers`, and *Enter* opens a 
list of them to choose from.
+  it; when more than one does, it shows `↵ 2 callers`, and *Enter* opens a 
list of them to choose from. Before the
+  route, `↳` says that the route waits for the answer (`↵ ↳ audit` for a 
`direct` route, a request-reply with
+  `exchangePattern=InOut`, an `enrich`) and `⇢` that it hands the message off 
(`↵ ⇢ shipping` for a queue, a topic or a
+  `wireTap`), as in the About view. While the integration runs, and its 
Diagram, Routes or About view has been open, the
+  running routes decide: a route that consumes a request, from 
`platform-http`, also waits on a send to a queue.
 * *Go to route* -- *g* lists all the routes of the project; type to filter, 
and *Enter* opens the route.
 * *Bean jumps* -- a line that refers to a bean shows `↵ name`, and *Enter* 
goes to where the project declares it.
 * *Usages* -- *u* on a line with an endpoint lists where it is used: the 
routes that consume from it and the steps that
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTopology.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTopology.java
index c97a69d92e81..47981e7978cd 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTopology.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTopology.java
@@ -60,6 +60,16 @@ final class DiagramTopology {
 
     private volatile Structure structure;
 
+    /**
+     * The topology asked for last, when it is of this integration, without 
asking for it: null until a tab (Diagram,
+     * Routes, About) has asked for it. For what must not wait on the 
integration, such as the jump links of the source
+     * editor.
+     */
+    JsonObject cached(String pid) {
+        Structure s = structure;
+        return s != null && pid != null && s.pid().equals(pid) ? s.topology() 
: null;
+    }
+
     /**
      * The step of a route by its id, from the structure asked for last (the 
Routes tab shows what each step does); null
      * until the structure of the integration has been asked for, or when it 
has no such step.
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/JumpLink.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/JumpLink.java
new file mode 100644
index 000000000000..e2e99b5e8b73
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/JumpLink.java
@@ -0,0 +1,43 @@
+/*
+ * 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;
+
+/**
+ * A link on a line to where it leads; callersOf is the endpoint of a route 
sent to from more than one place, whose link
+ * opens a popup to choose one (CAMEL-25411), else null. how is how the send 
reaches the route: call when the sending
+ * route waits for the answer, async when it hands the message off, null when 
not a send between routes.
+ */
+record JumpLink(String routeId, String filePath, int targetLine, String 
callersOf, String how) {
+
+    JumpLink(String routeId, String filePath, int targetLine) {
+        this(routeId, filePath, targetLine, null, null);
+    }
+
+    JumpLink(String routeId, String filePath, int targetLine, String 
callersOf) {
+        this(routeId, filePath, targetLine, callersOf, null);
+    }
+
+    JumpLink withHow(String how) {
+        return new JumpLink(routeId, filePath, targetLine, callersOf, how);
+    }
+
+    /** The hint after the line: ↳ the route waits for the answer, ⇢ a 
hand-off, as the About view marks them. */
+    String hint() {
+        String mark = "call".equals(how) ? "↳ " : "async".equals(how) ? "⇢ " : 
"";
+        return " ↵ " + mark + routeId;
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SendHow.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SendHow.java
new file mode 100644
index 000000000000..a970fe7f633d
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SendHow.java
@@ -0,0 +1,120 @@
+/*
+ * 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.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+import org.apache.camel.util.json.JsonObject;
+
+/**
+ * How a send of a route reaches the route that consumes it, for the jump 
links of the source editor: call when the
+ * sending route waits for the answer, async when it hands the message off 
(CAMEL-26823).
+ */
+final class SendHow {
+
+    // a uri that asks for the answer of what it sends to, and the key of a 
step block that does
+    private static final Pattern IN_OUT_PARAMETER = 
Pattern.compile("[?&]exchangePattern=InOut(&|$)", Pattern.CASE_INSENSITIVE);
+    private static final Pattern IN_OUT_KEY
+            = 
Pattern.compile("(exchangePattern|pattern):\\s*[\"']?InOut[\"']?\\s*(#.*)?", 
Pattern.CASE_INSENSITIVE);
+
+    private SendHow() {
+    }
+
+    /**
+     * How a step reaches the route that consumes the endpoint it sends to, as 
the sources say it: call when the route
+     * waits for the answer (direct and kamelet run the route before it goes 
on, enrich waits for it, and
+     * exchangePattern=InOut asks for it, as a request-reply over a broker 
does), async when it hands the message off (a
+     * wire tap always does). null for a dead letter channel or a pollEnrich, 
which are no send the route waits on or
+     * hands off. A route that consumes an InOut message (from platform-http) 
waits on its sends too; only the running
+     * integration knows that (see {@link #how(String, String, String, Map)}).
+     */
+    static String of(String keyword, String uri, boolean inOut) {
+        if (uri == null || "deadLetterChannel".equals(keyword) || 
"pollEnrich".equals(keyword)) {
+            return null;
+        }
+        if ("wireTap".equals(keyword)) {
+            return "async";
+        }
+        int colon = uri.indexOf(':');
+        String scheme = colon > 0 ? uri.substring(0, 
colon).toLowerCase(Locale.ROOT) : "";
+        if ("enrich".equals(keyword) || "direct".equals(scheme) || 
"kamelet".equals(scheme) || inOut
+                || IN_OUT_PARAMETER.matcher(uri).find()) {
+            return "call";
+        }
+        return "async";
+    }
+
+    /** Whether the block of a step (its parameters) asks for the answer: 
exchangePattern: InOut, or pattern: InOut. */
+    static boolean blockSaysInOut(List<String> lines, int start, int indent) {
+        if (start < 0) {
+            return false;
+        }
+        for (int j = start + 1; j < lines.size(); j++) {
+            String line = lines.get(j);
+            String trimmed = line.trim();
+            if (trimmed.isEmpty() || trimmed.startsWith("#")) {
+                continue;
+            }
+            if (SourceTab.lineIndent(line) <= indent) {
+                break;
+            }
+            if (IN_OUT_KEY.matcher(trimmed).matches()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * How the route reaches the other: as the running integration has it when 
its topology was asked for, else static.
+     */
+    static String how(String fromRouteId, String toRouteId, String staticHow, 
Map<String, String> runtime) {
+        String known = fromRouteId != null ? runtime.get(fromRouteId + ">" + 
toRouteId) : null;
+        return known != null ? known : staticHow;
+    }
+
+    /**
+     * How the routes of the selected integration reach each other, by 
"from>to": call when the route waits for the
+     * answer, else async. Only from the topology a tab has asked for already, 
so the jump links never wait on the
+     * integration; empty when there is none.
+     */
+    static Map<String, String> runtime(MonitorContext ctx) {
+        IntegrationInfo info = ctx.findSelectedIntegration();
+        JsonObject topology = info != null ? 
ctx.diagramTopology.cached(info.pid) : null;
+        Collection<?> edges = topology != null ? 
topology.getCollection("edges") : null;
+        if (edges == null) {
+            return Map.of();
+        }
+        Map<String, String> answer = new HashMap<>();
+        for (Object o : edges) {
+            if (o instanceof JsonObject e && 
!"error".equals(e.getString("kind"))) {
+                String from = e.getString("fromRouteId");
+                String to = e.getString("toRouteId");
+                if (from != null && to != null) {
+                    answer.merge(from + ">" + to, 
Boolean.TRUE.equals(e.get("reply")) ? "call" : "async",
+                            (a, b) -> "call".equals(a) || "call".equals(b) ? 
"call" : a);
+                }
+            }
+        }
+        return answer;
+    }
+}
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 a7e361624029..a506de36dc08 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
@@ -143,7 +143,8 @@ class SourceTab extends AbstractTab {
     record RouteEntry(String routeId, String fromUri, String filePath, int 
fromLine) {
     }
 
-    record ToEntry(String routeId, String toUri, String filePath, int toLine) {
+    /** A send of a route; how is how it reaches the route it sends to, see 
{@link SendHow#of}. */
+    record ToEntry(String routeId, String toUri, String filePath, int toLine, 
String how) {
     }
 
     private List<RouteEntry> routeIndex = Collections.emptyList();
@@ -1357,9 +1358,11 @@ class SourceTab extends AbstractTab {
             emitRouteEntry(fromEntries, route.id(), 
JavaRouteScanner.uri(route.fromUri()), filePath, route.line());
             String routeId = fromEntries.size() > before ? 
fromEntries.get(before).routeId() : "";
             for (ScannedRoute.To to : route.tos()) {
-                String toUri = 
stripQueryParams(JavaRouteScanner.uri(to.uri()));
+                String uri = JavaRouteScanner.uri(to.uri());
+                String toUri = stripQueryParams(uri);
                 if (toUri != null && !toUri.isEmpty()) {
-                    toEntries.add(new ToEntry(routeId, toUri, filePath, 
to.line()));
+                    // the scanners do not say which step sends (a wire tap): 
the uri decides
+                    toEntries.add(new ToEntry(routeId, toUri, filePath, 
to.line(), SendHow.of(null, uri, false)));
                 }
             }
         }
@@ -1420,6 +1423,8 @@ class SourceTab extends AbstractTab {
         int pendingFromLine = -1;
         boolean inLinkableBlock = false;
         int linkableBlockIndent = -1;
+        String blockKeyword = null;
+        int blockStart = -1;
 
         for (int i = 0; i < lines.size(); i++) {
             String line = lines.get(i);
@@ -1491,14 +1496,18 @@ class SourceTab extends AbstractTab {
                             ? trimmed.substring(prefix2.length()).trim()
                             : trimmed.substring(prefix1.length()).trim();
                     if (!after.isEmpty() && !after.equals("{") && 
!after.startsWith("#")) {
-                        String toUri = stripQueryParams(unquote(after));
+                        String uri = unquote(after);
+                        String toUri = stripQueryParams(uri);
                         if (toUri != null && !toUri.isEmpty()) {
                             toEntries.add(new ToEntry(
-                                    currentRouteId != null ? currentRouteId : 
"", toUri, filePath, i));
+                                    currentRouteId != null ? currentRouteId : 
"", toUri, filePath, i,
+                                    SendHow.of(kw, uri, false)));
                         }
                     } else {
                         inLinkableBlock = true;
                         linkableBlockIndent = indent;
+                        blockKeyword = kw;
+                        blockStart = i;
                     }
                     break;
                 }
@@ -1511,7 +1520,8 @@ class SourceTab extends AbstractTab {
                     String toUri = stripQueryParams(val);
                     if (toUri != null && !toUri.isEmpty()) {
                         toEntries.add(new ToEntry(
-                                currentRouteId != null ? currentRouteId : "", 
toUri, filePath, i));
+                                currentRouteId != null ? currentRouteId : "", 
toUri, filePath, i,
+                                SendHow.of(blockKeyword, val, 
SendHow.blockSaysInOut(lines, blockStart, linkableBlockIndent))));
                     }
                 }
             }
@@ -1561,8 +1571,8 @@ class SourceTab extends AbstractTab {
     }
 
     /** The jump links of a file: to the routes its steps send to or are 
called from, and to the beans it refers to. */
-    Map<Integer, SourceViewer.JumpLink> computeJumpLinks(Path currentFile) {
-        Map<Integer, SourceViewer.JumpLink> result = new 
LinkedHashMap<>(computeRouteJumpLinks(currentFile));
+    Map<Integer, JumpLink> computeJumpLinks(Path currentFile) {
+        Map<Integer, JumpLink> result = new 
LinkedHashMap<>(computeRouteJumpLinks(currentFile));
         ProjectBeans beans = projectBeans();
         List<String> lines;
         try {
@@ -1573,7 +1583,7 @@ class SourceTab extends AbstractTab {
         for (int i = 0; i < lines.size(); i++) {
             ProjectBeans.Location bean = beans.refOn(lines.get(i));
             if (bean != null && 
!(bean.filePath().equals(currentFile.toString()) && bean.line() == i)) {
-                result.putIfAbsent(i, new SourceViewer.JumpLink(bean.label(), 
bean.filePath(), bean.line()));
+                result.putIfAbsent(i, new JumpLink(bean.label(), 
bean.filePath(), bean.line()));
             }
         }
         return result;
@@ -1642,7 +1652,7 @@ class SourceTab extends AbstractTab {
         };
     }
 
-    private Map<Integer, SourceViewer.JumpLink> computeRouteJumpLinks(Path 
currentFile) {
+    private Map<Integer, JumpLink> computeRouteJumpLinks(Path currentFile) {
         if (routeIndex.isEmpty()) {
             return Collections.emptyMap();
         }
@@ -1655,7 +1665,8 @@ class SourceTab extends AbstractTab {
         }
 
         String currentFilePath = currentFile.toString();
-        Map<Integer, SourceViewer.JumpLink> result = new LinkedHashMap<>();
+        Map<Integer, JumpLink> result = new LinkedHashMap<>();
+        Map<String, String> runtime = SendHow.runtime(ctx);
 
         Map<String, RouteEntry> fromUriToRoute = new HashMap<>();
         for (RouteEntry re : routeIndex) {
@@ -1669,17 +1680,20 @@ class SourceTab extends AbstractTab {
                     RouteEntry target = fromUriToRoute.get(te.toUri());
                     if (target != null && 
!target.routeId().equals(te.routeId())) {
                         result.putIfAbsent(te.toLine(),
-                                new SourceViewer.JumpLink(target.routeId(), 
target.filePath(), target.fromLine()));
+                                new JumpLink(target.routeId(), 
target.filePath(), target.fromLine())
+                                        .withHow(SendHow.how(te.routeId(), 
target.routeId(), te.how(), runtime)));
                     }
                 }
             }
-            addReverseLinks(currentFilePath, result);
+            addReverseLinks(currentFilePath, result, runtime);
             return result;
         }
 
         // forward links: to/toD/wireTap → target route's from
         boolean inLinkableBlock = false;
         int linkableBlockIndent = -1;
+        String blockKeyword = null;
+        int blockStart = -1;
         String currentRouteId = null;
 
         for (int i = 0; i < lines.size(); i++) {
@@ -1710,6 +1724,7 @@ class SourceTab extends AbstractTab {
             }
 
             String uri = null;
+            String how = null;
             for (String kw : LINKABLE_KEYWORDS) {
                 String prefix1 = kw + ":";
                 String prefix2 = "- " + kw + ":";
@@ -1719,9 +1734,12 @@ class SourceTab extends AbstractTab {
                             : trimmed.substring(prefix1.length()).trim();
                     if (!after.isEmpty() && !after.equals("{") && 
!after.startsWith("#")) {
                         uri = unquote(after);
+                        how = SendHow.of(kw, uri, false);
                     } else {
                         inLinkableBlock = true;
                         linkableBlockIndent = indent;
+                        blockKeyword = kw;
+                        blockStart = i;
                     }
                     break;
                 }
@@ -1731,6 +1749,7 @@ class SourceTab extends AbstractTab {
                 String val = extractYamlValue(uriLine(trimmed), "uri");
                 if (val != null && !val.isEmpty()) {
                     uri = val;
+                    how = SendHow.of(blockKeyword, val, 
SendHow.blockSaysInOut(lines, blockStart, linkableBlockIndent));
                 }
             }
 
@@ -1738,12 +1757,13 @@ class SourceTab extends AbstractTab {
                 String baseUri = stripQueryParams(uri);
                 RouteEntry target = fromUriToRoute.get(baseUri);
                 if (target != null && 
!target.routeId().equals(currentRouteId)) {
-                    result.put(i, new SourceViewer.JumpLink(target.routeId(), 
target.filePath(), target.fromLine()));
+                    result.put(i, new JumpLink(target.routeId(), 
target.filePath(), target.fromLine())
+                            .withHow(SendHow.how(currentRouteId, 
target.routeId(), how, runtime)));
                 }
             }
         }
 
-        addReverseLinks(currentFilePath, result);
+        addReverseLinks(currentFilePath, result, runtime);
         return result;
     }
 
@@ -1751,7 +1771,8 @@ class SourceTab extends AbstractTab {
      * Reverse links: a from line to the route that sends to it (jumps to the 
caller's to line), or when several do, to
      * a popup to choose one of them: a Kamelet or a direct: route used by 
more than one route.
      */
-    private void addReverseLinks(String currentFilePath, Map<Integer, 
SourceViewer.JumpLink> result) {
+    private void addReverseLinks(
+            String currentFilePath, Map<Integer, JumpLink> result, Map<String, 
String> runtime) {
         for (RouteEntry re : routeIndex) {
             if (!currentFilePath.equals(re.filePath())) {
                 continue;
@@ -1760,10 +1781,11 @@ class SourceTab extends AbstractTab {
             if (callers.size() == 1) {
                 ToEntry te = callers.get(0);
                 String callerRouteId = te.routeId().isEmpty() ? "route" : 
te.routeId();
-                result.putIfAbsent(re.fromLine(), new 
SourceViewer.JumpLink(callerRouteId, te.filePath(), te.toLine()));
+                result.putIfAbsent(re.fromLine(), new JumpLink(callerRouteId, 
te.filePath(), te.toLine())
+                        .withHow(SendHow.how(te.routeId(), re.routeId(), 
te.how(), runtime)));
             } else if (callers.size() > 1) {
                 result.putIfAbsent(re.fromLine(),
-                        new SourceViewer.JumpLink(callers.size() + " callers", 
null, -1, re.fromUri()));
+                        new JumpLink(callers.size() + " callers", null, -1, 
re.fromUri()));
             }
         }
     }
@@ -1876,7 +1898,7 @@ class SourceTab extends AbstractTab {
         }
     }
 
-    private void handleJumpLink(SourceViewer.JumpLink link) {
+    private void handleJumpLink(JumpLink link) {
         if (link.callersOf() != null) {
             // sent to from more than one place: choose which
             gotoRoutePopup.openItems(callersOf(link.callersOf()), "Callers of 
" + link.callersOf());
@@ -1959,7 +1981,7 @@ class SourceTab extends AbstractTab {
         return q >= 0 ? uri.substring(0, q) : uri;
     }
 
-    private static int lineIndent(String line) {
+    static int lineIndent(String line) {
         int indent = 0;
         for (int i = 0; i < line.length(); i++) {
             if (line.charAt(i) == ' ') {
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 f696cd38d0de..fcbeea91604c 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
@@ -118,17 +118,6 @@ class SourceViewer {
         Set<Integer> scan(List<JsonObject> codeData);
     }
 
-    /**
-     * A link on a line to where it leads; callersOf is the endpoint of a 
route sent to from more than one place, whose
-     * link opens a popup to choose one (CAMEL-25411), else null.
-     */
-    record JumpLink(String routeId, String filePath, int targetLine, String 
callersOf) {
-
-        JumpLink(String routeId, String filePath, int targetLine) {
-            this(routeId, filePath, targetLine, null);
-        }
-    }
-
     private boolean visible;
     private List<String> lines = Collections.emptyList();
     private List<JsonObject> codeData = Collections.emptyList();
@@ -2845,7 +2834,7 @@ class SourceViewer {
             if (isSelected) {
                 linkStyle = linkStyle.patch(selBg);
             }
-            spans.add(Span.styled(" ↵ " + jl.routeId(), linkStyle));
+            spans.add(Span.styled(jl.hint(), linkStyle));
         }
         liveColumn.addBodyTypes(spans, lineIndex, plainMode, isSelected ? 
selBg : null, viewportWidth - 1 + hSkip);
         Line full = Line.from(spans);
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TopologyGroupFolds.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TopologyGroupFolds.java
index dddbaef0c676..b2b4a69f8fa2 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TopologyGroupFolds.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TopologyGroupFolds.java
@@ -17,7 +17,7 @@
 package org.apache.camel.dsl.jbang.core.commands.tui;
 
 import java.util.ArrayList;
-import java.util.HashSet;
+import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
@@ -94,7 +94,7 @@ final class TopologyGroupFolds {
         nodes.addAll(kept);
 
         List<TopologyEdgeInfo> moved = new ArrayList<>();
-        Set<String> seen = new HashSet<>();
+        Map<String, TopologyEdgeInfo> seen = new HashMap<>();
         for (TopologyEdgeInfo e : edges) {
             String from = boxOf.getOrDefault(e.fromRouteId, e.fromRouteId);
             String to = boxOf.getOrDefault(e.toRouteId, e.toRouteId);
@@ -102,10 +102,14 @@ final class TopologyGroupFolds {
                 // between the routes of the group
                 continue;
             }
-            if (seen.add(from + "\u0000" + to + "\u0000" + e.kind)) {
+            TopologyEdgeInfo known = seen.putIfAbsent(from + "\u0000" + to + 
"\u0000" + e.kind, e);
+            if (known == null) {
                 e.fromRouteId = from;
                 e.toRouteId = to;
                 moved.add(e);
+            } else if (e.reply) {
+                // one of the routes of the group waits for the answer: the 
box does
+                known.reply = true;
             }
         }
         edges.clear();
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/diagram/DiagramColors.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/diagram/DiagramColors.java
index 86416c408201..ca51b93708f1 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/diagram/DiagramColors.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/diagram/DiagramColors.java
@@ -33,6 +33,8 @@ public final class DiagramColors {
     static final char T_UP = '┴';
     static final char CROSS = '┼';
     static final char ARROW = '▼';
+    // the head of an edge whose source route waits for the answer: there and 
back
+    static final char REPLY_ARROW = '⇅';
     static final char DASH_V = '┆';
     static final char DASH_H = '┄';
 
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/diagram/TopologyDiagramWidget.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/diagram/TopologyDiagramWidget.java
index da63efad5199..cbf10d9559a6 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/diagram/TopologyDiagramWidget.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/diagram/TopologyDiagramWidget.java
@@ -601,6 +601,8 @@ public class TopologyDiagramWidget implements Widget {
         }
 
         boolean dashed = isExternal(edge.from) || isExternal(edge.to) || 
ERROR_HANDLING.equals(edge.to.nodeType);
+        // ⇅ the route waits for the answer (a call, a request-reply over a 
broker), ▼ it hands the message off
+        char head = edge.reply ? REPLY_ARROW : ARROW;
         char vChar = dashed ? DASH_V : V;
         char hChar = dashed ? DASH_H : H;
         boolean edgeHighlighted = !dashed
@@ -617,7 +619,7 @@ public class TopologyDiagramWidget implements Widget {
             for (int r = fromBottom; r < toTop - 1; r++) {
                 plotLine(buffer, area, r, fromCx, vChar, edgeStyle);
             }
-            setChar(buffer, area, toTop - 1, toCx, ARROW, edgeStyle);
+            setChar(buffer, area, toTop - 1, toCx, head, edgeStyle);
         } else {
             int midRow = fromBottom + (toTop - fromBottom) / 2;
 
@@ -637,7 +639,7 @@ public class TopologyDiagramWidget implements Widget {
             for (int r = midRow + 1; r < toTop - 1; r++) {
                 plotLine(buffer, area, r, toCx, vChar, edgeStyle);
             }
-            setChar(buffer, area, toTop - 1, toCx, ARROW, edgeStyle);
+            setChar(buffer, area, toTop - 1, toCx, head, edgeStyle);
         }
     }
 
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/diagram.md 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/diagram.md
index 6e2c4afb5c6d..74e696d1fdb4 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/diagram.md
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/diagram.md
@@ -33,6 +33,12 @@ Each box represents a route. The first line is the route ID,
 the second line shows the `from` endpoint, and the bottom line
 shows metrics when enabled. Arrows show how routes connect.
 
+The head of an arrow says whether the route waits for the answer
+of the route it sends to:
+- `⇅` it waits: a call to a `direct` route, or a request-reply over
+  a broker (`exchangePattern=InOut`)
+- `▼` it hands the message off: a queue, a topic, a `wireTap`
+
 ## Metrics
 
 When metrics are enabled, each route box shows exchange counts:
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 340c3775f65c..04773a4400d0 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
@@ -188,6 +188,11 @@ Lines with `to:`, `toD:`, `wireTap:`, or similar endpoints 
that reference
 another route show a **↵ routeId** indicator. Press **Enter** on such a line
 to jump to the target route's definition (within the same file or across 
files).
 Reverse links are shown on `from:` lines, indicating which route calls this 
one.
+Before the route, **↳** says the route waits for the answer (a `direct` route,
+`exchangePattern=InOut`, an `enrich`) and **⇢** that it hands the message off
+(a queue, a topic, a `wireTap`), as in the About view. While the integration
+runs, and its Diagram, Routes or About has been open, the running routes decide
+it: a route that consumes a request (from `platform-http`) waits on a queue 
too.
 This works for YAML, XML and Java DSL routes, across all the folders of the
 project and files of different DSLs (build output and `src/test` are left out);
 Java routes are read without compiling them. The case and otherwise of a
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceEditorProjectInfoTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceEditorProjectInfoTest.java
index bc9d3224205b..e6c65d9cbeb8 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceEditorProjectInfoTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceEditorProjectInfoTest.java
@@ -157,7 +157,7 @@ class SourceEditorProjectInfoTest {
         SourceTab tab = newTab();
         assertThat(tab.loadDirectory(dir)).isTrue();
 
-        Map<Integer, SourceViewer.JumpLink> links = 
tab.computeJumpLinks(route);
+        Map<Integer, JumpLink> links = tab.computeJumpLinks(route);
         assertThat(links.get(5).routeId()).isEqualTo("orders");
         assertThat(links.get(5).filePath()).endsWith("OrderService.java");
         assertThat(links.get(5).targetLine()).isEqualTo(2);
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceSizeTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceSizeTest.java
index fe7d84bb4d54..1ebcca7a9b16 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceSizeTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceSizeTest.java
@@ -75,7 +75,7 @@ class SourceSizeTest {
             SendMessagePopup.handleKeyEvent 220
             SourceEditAssist.java 2006
             SourceViewer.handleEditKeyEvent 231
-            SourceViewer.java 2953
+            SourceViewer.java 2942
             SourceViewer.renderContent 207
             SourceViewer.renderEditMode 256
             TuiToolDefinitions.addInteractionTools 183
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTabJumpLinksTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTabJumpLinksTest.java
index b7da0484a30b..a02cbc49a08f 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTabJumpLinksTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTabJumpLinksTest.java
@@ -81,7 +81,7 @@ class SourceTabJumpLinksTest {
         SourceTab tab = newTab();
         assertThat(tab.loadDirectory(dir)).isTrue();
 
-        Map<Integer, SourceViewer.JumpLink> links = 
tab.computeJumpLinks(tickets);
+        Map<Integer, JumpLink> links = tab.computeJumpLinks(tickets);
         assertThat(links.get(11).routeId()).isEqualTo("billing");
         assertThat(links.get(12).routeId()).isEqualTo("technical");
         // a route without an id is named by its from endpoint
@@ -129,7 +129,7 @@ class SourceTabJumpLinksTest {
                             uri: kamelet:tag-order-action
                 """);
         assertThat(tab.loadDirectory(dir)).isTrue();
-        SourceViewer.JumpLink link = tab.computeJumpLinks(kamelet).get(6);
+        JumpLink link = tab.computeJumpLinks(kamelet).get(6);
         assertThat(link.routeId()).isEqualTo("2 callers");
         assertThat(link.callersOf()).isEqualTo("kamelet:tag-order-action");
         assertThat(tab.callersOf("kamelet:tag-order-action"))
@@ -169,19 +169,124 @@ class SourceTabJumpLinksTest {
         SourceTab tab = newTab();
         assertThat(tab.loadDirectory(dir)).isTrue();
 
-        Map<Integer, SourceViewer.JumpLink> links = 
tab.computeJumpLinks(orders);
+        Map<Integer, JumpLink> links = tab.computeJumpLinks(orders);
         assertThat(links.get(5).routeId()).isEqualTo("billing");
         assertThat(links.get(5).filePath()).isEqualTo(billing.toString());
         assertThat(links.get(9).routeId()).isEqualTo("billing");
 
         // and back: the from: line of the YAML route has two Java callers, to 
choose from in a popup (CAMEL-25411)
-        SourceViewer.JumpLink back = tab.computeJumpLinks(billing).get(2);
+        JumpLink back = tab.computeJumpLinks(billing).get(2);
         assertThat(back.routeId()).isEqualTo("2 callers");
         assertThat(back.callersOf()).isEqualTo("direct:billing");
         
assertThat(tab.callersOf("direct:billing")).extracting(GotoRoutePopup.RouteItem::filePath)
                 .containsOnly(orders.toString());
     }
 
+    @Test
+    void aHintSaysWhetherTheRouteWaitsForTheAnswer() throws Exception {
+        // CAMEL-26823: ↳ the route waits for the answer, ⇢ it hands the 
message off, as the About view marks them
+        Path orders = Files.writeString(dir.resolve("orders.camel.yaml"), """
+                - route:
+                    id: reserve-order
+                    from:
+                      uri: seda:orders
+                      steps:
+                        - to:
+                            uri: seda:stock
+                            parameters:
+                              exchangePattern: InOut
+                        - to: direct:audit
+                        - wireTap:
+                            uri: seda:metrics
+                        - to: seda:reserved
+                - route:
+                    id: stock-service
+                    from:
+                      uri: seda:stock
+                      steps:
+                        - log: stock
+                - route:
+                    id: audit
+                    from:
+                      uri: direct:audit
+                      steps:
+                        - log: audit
+                - route:
+                    id: metrics
+                    from:
+                      uri: seda:metrics
+                      steps:
+                        - log: metrics
+                - route:
+                    id: shipping
+                    from:
+                      uri: seda:reserved
+                      steps:
+                        - log: ship
+                """);
+        Path quotes = Files.writeString(dir.resolve("Quotes.java"), """
+                import org.apache.camel.builder.RouteBuilder;
+
+                public class Quotes extends RouteBuilder {
+                    public void configure() {
+                        from("timer:quote").routeId("quotes")
+                            .to("seda:stock?exchangePattern=InOut")
+                            .to("seda:reserved");
+                    }
+                }
+                """);
+        SourceTab tab = newTab();
+        assertThat(tab.loadDirectory(dir)).isTrue();
+
+        List<String> lines = Files.readAllLines(orders);
+        Map<Integer, JumpLink> links = tab.computeJumpLinks(orders);
+        assertThat(links.get(lineOf(lines, "uri: 
seda:stock")).hint()).isEqualTo(" ↵ ↳ stock-service");
+        assertThat(links.get(lineOf(lines, "- to: 
direct:audit")).hint()).isEqualTo(" ↵ ↳ audit");
+        assertThat(links.get(lineOf(lines, "uri: 
seda:metrics")).hint()).isEqualTo(" ↵ ⇢ metrics");
+        assertThat(links.get(lineOf(lines, "- to: 
seda:reserved")).hint()).isEqualTo(" ↵ ⇢ shipping");
+        // and back: the from line of a route called from one place says how 
its caller reaches it
+        assertThat(links.get(lineOf(lines, "uri: direct:audit") - 
1).hint()).isEqualTo(" ↵ ↳ reserve-order");
+
+        // a Java route asks for the answer in the uri
+        Map<Integer, JumpLink> java = tab.computeJumpLinks(quotes);
+        assertThat(java.get(5).hint()).isEqualTo(" ↵ ↳ stock-service");
+        assertThat(java.get(6).hint()).isEqualTo(" ↵ ⇢ shipping");
+    }
+
+    @Test
+    void howAStepReachesTheRouteItSendsTo() {
+        assertThat(SendHow.of("to", "direct:audit", false)).isEqualTo("call");
+        assertThat(SendHow.of("to", "kamelet:my-sink", 
false)).isEqualTo("call");
+        assertThat(SendHow.of("to", "jms:queue:stock?exchangePattern=InOut", 
false)).isEqualTo("call");
+        assertThat(SendHow.of("to", "jms:queue:stock?exchangePattern=InOnly", 
false)).isEqualTo("async");
+        assertThat(SendHow.of("to", "seda:stock", true)).isEqualTo("call");
+        assertThat(SendHow.of("enrich", "seda:prices", 
false)).isEqualTo("call");
+        assertThat(SendHow.of("wireTap", "direct:audit", 
false)).isEqualTo("async");
+        assertThat(SendHow.of("to", "seda:reserved", 
false)).isEqualTo("async");
+        assertThat(SendHow.of("deadLetterChannel", "seda:dead", 
false)).isNull();
+        assertThat(SendHow.of("pollEnrich", "seda:prices", false)).isNull();
+
+        List<String> block = List.of(
+                "        - to:",
+                "            uri: seda:stock",
+                "            pattern: \"InOut\"",
+                "        - to: seda:next");
+        assertThat(SendHow.blockSaysInOut(block, 0, 8)).isTrue();
+        assertThat(SendHow.blockSaysInOut(block.subList(0, 2), 0, 
8)).isFalse();
+        // the next step is not part of the block
+        assertThat(SendHow.blockSaysInOut(List.of("        - to:", "           
 uri: seda:a", "        - to:",
+                "            pattern: InOut"), 0, 8)).isFalse();
+    }
+
+    private static int lineOf(List<String> lines, String text) {
+        for (int i = 0; i < lines.size(); i++) {
+            if (lines.get(i).trim().equals(text)) {
+                return i;
+            }
+        }
+        throw new IllegalArgumentException(text);
+    }
+
     @Test
     void xmlRoutesLinkToAndFromTheOthers() throws Exception {
         // CAMEL-25196: XML routes have the jump links of the YAML and Java 
ones
@@ -205,10 +310,10 @@ class SourceTabJumpLinksTest {
         SourceTab tab = newTab();
         assertThat(tab.loadDirectory(dir)).isTrue();
 
-        SourceViewer.JumpLink link = tab.computeJumpLinks(orders).get(3);
+        JumpLink link = tab.computeJumpLinks(orders).get(3);
         assertThat(link.routeId()).isEqualTo("billing");
         assertThat(link.filePath()).isEqualTo(billing.toString());
-        SourceViewer.JumpLink back = tab.computeJumpLinks(billing).get(2);
+        JumpLink back = tab.computeJumpLinks(billing).get(2);
         assertThat(back.routeId()).isEqualTo("orders");
         assertThat(back.targetLine()).isEqualTo(3);
     }
@@ -251,7 +356,7 @@ class SourceTabJumpLinksTest {
         tab.showSourceDirectory(dir);
         assertThat(tab.routeSources()).containsExactlyInAnyOrder(orders, 
billing);
 
-        SourceViewer.JumpLink link = tab.computeJumpLinks(orders).get(5);
+        JumpLink link = tab.computeJumpLinks(orders).get(5);
         assertThat(link.routeId()).isEqualTo("billing");
         assertThat(link.filePath()).isEqualTo(billing.toString());
         
assertThat(tab.computeJumpLinks(billing).get(4).filePath()).isEqualTo(orders.toString());
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TopologyGroupFoldsTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TopologyGroupFoldsTest.java
index 748e96de5a4d..9b07d473227b 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TopologyGroupFoldsTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TopologyGroupFoldsTest.java
@@ -79,6 +79,21 @@ class TopologyGroupFoldsTest {
         assertThat(TopologyGroupFolds.groupOf("orders")).isNull();
     }
 
+    @Test
+    void aFoldedGroupWaitsWhenOneOfItsRoutesIsWaitedOn() {
+        // CAMEL-26823: orders hands off to audit but waits for archive; the 
one link to the box waits
+        List<TopologyNodeInfo> nodes = new ArrayList<>(
+                List.of(route("orders", 10, 0), route("audit", 8, 1), 
route("archive", 9, 2)));
+        TopologyEdgeInfo archive = call("orders", "archive");
+        archive.reply = true;
+        List<TopologyEdgeInfo> edges = new ArrayList<>(List.of(call("orders", 
"audit"), archive));
+
+        TopologyGroupFolds.fold(nodes, edges, tags, Set.of("utility"));
+
+        assertThat(edges).extracting(e -> e.fromRouteId + ">" + e.toRouteId + 
(e.reply ? " waits" : ""))
+                .containsExactly("orders>group:utility waits");
+    }
+
     @Test
     void nothingFoldedLeavesTheTopologyAsItIs() {
         List<TopologyNodeInfo> nodes = new ArrayList<>(List.of(route("orders", 
10, 0), route("audit", 8, 1)));
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/diagram/TopologyReplyArrowTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/diagram/TopologyReplyArrowTest.java
new file mode 100644
index 000000000000..0bec779990e3
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/diagram/TopologyReplyArrowTest.java
@@ -0,0 +1,89 @@
+/*
+ * 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.diagram;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import dev.tamboui.buffer.Buffer;
+import dev.tamboui.layout.Rect;
+import org.apache.camel.diagram.TopologyLayoutEngine;
+import org.apache.camel.diagram.TopologyLayoutEngine.TopologyEdgeInfo;
+import org.apache.camel.diagram.TopologyLayoutEngine.TopologyLayoutResult;
+import org.apache.camel.diagram.TopologyLayoutEngine.TopologyNodeInfo;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * CAMEL-26823: the head of an edge in the topology says whether the route 
waits for the answer of the route it sends
+ * to: ⇅ there and back (a direct call, a request-reply over a broker), ▼ a 
hand-off.
+ */
+class TopologyReplyArrowTest {
+
+    @Test
+    void anEdgeTheRouteWaitsOnEndsInATwoWayHead() {
+        TopologyEdgeInfo stock = edge("reserve-order", "stock-service", 
"seda:stock", true);
+        TopologyEdgeInfo shipping = edge("reserve-order", "shipping", 
"seda:reserved", false);
+        TopologyLayoutEngine engine = new TopologyLayoutEngine();
+        TopologyLayoutResult layout = engine.layout(
+                new ArrayList<>(List.of(route("reserve-order"), 
route("stock-service"), route("shipping"))),
+                new ArrayList<>(List.of(stock, shipping)));
+        assertThat(layout.edges).extracting(e -> e.to.routeId + (e.reply ? " 
waits" : ""))
+                .containsExactlyInAnyOrder("stock-service waits", "shipping");
+
+        TopologyDiagramWidget widget = new TopologyDiagramWidget(layout, 
engine.getNodeWidth(), -1, 0, 0, false, false);
+        Rect area = new Rect(0, 0, Math.max(100, widget.getTotalCols()), 
Math.max(20, widget.getTotalRows()));
+        Buffer buffer = Buffer.empty(area);
+        widget.render(area, buffer);
+
+        assertThat(headAbove(buffer, widget, "stock-service")).isEqualTo("⇅");
+        assertThat(headAbove(buffer, widget, "shipping")).isEqualTo("▼");
+    }
+
+    /** The arrow head on the row above the box of the route. */
+    private static String headAbove(Buffer buffer, TopologyDiagramWidget 
widget, String routeId) {
+        TopologyDiagramWidget.NodeBox box = widget.getNodeBoxes().stream()
+                .filter(b -> 
routeId.equals(b.routeId())).findFirst().orElseThrow();
+        StringBuilder heads = new StringBuilder();
+        for (int col = box.startCol(); col <= box.endCol(); col++) {
+            String symbol = buffer.get(col, box.startRow() - 1).symbol();
+            if ("⇅".equals(symbol) || "▼".equals(symbol)) {
+                heads.append(symbol);
+            }
+        }
+        return heads.toString();
+    }
+
+    private static TopologyNodeInfo route(String id) {
+        TopologyNodeInfo n = new TopologyNodeInfo();
+        n.routeId = id;
+        n.from = "seda:" + id;
+        n.nodeType = "route";
+        return n;
+    }
+
+    private static TopologyEdgeInfo edge(String from, String to, String 
endpoint, boolean reply) {
+        TopologyEdgeInfo e = new TopologyEdgeInfo();
+        e.fromRouteId = from;
+        e.toRouteId = to;
+        e.endpoint = endpoint;
+        e.connectionType = "internal";
+        e.reply = reply;
+        return e;
+    }
+}

Reply via email to