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 ae66a7e04efe camel-jbang - Routes tab: remap keys, add detail panel 
and go-to popup
ae66a7e04efe is described below

commit ae66a7e04efee838f77e73cfb278c7e618897490
Author: Claus Ibsen <[email protected]>
AuthorDate: Fri Jul 24 13:12:39 2026 +0200

    camel-jbang - Routes tab: remap keys, add detail panel and go-to popup
    
    Enter in table mode now opens route diagram directly. In drill-down
    mode, d toggles a detail panel showing EIP/component catalog docs,
    and g opens a fuzzy-search popup to jump to any node in the route.
    
    camel-jbang - Human-readable duration formatting and go-to node popup 
improvements
    
    Format processing time values (mean/max/min/last/delta/p50/p95/p99) as
    human-readable durations (e.g. 146442ms -> 2m26s) across RoutesTab,
    DiagramTab, and OverviewTab. Add go-to node popup (g key) to DiagramTab
    drill-down mode. Fix GotoNodePopup to pre-select current node on open,
    and prevent Esc/mouse-scroll from leaking through the popup overlay.
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../dsl/jbang/core/commands/tui/DiagramTab.java    |  70 ++-
 .../dsl/jbang/core/commands/tui/GotoNodePopup.java | 261 +++++++++++
 .../dsl/jbang/core/commands/tui/OverviewTab.java   |  54 +--
 .../dsl/jbang/core/commands/tui/RoutesTab.java     | 488 ++++++++++++++++++---
 .../dsl/jbang/core/commands/tui/TuiHelper.java     |  23 +
 .../core/commands/tui/RoutesTabRenderTest.java     |   2 +-
 6 files changed, 783 insertions(+), 115 deletions(-)

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 15eca4f9c1c2..eb1220176bf6 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
@@ -63,6 +63,7 @@ class DiagramTab extends AbstractTab {
 
     private final DiagramSupport diagram = new DiagramSupport();
     private final SourceViewer sourceViewer = new SourceViewer();
+    private final GotoNodePopup gotoNodePopup = new GotoNodePopup();
     private boolean diagramMetrics = true;
     private static final String[] EXTERNAL_LABELS = { " [off]", " [edges]", " 
[all]" };
     private int externalMode;
@@ -92,8 +93,24 @@ class DiagramTab extends AbstractTab {
         return diagram.isShowDiagram();
     }
 
+    @Override
+    public boolean isOverlayActive() {
+        return sourceViewer.isVisible() || gotoNodePopup.isVisible();
+    }
+
     @Override
     public boolean handleKeyEvent(KeyEvent ke) {
+        // Go-to node popup (takes priority when active)
+        if (gotoNodePopup.isVisible()) {
+            gotoNodePopup.handleKeyEvent(ke);
+            int sel = gotoNodePopup.consumeSelectedIndex();
+            if (sel >= 0) {
+                diagram.setSelectedEipNodeIndex(sel);
+                diagram.scrollToSelectedEipNode();
+            }
+            return true;
+        }
+
         // Source view scrolling (takes priority when active)
         if (sourceViewer.handleKeyEvent(ke)) {
             return true;
@@ -192,6 +209,13 @@ class DiagramTab extends AbstractTab {
             return true;
         }
 
+        // Go-to node popup in drill-down mode
+        if (!topologyMode && diagram.isShowDiagram() && 
!diagram.getEipNodeBoxes().isEmpty()
+                && ke.isChar('g')) {
+            gotoNodePopup.open(diagram.getEipNodeBoxes(), 
diagram.getSelectedEipNodeIndex());
+            return true;
+        }
+
         // Detail panel scroll
         if (!topologyMode && detailMode && diagram.isShowDiagram()) {
             if (ke.isPageUp() || ke.isKey(KeyCode.PAGE_UP)) {
@@ -306,6 +330,9 @@ class DiagramTab extends AbstractTab {
 
     @Override
     public boolean handleMouseEvent(MouseEvent me, Rect area) {
+        if (gotoNodePopup.isVisible()) {
+            return true;
+        }
         if (detailMode && lastDetailArea != null && 
lastDetailArea.contains(me.x(), me.y())) {
             if (me.kind() == MouseEventKind.SCROLL_UP) {
                 detailScroll = Math.max(0, detailScroll - 3);
@@ -346,6 +373,10 @@ class DiagramTab extends AbstractTab {
 
     @Override
     public boolean handleEscape() {
+        if (gotoNodePopup.isVisible()) {
+            gotoNodePopup.close();
+            return true;
+        }
         if (sourceViewer.isVisible()) {
             sourceViewer.hide();
             return true;
@@ -503,6 +534,11 @@ class DiagramTab extends AbstractTab {
                                 frame, area, title, diagramMetrics, 
drillDownRouteId, routeLayout);
                     }
                 }
+
+                // Render go-to popup overlay
+                if (gotoNodePopup.isVisible()) {
+                    gotoNodePopup.render(frame, area);
+                }
                 return;
             }
         }
@@ -575,28 +611,26 @@ class DiagramTab extends AbstractTab {
 
             lines.add(Line.from(Span.raw("")));
             if (route.total > 0) {
-                int tw = numWidth(route.meanTime, route.maxTime, 
route.minTime);
                 lines.add(Line.from(
                         Span.styled(" Mean: ", Theme.muted()),
-                        Span.raw(String.format("%" + tw + "d ms", 
route.meanTime))));
+                        Span.raw(formatDurationMs(route.meanTime))));
                 lines.add(Line.from(
                         Span.styled(" Max:  ", Theme.muted()),
-                        Span.raw(String.format("%" + tw + "d ms", 
route.maxTime))));
+                        Span.raw(formatDurationMs(route.maxTime))));
                 lines.add(Line.from(
                         Span.styled(" Min:  ", Theme.muted()),
-                        Span.raw(String.format("%" + tw + "d ms", 
route.minTime))));
+                        Span.raw(formatDurationMs(route.minTime))));
                 if (route.p50Time >= 0) {
-                    int pw = numWidth(route.p50Time, route.p95Time, 
route.p99Time);
                     lines.add(Line.from(Span.raw("")));
                     lines.add(Line.from(
                             Span.styled(" p50:  ", Theme.muted()),
-                            Span.raw(String.format("%" + pw + "d ms", 
route.p50Time))));
+                            Span.raw(formatDurationMs(route.p50Time))));
                     lines.add(Line.from(
                             Span.styled(" p95:  ", Theme.muted()),
-                            Span.raw(String.format("%" + pw + "d ms", 
route.p95Time))));
+                            Span.raw(formatDurationMs(route.p95Time))));
                     lines.add(Line.from(
                             Span.styled(" p99:  ", Theme.muted()),
-                            Span.raw(String.format("%" + pw + "d ms", 
route.p99Time))));
+                            Span.raw(formatDurationMs(route.p99Time))));
                     lines.add(buildPercentileBarLine(
                             route.p50Time, route.p95Time, route.p99Time, 
area.width() - 3));
                 }
@@ -739,33 +773,29 @@ class DiagramTab extends AbstractTab {
 
                 if (stat.exchangesTotal > 0) {
                     lines.add(Line.from(Span.raw("")));
-                    int tw = numWidth(stat.meanProcessingTime, 
stat.maxProcessingTime,
-                            stat.minProcessingTime, stat.lastProcessingTime);
                     lines.add(Line.from(
                             Span.styled(" Mean: ", Theme.muted()),
-                            Span.raw(String.format("%" + tw + "d ms", 
stat.meanProcessingTime))));
+                            
Span.raw(formatDurationMs(stat.meanProcessingTime))));
                     lines.add(Line.from(
                             Span.styled(" Max:  ", Theme.muted()),
-                            Span.raw(String.format("%" + tw + "d ms", 
stat.maxProcessingTime))));
+                            
Span.raw(formatDurationMs(stat.maxProcessingTime))));
                     lines.add(Line.from(
                             Span.styled(" Min:  ", Theme.muted()),
-                            Span.raw(String.format("%" + tw + "d ms", 
stat.minProcessingTime))));
+                            
Span.raw(formatDurationMs(stat.minProcessingTime))));
                     lines.add(Line.from(
                             Span.styled(" Last: ", Theme.muted()),
-                            Span.raw(String.format("%" + tw + "d ms", 
stat.lastProcessingTime))));
+                            
Span.raw(formatDurationMs(stat.lastProcessingTime))));
                     if (stat.p50ProcessingTime >= 0) {
-                        int pw = numWidth(stat.p50ProcessingTime, 
stat.p95ProcessingTime,
-                                stat.p99ProcessingTime);
                         lines.add(Line.from(Span.raw("")));
                         lines.add(Line.from(
                                 Span.styled(" p50:  ", Theme.muted()),
-                                Span.raw(String.format("%" + pw + "d ms", 
stat.p50ProcessingTime))));
+                                
Span.raw(formatDurationMs(stat.p50ProcessingTime))));
                         lines.add(Line.from(
                                 Span.styled(" p95:  ", Theme.muted()),
-                                Span.raw(String.format("%" + pw + "d ms", 
stat.p95ProcessingTime))));
+                                
Span.raw(formatDurationMs(stat.p95ProcessingTime))));
                         lines.add(Line.from(
                                 Span.styled(" p99:  ", Theme.muted()),
-                                Span.raw(String.format("%" + pw + "d ms", 
stat.p99ProcessingTime))));
+                                
Span.raw(formatDurationMs(stat.p99ProcessingTime))));
                         lines.add(buildPercentileBarLine(
                                 stat.p50ProcessingTime, stat.p95ProcessingTime,
                                 stat.p99ProcessingTime, area.width() - 3));
@@ -818,6 +848,7 @@ class DiagramTab extends AbstractTab {
                 hint(spans, "PgUp/PgDn", "page");
                 hint(spans, "c", "source");
                 hint(spans, "d", "detail" + (detailMode ? " [on]" : " [off]"));
+                hint(spans, "g", "go to");
             } else if (!topologyMode) {
                 hint(spans, "Esc", "back");
                 hint(spans, "t", "topology");
@@ -994,6 +1025,7 @@ class DiagramTab extends AbstractTab {
                 - `Enter` — jump to linked route (when `↵` indicator shown)
                 - `c` — show source code at selected node
                 - `d` — toggle EIP detail panel (shows configured options)
+                - `g` — go to node (fuzzy search popup)
                 - `Esc` — go back (previous route or topology)
                 - `t` — jump back to topology view
 
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/GotoNodePopup.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/GotoNodePopup.java
new file mode 100644
index 000000000000..076b283b881b
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/GotoNodePopup.java
@@ -0,0 +1,261 @@
+/*
+ * 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.Rect;
+import dev.tamboui.style.Style;
+import dev.tamboui.terminal.Frame;
+import dev.tamboui.text.Line;
+import dev.tamboui.text.Span;
+import dev.tamboui.tui.event.KeyCode;
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.widgets.Clear;
+import dev.tamboui.widgets.block.Block;
+import dev.tamboui.widgets.block.BorderType;
+import dev.tamboui.widgets.block.Borders;
+import dev.tamboui.widgets.list.ListItem;
+import dev.tamboui.widgets.list.ListState;
+import dev.tamboui.widgets.list.ListWidget;
+import dev.tamboui.widgets.list.ScrollMode;
+import dev.tamboui.widgets.scrollbar.Scrollbar;
+import dev.tamboui.widgets.scrollbar.ScrollbarState;
+import org.apache.camel.dsl.jbang.core.commands.tui.diagram.RouteDiagramWidget;
+
+class GotoNodePopup {
+
+    private boolean visible;
+    private final FuzzyFilter filter = new FuzzyFilter();
+    private final ListState listState = new ListState();
+    private final ScrollbarState scrollbarState = new ScrollbarState();
+    private List<NodeEntry> allEntries;
+    private List<NodeEntry> filteredEntries;
+    private int selectedIndex = -1;
+
+    record NodeEntry(int index, String type, String label, String nodeId) {
+    }
+
+    boolean isVisible() {
+        return visible;
+    }
+
+    void open(List<RouteDiagramWidget.EipNodeBox> eipNodeBoxes, int 
currentNodeIndex) {
+        allEntries = new ArrayList<>();
+        for (int i = 0; i < eipNodeBoxes.size(); i++) {
+            var box = eipNodeBoxes.get(i);
+            var ln = box.layoutNode();
+            String type = ln != null ? ln.type : box.type();
+            String label = "";
+            if (ln != null && ln.wrappedLines != null && 
!ln.wrappedLines.isEmpty()) {
+                label = String.join(" ", ln.wrappedLines);
+            }
+            String nodeId = box.nodeId() != null ? box.nodeId() : (ln != null 
? ln.id : "");
+            allEntries.add(new NodeEntry(i, type != null ? type : "", label, 
nodeId != null ? nodeId : ""));
+        }
+        visible = true;
+        filter.clearFilter();
+        rebuildList();
+        if (currentNodeIndex >= 0 && currentNodeIndex < allEntries.size()) {
+            listState.select(currentNodeIndex);
+        }
+    }
+
+    void close() {
+        visible = false;
+        filter.clearFilter();
+    }
+
+    int consumeSelectedIndex() {
+        int idx = selectedIndex;
+        selectedIndex = -1;
+        return idx;
+    }
+
+    boolean handleKeyEvent(KeyEvent ke) {
+        int size = filteredEntries != null ? filteredEntries.size() : 0;
+        if (ke.isCancel()) {
+            close();
+            return true;
+        }
+        if (ke.isUp()) {
+            listState.selectPrevious();
+            return true;
+        }
+        if (ke.isDown()) {
+            listState.selectNext(size);
+            return true;
+        }
+        if (ke.isPageUp() || ke.isKey(KeyCode.PAGE_UP)) {
+            for (int i = 0; i < 5; i++) {
+                listState.selectPrevious();
+            }
+            return true;
+        }
+        if (ke.isPageDown() || ke.isKey(KeyCode.PAGE_DOWN)) {
+            for (int i = 0; i < 5; i++) {
+                listState.selectNext(size);
+            }
+            return true;
+        }
+        if (ke.isHome() || ke.isKey(KeyCode.HOME)) {
+            listState.selectFirst();
+            return true;
+        }
+        if (ke.isEnd() || ke.isKey(KeyCode.END)) {
+            listState.selectLast(size);
+            return true;
+        }
+        if (ke.isConfirm()) {
+            Integer sel = listState.selected();
+            if (sel != null && filteredEntries != null && sel < 
filteredEntries.size()) {
+                NodeEntry entry = filteredEntries.get(sel);
+                selectedIndex = entry.index();
+                close();
+            }
+            return true;
+        }
+        if (ke.isKey(KeyCode.BACKSPACE)) {
+            filter.deleteChar();
+            rebuildList();
+            return true;
+        }
+        if (ke.code() == KeyCode.CHAR && !ke.hasCtrl() && !ke.hasAlt()) {
+            filter.appendChar(ke.string().charAt(0));
+            rebuildList();
+            return true;
+        }
+        return true;
+    }
+
+    void render(Frame frame, Rect area) {
+        if (filteredEntries == null) {
+            return;
+        }
+        int popupW = Math.min(80, area.width() - 4);
+        int contentH = filteredEntries.size() + 2;
+        int maxH = area.height() - 4;
+        int popupH = contentH + 2 <= maxH ? contentH + 2 : Math.min(contentH + 
2, maxH - 6);
+        int x = area.left() + Math.max(0, (area.width() - popupW) / 2);
+        int y = area.top() + 2;
+        Rect popup = new Rect(x, y, Math.min(popupW, area.width()), 
Math.min(popupH, area.height() - 2));
+
+        frame.renderWidget(Clear.INSTANCE, popup);
+
+        String filterText = filter.hasFilter() ? filter.filter() : "";
+        String prompt = "> " + filterText + "█";
+
+        List<ListItem> items = new ArrayList<>();
+        items.add(ListItem.from(Line.from(Span.styled(prompt, Theme.info()))));
+        String sep = "─".repeat(Math.max(1, popupW - 2));
+        items.add(ListItem.from(Line.from(Span.styled(sep, 
Style.EMPTY.dim()))));
+
+        Style normalStyle = Style.EMPTY;
+        Style matchStyle = Theme.label().bold();
+        Style dimStyle = Style.EMPTY.dim();
+        for (NodeEntry entry : filteredEntries) {
+            List<Span> spans = new ArrayList<>();
+            spans.add(Span.raw(" "));
+
+            dev.tamboui.style.Color eipColor
+                    = 
org.apache.camel.dsl.jbang.core.commands.tui.diagram.DiagramColors.getEipColor(entry.type());
+            spans.add(Span.styled("[" + entry.type() + "]", 
Style.EMPTY.fg(eipColor).bold()));
+            spans.add(Span.raw(" "));
+
+            String searchable = entry.label().isBlank() ? entry.nodeId() : 
entry.label();
+            int maxLabelW = popupW - entry.type().length() - 
entry.nodeId().length() - 10;
+            if (searchable.length() > maxLabelW && maxLabelW > 3) {
+                searchable = searchable.substring(0, maxLabelW - 1) + "…";
+            }
+
+            if (filter.hasFilter()) {
+                int[] nameMatch = FuzzyFilter.fuzzyMatch(searchable, 
filter.filter());
+                if (nameMatch != null) {
+                    Line hl = FuzzyFilter.highlightLine(searchable, nameMatch, 
normalStyle, matchStyle);
+                    spans.addAll(hl.spans());
+                } else {
+                    spans.add(Span.styled(searchable, normalStyle));
+                }
+            } else {
+                spans.add(Span.styled(searchable, normalStyle));
+            }
+
+            spans.add(Span.styled(" (" + entry.nodeId() + ")", dimStyle));
+            items.add(ListItem.from(Line.from(spans)));
+        }
+
+        ListState renderState = new ListState();
+        Integer sel = listState.selected();
+        if (sel != null) {
+            renderState.select(sel + 2);
+        }
+
+        int total = allEntries != null ? allEntries.size() : 0;
+        int shown = filteredEntries.size();
+        String title = shown == total
+                ? " Go to Node (" + total + ") "
+                : " Go to Node (" + shown + "/" + total + ") ";
+
+        ListWidget list = ListWidget.builder()
+                .items(items.toArray(ListItem[]::new))
+                .highlightStyle(Theme.selectionBg())
+                .highlightSymbol("")
+                .scrollMode(ScrollMode.AUTO_SCROLL)
+                .block(Block.builder()
+                        .borderType(BorderType.ROUNDED).borders(Borders.ALL)
+                        .title(title)
+                        .build())
+                .build();
+        frame.renderStatefulWidget(list, popup, renderState);
+
+        int visibleRows = Math.max(1, popup.height() - 2);
+        if (shown + 2 > visibleRows) {
+            scrollbarState
+                    .contentLength(shown)
+                    .viewportContentLength(visibleRows)
+                    .position(sel != null ? sel : 0);
+            frame.renderStatefulWidget(Scrollbar.builder().build(), popup, 
scrollbarState);
+        }
+    }
+
+    private void rebuildList() {
+        if (allEntries == null) {
+            filteredEntries = List.of();
+            return;
+        }
+        if (!filter.hasFilter()) {
+            filteredEntries = new ArrayList<>(allEntries);
+        } else {
+            filteredEntries = new ArrayList<>();
+            String f = filter.filter();
+            for (NodeEntry entry : allEntries) {
+                String searchText = entry.type() + " " + entry.label() + " " + 
entry.nodeId();
+                boolean matches;
+                if (f.length() <= 2) {
+                    matches = searchText.toLowerCase().contains(f);
+                } else {
+                    matches = filter.match(searchText) != null;
+                }
+                if (matches) {
+                    filteredEntries.add(entry);
+                }
+            }
+        }
+        listState.select(filteredEntries.isEmpty() ? null : 0);
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java
index c59b79374d85..970e4ed5add3 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java
@@ -382,17 +382,17 @@ class OverviewTab extends AbstractTab {
                     }
                     rows.add(Row.from(
                             Cell.from(nameLine),
-                            rightCell(info.exchangesTotal > 0 ? 
String.valueOf(info.meanTime) : "", 6,
+                            rightCell(info.exchangesTotal > 0 ? 
formatDurationMs(info.meanTime) : "", 8,
                                     TuiHelper.topTimeStyle(info.meanTime)),
-                            rightCell(info.exchangesTotal > 0 ? 
String.valueOf(info.maxTime) : "", 6,
+                            rightCell(info.exchangesTotal > 0 ? 
formatDurationMs(info.maxTime) : "", 8,
                                     TuiHelper.topTimeStyle(info.maxTime)),
-                            rightCell(info.exchangesTotal > 0 ? 
String.valueOf(info.minTime) : "", 6),
-                            rightCell(info.exchangesTotal > 0 ? 
String.valueOf(info.lastTime) : "", 6),
-                            rightCell(info.deltaTime != 0 ? 
String.valueOf(info.deltaTime) : "", 6,
+                            rightCell(info.exchangesTotal > 0 ? 
formatDurationMs(info.minTime) : "", 8),
+                            rightCell(info.exchangesTotal > 0 ? 
formatDurationMs(info.lastTime) : "", 8),
+                            rightCell(info.deltaTime != 0 ? 
formatDurationMs(info.deltaTime) : "", 8,
                                     TuiHelper.topDeltaStyle(info.deltaTime)),
-                            rightCell(info.p50Time >= 0 ? 
String.valueOf(info.p50Time) : "", 6),
-                            rightCell(info.p95Time >= 0 ? 
String.valueOf(info.p95Time) : "", 6),
-                            rightCell(info.p99Time >= 0 ? 
String.valueOf(info.p99Time) : "", 6),
+                            rightCell(info.p50Time >= 0 ? 
formatDurationMs(info.p50Time) : "", 8),
+                            rightCell(info.p95Time >= 0 ? 
formatDurationMs(info.p95Time) : "", 8),
+                            rightCell(info.p99Time >= 0 ? 
formatDurationMs(info.p99Time) : "", 8),
                             rightCell(String.valueOf(info.exchangesTotal), 8),
                             rightCell(String.valueOf(info.failed), 6, 
failStyle),
                             rightCell(String.valueOf(info.inflight), 8),
@@ -405,14 +405,14 @@ class OverviewTab extends AbstractTab {
 
             header = Row.from(
                     Cell.from(Span.styled("NAME", Style.EMPTY.bold())),
-                    rightCell(topSortLabel("MEAN", "mean"), 6, 
topSortStyle("mean")),
-                    rightCell(topSortLabel("MAX", "max"), 6, 
topSortStyle("max")),
-                    rightCell(topSortLabel("MIN", "min"), 6, 
topSortStyle("min")),
-                    rightCell(topSortLabel("LAST", "last"), 6, 
topSortStyle("last")),
-                    rightCell(topSortLabel("DELTA", "delta"), 6, 
topSortStyle("delta")),
-                    rightCell(topSortLabel("P50", "p50"), 6, 
topSortStyle("p50")),
-                    rightCell(topSortLabel("P95", "p95"), 6, 
topSortStyle("p95")),
-                    rightCell(topSortLabel("P99", "p99"), 6, 
topSortStyle("p99")),
+                    rightCell(topSortLabel("MEAN", "mean"), 8, 
topSortStyle("mean")),
+                    rightCell(topSortLabel("MAX", "max"), 8, 
topSortStyle("max")),
+                    rightCell(topSortLabel("MIN", "min"), 8, 
topSortStyle("min")),
+                    rightCell(topSortLabel("LAST", "last"), 8, 
topSortStyle("last")),
+                    rightCell(topSortLabel("DELTA", "delta"), 8, 
topSortStyle("delta")),
+                    rightCell(topSortLabel("P50", "p50"), 8, 
topSortStyle("p50")),
+                    rightCell(topSortLabel("P95", "p95"), 8, 
topSortStyle("p95")),
+                    rightCell(topSortLabel("P99", "p99"), 8, 
topSortStyle("p99")),
                     rightCell("TOTAL", 8, Style.EMPTY.bold()),
                     rightCell("FAIL", 6, Style.EMPTY.bold()),
                     rightCell("INFLIGHT", 8, Style.EMPTY.bold()),
@@ -421,14 +421,14 @@ class OverviewTab extends AbstractTab {
 
             widths = new Constraint[] {
                     Constraint.fill(),
-                    Constraint.length(6),
-                    Constraint.length(6),
-                    Constraint.length(6),
-                    Constraint.length(6),
-                    Constraint.length(6),
-                    Constraint.length(6),
-                    Constraint.length(6),
-                    Constraint.length(6),
+                    Constraint.length(8),
+                    Constraint.length(8),
+                    Constraint.length(8),
+                    Constraint.length(8),
+                    Constraint.length(8),
+                    Constraint.length(8),
+                    Constraint.length(8),
+                    Constraint.length(8),
                     Constraint.length(8),
                     Constraint.length(6),
                     Constraint.length(8),
@@ -511,9 +511,11 @@ class OverviewTab extends AbstractTab {
                     }
                     String timingCol;
                     if (hasPercentiles && info.p50Time >= 0) {
-                        timingCol = info.p50Time + "/" + info.p95Time + "/" + 
info.p99Time;
+                        timingCol = formatDurationMs(info.p50Time) + "/" + 
formatDurationMs(info.p95Time) + "/"
+                                    + formatDurationMs(info.p99Time);
                     } else if (info.exchangesTotal > 0) {
-                        timingCol = info.minTime + "/" + info.maxTime + "/" + 
info.meanTime;
+                        timingCol = formatDurationMs(info.minTime) + "/" + 
formatDurationMs(info.maxTime) + "/"
+                                    + formatDurationMs(info.meanTime);
                     } else {
                         timingCol = "";
                     }
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 a5d6a73c9c9a..f05948ea43b4 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
@@ -16,21 +16,30 @@
  */
 package org.apache.camel.dsl.jbang.core.commands.tui;
 
+import java.net.URISyntaxException;
 import java.nio.file.Path;
 import java.util.ArrayDeque;
 import java.util.ArrayList;
 import java.util.Deque;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
 
 import dev.tamboui.layout.Constraint;
 import dev.tamboui.layout.Layout;
 import dev.tamboui.layout.Rect;
+import dev.tamboui.markdown.MarkdownView;
 import dev.tamboui.style.Color;
 import dev.tamboui.style.Style;
 import dev.tamboui.terminal.Frame;
+import dev.tamboui.text.CharWidth;
 import dev.tamboui.text.Line;
 import dev.tamboui.text.Span;
 import dev.tamboui.text.Text;
+import dev.tamboui.tui.event.KeyCode;
 import dev.tamboui.tui.event.KeyEvent;
 import dev.tamboui.tui.event.MouseEvent;
 import dev.tamboui.tui.event.MouseEventKind;
@@ -43,7 +52,12 @@ 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.dsl.jbang.core.common.CatalogLoader;
 import org.apache.camel.dsl.jbang.core.common.PathUtils;
+import org.apache.camel.tooling.model.BaseOptionModel;
+import org.apache.camel.tooling.model.ComponentModel;
+import org.apache.camel.tooling.model.EipModel;
 import org.apache.camel.util.TimeUtils;
 import org.apache.camel.util.json.JsonArray;
 import org.apache.camel.util.json.JsonObject;
@@ -86,6 +100,20 @@ class RoutesTab extends AbstractTab {
     private String drillDownRouteId;
     private final Deque<String> routeNavigationStack = new ArrayDeque<>();
 
+    // Detail panel (d toggle in drill-down mode)
+    private boolean detailMode;
+    private int detailScroll;
+    private String lastDetailNodeId;
+    private volatile JsonObject cachedRouteDetail;
+    private volatile String cachedRouteDetailId;
+    private volatile String detailLoadingRouteId;
+    private volatile boolean detailLoading;
+    private final Map<String, CamelCatalog> catalogCache = new HashMap<>();
+    private final Set<String> catalogLoadFailed = new HashSet<>();
+
+    // Go-to node popup (g in drill-down mode)
+    private final GotoNodePopup gotoNodePopup = new GotoNodePopup();
+
     RoutesTab(MonitorContext ctx) {
         super(ctx);
     }
@@ -104,11 +132,22 @@ class RoutesTab extends AbstractTab {
 
     @Override
     public boolean isOverlayActive() {
-        return sourceViewer.isVisible();
+        return sourceViewer.isVisible() || gotoNodePopup.isVisible();
     }
 
     @Override
     public boolean handleKeyEvent(KeyEvent ke) {
+        // Go-to node popup (takes priority when active)
+        if (gotoNodePopup.isVisible()) {
+            gotoNodePopup.handleKeyEvent(ke);
+            int sel = gotoNodePopup.consumeSelectedIndex();
+            if (sel >= 0) {
+                diagram.setSelectedEipNodeIndex(sel);
+                diagram.scrollToSelectedEipNode();
+            }
+            return true;
+        }
+
         // Source view scrolling (takes priority when active)
         if (sourceViewer.handleKeyEvent(ke)) {
             return true;
@@ -212,6 +251,18 @@ class RoutesTab extends AbstractTab {
             return true;
         }
 
+        // Detail panel scrolling (PgUp/PgDn when detail mode is active)
+        if (detailMode && diagram.isShowDiagram() && !topologyMode) {
+            if (ke.isPageUp() || ke.isKey(KeyCode.PAGE_UP)) {
+                detailScroll = Math.max(0, detailScroll - 5);
+                return true;
+            }
+            if (ke.isPageDown() || ke.isKey(KeyCode.PAGE_DOWN)) {
+                detailScroll += 5;
+                return true;
+            }
+        }
+
         // Diagram scrolling (PgUp/PgDn etc)
         if (diagram.handleScrollKeys(ke)) {
             return true;
@@ -320,19 +371,28 @@ class RoutesTab extends AbstractTab {
             return true;
         }
 
-        // Enter in table mode opens topology
+        // Enter in table mode opens route diagram
         if (!diagram.isShowDiagram() && !sourceViewer.isVisible() && 
ke.isConfirm()) {
-            openTopology();
+            openRouteDiagram();
             return true;
         }
 
-        // d in table mode opens route diagram, in diagram mode closes it
-        if (ke.isCharIgnoreCase('d')) {
-            if (diagram.isShowDiagram()) {
-                closeDiagram();
-            } else {
-                openRouteDiagram();
-            }
+        // d in drill-down mode toggles detail panel
+        if (diagram.isShowDiagram() && !topologyMode && 
!diagram.getEipNodeBoxes().isEmpty()
+                && ke.isCharIgnoreCase('d')) {
+            detailMode = !detailMode;
+            detailScroll = 0;
+            cachedRouteDetail = null;
+            cachedRouteDetailId = null;
+            detailLoadingRouteId = null;
+            detailLoading = false;
+            return true;
+        }
+
+        // g in drill-down mode opens go-to node popup
+        if (diagram.isShowDiagram() && !topologyMode && 
!diagram.getEipNodeBoxes().isEmpty()
+                && ke.isChar('g')) {
+            gotoNodePopup.open(diagram.getEipNodeBoxes(), 
diagram.getSelectedEipNodeIndex());
             return true;
         }
 
@@ -363,6 +423,10 @@ class RoutesTab extends AbstractTab {
 
     @Override
     public boolean handleEscape() {
+        if (gotoNodePopup.isVisible()) {
+            gotoNodePopup.close();
+            return true;
+        }
         if (sourceViewer.isVisible()) {
             sourceViewer.hide();
             return true;
@@ -397,6 +461,9 @@ class RoutesTab extends AbstractTab {
 
     @Override
     public boolean handleMouseEvent(MouseEvent me, Rect area) {
+        if (gotoNodePopup.isVisible()) {
+            return true;
+        }
         if (!diagram.isShowDiagram() && vSplit.handleMouse(me, me.y())) {
             if (vSplit.isDragging() && me.kind() == MouseEventKind.DRAG) {
                 topPanelHeight = Math.max(3, Math.min(me.y() - area.y(), 
area.height() - 5));
@@ -518,17 +585,35 @@ class RoutesTab extends AbstractTab {
                     && diagram.getRouteLayout(drillDownRouteId) != null) {
                 Line title = buildBreadcrumbTitle();
                 var routeLayout = diagram.getRouteLayout(drillDownRouteId);
-                if (area.width() > 60) {
-                    infoPanelWidth = Math.max(10, Math.min(infoPanelWidth, 
area.width() - 20));
+
+                // Split for detail panel when active
+                Rect diagramArea = area;
+                if (detailMode) {
+                    int detailH = Math.max(5, area.height() * 60 / 100);
+                    List<Rect> vChunks = Layout.vertical()
+                            .constraints(Constraint.fill(), 
Constraint.length(detailH))
+                            .split(area);
+                    diagramArea = vChunks.get(0);
+                    renderDetail(frame, vChunks.get(1), info);
+                }
+
+                if (diagramArea.width() > 60) {
+                    infoPanelWidth = Math.max(10, Math.min(infoPanelWidth, 
diagramArea.width() - 20));
                     List<Rect> hChunks = Layout.horizontal()
                             .constraints(Constraint.length(infoPanelWidth), 
Constraint.fill())
-                            .split(area);
+                            .split(diagramArea);
                     hSplit.setBorderPos(hChunks.get(1).x());
                     renderEipInfoPanel(frame, hChunks.get(0));
                     diagram.renderNativeRouteDiagram(
                             frame, hChunks.get(1), title, diagramMetrics, 
drillDownRouteId, routeLayout);
                 } else {
-                    diagram.renderNativeRouteDiagram(frame, area, title, 
diagramMetrics, drillDownRouteId, routeLayout);
+                    diagram.renderNativeRouteDiagram(frame, diagramArea, 
title, diagramMetrics, drillDownRouteId,
+                            routeLayout);
+                }
+
+                // Render go-to popup overlay
+                if (gotoNodePopup.isVisible()) {
+                    gotoNodePopup.render(frame, area);
                 }
                 return;
             }
@@ -569,15 +654,17 @@ class RoutesTab extends AbstractTab {
                 routeRows.add(Row.from(
                         Cell.from(Span.styled(route.routeId != null ? 
route.routeId : "", Style.EMPTY.fg(Theme.accent()))),
                         Cell.from(routeFromLabel(route)),
-                        rightCell(route.total > 0 ? 
String.valueOf(route.meanTime) : "", 6, topTimeStyle(route.meanTime)),
-                        rightCell(route.total > 0 ? 
String.valueOf(route.maxTime) : "", 6, topTimeStyle(route.maxTime)),
-                        rightCell(route.total > 0 ? 
String.valueOf(route.minTime) : "", 6),
-                        rightCell(route.total > 0 ? 
String.valueOf(route.lastTime) : "", 6),
-                        rightCell(route.deltaTime != 0 ? 
String.valueOf(route.deltaTime) : "", 6,
+                        rightCell(route.total > 0 ? 
formatDurationMs(route.meanTime) : "", 8,
+                                topTimeStyle(route.meanTime)),
+                        rightCell(route.total > 0 ? 
formatDurationMs(route.maxTime) : "", 8,
+                                topTimeStyle(route.maxTime)),
+                        rightCell(route.total > 0 ? 
formatDurationMs(route.minTime) : "", 8),
+                        rightCell(route.total > 0 ? 
formatDurationMs(route.lastTime) : "", 8),
+                        rightCell(route.deltaTime != 0 ? 
formatDurationMs(route.deltaTime) : "", 8,
                                 topDeltaStyle(route.deltaTime)),
-                        rightCell(route.p50Time >= 0 ? 
String.valueOf(route.p50Time) : "", 6),
-                        rightCell(route.p95Time >= 0 ? 
String.valueOf(route.p95Time) : "", 6),
-                        rightCell(route.p99Time >= 0 ? 
String.valueOf(route.p99Time) : "", 6),
+                        rightCell(route.p50Time >= 0 ? 
formatDurationMs(route.p50Time) : "", 8),
+                        rightCell(route.p95Time >= 0 ? 
formatDurationMs(route.p95Time) : "", 8),
+                        rightCell(route.p99Time >= 0 ? 
formatDurationMs(route.p99Time) : "", 8),
                         rightCell(String.valueOf(route.total), 8),
                         rightCell(String.valueOf(route.failed), 6, failStyle),
                         rightCell(String.valueOf(route.inflight), 8),
@@ -591,14 +678,14 @@ class RoutesTab extends AbstractTab {
                 routeRows.add(Row.from(
                         Cell.from(Span.styled("GLOBAL", ts)),
                         Cell.from(""),
-                        rightCell(String.valueOf(selTop.meanTime), 6, ts),
-                        rightCell(String.valueOf(selTop.maxTime), 6, ts),
-                        rightCell(String.valueOf(selTop.minTime), 6, ts),
-                        rightCell(String.valueOf(selTop.lastTime), 6, ts),
-                        rightCell(selTop.deltaTime != 0 ? 
String.valueOf(selTop.deltaTime) : "", 6, ts),
-                        rightCell(selTop.p50Time >= 0 ? 
String.valueOf(selTop.p50Time) : "", 6, ts),
-                        rightCell(selTop.p95Time >= 0 ? 
String.valueOf(selTop.p95Time) : "", 6, ts),
-                        rightCell(selTop.p99Time >= 0 ? 
String.valueOf(selTop.p99Time) : "", 6, ts),
+                        rightCell(formatDurationMs(selTop.meanTime), 8, ts),
+                        rightCell(formatDurationMs(selTop.maxTime), 8, ts),
+                        rightCell(formatDurationMs(selTop.minTime), 8, ts),
+                        rightCell(formatDurationMs(selTop.lastTime), 8, ts),
+                        rightCell(selTop.deltaTime != 0 ? 
formatDurationMs(selTop.deltaTime) : "", 8, ts),
+                        rightCell(selTop.p50Time >= 0 ? 
formatDurationMs(selTop.p50Time) : "", 8, ts),
+                        rightCell(selTop.p95Time >= 0 ? 
formatDurationMs(selTop.p95Time) : "", 8, ts),
+                        rightCell(selTop.p99Time >= 0 ? 
formatDurationMs(selTop.p99Time) : "", 8, ts),
                         rightCell(String.valueOf(selTop.exchangesTotal), 8, 
ts),
                         rightCell(String.valueOf(selTop.failed), 6,
                                 selTop.failed > 0 ? Theme.error().bold() : ts),
@@ -613,14 +700,14 @@ class RoutesTab extends AbstractTab {
                     .header(Row.from(
                             Cell.from(Span.styled("ROUTE", 
Style.EMPTY.bold())),
                             Cell.from(Span.styled("FROM", Style.EMPTY.bold())),
-                            rightCell(routeTopSortLabel("MEAN", "mean"), 6, 
routeTopSortStyle("mean")),
-                            rightCell(routeTopSortLabel("MAX", "max"), 6, 
routeTopSortStyle("max")),
-                            rightCell(routeTopSortLabel("MIN", "min"), 6, 
routeTopSortStyle("min")),
-                            rightCell(routeTopSortLabel("LAST", "last"), 6, 
routeTopSortStyle("last")),
-                            rightCell(routeTopSortLabel("DELTA", "delta"), 6, 
routeTopSortStyle("delta")),
-                            rightCell(routeTopSortLabel("P50", "p50"), 6, 
routeTopSortStyle("p50")),
-                            rightCell(routeTopSortLabel("P95", "p95"), 6, 
routeTopSortStyle("p95")),
-                            rightCell(routeTopSortLabel("P99", "p99"), 6, 
routeTopSortStyle("p99")),
+                            rightCell(routeTopSortLabel("MEAN", "mean"), 8, 
routeTopSortStyle("mean")),
+                            rightCell(routeTopSortLabel("MAX", "max"), 8, 
routeTopSortStyle("max")),
+                            rightCell(routeTopSortLabel("MIN", "min"), 8, 
routeTopSortStyle("min")),
+                            rightCell(routeTopSortLabel("LAST", "last"), 8, 
routeTopSortStyle("last")),
+                            rightCell(routeTopSortLabel("DELTA", "delta"), 8, 
routeTopSortStyle("delta")),
+                            rightCell(routeTopSortLabel("P50", "p50"), 8, 
routeTopSortStyle("p50")),
+                            rightCell(routeTopSortLabel("P95", "p95"), 8, 
routeTopSortStyle("p95")),
+                            rightCell(routeTopSortLabel("P99", "p99"), 8, 
routeTopSortStyle("p99")),
                             rightCell("TOTAL", 8, Style.EMPTY.bold()),
                             rightCell("FAIL", 6, Style.EMPTY.bold()),
                             rightCell("INFLIGHT", 8, Style.EMPTY.bold()),
@@ -629,14 +716,14 @@ class RoutesTab extends AbstractTab {
                     .widths(
                             Constraint.length(24),
                             Constraint.fill(),
-                            Constraint.length(6),
-                            Constraint.length(6),
-                            Constraint.length(6),
-                            Constraint.length(6),
-                            Constraint.length(6),
-                            Constraint.length(6),
-                            Constraint.length(6),
-                            Constraint.length(6),
+                            Constraint.length(8),
+                            Constraint.length(8),
+                            Constraint.length(8),
+                            Constraint.length(8),
+                            Constraint.length(8),
+                            Constraint.length(8),
+                            Constraint.length(8),
+                            Constraint.length(8),
                             Constraint.length(8),
                             Constraint.length(6),
                             Constraint.length(8),
@@ -777,7 +864,9 @@ class RoutesTab extends AbstractTab {
                 hint(spans, "Esc", "back");
                 hint(spans, "t", "topology");
                 hint(spans, TuiIcons.HINT_NAV, "navigate");
-                hint(spans, "PgUp/PgDn", "page");
+                hint(spans, "d", "detail" + (detailMode ? " [on]" : " [off]"));
+                hint(spans, "g", "go to");
+                hint(spans, "PgUp/PgDn", detailMode ? "detail" : "page");
                 hint(spans, "c", "source");
             } else if (!topologyMode) {
                 hint(spans, "Esc", "back");
@@ -801,8 +890,7 @@ class RoutesTab extends AbstractTab {
         } else {
             hint(spans, "Esc", "back");
             hint(spans, TuiIcons.HINT_SCROLL, "navigate");
-            hint(spans, "Enter", "topology");
-            hint(spans, "d", "diagram");
+            hint(spans, "Enter", "diagram");
             hint(spans, "s", "sort");
             hint(spans, "n", "description" + (showDescription ? " [on]" : " 
[off]"));
             hint(spans, "t", routeTopMode ? "top [on]" : "top [off]");
@@ -942,28 +1030,26 @@ class RoutesTab extends AbstractTab {
 
             lines.add(Line.from(Span.raw("")));
             if (route.total > 0) {
-                int tw = numWidth(route.meanTime, route.maxTime, 
route.minTime);
                 lines.add(Line.from(
                         Span.styled(" Mean: ", Theme.muted()),
-                        Span.raw(String.format("%" + tw + "d ms", 
route.meanTime))));
+                        Span.raw(formatDurationMs(route.meanTime))));
                 lines.add(Line.from(
                         Span.styled(" Max:  ", Theme.muted()),
-                        Span.raw(String.format("%" + tw + "d ms", 
route.maxTime))));
+                        Span.raw(formatDurationMs(route.maxTime))));
                 lines.add(Line.from(
                         Span.styled(" Min:  ", Theme.muted()),
-                        Span.raw(String.format("%" + tw + "d ms", 
route.minTime))));
+                        Span.raw(formatDurationMs(route.minTime))));
                 if (route.p50Time >= 0) {
-                    int pw = numWidth(route.p50Time, route.p95Time, 
route.p99Time);
                     lines.add(Line.from(Span.raw("")));
                     lines.add(Line.from(
                             Span.styled(" p50:  ", Theme.muted()),
-                            Span.raw(String.format("%" + pw + "d ms", 
route.p50Time))));
+                            Span.raw(formatDurationMs(route.p50Time))));
                     lines.add(Line.from(
                             Span.styled(" p95:  ", Theme.muted()),
-                            Span.raw(String.format("%" + pw + "d ms", 
route.p95Time))));
+                            Span.raw(formatDurationMs(route.p95Time))));
                     lines.add(Line.from(
                             Span.styled(" p99:  ", Theme.muted()),
-                            Span.raw(String.format("%" + pw + "d ms", 
route.p99Time))));
+                            Span.raw(formatDurationMs(route.p99Time))));
                     lines.add(buildPercentileBarLine(
                             route.p50Time, route.p95Time, route.p99Time, 
area.width() - 3));
                 }
@@ -1102,33 +1188,29 @@ class RoutesTab extends AbstractTab {
 
                 if (stat.exchangesTotal > 0) {
                     lines.add(Line.from(Span.raw("")));
-                    int tw = numWidth(stat.meanProcessingTime, 
stat.maxProcessingTime,
-                            stat.minProcessingTime, stat.lastProcessingTime);
                     lines.add(Line.from(
                             Span.styled(" Mean: ", Style.EMPTY.dim()),
-                            Span.raw(String.format("%" + tw + "d ms", 
stat.meanProcessingTime))));
+                            
Span.raw(formatDurationMs(stat.meanProcessingTime))));
                     lines.add(Line.from(
                             Span.styled(" Max:  ", Style.EMPTY.dim()),
-                            Span.raw(String.format("%" + tw + "d ms", 
stat.maxProcessingTime))));
+                            
Span.raw(formatDurationMs(stat.maxProcessingTime))));
                     lines.add(Line.from(
                             Span.styled(" Min:  ", Style.EMPTY.dim()),
-                            Span.raw(String.format("%" + tw + "d ms", 
stat.minProcessingTime))));
+                            
Span.raw(formatDurationMs(stat.minProcessingTime))));
                     lines.add(Line.from(
                             Span.styled(" Last: ", Style.EMPTY.dim()),
-                            Span.raw(String.format("%" + tw + "d ms", 
stat.lastProcessingTime))));
+                            
Span.raw(formatDurationMs(stat.lastProcessingTime))));
                     if (stat.p50ProcessingTime >= 0) {
-                        int pw = numWidth(stat.p50ProcessingTime, 
stat.p95ProcessingTime,
-                                stat.p99ProcessingTime);
                         lines.add(Line.from(Span.raw("")));
                         lines.add(Line.from(
                                 Span.styled(" p50:  ", Style.EMPTY.dim()),
-                                Span.raw(String.format("%" + pw + "d ms", 
stat.p50ProcessingTime))));
+                                
Span.raw(formatDurationMs(stat.p50ProcessingTime))));
                         lines.add(Line.from(
                                 Span.styled(" p95:  ", Style.EMPTY.dim()),
-                                Span.raw(String.format("%" + pw + "d ms", 
stat.p95ProcessingTime))));
+                                
Span.raw(formatDurationMs(stat.p95ProcessingTime))));
                         lines.add(Line.from(
                                 Span.styled(" p99:  ", Style.EMPTY.dim()),
-                                Span.raw(String.format("%" + pw + "d ms", 
stat.p99ProcessingTime))));
+                                
Span.raw(formatDurationMs(stat.p99ProcessingTime))));
                         lines.add(buildPercentileBarLine(
                                 stat.p50ProcessingTime, stat.p95ProcessingTime,
                                 stat.p99ProcessingTime, area.width() - 3));
@@ -1693,6 +1775,271 @@ class RoutesTab extends AbstractTab {
         return new SelectionContext("table", items, sel != null ? sel : -1, 
items.size(), "Routes");
     }
 
+    // ---- Detail panel ----
+
+    private void renderDetail(Frame frame, Rect area, IntegrationInfo info) {
+        var selected = diagram.getSelectedEipNodeBox();
+        if (selected == null || selected.layoutNode() == null) {
+            frame.renderWidget(
+                    MarkdownView.builder()
+                            .source("*Select an EIP node*")
+                            
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+                                    .title(" EIP Detail ").build())
+                            .styles(Theme.markdownStyles())
+                            .build(),
+                    area);
+            return;
+        }
+
+        String nodeId = selected.layoutNode().id;
+        String eipType = selected.layoutNode().type;
+        if (nodeId == null || eipType == null) {
+            frame.renderWidget(
+                    MarkdownView.builder()
+                            .source("*No detail available*")
+                            
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+                                    .title(" EIP Detail ").build())
+                            .styles(Theme.markdownStyles())
+                            .build(),
+                    area);
+            return;
+        }
+
+        if (!nodeId.equals(lastDetailNodeId)) {
+            lastDetailNodeId = nodeId;
+            detailScroll = 0;
+        }
+
+        if (drillDownRouteId != null && 
!drillDownRouteId.equals(cachedRouteDetailId)
+                && !drillDownRouteId.equals(detailLoadingRouteId)) {
+            detailLoadingRouteId = drillDownRouteId;
+            detailLoading = true;
+            String rid = drillDownRouteId;
+            if (ctx.runner != null) {
+                ctx.runner.scheduler().execute(() -> {
+                    JsonObject result = requestRouteProcessorDetail(rid);
+                    cachedRouteDetail = result;
+                    cachedRouteDetailId = rid;
+                    detailLoading = false;
+                });
+            }
+        }
+
+        JsonObject processorEntry = findProcessorEntry(nodeId);
+
+        StringBuilder md = new StringBuilder();
+        CamelCatalog catalog = getCatalog(info);
+
+        if (processorEntry != null) {
+            String type = processorEntry.getString("type");
+            String endpointUri = processorEntry.getString("endpointUri");
+
+            if ("from".equals(type) && endpointUri != null && catalog != null) 
{
+                renderEndpointDetail(md, catalog, endpointUri);
+            } else if (type != null) {
+                renderEipDetail(md, catalog, type, 
processorEntry.getMap("options"));
+            } else {
+                md.append("*No detail available*\n");
+            }
+        } else if (detailLoading) {
+            md.append("*Loading...*\n");
+        } else {
+            md.append("*No detail available*\n");
+        }
+
+        String title = " " + eipType + " [" + nodeId + "] ";
+        if (CharWidth.of(title) > area.width() - 4) {
+            title = " " + CharWidth.truncateWithEllipsis(
+                    eipType + " [" + nodeId + "]", area.width() - 6, 
CharWidth.TruncatePosition.MIDDLE) + " ";
+        }
+
+        frame.renderWidget(
+                MarkdownView.builder()
+                        .source(md.toString())
+                        .scroll(detailScroll)
+                        
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+                                .title(title).build())
+                        .styles(Theme.markdownStyles())
+                        .build(),
+                area);
+    }
+
+    private void renderEipDetail(StringBuilder md, CamelCatalog catalog, 
String type, JsonObject opts) {
+        EipModel model = catalog != null ? catalog.eipModel(type) : null;
+
+        if (model != null && model.getTitle() != null) {
+            md.append("## ").append(model.getTitle()).append("\n\n");
+            if (model.getDescription() != null && 
!model.getDescription().isEmpty()) {
+                md.append(model.getDescription()).append("\n\n");
+            }
+        } else {
+            md.append("## ").append(type).append("\n\n");
+        }
+
+        if (opts != null && !opts.isEmpty()) {
+            Map<String, BaseOptionModel> optionDocs = new LinkedHashMap<>();
+            if (model != null) {
+                for (BaseOptionModel opt : model.getOptions()) {
+                    if (opt.getName() != null) {
+                        optionDocs.put(opt.getName(), opt);
+                    }
+                }
+            }
+
+            for (Map.Entry<String, Object> entry : opts.entrySet()) {
+                String optName = entry.getKey();
+                String optValue = String.valueOf(entry.getValue());
+                appendOptionDetail(md, optName, optValue, 
optionDocs.get(optName));
+            }
+        } else {
+            md.append("*No configured options*\n");
+        }
+    }
+
+    private void renderEndpointDetail(StringBuilder md, CamelCatalog catalog, 
String uri) {
+        String component = uri.contains(":") ? uri.substring(0, 
uri.indexOf(':')) : uri;
+        ComponentModel model = catalog.componentModel(component);
+
+        if (model != null) {
+            String compTitle = model.getTitle() != null ? model.getTitle() : 
component;
+            md.append("## ").append(compTitle).append("\n\n");
+            if (model.getDescription() != null && 
!model.getDescription().isEmpty()) {
+                md.append(model.getDescription()).append("\n\n");
+            }
+
+            Map<String, String> parsedOptions;
+            try {
+                parsedOptions = catalog.endpointProperties(uri);
+            } catch (URISyntaxException e) {
+                parsedOptions = Map.of();
+            }
+
+            if (parsedOptions.isEmpty()) {
+                md.append("*No configured options*\n");
+            } else {
+                Map<String, BaseOptionModel> optionDocs = new 
LinkedHashMap<>();
+                for (ComponentModel.EndpointOptionModel opt : 
model.getEndpointOptions()) {
+                    if (opt.getName() != null) {
+                        optionDocs.put(opt.getName(), opt);
+                    }
+                }
+
+                for (Map.Entry<String, String> entry : 
parsedOptions.entrySet()) {
+                    appendOptionDetail(md, entry.getKey(), entry.getValue(), 
optionDocs.get(entry.getKey()));
+                }
+            }
+        } else {
+            md.append("## ").append(component).append("\n\n");
+            md.append("*No catalog documentation for: 
").append(component).append("*\n");
+        }
+    }
+
+    private static void appendOptionDetail(StringBuilder md, String optName, 
String optValue, BaseOptionModel doc) {
+        md.append("---\n\n");
+        md.append("**").append(optName).append("** = 
**").append(optValue).append("**\n\n");
+
+        if (doc != null) {
+            if (doc.getDescription() != null && 
!doc.getDescription().isEmpty()) {
+                md.append(doc.getDescription()).append("\n\n");
+            }
+            List<String> meta = new ArrayList<>();
+            if (doc.getType() != null) {
+                meta.add("Type: `" + doc.getType() + "`");
+            }
+            if (doc.getDefaultValue() != null) {
+                meta.add("Default: `" + doc.getDefaultValue() + "`");
+            }
+            if (doc.isRequired()) {
+                meta.add("Required: yes");
+            }
+            if (doc.getEnums() != null && !doc.getEnums().isEmpty()) {
+                meta.add("Enum: " + String.join(", ", doc.getEnums()));
+            }
+            if (doc.getGroup() != null && !doc.getGroup().isEmpty()) {
+                meta.add("Group: " + doc.getGroup());
+            }
+            if (doc.isDeprecated()) {
+                String depText = "Deprecated";
+                if (doc.getDeprecationNote() != null && 
!doc.getDeprecationNote().isEmpty()) {
+                    depText += " — " + doc.getDeprecationNote();
+                }
+                meta.add(depText);
+            }
+            if (!meta.isEmpty()) {
+                for (String m : meta) {
+                    md.append("- ").append(m).append("\n");
+                }
+                md.append("\n");
+            }
+        }
+    }
+
+    private CamelCatalog getCatalog(IntegrationInfo info) {
+        String version = info.camelVersion;
+        if (version == null) {
+            return null;
+        }
+        CamelCatalog cached = catalogCache.get(version);
+        if (cached != null) {
+            return cached;
+        }
+        if (catalogLoadFailed.contains(version)) {
+            return null;
+        }
+        try {
+            cached = CatalogLoader.loadCatalog(null, version, true);
+            if (cached != null) {
+                catalogCache.put(version, cached);
+            } else {
+                catalogLoadFailed.add(version);
+            }
+            return cached;
+        } catch (Exception e) {
+            catalogLoadFailed.add(version);
+            return null;
+        }
+    }
+
+    private JsonObject requestRouteProcessorDetail(String routeId) {
+        if (ctx.selectedPid == null) {
+            return null;
+        }
+        try {
+            Path outputFile = ctx.getOutputFile(ctx.selectedPid);
+            PathUtils.deleteFile(outputFile);
+
+            JsonObject root = new JsonObject();
+            root.put("action", "processor-detail");
+            root.put("routeId", routeId);
+
+            Path actionFile = ctx.getActionFile(ctx.selectedPid);
+            PathUtils.writeTextSafely(root.toJson(), actionFile);
+
+            JsonObject jo = TuiHelper.pollJsonResponse(outputFile, 5000);
+            PathUtils.deleteFile(outputFile);
+            return jo;
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    private JsonObject findProcessorEntry(String nodeId) {
+        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;
+            if (nodeId.equals(p.getString("id"))) {
+                return p;
+            }
+        }
+        return null;
+    }
+
     @Override
     public String description() {
         return "Route list with state, message counts, throughput, and failure 
statistics";
@@ -1815,9 +2162,9 @@ class RoutesTab extends AbstractTab {
 
                 **Route table:**
                 - `Up/Down` — select route
+                - `Enter` — open route diagram
                 - `p` — start/stop selected route
                 - `P` — suspend/resume selected route
-                - `d` — show topology diagram
                 - `c` — show route source code
                 - `n` — toggle description labels
                 - `s` — cycle sort column
@@ -1836,11 +2183,14 @@ class RoutesTab extends AbstractTab {
                 **Route diagram (drill-down):**
                 - `↑↓←→` — navigate between EIP nodes
                 - `Enter` — jump to linked route (when `↵` indicator shown)
+                - `d` — toggle detail panel (EIP/component catalog docs)
+                - `g` — go to node (fuzzy search popup)
                 - `c` — show source code at selected node
                 - `Esc` — go back (previous route or topology)
                 - `t` — jump back to topology view
                 - `m` — toggle metrics
                 - `n` — toggle description labels
+                - `PgUp/PgDn` — scroll detail panel (when detail is on)
 
                 **Source view:**
                 - `↑↓` — move cursor between lines
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiHelper.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiHelper.java
index 8554c89c1ae9..04b6d60337f4 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiHelper.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiHelper.java
@@ -543,6 +543,29 @@ final class TuiHelper {
         }
     }
 
+    static String formatDurationMs(long ms) {
+        if (ms <= 0) {
+            return "0ms";
+        }
+        if (ms < 1000) {
+            return ms + "ms";
+        }
+        long seconds = ms / 1000;
+        long minutes = seconds / 60;
+        long hours = minutes / 60;
+        long days = hours / 24;
+        if (days > 0) {
+            return days + "d" + (hours % 24) + "h";
+        }
+        if (hours > 0) {
+            return hours + "h" + (minutes % 60) + "m";
+        }
+        if (minutes > 0) {
+            return minutes + "m" + (seconds % 60) + "s";
+        }
+        return seconds + "s" + (ms % 1000) + "ms";
+    }
+
     static String formatLoad(String l1, String l5, String l15) {
         String s1 = l1 != null && !"0.00".equals(l1) ? l1 : "0";
         String s5 = l5 != null && !"0.00".equals(l5) ? l5 : "0";
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTabRenderTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTabRenderTest.java
index f21f6dfab22a..40204ebe856b 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTabRenderTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTabRenderTest.java
@@ -263,7 +263,7 @@ class RoutesTabRenderTest {
 
         assertTrue(footer.contains("Esc"), "Footer should contain Esc hint");
         assertTrue(footer.contains("sort"), "Footer should contain sort hint");
-        assertTrue(footer.contains("topology"), "Footer should contain 
topology hint");
+        assertTrue(footer.contains("diagram"), "Footer should contain diagram 
hint");
     }
 
     @Test

Reply via email to