You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by gn...@apache.org on 2026/03/23 14:22:11 UTC

(camel) 01/01: CAMEL-23226: Add TUI dashboards for Camel JBang

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

gnodet pushed a commit to branch Guillaume-Nodet/camel-tui
in repository https://gitbox.apache.org/repos/asf/camel.git

commit df9143e6717488ea4cf9a3535d23d0b10c57f8a5
Author: Guillaume Nodet <gn...@gmail.com>
AuthorDate: Mon Mar 23 15:21:51 2026 +0100

    CAMEL-23226: Add TUI dashboards for Camel JBang
    
    - Add `camel monitor` full-screen TUI dashboard with tabs for overview,
      routes, health checks, endpoints, and log viewer
    - Add `camel top tui` htop-style route performance dashboard with
      sortable columns and live refresh
    - Add `camel log tui` scrollable log viewer with level filtering,
      follow mode, and grep support
    - Add `camel trace tui` exchange trace viewer with detail panel showing
      headers, body, and exchange properties
    - Add `camel health` TUI health check dashboard with UP/DOWN indicators
      and memory gauge
    - Add `camel catalog tui` interactive component catalog browser with
      search/filter and component detail view
    - Upgrade JLine from 4.0.7 to 4.0.9
    - Add TamboUI dependencies for terminal UI rendering
    - Use TamboUI's built-in JLine 3 backend (compatible with JLine 4.0.9)
    
    Co-Authored-By: Claude Opus 4.6 <no...@anthropic.com>
---
 dsl/camel-jbang/camel-jbang-core/pom.xml           |   28 +
 .../dsl/jbang/core/commands/CamelJBangMain.java    |   15 +-
 .../jbang/core/commands/tui/CamelCatalogTui.java   |  697 +++++++++++
 .../jbang/core/commands/tui/CamelHealthTui.java    |  498 ++++++++
 .../dsl/jbang/core/commands/tui/CamelLogTui.java   |  498 ++++++++
 .../dsl/jbang/core/commands/tui/CamelMonitor.java  | 1301 ++++++++++++++++++++
 .../dsl/jbang/core/commands/tui/CamelTopTui.java   |  488 ++++++++
 .../dsl/jbang/core/commands/tui/CamelTraceTui.java |  635 ++++++++++
 parent/pom.xml                                     |    2 +-
 9 files changed, 4157 insertions(+), 5 deletions(-)

diff --git a/dsl/camel-jbang/camel-jbang-core/pom.xml b/dsl/camel-jbang/camel-jbang-core/pom.xml
index 31dccd48d50d..58241f214dc0 100644
--- a/dsl/camel-jbang/camel-jbang-core/pom.xml
+++ b/dsl/camel-jbang/camel-jbang-core/pom.xml
@@ -175,8 +175,36 @@
             <version>${awaitility-version}</version>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>dev.tamboui</groupId>
+            <artifactId>tamboui-tui</artifactId>
+            <version>0.2.0-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>dev.tamboui</groupId>
+            <artifactId>tamboui-widgets</artifactId>
+            <version>0.2.0-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>dev.tamboui</groupId>
+            <artifactId>tamboui-jline3-backend</artifactId>
+            <version>0.2.0-SNAPSHOT</version>
+        </dependency>
     </dependencies>
 
+    <repositories>
+        <repository>
+            <id>sonatype-snapshots</id>
+            <url>https://central.sonatype.com/repository/maven-snapshots/</url>
+            <snapshots>
+                <enabled>true</enabled>
+            </snapshots>
+            <releases>
+                <enabled>false</enabled>
+            </releases>
+        </repository>
+    </repositories>
+
     <build>
         <plugins>
             <plugin>
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java
index bd03af9ee39c..92febdab4659 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java
@@ -49,6 +49,7 @@ import org.apache.camel.dsl.jbang.core.commands.plugin.PluginCommand;
 import org.apache.camel.dsl.jbang.core.commands.plugin.PluginDelete;
 import org.apache.camel.dsl.jbang.core.commands.plugin.PluginGet;
 import org.apache.camel.dsl.jbang.core.commands.process.*;
+import org.apache.camel.dsl.jbang.core.commands.tui.*;
 import org.apache.camel.dsl.jbang.core.commands.update.UpdateCommand;
 import org.apache.camel.dsl.jbang.core.commands.update.UpdateList;
 import org.apache.camel.dsl.jbang.core.commands.update.UpdateRun;
@@ -96,7 +97,8 @@ public class CamelJBangMain implements Callable<Integer> {
                         .addSubcommand("kamelet", new CommandLine(new CatalogKamelet(this)))
                         .addSubcommand("transformer", new CommandLine(new CatalogTransformer(this)))
                         .addSubcommand("language", new CommandLine(new CatalogLanguage(this)))
-                        .addSubcommand("other", new CommandLine(new CatalogOther(this))))
+                        .addSubcommand("other", new CommandLine(new CatalogOther(this)))
+                        .addSubcommand("tui", new CommandLine(new CamelCatalogTui(this))))
                 .addSubcommand("cmd", new CommandLine(new CamelAction(this))
                         .addSubcommand("browse", new CommandLine(new CamelBrowseAction(this)))
                         .addSubcommand("disable-processor", new CommandLine(new CamelProcessorDisableAction(this)))
@@ -177,7 +179,9 @@ public class CamelJBangMain implements Callable<Integer> {
                         .addSubcommand("stop", new CommandLine(new InfraStop(this))))
                 .addSubcommand("init", new CommandLine(new Init(this)))
                 .addSubcommand("jolokia", new CommandLine(new Jolokia(this)))
-                .addSubcommand("log", new CommandLine(new CamelLogAction(this)))
+                .addSubcommand("health", new CommandLine(new CamelHealthTui(this)))
+                .addSubcommand("log", new CommandLine(new CamelLogAction(this))
+                        .addSubcommand("tui", new CommandLine(new CamelLogTui(this))))
                 .addSubcommand("nano", new CommandLine(new Nano(this)))
                 .addSubcommand("plugin", new CommandLine(new PluginCommand(this))
                         .addSubcommand("add", new CommandLine(new PluginAdd(this)))
@@ -187,6 +191,7 @@ public class CamelJBangMain implements Callable<Integer> {
                 .addSubcommand("run", new CommandLine(new Run(this)))
                 .addSubcommand("sbom", new CommandLine(new SBOMGenerator(this)))
                 .addSubcommand("script", new CommandLine(new Script(this)))
+                .addSubcommand("monitor", new CommandLine(new CamelMonitor(this)))
                 .addSubcommand("shell", new CommandLine(new Shell(this)))
                 .addSubcommand("stop", new CommandLine(new StopProcess(this)))
                 .addSubcommand("top", new CommandLine(new CamelTop(this))
@@ -194,8 +199,10 @@ public class CamelJBangMain implements Callable<Integer> {
                         .addSubcommand("group", new CommandLine(new CamelRouteGroupTop(this)))
                         .addSubcommand("processor", new CommandLine(new CamelProcessorTop(this)))
                         .addSubcommand("route", new CommandLine(new CamelRouteTop(this)))
-                        .addSubcommand("source", new CommandLine(new CamelSourceTop(this))))
-                .addSubcommand("trace", new CommandLine(new CamelTraceAction(this)))
+                        .addSubcommand("source", new CommandLine(new CamelSourceTop(this)))
+                        .addSubcommand("tui", new CommandLine(new CamelTopTui(this))))
+                .addSubcommand("trace", new CommandLine(new CamelTraceAction(this))
+                        .addSubcommand("tui", new CommandLine(new CamelTraceTui(this))))
                 .addSubcommand("transform", new CommandLine(new TransformCommand(this))
                         .addSubcommand("dataweave", new CommandLine(new TransformDataWeave(this)))
                         .addSubcommand("message", new CommandLine(new TransformMessageAction(this)))
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelCatalogTui.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelCatalogTui.java
new file mode 100644
index 000000000000..2c85d1c113a1
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelCatalogTui.java
@@ -0,0 +1,697 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import dev.tamboui.layout.Constraint;
+import dev.tamboui.layout.Layout;
+import dev.tamboui.layout.Rect;
+import dev.tamboui.style.Color;
+import dev.tamboui.style.Overflow;
+import dev.tamboui.style.Style;
+import dev.tamboui.terminal.Frame;
+import dev.tamboui.text.Line;
+import dev.tamboui.text.Span;
+import dev.tamboui.text.Text;
+import dev.tamboui.tui.TuiRunner;
+import dev.tamboui.tui.event.Event;
+import dev.tamboui.tui.event.KeyCode;
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.widgets.block.Block;
+import dev.tamboui.widgets.block.BorderType;
+import dev.tamboui.widgets.paragraph.Paragraph;
+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.catalog.CamelCatalog;
+import org.apache.camel.catalog.DefaultCamelCatalog;
+import org.apache.camel.dsl.jbang.core.commands.CamelCommand;
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
+import picocli.CommandLine.Command;
+
+@Command(name = "catalog-tui",
+         description = "Interactive TUI catalog browser",
+         sortOptions = false)
+public class CamelCatalogTui extends CamelCommand {
+
+    private static final int FOCUS_LIST = 0;
+    private static final int FOCUS_DETAILS = 1;
+
+    // Catalog data
+    private List<ComponentInfo> allComponents = Collections.emptyList();
+    private List<ComponentInfo> filteredComponents = Collections.emptyList();
+
+    // UI state
+    private final TableState listTableState = new TableState();
+    private final TableState optionsTableState = new TableState();
+    private int focus = FOCUS_LIST;
+    private boolean searching;
+    private final StringBuilder searchText = new StringBuilder();
+    private int detailScroll;
+
+    // Selected component details
+    private ComponentDetail selectedDetail;
+
+    public CamelCatalogTui(CamelJBangMain main) {
+        super(main);
+    }
+
+    @Override
+    public Integer doCall() throws Exception {
+        try {
+            Class.forName("dev.tamboui.tui.event.KeyModifiers");
+            Class.forName("dev.tamboui.tui.event.KeyEvent");
+            Class.forName("dev.tamboui.tui.event.KeyCode");
+            Class.forName("picocli.CommandLine$IExitCodeGenerator");
+        } catch (ClassNotFoundException e) {
+            // ignore
+        }
+
+        loadCatalog();
+
+        try (var tui = TuiRunner.create()) {
+            sun.misc.Signal.handle(new sun.misc.Signal("INT"), sig -> tui.quit());
+            tui.run(this::handleEvent, this::render);
+        }
+        return 0;
+    }
+
+    // ---- Catalog Loading ----
+
+    @SuppressWarnings("unchecked")
+    private void loadCatalog() {
+        CamelCatalog catalog = new DefaultCamelCatalog();
+        List<String> names = catalog.findComponentNames();
+        Collections.sort(names);
+
+        allComponents = new ArrayList<>();
+        for (String name : names) {
+            try {
+                String json = catalog.componentJSonSchema(name);
+                if (json == null) {
+                    continue;
+                }
+                JsonObject root = (JsonObject) Jsoner.deserialize(json);
+                JsonObject component = (JsonObject) root.get("component");
+                if (component == null) {
+                    continue;
+                }
+
+                ComponentInfo info = new ComponentInfo();
+                info.name = name;
+                info.title = component.getStringOrDefault("title", name);
+                info.description = component.getStringOrDefault("description", "");
+                info.label = component.getStringOrDefault("label", "");
+                info.groupId = component.getStringOrDefault("groupId", "");
+                info.artifactId = component.getStringOrDefault("artifactId", "");
+                info.version = component.getStringOrDefault("version", "");
+                info.scheme = component.getStringOrDefault("scheme", name);
+                info.syntax = component.getStringOrDefault("syntax", "");
+                info.deprecated = component.getBooleanOrDefault("deprecated", false);
+
+                // Parse component properties
+                JsonObject properties = (JsonObject) root.get("properties");
+                if (properties != null) {
+                    for (Map.Entry<String, Object> entry : properties.entrySet()) {
+                        JsonObject prop = (JsonObject) entry.getValue();
+                        OptionInfo opt = parseOption(entry.getKey(), prop);
+                        opt.kind = "property";
+                        info.componentOptions.add(opt);
+                    }
+                }
+
+                // Parse endpoint properties (headers in newer catalogs, but "properties" is the standard)
+                // The endpoint options are typically under "properties" for the endpoint
+                // In the catalog JSON, component-level options have "kind":"property"
+                // and endpoint options have "kind":"path" or "kind":"parameter"
+                // So we re-scan and separate them
+                info.componentOptions.clear();
+                info.endpointOptions.clear();
+                if (properties != null) {
+                    for (Map.Entry<String, Object> entry : properties.entrySet()) {
+                        JsonObject prop = (JsonObject) entry.getValue();
+                        OptionInfo opt = parseOption(entry.getKey(), prop);
+                        String kind = prop.getStringOrDefault("kind", "parameter");
+                        opt.kind = kind;
+                        if ("property".equals(kind)) {
+                            info.componentOptions.add(opt);
+                        } else {
+                            info.endpointOptions.add(opt);
+                        }
+                    }
+                }
+
+                allComponents.add(info);
+            } catch (Exception e) {
+                // skip unparseable components
+            }
+        }
+
+        applyFilter();
+        if (!filteredComponents.isEmpty()) {
+            listTableState.select(0);
+            updateSelectedDetail();
+        }
+    }
+
+    private OptionInfo parseOption(String name, JsonObject prop) {
+        OptionInfo opt = new OptionInfo();
+        opt.name = name;
+        opt.type = prop.getStringOrDefault("type", "");
+        opt.required = prop.getBooleanOrDefault("required", false);
+        opt.defaultValue = prop.getStringOrDefault("defaultValue", "");
+        opt.description = prop.getStringOrDefault("description", "");
+        opt.group = prop.getStringOrDefault("group", "");
+        opt.enumValues = "";
+        Object enums = prop.get("enum");
+        if (enums instanceof JsonArray arr) {
+            List<String> vals = new ArrayList<>();
+            for (Object v : arr) {
+                vals.add(String.valueOf(v));
+            }
+            opt.enumValues = String.join(", ", vals);
+        }
+        return opt;
+    }
+
+    private void applyFilter() {
+        String filter = searchText.toString().toLowerCase();
+        if (filter.isEmpty()) {
+            filteredComponents = new ArrayList<>(allComponents);
+        } else {
+            filteredComponents = new ArrayList<>();
+            for (ComponentInfo c : allComponents) {
+                if (c.name.toLowerCase().contains(filter)
+                        || c.title.toLowerCase().contains(filter)
+                        || c.description.toLowerCase().contains(filter)
+                        || c.label.toLowerCase().contains(filter)) {
+                    filteredComponents.add(c);
+                }
+            }
+        }
+        // Reset selection
+        if (!filteredComponents.isEmpty()) {
+            listTableState.select(0);
+        } else {
+            listTableState.clearSelection();
+        }
+        updateSelectedDetail();
+    }
+
+    private void updateSelectedDetail() {
+        Integer sel = listTableState.selected();
+        if (sel != null && sel >= 0 && sel < filteredComponents.size()) {
+            ComponentInfo info = filteredComponents.get(sel);
+            selectedDetail = new ComponentDetail(info);
+            detailScroll = 0;
+        } else {
+            selectedDetail = null;
+        }
+    }
+
+    // ---- Event Handling ----
+
+    private boolean handleEvent(Event event, TuiRunner runner) {
+        if (event instanceof KeyEvent ke) {
+            // Search mode handling
+            if (searching) {
+                if (ke.isKey(KeyCode.ESCAPE)) {
+                    if (searchText.isEmpty()) {
+                        searching = false;
+                    } else {
+                        searchText.setLength(0);
+                        applyFilter();
+                    }
+                    return true;
+                }
+                if (ke.isKey(KeyCode.ENTER)) {
+                    searching = false;
+                    return true;
+                }
+                if (ke.isKey(KeyCode.BACKSPACE)) {
+                    if (!searchText.isEmpty()) {
+                        searchText.deleteCharAt(searchText.length() - 1);
+                        applyFilter();
+                    }
+                    return true;
+                }
+                if (ke.isUp()) {
+                    listTableState.selectPrevious();
+                    updateSelectedDetail();
+                    return true;
+                }
+                if (ke.isDown()) {
+                    listTableState.selectNext(filteredComponents.size());
+                    updateSelectedDetail();
+                    return true;
+                }
+                // Typed character
+                if (ke.code() == KeyCode.CHAR) {
+                    searchText.append(ke.character());
+                    applyFilter();
+                    return true;
+                }
+                return true;
+            }
+
+            // Normal mode
+            if (ke.isQuit() || ke.isCharIgnoreCase('q') || ke.isKey(KeyCode.ESCAPE)) {
+                runner.quit();
+                return true;
+            }
+
+            if (ke.isChar('/')) {
+                searching = true;
+                return true;
+            }
+
+            if (ke.isKey(KeyCode.TAB)) {
+                focus = (focus == FOCUS_LIST) ? FOCUS_DETAILS : FOCUS_LIST;
+                return true;
+            }
+
+            if (ke.isUp()) {
+                if (focus == FOCUS_LIST) {
+                    listTableState.selectPrevious();
+                    updateSelectedDetail();
+                } else {
+                    detailScroll = Math.max(0, detailScroll - 1);
+                }
+                return true;
+            }
+            if (ke.isDown()) {
+                if (focus == FOCUS_LIST) {
+                    listTableState.selectNext(filteredComponents.size());
+                    updateSelectedDetail();
+                } else {
+                    detailScroll++;
+                }
+                return true;
+            }
+            if (ke.isKey(KeyCode.PAGE_UP)) {
+                if (focus == FOCUS_DETAILS) {
+                    detailScroll = Math.max(0, detailScroll - 20);
+                }
+                return true;
+            }
+            if (ke.isKey(KeyCode.PAGE_DOWN)) {
+                if (focus == FOCUS_DETAILS) {
+                    detailScroll += 20;
+                }
+                return true;
+            }
+            if (ke.isKey(KeyCode.ENTER)) {
+                if (focus == FOCUS_LIST) {
+                    updateSelectedDetail();
+                    focus = FOCUS_DETAILS;
+                }
+                return true;
+            }
+        }
+        return false;
+    }
+
+    // ---- Rendering ----
+
+    private void render(Frame frame) {
+        Rect area = frame.area();
+
+        // Layout: header (3 rows) + content (fill) + footer (1 row)
+        List<Rect> mainChunks = Layout.vertical()
+                .constraints(
+                        Constraint.length(3),
+                        Constraint.fill(),
+                        Constraint.length(1))
+                .split(area);
+
+        renderHeader(frame, mainChunks.get(0));
+        renderContent(frame, mainChunks.get(1));
+        renderFooter(frame, mainChunks.get(2));
+    }
+
+    private void renderHeader(Frame frame, Rect area) {
+        String searchStatus;
+        if (searching) {
+            searchStatus = "  Search: " + searchText + "_";
+        } else if (!searchText.isEmpty()) {
+            searchStatus = "  Filter: \"" + searchText + "\"";
+        } else {
+            searchStatus = "";
+        }
+
+        Line titleLine = Line.from(
+                Span.styled(" Camel Catalog", Style.create().fg(Color.rgb(0xF6, 0x91, 0x23)).bold()),
+                Span.raw("  "),
+                Span.styled(filteredComponents.size() + "/" + allComponents.size() + " components",
+                        Style.create().fg(Color.CYAN)),
+                Span.styled(searchStatus, Style.create().fg(Color.YELLOW)));
+
+        Block headerBlock = Block.builder()
+                .borderType(BorderType.ROUNDED)
+                .title(" Apache Camel ")
+                .build();
+
+        frame.renderWidget(
+                Paragraph.builder().text(Text.from(titleLine)).block(headerBlock).build(),
+                area);
+    }
+
+    private void renderContent(Frame frame, Rect area) {
+        // Split horizontally: left panel (30%) + right panel (70%)
+        List<Rect> chunks = Layout.horizontal()
+                .constraints(
+                        Constraint.percentage(30),
+                        Constraint.percentage(70))
+                .split(area);
+
+        renderComponentList(frame, chunks.get(0));
+        renderComponentDetails(frame, chunks.get(1));
+    }
+
+    private void renderComponentList(Frame frame, Rect area) {
+        List<Row> rows = new ArrayList<>();
+        for (ComponentInfo comp : filteredComponents) {
+            Style nameStyle = comp.deprecated
+                    ? Style.create().fg(Color.RED).dim()
+                    : Style.create().fg(Color.CYAN);
+            String label = comp.name;
+            if (comp.deprecated) {
+                label = label + " (deprecated)";
+            }
+            rows.add(Row.from(Cell.from(Span.styled(label, nameStyle))));
+        }
+
+        if (rows.isEmpty()) {
+            rows.add(Row.from(Cell.from(Span.styled("No matching components", Style.create().dim()))));
+        }
+
+        Style borderStyle = focus == FOCUS_LIST
+                ? Style.create().fg(Color.rgb(0xF6, 0x91, 0x23))
+                : Style.create();
+
+        Table table = Table.builder()
+                .rows(rows)
+                .widths(Constraint.fill())
+                .highlightStyle(Style.create().fg(Color.WHITE).bold().onBlue())
+                .block(Block.builder()
+                        .borderType(BorderType.ROUNDED)
+                        .borderStyle(borderStyle)
+                        .title(" Components ")
+                        .build())
+                .build();
+
+        frame.renderStatefulWidget(table, area, listTableState);
+    }
+
+    private void renderComponentDetails(Frame frame, Rect area) {
+        Style borderStyle = focus == FOCUS_DETAILS
+                ? Style.create().fg(Color.rgb(0xF6, 0x91, 0x23))
+                : Style.create();
+
+        if (selectedDetail == null) {
+            frame.renderWidget(
+                    Paragraph.builder()
+                            .text(Text.from(Line.from(
+                                    Span.styled(" Select a component from the list",
+                                            Style.create().dim()))))
+                            .block(Block.builder()
+                                    .borderType(BorderType.ROUNDED)
+                                    .borderStyle(borderStyle)
+                                    .title(" Details ")
+                                    .build())
+                            .build(),
+                    area);
+            return;
+        }
+
+        ComponentInfo comp = selectedDetail.info;
+
+        // Split: info area (top) + options table (bottom)
+        List<Rect> chunks = Layout.vertical()
+                .constraints(
+                        Constraint.length(9),
+                        Constraint.fill())
+                .split(area);
+
+        // Component info panel
+        List<Line> infoLines = new ArrayList<>();
+        infoLines.add(Line.from(
+                Span.styled(" Title: ", Style.create().bold()),
+                Span.styled(comp.title, Style.create().fg(Color.CYAN))));
+        infoLines.add(Line.from(
+                Span.styled(" Scheme: ", Style.create().bold()),
+                Span.raw(comp.scheme)));
+        infoLines.add(Line.from(
+                Span.styled(" Syntax: ", Style.create().bold()),
+                Span.raw(comp.syntax)));
+        infoLines.add(Line.from(
+                Span.styled(" Label: ", Style.create().bold()),
+                Span.styled(comp.label, Style.create().fg(Color.GREEN))));
+        infoLines.add(Line.from(
+                Span.styled(" Maven: ", Style.create().bold()),
+                Span.raw(comp.groupId + ":" + comp.artifactId + ":" + comp.version)));
+        if (comp.deprecated) {
+            infoLines.add(Line.from(
+                    Span.styled(" Status: ", Style.create().bold()),
+                    Span.styled("DEPRECATED", Style.create().fg(Color.RED).bold())));
+        }
+        infoLines.add(Line.from(Span.raw("")));
+        // Word-wrap description into the available width
+        int descWidth = Math.max(10, chunks.get(0).width() - 4);
+        String desc = comp.description;
+        if (desc.length() > descWidth) {
+            infoLines.add(Line.from(
+                    Span.styled(" " + desc.substring(0, descWidth), Style.create().dim())));
+            if (desc.length() > descWidth * 2) {
+                infoLines.add(Line.from(
+                        Span.styled(" " + desc.substring(descWidth, descWidth * 2) + "...", Style.create().dim())));
+            } else {
+                infoLines.add(Line.from(
+                        Span.styled(" " + desc.substring(descWidth), Style.create().dim())));
+            }
+        } else {
+            infoLines.add(Line.from(Span.styled(" " + desc, Style.create().dim())));
+        }
+
+        frame.renderWidget(
+                Paragraph.builder()
+                        .text(Text.from(infoLines))
+                        .overflow(Overflow.CLIP)
+                        .block(Block.builder()
+                                .borderType(BorderType.ROUNDED)
+                                .borderStyle(borderStyle)
+                                .title(" " + comp.title + " ")
+                                .build())
+                        .build(),
+                chunks.get(0));
+
+        // Options table
+        renderOptionsTable(frame, chunks.get(1), borderStyle);
+    }
+
+    private void renderOptionsTable(Frame frame, Rect area, Style borderStyle) {
+        if (selectedDetail == null) {
+            return;
+        }
+
+        ComponentInfo comp = selectedDetail.info;
+
+        // Combine all options with section headers
+        List<Row> rows = new ArrayList<>();
+
+        if (!comp.endpointOptions.isEmpty()) {
+            rows.add(Row.from(
+                    Cell.from(Span.styled("--- Endpoint Options ---", Style.create().fg(Color.YELLOW).bold())),
+                    Cell.from(""),
+                    Cell.from(""),
+                    Cell.from(""),
+                    Cell.from("")));
+            for (OptionInfo opt : comp.endpointOptions) {
+                rows.add(optionToRow(opt));
+            }
+        }
+
+        if (!comp.componentOptions.isEmpty()) {
+            rows.add(Row.from(
+                    Cell.from(Span.styled("--- Component Options ---", Style.create().fg(Color.YELLOW).bold())),
+                    Cell.from(""),
+                    Cell.from(""),
+                    Cell.from(""),
+                    Cell.from("")));
+            for (OptionInfo opt : comp.componentOptions) {
+                rows.add(optionToRow(opt));
+            }
+        }
+
+        if (rows.isEmpty()) {
+            rows.add(Row.from(
+                    Cell.from(Span.styled("No options", Style.create().dim())),
+                    Cell.from(""),
+                    Cell.from(""),
+                    Cell.from(""),
+                    Cell.from("")));
+        }
+
+        // Apply scrolling
+        int innerHeight = Math.max(1, area.height() - 3); // border + header
+        int maxScroll = Math.max(0, rows.size() - innerHeight);
+        if (detailScroll > maxScroll) {
+            detailScroll = maxScroll;
+        }
+        List<Row> visibleRows;
+        if (detailScroll > 0 && detailScroll < rows.size()) {
+            int end = Math.min(detailScroll + innerHeight, rows.size());
+            visibleRows = rows.subList(detailScroll, end);
+        } else {
+            visibleRows = rows;
+        }
+
+        Row header = Row.from(
+                Cell.from(Span.styled("NAME", Style.create().bold())),
+                Cell.from(Span.styled("TYPE", Style.create().bold())),
+                Cell.from(Span.styled("REQ", Style.create().bold())),
+                Cell.from(Span.styled("DEFAULT", Style.create().bold())),
+                Cell.from(Span.styled("DESCRIPTION", Style.create().bold())));
+
+        String scrollInfo = rows.size() > innerHeight
+                ? " [" + (detailScroll + 1) + "-" + Math.min(detailScroll + innerHeight, rows.size())
+                  + "/" + rows.size() + "] "
+                : " ";
+
+        Table table = Table.builder()
+                .rows(visibleRows)
+                .header(header)
+                .widths(
+                        Constraint.length(25),
+                        Constraint.length(12),
+                        Constraint.length(4),
+                        Constraint.length(12),
+                        Constraint.fill())
+                .block(Block.builder()
+                        .borderType(BorderType.ROUNDED)
+                        .borderStyle(borderStyle)
+                        .title(" Options" + scrollInfo)
+                        .build())
+                .build();
+
+        frame.renderStatefulWidget(table, area, optionsTableState);
+    }
+
+    private Row optionToRow(OptionInfo opt) {
+        Style nameStyle = opt.required
+                ? Style.create().fg(Color.CYAN).bold()
+                : Style.create().fg(Color.CYAN);
+
+        String desc = opt.description;
+        if (!opt.enumValues.isEmpty()) {
+            desc = desc + " [" + opt.enumValues + "]";
+        }
+
+        return Row.from(
+                Cell.from(Span.styled(opt.name, nameStyle)),
+                Cell.from(Span.styled(opt.type, Style.create().dim())),
+                Cell.from(opt.required
+                        ? Span.styled("*", Style.create().fg(Color.RED).bold())
+                        : Span.raw("")),
+                Cell.from(Span.styled(opt.defaultValue, Style.create().dim())),
+                Cell.from(truncate(desc, 80)));
+    }
+
+    private void renderFooter(Frame frame, Rect area) {
+        Line footer;
+        if (searching) {
+            footer = Line.from(
+                    Span.styled(" Type", Style.create().fg(Color.YELLOW).bold()),
+                    Span.raw(" to filter  "),
+                    Span.styled("Enter", Style.create().fg(Color.YELLOW).bold()),
+                    Span.raw(" confirm  "),
+                    Span.styled("Esc", Style.create().fg(Color.YELLOW).bold()),
+                    Span.raw(" clear/cancel  "),
+                    Span.styled("\u2191\u2193", Style.create().fg(Color.YELLOW).bold()),
+                    Span.raw(" navigate"));
+        } else {
+            footer = Line.from(
+                    Span.styled(" q", Style.create().fg(Color.YELLOW).bold()),
+                    Span.raw(" quit  "),
+                    Span.styled("/", Style.create().fg(Color.YELLOW).bold()),
+                    Span.raw(" search  "),
+                    Span.styled("\u2191\u2193", Style.create().fg(Color.YELLOW).bold()),
+                    Span.raw(" navigate  "),
+                    Span.styled("Tab", Style.create().fg(Color.YELLOW).bold()),
+                    Span.raw(" switch panel  "),
+                    Span.styled("Enter", Style.create().fg(Color.YELLOW).bold()),
+                    Span.raw(" select  "),
+                    Span.styled("PgUp/PgDn", Style.create().fg(Color.YELLOW).bold()),
+                    Span.raw(" scroll details"));
+        }
+
+        frame.renderWidget(Paragraph.from(footer), area);
+    }
+
+    // ---- Helpers ----
+
+    private static String truncate(String s, int max) {
+        if (s == null) {
+            return "";
+        }
+        return s.length() > max ? s.substring(0, max - 1) + "\u2026" : s;
+    }
+
+    // ---- Data Classes ----
+
+    static class ComponentInfo {
+        String name;
+        String title;
+        String description;
+        String label;
+        String groupId;
+        String artifactId;
+        String version;
+        String scheme;
+        String syntax;
+        boolean deprecated;
+        final List<OptionInfo> componentOptions = new ArrayList<>();
+        final List<OptionInfo> endpointOptions = new ArrayList<>();
+    }
+
+    static class OptionInfo {
+        String name;
+        String kind;
+        String type;
+        boolean required;
+        String defaultValue;
+        String description;
+        String group;
+        String enumValues;
+    }
+
+    static class ComponentDetail {
+        final ComponentInfo info;
+
+        ComponentDetail(ComponentInfo info) {
+            this.info = info;
+        }
+    }
+}
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelHealthTui.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelHealthTui.java
new file mode 100644
index 000000000000..e0e1d46548ed
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelHealthTui.java
@@ -0,0 +1,498 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+import dev.tamboui.layout.Constraint;
+import dev.tamboui.layout.Layout;
+import dev.tamboui.layout.Rect;
+import dev.tamboui.style.Color;
+import dev.tamboui.style.Style;
+import dev.tamboui.terminal.Frame;
+import dev.tamboui.text.Line;
+import dev.tamboui.text.Span;
+import dev.tamboui.text.Text;
+import dev.tamboui.tui.TuiRunner;
+import dev.tamboui.tui.event.Event;
+import dev.tamboui.tui.event.KeyCode;
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.tui.event.TickEvent;
+import dev.tamboui.widgets.block.Block;
+import dev.tamboui.widgets.block.BorderType;
+import dev.tamboui.widgets.gauge.Gauge;
+import dev.tamboui.widgets.paragraph.Paragraph;
+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.dsl.jbang.core.commands.CamelCommand;
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.apache.camel.dsl.jbang.core.common.ProcessHelper;
+import org.apache.camel.support.PatternHelper;
+import org.apache.camel.util.FileUtil;
+import org.apache.camel.util.TimeUtils;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
+import picocli.CommandLine;
+import picocli.CommandLine.Command;
+
+@Command(name = "health-tui",
+         description = "TUI health check dashboard",
+         sortOptions = false)
+public class CamelHealthTui extends CamelCommand {
+
+    @CommandLine.Parameters(description = "Name or pid of running Camel integration", arity = "0..1")
+    String name = "*";
+
+    @CommandLine.Option(names = { "--refresh" },
+                        description = "Refresh interval in milliseconds (default: ${DEFAULT-VALUE})",
+                        defaultValue = "1000")
+    long refreshInterval = 1000;
+
+    // State
+    private final AtomicReference<List<HealthRow>> data = new AtomicReference<>(Collections.emptyList());
+    private final TableState tableState = new TableState();
+    private boolean showOnlyDown;
+    private volatile long lastRefresh;
+
+    // Memory info for the selected row's integration
+    private long selectedHeapUsed;
+    private long selectedHeapMax;
+
+    public CamelHealthTui(CamelJBangMain main) {
+        super(main);
+    }
+
+    @Override
+    public Integer doCall() throws Exception {
+        // Eagerly load classes used by the input reader thread and picocli
+        // post-processing to avoid ClassNotFoundException during shutdown
+        // when the Spring Boot LaunchedClassLoader may already be closing
+        try {
+            Class.forName("dev.tamboui.tui.event.KeyModifiers");
+            Class.forName("dev.tamboui.tui.event.KeyEvent");
+            Class.forName("dev.tamboui.tui.event.KeyCode");
+            Class.forName("picocli.CommandLine$IExitCodeGenerator");
+        } catch (ClassNotFoundException e) {
+            // ignore
+        }
+
+        // Initial data load
+        refreshData();
+
+        try (var tui = TuiRunner.create()) {
+            sun.misc.Signal.handle(new sun.misc.Signal("INT"), sig -> tui.quit());
+            tui.run(
+                    this::handleEvent,
+                    this::render);
+        }
+        return 0;
+    }
+
+    // ---- Event Handling ----
+
+    private boolean handleEvent(Event event, TuiRunner runner) {
+        if (event instanceof KeyEvent ke) {
+            if (ke.isQuit() || ke.isCharIgnoreCase('q') || ke.isKey(KeyCode.ESCAPE)) {
+                runner.quit();
+                return true;
+            }
+            if (ke.isChar('r')) {
+                refreshData();
+                return true;
+            }
+            if (ke.isChar('d')) {
+                showOnlyDown = !showOnlyDown;
+                return true;
+            }
+            if (ke.isUp()) {
+                tableState.selectPrevious();
+                return true;
+            }
+            if (ke.isDown()) {
+                List<HealthRow> rows = getFilteredRows();
+                tableState.selectNext(rows.size());
+                return true;
+            }
+        }
+        if (event instanceof TickEvent) {
+            long now = System.currentTimeMillis();
+            if (now - lastRefresh >= refreshInterval) {
+                refreshData();
+            }
+            return true;
+        }
+        return false;
+    }
+
+    // ---- Rendering ----
+
+    private void render(Frame frame) {
+        Rect area = frame.area();
+
+        // Layout: header (3 rows) + health table (fill) + memory gauge (3 rows) + footer (1 row)
+        List<Rect> chunks = Layout.vertical()
+                .constraints(
+                        Constraint.length(3),
+                        Constraint.fill(),
+                        Constraint.length(3),
+                        Constraint.length(1))
+                .split(area);
+
+        renderHeader(frame, chunks.get(0));
+        renderHealthTable(frame, chunks.get(1));
+        renderMemoryGauge(frame, chunks.get(2));
+        renderFooter(frame, chunks.get(3));
+    }
+
+    private void renderHeader(Frame frame, Rect area) {
+        List<HealthRow> allRows = data.get();
+        long upCount = allRows.stream().filter(r -> "UP".equals(r.state)).count();
+        long downCount = allRows.stream().filter(r -> "DOWN".equals(r.state)).count();
+
+        Line titleLine = Line.from(
+                Span.styled(" Health Dashboard", Style.create().fg(Color.rgb(0xF6, 0x91, 0x23)).bold()),
+                Span.raw("  "),
+                Span.styled(upCount + " UP", Style.create().fg(Color.GREEN).bold()),
+                Span.raw(", "),
+                downCount > 0
+                        ? Span.styled(downCount + " DOWN", Style.create().fg(Color.RED).bold())
+                        : Span.styled("0 DOWN", Style.create().fg(Color.GREEN)),
+                showOnlyDown ? Span.styled("  [DOWN only]", Style.create().fg(Color.YELLOW)) : Span.raw(""));
+
+        Block headerBlock = Block.builder()
+                .borderType(BorderType.ROUNDED)
+                .title(" Camel Health Checks ")
+                .build();
+
+        frame.renderWidget(
+                Paragraph.builder().text(Text.from(titleLine)).block(headerBlock).build(),
+                area);
+    }
+
+    private void renderHealthTable(Frame frame, Rect area) {
+        List<HealthRow> rows = getFilteredRows();
+
+        // Update selected integration's memory info
+        Integer sel = tableState.selected();
+        if (sel != null && sel >= 0 && sel < rows.size()) {
+            HealthRow selected = rows.get(sel);
+            selectedHeapUsed = selected.heapMemUsed;
+            selectedHeapMax = selected.heapMemMax;
+        } else if (!rows.isEmpty()) {
+            selectedHeapUsed = rows.get(0).heapMemUsed;
+            selectedHeapMax = rows.get(0).heapMemMax;
+        } else {
+            selectedHeapUsed = 0;
+            selectedHeapMax = 0;
+        }
+
+        List<Row> tableRows = new ArrayList<>();
+        for (HealthRow hr : rows) {
+            Style stateStyle;
+            String icon;
+            if ("UP".equals(hr.state)) {
+                stateStyle = Style.create().fg(Color.GREEN);
+                icon = "\u2714 ";
+            } else if ("DOWN".equals(hr.state)) {
+                stateStyle = Style.create().fg(Color.RED);
+                icon = "\u2716 ";
+            } else {
+                stateStyle = Style.create().fg(Color.YELLOW);
+                icon = "\u26A0 ";
+            }
+
+            String rate = "";
+            if (hr.readiness) {
+                rate += "R";
+            }
+            if (hr.liveness) {
+                rate += rate.isEmpty() ? "L" : "/L";
+            }
+
+            tableRows.add(Row.from(
+                    Cell.from(hr.pid),
+                    Cell.from(Span.styled(truncate(hr.integrationName, 20), Style.create().fg(Color.CYAN))),
+                    Cell.from(hr.group != null ? truncate(hr.group, 12) : ""),
+                    Cell.from(Span.styled(truncate(hr.checkId, 25), Style.create().fg(Color.CYAN))),
+                    Cell.from(Span.styled(icon + hr.state, stateStyle)),
+                    Cell.from(rate),
+                    Cell.from(hr.since != null ? hr.since : ""),
+                    Cell.from(hr.message != null ? truncate(hr.message, 40) : "")));
+        }
+
+        if (tableRows.isEmpty()) {
+            tableRows.add(Row.from(
+                    Cell.from(""),
+                    Cell.from(Span.styled("No health checks found", Style.create().dim())),
+                    Cell.from(""),
+                    Cell.from(""),
+                    Cell.from(""),
+                    Cell.from(""),
+                    Cell.from(""),
+                    Cell.from("")));
+        }
+
+        Row header = Row.from(
+                Cell.from(Span.styled("PID", Style.create().bold())),
+                Cell.from(Span.styled("NAME", Style.create().bold())),
+                Cell.from(Span.styled("GROUP", Style.create().bold())),
+                Cell.from(Span.styled("CHECK", Style.create().bold())),
+                Cell.from(Span.styled("STATUS", Style.create().bold())),
+                Cell.from(Span.styled("RATE", Style.create().bold())),
+                Cell.from(Span.styled("SINCE", Style.create().bold())),
+                Cell.from(Span.styled("MESSAGE", Style.create().bold())));
+
+        Table table = Table.builder()
+                .rows(tableRows)
+                .header(header)
+                .widths(
+                        Constraint.length(8),
+                        Constraint.length(20),
+                        Constraint.length(12),
+                        Constraint.length(25),
+                        Constraint.length(10),
+                        Constraint.length(6),
+                        Constraint.length(8),
+                        Constraint.fill())
+                .highlightStyle(Style.create().fg(Color.WHITE).bold().onBlue())
+                .block(Block.builder().borderType(BorderType.ROUNDED)
+                        .title(showOnlyDown ? " Health Checks [DOWN only] " : " Health Checks ").build())
+                .build();
+
+        frame.renderStatefulWidget(table, area, tableState);
+    }
+
+    private void renderMemoryGauge(Frame frame, Rect area) {
+        if (selectedHeapMax > 0) {
+            int pct = (int) (100.0 * selectedHeapUsed / selectedHeapMax);
+            Style gaugeStyle = pct > 80 ? Style.create().fg(Color.RED)
+                    : pct > 60 ? Style.create().fg(Color.YELLOW) : Style.create().fg(Color.GREEN);
+            Gauge gauge = Gauge.builder()
+                    .percent(pct)
+                    .label(String.format("Heap: %s / %s (%d%%)",
+                            formatBytes(selectedHeapUsed), formatBytes(selectedHeapMax), pct))
+                    .gaugeStyle(gaugeStyle)
+                    .block(Block.builder().borderType(BorderType.ROUNDED).title(" Memory ").build())
+                    .build();
+            frame.renderWidget(gauge, area);
+        } else {
+            frame.renderWidget(
+                    Paragraph.builder()
+                            .text(Text.from(Line.from(Span.styled(" No memory data", Style.create().dim()))))
+                            .block(Block.builder().borderType(BorderType.ROUNDED).title(" Memory ").build())
+                            .build(),
+                    area);
+        }
+    }
+
+    private void renderFooter(Frame frame, Rect area) {
+        String refreshLabel = refreshInterval >= 1000
+                ? (refreshInterval / 1000) + "s"
+                : refreshInterval + "ms";
+
+        Line footer = Line.from(
+                Span.styled(" q", Style.create().fg(Color.YELLOW).bold()),
+                Span.raw("/"),
+                Span.styled("Esc", Style.create().fg(Color.YELLOW).bold()),
+                Span.raw(" quit  "),
+                Span.styled("\u2191\u2193", Style.create().fg(Color.YELLOW).bold()),
+                Span.raw(" navigate  "),
+                Span.styled("r", Style.create().fg(Color.YELLOW).bold()),
+                Span.raw(" refresh  "),
+                Span.styled("d", Style.create().fg(Color.YELLOW).bold()),
+                Span.raw(" toggle DOWN  "),
+                Span.styled("Refresh: " + refreshLabel, Style.create().dim()));
+
+        frame.renderWidget(Paragraph.from(footer), area);
+    }
+
+    // ---- Data Loading ----
+
+    private void refreshData() {
+        lastRefresh = System.currentTimeMillis();
+        try {
+            List<HealthRow> rows = new ArrayList<>();
+            List<Long> pids = findPids(name);
+            ProcessHandle.allProcesses()
+                    .filter(ph -> pids.contains(ph.pid()))
+                    .forEach(ph -> {
+                        JsonObject root = loadStatus(ph.pid());
+                        if (root != null) {
+                            parseHealthRows(ph, root, rows);
+                        }
+                    });
+            data.set(rows);
+        } catch (Exception e) {
+            // ignore refresh errors
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    private void parseHealthRows(ProcessHandle ph, JsonObject root, List<HealthRow> rows) {
+        JsonObject context = (JsonObject) root.get("context");
+        if (context == null) {
+            return;
+        }
+
+        String pid = Long.toString(ph.pid());
+        String integrationName = context.getString("name");
+        if ("CamelJBang".equals(integrationName)) {
+            integrationName = ProcessHelper.extractName(root, ph);
+        }
+
+        String since = TimeUtils.printSince(
+                ph.info().startInstant().map(Instant::toEpochMilli).orElse(0L));
+
+        // Parse memory
+        long heapUsed = 0;
+        long heapMax = 0;
+        JsonObject mem = (JsonObject) root.get("memory");
+        if (mem != null) {
+            heapUsed = mem.getLong("heapMemoryUsed");
+            heapMax = mem.getLong("heapMemoryMax");
+        }
+
+        // Parse health checks
+        JsonObject healthChecks = (JsonObject) root.get("healthChecks");
+        if (healthChecks != null) {
+            JsonArray checks = (JsonArray) healthChecks.get("checks");
+            if (checks != null) {
+                for (Object c : checks) {
+                    JsonObject cj = (JsonObject) c;
+                    HealthRow hr = new HealthRow();
+                    hr.pid = pid;
+                    hr.integrationName = integrationName;
+                    hr.checkId = cj.getString("id");
+                    hr.group = cj.getString("group");
+                    hr.state = cj.getString("state");
+                    hr.readiness = cj.getBooleanOrDefault("readiness", false);
+                    hr.liveness = cj.getBooleanOrDefault("liveness", false);
+                    hr.since = since;
+                    hr.heapMemUsed = heapUsed;
+                    hr.heapMemMax = heapMax;
+
+                    // Extract failure message from details
+                    JsonObject details = (JsonObject) cj.get("details");
+                    if (details != null && details.containsKey("failure.error.message")) {
+                        hr.message = details.getString("failure.error.message");
+                    }
+
+                    rows.add(hr);
+                }
+            }
+        }
+    }
+
+    // ---- Helpers ----
+
+    private List<HealthRow> getFilteredRows() {
+        List<HealthRow> allRows = data.get();
+        if (showOnlyDown) {
+            return allRows.stream().filter(r -> "DOWN".equals(r.state)).toList();
+        }
+        return allRows;
+    }
+
+    private List<Long> findPids(String name) {
+        List<Long> pids = new ArrayList<>();
+        final long cur = ProcessHandle.current().pid();
+        String pattern = name;
+        if (!pattern.matches("\\d+") && !pattern.endsWith("*")) {
+            pattern = pattern + "*";
+        }
+        final String pat = pattern;
+        ProcessHandle.allProcesses()
+                .filter(ph -> ph.pid() != cur)
+                .forEach(ph -> {
+                    JsonObject root = loadStatus(ph.pid());
+                    if (root != null) {
+                        String pName = ProcessHelper.extractName(root, ph);
+                        pName = FileUtil.onlyName(pName);
+                        if (pName != null && !pName.isEmpty() && PatternHelper.matchPattern(pName, pat)) {
+                            pids.add(ph.pid());
+                        } else {
+                            JsonObject context = (JsonObject) root.get("context");
+                            if (context != null) {
+                                pName = context.getString("name");
+                                if ("CamelJBang".equals(pName)) {
+                                    pName = null;
+                                }
+                                if (pName != null && !pName.isEmpty() && PatternHelper.matchPattern(pName, pat)) {
+                                    pids.add(ph.pid());
+                                }
+                            }
+                        }
+                    }
+                });
+        return pids;
+    }
+
+    private JsonObject loadStatus(long pid) {
+        try {
+            Path f = getStatusFile(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;
+    }
+
+    private static String truncate(String s, int max) {
+        if (s == null) {
+            return "";
+        }
+        return s.length() > max ? s.substring(0, max - 1) + "\u2026" : s;
+    }
+
+    private static String formatBytes(long bytes) {
+        if (bytes < 1024) {
+            return bytes + "B";
+        }
+        if (bytes < 1024 * 1024) {
+            return (bytes / 1024) + "K";
+        }
+        return (bytes / (1024 * 1024)) + "M";
+    }
+
+    // ---- Data Class ----
+
+    static class HealthRow {
+        String pid;
+        String integrationName;
+        String group;
+        String checkId;
+        String state;
+        boolean readiness;
+        boolean liveness;
+        String since;
+        String message;
+        long heapMemUsed;
+        long heapMemMax;
+    }
+}
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelLogTui.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelLogTui.java
new file mode 100644
index 000000000000..1bede16a8396
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelLogTui.java
@@ -0,0 +1,498 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+
+import dev.tamboui.layout.Constraint;
+import dev.tamboui.layout.Layout;
+import dev.tamboui.layout.Rect;
+import dev.tamboui.style.Color;
+import dev.tamboui.style.Overflow;
+import dev.tamboui.style.Style;
+import dev.tamboui.terminal.Frame;
+import dev.tamboui.text.Line;
+import dev.tamboui.text.Span;
+import dev.tamboui.text.Text;
+import dev.tamboui.tui.TuiRunner;
+import dev.tamboui.tui.event.Event;
+import dev.tamboui.tui.event.KeyCode;
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.tui.event.TickEvent;
+import dev.tamboui.widgets.block.Block;
+import dev.tamboui.widgets.block.BorderType;
+import dev.tamboui.widgets.paragraph.Paragraph;
+import org.apache.camel.dsl.jbang.core.commands.CamelCommand;
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
+import org.apache.camel.dsl.jbang.core.common.ProcessHelper;
+import org.apache.camel.support.PatternHelper;
+import org.apache.camel.util.FileUtil;
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
+import picocli.CommandLine;
+import picocli.CommandLine.Command;
+
+@Command(name = "log-tui",
+         description = "TUI log viewer for Camel integrations",
+         sortOptions = false)
+public class CamelLogTui extends CamelCommand {
+
+    private static final int MAX_READ_BYTES = 64 * 1024;
+
+    @CommandLine.Parameters(description = "Name or pid of running Camel integration", arity = "0..1")
+    String name = "*";
+
+    @CommandLine.Option(names = { "--refresh" },
+                        description = "Refresh interval in milliseconds (default: ${DEFAULT-VALUE})",
+                        defaultValue = "500")
+    long refreshInterval = 500;
+
+    @CommandLine.Option(names = { "--grep" },
+                        description = "Filter logs to matching lines")
+    String grep;
+
+    @CommandLine.Option(names = { "--level" },
+                        description = "Filter by log level (INFO, WARN, ERROR, DEBUG)")
+    String level;
+
+    // State
+    private final List<String> logLines = new ArrayList<>();
+    private final List<String> filteredLines = new ArrayList<>();
+    private int scrollOffset;
+    private boolean followMode = true;
+
+    // Level toggle filters (all enabled by default)
+    private boolean showTrace = true;
+    private boolean showDebug = true;
+    private boolean showInfo = true;
+    private boolean showWarn = true;
+    private boolean showError = true;
+
+    // Integration info
+    private String resolvedPid;
+    private String integrationName;
+    private Path logFilePath;
+
+    private volatile long lastRefresh;
+
+    public CamelLogTui(CamelJBangMain main) {
+        super(main);
+    }
+
+    @Override
+    public Integer doCall() throws Exception {
+        // Eagerly load classes used by the input reader thread and picocli
+        // post-processing to avoid ClassNotFoundException during shutdown
+        try {
+            Class.forName("dev.tamboui.tui.event.KeyModifiers");
+            Class.forName("dev.tamboui.tui.event.KeyEvent");
+            Class.forName("dev.tamboui.tui.event.KeyCode");
+            Class.forName("picocli.CommandLine$IExitCodeGenerator");
+        } catch (ClassNotFoundException e) {
+            // ignore
+        }
+
+        // Apply --level as initial level filter
+        if (level != null) {
+            String lv = level.toUpperCase();
+            showTrace = "TRACE".equals(lv);
+            showDebug = "DEBUG".equals(lv) || "TRACE".equals(lv);
+            showInfo = "INFO".equals(lv) || "DEBUG".equals(lv) || "TRACE".equals(lv);
+            showWarn = "WARN".equals(lv) || "INFO".equals(lv) || "DEBUG".equals(lv) || "TRACE".equals(lv);
+            showError = true; // always show errors
+        }
+
+        // Initial data load
+        resolveIntegration();
+        refreshLogData();
+
+        try (var tui = TuiRunner.create()) {
+            sun.misc.Signal.handle(new sun.misc.Signal("INT"), sig -> tui.quit());
+            tui.run(
+                    this::handleEvent,
+                    this::render);
+        }
+        return 0;
+    }
+
+    // ---- Event Handling ----
+
+    private boolean handleEvent(Event event, TuiRunner runner) {
+        if (event instanceof KeyEvent ke) {
+            if (ke.isQuit() || ke.isCharIgnoreCase('q') || ke.isKey(KeyCode.ESCAPE)) {
+                runner.quit();
+                return true;
+            }
+            if (ke.isUp()) {
+                followMode = false;
+                scrollOffset = Math.max(0, scrollOffset - 1);
+                return true;
+            }
+            if (ke.isDown()) {
+                scrollOffset++;
+                return true;
+            }
+            if (ke.isKey(KeyCode.PAGE_UP)) {
+                followMode = false;
+                scrollOffset = Math.max(0, scrollOffset - 20);
+                return true;
+            }
+            if (ke.isKey(KeyCode.PAGE_DOWN)) {
+                scrollOffset += 20;
+                return true;
+            }
+            if (ke.isChar('g')) {
+                followMode = false;
+                scrollOffset = 0;
+                return true;
+            }
+            if (ke.isChar('G')) {
+                followMode = true;
+                return true;
+            }
+            if (ke.isChar('f')) {
+                followMode = !followMode;
+                return true;
+            }
+            // Level toggles: 1=TRACE, 2=DEBUG, 3=INFO, 4=WARN, 5=ERROR
+            if (ke.isChar('1')) {
+                showTrace = !showTrace;
+                applyFilters();
+                return true;
+            }
+            if (ke.isChar('2')) {
+                showDebug = !showDebug;
+                applyFilters();
+                return true;
+            }
+            if (ke.isChar('3')) {
+                showInfo = !showInfo;
+                applyFilters();
+                return true;
+            }
+            if (ke.isChar('4')) {
+                showWarn = !showWarn;
+                applyFilters();
+                return true;
+            }
+            if (ke.isChar('5')) {
+                showError = !showError;
+                applyFilters();
+                return true;
+            }
+        }
+        if (event instanceof TickEvent) {
+            long now = System.currentTimeMillis();
+            if (now - lastRefresh >= refreshInterval) {
+                resolveIntegration();
+                refreshLogData();
+            }
+            return true;
+        }
+        return false;
+    }
+
+    // ---- Rendering ----
+
+    private void render(Frame frame) {
+        Rect area = frame.area();
+
+        // Layout: header (3 rows) + log area (fill) + footer (1 row)
+        List<Rect> mainChunks = Layout.vertical()
+                .constraints(
+                        Constraint.length(3),
+                        Constraint.fill(),
+                        Constraint.length(1))
+                .split(area);
+
+        renderHeader(frame, mainChunks.get(0));
+        renderLogArea(frame, mainChunks.get(1));
+        renderFooter(frame, mainChunks.get(2));
+    }
+
+    private void renderHeader(Frame frame, Rect area) {
+        String titleText = integrationName != null ? integrationName : name;
+        String logInfo = logFilePath != null ? logFilePath.getFileName().toString() : "no log file";
+        String lineCount = filteredLines.size() + " lines";
+
+        Line titleLine = Line.from(
+                Span.styled(" Log Viewer", Style.create().fg(Color.rgb(0xF6, 0x91, 0x23)).bold()),
+                Span.raw("  "),
+                Span.styled(titleText, Style.create().fg(Color.CYAN)),
+                Span.raw("  "),
+                Span.styled(logInfo, Style.create().dim()),
+                Span.raw("  "),
+                Span.styled(lineCount, Style.create().fg(Color.GREEN)),
+                followMode
+                        ? Span.styled("  [FOLLOW]", Style.create().fg(Color.YELLOW).bold())
+                        : Span.raw(""));
+
+        Block headerBlock = Block.builder()
+                .borderType(BorderType.ROUNDED)
+                .title(" Camel Log ")
+                .build();
+
+        frame.renderWidget(
+                Paragraph.builder().text(Text.from(titleLine)).block(headerBlock).build(),
+                area);
+    }
+
+    private void renderLogArea(Frame frame, Rect area) {
+        Block logBlock = Block.builder()
+                .borderType(BorderType.ROUNDED)
+                .title(buildLevelFilterTitle())
+                .build();
+
+        int innerHeight = Math.max(1, area.height() - 2);
+        int totalLines = filteredLines.size();
+
+        int startLine;
+        if (followMode) {
+            startLine = Math.max(0, totalLines - innerHeight);
+            scrollOffset = startLine;
+        } else {
+            scrollOffset = Math.max(0, Math.min(scrollOffset, Math.max(0, totalLines - innerHeight)));
+            startLine = scrollOffset;
+        }
+
+        List<Line> visibleLines = new ArrayList<>();
+        for (int i = startLine; i < Math.min(startLine + innerHeight, totalLines); i++) {
+            visibleLines.add(colorizeLogLine(filteredLines.get(i)));
+        }
+
+        // Fill remaining space
+        while (visibleLines.size() < innerHeight) {
+            visibleLines.add(Line.from(Span.raw("")));
+        }
+
+        Paragraph logParagraph = Paragraph.builder()
+                .text(Text.from(visibleLines))
+                .overflow(Overflow.CLIP)
+                .block(logBlock)
+                .build();
+
+        frame.renderWidget(logParagraph, area);
+    }
+
+    private String buildLevelFilterTitle() {
+        StringBuilder sb = new StringBuilder(" Levels: ");
+        sb.append(showTrace ? "[1:TRACE] " : "[1:trace] ");
+        sb.append(showDebug ? "[2:DEBUG] " : "[2:debug] ");
+        sb.append(showInfo ? "[3:INFO] " : "[3:info] ");
+        sb.append(showWarn ? "[4:WARN] " : "[4:warn] ");
+        sb.append(showError ? "[5:ERROR] " : "[5:error] ");
+        return sb.toString();
+    }
+
+    private void renderFooter(Frame frame, Rect area) {
+        Line footer = Line.from(
+                Span.styled(" q", Style.create().fg(Color.YELLOW).bold()),
+                Span.raw(" quit  "),
+                Span.styled("\u2191\u2193", Style.create().fg(Color.YELLOW).bold()),
+                Span.raw(" scroll  "),
+                Span.styled("g", Style.create().fg(Color.YELLOW).bold()),
+                Span.raw("/"),
+                Span.styled("G", Style.create().fg(Color.YELLOW).bold()),
+                Span.raw(" top/bottom  "),
+                Span.styled("f", Style.create().fg(Color.YELLOW).bold()),
+                Span.raw(" follow  "),
+                Span.styled("PgUp/PgDn", Style.create().fg(Color.YELLOW).bold()),
+                Span.raw(" page  "),
+                Span.styled("1-5", Style.create().fg(Color.YELLOW).bold()),
+                Span.raw(" toggle levels"),
+                grep != null
+                        ? Span.styled("  grep:" + grep, Style.create().fg(Color.MAGENTA))
+                        : Span.raw(""));
+
+        frame.renderWidget(Paragraph.from(footer), area);
+    }
+
+    private Line colorizeLogLine(String line) {
+        if (line.contains(" ERROR ") || line.contains(" FATAL ")) {
+            return Line.from(Span.styled(line, Style.create().fg(Color.RED)));
+        } else if (line.contains(" WARN ")) {
+            return Line.from(Span.styled(line, Style.create().fg(Color.YELLOW)));
+        } else if (line.contains(" DEBUG ")) {
+            return Line.from(Span.styled(line, Style.create().dim()));
+        } else if (line.contains(" TRACE ")) {
+            return Line.from(Span.styled(line, Style.create().dim()));
+        }
+        return Line.from(Span.raw(line));
+    }
+
+    // ---- Data Loading ----
+
+    private void resolveIntegration() {
+        if (resolvedPid != null) {
+            // Check if still alive
+            try {
+                ProcessHandle.of(Long.parseLong(resolvedPid)).ifPresentOrElse(
+                        ph -> {
+                            if (!ph.isAlive()) {
+                                resolvedPid = null;
+                                integrationName = null;
+                                logFilePath = null;
+                            }
+                        },
+                        () -> {
+                            resolvedPid = null;
+                            integrationName = null;
+                            logFilePath = null;
+                        });
+            } catch (NumberFormatException e) {
+                // ignore
+            }
+        }
+
+        if (resolvedPid == null) {
+            List<Long> pids = findPids(name);
+            if (!pids.isEmpty()) {
+                long pid = pids.get(0);
+                resolvedPid = Long.toString(pid);
+                logFilePath = CommandLineHelper.getCamelDir().resolve(resolvedPid + ".log");
+
+                // Load integration name
+                JsonObject root = loadStatus(pid);
+                if (root != null) {
+                    JsonObject context = (JsonObject) root.get("context");
+                    if (context != null) {
+                        integrationName = context.getString("name");
+                        if ("CamelJBang".equals(integrationName)) {
+                            ProcessHandle.of(pid).ifPresent(
+                                    ph -> integrationName = ProcessHelper.extractName(root, ph));
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    private void refreshLogData() {
+        lastRefresh = System.currentTimeMillis();
+        readLogFile();
+        applyFilters();
+    }
+
+    private void readLogFile() {
+        logLines.clear();
+        if (logFilePath == null || !Files.exists(logFilePath)) {
+            return;
+        }
+        try (RandomAccessFile raf = new RandomAccessFile(logFilePath.toFile(), "r")) {
+            long length = raf.length();
+            long startPos = Math.max(0, length - MAX_READ_BYTES);
+            raf.seek(startPos);
+            if (startPos > 0) {
+                raf.readLine(); // skip partial line
+            }
+            byte[] remaining = new byte[(int) (length - raf.getFilePointer())];
+            raf.readFully(remaining);
+            String content = new String(remaining, StandardCharsets.UTF_8);
+            String[] lines = content.split("\n", -1);
+            for (String line : lines) {
+                if (!line.isEmpty()) {
+                    logLines.add(line);
+                }
+            }
+        } catch (IOException e) {
+            // ignore
+        }
+    }
+
+    private void applyFilters() {
+        filteredLines.clear();
+        for (String line : logLines) {
+            if (!matchesLevelFilter(line)) {
+                continue;
+            }
+            if (grep != null && !grep.isEmpty() && !line.contains(grep)) {
+                continue;
+            }
+            filteredLines.add(line);
+        }
+    }
+
+    private boolean matchesLevelFilter(String line) {
+        if (line.contains(" ERROR ") || line.contains(" FATAL ")) {
+            return showError;
+        } else if (line.contains(" WARN ")) {
+            return showWarn;
+        } else if (line.contains(" DEBUG ")) {
+            return showDebug;
+        } else if (line.contains(" TRACE ")) {
+            return showTrace;
+        }
+        // Lines without a recognized level are treated as INFO
+        return showInfo;
+    }
+
+    // ---- Helpers ----
+
+    private List<Long> findPids(String name) {
+        List<Long> pids = new ArrayList<>();
+        final long cur = ProcessHandle.current().pid();
+        String pattern = name;
+        if (!pattern.matches("\\d+") && !pattern.endsWith("*")) {
+            pattern = pattern + "*";
+        }
+        final String pat = pattern;
+        ProcessHandle.allProcesses()
+                .filter(ph -> ph.pid() != cur)
+                .forEach(ph -> {
+                    JsonObject root = loadStatus(ph.pid());
+                    if (root != null) {
+                        String pName = ProcessHelper.extractName(root, ph);
+                        pName = FileUtil.onlyName(pName);
+                        if (pName != null && !pName.isEmpty() && PatternHelper.matchPattern(pName, pat)) {
+                            pids.add(ph.pid());
+                        } else {
+                            JsonObject context = (JsonObject) root.get("context");
+                            if (context != null) {
+                                pName = context.getString("name");
+                                if ("CamelJBang".equals(pName)) {
+                                    pName = null;
+                                }
+                                if (pName != null && !pName.isEmpty() && PatternHelper.matchPattern(pName, pat)) {
+                                    pids.add(ph.pid());
+                                }
+                            }
+                        }
+                    }
+                });
+        return pids;
+    }
+
+    private JsonObject loadStatus(long pid) {
+        try {
+            Path f = getStatusFile(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;
+    }
+}
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
new file mode 100644
index 000000000000..df8d3dc0dfae
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
@@ -0,0 +1,1301 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Collectors;
+
+import dev.tamboui.layout.Constraint;
+import dev.tamboui.layout.Layout;
+import dev.tamboui.layout.Rect;
+import dev.tamboui.style.Color;
+import dev.tamboui.style.Overflow;
+import dev.tamboui.style.Style;
+import dev.tamboui.terminal.Frame;
+import dev.tamboui.text.Line;
+import dev.tamboui.text.Span;
+import dev.tamboui.text.Text;
+import dev.tamboui.tui.TuiRunner;
+import dev.tamboui.tui.event.Event;
+import dev.tamboui.tui.event.KeyCode;
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.tui.event.TickEvent;
+import dev.tamboui.widgets.barchart.Bar;
+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.gauge.Gauge;
+import dev.tamboui.widgets.paragraph.Paragraph;
+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 dev.tamboui.widgets.tabs.Tabs;
+import dev.tamboui.widgets.tabs.TabsState;
+import org.apache.camel.dsl.jbang.core.commands.CamelCommand;
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
+import org.apache.camel.dsl.jbang.core.common.ProcessHelper;
+import org.apache.camel.dsl.jbang.core.common.VersionHelper;
+import org.apache.camel.support.PatternHelper;
+import org.apache.camel.util.FileUtil;
+import org.apache.camel.util.TimeUtils;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
+import picocli.CommandLine;
+import picocli.CommandLine.Command;
+
+import static org.apache.camel.dsl.jbang.core.common.CamelCommandHelper.extractState;
+
+@Command(name = "monitor",
+         description = "Live TUI dashboard for monitoring Camel integrations",
+         sortOptions = false)
+public class CamelMonitor extends CamelCommand {
+
+    private static final long VANISH_DURATION_MS = 6000;
+    private static final long DEFAULT_REFRESH_MS = 100;
+    private static final int MAX_SPARKLINE_POINTS = 60;
+    private static final int MAX_LOG_LINES = 200;
+
+    // Tab indices
+    private static final int TAB_OVERVIEW = 0;
+    private static final int TAB_ROUTES = 1;
+    private static final int TAB_HEALTH = 2;
+    private static final int TAB_ENDPOINTS = 3;
+    private static final int TAB_LOG = 4;
+
+    @CommandLine.Parameters(description = "Name or pid of running Camel integration", arity = "0..1")
+    String name = "*";
+
+    @CommandLine.Option(names = { "--refresh" },
+                        description = "Refresh interval in milliseconds (default: ${DEFAULT-VALUE})",
+                        defaultValue = "100")
+    long refreshInterval = DEFAULT_REFRESH_MS;
+
+    // State
+    private final AtomicReference<List<IntegrationInfo>> data = new AtomicReference<>(Collections.emptyList());
+    private final Map<String, VanishingInfo> vanishing = new ConcurrentHashMap<>();
+    private final TableState overviewTableState = new TableState();
+    private final TableState routeTableState = new TableState();
+    private final TableState healthTableState = new TableState();
+    private final TableState endpointTableState = new TableState();
+    private final TabsState tabsState = new TabsState(TAB_OVERVIEW);
+
+    // Sparkline: throughput history per PID (one point per second)
+    private final Map<String, LinkedList<Long>> throughputHistory = new ConcurrentHashMap<>();
+    // Sliding window of [timestamp, exchangesTotal] samples for smoothing
+    private final Map<String, LinkedList<long[]>> throughputSamples = new ConcurrentHashMap<>();
+    // Track last time a sparkline point was recorded
+    private final Map<String, Long> previousExchangesTime = new ConcurrentHashMap<>();
+
+    // Log state
+    private final List<String> logLines = new ArrayList<>();
+    private int logScroll;
+
+    // Selected integration for detail views
+    private String selectedPid;
+
+    private volatile long lastRefresh;
+
+    public CamelMonitor(CamelJBangMain main) {
+        super(main);
+    }
+
+    @Override
+    public Integer doCall() throws Exception {
+        // Eagerly load classes used by the input reader thread and picocli
+        // post-processing to avoid ClassNotFoundException during shutdown
+        // when the Spring Boot LaunchedClassLoader may already be closing
+        try {
+            Class.forName("dev.tamboui.tui.event.KeyModifiers");
+            Class.forName("dev.tamboui.tui.event.KeyEvent");
+            Class.forName("dev.tamboui.tui.event.KeyCode");
+            Class.forName("picocli.CommandLine$IExitCodeGenerator");
+        } catch (ClassNotFoundException e) {
+            // ignore
+        }
+
+        // Initial data load
+        refreshData();
+
+        try (var tui = TuiRunner.create()) {
+            // Intercept Ctrl+C: quit the TUI cleanly instead of letting
+            // the JVM tear down the classloader while we're still running
+            sun.misc.Signal.handle(new sun.misc.Signal("INT"), sig -> tui.quit());
+            tui.run(
+                    this::handleEvent,
+                    this::render);
+        }
+        return 0;
+    }
+
+    // ---- Event Handling ----
+
+    private boolean handleEvent(Event event, TuiRunner runner) {
+        if (event instanceof KeyEvent ke) {
+            // Global keys
+            if (ke.isQuit() || ke.isCharIgnoreCase('q') || ke.isKey(KeyCode.ESCAPE)) {
+                // If in a detail tab, go back to overview first
+                if (tabsState.selected() != TAB_OVERVIEW) {
+                    tabsState.select(TAB_OVERVIEW);
+                    selectedPid = null;
+                    return true;
+                }
+                runner.quit();
+                return true;
+            }
+            if (ke.isChar('r')) {
+                refreshData();
+                return true;
+            }
+
+            // Tab switching with number keys
+            if (ke.isChar('1')) {
+                tabsState.select(TAB_OVERVIEW);
+                selectedPid = null;
+                return true;
+            }
+            if (ke.isChar('2')) {
+                selectCurrentIntegration();
+                tabsState.select(TAB_ROUTES);
+                return true;
+            }
+            if (ke.isChar('3')) {
+                selectCurrentIntegration();
+                tabsState.select(TAB_HEALTH);
+                return true;
+            }
+            if (ke.isChar('4')) {
+                selectCurrentIntegration();
+                tabsState.select(TAB_ENDPOINTS);
+                return true;
+            }
+            if (ke.isChar('5')) {
+                selectCurrentIntegration();
+                tabsState.select(TAB_LOG);
+                logScroll = 0;
+                return true;
+            }
+
+            // Tab cycling
+            if (ke.isKey(KeyCode.TAB)) {
+                int next = (tabsState.selected() + 1) % 5;
+                if (next != TAB_OVERVIEW) {
+                    selectCurrentIntegration();
+                }
+                tabsState.select(next);
+                if (next == TAB_LOG) {
+                    logScroll = 0;
+                }
+                return true;
+            }
+
+            // Navigation
+            if (ke.isUp()) {
+                navigateUp();
+                return true;
+            }
+            if (ke.isDown()) {
+                navigateDown();
+                return true;
+            }
+            if (ke.isKey(KeyCode.PAGE_UP)) {
+                if (tabsState.selected() == TAB_LOG) {
+                    logScroll = Math.max(0, logScroll - 20);
+                }
+                return true;
+            }
+            if (ke.isKey(KeyCode.PAGE_DOWN)) {
+                if (tabsState.selected() == TAB_LOG) {
+                    logScroll += 20;
+                }
+                return true;
+            }
+
+            // Enter to drill into selected integration
+            if (ke.isKey(KeyCode.ENTER) && tabsState.selected() == TAB_OVERVIEW) {
+                selectCurrentIntegration();
+                if (selectedPid != null) {
+                    tabsState.select(TAB_ROUTES);
+                }
+                return true;
+            }
+        }
+        if (event instanceof TickEvent) {
+            long now = System.currentTimeMillis();
+            if (now - lastRefresh >= refreshInterval) {
+                refreshData();
+            }
+            return true;
+        }
+        return false;
+    }
+
+    private void selectCurrentIntegration() {
+        if (selectedPid != null) {
+            return;
+        }
+        List<IntegrationInfo> infos = data.get().stream().filter(i -> !i.vanishing).toList();
+        Integer sel = overviewTableState.selected();
+        if (sel != null && sel >= 0 && sel < infos.size()) {
+            selectedPid = infos.get(sel).pid;
+        } else if (infos.size() == 1) {
+            selectedPid = infos.get(0).pid;
+        }
+    }
+
+    private void navigateUp() {
+        switch (tabsState.selected()) {
+            case TAB_OVERVIEW -> overviewTableState.selectPrevious();
+            case TAB_ROUTES -> routeTableState.selectPrevious();
+            case TAB_HEALTH -> healthTableState.selectPrevious();
+            case TAB_ENDPOINTS -> endpointTableState.selectPrevious();
+            case TAB_LOG -> logScroll = Math.max(0, logScroll - 1);
+        }
+    }
+
+    private void navigateDown() {
+        List<IntegrationInfo> infos = data.get().stream().filter(i -> !i.vanishing).toList();
+        switch (tabsState.selected()) {
+            case TAB_OVERVIEW -> overviewTableState.selectNext(infos.size());
+            case TAB_ROUTES -> {
+                IntegrationInfo info = findSelectedIntegration();
+                routeTableState.selectNext(info != null ? info.routes.size() : 0);
+            }
+            case TAB_HEALTH -> {
+                IntegrationInfo info = findSelectedIntegration();
+                healthTableState.selectNext(info != null ? info.healthChecks.size() : 0);
+            }
+            case TAB_ENDPOINTS -> {
+                IntegrationInfo info = findSelectedIntegration();
+                endpointTableState.selectNext(info != null ? info.endpoints.size() : 0);
+            }
+            case TAB_LOG -> logScroll++;
+        }
+    }
+
+    // ---- Rendering ----
+
+    private void render(Frame frame) {
+        Rect area = frame.area();
+
+        // Layout: header (3 rows) + tabs (2 rows) + content (fill) + footer (1 row)
+        List<Rect> mainChunks = Layout.vertical()
+                .constraints(
+                        Constraint.length(3),
+                        Constraint.length(2),
+                        Constraint.fill(),
+                        Constraint.length(1))
+                .split(area);
+
+        renderHeader(frame, mainChunks.get(0));
+        renderTabs(frame, mainChunks.get(1));
+        renderContent(frame, mainChunks.get(2));
+        renderFooter(frame, mainChunks.get(3));
+    }
+
+    private void renderHeader(Frame frame, Rect area) {
+        List<IntegrationInfo> infos = data.get();
+        String camelVersion = VersionHelper.extractCamelVersion();
+        long activeCount = infos.stream().filter(i -> !i.vanishing).count();
+
+        Line titleLine = Line.from(
+                Span.styled(" Camel Monitor", Style.create().fg(Color.rgb(0xF6, 0x91, 0x23)).bold()),
+                Span.raw("  "),
+                Span.styled(camelVersion != null ? "v" + camelVersion : "", Style.create().fg(Color.GREEN)),
+                Span.raw("  "),
+                Span.styled(activeCount + " integration(s)", Style.create().fg(Color.CYAN)));
+
+        Block headerBlock = Block.builder()
+                .borderType(BorderType.ROUNDED)
+                .title(" Apache Camel ")
+                .build();
+
+        frame.renderWidget(
+                Paragraph.builder().text(Text.from(titleLine)).block(headerBlock).build(),
+                area);
+    }
+
+    private void renderTabs(Frame frame, Rect area) {
+        String sel = selectedPid != null ? " [" + selectedName() + "]" : "";
+        Tabs tabs = Tabs.builder()
+                .titles(
+                        " 1 Overview ",
+                        " 2 Routes" + sel + " ",
+                        " 3 Health" + sel + " ",
+                        " 4 Endpoints" + sel + " ",
+                        " 5 Log" + sel + " ")
+                .highlightStyle(Style.create().fg(Color.rgb(0xF6, 0x91, 0x23)).bold())
+                .divider(Span.styled(" | ", Style.create().dim()))
+                .build();
+
+        frame.renderStatefulWidget(tabs, area, tabsState);
+    }
+
+    private void renderContent(Frame frame, Rect area) {
+        // Clear the content area to prevent artifacts when switching tabs
+        frame.buffer().clear(area);
+        switch (tabsState.selected()) {
+            case TAB_OVERVIEW -> renderOverview(frame, area);
+            case TAB_ROUTES -> renderRoutes(frame, area);
+            case TAB_HEALTH -> renderHealth(frame, area);
+            case TAB_ENDPOINTS -> renderEndpoints(frame, area);
+            case TAB_LOG -> renderLog(frame, area);
+        }
+    }
+
+    // ---- Tab 1: Overview ----
+
+    private void renderOverview(Frame frame, Rect area) {
+        List<IntegrationInfo> infos = data.get();
+
+        // Split: table (fill) + sparkline (height 8) if we have data
+        boolean hasSparkline = !throughputHistory.isEmpty();
+        List<Rect> chunks;
+        if (hasSparkline) {
+            chunks = Layout.vertical()
+                    .constraints(Constraint.fill(), Constraint.length(8))
+                    .split(area);
+        } else {
+            chunks = List.of(area);
+        }
+
+        // Integration table
+        List<Row> rows = new ArrayList<>();
+        for (IntegrationInfo info : infos) {
+            if (info.vanishing) {
+                long elapsed = System.currentTimeMillis() - info.vanishStart;
+                float fade = 1.0f - Math.min(1.0f, (float) elapsed / VANISH_DURATION_MS);
+                int gray = (int) (100 * fade);
+                Style dimStyle = Style.create().fg(Color.indexed(232 + Math.min(gray / 4, 23)));
+
+                rows.add(Row.from(
+                        Cell.from(Span.styled(info.pid, dimStyle)),
+                        Cell.from(Span.styled(truncate(info.name, 25), dimStyle)),
+                        Cell.from(Span.styled(info.platform != null ? info.platform : "", dimStyle)),
+                        Cell.from(Span.styled("\u2716 Stopped", Style.create().fg(Color.RED).dim())),
+                        Cell.from(Span.styled(info.ago != null ? info.ago : "", dimStyle)),
+                        Cell.from(Span.styled("", dimStyle)),
+                        Cell.from(Span.styled("", dimStyle)),
+                        Cell.from(Span.styled("", dimStyle))));
+            } else {
+                Style statusStyle = switch (extractState(info.state)) {
+                    case "Started" -> Style.create().fg(Color.GREEN);
+                    case "Stopped" -> Style.create().fg(Color.RED);
+                    default -> Style.create().fg(Color.YELLOW);
+                };
+
+                rows.add(Row.from(
+                        Cell.from(info.pid),
+                        Cell.from(Span.styled(truncate(info.name, 25), Style.create().fg(Color.CYAN))),
+                        Cell.from(info.platform != null ? info.platform : ""),
+                        Cell.from(Span.styled(extractState(info.state), statusStyle)),
+                        Cell.from(info.ago != null ? info.ago : ""),
+                        Cell.from(info.throughput != null ? info.throughput : ""),
+                        Cell.from(formatMemory(info.heapMemUsed, info.heapMemMax)),
+                        Cell.from(formatThreads(info.threadCount, info.peakThreadCount))));
+            }
+        }
+
+        Row header = Row.from(
+                Cell.from(Span.styled("PID", Style.create().bold())),
+                Cell.from(Span.styled("NAME", Style.create().bold())),
+                Cell.from(Span.styled("PLATFORM", Style.create().bold())),
+                Cell.from(Span.styled("STATUS", Style.create().bold())),
+                Cell.from(Span.styled("AGE", Style.create().bold())),
+                Cell.from(Span.styled("THRUPUT", Style.create().bold())),
+                Cell.from(Span.styled("HEAP", Style.create().bold())),
+                Cell.from(Span.styled("THREADS", Style.create().bold())));
+
+        Table table = Table.builder()
+                .rows(rows)
+                .header(header)
+                .widths(
+                        Constraint.length(8),
+                        Constraint.fill(),
+                        Constraint.length(10),
+                        Constraint.length(10),
+                        Constraint.length(8),
+                        Constraint.length(10),
+                        Constraint.length(15),
+                        Constraint.length(12))
+                .highlightStyle(Style.create().fg(Color.WHITE).bold().onBlue())
+                .block(Block.builder().borderType(BorderType.ROUNDED).title(" Integrations ").build())
+                .build();
+
+        frame.renderStatefulWidget(table, chunks.get(0), overviewTableState);
+
+        // Bar chart for throughput
+        if (hasSparkline && chunks.size() > 1) {
+            // Merge all throughput histories for overview chart
+            LinkedList<Long> merged = new LinkedList<>();
+            for (int i = 0; i < MAX_SPARKLINE_POINTS; i++) {
+                long sum = 0;
+                for (LinkedList<Long> hist : throughputHistory.values()) {
+                    if (i < hist.size()) {
+                        sum += hist.get(hist.size() - 1 - i);
+                    }
+                }
+                merged.addFirst(sum);
+            }
+
+            // Compute stats for title display
+            long maxTp = merged.stream().mapToLong(Long::longValue).max().orElse(0);
+            long curTp = merged.isEmpty() ? 0 : merged.get(merged.size() - 1);
+            String chartTitle = String.format(" Throughput: %d msg/s (peak: %d) ", curTp, maxTp);
+
+            // Build bar groups — one bar per data point
+            List<BarGroup> groups = new ArrayList<>();
+            for (Long value : merged) {
+                groups.add(BarGroup.of(Bar.of(value)));
+            }
+
+            BarChart barChart = BarChart.builder()
+                    .data(groups)
+                    .barWidth(1)
+                    .barGap(0)
+                    .barStyle(Style.create().fg(Color.GREEN))
+                    .block(Block.builder().borderType(BorderType.ROUNDED).title(chartTitle).build())
+                    .build();
+
+            frame.renderWidget(barChart, chunks.get(1));
+        }
+    }
+
+    // ---- Tab 2: Routes ----
+
+    private void renderRoutes(Frame frame, Rect area) {
+        IntegrationInfo info = findSelectedIntegration();
+        if (info == null) {
+            renderNoSelection(frame, area);
+            return;
+        }
+
+        // Split: routes table (top half) + processors table (bottom half)
+        List<Rect> chunks = Layout.vertical()
+                .constraints(Constraint.percentage(45), Constraint.percentage(55))
+                .split(area);
+
+        // Routes table
+        List<Row> routeRows = new ArrayList<>();
+        for (RouteInfo route : info.routes) {
+            Style stateStyle = "Started".equals(route.state)
+                    ? Style.create().fg(Color.GREEN)
+                    : Style.create().fg(Color.RED);
+
+            routeRows.add(Row.from(
+                    Cell.from(Span.styled(truncate(route.routeId, 12), Style.create().fg(Color.CYAN))),
+                    Cell.from(truncate(route.from, 30)),
+                    Cell.from(Span.styled(route.state, stateStyle)),
+                    Cell.from(route.uptime != null ? route.uptime : ""),
+                    Cell.from(route.throughput != null ? route.throughput : ""),
+                    Cell.from(String.valueOf(route.total)),
+                    Cell.from(route.failed > 0
+                            ? Span.styled(String.valueOf(route.failed), Style.create().fg(Color.RED))
+                            : Span.raw("0")),
+                    Cell.from(route.meanTime + "/" + route.maxTime)));
+        }
+
+        Table routeTable = Table.builder()
+                .rows(routeRows)
+                .header(Row.from(
+                        Cell.from(Span.styled("ROUTE", Style.create().bold())),
+                        Cell.from(Span.styled("FROM", Style.create().bold())),
+                        Cell.from(Span.styled("STATE", Style.create().bold())),
+                        Cell.from(Span.styled("UPTIME", Style.create().bold())),
+                        Cell.from(Span.styled("THRUPUT", Style.create().bold())),
+                        Cell.from(Span.styled("TOTAL", Style.create().bold())),
+                        Cell.from(Span.styled("FAILED", Style.create().bold())),
+                        Cell.from(Span.styled("MEAN/MAX", Style.create().bold()))))
+                .widths(
+                        Constraint.length(12),
+                        Constraint.fill(),
+                        Constraint.length(10),
+                        Constraint.length(8),
+                        Constraint.length(10),
+                        Constraint.length(8),
+                        Constraint.length(8),
+                        Constraint.length(12))
+                .highlightStyle(Style.create().fg(Color.WHITE).bold().onBlue())
+                .block(Block.builder().borderType(BorderType.ROUNDED)
+                        .title(" Routes [" + info.name + "] ").build())
+                .build();
+
+        frame.renderStatefulWidget(routeTable, chunks.get(0), routeTableState);
+
+        // Processors for selected route
+        Integer selectedRoute = routeTableState.selected();
+        if (selectedRoute != null && selectedRoute >= 0 && selectedRoute < info.routes.size()) {
+            RouteInfo route = info.routes.get(selectedRoute);
+            renderProcessors(frame, chunks.get(1), route);
+        } else if (!info.routes.isEmpty()) {
+            renderProcessors(frame, chunks.get(1), info.routes.get(0));
+        } else {
+            frame.renderWidget(
+                    Paragraph.builder()
+                            .text(Text.from(Line.from(Span.styled("No routes", Style.create().dim()))))
+                            .block(Block.builder().borderType(BorderType.ROUNDED).title(" Processors ").build())
+                            .build(),
+                    chunks.get(1));
+        }
+    }
+
+    private void renderProcessors(Frame frame, Rect area, RouteInfo route) {
+        List<Row> rows = new ArrayList<>();
+        for (ProcessorInfo proc : route.processors) {
+            String indent = "  ".repeat(proc.level);
+            Style nameStyle = proc.failed > 0 ? Style.create().fg(Color.RED) : Style.create().fg(Color.CYAN);
+
+            rows.add(Row.from(
+                    Cell.from(Span.styled(indent + proc.id, nameStyle)),
+                    Cell.from(proc.processor),
+                    Cell.from(String.valueOf(proc.total)),
+                    Cell.from(proc.failed > 0
+                            ? Span.styled(String.valueOf(proc.failed), Style.create().fg(Color.RED))
+                            : Span.raw("0")),
+                    Cell.from(proc.meanTime + "ms"),
+                    Cell.from(proc.maxTime + "ms"),
+                    Cell.from(proc.lastTime + "ms")));
+        }
+
+        Table table = Table.builder()
+                .rows(rows)
+                .header(Row.from(
+                        Cell.from(Span.styled("PROCESSOR", Style.create().bold())),
+                        Cell.from(Span.styled("TYPE", Style.create().bold())),
+                        Cell.from(Span.styled("TOTAL", Style.create().bold())),
+                        Cell.from(Span.styled("FAILED", Style.create().bold())),
+                        Cell.from(Span.styled("MEAN", Style.create().bold())),
+                        Cell.from(Span.styled("MAX", Style.create().bold())),
+                        Cell.from(Span.styled("LAST", Style.create().bold()))))
+                .widths(
+                        Constraint.fill(),
+                        Constraint.length(15),
+                        Constraint.length(8),
+                        Constraint.length(8),
+                        Constraint.length(8),
+                        Constraint.length(8),
+                        Constraint.length(8))
+                .block(Block.builder().borderType(BorderType.ROUNDED)
+                        .title(" Processors [" + route.routeId + "] ").build())
+                .build();
+
+        frame.renderStatefulWidget(table, area, new TableState());
+    }
+
+    // ---- Tab 3: Health ----
+
+    private void renderHealth(Frame frame, Rect area) {
+        IntegrationInfo info = findSelectedIntegration();
+        if (info == null) {
+            renderNoSelection(frame, area);
+            return;
+        }
+
+        // Split: health table (fill) + memory gauge (3 rows)
+        List<Rect> chunks = Layout.vertical()
+                .constraints(Constraint.fill(), Constraint.length(3))
+                .split(area);
+
+        List<Row> rows = new ArrayList<>();
+        for (HealthCheckInfo hc : info.healthChecks) {
+            Style stateStyle;
+            String icon;
+            if ("UP".equals(hc.state)) {
+                stateStyle = Style.create().fg(Color.GREEN);
+                icon = "\u2714 ";
+            } else if ("DOWN".equals(hc.state)) {
+                stateStyle = Style.create().fg(Color.RED);
+                icon = "\u2716 ";
+            } else {
+                stateStyle = Style.create().fg(Color.YELLOW);
+                icon = "\u26A0 ";
+            }
+
+            rows.add(Row.from(
+                    Cell.from(Span.styled(truncate(hc.group != null ? hc.group : "", 12), Style.create().dim())),
+                    Cell.from(Span.styled(truncate(hc.name, 30), Style.create().fg(Color.CYAN))),
+                    Cell.from(Span.styled(icon + hc.state, stateStyle)),
+                    Cell.from(hc.message != null ? truncate(hc.message, 50) : "")));
+        }
+
+        if (rows.isEmpty()) {
+            rows.add(Row.from(
+                    Cell.from(""),
+                    Cell.from(Span.styled("No health checks registered", Style.create().dim())),
+                    Cell.from(""),
+                    Cell.from("")));
+        }
+
+        Table table = Table.builder()
+                .rows(rows)
+                .header(Row.from(
+                        Cell.from(Span.styled("GROUP", Style.create().bold())),
+                        Cell.from(Span.styled("NAME", Style.create().bold())),
+                        Cell.from(Span.styled("STATUS", Style.create().bold())),
+                        Cell.from(Span.styled("MESSAGE", Style.create().bold()))))
+                .widths(
+                        Constraint.length(12),
+                        Constraint.length(30),
+                        Constraint.length(12),
+                        Constraint.fill())
+                .highlightStyle(Style.create().fg(Color.WHITE).bold().onBlue())
+                .block(Block.builder().borderType(BorderType.ROUNDED)
+                        .title(" Health [" + info.name + "] ").build())
+                .build();
+
+        frame.renderStatefulWidget(table, chunks.get(0), healthTableState);
+
+        // Memory gauge
+        if (info.heapMemMax > 0) {
+            int pct = (int) (100.0 * info.heapMemUsed / info.heapMemMax);
+            Style gaugeStyle = pct > 80 ? Style.create().fg(Color.RED)
+                    : pct > 60 ? Style.create().fg(Color.YELLOW) : Style.create().fg(Color.GREEN);
+            Gauge gauge = Gauge.builder()
+                    .percent(pct)
+                    .label(String.format("Heap: %s / %s (%d%%)", formatBytes(info.heapMemUsed),
+                            formatBytes(info.heapMemMax), pct))
+                    .gaugeStyle(gaugeStyle)
+                    .block(Block.builder().borderType(BorderType.ROUNDED).build())
+                    .build();
+
+            frame.renderWidget(gauge, chunks.get(1));
+        }
+    }
+
+    // ---- Tab 4: Endpoints ----
+
+    private void renderEndpoints(Frame frame, Rect area) {
+        IntegrationInfo info = findSelectedIntegration();
+        if (info == null) {
+            renderNoSelection(frame, area);
+            return;
+        }
+
+        List<Row> rows = new ArrayList<>();
+        for (EndpointInfo ep : info.endpoints) {
+            Style dirStyle = switch (ep.direction) {
+                case "in" -> Style.create().fg(Color.GREEN);
+                case "out" -> Style.create().fg(Color.BLUE);
+                default -> Style.create().fg(Color.YELLOW);
+            };
+            String arrow = switch (ep.direction) {
+                case "in" -> "\u2192 ";
+                case "out" -> "\u2190 ";
+                default -> "\u2194 ";
+            };
+
+            rows.add(Row.from(
+                    Cell.from(Span.styled(ep.component, Style.create().fg(Color.CYAN))),
+                    Cell.from(Span.styled(arrow + ep.direction, dirStyle)),
+                    Cell.from(truncate(ep.uri, 60)),
+                    Cell.from(ep.routeId != null ? ep.routeId : "")));
+        }
+
+        if (rows.isEmpty()) {
+            rows.add(Row.from(
+                    Cell.from(""),
+                    Cell.from(Span.styled("No endpoints", Style.create().dim())),
+                    Cell.from(""),
+                    Cell.from("")));
+        }
+
+        Table table = Table.builder()
+                .rows(rows)
+                .header(Row.from(
+                        Cell.from(Span.styled("COMPONENT", Style.create().bold())),
+                        Cell.from(Span.styled("DIR", Style.create().bold())),
+                        Cell.from(Span.styled("URI", Style.create().bold())),
+                        Cell.from(Span.styled("ROUTE", Style.create().bold()))))
+                .widths(
+                        Constraint.length(15),
+                        Constraint.length(8),
+                        Constraint.fill(),
+                        Constraint.length(12))
+                .highlightStyle(Style.create().fg(Color.WHITE).bold().onBlue())
+                .block(Block.builder().borderType(BorderType.ROUNDED)
+                        .title(" Endpoints [" + info.name + "] ").build())
+                .build();
+
+        frame.renderStatefulWidget(table, area, endpointTableState);
+    }
+
+    // ---- Tab 5: Log ----
+
+    private void renderLog(Frame frame, Rect area) {
+        IntegrationInfo info = findSelectedIntegration();
+        if (info == null) {
+            renderNoSelection(frame, area);
+            return;
+        }
+
+        // Read log lines
+        readLogFile(info.pid);
+
+        Block logBlock = Block.builder().borderType(BorderType.ROUNDED)
+                .title(" Log [" + info.name + "] ")
+                .build();
+
+        int innerHeight = Math.max(1, area.height() - 2); // account for border
+        int totalLines = logLines.size();
+
+        // Auto-scroll to bottom if logScroll is 0 (default)
+        int startLine;
+        if (logScroll == 0) {
+            startLine = Math.max(0, totalLines - innerHeight);
+        } else {
+            startLine = Math.max(0, Math.min(logScroll, totalLines - innerHeight));
+        }
+
+        List<Line> visibleLines = new ArrayList<>();
+        for (int i = startLine; i < Math.min(startLine + innerHeight, totalLines); i++) {
+            visibleLines.add(colorizeLogLine(logLines.get(i)));
+        }
+
+        // Fill remaining space
+        while (visibleLines.size() < innerHeight) {
+            visibleLines.add(Line.from(Span.raw("")));
+        }
+
+        Paragraph logParagraph = Paragraph.builder()
+                .text(Text.from(visibleLines))
+                .overflow(Overflow.CLIP)
+                .block(logBlock)
+                .build();
+
+        frame.renderWidget(logParagraph, area);
+    }
+
+    private Line colorizeLogLine(String line) {
+        if (line.contains(" ERROR ") || line.contains(" FATAL ")) {
+            return Line.from(Span.styled(line, Style.create().fg(Color.RED)));
+        } else if (line.contains(" WARN ")) {
+            return Line.from(Span.styled(line, Style.create().fg(Color.YELLOW)));
+        } else if (line.contains(" DEBUG ") || line.contains(" TRACE ")) {
+            return Line.from(Span.styled(line, Style.create().dim()));
+        }
+        return Line.from(Span.raw(line));
+    }
+
+    private void readLogFile(String pid) {
+        logLines.clear();
+        Path logFile = CommandLineHelper.getCamelDir().resolve(pid + ".log");
+        if (!Files.exists(logFile)) {
+            return;
+        }
+        try (RandomAccessFile raf = new RandomAccessFile(logFile.toFile(), "r")) {
+            long length = raf.length();
+            // Read last ~64KB to get recent lines
+            long startPos = Math.max(0, length - 64 * 1024);
+            raf.seek(startPos);
+            if (startPos > 0) {
+                raf.readLine(); // skip partial line
+            }
+            // Read remaining bytes and split into lines using proper encoding
+            byte[] remaining = new byte[(int) (length - raf.getFilePointer())];
+            raf.readFully(remaining);
+            String content = new String(remaining, java.nio.charset.StandardCharsets.UTF_8);
+            String[] lines = content.split("\n", -1);
+            int start = Math.max(0, lines.length - MAX_LOG_LINES);
+            for (int i = start; i < lines.length; i++) {
+                String line = lines[i];
+                if (!line.isEmpty()) {
+                    logLines.add(line);
+                }
+            }
+        } catch (IOException e) {
+            // ignore
+        }
+    }
+
+    // ---- Shared rendering ----
+
+    private void renderNoSelection(Frame frame, Rect area) {
+        frame.renderWidget(
+                Paragraph.builder()
+                        .text(Text.from(Line.from(
+                                Span.styled(" Select an integration from the Overview tab (press 1)",
+                                        Style.create().dim()))))
+                        .block(Block.builder().borderType(BorderType.ROUNDED)
+                                .title(" No integration selected ").build())
+                        .build(),
+                area);
+    }
+
+    private void renderFooter(Frame frame, Rect area) {
+        String refreshLabel = refreshInterval >= 1000
+                ? (refreshInterval / 1000) + "s"
+                : refreshInterval + "ms";
+        Line footer;
+        if (tabsState.selected() == TAB_OVERVIEW) {
+            footer = Line.from(
+                    Span.styled(" q", Style.create().fg(Color.YELLOW).bold()),
+                    Span.raw(" quit  "),
+                    Span.styled("r", Style.create().fg(Color.YELLOW).bold()),
+                    Span.raw(" refresh  "),
+                    Span.styled("\u2191\u2193", Style.create().fg(Color.YELLOW).bold()),
+                    Span.raw(" navigate  "),
+                    Span.styled("Enter", Style.create().fg(Color.YELLOW).bold()),
+                    Span.raw(" details  "),
+                    Span.styled("1-5", Style.create().fg(Color.YELLOW).bold()),
+                    Span.raw(" tabs  "),
+                    Span.styled("Refresh: " + refreshLabel, Style.create().dim()));
+        } else if (tabsState.selected() == TAB_LOG) {
+            footer = Line.from(
+                    Span.styled(" Esc", Style.create().fg(Color.YELLOW).bold()),
+                    Span.raw(" back  "),
+                    Span.styled("\u2191\u2193", Style.create().fg(Color.YELLOW).bold()),
+                    Span.raw(" scroll  "),
+                    Span.styled("PgUp/PgDn", Style.create().fg(Color.YELLOW).bold()),
+                    Span.raw(" page  "),
+                    Span.styled("1-5", Style.create().fg(Color.YELLOW).bold()),
+                    Span.raw(" tabs  "),
+                    Span.styled("Refresh: " + refreshLabel, Style.create().dim()));
+        } else {
+            footer = Line.from(
+                    Span.styled(" Esc", Style.create().fg(Color.YELLOW).bold()),
+                    Span.raw(" back  "),
+                    Span.styled("\u2191\u2193", Style.create().fg(Color.YELLOW).bold()),
+                    Span.raw(" navigate  "),
+                    Span.styled("1-5", Style.create().fg(Color.YELLOW).bold()),
+                    Span.raw(" tabs  "),
+                    Span.styled("Refresh: " + refreshLabel, Style.create().dim()));
+        }
+
+        frame.renderWidget(Paragraph.from(footer), area);
+    }
+
+    // ---- Data Loading ----
+
+    private void refreshData() {
+        lastRefresh = System.currentTimeMillis();
+        try {
+            List<IntegrationInfo> infos = new ArrayList<>();
+            List<Long> pids = findPids(name);
+            ProcessHandle.allProcesses()
+                    .filter(ph -> pids.contains(ph.pid()))
+                    .forEach(ph -> {
+                        JsonObject root = loadStatus(ph.pid());
+                        if (root != null) {
+                            IntegrationInfo info = parseIntegration(ph, root);
+                            if (info != null) {
+                                infos.add(info);
+                                // Track throughput for sparkline
+                                updateThroughputHistory(info);
+                            }
+                        }
+                    });
+
+            // Detect disappeared integrations and start vanishing
+            Set<String> livePids = infos.stream().map(i -> i.pid).collect(Collectors.toSet());
+            List<IntegrationInfo> previous = data.get();
+            for (IntegrationInfo prev : previous) {
+                if (!prev.vanishing && !livePids.contains(prev.pid) && !vanishing.containsKey(prev.pid)) {
+                    vanishing.put(prev.pid, new VanishingInfo(prev, System.currentTimeMillis()));
+                }
+            }
+
+            // Expire old vanishing entries
+            long now = System.currentTimeMillis();
+            Iterator<Map.Entry<String, VanishingInfo>> it = vanishing.entrySet().iterator();
+            while (it.hasNext()) {
+                Map.Entry<String, VanishingInfo> entry = it.next();
+                if (now - entry.getValue().startTime > VANISH_DURATION_MS) {
+                    it.remove();
+                    throughputHistory.remove(entry.getKey());
+                } else if (!livePids.contains(entry.getKey())) {
+                    IntegrationInfo ghost = entry.getValue().info;
+                    ghost.vanishing = true;
+                    ghost.vanishStart = entry.getValue().startTime;
+                    infos.add(ghost);
+                } else {
+                    it.remove();
+                }
+            }
+
+            data.set(infos);
+        } catch (Exception e) {
+            // ignore refresh errors
+        }
+    }
+
+    private void updateThroughputHistory(IntegrationInfo info) {
+        // Track exchangesTotal over a 1-second sliding window for stable throughput
+        long currentTotal = info.exchangesTotal;
+        long now = System.currentTimeMillis();
+
+        String pid = info.pid;
+        LinkedList<long[]> samples = throughputSamples.computeIfAbsent(pid, k -> new LinkedList<>());
+        samples.add(new long[] { now, currentTotal });
+
+        // Remove samples older than 1 second
+        while (!samples.isEmpty() && now - samples.get(0)[0] > 1000) {
+            samples.remove(0);
+        }
+
+        // Compute throughput over the window
+        if (samples.size() >= 2) {
+            long[] oldest = samples.get(0);
+            long[] newest = samples.get(samples.size() - 1);
+            long deltaExchanges = newest[1] - oldest[1];
+            long deltaTimeMs = newest[0] - oldest[0];
+            long tp = deltaTimeMs > 0 ? (deltaExchanges * 1000) / deltaTimeMs : 0;
+
+            LinkedList<Long> hist = throughputHistory.computeIfAbsent(pid, k -> new LinkedList<>());
+            // Only add one point per second to keep the sparkline meaningful
+            Long lastTime = previousExchangesTime.get(pid);
+            if (lastTime == null || now - lastTime >= 1000) {
+                previousExchangesTime.put(pid, now);
+                hist.add(tp);
+                while (hist.size() > MAX_SPARKLINE_POINTS) {
+                    hist.remove(0);
+                }
+            }
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    private IntegrationInfo parseIntegration(ProcessHandle ph, JsonObject root) {
+        JsonObject context = (JsonObject) root.get("context");
+        if (context == null) {
+            return null;
+        }
+
+        IntegrationInfo info = new IntegrationInfo();
+        info.name = context.getString("name");
+        if ("CamelJBang".equals(info.name)) {
+            info.name = ProcessHelper.extractName(root, ph);
+        }
+        info.pid = Long.toString(ph.pid());
+        info.uptime = extractSince(ph);
+        info.ago = TimeUtils.printSince(info.uptime);
+        info.state = context.getInteger("phase");
+
+        JsonObject runtime = (JsonObject) root.get("runtime");
+        info.platform = runtime != null ? runtime.getString("platform") : null;
+
+        Map<String, ?> stats = context.getMap("statistics");
+        if (stats != null) {
+            Object thp = stats.get("exchangesThroughput");
+            if (thp != null) {
+                info.throughput = thp.toString();
+            }
+            info.exchangesTotal = objToLong(stats.get("exchangesTotal"));
+        }
+
+        JsonObject mem = (JsonObject) root.get("memory");
+        if (mem != null) {
+            info.heapMemUsed = mem.getLong("heapMemoryUsed");
+            info.heapMemMax = mem.getLong("heapMemoryMax");
+        }
+
+        JsonObject threads = (JsonObject) root.get("threads");
+        if (threads != null) {
+            info.threadCount = threads.getInteger("threadCount");
+            info.peakThreadCount = threads.getInteger("peakThreadCount");
+        }
+
+        // Parse routes
+        JsonArray routes = (JsonArray) root.get("routes");
+        if (routes != null) {
+            for (Object r : routes) {
+                JsonObject rj = (JsonObject) r;
+                RouteInfo ri = new RouteInfo();
+                ri.routeId = rj.getString("routeId");
+                ri.from = rj.getString("from");
+                ri.state = rj.getString("state");
+                ri.uptime = rj.getString("uptime");
+
+                Map<String, ?> rs = rj.getMap("statistics");
+                if (rs != null) {
+                    ri.throughput = objToString(rs.get("exchangesThroughput"));
+                    ri.total = objToLong(rs.get("exchangesTotal"));
+                    ri.failed = objToLong(rs.get("exchangesFailed"));
+                    ri.meanTime = objToLong(rs.get("meanProcessingTime"));
+                    ri.maxTime = objToLong(rs.get("maxProcessingTime"));
+                }
+
+                // Parse processors
+                JsonArray procs = (JsonArray) rj.get("processors");
+                if (procs != null) {
+                    for (Object p : procs) {
+                        JsonObject pj = (JsonObject) p;
+                        ProcessorInfo pi = new ProcessorInfo();
+                        pi.id = pj.getString("id");
+                        pi.processor = pj.getString("processor");
+                        pi.level = pj.getIntegerOrDefault("level", 0);
+
+                        Map<String, ?> ps = pj.getMap("statistics");
+                        if (ps != null) {
+                            pi.total = objToLong(ps.get("exchangesTotal"));
+                            pi.failed = objToLong(ps.get("exchangesFailed"));
+                            pi.meanTime = objToLong(ps.get("meanProcessingTime"));
+                            pi.maxTime = objToLong(ps.get("maxProcessingTime"));
+                            pi.lastTime = objToLong(ps.get("lastProcessingTime"));
+                        }
+
+                        ri.processors.add(pi);
+                    }
+                }
+
+                info.routes.add(ri);
+            }
+        }
+
+        // Parse health checks
+        JsonObject healthChecks = (JsonObject) root.get("healthChecks");
+        if (healthChecks != null) {
+            JsonArray checks = (JsonArray) healthChecks.get("checks");
+            if (checks != null) {
+                for (Object c : checks) {
+                    JsonObject cj = (JsonObject) c;
+                    HealthCheckInfo hc = new HealthCheckInfo();
+                    hc.group = cj.getString("group");
+                    hc.name = cj.getString("id");
+                    hc.state = cj.getString("state");
+                    // Extract message from details if available
+                    JsonObject details = (JsonObject) cj.get("details");
+                    if (details != null && details.containsKey("failure.error.message")) {
+                        hc.message = details.getString("failure.error.message");
+                    }
+                    info.healthChecks.add(hc);
+                }
+            }
+        }
+
+        // Parse endpoints (top-level "endpoints" is a JsonObject with nested "endpoints" array)
+        JsonObject endpointsObj = (JsonObject) root.get("endpoints");
+        if (endpointsObj != null) {
+            JsonArray endpointList = (JsonArray) endpointsObj.get("endpoints");
+            if (endpointList != null) {
+                for (Object e : endpointList) {
+                    JsonObject ej = (JsonObject) e;
+                    EndpointInfo ep = new EndpointInfo();
+                    ep.uri = ej.getString("uri");
+                    ep.direction = ej.getString("direction");
+                    ep.routeId = ej.getString("routeId");
+                    // Extract component from URI (e.g., "timer://tick" -> "timer")
+                    if (ep.uri != null) {
+                        int idx = ep.uri.indexOf(':');
+                        ep.component = idx > 0 ? ep.uri.substring(0, idx) : ep.uri;
+                    }
+                    info.endpoints.add(ep);
+                }
+            }
+        }
+
+        return info;
+    }
+
+    // ---- Helpers ----
+
+    private IntegrationInfo findSelectedIntegration() {
+        if (selectedPid == null) {
+            return null;
+        }
+        return data.get().stream()
+                .filter(i -> selectedPid.equals(i.pid) && !i.vanishing)
+                .findFirst().orElse(null);
+    }
+
+    private String selectedName() {
+        IntegrationInfo info = findSelectedIntegration();
+        return info != null ? truncate(info.name, 20) : "?";
+    }
+
+    private List<Long> findPids(String name) {
+        List<Long> pids = new ArrayList<>();
+        final long cur = ProcessHandle.current().pid();
+        String pattern = name;
+        if (!pattern.matches("\\d+") && !pattern.endsWith("*")) {
+            pattern = pattern + "*";
+        }
+        final String pat = pattern;
+        ProcessHandle.allProcesses()
+                .filter(ph -> ph.pid() != cur)
+                .forEach(ph -> {
+                    JsonObject root = loadStatus(ph.pid());
+                    if (root != null) {
+                        String pName = ProcessHelper.extractName(root, ph);
+                        pName = FileUtil.onlyName(pName);
+                        if (pName != null && !pName.isEmpty() && PatternHelper.matchPattern(pName, pat)) {
+                            pids.add(ph.pid());
+                        } else {
+                            JsonObject context = (JsonObject) root.get("context");
+                            if (context != null) {
+                                pName = context.getString("name");
+                                if ("CamelJBang".equals(pName)) {
+                                    pName = null;
+                                }
+                                if (pName != null && !pName.isEmpty() && PatternHelper.matchPattern(pName, pat)) {
+                                    pids.add(ph.pid());
+                                }
+                            }
+                        }
+                    }
+                });
+        return pids;
+    }
+
+    private JsonObject loadStatus(long pid) {
+        try {
+            Path f = getStatusFile(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;
+    }
+
+    private static long extractSince(ProcessHandle ph) {
+        return ph.info().startInstant().map(Instant::toEpochMilli).orElse(0L);
+    }
+
+    private static String truncate(String s, int max) {
+        if (s == null) {
+            return "";
+        }
+        return s.length() > max ? s.substring(0, max - 1) + "\u2026" : s;
+    }
+
+    private static String formatMemory(long used, long max) {
+        if (used <= 0) {
+            return "";
+        }
+        String u = formatBytes(used);
+        if (max > 0) {
+            return u + "/" + formatBytes(max);
+        }
+        return u;
+    }
+
+    private static String formatBytes(long bytes) {
+        if (bytes < 1024) {
+            return bytes + "B";
+        }
+        if (bytes < 1024 * 1024) {
+            return (bytes / 1024) + "K";
+        }
+        return (bytes / (1024 * 1024)) + "M";
+    }
+
+    private static String formatThreads(int count, int peak) {
+        if (count <= 0) {
+            return "";
+        }
+        return count + "/" + peak;
+    }
+
+    private static String objToString(Object o) {
+        return o != null ? o.toString() : "";
+    }
+
+    private static long objToLong(Object o) {
+        if (o instanceof Number n) {
+            return n.longValue();
+        }
+        if (o != null) {
+            try {
+                return Long.parseLong(o.toString());
+            } catch (NumberFormatException e) {
+                // ignore
+            }
+        }
+        return 0;
+    }
+
+    // ---- Data Classes ----
+
+    static class IntegrationInfo {
+        String pid;
+        String name;
+        String platform;
+        int state;
+        long uptime;
+        String ago;
+        String throughput;
+        long exchangesTotal;
+        long heapMemUsed;
+        long heapMemMax;
+        int threadCount;
+        int peakThreadCount;
+        boolean vanishing;
+        long vanishStart;
+        final List<RouteInfo> routes = new ArrayList<>();
+        final List<HealthCheckInfo> healthChecks = new ArrayList<>();
+        final List<EndpointInfo> endpoints = new ArrayList<>();
+    }
+
+    static class RouteInfo {
+        String routeId;
+        String from;
+        String state;
+        String uptime;
+        String throughput;
+        long total;
+        long failed;
+        long meanTime;
+        long maxTime;
+        final List<ProcessorInfo> processors = new ArrayList<>();
+    }
+
+    static class ProcessorInfo {
+        String id;
+        String processor;
+        int level;
+        long total;
+        long failed;
+        long meanTime;
+        long maxTime;
+        long lastTime;
+    }
+
+    static class HealthCheckInfo {
+        String group;
+        String name;
+        String state;
+        String message;
+    }
+
+    static class EndpointInfo {
+        String uri;
+        String component;
+        String direction;
+        String routeId;
+    }
+
+    record VanishingInfo(IntegrationInfo info, long startTime) {
+    }
+}
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelTopTui.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelTopTui.java
new file mode 100644
index 000000000000..54305b18a31c
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelTopTui.java
@@ -0,0 +1,488 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
+
+import dev.tamboui.layout.Constraint;
+import dev.tamboui.layout.Layout;
+import dev.tamboui.layout.Rect;
+import dev.tamboui.style.Color;
+import dev.tamboui.style.Style;
+import dev.tamboui.terminal.Frame;
+import dev.tamboui.text.Line;
+import dev.tamboui.text.Span;
+import dev.tamboui.text.Text;
+import dev.tamboui.tui.TuiRunner;
+import dev.tamboui.tui.event.Event;
+import dev.tamboui.tui.event.KeyCode;
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.tui.event.TickEvent;
+import dev.tamboui.widgets.block.Block;
+import dev.tamboui.widgets.block.BorderType;
+import dev.tamboui.widgets.paragraph.Paragraph;
+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.dsl.jbang.core.commands.CamelCommand;
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.apache.camel.dsl.jbang.core.common.ProcessHelper;
+import org.apache.camel.support.PatternHelper;
+import org.apache.camel.util.FileUtil;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
+import picocli.CommandLine;
+import picocli.CommandLine.Command;
+
+@Command(name = "top-tui",
+         description = "Live TUI dashboard for route performance",
+         sortOptions = false)
+public class CamelTopTui extends CamelCommand {
+
+    private static final String[] SORT_COLUMNS = { "mean", "max", "total", "failed", "name" };
+
+    @CommandLine.Parameters(description = "Name or pid of running Camel integration", arity = "0..1")
+    String name = "*";
+
+    @CommandLine.Option(names = { "--refresh" },
+                        description = "Refresh interval in milliseconds (default: ${DEFAULT-VALUE})",
+                        defaultValue = "500")
+    long refreshInterval = 500;
+
+    @CommandLine.Option(names = { "--sort" },
+                        description = "Sort column: mean, max, total, failed, name (default: ${DEFAULT-VALUE})",
+                        defaultValue = "mean")
+    String sort = "mean";
+
+    // State
+    private final AtomicReference<List<RouteRow>> data = new AtomicReference<>(Collections.emptyList());
+    private final TableState tableState = new TableState();
+    private int sortIndex;
+    private volatile long lastRefresh;
+
+    public CamelTopTui(CamelJBangMain main) {
+        super(main);
+    }
+
+    @Override
+    public Integer doCall() throws Exception {
+        // Eagerly load classes used by the input reader thread and picocli
+        // post-processing to avoid ClassNotFoundException during shutdown
+        try {
+            Class.forName("dev.tamboui.tui.event.KeyModifiers");
+            Class.forName("dev.tamboui.tui.event.KeyEvent");
+            Class.forName("dev.tamboui.tui.event.KeyCode");
+            Class.forName("picocli.CommandLine$IExitCodeGenerator");
+        } catch (ClassNotFoundException e) {
+            // ignore
+        }
+
+        // Resolve initial sort index
+        sortIndex = indexOfSort(sort);
+
+        // Initial data load
+        refreshData();
+
+        try (var tui = TuiRunner.create()) {
+            sun.misc.Signal.handle(new sun.misc.Signal("INT"), sig -> tui.quit());
+            tui.run(this::handleEvent, this::render);
+        }
+        return 0;
+    }
+
+    // ---- Event Handling ----
+
+    private boolean handleEvent(Event event, TuiRunner runner) {
+        if (event instanceof KeyEvent ke) {
+            if (ke.isQuit() || ke.isCharIgnoreCase('q') || ke.isKey(KeyCode.ESCAPE)) {
+                runner.quit();
+                return true;
+            }
+            if (ke.isChar('r')) {
+                refreshData();
+                return true;
+            }
+            if (ke.isCharIgnoreCase('s')) {
+                // Cycle sort column
+                sortIndex = (sortIndex + 1) % SORT_COLUMNS.length;
+                sort = SORT_COLUMNS[sortIndex];
+                sortData();
+                return true;
+            }
+            if (ke.isUp()) {
+                tableState.selectPrevious();
+                return true;
+            }
+            if (ke.isDown()) {
+                tableState.selectNext(data.get().size());
+                return true;
+            }
+        }
+        if (event instanceof TickEvent) {
+            long now = System.currentTimeMillis();
+            if (now - lastRefresh >= refreshInterval) {
+                refreshData();
+            }
+            return true;
+        }
+        return false;
+    }
+
+    // ---- Rendering ----
+
+    private void render(Frame frame) {
+        Rect area = frame.area();
+
+        // Layout: header (3 rows) + table (fill) + footer (1 row)
+        List<Rect> chunks = Layout.vertical()
+                .constraints(
+                        Constraint.length(3),
+                        Constraint.fill(),
+                        Constraint.length(1))
+                .split(area);
+
+        renderHeader(frame, chunks.get(0));
+        renderTable(frame, chunks.get(1));
+        renderFooter(frame, chunks.get(2));
+    }
+
+    private void renderHeader(Frame frame, Rect area) {
+        List<RouteRow> rows = data.get();
+        long totalExchanges = rows.stream().mapToLong(r -> r.total).sum();
+        long totalFailed = rows.stream().mapToLong(r -> r.failed).sum();
+        int routeCount = rows.size();
+        long pidCount = rows.stream().map(r -> r.pid).distinct().count();
+
+        Line titleLine = Line.from(
+                Span.styled(" Camel Top", Style.create().fg(Color.rgb(0xF6, 0x91, 0x23)).bold()),
+                Span.raw("  "),
+                Span.styled(pidCount + " integration(s)", Style.create().fg(Color.CYAN)),
+                Span.raw("  "),
+                Span.styled(routeCount + " route(s)", Style.create().fg(Color.GREEN)),
+                Span.raw("  "),
+                Span.styled("total: " + totalExchanges, Style.create().fg(Color.WHITE)),
+                Span.raw("  "),
+                Span.styled("failed: " + totalFailed,
+                        totalFailed > 0 ? Style.create().fg(Color.RED).bold() : Style.create().dim()),
+                Span.raw("  "),
+                Span.styled("sort: " + sort, Style.create().fg(Color.YELLOW)));
+
+        Block headerBlock = Block.builder()
+                .borderType(BorderType.ROUNDED)
+                .title(" Apache Camel - Route Performance ")
+                .build();
+
+        frame.renderWidget(
+                Paragraph.builder().text(Text.from(titleLine)).block(headerBlock).build(),
+                area);
+    }
+
+    private void renderTable(Frame frame, Rect area) {
+        List<RouteRow> rows = data.get();
+
+        List<Row> tableRows = new ArrayList<>();
+        for (RouteRow r : rows) {
+            Style statusStyle = "Started".equals(r.state)
+                    ? Style.create().fg(Color.GREEN)
+                    : Style.create().fg(Color.RED);
+
+            Style failStyle = r.failed > 0
+                    ? Style.create().fg(Color.RED).bold()
+                    : Style.create();
+
+            tableRows.add(Row.from(
+                    Cell.from(r.pid),
+                    Cell.from(Span.styled(truncate(r.name, 20), Style.create().fg(Color.CYAN))),
+                    Cell.from(Span.styled(truncate(r.routeId, 18), Style.create().fg(Color.WHITE))),
+                    Cell.from(truncate(r.from, 30)),
+                    Cell.from(Span.styled(r.state != null ? r.state : "", statusStyle)),
+                    Cell.from(String.valueOf(r.total)),
+                    Cell.from(Span.styled(String.valueOf(r.failed), failStyle)),
+                    Cell.from(String.valueOf(r.inflight)),
+                    Cell.from(r.mean >= 0 ? String.valueOf(r.mean) : ""),
+                    Cell.from(r.min >= 0 ? String.valueOf(r.min) : ""),
+                    Cell.from(r.max >= 0 ? String.valueOf(r.max) : ""),
+                    Cell.from(r.last >= 0 ? String.valueOf(r.last) : ""),
+                    Cell.from(r.throughput != null ? r.throughput : "")));
+        }
+
+        Row header = Row.from(
+                Cell.from(Span.styled("PID", Style.create().bold())),
+                Cell.from(Span.styled("NAME", Style.create().bold())),
+                Cell.from(Span.styled("ROUTE", Style.create().bold())),
+                Cell.from(Span.styled("FROM", Style.create().bold())),
+                Cell.from(Span.styled("STATUS", Style.create().bold())),
+                Cell.from(Span.styled("TOTAL", Style.create().bold())),
+                Cell.from(Span.styled("FAIL", Style.create().bold())),
+                Cell.from(Span.styled("INFLIGHT", Style.create().bold())),
+                Cell.from(Span.styled(sortLabel("MEAN", "mean"), sortStyle("mean"))),
+                Cell.from(Span.styled(sortLabel("MIN", "min"), sortStyle("min"))),
+                Cell.from(Span.styled(sortLabel("MAX", "max"), sortStyle("max"))),
+                Cell.from(Span.styled("LAST", Style.create().bold())),
+                Cell.from(Span.styled("THRUPUT", Style.create().bold())));
+
+        Table table = Table.builder()
+                .rows(tableRows)
+                .header(header)
+                .widths(
+                        Constraint.length(8),   // PID
+                        Constraint.length(20),  // NAME
+                        Constraint.length(18),  // ROUTE
+                        Constraint.fill(),      // FROM
+                        Constraint.length(9),   // STATUS
+                        Constraint.length(8),   // TOTAL
+                        Constraint.length(6),   // FAIL
+                        Constraint.length(9),   // INFLIGHT
+                        Constraint.length(6),   // MEAN
+                        Constraint.length(6),   // MIN
+                        Constraint.length(6),   // MAX
+                        Constraint.length(6),   // LAST
+                        Constraint.length(9))   // THRUPUT
+                .highlightStyle(Style.create().fg(Color.WHITE).bold().onBlue())
+                .block(Block.builder().borderType(BorderType.ROUNDED).title(" Routes ").build())
+                .build();
+
+        frame.renderStatefulWidget(table, area, tableState);
+    }
+
+    private void renderFooter(Frame frame, Rect area) {
+        String refreshLabel = refreshInterval >= 1000
+                ? (refreshInterval / 1000) + "s"
+                : refreshInterval + "ms";
+
+        Line footer = Line.from(
+                Span.styled(" q", Style.create().fg(Color.YELLOW).bold()),
+                Span.raw(" quit  "),
+                Span.styled("s", Style.create().fg(Color.YELLOW).bold()),
+                Span.raw(" sort  "),
+                Span.styled("r", Style.create().fg(Color.YELLOW).bold()),
+                Span.raw(" refresh  "),
+                Span.styled("\u2191\u2193", Style.create().fg(Color.YELLOW).bold()),
+                Span.raw(" navigate  "),
+                Span.styled("Refresh: " + refreshLabel, Style.create().dim()));
+
+        frame.renderWidget(Paragraph.from(footer), area);
+    }
+
+    // ---- Data Loading ----
+
+    @SuppressWarnings("unchecked")
+    private void refreshData() {
+        lastRefresh = System.currentTimeMillis();
+        try {
+            List<RouteRow> rows = new ArrayList<>();
+            List<Long> pids = findPids(name);
+            ProcessHandle.allProcesses()
+                    .filter(ph -> pids.contains(ph.pid()))
+                    .forEach(ph -> {
+                        JsonObject root = loadStatus(ph.pid());
+                        if (root != null) {
+                            JsonObject context = (JsonObject) root.get("context");
+                            if (context == null) {
+                                return;
+                            }
+                            String integrationName = context.getString("name");
+                            if ("CamelJBang".equals(integrationName)) {
+                                integrationName = ProcessHelper.extractName(root, ph);
+                            }
+                            String pid = Long.toString(ph.pid());
+
+                            JsonArray routesArray = (JsonArray) root.get("routes");
+                            if (routesArray != null) {
+                                for (Object r : routesArray) {
+                                    JsonObject rj = (JsonObject) r;
+                                    RouteRow row = new RouteRow();
+                                    row.pid = pid;
+                                    row.name = integrationName;
+                                    row.routeId = rj.getString("routeId");
+                                    row.from = rj.getString("from");
+                                    row.state = rj.getString("state");
+
+                                    Map<String, ?> stats = rj.getMap("statistics");
+                                    if (stats != null) {
+                                        row.total = objToLong(stats.get("exchangesTotal"));
+                                        row.failed = objToLong(stats.get("exchangesFailed"));
+                                        row.inflight = objToLong(stats.get("exchangesInflight"));
+                                        row.mean = objToLong(stats.get("meanProcessingTime"));
+                                        row.min = objToLong(stats.get("minProcessingTime"));
+                                        row.max = objToLong(stats.get("maxProcessingTime"));
+                                        row.last = objToLong(stats.get("lastProcessingTime"));
+                                        Object thp = stats.get("exchangesThroughput");
+                                        if (thp != null) {
+                                            row.throughput = thp.toString();
+                                        }
+                                    }
+                                    rows.add(row);
+                                }
+                            }
+                        }
+                    });
+
+            // Sort
+            rows.sort(this::sortRow);
+            data.set(rows);
+        } catch (Exception e) {
+            // ignore refresh errors
+        }
+    }
+
+    private void sortData() {
+        List<RouteRow> rows = new ArrayList<>(data.get());
+        rows.sort(this::sortRow);
+        data.set(rows);
+    }
+
+    private int sortRow(RouteRow o1, RouteRow o2) {
+        switch (sort) {
+            case "mean":
+                return Long.compare(o2.mean, o1.mean); // highest first
+            case "max":
+                return Long.compare(o2.max, o1.max);
+            case "total":
+                return Long.compare(o2.total, o1.total);
+            case "failed":
+                return Long.compare(o2.failed, o1.failed);
+            case "name":
+                int c = o1.name != null && o2.name != null
+                        ? o1.name.compareToIgnoreCase(o2.name)
+                        : 0;
+                if (c == 0) {
+                    c = o1.routeId != null && o2.routeId != null
+                            ? o1.routeId.compareToIgnoreCase(o2.routeId)
+                            : 0;
+                }
+                return c;
+            default:
+                return 0;
+        }
+    }
+
+    // ---- Helpers ----
+
+    private List<Long> findPids(String name) {
+        List<Long> pids = new ArrayList<>();
+        final long cur = ProcessHandle.current().pid();
+        String pattern = name;
+        if (!pattern.matches("\\d+") && !pattern.endsWith("*")) {
+            pattern = pattern + "*";
+        }
+        final String pat = pattern;
+        ProcessHandle.allProcesses()
+                .filter(ph -> ph.pid() != cur)
+                .forEach(ph -> {
+                    JsonObject root = loadStatus(ph.pid());
+                    if (root != null) {
+                        String pName = ProcessHelper.extractName(root, ph);
+                        pName = FileUtil.onlyName(pName);
+                        if (pName != null && !pName.isEmpty() && PatternHelper.matchPattern(pName, pat)) {
+                            pids.add(ph.pid());
+                        } else {
+                            JsonObject context = (JsonObject) root.get("context");
+                            if (context != null) {
+                                pName = context.getString("name");
+                                if ("CamelJBang".equals(pName)) {
+                                    pName = null;
+                                }
+                                if (pName != null && !pName.isEmpty() && PatternHelper.matchPattern(pName, pat)) {
+                                    pids.add(ph.pid());
+                                }
+                            }
+                        }
+                    }
+                });
+        return pids;
+    }
+
+    private JsonObject loadStatus(long pid) {
+        try {
+            Path f = getStatusFile(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;
+    }
+
+    private static int indexOfSort(String s) {
+        for (int i = 0; i < SORT_COLUMNS.length; i++) {
+            if (SORT_COLUMNS[i].equals(s)) {
+                return i;
+            }
+        }
+        return 0;
+    }
+
+    private String sortLabel(String label, String column) {
+        return sort.equals(column) ? label + "\u25BC" : label;
+    }
+
+    private Style sortStyle(String column) {
+        return sort.equals(column)
+                ? Style.create().fg(Color.YELLOW).bold()
+                : Style.create().bold();
+    }
+
+    private static String truncate(String s, int max) {
+        if (s == null) {
+            return "";
+        }
+        return s.length() > max ? s.substring(0, max - 1) + "\u2026" : s;
+    }
+
+    private static long objToLong(Object o) {
+        if (o instanceof Number n) {
+            return n.longValue();
+        }
+        if (o != null) {
+            try {
+                return Long.parseLong(o.toString());
+            } catch (NumberFormatException e) {
+                // ignore
+            }
+        }
+        return 0;
+    }
+
+    // ---- Data Class ----
+
+    static class RouteRow {
+        String pid;
+        String name;
+        String routeId;
+        String from;
+        String state;
+        long total;
+        long failed;
+        long inflight;
+        long mean;
+        long min;
+        long max;
+        long last;
+        String throughput;
+    }
+}
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelTraceTui.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelTraceTui.java
new file mode 100644
index 000000000000..05d56543dcb4
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelTraceTui.java
@@ -0,0 +1,635 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
+
+import dev.tamboui.layout.Constraint;
+import dev.tamboui.layout.Layout;
+import dev.tamboui.layout.Rect;
+import dev.tamboui.style.Color;
+import dev.tamboui.style.Overflow;
+import dev.tamboui.style.Style;
+import dev.tamboui.terminal.Frame;
+import dev.tamboui.text.Line;
+import dev.tamboui.text.Span;
+import dev.tamboui.text.Text;
+import dev.tamboui.tui.TuiRunner;
+import dev.tamboui.tui.event.Event;
+import dev.tamboui.tui.event.KeyCode;
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.tui.event.TickEvent;
+import dev.tamboui.widgets.block.Block;
+import dev.tamboui.widgets.block.BorderType;
+import dev.tamboui.widgets.paragraph.Paragraph;
+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.dsl.jbang.core.commands.CamelCommand;
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
+import org.apache.camel.dsl.jbang.core.common.ProcessHelper;
+import org.apache.camel.support.PatternHelper;
+import org.apache.camel.util.FileUtil;
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
+import picocli.CommandLine;
+import picocli.CommandLine.Command;
+
+@Command(name = "trace-tui",
+         description = "TUI exchange trace viewer",
+         sortOptions = false)
+public class CamelTraceTui extends CamelCommand {
+
+    private static final int MAX_TRACES = 200;
+
+    @CommandLine.Parameters(description = "Name or pid of running Camel integration", arity = "0..1")
+    String name = "*";
+
+    @CommandLine.Option(names = { "--refresh" },
+                        description = "Refresh interval in milliseconds (default: ${DEFAULT-VALUE})",
+                        defaultValue = "200")
+    long refreshInterval = 200;
+
+    @CommandLine.Option(names = { "--grep" },
+                        description = "Filter traces by text")
+    String grep;
+
+    // State
+    private final AtomicReference<List<TraceEntry>> traces = new AtomicReference<>(Collections.emptyList());
+    private final TableState traceTableState = new TableState();
+    private final Map<String, Long> traceFilePositions = new LinkedHashMap<>();
+
+    private boolean showHeaders = true;
+    private boolean showBody = true;
+    private boolean followMode = true;
+    private volatile long lastRefresh;
+
+    public CamelTraceTui(CamelJBangMain main) {
+        super(main);
+    }
+
+    @Override
+    public Integer doCall() throws Exception {
+        // Eagerly load classes used by the input reader thread and picocli
+        // post-processing to avoid ClassNotFoundException during shutdown
+        try {
+            Class.forName("dev.tamboui.tui.event.KeyModifiers");
+            Class.forName("dev.tamboui.tui.event.KeyEvent");
+            Class.forName("dev.tamboui.tui.event.KeyCode");
+            Class.forName("picocli.CommandLine$IExitCodeGenerator");
+        } catch (ClassNotFoundException e) {
+            // ignore
+        }
+
+        // Initial data load
+        refreshData();
+
+        try (var tui = TuiRunner.create()) {
+            sun.misc.Signal.handle(new sun.misc.Signal("INT"), sig -> tui.quit());
+            tui.run(
+                    this::handleEvent,
+                    this::render);
+        }
+        return 0;
+    }
+
+    // ---- Event Handling ----
+
+    private boolean handleEvent(Event event, TuiRunner runner) {
+        if (event instanceof KeyEvent ke) {
+            if (ke.isQuit() || ke.isCharIgnoreCase('q') || ke.isKey(KeyCode.ESCAPE)) {
+                runner.quit();
+                return true;
+            }
+            if (ke.isUp()) {
+                followMode = false;
+                traceTableState.selectPrevious();
+                return true;
+            }
+            if (ke.isDown()) {
+                List<TraceEntry> current = traces.get();
+                traceTableState.selectNext(current.size());
+                return true;
+            }
+            if (ke.isCharIgnoreCase('h')) {
+                showHeaders = !showHeaders;
+                return true;
+            }
+            if (ke.isCharIgnoreCase('b')) {
+                showBody = !showBody;
+                return true;
+            }
+            if (ke.isCharIgnoreCase('f')) {
+                followMode = !followMode;
+                if (followMode) {
+                    List<TraceEntry> current = traces.get();
+                    if (!current.isEmpty()) {
+                        traceTableState.select(current.size() - 1);
+                    }
+                }
+                return true;
+            }
+        }
+        if (event instanceof TickEvent) {
+            long now = System.currentTimeMillis();
+            if (now - lastRefresh >= refreshInterval) {
+                refreshData();
+            }
+            return true;
+        }
+        return false;
+    }
+
+    // ---- Rendering ----
+
+    private void render(Frame frame) {
+        Rect area = frame.area();
+
+        // Layout: header (3 rows) + trace list (50%) + detail panel (50%) + footer (1 row)
+        List<Rect> mainChunks = Layout.vertical()
+                .constraints(
+                        Constraint.length(3),
+                        Constraint.percentage(50),
+                        Constraint.fill(),
+                        Constraint.length(1))
+                .split(area);
+
+        renderHeader(frame, mainChunks.get(0));
+        renderTraceList(frame, mainChunks.get(1));
+        renderDetailPanel(frame, mainChunks.get(2));
+        renderFooter(frame, mainChunks.get(3));
+    }
+
+    private void renderHeader(Frame frame, Rect area) {
+        List<TraceEntry> current = traces.get();
+        String filterInfo = grep != null ? "  filter: " + grep : "";
+
+        Line titleLine = Line.from(
+                Span.styled(" Camel Trace Viewer", Style.create().fg(Color.rgb(0xF6, 0x91, 0x23)).bold()),
+                Span.raw("  "),
+                Span.styled(current.size() + " trace(s)", Style.create().fg(Color.CYAN)),
+                Span.raw("  "),
+                Span.styled(followMode ? "[FOLLOW]" : "[SCROLL]",
+                        Style.create().fg(followMode ? Color.GREEN : Color.YELLOW).bold()),
+                Span.styled(filterInfo, Style.create().dim()));
+
+        Block headerBlock = Block.builder()
+                .borderType(BorderType.ROUNDED)
+                .title(" Apache Camel Traces ")
+                .build();
+
+        frame.renderWidget(
+                Paragraph.builder().text(Text.from(titleLine)).block(headerBlock).build(),
+                area);
+    }
+
+    private void renderTraceList(Frame frame, Rect area) {
+        List<TraceEntry> current = traces.get();
+
+        // Auto-follow: select last entry
+        if (followMode && !current.isEmpty()) {
+            traceTableState.select(current.size() - 1);
+        }
+
+        List<Row> rows = new ArrayList<>();
+        for (TraceEntry entry : current) {
+            Style statusStyle = switch (entry.status) {
+                case "Created" -> Style.create().fg(Color.CYAN);
+                case "Routing", "Processing" -> Style.create().fg(Color.YELLOW);
+                case "Sent" -> Style.create().fg(Color.GREEN);
+                default -> Style.create().fg(Color.WHITE);
+            };
+
+            String bodyPreview = entry.bodyPreview != null ? truncate(entry.bodyPreview, 40) : "";
+
+            rows.add(Row.from(
+                    Cell.from(entry.timestamp != null ? truncate(entry.timestamp, 12) : ""),
+                    Cell.from(entry.pid != null ? entry.pid : ""),
+                    Cell.from(Span.styled(
+                            entry.routeId != null ? truncate(entry.routeId, 15) : "",
+                            Style.create().fg(Color.CYAN))),
+                    Cell.from(entry.nodeId != null ? truncate(entry.nodeId, 15) : ""),
+                    Cell.from(Span.styled(entry.status != null ? entry.status : "", statusStyle)),
+                    Cell.from(entry.elapsed + "ms"),
+                    Cell.from(bodyPreview)));
+        }
+
+        Row header = Row.from(
+                Cell.from(Span.styled("TIME", Style.create().bold())),
+                Cell.from(Span.styled("PID", Style.create().bold())),
+                Cell.from(Span.styled("ROUTE", Style.create().bold())),
+                Cell.from(Span.styled("NODE", Style.create().bold())),
+                Cell.from(Span.styled("STATUS", Style.create().bold())),
+                Cell.from(Span.styled("ELAPSED", Style.create().bold())),
+                Cell.from(Span.styled("BODY", Style.create().bold())));
+
+        Table table = Table.builder()
+                .rows(rows)
+                .header(header)
+                .widths(
+                        Constraint.length(12),
+                        Constraint.length(8),
+                        Constraint.length(15),
+                        Constraint.length(15),
+                        Constraint.length(12),
+                        Constraint.length(10),
+                        Constraint.fill())
+                .highlightStyle(Style.create().fg(Color.WHITE).bold().onBlue())
+                .block(Block.builder().borderType(BorderType.ROUNDED).title(" Traces ").build())
+                .build();
+
+        frame.renderStatefulWidget(table, area, traceTableState);
+    }
+
+    private void renderDetailPanel(Frame frame, Rect area) {
+        List<TraceEntry> current = traces.get();
+        Integer sel = traceTableState.selected();
+
+        if (sel == null || sel < 0 || sel >= current.size()) {
+            frame.renderWidget(
+                    Paragraph.builder()
+                            .text(Text.from(Line.from(
+                                    Span.styled(" Select a trace entry to view details",
+                                            Style.create().dim()))))
+                            .block(Block.builder().borderType(BorderType.ROUNDED)
+                                    .title(" Detail ").build())
+                            .build(),
+                    area);
+            return;
+        }
+
+        TraceEntry entry = current.get(sel);
+        List<Line> lines = new ArrayList<>();
+
+        // Exchange info
+        lines.add(Line.from(
+                Span.styled(" Exchange: ", Style.create().fg(Color.YELLOW).bold()),
+                Span.raw(entry.exchangeId != null ? entry.exchangeId : "")));
+        lines.add(Line.from(
+                Span.styled(" UID:      ", Style.create().fg(Color.YELLOW).bold()),
+                Span.raw(entry.uid != null ? entry.uid : "")));
+        lines.add(Line.from(
+                Span.styled(" Location: ", Style.create().fg(Color.YELLOW).bold()),
+                Span.raw(entry.location != null ? entry.location : "")));
+        lines.add(Line.from(
+                Span.styled(" Route:    ", Style.create().fg(Color.YELLOW).bold()),
+                Span.raw(entry.routeId != null ? entry.routeId : ""),
+                Span.raw("  Node: "),
+                Span.raw(entry.nodeId != null ? entry.nodeId : "")));
+        lines.add(Line.from(
+                Span.styled(" Status:   ", Style.create().fg(Color.YELLOW).bold()),
+                Span.raw(entry.status != null ? entry.status : ""),
+                Span.raw("  Elapsed: "),
+                Span.raw(entry.elapsed + "ms")));
+        lines.add(Line.from(Span.raw("")));
+
+        // Headers
+        if (showHeaders && entry.headers != null && !entry.headers.isEmpty()) {
+            lines.add(Line.from(Span.styled(" Headers:", Style.create().fg(Color.GREEN).bold())));
+            for (Map.Entry<String, Object> h : entry.headers.entrySet()) {
+                lines.add(Line.from(
+                        Span.styled("   " + h.getKey(), Style.create().fg(Color.CYAN)),
+                        Span.raw(" = "),
+                        Span.raw(h.getValue() != null ? h.getValue().toString() : "null")));
+            }
+            lines.add(Line.from(Span.raw("")));
+        }
+
+        // Body
+        if (showBody && entry.body != null) {
+            lines.add(Line.from(Span.styled(" Body:", Style.create().fg(Color.GREEN).bold())));
+            // Split body into lines for display
+            String[] bodyLines = entry.body.split("\n");
+            for (String bl : bodyLines) {
+                lines.add(Line.from(Span.raw("   " + bl)));
+            }
+            lines.add(Line.from(Span.raw("")));
+        }
+
+        // Exchange properties
+        if (entry.exchangeProperties != null && !entry.exchangeProperties.isEmpty()) {
+            lines.add(Line.from(Span.styled(" Exchange Properties:", Style.create().fg(Color.GREEN).bold())));
+            for (Map.Entry<String, Object> p : entry.exchangeProperties.entrySet()) {
+                lines.add(Line.from(
+                        Span.styled("   " + p.getKey(), Style.create().fg(Color.CYAN)),
+                        Span.raw(" = "),
+                        Span.raw(p.getValue() != null ? p.getValue().toString() : "null")));
+            }
+            lines.add(Line.from(Span.raw("")));
+        }
+
+        // Exchange variables
+        if (entry.exchangeVariables != null && !entry.exchangeVariables.isEmpty()) {
+            lines.add(Line.from(Span.styled(" Exchange Variables:", Style.create().fg(Color.GREEN).bold())));
+            for (Map.Entry<String, Object> v : entry.exchangeVariables.entrySet()) {
+                lines.add(Line.from(
+                        Span.styled("   " + v.getKey(), Style.create().fg(Color.CYAN)),
+                        Span.raw(" = "),
+                        Span.raw(v.getValue() != null ? v.getValue().toString() : "null")));
+            }
+        }
+
+        String title = String.format(" Detail [%s] ", entry.exchangeId != null ? truncate(entry.exchangeId, 30) : "");
+
+        Paragraph detail = Paragraph.builder()
+                .text(Text.from(lines))
+                .overflow(Overflow.CLIP)
+                .block(Block.builder().borderType(BorderType.ROUNDED).title(title).build())
+                .build();
+
+        frame.renderWidget(detail, area);
+    }
+
+    private void renderFooter(Frame frame, Rect area) {
+        Line footer = Line.from(
+                Span.styled(" q", Style.create().fg(Color.YELLOW).bold()),
+                Span.raw(" quit  "),
+                Span.styled("\u2191\u2193", Style.create().fg(Color.YELLOW).bold()),
+                Span.raw(" navigate  "),
+                Span.styled("h", Style.create().fg(Color.YELLOW).bold()),
+                Span.raw(" headers" + (showHeaders ? " [on]" : " [off]") + "  "),
+                Span.styled("b", Style.create().fg(Color.YELLOW).bold()),
+                Span.raw(" body" + (showBody ? " [on]" : " [off]") + "  "),
+                Span.styled("f", Style.create().fg(Color.YELLOW).bold()),
+                Span.raw(" follow" + (followMode ? " [on]" : " [off]") + "  "),
+                Span.styled("Refresh: " + refreshInterval + "ms", Style.create().dim()));
+
+        frame.renderWidget(Paragraph.from(footer), area);
+    }
+
+    // ---- Data Loading ----
+
+    private void refreshData() {
+        lastRefresh = System.currentTimeMillis();
+        try {
+            List<Long> pids = findPids(name);
+            List<TraceEntry> allTraces = new ArrayList<>(traces.get());
+
+            for (Long pid : pids) {
+                readTraceFile(Long.toString(pid), allTraces);
+            }
+
+            // Sort by timestamp
+            allTraces.sort((a, b) -> {
+                if (a.timestamp == null && b.timestamp == null) {
+                    return 0;
+                }
+                if (a.timestamp == null) {
+                    return -1;
+                }
+                if (b.timestamp == null) {
+                    return 1;
+                }
+                return a.timestamp.compareTo(b.timestamp);
+            });
+
+            // Keep only last MAX_TRACES
+            if (allTraces.size() > MAX_TRACES) {
+                allTraces = new ArrayList<>(allTraces.subList(allTraces.size() - MAX_TRACES, allTraces.size()));
+            }
+
+            // Apply grep filter
+            if (grep != null && !grep.isEmpty()) {
+                String lowerGrep = grep.toLowerCase();
+                allTraces = allTraces.stream()
+                        .filter(t -> matchesGrep(t, lowerGrep))
+                        .collect(java.util.stream.Collectors.toList());
+            }
+
+            traces.set(allTraces);
+        } catch (Exception e) {
+            // ignore refresh errors
+        }
+    }
+
+    private boolean matchesGrep(TraceEntry entry, String lowerGrep) {
+        if (entry.exchangeId != null && entry.exchangeId.toLowerCase().contains(lowerGrep)) {
+            return true;
+        }
+        if (entry.routeId != null && entry.routeId.toLowerCase().contains(lowerGrep)) {
+            return true;
+        }
+        if (entry.nodeId != null && entry.nodeId.toLowerCase().contains(lowerGrep)) {
+            return true;
+        }
+        if (entry.status != null && entry.status.toLowerCase().contains(lowerGrep)) {
+            return true;
+        }
+        if (entry.body != null && entry.body.toLowerCase().contains(lowerGrep)) {
+            return true;
+        }
+        if (entry.location != null && entry.location.toLowerCase().contains(lowerGrep)) {
+            return true;
+        }
+        return false;
+    }
+
+    @SuppressWarnings("unchecked")
+    private void readTraceFile(String pid, List<TraceEntry> allTraces) {
+        Path traceFile = CommandLineHelper.getCamelDir().resolve(pid + "-trace.json");
+        if (!Files.exists(traceFile)) {
+            return;
+        }
+
+        long lastPos = traceFilePositions.getOrDefault(pid, 0L);
+
+        try (RandomAccessFile raf = new RandomAccessFile(traceFile.toFile(), "r")) {
+            long length = raf.length();
+            if (length <= lastPos) {
+                return; // no new data
+            }
+
+            raf.seek(lastPos);
+            // If we're resuming mid-file, skip any partial line
+            if (lastPos > 0) {
+                raf.readLine();
+            }
+
+            // Read remaining bytes
+            long startPos = raf.getFilePointer();
+            byte[] remaining = new byte[(int) (length - startPos)];
+            raf.readFully(remaining);
+            String content = new String(remaining, StandardCharsets.UTF_8);
+
+            traceFilePositions.put(pid, length);
+
+            // Each line is a separate JSON object
+            String[] lines = content.split("\n");
+            for (String line : lines) {
+                line = line.trim();
+                if (line.isEmpty()) {
+                    continue;
+                }
+                try {
+                    JsonObject json = (JsonObject) Jsoner.deserialize(line);
+                    TraceEntry entry = parseTraceEntry(json, pid);
+                    if (entry != null) {
+                        allTraces.add(entry);
+                    }
+                } catch (Exception e) {
+                    // skip malformed lines
+                }
+            }
+        } catch (IOException e) {
+            // ignore
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    private TraceEntry parseTraceEntry(JsonObject json, String pid) {
+        TraceEntry entry = new TraceEntry();
+        entry.pid = pid;
+        entry.uid = json.getString("uid");
+        entry.exchangeId = json.getString("exchangeId");
+        entry.timestamp = json.getString("timestamp");
+        entry.routeId = json.getString("routeId");
+        entry.nodeId = json.getString("nodeId");
+        entry.location = json.getString("location");
+        entry.status = json.getString("status");
+
+        Object elapsedObj = json.get("elapsed");
+        if (elapsedObj instanceof Number n) {
+            entry.elapsed = n.longValue();
+        } else if (elapsedObj != null) {
+            try {
+                entry.elapsed = Long.parseLong(elapsedObj.toString());
+            } catch (NumberFormatException e) {
+                // ignore
+            }
+        }
+
+        // Parse message object
+        JsonObject message = (JsonObject) json.get("message");
+        if (message != null) {
+            // Headers
+            Object headersObj = message.get("headers");
+            if (headersObj instanceof Map) {
+                entry.headers = new LinkedHashMap<>((Map<String, Object>) headersObj);
+            }
+
+            // Body
+            Object bodyObj = message.get("body");
+            if (bodyObj != null) {
+                entry.body = bodyObj.toString();
+                // Create a preview (first line, truncated)
+                String preview = entry.body.replace("\n", " ").replace("\r", "");
+                entry.bodyPreview = preview;
+            }
+
+            // Exchange properties
+            Object propsObj = message.get("exchangeProperties");
+            if (propsObj instanceof Map) {
+                entry.exchangeProperties = new LinkedHashMap<>((Map<String, Object>) propsObj);
+            }
+
+            // Exchange variables
+            Object varsObj = message.get("exchangeVariables");
+            if (varsObj instanceof Map) {
+                entry.exchangeVariables = new LinkedHashMap<>((Map<String, Object>) varsObj);
+            }
+        }
+
+        return entry;
+    }
+
+    // ---- Helpers ----
+
+    private List<Long> findPids(String name) {
+        List<Long> pids = new ArrayList<>();
+        final long cur = ProcessHandle.current().pid();
+        String pattern = name;
+        if (!pattern.matches("\\d+") && !pattern.endsWith("*")) {
+            pattern = pattern + "*";
+        }
+        final String pat = pattern;
+        ProcessHandle.allProcesses()
+                .filter(ph -> ph.pid() != cur)
+                .forEach(ph -> {
+                    JsonObject root = loadStatus(ph.pid());
+                    if (root != null) {
+                        String pName = ProcessHelper.extractName(root, ph);
+                        pName = FileUtil.onlyName(pName);
+                        if (pName != null && !pName.isEmpty() && PatternHelper.matchPattern(pName, pat)) {
+                            pids.add(ph.pid());
+                        } else {
+                            JsonObject context = (JsonObject) root.get("context");
+                            if (context != null) {
+                                pName = context.getString("name");
+                                if ("CamelJBang".equals(pName)) {
+                                    pName = null;
+                                }
+                                if (pName != null && !pName.isEmpty() && PatternHelper.matchPattern(pName, pat)) {
+                                    pids.add(ph.pid());
+                                }
+                            }
+                        }
+                    }
+                });
+        return pids;
+    }
+
+    private JsonObject loadStatus(long pid) {
+        try {
+            Path f = getStatusFile(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;
+    }
+
+    private static String truncate(String s, int max) {
+        if (s == null) {
+            return "";
+        }
+        return s.length() > max ? s.substring(0, max - 1) + "\u2026" : s;
+    }
+
+    // ---- Data Classes ----
+
+    static class TraceEntry {
+        String pid;
+        String uid;
+        String exchangeId;
+        String timestamp;
+        String routeId;
+        String nodeId;
+        String location;
+        String status;
+        long elapsed;
+        String body;
+        String bodyPreview;
+        Map<String, Object> headers;
+        Map<String, Object> exchangeProperties;
+        Map<String, Object> exchangeVariables;
+    }
+}
diff --git a/parent/pom.xml b/parent/pom.xml
index 891f9e99f8af..050a06d85223 100644
--- a/parent/pom.xml
+++ b/parent/pom.xml
@@ -327,7 +327,7 @@
         <jgroups-raft-leveldbjni-version>1.8</jgroups-raft-leveldbjni-version>
         <jgroups-raft-mapdb-version>1.0.8</jgroups-raft-mapdb-version>
         <jira-rest-client-api-version>6.0.2</jira-rest-client-api-version>
-        <jline-version>4.0.7</jline-version>
+        <jline-version>4.0.9</jline-version>
         <libthrift-version>0.22.0</libthrift-version>
         <jodatime2-version>2.14.1</jodatime2-version>
         <jolokia-version>2.5.1</jolokia-version>