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 dba11ca84104 CAMEL-25432: camel-jbang - route tree beside the source 
editor, Diagram levels in the title (#27521)
dba11ca84104 is described below

commit dba11ca841043abe4df0614f3fdf91bc73c44454
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu Oct 8 02:30:16 2026 +0200

    CAMEL-25432: camel-jbang - route tree beside the source editor, Diagram 
levels in the title (#27521)
    
    * CAMEL-25432: camel-jbang - the Diagram tab shows its levels in the title, 
as the Source tab shows the DSLs
    
    The levels of the Diagram tab were on the bar above it as dots and arrows
    (Architecture > Topology > Route). They are now in the title of the
    diagram, as the Source tab shows YAML | Java | XML: Architecture | Topology
    | Route, the level shown bold and the others dim. v moves through them, as
    before, and the footer says so; the bar above has the view settings of the
    level shown.
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
    
    * CAMEL-25432: camel-jbang - the source editor can show the tree of the 
route the cursor is in
    
    Ctrl+T shows a small panel at the top right of the source with the tree of
    the route the cursor is in: its from, then each step, with the branches
    indented and the step under the cursor marked. It follows the cursor while
    moving and editing, for YAML, XML and Java routes, also when the route does
    not run, with the scanners Go to Node (Ctrl+G) uses. The breadcrumb in the
    title shows the path to one line; the tree shows the whole route around it.
    
    A Route Tree setting says whether it is shown at first (off by default).
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
    
    * CAMEL-25432: camel-jbang - the route tree keeps the step under the cursor 
in view
    
    Review follow-up: the window of steps was worked out before the from line
    took a row, so at the boundary the step under the cursor was just below it.
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
    
    ---------
    
    Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
---
 .../ROOT/pages/camel-jbang-tui-source-editor.adoc  |   4 +
 .../jbang/core/commands/tui/ArchitectureView.java  |  12 ++
 .../dsl/jbang/core/commands/tui/CamelMonitor.java  |   1 +
 .../dsl/jbang/core/commands/tui/DiagramTab.java    |  62 +++++---
 .../core/commands/tui/GotoSourceNodePopup.java     |   2 +-
 .../jbang/core/commands/tui/MonitorContext.java    |   2 +
 .../jbang/core/commands/tui/RouteTreePanel.java    | 161 +++++++++++++++++++++
 .../dsl/jbang/core/commands/tui/SettingsPopup.java |  45 ++++--
 .../dsl/jbang/core/commands/tui/SourceTab.java     |  13 ++
 .../dsl/jbang/core/commands/tui/SourceViewer.java  |  33 +++++
 .../dsl/jbang/core/commands/tui/TuiSettings.java   |  17 +++
 .../src/main/resources/tui/help/source.md          |   8 +
 .../core/commands/tui/RouteTreePanelTest.java      | 107 ++++++++++++++
 .../core/commands/tui/SettingsPopupRenderTest.java |   4 +-
 .../jbang/core/commands/tui/SettingsPopupTest.java |  28 ++--
 15 files changed, 452 insertions(+), 47 deletions(-)

diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-source-editor.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-source-editor.adoc
index aced09a79f96..413520965549 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-source-editor.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-source-editor.adoc
@@ -247,6 +247,9 @@ image::jbang/camel-tui-source-xml-quick-doc.png[The 
documentation of the logging
 * *Go to node* -- *Ctrl+G* shows the routes of the YAML, Java and XML files 
and their processors as a tree (the `when`
   and `otherwise` of a choice included). Type to filter by route ID, EIP or 
label, or type a line number, and *Enter*
   jumps there.
+* *Route tree* -- *Ctrl+T* shows the tree of the route the cursor is in at the 
top right of the source, from its `from`
+  through each step, with the step under the cursor marked. It follows the 
cursor as you move and edit, for YAML, XML
+  and Java routes. The *Route Tree* setting says whether it is shown at first 
(off by default).
 * *Search* -- */* searches the source, *n*/*N* go to the next and previous 
match, and *h* highlights a text.
 
 image::jbang/camel-tui-source-usages.png[The usages of a seda endpoint: its 
consumer and the steps sending to it]
@@ -329,6 +332,7 @@ The template of a Kamelet is a route of the project: *g* 
lists it under the name
 | *p* | Toggle plain mode (borderless)
 | *g* | Go to route
 | *Ctrl+G* | Go to node or line
+| *Ctrl+T* | Show or hide the route tree
 | *u* | Usages of the endpoint of the line
 | *F9* | Next problem
 | *Esc* / *c* | Close source viewer
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ArchitectureView.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ArchitectureView.java
index 05683cc87bc3..2677a14ac04a 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ArchitectureView.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ArchitectureView.java
@@ -502,6 +502,11 @@ final class ArchitectureView {
     // ---- rendering ----
 
     void render(Frame frame, Rect area, String integrationName) {
+        render(frame, area, integrationName, List.of());
+    }
+
+    /** Renders the view, with the given spans (the levels of the Diagram tab) 
after its title. */
+    void render(Frame frame, Rect area, String integrationName, List<Span> 
titleSuffix) {
         checkForChanges();
         // the AI's groups come and go with its hints (the ai view setting, 
the AI overview setting)
         Capabilities want = IntegrationSummaryHints.enabled() ? aiCapabilities 
: factCapabilities;
@@ -520,6 +525,13 @@ final class ArchitectureView {
             return;
         }
         Line title = title(integrationName);
+        if (!titleSuffix.isEmpty()) {
+            // in the Diagram tab the levels after the title name the level
+            List<Span> spans = new ArrayList<>(title.spans());
+            spans.set(0, Span.raw(" Diagram"));
+            spans.addAll(titleSuffix);
+            title = Line.from(spans);
+        }
         Group selected = selectedGroup();
         if (selected != null && area.width() > 70) {
             List<Rect> chunks = Layout.horizontal()
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 f59d77b89510..a82104d6d3bb 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
@@ -1304,6 +1304,7 @@ public class CamelMonitor extends CamelCommand {
         TuiSettings settings = TuiSettings.load();
         ctx.confirmActions = settings.isConfirmActions();
         ctx.validateOnSave = settings.isValidateOnSave();
+        ctx.routeTree = settings.isRouteTree();
         ctx.panelTop = settings.isPanelTop();
         ctx.panelOverlay = settings.isPanelOverlay();
     }
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 6fad8b926ce8..f84f155b1962 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
@@ -637,20 +637,38 @@ class DiagramTab extends AbstractTab {
         if (ctx.findSelectedIntegration() == null || sourceViewer.isVisible() 
|| !diagram.isShowDiagram()) {
             return null;
         }
+        // the levels are in the title of the diagram, as the DSLs of the 
Source tab are (v moves through them); the
+        // bar has the view settings of the level shown
+        return new SubViewBar.Spec(null, List.of(), viewToggles(), true);
+    }
+
+    /**
+     * The levels for the title of the diagram: Architecture │ Topology │ 
Route, the one shown bold and the others dim,
+     * as the Source tab shows YAML │ Java │ XML. The Route level is dimmer 
when no route is selected to go to.
+     */
+    List<Span> levelSpans() {
         Level current = level();
-        String group = diagram.getFocusName();
-        String route = routeCandidate();
-        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);
+        List<Span> spans = new ArrayList<>();
+        spans.add(Span.raw(" "));
+        String[] labels = { "Architecture", "Topology", "Route" };
+        Level[] levels = { Level.ARCHITECTURE, Level.TOPOLOGY, Level.ROUTE };
+        for (int i = 0; i < levels.length; i++) {
+            if (i > 0) {
+                spans.add(Span.styled(" \u2502 ", Style.EMPTY.dim()));
+            }
+            Style style = levels[i] == current ? Style.EMPTY.bold()
+                    : levels[i] == Level.ROUTE && routeCandidate() == null ? 
Theme.muted().dim() : Style.EMPTY.dim();
+            spans.add(Span.styled(labels[i], style));
+        }
+        spans.add(Span.raw(" "));
+        return spans;
+    }
+
+    /** The title with the levels after it. */
+    private Line withLevels(Line title) {
+        List<Span> spans = new ArrayList<>(title.spans());
+        spans.addAll(levelSpans());
+        return Line.from(spans);
     }
 
     /**
@@ -789,7 +807,7 @@ class DiagramTab extends AbstractTab {
         }
 
         if (architecture.isActive()) {
-            architecture.render(frame, area, info.name);
+            architecture.render(frame, area, info.name, levelSpans());
             return;
         }
 
@@ -799,13 +817,15 @@ class DiagramTab extends AbstractTab {
             if (topologyMode && diagram.hasNativeLayout()) {
                 Line title;
                 if (info.name != null) {
+                    // the level is named by the levels after it, as on the 
Source tab
                     title = Line.from(
-                            Span.raw(" Topology ["),
+                            Span.raw(" Diagram ["),
                             Span.styled(info.name, Theme.label().bold()),
                             Span.raw("] "));
                 } else {
-                    title = Line.from(Span.raw(" Topology "));
+                    title = Line.from(Span.raw(" Diagram "));
                 }
+                title = withLevels(title);
                 if (selectedRouteId != null && area.width() > 60) {
                     infoPanelWidth = Math.max(10, Math.min(infoPanelWidth, 
area.width() - 20));
                     List<Rect> hChunks = Layout.horizontal()
@@ -822,9 +842,9 @@ class DiagramTab extends AbstractTab {
                 return;
             } else if (!topologyMode && drillDownRouteId != null
                     && diagram.getRouteLayout(drillDownRouteId) != null) {
-                Line title = DiagramDetailSupport.withRouteContext(
+                Line title = withLevels(DiagramDetailSupport.withRouteContext(
                         
DiagramDetailSupport.buildBreadcrumbTitle(routeNavigationStack, 
drillDownRouteId), info,
-                        drillDownRouteId, selectedSourceDirectory(), 
isShowGroups());
+                        drillDownRouteId, selectedSourceDirectory(), 
isShowGroups()));
                 var routeLayout = diagram.getRouteLayout(drillDownRouteId);
                 if (area.width() > 60) {
                     infoPanelWidth = Math.max(10, Math.min(infoPanelWidth, 
area.width() - 20));
@@ -888,12 +908,16 @@ class DiagramTab extends AbstractTab {
             sourceViewer.renderFooter(spans);
             return;
         }
+        if (diagram.isShowDiagram() && ctx.findSelectedIntegration() != null) {
+            // the levels are in the title of the diagram
+            hint(spans, "v", "level");
+        }
         if (architecture.isActive()) {
             architecture.renderFooter(spans);
             return;
         }
         if (diagram.isShowDiagram()) {
-            // actions only: the levels are on the level bar, the view 
settings beside it
+            // actions only: the level is in the title, the view settings on 
the bar above
             if (!topologyMode && !diagram.getEipNodeBoxes().isEmpty()) {
                 hint(spans, "Esc", "back");
                 hint(spans, "c", "source");
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/GotoSourceNodePopup.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/GotoSourceNodePopup.java
index 059790d2edfb..6d1b1765da51 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/GotoSourceNodePopup.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/GotoSourceNodePopup.java
@@ -278,7 +278,7 @@ class GotoSourceNodePopup {
         }
     }
 
-    private static String buildTreePrefix(
+    static String buildTreePrefix(
             List<YamlRouteNodeScanner.NodeEntry> entries, int idx, 
YamlRouteNodeScanner.NodeEntry entry) {
         // Normalize indent to sequential depth within the route group
         int depth = normalizeDepth(entries, idx, entry);
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContext.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContext.java
index 5af4f3701998..f966d66ae5c1 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContext.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContext.java
@@ -58,6 +58,8 @@ class MonitorContext {
     boolean ratePerMinute;
     boolean confirmActions;
     boolean validateOnSave = true;
+    // whether the source editor shows the route tree at first (the Route Tree 
setting)
+    boolean routeTree;
     /** True while the shell (F6) or AI (F8) panel is open and owns keyboard 
focus. */
     boolean bottomPanelFocused;
     /** Shell/AI panel opens at the top of the content area instead of the 
bottom. */
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteTreePanel.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteTreePanel.java
new file mode 100644
index 000000000000..5c2325aa197c
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteTreePanel.java
@@ -0,0 +1,161 @@
+/*
+ * 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 java.util.Locale;
+import java.util.Map;
+
+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.text.Text;
+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.paragraph.Paragraph;
+import org.apache.camel.dsl.jbang.core.commands.tui.diagram.DiagramColors;
+
+/**
+ * The mini panel of the source editor: the tree of the route the cursor is in 
(from, then each step, the branches
+ * indented), with the step under the cursor marked, so the structure of a 
large route stays in view while editing it.
+ * It is built from the text as it is (the route need not run), with the 
scanners of Go to Node (Ctrl+G), for the YAML,
+ * XML and Java DSLs. Ctrl+T shows or hides it.
+ */
+final class RouteTreePanel {
+
+    static final int MIN_EDITOR_WIDTH = 90;
+    static final int MIN_EDITOR_HEIGHT = 12;
+
+    private int scannedHash;
+    private String scannedName;
+    private List<YamlRouteNodeScanner.NodeEntry> entries = List.of();
+
+    /** The nodes of the route the cursor is in, and which of them is under 
the cursor (-1 for none). */
+    record RouteTree(String routeId, String fromUri, 
List<YamlRouteNodeScanner.NodeEntry> nodes, int current) {
+    }
+
+    /**
+     * Draws the panel at the top right of the editor area, when it is wide 
enough and the file has a route.
+     *
+     * @param cursorRow the line of the cursor, 0-based
+     */
+    void render(Frame frame, Rect area, List<String> lines, String fileName, 
int cursorRow) {
+        if (area.width() < MIN_EDITOR_WIDTH || area.height() < 
MIN_EDITOR_HEIGHT || fileName == null) {
+            return;
+        }
+        RouteTree tree = treeAt(scan(lines, fileName), cursorRow);
+        if (tree == null || tree.nodes().isEmpty()) {
+            return;
+        }
+        int w = Math.min(46, area.width() / 3);
+        int maxRows = Math.max(3, area.height() * 2 / 3 - 2);
+        // the from line takes one row; the steps the rest
+        int rows = Math.min(tree.nodes().size(), maxRows - 1);
+        int top = windowTop(tree.current(), tree.nodes().size(), rows);
+        List<Line> out = new ArrayList<>();
+        // where the route starts
+        String from = tree.fromUri() != null ? "from " + tree.fromUri() : 
"from";
+        Style fromStyle = tree.current() < 0
+                ? Theme.selectionBg().bold()
+                : Style.EMPTY.fg(DiagramColors.getEipColor("from"));
+        out.add(Line.from(Span.styled(TuiHelper.truncate((tree.current() < 0 ? 
"▶" : " ") + from, w - 2), fromStyle)));
+        for (int i = top; i < top + rows; i++) {
+            out.add(line(tree.nodes(), i, i == tree.current(), w - 2));
+        }
+        Rect rect = new Rect(area.x() + area.width() - w - 2, area.y() + 1, w, 
out.size() + 2);
+        frame.renderWidget(Clear.INSTANCE, rect);
+        String title = tree.routeId() != null ? " Route: " + tree.routeId() + 
" " : " Route ";
+        Block block = 
Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+                .borderStyle(Theme.muted())
+                .title(TuiHelper.truncate(title, w - 4))
+                .build();
+        
frame.renderWidget(Paragraph.builder().text(Text.from(out)).block(block).build(),
 rect);
+    }
+
+    /** The first step shown, so the step under the cursor is in the window of 
the given rows. */
+    static int windowTop(int current, int size, int rows) {
+        if (current < rows) {
+            return 0;
+        }
+        return Math.max(0, Math.min(current - rows / 2, size - rows));
+    }
+
+    /** The route the cursor is in: the last route that starts at or above it, 
else the first one. */
+    static RouteTree treeAt(List<YamlRouteNodeScanner.NodeEntry> entries, int 
cursorRow) {
+        int start = -1;
+        for (int i = 0; i < entries.size(); i++) {
+            YamlRouteNodeScanner.NodeEntry e = entries.get(i);
+            if (e.kind() == YamlRouteNodeScanner.EntryKind.ROUTE && (start < 0 
|| e.lineIndex() <= cursorRow)) {
+                start = i;
+            }
+        }
+        if (start < 0) {
+            return null;
+        }
+        String routeId = entries.get(start).routeId();
+        String fromUri = entries.get(start).fromUri();
+        List<YamlRouteNodeScanner.NodeEntry> nodes = new ArrayList<>();
+        int current = -1;
+        for (int i = start + 1; i < entries.size(); i++) {
+            YamlRouteNodeScanner.NodeEntry e = entries.get(i);
+            if (e.kind() == YamlRouteNodeScanner.EntryKind.ROUTE) {
+                break;
+            }
+            nodes.add(e);
+            if (e.lineIndex() <= cursorRow) {
+                current = nodes.size() - 1;
+            }
+        }
+        return new RouteTree(routeId, fromUri, nodes, current);
+    }
+
+    private List<YamlRouteNodeScanner.NodeEntry> scan(List<String> lines, 
String fileName) {
+        int hash = lines.hashCode();
+        if (hash == scannedHash && fileName.equals(scannedName)) {
+            return entries;
+        }
+        String name = fileName.toLowerCase(Locale.ROOT);
+        if (name.endsWith(".yaml") || name.endsWith(".yml")) {
+            entries = YamlRouteNodeScanner.scanLines(lines, fileName);
+        } else if (name.endsWith(".xml") || name.endsWith(".java")) {
+            entries = ModelRouteNodeScanner.scan(fileName, fileName, 
String.join("\n", lines), Map.of(),
+                    ArchitectureView.catalog());
+        } else {
+            entries = List.of();
+        }
+        scannedHash = hash;
+        scannedName = fileName;
+        return entries;
+    }
+
+    private static Line line(List<YamlRouteNodeScanner.NodeEntry> nodes, int 
idx, boolean current, int width) {
+        YamlRouteNodeScanner.NodeEntry e = nodes.get(idx);
+        String prefix = GotoSourceNodePopup.buildTreePrefix(nodes, idx, e);
+        String text = e.type() + (e.label().isBlank() ? "" : " " + e.label());
+        String mark = current ? "▶" : " ";
+        String all = TuiHelper.truncate(mark + prefix.substring(Math.min(1, 
prefix.length())) + text, width);
+        Style style = current
+                ? Theme.selectionBg().bold()
+                : 
Style.EMPTY.fg(DiagramColors.getEipColor(YamlSourceContext.dashToCamelCase(e.type())));
+        return Line.from(Span.styled(all, style));
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SettingsPopup.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SettingsPopup.java
index 0b88c05ca567..d6d2498f5a16 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SettingsPopup.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SettingsPopup.java
@@ -54,19 +54,20 @@ class SettingsPopup {
     private static final int ROW_PANEL_SPACE = 6;
     private static final int ROW_CONFIRM_ACTIONS = 7;
     private static final int ROW_VALIDATE_ON_SAVE = 8;
-    private static final int ROW_FOLDER = 9;
-    private static final int ROW_PROXY_HOST = 10;
-    private static final int ROW_PROXY_PORT = 11;
-    private static final int ROW_SHELL_HISTORY = 12;
-    private static final int ROW_AI_PROVIDER = 13;
-    private static final int ROW_AI_MODEL = 14;
-    private static final int ROW_AI_URL = 15;
-    private static final int ROW_AI_TOOLS = 16;
-    private static final int ROW_AI_OVERVIEW = 17;
-    private static final int ROW_AI_PROMPT_HISTORY = 18;
-    private static final int ROW_AI_ACP_COMMAND = 19;
-    static final int ROW_COUNT = 20;
-    /** Separator lines drawn between the row groups, after rows 2, 6, 9 and 
12. */
+    private static final int ROW_ROUTE_TREE = 9;
+    private static final int ROW_FOLDER = 10;
+    private static final int ROW_PROXY_HOST = 11;
+    private static final int ROW_PROXY_PORT = 12;
+    private static final int ROW_SHELL_HISTORY = 13;
+    private static final int ROW_AI_PROVIDER = 14;
+    private static final int ROW_AI_MODEL = 15;
+    private static final int ROW_AI_URL = 16;
+    private static final int ROW_AI_TOOLS = 17;
+    private static final int ROW_AI_OVERVIEW = 18;
+    private static final int ROW_AI_PROMPT_HISTORY = 19;
+    private static final int ROW_AI_ACP_COMMAND = 20;
+    static final int ROW_COUNT = 21;
+    /** Separator lines drawn between the row groups, after rows 2, 6, 10 and 
13. */
     static final int DIVIDERS = 4;
 
     private static final String[] LOG_PIN_OPTIONS = { "off", "25", "50", "75" 
};
@@ -109,6 +110,7 @@ class SettingsPopup {
     private int panelSpaceIndex;
     private int confirmActionsIndex;
     private int validateOnSaveIndex;
+    private int routeTreeIndex;
     private int aiProviderIndex;
     private int aiToolsIndex;
     private int aiOverviewIndex;
@@ -191,6 +193,7 @@ class SettingsPopup {
 
         confirmActionsIndex = settings.isConfirmActions() ? 1 : 0;
         validateOnSaveIndex = settings.isValidateOnSave() ? 1 : 0;
+        routeTreeIndex = settings.isRouteTree() ? 1 : 0;
 
         folderInput = new TextInputState(settings.getDefaultFolder() != null ? 
settings.getDefaultFolder() : "");
         proxyHostInput = new TextInputState(settings.getProxyHost() != null ? 
settings.getProxyHost() : "");
@@ -318,6 +321,12 @@ class SettingsPopup {
             }
             return true;
         }
+        if (selectedRow == ROW_ROUTE_TREE) {
+            if (ke.isChar(' ') || ke.isRight() || ke.isLeft()) {
+                routeTreeIndex = routeTreeIndex == 0 ? 1 : 0;
+            }
+            return true;
+        }
         if (selectedRow == ROW_FOLDER) {
             handleTextInput(ke, folderInput);
             return true;
@@ -408,6 +417,10 @@ class SettingsPopup {
         if (monitorContext != null) {
             monitorContext.validateOnSave = validateOnSaveIndex == 1;
         }
+        settings.setRouteTree(routeTreeIndex == 1 ? "true" : "false");
+        if (monitorContext != null) {
+            monitorContext.routeTree = routeTreeIndex == 1;
+        }
         
settings.setDefaultFolder(stripControlChars(folderInput.text().trim()));
         settings.setProxyHost(stripControlChars(proxyHostInput.text().trim()));
         settings.setProxyPort(stripControlChars(proxyPortInput.text().trim()));
@@ -518,6 +531,11 @@ class SettingsPopup {
                 selectedRow == ROW_VALIDATE_ON_SAVE);
         rowY++;
 
+        renderLabel(frame, innerX, rowY, labelW, "Route Tree:", selectedRow == 
ROW_ROUTE_TREE);
+        renderValue(frame, innerX + labelW, rowY, fieldW, routeTreeIndex == 1 
? "on" : "off",
+                selectedRow == ROW_ROUTE_TREE);
+        rowY++;
+
         renderLabel(frame, innerX, rowY, labelW, "Default Folder:", 
selectedRow == ROW_FOLDER);
         renderFolder(frame, innerX + labelW, rowY, fieldW, selectedRow == 
ROW_FOLDER);
         rowY++;
@@ -581,6 +599,7 @@ class SettingsPopup {
                 || selectedRow == ROW_LOG_PIN || selectedRow == ROW_RATE_PER
                 || selectedRow == ROW_PANEL_POSITION || selectedRow == 
ROW_PANEL_SPACE
                 || selectedRow == ROW_CONFIRM_ACTIONS || selectedRow == 
ROW_VALIDATE_ON_SAVE
+                || selectedRow == ROW_ROUTE_TREE
                 || selectedRow == ROW_AI_PROVIDER || selectedRow == 
ROW_AI_TOOLS || selectedRow == ROW_AI_OVERVIEW) {
             hint(spans, "Space", "cycle");
         }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
index 7bb5b0a2057c..cc0e3f3c0a43 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
@@ -256,6 +256,12 @@ class SourceTab extends AbstractTab {
             return true;
         }
 
+        // the route tree beside the source, wherever the focus is, as Ctrl+G
+        if (ke.hasCtrl() && ke.isCharIgnoreCase('t') && 
sourceViewer.isVisible()) {
+            sourceViewer.setRouteTreeShown(!sourceViewer.isRouteTreeShown());
+            return true;
+        }
+
         if (sourceViewer.isEditMode() && sourceViewer.isVisible()) {
             return sourceViewer.handleKeyEvent(ke);
         }
@@ -1162,7 +1168,14 @@ class SourceTab extends AbstractTab {
         return re.fromUri() != null ? re.fromUri() : "";
     }
 
+    // the Route Tree setting last given to the editor: it is given again only 
when it changes, so Ctrl+T holds
+    private Boolean routeTreeDefault;
+
     private void renderSourcePanel(Frame frame, Rect area) {
+        if (routeTreeDefault == null || routeTreeDefault != ctx.routeTree) {
+            routeTreeDefault = ctx.routeTree;
+            sourceViewer.setRouteTreeShown(ctx.routeTree);
+        }
         Style sourceTitleStyle = focusOnViewer ? Theme.title() : 
Style.EMPTY.fg(Theme.accent());
         Style sourceBorderStyle = ctx.paneBorder(focusOnViewer);
         if (sourceViewer.isVisible()) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewer.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewer.java
index f0aab161f5df..2cb80266693b 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewer.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewer.java
@@ -22,6 +22,7 @@ import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.StandardOpenOption;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
@@ -220,6 +221,9 @@ class SourceViewer {
     private AutocompletePopup autocompletePopup;
     private RefactorPopup refactorPopup;
     private boolean validateOnSave = true;
+    // the tree of the route the cursor is in, at the top right (Ctrl+T); the 
setting gives the default
+    private boolean routeTreeShown;
+    private final RouteTreePanel routeTree = new RouteTreePanel();
     private org.apache.camel.dsl.yaml.validator.YamlValidator yamlValidator;
     private PropertiesValidator propertiesValidator;
     private EndpointValidator endpointValidator;
@@ -311,6 +315,14 @@ class SourceViewer {
         this.listItemNodeChecker = checker;
     }
 
+    void setRouteTreeShown(boolean routeTreeShown) {
+        this.routeTreeShown = routeTreeShown;
+    }
+
+    boolean isRouteTreeShown() {
+        return routeTreeShown;
+    }
+
     void setValidateOnSave(boolean validateOnSave) {
         this.validateOnSave = validateOnSave;
     }
@@ -792,6 +804,10 @@ class SourceViewer {
         if (!visible) {
             return false;
         }
+        if (ke.hasCtrl() && ke.isCharIgnoreCase('t') && !markdownMode) {
+            routeTreeShown = !routeTreeShown;
+            return true;
+        }
         if (editMode) {
             return handleEditKeyEvent(ke);
         }
@@ -2596,6 +2612,23 @@ class SourceViewer {
     }
 
     void render(Frame frame, Rect area) {
+        renderContent(frame, area);
+        if (routeTreeShown && !markdownMode && !diffOverlay) {
+            if (editMode) {
+                List<String> text = Arrays.asList(editState.text().split("\n", 
-1));
+                String name = editableFile != null ? 
editableFile.getFileName().toString() : title;
+                routeTree.render(frame, area, text, name, 
editState.cursorRow());
+            } else {
+                String name = currentRouteId != null ? "route." + 
currentFormat : title;
+                // the code without the line numbers of the view
+                List<String> code = codeData != null
+                        ? codeData.stream().map(c -> 
c.getStringOrDefault("code", "")).toList() : List.of();
+                routeTree.render(frame, area, code, name, selectedLine);
+            }
+        }
+    }
+
+    private void renderContent(Frame frame, Rect area) {
         if (editMode) {
             renderEditMode(frame, area);
             return;
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiSettings.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiSettings.java
index 37daa098baa8..60742140bfd5 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiSettings.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiSettings.java
@@ -48,6 +48,7 @@ final class TuiSettings {
     static final String PROP_AI_PROMPT_HISTORY = "camel.tui.ai.promptHistory";
     static final String PROP_CONFIRM_ACTIONS = "camel.tui.confirmActions";
     static final String PROP_VALIDATE_ON_SAVE = "camel.tui.validateOnSave";
+    static final String PROP_ROUTE_TREE = "camel.tui.routeTree";
     static final String PROP_PANEL_POSITION = "camel.tui.panelPosition";
     static final String PROP_PANEL_SPACE = "camel.tui.panelSpace";
 
@@ -69,6 +70,7 @@ final class TuiSettings {
     private String aiPromptHistory;
     private String confirmActions;
     private String validateOnSave;
+    private String routeTree;
     private String panelPosition;
     private String panelSpace;
 
@@ -237,6 +239,19 @@ final class TuiSettings {
         this.validateOnSave = validateOnSave;
     }
 
+    String getRouteTree() {
+        return routeTree;
+    }
+
+    void setRouteTree(String routeTree) {
+        this.routeTree = routeTree;
+    }
+
+    /** Whether the source editor shows the route tree at first (Ctrl+T shows 
or hides it); off by default. */
+    boolean isRouteTree() {
+        return "true".equals(routeTree);
+    }
+
     boolean isValidateOnSave() {
         return !"false".equals(validateOnSave);
     }
@@ -293,6 +308,7 @@ final class TuiSettings {
             settings.aiPromptHistory = 
trimToNull(TuiUserConfig.read(PROP_AI_PROMPT_HISTORY));
             settings.confirmActions = 
trimToNull(TuiUserConfig.read(PROP_CONFIRM_ACTIONS));
             settings.validateOnSave = 
trimToNull(TuiUserConfig.read(PROP_VALIDATE_ON_SAVE));
+            settings.routeTree = 
trimToNull(TuiUserConfig.read(PROP_ROUTE_TREE));
             settings.panelPosition = 
trimToNull(TuiUserConfig.read(PROP_PANEL_POSITION));
             settings.panelSpace = 
trimToNull(TuiUserConfig.read(PROP_PANEL_SPACE));
         } catch (RuntimeException e) {
@@ -326,6 +342,7 @@ final class TuiSettings {
             TuiUserConfig.write(PROP_AI_PROMPT_HISTORY, aiPromptHistory);
             TuiUserConfig.write(PROP_CONFIRM_ACTIONS, confirmActions);
             TuiUserConfig.write(PROP_VALIDATE_ON_SAVE, validateOnSave);
+            TuiUserConfig.write(PROP_ROUTE_TREE, routeTree);
             TuiUserConfig.write(PROP_PANEL_POSITION, panelPosition);
             TuiUserConfig.write(PROP_PANEL_SPACE, panelSpace);
         } catch (RuntimeException e) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/source.md 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/source.md
index 6b551084b7d8..cbf7c62a4542 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/source.md
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/source.md
@@ -174,6 +174,14 @@ a YAML or XML file); **Enter** goes to its declaration.
   node in the source editor. Type a **line number** (e.g. `47`) and press
   **Enter** to jump directly to that line.
 
+## Route Tree
+- **Ctrl+T** — show or hide the route tree at the top right of the source:
+  the route the cursor is in, from its `from` through each step, with the
+  branches indented and the step under the cursor marked with ▶. It follows
+  the cursor as you move and edit, for YAML, XML and Java routes, also when
+  the route does not run. The **Route Tree** setting (F2 → Settings) says
+  whether it is shown at first. It needs an editor of 90 columns or more.
+
 ## General
 - **Tab** — toggle focus between file list and source viewer
 - The focused panel title is highlighted; the unfocused panel dims
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteTreePanelTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteTreePanelTest.java
new file mode 100644
index 000000000000..db2fad1bf960
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteTreePanelTest.java
@@ -0,0 +1,107 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+
+import dev.tamboui.buffer.Buffer;
+import dev.tamboui.layout.Rect;
+import dev.tamboui.terminal.Frame;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * The route tree beside the source editor shows the route the cursor is in, 
and marks the step under the cursor.
+ */
+class RouteTreePanelTest {
+
+    private static final List<String> SOURCE = List.of(
+            "- route:",
+            "    id: checkout",
+            "    from:",
+            "      uri: file:orders",
+            "      steps:",
+            "        - unmarshal:",
+            "            json: {}",
+            "        - choice:",
+            "            when:",
+            "              - simple: \"${body} == 'a'\"",
+            "                steps:",
+            "                  - log: a",
+            "            otherwise:",
+            "              steps:",
+            "                - to: direct:other",
+            "- route:",
+            "    id: other",
+            "    from:",
+            "      uri: direct:other",
+            "      steps:",
+            "        - log: other");
+
+    @Test
+    void theRouteOfTheCursorWithTheStepUnderIt() {
+        List<YamlRouteNodeScanner.NodeEntry> entries = 
YamlRouteNodeScanner.scanLines(SOURCE, "orders.camel.yaml");
+
+        RouteTreePanel.RouteTree tree = RouteTreePanel.treeAt(entries, 11);
+
+        assertThat(tree.routeId()).isEqualTo("checkout");
+        
assertThat(tree.nodes()).extracting(YamlRouteNodeScanner.NodeEntry::type).contains("unmarshal",
 "choice", "log");
+        assertThat(tree.nodes().get(tree.current()).type()).isEqualTo("log");
+
+        assertThat(RouteTreePanel.treeAt(entries, 
20).routeId()).isEqualTo("other");
+    }
+
+    @Test
+    void theEditorDrawsThePanelWhenItIsShown(@TempDir Path dir) throws 
Exception {
+        Path file = dir.resolve("orders.camel.yaml");
+        Files.write(file, SOURCE);
+        Theme.resetForTesting();
+        SourceViewer viewer = new SourceViewer();
+        viewer.loadFile(file);
+        viewer.setRouteTreeShown(true);
+        Rect area = new Rect(0, 0, 120, 30);
+        Buffer buffer = Buffer.empty(area);
+
+        viewer.render(Frame.forTesting(buffer), area);
+
+        StringBuilder screen = new StringBuilder();
+        for (int y = 0; y < 30; y++) {
+            for (int x = 0; x < 120; x++) {
+                screen.append(buffer.get(x, y).symbol());
+            }
+            screen.append('\n');
+        }
+        assertThat(screen.toString()).contains("Route: checkout");
+    }
+
+    @Test
+    void theStepUnderTheCursorIsAlwaysInTheWindow() {
+        for (int size = 1; size <= 30; size++) {
+            for (int rows = 1; rows <= size; rows++) {
+                for (int current = 0; current < size; current++) {
+                    int top = RouteTreePanel.windowTop(current, size, rows);
+                    assertThat(current).as("size %d rows %d", size, 
rows).isBetween(top, top + rows - 1);
+                    assertThat(top + rows).isLessThanOrEqualTo(size);
+                }
+            }
+        }
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SettingsPopupRenderTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SettingsPopupRenderTest.java
index 822476b68ead..756a0bb5e867 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SettingsPopupRenderTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SettingsPopupRenderTest.java
@@ -72,7 +72,7 @@ class SettingsPopupRenderTest {
                 new TabRegistry.TabEntry("🩺", "Health", "health", "7", 6, 
-1)));
         popup.open();
 
-        Rect area = new Rect(0, 0, 80, 28);
+        Rect area = new Rect(0, 0, 80, 29);
         Buffer buffer = Buffer.empty(area);
         Frame frame = Frame.forTesting(buffer);
         popup.render(frame, area);
@@ -112,7 +112,7 @@ class SettingsPopupRenderTest {
         String rendered = TuiTestHelper.bufferToString(buffer);
         assertTrue(rendered.contains("Theme:"), rendered);
         assertFalse(rendered.contains("ACP Command"), "the last rows do not 
fit in 25 lines");
-        for (int i = 0; i < 19; i++) {
+        for (int i = 0; i < 20; i++) {
             popup.handleKeyEvent(KeyEvent.ofKey(KeyCode.DOWN, 
KeyModifiers.NONE));
         }
         buffer = Buffer.empty(area);
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SettingsPopupTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SettingsPopupTest.java
index 070cb8857d51..23676a64e33f 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SettingsPopupTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SettingsPopupTest.java
@@ -142,7 +142,8 @@ class SettingsPopupTest {
         popup.handleKeyEvent(key(KeyCode.DOWN));
         popup.handleKeyEvent(key(KeyCode.DOWN));
         popup.handleKeyEvent(key(KeyCode.DOWN));
-        assertEquals(9, popup.selectedRow());
+        popup.handleKeyEvent(key(KeyCode.DOWN));
+        assertEquals(10, popup.selectedRow());
         popup.handleKeyEvent(KeyEvent.ofChar('/'));
         popup.handleKeyEvent(KeyEvent.ofChar('a'));
         assertEquals("/a", popup.folderText());
@@ -164,7 +165,8 @@ class SettingsPopupTest {
         popup.handleKeyEvent(key(KeyCode.DOWN));
         popup.handleKeyEvent(key(KeyCode.DOWN));
         popup.handleKeyEvent(key(KeyCode.DOWN));
-        assertEquals(9, popup.selectedRow());
+        popup.handleKeyEvent(key(KeyCode.DOWN));
+        assertEquals(10, popup.selectedRow());
         popup.handleKeyEvent(KeyEvent.ofChar(0x01));
         popup.handleKeyEvent(KeyEvent.ofChar(0x00));
         popup.handleKeyEvent(KeyEvent.ofChar('x'));
@@ -192,7 +194,8 @@ class SettingsPopupTest {
         popup.handleKeyEvent(key(KeyCode.DOWN));
         popup.handleKeyEvent(key(KeyCode.DOWN));
         popup.handleKeyEvent(key(KeyCode.DOWN));
-        assertEquals(13, popup.selectedRow());
+        popup.handleKeyEvent(key(KeyCode.DOWN));
+        assertEquals(14, popup.selectedRow());
         assertEquals("auto", popup.selectedAiProvider());
         popup.handleKeyEvent(KeyEvent.ofChar(' '));
         assertEquals("ollama", popup.selectedAiProvider());
@@ -225,10 +228,10 @@ class SettingsPopupTest {
         popup.open();
 
         // navigate to AI Tools (row 16)
-        for (int i = 0; i < 16; i++) {
+        for (int i = 0; i < 17; i++) {
             popup.handleKeyEvent(key(KeyCode.DOWN));
         }
-        assertEquals(16, popup.selectedRow());
+        assertEquals(17, popup.selectedRow());
         assertEquals("auto", popup.selectedAiTools());
 
         popup.handleKeyEvent(KeyEvent.ofChar(' '));
@@ -248,7 +251,7 @@ class SettingsPopupTest {
         reopened.setTabEntries(tabs());
         reopened.open();
         assertEquals("full", reopened.selectedAiTools());
-        for (int i = 0; i < 16; i++) {
+        for (int i = 0; i < 17; i++) {
             reopened.handleKeyEvent(key(KeyCode.DOWN));
         }
         reopened.handleKeyEvent(key(KeyCode.RIGHT));
@@ -265,10 +268,10 @@ class SettingsPopupTest {
         popup.open();
 
         // navigate to AI Overview (row 17)
-        for (int i = 0; i < 17; i++) {
+        for (int i = 0; i < 18; i++) {
             popup.handleKeyEvent(key(KeyCode.DOWN));
         }
-        assertEquals(17, popup.selectedRow());
+        assertEquals(18, popup.selectedRow());
         assertEquals("manual", popup.selectedAiOverview());
         popup.handleKeyEvent(KeyEvent.ofChar(' '));
         assertEquals("auto", popup.selectedAiOverview());
@@ -282,7 +285,7 @@ class SettingsPopupTest {
         reopened.setTabEntries(tabs());
         reopened.open();
         assertEquals("off", reopened.selectedAiOverview());
-        for (int i = 0; i < 17; i++) {
+        for (int i = 0; i < 18; i++) {
             reopened.handleKeyEvent(key(KeyCode.DOWN));
         }
         reopened.handleKeyEvent(key(KeyCode.RIGHT));
@@ -299,10 +302,10 @@ class SettingsPopupTest {
         popup.open();
 
         // navigate to Shell History (row 10)
-        for (int i = 0; i < 12; i++) {
+        for (int i = 0; i < 13; i++) {
             popup.handleKeyEvent(key(KeyCode.DOWN));
         }
-        assertEquals(12, popup.selectedRow());
+        assertEquals(13, popup.selectedRow());
         for (char c : "50".toCharArray()) {
             popup.handleKeyEvent(KeyEvent.ofChar(c));
         }
@@ -312,7 +315,7 @@ class SettingsPopupTest {
         for (int i = 0; i < 6; i++) {
             popup.handleKeyEvent(key(KeyCode.DOWN));
         }
-        assertEquals(18, popup.selectedRow());
+        assertEquals(19, popup.selectedRow());
         for (char c : "200".toCharArray()) {
             popup.handleKeyEvent(KeyEvent.ofChar(c));
         }
@@ -349,6 +352,7 @@ class SettingsPopupTest {
         popup.handleKeyEvent(key(KeyCode.DOWN)); // panel space
         popup.handleKeyEvent(key(KeyCode.DOWN)); // confirm actions
         popup.handleKeyEvent(key(KeyCode.DOWN)); // validate on save
+        popup.handleKeyEvent(key(KeyCode.DOWN)); // route tree
         popup.handleKeyEvent(key(KeyCode.DOWN)); // folder
         for (char c : "/tmp/p".toCharArray()) {
             popup.handleKeyEvent(KeyEvent.ofChar(c));

Reply via email to