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

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


The following commit(s) were added to refs/heads/main by this push:
     new bc74c9cad682 CAMEL-24259: TUI - Add Producers, Events, Route 
Controller tabs and fix inline docs
bc74c9cad682 is described below

commit bc74c9cad6825930b3786ae1abf030e01222e1ed
Author: Claus Ibsen <[email protected]>
AuthorDate: Sun Jul 26 15:54:38 2026 +0200

    CAMEL-24259: TUI - Add Producers, Events, Route Controller tabs and fix 
inline docs
    
    Add three new TUI tabs in the More submenu: Producers, Events (exchange),
    and Route Controller. Fix inline doc scroll bug where toggling docs caused
    cursor-down to scroll selected line out of view. Fix multi-route inline
    docs to load processor detail for all routes. Fix YAML parameters block
    to document individual endpoint options with component name resolution.
    
    Closes #25114
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
---
 .../camel/cli/connector/LocalCliConnector.java     |   8 +
 .../dsl/jbang/core/commands/tui/DiagramTab.java    |  37 ++-
 .../dsl/jbang/core/commands/tui/EventInfo.java     |  25 ++
 .../dsl/jbang/core/commands/tui/EventTab.java      | 234 ++++++++++++++++
 .../jbang/core/commands/tui/IntegrationInfo.java   |   7 +
 .../dsl/jbang/core/commands/tui/ProducerInfo.java  |  27 ++
 .../dsl/jbang/core/commands/tui/ProducersTab.java  | 234 ++++++++++++++++
 .../core/commands/tui/RouteControllerInfo.java     |  29 ++
 .../core/commands/tui/RouteControllerTab.java      | 293 +++++++++++++++++++++
 .../dsl/jbang/core/commands/tui/RoutesTab.java     | 178 +++++++++++--
 .../dsl/jbang/core/commands/tui/SourceViewer.java  |  40 ++-
 .../dsl/jbang/core/commands/tui/StatusParser.java  |  70 +++++
 .../dsl/jbang/core/commands/tui/TabRegistry.java   |  11 +
 .../dsl/jbang/core/commands/tui/TuiIcons.java      |   3 +
 .../jbang/core/commands/tui/TabRegistryTest.java   |   8 +-
 15 files changed, 1149 insertions(+), 55 deletions(-)

diff --git 
a/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
 
b/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
index 290f2ec1c903..ad33fe6a3e93 100644
--- 
a/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
+++ 
b/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
@@ -1529,6 +1529,14 @@ public class LocalCliConnector extends ServiceSupport 
implements CliConnector, C
                         root.put("producers", json);
                     }
                 }
+                DevConsole dc14c = dcr.resolveById("route-controller");
+                if (dc14c != null) {
+                    JsonObject json
+                            = (JsonObject) 
dc14c.call(DevConsole.MediaType.JSON, Map.of("stacktrace", "false"));
+                    if (json != null && !json.isEmpty()) {
+                        root.put("routeController", json);
+                    }
+                }
                 DevConsole dc15 = dcr.resolveById("variables");
                 if (dc15 != null) {
                     JsonObject json = (JsonObject) 
dc15.call(DevConsole.MediaType.JSON);
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTab.java
index 3e33db3fbddc..7c7ac2116bac 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTab.java
@@ -1201,17 +1201,16 @@ class DiagramTab extends AbstractTab {
             detailScroll = 0;
         }
 
-        // load route detail once per route (covers all processors)
-        if (drillDownRouteId != null && 
!drillDownRouteId.equals(cachedRouteDetailId)
-                && !drillDownRouteId.equals(detailLoadingRouteId)) {
-            detailLoadingRouteId = drillDownRouteId;
+        // load route detail once (covers all routes and processors)
+        if (drillDownRouteId != null && cachedRouteDetail == null
+                && !"*".equals(detailLoadingRouteId)) {
+            detailLoadingRouteId = "*";
             detailLoading = true;
-            String rid = drillDownRouteId;
             if (ctx.runner != null) {
                 ctx.backgroundExecutor.execute(() -> {
-                    JsonObject result = requestRouteProcessorDetail(rid);
+                    JsonObject result = requestRouteProcessorDetail("*");
                     cachedRouteDetail = result;
-                    cachedRouteDetailId = rid;
+                    cachedRouteDetailId = "*";
                     detailLoading = false;
                 });
             }
@@ -1397,12 +1396,7 @@ class DiagramTab extends AbstractTab {
         if (cachedRouteDetail == null || nodeId == null) {
             return null;
         }
-        JsonArray processors = (JsonArray) cachedRouteDetail.get("processors");
-        if (processors == null) {
-            return null;
-        }
-        for (Object obj : processors) {
-            JsonObject p = (JsonObject) obj;
+        for (JsonObject p : RoutesTab.getAllProcessors(cachedRouteDetail)) {
             if (nodeId.equals(p.getString("id"))) {
                 return p;
             }
@@ -1417,7 +1411,7 @@ class DiagramTab extends AbstractTab {
         try {
             JsonObject root = new JsonObject();
             root.put("action", "processor-detail");
-            root.put("routeId", routeId);
+            root.put("routeId", "*");
             return ctx.executeAction(ctx.selectedPid, root, 5000);
         } catch (Exception e) {
             return null;
@@ -1431,8 +1425,8 @@ class DiagramTab extends AbstractTab {
             return Map.of();
         }
 
-        JsonArray processors = (JsonArray) cachedRouteDetail.get("processors");
-        if (processors == null || processors.isEmpty()) {
+        List<JsonObject> processors = 
RoutesTab.getAllProcessors(cachedRouteDetail);
+        if (processors.isEmpty()) {
             return Map.of();
         }
 
@@ -1440,8 +1434,7 @@ class DiagramTab extends AbstractTab {
         CamelCatalog catalog = info != null ? getCatalog(info) : null;
 
         Map<Integer, List<String>> result = new LinkedHashMap<>();
-        for (Object obj : processors) {
-            JsonObject proc = (JsonObject) obj;
+        for (JsonObject proc : processors) {
             Integer line = proc.getInteger("line");
             if (line == null || line <= 0) {
                 continue;
@@ -1465,14 +1458,14 @@ class DiagramTab extends AbstractTab {
 
     private void ensureProcessorDetailLoaded(String routeId) {
         if (routeId != null && cachedRouteDetail == null
-                && !routeId.equals(detailLoadingRouteId)) {
-            detailLoadingRouteId = routeId;
+                && !"*".equals(detailLoadingRouteId)) {
+            detailLoadingRouteId = "*";
             detailLoading = true;
             if (ctx.runner != null) {
                 ctx.backgroundExecutor.execute(() -> {
-                    JsonObject result = requestRouteProcessorDetail(routeId);
+                    JsonObject result = requestRouteProcessorDetail("*");
                     cachedRouteDetail = result;
-                    cachedRouteDetailId = routeId;
+                    cachedRouteDetailId = "*";
                     detailLoading = false;
                 });
             }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EventInfo.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EventInfo.java
new file mode 100644
index 000000000000..ed3893d11b79
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EventInfo.java
@@ -0,0 +1,25 @@
+/*
+ * 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;
+
+class EventInfo {
+    String type;
+    String category;
+    long timestamp;
+    String exchangeId;
+    String message;
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EventTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EventTab.java
new file mode 100644
index 000000000000..0a9ad6be45cc
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EventTab.java
@@ -0,0 +1,234 @@
+/*
+ * 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.List;
+
+import dev.tamboui.layout.Constraint;
+import dev.tamboui.layout.Rect;
+import dev.tamboui.style.Style;
+import dev.tamboui.terminal.Frame;
+import dev.tamboui.text.Span;
+import dev.tamboui.tui.event.KeyCode;
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.tui.event.MouseEvent;
+import dev.tamboui.widgets.block.Block;
+import dev.tamboui.widgets.block.BorderType;
+import dev.tamboui.widgets.block.Borders;
+import dev.tamboui.widgets.table.Cell;
+import dev.tamboui.widgets.table.Row;
+import dev.tamboui.widgets.table.Table;
+import org.apache.camel.util.TimeUtils;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+
+import static org.apache.camel.dsl.jbang.core.commands.tui.TuiHelper.*;
+
+class EventTab extends AbstractTableTab {
+
+    EventTab(MonitorContext ctx) {
+        super(ctx, "timestamp", "category", "type", "message");
+        this.sort = "timestamp";
+    }
+
+    @Override
+    public void navigateUp() {
+    }
+
+    @Override
+    public void navigateDown() {
+    }
+
+    @Override
+    public boolean handleKeyEvent(KeyEvent ke) {
+        if (ke.isPageUp() || ke.isKey(KeyCode.PAGE_UP)
+                || ke.isPageDown() || ke.isKey(KeyCode.PAGE_DOWN)
+                || ke.isHome() || ke.isEnd()) {
+            return false;
+        }
+        return super.handleKeyEvent(ke);
+    }
+
+    @Override
+    public boolean handleMouseEvent(MouseEvent me, Rect area) {
+        return false;
+    }
+
+    @Override
+    protected int getRowCount() {
+        IntegrationInfo info = ctx.findSelectedIntegration();
+        return info != null ? info.events.size() : 0;
+    }
+
+    @Override
+    protected void renderContent(Frame frame, Rect area, IntegrationInfo info) 
{
+        List<EventInfo> sorted = new ArrayList<>(info.events);
+        sorted.sort(this::sortEvent);
+
+        List<Row> rows = new ArrayList<>();
+        for (EventInfo ei : sorted) {
+            String ts = ei.timestamp > 0 ? TimeUtils.printSince(ei.timestamp) 
: "";
+            Style categoryStyle = categoryStyle(ei.category);
+
+            rows.add(Row.from(
+                    rightCell(ts, 12),
+                    Cell.from(Span.styled(ei.category != null ? ei.category : 
"", categoryStyle)),
+                    Cell.from(Span.styled(ei.type != null ? ei.type : "", 
Style.EMPTY.fg(Theme.accent()))),
+                    Cell.from(ei.message != null ? ei.message : "")));
+        }
+
+        if (rows.isEmpty()) {
+            rows.add(emptyRow("No events", 4));
+        }
+
+        Table table = Table.builder()
+                .rows(rows)
+                .header(Row.from(
+                        rightCell(sortLabel("AGO", "timestamp"), 12, 
sortStyle("timestamp")),
+                        Cell.from(Span.styled(sortLabel("CATEGORY", 
"category"), sortStyle("category"))),
+                        Cell.from(Span.styled(sortLabel("TYPE", "type"), 
sortStyle("type"))),
+                        Cell.from(Span.styled(sortLabel("MESSAGE", "message"), 
sortStyle("message")))))
+                .widths(
+                        Constraint.length(12),
+                        Constraint.length(12),
+                        Constraint.length(30),
+                        Constraint.fill())
+                
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+                        .title(" Events ").build())
+                .build();
+
+        lastTableArea = area;
+        frame.renderStatefulWidget(table, area, tableState);
+        renderScrollbar(frame, sorted.size());
+    }
+
+    private int sortEvent(EventInfo a, EventInfo b) {
+        int result = switch (sort) {
+            case "category" -> {
+                String ca = a.category != null ? a.category : "";
+                String cb = b.category != null ? b.category : "";
+                yield ca.compareToIgnoreCase(cb);
+            }
+            case "type" -> {
+                String ta = a.type != null ? a.type : "";
+                String tb = b.type != null ? b.type : "";
+                yield ta.compareToIgnoreCase(tb);
+            }
+            case "message" -> {
+                String ma = a.message != null ? a.message : "";
+                String mb = b.message != null ? b.message : "";
+                yield ma.compareToIgnoreCase(mb);
+            }
+            default -> // "timestamp" — newest first by default
+                Long.compare(b.timestamp, a.timestamp);
+        };
+        return sortReversed ? -result : result;
+    }
+
+    private static Style categoryStyle(String category) {
+        if (category == null) {
+            return Style.EMPTY;
+        }
+        return switch (category) {
+            case "route" -> Style.EMPTY.fg(Theme.accent());
+            case "exchange" -> Theme.success();
+            default -> Style.EMPTY;
+        };
+    }
+
+    @Override
+    public SelectionContext getSelectionContext() {
+        IntegrationInfo info = ctx.findSelectedIntegration();
+        if (info == null || info.events.isEmpty()) {
+            return null;
+        }
+        List<EventInfo> sorted = new ArrayList<>(info.events);
+        sorted.sort(this::sortEvent);
+        List<String> items = sorted.stream().map(e -> e.type != null ? e.type 
: "").toList();
+        Integer sel = tableState.selected();
+        return new SelectionContext("table", items, sel != null ? sel : -1, 
items.size(), "Events");
+    }
+
+    @Override
+    public String description() {
+        return "Camel lifecycle events (context, route, and exchange events)";
+    }
+
+    @Override
+    public String getHelpText() {
+        return """
+                # Events
+
+                Shows Camel lifecycle events captured by the event notifier. 
Events are
+                grouped into three categories:
+
+                - **general** — Context-level events: CamelContext 
starting/started/stopping,
+                  service add/remove, component add/remove
+                - **route** — Route lifecycle events: route 
added/removed/started/stopped/reloaded
+                - **exchange** — Exchange events: exchange 
created/completed/failed/sending/sent
+
+                Events are stored in a circular buffer (default capacity 25 
per category).
+                Only the most recent events are shown.
+
+                ## Table Columns
+
+                - **AGO** — How long ago the event occurred (e.g., `2s`, 
`1m30s`)
+                - **CATEGORY** — Event category: `general`, `route`, or 
`exchange`
+                - **TYPE** — The specific event type (e.g., 
`CamelContextStartedEvent`,
+                  `RouteStartedEvent`, `ExchangeCompletedEvent`)
+                - **MESSAGE** — Human-readable event description
+
+                ## Keys
+
+                - `Up/Down` — select event
+                - `s` — cycle sort column
+                - `S` — reverse sort order
+                """;
+    }
+
+    @Override
+    public JsonObject getTableDataAsJson() {
+        IntegrationInfo info = ctx.findSelectedIntegration();
+        if (info == null) {
+            return null;
+        }
+        JsonObject result = new JsonObject();
+        result.put("tab", "Events");
+        JsonArray rows = new JsonArray();
+        List<EventInfo> sorted = new ArrayList<>(info.events);
+        sorted.sort(this::sortEvent);
+        for (EventInfo ei : sorted) {
+            JsonObject row = new JsonObject();
+            row.put("type", ei.type);
+            row.put("category", ei.category);
+            if (ei.timestamp > 0) {
+                row.put("timestamp", ei.timestamp);
+            }
+            if (ei.exchangeId != null) {
+                row.put("exchangeId", ei.exchangeId);
+            }
+            row.put("message", ei.message);
+            rows.add(row);
+        }
+        result.put("rows", rows);
+        result.put("totalRows", info.events.size());
+        Integer sel = tableState.selected();
+        result.put("selectedIndex", sel != null ? sel : -1);
+        return result;
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationInfo.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationInfo.java
index 6598567a4198..88a2b654a942 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationInfo.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationInfo.java
@@ -80,6 +80,13 @@ class IntegrationInfo {
     long vanishStart;
     final List<RouteInfo> routes = new ArrayList<>();
     final List<ConsumerInfo> consumers = new ArrayList<>();
+    final List<ProducerInfo> producers = new ArrayList<>();
+    final List<EventInfo> events = new ArrayList<>();
+    String routeControllerType;
+    boolean routeControllerUnhealthy;
+    int routeControllerRestartingRoutes;
+    int routeControllerExhaustedRoutes;
+    final List<RouteControllerInfo> routeControllerRoutes = new ArrayList<>();
     final List<HealthCheckInfo> healthChecks = new ArrayList<>();
     final List<EndpointInfo> endpoints = new ArrayList<>();
     final List<CircuitBreakerInfo> circuitBreakers = new ArrayList<>();
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProducerInfo.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProducerInfo.java
new file mode 100644
index 000000000000..e1e697163eac
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProducerInfo.java
@@ -0,0 +1,27 @@
+/*
+ * 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;
+
+class ProducerInfo {
+    String uri;
+    String state;
+    String className;
+    String routeId;
+    String stepId;
+    boolean remote;
+    boolean singleton;
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProducersTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProducersTab.java
new file mode 100644
index 000000000000..38c63698c796
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProducersTab.java
@@ -0,0 +1,234 @@
+/*
+ * 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.List;
+
+import dev.tamboui.layout.Constraint;
+import dev.tamboui.layout.Rect;
+import dev.tamboui.style.Style;
+import dev.tamboui.terminal.Frame;
+import dev.tamboui.text.Span;
+import dev.tamboui.tui.event.KeyCode;
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.tui.event.MouseEvent;
+import dev.tamboui.widgets.block.Block;
+import dev.tamboui.widgets.block.BorderType;
+import dev.tamboui.widgets.block.Borders;
+import dev.tamboui.widgets.table.Cell;
+import dev.tamboui.widgets.table.Row;
+import dev.tamboui.widgets.table.Table;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+
+import static org.apache.camel.dsl.jbang.core.commands.tui.TuiHelper.*;
+
+class ProducersTab extends AbstractTableTab {
+
+    ProducersTab(MonitorContext ctx) {
+        super(ctx, "route", "status", "type", "uri");
+    }
+
+    @Override
+    public void navigateUp() {
+    }
+
+    @Override
+    public void navigateDown() {
+    }
+
+    @Override
+    public boolean handleKeyEvent(KeyEvent ke) {
+        if (ke.isPageUp() || ke.isKey(KeyCode.PAGE_UP)
+                || ke.isPageDown() || ke.isKey(KeyCode.PAGE_DOWN)
+                || ke.isHome() || ke.isEnd()) {
+            return false;
+        }
+        return super.handleKeyEvent(ke);
+    }
+
+    @Override
+    public boolean handleMouseEvent(MouseEvent me, Rect area) {
+        return false;
+    }
+
+    @Override
+    protected int getRowCount() {
+        IntegrationInfo info = ctx.findSelectedIntegration();
+        return info != null ? info.producers.size() : 0;
+    }
+
+    @Override
+    protected void renderContent(Frame frame, Rect area, IntegrationInfo info) 
{
+        List<ProducerInfo> sorted = new ArrayList<>(info.producers);
+        sorted.sort(this::sortProducer);
+
+        List<Row> rows = new ArrayList<>();
+        for (ProducerInfo pi : sorted) {
+            String type = producerType(pi);
+            Style statusStyle = "Started".equals(pi.state) ? Theme.success() : 
Theme.error();
+
+            rows.add(Row.from(
+                    Cell.from(Span.styled(" " + (pi.routeId != null ? 
pi.routeId : ""), Style.EMPTY.fg(Theme.accent()))),
+                    Cell.from(Span.styled(pi.state != null ? pi.state : "", 
statusStyle)),
+                    Cell.from(type),
+                    Cell.from(pi.remote ? "Yes" : "No"),
+                    Cell.from(pi.uri != null ? pi.uri : "")));
+        }
+
+        if (rows.isEmpty()) {
+            rows.add(emptyRow("No producers", 5));
+        }
+
+        Table table = Table.builder()
+                .rows(rows)
+                .header(Row.from(
+                        Cell.from(Span.styled(" " + sortLabel("ROUTE", 
"route"), sortStyle("route"))),
+                        Cell.from(Span.styled(sortLabel("STATUS", "status"), 
sortStyle("status"))),
+                        Cell.from(Span.styled(sortLabel("TYPE", "type"), 
sortStyle("type"))),
+                        Cell.from(Span.styled("REMOTE", Style.EMPTY.bold())),
+                        Cell.from(Span.styled(sortLabel("URI", "uri"), 
sortStyle("uri")))))
+                .widths(
+                        Constraint.length(20),
+                        Constraint.length(10),
+                        Constraint.length(20),
+                        Constraint.length(8),
+                        Constraint.fill())
+                
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+                        .title(" Producers ").build())
+                .build();
+
+        lastTableArea = area;
+        frame.renderStatefulWidget(table, area, tableState);
+        renderScrollbar(frame, sorted.size());
+    }
+
+    private int sortProducer(ProducerInfo a, ProducerInfo b) {
+        int result = switch (sort) {
+            case "status" -> {
+                String sa = a.state != null ? a.state : "";
+                String sb = b.state != null ? b.state : "";
+                yield sa.compareToIgnoreCase(sb);
+            }
+            case "type" -> {
+                String ta = producerType(a);
+                String tb = producerType(b);
+                yield ta.compareToIgnoreCase(tb);
+            }
+            case "uri" -> {
+                String ua = a.uri != null ? a.uri : "";
+                String ub = b.uri != null ? b.uri : "";
+                yield ua.compareToIgnoreCase(ub);
+            }
+            default -> { // "route"
+                String ra = a.routeId != null ? a.routeId : "";
+                String rb = b.routeId != null ? b.routeId : "";
+                yield ra.compareToIgnoreCase(rb);
+            }
+        };
+        return sortReversed ? -result : result;
+    }
+
+    private static String producerType(ProducerInfo pi) {
+        if (pi.className == null) {
+            return "";
+        }
+        String s = pi.className;
+        if (s.endsWith("Producer")) {
+            s = s.substring(0, s.length() - 8);
+        }
+        int dot = s.lastIndexOf('.');
+        return dot >= 0 ? s.substring(dot + 1) : s;
+    }
+
+    @Override
+    public SelectionContext getSelectionContext() {
+        IntegrationInfo info = ctx.findSelectedIntegration();
+        if (info == null || info.producers.isEmpty()) {
+            return null;
+        }
+        List<ProducerInfo> sorted = new ArrayList<>(info.producers);
+        sorted.sort(this::sortProducer);
+        List<String> items = sorted.stream().map(p -> p.routeId != null ? 
p.routeId : "").toList();
+        Integer sel = tableState.selected();
+        return new SelectionContext("table", items, sel != null ? sel : -1, 
items.size(), "Producers");
+    }
+
+    @Override
+    public String description() {
+        return "Producer statistics (output endpoints sending data to external 
systems)";
+    }
+
+    @Override
+    public String getHelpText() {
+        return """
+                # Producers
+
+                Producers are the **output** side of a Camel route. They send 
data to
+                external systems (message brokers, HTTP endpoints, databases, 
files, etc.).
+
+                Unlike consumers (one per route), a route can have multiple 
producers
+                — each `.to()` or `.toD()` call in the route creates a 
producer.
+
+                ## Table Columns
+
+                - **ROUTE** — The route this producer belongs to
+                - **STATUS** — Producer state: `Started` (running normally) or 
`Stopped`
+                - **TYPE** — The Camel component type (e.g., `Kafka`, `Http`, 
`Log`, `Seda`)
+                - **REMOTE** — Whether this producer sends to a remote system 
(`Yes`) or is in-process (`No`)
+                - **URI** — The full endpoint URI (e.g., `kafka://my-topic`, 
`log://mylogger`)
+
+                ## Keys
+
+                - `Up/Down` — select producer
+                - `s` — cycle sort column
+                - `S` — reverse sort order
+                """;
+    }
+
+    @Override
+    public JsonObject getTableDataAsJson() {
+        IntegrationInfo info = ctx.findSelectedIntegration();
+        if (info == null) {
+            return null;
+        }
+        JsonObject result = new JsonObject();
+        result.put("tab", "Producers");
+        JsonArray rows = new JsonArray();
+        List<ProducerInfo> sorted = new ArrayList<>(info.producers);
+        sorted.sort(this::sortProducer);
+        for (ProducerInfo pi : sorted) {
+            JsonObject row = new JsonObject();
+            row.put("routeId", pi.routeId);
+            row.put("uri", pi.uri);
+            row.put("state", pi.state);
+            row.put("className", pi.className);
+            row.put("remote", pi.remote);
+            row.put("singleton", pi.singleton);
+            if (pi.stepId != null) {
+                row.put("stepId", pi.stepId);
+            }
+            rows.add(row);
+        }
+        result.put("rows", rows);
+        result.put("totalRows", info.producers.size());
+        Integer sel = tableState.selected();
+        result.put("selectedIndex", sel != null ? sel : -1);
+        return result;
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteControllerInfo.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteControllerInfo.java
new file mode 100644
index 000000000000..50d9c0296234
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteControllerInfo.java
@@ -0,0 +1,29 @@
+/*
+ * 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;
+
+class RouteControllerInfo {
+    String routeId;
+    String status;
+    String uri;
+    String supervising;
+    long attempts;
+    long lastAttempt;
+    long nextAttempt;
+    long elapsed;
+    String error;
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteControllerTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteControllerTab.java
new file mode 100644
index 000000000000..492cfdb34ccf
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteControllerTab.java
@@ -0,0 +1,293 @@
+/*
+ * 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.List;
+
+import dev.tamboui.layout.Constraint;
+import dev.tamboui.layout.Rect;
+import dev.tamboui.style.Style;
+import dev.tamboui.terminal.Frame;
+import dev.tamboui.text.Span;
+import dev.tamboui.tui.event.KeyCode;
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.tui.event.MouseEvent;
+import dev.tamboui.widgets.block.Block;
+import dev.tamboui.widgets.block.BorderType;
+import dev.tamboui.widgets.block.Borders;
+import dev.tamboui.widgets.table.Cell;
+import dev.tamboui.widgets.table.Row;
+import dev.tamboui.widgets.table.Table;
+import org.apache.camel.util.TimeUtils;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+
+import static org.apache.camel.dsl.jbang.core.commands.tui.TuiHelper.*;
+
+class RouteControllerTab extends AbstractTableTab {
+
+    RouteControllerTab(MonitorContext ctx) {
+        super(ctx, "route", "status", "supervising", "attempts", "uri");
+    }
+
+    @Override
+    public void navigateUp() {
+    }
+
+    @Override
+    public void navigateDown() {
+    }
+
+    @Override
+    public boolean handleKeyEvent(KeyEvent ke) {
+        if (ke.isPageUp() || ke.isKey(KeyCode.PAGE_UP)
+                || ke.isPageDown() || ke.isKey(KeyCode.PAGE_DOWN)
+                || ke.isHome() || ke.isEnd()) {
+            return false;
+        }
+        return super.handleKeyEvent(ke);
+    }
+
+    @Override
+    public boolean handleMouseEvent(MouseEvent me, Rect area) {
+        return false;
+    }
+
+    @Override
+    protected int getRowCount() {
+        IntegrationInfo info = ctx.findSelectedIntegration();
+        return info != null ? info.routeControllerRoutes.size() : 0;
+    }
+
+    @Override
+    protected void renderContent(Frame frame, Rect area, IntegrationInfo info) 
{
+        // Handle non-supervised controller
+        if ("DefaultRouteController".equals(info.routeControllerType)) {
+            renderEmptyState(frame, area, "Route controller: Default (not 
supervised)", " Route Controller ");
+            return;
+        }
+
+        List<RouteControllerInfo> sorted = new 
ArrayList<>(info.routeControllerRoutes);
+        sorted.sort(this::sortRouteController);
+
+        // Handle supervised controller with all routes started successfully
+        if ("SupervisingRouteController".equals(info.routeControllerType)
+                && !info.routeControllerUnhealthy
+                && info.routeControllerRestartingRoutes == 0
+                && info.routeControllerExhaustedRoutes == 0
+                && sorted.stream().allMatch(r -> "Started".equals(r.status))) {
+            renderEmptyState(frame, area, "All routes started successfully", " 
Route Controller (Supervised) ");
+            return;
+        }
+
+        List<Row> rows = new ArrayList<>();
+        for (RouteControllerInfo rc : sorted) {
+            Style statusStyle = "Started".equals(rc.status) ? Theme.success() 
: Theme.error();
+            String supervising = rc.supervising != null ? rc.supervising : "";
+            String attempts = rc.attempts > 0 ? String.valueOf(rc.attempts) : 
"";
+            String lastAttempt = rc.lastAttempt > 0 ? 
TimeUtils.printSince(rc.lastAttempt) : "";
+            String nextAttempt = rc.nextAttempt > 0 ? 
TimeUtils.printSince(rc.nextAttempt) : "";
+            String uriOrError = rc.error != null ? rc.error : (rc.uri != null 
? rc.uri : "");
+            Style uriStyle = rc.error != null ? Theme.error() : Style.EMPTY;
+
+            rows.add(Row.from(
+                    Cell.from(Span.styled(" " + (rc.routeId != null ? 
rc.routeId : ""), Style.EMPTY.fg(Theme.accent()))),
+                    Cell.from(Span.styled(rc.status != null ? rc.status : "", 
statusStyle)),
+                    Cell.from(supervising),
+                    rightCell(attempts, 10),
+                    rightCell(lastAttempt, 12),
+                    rightCell(nextAttempt, 12),
+                    Cell.from(Span.styled(uriOrError, uriStyle))));
+        }
+
+        if (rows.isEmpty()) {
+            rows.add(emptyRow("No routes under supervision", 7));
+        }
+
+        String title = 
"SupervisingRouteController".equals(info.routeControllerType)
+                ? " Route Controller (Supervised) "
+                : " Route Controller ";
+
+        Table table = Table.builder()
+                .rows(rows)
+                .header(Row.from(
+                        Cell.from(Span.styled(" " + sortLabel("ROUTE", 
"route"), sortStyle("route"))),
+                        Cell.from(Span.styled(sortLabel("STATUS", "status"), 
sortStyle("status"))),
+                        Cell.from(Span.styled(sortLabel("SUPERVISING", 
"supervising"), sortStyle("supervising"))),
+                        rightCell(sortLabel("ATTEMPTS", "attempts"), 10, 
sortStyle("attempts")),
+                        rightCell("LAST", 12, Style.EMPTY.bold()),
+                        rightCell("NEXT", 12, Style.EMPTY.bold()),
+                        Cell.from(Span.styled(sortLabel("URI", "uri"), 
sortStyle("uri")))))
+                .widths(
+                        Constraint.length(20),
+                        Constraint.length(10),
+                        Constraint.length(14),
+                        Constraint.length(10),
+                        Constraint.length(12),
+                        Constraint.length(12),
+                        Constraint.fill())
+                
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+                        .title(title).build())
+                .build();
+
+        lastTableArea = area;
+        frame.renderStatefulWidget(table, area, tableState);
+        renderScrollbar(frame, sorted.size());
+    }
+
+    private void renderEmptyState(Frame frame, Rect area, String message, 
String title) {
+        List<Row> rows = new ArrayList<>();
+        rows.add(Row.from(Cell.from(Span.styled(" " + message, 
Theme.muted()))));
+
+        Table table = Table.builder()
+                .rows(rows)
+                .header(Row.from(Cell.from(Span.styled(" INFO", 
Style.EMPTY.bold()))))
+                .widths(Constraint.fill())
+                
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+                        .title(title).build())
+                .build();
+
+        lastTableArea = area;
+        frame.renderStatefulWidget(table, area, tableState);
+    }
+
+    private int sortRouteController(RouteControllerInfo a, RouteControllerInfo 
b) {
+        int result = switch (sort) {
+            case "status" -> {
+                String sa = a.status != null ? a.status : "";
+                String sb = b.status != null ? b.status : "";
+                yield sa.compareToIgnoreCase(sb);
+            }
+            case "supervising" -> {
+                String sa = a.supervising != null ? a.supervising : "";
+                String sb = b.supervising != null ? b.supervising : "";
+                yield sa.compareToIgnoreCase(sb);
+            }
+            case "attempts" -> Long.compare(b.attempts, a.attempts);
+            case "uri" -> {
+                String ua = a.uri != null ? a.uri : "";
+                String ub = b.uri != null ? b.uri : "";
+                yield ua.compareToIgnoreCase(ub);
+            }
+            default -> { // "route"
+                String ra = a.routeId != null ? a.routeId : "";
+                String rb = b.routeId != null ? b.routeId : "";
+                yield ra.compareToIgnoreCase(rb);
+            }
+        };
+        return sortReversed ? -result : result;
+    }
+
+    @Override
+    public SelectionContext getSelectionContext() {
+        IntegrationInfo info = ctx.findSelectedIntegration();
+        if (info == null || info.routeControllerRoutes.isEmpty()) {
+            return null;
+        }
+        List<RouteControllerInfo> sorted = new 
ArrayList<>(info.routeControllerRoutes);
+        sorted.sort(this::sortRouteController);
+        List<String> items = sorted.stream().map(r -> r.routeId != null ? 
r.routeId : "").toList();
+        Integer sel = tableState.selected();
+        return new SelectionContext("table", items, sel != null ? sel : -1, 
items.size(), "Route Controller");
+    }
+
+    @Override
+    public String description() {
+        return "Route controller status (supervised route startup and restart 
attempts)";
+    }
+
+    @Override
+    public String getHelpText() {
+        return """
+                # Route Controller
+
+                Shows the status of the Camel route controller. The route 
controller manages
+                route startup and can automatically restart routes that fail 
to start.
+
+                There are two types of route controllers:
+
+                - **Default** — Routes are started in order and a failure 
stops the context.
+                  This tab shows "Route controller: Default (not supervised)" 
in this case.
+                - **Supervising** — Routes that fail to start are retried with 
exponential
+                  backoff. The controller tracks attempts and manages restart 
scheduling.
+
+                When using the supervised controller and all routes start 
successfully,
+                the tab shows "All routes started successfully".
+
+                ## Table Columns
+
+                - **ROUTE** — The route ID
+                - **STATUS** — Route state: `Started`, `Stopped`, or other 
lifecycle states
+                - **SUPERVISING** — Supervision status (e.g., `Active` when 
the route is being
+                  restarted by the controller)
+                - **ATTEMPTS** — Number of restart attempts so far
+                - **LAST** — Time since the last restart attempt
+                - **NEXT** — Time until the next scheduled restart attempt
+                - **URI** — The route's consumer endpoint URI. If the route 
has a startup error,
+                  the error message is shown here instead (in red)
+
+                ## Keys
+
+                - `Up/Down` — select route
+                - `s` — cycle sort column
+                - `S` — reverse sort order
+                """;
+    }
+
+    @Override
+    public JsonObject getTableDataAsJson() {
+        IntegrationInfo info = ctx.findSelectedIntegration();
+        if (info == null) {
+            return null;
+        }
+        JsonObject result = new JsonObject();
+        result.put("tab", "Route Controller");
+        result.put("controllerType", info.routeControllerType);
+        result.put("unhealthy", info.routeControllerUnhealthy);
+        result.put("restartingRoutes", info.routeControllerRestartingRoutes);
+        result.put("exhaustedRoutes", info.routeControllerExhaustedRoutes);
+        JsonArray rows = new JsonArray();
+        List<RouteControllerInfo> sorted = new 
ArrayList<>(info.routeControllerRoutes);
+        sorted.sort(this::sortRouteController);
+        for (RouteControllerInfo rc : sorted) {
+            JsonObject row = new JsonObject();
+            row.put("routeId", rc.routeId);
+            row.put("status", rc.status);
+            row.put("uri", rc.uri);
+            if (rc.supervising != null) {
+                row.put("supervising", rc.supervising);
+            }
+            row.put("attempts", rc.attempts);
+            if (rc.lastAttempt > 0) {
+                row.put("lastAttempt", rc.lastAttempt);
+            }
+            if (rc.nextAttempt > 0) {
+                row.put("nextAttempt", rc.nextAttempt);
+            }
+            if (rc.error != null) {
+                row.put("error", rc.error);
+            }
+            rows.add(row);
+        }
+        result.put("rows", rows);
+        result.put("totalRows", info.routeControllerRoutes.size());
+        Integer sel = tableState.selected();
+        result.put("selectedIndex", sel != null ? sel : -1);
+        return result;
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTab.java
index ad5e22996c01..6590530ad796 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTab.java
@@ -1812,16 +1812,15 @@ class RoutesTab extends AbstractTab {
             detailScroll = 0;
         }
 
-        if (drillDownRouteId != null && 
!drillDownRouteId.equals(cachedRouteDetailId)
-                && !drillDownRouteId.equals(detailLoadingRouteId)) {
-            detailLoadingRouteId = drillDownRouteId;
+        if (drillDownRouteId != null && cachedRouteDetail == null
+                && !"*".equals(detailLoadingRouteId)) {
+            detailLoadingRouteId = "*";
             detailLoading = true;
-            String rid = drillDownRouteId;
             if (ctx.runner != null) {
                 ctx.backgroundExecutor.execute(() -> {
-                    JsonObject result = requestRouteProcessorDetail(rid);
+                    JsonObject result = requestRouteProcessorDetail("*");
                     cachedRouteDetail = result;
-                    cachedRouteDetailId = rid;
+                    cachedRouteDetailId = "*";
                     detailLoading = false;
                 });
             }
@@ -2009,7 +2008,7 @@ class RoutesTab extends AbstractTab {
         try {
             JsonObject root = new JsonObject();
             root.put("action", "processor-detail");
-            root.put("routeId", routeId);
+            root.put("routeId", "*");
             return ctx.executeAction(ctx.selectedPid, root, 5000);
         } catch (Exception e) {
             return null;
@@ -2020,12 +2019,7 @@ class RoutesTab extends AbstractTab {
         if (cachedRouteDetail == null || nodeId == null) {
             return null;
         }
-        JsonArray processors = (JsonArray) cachedRouteDetail.get("processors");
-        if (processors == null) {
-            return null;
-        }
-        for (Object obj : processors) {
-            JsonObject p = (JsonObject) obj;
+        for (JsonObject p : getAllProcessors(cachedRouteDetail)) {
             if (nodeId.equals(p.getString("id"))) {
                 return p;
             }
@@ -2033,6 +2027,35 @@ class RoutesTab extends AbstractTab {
         return null;
     }
 
+    static List<JsonObject> getAllProcessors(JsonObject routeDetail) {
+        if (routeDetail == null) {
+            return List.of();
+        }
+        JsonArray routes = (JsonArray) routeDetail.get("routes");
+        if (routes != null) {
+            List<JsonObject> all = new ArrayList<>();
+            for (Object obj : routes) {
+                JsonObject route = (JsonObject) obj;
+                JsonArray procs = (JsonArray) route.get("processors");
+                if (procs != null) {
+                    for (Object p : procs) {
+                        all.add((JsonObject) p);
+                    }
+                }
+            }
+            return all;
+        }
+        JsonArray procs = (JsonArray) routeDetail.get("processors");
+        if (procs != null) {
+            List<JsonObject> all = new ArrayList<>();
+            for (Object p : procs) {
+                all.add((JsonObject) p);
+            }
+            return all;
+        }
+        return List.of();
+    }
+
     // ---- Quick doc (q toggle in source viewer) ----
 
     private Map<Integer, List<String>> provideAllQuickDocs(List<JsonObject> 
cd) {
@@ -2040,8 +2063,8 @@ class RoutesTab extends AbstractTab {
             return Map.of();
         }
 
-        JsonArray processors = (JsonArray) cachedRouteDetail.get("processors");
-        if (processors == null || processors.isEmpty()) {
+        List<JsonObject> processors = getAllProcessors(cachedRouteDetail);
+        if (processors.isEmpty()) {
             return Map.of();
         }
 
@@ -2049,8 +2072,7 @@ class RoutesTab extends AbstractTab {
         CamelCatalog catalog = info != null ? getCatalog(info) : null;
 
         Map<Integer, List<String>> result = new LinkedHashMap<>();
-        for (Object obj : processors) {
-            JsonObject proc = (JsonObject) obj;
+        for (JsonObject proc : processors) {
             Integer line = proc.getInteger("line");
             if (line == null || line <= 0) {
                 continue;
@@ -2074,14 +2096,14 @@ class RoutesTab extends AbstractTab {
 
     private void ensureProcessorDetailLoaded(String routeId) {
         if (routeId != null && cachedRouteDetail == null
-                && !routeId.equals(detailLoadingRouteId)) {
-            detailLoadingRouteId = routeId;
+                && !"*".equals(detailLoadingRouteId)) {
+            detailLoadingRouteId = "*";
             detailLoading = true;
             if (ctx.runner != null) {
                 ctx.backgroundExecutor.execute(() -> {
-                    JsonObject result = requestRouteProcessorDetail(routeId);
+                    JsonObject result = requestRouteProcessorDetail("*");
                     cachedRouteDetail = result;
-                    cachedRouteDetailId = routeId;
+                    cachedRouteDetailId = "*";
                     detailLoading = false;
                 });
             }
@@ -2148,10 +2170,31 @@ class RoutesTab extends AbstractTab {
             CamelCatalog catalog, String type, JsonObject opts, int eipIdx) {
 
         EipModel model = catalog != null ? catalog.eipModel(type) : null;
+
+        // For endpoint-bearing EIPs, resolve the component from the uri option
+        ComponentModel compModel = null;
+        if (opts != null && catalog != null) {
+            Object uriObj = opts.get("uri");
+            if (uriObj != null) {
+                String uri = uriObj.toString();
+                String comp = uri.contains(":") ? uri.substring(0, 
uri.indexOf(':')) : uri;
+                compModel = catalog.componentModel(comp);
+            }
+        }
+
         List<String> titleLines = new ArrayList<>();
         if (model != null && model.getTitle() != null) {
-            String desc = model.getDescription() != null ? 
truncateText(model.getDescription(), 80) : "";
-            titleLines.add(model.getTitle() + " — " + desc);
+            String eipTitle = model.getTitle();
+            if (compModel != null && compModel.getTitle() != null) {
+                eipTitle += " (" + compModel.getTitle() + ")";
+            }
+            String desc;
+            if (compModel != null && compModel.getDescription() != null) {
+                desc = truncateText(compModel.getDescription(), 80);
+            } else {
+                desc = model.getDescription() != null ? 
truncateText(model.getDescription(), 80) : "";
+            }
+            titleLines.add(eipTitle + " — " + desc);
         } else {
             titleLines.add(type);
         }
@@ -2166,20 +2209,31 @@ class RoutesTab extends AbstractTab {
                     optionDocs.put(opt.getName(), opt);
                 }
             }
+            if (compModel != null) {
+                for (ComponentModel.EndpointOptionModel opt : 
compModel.getEndpointOptions()) {
+                    if (opt.getName() != null && 
!optionDocs.containsKey(opt.getName())) {
+                        optionDocs.put(opt.getName(), opt);
+                    }
+                }
+            }
             inlineParameterDocs(result, cd, eipIdx, optionDocs);
 
             // For Java/XML where options are on the same line,
             // cluster the option docs under the title
             if (result.size() == beforeSize + 1 && opts != null) {
-                clusterEipOptions(titleLines, opts, optionDocs);
+                boolean hasParams = hasParametersChild(cd, eipIdx);
+                clusterEipOptions(titleLines, opts, optionDocs, hasParams);
             }
         }
     }
 
     private static void clusterEipOptions(
             List<String> docLines, JsonObject opts,
-            Map<String, BaseOptionModel> optionDocs) {
+            Map<String, BaseOptionModel> optionDocs, boolean skipUri) {
         for (Map.Entry<String, Object> entry : opts.entrySet()) {
+            if (skipUri && "uri".equals(entry.getKey())) {
+                continue;
+            }
             BaseOptionModel optModel = optionDocs.get(entry.getKey());
             if (optModel != null) {
                 String optDoc = formatOptionDoc(optModel);
@@ -2190,6 +2244,22 @@ class RoutesTab extends AbstractTab {
         }
     }
 
+    static boolean hasParametersChild(List<JsonObject> cd, int eipIdx) {
+        int eipIndent = lineIndent(cd, eipIdx);
+        for (int i = eipIdx + 1; i < cd.size(); i++) {
+            String code = cd.get(i).get("code") != null ? 
cd.get(i).get("code").toString() : "";
+            int indent = leadingSpaces(code);
+            if (indent < eipIndent && !code.isBlank()) {
+                break;
+            }
+            String trimmed = code.stripLeading();
+            if (trimmed.startsWith("parameters:")) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     static void inlineParameterDocs(
             Map<Integer, List<String>> result, List<JsonObject> cd,
             int eipIdx, Map<String, BaseOptionModel> optionDocs) {
@@ -2200,6 +2270,9 @@ class RoutesTab extends AbstractTab {
 
         int eipIndent = lineIndent(cd, eipIdx);
         int childIndent = -1;
+        boolean inParameters = false;
+        int paramIndent = -1;
+        int parametersLineIndent = -1;
 
         for (int i = eipIdx + 1; i < cd.size(); i++) {
             String code = cd.get(i).get("code") != null ? 
cd.get(i).get("code").toString() : "";
@@ -2210,6 +2283,35 @@ class RoutesTab extends AbstractTab {
             if (childIndent < 0 && indent > eipIndent) {
                 childIndent = indent;
             }
+
+            if (inParameters) {
+                if (paramIndent < 0 && indent > parametersLineIndent && 
!code.isBlank()) {
+                    paramIndent = indent;
+                }
+                if (paramIndent > 0 && indent <= parametersLineIndent && 
!code.isBlank()) {
+                    inParameters = false;
+                    paramIndent = -1;
+                    parametersLineIndent = -1;
+                } else if (paramIndent > 0 && indent == paramIndent) {
+                    String trimmed = code.stripLeading();
+                    int colon = trimmed.indexOf(':');
+                    if (colon > 0) {
+                        String key = trimmed.substring(0, colon).strip();
+                        BaseOptionModel doc = optionDocs.get(key);
+                        if (doc != null) {
+                            String docLine = formatOptionDoc(doc);
+                            if (docLine != null) {
+                                int docIdx = lastContinuationLine(cd, i, 
indent);
+                                result.put(docIdx, List.of(docLine));
+                            }
+                        }
+                    }
+                    continue;
+                } else {
+                    continue;
+                }
+            }
+
             if (childIndent > 0 && indent != childIndent) {
                 continue;
             }
@@ -2219,7 +2321,12 @@ class RoutesTab extends AbstractTab {
                 continue;
             }
             String key = trimmed.substring(0, colon).strip();
-            if ("parameters".equals(key) || "uri".equals(key) || 
"steps".equals(key)
+            if ("parameters".equals(key)) {
+                inParameters = true;
+                parametersLineIndent = indent;
+                continue;
+            }
+            if ("uri".equals(key) || "steps".equals(key)
                     || "id".equals(key) || "description".equals(key)) {
                 continue;
             }
@@ -2228,12 +2335,29 @@ class RoutesTab extends AbstractTab {
             if (doc != null) {
                 String docLine = formatOptionDoc(doc);
                 if (docLine != null) {
-                    result.put(i, List.of(docLine));
+                    int docIdx = lastContinuationLine(cd, i, indent);
+                    result.put(docIdx, List.of(docLine));
                 }
             }
         }
     }
 
+    static int lastContinuationLine(List<JsonObject> cd, int keyIdx, int 
keyIndent) {
+        int last = keyIdx;
+        for (int j = keyIdx + 1; j < cd.size(); j++) {
+            String c = cd.get(j).get("code") != null ? 
cd.get(j).get("code").toString() : "";
+            if (c.isBlank()) {
+                continue;
+            }
+            if (leadingSpaces(c) > keyIndent) {
+                last = j;
+            } else {
+                break;
+            }
+        }
+        return last;
+    }
+
     static String formatOptionDoc(BaseOptionModel doc) {
         StringBuilder sb = new StringBuilder();
         if (doc.getDescription() != null && !doc.getDescription().isEmpty()) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewer.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewer.java
index c685be6ed7fe..83bf0e67da04 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewer.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewer.java
@@ -363,7 +363,6 @@ class SourceViewer {
 
         int visibleLines = inner.height();
         lastVisibleLines = visibleLines;
-        int maxScroll = Math.max(0, lines.size() - visibleLines);
 
         // On initial load, position selected line at 2/3 of viewport
         if (pendingScroll && selectedLine >= 0) {
@@ -372,14 +371,37 @@ class SourceViewer {
             pendingScroll = false;
         }
 
-        // Auto-scroll to keep selected line visible
+        // Auto-scroll to keep selected line visible (accounting for inline 
doc lines)
         if (selectedLine >= 0) {
             if (selectedLine < scrollY) {
                 scrollY = selectedLine;
+            } else if (quickDocEnabled && !quickDocEntries.isEmpty()) {
+                while (scrollY < selectedLine && countVisualRows(scrollY, 
selectedLine) + 1 > visibleLines) {
+                    scrollY++;
+                }
             } else if (selectedLine >= scrollY + visibleLines) {
                 scrollY = selectedLine - visibleLines + 1;
             }
         }
+
+        int maxScroll;
+        if (quickDocEnabled && !quickDocEntries.isEmpty()) {
+            maxScroll = 0;
+            int visualFromEnd = 0;
+            for (int i = lines.size() - 1; i >= 0; i--) {
+                visualFromEnd++;
+                List<String> docs = quickDocEntries.get(i);
+                if (docs != null) {
+                    visualFromEnd += docs.size();
+                }
+                if (visualFromEnd >= visibleLines) {
+                    maxScroll = i;
+                    break;
+                }
+            }
+        } else {
+            maxScroll = Math.max(0, lines.size() - visibleLines);
+        }
         scrollY = Math.min(scrollY, maxScroll);
 
         int hSkip = wordWrap ? 0 : scrollX;
@@ -1089,6 +1111,20 @@ class SourceViewer {
         return result;
     }
 
+    private int countVisualRows(int fromLine, int toLine) {
+        int count = 0;
+        for (int i = fromLine; i < toLine && i < lines.size(); i++) {
+            count++;
+            if (quickDocEnabled) {
+                List<String> docs = quickDocEntries.get(i);
+                if (docs != null) {
+                    count += docs.size();
+                }
+            }
+        }
+        return count;
+    }
+
     private static String objToString(Object o) {
         return o != null ? o.toString() : "";
     }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParser.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParser.java
index f0a3be50cd7a..db04860676c5 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParser.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParser.java
@@ -326,6 +326,60 @@ final class StatusParser {
             }
         }
 
+        // Parse producers
+        JsonObject producersObj = (JsonObject) root.get("producers");
+        if (producersObj != null) {
+            JsonArray producerList = (JsonArray) producersObj.get("producers");
+            if (producerList != null) {
+                for (Object p : producerList) {
+                    JsonObject pj = (JsonObject) p;
+                    ProducerInfo pi = new ProducerInfo();
+                    pi.uri = pj.getString("uri");
+                    pi.state = pj.getString("state");
+                    pi.className = pj.getString("class");
+                    pi.routeId = pj.getString("routeId");
+                    pi.stepId = pj.getString("stepId");
+                    pi.remote = Boolean.TRUE.equals(pj.get("remote"));
+                    pi.singleton = Boolean.TRUE.equals(pj.get("singleton"));
+                    info.producers.add(pi);
+                }
+            }
+        }
+
+        // Parse events
+        JsonObject eventsObj = (JsonObject) root.get("events");
+        if (eventsObj != null) {
+            parseEventArray(eventsObj, "events", "general", info);
+            parseEventArray(eventsObj, "routeEvents", "route", info);
+            parseEventArray(eventsObj, "exchangeEvents", "exchange", info);
+        }
+
+        // Parse route controller
+        JsonObject rcObj = (JsonObject) root.get("routeController");
+        if (rcObj != null) {
+            info.routeControllerType = rcObj.getString("controller");
+            info.routeControllerUnhealthy = 
Boolean.TRUE.equals(rcObj.get("unhealthyRoutes"));
+            info.routeControllerRestartingRoutes = 
rcObj.getIntegerOrDefault("restartingRoutes", 0);
+            info.routeControllerExhaustedRoutes = 
rcObj.getIntegerOrDefault("exhaustedRoutes", 0);
+            JsonArray rcRoutes = (JsonArray) rcObj.get("routes");
+            if (rcRoutes != null) {
+                for (Object r : rcRoutes) {
+                    JsonObject rj = (JsonObject) r;
+                    RouteControllerInfo rc = new RouteControllerInfo();
+                    rc.routeId = rj.getString("routeId");
+                    rc.status = rj.getString("status");
+                    rc.uri = rj.getString("uri");
+                    rc.supervising = rj.getString("supervising");
+                    rc.attempts = rj.getLongOrDefault("attempts", 0);
+                    rc.lastAttempt = rj.getLongOrDefault("lastAttempt", 0);
+                    rc.nextAttempt = rj.getLongOrDefault("nextAttempt", 0);
+                    rc.elapsed = rj.getLongOrDefault("elapsed", 0);
+                    rc.error = rj.getString("error");
+                    info.routeControllerRoutes.add(rc);
+                }
+            }
+        }
+
         // Parse endpoints (top-level "endpoints" is a JsonObject with nested 
"endpoints" array)
         JsonObject endpointsObj = (JsonObject) root.get("endpoints");
         if (endpointsObj != null) {
@@ -1220,4 +1274,20 @@ final class StatusParser {
     static long objToLong(Object o) {
         return TuiHelper.objToLong(o);
     }
+
+    private static void parseEventArray(JsonObject eventsObj, String arrayKey, 
String category, IntegrationInfo info) {
+        JsonArray arr = (JsonArray) eventsObj.get(arrayKey);
+        if (arr != null) {
+            for (Object e : arr) {
+                JsonObject ej = (JsonObject) e;
+                EventInfo ei = new EventInfo();
+                ei.type = ej.getString("type");
+                ei.category = category;
+                ei.timestamp = ej.getLongOrDefault("timestamp", 0);
+                ei.exchangeId = ej.getString("exchangeId");
+                ei.message = ej.getString("message");
+                info.events.add(ei);
+            }
+        }
+    }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
index ab90e81eece4..0fdc0a31873c 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
@@ -70,6 +70,9 @@ class TabRegistry {
     private DiagramTab diagramTab;
     private RoutesTab routesTab;
     private ConsumersTab consumersTab;
+    private ProducersTab producersTab;
+    private EventTab eventTab;
+    private RouteControllerTab routeControllerTab;
     private EndpointsTab endpointsTab;
     private HttpTab httpTab;
     private HealthTab healthTab;
@@ -115,6 +118,9 @@ class TabRegistry {
         diagramTab = new DiagramTab(ctx);
         routesTab = new RoutesTab(ctx);
         consumersTab = new ConsumersTab(ctx);
+        producersTab = new ProducersTab(ctx);
+        eventTab = new EventTab(ctx);
+        routeControllerTab = new RouteControllerTab(ctx);
         kafkaTab = new KafkaTab(ctx);
         dataSourceTab = new DataSourceTab(ctx);
         heapHistogramTab = new HeapHistogramTab(ctx);
@@ -160,9 +166,14 @@ class TabRegistry {
                 new MoreTab(TuiIcons.TAB_CIRCUIT_BREAKER, "Circuit Breaker", 
"&Circuit Breaker", circuitBreakerTab, "Routing"),
                 new MoreTab(TuiIcons.TAB_CONSUMERS, "Consumers", "Co&nsumers", 
consumersTab, "Routing"),
                 new MoreTab(TuiIcons.TAB_INFLIGHT, "Inflight", "In&flight", 
inflightTab, "Routing"),
+                new MoreTab(TuiIcons.TAB_PRODUCERS, "Producers", "Prod&ucers", 
producersTab, "Routing"),
+                new MoreTab(
+                        TuiIcons.TAB_ROUTE_CONTROLLER, "Route Controller", 
"Route Organi&zer", routeControllerTab,
+                        "Routing"),
                 // Observability
                 new MoreTab(TuiIcons.TAB_HEALTH, "Health", "H&ealth", 
healthTab, "Observability"),
                 new MoreTab(TuiIcons.TAB_METRICS, "Metrics", "Metr&ics", 
metricsTab, "Observability"),
+                new MoreTab(TuiIcons.TAB_EVENTS, "Events", "E&xchange Events", 
eventTab, "Observability"),
                 new MoreTab(TuiIcons.TAB_SPANS, "Spans", "&OTel Spans", 
spansTab, "Observability"),
                 // Data
                 new MoreTab(TuiIcons.TAB_DATASOURCE, "JDBC DataSource", "&JDBC 
DataSource", dataSourceTab, "Data"),
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIcons.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIcons.java
index 395d0a118149..ad41009894f1 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIcons.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIcons.java
@@ -146,6 +146,9 @@ final class TuiIcons {
     static final String TAB_PROCESS = CLIPBOARD;
     static final String TAB_STARTUP = QUARKUS;
     static final String TAB_THREADS = "🧵";
+    static final String TAB_PRODUCERS = "📤";
+    static final String TAB_EVENTS = "📣";
+    static final String TAB_ROUTE_CONTROLLER = "🚦";
 
     /** Icons for {@link TabRegistry#TAB_OVERVIEW}..{@link 
TabRegistry#TAB_MORE} (in order). */
     static final List<String> PRIMARY_TAB_ICONS = List.of(
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistryTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistryTest.java
index e4076c785e7b..86d153cac9c6 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistryTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistryTest.java
@@ -82,8 +82,8 @@ class TabRegistryTest {
     }
 
     @Test
-    void moreTabsHasTwentyTwoEntries() {
-        assertEquals(23, registry.moreTabs().size());
+    void moreTabsHasTwentySixEntries() {
+        assertEquals(26, registry.moreTabs().size());
     }
 
     @Test
@@ -109,8 +109,8 @@ class TabRegistryTest {
         // MORE_SHORTCUTS array carried before the MoreTab refactor. A label 
edit that repoints a key must fail here.
         List<Character> shortcuts = 
registry.moreTabs().stream().map(TabRegistry.MoreTab::shortcut).toList();
         assertEquals(
-                List.of('W', 'C', 'N', 'F', 'E', 'I', 'O', 'J', 'K', 'Q', 'R', 
'A', 'H', 'M', 'Y', 'P', 'S', 'T', 'B', 'L',
-                        'G', 'V', 'D'),
+                List.of('W', 'C', 'N', 'F', 'U', 'Z', 'E', 'I', 'X', 'O', 'J', 
'K', 'Q', 'R', 'A', 'H', 'M', 'Y', 'P', 'S',
+                        'T', 'B', 'L', 'G', 'V', 'D'),
                 shortcuts, "More tab shortcut letters must match the 
historical sequence");
     }
 

Reply via email to