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 f6301f02280a CAMEL-25892: camel-jbang - show the body types each step 
has been reached with in the Routes and Diagram views (#28192)
f6301f02280a is described below

commit f6301f02280a6d31e861ecfcab3c71de7f5df5ec
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Oct 20 22:11:10 2026 +0200

    CAMEL-25892: camel-jbang - show the body types each step has been reached 
with in the Routes and Diagram views (#28192)
    
    * CAMEL-25892: camel-diagram - the steps of a route diagram have the body 
types they have been reached with
    
    The route-structure console has the body types each step has been reached 
with,
    across the messages, and their kinds (CAMEL-25750). The node of a route 
diagram
    now keeps them, so a renderer can draw them: RouteDiagramHelper reads them, 
and
    the steps of an onException block copied into the error frame keep them.
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    * CAMEL-25892: camel-jbang - show the body types each step has been reached 
with in the Routes and Diagram views
    
    Routes: a BODY TYPES column says how many steps of a route have been reached
    with more than one body type, and how many of them with bodies of more than 
one
    kind, which need attention; the processors of the selected route have the 
types
    of each step.
    
    Diagram: y shows the types beside the arrow into each step of a route, on 
the
    shaft above the head, cut to the room there is where the arrows of a branch 
are
    side by side (the first types and how many more). A step reached with 
bodies of
    more than one kind has a warning sign in its top border. The Info panel of 
a step
    lists all its types with their kind, the Info panel of a route in the 
topology
    its counts, and the diagram state of the MCP tools has them. The types 
follow the
    status file while they are shown, as the metrics do.
    
    DiagramSupport was at its size limit: its layout helpers moved to 
DiagramLayouts.
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    ---------
    
    Signed-off-by: Claus Ibsen <[email protected]>
    Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
---
 .../apache/camel/diagram/RouteDiagramHelper.java   |  16 ++
 .../camel/diagram/RouteDiagramLayoutEngine.java    |  10 +
 .../org/apache/camel/diagram/RouteErrorFrame.java  |   2 +
 .../camel/diagram/RouteDiagramHelperTest.java      |  29 ++
 .../apache/camel/diagram/RouteErrorFrameTest.java  |  15 +
 .../images/jbang/camel-tui-diagram-body-types.png  | Bin 0 -> 329211 bytes
 .../images/jbang/camel-tui-routes-body-types.png   | Bin 0 -> 350752 bytes
 .../ROOT/pages/camel-jbang-tui-diagram.adoc        |  41 ++-
 .../core/commands/tui/AbstractDiagramTab.java      |  13 +-
 .../dsl/jbang/core/commands/tui/BodyTypes.java     | 122 ++++++++
 .../core/commands/tui/DiagramDetailSupport.java    | 285 +++++++++++--------
 .../jbang/core/commands/tui/DiagramLayouts.java    |  95 +++++++
 .../jbang/core/commands/tui/DiagramSupport.java    |  99 ++-----
 .../dsl/jbang/core/commands/tui/DiagramTab.java    |   7 +
 .../jbang/core/commands/tui/DiagramTopology.java   |  12 +-
 .../dsl/jbang/core/commands/tui/ProcessorInfo.java |   5 +
 .../dsl/jbang/core/commands/tui/RouteInfo.java     |   4 +
 .../dsl/jbang/core/commands/tui/RoutesTab.java     | 316 +++++++++++++--------
 .../dsl/jbang/core/commands/tui/StatusParser.java  |  20 +-
 .../commands/tui/diagram/RouteDiagramWidget.java   | 149 +++++++++-
 .../src/main/resources/tui/help/diagram.md         |  35 ++-
 .../src/main/resources/tui/help/routes.md          |   2 +
 .../core/commands/tui/AbstractDiagramTabTest.java  |  16 ++
 .../core/commands/tui/BodyTypesViewsTest.java      | 198 +++++++++++++
 .../core/commands/tui/DiagramTopologyTest.java     |  19 ++
 .../jbang/core/commands/tui/SourceSizeTest.java    |   5 +-
 .../tui/diagram/BodyTypesOnArrowsTest.java         | 142 +++++++++
 27 files changed, 1328 insertions(+), 329 deletions(-)

diff --git 
a/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramHelper.java
 
b/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramHelper.java
index 48f94cb6e637..f7964e845675 100644
--- 
a/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramHelper.java
+++ 
b/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramHelper.java
@@ -81,6 +81,8 @@ public final class RouteDiagramHelper {
                     node.level = level != null ? level : 0;
                     Integer lineNum = line.getInteger("line");
                     node.line = lineNum != null ? lineNum : 0;
+                    node.bodyTypes = strings(line.get("bodyTypes"));
+                    node.bodyKinds = strings(line.get("bodyKinds"));
 
                     if (line.containsKey("statistics")) {
                         JsonObject ls = line.getJsonObject("statistics");
@@ -125,6 +127,20 @@ public final class RouteDiagramHelper {
         return routes;
     }
 
+    /** The strings of a JSON array, such as the body types of a step; empty 
when there are none. */
+    private static List<String> strings(Object array) {
+        if (!(array instanceof List<?> list) || list.isEmpty()) {
+            return List.of();
+        }
+        List<String> answer = new ArrayList<>(list.size());
+        for (Object o : list) {
+            if (o != null) {
+                answer.add(o.toString());
+            }
+        }
+        return List.copyOf(answer);
+    }
+
     static List<String> wrapText(String text, int maxWidth) {
         if (maxWidth <= 0 || text.length() <= maxWidth) {
             return List.of(text);
diff --git 
a/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramLayoutEngine.java
 
b/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramLayoutEngine.java
index 24bea721acf7..85f31f1a2415 100644
--- 
a/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramLayoutEngine.java
+++ 
b/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramLayoutEngine.java
@@ -186,6 +186,16 @@ public class RouteDiagramLayoutEngine {
         public int line;
         public boolean remote;
         public StatInfo stat;
+        /**
+         * The body types the step has been reached with, across the messages, 
when the message history recorded them:
+         * the class names, at most a few.
+         */
+        public List<String> bodyTypes = List.of();
+        /**
+         * The kinds of those bodies: null, payload, data and collection. More 
than one is a step that needs attention,
+         * as a step that writes the body fails for the data, and one that 
reads fields fails for the payload.
+         */
+        public List<String> bodyKinds = List.of();
     }
 
     public static class RouteInfo {
diff --git 
a/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteErrorFrame.java
 
b/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteErrorFrame.java
index 336c771d1a64..91293c416c32 100644
--- 
a/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteErrorFrame.java
+++ 
b/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteErrorFrame.java
@@ -87,6 +87,8 @@ public record RouteErrorFrame(int topY, int bottomY, 
List<String> lines) {
         copy.line = n.line;
         copy.remote = n.remote;
         copy.stat = n.stat;
+        copy.bodyTypes = n.bodyTypes;
+        copy.bodyKinds = n.bodyKinds;
         return copy;
     }
 
diff --git 
a/components/camel-diagram/src/test/java/org/apache/camel/diagram/RouteDiagramHelperTest.java
 
b/components/camel-diagram/src/test/java/org/apache/camel/diagram/RouteDiagramHelperTest.java
index f1415f6bf9e8..c932f0d9aa7a 100644
--- 
a/components/camel-diagram/src/test/java/org/apache/camel/diagram/RouteDiagramHelperTest.java
+++ 
b/components/camel-diagram/src/test/java/org/apache/camel/diagram/RouteDiagramHelperTest.java
@@ -18,12 +18,41 @@ package org.apache.camel.diagram;
 
 import java.util.List;
 
+import org.apache.camel.diagram.RouteDiagramLayoutEngine.NodeInfo;
+import org.apache.camel.diagram.RouteDiagramLayoutEngine.RouteInfo;
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
 import org.junit.jupiter.api.Test;
 
 import static org.assertj.core.api.Assertions.assertThat;
 
 class RouteDiagramHelperTest {
 
+    @Test
+    void parseRoutesKeepsTheBodyTypesOfEachStep() {
+        JsonObject structure = (JsonObject) Jsoner.deserialize("""
+                {"routes": [{"routeId": "orders", "code": [
+                  {"type": "route", "id": "orders", "level": 0, "code": 
"route"},
+                  {"type": "from", "id": "from1", "level": 0, "code": 
"from[platform-http:/orders]"},
+                  {"type": "unmarshal", "id": "unmarshal1", "level": 1, 
"code": "unmarshal[json]",
+                   "bodyTypes": 
["org.apache.camel.converter.stream.ByteArrayInputStreamCache"], "bodyKinds": 
["payload"]},
+                  {"type": "to", "id": "to1", "level": 1, "code": 
"to[file:outbox]",
+                   "bodyTypes": ["byte[]", "java.util.LinkedHashMap"], 
"bodyKinds": ["payload", "data"]}]}]}
+                """, (JsonObject) null);
+
+        RouteInfo route = RouteDiagramHelper.parseRoutes(structure).get(0);
+
+        NodeInfo from = route.nodes.get(1);
+        NodeInfo to = route.nodes.get(3);
+        // the from has none: the body it gave is what the first step was 
reached with
+        assertThat(from.bodyTypes).isEmpty();
+        assertThat(from.bodyKinds).isEmpty();
+        assertThat(route.nodes.get(2).bodyTypes)
+                
.containsExactly("org.apache.camel.converter.stream.ByteArrayInputStreamCache");
+        assertThat(to.bodyTypes).containsExactly("byte[]", 
"java.util.LinkedHashMap");
+        assertThat(to.bodyKinds).containsExactly("payload", "data");
+    }
+
     @Test
     void wrapTextShortReturnedAsIs() {
         assertThat(RouteDiagramHelper.wrapText("short", 
20)).containsExactly("short");
diff --git 
a/components/camel-diagram/src/test/java/org/apache/camel/diagram/RouteErrorFrameTest.java
 
b/components/camel-diagram/src/test/java/org/apache/camel/diagram/RouteErrorFrameTest.java
index 86a214f716b9..4ae95ec3d485 100644
--- 
a/components/camel-diagram/src/test/java/org/apache/camel/diagram/RouteErrorFrameTest.java
+++ 
b/components/camel-diagram/src/test/java/org/apache/camel/diagram/RouteErrorFrameTest.java
@@ -65,6 +65,21 @@ class RouteErrorFrameTest {
                 .extracting(n -> n.id + "@" + 
n.level).containsExactly("onException1@0", "log1@1", "to1@1"));
     }
 
+    @Test
+    void theStepsOfAnOnExceptionKeepTheirBodyTypes() {
+        RouteInfo route = checkout();
+        NodeInfo log = route.nodes.get(2);
+        log.bodyTypes = List.of("java.lang.String", "java.util.LinkedHashMap");
+        log.bodyKinds = List.of("payload", "data");
+
+        RouteErrorFrame.Split split = RouteErrorFrame.split(route);
+
+        NodeInfo copy = split.blocks().get(0).nodes.get(1);
+        assertThat(copy.id).isEqualTo("log1");
+        assertThat(copy.bodyTypes).containsExactly("java.lang.String", 
"java.util.LinkedHashMap");
+        assertThat(copy.bodyKinds).containsExactly("payload", "data");
+    }
+
     @Test
     void theLinesSayWhereTheFailuresGoAndComeFrom() {
         assertThat(RouteErrorFrame.lines("checkout", PATHS, LineStyle.UNICODE))
diff --git 
a/docs/user-manual/modules/ROOT/images/jbang/camel-tui-diagram-body-types.png 
b/docs/user-manual/modules/ROOT/images/jbang/camel-tui-diagram-body-types.png
new file mode 100644
index 000000000000..0b3807880a4a
Binary files /dev/null and 
b/docs/user-manual/modules/ROOT/images/jbang/camel-tui-diagram-body-types.png 
differ
diff --git 
a/docs/user-manual/modules/ROOT/images/jbang/camel-tui-routes-body-types.png 
b/docs/user-manual/modules/ROOT/images/jbang/camel-tui-routes-body-types.png
new file mode 100644
index 000000000000..d4f882ffd2b5
Binary files /dev/null and 
b/docs/user-manual/modules/ROOT/images/jbang/camel-tui-routes-body-types.png 
differ
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 e0ae315405d3..33693e0a9107 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
@@ -16,6 +16,7 @@ See xref:camel-jbang-tui.adoc[Camel TUI] for getting started 
and the other pages
   topology and in a route, and in a box of its own in the architecture
 * <<_external_endpoints,*External endpoints*>> -- what the integration 
consumes from and sends to outside
 * <<_metrics_and_source_code,*Metrics and source*>> -- throughput and timing 
on the boxes, and the source a box comes from
+* <<_body_types,*Body types*>> -- the types of the bodies each step has been 
reached with, on the arrows into the steps
 
 == Route Topology Diagram
 
@@ -84,8 +85,8 @@ the summary once it is saved. *b* switches every level 
between the *business* vi
 labels and what capabilities achieve (the AI-assisted parts marked ✦), or 
route ids with their
 endpoints and the entry points and systems of each capability. The business 
view is the default when
 the routes have descriptions, from their source or the AI project overview; 
*n*, the key of the former
-description toggle, does the same. *m* metrics and *e* external systems apply 
to the topology, *m* and
-*d* detail to a route. *g* group, in the topology, shows the architecture one 
level down: each route
+description toggle, does the same. *m* metrics and *e* external systems apply 
to the topology, *m*,
+*y* body types and *d* detail to a route. *g* group, in the topology, shows 
the architecture one level down: each route
 box names its group (a route group, an AI capability marked ✦, shared services 
or utility), each group
 has the border colour of its box in the architecture, and a legend at the 
bottom of the topology lists
 the groups in their colours; it is on when the project has groups. *u* shows 
or hides the utility
@@ -181,6 +182,41 @@ Press *e* to cycle through three modes for external 
endpoints:
 * Press *c* to view the source code of the selected route, with syntax 
highlighting
 * Press *b* (or *n*) to switch between the business view (labels) and the 
technical view (route ids and endpoints)
 
+=== Body Types
+
+When the integration runs with the message history on, such as with `camel run 
--dev`, Camel keeps the body types
+each step has been reached with, across the messages (see
+xref:camel-jbang-diagnostics.adoc#_body_types[Body types] for the same in the 
Camel CLI). In a route, press *y* to show
+them beside the arrow into each step, so the arrows show how the body changes 
along the route:
+
+[source,text]
+----
+           ┌──────────────────────┐
+           │   unmarshal[json]    │
+           └──────────────────────┘
+                      │
+                      │ ⚠ byte[] | LinkedHashMap (payload and data)
+                    3 ▼ 12
+           ┌────────────────────⚠─┐
+           │   to[file:outbox]    │
+           └──────────────────────┘
+----
+
+A step reached with more than one type is a part of the route that is dynamic. 
When the types are of more than one
+kind (`null`, `payload`, `data` or `collection`), the step needs attention, as 
its body depends on the path a message
+takes: its box has a ⚠ in its top border, whether the types are shown or not. 
Here some messages reach the file
+endpoint with parsed data that was never marshalled.
+
+image::jbang/camel-tui-diagram-body-types.png[The orders route with its body 
types on the arrows: only one branch of the choice marshals, so the step after 
it is reached with a stream and a map, and its Info panel says the body there 
depends on the path a message takes]
+
+The arrows of a branch are side by side, so the types beside them are cut to 
the room there is: the first ones and how
+many more (`ByteArrayInputStreamCache +2`). The Info panel of the selected 
step lists them all with their kind. In the
+topology, the Info panel of a route says how many of its steps were reached 
with more than one type
+(`⚠ 1 mixed · 2 varied`), and the Routes tab has the same in its BODY TYPES 
column, with the types of each step in the
+table of the processors of the selected route.
+
+image::jbang/camel-tui-routes-body-types.png[The Routes tab: the orders route 
has four steps reached with bodies of more than one kind, and its processors 
list the types of each step]
+
 == Keyboard Shortcuts
 
 === Diagram Tab
@@ -194,6 +230,7 @@ Press *e* to cycle through three modes for external 
endpoints:
 | *Esc* | Go back (previous route or topology)
 | *t* | Jump to topology view
 | *m* | Toggle metrics overlay
+| *y* | In a route: show the body types each step has been reached with, 
beside the arrows into the steps
 | *e* | External systems: in the topology off, edges, all; in the architecture 
off or edges
 | *a* | AI-assisted parts shown, or the facts alone; without a 
`camel-summary.md` yet, has the AI write one
 | *b* (or *n*) | Business view (labels, what capabilities achieve) or 
technical view (route ids, endpoints)
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AbstractDiagramTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AbstractDiagramTab.java
index f60cd28aa4ff..7cd960f2cdc2 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AbstractDiagramTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AbstractDiagramTab.java
@@ -31,7 +31,8 @@ import dev.tamboui.tui.event.KeyEvent;
  * and Esc walks back along the routes followed, then to the topology; t jumps 
back to the topology</li>
  * <li>d shows the detail panel of the selected node (PgUp and PgDn scroll 
it), g goes to a node by name, c shows the
  * source of the selected node or route</li>
- * <li>m shows the metrics, e the external systems of the topology</li>
+ * <li>m shows the metrics, e the external systems of the topology, y the body 
types each step of a route has been
+ * reached with</li>
  * </ul>
  * A tab offers a key to {@link #handleGotoNodeKeys(KeyEvent)}, then to its 
own keys and those of the source, and to
  * {@link #handleDiagramKeys(KeyEvent)}; Esc to {@link #handleDiagramEscape()}.
@@ -156,6 +157,13 @@ abstract class AbstractDiagramTab extends AbstractTab {
             reloadDiagram();
             return true;
         }
+        if (!topologyMode && ke.isCharIgnoreCase('y')) {
+            // the body types each step has been reached with, beside the 
arrows into the steps (CAMEL-25750)
+            diagram.setShowBodyTypes(!diagram.isShowBodyTypes());
+            diagram.endLoad();
+            reloadDiagram();
+            return true;
+        }
         if (topologyMode && ke.isCharIgnoreCase('e')) {
             // external systems: off, edges, all
             externalMode = (externalMode + 1) % 3;
@@ -254,8 +262,9 @@ abstract class AbstractDiagramTab extends AbstractTab {
         }
     }
 
+    /** The diagram again with the numbers and the body types of the status 
file, while they are shown. */
     void refreshDiagramIfNeeded() {
-        if (diagram.isShowDiagram() && diagramMetrics) {
+        if (diagram.isShowDiagram() && (diagramMetrics || 
diagram.isShowBodyTypes())) {
             reloadDiagram();
         }
     }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/BodyTypes.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/BodyTypes.java
new file mode 100644
index 000000000000..47034b59dbdd
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/BodyTypes.java
@@ -0,0 +1,122 @@
+/*
+ * 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.StringJoiner;
+
+import dev.tamboui.style.Style;
+import dev.tamboui.text.CharWidth;
+
+/**
+ * The body types a step has been reached with, across the messages, as words 
for the views (CAMEL-25750): their simple
+ * class names, and their kinds (null, payload, data or collection) when there 
is more than one kind, which is what
+ * needs attention.
+ */
+public final class BodyTypes {
+
+    /** The mark of a step reached with bodies of more than one kind. */
+    public static final String MARK = TuiIcons.WARN;
+
+    private BodyTypes() {
+    }
+
+    /** Whether the bodies were of more than one kind: a step that needs 
attention. */
+    public static boolean mixed(List<String> kinds) {
+        return kinds != null && kinds.size() > 1;
+    }
+
+    /** The simple class name of a type: LinkedHashMap, byte[]. */
+    public static String simpleName(String type) {
+        return type.substring(type.lastIndexOf('.') + 1);
+    }
+
+    /** The types by their simple class names: String | LinkedHashMap. */
+    public static String names(List<String> types) {
+        StringJoiner sj = new StringJoiner(" | ");
+        for (String type : types) {
+            sj.add(simpleName(type));
+        }
+        return sj.toString();
+    }
+
+    /** The kinds in words: payload and data, or null, payload and data. */
+    public static String kinds(List<String> kinds) {
+        if (kinds.size() < 2) {
+            return String.join("", kinds);
+        }
+        return String.join(", ", kinds.subList(0, kinds.size() - 1)) + " and " 
+ kinds.get(kinds.size() - 1);
+    }
+
+    /**
+     * The types, with a warning sign and the kinds when there is more than 
one kind: "⚠ String | LinkedHashMap (payload
+     * and data)". Empty when the step has not been reached.
+     */
+    public static String label(List<String> types, List<String> kinds) {
+        if (types == null || types.isEmpty()) {
+            return "";
+        }
+        return mixed(kinds) ? MARK + " " + names(types) + " (" + kinds(kinds) 
+ ")" : names(types);
+    }
+
+    /**
+     * The types, with a warning sign when there is more than one kind, for a 
table where the room is short: "⚠ String |
+     * LinkedHashMap". Empty when the step has not been reached.
+     */
+    public static String shortLabel(List<String> types, List<String> kinds) {
+        if (types == null || types.isEmpty()) {
+            return "";
+        }
+        return mixed(kinds) ? MARK + " " + names(types) : names(types);
+    }
+
+    /**
+     * The types in at most the width, the most that fits of: all of them with 
their kinds, all of them, the first ones
+     * and how many more ("⚠ byte[] +1"), the first one cut and how many more. 
A view that cuts them shows them all
+     * elsewhere (the diagram in its Info panel).
+     */
+    public static String fit(List<String> types, List<String> kinds, int 
width) {
+        if (types == null || types.isEmpty() || width <= 0) {
+            return "";
+        }
+        String all = label(types, kinds);
+        if (CharWidth.of(all) <= width) {
+            return all;
+        }
+        String mark = mixed(kinds) ? MARK + " " : "";
+        for (int n = types.size(); n >= 1; n--) {
+            String more = n < types.size() ? " +" + (types.size() - n) : "";
+            String text = mark + names(types.subList(0, n)) + more;
+            if (CharWidth.of(text) <= width) {
+                return text;
+            }
+        }
+        String more = types.size() > 1 ? " +" + (types.size() - 1) : "";
+        int room = width - CharWidth.of(mark) - CharWidth.of(more);
+        if (room < 2) {
+            // no room for a name: the mark alone says the step needs attention
+            return mark.isEmpty() ? "" : MARK;
+        }
+        return mark + CharWidth.truncateWithEllipsis(simpleName(types.get(0)), 
room, "…", CharWidth.TruncatePosition.END)
+               + more;
+    }
+
+    /** The style of the types: a warning for more than one kind, muted 
otherwise. */
+    public static Style style(List<String> kinds) {
+        return mixed(kinds) ? Theme.warning() : Theme.muted();
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramDetailSupport.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramDetailSupport.java
index 4fed814873fd..60d3358369a8 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramDetailSupport.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramDetailSupport.java
@@ -35,6 +35,7 @@ import dev.tamboui.widgets.block.BorderType;
 import dev.tamboui.widgets.block.Borders;
 import dev.tamboui.widgets.paragraph.Paragraph;
 import org.apache.camel.catalog.CamelCatalog;
+import org.apache.camel.diagram.RouteDiagramLayoutEngine;
 import org.apache.camel.dsl.jbang.core.commands.tui.diagram.DiagramColors;
 import org.apache.camel.util.TimeUtils;
 import org.apache.camel.util.json.JsonObject;
@@ -295,6 +296,11 @@ final class DiagramDetailSupport {
                     Span.styled(stateLabel, stateStyle)));
             addGroup(lines, info, route, diagram.isShowErrorPaths());
             addDescription(lines, info, route, area.width() - 3);
+            if (route.variedBodyTypes > 0) {
+                // the steps reached with more than one body type 
(CAMEL-25750): the route diagram shows which, with y
+                lines.add(Line.from(Span.styled(" Body:  ", Theme.muted()), 
Span.styled(RoutesTab.bodyTypesLabel(route),
+                        route.mixedBodyKinds > 0 ? Theme.warning() : 
Style.EMPTY.fg(Theme.baseFg()))));
+            }
 
             lines.add(Line.from(Span.raw("")));
             lines.add(Line.from(
@@ -372,50 +378,7 @@ final class DiagramDetailSupport {
             }
 
         } else {
-            // External endpoint — show topology node data
-            var topoNode = diagram.getSelectedTopologyNode();
-            if (topoNode != null) {
-                boolean isInbound = "external-in".equals(topoNode.nodeType);
-                boolean isBridge = "external".equals(topoNode.nodeType);
-                String label = isBridge ? " External" : isInbound ? " Inbound" 
: " Outbound";
-                lines.add(Line.from(
-                        Span.styled(label, 
Style.EMPTY.fg(Theme.accent()).bold())));
-                lines.add(Line.from(Span.raw("")));
-                lines.add(Line.from(
-                        Span.styled(" URI: ", Theme.muted()),
-                        Span.raw(topoNode.from != null ? topoNode.from : "")));
-                if (topoNode.description != null && 
!topoNode.description.isBlank()) {
-                    lines.add(Line.from(
-                            Span.styled(" Path: ", Theme.muted()),
-                            Span.raw(topoNode.description)));
-                }
-                if (!isBridge) {
-                    String connectedRoute = 
diagram.getConnectedRouteId(routeId);
-                    if (connectedRoute != null) {
-                        lines.add(Line.from(Span.raw("")));
-                        lines.add(Line.from(
-                                Span.styled(isInbound ? " To route: " : " From 
route: ", Theme.muted()),
-                                Span.styled(connectedRoute, 
Style.EMPTY.fg(Theme.baseFg()))));
-                    }
-                }
-                if (topoNode.exchangesTotal > 0 || topoNode.exchangesFailed > 
0) {
-                    lines.add(Line.from(Span.raw("")));
-                    lines.add(Line.from(
-                            Span.styled(" Total:  ", Theme.muted()),
-                            
Span.raw(String.valueOf(topoNode.exchangesTotal))));
-                    if (topoNode.exchangesFailed > 0) {
-                        lines.add(Line.from(
-                                Span.styled(" Failed: ", Theme.muted()),
-                                
Span.styled(String.valueOf(topoNode.exchangesFailed),
-                                        Theme.error().bold())));
-                    }
-                }
-            } else {
-                lines.add(Line.from(
-                        Span.styled(" " + routeId, 
Style.EMPTY.fg(Theme.accent()).bold())));
-                lines.add(Line.from(
-                        Span.styled(" (external endpoint)", 
Style.EMPTY.dim())));
-            }
+            addExternalEndpoint(lines, routeId);
         }
 
         Paragraph paragraph = Paragraph.builder()
@@ -426,6 +389,56 @@ final class DiagramDetailSupport {
         frame.renderWidget(paragraph, area);
     }
 
+    /**
+     * A system outside the integration, selected in the topology: where it 
is, the route it is linked to, its counts.
+     */
+    private void addExternalEndpoint(List<Line> lines, String routeId) {
+        // External endpoint — show topology node data
+        var topoNode = diagram.getSelectedTopologyNode();
+        if (topoNode != null) {
+            boolean isInbound = "external-in".equals(topoNode.nodeType);
+            boolean isBridge = "external".equals(topoNode.nodeType);
+            String label = isBridge ? " External" : isInbound ? " Inbound" : " 
Outbound";
+            lines.add(Line.from(
+                    Span.styled(label, 
Style.EMPTY.fg(Theme.accent()).bold())));
+            lines.add(Line.from(Span.raw("")));
+            lines.add(Line.from(
+                    Span.styled(" URI: ", Theme.muted()),
+                    Span.raw(topoNode.from != null ? topoNode.from : "")));
+            if (topoNode.description != null && 
!topoNode.description.isBlank()) {
+                lines.add(Line.from(
+                        Span.styled(" Path: ", Theme.muted()),
+                        Span.raw(topoNode.description)));
+            }
+            if (!isBridge) {
+                String connectedRoute = diagram.getConnectedRouteId(routeId);
+                if (connectedRoute != null) {
+                    lines.add(Line.from(Span.raw("")));
+                    lines.add(Line.from(
+                            Span.styled(isInbound ? " To route: " : " From 
route: ", Theme.muted()),
+                            Span.styled(connectedRoute, 
Style.EMPTY.fg(Theme.baseFg()))));
+                }
+            }
+            if (topoNode.exchangesTotal > 0 || topoNode.exchangesFailed > 0) {
+                lines.add(Line.from(Span.raw("")));
+                lines.add(Line.from(
+                        Span.styled(" Total:  ", Theme.muted()),
+                        Span.raw(String.valueOf(topoNode.exchangesTotal))));
+                if (topoNode.exchangesFailed > 0) {
+                    lines.add(Line.from(
+                            Span.styled(" Failed: ", Theme.muted()),
+                            
Span.styled(String.valueOf(topoNode.exchangesFailed),
+                                    Theme.error().bold())));
+                }
+            }
+        } else {
+            lines.add(Line.from(
+                    Span.styled(" " + routeId, 
Style.EMPTY.fg(Theme.accent()).bold())));
+            lines.add(Line.from(
+                    Span.styled(" (external endpoint)", Style.EMPTY.dim())));
+        }
+    }
+
     void renderEipInfoPanel(Frame frame, Rect area, String drillDownRouteId) {
         List<Line> lines = new ArrayList<>();
         var selected = diagram.getSelectedEipNodeBox();
@@ -497,82 +510,11 @@ final class DiagramDetailSupport {
                         Span.styled(arrow, Theme.muted())));
             }
 
+            if (ln.treeNode != null) {
+                addBodyTypes(lines, ln.treeNode.info, area.width() - 3);
+            }
             if (ln.treeNode != null && ln.treeNode.info.stat != null) {
-                var stat = ln.treeNode.info.stat;
-                lines.add(Line.from(Span.raw("")));
-                int w = numWidth(stat.exchangesTotal, stat.exchangesFailed, 
stat.exchangesInflight);
-                lines.add(Line.from(
-                        Span.styled(" Total:    ", Theme.muted()),
-                        Span.raw(String.format("%" + w + "d", 
stat.exchangesTotal))));
-                Style failStyle = stat.exchangesFailed > 0
-                        ? Theme.error().bold() : Style.EMPTY;
-                lines.add(Line.from(
-                        Span.styled(" Failed:   ", Theme.muted()),
-                        Span.styled(String.format("%" + w + "d", 
stat.exchangesFailed), failStyle)));
-                lines.add(Line.from(
-                        Span.styled(" Inflight: ", Theme.muted()),
-                        Span.raw(String.format("%" + w + "d", 
stat.exchangesInflight))));
-                if (stat.exchangesThroughput != null && 
!stat.exchangesThroughput.isEmpty()) {
-                    String tpUnit = ctx.ratePerMinute ? " msg/m" : " msg/s";
-                    String tpValue = ctx.ratePerMinute
-                            ? 
TuiHelper.throughputPerMinute(stat.exchangesThroughput)
-                            : stat.exchangesThroughput;
-                    lines.add(Line.from(
-                            Span.styled(" Rate:     ", Theme.muted()),
-                            Span.raw(tpValue),
-                            Span.styled(tpUnit, Theme.muted())));
-                }
-
-                if (stat.exchangesTotal > 0) {
-                    lines.add(Line.from(Span.raw("")));
-                    lines.add(Line.from(
-                            Span.styled(" Mean: ", Theme.muted()),
-                            
Span.raw(formatDurationMs(stat.meanProcessingTime))));
-                    lines.add(Line.from(
-                            Span.styled(" Max:  ", Theme.muted()),
-                            
Span.raw(formatDurationMs(stat.maxProcessingTime))));
-                    lines.add(Line.from(
-                            Span.styled(" Min:  ", Theme.muted()),
-                            
Span.raw(formatDurationMs(stat.minProcessingTime))));
-                    lines.add(Line.from(
-                            Span.styled(" Last: ", Theme.muted()),
-                            
Span.raw(formatDurationMs(stat.lastProcessingTime))));
-                    if (stat.p50ProcessingTime >= 0) {
-                        lines.add(Line.from(Span.raw("")));
-                        lines.add(Line.from(
-                                Span.styled(" p50:  ", Theme.muted()),
-                                
Span.raw(formatDurationMs(stat.p50ProcessingTime))));
-                        lines.add(Line.from(
-                                Span.styled(" p95:  ", Theme.muted()),
-                                
Span.raw(formatDurationMs(stat.p95ProcessingTime))));
-                        lines.add(Line.from(
-                                Span.styled(" p99:  ", Theme.muted()),
-                                
Span.raw(formatDurationMs(stat.p99ProcessingTime))));
-                        lines.add(buildPercentileBarLine(
-                                stat.p50ProcessingTime, stat.p95ProcessingTime,
-                                stat.p99ProcessingTime, area.width() - 3));
-                    }
-
-                    if (stat.lastCompletedExchangeTimestamp > 0 || 
stat.lastFailedExchangeTimestamp > 0) {
-                        long now = System.currentTimeMillis();
-                        lines.add(Line.from(Span.raw("")));
-                        lines.add(Line.from(
-                                Span.styled(" Since last:", Theme.muted())));
-                        if (stat.lastCompletedExchangeTimestamp > 0) {
-                            long ago = now - 
stat.lastCompletedExchangeTimestamp;
-                            lines.add(Line.from(
-                                    Span.styled("   ok:   ", Theme.muted()),
-                                    Span.raw(TimeUtils.printDuration(ago, 
false))));
-                        }
-                        if (stat.lastFailedExchangeTimestamp > 0) {
-                            long ago = now - stat.lastFailedExchangeTimestamp;
-                            lines.add(Line.from(
-                                    Span.styled("   fail: ", Theme.muted()),
-                                    Span.styled(TimeUtils.printDuration(ago, 
false),
-                                            Theme.error())));
-                        }
-                    }
-                }
+                addStepStatistics(lines, ln.treeNode.info.stat, area.width() - 
3);
             }
         } else {
             lines.add(Line.from(Span.styled(" (no node selected)", 
Style.EMPTY.dim())));
@@ -586,6 +528,111 @@ final class DiagramDetailSupport {
         frame.renderWidget(paragraph, area);
     }
 
+    /**
+     * The body types the step has been reached with, across the messages 
(CAMEL-25750), one per line, with their kind;
+     * a step reached with bodies of more than one kind needs attention. The 
diagram shows them cut, and with y only.
+     */
+    static void addBodyTypes(List<Line> lines, 
RouteDiagramLayoutEngine.NodeInfo info, int width) {
+        if (info.bodyTypes.isEmpty()) {
+            return;
+        }
+        boolean mixed = BodyTypes.mixed(info.bodyKinds);
+        lines.add(Line.from(Span.raw("")));
+        lines.add(Line.from(Span.styled(
+                mixed || info.bodyKinds.isEmpty() ? " Body types:" : " Body 
types (" + info.bodyKinds.get(0) + "):",
+                Theme.muted())));
+        for (String type : info.bodyTypes) {
+            lines.add(Line.from(Span.raw("   " + 
truncate(BodyTypes.simpleName(type), Math.max(4, width - 2)))));
+        }
+        if (mixed) {
+            List<String> warning = wrapWords(
+                    BodyTypes.kinds(info.bodyKinds) + ": the body here depends 
on the path a message takes",
+                    Math.max(10, width - 2));
+            for (int i = 0; i < warning.size(); i++) {
+                lines.add(Line.from(Span.styled((i == 0 ? " " + BodyTypes.MARK 
+ " " : "   ") + warning.get(i),
+                        Theme.warning())));
+            }
+        }
+    }
+
+    /** The statistics of a step: its counts, its rate, its times, and how 
long since its last message. */
+    private void addStepStatistics(List<Line> lines, 
RouteDiagramLayoutEngine.StatInfo stat, int width) {
+        lines.add(Line.from(Span.raw("")));
+        int w = numWidth(stat.exchangesTotal, stat.exchangesFailed, 
stat.exchangesInflight);
+        lines.add(Line.from(
+                Span.styled(" Total:    ", Theme.muted()),
+                Span.raw(String.format("%" + w + "d", stat.exchangesTotal))));
+        Style failStyle = stat.exchangesFailed > 0
+                ? Theme.error().bold() : Style.EMPTY;
+        lines.add(Line.from(
+                Span.styled(" Failed:   ", Theme.muted()),
+                Span.styled(String.format("%" + w + "d", 
stat.exchangesFailed), failStyle)));
+        lines.add(Line.from(
+                Span.styled(" Inflight: ", Theme.muted()),
+                Span.raw(String.format("%" + w + "d", 
stat.exchangesInflight))));
+        if (stat.exchangesThroughput != null && 
!stat.exchangesThroughput.isEmpty()) {
+            String tpUnit = ctx.ratePerMinute ? " msg/m" : " msg/s";
+            String tpValue = ctx.ratePerMinute
+                    ? TuiHelper.throughputPerMinute(stat.exchangesThroughput)
+                    : stat.exchangesThroughput;
+            lines.add(Line.from(
+                    Span.styled(" Rate:     ", Theme.muted()),
+                    Span.raw(tpValue),
+                    Span.styled(tpUnit, Theme.muted())));
+        }
+
+        if (stat.exchangesTotal > 0) {
+            lines.add(Line.from(Span.raw("")));
+            lines.add(Line.from(
+                    Span.styled(" Mean: ", Theme.muted()),
+                    Span.raw(formatDurationMs(stat.meanProcessingTime))));
+            lines.add(Line.from(
+                    Span.styled(" Max:  ", Theme.muted()),
+                    Span.raw(formatDurationMs(stat.maxProcessingTime))));
+            lines.add(Line.from(
+                    Span.styled(" Min:  ", Theme.muted()),
+                    Span.raw(formatDurationMs(stat.minProcessingTime))));
+            lines.add(Line.from(
+                    Span.styled(" Last: ", Theme.muted()),
+                    Span.raw(formatDurationMs(stat.lastProcessingTime))));
+            if (stat.p50ProcessingTime >= 0) {
+                lines.add(Line.from(Span.raw("")));
+                lines.add(Line.from(
+                        Span.styled(" p50:  ", Theme.muted()),
+                        Span.raw(formatDurationMs(stat.p50ProcessingTime))));
+                lines.add(Line.from(
+                        Span.styled(" p95:  ", Theme.muted()),
+                        Span.raw(formatDurationMs(stat.p95ProcessingTime))));
+                lines.add(Line.from(
+                        Span.styled(" p99:  ", Theme.muted()),
+                        Span.raw(formatDurationMs(stat.p99ProcessingTime))));
+                lines.add(buildPercentileBarLine(
+                        stat.p50ProcessingTime, stat.p95ProcessingTime,
+                        stat.p99ProcessingTime, width));
+            }
+
+            if (stat.lastCompletedExchangeTimestamp > 0 || 
stat.lastFailedExchangeTimestamp > 0) {
+                long now = System.currentTimeMillis();
+                lines.add(Line.from(Span.raw("")));
+                lines.add(Line.from(
+                        Span.styled(" Since last:", Theme.muted())));
+                if (stat.lastCompletedExchangeTimestamp > 0) {
+                    long ago = now - stat.lastCompletedExchangeTimestamp;
+                    lines.add(Line.from(
+                            Span.styled("   ok:   ", Theme.muted()),
+                            Span.raw(TimeUtils.printDuration(ago, false))));
+                }
+                if (stat.lastFailedExchangeTimestamp > 0) {
+                    long ago = now - stat.lastFailedExchangeTimestamp;
+                    lines.add(Line.from(
+                            Span.styled("   fail: ", Theme.muted()),
+                            Span.styled(TimeUtils.printDuration(ago, false),
+                                    Theme.error())));
+                }
+            }
+        }
+    }
+
     void renderDetail(Frame frame, Rect area, IntegrationInfo info, String 
drillDownRouteId) {
         lastDetailArea = area;
         var selected = diagram.getSelectedEipNodeBox();
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramLayouts.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramLayouts.java
new file mode 100644
index 000000000000..a8042a40efda
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramLayouts.java
@@ -0,0 +1,95 @@
+/*
+ * 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.Set;
+
+import org.apache.camel.diagram.RouteDiagramLayoutEngine;
+import org.apache.camel.diagram.TopologyLayoutEngine.TopologyLayoutNode;
+import org.apache.camel.diagram.TopologyLayoutEngine.TopologyLayoutResult;
+
+/**
+ * What {@link DiagramSupport} does to a layout before it draws it: the 
diagram moved up to the top, and the steps a
+ * highlighted step is inside highlighted too.
+ */
+final class DiagramLayouts {
+
+    private DiagramLayouts() {
+    }
+
+    static void addParentNodes(List<RouteDiagramLayoutEngine.NodeInfo> nodes, 
Set<String> nodeIds) {
+        for (int i = 0; i < nodes.size(); i++) {
+            RouteDiagramLayoutEngine.NodeInfo node = nodes.get(i);
+            if (node.id == null || nodeIds.contains(node.id)) {
+                continue;
+            }
+            boolean hasHighlightedChild = false;
+            for (int j = i + 1; j < nodes.size(); j++) {
+                RouteDiagramLayoutEngine.NodeInfo child = nodes.get(j);
+                if (child.level <= node.level) {
+                    break;
+                }
+                if (child.id != null && nodeIds.contains(child.id)) {
+                    hasHighlightedChild = true;
+                    break;
+                }
+            }
+            if (hasHighlightedChild) {
+                nodeIds.add(node.id);
+            }
+        }
+    }
+
+    static void normalizeTopologyLayoutY(TopologyLayoutResult result) {
+        if (result.nodes.isEmpty()) {
+            return;
+        }
+        int minY = Integer.MAX_VALUE;
+        for (TopologyLayoutNode node : result.nodes) {
+            minY = Math.min(minY, node.y);
+        }
+        if (minY <= 0) {
+            return;
+        }
+        int shift = minY - 40;
+        for (TopologyLayoutNode node : result.nodes) {
+            node.y -= shift;
+        }
+    }
+
+    static void normalizeRouteLayoutY(RouteDiagramLayoutEngine.LayoutRoute lr) 
{
+        int minY = Integer.MAX_VALUE;
+        for (RouteDiagramLayoutEngine.LayoutNode ln : lr.nodes) {
+            if (!"route".equals(ln.type)) {
+                minY = Math.min(minY, ln.y);
+            }
+        }
+        if (minY <= 0 || minY == Integer.MAX_VALUE) {
+            return;
+        }
+        int shift = minY - 40;
+        for (RouteDiagramLayoutEngine.LayoutNode ln : lr.nodes) {
+            ln.y -= shift;
+            if (ln.connectFromMerge) {
+                ln.mergeY -= shift;
+            }
+        }
+        lr.labelY -= shift;
+        lr.maxY -= shift;
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramSupport.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramSupport.java
index 01d70c13f353..7866de503162 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramSupport.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramSupport.java
@@ -29,6 +29,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Supplier;
 
 import dev.tamboui.layout.Constraint;
 import dev.tamboui.layout.Layout;
@@ -63,10 +64,14 @@ import 
org.apache.camel.diagram.TopologyLayoutEngine.TopologyLayoutNode;
 import org.apache.camel.diagram.TopologyLayoutEngine.TopologyLayoutResult;
 import org.apache.camel.diagram.TopologyLayoutEngine.TopologyNodeInfo;
 import org.apache.camel.dsl.jbang.core.commands.ai.IntegrationSummary;
+import org.apache.camel.dsl.jbang.core.commands.tui.diagram.RouteDiagramWidget;
 import 
org.apache.camel.dsl.jbang.core.commands.tui.diagram.TopologyDiagramWidget;
 import org.apache.camel.util.json.JsonArray;
 import org.apache.camel.util.json.JsonObject;
 
+import static 
org.apache.camel.dsl.jbang.core.commands.tui.DiagramLayouts.addParentNodes;
+import static 
org.apache.camel.dsl.jbang.core.commands.tui.DiagramLayouts.normalizeRouteLayoutY;
+import static 
org.apache.camel.dsl.jbang.core.commands.tui.DiagramLayouts.normalizeTopologyLayoutY;
 import static org.apache.camel.dsl.jbang.core.commands.tui.TuiHelper.*;
 import static org.apache.camel.dsl.jbang.core.commands.tui.TuiHelper.hint;
 
@@ -80,6 +85,8 @@ class DiagramSupport {
     private boolean showDiagram;
     private boolean topologyMode;
     private boolean showDescription;
+    /** The body types each step has been reached with, beside the arrows of 
the route diagram (CAMEL-25750). */
+    private boolean showBodyTypes;
     private Path aiSourceDirectory;
     /** The AI labels of the decision points of the routes shown, by their 
node (CAMEL-25161). */
     private volatile Map<String, IntegrationSummary.StepLabel> stepLabels = 
Map.of();
@@ -168,6 +175,14 @@ class DiagramSupport {
         this.showDescription = showDescription;
     }
 
+    boolean isShowBodyTypes() {
+        return showBodyTypes;
+    }
+
+    void setShowBodyTypes(boolean showBodyTypes) {
+        this.showBodyTypes = showBodyTypes;
+    }
+
     List<TopologyAsciiRenderer.NodeBox> getNodeBoxes() {
         return nodeBoxes;
     }
@@ -1195,11 +1210,11 @@ class DiagramSupport {
 
     // ---- Route EIP node selection ----
 
-    private 
List<org.apache.camel.dsl.jbang.core.commands.tui.diagram.RouteDiagramWidget.EipNodeBox>
 eipNodeBoxes
+    private List<RouteDiagramWidget.EipNodeBox> eipNodeBoxes
             = Collections.emptyList();
     private int selectedEipNodeIndex = -1;
 
-    
List<org.apache.camel.dsl.jbang.core.commands.tui.diagram.RouteDiagramWidget.EipNodeBox>
 getEipNodeBoxes() {
+    List<RouteDiagramWidget.EipNodeBox> getEipNodeBoxes() {
         return eipNodeBoxes;
     }
 
@@ -1215,7 +1230,7 @@ class DiagramSupport {
         selectedEipNodeIndex = 0;
     }
 
-    
org.apache.camel.dsl.jbang.core.commands.tui.diagram.RouteDiagramWidget.EipNodeBox
 getSelectedEipNodeBox() {
+    RouteDiagramWidget.EipNodeBox getSelectedEipNodeBox() {
         if (selectedEipNodeIndex >= 0 && selectedEipNodeIndex < 
eipNodeBoxes.size()) {
             return eipNodeBoxes.get(selectedEipNodeIndex);
         }
@@ -1532,11 +1547,14 @@ class DiagramSupport {
         Map<String, String> linkable = 
computeLinkableEndpoints(currentRouteId);
         Map<String, String> routeDescs = showDescription ? 
computeRouteDescriptions() : Collections.emptyMap();
 
-        var widget = new 
org.apache.camel.dsl.jbang.core.commands.tui.diagram.RouteDiagramWidget(
+        // built again once the scroll is clamped to its size
+        Supplier<RouteDiagramWidget> widgets = () -> new RouteDiagramWidget(
                 routeLayout, nw, selectedEipNodeIndex, scrollX, scrollY, 
metrics, linkable,
                 showDescription, routeDescs)
                 .withErrorFrame(layoutShowErrorPaths ? 
routeErrorFrames.get(currentRouteId) : null)
-                .withErrorMark(errorMarkRouteIds().contains(currentRouteId));
+                .withErrorMark(errorMarkRouteIds().contains(currentRouteId))
+                .withBodyTypes(showBodyTypes).withBodyKindMarks(true);
+        var widget = widgets.get();
 
         int totalRows = widget.getTotalRows();
         int totalCols = widget.getTotalCols();
@@ -1550,11 +1568,7 @@ class DiagramSupport {
         scrollY = Math.min(scrollY, maxVScroll);
         scrollX = Math.min(scrollX, maxHScroll);
 
-        var finalWidget = new 
org.apache.camel.dsl.jbang.core.commands.tui.diagram.RouteDiagramWidget(
-                routeLayout, nw, selectedEipNodeIndex, scrollX, scrollY, 
metrics, linkable,
-                showDescription, routeDescs)
-                .withErrorFrame(layoutShowErrorPaths ? 
routeErrorFrames.get(currentRouteId) : null)
-                .withErrorMark(errorMarkRouteIds().contains(currentRouteId));
+        var finalWidget = widgets.get();
 
         List<Rect> vChunks = Layout.vertical()
                 .constraints(Constraint.fill(), Constraint.length(1))
@@ -2105,7 +2119,7 @@ class DiagramSupport {
         Map<String, String> linkable = computeLinkableEndpoints(routeId);
         Map<String, String> routeDescs = showDescription ? 
computeRouteDescriptions() : Collections.emptyMap();
 
-        var widget = new 
org.apache.camel.dsl.jbang.core.commands.tui.diagram.RouteDiagramWidget(
+        var widget = new RouteDiagramWidget(
                 routeLayout, nw, selIdx, scrollX, scrollY, false, linkable,
                 showDescription, routeDescs, activeHighlight, historyFailed);
 
@@ -2136,7 +2150,7 @@ class DiagramSupport {
             }
         }
 
-        var finalWidget = new 
org.apache.camel.dsl.jbang.core.commands.tui.diagram.RouteDiagramWidget(
+        var finalWidget = new RouteDiagramWidget(
                 routeLayout, nw, selIdx, scrollX, scrollY, false, linkable,
                 showDescription, routeDescs, activeHighlight, historyFailed);
 
@@ -2353,67 +2367,6 @@ class DiagramSupport {
         });
     }
 
-    static void addParentNodes(List<RouteDiagramLayoutEngine.NodeInfo> nodes, 
Set<String> nodeIds) {
-        for (int i = 0; i < nodes.size(); i++) {
-            RouteDiagramLayoutEngine.NodeInfo node = nodes.get(i);
-            if (node.id == null || nodeIds.contains(node.id)) {
-                continue;
-            }
-            boolean hasHighlightedChild = false;
-            for (int j = i + 1; j < nodes.size(); j++) {
-                RouteDiagramLayoutEngine.NodeInfo child = nodes.get(j);
-                if (child.level <= node.level) {
-                    break;
-                }
-                if (child.id != null && nodeIds.contains(child.id)) {
-                    hasHighlightedChild = true;
-                    break;
-                }
-            }
-            if (hasHighlightedChild) {
-                nodeIds.add(node.id);
-            }
-        }
-    }
-
-    private static void normalizeTopologyLayoutY(TopologyLayoutResult result) {
-        if (result.nodes.isEmpty()) {
-            return;
-        }
-        int minY = Integer.MAX_VALUE;
-        for (TopologyLayoutNode node : result.nodes) {
-            minY = Math.min(minY, node.y);
-        }
-        if (minY <= 0) {
-            return;
-        }
-        int shift = minY - 40;
-        for (TopologyLayoutNode node : result.nodes) {
-            node.y -= shift;
-        }
-    }
-
-    private static void 
normalizeRouteLayoutY(RouteDiagramLayoutEngine.LayoutRoute lr) {
-        int minY = Integer.MAX_VALUE;
-        for (RouteDiagramLayoutEngine.LayoutNode ln : lr.nodes) {
-            if (!"route".equals(ln.type)) {
-                minY = Math.min(minY, ln.y);
-            }
-        }
-        if (minY <= 0 || minY == Integer.MAX_VALUE) {
-            return;
-        }
-        int shift = minY - 40;
-        for (RouteDiagramLayoutEngine.LayoutNode ln : lr.nodes) {
-            ln.y -= shift;
-            if (ln.connectFromMerge) {
-                ln.mergeY -= shift;
-            }
-        }
-        lr.labelY -= shift;
-        lr.maxY -= shift;
-    }
-
     /**
      * Finds the index of the EIP node box whose source line is closest to the 
given line, preferring the closest node
      * at or before the line. Returns -1 when there are no boxes with line 
information.
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTab.java
index e20050189142..3856d58299f3 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTab.java
@@ -512,6 +512,7 @@ class DiagramTab extends AbstractDiagramTab {
             }
             case ROUTE -> List.of(view, new SubViewBar.Toggle("x", "errors", 
errorsToggle()),
                     new SubViewBar.Toggle("m", "metrics", metrics),
+                    new SubViewBar.Toggle("y", "types", 
diagram.isShowBodyTypes() ? "on" : "off"),
                     new SubViewBar.Toggle("d", "detail", detailMode ? "on" : 
"off"));
         };
     }
@@ -822,6 +823,12 @@ class DiagramTab extends AbstractDiagramTab {
                 if (linkedRoute != null) {
                     node.put("linkedRoute", linkedRoute);
                 }
+                var treeNode = eipBox.layoutNode().treeNode;
+                if (treeNode != null && !treeNode.info.bodyTypes.isEmpty()) {
+                    // the body types the step has been reached with; more 
than one kind is a step that needs attention
+                    node.put("bodyTypes", new 
JsonArray(treeNode.info.bodyTypes));
+                    node.put("bodyKinds", new 
JsonArray(treeNode.info.bodyKinds));
+                }
                 result.put("selectedNode", node);
             }
         }
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 e556d6b4bb09..c564f37a957d 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
@@ -164,7 +164,8 @@ final class DiagramTopology {
 
     /**
      * The statistics of each line of the route's code: the first two lines 
(the route and its from) have the route's,
-     * the others those of their step.
+     * the others those of their step, with the body types the step has been 
reached with so far (CAMEL-25750): the
+     * structure has those it was asked with.
      */
     private static void addStepStatistics(JsonObject route, RouteInfo info) {
         Collection<?> code = route.getCollection("code");
@@ -172,10 +173,14 @@ final class DiagramTopology {
             return;
         }
         Map<String, Map<String, ?>> steps = new HashMap<>();
+        Map<String, ProcessorInfo> bodies = new HashMap<>();
         for (ProcessorInfo p : info.processors) {
             if (p.id != null && p.statistics != null) {
                 steps.put(p.id, p.statistics);
             }
+            if (p.id != null && !p.bodyTypes.isEmpty()) {
+                bodies.put(p.id, p);
+            }
         }
         JsonArray lines = new JsonArray();
         int index = 0;
@@ -190,6 +195,11 @@ final class DiagramTopology {
                 if (statistics != null) {
                     line.put("statistics", statistics instanceof JsonObject jo 
? jo : new JsonObject(statistics));
                 }
+                ProcessorInfo body = index > 1 ? 
bodies.get(line.getString("id")) : null;
+                if (body != null) {
+                    line.put("bodyTypes", new JsonArray(body.bodyTypes));
+                    line.put("bodyKinds", new JsonArray(body.bodyKinds));
+                }
                 lines.add(line);
             } else {
                 lines.add(o);
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProcessorInfo.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProcessorInfo.java
index 38763a71ae0e..d193eb03c797 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProcessorInfo.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProcessorInfo.java
@@ -16,6 +16,7 @@
  */
 package org.apache.camel.dsl.jbang.core.commands.tui;
 
+import java.util.List;
 import java.util.Map;
 
 class ProcessorInfo {
@@ -43,4 +44,8 @@ class ProcessorInfo {
     String uri;
     /** The statistics as the status file has them, for the route diagram 
(DiagramTopology). */
     Map<String, ?> statistics;
+    /** The body types the step has been reached with, across the messages, in 
the order first seen (CAMEL-25750). */
+    List<String> bodyTypes = List.of();
+    /** The kinds of those bodies: null, payload, data or collection. More 
than one needs attention. */
+    List<String> bodyKinds = List.of();
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteInfo.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteInfo.java
index 0cea43e82b58..cbb595c5f1a0 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteInfo.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteInfo.java
@@ -51,5 +51,9 @@ class RouteInfo {
     String sinceLastFailed;
     /** The statistics as the status file has them, for the route diagram 
(DiagramTopology). */
     Map<String, ?> statistics;
+    /** The steps reached with more than one body type, across the messages 
(CAMEL-25750). */
+    int variedBodyTypes;
+    /** The steps reached with bodies of more than one kind, which need 
attention (CAMEL-25750). */
+    int mixedBodyKinds;
     final List<ProcessorInfo> processors = new ArrayList<>();
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTab.java
index 15bcaa96969a..b08eb9a78d8b 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTab.java
@@ -49,6 +49,9 @@ import static 
org.apache.camel.dsl.jbang.core.commands.tui.TuiHelper.*;
 
 class RoutesTab extends AbstractDiagramTab {
 
+    /** The widest the body types of a step are in the processors table: the 
info panel of the diagram has them all. */
+    private static final int MAX_BODY_TYPES_WIDTH = 40;
+
     private static final String[] ROUTE_SORT_COLUMNS = { "name", "from", 
"status", "total", "failed" };
     private static final String[] ROUTE_TOP_SORT_COLUMNS = { "mean", "max", 
"min", "last", "delta", "p50", "p95", "p99" };
 
@@ -535,8 +538,18 @@ class RoutesTab extends AbstractDiagramTab {
             totalColWidth = Math.max(totalColWidth, tw + since + 1);
             failColWidth = Math.max(failColWidth, fw + failSince + 1);
         }
+        // the steps reached with more than one body type (CAMEL-25750): a 
column only when a route has some
+        boolean bodyTypes = sortedRoutes.stream().anyMatch(r -> 
r.variedBodyTypes > 0);
+        int bodyColWidth = 0;
+        if (bodyTypes) {
+            bodyColWidth = CharWidth.of("BODY TYPES");
+            for (RouteInfo route : sortedRoutes) {
+                bodyColWidth = Math.max(bodyColWidth, 
CharWidth.of(bodyTypesLabel(route)));
+            }
+        }
         // FROM gets the rest of the width: the borders, ">> ", 7 gaps and the 
other columns
-        int fromWidth = Math.max(0, areaWidth - 2 - 3 - 7 - (24 + 10 + 10 + 
totalColWidth + failColWidth + 20 + 12));
+        int fromWidth = Math.max(0, areaWidth - 2 - 3 - 7 - (24 + 10 + 10 + 
totalColWidth + failColWidth + 20 + 12)
+                                    - (bodyTypes ? bodyColWidth + 1 : 0));
 
         List<Row> routeRows = new ArrayList<>();
         Map<String, String> aiDescriptions = aiDescriptions();
@@ -569,15 +582,21 @@ class RoutesTab extends AbstractDiagramTab {
                             Span.styled(" (" + route.sinceLastFailed + ")", 
Theme.muted()))
                     : Line.from(Span.styled(String.format("%" + fw + "d", 
route.failed), failStyle));
 
-            routeRows.add(Row.from(
-                    Cell.from(Span.styled(TuiHelper.truncate(route.routeId, 
24), Style.EMPTY.fg(Theme.accent()))),
-                    routeFromCell(route, aiDescriptions, fromWidth),
-                    Cell.from(Span.styled(route.state != null ? route.state : 
"", stateStyle)),
-                    rightCell(formatThroughput(route.throughput), 8),
-                    Cell.from(totalCell),
-                    Cell.from(failCell),
-                    rightCell(timingCol, 20),
-                    Cell.from(buildPercentileBarLine(route.p50Time, 
route.p95Time, route.p99Time, 10))));
+            List<Cell> cells = new ArrayList<>(
+                    List.of(
+                            
Cell.from(Span.styled(TuiHelper.truncate(route.routeId, 24), 
Style.EMPTY.fg(Theme.accent()))),
+                            routeFromCell(route, aiDescriptions, fromWidth),
+                            Cell.from(Span.styled(route.state != null ? 
route.state : "", stateStyle)),
+                            rightCell(formatThroughput(route.throughput), 8),
+                            Cell.from(totalCell),
+                            Cell.from(failCell)));
+            if (bodyTypes) {
+                cells.add(Cell.from(Span.styled(bodyTypesLabel(route),
+                        route.mixedBodyKinds > 0 ? Theme.warning() : 
Theme.muted())));
+            }
+            cells.add(rightCell(timingCol, 20));
+            cells.add(Cell.from(buildPercentileBarLine(route.p50Time, 
route.p95Time, route.p99Time, 10)));
+            routeRows.add(Row.from(cells));
         }
 
         IntegrationInfo selDef = ctx.findSelectedIntegration();
@@ -591,40 +610,54 @@ class RoutesTab extends AbstractDiagramTab {
                 totalTimingCol = formatDurationMs(selDef.minTime) + "/" + 
formatDurationMs(selDef.maxTime) + "/"
                                  + formatDurationMs(selDef.meanTime);
             }
-            routeRows.add(Row.from(
-                    Cell.from(Span.styled("GLOBAL", ts)),
-                    Cell.from(""),
-                    Cell.from(""),
-                    rightCell(formatThroughput(selDef.throughput), 8, ts),
-                    Cell.from(Span.styled(String.format("%" + tw + "d", 
selDef.exchangesTotal), ts)),
-                    Cell.from(Span.styled(String.format("%" + fw + "d", 
selDef.failed),
-                            selDef.failed > 0 ? Theme.error().bold() : ts)),
-                    rightCell(totalTimingCol, 20, ts),
-                    Cell.from(buildPercentileBarLine(selDef.p50Time, 
selDef.p95Time, selDef.p99Time, 10))));
+            List<Cell> cells = new ArrayList<>(
+                    List.of(
+                            Cell.from(Span.styled("GLOBAL", ts)),
+                            Cell.from(""),
+                            Cell.from(""),
+                            rightCell(formatThroughput(selDef.throughput), 8, 
ts),
+                            Cell.from(Span.styled(String.format("%" + tw + 
"d", selDef.exchangesTotal), ts)),
+                            Cell.from(Span.styled(String.format("%" + fw + 
"d", selDef.failed),
+                                    selDef.failed > 0 ? Theme.error().bold() : 
ts))));
+            if (bodyTypes) {
+                cells.add(Cell.from(""));
+            }
+            cells.add(rightCell(totalTimingCol, 20, ts));
+            cells.add(Cell.from(buildPercentileBarLine(selDef.p50Time, 
selDef.p95Time, selDef.p99Time, 10)));
+            routeRows.add(Row.from(cells));
         }
 
         String timingHeader = hasPercentiles ? "P50/P95/P99" : "MIN/MAX/MEAN";
 
-        return Table.builder()
-                .rows(routeRows)
-                .header(Row.from(
+        List<Cell> header = new ArrayList<>(
+                List.of(
                         Cell.from(Span.styled(routeSortLabel("ROUTE", "name"), 
routeSortStyle("name"))),
                         Cell.from(Span.styled(routeSortLabel("FROM", "from"), 
routeSortStyle("from"))),
                         Cell.from(Span.styled(routeSortLabel("STATUS", 
"status"), routeSortStyle("status"))),
                         rightCell(ctx.ratePerMinute ? "MSG/M" : "MSG/S", 8, 
Style.EMPTY.bold()),
                         centerCell(routeSortLabel("TOTAL", "total"), 14, 
routeSortStyle("total")),
-                        centerCell(routeSortLabel("FAIL", "failed"), 14, 
routeSortStyle("failed")),
-                        rightCell(timingHeader, 20, Style.EMPTY.bold()),
-                        Cell.from("")))
-                .widths(
+                        centerCell(routeSortLabel("FAIL", "failed"), 14, 
routeSortStyle("failed"))));
+        List<Constraint> widths = new ArrayList<>(
+                List.of(
                         Constraint.length(24),
                         Constraint.fill(),
                         Constraint.length(10),
                         Constraint.length(10),
                         Constraint.length(totalColWidth),
-                        Constraint.length(failColWidth),
-                        Constraint.length(20),
-                        Constraint.length(12))
+                        Constraint.length(failColWidth)));
+        if (bodyTypes) {
+            header.add(Cell.from(Span.styled("BODY TYPES", 
Style.EMPTY.bold())));
+            widths.add(Constraint.length(bodyColWidth));
+        }
+        header.add(rightCell(timingHeader, 20, Style.EMPTY.bold()));
+        header.add(Cell.from(""));
+        widths.add(Constraint.length(20));
+        widths.add(Constraint.length(12));
+
+        return Table.builder()
+                .rows(routeRows)
+                .header(Row.from(header))
+                .widths(widths.toArray(Constraint[]::new))
                 .highlightStyle(Theme.selectionBg())
                 .highlightSpacing(Table.HighlightSpacing.ALWAYS)
                 
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
@@ -632,6 +665,21 @@ class RoutesTab extends AbstractDiagramTab {
                 .build();
     }
 
+    /**
+     * The steps of a route reached with more than one body type, across the 
messages (CAMEL-25750): "⚠ 1 mixed · 2
+     * varied" when some are reached with bodies of more than one kind, which 
need attention; empty when none varied.
+     */
+    static String bodyTypesLabel(RouteInfo route) {
+        if (route.variedBodyTypes <= 0) {
+            return "";
+        }
+        if (route.mixedBodyKinds <= 0) {
+            return route.variedBodyTypes + " varied";
+        }
+        return TuiIcons.WARN + " " + route.mixedBodyKinds + " mixed"
+               + (route.variedBodyTypes > route.mixedBodyKinds ? " · " + 
route.variedBodyTypes + " varied" : "");
+    }
+
     @Override
     public SubViewBar.Spec subViewBar() {
         if (ctx.findSelectedIntegration() == null || sourceViewer.isVisible()) 
{
@@ -654,6 +702,7 @@ class RoutesTab extends AbstractDiagramTab {
                         "e", "external",
                         EXTERNAL_LABELS[externalMode].trim().replace("[", 
"").replace("]", "")));
             } else if (!diagram.getEipNodeBoxes().isEmpty()) {
+                toggles.add(new SubViewBar.Toggle("y", "types", 
diagram.isShowBodyTypes() ? "on" : "off"));
                 toggles.add(new SubViewBar.Toggle("d", "detail", detailMode ? 
"on" : "off"));
             }
             toggles.add(new SubViewBar.Toggle("n", "description", 
diagram.isShowDescription() ? "on" : "off"));
@@ -880,98 +929,139 @@ class RoutesTab extends AbstractDiagramTab {
                             .build())
                     .build();
         } else {
-            boolean hasProcPercentiles = route.p50Time >= 0
-                    || route.processors.stream().anyMatch(p -> p.p50Time >= 0);
-
-            List<Row> rows = new ArrayList<>();
+            table = buildProcessorTable(area, route);
+        }
 
-            // Synthetic top row representing the route itself
-            Style routeStyle = route.failed > 0 ? Theme.error() : 
Style.EMPTY.fg(Theme.accent());
-            String routeTimingCol;
-            if (hasProcPercentiles && route.p50Time >= 0) {
-                routeTimingCol = formatDurationMs(route.p50Time) + "/" + 
formatDurationMs(route.p95Time) + "/"
-                                 + formatDurationMs(route.p99Time);
-            } else if (route.total > 0) {
-                routeTimingCol = formatDurationMs(route.minTime) + "/" + 
formatDurationMs(route.maxTime) + "/"
-                                 + formatDurationMs(route.meanTime);
-            } else {
-                routeTimingCol = "";
-            }
-            // PROCESSOR gets the rest of the width: the borders, ">> ", 7 
gaps and the other columns; a long URI
-            // ends with an ellipsis where it is cut
-            int procWidth = Math.max(0, area.width() - 2 - 3 - 7 - (20 + 10 + 
8 + 6 + 8 + 20 + 12));
-            rows.add(Row.from(
-                    Cell.from("   route"),
-                    Cell.from(Span.styled(TuiHelper.truncate(
-                            route.from != null ? 
TuiHelper.displayUri(route.from) : route.routeId, procWidth),
-                            routeStyle)),
-                    rightCell(formatThroughput(route.throughput), 8),
-                    rightCell(String.valueOf(route.total), 8),
-                    rightCell(String.valueOf(route.failed), 6,
-                            route.failed > 0 ? Theme.error() : Style.EMPTY),
-                    rightCell(String.valueOf(route.inflight), 8),
-                    rightCell(routeTimingCol, 20),
-                    Cell.from(buildPercentileBarLine(route.p50Time, 
route.p95Time, route.p99Time, 10))));
+        frame.renderStatefulWidget(table, area, processorTableState);
+        int processorRowCount = routeTopMode ? route.processors.size() : 
route.processors.size() + 1;
+        renderTableScrollbar(frame, area, table, processorTableState, 
processorTableScrollState,
+                processorRowCount);
+    }
 
+    /**
+     * The processors of a route in the default mode (counters, timing), with 
the body types each step has been reached
+     * with, when the message history recorded them (CAMEL-25750).
+     */
+    private Table buildProcessorTable(Rect area, RouteInfo route) {
+        boolean hasProcPercentiles = route.p50Time >= 0
+                || route.processors.stream().anyMatch(p -> p.p50Time >= 0);
+        // a column only when a step has body types, as wide as the longest up 
to a limit: the info panel of the
+        // diagram has them all; a space before them sets them apart from the 
inflight count
+        boolean bodyTypes = route.processors.stream().anyMatch(p -> 
!p.bodyTypes.isEmpty());
+        int bodyColWidth = 0;
+        if (bodyTypes) {
+            bodyColWidth = CharWidth.of("BODY TYPES");
             for (ProcessorInfo proc : route.processors) {
-                String indent = "  ".repeat(proc.level);
-                Style nameStyle = proc.failed > 0 ? Theme.error() : 
Style.EMPTY.fg(Theme.accent());
+                bodyColWidth = Math.max(bodyColWidth, 
CharWidth.of(BodyTypes.shortLabel(proc.bodyTypes, proc.bodyKinds)));
+            }
+            bodyColWidth = Math.min(bodyColWidth, MAX_BODY_TYPES_WIDTH) + 1;
+        }
 
-                String procTimingCol;
-                if (hasProcPercentiles && proc.p50Time >= 0) {
-                    procTimingCol = formatDurationMs(proc.p50Time) + "/" + 
formatDurationMs(proc.p95Time) + "/"
-                                    + formatDurationMs(proc.p99Time);
-                } else if (proc.total > 0) {
-                    procTimingCol = formatDurationMs(proc.minTime) + "/" + 
formatDurationMs(proc.maxTime) + "/"
-                                    + formatDurationMs(proc.meanTime);
-                } else {
-                    procTimingCol = "";
-                }
+        List<Row> rows = new ArrayList<>();
+
+        // Synthetic top row representing the route itself
+        Style routeStyle = route.failed > 0 ? Theme.error() : 
Style.EMPTY.fg(Theme.accent());
+        String routeTimingCol;
+        if (hasProcPercentiles && route.p50Time >= 0) {
+            routeTimingCol = formatDurationMs(route.p50Time) + "/" + 
formatDurationMs(route.p95Time) + "/"
+                             + formatDurationMs(route.p99Time);
+        } else if (route.total > 0) {
+            routeTimingCol = formatDurationMs(route.minTime) + "/" + 
formatDurationMs(route.maxTime) + "/"
+                             + formatDurationMs(route.meanTime);
+        } else {
+            routeTimingCol = "";
+        }
+        // PROCESSOR gets the rest of the width: the borders, ">> ", 7 gaps 
and the other columns; a long URI
+        // ends with an ellipsis where it is cut
+        int procWidth = Math.max(0, area.width() - 2 - 3 - 7 - (20 + 10 + 8 + 
6 + 8 + 20 + 12)
+                                    - (bodyTypes ? bodyColWidth + 1 : 0));
+        List<Cell> routeCells = new ArrayList<>(
+                List.of(
+                        Cell.from("   route"),
+                        Cell.from(Span.styled(TuiHelper.truncate(
+                                route.from != null ? 
TuiHelper.displayUri(route.from) : route.routeId, procWidth),
+                                routeStyle)),
+                        rightCell(formatThroughput(route.throughput), 8),
+                        rightCell(String.valueOf(route.total), 8),
+                        rightCell(String.valueOf(route.failed), 6,
+                                route.failed > 0 ? Theme.error() : 
Style.EMPTY),
+                        rightCell(String.valueOf(route.inflight), 8)));
+        if (bodyTypes) {
+            routeCells.add(Cell.from(""));
+        }
+        routeCells.add(rightCell(routeTimingCol, 20));
+        routeCells.add(Cell.from(buildPercentileBarLine(route.p50Time, 
route.p95Time, route.p99Time, 10)));
+        rows.add(Row.from(routeCells));
+
+        for (ProcessorInfo proc : route.processors) {
+            String indent = "  ".repeat(proc.level);
+            Style nameStyle = proc.failed > 0 ? Theme.error() : 
Style.EMPTY.fg(Theme.accent());
+
+            String procTimingCol;
+            if (hasProcPercentiles && proc.p50Time >= 0) {
+                procTimingCol = formatDurationMs(proc.p50Time) + "/" + 
formatDurationMs(proc.p95Time) + "/"
+                                + formatDurationMs(proc.p99Time);
+            } else if (proc.total > 0) {
+                procTimingCol = formatDurationMs(proc.minTime) + "/" + 
formatDurationMs(proc.maxTime) + "/"
+                                + formatDurationMs(proc.meanTime);
+            } else {
+                procTimingCol = "";
+            }
 
-                rows.add(Row.from(
-                        Cell.from("   " + (proc.processor != null ? 
proc.processor : "")),
-                        Cell.from(Span.styled(TuiHelper.truncate(indent + 
(proc.id != null ? proc.id : ""), procWidth),
-                                nameStyle)),
-                        rightCell(formatThroughput(proc.throughput), 8),
-                        rightCell(String.valueOf(proc.total), 8),
-                        rightCell(String.valueOf(proc.failed), 6,
-                                proc.failed > 0 ? Theme.error() : Style.EMPTY),
-                        rightCell(String.valueOf(proc.inflight), 8),
-                        rightCell(procTimingCol, 20),
-                        Cell.from(buildPercentileBarLine(proc.p50Time, 
proc.p95Time, proc.p99Time, 10))));
+            List<Cell> cells = new ArrayList<>(
+                    List.of(
+                            Cell.from("   " + (proc.processor != null ? 
proc.processor : "")),
+                            Cell.from(Span.styled(TuiHelper.truncate(indent + 
(proc.id != null ? proc.id : ""), procWidth),
+                                    nameStyle)),
+                            rightCell(formatThroughput(proc.throughput), 8),
+                            rightCell(String.valueOf(proc.total), 8),
+                            rightCell(String.valueOf(proc.failed), 6,
+                                    proc.failed > 0 ? Theme.error() : 
Style.EMPTY),
+                            rightCell(String.valueOf(proc.inflight), 8)));
+            if (bodyTypes) {
+                cells.add(Cell.from(Span.styled(
+                        " " + 
TuiHelper.truncate(BodyTypes.shortLabel(proc.bodyTypes, proc.bodyKinds), 
bodyColWidth - 1),
+                        BodyTypes.style(proc.bodyKinds))));
             }
-            String procTimingHeader = hasProcPercentiles ? "P50/P95/P99" : 
"MIN/MAX/MEAN";
+            cells.add(rightCell(procTimingCol, 20));
+            cells.add(Cell.from(buildPercentileBarLine(proc.p50Time, 
proc.p95Time, proc.p99Time, 10)));
+            rows.add(Row.from(cells));
+        }
+        String procTimingHeader = hasProcPercentiles ? "P50/P95/P99" : 
"MIN/MAX/MEAN";
 
-            table = Table.builder()
-                    .rows(rows)
-                    .header(Row.from(
-                            Cell.from(Span.styled("   TYPE", 
Style.EMPTY.bold())),
-                            Cell.from(Span.styled("PROCESSOR", 
Style.EMPTY.bold())),
-                            rightCell(ctx.ratePerMinute ? "MSG/M" : "MSG/S", 
8, Style.EMPTY.bold()),
-                            rightCell("TOTAL", 8, Style.EMPTY.bold()),
-                            rightCell("FAIL", 6, Style.EMPTY.bold()),
-                            rightCell("INFLIGHT", 8, Style.EMPTY.bold()),
-                            rightCell(procTimingHeader, 20, 
Style.EMPTY.bold()),
-                            Cell.from("")))
-                    .widths(
-                            Constraint.length(20),
-                            Constraint.fill(),
-                            Constraint.length(10),
-                            Constraint.length(8),
-                            Constraint.length(6),
-                            Constraint.length(8),
-                            Constraint.length(20),
-                            Constraint.length(12))
-                    
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
-                            .title(" Processors [" + route.routeId + "] ")
-                            .build())
-                    .build();
+        List<Cell> header = new ArrayList<>(
+                List.of(
+                        Cell.from(Span.styled("   TYPE", Style.EMPTY.bold())),
+                        Cell.from(Span.styled("PROCESSOR", 
Style.EMPTY.bold())),
+                        rightCell(ctx.ratePerMinute ? "MSG/M" : "MSG/S", 8, 
Style.EMPTY.bold()),
+                        rightCell("TOTAL", 8, Style.EMPTY.bold()),
+                        rightCell("FAIL", 6, Style.EMPTY.bold()),
+                        rightCell("INFLIGHT", 8, Style.EMPTY.bold())));
+        List<Constraint> widths = new ArrayList<>(
+                List.of(
+                        Constraint.length(20),
+                        Constraint.fill(),
+                        Constraint.length(10),
+                        Constraint.length(8),
+                        Constraint.length(6),
+                        Constraint.length(8)));
+        if (bodyTypes) {
+            header.add(Cell.from(Span.styled(" BODY TYPES", 
Style.EMPTY.bold())));
+            widths.add(Constraint.length(bodyColWidth));
         }
+        header.add(rightCell(procTimingHeader, 20, Style.EMPTY.bold()));
+        header.add(Cell.from(""));
+        widths.add(Constraint.length(20));
+        widths.add(Constraint.length(12));
 
-        frame.renderStatefulWidget(table, area, processorTableState);
-        int processorRowCount = routeTopMode ? route.processors.size() : 
route.processors.size() + 1;
-        renderTableScrollbar(frame, area, table, processorTableState, 
processorTableScrollState,
-                processorRowCount);
+        return Table.builder()
+                .rows(rows)
+                .header(Row.from(header))
+                .widths(widths.toArray(Constraint[]::new))
+                
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+                        .title(" Processors [" + route.routeId + "] ")
+                        .build())
+                .build();
     }
 
     private String formatThroughput(String throughput) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParser.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParser.java
index bd714eeee0fd..e523c2bd131f 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParser.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParser.java
@@ -244,7 +244,7 @@ final class StatusParser {
     /**
      * Routes and their processors with statistics.
      */
-    private static void parseRoutes(IntegrationInfo info, JsonObject root) {
+    static void parseRoutes(IntegrationInfo info, JsonObject root) {
         JsonArray routes = (JsonArray) root.get("routes");
         if (routes != null) {
             for (Object r : routes) {
@@ -258,6 +258,9 @@ final class StatusParser {
                 ri.state = rj.getString("state");
                 ri.supportsSuspension = 
rj.getBooleanOrDefault("supportsSuspension", false);
                 ri.uptime = rj.getString("uptime");
+                // the steps reached with more than one body type, and with 
more than one kind (CAMEL-25750)
+                ri.variedBodyTypes = rj.getIntegerOrDefault("variedBodyTypes", 
0);
+                ri.mixedBodyKinds = rj.getIntegerOrDefault("mixedBodyKinds", 
0);
 
                 Map<String, ?> rs = rj.getMap("statistics");
                 ri.statistics = rs;
@@ -303,6 +306,9 @@ final class StatusParser {
                         pi.source = pj.getString("source");
                         pi.level = pj.getIntegerOrDefault("level", 0);
                         pi.uri = pj.getString("uri");
+                        // the body types the step has been reached with, and 
their kinds (CAMEL-25750)
+                        pi.bodyTypes = stringList(pj.get("bodyTypes"));
+                        pi.bodyKinds = stringList(pj.get("bodyKinds"));
 
                         Map<String, ?> ps = pj.getMap("statistics");
                         pi.statistics = ps;
@@ -1579,6 +1585,18 @@ final class StatusParser {
         return TuiHelper.objToLong(o);
     }
 
+    /** The strings of a JSON array, or an empty list. */
+    static List<String> stringList(Object o) {
+        if (!(o instanceof List<?> list) || list.isEmpty()) {
+            return List.of();
+        }
+        List<String> answer = new ArrayList<>(list.size());
+        for (Object item : list) {
+            answer.add(String.valueOf(item));
+        }
+        return answer;
+    }
+
     private static void parseVaultSecrets(
             JsonObject vaults, String key, String vaultName, IntegrationInfo 
info, boolean hasRegion) {
         JsonObject vo = (JsonObject) vaults.get(key);
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/diagram/RouteDiagramWidget.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/diagram/RouteDiagramWidget.java
index ab4efc2867b5..b3b3d01a6611 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/diagram/RouteDiagramWidget.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/diagram/RouteDiagramWidget.java
@@ -18,6 +18,7 @@ package org.apache.camel.dsl.jbang.core.commands.tui.diagram;
 
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -36,6 +37,7 @@ import 
org.apache.camel.diagram.RouteDiagramLayoutEngine.LayoutRoute;
 import org.apache.camel.diagram.RouteDiagramLayoutEngine.StatInfo;
 import org.apache.camel.diagram.RouteDiagramLayoutEngine.TreeNode;
 import org.apache.camel.diagram.RouteErrorFrame;
+import org.apache.camel.dsl.jbang.core.commands.tui.BodyTypes;
 import org.apache.camel.dsl.jbang.core.commands.tui.BusinessEndpointLabels;
 import org.apache.camel.dsl.jbang.core.commands.tui.Theme;
 
@@ -50,6 +52,10 @@ public class RouteDiagramWidget implements Widget {
     private static final int MIN_BOX_WIDTH = 16;
     private static final int X_DIVISOR = 15;
     private static final int MAX_WRAP_LINES = 3;
+    /** The widest the body types beside an arrow are: the Info panel has them 
all. */
+    private static final int MAX_BODY_TYPES_WIDTH = 48;
+    /** The room a count at the head of an arrow takes before the body types, 
at most. */
+    private static final int COUNT_WIDTH = 10;
 
     // Dashed scope box characters
     private static final char SCOPE_H = '╌';
@@ -151,6 +157,8 @@ public class RouteDiagramWidget implements Widget {
     @Override
     public void render(Rect area, Buffer buffer) {
         nodeBoxes.clear();
+        arrowCells.clear();
+        arrowEnds.clear();
 
         if (errorFrame != null) {
             drawErrorFrame(buffer, area);
@@ -189,6 +197,10 @@ public class RouteDiagramWidget implements Widget {
                 drawNode(buffer, area, ln);
             }
         }
+        // the body types beside the arrows, where the boxes, the arrows and 
the markers leave room for them
+        for (ArrowEnd end : arrowEnds) {
+            drawBodyTypes(buffer, area, end);
+        }
         // the markers beside the boxes last, so a long one stops at a box 
instead of drawing over it
         for (Marker m : markers) {
             writeMarker(buffer, area, m);
@@ -261,6 +273,94 @@ public class RouteDiagramWidget implements Widget {
         return this;
     }
 
+    private boolean bodyTypes;
+    private boolean bodyKindMarks;
+    /** The cells of the arrows and their counts, which the body types beside 
an arrow stop at. */
+    private final Set<Long> arrowCells = new HashSet<>();
+    private final List<ArrowEnd> arrowEnds = new ArrayList<>();
+
+    /** Where an arrow goes into a step, for the body types beside it: its 
columns and rows, the width of its count. */
+    private record ArrowEnd(LayoutNode to, int fromCx, int fromRow, int toCx, 
int toTop, int countWidth) {
+    }
+
+    /**
+     * The body types each step has been reached with, across the messages 
(CAMEL-25750), beside the arrow into it: as
+     * much of them as there is room for, as the arrows of a branch are side 
by side; the Info panel has them all.
+     */
+    public RouteDiagramWidget withBodyTypes(boolean show) {
+        this.bodyTypes = show;
+        return this;
+    }
+
+    /**
+     * A ⚠ on the top border of a step reached with bodies of more than one 
kind (CAMEL-25750), which needs attention,
+     * whether the body types are shown or not.
+     */
+    public RouteDiagramWidget withBodyKindMarks(boolean marks) {
+        this.bodyKindMarks = marks;
+        return this;
+    }
+
+    private void addArrowEnd(LayoutNode to, int fromCx, int fromRow, int toCx, 
int toTop, int countWidth) {
+        if (bodyTypes && to.treeNode != null && 
!to.treeNode.info.bodyTypes.isEmpty()) {
+            arrowEnds.add(new ArrowEnd(to, fromCx, fromRow, toCx, toTop, 
countWidth));
+        }
+    }
+
+    /**
+     * The body types beside the arrow into a step: on its shaft above the 
head, or after the count at the head where
+     * the arrow bends right above it (a branch); cut to the room before the 
next box, arrow or marker.
+     */
+    private void drawBodyTypes(Buffer buffer, Rect area, ArrowEnd end) {
+        int shaftTop = end.fromCx() == end.toCx() ? end.fromRow() : 
end.fromRow() + (end.toTop() - end.fromRow()) / 2 + 1;
+        int row = end.toTop() - 2;
+        int col = end.toCx() + 2;
+        if (row < shaftTop) {
+            row = end.toTop() - 1;
+            col += end.countWidth() > 0 ? end.countWidth() + 2 : 0;
+        }
+        var info = end.to().treeNode.info;
+        String text = BodyTypes.fit(info.bodyTypes, info.bodyKinds, room(row, 
col));
+        if (!text.isEmpty()) {
+            writeText(buffer, area, row, col, text, 
BodyTypes.style(info.bodyKinds));
+        }
+    }
+
+    /**
+     * The columns free on the row from the column, two left blank before a 
box, an arrow or a marker so the types do
+     * not read as part of it.
+     */
+    private int room(int row, int col) {
+        for (int c = col; c < col + MAX_BODY_TYPES_WIDTH; c++) {
+            if (arrowCells.contains(cell(row, c)) || inBox(row, c) || 
markerAt(row, c)) {
+                return Math.max(0, c - col - 2);
+            }
+        }
+        return MAX_BODY_TYPES_WIDTH;
+    }
+
+    private boolean markerAt(int row, int col) {
+        for (Marker m : markers) {
+            if (m.row() == row && col >= m.col() && col < m.col() + 
CharWidth.of(m.text())) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private static long cell(int row, int col) {
+        return ((long) row << 32) | (col & 0xffffffffL);
+    }
+
+    /** A cell of an arrow or of its count: the body types beside another 
arrow stop there. */
+    private void occupy(int row, int col, int width) {
+        if (bodyTypes) {
+            for (int c = col; c < col + width; c++) {
+                arrowCells.add(cell(row, c));
+            }
+        }
+    }
+
     /** The mark on the top border of the from box, at its right end (the 
route has error handling). */
     private void drawErrorMark(Buffer buffer, Rect area) {
         for (LayoutNode ln : layoutRoute.nodes) {
@@ -317,6 +417,15 @@ public class RouteDiagramWidget implements Widget {
                 cols = Math.max(cols, CharWidth.of(line) + 6);
             }
         }
+        if (bodyTypes) {
+            // the body types beside the arrows can be scrolled to: as wide as 
they are before something cuts them
+            for (LayoutNode ln : layoutRoute.nodes) {
+                if (!ln.tableRow && ln.treeNode != null && 
!ln.treeNode.info.bodyTypes.isEmpty()) {
+                    int label = 
CharWidth.of(BodyTypes.label(ln.treeNode.info.bodyTypes, 
ln.treeNode.info.bodyKinds));
+                    cols = Math.max(cols, centerCol(ln) + 2 + COUNT_WIDTH + 
Math.min(MAX_BODY_TYPES_WIDTH, label) + 2);
+                }
+            }
+        }
         return cols;
     }
 
@@ -388,6 +497,11 @@ public class RouteDiagramWidget implements Widget {
             }
         }
 
+        if (bodyKindMarks && node.treeNode != null && 
BodyTypes.mixed(node.treeNode.info.bodyKinds)) {
+            // bodies of more than one kind reach the step: it needs attention
+            writeText(buffer, area, row, col + boxWidth - 3, BodyTypes.MARK, 
Theme.warning());
+        }
+
         // Link indicator for nodes that connect to other routes
         String linkedRouteId = findLinkedRouteId(node);
         if (linkedRouteId != null) {
@@ -440,7 +554,7 @@ public class RouteDiagramWidget implements Widget {
                 && to.id != null && highlightNodeIds.contains(to.id);
 
         drawArrowPath(buffer, area, fromCx, fromBottom, toCx, toTop, dashed, 
external, bothHighlighted);
-        drawCounters(buffer, area, toCx, toTop, stat);
+        addArrowEnd(to, fromCx, fromBottom, toCx, toTop, drawCounters(buffer, 
area, toCx, toTop, stat));
     }
 
     private void drawMergeArrow(Buffer buffer, Rect area, LayoutNode to) {
@@ -457,7 +571,7 @@ public class RouteDiagramWidget implements Widget {
                 && to.id != null && highlightNodeIds.contains(to.id);
 
         drawArrowPath(buffer, area, fromCx, fromRow, toCx, toTop, dashed, 
false, highlighted);
-        drawCounters(buffer, area, toCx, toTop, stat);
+        addArrowEnd(to, fromCx, fromRow, toCx, toTop, drawCounters(buffer, 
area, toCx, toTop, stat));
     }
 
     private void drawArrowPath(
@@ -482,48 +596,59 @@ public class RouteDiagramWidget implements Widget {
 
         if (fromCx == toCx) {
             for (int r = fromRow; r < toRow - 1; r++) {
-                setChar(buffer, area, r, fromCx, vChar, edgeStyle);
+                arrowChar(buffer, area, r, fromCx, vChar, edgeStyle);
             }
-            setChar(buffer, area, toRow - 1, toCx, ARROW, edgeStyle);
+            arrowChar(buffer, area, toRow - 1, toCx, ARROW, edgeStyle);
         } else {
             int midRow = fromRow + (toRow - fromRow) / 2;
 
             for (int r = fromRow; r < midRow; r++) {
-                setChar(buffer, area, r, fromCx, vChar, edgeStyle);
+                arrowChar(buffer, area, r, fromCx, vChar, edgeStyle);
             }
 
             int minC = Math.min(fromCx, toCx);
             int maxC = Math.max(fromCx, toCx);
             for (int c = minC; c <= maxC; c++) {
-                setChar(buffer, area, midRow, c, hChar, edgeStyle);
+                arrowChar(buffer, area, midRow, c, hChar, edgeStyle);
             }
 
-            setChar(buffer, area, midRow, fromCx, T_UP, edgeStyle);
-            setChar(buffer, area, midRow, toCx, T_DOWN, edgeStyle);
+            arrowChar(buffer, area, midRow, fromCx, T_UP, edgeStyle);
+            arrowChar(buffer, area, midRow, toCx, T_DOWN, edgeStyle);
 
             for (int r = midRow + 1; r < toRow - 1; r++) {
-                setChar(buffer, area, r, toCx, vChar, edgeStyle);
+                arrowChar(buffer, area, r, toCx, vChar, edgeStyle);
             }
-            setChar(buffer, area, toRow - 1, toCx, ARROW, edgeStyle);
+            arrowChar(buffer, area, toRow - 1, toCx, ARROW, edgeStyle);
         }
     }
 
-    private void drawCounters(Buffer buffer, Rect area, int toCx, int toTop, 
StatInfo stat) {
+    private void arrowChar(Buffer buffer, Rect area, int row, int col, char 
ch, Style style) {
+        setChar(buffer, area, row, col, ch, style);
+        occupy(row, col, 1);
+    }
+
+    /** The counts at the head of an arrow, failed left of it and ok right of 
it; returns the width of the ok count. */
+    private int drawCounters(Buffer buffer, Rect area, int toCx, int toTop, 
StatInfo stat) {
         if (!showMetrics || stat == null) {
-            return;
+            return 0;
         }
         long total = stat.exchangesTotal;
         long failed = stat.exchangesFailed;
         long ok = total - failed;
+        int okWidth = 0;
         if (ok > 0) {
             String okStr = String.valueOf(ok);
             writeText(buffer, area, toTop - 1, toCx + 2, okStr, 
metricsOkStyle());
+            okWidth = okStr.length();
+            occupy(toTop - 1, toCx + 2, okWidth);
         }
         if (failed > 0) {
             String failStr = String.valueOf(failed);
             int col = toCx - 1 - failStr.length();
             writeText(buffer, area, toTop - 1, col, failStr, 
metricsFailStyle());
+            occupy(toTop - 1, col, failStr.length());
         }
+        return okWidth;
     }
 
     private void drawScopeBox(Buffer buffer, Rect area, LayoutNode scopeNode) {
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 fc89f6e876ed..5d10011bb852 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
@@ -81,8 +81,8 @@ state: `a` ai (off shows only what the sources and the runtime
 say: no AI labels, notes or capabilities; it does not change the AI
 overview setting; with no summary yet it has the AI write one, as
 `/overview` does), `b` view (business: labels and what capabilities achieve;
-technical: route ids and endpoints), `m` metrics, `e` external, `d`
-detail, `g` group, `u` utility (one setting for the architecture
+technical: route ids and endpoints), `m` metrics, `e` external, `y` types
+(the body types on the arrows of a route), `d` detail, `g` group, `u` utility 
(one setting for the architecture
 and the topology, so both show the same groups), `x` errors (the
 error handling in a frame below the routes; in the architecture
 a box of the routes reached only on error). The footer lists
@@ -149,6 +149,35 @@ its failures go, is in a frame below the route when `x` is 
on. The
 history view always shows the onException clauses, so a replay can
 step into them.
 
+## Body Types
+
+With the message history on (such as `camel run --dev`), Camel keeps the
+body types each step has been reached with, across the messages. In the
+route diagram, `y` shows them beside the arrow into each step, so the
+arrows show how the body changes along the route:
+
+```
+           ┌──────────────────────┐
+           │   unmarshal[json]    │
+           └──────────────────────┘
+                      │
+                      │ ⚠ byte[] | LinkedHashMap (payload and data)
+                    3 ▼ 12
+           ┌────────────────────⚠─┐
+           │   to[file:outbox]    │
+           └──────────────────────┘
+```
+
+A step reached with bodies of more than one kind (`null`, `payload`,
+`data` or `collection`) needs attention, as its body depends on the path
+a message takes: its box has a `⚠` in its top border, whether the types
+are shown or not. Where the arrows of a branch are side by side, the
+types are cut to the room there is: the first ones and how many more
+(`ByteArrayInputStreamCache +2`). The Info panel of the selected step
+lists them all with their kind, and in the topology the Info panel of a
+route says how many of its steps were reached with more than one type
+(`⚠ 1 mixed · 2 varied`).
+
 ## Route Structure Preview
 
 A compact tree structure preview appears in the bottom-right corner
@@ -199,6 +228,8 @@ you an at-a-glance view of where you are in the route.
 - `c` — show source code at selected node
 - `d` — toggle EIP detail panel (shows configured options)
 - `x` — show or hide the route's error handling, in a frame below it
+- `y` — show the body types each step has been reached with, beside the
+  arrows into the steps
 - `g` — go to node (fuzzy search popup)
 - `Esc` — go back (previous route or topology)
 - `t` — jump back to topology view
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/routes.md 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/routes.md
index 65a3fbb49da6..0be659c4e1a6 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/routes.md
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/routes.md
@@ -12,6 +12,7 @@ they are sent to. A typical integration has multiple routes 
working together.
 - **MSG/S** or **MSG/M** — Current message throughput (per second or per 
minute) for this route. Configure in Settings (F2)
 - **TOTAL** — Total exchanges processed by this route since startup
 - **FAIL** — Exchanges that ended with an unhandled error in this route
+- **BODY TYPES** — Shown when steps of a route have been reached with more 
than one body type, across the messages (with the message history on, such as 
`camel run --dev`): how many steps varied, and how many of them with bodies of 
more than one kind, which need attention (`⚠ 1 mixed · 2 varied`). The 
processors of the selected route then have a BODY TYPES column too, with the 
types each step has been reached with
 - **MIN** — Fastest exchange processing time in milliseconds. This is the time 
from when the exchange entered the route until it completed
 - **MEAN** — Average exchange processing time in milliseconds. A rising MEAN 
may indicate a downstream service getting slower
 - **MAX** — Slowest exchange processing time in milliseconds. A very high MAX 
compared to MEAN suggests occasional slow outliers
@@ -147,6 +148,7 @@ in the diagram.
 - `Esc` — go back (previous route or topology)
 - `t` — jump back to topology view
 - `m` — toggle metrics
+- `y` — toggle the body types each step has been reached with, beside the 
arrows
 - `n` — toggle description labels
 - `PgUp/PgDn` — scroll detail panel (when detail is on)
 
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AbstractDiagramTabTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AbstractDiagramTabTest.java
index 98f3e8e5e85b..1bfe1f67065b 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AbstractDiagramTabTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AbstractDiagramTabTest.java
@@ -92,6 +92,22 @@ class AbstractDiagramTabTest {
         assertThat(tab.detailMode).isFalse();
     }
 
+    @ParameterizedTest
+    @MethodSource("tabs")
+    void yShowsTheBodyTypesOnTheArrowsOfARoute(Function<MonitorContext, 
AbstractDiagramTab> factory) {
+        AbstractDiagramTab tab = inRoute(factory, false);
+
+        assertThat(tab.handleKeyEvent(KeyEvent.ofChar('y'))).isTrue();
+        assertThat(tab.diagram.isShowBodyTypes()).isTrue();
+        assertThat(tab.handleKeyEvent(KeyEvent.ofChar('y'))).isTrue();
+        assertThat(tab.diagram.isShowBodyTypes()).isFalse();
+
+        // the topology has no steps to show them for
+        tab.topologyMode = true;
+        tab.handleKeyEvent(KeyEvent.ofChar('y'));
+        assertThat(tab.diagram.isShowBodyTypes()).isFalse();
+    }
+
     @ParameterizedTest
     @MethodSource("tabs")
     void mAndEToggleWhatTheDiagramShows(Function<MonitorContext, 
AbstractDiagramTab> factory) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/BodyTypesViewsTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/BodyTypesViewsTest.java
new file mode 100644
index 000000000000..a27775637353
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/BodyTypesViewsTest.java
@@ -0,0 +1,198 @@
+/*
+ * 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.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+import dev.tamboui.text.Line;
+import org.apache.camel.diagram.RouteDiagramLayoutEngine;
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * The body types each step has been reached with, in the Routes view and the 
Info panel of the diagram (CAMEL-25892).
+ */
+class BodyTypesViewsTest {
+
+    private MonitorContext ctx;
+    private IntegrationInfo info;
+
+    @BeforeEach
+    void setUp() {
+        Theme.resetForTesting();
+        info = new IntegrationInfo();
+        info.pid = "5678";
+        info.name = "orders";
+        ctx = new MonitorContext(new AtomicReference<>(List.of(info)), new 
AtomicReference<>(List.of()));
+        ctx.selectedPid = "5678";
+    }
+
+    @Test
+    void theRoutesWithStepsThatVaried() {
+        RouteInfo orders = route("orders", "platform-http:/orders");
+        orders.variedBodyTypes = 2;
+        orders.mixedBodyKinds = 1;
+        RouteInfo stock = route("stock", "timer:stock");
+        RouteInfo invoices = route("invoices", "file:inbox");
+        invoices.variedBodyTypes = 1;
+
+        String rendered = TuiTestHelper.renderToString(new RoutesTab(ctx), 
160, 30);
+
+        assertThat(rendered).contains("BODY TYPES");
+        assertThat(rendered).contains(TuiIcons.WARN + " 1 mixed · 2 varied");
+        assertThat(rendered).contains("1 varied");
+        assertThat(RoutesTab.bodyTypesLabel(stock)).isEmpty();
+    }
+
+    @Test
+    void noColumnWhenNothingVaried() {
+        route("stock", "timer:stock");
+
+        String rendered = TuiTestHelper.renderToString(new RoutesTab(ctx), 
160, 30);
+
+        assertThat(rendered).doesNotContain("BODY TYPES");
+    }
+
+    @Test
+    void theBodyTypesOfEachStep() {
+        RouteInfo orders = route("orders", "platform-http:/orders");
+        orders.variedBodyTypes = 1;
+        orders.mixedBodyKinds = 1;
+        orders.processors.add(processor("unmarshal1", "unmarshal", 1,
+                
List.of("org.apache.camel.converter.stream.ByteArrayInputStreamCache"), 
List.of("payload")));
+        orders.processors.add(processor("to1", "to", 1,
+                List.of("byte[]", "java.util.LinkedHashMap"), 
List.of("payload", "data")));
+
+        String rendered = TuiTestHelper.renderToString(new RoutesTab(ctx), 
160, 40);
+
+        assertThat(rendered).contains("ByteArrayInputStreamCache");
+        assertThat(rendered).contains(TuiIcons.WARN + " byte[] | 
LinkedHashMap");
+    }
+
+    @Test
+    void theStatusFileHasThem() {
+        JsonObject status = Jsoner.deserialize("""
+                {"routes": [{"routeId": "orders", "from": 
"platform-http:/orders", "state": "Started",
+                  "variedBodyTypes": 1, "mixedBodyKinds": 1,
+                  "processors": [{"id": "to1", "processor": "to", "level": 1,
+                    "bodyTypes": ["byte[]", "java.util.LinkedHashMap"], 
"bodyKinds": ["payload", "data"]}]}]}
+                """, (JsonObject) null);
+        IntegrationInfo parsed = new IntegrationInfo();
+
+        StatusParser.parseRoutes(parsed, status);
+
+        RouteInfo route = parsed.routes.get(0);
+        assertThat(route.variedBodyTypes).isEqualTo(1);
+        assertThat(route.mixedBodyKinds).isEqualTo(1);
+        
assertThat(route.processors.get(0).bodyTypes).containsExactly("byte[]", 
"java.util.LinkedHashMap");
+        
assertThat(route.processors.get(0).bodyKinds).containsExactly("payload", 
"data");
+    }
+
+    @Test
+    void theWords() {
+        assertThat(BodyTypes.label(List.of("java.lang.String", "byte[]"), 
List.of("payload")))
+                .isEqualTo("String | byte[]");
+        assertThat(BodyTypes.label(List.of("null", "java.util.LinkedHashMap"), 
List.of("null", "data")))
+                .isEqualTo(TuiIcons.WARN + " null | LinkedHashMap (null and 
data)");
+        assertThat(BodyTypes.label(List.of(), List.of())).isEmpty();
+        assertThat(BodyTypes.shortLabel(List.of("null", 
"java.util.LinkedHashMap"), List.of("null", "data")))
+                .isEqualTo(TuiIcons.WARN + " null | LinkedHashMap");
+        assertThat(BodyTypes.kinds(List.of("null", "payload", 
"data"))).isEqualTo("null, payload and data");
+    }
+
+    @Test
+    void theTypesCutToTheRoomThereIs() {
+        List<String> types = 
List.of("org.apache.camel.converter.stream.ByteArrayInputStreamCache", 
"java.util.LinkedHashMap");
+        List<String> kinds = List.of("payload", "data");
+
+        assertThat(BodyTypes.fit(types, kinds, 80))
+                .isEqualTo(TuiIcons.WARN + " ByteArrayInputStreamCache | 
LinkedHashMap (payload and data)");
+        assertThat(BodyTypes.fit(types, kinds, 45)).isEqualTo(TuiIcons.WARN + 
" ByteArrayInputStreamCache | LinkedHashMap");
+        // the first type, and how many more
+        assertThat(BodyTypes.fit(types, kinds, 30)).isEqualTo(TuiIcons.WARN + 
" ByteArrayInputStreamCache +1");
+        assertThat(BodyTypes.fit(types, kinds, 16)).isEqualTo(TuiIcons.WARN + 
" ByteArrayI… +1");
+        // no room for a name: the step still says it needs attention
+        assertThat(BodyTypes.fit(types, kinds, 3)).isEqualTo(TuiIcons.WARN);
+        assertThat(BodyTypes.fit(List.of("java.lang.String"), 
List.of("payload"), 3)).isEqualTo("St…");
+        assertThat(BodyTypes.fit(List.of("java.lang.String"), 
List.of("payload"), 1)).isEmpty();
+        assertThat(BodyTypes.fit(List.of(), List.of(), 40)).isEmpty();
+    }
+
+    @Test
+    void theInfoPanelHasThemAll() {
+        RouteDiagramLayoutEngine.NodeInfo to = new 
RouteDiagramLayoutEngine.NodeInfo();
+        to.bodyTypes = 
List.of("org.apache.camel.converter.stream.ByteArrayInputStreamCache", 
"java.util.LinkedHashMap");
+        to.bodyKinds = List.of("payload", "data");
+        List<Line> lines = new ArrayList<>();
+
+        DiagramDetailSupport.addBodyTypes(lines, to, 27);
+
+        assertThat(lines).extracting(BodyTypesViewsTest::text).containsExactly(
+                "",
+                " Body types:",
+                "   ByteArrayInputStreamCache",
+                "   LinkedHashMap",
+                " " + TuiIcons.WARN + " payload and data: the",
+                "   body here depends on the",
+                "   path a message takes");
+
+        RouteDiagramLayoutEngine.NodeInfo log = new 
RouteDiagramLayoutEngine.NodeInfo();
+        log.bodyTypes = List.of("java.lang.String", "byte[]");
+        log.bodyKinds = List.of("payload");
+        lines.clear();
+        DiagramDetailSupport.addBodyTypes(lines, log, 27);
+        assertThat(lines).extracting(BodyTypesViewsTest::text)
+                .containsExactly("", " Body types (payload):", "   String", "  
 byte[]");
+
+        // not reached: nothing
+        lines.clear();
+        DiagramDetailSupport.addBodyTypes(lines, new 
RouteDiagramLayoutEngine.NodeInfo(), 27);
+        assertThat(lines).isEmpty();
+    }
+
+    private static String text(Line line) {
+        StringBuilder sb = new StringBuilder();
+        line.spans().forEach(span -> sb.append(span.content()));
+        return sb.toString();
+    }
+
+    private RouteInfo route(String id, String from) {
+        RouteInfo route = new RouteInfo();
+        route.routeId = id;
+        route.from = from;
+        route.state = "Started";
+        info.routes.add(route);
+        return route;
+    }
+
+    private static ProcessorInfo processor(
+            String id, String type, int level, List<String> bodyTypes, 
List<String> bodyKinds) {
+        ProcessorInfo proc = new ProcessorInfo();
+        proc.id = id;
+        proc.processor = type;
+        proc.level = level;
+        proc.bodyTypes = bodyTypes;
+        proc.bodyKinds = bodyKinds;
+        return proc;
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTopologyTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTopologyTest.java
index 75b0c1d114c1..02ff1dd67f03 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTopologyTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTopologyTest.java
@@ -139,6 +139,25 @@ class DiagramTopologyTest {
                 .doesNotContainKeys("coverage", "load01", "load05", "load15");
     }
 
+    @Test
+    void theStepsHaveTheBodyTypesOfTheStatusFileAsMoreMessagesCome() {
+        IntegrationInfo info = status();
+        ProcessorInfo to1 = info.routes.get(0).processors.get(0);
+        to1.bodyTypes = List.of("byte[]", "java.util.LinkedHashMap");
+        to1.bodyKinds = List.of("payload", "data");
+
+        JsonObject topology = DiagramTopology.withNumbers(json(TOPOLOGY), 
info);
+
+        List<RouteDiagramLayoutEngine.NodeInfo> lines = 
RouteDiagramHelper.parseRoutes(topology).get(0).nodes;
+        assertThat(lines.get(2).id).isEqualTo("to1");
+        assertThat(lines.get(2).bodyTypes).containsExactly("byte[]", 
"java.util.LinkedHashMap");
+        assertThat(lines.get(2).bodyKinds).containsExactly("payload", "data");
+        // a step not reached yet has none, nor have the route and its from
+        assertThat(lines.get(3).bodyTypes).isEmpty();
+        assertThat(lines.get(0).bodyTypes).isEmpty();
+        assertThat(lines.get(1).bodyTypes).isEmpty();
+    }
+
     @Test
     void theStructureKeptForTheNextTimeIsLeftWithoutNumbers() {
         JsonObject structure = json(TOPOLOGY);
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 a80d8046e18e..c36832270de2 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
@@ -60,9 +60,7 @@ class SourceSizeTest {
             AiPanel.runAgentLoop 159
             AiUsageStats.render 166
             CamelMonitor.java 2761
-            DiagramDetailSupport.renderEipInfoPanel 159
-            DiagramDetailSupport.renderRouteInfoPanel 154
-            DiagramSupport.java 2460
+            DiagramSupport.java 2413
             DiagramSupport.loadAllDiagramsInBackground 180
             DiagramSupport.loadHighlightedNativeDiagramInBackground 212
             EndpointsTab.renderContent 170
@@ -74,7 +72,6 @@ class SourceSizeTest {
             OllamaMonitor.toJson 200
             OverviewTab.buildOverviewTable 185
             OverviewTab.renderThroughputChart 158
-            RoutesTab.renderProcessors 179
             SendMessagePopup.doSend 156
             SendMessagePopup.handleKeyEvent 220
             SourceEditAssist.java 2006
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/diagram/BodyTypesOnArrowsTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/diagram/BodyTypesOnArrowsTest.java
new file mode 100644
index 000000000000..4751dff700ca
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/diagram/BodyTypesOnArrowsTest.java
@@ -0,0 +1,142 @@
+/*
+ * 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.List;
+import java.util.Map;
+
+import dev.tamboui.buffer.Buffer;
+import dev.tamboui.layout.Rect;
+import org.apache.camel.diagram.RouteDiagramLayoutEngine;
+import org.apache.camel.diagram.RouteDiagramLayoutEngine.LayoutRoute;
+import org.apache.camel.diagram.RouteDiagramLayoutEngine.NodeInfo;
+import org.apache.camel.diagram.RouteDiagramLayoutEngine.RouteInfo;
+import org.apache.camel.dsl.jbang.core.commands.tui.BodyTypes;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * The body types each step has been reached with, beside the arrow into it 
(CAMEL-25892): on the shaft above the head,
+ * cut where the arrows of a branch are side by side, and a ⚠ on a step 
reached with bodies of more than one kind.
+ */
+class BodyTypesOnArrowsTest {
+
+    private static final String STREAM = 
"org.apache.camel.converter.stream.ByteArrayInputStreamCache";
+
+    @Test
+    void theTypesAreOnTheShaftAboveTheHeadOfTheArrow() {
+        RouteInfo route = route();
+        route.nodes.add(node("unmarshal", "unmarshal1", 1, List.of(STREAM), 
List.of("payload")));
+        route.nodes.add(node("to", "to1", 1, List.of("byte[]", 
"java.util.LinkedHashMap"), List.of("payload", "data")));
+
+        Buffer buffer = render(route, true);
+
+        int[] stream = find(buffer, "ByteArrayInputStreamCache");
+        assertThat(stream).isNotNull();
+        // the shaft is two columns left of it, and the head of the arrow 
right below
+        assertThat(symbol(buffer, stream[0] - 2, stream[1])).isEqualTo("│");
+        assertThat(symbol(buffer, stream[0] - 2, stream[1] + 
1)).isEqualTo("▼");
+        // with more than one kind: the kinds as well, as there is room
+        int[] mixed = find(buffer, BodyTypes.MARK + " byte[] | LinkedHashMap 
(payload and data)");
+        assertThat(mixed).isNotNull();
+        assertThat(symbol(buffer, mixed[0] - 2, mixed[1] + 1)).isEqualTo("▼");
+        // and its box has a ⚠ in its top border
+        assertThat(row(buffer, mixed[1] + 2)).contains(BodyTypes.MARK);
+    }
+
+    @Test
+    void theArrowsOfABranchSideBySideCutTheTypes() {
+        RouteInfo route = route();
+        route.nodes.add(node("multicast", "multicast1", 1, List.of(), 
List.of()));
+        route.nodes.add(node("to", "to1", 2,
+                List.of(STREAM, "java.util.LinkedHashMap", 
"java.util.ArrayList"), List.of("payload", "data", "collection")));
+        route.nodes.add(node("to", "to2", 2, List.of("java.lang.String"), 
List.of("payload")));
+
+        Buffer buffer = render(route, true);
+
+        // the first one stops before the arrow of the second: the first type, 
and how many more
+        int[] left = find(buffer, BodyTypes.MARK + " ByteArrayInputStreamCache 
+2 ");
+        assertThat(left).isNotNull();
+        assertThat(row(buffer, 
left[1])).doesNotContain("LinkedHashMap").contains("▼ String");
+    }
+
+    @Test
+    void offTheStepsNeedingAttentionStillHaveTheirMark() {
+        RouteInfo route = route();
+        route.nodes.add(node("to", "to1", 1, List.of("byte[]", 
"java.util.LinkedHashMap"), List.of("payload", "data")));
+
+        Buffer buffer = render(route, false);
+
+        assertThat(find(buffer, "LinkedHashMap")).isNull();
+        assertThat(find(buffer, BodyTypes.MARK)).isNotNull();
+    }
+
+    private static Buffer render(RouteInfo route, boolean bodyTypes) {
+        RouteDiagramLayoutEngine engine = new RouteDiagramLayoutEngine();
+        LayoutRoute layout = engine.layoutRoute(route, 0);
+        RouteDiagramWidget widget = new RouteDiagramWidget(layout, 
engine.getNodeWidth(), -1, 0, 0, false, Map.of())
+                .withBodyTypes(bodyTypes).withBodyKindMarks(true);
+        Rect area = new Rect(0, 0, 160, 50);
+        Buffer buffer = Buffer.empty(area);
+        widget.render(area, buffer);
+        return buffer;
+    }
+
+    private static RouteInfo route() {
+        RouteInfo route = new RouteInfo();
+        route.routeId = "orders";
+        NodeInfo r = node("route", "orders", 0, List.of(), List.of());
+        r.code = "route[orders]";
+        route.nodes.add(r);
+        route.nodes.add(node("from", "from1", 0, List.of(), List.of()));
+        return route;
+    }
+
+    private static NodeInfo node(String type, String id, int level, 
List<String> bodyTypes, List<String> bodyKinds) {
+        NodeInfo n = new NodeInfo();
+        n.type = type;
+        n.id = id;
+        n.code = type;
+        n.level = level;
+        n.bodyTypes = bodyTypes;
+        n.bodyKinds = bodyKinds;
+        return n;
+    }
+
+    private static String symbol(Buffer buffer, int x, int y) {
+        return buffer.get(x, y).symbol();
+    }
+
+    private static String row(Buffer buffer, int y) {
+        StringBuilder sb = new StringBuilder();
+        for (int x = 0; x < buffer.width(); x++) {
+            sb.append(buffer.get(x, y).symbol());
+        }
+        return sb.toString();
+    }
+
+    private static int[] find(Buffer buffer, String text) {
+        for (int y = 0; y < buffer.height(); y++) {
+            int x = row(buffer, y).indexOf(text);
+            if (x >= 0) {
+                return new int[] { x, y };
+            }
+        }
+        return null;
+    }
+}

Reply via email to