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 989219035de5 CAMEL-23514: Apply TamboUI best practices to 
camel-jbang-plugin-tui (#23254)
989219035de5 is described below

commit 989219035de5df5dcfbc2f4425ef4f40a276b261
Author: Claus Ibsen <[email protected]>
AuthorDate: Fri May 15 18:14:50 2026 +0200

    CAMEL-23514: Apply TamboUI best practices to camel-jbang-plugin-tui (#23254)
    
    * CAMEL-23514: Fix scrollbar not showing in word-wrap mode for History and 
Trace detail panels
    
    The scrollbar visibility check used raw line count instead of wrapped 
content
    height, so the scrollbar was hidden even when content exceeded the viewport
    in word-wrap mode.
    
    Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
    
    * CAMEL-23514: Fix O(n²) getTraceExchangeIds and remove no-op 
runOnRenderThread
    
    Replace List.contains() loop with LinkedHashSet for O(1) deduplication while
    preserving insertion order. Remove the empty runOnRenderThread lambda that 
was
    called after background refresh with no effect.
    
    Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
    
    * CAMEL-23514: Consolidate duplicated sort helpers, formatSinceLast, and 
colorStyleForLevel
    
    - Extract generic sortLabel()/sortStyle() called by all three sort-column 
pairs
    - Collapse formatSinceLast(IntegrationInfo) and 
formatSinceLastRoute(RouteInfo)
      into shared formatSinceLast(String, String, String) primitive
    - Use existing colorStyleForLevel() in log table row loop instead of inline 
switch
    
    Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
    
    * CAMEL-23514: Promote detail-panel ScrollbarState to class-level fields
    
    Avoid allocating new ScrollbarState() on every render frame for History and
    Trace detail panels. Promote to class-level fields alongside the existing
    diagramVScrollState/diagramHScrollState fields.
    
    Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
    
    * CAMEL-23514: Extract parseMessage() to eliminate duplicate JSON parsing 
in trace/history
    
    Both parseTraceEntry() and parseHistoryEntry() parsed headers, body,
    exchangeProperties, and exchangeVariables with identical code (~70 lines 
each).
    Extract into a shared parseMessage(JsonObject) that returns a MessageData 
record.
    
    Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
    
    * CAMEL-23514: Extract shared step table builder for History and Trace 
detail views
    
    Both renderHistory and renderTraceExchangeDetail rendered an identical 
6-column
    step table (direction + time + route + nodeId + processor + elapsed). 
Extract
    buildStepRow() and buildStepTable() helpers shared by both call sites.
    
    Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
    
    * CAMEL-23514: Extract addExchangeInfoLines, addKvLines, addBodyLines, and 
renderDetailPanel shared helpers
    
    Eliminate duplicate exchange-info header rendering, key-value section
    rendering, body rendering, and detail-panel scroll/paragraph/scrollbar
    logic that was copy-pasted between renderTraceStepDetail and
    renderHistoryDetail.
    
    ---------
    
    Co-authored-by: Claude Sonnet 4.6 <[email protected]>
---
 .../dsl/jbang/core/commands/tui/CamelMonitor.java  | 812 ++++++++-------------
 1 file changed, 300 insertions(+), 512 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 5f9b9e853a55..5caed2dfb0e8 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
@@ -27,6 +27,7 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Iterator;
 import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
@@ -211,6 +212,8 @@ public class CamelMonitor extends CamelCommand {
     private int diagramScrollX;
     private final ScrollbarState diagramVScrollState = new ScrollbarState();
     private final ScrollbarState diagramHScrollState = new ScrollbarState();
+    private final ScrollbarState traceDetailScrollState = new ScrollbarState();
+    private final ScrollbarState historyDetailScrollState = new 
ScrollbarState();
     private String diagramRouteId;
     private ImageData diagramImageData;
     private ImageData diagramFullImageData;
@@ -1085,13 +1088,11 @@ public class CamelMonitor extends CamelCommand {
     }
 
     private String overviewSortLabel(String label, String column) {
-        return overviewSort.equals(column) ? label + "▼" : label;
+        return sortLabel(label, column, overviewSort);
     }
 
     private Style overviewSortStyle(String column) {
-        return overviewSort.equals(column)
-                ? Style.EMPTY.fg(Color.YELLOW).bold()
-                : Style.EMPTY.bold();
+        return sortStyle(column, overviewSort);
     }
 
     private int sortRoute(RouteInfo a, RouteInfo b) {
@@ -1113,21 +1114,27 @@ public class CamelMonitor extends CamelCommand {
     }
 
     private String traceSortLabel(String label, String column) {
-        return traceSort.equals(column) ? label + "▼" : label;
+        return sortLabel(label, column, traceSort);
     }
 
     private Style traceSortStyle(String column) {
-        return traceSort.equals(column)
-                ? Style.EMPTY.fg(Color.YELLOW).bold()
-                : Style.EMPTY.bold();
+        return sortStyle(column, traceSort);
     }
 
     private String routeSortLabel(String label, String column) {
-        return routeSort.equals(column) ? label + "\u25BC" : label;
+        return sortLabel(label, column, routeSort);
     }
 
     private Style routeSortStyle(String column) {
-        return routeSort.equals(column)
+        return sortStyle(column, routeSort);
+    }
+
+    private static String sortLabel(String label, String column, String 
currentSort) {
+        return currentSort.equals(column) ? label + "▼" : label;
+    }
+
+    private static Style sortStyle(String column, String currentSort) {
+        return currentSort.equals(column)
                 ? Style.EMPTY.fg(Color.YELLOW).bold()
                 : Style.EMPTY.bold();
     }
@@ -1910,13 +1917,7 @@ public class CamelMonitor extends CamelCommand {
         // Log table
         List<Row> rows = new ArrayList<>();
         for (LogEntry entry : filteredLogEntries) {
-            Style levelStyle = switch (entry.level) {
-                case "ERROR", "FATAL" -> Style.EMPTY.fg(Color.RED);
-                case "WARN" -> Style.EMPTY.fg(Color.YELLOW);
-                case "DEBUG", "TRACE" -> Style.EMPTY.dim();
-                default -> Style.EMPTY;
-            };
-
+            Style levelStyle = colorStyleForLevel(entry.level);
             rows.add(Row.from(
                     Cell.from(Span.styled(entry.time, Style.EMPTY.dim())),
                     Cell.from(Span.styled(entry.level, levelStyle)),
@@ -2215,56 +2216,16 @@ public class CamelMonitor extends CamelCommand {
                 .constraints(Constraint.length(10), Constraint.fill())
                 .split(area);
 
-        // Step table (like History)
         List<Row> rows = new ArrayList<>();
         for (TraceEntry entry : steps) {
-            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)));
+            rows.add(buildStepRow(
+                    entry.direction, entry.first, entry.last, entry.failed,
+                    entry.timestamp, entry.routeId, entry.nodeId, 
entry.processor, entry.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())));
-
         String stepTitle = String.format(" Trace [%s] ", 
truncate(traceSelectedExchangeId, 30));
-
-        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(stepTitle).build())
-                .build();
-
-        frame.renderStatefulWidget(table, chunks.get(0), traceStepTableState);
+        frame.renderStatefulWidget(
+                buildStepTable(rows, stepTitle), chunks.get(0), 
traceStepTableState);
 
         // Detail panel for selected step
         renderTraceStepDetail(frame, chunks.get(1), steps);
@@ -2288,169 +2249,36 @@ public class CamelMonitor extends CamelCommand {
         TraceEntry entry = steps.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())));
+        addExchangeInfoLines(lines, entry.exchangeId, entry.routeId, 
entry.nodeId, entry.nodeLabel,
+                entry.location, entry.elapsed, entry.threadName, entry.failed);
+        if (showTraceProperties) {
+            addKvLines(lines, " Exchange Properties:", 
entry.exchangeProperties, entry.exchangePropertyTypes);
         }
-        lines.add(Line.from(Span.raw("")));
-
-        // Exchange Properties
-        if (showTraceProperties && entry.exchangeProperties != null && 
!entry.exchangeProperties.isEmpty()) {
-            lines.add(Line.from(Span.styled(" Exchange Properties:", 
Style.EMPTY.fg(Color.GREEN).bold())));
-            for (Map.Entry<String, Object> p : 
entry.exchangeProperties.entrySet()) {
-                String type = entry.exchangePropertyTypes != null ? 
entry.exchangePropertyTypes.get(p.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(p.getKey(), Style.EMPTY.fg(Color.CYAN)),
-                        Span.raw(" = "),
-                        Span.raw(p.getValue() != null ? 
p.getValue().toString() : "null")));
-            }
-            lines.add(Line.from(Span.raw("")));
-        }
-
-        // Exchange Variables
-        if (showTraceVariables && entry.exchangeVariables != null && 
!entry.exchangeVariables.isEmpty()) {
-            lines.add(Line.from(Span.styled(" Exchange Variables:", 
Style.EMPTY.fg(Color.GREEN).bold())));
-            for (Map.Entry<String, Object> v : 
entry.exchangeVariables.entrySet()) {
-                String type = entry.exchangeVariableTypes != null ? 
entry.exchangeVariableTypes.get(v.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(v.getKey(), Style.EMPTY.fg(Color.CYAN)),
-                        Span.raw(" = "),
-                        Span.raw(v.getValue() != null ? 
v.getValue().toString() : "null")));
-            }
-            lines.add(Line.from(Span.raw("")));
+        if (showTraceVariables) {
+            addKvLines(lines, " Exchange Variables:", entry.exchangeVariables, 
entry.exchangeVariableTypes);
         }
-
-        // Headers
-        if (showTraceHeaders && 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("")));
+        if (showTraceHeaders) {
+            addKvLines(lines, " Headers:", entry.headers, entry.headerTypes);
         }
-
-        // Body
         if (showTraceBody) {
-            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("")));
-        }
-
-        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 (traceWordWrap) {
-            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();
+            addBodyLines(lines, entry.body, entry.bodyType);
         }
-        int maxScroll = Math.max(0, contentHeight - visibleHeight);
-        if (traceDetailScroll > maxScroll) {
-            traceDetailScroll = maxScroll;
-        }
-
-        List<Rect> hChunks = Layout.horizontal()
-                .constraints(Constraint.fill(), Constraint.length(1))
-                .split(inner);
-
-        Paragraph detail = Paragraph.builder()
-                .text(Text.from(lines))
-                .overflow(traceWordWrap ? Overflow.WRAP_WORD : Overflow.CLIP)
-                .scroll(traceDetailScroll)
-                .build();
-        frame.renderWidget(detail, hChunks.get(0));
 
-        if (lines.size() > visibleHeight) {
-            ScrollbarState scrollState = new ScrollbarState();
-            scrollState.contentLength(lines.size());
-            scrollState.viewportContentLength(visibleHeight);
-            scrollState.position(traceDetailScroll);
-            frame.renderStatefulWidget(
-                    Scrollbar.builder().build(),
-                    hChunks.get(1), scrollState);
-        }
+        int[] scroll = { traceDetailScroll };
+        renderDetailPanel(frame, area, lines, traceWordWrap, scroll, 
traceDetailScrollState);
+        traceDetailScroll = scroll[0];
     }
 
     private List<String> getTraceExchangeIds() {
         List<TraceEntry> current = traces.get();
-        List<String> ids = new ArrayList<>();
+        // LinkedHashSet: O(1) contains, preserves first-seen insertion order
+        LinkedHashSet<String> seen = new LinkedHashSet<>();
         for (TraceEntry e : current) {
-            if (e.exchangeId != null && !ids.contains(e.exchangeId)) {
-                ids.add(e.exchangeId);
+            if (e.exchangeId != null) {
+                seen.add(e.exchangeId);
             }
         }
-        return ids;
+        return new ArrayList<>(seen);
     }
 
     private List<TraceEntry> getTraceSteps(String exchangeId) {
@@ -2492,53 +2320,14 @@ public class CamelMonitor extends CamelCommand {
         // 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)));
+            rows.add(buildStepRow(
+                    entry.direction, entry.first, entry.last, entry.failed,
+                    entry.timestamp, entry.routeId, entry.nodeId, 
entry.processor, entry.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);
+        frame.renderStatefulWidget(
+                buildStepTable(rows, historyTitle), chunks.get(0), 
historyTableState);
 
         // Detail panel
         renderHistoryDetail(frame, chunks.get(1), current);
@@ -2563,131 +2352,113 @@ public class CamelMonitor extends CamelCommand {
         HistoryEntry entry = current.get(sel);
         List<Line> lines = new ArrayList<>();
 
-        // Exchange info
+        addExchangeInfoLines(lines, entry.exchangeId, entry.routeId, 
entry.nodeId, entry.nodeLabel,
+                entry.location, entry.elapsed, entry.threadName, entry.failed);
+        if (showHistoryProperties) {
+            addKvLines(lines, " Exchange Properties:", 
entry.exchangeProperties, entry.exchangePropertyTypes);
+        }
+        if (showHistoryVariables) {
+            addKvLines(lines, " Exchange Variables:", entry.exchangeVariables, 
entry.exchangeVariableTypes);
+        }
+        if (showHistoryHeaders) {
+            addKvLines(lines, " Headers:", entry.headers, entry.headerTypes);
+        }
+        if (showHistoryBody) {
+            addBodyLines(lines, entry.body, entry.bodyType);
+        }
+        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)));
+        }
+
+        int[] scroll = { historyDetailScroll };
+        renderDetailPanel(frame, area, lines, historyWordWrap, scroll, 
historyDetailScrollState);
+        historyDetailScroll = scroll[0];
+    }
+
+    private static void addExchangeInfoLines(
+            List<Line> lines, String exchangeId, String routeId,
+            String nodeId, String nodeLabel, String location, long elapsed, 
String threadName, boolean failed) {
         lines.add(Line.from(
                 Span.styled(" Exchange: ", 
Style.EMPTY.fg(Color.YELLOW).bold()),
-                Span.raw(entry.exchangeId != null ? entry.exchangeId : "")));
+                Span.raw(exchangeId != null ? exchangeId : "")));
         lines.add(Line.from(
                 Span.styled(" Route:    ", 
Style.EMPTY.fg(Color.YELLOW).bold()),
-                Span.raw(entry.routeId != null ? entry.routeId : ""),
+                Span.raw(routeId != null ? routeId : ""),
                 Span.styled("  Node: ", Style.EMPTY.fg(Color.YELLOW).bold()),
-                Span.raw(entry.nodeId != null ? entry.nodeId : ""),
-                Span.raw(entry.nodeLabel != null ? " (" + entry.nodeLabel + 
")" : "")));
+                Span.raw(nodeId != null ? nodeId : ""),
+                Span.raw(nodeLabel != null ? " (" + nodeLabel + ")" : "")));
         lines.add(Line.from(
                 Span.styled(" Location: ", 
Style.EMPTY.fg(Color.YELLOW).bold()),
-                Span.raw(entry.location != null ? entry.location : "")));
+                Span.raw(location != null ? location : "")));
         lines.add(Line.from(
                 Span.styled(" Elapsed:  ", 
Style.EMPTY.fg(Color.YELLOW).bold()),
-                Span.raw(entry.elapsed >= 0 ? entry.elapsed + "ms" : ""),
+                Span.raw(elapsed >= 0 ? elapsed + "ms" : ""),
                 Span.styled("  Thread: ", Style.EMPTY.fg(Color.YELLOW).bold()),
-                Span.raw(entry.threadName != null ? entry.threadName : "")));
-        if (entry.failed) {
+                Span.raw(threadName != null ? threadName : "")));
+        if (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
-        // Exchange Properties
-        if (showHistoryProperties && entry.exchangeProperties != null && 
!entry.exchangeProperties.isEmpty()) {
-            lines.add(Line.from(Span.styled(" Exchange Properties:", 
Style.EMPTY.fg(Color.GREEN).bold())));
-            for (Map.Entry<String, Object> p : 
entry.exchangeProperties.entrySet()) {
-                String type = entry.exchangePropertyTypes != null ? 
entry.exchangePropertyTypes.get(p.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(p.getKey(), Style.EMPTY.fg(Color.CYAN)),
-                        Span.raw(" = "),
-                        Span.raw(p.getValue() != null ? 
p.getValue().toString() : "null")));
-            }
-            lines.add(Line.from(Span.raw("")));
-        }
-
-        // Exchange Variables
-        if (showHistoryVariables && entry.exchangeVariables != null && 
!entry.exchangeVariables.isEmpty()) {
-            lines.add(Line.from(Span.styled(" Exchange Variables:", 
Style.EMPTY.fg(Color.GREEN).bold())));
-            for (Map.Entry<String, Object> v : 
entry.exchangeVariables.entrySet()) {
-                String type = entry.exchangeVariableTypes != null ? 
entry.exchangeVariableTypes.get(v.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(v.getKey(), Style.EMPTY.fg(Color.CYAN)),
-                        Span.raw(" = "),
-                        Span.raw(v.getValue() != null ? 
v.getValue().toString() : "null")));
-            }
-            lines.add(Line.from(Span.raw("")));
+    private static void addKvLines(
+            List<Line> lines, String section,
+            Map<String, Object> map, Map<String, String> types) {
+        if (map == null || map.isEmpty()) {
+            return;
         }
-
-        // 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.styled(section, 
Style.EMPTY.fg(Color.GREEN).bold())));
+        for (Map.Entry<String, Object> entry : map.entrySet()) {
+            String type = types != null ? types.get(entry.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.raw("")));
+            lines.add(Line.from(
+                    Span.styled("   " + typeLabel, Style.EMPTY.dim()),
+                    Span.styled(entry.getKey(), Style.EMPTY.fg(Color.CYAN)),
+                    Span.raw(" = "),
+                    Span.raw(entry.getValue() != null ? 
entry.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)));
-                }
+    private static void addBodyLines(List<Line> lines, String body, String 
bodyType) {
+        if (body != null) {
+            if (bodyType != null) {
+                lines.add(Line.from(
+                        Span.styled(" Body: ", 
Style.EMPTY.fg(Color.GREEN).bold()),
+                        Span.styled("(" + bodyType + ")", Style.EMPTY.dim())));
             } else {
-                lines.add(Line.from(Span.styled(" Body is null", 
Style.EMPTY.fg(Color.GREEN).bold())));
+                lines.add(Line.from(Span.styled(" Body:", 
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)));
+            String[] bodyParts = body.split("\n");
+            for (String bl : bodyParts) {
+                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("")));
+    }
 
+    private void renderDetailPanel(
+            Frame frame, Rect area, List<Line> lines,
+            boolean wordWrap, int[] scroll, ScrollbarState scrollState) {
         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) {
+        if (wordWrap) {
             int width = Math.max(1, inner.width() - 1);
             contentHeight = 0;
             for (Line l : lines) {
@@ -2698,8 +2469,8 @@ public class CamelMonitor extends CamelCommand {
             contentHeight = lines.size();
         }
         int maxScroll = Math.max(0, contentHeight - visibleHeight);
-        if (historyDetailScroll > maxScroll) {
-            historyDetailScroll = maxScroll;
+        if (scroll[0] > maxScroll) {
+            scroll[0] = maxScroll;
         }
 
         List<Rect> hChunks = Layout.horizontal()
@@ -2708,22 +2479,69 @@ public class CamelMonitor extends CamelCommand {
 
         Paragraph detail = Paragraph.builder()
                 .text(Text.from(lines))
-                .overflow(historyWordWrap ? Overflow.WRAP_WORD : Overflow.CLIP)
-                .scroll(historyDetailScroll)
+                .overflow(wordWrap ? Overflow.WRAP_WORD : Overflow.CLIP)
+                .scroll(scroll[0])
                 .build();
         frame.renderWidget(detail, hChunks.get(0));
 
-        if (lines.size() > visibleHeight) {
-            ScrollbarState scrollState = new ScrollbarState();
-            scrollState.contentLength(lines.size());
+        if (contentHeight > visibleHeight) {
+            scrollState.contentLength(contentHeight);
             scrollState.viewportContentLength(visibleHeight);
-            scrollState.position(historyDetailScroll);
+            scrollState.position(scroll[0]);
             frame.renderStatefulWidget(
                     Scrollbar.builder().build(),
                     hChunks.get(1), scrollState);
         }
     }
 
+    private static Row buildStepRow(
+            String direction, boolean first, boolean last, boolean failed,
+            String timestamp, String routeId, String nodeId, String processor, 
long elapsed) {
+        Style dirStyle;
+        if (first) {
+            dirStyle = Style.EMPTY.fg(Color.GREEN);
+        } else if (last) {
+            dirStyle = failed ? Style.EMPTY.fg(Color.RED) : 
Style.EMPTY.fg(Color.GREEN);
+        } else {
+            dirStyle = Style.EMPTY;
+        }
+        String elapsedStr = elapsed >= 0 ? elapsed + "ms" : "";
+        return Row.from(
+                Cell.from(Span.styled(direction, dirStyle)),
+                Cell.from(timestamp != null ? truncate(timestamp, 12) : ""),
+                Cell.from(Span.styled(routeId != null ? truncate(routeId, 15) 
: "", Style.EMPTY.fg(Color.CYAN))),
+                Cell.from(nodeId != null ? truncate(nodeId, 15) : ""),
+                Cell.from(processor != null ? processor : ""),
+                Cell.from(elapsedStr));
+    }
+
+    private static Table buildStepTable(List<Row> rows, Object title) {
+        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())));
+        Block block = title instanceof Title t
+                ? 
Block.builder().borderType(BorderType.ROUNDED).title(t).build()
+                : 
Block.builder().borderType(BorderType.ROUNDED).title(title.toString()).build();
+        return 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)
+                .build();
+    }
+
     private Title buildHistoryTitle(List<HistoryEntry> entries) {
         if (entries.isEmpty()) {
             return Title.from(" History of last completed ");
@@ -2846,23 +2664,7 @@ public class CamelMonitor extends CamelCommand {
     }
 
     private static String formatSinceLastRoute(RouteInfo route) {
-        StringBuilder sb = new StringBuilder();
-        if (route.sinceLastStarted != null) {
-            sb.append(route.sinceLastStarted);
-        }
-        if (route.sinceLastCompleted != null) {
-            if (!sb.isEmpty()) {
-                sb.append('/');
-            }
-            sb.append(route.sinceLastCompleted);
-        }
-        if (route.sinceLastFailed != null) {
-            if (!sb.isEmpty()) {
-                sb.append('/');
-            }
-            sb.append(route.sinceLastFailed);
-        }
-        return sb.toString();
+        return formatSinceLast(route.sinceLastStarted, 
route.sinceLastCompleted, route.sinceLastFailed);
     }
 
     private static Cell rightCell(String text, int width) {
@@ -2913,8 +2715,6 @@ public class CamelMonitor extends CamelCommand {
             } finally {
                 refreshInProgress.set(false);
             }
-            runner.runOnRenderThread(() -> {
-            });
         });
     }
 
@@ -3186,75 +2986,18 @@ public class CamelMonitor extends CamelCommand {
         // Parse message object
         Object msgObj = json.get("message");
         if (msgObj instanceof JsonObject message) {
-            // Headers
-            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);
-            }
-
-            // Body
-            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();
-            }
+            MessageData md = parseMessage(message);
+            entry.headers = md.headers();
+            entry.headerTypes = md.headerTypes();
+            entry.body = md.body();
+            entry.bodyType = md.bodyType();
             if (entry.body != null) {
                 entry.bodyPreview = entry.body.replace("\n", " 
").replace("\r", "");
             }
-
-            // Exchange properties
-            Object propsObj = message.get("exchangeProperties");
-            if (propsObj instanceof List<?> propList) {
-                entry.exchangeProperties = new LinkedHashMap<>();
-                entry.exchangePropertyTypes = new LinkedHashMap<>();
-                for (Object p : propList) {
-                    if (p instanceof JsonObject pObj) {
-                        String key = String.valueOf(pObj.get("key"));
-                        entry.exchangeProperties.put(key, pObj.get("value"));
-                        Object type = pObj.get("type");
-                        if (type != null) {
-                            entry.exchangePropertyTypes.put(key, 
TuiHelper.shortTypeName(type.toString()));
-                        }
-                    }
-                }
-            } else if (propsObj instanceof Map) {
-                entry.exchangeProperties = new LinkedHashMap<>((Map<String, 
Object>) propsObj);
-            }
-
-            // Exchange variables
-            Object varsObj = message.get("exchangeVariables");
-            if (varsObj instanceof List<?> varList) {
-                entry.exchangeVariables = new LinkedHashMap<>();
-                entry.exchangeVariableTypes = new LinkedHashMap<>();
-                for (Object v : varList) {
-                    if (v instanceof JsonObject vObj) {
-                        String key = String.valueOf(vObj.get("key"));
-                        entry.exchangeVariables.put(key, vObj.get("value"));
-                        Object type = vObj.get("type");
-                        if (type != null) {
-                            entry.exchangeVariableTypes.put(key, 
TuiHelper.shortTypeName(type.toString()));
-                        }
-                    }
-                }
-            } else if (varsObj instanceof Map) {
-                entry.exchangeVariables = new LinkedHashMap<>((Map<String, 
Object>) varsObj);
-            }
+            entry.exchangeProperties = md.exchangeProperties();
+            entry.exchangePropertyTypes = md.exchangePropertyTypes();
+            entry.exchangeVariables = md.exchangeVariables();
+            entry.exchangeVariableTypes = md.exchangeVariableTypes();
         }
 
         return entry;
@@ -3353,68 +3096,15 @@ public class CamelMonitor extends CamelCommand {
         // 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();
-            }
-
-            Object propsObj = message.get("exchangeProperties");
-            if (propsObj instanceof List<?> propList) {
-                entry.exchangeProperties = new LinkedHashMap<>();
-                entry.exchangePropertyTypes = new LinkedHashMap<>();
-                for (Object p : propList) {
-                    if (p instanceof JsonObject pObj) {
-                        String key = String.valueOf(pObj.get("key"));
-                        entry.exchangeProperties.put(key, pObj.get("value"));
-                        Object type = pObj.get("type");
-                        if (type != null) {
-                            entry.exchangePropertyTypes.put(key, 
TuiHelper.shortTypeName(type.toString()));
-                        }
-                    }
-                }
-            } else if (propsObj instanceof Map) {
-                entry.exchangeProperties = new LinkedHashMap<>((Map<String, 
Object>) propsObj);
-            }
-
-            Object varsObj = message.get("exchangeVariables");
-            if (varsObj instanceof List<?> varList) {
-                entry.exchangeVariables = new LinkedHashMap<>();
-                entry.exchangeVariableTypes = new LinkedHashMap<>();
-                for (Object v : varList) {
-                    if (v instanceof JsonObject vObj) {
-                        String key = String.valueOf(vObj.get("key"));
-                        entry.exchangeVariables.put(key, vObj.get("value"));
-                        Object type = vObj.get("type");
-                        if (type != null) {
-                            entry.exchangeVariableTypes.put(key, 
TuiHelper.shortTypeName(type.toString()));
-                        }
-                    }
-                }
-            } else if (varsObj instanceof Map) {
-                entry.exchangeVariables = new LinkedHashMap<>((Map<String, 
Object>) varsObj);
-            }
+            MessageData md = parseMessage(message);
+            entry.headers = md.headers();
+            entry.headerTypes = md.headerTypes();
+            entry.body = md.body();
+            entry.bodyType = md.bodyType();
+            entry.exchangeProperties = md.exchangeProperties();
+            entry.exchangePropertyTypes = md.exchangePropertyTypes();
+            entry.exchangeVariables = md.exchangeVariables();
+            entry.exchangeVariableTypes = md.exchangeVariableTypes();
         }
 
         // Exception
@@ -3430,6 +3120,100 @@ public class CamelMonitor extends CamelCommand {
         return obj != null ? obj.toString() : null;
     }
 
+    record MessageData(
+            Map<String, Object> headers,
+            Map<String, String> headerTypes,
+            String body,
+            String bodyType,
+            Map<String, Object> exchangeProperties,
+            Map<String, String> exchangePropertyTypes,
+            Map<String, Object> exchangeVariables,
+            Map<String, String> exchangeVariableTypes) {
+    }
+
+    @SuppressWarnings("unchecked")
+    private static MessageData parseMessage(JsonObject message) {
+        Map<String, Object> headers = null;
+        Map<String, String> headerTypes = null;
+        String body = null;
+        String bodyType = null;
+        Map<String, Object> exchangeProperties = null;
+        Map<String, String> exchangePropertyTypes = null;
+        Map<String, Object> exchangeVariables = null;
+        Map<String, String> exchangeVariableTypes = null;
+
+        // Headers
+        Object headersObj = message.get("headers");
+        if (headersObj instanceof List<?> headerList) {
+            headers = new LinkedHashMap<>();
+            headerTypes = new LinkedHashMap<>();
+            for (Object h : headerList) {
+                if (h instanceof JsonObject hObj) {
+                    String key = String.valueOf(hObj.get("key"));
+                    headers.put(key, hObj.get("value"));
+                    Object type = hObj.get("type");
+                    if (type != null) {
+                        headerTypes.put(key, 
TuiHelper.shortTypeName(type.toString()));
+                    }
+                }
+            }
+        } else if (headersObj instanceof Map) {
+            headers = new LinkedHashMap<>((Map<String, Object>) headersObj);
+        }
+
+        // Body
+        Object bodyObj = message.get("body");
+        if (bodyObj instanceof JsonObject bodyJson) {
+            Object val = bodyJson.get("value");
+            body = val != null ? val.toString() : null;
+            bodyType = TuiHelper.shortTypeName(bodyJson.getString("type"));
+        } else if (bodyObj != null) {
+            body = bodyObj.toString();
+        }
+
+        // Exchange properties
+        Object propsObj = message.get("exchangeProperties");
+        if (propsObj instanceof List<?> propList) {
+            exchangeProperties = new LinkedHashMap<>();
+            exchangePropertyTypes = new LinkedHashMap<>();
+            for (Object p : propList) {
+                if (p instanceof JsonObject pObj) {
+                    String key = String.valueOf(pObj.get("key"));
+                    exchangeProperties.put(key, pObj.get("value"));
+                    Object type = pObj.get("type");
+                    if (type != null) {
+                        exchangePropertyTypes.put(key, 
TuiHelper.shortTypeName(type.toString()));
+                    }
+                }
+            }
+        } else if (propsObj instanceof Map) {
+            exchangeProperties = new LinkedHashMap<>((Map<String, Object>) 
propsObj);
+        }
+
+        // Exchange variables
+        Object varsObj = message.get("exchangeVariables");
+        if (varsObj instanceof List<?> varList) {
+            exchangeVariables = new LinkedHashMap<>();
+            exchangeVariableTypes = new LinkedHashMap<>();
+            for (Object v : varList) {
+                if (v instanceof JsonObject vObj) {
+                    String key = String.valueOf(vObj.get("key"));
+                    exchangeVariables.put(key, vObj.get("value"));
+                    Object type = vObj.get("type");
+                    if (type != null) {
+                        exchangeVariableTypes.put(key, 
TuiHelper.shortTypeName(type.toString()));
+                    }
+                }
+            }
+        } else if (varsObj instanceof Map) {
+            exchangeVariables = new LinkedHashMap<>((Map<String, Object>) 
varsObj);
+        }
+
+        return new MessageData(
+                headers, headerTypes, body, bodyType,
+                exchangeProperties, exchangePropertyTypes, exchangeVariables, 
exchangeVariableTypes);
+    }
+
     // ---- Integration Parsing ----
 
     @SuppressWarnings("unchecked")
@@ -3664,21 +3448,25 @@ public class CamelMonitor extends CamelCommand {
     }
 
     private static String formatSinceLast(IntegrationInfo info) {
+        return formatSinceLast(info.sinceLastStarted, info.sinceLastCompleted, 
info.sinceLastFailed);
+    }
+
+    private static String formatSinceLast(String started, String completed, 
String failed) {
         StringBuilder sb = new StringBuilder();
-        if (info.sinceLastStarted != null) {
-            sb.append(info.sinceLastStarted);
+        if (started != null) {
+            sb.append(started);
         }
-        if (info.sinceLastCompleted != null) {
+        if (completed != null) {
             if (!sb.isEmpty()) {
                 sb.append('/');
             }
-            sb.append(info.sinceLastCompleted);
+            sb.append(completed);
         }
-        if (info.sinceLastFailed != null) {
+        if (failed != null) {
             if (!sb.isEmpty()) {
                 sb.append('/');
             }
-            sb.append(info.sinceLastFailed);
+            sb.append(failed);
         }
         return sb.toString();
     }

Reply via email to