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 817ca08ec940 chore: camel-jbang - a view bar on every tab with views 
(Inspect, Errors, Route, Endpoint, HTTP, Metrics, Memory Leak), as the Diagram 
tab (#27203)
817ca08ec940 is described below

commit 817ca08ec9404252692f44ae6c9ada02b992ecd3
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu Oct 1 15:12:21 2026 +0200

    chore: camel-jbang - a view bar on every tab with views (Inspect, Errors, 
Route, Endpoint, HTTP, Metrics, Memory Leak), as the Diagram tab (#27203)
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
---
 .../dsl/jbang/core/commands/tui/AbstractTab.java   |   6 +
 .../dsl/jbang/core/commands/tui/CamelMonitor.java  |  27 ++++
 .../jbang/core/commands/tui/DiagramLevelBar.java   | 131 ----------------
 .../dsl/jbang/core/commands/tui/DiagramTab.java    | 110 ++++++-------
 .../dsl/jbang/core/commands/tui/EndpointsTab.java  |  30 ++--
 .../dsl/jbang/core/commands/tui/ErrorsTab.java     |  42 ++++-
 .../dsl/jbang/core/commands/tui/HistoryTab.java    |  54 +++++--
 .../camel/dsl/jbang/core/commands/tui/HttpTab.java |  28 +++-
 .../dsl/jbang/core/commands/tui/MemoryLeakTab.java |  20 ++-
 .../dsl/jbang/core/commands/tui/MetricsTab.java    |  28 +++-
 .../dsl/jbang/core/commands/tui/MonitorTab.java    |  19 ++-
 .../dsl/jbang/core/commands/tui/RoutesTab.java     |  40 ++++-
 .../dsl/jbang/core/commands/tui/SubViewBar.java    | 173 +++++++++++++++++++++
 .../core/commands/tui/DiagramLevelBarTest.java     |  67 --------
 .../core/commands/tui/EndpointsTabRenderTest.java  |   6 +-
 .../core/commands/tui/ErrorsTabRenderTest.java     |   7 +-
 .../core/commands/tui/MetricsTabRenderTest.java    |   5 +-
 .../jbang/core/commands/tui/SubViewBarTest.java    |  98 ++++++++++++
 18 files changed, 583 insertions(+), 308 deletions(-)

diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AbstractTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AbstractTab.java
index fd1249cc3673..3ccc74ce9fe1 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AbstractTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AbstractTab.java
@@ -26,6 +26,7 @@ import dev.tamboui.terminal.Frame;
 import dev.tamboui.text.Line;
 import dev.tamboui.text.Span;
 import dev.tamboui.text.Text;
+import dev.tamboui.tui.event.KeyEvent;
 import dev.tamboui.tui.event.MouseEvent;
 import dev.tamboui.widgets.block.Block;
 import dev.tamboui.widgets.block.BorderType;
@@ -41,6 +42,11 @@ import dev.tamboui.widgets.table.TableState;
 
 abstract class AbstractTab implements MonitorTab {
 
+    /** Presses a key of the tab, as a click on a view of the view bar does. */
+    protected void pressKey(char key) {
+        handleKeyEvent(KeyEvent.ofChar(key));
+    }
+
     protected final MonitorContext ctx;
 
     protected AbstractTab(MonitorContext ctx) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
index 61bd5a2aac2d..32fec82f9560 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
@@ -222,6 +222,8 @@ public class CamelMonitor extends CamelCommand {
     private String lastTabDivider;
     // Panel resize drag state
     private final DragSplit panelSplit = new DragSplit();
+    /** The views and view settings of the active tab, on the first row of its 
content. */
+    private final SubViewBar subViewBar = new SubViewBar();
     // Footer key-binding hit-testing: each clickable hint records its 
[startX, endX) column range on
     // the footer row and the KeyEvent to synthesize when clicked.
     private int footerRowY = -1;
@@ -1664,6 +1666,23 @@ public class CamelMonitor extends CamelCommand {
             return true;
         }
 
+        // The view bar of the tab: a click on a view goes there, a click on a 
view setting presses its key
+        if (subViewBar.isOnRow(me.y()) && TuiHelper.contains(lastContentArea, 
me.x(), me.y())
+                && !popupManager.isMorePopupVisible() && 
!popupManager.isSwitchPopupVisible()) {
+            if (me.isClick()) {
+                SubViewBar.View view = subViewBar.viewAt(me.x(), me.y());
+                if (view != null) {
+                    view.select().run();
+                    return true;
+                }
+                KeyEvent key = footerKeyEvent(subViewBar.keyAt(me.x(), 
me.y()));
+                if (key != null) {
+                    return handleEvent(key, runner);
+                }
+            }
+            return true;
+        }
+
         // Mouse events in the content area: delegate to the active tab
         if (TuiHelper.contains(lastContentArea, me.x(), me.y())) {
             if (popupManager.isMorePopupVisible() || 
popupManager.isSwitchPopupVisible()) {
@@ -2283,6 +2302,14 @@ public class CamelMonitor extends CamelCommand {
         frame.buffer().clear(area);
         MonitorTab tab = tabRegistry.activeTab();
         if (tab != null) {
+            SubViewBar.Spec spec = tab.subViewBar();
+            if (spec != null && area.height() > 3) {
+                List<Rect> rows = 
Layout.vertical().constraints(Constraint.length(1), 
Constraint.fill()).split(area);
+                subViewBar.render(frame, rows.get(0), spec);
+                area = rows.get(1);
+            } else {
+                subViewBar.clear();
+            }
             tab.render(frame, area);
         }
     }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramLevelBar.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramLevelBar.java
deleted file mode 100644
index f1fa9b4ade97..000000000000
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramLevelBar.java
+++ /dev/null
@@ -1,131 +0,0 @@
-/*
- * 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.CharWidth;
-import dev.tamboui.text.Line;
-import dev.tamboui.text.Span;
-import dev.tamboui.text.Text;
-import dev.tamboui.widgets.paragraph.Paragraph;
-
-/**
- * The zoom levels of the Diagram tab as one line above the diagram 
(CAMEL-25147): Architecture › Topology › Route. The
- * level shown is highlighted, the others are dim but visible so the user 
knows they exist, and a level that does not
- * apply yet (no route selected) is greyed out. The labels carry the path: the 
capability a topology is filtered to, the
- * route a diagram shows. A click on a level goes there.
- */
-final class DiagramLevelBar {
-
-    enum Level {
-        ARCHITECTURE,
-        TOPOLOGY,
-        ROUTE
-    }
-
-    /** One level as the bar shows it. */
-    record Segment(Level level, String label, boolean enabled) {
-    }
-
-    /** A view setting of the level shown: its key, name and state (on, off, 
or a mode such as edges). */
-    record Toggle(String key, String label, String state) {
-
-        boolean on() {
-            return !"off".equals(state);
-        }
-    }
-
-    private static final String SEPARATOR = "  ›  ";
-
-    private final List<int[]> hitBoxes = new ArrayList<>();
-    private final List<Level> hitLevels = new ArrayList<>();
-    private int row = -1;
-
-    /** Draws the bar in a one-row area, with the view settings of the level 
at the right. */
-    void render(Frame frame, Rect area, List<Segment> segments, Level current, 
List<Toggle> toggles) {
-        hitBoxes.clear();
-        hitLevels.clear();
-        row = area.y();
-        List<Span> spans = new ArrayList<>();
-        // the key that moves through the levels, as the settings on the right 
show theirs
-        String key = " v ";
-        spans.add(Span.raw(" "));
-        spans.add(Span.styled(key, Theme.hintKey()));
-        spans.add(Span.raw(" "));
-        int x = area.x() + 1 + CharWidth.of(key) + 1;
-        for (int i = 0; i < segments.size(); i++) {
-            Segment s = segments.get(i);
-            if (i > 0) {
-                spans.add(Span.styled(SEPARATOR, Theme.muted()));
-                x += CharWidth.of(SEPARATOR);
-            }
-            boolean active = s.level() == current;
-            String text = (active ? "◆ " : "◇ ") + s.label();
-            Style style = active ? Theme.accentBg()
-                    : s.enabled() ? Style.EMPTY.fg(Theme.baseFg()) : 
Theme.muted().dim();
-            String shown = active ? " " + text + " " : text;
-            spans.add(Span.styled(shown, style));
-            int w = CharWidth.of(shown);
-            hitBoxes.add(new int[] { x, x + w });
-            hitLevels.add(s.enabled() ? s.level() : null);
-            x += w;
-        }
-        int used = x - area.x();
-        // the settings at the right, as many as fit
-        List<Span> right = new ArrayList<>();
-        int rightWidth = 0;
-        for (Toggle t : toggles) {
-            // an on/off setting shows its state as a mark, a setting with 
modes names the mode
-            boolean onOff = "on".equals(t.state()) || "off".equals(t.state());
-            String chip = " " + t.key() + " ";
-            String label = " " + t.label() + (onOff ? "" : ": " + t.state()) + 
" ";
-            String mark = (onOff ? (t.on() ? "\u25cf" : "\u25cb") : "") + "  ";
-            int w = CharWidth.of(chip) + CharWidth.of(label) + 
CharWidth.of(mark);
-            if (used + rightWidth + w + 2 > area.width()) {
-                break;
-            }
-            right.add(Span.styled(chip, Theme.hintKey()));
-            right.add(Span.raw(label));
-            right.add(Span.styled(mark, t.on() ? 
Style.EMPTY.fg(Theme.accent()) : Theme.muted()));
-            rightWidth += w;
-        }
-        if (!right.isEmpty()) {
-            spans.add(Span.raw(" ".repeat(area.width() - used - rightWidth)));
-            spans.addAll(right);
-        }
-        
frame.renderWidget(Paragraph.builder().text(Text.from(Line.from(spans))).build(),
 area);
-    }
-
-    /** The level under a click on the bar, or null when the click is 
elsewhere or on a level that does not apply. */
-    Level hit(int x, int y) {
-        if (y != row) {
-            return null;
-        }
-        for (int i = 0; i < hitBoxes.size(); i++) {
-            int[] box = hitBoxes.get(i);
-            if (x >= box[0] && x < box[1]) {
-                return hitLevels.get(i);
-            }
-        }
-        return null;
-    }
-}
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 fe251b887402..cdee18e4f199 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
@@ -48,6 +48,13 @@ import static 
org.apache.camel.dsl.jbang.core.commands.tui.TuiHelper.*;
 
 class DiagramTab extends AbstractTab {
 
+    /** The zoom levels of the tab (CAMEL-25147): Architecture › Topology › 
Route. */
+    enum Level {
+        ARCHITECTURE,
+        TOPOLOGY,
+        ROUTE
+    }
+
     private final DiagramSupport diagram = new DiagramSupport();
     private final SourceViewer sourceViewer = new SourceViewer();
     private final GotoNodePopup gotoNodePopup = new GotoNodePopup();
@@ -64,7 +71,6 @@ class DiagramTab extends AbstractTab {
     private final DiagramDetailSupport detail = new DiagramDetailSupport(ctx, 
diagram);
     private final ArchitectureView architecture
             = new ArchitectureView(ctx, this::focusGroup, () -> 
diagram.isShowDescription());
-    private final DiagramLevelBar levelBar = new DiagramLevelBar();
     /** Whether the user chose descriptions on or off with n; until then they 
are on when the routes have any. */
     private boolean descriptionChosen;
     /** Whether the topology shows each route's group; null until the user 
presses g (then on when there are groups). */
@@ -385,13 +391,6 @@ class DiagramTab extends AbstractTab {
         if (gotoNodePopup.isVisible()) {
             return true;
         }
-        if (me.isClick()) {
-            DiagramLevelBar.Level clicked = levelBar.hit(me.x(), me.y());
-            if (clicked != null) {
-                goToLevel(clicked);
-                return true;
-            }
-        }
         if (detailMode && detail.containsMouse(me.x(), me.y())) {
             if (me.kind() == MouseEventKind.SCROLL_UP) {
                 detail.scrollBy(-3);
@@ -445,7 +444,7 @@ class DiagramTab extends AbstractTab {
         }
         if (topologyMode && diagram.isShowDiagram()) {
             // Esc zooms out: from the topology up to the architecture
-            goToLevel(DiagramLevelBar.Level.ARCHITECTURE);
+            goToLevel(Level.ARCHITECTURE);
             return true;
         }
         if (!topologyMode) {
@@ -526,11 +525,11 @@ class DiagramTab extends AbstractTab {
     // ---- zoom levels (CAMEL-25147) ----
 
     /** The level shown: the capability groups, the topology of all routes, or 
one route's diagram. */
-    DiagramLevelBar.Level level() {
+    Level level() {
         if (architecture.isActive()) {
-            return DiagramLevelBar.Level.ARCHITECTURE;
+            return Level.ARCHITECTURE;
         }
-        return topologyMode ? DiagramLevelBar.Level.TOPOLOGY : 
DiagramLevelBar.Level.ROUTE;
+        return topologyMode ? Level.TOPOLOGY : Level.ROUTE;
     }
 
     /**
@@ -550,20 +549,20 @@ class DiagramTab extends AbstractTab {
     /** v: architecture, topology, route, and round again; the route level is 
skipped when no route is selected. */
     private void cycleLevel() {
         goToLevel(switch (level()) {
-            case ARCHITECTURE -> DiagramLevelBar.Level.TOPOLOGY;
-            case TOPOLOGY -> routeCandidate() != null ? 
DiagramLevelBar.Level.ROUTE : DiagramLevelBar.Level.ARCHITECTURE;
-            case ROUTE -> DiagramLevelBar.Level.ARCHITECTURE;
+            case ARCHITECTURE -> Level.TOPOLOGY;
+            case TOPOLOGY -> routeCandidate() != null ? Level.ROUTE : 
Level.ARCHITECTURE;
+            case ROUTE -> Level.ARCHITECTURE;
         });
     }
 
-    void goToLevel(DiagramLevelBar.Level target) {
-        DiagramLevelBar.Level now = level();
+    void goToLevel(Level target) {
+        Level now = level();
         if (target == now) {
             return;
         }
         switch (target) {
             case ARCHITECTURE -> {
-                if (now == DiagramLevelBar.Level.ROUTE) {
+                if (now == Level.ROUTE) {
                     leaveRoute();
                 }
                 diagram.setFocus(null, null);
@@ -574,7 +573,7 @@ class DiagramTab extends AbstractTab {
                 }
             }
             case TOPOLOGY -> {
-                if (now == DiagramLevelBar.Level.ROUTE) {
+                if (now == Level.ROUTE) {
                     leaveRoute();
                 } else if (!architecture.openSelectedGroup()) {
                     architecture.close();
@@ -633,19 +632,25 @@ class DiagramTab extends AbstractTab {
         }
     }
 
-    private void renderLevelBar(Frame frame, Rect row) {
-        DiagramLevelBar.Level current = level();
+    @Override
+    public SubViewBar.Spec subViewBar() {
+        if (ctx.findSelectedIntegration() == null || sourceViewer.isVisible() 
|| !diagram.isShowDiagram()) {
+            return null;
+        }
+        Level current = level();
         String group = diagram.getFocusName();
         String route = routeCandidate();
-        List<DiagramLevelBar.Segment> segments = List.of(
-                new 
DiagramLevelBar.Segment(DiagramLevelBar.Level.ARCHITECTURE, "Architecture", 
true),
-                new DiagramLevelBar.Segment(
-                        DiagramLevelBar.Level.TOPOLOGY,
-                        group != null ? "Topology \u00b7 " + group : 
"Topology", true),
-                new DiagramLevelBar.Segment(
-                        DiagramLevelBar.Level.ROUTE,
-                        route != null ? "Route: " + route : "Route", route != 
null));
-        levelBar.render(frame, row, segments, current, viewToggles());
+        List<SubViewBar.View> views = List.of(
+                new SubViewBar.View(
+                        "Architecture", current == Level.ARCHITECTURE, true,
+                        () -> goToLevel(Level.ARCHITECTURE)),
+                new SubViewBar.View(
+                        group != null ? "Topology \u00b7 " + group : 
"Topology", current == Level.TOPOLOGY,
+                        true, () -> goToLevel(Level.TOPOLOGY)),
+                new SubViewBar.View(
+                        route != null ? "Route: " + route : "Route", current 
== Level.ROUTE, route != null,
+                        () -> goToLevel(Level.ROUTE)));
+        return new SubViewBar.Spec("v", views, viewToggles(), true);
     }
 
     /**
@@ -685,34 +690,34 @@ class DiagramTab extends AbstractTab {
     }
 
     /** The view settings of the level shown, with their state: they sit on 
the level bar, beside the diagram. */
-    List<DiagramLevelBar.Toggle> viewToggles() {
-        List<DiagramLevelBar.Toggle> toggles = new ArrayList<>();
+    List<SubViewBar.Toggle> viewToggles() {
+        List<SubViewBar.Toggle> toggles = new ArrayList<>();
         if (IntegrationSummaryHints.settingEnabled()) {
             // without a summary yet the setting is off, and a has the AI 
write one
-            toggles.add(new DiagramLevelBar.Toggle(
+            toggles.add(new SubViewBar.Toggle(
                     "a", "ai", hasAiHints() && 
IntegrationSummaryHints.isShown() ? "on" : "off"));
         }
         toggles.addAll(levelToggles());
         return toggles;
     }
 
-    private List<DiagramLevelBar.Toggle> levelToggles() {
-        DiagramLevelBar.Toggle view
-                = new DiagramLevelBar.Toggle("b", "view", 
diagram.isShowDescription() ? "business" : "technical");
+    private List<SubViewBar.Toggle> levelToggles() {
+        SubViewBar.Toggle view
+                = new SubViewBar.Toggle("b", "view", 
diagram.isShowDescription() ? "business" : "technical");
         String metrics = diagramMetrics ? "on" : "off";
         return switch (level()) {
             case ARCHITECTURE -> List.of(view,
-                    new DiagramLevelBar.Toggle("u", "utility", 
architecture.isShowUtility() ? "on" : "off"),
-                    new DiagramLevelBar.Toggle("e", "external", 
architecture.isShowExternal() ? "edges" : "off"));
+                    new SubViewBar.Toggle("u", "utility", 
architecture.isShowUtility() ? "on" : "off"),
+                    new SubViewBar.Toggle("e", "external", 
architecture.isShowExternal() ? "edges" : "off"));
             case TOPOLOGY -> {
-                List<DiagramLevelBar.Toggle> toggles = new ArrayList<>();
+                List<SubViewBar.Toggle> toggles = new ArrayList<>();
                 toggles.add(view);
-                toggles.add(new DiagramLevelBar.Toggle("g", "group", 
isShowGroups() ? "on" : "off"));
+                toggles.add(new SubViewBar.Toggle("g", "group", isShowGroups() 
? "on" : "off"));
                 if (hasUtilityRoutes()) {
-                    toggles.add(new DiagramLevelBar.Toggle("u", "utility", 
architecture.isShowUtility() ? "on" : "off"));
+                    toggles.add(new SubViewBar.Toggle("u", "utility", 
architecture.isShowUtility() ? "on" : "off"));
                 }
-                toggles.add(new DiagramLevelBar.Toggle("m", "metrics", 
metrics));
-                toggles.add(new DiagramLevelBar.Toggle(
+                toggles.add(new SubViewBar.Toggle("m", "metrics", metrics));
+                toggles.add(new SubViewBar.Toggle(
                         "e", "external",
                         switch (externalMode) {
                             case 1 -> "edges";
@@ -721,20 +726,11 @@ class DiagramTab extends AbstractTab {
                         }));
                 yield toggles;
             }
-            case ROUTE -> List.of(view, new DiagramLevelBar.Toggle("m", 
"metrics", metrics),
-                    new DiagramLevelBar.Toggle("d", "detail", detailMode ? 
"on" : "off"));
+            case ROUTE -> List.of(view, new SubViewBar.Toggle("m", "metrics", 
metrics),
+                    new SubViewBar.Toggle("d", "detail", detailMode ? "on" : 
"off"));
         };
     }
 
-    @Override
-    public void renderViewToggles(List<Span> spans) {
-        if (diagram.isShowDiagram() && !sourceViewer.isVisible()) {
-            for (DiagramLevelBar.Toggle t : viewToggles()) {
-                hint(spans, t.key(), t.label() + " [" + t.state() + "]");
-            }
-        }
-    }
-
     /**
      * Opens the route diagram of a route (the Route level), when the 
integration runs that route.
      *
@@ -792,12 +788,6 @@ class DiagramTab extends AbstractTab {
             return;
         }
 
-        if (diagram.isShowDiagram() && area.height() > 3) {
-            List<Rect> rows = 
Layout.vertical().constraints(Constraint.length(1), 
Constraint.fill()).split(area);
-            renderLevelBar(frame, rows.get(0));
-            area = rows.get(1);
-        }
-
         if (architecture.isActive()) {
             architecture.render(frame, area, info.name);
             return;
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java
index c17bdd22c20b..0976b4c7b94e 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java
@@ -384,19 +384,31 @@ class EndpointsTab extends AbstractTableTab {
         }
     }
 
+    @Override
+    public SubViewBar.Spec subViewBar() {
+        if (ctx.findSelectedIntegration() == null) {
+            return null;
+        }
+        String[] filterLabels = { "all", "remote", "remote+stub" };
+        List<SubViewBar.View> views = List.of(
+                new SubViewBar.View("Chart", panelMode == PANEL_CHART, true, 
() -> pressKey('d')),
+                new SubViewBar.View("Detail", panelMode == PANEL_DETAIL, true, 
() -> pressKey('d')));
+        List<SubViewBar.Toggle> toggles = new ArrayList<>();
+        toggles.add(new SubViewBar.Toggle("f", "filter", 
filterLabels[filter]));
+        if (panelMode == PANEL_CHART) {
+            toggles.add(new SubViewBar.Toggle("a", "chart", switch (chartMode) 
{
+                case CHART_ALL -> "all";
+                case CHART_SINGLE -> "single";
+                default -> "off";
+            }));
+        }
+        return new SubViewBar.Spec("d", views, toggles, false);
+    }
+
     @Override
     public void renderFooter(List<Span> spans) {
         hint(spans, "Esc", "back");
         hint(spans, "s", "sort");
-        String[] filterLabels = { "all", "remote", "remote+stub" };
-        hint(spans, "f", "filter [" + filterLabels[filter] + "]");
-        String chartLabel = switch (chartMode) {
-            case CHART_ALL -> "[all]";
-            case CHART_SINGLE -> "[single]";
-            default -> "[off]";
-        };
-        hint(spans, "a", "chart " + chartLabel);
-        hint(spans, "d", "detail " + (panelMode == PANEL_DETAIL ? "[on]" : 
"[off]"));
         if (panelMode == PANEL_DETAIL) {
             hint(spans, "Tab", detailFocused ? "table" : "detail");
         }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTab.java
index d5490b6074b2..ab5923980a1e 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTab.java
@@ -401,6 +401,29 @@ class ErrorsTab extends AbstractTableTab {
         }
     }
 
+    @Override
+    public SubViewBar.Spec subViewBar() {
+        if (ctx.findSelectedIntegration() == null) {
+            return null;
+        }
+        boolean diagramShown = diagram.isShowDiagram();
+        boolean historyDiagram = diagramShown && diagram.isHistoryMode() && 
diagram.hasHistoryData();
+        List<SubViewBar.View> views = List.of(
+                new SubViewBar.View("Errors", !diagramShown, true, () -> 
pressKey('d')),
+                new SubViewBar.View("Diagram", diagramShown, diagramShown || 
selectedErrorHasHistory(), () -> pressKey('d')));
+        List<SubViewBar.Toggle> toggles = new ArrayList<>();
+        if (!diagramShown) {
+            toggles.add(new SubViewBar.Toggle("f", "handled", handledFilter));
+        }
+        if (historyDiagram) {
+            toggles.add(new SubViewBar.Toggle("n", "description", 
diagram.isShowDescription() ? "on" : "off"));
+        }
+        if (!diagramShown || historyDiagram) {
+            toggles.add(new SubViewBar.Toggle("w", "wrap", wordWrap ? "on" : 
"off"));
+        }
+        return new SubViewBar.Spec("d", views, toggles, false);
+    }
+
     @Override
     public void renderFooter(List<Span> spans) {
         if (diagram.isShowDiagram()) {
@@ -415,9 +438,7 @@ class ErrorsTab extends AbstractTableTab {
                     hint(spans, TuiIcons.HINT_NAV, "navigate");
                     hint(spans, "Enter", "drill-down");
                     hint(spans, "i", infoLabel);
-                    hint(spans, "n", "description" + 
(diagram.isShowDescription() ? " [on]" : ""));
                     hintShowBhpv(spans, showBody, showHeaders, showProperties, 
showVariables);
-                    hintLast(spans, "w", "wrap" + (wordWrap ? " [on]" : " 
[off]"));
                 } else {
                     hint(spans, "d", "close");
                     hint(spans, "Esc", "back");
@@ -425,9 +446,7 @@ class ErrorsTab extends AbstractTableTab {
                     hint(spans, TuiIcons.HINT_H, "h-scroll");
                     hint(spans, "t", "topology");
                     hint(spans, "i", infoLabel);
-                    hint(spans, "n", "description" + 
(diagram.isShowDescription() ? " [on]" : ""));
                     hintShowBhpv(spans, showBody, showHeaders, showProperties, 
showVariables);
-                    hintLast(spans, "w", "wrap" + (wordWrap ? " [on]" : " 
[off]"));
                 }
                 return;
             }
@@ -441,10 +460,7 @@ class ErrorsTab extends AbstractTableTab {
         }
         hint(spans, "Home/End", "top/end");
         hint(spans, "s", "sort");
-        hint(spans, "d", "diagram");
-        hint(spans, "f", "handled [" + handledFilter + "]");
         hintShowBhpv(spans, showBody, showHeaders, showProperties, 
showVariables);
-        hint(spans, "w", "wrap [" + (wordWrap ? "on" : "off") + "]");
     }
 
     /** How often this kind of error happened, and how long it has been going 
on (CAMEL-24911). */
@@ -750,6 +766,18 @@ class ErrorsTab extends AbstractTableTab {
 
     // ---- Diagram ----
 
+    /** Whether the selected error has the message history its diagram is 
drawn from. */
+    private boolean selectedErrorHasHistory() {
+        IntegrationInfo info = ctx.findSelectedIntegration();
+        if (info == null || info.errors.isEmpty()) {
+            return false;
+        }
+        List<ErrorInfo> sorted = applyFilter(info.errors);
+        Integer sel = tableState.selected();
+        ErrorInfo selected = sel != null && sel >= 0 && sel < sorted.size() ? 
sorted.get(sel) : null;
+        return selected != null && selected.messageHistory != null && 
selected.messageHistory.length > 0;
+    }
+
     private void loadDiagramForSelectedError() {
         if (ctx.selectedPid == null || ctx.runner == null) {
             return;
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTab.java
index 083ef5512c1d..4b6fed1c60ac 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTab.java
@@ -791,6 +791,46 @@ class HistoryTab extends AbstractTab {
         }
     }
 
+    @Override
+    public SubViewBar.Spec subViewBar() {
+        if (ctx.findSelectedIntegration() == null) {
+            return null;
+        }
+        boolean diagramShown = diagram.isShowDiagram();
+        boolean historyDiagram = diagramShown && diagram.isHistoryMode() && 
diagram.hasHistoryData();
+        boolean tracerActive = !traces.get().isEmpty();
+        boolean exchangeList = tracerActive && !traceDetailView;
+        boolean waterfallApplies = !exchangeList;
+        boolean waterfall = !diagramShown && showWaterfall && waterfallApplies;
+        List<SubViewBar.View> views = List.of(
+                new SubViewBar.View(exchangeList ? "Exchanges" : "Steps", 
!diagramShown && !waterfall, true, () -> {
+                    if (diagram.isShowDiagram()) {
+                        pressKey('d');
+                    }
+                    if (showWaterfall) {
+                        pressKey('g');
+                    }
+                }),
+                new SubViewBar.View("Waterfall", waterfall, waterfallApplies, 
() -> {
+                    if (diagram.isShowDiagram()) {
+                        pressKey('d');
+                    }
+                    if (!showWaterfall) {
+                        pressKey('g');
+                    }
+                }),
+                new SubViewBar.View("Diagram", diagramShown, true, () -> 
pressKey('d')));
+        List<SubViewBar.Toggle> toggles = new ArrayList<>();
+        if (!diagramShown || historyDiagram) {
+            toggles.add(new SubViewBar.Toggle("n", "description", 
showDescription ? "on" : "off"));
+        }
+        boolean traceWrap = historyDiagram ? !diagramTraceSteps.isEmpty() : 
tracerActive;
+        if (historyDiagram || !diagramShown && !waterfall && !exchangeList) {
+            toggles.add(new SubViewBar.Toggle("w", "wrap", (traceWrap ? 
traceWordWrap : historyWordWrap) ? "on" : "off"));
+        }
+        return new SubViewBar.Spec(null, views, toggles, false);
+    }
+
     @Override
     public void renderFooter(List<Span> spans) {
         if (diagram.isShowDiagram()) {
@@ -800,7 +840,6 @@ class HistoryTab extends AbstractTab {
                 boolean sh = isTraceMode ? showTraceHeaders : 
showHistoryHeaders;
                 boolean sp = isTraceMode ? showTraceProperties : 
showHistoryProperties;
                 boolean sv = isTraceMode ? showTraceVariables : 
showHistoryVariables;
-                boolean sw = isTraceMode ? traceWordWrap : historyWordWrap;
                 String infoLabel = switch (infoPanelSize) {
                     case INFO_WIDE -> "info [wide]";
                     case INFO_FULL -> "info [full]";
@@ -811,9 +850,7 @@ class HistoryTab extends AbstractTab {
                     hint(spans, TuiIcons.HINT_NAV, "navigate");
                     hint(spans, "Enter", "drill-down");
                     hint(spans, "i", infoLabel);
-                    hint(spans, "n", "description" + (showDescription ? " 
[on]" : ""));
                     hintShowBhpv(spans, sb, sh, sp, sv);
-                    hintLast(spans, "w", "wrap" + (sw ? " [on]" : " [off]"));
                 } else {
                     hint(spans, "d", "close");
                     hint(spans, "Esc", "back");
@@ -821,9 +858,7 @@ class HistoryTab extends AbstractTab {
                     hint(spans, TuiIcons.HINT_H, "h-scroll");
                     hint(spans, "t", "topology");
                     hint(spans, "i", infoLabel);
-                    hint(spans, "n", "description" + (showDescription ? " 
[on]" : ""));
                     hintShowBhpv(spans, sb, sh, sp, sv);
-                    hintLast(spans, "w", "wrap" + (sw ? " [on]" : " [off]"));
                 }
                 return;
             }
@@ -839,17 +874,14 @@ class HistoryTab extends AbstractTab {
             if (!showWaterfall && !traceWordWrap) {
                 hint(spans, TuiIcons.HINT_H, "h-scroll");
             }
-            hint(spans, "n", "description" + (showDescription ? " [on]" : ""));
-            hint(spans, "g", "waterfall" + (showWaterfall ? " [on]" : ""));
+            hint(spans, "g", "waterfall");
             hint(spans, "d", "diagram");
             if (!showWaterfall) {
                 hintShowBhpv(spans, showTraceBody, showTraceHeaders, 
showTraceProperties, showTraceVariables);
             }
-            hintLast(spans, "w", "wrap" + (traceWordWrap ? " [on]" : " 
[off]"));
         } else if (tracerActive) {
             hint(spans, "Esc", "back");
             hint(spans, "s", "sort");
-            hint(spans, "n", "description" + (showDescription ? " [on]" : ""));
             hint(spans, "d", "diagram");
             hint(spans, "Enter", "details");
             hintLast(spans, "F5", "refresh");
@@ -861,12 +893,10 @@ class HistoryTab extends AbstractTab {
             if (!showWaterfall && !historyWordWrap) {
                 hint(spans, TuiIcons.HINT_H, "h-scroll");
             }
-            hint(spans, "n", "description" + (showDescription ? " [on]" : ""));
-            hint(spans, "g", "waterfall" + (showWaterfall ? " [on]" : ""));
+            hint(spans, "g", "waterfall");
             hint(spans, "d", "diagram");
             if (!showWaterfall) {
                 hintShowBhpv(spans, showHistoryBody, showHistoryHeaders, 
showHistoryProperties, showHistoryVariables);
-                hint(spans, "w", "wrap" + (historyWordWrap ? " [on]" : " 
[off]"));
             }
             hintLast(spans, "F5", "refresh");
         }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HttpTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HttpTab.java
index b53942844fc5..b7a465333646 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HttpTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HttpTab.java
@@ -226,6 +226,31 @@ class HttpTab extends AbstractTableTab {
         renderDetail(frame, chunks.get(1), visible);
     }
 
+    @Override
+    public SubViewBar.Spec subViewBar() {
+        IntegrationInfo info = ctx.findSelectedIntegration();
+        if (info == null) {
+            return null;
+        }
+        List<HttpEndpointInfo> visible = sortedVisibleEndpoints(info);
+        Integer sel = tableState.selected();
+        HttpEndpointInfo selected = sel != null && sel >= 0 && sel < 
visible.size() ? visible.get(sel) : null;
+        boolean table = !probe.isActive() && !showSpec;
+        List<SubViewBar.View> views = List.of(
+                new SubViewBar.View("Endpoints", table, true, 
this::handleEscape),
+                new SubViewBar.View("Probe", probe.isActive(), table && 
selected != null, this::enterProbeModeFromTable),
+                new SubViewBar.View(
+                        "Spec", showSpec, table && selected != null && 
selected.specificationUri != null,
+                        this::loadSpecForSelectedEndpoint));
+        List<SubViewBar.Toggle> toggles = new ArrayList<>();
+        if (table) {
+            String[] filterLabels = { "all", "rest", "http" };
+            toggles.add(new SubViewBar.Toggle("f", "filter", 
filterLabels[filter]));
+            toggles.add(new SubViewBar.Toggle("m", "management", 
showManagement ? "on" : "off"));
+        }
+        return new SubViewBar.Spec(null, views, toggles, false);
+    }
+
     @Override
     public void renderFooter(List<Span> spans) {
         if (probe.isActive()) {
@@ -240,9 +265,6 @@ class HttpTab extends AbstractTableTab {
         hint(spans, "Esc", "back");
         hint(spans, "Enter", "probe");
         hint(spans, "s", "sort");
-        String[] filterLabels = { "all", "rest", "http" };
-        hint(spans, "f", "filter [" + filterLabels[filter] + "]");
-        hint(spans, "m", "management" + (showManagement ? " [on]" : " [off]"));
         List<HttpEndpointInfo> hVisible = 
sortedVisibleEndpoints(ctx.findSelectedIntegration());
         Integer hSel = tableState.selected();
         if (hSel != null && hSel >= 0 && hSel < hVisible.size() && 
hVisible.get(hSel).specificationUri != null) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MemoryLeakTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MemoryLeakTab.java
index 564dfdd58608..24488ece1d90 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MemoryLeakTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MemoryLeakTab.java
@@ -1175,6 +1175,23 @@ class MemoryLeakTab extends AbstractTab {
         };
     }
 
+    @Override
+    public SubViewBar.Spec subViewBar() {
+        if (state != State.HAS_RESULTS) {
+            return null;
+        }
+        String modeLabel = recordingMode == RecordingMode.DUAL ? "dual" : 
"single";
+        List<SubViewBar.View> views = List.of(
+                new SubViewBar.View("Samples", !dominatorView, true, () -> 
pressKey('v')),
+                new SubViewBar.View("Dominators", dominatorView, true, () -> 
pressKey('v')));
+        List<SubViewBar.Toggle> toggles = new ArrayList<>();
+        if (comparisons == null && !dominatorView) {
+            toggles.add(new SubViewBar.Toggle("m", "min-size", 
MIN_SIZE_LABELS[minSizeIndex]));
+        }
+        toggles.add(new SubViewBar.Toggle("d", "mode", modeLabel));
+        return new SubViewBar.Spec("v", views, toggles, false);
+    }
+
     @Override
     public void renderFooter(List<Span> spans) {
         String modeLabel = recordingMode == RecordingMode.DUAL ? "dual" : 
"single";
@@ -1195,11 +1212,8 @@ class MemoryLeakTab extends AbstractTab {
                 hint(spans, "Esc", "back");
                 if (comparisons == null && !dominatorView) {
                     hint(spans, "s", "sort");
-                    hint(spans, "m", "min-size [" + 
MIN_SIZE_LABELS[minSizeIndex] + "]");
                 }
-                hint(spans, "v", dominatorView ? "samples" : "dominators");
                 hint(spans, "r", "new recording");
-                hint(spans, "d", "mode [" + modeLabel + "]");
                 hint(spans, "+/-", "duration [" + duration + "s]");
                 hint(spans, "h", "heap dump");
                 hintLast(spans, "PgUp/Dn", "detail");
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MetricsTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MetricsTab.java
index 88457d728c6d..1733228dbd60 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MetricsTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MetricsTab.java
@@ -612,6 +612,32 @@ class MetricsTab extends AbstractTableTab {
         });
     }
 
+    @Override
+    public SubViewBar.Spec subViewBar() {
+        IntegrationInfo info = ctx.findSelectedIntegration();
+        if (info == null) {
+            return null;
+        }
+        boolean raw = findMetricsUrl(info) != null;
+        List<SubViewBar.View> views = List.of(
+                new SubViewBar.View("Dashboard", !showRaw && !tableMode, true, 
() -> {
+                    handleEscape();
+                    if (tableMode) {
+                        pressKey('d');
+                    }
+                }),
+                new SubViewBar.View("Table", !showRaw && tableMode, true, () 
-> {
+                    handleEscape();
+                    if (!tableMode) {
+                        pressKey('d');
+                    }
+                }),
+                new SubViewBar.View("Prometheus", showRaw, raw, () -> 
pressKey('r')));
+        List<SubViewBar.Toggle> toggles = !showRaw && tableMode
+                ? List.of(new SubViewBar.Toggle("f", "filter", filterType)) : 
List.of();
+        return new SubViewBar.Spec("d", views, toggles, false);
+    }
+
     @Override
     public void renderFooter(List<Span> spans) {
         if (showRaw) {
@@ -621,10 +647,8 @@ class MetricsTab extends AbstractTableTab {
             return;
         }
         hint(spans, "Esc", "back");
-        hint(spans, "d", tableMode ? "dashboard" : "table");
         if (tableMode) {
             hint(spans, "s", "sort");
-            hint(spans, "f", "filter:" + filterType);
         }
         IntegrationInfo info = ctx.findSelectedIntegration();
         if (info != null && findMetricsUrl(info) != null) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorTab.java
index cd62efd24b7f..7aff33679d51 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorTab.java
@@ -56,10 +56,25 @@ interface MonitorTab {
     void render(Frame frame, Rect area);
 
     /**
-     * The view settings of the tab and their state, as key hints with {@code 
[state]}: shown in the tab itself (the
-     * Diagram tab's level bar) rather than the footer, and listed with the 
footer actions for MCP agents.
+     * The views of the tab and its view settings, shown as a bar on the first 
row of the tab (the levels of the Diagram
+     * tab, the views of the Inspect tab); null for a tab without views. The 
settings shown there are left out of the
+     * footer.
+     */
+    default SubViewBar.Spec subViewBar() {
+        return null;
+    }
+
+    /**
+     * The view settings of the tab and their state, as key hints with {@code 
[state]}: shown in the tab's view bar
+     * rather than the footer, and listed with the footer actions for MCP 
agents.
      */
     default void renderViewToggles(List<Span> spans) {
+        SubViewBar.Spec spec = subViewBar();
+        if (spec != null) {
+            for (SubViewBar.Toggle t : spec.toggles()) {
+                TuiHelper.hint(spans, t.key(), t.label() + " [" + t.state() + 
"]");
+            }
+        }
     }
 
     default void renderFooter(List<Span> spans) {
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 3a3b6d807bb1..8b3cc0321f01 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
@@ -869,6 +869,38 @@ class RoutesTab extends AbstractTab {
                 .build();
     }
 
+    @Override
+    public SubViewBar.Spec subViewBar() {
+        if (ctx.findSelectedIntegration() == null || sourceViewer.isVisible()) 
{
+            return null;
+        }
+        boolean diagramShown = diagram.isShowDiagram();
+        List<SubViewBar.View> views = List.of(
+                new SubViewBar.View("Routes", !diagramShown, true, () -> {
+                    // Esc goes back a level at a time: from a route's diagram 
to the topology, then to the table
+                    for (int i = 0; i < 3 && diagram.isShowDiagram(); i++) {
+                        handleEscape();
+                    }
+                }),
+                new SubViewBar.View("Diagram", diagramShown, true, () -> 
handleKeyEvent(KeyEvent.ofKey(KeyCode.ENTER))));
+        List<SubViewBar.Toggle> toggles = new ArrayList<>();
+        if (diagramShown) {
+            toggles.add(new SubViewBar.Toggle("m", "metrics", diagramMetrics ? 
"on" : "off"));
+            if (topologyMode) {
+                toggles.add(new SubViewBar.Toggle(
+                        "e", "external",
+                        EXTERNAL_LABELS[externalMode].trim().replace("[", 
"").replace("]", "")));
+            } else if (!diagram.getEipNodeBoxes().isEmpty()) {
+                toggles.add(new SubViewBar.Toggle("d", "detail", detailMode ? 
"on" : "off"));
+            }
+            toggles.add(new SubViewBar.Toggle("n", "description", 
diagram.isShowDescription() ? "on" : "off"));
+        } else {
+            toggles.add(new SubViewBar.Toggle("n", "description", 
showDescription ? "on" : "off"));
+            toggles.add(new SubViewBar.Toggle("t", "top", routeTopMode ? "on" 
: "off"));
+        }
+        return new SubViewBar.Spec(null, views, toggles, false);
+    }
+
     @Override
     public void renderFooter(List<Span> spans) {
         if (sourceViewer.isVisible()) {
@@ -878,7 +910,6 @@ class RoutesTab extends AbstractTab {
                 hint(spans, "Esc", "back");
                 hint(spans, "t", "topology");
                 hint(spans, TuiIcons.HINT_NAV, "navigate");
-                hint(spans, "d", "detail" + (detailMode ? " [on]" : " [off]"));
                 hint(spans, "g", "go to");
                 hint(spans, "PgUp/PgDn", detailMode ? "detail" : "page");
                 hint(spans, "c", "source");
@@ -896,17 +927,10 @@ class RoutesTab extends AbstractTab {
             } else {
                 diagram.renderFooterHints(spans);
             }
-            hint(spans, "m", "metrics" + (diagramMetrics ? " [on]" : " 
[off]"));
-            if (topologyMode) {
-                hint(spans, "e", "external" + EXTERNAL_LABELS[externalMode]);
-            }
-            hint(spans, "n", "description" + (diagram.isShowDescription() ? " 
[on]" : " [off]"));
         } else {
             hint(spans, "Esc", "back");
             hint(spans, "Enter", "diagram");
             hint(spans, "s", "sort");
-            hint(spans, "n", "description" + (showDescription ? " [on]" : " 
[off]"));
-            hint(spans, "t", routeTopMode ? "top [on]" : "top [off]");
             if (!routeTopMode) {
                 hint(spans, "c", "source");
                 String routeState = selectedRouteState();
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SubViewBar.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SubViewBar.java
new file mode 100644
index 000000000000..4867202f8045
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SubViewBar.java
@@ -0,0 +1,173 @@
+/*
+ * 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.CharWidth;
+import dev.tamboui.text.Line;
+import dev.tamboui.text.Span;
+import dev.tamboui.text.Text;
+import dev.tamboui.widgets.paragraph.Paragraph;
+
+/**
+ * The views of a tab as one line above its content, with the view settings at 
the right: Architecture › Topology ›
+ * Route in the Diagram tab, History │ Waterfall │ Diagram in the Inspect tab. 
The view shown is highlighted, the others
+ * are dim but visible so the user knows they exist, and a view that does not 
apply yet is greyed out. A click on a view
+ * goes there; a click on a setting presses its key, as a click on a footer 
hint does.
+ */
+final class SubViewBar {
+
+    /**
+     * One view of a tab.
+     *
+     * @param label   what the bar shows
+     * @param active  whether it is the view shown
+     * @param enabled whether it applies now (a route view needs a route)
+     * @param select  goes to the view; null when it cannot be chosen by a 
click
+     */
+    record View(String label, boolean active, boolean enabled, Runnable 
select) {
+    }
+
+    /** A view setting: its key, name and state (on, off, or a mode such as a 
filter value). */
+    record Toggle(String key, String label, String state) {
+
+        boolean on() {
+            return !"off".equals(state);
+        }
+    }
+
+    /**
+     * What a tab shows in the bar.
+     *
+     * @param cycleKey the key that moves through the views, or null
+     * @param levels   whether the views are levels of one another (separated 
by ›) rather than peers (│)
+     */
+    record Spec(String cycleKey, List<View> views, List<Toggle> toggles, 
boolean levels) {
+    }
+
+    private record Hit(int from, int to, View view, String key) {
+    }
+
+    private final List<Hit> hits = new ArrayList<>();
+    private int row = -1;
+
+    /** Draws the bar in a one-row area. */
+    void render(Frame frame, Rect area, Spec spec) {
+        hits.clear();
+        row = area.y();
+        List<Span> spans = new ArrayList<>();
+        spans.add(Span.raw(" "));
+        int x = area.x() + 1;
+        if (spec.cycleKey() != null) {
+            String key = " " + spec.cycleKey() + " ";
+            spans.add(Span.styled(key, Theme.hintKey()));
+            spans.add(Span.raw(" "));
+            x += CharWidth.of(key) + 1;
+        }
+        String separator = spec.levels() ? "  ›  " : "  │  ";
+        List<View> views = spec.views();
+        for (int i = 0; i < views.size(); i++) {
+            View v = views.get(i);
+            if (i > 0) {
+                spans.add(Span.styled(separator, Theme.muted()));
+                x += CharWidth.of(separator);
+            }
+            String text = (v.active() ? "◆ " : "◇ ") + v.label();
+            Style style = v.active() ? Theme.accentBg()
+                    : v.enabled() ? Style.EMPTY.fg(Theme.baseFg()) : 
Theme.muted().dim();
+            String shown = v.active() ? " " + text + " " : text;
+            spans.add(Span.styled(shown, style));
+            int w = CharWidth.of(shown);
+            if (v.enabled() && !v.active() && v.select() != null) {
+                hits.add(new Hit(x, x + w, v, null));
+            }
+            x += w;
+        }
+        int used = x - area.x();
+        // the settings at the right, as many as fit
+        List<Span> right = new ArrayList<>();
+        List<int[]> rightBoxes = new ArrayList<>();
+        List<String> rightKeys = new ArrayList<>();
+        int rightWidth = 0;
+        for (Toggle t : spec.toggles()) {
+            // an on/off setting shows its state as a mark, a setting with 
modes names the mode
+            boolean onOff = "on".equals(t.state()) || "off".equals(t.state());
+            String chip = " " + t.key() + " ";
+            String label = " " + t.label() + (onOff ? "" : ": " + t.state()) + 
" ";
+            String mark = (onOff ? (t.on() ? "●" : "○") : "") + "  ";
+            int w = CharWidth.of(chip) + CharWidth.of(label) + 
CharWidth.of(mark);
+            if (used + rightWidth + w + 2 > area.width()) {
+                break;
+            }
+            right.add(Span.styled(chip, Theme.hintKey()));
+            right.add(Span.raw(label));
+            right.add(Span.styled(mark, t.on() ? 
Style.EMPTY.fg(Theme.accent()) : Theme.muted()));
+            rightBoxes.add(new int[] { rightWidth, rightWidth + w - 2 });
+            rightKeys.add(t.key());
+            rightWidth += w;
+        }
+        if (!right.isEmpty()) {
+            int start = area.x() + area.width() - rightWidth;
+            spans.add(Span.raw(" ".repeat(area.width() - used - rightWidth)));
+            spans.addAll(right);
+            for (int i = 0; i < rightBoxes.size(); i++) {
+                hits.add(new Hit(start + rightBoxes.get(i)[0], start + 
rightBoxes.get(i)[1], null, rightKeys.get(i)));
+            }
+        }
+        
frame.renderWidget(Paragraph.builder().text(Text.from(Line.from(spans))).build(),
 area);
+    }
+
+    /** The view under a click on the bar, or null. */
+    View viewAt(int x, int y) {
+        Hit h = hitAt(x, y);
+        return h != null ? h.view() : null;
+    }
+
+    /** The key of the setting under a click on the bar, or null. */
+    String keyAt(int x, int y) {
+        Hit h = hitAt(x, y);
+        return h != null ? h.key() : null;
+    }
+
+    /** Whether the bar was drawn on the row: a click there is the bar's, even 
between its items. */
+    boolean isOnRow(int y) {
+        return row >= 0 && y == row;
+    }
+
+    /** Forgets where the bar was, when a tab without one is shown. */
+    void clear() {
+        hits.clear();
+        row = -1;
+    }
+
+    private Hit hitAt(int x, int y) {
+        if (y != row) {
+            return null;
+        }
+        for (Hit h : hits) {
+            if (x >= h.from() && x < h.to()) {
+                return h;
+            }
+        }
+        return null;
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramLevelBarTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramLevelBarTest.java
deleted file mode 100644
index bb31eff1471d..000000000000
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramLevelBarTest.java
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * 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.List;
-
-import dev.tamboui.buffer.Buffer;
-import dev.tamboui.layout.Rect;
-import dev.tamboui.terminal.Frame;
-import org.apache.camel.dsl.jbang.core.commands.tui.DiagramLevelBar.Level;
-import org.apache.camel.dsl.jbang.core.commands.tui.DiagramLevelBar.Segment;
-import org.junit.jupiter.api.Test;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNull;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-/**
- * The Diagram tab's level bar (CAMEL-25147): every level shown, the current 
one marked, the path in the labels, and a
- * click goes to a level unless it does not apply.
- */
-class DiagramLevelBarTest {
-
-    @Test
-    void showsAllLevelsAndTheCurrentOne() {
-        DiagramLevelBar bar = new DiagramLevelBar();
-        Rect area = new Rect(0, 0, 160, 1);
-        Buffer buffer = Buffer.empty(area);
-        bar.render(Frame.forTesting(buffer), area, List.of(
-                new Segment(Level.ARCHITECTURE, "Architecture", true),
-                new Segment(Level.TOPOLOGY, "Topology: Order intake", true),
-                new Segment(Level.ROUTE, "Route", false)), Level.TOPOLOGY,
-                List.of(new DiagramLevelBar.Toggle("b", "view", "business"),
-                        new DiagramLevelBar.Toggle("e", "external", "edges"),
-                        new DiagramLevelBar.Toggle("m", "metrics", "off")));
-        String line = TuiTestHelper.bufferToString(buffer);
-        assertTrue(line.contains("◇ Architecture"), line);
-        assertTrue(line.indexOf(" v ") >= 0 && line.indexOf(" v ") < 
line.indexOf("Architecture"),
-                "the key that moves through the levels comes first: " + line);
-        assertTrue(line.contains("◆ Topology: Order intake"), "the current 
level: " + line);
-        assertTrue(line.contains("◇ Route"), line);
-        // the view settings of the level, with their state, beside the diagram
-        assertTrue(line.contains(" b  view: business "), "a mode is named: " + 
line);
-        assertTrue(line.contains(" e  external: edges "), line);
-        assertTrue(line.contains(" m  metrics \u25cb"), line);
-
-        int capabilities = line.indexOf("Architecture");
-        assertEquals(Level.ARCHITECTURE, bar.hit(capabilities, 0));
-        assertEquals(Level.TOPOLOGY, bar.hit(line.indexOf("Order intake"), 0));
-        assertNull(bar.hit(line.indexOf("Route"), 0), "a level that does not 
apply");
-        assertNull(bar.hit(capabilities, 1), "another row");
-    }
-}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTabRenderTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTabRenderTest.java
index 8c766dabe5b1..d9f09dc77c34 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTabRenderTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTabRenderTest.java
@@ -193,7 +193,11 @@ class EndpointsTabRenderTest {
 
         assertTrue(footer.contains("Esc"), "Footer should contain Esc hint");
         assertTrue(footer.contains("sort"), "Footer should contain sort hint");
-        assertTrue(footer.contains("filter"), "Footer should contain filter 
hint");
+        // the filter is a view setting: in the view bar, with the Chart and 
Detail views
+        SubViewBar.Spec bar = tab.subViewBar();
+        assertTrue(bar.toggles().stream().anyMatch(t -> t.key().equals("f") && 
t.state().equals("all")),
+                "View bar should contain the filter");
+        assertTrue(bar.views().stream().anyMatch(v -> 
v.label().equals("Detail")), "View bar should list Detail");
     }
 
     // ---- Helper methods ----
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTabRenderTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTabRenderTest.java
index 30bf5da8c013..f69d33048add 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTabRenderTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTabRenderTest.java
@@ -248,8 +248,11 @@ class ErrorsTabRenderTest {
 
         assertTrue(footer.contains("Esc"), "Footer should contain Esc hint");
         assertTrue(footer.contains("sort"), "Footer should contain sort hint");
-        assertTrue(footer.contains("handled"), "Footer should contain handled 
filter hint");
-        assertTrue(footer.contains("wrap"), "Footer should contain wrap hint");
+        // the handled filter and wrap are view settings: in the view bar, 
with the Errors and Diagram views
+        SubViewBar.Spec bar = tab.subViewBar();
+        assertTrue(bar.toggles().stream().anyMatch(t -> t.key().equals("f") && 
t.label().equals("handled")),
+                "View bar should contain the handled filter");
+        assertTrue(bar.toggles().stream().anyMatch(t -> t.key().equals("w")), 
"View bar should contain wrap");
     }
 
     @Test
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/MetricsTabRenderTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/MetricsTabRenderTest.java
index 069c92547083..a7dcb4357219 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/MetricsTabRenderTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/MetricsTabRenderTest.java
@@ -146,7 +146,10 @@ class MetricsTabRenderTest {
                 .reduce("", String::concat);
 
         assertTrue(footer.contains("Esc"), "Footer should contain Esc hint");
-        assertTrue(footer.contains("d"), "Footer should contain 'd' toggle 
hint");
+        // dashboard and table are views of the view bar, d moves between them
+        SubViewBar.Spec bar = tab.subViewBar();
+        assertTrue(bar != null && "d".equals(bar.cycleKey()), "View bar should 
be moved through with d");
+        assertTrue(bar.views().stream().anyMatch(v -> 
v.label().equals("Table")), "View bar should list Table");
     }
 
     @Test
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SubViewBarTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SubViewBarTest.java
new file mode 100644
index 000000000000..7216366dc5f0
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SubViewBarTest.java
@@ -0,0 +1,98 @@
+/*
+ * 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.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+import dev.tamboui.buffer.Buffer;
+import dev.tamboui.layout.Rect;
+import dev.tamboui.terminal.Frame;
+import org.apache.camel.dsl.jbang.core.commands.tui.SubViewBar.Spec;
+import org.apache.camel.dsl.jbang.core.commands.tui.SubViewBar.Toggle;
+import org.apache.camel.dsl.jbang.core.commands.tui.SubViewBar.View;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * The view bar of a tab: every view shown, the current one marked, a click 
goes to a view unless it does not apply, and
+ * a click on a view setting presses its key.
+ */
+class SubViewBarTest {
+
+    private final AtomicReference<String> selected = new AtomicReference<>();
+
+    private Spec levels() {
+        return new Spec(
+                "v", List.of(
+                        new View("Architecture", false, true, () -> 
selected.set("architecture")),
+                        new View("Topology · Order intake", true, true, () -> 
selected.set("topology")),
+                        new View("Route", false, false, () -> 
selected.set("route"))),
+                List.of(new Toggle("b", "view", "business"), new Toggle("e", 
"external", "edges"),
+                        new Toggle("m", "metrics", "off")),
+                true);
+    }
+
+    private static String render(SubViewBar bar, Spec spec, int y) {
+        Rect area = new Rect(0, y, 160, 1);
+        Buffer buffer = Buffer.empty(new Rect(0, 0, 160, y + 1));
+        bar.render(Frame.forTesting(buffer), area, spec);
+        String all = TuiTestHelper.bufferToString(buffer);
+        String[] rows = all.split("\n");
+        return rows[rows.length - 1];
+    }
+
+    @Test
+    void showsAllViewsTheCurrentOneAndTheSettings() {
+        SubViewBar bar = new SubViewBar();
+        String line = render(bar, levels(), 3);
+
+        assertThat(line).contains("◇ Architecture", "◆ Topology · Order 
intake", "◇ Route", " › ");
+        assertThat(line.indexOf(" v ")).isBetween(0, 
line.indexOf("Architecture"));
+        // the view settings, with their state
+        assertThat(line).contains(" b  view: business ", " e  external: edges 
", " m  metrics ○");
+    }
+
+    @Test
+    void aClickGoesToAViewOrPressesTheKeyOfASetting() {
+        SubViewBar bar = new SubViewBar();
+        String line = render(bar, levels(), 3);
+
+        bar.viewAt(line.indexOf("Architecture"), 3).select().run();
+        assertThat(selected.get()).isEqualTo("architecture");
+        assertThat(bar.viewAt(line.indexOf("Order intake"), 3)).as("the view 
shown").isNull();
+        assertThat(bar.viewAt(line.indexOf("Route"), 3)).as("a view that does 
not apply").isNull();
+        assertThat(bar.viewAt(line.indexOf("Architecture"), 2)).as("another 
row").isNull();
+
+        assertThat(bar.keyAt(line.indexOf("metrics"), 3)).isEqualTo("m");
+        assertThat(bar.keyAt(line.indexOf(" e  external"), 3)).isEqualTo("e");
+        assertThat(bar.isOnRow(3)).isTrue();
+    }
+
+    @Test
+    void peerViewsAreSeparatedByABar() {
+        SubViewBar bar = new SubViewBar();
+        String line = render(bar, new Spec(
+                null, List.of(
+                        new View("History", true, true, null), new 
View("Waterfall", false, true, () -> {
+                        })),
+                List.of(), false), 0);
+
+        assertThat(line).contains("◆ History", " │ ", "◇ 
Waterfall").doesNotContain(" › ");
+    }
+}

Reply via email to