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 9692de5beecf CAMEL-25897: camel-jbang - the source view shows the body 
types each step has been reached with at the end of its line (#28234)
9692de5beecf is described below

commit 9692de5beecfa8b08c6323a73e6ff4631750f6d3
Author: Claus Ibsen <[email protected]>
AuthorDate: Sat Oct 24 11:13:08 2026 +0200

    CAMEL-25897: camel-jbang - the source view shows the body types each step 
has been reached with at the end of its line (#28234)
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
---
 .../images/jbang/camel-tui-source-body-types.png   | Bin 0 -> 366120 bytes
 .../ROOT/pages/camel-jbang-tui-diagram.adoc        |   3 +-
 .../ROOT/pages/camel-jbang-tui-source-editor.adoc  |  21 +-
 .../dsl/jbang/core/commands/tui/LiveRunLines.java  |  43 ++-
 .../jbang/core/commands/tui/SourceLiveColumn.java  | 136 ++++++++-
 .../jbang/core/commands/tui/SourceRunState.java    | 131 ++++++---
 .../dsl/jbang/core/commands/tui/SourceTab.java     |  23 +-
 .../dsl/jbang/core/commands/tui/SourceViewer.java  |   8 +-
 .../src/main/resources/tui/help/source.md          |  15 +
 .../core/commands/tui/SourceBodyTypesTest.java     | 303 +++++++++++++++++++++
 .../core/commands/tui/SourceRunStateTest.java      |  25 +-
 11 files changed, 656 insertions(+), 52 deletions(-)

diff --git 
a/docs/user-manual/modules/ROOT/images/jbang/camel-tui-source-body-types.png 
b/docs/user-manual/modules/ROOT/images/jbang/camel-tui-source-body-types.png
new file mode 100644
index 000000000000..8a30469ce191
Binary files /dev/null and 
b/docs/user-manual/modules/ROOT/images/jbang/camel-tui-source-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 c50d9a84a421..6fc27a5b1c34 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
@@ -213,7 +213,8 @@ The arrows of a branch are side by side, so the types 
beside them are cut to the
 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.
+table of the processors of the selected route. The source view of the Source 
tab has them at the end of the line of each
+step (see xref:camel-jbang-tui-source-editor.adoc#_body_types[Body Types]).
 
 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]
 
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-source-editor.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-source-editor.adoc
index b020d717ec43..be04781198d1 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-source-editor.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-source-editor.adoc
@@ -8,7 +8,7 @@ routes.
 == Top Features
 
 * <<_live_run_data,*Live run data*>> -- the exchanges and failures of each 
line of the running integration, next to
-  the code.
+  the code, and the body types each step has been reached with at the end of 
its line.
 * <<_running_as_saved,*Running as saved*>> -- whether the integration runs the 
file as it is saved, or needs a restart.
 * <<_problems_as_you_type,*Problems as you type*>> -- endpoint options, Simple 
expressions and more are checked as soon
   as a file opens and while you type, in YAML, Java and XML routes; a YAML or 
XML file with problems is not saved.
@@ -55,6 +55,23 @@ In the example the timer fired 227 times; the `filter` let 
137 of those through
 The column keeps its width, so the code does not move as the numbers grow. It 
works for YAML, Java and XML routes, as
 `camel run` records the source line of every processor.
 
+=== Body Types
+
+With the message history on, such as with `camel run --dev`, the body types 
each step has been reached with go at the
+end of its line, dimmed and lined up. 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: it has a `⚠` in the gutter, and its
+types and kinds are in the warning color. The quick documentation of the line 
lists all the types, which the end of the
+line cuts to the room there is.
+
+image::jbang/camel-tui-source-body-types.png[The orders route in the source 
view: the steps after the choice are reached with a map and a stream, as only 
one branch marshals, so they have a warning in the gutter, and the quick 
documentation of the selected line says why]
+
+The same body types are in the Routes and Diagram tabs (see
+xref:camel-jbang-tui-diagram.adoc#_body_types[Body Types]).
+
+The live run data and the body types are of the version of the file the 
integration runs. While it does not run the
+file as saved (see <<_running_as_saved,Running as Saved>>), the numbers are 
dimmed and the body types are left out, as
+the lines of the version that runs may be others.
+
 == Running as Saved
 
 The bottom border of the source says whether the integration runs the routes 
of the file as it is saved:
@@ -64,6 +81,8 @@ The bottom border of the source says whether the integration 
runs the routes of
 * `○ not running yet` -- the file was saved after the integration loaded it. 
An integration that runs without `--dev`
   does not reload a file when it is saved: restart it. The log of one in dev 
mode says why it did not reload the file.
 * `⚠ the reload failed` -- the integration runs the routes it ran before the 
reload, until a reload succeeds.
+* `○ the routes of this file are stopped` -- stopped (or suspended) by hand. 
The live run data and the body types still
+  show when the integration loaded the file as saved, which is not known for a 
route stopped after a reload.
 
 image::jbang/camel-tui-source-running-as-saved.png[The bottom border of an 
edited route file: the integration runs without --dev, so the saved file is not 
running yet]
 
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LiveRunLines.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LiveRunLines.java
index a65e5aec6a92..fafddd111129 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LiveRunLines.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LiveRunLines.java
@@ -17,8 +17,10 @@
 package org.apache.camel.dsl.jbang.core.commands.tui;
 
 import java.util.HashMap;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 /**
  * The live run data of a source file: what the processors of a running 
integration on each line of it did, read from
@@ -33,10 +35,12 @@ final class LiveRunLines {
     /**
      * @param  routes   the routes of the running integration, with their 
processors
      * @param  filePath the file shown
-     * @return          per 0-based line of the file, the exchanges, failures 
and mean time of the processors on it
+     * @return          per 0-based line of the file, the exchanges, failures 
and mean time of the processors on it, and
+     *                  the body types they have been reached with
      */
     static Map<Integer, SourceLiveColumn.LiveLine> of(List<RouteInfo> routes, 
String filePath) {
         Map<Integer, long[]> sums = new HashMap<>();
+        Map<Integer, LineTypes> types = new HashMap<>();
         String name = nameOf(filePath);
         if (routes == null || name == null) {
             return Map.of();
@@ -46,13 +50,48 @@ final class LiveRunLines {
             add(sums, name, r.source, r.total, r.failed, r.meanTime);
             for (ProcessorInfo p : r.processors) {
                 add(sums, name, p.source, p.total, p.failed, p.meanTime);
+                addBodyTypes(types, name, p);
             }
         }
         Map<Integer, SourceLiveColumn.LiveLine> answer = new HashMap<>();
-        sums.forEach((line, s) -> answer.put(line, new 
SourceLiveColumn.LiveLine(s[0], s[1], s[0] > 0 ? s[2] / s[0] : 0)));
+        sums.forEach((line, s) -> {
+            LineTypes t = types.get(line);
+            answer.put(line, t == null
+                    ? new SourceLiveColumn.LiveLine(s[0], s[1], s[0] > 0 ? 
s[2] / s[0] : 0)
+                    : new SourceLiveColumn.LiveLine(
+                            s[0], s[1], s[0] > 0 ? s[2] / s[0] : 0, 
List.copyOf(t.types), List.copyOf(t.kinds), t.mixed));
+        });
         return answer;
     }
 
+    /** The body types of the processors on a line, and whether one of them 
was reached with more than one kind. */
+    private static final class LineTypes {
+        final Set<String> types = new LinkedHashSet<>();
+        final Set<String> kinds = new LinkedHashSet<>();
+        boolean mixed;
+    }
+
+    /**
+     * Adds the body types a processor has been reached with to its line 
(CAMEL-25897): the types and the kinds of all
+     * the processors on the line, which is mixed when one of them was reached 
with bodies of more than one kind, not
+     * when two of them each had their own kind.
+     */
+    private static void addBodyTypes(Map<Integer, LineTypes> types, String 
name, ProcessorInfo p) {
+        if (p.bodyTypes == null || p.bodyTypes.isEmpty()) {
+            return;
+        }
+        int line = lineOf(p.source, name);
+        if (line < 0) {
+            return;
+        }
+        LineTypes t = types.computeIfAbsent(line, k -> new LineTypes());
+        t.types.addAll(p.bodyTypes);
+        if (p.bodyKinds != null) {
+            t.kinds.addAll(p.bodyKinds);
+        }
+        t.mixed |= BodyTypes.mixed(p.bodyKinds);
+    }
+
     /** Adds what a processor did to its line: totals and failures summed, the 
mean time weighted by the exchanges. */
     private static void add(Map<Integer, long[]> sums, String name, String 
source, long total, long failed, long mean) {
         int line = lineOf(source, name);
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceLiveColumn.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceLiveColumn.java
index 580a435ddd34..d5eb29c645e2 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceLiveColumn.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceLiveColumn.java
@@ -24,20 +24,37 @@ import java.util.function.BooleanSupplier;
 import java.util.function.Supplier;
 
 import dev.tamboui.style.Style;
+import dev.tamboui.text.CharWidth;
 import dev.tamboui.text.Span;
 
 /**
  * The live run data of the running integration in the source view ({@link 
SourceViewer}), a column at the end of the
  * line numbers: how many exchanges the processors on a line handled, how many 
failed, and their mean time (a heat map
  * of the source while it runs). A line that failed is what Shift+F8 asks the 
AI to fix when it has no problem.
+ * <p/>
+ * The body types the steps of a line have been reached with go at the end of 
the line, lined up, with a ⚠ in the gutter
+ * of a step reached with bodies of more than one kind (CAMEL-25897). They 
only hold while the view has the version of
+ * the file that runs: once the file is saved and the integration did not load 
it, the live run data is dimmed and the
+ * body types are left out.
  */
 class SourceLiveColumn {
 
+    /**
+     * The most a line (its number and code) counts for lining up the body 
types: a long one does not push them away.
+     */
+    private static final int MAX_BODY_TYPES_ALIGN = 72;
+
     /**
      * What a line of a running integration's source did: the exchanges of the 
processors on it, the failed ones and the
-     * mean processing time.
+     * mean processing time; and the body types they have been reached with, 
mixed when one of them was reached with
+     * bodies of more than one kind.
      */
-    record LiveLine(long total, long failed, long meanMillis) {
+    record LiveLine(long total, long failed, long meanMillis, List<String> 
bodyTypes, List<String> bodyKinds,
+            boolean mixed) {
+
+        LiveLine(long total, long failed, long meanMillis) {
+            this(total, failed, meanMillis, List.of(), List.of(), false);
+        }
     }
 
     /** The live run data of a file by its path: per 0-based line, what the 
processors on it did. */
@@ -54,6 +71,10 @@ class SourceLiveColumn {
 
     private final Supplier<String> loadedFile;
     private final BooleanSupplier dirty;
+    private final Supplier<List<String>> lines;
+    private BooleanSupplier stale = () -> false;
+    // the column the body types of the lines line up at, from the lines that 
have them
+    private int bodyTypesAlign;
     private LiveRunData liveRunData;
     private LineFailures lineFailures;
     private Map<Integer, LiveLine> liveLines = Collections.emptyMap();
@@ -62,14 +83,32 @@ class SourceLiveColumn {
     private int liveFailedWidth;
     private int liveMeanWidth;
     private String liveWidthsFile;
+    // the file the live run data was read for: another one is read at once, 
not after a second
+    private String liveLinesFile;
 
     /**
      * @param loadedFile the path of the file shown, whose live run data is 
read; null when none is
      * @param dirty      whether the file is edited and not saved, so the live 
run data is not of what is shown
+     * @param lines      the lines shown (the line number, then the code), to 
line up the body types after them
      */
-    SourceLiveColumn(Supplier<String> loadedFile, BooleanSupplier dirty) {
+    SourceLiveColumn(Supplier<String> loadedFile, BooleanSupplier dirty, 
Supplier<List<String>> lines) {
         this.loadedFile = loadedFile;
         this.dirty = dirty;
+        this.lines = lines;
+    }
+
+    /**
+     * Whether the integration runs another version of the file than the one 
shown, such as one saved since it loaded
+     * the file (CAMEL-25771): the live run data is of other lines then, so it 
is dimmed and the body types are left
+     * out.
+     */
+    void setStale(BooleanSupplier stale) {
+        this.stale = stale;
+    }
+
+    /** Whether what the integration did is of the lines shown: the file is 
not edited, and it runs as shown. */
+    private boolean current() {
+        return !dirty.getAsBoolean() && !stale.getAsBoolean();
     }
 
     /**
@@ -105,7 +144,7 @@ class SourceLiveColumn {
      * of each frame asks it; the exception is only read on Shift+F8).
      */
     boolean failsAtRuntime(int row) {
-        if (dirty.getAsBoolean() || row < 0) {
+        if (!current() || row < 0) {
             return false;
         }
         LiveLine live = liveLines().get(row);
@@ -130,6 +169,11 @@ class SourceLiveColumn {
         Style totalStyle = data ? Theme.label() : Style.EMPTY;
         Style failedStyle = Theme.error().bold();
         Style dim = Style.EMPTY.dim();
+        if (stale.getAsBoolean()) {
+            // of the version of the file that runs, whose lines may be others
+            totalStyle = dim;
+            failedStyle = dim;
+        }
         if (selBg != null) {
             totalStyle = totalStyle.patch(selBg);
             failedStyle = failedStyle.patch(selBg);
@@ -145,6 +189,61 @@ class SourceLiveColumn {
         spans.add(Span.styled(" │", dim));
     }
 
+    /**
+     * The gutter of a line: a ⚠ when a step on it has been reached with 
bodies of more than one kind, which needs
+     * attention (CAMEL-25897); blank otherwise.
+     */
+    Span gutter(int lineIndex) {
+        LiveLine live = current() ? liveLines().get(lineIndex) : null;
+        return live != null && live.mixed() ? Span.styled(" " + BodyTypes.MARK 
+ " ", Theme.warning()) : Span.raw("   ");
+    }
+
+    /**
+     * The body types the steps of a line have been reached with, at the end 
of the line, dimmed (a warning when they
+     * are of more than one kind), lined up with those of the other lines, and 
cut to the room up to the right edge
+     * (CAMEL-25897). Nothing in the plain mode, or when the view does not 
have the version of the file that runs.
+     *
+     * @param spans     the spans of the line so far: the gutter, the line 
number, the live run data and the code
+     * @param rightEdge the column of the right edge of the view, in the 
columns of the line
+     */
+    void addBodyTypes(List<Span> spans, int lineIndex, boolean plain, Style 
selBg, int rightEdge) {
+        LiveLine live = plain || !current() ? null : 
liveLines().get(lineIndex);
+        if (live == null || live.bodyTypes().isEmpty()) {
+            return;
+        }
+        int width = 0;
+        for (Span span : spans) {
+            width += CharWidth.of(span.content());
+        }
+        int pad = Math.max(2, bodyTypesAlign - width);
+        String text = BodyTypes.fit(live.bodyTypes(), live.mixed() ? 
live.bodyKinds() : List.of(), rightEdge - width - pad);
+        if (text.isEmpty()) {
+            return;
+        }
+        Style style = live.mixed() ? Theme.warning() : Theme.muted();
+        spans.add(Span.styled(" ".repeat(pad), selBg != null ? selBg : 
Style.EMPTY));
+        spans.add(Span.styled(text, selBg != null ? style.patch(selBg) : 
style));
+    }
+
+    /**
+     * The body types of the steps of a line for the quick documentation 
(CAMEL-25897), all of them, and why a mix of
+     * kinds needs attention; null when the line has none, or the editor does 
not have the version of the file that
+     * runs.
+     */
+    SourceViewer.DocEntry bodyTypesDoc(int row) {
+        LiveLine live = current() ? liveLines().get(row) : null;
+        if (live == null || live.bodyTypes().isEmpty()) {
+            return null;
+        }
+        if (live.mixed()) {
+            return SourceViewer.DocEntry.withTitle("Body types", 
BodyTypes.names(live.bodyTypes()) + "   " + BodyTypes.MARK
+                                                                 + " " + 
BodyTypes.kinds(live.bodyKinds())
+                                                                 + ": the body 
here depends on the path a message takes");
+        }
+        String title = live.bodyKinds().size() == 1 ? "Body types (" + 
live.bodyKinds().get(0) + ")" : "Body types";
+        return SourceViewer.DocEntry.withTitle(title, 
BodyTypes.names(live.bodyTypes()));
+    }
+
     /** The live run data of the loaded file, read again at most once a second 
(the status is polled about as often). */
     private Map<Integer, LiveLine> liveLines() {
         String loadedFilePath = loadedFile.get();
@@ -152,7 +251,7 @@ class SourceLiveColumn {
             return Collections.emptyMap();
         }
         long now = MonotonicClock.millis();
-        if (now - liveLinesTime > 1000) {
+        if (now - liveLinesTime > 1000 || 
!loadedFilePath.equals(liveLinesFile)) {
             refreshLiveLines(now);
         }
         return liveLines;
@@ -178,6 +277,7 @@ class SourceLiveColumn {
             Map<Integer, LiveLine> lines = liveRunData.lines(loadedFilePath);
             liveLines = lines != null ? lines : Collections.emptyMap();
             liveLinesTime = now;
+            liveLinesFile = loadedFilePath;
             // the widths of the column, for all the lines alike; they only 
grow while the same file is shown (from
             // a minimum), so the column does not jitter when a number crosses 
99 -> 100 and back (a mean time going
             // up and down, a reset of the statistics); another file, or the 
integration stopping, starts afresh
@@ -198,6 +298,32 @@ class SourceLiveColumn {
                     }
                 }
             }
+            bodyTypesAlign = bodyTypesAlign();
+        }
+    }
+
+    /**
+     * The column the body types line up at: after the longest of the lines 
that have them (the gutter, the line number,
+     * the live run data and the code), and at most {@link 
#MAX_BODY_TYPES_ALIGN} into the code, so a long line does not
+     * push the body types of the others off screen.
+     */
+    private int bodyTypesAlign() {
+        List<String> shown = lines.get();
+        int longest = 0;
+        for (Map.Entry<Integer, LiveLine> e : liveLines.entrySet()) {
+            int i = e.getKey();
+            if (!e.getValue().bodyTypes().isEmpty() && shown != null && i >= 0 
&& i < shown.size()) {
+                longest = Math.max(longest, 
Math.min(CharWidth.of(shown.get(i)), MAX_BODY_TYPES_ALIGN));
+            }
+        }
+        if (longest == 0) {
+            return 0;
         }
+        // the gutter before the line number, and the live run data after it
+        int live = liveTotalWidth == 0
+                ? 0
+                : 1 + liveTotalWidth + (liveFailedWidth > 0 ? 1 + 
liveFailedWidth : 0)
+                  + (liveMeanWidth > 0 ? 1 + liveMeanWidth : 0) + 2;
+        return 3 + longest + live + 3;
     }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceRunState.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceRunState.java
index 76ee51aaed46..e038144d3741 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceRunState.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceRunState.java
@@ -21,6 +21,8 @@ import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicLong;
 
 import dev.tamboui.layout.Rect;
 import dev.tamboui.terminal.Frame;
@@ -45,7 +47,9 @@ import org.apache.camel.util.json.JsonObject;
  * <li>after a reload, a route started after the save runs what is saved. The 
integration does not say when it loaded a
  * route, so a route stopped and started again by hand after a reload is taken 
as loaded then.</li>
  * </ul>
- * A failed reload puts back the routes that ran before it, so until a reload 
succeeds the state is the reload error.
+ * A failed reload puts back the routes that ran before it, so until a reload 
succeeds the state is the reload error. A
+ * file whose routes are all stopped is shown as such; the facts of its lines 
still hold when the integration loaded it
+ * as saved, which is known without a reload, but not for a route stopped 
after a reload.
  */
 final class SourceRunState {
 
@@ -60,13 +64,15 @@ final class SourceRunState {
         /** the last reload failed, so the routes that ran before it run */
         RELOAD_FAILED,
         /** saved after the routes were loaded, and the integration did not 
load it again */
-        NOT_RUNNING
+        NOT_RUNNING,
+        /** the routes the integration has from the file are all stopped (or 
suspended) */
+        STOPPED
     }
 
     /**
      * @param kind   the state
      * @param routes the routes the integration has from the file
-     * @param stale  how many of them were loaded before the file was saved
+     * @param stale  how many of them were loaded before the file was saved, 
or at a time not known
      * @param detail why not running, or the reload error; null when running
      */
     record State(Kind kind, int routes, int stale, String detail) {
@@ -90,21 +96,23 @@ final class SourceRunState {
         // an integration that has not reloaded loaded its routes when it 
started; after a reload, when each started
         boolean reloaded = info.reloaded > 0 || info.reloadError != null || 
info.uptime <= 0;
         int routes = 0;
+        int started = 0;
         int stale = 0;
+        int unknown = 0;
         for (RouteInfo r : info.routes) {
-            if (!"Started".equals(r.state) || LiveRunLines.lineOf(r.source, 
name) < 0) {
+            if (LiveRunLines.lineOf(r.source, name) < 0) {
                 continue;
             }
-            long loaded = info.uptime;
-            if (reloaded) {
-                long uptime = uptimeMillis(r.uptime);
-                if (uptime < 0) {
-                    continue;
-                }
-                loaded = Math.max(info.uptime, now - uptime);
-            }
             routes++;
-            if (loaded < fileModified) {
+            boolean running = "Started".equals(r.state);
+            if (running) {
+                started++;
+            }
+            long uptime = running ? uptimeMillis(r.uptime) : -1;
+            if (reloaded && uptime < 0) {
+                // a stopped route after a reload: when it was loaded is not 
known, so it may be of another version
+                unknown++;
+            } else if ((reloaded ? Math.max(info.uptime, now - uptime) : 
info.uptime) < fileModified) {
                 stale++;
             }
         }
@@ -112,11 +120,13 @@ final class SourceRunState {
             return null;
         }
         if (info.reloadError != null) {
-            return new State(Kind.RELOAD_FAILED, routes, stale, 
info.reloadError);
+            return new State(Kind.RELOAD_FAILED, routes, stale + unknown, 
info.reloadError);
         }
         if (stale == 0) {
-            return new State(Kind.RUNNING, routes, 0, null);
+            // what a route stopped by hand says of the lines holds when it is 
the version that was loaded
+            return new State(started == 0 ? Kind.STOPPED : Kind.RUNNING, 
routes, unknown, null);
         }
+        stale += unknown;
         if (info.devMode) {
             if (now - fileModified < RELOAD_GRACE_MILLIS) {
                 return new State(Kind.RELOADING, routes, stale, null);
@@ -150,6 +160,7 @@ final class SourceRunState {
             case NOT_RUNNING -> Line.from(
                     Span.styled(TuiIcons.IDLE + " not running yet", 
Theme.warning()),
                     Span.styled(": " + state.detail(), Theme.muted()));
+            case STOPPED -> Line.from(Span.styled(TuiIcons.IDLE + " the routes 
of this file are stopped", Theme.muted()));
         };
     }
 
@@ -161,6 +172,7 @@ final class SourceRunState {
             case RELOADING -> "reloading";
             case RELOAD_FAILED -> "reloadFailed";
             case NOT_RUNNING -> "notRunning";
+            case STOPPED -> "stopped";
         });
         jo.put("routes", state.routes());
         jo.put("routesLoadedBeforeSave", state.stale());
@@ -176,44 +188,101 @@ final class SourceRunState {
      */
     static final class Tracker {
 
+        /** The state computed for a file: null when the integration has no 
route from it. */
         private record Computed(String file, State state) {
         }
 
         private volatile String file;
         private volatile Computed computed;
+        // a save in the editor makes the state unknown until a refresh after 
it computes it again (written by the UI
+        // thread only)
+        private volatile int saves;
+        private volatile int computedSaves;
+        private boolean wasDirty;
+        // the refreshes in the order they started: a later one wins over one 
that ends after it (guarded by this)
+        private final AtomicLong refreshes = new AtomicLong();
+        private long written;
 
-        /** The file the source panel shows, on the UI thread when it draws; 
null when none is. */
-        void shown(String file) {
+        /**
+         * The file the source panel shows, on the UI thread when it draws; 
null when none is. Edits that are no longer
+         * unsaved were saved (or discarded): the state is unknown until the 
next refresh.
+         *
+         * @return whether the state is to be computed now: another file is 
shown, or the file was saved
+         */
+        boolean shown(String file, boolean dirty) {
+            boolean saved = wasDirty && !dirty;
+            if (saved) {
+                saves++;
+            }
+            wasDirty = dirty;
+            boolean other = !Objects.equals(file, this.file);
             this.file = file;
+            return file != null && (saved || other);
         }
 
-        /** Computes the state of the file shown, on the background thread of 
the data refresh. */
+        /**
+         * Computes the state of the file shown, off the UI thread: on the 
data refresh, when another file is shown or
+         * the file is saved, and when tui_get_table reads the tab.
+         */
         void refresh(IntegrationInfo info) {
+            long order = refreshes.incrementAndGet();
+            int at = saves;
             String shown = file;
-            if (shown == null || info == null) {
-                computed = null;
-                return;
+            Computed result = null;
+            if (shown != null && info != null) {
+                try {
+                    // the file system keeps the time of the save by the wall 
clock, so it is compared with the wall clock
+                    long modified = 
Files.getLastModifiedTime(Path.of(shown)).toMillis();
+                    result = new Computed(shown, of(info, shown, modified, 
System.currentTimeMillis()));
+                } catch (IOException | RuntimeException e) {
+                    // not known
+                }
             }
-            try {
-                // the file system keeps the time of the save by the wall 
clock, so it is compared with the wall clock
-                long modified = 
Files.getLastModifiedTime(Path.of(shown)).toMillis();
-                State state = of(info, shown, modified, 
System.currentTimeMillis());
-                computed = state != null ? new Computed(shown, state) : null;
-            } catch (IOException | RuntimeException e) {
-                computed = null;
+            synchronized (this) {
+                if (order > written) {
+                    written = order;
+                    computed = result;
+                    computedSaves = at;
+                }
             }
         }
 
         void reset() {
             file = null;
-            computed = null;
+            synchronized (this) {
+                // a refresh that started before does not write after
+                written = refreshes.incrementAndGet();
+                computed = null;
+                computedSaves = saves;
+            }
         }
 
-        /** The state of the file shown; null when unknown, or when it was 
computed for another file. */
+        /**
+         * The state of the file shown; null when unknown: computed for 
another file, or before the last save in the
+         * editor.
+         */
         State current() {
             Computed c = computed;
             String shown = file;
-            return c != null && c.file().equals(shown) ? c.state() : null;
+            return c != null && c.file().equals(shown) && computedSaves == 
saves ? c.state() : null;
+        }
+
+        /**
+         * Whether the integration may run another version of the file than 
the one shown, so the live run data is of
+         * other lines (CAMEL-25897): not known yet for the file shown (just 
opened, or saved in the editor since),
+         * saved after the integration loaded it, a failed reload, or a route 
stopped after a reload.
+         */
+        boolean stale() {
+            Computed c = computed;
+            String shown = file;
+            if (shown == null) {
+                return false;
+            }
+            if (c == null || !c.file().equals(shown) || computedSaves != 
saves) {
+                return true;
+            }
+            State state = c.state();
+            return state != null && (state.kind() == Kind.RELOAD_FAILED || 
state.stale() > 0);
         }
 
         /** The state on the bottom border of the source panel, left of the 
cursor position of the editor. */
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
index 35109301f038..1031a67a5b69 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
@@ -165,11 +165,13 @@ class SourceTab extends AbstractTab {
             }
         });
         sourceViewer.setOnFileCreated(this::refreshFiles);
-        // the live run data of the selected integration at the end of the 
lines of its source
+        // the live run data of the selected integration at the end of the 
lines of its source, and the body types of
+        // its steps; dimmed and left out while it runs another version of the 
file (CAMEL-25897)
         sourceViewer.liveColumn().setLiveRunData(path -> {
             IntegrationInfo info = ctx.findSelectedIntegration();
             return info != null ? LiveRunLines.of(info.routes, path) : 
Map.of();
         });
+        sourceViewer.liveColumn().setStale(runState::stale);
         // the exception of the last failure on a line, for fix with AI 
(Shift+F8) on a line that fails at runtime
         sourceViewer.liveColumn().setLineFailures((path, line) -> {
             IntegrationInfo info = ctx.findSelectedIntegration();
@@ -1208,10 +1210,13 @@ class SourceTab extends AbstractTab {
             sourceViewer.setFocused(focusOnViewer);
             sourceViewer.render(frame, area);
             // whether the integration runs the file shown as it is saved, in 
the view and in the editor (CAMEL-25771)
-            runState.shown(sourceViewer.getCurrentFilePath());
+            if (runState.shown(sourceViewer.getCurrentFilePath(), 
sourceViewer.isDirty())) {
+                // another file, or a save: its run state now, rather than at 
the next refresh
+                ctx.backgroundExecutor.execute(() -> 
runState.refresh(ctx.findSelectedIntegration()));
+            }
             runState.render(frame, area);
         } else {
-            runState.shown(null);
+            runState.shown(null, false);
             List<Line> lines = emptySourceLines(routeIndex, rootDir);
 
             frame.renderWidget(
@@ -1641,8 +1646,8 @@ class SourceTab extends AbstractTab {
     }
 
     /**
-     * The quick doc of a line with what the project knows about it put first: 
the values of its property placeholders
-     * and where the bean it refers to is declared.
+     * The quick doc of a line with what the project knows about it put first: 
the body types its steps have been
+     * reached with, the values of its property placeholders and where the 
bean it refers to is declared.
      */
     private SourceViewer.EditQuickDocProvider 
withProjectDocs(SourceViewer.EditQuickDocProvider provider) {
         return (lines, row) -> {
@@ -1650,7 +1655,13 @@ class SourceTab extends AbstractTab {
             if (row < 0 || row >= lines.size()) {
                 return base;
             }
-            List<SourceViewer.DocEntry> extra = new 
ArrayList<>(assist.placeholderDocs(lines.get(row)));
+            List<SourceViewer.DocEntry> extra = new ArrayList<>();
+            // the body types the steps of the line have been reached with, 
while it runs as shown (CAMEL-25897)
+            SourceViewer.DocEntry bodyTypes = 
sourceViewer.liveColumn().bodyTypesDoc(row);
+            if (bodyTypes != null) {
+                extra.add(bodyTypes);
+            }
+            extra.addAll(assist.placeholderDocs(lines.get(row)));
             ProjectBeans.Location bean = projectBeans().refOn(lines.get(row));
             if (bean != null) {
                 extra.add(new 
SourceViewer.DocEntry(ProjectBeans.describe(bean), false, "Bean"));
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewer.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewer.java
index 0a06ded5bda5..f696cd38d0de 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewer.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewer.java
@@ -196,7 +196,8 @@ class SourceViewer {
             dirty = true;
         }
     });
-    private final SourceLiveColumn liveColumn = new SourceLiveColumn(() -> 
this.loadedFilePath, () -> this.dirty);
+    private final SourceLiveColumn liveColumn
+            = new SourceLiveColumn(() -> this.loadedFilePath, () -> 
this.dirty, () -> this.lines);
     private final SourceRefactorActions refactor = new 
SourceRefactorActions(editState, new SourceRefactorActions.Editor() {
         @Override
         public Path editableFile() {
@@ -2829,9 +2830,7 @@ class SourceViewer {
                         ? Span.styled(" ⚠ ", Theme.warning().bold())
                         : Span.styled(" ✗ ", Theme.error().bold()));
             } else {
-                spans.add(isDeprecated
-                        ? Span.styled(" ⚠ ", Theme.warning())
-                        : Span.raw("   "));
+                spans.add(isDeprecated ? Span.styled(" ⚠ ", Theme.warning()) : 
liveColumn.gutter(lineIndex));
             }
             if (!prefix.isEmpty()) {
                 spans.add(Span.styled(prefix, Style.EMPTY.dim()));
@@ -2848,6 +2847,7 @@ class SourceViewer {
             }
             spans.add(Span.styled(" ↵ " + jl.routeId(), linkStyle));
         }
+        liveColumn.addBodyTypes(spans, lineIndex, plainMode, isSelected ? 
selBg : null, viewportWidth - 1 + hSkip);
         Line full = Line.from(spans);
 
         if (hSkip > 0) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/source.md 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/source.md
index 20faa055005f..340c3775f65c 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/source.md
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/source.md
@@ -51,6 +51,18 @@ processors on each line do: the exchanges they handled 
(`79`), how many failed
 its width, so the code does not move as the numbers grow. The source reads as
 a heat map of the route: where messages go, where they fail.
 
+With the message history on (such as `camel run --dev`), the body types each
+step has been reached with go at the end of its line, dimmed and lined up
+(`ByteArrayInputStreamCache`, `String | byte[]`). 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: it has a `⚠` in the gutter and its
+types in the warning color (`⚠ byte[] | LinkedHashMap (payload and data)`). 
The quick
+documentation of the line lists them all.
+
+The live run data and the body types are of the version of the file the
+integration runs: while it is not running as saved (see below), the numbers
+are dimmed and the body types are left out, as the lines may be others.
+
 ## Running as Saved
 The bottom border of the source says whether the integration runs the routes
 of the file as it is saved:
@@ -61,6 +73,9 @@ of the file as it is saved:
   reload the file
 - `⚠ the reload failed` — the integration runs what it ran before the reload,
   until a reload succeeds
+- `○ the routes of this file are stopped` — stopped (or suspended) by hand: the
+  live run data and the body types still show when the integration loaded the
+  file as saved, which is not known for a route stopped after a reload
 
 It compares when the file was saved with when the integration loaded the
 routes it has from it, and shows nothing for a file it has no route from.
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceBodyTypesTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceBodyTypesTest.java
new file mode 100644
index 000000000000..3487ad7eee8e
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceBodyTypesTest.java
@@ -0,0 +1,303 @@
+/*
+ * 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.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.attribute.FileTime;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import dev.tamboui.buffer.Buffer;
+import dev.tamboui.layout.Rect;
+import dev.tamboui.terminal.Frame;
+import dev.tamboui.tui.event.KeyEvent;
+import org.apache.camel.dsl.jbang.core.commands.tui.SourceLiveColumn.LiveLine;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * The body types each step has been reached with, at the end of its line in 
the source view, with a ⚠ in the gutter of
+ * a step reached with bodies of more than one kind (CAMEL-25897).
+ */
+class SourceBodyTypesTest {
+
+    private static final String ROUTE = """
+            - route:
+                id: orders
+                from:
+                  uri: timer:orders
+                  steps:
+                    - unmarshal:
+                        json: {}
+                    - choice:
+                        when:
+                          - simple: "${body[priority]} == 1"
+                            steps:
+                              - marshal:
+                                  json: {}
+                    - to:
+                        uri: log:outbox
+            """;
+
+    private static final int UNMARSHAL = 5;
+    private static final int CHOICE = 7;
+    private static final int TO = 13;
+
+    @TempDir
+    Path dir;
+
+    private final Map<Integer, LiveLine> data = new HashMap<>();
+    private SourceViewer viewer;
+    private Path file;
+
+    @BeforeEach
+    void open() throws Exception {
+        Theme.resetForTesting();
+        file = Files.writeString(dir.resolve("orders.camel.yaml"), ROUTE);
+        viewer = new SourceViewer();
+        viewer.loadFile(file);
+        viewer.liveColumn().setLiveRunData(path -> data);
+        data.put(UNMARSHAL, 
line(List.of("org.apache.camel.converter.stream.ByteArrayInputStreamCache"), 
List.of("payload"),
+                false));
+        data.put(CHOICE, line(List.of("java.util.LinkedHashMap"), 
List.of("data"), false));
+        data.put(TO, line(List.of("byte[]", "java.util.LinkedHashMap"), 
List.of("payload", "data"), true));
+        viewer.liveColumn().refreshLiveLinesForTesting();
+    }
+
+    @Test
+    void theBodyTypesOfAStepAtTheEndOfItsLineLinedUp() {
+        String[] rows = render(120).split("\n");
+
+        String unmarshal = row(rows, "- unmarshal:");
+        String choice = row(rows, "- choice:");
+        String to = row(rows, "- to:");
+        assertThat(unmarshal).contains("ByteArrayInputStreamCache");
+        assertThat(choice).contains("LinkedHashMap");
+        assertThat(to).contains(BodyTypes.MARK + " byte[] | LinkedHashMap 
(payload and data)");
+        // in a column of their own
+        assertThat(unmarshal.indexOf("ByteArrayInputStreamCache"))
+                .isEqualTo(choice.indexOf("LinkedHashMap"))
+                .isEqualTo(to.indexOf(BodyTypes.MARK + " byte[]"));
+        // the step reached with bodies of more than one kind has a ⚠ in the 
gutter, the others not
+        assertThat(to.substring(0, 4)).contains(BodyTypes.MARK);
+        assertThat(unmarshal.substring(0, 4)).doesNotContain(BodyTypes.MARK);
+        // a line without a step has nothing
+        assertThat(row(rows, "json: 
{}")).doesNotContain("ByteArrayInputStreamCache").doesNotContain("LinkedHashMap");
+    }
+
+    @Test
+    void theTypesAreCutToTheRoomThereIs() {
+        String to = row(render(56).split("\n"), "- to:");
+
+        assertThat(to).contains(BodyTypes.MARK + " 
byte[]").doesNotContain("(payload and data)");
+    }
+
+    @Test
+    void aCutNameKeepsItsEllipsisAndTheCountItsDigits() {
+        // the last column of the view is the scrollbar's: what is cut ends 
before it
+        boolean cut = false;
+        for (int width = 44; width <= 72; width++) {
+            String[] rows = render(width).split("\n");
+            String unmarshal = row(rows, "- unmarshal:");
+            if (unmarshal.contains("ByteArr") && 
!unmarshal.contains("ByteArrayInputStreamCache")) {
+                assertThat(unmarshal).as("width %d", width).contains("…");
+                cut = true;
+            }
+            String to = row(rows, "- to:");
+            if (to.contains(" +")) {
+                assertThat(to).as("width %d", width).contains(" +1");
+            }
+        }
+        assertThat(cut).isTrue();
+    }
+
+    @Test
+    void anotherFileHasItsOwnLiveRunDataAtOnce() throws Exception {
+        Path other = Files.writeString(dir.resolve("stock.camel.yaml"), "- 
from:\n    uri: timer:stock\n");
+        Map<String, Map<Integer, LiveLine>> byFile = Map.of(
+                file.toString(), Map.of(0, new LiveLine(120, 0, 0)),
+                other.toString(), Map.of(0, new LiveLine(7, 0, 0)));
+        viewer.liveColumn().setLiveRunData(byFile::get);
+        viewer.liveColumn().refreshLiveLinesForTesting();
+        
assertThat(viewer.liveColumn().liveColumnForTesting(0)).contains("120");
+
+        // not the numbers of the file shown before for up to a second
+        viewer.loadFile(other);
+        
assertThat(viewer.liveColumn().liveColumnForTesting(0)).contains("7").doesNotContain("120");
+    }
+
+    @Test
+    void anotherFileIsStaleUntilItsStateIsKnown() throws Exception {
+        Path other = Files.writeString(dir.resolve("stock.camel.yaml"), "- 
from:\n    uri: timer:stock\n");
+        long now = System.currentTimeMillis();
+        Files.setLastModifiedTime(file, FileTime.fromMillis(now - 3_600_000));
+        IntegrationInfo info = new IntegrationInfo();
+        info.uptime = now - 60_000;
+        RouteInfo route = new RouteInfo();
+        route.routeId = "orders";
+        route.source = "orders.camel.yaml:1";
+        route.state = "Started";
+        route.uptime = "59s";
+        info.routes.add(route);
+        SourceRunState.Tracker tracker = new SourceRunState.Tracker();
+
+        // shown: computed at once
+        assertThat(tracker.shown(file.toString(), false)).isTrue();
+        assertThat(tracker.stale()).isTrue();
+        tracker.refresh(info);
+        assertThat(tracker.stale()).isFalse();
+        assertThat(tracker.shown(file.toString(), false)).isFalse();
+
+        // another file: not known until computed, then known to have no route 
of the integration
+        assertThat(tracker.shown(other.toString(), false)).isTrue();
+        assertThat(tracker.stale()).isTrue();
+        tracker.refresh(info);
+        assertThat(tracker.current()).isNull();
+        assertThat(tracker.stale()).isFalse();
+
+        // a save is computed at once too
+        assertThat(tracker.shown(other.toString(), true)).isFalse();
+        assertThat(tracker.shown(other.toString(), false)).isTrue();
+    }
+
+    @Test
+    void nothingInThePlainMode() {
+        viewer.handleKeyEvent(KeyEvent.ofChar('p'));
+
+        String rendered = render(120);
+
+        assertThat(rendered).contains("- 
unmarshal:").doesNotContain("ByteArrayInputStreamCache");
+    }
+
+    @Test
+    void nothingWhileTheIntegrationRunsAnotherVersionOfTheFile() {
+        data.put(TO, new LiveLine(10, 2, 0, List.of("byte[]", 
"java.util.LinkedHashMap"), List.of("payload", "data"), true));
+        viewer.liveColumn().refreshLiveLinesForTesting();
+        assertThat(viewer.liveColumn().failsAtRuntime(TO)).isTrue();
+
+        // saved after the integration loaded it: the lines it ran may be 
others
+        viewer.liveColumn().setStale(() -> true);
+        String[] rows = render(120).split("\n");
+
+        assertThat(row(rows, "- 
unmarshal:")).doesNotContain("ByteArrayInputStreamCache");
+        String to = row(rows, "- to:");
+        assertThat(to).doesNotContain("byte[]");
+        assertThat(to.substring(0, 4)).doesNotContain(BodyTypes.MARK);
+        // the live run data stays, dimmed, and no line is offered to fix as 
failing at runtime
+        assertThat(to).contains("10");
+        assertThat(viewer.liveColumn().failsAtRuntime(TO)).isFalse();
+        assertThat(viewer.liveColumn().bodyTypesDoc(TO)).isNull();
+    }
+
+    @Test
+    void theQuickDocumentationHasThemAll() {
+        SourceViewer.DocEntry to = viewer.liveColumn().bodyTypesDoc(TO);
+        assertThat(to.title()).isEqualTo("Body types");
+        assertThat(to.text()).isEqualTo("byte[] | LinkedHashMap   " + 
BodyTypes.MARK
+                                        + " payload and data: the body here 
depends on the path a message takes");
+
+        SourceViewer.DocEntry unmarshal = 
viewer.liveColumn().bodyTypesDoc(UNMARSHAL);
+        assertThat(unmarshal.title()).isEqualTo("Body types (payload)");
+        assertThat(unmarshal.text()).isEqualTo("ByteArrayInputStreamCache");
+
+        assertThat(viewer.liveColumn().bodyTypesDoc(UNMARSHAL + 1)).isNull();
+    }
+
+    @Test
+    void theStepsOfALineTogether() {
+        RouteInfo route = new RouteInfo();
+        route.source = "MyRoute.java:10";
+        // two steps on one line, each with a kind of its own: the line is not 
mixed
+        route.processors.add(processor("MyRoute.java:12", 
List.of("java.lang.String"), List.of("payload")));
+        route.processors.add(processor("MyRoute.java:12", 
List.of("java.util.LinkedHashMap"), List.of("data")));
+        route.processors.add(processor("MyRoute.java:14", List.of("byte[]", 
"java.util.LinkedHashMap"),
+                List.of("payload", "data")));
+        route.processors.add(processor("MyRoute.java:15", List.of(), 
List.of()));
+
+        Map<Integer, LiveLine> lines = LiveRunLines.of(List.of(route), 
"/work/MyRoute.java");
+
+        
assertThat(lines.get(11).bodyTypes()).containsExactly("java.lang.String", 
"java.util.LinkedHashMap");
+        assertThat(lines.get(11).mixed()).isFalse();
+        assertThat(lines.get(13).mixed()).isTrue();
+        assertThat(lines.get(14).bodyTypes()).isEmpty();
+        assertThat(lines.get(14)).isEqualTo(new LiveLine(0, 0, 0));
+    }
+
+    @Test
+    void aSaveInTheEditorLeavesThemOutUntilTheIntegrationIsSeenToLoadIt() 
throws Exception {
+        long now = System.currentTimeMillis();
+        Files.setLastModifiedTime(file, FileTime.fromMillis(now - 3_600_000));
+        IntegrationInfo info = new IntegrationInfo();
+        info.uptime = now - 60_000;
+        RouteInfo route = new RouteInfo();
+        route.routeId = "orders";
+        route.source = "orders.camel.yaml:1";
+        route.state = "Started";
+        route.uptime = "59s";
+        info.routes.add(route);
+        SourceRunState.Tracker tracker = new SourceRunState.Tracker();
+        tracker.shown(file.toString(), false);
+        tracker.refresh(info);
+        assertThat(tracker.stale()).isFalse();
+        
assertThat(tracker.current().kind()).isEqualTo(SourceRunState.Kind.RUNNING);
+
+        // edited, then saved
+        tracker.shown(file.toString(), true);
+        tracker.shown(file.toString(), false);
+        assertThat(tracker.stale()).isTrue();
+        assertThat(tracker.current()).isNull();
+
+        // the next refresh knows again: saved now, after the integration 
loaded the file
+        Files.setLastModifiedTime(file, FileTime.fromMillis(now));
+        tracker.refresh(info);
+        
assertThat(tracker.current().kind()).isEqualTo(SourceRunState.Kind.NOT_RUNNING);
+        assertThat(tracker.stale()).isTrue();
+    }
+
+    private String render(int width) {
+        Rect area = new Rect(0, 0, width, 24);
+        Buffer buffer = Buffer.empty(area);
+        viewer.render(Frame.forTesting(buffer), area);
+        return TuiTestHelper.bufferToString(buffer);
+    }
+
+    private static String row(String[] rows, String code) {
+        for (String row : rows) {
+            if (row.contains(code)) {
+                return row;
+            }
+        }
+        throw new AssertionError("No row with " + code);
+    }
+
+    private static LiveLine line(List<String> types, List<String> kinds, 
boolean mixed) {
+        return new LiveLine(10, 0, 0, types, kinds, mixed);
+    }
+
+    private static ProcessorInfo processor(String source, List<String> 
bodyTypes, List<String> bodyKinds) {
+        ProcessorInfo proc = new ProcessorInfo();
+        proc.source = source;
+        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/SourceRunStateTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceRunStateTest.java
index 9539dea0cc03..983dd5d9ba97 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceRunStateTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceRunStateTest.java
@@ -132,9 +132,8 @@ class SourceRunStateTest {
     }
 
     @Test
-    void onlyTheStartedRoutesOfTheFile() {
+    void onlyTheRoutesOfTheFile() {
         route("stock", "stock.camel.yaml:3", "59s");
-        route("orders", "orders.camel.yaml:3", "59s").state = "Stopped";
 
         assertThat(SourceRunState.of(info, FILE, NOW - 10_000, NOW)).isNull();
 
@@ -142,6 +141,28 @@ class SourceRunStateTest {
         assertThat(SourceRunState.of(info, "/work/README.md", NOW - 10_000, 
NOW)).isNull();
     }
 
+    @Test
+    void aFileWhoseRoutesAreStopped() {
+        RouteInfo orders = route("orders", "orders.camel.yaml:3", "");
+        orders.state = "Stopped";
+
+        // loaded when the integration started, as saved: what its lines say 
holds
+        SourceRunState.State state = SourceRunState.of(info, FILE, NOW - 
3_600_000, NOW);
+        assertThat(state.kind()).isEqualTo(SourceRunState.Kind.STOPPED);
+        assertThat(state.stale()).isZero();
+        assertThat(text(SourceRunState.line(state))).isEqualTo(TuiIcons.IDLE + 
" the routes of this file are stopped");
+        
assertThat(SourceRunState.toJson(state).getString("state")).isEqualTo("stopped");
+
+        // saved since: the saved file is not running, stopped or not
+        assertThat(SourceRunState.of(info, FILE, NOW - 10_000, 
NOW).kind()).isEqualTo(SourceRunState.Kind.NOT_RUNNING);
+
+        // stopped after a reload: when it was loaded is not known
+        info.reloaded = 1;
+        state = SourceRunState.of(info, FILE, NOW - 3_600_000, NOW);
+        assertThat(state.kind()).isEqualTo(SourceRunState.Kind.STOPPED);
+        assertThat(state.stale()).isEqualTo(1);
+    }
+
     @Test
     void theUptimeOfARoute() {
         assertThat(SourceRunState.uptimeMillis("0s")).isZero();

Reply via email to