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 4692fb6d9108fd5f3ec9bb08a090bf6e38a2701b
Author: Claus Ibsen <[email protected]>
AuthorDate: Fri Jun 5 20:00:11 2026 +0200

    CAMEL-23672: camel-jbang - TUI History tab improvements
    
    - Add BHPV change indicators column showing 
body/headers/properties/variables mutations
    - Add info panel to diagram view showing trace metadata for current step
    - Compact footer: collapse b/h/p/v toggles into single `show BHpv` hint
    - Diagram keybindings: d closes diagram, Esc navigates back in drill-down
    - Remove route column indent, widen route and ID columns
    - Fix route detail panel jitter by padding route ID to fixed width
    - Info panel respects b/h/p/v and w toggles from table view
    - Update F1 help text
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../jbang/core/commands/tui/DiagramSupport.java    |   2 +-
 .../dsl/jbang/core/commands/tui/HistoryTab.java    | 385 +++++++++++++++++++--
 2 files changed, 354 insertions(+), 33 deletions(-)

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 2f80ee640e0f..9985da120c63 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
@@ -420,7 +420,7 @@ class DiagramSupport {
     }
 
     void renderFooterHints(List<Span> spans) {
-        hint(spans, "Esc", "close");
+        hint(spans, "d/Esc", "close");
         hint(spans, "↑↓←→", "scroll");
         hint(spans, "PgUp/PgDn", "page");
         hint(spans, "Home/End", "top/end");
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 875e836f618a..1ee32be68e9f 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
@@ -25,6 +25,7 @@ import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.atomic.AtomicReference;
 
@@ -104,6 +105,9 @@ class HistoryTab implements MonitorTab {
 
     private final DiagramSupport diagram = new DiagramSupport();
 
+    private List<TraceEntry> diagramTraceSteps = Collections.emptyList();
+    private List<HistoryEntry> diagramHistorySteps = Collections.emptyList();
+
     volatile List<HistoryEntry> historyEntries = Collections.emptyList();
     private final TableState historyTableState = new TableState();
     private boolean showHistoryProperties;
@@ -184,6 +188,47 @@ class HistoryTab implements MonitorTab {
                 loadDiagramForCurrentView();
                 return true;
             }
+            boolean isTraceMode = !diagramTraceSteps.isEmpty();
+            if (ke.isChar('b')) {
+                if (isTraceMode) {
+                    showTraceBody = !showTraceBody;
+                } else {
+                    showHistoryBody = !showHistoryBody;
+                }
+                return true;
+            }
+            if (ke.isChar('h')) {
+                if (isTraceMode) {
+                    showTraceHeaders = !showTraceHeaders;
+                } else {
+                    showHistoryHeaders = !showHistoryHeaders;
+                }
+                return true;
+            }
+            if (ke.isChar('p')) {
+                if (isTraceMode) {
+                    showTraceProperties = !showTraceProperties;
+                } else {
+                    showHistoryProperties = !showHistoryProperties;
+                }
+                return true;
+            }
+            if (ke.isChar('v')) {
+                if (isTraceMode) {
+                    showTraceVariables = !showTraceVariables;
+                } else {
+                    showHistoryVariables = !showHistoryVariables;
+                }
+                return true;
+            }
+            if (ke.isChar('w')) {
+                if (isTraceMode) {
+                    traceWordWrap = !traceWordWrap;
+                } else {
+                    historyWordWrap = !historyWordWrap;
+                }
+                return true;
+            }
         }
         if (diagram.handleScrollKeys(ke)) {
             return true;
@@ -374,8 +419,10 @@ class HistoryTab implements MonitorTab {
 
     @Override
     public boolean handleEscape() {
-        if (diagram.isShowDiagram() && diagram.isHistoryMode() && 
!diagram.isHistoryTopologyMode()) {
-            diagram.historyGoBack();
+        if (diagram.isShowDiagram() && diagram.isHistoryMode()) {
+            if (!diagram.isHistoryTopologyMode()) {
+                diagram.historyGoBack();
+            }
             return true;
         }
         if (diagram.handleEscape()) {
@@ -457,16 +504,29 @@ class HistoryTab implements MonitorTab {
         }
 
         if (diagram.isShowDiagram() && diagram.isHistoryMode() && 
diagram.hasHistoryData()) {
+            Rect diagramArea = area;
+            Rect infoArea = null;
+            if (area.width() > 70) {
+                int panelWidth = 35;
+                List<Rect> hParts = Layout.horizontal()
+                        .constraints(Constraint.length(panelWidth), 
Constraint.fill())
+                        .split(area);
+                infoArea = hParts.get(0);
+                diagramArea = hParts.get(1);
+            }
             if (diagram.isHistoryTopologyMode()) {
                 Line title = Line.from(Span.styled(
                         String.format(" History Topology — step %d/%d ",
                                 diagram.getHistoryStepIndex() + 1, 
diagram.getHistoryStepCount()),
                         Style.EMPTY.fg(Color.WHITE)));
-                diagram.renderHistoryTopologyDiagram(frame, area, title);
+                diagram.renderHistoryTopologyDiagram(frame, diagramArea, 
title);
             } else {
                 String routeId = diagram.getHistoryDrillDownRouteId();
                 Line title = buildHistoryBreadcrumbTitle();
-                diagram.renderHistoryRouteDiagram(frame, area, title, routeId);
+                diagram.renderHistoryRouteDiagram(frame, diagramArea, title, 
routeId);
+            }
+            if (infoArea != null) {
+                renderDiagramInfoPanel(frame, infoArea);
             }
             return;
         }
@@ -494,17 +554,28 @@ class HistoryTab implements MonitorTab {
     public void renderFooter(List<Span> spans) {
         if (diagram.isShowDiagram()) {
             if (diagram.isHistoryMode() && diagram.hasHistoryData()) {
+                boolean isTraceMode = !diagramTraceSteps.isEmpty();
+                boolean sb = isTraceMode ? showTraceBody : showHistoryBody;
+                boolean sh = isTraceMode ? showTraceHeaders : 
showHistoryHeaders;
+                boolean sp = isTraceMode ? showTraceProperties : 
showHistoryProperties;
+                boolean sv = isTraceMode ? showTraceVariables : 
showHistoryVariables;
+                boolean sw = isTraceMode ? traceWordWrap : historyWordWrap;
                 if (diagram.isHistoryTopologyMode()) {
-                    hint(spans, "Esc", "close");
+                    hint(spans, "d", "close");
                     hint(spans, "↑↓←→", "navigate");
                     hint(spans, "Enter", "drill-down");
                     hint(spans, "n", "description" + (showDescription ? " 
[on]" : ""));
+                    hintShowBhpv(spans, sb, sh, sp, sv);
+                    hintLast(spans, "w", "wrap" + (sw ? " [on]" : " [off]"));
                 } else {
+                    hint(spans, "d", "close");
                     hint(spans, "Esc", "back");
                     hint(spans, "↑↓", "step through path");
                     hint(spans, "←→", "h-scroll");
                     hint(spans, "t", "topology");
                     hint(spans, "n", "description" + (showDescription ? " 
[on]" : ""));
+                    hintShowBhpv(spans, sb, sh, sp, sv);
+                    hintLast(spans, "w", "wrap" + (sw ? " [on]" : " [off]"));
                 }
                 return;
             }
@@ -523,10 +594,7 @@ class HistoryTab implements MonitorTab {
             hint(spans, "g", "waterfall" + (showWaterfall ? " [on]" : ""));
             hint(spans, "d", "diagram");
             if (!showWaterfall) {
-                hint(spans, "p", "properties" + (showTraceProperties ? " [on]" 
: " [off]"));
-                hint(spans, "v", "variables" + (showTraceVariables ? " [on]" : 
" [off]"));
-                hint(spans, "h", "headers" + (showTraceHeaders ? " [on]" : " 
[off]"));
-                hint(spans, "b", "body" + (showTraceBody ? " [on]" : " 
[off]"));
+                hintShowBhpv(spans, showTraceBody, showTraceHeaders, 
showTraceProperties, showTraceVariables);
             }
             hintLast(spans, "w", "wrap" + (traceWordWrap ? " [on]" : " 
[off]"));
         } else if (tracerActive) {
@@ -548,10 +616,7 @@ class HistoryTab implements MonitorTab {
             hint(spans, "g", "waterfall" + (showWaterfall ? " [on]" : ""));
             hint(spans, "d", "diagram");
             if (!showWaterfall) {
-                hint(spans, "p", "properties" + (showHistoryProperties ? " 
[on]" : " [off]"));
-                hint(spans, "v", "variables" + (showHistoryVariables ? " [on]" 
: " [off]"));
-                hint(spans, "h", "headers" + (showHistoryHeaders ? " [on]" : " 
[off]"));
-                hint(spans, "b", "body" + (showHistoryBody ? " [on]" : " 
[off]"));
+                hintShowBhpv(spans, showHistoryBody, showHistoryHeaders, 
showHistoryProperties, showHistoryVariables);
                 hint(spans, "w", "wrap" + (historyWordWrap ? " [on]" : " 
[off]"));
             }
             hintLast(spans, "F5", "refresh");
@@ -578,6 +643,181 @@ class HistoryTab implements MonitorTab {
         return Line.from(spans);
     }
 
+    private void renderDiagramInfoPanel(Frame frame, Rect area) {
+        int stepIdx = diagram.getHistoryStepIndex();
+        List<Line> lines = new ArrayList<>();
+
+        String exchangeId = null;
+        String routeId = null;
+        String nodeId = null;
+        String processor = null;
+        String direction = null;
+        String timestamp = null;
+        String threadName = null;
+        long elapsed = -1;
+        boolean failed = false;
+        String body = null;
+        String bodyType = null;
+        String exception = null;
+        Map<String, Object> headers = null;
+        Map<String, Object> properties = null;
+        Map<String, Object> variables = null;
+
+        if (!diagramTraceSteps.isEmpty() && stepIdx >= 0 && stepIdx < 
diagramTraceSteps.size()) {
+            TraceEntry e = diagramTraceSteps.get(stepIdx);
+            exchangeId = e.exchangeId;
+            routeId = e.routeId;
+            nodeId = e.nodeId;
+            processor = e.processor;
+            direction = e.direction;
+            timestamp = e.timestamp;
+            threadName = e.threadName;
+            elapsed = e.elapsed;
+            failed = e.failed;
+            body = e.body;
+            bodyType = e.bodyType;
+            exception = e.exception;
+            headers = e.headers;
+            properties = e.exchangeProperties;
+            variables = e.exchangeVariables;
+        } else if (!diagramHistorySteps.isEmpty() && stepIdx >= 0 && stepIdx < 
diagramHistorySteps.size()) {
+            HistoryEntry e = diagramHistorySteps.get(stepIdx);
+            exchangeId = e.exchangeId;
+            routeId = e.routeId;
+            nodeId = e.nodeId;
+            processor = e.processor;
+            direction = e.direction;
+            timestamp = e.timestamp;
+            threadName = e.threadName;
+            elapsed = e.elapsed;
+            failed = e.failed;
+            body = e.body;
+            bodyType = e.bodyType;
+            exception = e.exception;
+            headers = e.headers;
+            properties = e.exchangeProperties;
+            variables = e.exchangeVariables;
+        }
+
+        if (exchangeId == null) {
+            frame.renderWidget(
+                    Paragraph.builder()
+                            .text(Text.from(Line.from(Span.styled("No step 
selected", Style.EMPTY.dim()))))
+                            
.block(Block.builder().borderType(BorderType.ROUNDED).title(" Info ").build())
+                            .build(),
+                    area);
+            return;
+        }
+
+        List<Span> stepSpans = new ArrayList<>();
+        stepSpans.add(Span.styled(" Step:     ", 
Style.EMPTY.fg(Color.YELLOW).bold()));
+        stepSpans.add(Span.raw(String.format("%d/%d", stepIdx + 1, 
diagram.getHistoryStepCount())));
+        if (direction != null && !direction.isBlank()) {
+            Style dirStyle = failed ? Style.EMPTY.fg(Color.LIGHT_RED) : 
Style.EMPTY.fg(Color.GREEN);
+            stepSpans.add(Span.raw(" "));
+            stepSpans.add(Span.styled(direction, dirStyle));
+        }
+        lines.add(Line.from(stepSpans));
+        lines.add(Line.from(
+                Span.styled(" Exchange: ", 
Style.EMPTY.fg(Color.YELLOW).bold()),
+                Span.raw(exchangeId)));
+        lines.add(Line.from(
+                Span.styled(" Route:    ", 
Style.EMPTY.fg(Color.YELLOW).bold()),
+                Span.styled(routeId != null ? routeId : "", 
Style.EMPTY.fg(Color.CYAN))));
+        lines.add(Line.from(
+                Span.styled(" Node:     ", 
Style.EMPTY.fg(Color.YELLOW).bold()),
+                Span.raw(nodeId != null ? nodeId : "")));
+        if (processor != null) {
+            lines.add(Line.from(
+                    Span.styled(" Processor:", 
Style.EMPTY.fg(Color.YELLOW).bold()),
+                    Span.raw(" " + processor.strip())));
+        }
+        if (elapsed >= 0) {
+            lines.add(Line.from(
+                    Span.styled(" Elapsed:  ", 
Style.EMPTY.fg(Color.YELLOW).bold()),
+                    Span.raw(elapsed + "ms")));
+        }
+        if (timestamp != null) {
+            lines.add(Line.from(
+                    Span.styled(" Time:     ", 
Style.EMPTY.fg(Color.YELLOW).bold()),
+                    Span.raw(timestamp)));
+        }
+        if (threadName != null) {
+            lines.add(Line.from(
+                    Span.styled(" Thread:   ", 
Style.EMPTY.fg(Color.YELLOW).bold()),
+                    Span.raw(threadName)));
+        }
+        if (failed) {
+            lines.add(Line.from(
+                    Span.styled(" Status:   ", 
Style.EMPTY.fg(Color.YELLOW).bold()),
+                    Span.styled("Failed", 
Style.EMPTY.fg(Color.LIGHT_RED).bold())));
+        }
+
+        boolean isTraceMode = !diagramTraceSteps.isEmpty();
+        boolean showBody = isTraceMode ? showTraceBody : showHistoryBody;
+        boolean showHeaders = isTraceMode ? showTraceHeaders : 
showHistoryHeaders;
+        boolean showProps = isTraceMode ? showTraceProperties : 
showHistoryProperties;
+        boolean showVars = isTraceMode ? showTraceVariables : 
showHistoryVariables;
+
+        if (exception != null && !exception.isBlank()) {
+            lines.add(Line.from(Span.raw("")));
+            lines.add(Line.from(Span.styled(" Exception", 
Style.EMPTY.fg(Color.LIGHT_RED).bold())));
+            lines.add(Line.from(Span.raw(" " + exception)));
+        }
+
+        if (showBody && body != null) {
+            lines.add(Line.from(Span.raw("")));
+            lines.add(Line.from(
+                    Span.styled(" Body", Style.EMPTY.fg(Color.GREEN).bold()),
+                    bodyType != null ? Span.styled(" (" + bodyType + ")", 
Style.EMPTY.dim()) : Span.raw("")));
+            for (String line : body.split("\n")) {
+                lines.add(Line.from(Span.raw(" " + line)));
+            }
+        }
+
+        if (showHeaders && headers != null && !headers.isEmpty()) {
+            lines.add(Line.from(Span.raw("")));
+            lines.add(Line.from(Span.styled(" Headers", 
Style.EMPTY.fg(Color.GREEN).bold())));
+            for (var entry : headers.entrySet()) {
+                String val = entry.getValue() != null ? 
entry.getValue().toString() : "null";
+                lines.add(Line.from(
+                        Span.styled(" " + entry.getKey(), 
Style.EMPTY.fg(Color.CYAN)),
+                        Span.raw(" = " + val)));
+            }
+        }
+
+        if (showProps && properties != null && !properties.isEmpty()) {
+            lines.add(Line.from(Span.raw("")));
+            lines.add(Line.from(Span.styled(" Properties", 
Style.EMPTY.fg(Color.GREEN).bold())));
+            for (var entry : properties.entrySet()) {
+                String val = entry.getValue() != null ? 
entry.getValue().toString() : "null";
+                lines.add(Line.from(
+                        Span.styled(" " + entry.getKey(), 
Style.EMPTY.fg(Color.CYAN)),
+                        Span.raw(" = " + val)));
+            }
+        }
+
+        if (showVars && variables != null && !variables.isEmpty()) {
+            lines.add(Line.from(Span.raw("")));
+            lines.add(Line.from(Span.styled(" Variables", 
Style.EMPTY.fg(Color.GREEN).bold())));
+            for (var entry : variables.entrySet()) {
+                String val = entry.getValue() != null ? 
entry.getValue().toString() : "null";
+                lines.add(Line.from(
+                        Span.styled(" " + entry.getKey(), 
Style.EMPTY.fg(Color.CYAN)),
+                        Span.raw(" = " + val)));
+            }
+        }
+
+        boolean wordWrap = !diagramTraceSteps.isEmpty() ? traceWordWrap : 
historyWordWrap;
+        Paragraph.Builder pb = Paragraph.builder()
+                .text(Text.from(lines))
+                .block(Block.builder().borderType(BorderType.ROUNDED).title(" 
Info ").build());
+        if (wordWrap) {
+            pb.overflow(Overflow.WRAP_WORD);
+        }
+        frame.renderWidget(pb.build(), area);
+    }
+
     // ---- Diagram loading ----
 
     private void loadDiagramForCurrentView() {
@@ -611,6 +851,8 @@ class HistoryTab implements MonitorTab {
                 diagram.endLoad();
                 return;
             }
+            diagramTraceSteps = steps;
+            diagramHistorySteps = Collections.emptyList();
             messageHistory = new String[steps.size()];
             for (int i = 0; i < steps.size(); i++) {
                 TraceEntry e = steps.get(i);
@@ -630,6 +872,8 @@ class HistoryTab implements MonitorTab {
                 diagram.endLoad();
                 return;
             }
+            diagramHistorySteps = entries;
+            diagramTraceSteps = Collections.emptyList();
             messageHistory = new String[entries.size()];
             for (int i = 0; i < entries.size(); i++) {
                 HistoryEntry e = entries.get(i);
@@ -779,10 +1023,12 @@ class HistoryTab implements MonitorTab {
         List<Row> rows = new ArrayList<>();
         for (int i = 0; i < steps.size(); i++) {
             TraceEntry entry = steps.get(i);
+            TraceEntry prev = i > 0 ? steps.get(i - 1) : null;
             String desc = showDescription ? descMap.get(entry.routeId) : null;
+            String changes = computeTraceChanges(prev, entry);
             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));
+                    entry.timestamp, entry.routeId, entry.nodeId, 
entry.processor, desc, entry.elapsed, changes));
         }
 
         String stepTitle = String.format(" Trace [%s] — %d steps ", 
truncate(traceSelectedExchangeId, 30), steps.size());
@@ -1038,10 +1284,12 @@ class HistoryTab implements MonitorTab {
         List<Row> rows = new ArrayList<>();
         for (int i = 0; i < current.size(); i++) {
             HistoryEntry entry = current.get(i);
+            HistoryEntry prev = i > 0 ? current.get(i - 1) : null;
             String desc = showDescription ? descMap.get(entry.routeId) : null;
+            String changes = computeHistoryChanges(prev, entry);
             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));
+                    entry.timestamp, entry.routeId, entry.nodeId, 
entry.processor, desc, entry.elapsed, changes));
         }
 
         Title historyTitle = buildHistoryTitle(current);
@@ -1367,6 +1615,16 @@ class HistoryTab implements MonitorTab {
         return allChildren;
     }
 
+    private static void hintShowBhpv(List<Span> spans, boolean body, boolean 
headers, boolean props, boolean vars) {
+        spans.add(Span.styled(" show", HINT_KEY_STYLE));
+        spans.add(Span.raw(" "));
+        spans.add(Span.styled(body ? "B" : "b", body ? 
Style.EMPTY.fg(Color.WHITE).bold() : Style.EMPTY.dim()));
+        spans.add(Span.styled(headers ? "H" : "h", headers ? 
Style.EMPTY.fg(Color.WHITE).bold() : Style.EMPTY.dim()));
+        spans.add(Span.styled(props ? "P" : "p", props ? 
Style.EMPTY.fg(Color.WHITE).bold() : Style.EMPTY.dim()));
+        spans.add(Span.styled(vars ? "V" : "v", vars ? 
Style.EMPTY.fg(Color.WHITE).bold() : Style.EMPTY.dim()));
+        spans.add(Span.raw("  "));
+    }
+
     private String traceSortLabel(String label, String column) {
         return MonitorContext.sortLabel(label, column, traceSort, 
traceSortReversed);
     }
@@ -1379,7 +1637,7 @@ class HistoryTab implements MonitorTab {
             int stepNumber, int inlineDepth,
             String direction, boolean first, boolean last, boolean failed,
             String timestamp, String routeId, String nodeId, String processor,
-            String description, long elapsed) {
+            String description, long elapsed, String changes) {
         Style dirStyle;
         if (first || last || !direction.isBlank()) {
             dirStyle = failed ? Style.EMPTY.fg(Color.LIGHT_RED) : 
Style.EMPTY.fg(Color.GREEN);
@@ -1389,16 +1647,58 @@ class HistoryTab implements MonitorTab {
         String elapsedStr = elapsed >= 0 ? elapsed + "ms" : "";
         String display = description != null ? description : (processor != 
null ? processor : "");
         String indent = inlineDepth > 0 ? "  ".repeat(inlineDepth) : "";
+        List<Span> changeSpans = buildChangeSpans(changes);
         return Row.from(
                 rightCell(String.valueOf(stepNumber), 3),
                 Cell.from(Span.styled(direction, dirStyle)),
                 Cell.from(timestamp != null ? truncate(timestamp, 12) : ""),
-                Cell.from(Span.styled(indent + (routeId != null ? 
truncate(routeId, 25) : ""), Style.EMPTY.fg(Color.CYAN))),
-                Cell.from(indent + (nodeId != null ? truncate(nodeId, 15) : 
"")),
+                Cell.from(Span.styled(routeId != null ? truncate(routeId, 25) 
: "", Style.EMPTY.fg(Color.CYAN))),
+                Cell.from(indent + (nodeId != null ? truncate(nodeId, 25) : 
"")),
                 Cell.from(indent + display),
+                Cell.from(Line.from(changeSpans)),
                 rightCell(elapsedStr, 10));
     }
 
+    private static List<Span> buildChangeSpans(String changes) {
+        if (changes == null || changes.isEmpty()) {
+            return List.of(Span.raw(""));
+        }
+        List<Span> spans = new ArrayList<>();
+        for (int i = 0; i < changes.length(); i++) {
+            char c = changes.charAt(i);
+            if (c == ' ') {
+                spans.add(Span.styled(String.valueOf(c), Style.EMPTY.dim()));
+            } else {
+                spans.add(Span.styled(String.valueOf(c), 
Style.EMPTY.fg(Color.YELLOW)));
+            }
+        }
+        return spans;
+    }
+
+    static String computeTraceChanges(TraceEntry prev, TraceEntry curr) {
+        if (prev == null) {
+            return "";
+        }
+        StringBuilder sb = new StringBuilder();
+        sb.append(!Objects.equals(prev.body, curr.body) ? 'B' : ' ');
+        sb.append(!Objects.equals(prev.headers, curr.headers) ? 'H' : ' ');
+        sb.append(!Objects.equals(prev.exchangeProperties, 
curr.exchangeProperties) ? 'P' : ' ');
+        sb.append(!Objects.equals(prev.exchangeVariables, 
curr.exchangeVariables) ? 'V' : ' ');
+        return sb.toString().isBlank() ? "" : sb.toString();
+    }
+
+    static String computeHistoryChanges(HistoryEntry prev, HistoryEntry curr) {
+        if (prev == null) {
+            return "";
+        }
+        StringBuilder sb = new StringBuilder();
+        sb.append(!Objects.equals(prev.body, curr.body) ? 'B' : ' ');
+        sb.append(!Objects.equals(prev.headers, curr.headers) ? 'H' : ' ');
+        sb.append(!Objects.equals(prev.exchangeProperties, 
curr.exchangeProperties) ? 'P' : ' ');
+        sb.append(!Objects.equals(prev.exchangeVariables, 
curr.exchangeVariables) ? 'V' : ' ');
+        return sb.toString().isBlank() ? "" : sb.toString();
+    }
+
     private static Table buildStepTable(List<Row> rows, Object title, boolean 
descriptionMode) {
         Row header = Row.from(
                 rightCell("#", 3, Style.EMPTY.bold()),
@@ -1407,6 +1707,7 @@ class HistoryTab implements MonitorTab {
                 Cell.from(Span.styled("ROUTE", Style.EMPTY.bold())),
                 Cell.from(Span.styled("ID", Style.EMPTY.bold())),
                 Cell.from(Span.styled(descriptionMode ? "DESCRIPTION" : 
"PROCESSOR", Style.EMPTY.bold())),
+                Cell.from(Span.styled("BHPV", Style.EMPTY.bold())),
                 rightCell("ELAPSED", 10, Style.EMPTY.bold()));
         Block block = title instanceof Title t
                 ? 
Block.builder().borderType(BorderType.ROUNDED).title(t).build()
@@ -1418,9 +1719,10 @@ class HistoryTab implements MonitorTab {
                         Constraint.length(3),
                         Constraint.length(4),
                         Constraint.length(12),
-                        Constraint.length(15),
-                        Constraint.length(15),
+                        Constraint.length(25),
+                        Constraint.length(25),
                         Constraint.fill(),
+                        Constraint.length(4),
                         Constraint.length(10))
                 .highlightStyle(Style.EMPTY.fg(Color.WHITE).bold().onBlue())
                 .highlightSpacing(Table.HighlightSpacing.ALWAYS)
@@ -1468,7 +1770,7 @@ class HistoryTab implements MonitorTab {
                 Span.raw(exchangeId != null ? exchangeId : "")));
         lines.add(Line.from(
                 Span.styled(" Route:    ", 
Style.EMPTY.fg(Color.YELLOW).bold()),
-                Span.raw(routeId != null ? routeId : ""),
+                Span.raw(String.format("%-25s", routeId != null ? routeId : 
"")),
                 Span.styled("  Node: ", Style.EMPTY.fg(Color.YELLOW).bold()),
                 Span.raw(nodeId != null ? nodeId : ""),
                 Span.raw(nodeLabel != null ? " (" + nodeLabel + ")" : "")));
@@ -1753,20 +2055,38 @@ class HistoryTab implements MonitorTab {
                 took:
 
                 ```
-                      RouteId        NodeId     Processor            Elapsed
-                 *->  timer-to-log   timer1     from[timer:hello]    0ms
-                      timer-to-log   setBody1   setBody[simple]      0ms
-                      timer-to-log   choice1    choice               0ms
-                      timer-to-log   when1      when[simple]         0ms
-                 --->  timer-to-log   to1       to[kafka:orders]     2ms
-                      timer-to-log   log1       log[HIGH: ${body}]   0ms
-                 <-*  timer-to-log   timer1     from[timer:hello]    3ms
+                      RouteId        NodeId     Processor            BHPV  
Elapsed
+                 *->  timer-to-log   timer1     from[timer:hello]          0ms
+                      timer-to-log   setBody1   setBody[simple]      B     0ms
+                      timer-to-log   choice1    choice                     0ms
+                      timer-to-log   when1      when[simple]               0ms
+                 --->  timer-to-log   to1       to[kafka:orders]      H    2ms
+                      timer-to-log   log1       log[HIGH: ${body}]         0ms
+                 <-*  timer-to-log   timer1     from[timer:hello]          3ms
                 ```
 
                 This tells you the message entered via the timer, went through 
setBody,
                 reached a choice node, matched the `when` condition, and was 
logged.
                 The elapsed time for each step helps identify bottlenecks.
 
+                **Change Indicators (BHPV)** — The BHPV column shows at a 
glance
+                which parts of the exchange were modified at each step 
compared to
+                the previous step:
+
+                - `B` — Body changed
+                - `H` — Headers changed
+                - `P` — Exchange properties changed
+                - `V` — Exchange variables changed
+
+                Steps with no changes leave the column blank, so mutations 
stand
+                out visually.
+
+                **Depth-first ordering** — When an exchange spans multiple 
routes
+                via async EIPs (multicast, splitter, recipientList), child 
exchange
+                steps are inlined under the parent step that triggered them, 
indented
+                with 2 spaces per depth level. This keeps the logical flow 
readable
+                instead of interleaving concurrent branches.
+
                 ## Direction Arrows
 
                 The first column shows direction arrows that indicate the type
@@ -1820,13 +2140,15 @@ class HistoryTab implements MonitorTab {
                 large routes. This is the same minimap available on the Routes 
and
                 Diagram tabs.
 
-                Press `Esc` to close the diagram and return to the exchange 
list.
+                Press `d` to close the diagram and return to the table.
+                Press `Esc` to navigate back one route in drill-down mode.
 
                 ## Keys
 
                 - `Up/Down` — select exchange (or step through path in diagram)
                 - `Enter` — view exchange details
-                - `d` — toggle route diagram
+                - `d` — toggle route diagram (open and close)
+                - `Esc` — back to list / back one route in diagram drill-down
                 - `n` — toggle description mode
                 - `g` — toggle waterfall view
                 - `h` — toggle headers
@@ -1839,7 +2161,6 @@ class HistoryTab implements MonitorTab {
                 - `Left/Right` — horizontal scroll (diagram or detail)
                 - `PgUp/PgDn` — page scroll
                 - `F5` — refresh data
-                - `Esc` — back to list / close diagram
                 """;
     }
 

Reply via email to