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

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

commit c9263e16e170ca40630ff6f0c3f898942df5e233
Author: Claus Ibsen <[email protected]>
AuthorDate: Wed May 27 19:55:54 2026 +0200

    CAMEL-23623: Enhance Errors tab in Camel TUI
    
    - Separate error data into dedicated {pid}-error.json file to keep status 
file lightweight
    - Status file only contains error metadata (count); full data loaded on 
demand when Errors tab is active
    - Add master/detail layout with sortable table (ID, AGO, ROUTE, NODE, 
HANDLED, EXCEPTION, MESSAGE)
    - Detail pane shows exception with unescaped stack trace, message history, 
properties, variables, headers, body
    - Add toggles: f=handled filter, p=properties, v=variables, h=headers, 
b=body, w=word-wrap, s=sort
    - Add Home/End keys for jumping to top/bottom of detail pane
    - Fix word-wrap content height underestimation in renderDetailPanel that 
prevented scrolling to bottom
    - Update camel get error CLI to read from separate error file
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
---
 .../camel/cli/connector/LocalCliConnector.java     |  30 +++-
 .../dsl/jbang/core/commands/CamelCommand.java      |   4 +
 .../dsl/jbang/core/commands/process/ListError.java |   6 +-
 .../core/commands/process/ProcessBaseCommand.java  |  13 ++
 .../dsl/jbang/core/commands/tui/CamelMonitor.java  | 155 ++++++++++++++-------
 .../dsl/jbang/core/commands/tui/ErrorsTab.java     | 136 ++++++++++++++----
 .../dsl/jbang/core/commands/tui/HistoryTab.java    |   3 +
 .../jbang/core/commands/tui/IntegrationInfo.java   |   1 +
 8 files changed, 266 insertions(+), 82 deletions(-)

diff --git 
a/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
 
b/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
index a4ca0c6ccaf9..f3514777cebe 100644
--- 
a/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
+++ 
b/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
@@ -125,6 +125,7 @@ public class LocalCliConnector extends ServiceSupport 
implements CliConnector, C
     private long traceFilePos;   // keep track of trace offset
     private File messageHistoryFile;
     private File debugFile;
+    private File errorFile;
     private File receiveFile;
     private long receiveFilePos; // keep track of receive offset
     private byte[] lastSource;
@@ -196,6 +197,7 @@ public class LocalCliConnector extends ServiceSupport 
implements CliConnector, C
             outputFile = createLockFile(lockFile.getName() + "-output.json");
             traceFile = createLockFile(lockFile.getName() + "-trace.json");
             messageHistoryFile = createLockFile(lockFile.getName() + 
"-history.json");
+            errorFile = createLockFile(lockFile.getName() + "-error.json");
             debugFile = createLockFile(lockFile.getName() + "-debug.json");
             receiveFile = createLockFile(lockFile.getName() + "-receive.json");
             scheduledFuture = executor.scheduleWithFixedDelay(this::task, 0, 
delay, TimeUnit.MILLISECONDS);
@@ -1389,7 +1391,13 @@ public class LocalCliConnector extends ServiceSupport 
implements CliConnector, C
                     JsonObject json = (JsonObject) 
dc26.call(DevConsole.MediaType.JSON,
                             Map.of("stackTrace", "true"));
                     if (json != null && !json.isEmpty()) {
-                        root.put("errors", json);
+                        // only include metadata in status file (full error 
data is in the error file)
+                        JsonObject summary = new JsonObject();
+                        summary.put("enabled", json.get("enabled"));
+                        summary.put("size", json.get("size"));
+                        summary.put("maximumEntries", 
json.get("maximumEntries"));
+                        summary.put("timeToLive", json.get("timeToLive"));
+                        root.put("errors", summary);
                     }
                 }
             }
@@ -1481,6 +1489,23 @@ public class LocalCliConnector extends ServiceSupport 
implements CliConnector, C
             LOG.trace("Error updating message-history file: {} due to: {}. 
This exception is ignored.",
                     messageHistoryFile, e.getMessage(), e);
         }
+        try {
+            DevConsole dc13c = 
camelContext.getCamelContextExtension().getContextPlugin(DevConsoleRegistry.class)
+                    .resolveById("errors");
+            if (dc13c != null) {
+                JsonObject json = (JsonObject) 
dc13c.call(DevConsole.MediaType.JSON,
+                        Map.of("stackTrace", "true"));
+                if (json != null && !json.isEmpty()) {
+                    LOG.trace("Updating error file: {}", errorFile);
+                    String data = json.toJson() + System.lineSeparator();
+                    IOHelper.writeText(data, errorFile);
+                }
+            }
+        } catch (Exception e) {
+            // ignore
+            LOG.trace("Error updating error file: {} due to: {}. This 
exception is ignored.",
+                    errorFile, e.getMessage(), e);
+        }
         try {
             DevConsole dc14 = 
camelContext.getCamelContextExtension().getContextPlugin(DevConsoleRegistry.class)
                     .resolveById("receive");
@@ -1643,6 +1668,9 @@ public class LocalCliConnector extends ServiceSupport 
implements CliConnector, C
         if (messageHistoryFile != null) {
             FileUtil.deleteFile(messageHistoryFile);
         }
+        if (errorFile != null) {
+            FileUtil.deleteFile(errorFile);
+        }
         if (debugFile != null) {
             FileUtil.deleteFile(debugFile);
         }
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelCommand.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelCommand.java
index fdf0c4b63574..4d675773de1d 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelCommand.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelCommand.java
@@ -119,6 +119,10 @@ public abstract class CamelCommand implements 
Callable<Integer> {
         return CommandLineHelper.getCamelDir().resolve(pid + "-trace.json");
     }
 
+    public Path getErrorFile(String pid) {
+        return CommandLineHelper.getCamelDir().resolve(pid + "-error.json");
+    }
+
     public Path getReceiveFile(String pid) {
         return CommandLineHelper.getCamelDir().resolve(pid + "-receive.json");
     }
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListError.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListError.java
index 881eb3115ffa..b1ae97222215 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListError.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListError.java
@@ -131,9 +131,9 @@ public class ListError extends ProcessWatchCommand {
                         }
                         String pid = Long.toString(ph.pid());
 
-                        JsonObject errors = (JsonObject) root.get("errors");
-                        if (errors != null) {
-                            JsonArray arr = (JsonArray) errors.get("errors");
+                        JsonObject errorRoot = loadErrorFile(ph.pid());
+                        if (errorRoot != null) {
+                            JsonArray arr = (JsonArray) 
errorRoot.get("errors");
                             if (arr != null) {
                                 for (Object o : arr) {
                                     JsonObject jo = (JsonObject) o;
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ProcessBaseCommand.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ProcessBaseCommand.java
index 92d8db2301e9..b2176c9bf49e 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ProcessBaseCommand.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ProcessBaseCommand.java
@@ -105,6 +105,19 @@ abstract class ProcessBaseCommand extends CamelCommand {
         return null;
     }
 
+    JsonObject loadErrorFile(long pid) {
+        try {
+            Path f = getErrorFile(Long.toString(pid));
+            if (f != null && Files.exists(f)) {
+                String text = Files.readString(f);
+                return (JsonObject) Jsoner.deserialize(text);
+            }
+        } catch (Exception e) {
+            // ignore
+        }
+        return null;
+    }
+
     String sourceLocLine(String location) {
         while (StringHelper.countChar(location, ':') > 1) {
             location = location.substring(location.indexOf(':') + 1);
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 31201caef382..9850a845f6d2 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
@@ -736,6 +736,15 @@ public class CamelMonitor extends CamelCommand {
         if (tab == TAB_CIRCUIT_BREAKER) {
             circuitBreakerTab.onTabSelected();
         }
+        if (tab == TAB_ERRORS && ctx.selectedPid != null) {
+            try {
+                long pid = Long.parseLong(ctx.selectedPid);
+                refreshErrorData(List.of(pid));
+            } catch (NumberFormatException e) {
+                // ignore
+            }
+            errorsTab.onTabSelected();
+        }
         tabsState.select(tab);
         return true;
     }
@@ -1035,7 +1044,7 @@ public class CamelMonitor extends CamelCommand {
             } else if (cbCount > 0) {
                 badgeTexts[TAB_CIRCUIT_BREAKER] = "(" + cbCount + ")";
             }
-            int errorCount = hasSelection ? sel.errors.size() : 0;
+            int errorCount = hasSelection ? sel.errorCount : 0;
             if (errorCount > 0) {
                 badgeTexts[TAB_ERRORS] = "(" + errorCount + ")";
                 badgeStyles[TAB_ERRORS] = red;
@@ -2012,6 +2021,11 @@ public class CamelMonitor extends CamelCommand {
                 }
             }
 
+            // Refresh error data only when the Errors tab is visible
+            if (tabsState.selected() == TAB_ERRORS) {
+                refreshErrorData(pids);
+            }
+
             // Refresh trace data only when the Inspect tab is visible
             if (tabsState.selected() == TAB_HISTORY) {
                 refreshTraceData(pids);
@@ -3009,58 +3023,10 @@ public class CamelMonitor extends CamelCommand {
             }
         }
 
-        // Parse errors from error registry
+        // Parse error count from error registry (full error data is loaded on 
demand by ErrorsTab)
         JsonObject errorsObj = (JsonObject) root.get("errors");
         if (errorsObj != null) {
-            JsonArray errorList = (JsonArray) errorsObj.get("errors");
-            if (errorList != null) {
-                for (Object e : errorList) {
-                    JsonObject ej = (JsonObject) e;
-                    ErrorInfo ei = new ErrorInfo();
-                    ei.routeId = ej.getString("routeId");
-                    ei.nodeId = ej.getString("nodeId");
-                    ei.exchangeId = ej.getString("exchangeId");
-                    ei.handled = ej.getBooleanOrDefault("handled", false);
-                    Long ts = ej.getLong("timestamp");
-                    if (ts != null) {
-                        ei.timestamp = ts;
-                    }
-                    ei.location = ej.getString("location");
-                    ei.threadName = ej.getString("threadName");
-                    Long elapsed = ej.getLong("elapsed");
-                    if (elapsed != null) {
-                        ei.elapsed = elapsed;
-                    }
-                    ei.endpointUri = ej.getString("endpointUri");
-                    ei.fromEndpointUri = ej.getString("fromEndpointUri");
-                    // exception
-                    JsonObject ex = (JsonObject) ej.get("exception");
-                    if (ex != null) {
-                        ei.exceptionType = ex.getString("type");
-                        ei.exceptionMessage = ex.getString("message");
-                        ei.stackTrace = ex.getString("stackTrace");
-                    }
-                    // message history
-                    Object mhObj = ej.get("messageHistory");
-                    if (mhObj instanceof JsonArray mhArr) {
-                        ei.messageHistory = new String[mhArr.size()];
-                        for (int i = 0; i < mhArr.size(); i++) {
-                            ei.messageHistory[i] = mhArr.get(i).toString();
-                        }
-                    }
-                    // message (body, headers)
-                    JsonObject msg = (JsonObject) ej.get("message");
-                    if (msg != null) {
-                        ei.body = msg.getString("body");
-                        ei.bodyType = msg.getString("bodyType");
-                        parseKvArray(msg.getCollection("headers"), ei.headers, 
ei.headerTypes);
-                    }
-                    // exchange properties and variables
-                    parseKvArray(ej.getCollection("exchangeProperties"), 
ei.properties, ei.propertyTypes);
-                    parseKvArray(ej.getCollection("exchangeVariables"), 
ei.variables, ei.variableTypes);
-                    info.errors.add(ei);
-                }
-            }
+            info.errorCount = errorsObj.getIntegerOrDefault("size", 0);
         }
 
         // Parse REST DSL services
@@ -3192,6 +3158,93 @@ public class CamelMonitor extends CamelCommand {
         }
     }
 
+    private JsonObject loadErrorFile(long pid) {
+        return TuiHelper.loadStatus(pid, this::getErrorFile);
+    }
+
+    private void refreshErrorData(List<Long> pids) {
+        IntegrationInfo sel = findSelectedIntegration();
+        if (sel == null) {
+            return;
+        }
+        try {
+            long pid = Long.parseLong(sel.pid);
+            JsonObject root = loadErrorFile(pid);
+            if (root == null) {
+                return;
+            }
+            JsonArray errorList = (JsonArray) root.get("errors");
+            if (errorList == null) {
+                return;
+            }
+            List<ErrorInfo> parsed = new ArrayList<>();
+            for (Object e : errorList) {
+                JsonObject ej = (JsonObject) e;
+                ErrorInfo ei = new ErrorInfo();
+                ei.routeId = ej.getString("routeId");
+                ei.nodeId = ej.getString("nodeId");
+                ei.exchangeId = ej.getString("exchangeId");
+                ei.handled = Boolean.TRUE.equals(ej.get("handled"));
+                Long ts = ej.getLong("timestamp");
+                if (ts != null) {
+                    ei.timestamp = ts;
+                }
+                ei.location = ej.getString("location");
+                ei.threadName = ej.getString("threadName");
+                Long elapsed = ej.getLong("elapsed");
+                if (elapsed != null) {
+                    ei.elapsed = elapsed;
+                }
+                ei.endpointUri = ej.getString("endpointUri");
+                ei.fromEndpointUri = ej.getString("fromEndpointUri");
+                // exception
+                JsonObject ex = (JsonObject) ej.get("exception");
+                if (ex != null) {
+                    ei.exceptionType = ex.getString("type");
+                    ei.exceptionMessage = ex.getString("message");
+                    ei.stackTrace = ex.getString("stackTrace");
+                }
+                // message history
+                Object mhObj = ej.get("messageHistory");
+                if (mhObj instanceof JsonArray mhArr) {
+                    ei.messageHistory = new String[mhArr.size()];
+                    for (int i = 0; i < mhArr.size(); i++) {
+                        ei.messageHistory[i] = mhArr.get(i).toString();
+                    }
+                }
+                // message (body, headers)
+                JsonObject msg = (JsonObject) ej.get("message");
+                if (msg != null) {
+                    Object bodyObj = msg.get("body");
+                    if (bodyObj instanceof JsonObject bodyJson) {
+                        ei.body = bodyJson.getString("value");
+                        ei.bodyType = bodyJson.getString("type");
+                    } else if (bodyObj != null) {
+                        ei.body = bodyObj.toString();
+                    }
+                    JsonArray hdrs = msg.getCollection("headers");
+                    if (hdrs != null) {
+                        parseKvArray(hdrs, ei.headers, ei.headerTypes);
+                    }
+                }
+                // exchange properties and variables
+                JsonArray props = ej.getCollection("exchangeProperties");
+                if (props != null) {
+                    parseKvArray(props, ei.properties, ei.propertyTypes);
+                }
+                JsonArray vars = ej.getCollection("exchangeVariables");
+                if (vars != null) {
+                    parseKvArray(vars, ei.variables, ei.variableTypes);
+                }
+                parsed.add(ei);
+            }
+            sel.errors.clear();
+            sel.errors.addAll(parsed);
+        } catch (Exception e) {
+            // ignore
+        }
+    }
+
     // ---- Helpers ----
 
     private IntegrationInfo findSelectedIntegration() {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTab.java
index c74e3c7ceb39..9f5d9663aff6 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTab.java
@@ -35,22 +35,30 @@ import dev.tamboui.widgets.table.Cell;
 import dev.tamboui.widgets.table.Row;
 import dev.tamboui.widgets.table.Table;
 import dev.tamboui.widgets.table.TableState;
+import org.apache.camel.util.json.Jsoner;
 
 import static org.apache.camel.dsl.jbang.core.commands.tui.MonitorContext.*;
 
 class ErrorsTab implements MonitorTab {
 
-    private static final String[] SORT_COLUMNS = { "age", "route", "exception" 
};
+    private static final String[] SORT_COLUMNS = { "id", "age", "route", 
"node", "exception" };
 
     private final MonitorContext ctx;
     private final TableState tableState = new TableState();
     private final ScrollbarState detailScrollState = new ScrollbarState();
-    private String sort = "age";
+    private String sort = "id";
     private int sortIndex;
     private boolean sortReversed;
+    private static final String[] HANDLED_FILTER = { "all", "true", "false" };
+    private int handledIndex;
+    private String handledFilter = "all";
     private int detailScroll;
     private int detailHScroll;
     private boolean wordWrap = true;
+    private boolean showProperties;
+    private boolean showVariables;
+    private boolean showHeaders = true;
+    private boolean showBody = true;
 
     ErrorsTab(MonitorContext ctx) {
         this.ctx = ctx;
@@ -80,6 +88,35 @@ class ErrorsTab implements MonitorTab {
             wordWrap = !wordWrap;
             return true;
         }
+        if (ke.isCharIgnoreCase('f')) {
+            handledIndex = (handledIndex + 1) % HANDLED_FILTER.length;
+            handledFilter = HANDLED_FILTER[handledIndex];
+            return true;
+        }
+        if (ke.isCharIgnoreCase('p')) {
+            showProperties = !showProperties;
+            return true;
+        }
+        if (ke.isCharIgnoreCase('v')) {
+            showVariables = !showVariables;
+            return true;
+        }
+        if (ke.isCharIgnoreCase('h')) {
+            showHeaders = !showHeaders;
+            return true;
+        }
+        if (ke.isCharIgnoreCase('b')) {
+            showBody = !showBody;
+            return true;
+        }
+        if (ke.isHome()) {
+            detailScroll = 0;
+            return true;
+        }
+        if (ke.isEnd()) {
+            detailScroll = Integer.MAX_VALUE;
+            return true;
+        }
         if (ke.isPageUp()) {
             detailScroll = Math.max(0, detailScroll - 5);
             return true;
@@ -113,8 +150,7 @@ class ErrorsTab implements MonitorTab {
     @Override
     public void navigateDown() {
         detailScroll = 0;
-        IntegrationInfo info = ctx.findSelectedIntegration();
-        tableState.selectNext(info != null ? info.errors.size() : 0);
+        tableState.selectNext(filteredSize());
     }
 
     @Override
@@ -125,8 +161,7 @@ class ErrorsTab implements MonitorTab {
             return;
         }
 
-        List<ErrorInfo> sorted = new ArrayList<>(info.errors);
-        sorted.sort(this::sortError);
+        List<ErrorInfo> sorted = applyFilter(info.errors);
 
         List<Row> rows = new ArrayList<>();
         for (ErrorInfo ei : sorted) {
@@ -138,6 +173,7 @@ class ErrorsTab implements MonitorTab {
             String shortException = shortExceptionType(ei.exceptionType);
 
             rows.add(Row.from(
+                    Cell.from(ei.exchangeId != null ? ei.exchangeId : ""),
                     Cell.from(ago),
                     Cell.from(Span.styled(ei.routeId != null ? ei.routeId : 
"", Style.EMPTY.fg(Color.CYAN))),
                     Cell.from(ei.nodeId != null ? ei.nodeId : ""),
@@ -150,7 +186,7 @@ class ErrorsTab implements MonitorTab {
             rows.add(Row.from(
                     Cell.from(Span.styled("No errors captured", 
Style.EMPTY.dim())),
                     Cell.from(""), Cell.from(""), Cell.from(""),
-                    Cell.from(""), Cell.from("")));
+                    Cell.from(""), Cell.from(""), Cell.from("")));
         }
 
         ErrorInfo selectedError = null;
@@ -161,20 +197,22 @@ class ErrorsTab implements MonitorTab {
         boolean showDetail = selectedError != null;
         List<Rect> chunks = showDetail
                 ? Layout.vertical()
-                        .constraints(Constraint.length(Math.min(sorted.size() 
+ 3, 12)), Constraint.fill())
+                        .constraints(Constraint.length(13), 
Constraint.length(1), Constraint.fill())
                         .split(area)
                 : List.of(area);
 
         Table table = Table.builder()
                 .rows(rows)
                 .header(Row.from(
+                        Cell.from(Span.styled(sortLabel("ID", "id"), 
sortStyle("id"))),
                         Cell.from(Span.styled(sortLabel("AGO", "age"), 
sortStyle("age"))),
                         Cell.from(Span.styled(sortLabel("ROUTE", "route"), 
sortStyle("route"))),
-                        Cell.from(Span.styled("NODE", Style.EMPTY.bold())),
+                        Cell.from(Span.styled(sortLabel("NODE", "node"), 
sortStyle("node"))),
                         Cell.from(Span.styled("HANDLED", Style.EMPTY.bold())),
                         Cell.from(Span.styled(sortLabel("EXCEPTION", 
"exception"), sortStyle("exception"))),
                         Cell.from(Span.styled("MESSAGE", Style.EMPTY.bold()))))
                 .widths(
+                        Constraint.length(38),
                         Constraint.length(8),
                         Constraint.length(20),
                         Constraint.length(20),
@@ -189,7 +227,7 @@ class ErrorsTab implements MonitorTab {
         frame.renderStatefulWidget(table, chunks.get(0), tableState);
 
         if (showDetail) {
-            renderDetail(frame, chunks.get(1), selectedError);
+            renderDetail(frame, chunks.get(2), selectedError);
         }
     }
 
@@ -197,9 +235,18 @@ class ErrorsTab implements MonitorTab {
     public void renderFooter(List<Span> spans) {
         hint(spans, "Esc", "back");
         hint(spans, "↑↓", "navigate");
+        hint(spans, "PgUp/Dn", "scroll detail");
+        if (!wordWrap) {
+            hint(spans, "←→", "h-scroll");
+        }
+        hint(spans, "Home/End", "top/end");
         hint(spans, "s", "sort");
-        hint(spans, "w", "wrap");
-        hint(spans, "PgUp/Dn", "scroll");
+        hint(spans, "f", "handled [" + handledFilter + "]");
+        hint(spans, "p", "properties [" + (showProperties ? "on" : "off") + 
"]");
+        hint(spans, "v", "variables [" + (showVariables ? "on" : "off") + "]");
+        hint(spans, "h", "headers [" + (showHeaders ? "on" : "off") + "]");
+        hint(spans, "b", "body [" + (showBody ? "on" : "off") + "]");
+        hint(spans, "w", "wrap [" + (wordWrap ? "on" : "off") + "]");
         hint(spans, "1-0", "tabs");
     }
 
@@ -216,10 +263,22 @@ class ErrorsTab implements MonitorTab {
             StringBuilder sb = new StringBuilder();
             sb.append(ei.exceptionType);
             if (ei.exceptionMessage != null) {
-                sb.append(": ").append(ei.exceptionMessage);
+                String msg = ei.exceptionMessage;
+                try {
+                    msg = Jsoner.unescape(msg);
+                } catch (Exception e) {
+                    // ignore
+                }
+                sb.append(": ").append(msg);
             }
             if (ei.stackTrace != null) {
-                sb.append("\n").append(ei.stackTrace);
+                String st = ei.stackTrace;
+                try {
+                    st = Jsoner.unescape(st);
+                } catch (Exception e) {
+                    // ignore
+                }
+                sb.append("\n").append(st);
             }
             exception = sb.toString();
         }
@@ -229,24 +288,24 @@ class ErrorsTab implements MonitorTab {
         if (ei.messageHistory != null && ei.messageHistory.length > 0) {
             lines.add(Line.from(Span.styled(" Message History:", 
Style.EMPTY.fg(Color.MAGENTA).bold())));
             for (String step : ei.messageHistory) {
-                lines.add(Line.from(Span.raw("   " + step)));
+                lines.add(Line.from(Span.raw("   " + 
TuiHelper.fixControlChars(step))));
             }
             lines.add(Line.from(Span.raw("")));
         }
 
-        // variables, properties, headers
-        if (!ei.variables.isEmpty()) {
-            HistoryTab.addKvLines(lines, " Variables:", ei.variables, 
ei.variableTypes);
+        // exchange properties, variables, headers, body
+        if (showProperties && !ei.properties.isEmpty()) {
+            HistoryTab.addKvLines(lines, " Exchange Properties:", 
ei.properties, ei.propertyTypes);
         }
-        if (!ei.properties.isEmpty()) {
-            HistoryTab.addKvLines(lines, " Properties:", ei.properties, 
ei.propertyTypes);
+        if (showVariables && !ei.variables.isEmpty()) {
+            HistoryTab.addKvLines(lines, " Exchange Variables:", ei.variables, 
ei.variableTypes);
         }
-        if (!ei.headers.isEmpty()) {
+        if (showHeaders && !ei.headers.isEmpty()) {
             HistoryTab.addKvLines(lines, " Headers:", ei.headers, 
ei.headerTypes);
         }
-
-        // body
-        HistoryTab.addBodyLines(lines, ei.body, ei.bodyType);
+        if (showBody) {
+            HistoryTab.addBodyLines(lines, ei.body, ei.bodyType);
+        }
 
         int[] scroll = { detailScroll };
         int[] hScroll = { detailHScroll };
@@ -265,7 +324,9 @@ class ErrorsTab implements MonitorTab {
 
     private int sortError(ErrorInfo a, ErrorInfo b) {
         int result = switch (sort) {
+            case "id" -> compareStr(a.exchangeId, b.exchangeId);
             case "route" -> compareStr(a.routeId, b.routeId);
+            case "node" -> compareStr(a.nodeId, b.nodeId);
             case "exception" -> compareStr(a.exceptionType, b.exceptionType);
             default -> Long.compare(b.timestamp, a.timestamp); // newest first
         };
@@ -289,12 +350,33 @@ class ErrorsTab implements MonitorTab {
         if (info == null || info.errors.isEmpty()) {
             return null;
         }
-        List<ErrorInfo> sorted = new ArrayList<>(info.errors);
-        sorted.sort(this::sortError);
-        List<String> items = sorted.stream()
+        List<ErrorInfo> filtered = applyFilter(info.errors);
+        List<String> items = filtered.stream()
                 .map(e -> e.exchangeId != null ? e.exchangeId : "")
                 .toList();
         Integer sel = tableState.selected();
         return new SelectionContext("table", items, sel != null ? sel : -1, 
items.size(), "Errors");
     }
+
+    private List<ErrorInfo> applyFilter(List<ErrorInfo> errors) {
+        List<ErrorInfo> list = new ArrayList<>(errors);
+        list.sort(this::sortError);
+        if (!"all".equals(handledFilter)) {
+            boolean filterVal = "true".equals(handledFilter);
+            list.removeIf(e -> e.handled != filterVal);
+        }
+        return list;
+    }
+
+    private int filteredSize() {
+        IntegrationInfo info = ctx.findSelectedIntegration();
+        if (info == null) {
+            return 0;
+        }
+        if ("all".equals(handledFilter)) {
+            return info.errors.size();
+        }
+        boolean filterVal = "true".equals(handledFilter);
+        return (int) info.errors.stream().filter(e -> e.handled == 
filterVal).count();
+    }
 }
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 668bfce4bbbf..19021e2f9dab 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
@@ -844,6 +844,9 @@ class HistoryTab implements MonitorTab {
                 int w = l.width();
                 contentHeight += Math.max(1, (w + visibleWidth - 1) / 
visibleWidth);
             }
+            // word-wrap breaks at word boundaries which can produce more lines
+            // than char-based math; add padding so last section is always 
reachable
+            contentHeight += visibleHeight;
         } else {
             contentHeight = lines.size();
         }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationInfo.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationInfo.java
index ffaa18c73fbb..099e7b2294ca 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationInfo.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationInfo.java
@@ -62,6 +62,7 @@ class IntegrationInfo {
     final List<HealthCheckInfo> healthChecks = new ArrayList<>();
     final List<EndpointInfo> endpoints = new ArrayList<>();
     final List<CircuitBreakerInfo> circuitBreakers = new ArrayList<>();
+    int errorCount;
     final List<ErrorInfo> errors = new ArrayList<>();
     final List<HttpEndpointInfo> httpEndpoints = new ArrayList<>();
     String httpServer;

Reply via email to