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

davsclaus pushed a commit to branch fix/CAMEL-23698
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 4bd945ed56c43156782ec14e4f6b325a9843e211
Author: Claus Ibsen <[email protected]>
AuthorDate: Fri Jun 5 19:17:17 2026 +0200

    CAMEL-23672: camel-jbang - Depth-first reordering of trace steps in TUI 
History tab
    
    When exchanges flow through multicast, splitter, or async EIPs (kafka, 
seda),
    the BacklogTracer records events in wall-clock order causing interleaving 
between
    branches. This makes diagram stepping disorienting as it bounces between 
routes.
    
    Reorder trace steps into depth-first traversal order so each branch 
completes
    before the next begins. Child exchanges from multicast/splitter are inlined 
at
    their call points. The table indents child exchange rows (route, id, 
processor
    columns) with 2 spaces per depth level for visual hierarchy.
    
    Applies to both trace mode and history (last completed) mode.
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    
    Signed-off-by: Claus Ibsen <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../dsl/jbang/core/commands/tui/HistoryEntry.java  |   1 +
 .../dsl/jbang/core/commands/tui/HistoryTab.java    | 121 +++++++++++++++++++--
 2 files changed, 111 insertions(+), 11 deletions(-)

diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryEntry.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryEntry.java
index 638ac9c0d9d5..06339e4a7d1a 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryEntry.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryEntry.java
@@ -48,4 +48,5 @@ class HistoryEntry {
     Map<String, String> exchangePropertyTypes;
     Map<String, Object> exchangeVariables;
     Map<String, String> exchangeVariableTypes;
+    int inlineDepth;
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTab.java
index ba2d905209bc..99b23fd4fd60 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTab.java
@@ -625,7 +625,7 @@ class HistoryTab implements MonitorTab {
                 }
             }
         } else {
-            List<HistoryEntry> entries = historyEntries;
+            List<HistoryEntry> entries = 
reorderHistoryDepthFirst(historyEntries);
             if (entries.isEmpty()) {
                 diagram.endLoad();
                 return;
@@ -1023,7 +1023,7 @@ class HistoryTab implements MonitorTab {
             return;
         }
 
-        List<HistoryEntry> current = historyEntries;
+        List<HistoryEntry> current = reorderHistoryDepthFirst(historyEntries);
 
         List<Rect> chunks = Layout.vertical()
                 .constraints(Constraint.length(10), Constraint.length(1), 
Constraint.fill())
@@ -1034,7 +1034,7 @@ class HistoryTab implements MonitorTab {
         for (int i = 0; i < current.size(); i++) {
             HistoryEntry entry = current.get(i);
             String desc = showDescription ? descMap.get(entry.routeId) : null;
-            rows.add(buildStepRow(i + 1, 0,
+            rows.add(buildStepRow(i + 1, entry.inlineDepth,
                     entry.direction, entry.first, entry.last, entry.failed,
                     entry.timestamp, entry.routeId, entry.nodeId, 
entry.processor, desc, entry.elapsed));
         }
@@ -1210,9 +1210,100 @@ class HistoryTab implements MonitorTab {
         }
     }
 
+    private List<HistoryEntry> reorderHistoryDepthFirst(List<HistoryEntry> 
entries) {
+        if (entries.isEmpty()) {
+            return entries;
+        }
+
+        Map<String, List<HistoryEntry>> byExchange = new LinkedHashMap<>();
+        for (HistoryEntry e : entries) {
+            if (e.exchangeId != null) {
+                byExchange.computeIfAbsent(e.exchangeId, k -> new 
ArrayList<>()).add(e);
+            }
+        }
+
+        Map<String, List<String>> fromIndex = new LinkedHashMap<>();
+        for (var entry : byExchange.entrySet()) {
+            List<HistoryEntry> steps = entry.getValue();
+            if (!steps.isEmpty() && steps.get(0).first) {
+                String ep = extractFromEndpoint(steps.get(0).nodeLabel);
+                if (ep != null) {
+                    fromIndex.computeIfAbsent(ep, k -> new 
ArrayList<>()).add(entry.getKey());
+                }
+            }
+        }
+
+        Map<String, List<String>> branchChildren = new LinkedHashMap<>();
+        for (var entry : byExchange.entrySet()) {
+            List<HistoryEntry> steps = entry.getValue();
+            if (!steps.isEmpty() && !steps.get(0).first) {
+                HistoryEntry firstStep = steps.get(0);
+                if (firstStep.routeId != null && 
extractToEndpoint(firstStep.nodeLabel) != null) {
+                    branchChildren.computeIfAbsent(firstStep.routeId, k -> new 
ArrayList<>()).add(entry.getKey());
+                }
+            }
+        }
+
+        String rootExchangeId = entries.get(0).exchangeId;
+        if (rootExchangeId == null) {
+            return entries;
+        }
+
+        List<HistoryEntry> result = new ArrayList<>();
+        Set<String> visited = new HashSet<>();
+        inlineHistoryExchange(rootExchangeId, byExchange, fromIndex, 
branchChildren, visited, result, 0);
+        return result;
+    }
+
+    private void inlineHistoryExchange(
+            String exchangeId, Map<String, List<HistoryEntry>> byExchange,
+            Map<String, List<String>> fromIndex, Map<String, List<String>> 
branchChildren,
+            Set<String> visited, List<HistoryEntry> result, int depth) {
+        if (!visited.add(exchangeId)) {
+            return;
+        }
+        List<HistoryEntry> steps = byExchange.get(exchangeId);
+        if (steps == null) {
+            return;
+        }
+        for (HistoryEntry step : steps) {
+            step.inlineDepth = depth;
+            result.add(step);
+
+            if (isMulticastNode(step.nodeShortName)) {
+                List<String> children = branchChildren.get(step.routeId);
+                if (children != null) {
+                    for (String childId : children) {
+                        if (!visited.contains(childId)) {
+                            inlineHistoryExchange(childId, byExchange, 
fromIndex, branchChildren, visited, result,
+                                    depth + 1);
+                        }
+                    }
+                }
+            }
+
+            String targetEndpoint = extractToEndpoint(step.nodeLabel);
+            if (targetEndpoint != null) {
+                List<String> children = fromIndex.get(targetEndpoint);
+                if (children != null) {
+                    for (String childId : children) {
+                        if (!visited.contains(childId)) {
+                            inlineHistoryExchange(childId, byExchange, 
fromIndex, branchChildren, visited, result,
+                                    depth + 1);
+                        }
+                    }
+                }
+            }
+        }
+    }
+
     private static boolean isMulticastNode(TraceEntry entry) {
-        if (entry.nodeShortName != null) {
-            return switch (entry.nodeShortName) {
+        return isMulticastNode(entry.nodeShortName);
+    }
+
+    private static boolean isMulticastNode(String nodeShortName) {
+        if (nodeShortName != null) {
+            return switch (nodeShortName) {
                 case "multicast", "recipientList", "split", "routingSlip", 
"enrich", "pollEnrich",
                         "wireTap", "dynamicRouter" ->
                     true;
@@ -1223,17 +1314,25 @@ class HistoryTab implements MonitorTab {
     }
 
     private static String extractFromEndpoint(TraceEntry entry) {
-        if (entry.nodeLabel != null) {
-            return normalizeEndpoint(entry.nodeLabel);
+        return extractFromEndpoint(entry.nodeLabel);
+    }
+
+    private static String extractFromEndpoint(String nodeLabel) {
+        if (nodeLabel != null) {
+            return normalizeEndpoint(nodeLabel);
         }
         return null;
     }
 
     private static String extractToEndpoint(TraceEntry entry) {
-        if (entry.nodeLabel != null && entry.nodeLabel.startsWith("to[")) {
-            int end = entry.nodeLabel.indexOf(']');
+        return extractToEndpoint(entry.nodeLabel);
+    }
+
+    private static String extractToEndpoint(String nodeLabel) {
+        if (nodeLabel != null && nodeLabel.startsWith("to[")) {
+            int end = nodeLabel.indexOf(']');
             if (end > 3) {
-                return normalizeEndpoint(entry.nodeLabel.substring(3, end));
+                return normalizeEndpoint(nodeLabel.substring(3, end));
             }
         }
         return null;
@@ -1883,7 +1982,7 @@ class HistoryTab implements MonitorTab {
             }
         } else {
             result.put("tab", "History");
-            List<HistoryEntry> entries = historyEntries;
+            List<HistoryEntry> entries = 
reorderHistoryDepthFirst(historyEntries);
             JsonArray rows = new JsonArray();
             for (int i = 0; i < entries.size(); i++) {
                 HistoryEntry h = entries.get(i);

Reply via email to