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 9dba327a39b2 CAMEL-23517: Add History tab to TUI monitor (#23225)
9dba327a39b2 is described below

commit 9dba327a39b24642b9abed32edde8689595809d5
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu May 14 21:30:21 2026 +0200

    CAMEL-23517: Add History tab to TUI monitor (#23225)
    
    Add a new History tab (tab 6) to the TUI monitor that shows the last
    message history, similar to the CLI 'camel get history' command.
    Reads from the pid-history.json file on demand (F5 or tab switch).
    
    Features:
    - Direction arrows colored: --> green (first), <-- green/red (last)
    - Processor column indented by nodeLevel for tree structure
    - Columns: direction, time, route, node ID, processor, elapsed
    - Detail panel: exchange info, headers with type prefix, body with type, 
exception
    - Header types use shortTypeName (same logic as CLI camel get history)
    - Body type shown dimmed: Body: (String)
    - Styled title: status green/red, elapsed, ago (computed from first entry 
timestamp)
    - h/b toggles for headers/body display
    - F5 to refresh, data loaded on tab switch
    - Fixed 8-row table height for larger detail panel
    
    Signed-off-by: Claus Ibsen <[email protected]>
    Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
---
 .../dsl/jbang/core/commands/tui/CamelMonitor.java  | 477 ++++++++++++++++++++-
 .../dsl/jbang/core/commands/tui/TuiHelper.java     |  26 ++
 2 files changed, 494 insertions(+), 9 deletions(-)

diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
index 07f3419d43a2..9fa43dfb3209 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
@@ -43,6 +43,7 @@ import dev.tamboui.image.ImageData;
 import dev.tamboui.image.ImageScaling;
 import dev.tamboui.image.capability.TerminalImageCapabilities;
 import dev.tamboui.image.protocol.ImageProtocol;
+import dev.tamboui.layout.Alignment;
 import dev.tamboui.layout.Constraint;
 import dev.tamboui.layout.Layout;
 import dev.tamboui.layout.Rect;
@@ -64,6 +65,7 @@ import dev.tamboui.widgets.barchart.BarChart;
 import dev.tamboui.widgets.barchart.BarGroup;
 import dev.tamboui.widgets.block.Block;
 import dev.tamboui.widgets.block.BorderType;
+import dev.tamboui.widgets.block.Title;
 import dev.tamboui.widgets.gauge.Gauge;
 import dev.tamboui.widgets.paragraph.Paragraph;
 import dev.tamboui.widgets.scrollbar.Scrollbar;
@@ -103,7 +105,7 @@ public class CamelMonitor extends CamelCommand {
     private static final int MAX_SPARKLINE_POINTS = 60;
     private static final int MAX_LOG_LINES = 200;
     private static final int MAX_TRACES = 200;
-    private static final int NUM_TABS = 6;
+    private static final int NUM_TABS = 7;
 
     // Tab indices
     private static final int TAB_OVERVIEW = 0;
@@ -111,7 +113,8 @@ public class CamelMonitor extends CamelCommand {
     private static final int TAB_LOG = 2;
     private static final int TAB_ENDPOINTS = 3;
     private static final int TAB_HEALTH = 4;
-    private static final int TAB_TRACE = 5;
+    private static final int TAB_HISTORY = 5;
+    private static final int TAB_TRACE = 6;
 
     // Overview sort columns
     private static final String[] OVERVIEW_SORT_COLUMNS = { "pid", "name", 
"status", "total", "fail" };
@@ -175,6 +178,14 @@ public class CamelMonitor extends CamelCommand {
     private boolean showTraceBody = true;
     private boolean traceFollowMode = true;
 
+    // History state
+    private volatile List<HistoryEntry> historyEntries = 
Collections.emptyList();
+    private final TableState historyTableState = new TableState();
+    private boolean showHistoryHeaders = true;
+    private boolean showHistoryBody = true;
+    private boolean historyWordWrap;
+    private int historyDetailScroll;
+
     // Selected integration for detail views
     private String selectedPid;
 
@@ -272,6 +283,9 @@ public class CamelMonitor extends CamelCommand {
                 return handleTabKey(TAB_HEALTH);
             }
             if (ke.isChar('6')) {
+                return handleTabKey(TAB_HISTORY);
+            }
+            if (ke.isChar('7')) {
                 return handleTabKey(TAB_TRACE);
             }
 
@@ -305,7 +319,7 @@ public class CamelMonitor extends CamelCommand {
                 }
                 return true;
             }
-            if (ke.isPageUp()) {
+            if (ke.isPageUp() || ke.isKey(KeyCode.PAGE_UP)) {
                 if (showDiagram && tab == TAB_ROUTES) {
                     diagramScroll = Math.max(0, diagramScroll - 20);
                 } else if (tab == TAB_LOG) {
@@ -313,16 +327,20 @@ public class CamelMonitor extends CamelCommand {
                     for (int i = 0; i < 20; i++) {
                         logTableState.selectPrevious();
                     }
+                } else if (tab == TAB_HISTORY) {
+                    historyDetailScroll = Math.max(0, historyDetailScroll - 5);
                 }
                 return true;
             }
-            if (ke.isPageDown()) {
+            if (ke.isPageDown() || ke.isKey(KeyCode.PAGE_DOWN)) {
                 if (showDiagram && tab == TAB_ROUTES) {
                     diagramScroll += 20;
                 } else if (tab == TAB_LOG) {
                     for (int i = 0; i < 20; i++) {
                         logTableState.selectNext(filteredLogEntries.size());
                     }
+                } else if (tab == TAB_HISTORY) {
+                    historyDetailScroll += 5;
                 }
                 return true;
             }
@@ -478,6 +496,29 @@ public class CamelMonitor extends CamelCommand {
                     return true;
                 }
             }
+
+            // History tab: headers/body toggle and refresh
+            if (tab == TAB_HISTORY) {
+                if (ke.isCharIgnoreCase('h')) {
+                    showHistoryHeaders = !showHistoryHeaders;
+                    return true;
+                }
+                if (ke.isCharIgnoreCase('b')) {
+                    showHistoryBody = !showHistoryBody;
+                    return true;
+                }
+                if (ke.isCharIgnoreCase('w')) {
+                    historyWordWrap = !historyWordWrap;
+                    historyDetailScroll = 0;
+                    return true;
+                }
+                if (ke.isKey(KeyCode.F5)) {
+                    if (selectedPid != null) {
+                        
refreshHistoryData(List.of(Long.parseLong(selectedPid)));
+                    }
+                    return true;
+                }
+            }
         }
         if (event instanceof TickEvent) {
             long now = System.currentTimeMillis();
@@ -501,6 +542,12 @@ public class CamelMonitor extends CamelCommand {
         } else {
             selectedPid = null;
         }
+        if (tab == TAB_HISTORY && selectedPid != null) {
+            refreshHistoryData(List.of(Long.parseLong(selectedPid)));
+            if (!historyEntries.isEmpty()) {
+                historyTableState.select(0);
+            }
+        }
         tabsState.select(tab);
         return true;
     }
@@ -532,6 +579,10 @@ public class CamelMonitor extends CamelCommand {
                 traceFollowMode = false;
                 traceTableState.selectPrevious();
             }
+            case TAB_HISTORY -> {
+                historyTableState.selectPrevious();
+                historyDetailScroll = 0;
+            }
         }
     }
 
@@ -556,6 +607,10 @@ public class CamelMonitor extends CamelCommand {
                 List<TraceEntry> current = traces.get();
                 traceTableState.selectNext(current.size());
             }
+            case TAB_HISTORY -> {
+                historyTableState.selectNext(historyEntries.size());
+                historyDetailScroll = 0;
+            }
         }
     }
 
@@ -610,7 +665,8 @@ public class CamelMonitor extends CamelCommand {
                         " 3 Log" + sel + " ",
                         " 4 Endpoints" + sel + " ",
                         " 5 Health" + sel + " ",
-                        " 6 Trace" + sel + " ")
+                        " 6 History" + sel + " ",
+                        " 7 Trace" + sel + " ")
                 .highlightStyle(Style.EMPTY.fg(Color.rgb(0xF6, 0x91, 
0x23)).bold())
                 .divider(Span.styled(" | ", Style.EMPTY.dim()))
                 .build();
@@ -626,6 +682,7 @@ public class CamelMonitor extends CamelCommand {
             case TAB_ENDPOINTS -> renderEndpoints(frame, area);
             case TAB_LOG -> renderLog(frame, area);
             case TAB_TRACE -> renderTrace(frame, area);
+            case TAB_HISTORY -> renderHistory(frame, area);
         }
     }
 
@@ -2084,6 +2141,244 @@ public class CamelMonitor extends CamelCommand {
         frame.renderWidget(detail, area);
     }
 
+    // ---- Tab 7: History ----
+
+    private void renderHistory(Frame frame, Rect area) {
+        IntegrationInfo info = findSelectedIntegration();
+        if (info == null) {
+            renderNoSelection(frame, area);
+            return;
+        }
+
+        List<HistoryEntry> current = historyEntries;
+
+        // Layout: history list (fixed 6 rows + header + borders) + detail 
panel (fill)
+        List<Rect> chunks = Layout.vertical()
+                .constraints(Constraint.length(10), Constraint.fill())
+                .split(area);
+
+        // History list
+        List<Row> rows = new ArrayList<>();
+        for (HistoryEntry entry : current) {
+            Style dirStyle;
+            if (entry.first) {
+                dirStyle = Style.EMPTY.fg(Color.GREEN);
+            } else if (entry.last) {
+                dirStyle = entry.failed ? Style.EMPTY.fg(Color.RED) : 
Style.EMPTY.fg(Color.GREEN);
+            } else {
+                dirStyle = Style.EMPTY;
+            }
+            String elapsed = entry.elapsed >= 0 ? entry.elapsed + "ms" : "";
+
+            rows.add(Row.from(
+                    Cell.from(Span.styled(entry.direction, dirStyle)),
+                    Cell.from(entry.timestamp != null ? 
truncate(entry.timestamp, 12) : ""),
+                    Cell.from(Span.styled(
+                            entry.routeId != null ? truncate(entry.routeId, 
15) : "",
+                            Style.EMPTY.fg(Color.CYAN))),
+                    Cell.from(entry.nodeId != null ? truncate(entry.nodeId, 
15) : ""),
+                    Cell.from(entry.processor != null ? entry.processor : ""),
+                    Cell.from(elapsed)));
+        }
+
+        Row header = Row.from(
+                Cell.from(Span.styled("", Style.EMPTY.bold())),
+                Cell.from(Span.styled("TIME", Style.EMPTY.bold())),
+                Cell.from(Span.styled("ROUTE", Style.EMPTY.bold())),
+                Cell.from(Span.styled("ID", Style.EMPTY.bold())),
+                Cell.from(Span.styled("PROCESSOR", Style.EMPTY.bold())),
+                Cell.from(Span.styled("ELAPSED", Style.EMPTY.bold())));
+
+        Title historyTitle = buildHistoryTitle(current);
+
+        Table table = Table.builder()
+                .rows(rows)
+                .header(header)
+                .widths(
+                        Constraint.length(4),
+                        Constraint.length(12),
+                        Constraint.length(15),
+                        Constraint.length(15),
+                        Constraint.fill(),
+                        Constraint.length(10))
+                .highlightStyle(Style.EMPTY.fg(Color.WHITE).bold().onBlue())
+                .highlightSpacing(Table.HighlightSpacing.ALWAYS)
+                
.block(Block.builder().borderType(BorderType.ROUNDED).title(historyTitle).build())
+                .build();
+
+        frame.renderStatefulWidget(table, chunks.get(0), historyTableState);
+
+        // Detail panel
+        renderHistoryDetail(frame, chunks.get(1), current);
+    }
+
+    private void renderHistoryDetail(Frame frame, Rect area, 
List<HistoryEntry> current) {
+        Integer sel = historyTableState.selected();
+
+        if (sel == null || sel < 0 || sel >= current.size()) {
+            frame.renderWidget(
+                    Paragraph.builder()
+                            .text(Text.from(Line.from(
+                                    Span.styled(" Select a history entry to 
view details",
+                                            Style.EMPTY.dim()))))
+                            
.block(Block.builder().borderType(BorderType.ROUNDED)
+                                    .title(" Detail ").build())
+                            .build(),
+                    area);
+            return;
+        }
+
+        HistoryEntry entry = current.get(sel);
+        List<Line> lines = new ArrayList<>();
+
+        // Exchange info
+        lines.add(Line.from(
+                Span.styled(" Exchange: ", 
Style.EMPTY.fg(Color.YELLOW).bold()),
+                Span.raw(entry.exchangeId != null ? entry.exchangeId : "")));
+        lines.add(Line.from(
+                Span.styled(" Route:    ", 
Style.EMPTY.fg(Color.YELLOW).bold()),
+                Span.raw(entry.routeId != null ? entry.routeId : ""),
+                Span.styled("  Node: ", Style.EMPTY.fg(Color.YELLOW).bold()),
+                Span.raw(entry.nodeId != null ? entry.nodeId : ""),
+                Span.raw(entry.nodeLabel != null ? " (" + entry.nodeLabel + 
")" : "")));
+        lines.add(Line.from(
+                Span.styled(" Location: ", 
Style.EMPTY.fg(Color.YELLOW).bold()),
+                Span.raw(entry.location != null ? entry.location : "")));
+        lines.add(Line.from(
+                Span.styled(" Elapsed:  ", 
Style.EMPTY.fg(Color.YELLOW).bold()),
+                Span.raw(entry.elapsed >= 0 ? entry.elapsed + "ms" : ""),
+                Span.styled("  Thread: ", Style.EMPTY.fg(Color.YELLOW).bold()),
+                Span.raw(entry.threadName != null ? entry.threadName : "")));
+        if (entry.failed) {
+            lines.add(Line.from(
+                    Span.styled(" Status:   ", 
Style.EMPTY.fg(Color.YELLOW).bold()),
+                    Span.styled("Failed", Style.EMPTY.fg(Color.RED).bold())));
+        }
+        lines.add(Line.from(Span.raw("")));
+
+        // Headers
+        if (showHistoryHeaders && entry.headers != null && 
!entry.headers.isEmpty()) {
+            lines.add(Line.from(Span.styled(" Headers:", 
Style.EMPTY.fg(Color.GREEN).bold())));
+            for (Map.Entry<String, Object> h : entry.headers.entrySet()) {
+                String type = entry.headerTypes != null ? 
entry.headerTypes.get(h.getKey()) : null;
+                String typeLabel;
+                if (type != null) {
+                    String t = "(" + type + ")";
+                    t = truncate(t, 20);
+                    typeLabel = String.format("%-20s ", t);
+                } else {
+                    typeLabel = String.format("%-21s", "");
+                }
+                lines.add(Line.from(
+                        Span.styled("   " + typeLabel, Style.EMPTY.dim()),
+                        Span.styled(h.getKey(), Style.EMPTY.fg(Color.CYAN)),
+                        Span.raw(" = "),
+                        Span.raw(h.getValue() != null ? 
h.getValue().toString() : "null")));
+            }
+            lines.add(Line.from(Span.raw("")));
+        }
+
+        // Body
+        if (showHistoryBody) {
+            if (entry.body != null) {
+                if (entry.bodyType != null) {
+                    lines.add(Line.from(
+                            Span.styled(" Body: ", 
Style.EMPTY.fg(Color.GREEN).bold()),
+                            Span.styled("(" + entry.bodyType + ")", 
Style.EMPTY.dim())));
+                } else {
+                    lines.add(Line.from(Span.styled(" Body:", 
Style.EMPTY.fg(Color.GREEN).bold())));
+                }
+                String[] bodyLines = entry.body.split("\n");
+                for (String bl : bodyLines) {
+                    lines.add(Line.from(Span.raw("   " + bl)));
+                }
+            } else {
+                lines.add(Line.from(Span.styled(" Body is null", 
Style.EMPTY.fg(Color.GREEN).bold())));
+            }
+            lines.add(Line.from(Span.raw("")));
+        }
+
+        // Exception
+        if (entry.exception != null) {
+            lines.add(Line.from(Span.styled(" Exception:", 
Style.EMPTY.fg(Color.RED).bold())));
+            lines.add(Line.from(Span.raw("   " + entry.exception)));
+        }
+
+        Block block = Block.builder().borderType(BorderType.ROUNDED).build();
+        frame.renderWidget(block, area);
+
+        Rect inner = block.inner(area);
+        int visibleHeight = Math.max(1, inner.height());
+        int contentHeight;
+        if (historyWordWrap) {
+            int width = Math.max(1, inner.width() - 1);
+            contentHeight = 0;
+            for (Line l : lines) {
+                int w = l.width();
+                contentHeight += Math.max(1, (w + width - 1) / width);
+            }
+        } else {
+            contentHeight = lines.size();
+        }
+        int maxScroll = Math.max(0, contentHeight - visibleHeight);
+        if (historyDetailScroll > maxScroll) {
+            historyDetailScroll = maxScroll;
+        }
+
+        List<Rect> hChunks = Layout.horizontal()
+                .constraints(Constraint.fill(), Constraint.length(1))
+                .split(inner);
+
+        Paragraph detail = Paragraph.builder()
+                .text(Text.from(lines))
+                .overflow(historyWordWrap ? Overflow.WRAP_WORD : Overflow.CLIP)
+                .scroll(historyDetailScroll)
+                .build();
+        frame.renderWidget(detail, hChunks.get(0));
+
+        if (lines.size() > visibleHeight) {
+            ScrollbarState scrollState = new ScrollbarState();
+            scrollState.contentLength(lines.size());
+            scrollState.viewportContentLength(visibleHeight);
+            scrollState.position(historyDetailScroll);
+            frame.renderStatefulWidget(
+                    Scrollbar.builder().build(),
+                    hChunks.get(1), scrollState);
+        }
+    }
+
+    private Title buildHistoryTitle(List<HistoryEntry> entries) {
+        if (entries.isEmpty()) {
+            return Title.from(" History of last completed ");
+        }
+        HistoryEntry first = entries.get(0);
+        HistoryEntry last = null;
+        for (HistoryEntry e : entries) {
+            if (e.last) {
+                last = e;
+                break;
+            }
+        }
+        if (last == null) {
+            last = entries.get(entries.size() - 1);
+        }
+
+        List<Span> spans = new ArrayList<>();
+        spans.add(Span.raw(" History of last completed ("));
+        boolean failed = last.failed;
+        spans.add(Span.styled("status:" + (failed ? "failed" : "success"),
+                failed ? Style.EMPTY.fg(Color.RED).bold() : 
Style.EMPTY.fg(Color.GREEN).bold()));
+        if (last.elapsed >= 0) {
+            spans.add(Span.raw(" elapsed:" + 
TimeUtils.printDuration(last.elapsed, true)));
+        }
+        if (first.epochMs > 0) {
+            String ago = TimeUtils.printSince(first.epochMs);
+            spans.add(Span.raw(" ago:" + ago));
+        }
+        spans.add(Span.raw(") "));
+        return new Title(Line.from(spans), Alignment.LEFT, Overflow.CLIP);
+    }
+
     // ---- Shared rendering ----
 
     private void renderNoSelection(Frame frame, Rect area) {
@@ -2107,7 +2402,7 @@ public class CamelMonitor extends CamelCommand {
             hint(spans, "\u2191\u2193", "navigate");
             hint(spans, "s", "sort");
             hint(spans, "Enter", "details");
-            hint(spans, "1-6", "tabs");
+            hint(spans, "1-7", "tabs");
         } else if (tab == TAB_ROUTES && showDiagram) {
             String closeKey = diagramTextMode ? "D" : "d";
             hint(spans, closeKey + "/Esc", "close");
@@ -2126,12 +2421,12 @@ public class CamelMonitor extends CamelCommand {
             hint(spans, "s", "sort");
             hint(spans, "d", "diagram");
             hint(spans, "D", "text diagram");
-            hint(spans, "1-6", "tabs");
+            hint(spans, "1-7", "tabs");
         } else if (tab == TAB_HEALTH) {
             hint(spans, "Esc", "back");
             hint(spans, "\u2191\u2193", "navigate");
             hint(spans, "d", "toggle DOWN");
-            hint(spans, "1-6", "tabs");
+            hint(spans, "1-7", "tabs");
         } else if (tab == TAB_LOG) {
             hint(spans, "Esc", "back");
             hint(spans, "\u2191\u2193", "scroll");
@@ -2145,10 +2440,18 @@ public class CamelMonitor extends CamelCommand {
             hint(spans, "h", "headers" + (showTraceHeaders ? " [on]" : " 
[off]"));
             hint(spans, "b", "body" + (showTraceBody ? " [on]" : " [off]"));
             hintLast(spans, "f", "follow" + (traceFollowMode ? " [on]" : " 
[off]"));
+        } else if (tab == TAB_HISTORY) {
+            hint(spans, "Esc", "back");
+            hint(spans, "\u2191\u2193", "navigate");
+            hint(spans, "PgUp/PgDn", "scroll detail");
+            hint(spans, "h", "headers" + (showHistoryHeaders ? " [on]" : " 
[off]"));
+            hint(spans, "b", "body" + (showHistoryBody ? " [on]" : " [off]"));
+            hint(spans, "w", "wrap" + (historyWordWrap ? " [on]" : " [off]"));
+            hintLast(spans, "F5", "refresh");
         } else {
             hint(spans, "Esc", "back");
             hint(spans, "\u2191\u2193", "navigate");
-            hint(spans, "1-6", "tabs");
+            hint(spans, "1-7", "tabs");
         }
 
         frame.renderWidget(Paragraph.from(Line.from(spans)), area);
@@ -2523,6 +2826,136 @@ public class CamelMonitor extends CamelCommand {
         return entry;
     }
 
+    @SuppressWarnings("unchecked")
+    private void refreshHistoryData(List<Long> pids) {
+        List<HistoryEntry> allEntries = new ArrayList<>();
+        for (Long pid : pids) {
+            Path historyFile = CommandLineHelper.getCamelDir().resolve(pid + 
"-history.json");
+            if (!Files.exists(historyFile)) {
+                continue;
+            }
+            try {
+                String content = Files.readString(historyFile);
+                if (content == null || content.isBlank()) {
+                    continue;
+                }
+                JsonObject json = (JsonObject) Jsoner.deserialize(content);
+                Object tracesArray = json.get("traces");
+                if (tracesArray instanceof List<?> traceList) {
+                    for (Object traceObj : traceList) {
+                        if (traceObj instanceof JsonObject traceJson) {
+                            HistoryEntry entry = parseHistoryEntry(traceJson, 
Long.toString(pid));
+                            if (entry != null) {
+                                allEntries.add(entry);
+                            }
+                        }
+                    }
+                }
+            } catch (Exception e) {
+                // ignore
+            }
+        }
+        historyEntries = allEntries;
+    }
+
+    @SuppressWarnings("unchecked")
+    private HistoryEntry parseHistoryEntry(JsonObject json, String pid) {
+        HistoryEntry entry = new HistoryEntry();
+        entry.pid = pid;
+        entry.exchangeId = json.getString("exchangeId");
+        entry.routeId = json.getString("routeId");
+        entry.fromRouteId = json.getString("fromRouteId");
+        entry.nodeId = json.getString("nodeId");
+        entry.nodeShortName = json.getString("nodeShortName");
+        entry.nodeLabel = json.getString("nodeLabel");
+        entry.location = json.getString("location");
+        entry.threadName = json.getString("threadName");
+        entry.first = json.getBooleanOrDefault("first", false);
+        entry.last = json.getBooleanOrDefault("last", false);
+        entry.failed = json.getBooleanOrDefault("failed", false);
+        entry.nodeLevel = json.getIntegerOrDefault("nodeLevel", 0);
+
+        Object elapsedObj = json.get("elapsed");
+        if (elapsedObj instanceof Number n) {
+            entry.elapsed = n.longValue();
+        } else {
+            entry.elapsed = -1;
+        }
+
+        // Compute direction arrow
+        if (entry.first) {
+            entry.direction = "-->";
+        } else if (entry.last) {
+            entry.direction = "<--";
+        } else {
+            entry.direction = "  >";
+        }
+
+        // Compute processor label with tree indentation
+        if (entry.first || entry.last) {
+            entry.nodeLevel = Math.max(0, entry.nodeLevel - 1);
+        }
+        String indent = "  ".repeat(entry.nodeLevel);
+        if (entry.first) {
+            String uri = json.getString("endpointUri");
+            entry.processor = indent + "from[" + (uri != null ? uri : "") + 
"]";
+        } else {
+            entry.processor = indent + (entry.nodeLabel != null ? 
entry.nodeLabel : "");
+        }
+
+        // Timestamp
+        Object tsObj = json.get("timestamp");
+        if (tsObj instanceof Number n) {
+            long epochMs = n.longValue();
+            entry.epochMs = epochMs;
+            entry.timestamp = Instant.ofEpochMilli(epochMs)
+                    .atZone(ZoneId.systemDefault())
+                    .toLocalTime().toString();
+            if (entry.timestamp.length() > 12) {
+                entry.timestamp = entry.timestamp.substring(0, 12);
+            }
+        }
+
+        // Parse message
+        Object msgObj = json.get("message");
+        if (msgObj instanceof JsonObject message) {
+            Object headersObj = message.get("headers");
+            if (headersObj instanceof List<?> headerList) {
+                entry.headers = new LinkedHashMap<>();
+                entry.headerTypes = new LinkedHashMap<>();
+                for (Object h : headerList) {
+                    if (h instanceof JsonObject hObj) {
+                        String key = String.valueOf(hObj.get("key"));
+                        entry.headers.put(key, hObj.get("value"));
+                        Object type = hObj.get("type");
+                        if (type != null) {
+                            entry.headerTypes.put(key, 
TuiHelper.shortTypeName(type.toString()));
+                        }
+                    }
+                }
+            } else if (headersObj instanceof Map) {
+                entry.headers = new LinkedHashMap<>((Map<String, Object>) 
headersObj);
+            }
+
+            Object bodyObj = message.get("body");
+            if (bodyObj instanceof JsonObject bodyJson) {
+                Object val = bodyJson.get("value");
+                entry.body = val != null ? val.toString() : null;
+                entry.bodyType = 
TuiHelper.shortTypeName(bodyJson.getString("type"));
+            } else if (bodyObj != null) {
+                entry.body = bodyObj.toString();
+            }
+        }
+
+        // Exception
+        Object excObj = json.get("exception");
+        if (excObj instanceof JsonObject excJson) {
+            entry.exception = excJson.getString("message");
+        }
+
+        return entry;
+    }
+
     private static String stringValue(Object obj) {
         return obj != null ? obj.toString() : null;
     }
@@ -2904,6 +3337,32 @@ public class CamelMonitor extends CamelCommand {
         String message = "";
     }
 
+    static class HistoryEntry {
+        String pid;
+        String exchangeId;
+        String timestamp;
+        String routeId;
+        String fromRouteId;
+        String nodeId;
+        String nodeShortName;
+        String nodeLabel;
+        String location;
+        String processor;
+        String direction;
+        String threadName;
+        boolean first;
+        boolean last;
+        boolean failed;
+        int nodeLevel;
+        long elapsed;
+        long epochMs;
+        String body;
+        String bodyType;
+        String exception;
+        Map<String, Object> headers;
+        Map<String, String> headerTypes;
+    }
+
     record VanishingInfo(IntegrationInfo info, long startTime) {
     }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiHelper.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiHelper.java
index 0183a53b139e..7384c9bf529d 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiHelper.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiHelper.java
@@ -131,4 +131,30 @@ final class TuiHelper {
         }
         return 0;
     }
+
+    static String shortTypeName(String type) {
+        if (type == null) {
+            return "null";
+        } else if (type.startsWith("java.util.concurrent")) {
+            return type.substring(21);
+        } else if (type.startsWith("java.lang.") || 
type.startsWith("java.util.")) {
+            return type.substring(10);
+        } else if (type.startsWith("org.apache.camel.support.")) {
+            return type.substring(25);
+        } else if 
(type.equals("org.apache.camel.converter.stream.CachedOutputStream.WrappedInputStream"))
 {
+            return "WrappedInputStream";
+        } else if (type.startsWith("org.apache.camel.converter.stream.")) {
+            return type.substring(34);
+        } else if (type.equals(
+                
"org.apache.camel.processor.aggregate.AbstractListAggregationStrategy.GroupedExchangeList"))
 {
+            return "GroupedExchangeList";
+        } else if (type.length() > 34) {
+            int pos = type.lastIndexOf('.');
+            if (pos == -1) {
+                pos = type.length() - 34;
+            }
+            return type.substring(pos + 1);
+        }
+        return type;
+    }
 }

Reply via email to