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

davsclaus pushed a commit to branch feature/CAMEL-24259-tui-tabs-and-inline-docs
in repository https://gitbox.apache.org/repos/asf/camel.git

commit a27c7bea02a3a07cc87396e085623473672f5f4d
Author: Claus Ibsen <[email protected]>
AuthorDate: Sun Jul 26 15:58:16 2026 +0200

    CAMEL-24259: camel-jbang - TUI add Source tab with file explorer and inline 
docs
    
    Replace HTTP as a primary tab with a new Source tab (key 2) featuring
    a left-right split layout with file explorer and source viewer. HTTP
    moves to the More submenu with cycling duplicate mnemonic support.
    
    - File explorer with directory navigation, info panel showing 
size/modified/type
    - Source viewer with syntax highlighting and Space to cycle format 
(YAML/Java/XML)
    - Tab key toggles focus between panels with dimmed selection on unfocused 
panel
    - Inline Camel documentation via i toggle for Camel source files
    - F1 help text and enriched JSON data for AI agents
    - Remove Tab/Shift+Tab cycling between primary tabs (use number keys 
instead)
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../dsl/jbang/core/commands/tui/CamelMonitor.java  |  66 +-
 .../dsl/jbang/core/commands/tui/McpFacade.java     |   4 +-
 .../dsl/jbang/core/commands/tui/PopupManager.java  |  33 +-
 .../dsl/jbang/core/commands/tui/SourceTab.java     | 679 +++++++++++++++++++++
 .../dsl/jbang/core/commands/tui/SourceViewer.java  |  37 +-
 .../dsl/jbang/core/commands/tui/TabRegistry.java   |  35 +-
 .../dsl/jbang/core/commands/tui/TuiHelper.java     |   4 +-
 .../dsl/jbang/core/commands/tui/TuiIcons.java      |   5 +-
 .../jbang/core/commands/tui/TabBarRenderTest.java  |  18 +-
 .../jbang/core/commands/tui/TabRegistryTest.java   |  40 +-
 .../dsl/jbang/core/commands/tui/TuiIconsTest.java  |   2 +-
 11 files changed, 816 insertions(+), 107 deletions(-)

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 403d094ff55b..14f4418c8838 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
@@ -691,8 +691,11 @@ public class CamelMonitor extends CamelCommand {
             }
             return true;
         }
-        boolean probeEditing = tabRegistry.selectedTabIndex() == TAB_HTTP
+        boolean probeEditing = tabRegistry.selectedTabIndex() == TAB_MORE
+                && tabRegistry.getActiveMoreTab() == tabRegistry.httpTab()
                 && tabRegistry.httpTab().isProbeMode();
+        boolean sourceSearchActive = tabRegistry.selectedTabIndex() == 
TAB_SOURCE
+                && tabRegistry.sourceTab().isSourceViewerSearchActive();
         boolean logSearchActive = tabRegistry.selectedTabIndex() == TAB_LOG
                 && tabRegistry.logTab().isSearchInputActive();
         boolean spanFilterActive = tabRegistry.selectedTabIndex() == TAB_MORE
@@ -713,8 +716,9 @@ public class CamelMonitor extends CamelCommand {
         boolean catalogFilterActive = tabRegistry.selectedTabIndex() == 
TAB_MORE
                 && tabRegistry.getActiveMoreTab() == tabRegistry.catalogTab()
                 && tabRegistry.catalogTab().isFilterInputActive();
-        boolean textEditing = probeEditing || logSearchActive || 
spanFilterActive || beanFilterActive
-                || classpathFilterActive || mavenDepsFilterActive || 
sqlInputActive || catalogFilterActive;
+        boolean textEditing = probeEditing || sourceSearchActive || 
logSearchActive || spanFilterActive
+                || beanFilterActive || classpathFilterActive || 
mavenDepsFilterActive || sqlInputActive
+                || catalogFilterActive;
         if (!textEditing && (ke.isCharIgnoreCase('q') || ke.isCtrlC())) {
             runner.quit();
             return true;
@@ -728,23 +732,23 @@ public class CamelMonitor extends CamelCommand {
                 return tabRegistry.handleTabKey(TAB_OVERVIEW, ctx, 
dataService);
             }
             if (ke.isChar('2')) {
+                return tabRegistry.handleTabKey(TAB_SOURCE, ctx, dataService);
+            }
+            if (ke.isChar('3')) {
                 return tabRegistry.handleTabKey(TAB_LOG, ctx, dataService);
             }
             if (!isInfraSelected()) {
-                if (ke.isChar('3')) {
-                    return tabRegistry.handleTabKey(TAB_ACTIVITY, ctx, 
dataService);
-                }
                 if (ke.isChar('4')) {
-                    return tabRegistry.handleTabKey(TAB_DIAGRAM, ctx, 
dataService);
+                    return tabRegistry.handleTabKey(TAB_ACTIVITY, ctx, 
dataService);
                 }
                 if (ke.isChar('5')) {
-                    return tabRegistry.handleTabKey(TAB_ROUTES, ctx, 
dataService);
+                    return tabRegistry.handleTabKey(TAB_DIAGRAM, ctx, 
dataService);
                 }
                 if (ke.isChar('6')) {
-                    return tabRegistry.handleTabKey(TAB_ENDPOINTS, ctx, 
dataService);
+                    return tabRegistry.handleTabKey(TAB_ROUTES, ctx, 
dataService);
                 }
                 if (ke.isChar('7')) {
-                    return tabRegistry.handleTabKey(TAB_HTTP, ctx, 
dataService);
+                    return tabRegistry.handleTabKey(TAB_ENDPOINTS, ctx, 
dataService);
                 }
                 if (ke.isChar('8')) {
                     return tabRegistry.handleTabKey(TAB_HISTORY, ctx, 
dataService);
@@ -759,26 +763,6 @@ public class CamelMonitor extends CamelCommand {
         }
         MonitorTab activeMonitorTab = tabRegistry.activeTab();
         boolean overlayActive = activeMonitorTab != null && 
activeMonitorTab.isOverlayActive();
-        if (ke.isFocusPrevious() && !textEditing && !overlayActive) {
-            if (isInfraSelected()) {
-                int prev = tabRegistry.selectedTabIndex() == TAB_OVERVIEW ? 
TAB_LOG : TAB_OVERVIEW;
-                tabsState.select(prev);
-            } else {
-                int prev = (tabRegistry.selectedTabIndex() - 1 + NUM_TABS) % 
NUM_TABS;
-                tabRegistry.handleTabKey(prev, ctx, dataService);
-            }
-            return true;
-        }
-        if (ke.isFocusNext() && !textEditing && !overlayActive) {
-            if (isInfraSelected()) {
-                int next = tabRegistry.selectedTabIndex() == TAB_OVERVIEW ? 
TAB_LOG : TAB_OVERVIEW;
-                tabsState.select(next);
-            } else {
-                int next = (tabRegistry.selectedTabIndex() + 1) % NUM_TABS;
-                tabRegistry.handleTabKey(next, ctx, dataService);
-            }
-            return true;
-        }
         if (ke.isKey(KeyCode.F5) && ke.hasShift()) {
             recordingManager.takeScreenshot();
             return true;
@@ -1164,6 +1148,10 @@ public class CamelMonitor extends CamelCommand {
             filesBrowser.handlePaste(pe.text());
             return true;
         }
+        if (tabRegistry.sourceTab().isSourceViewerSearchActive()) {
+            tabRegistry.sourceTab().handlePaste(pe.text());
+            return true;
+        }
         if (tabRegistry.getActiveMoreTab() == tabRegistry.sqlQueryTab()
                 && tabRegistry.sqlQueryTab().isInputActive()) {
             tabRegistry.sqlQueryTab().handlePaste(pe.text());
@@ -1474,7 +1462,7 @@ public class CamelMonitor extends CamelCommand {
             // Infra mode: only Overview and Log tabs
             Line[] labels = {
                     Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_OVERVIEW, 
"1", "Overview")),
-                    Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_LOG, "2", 
"Log"))
+                    Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_LOG, "3", 
"Log"))
             };
 
             // Map real tab index to infra tab index for highlight
@@ -1498,12 +1486,12 @@ public class CamelMonitor extends CamelCommand {
 
         Line[] labels = {
                 Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_OVERVIEW, 
"1", "Overview")),
-                Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_LOG, "2", 
"Log")),
-                Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_ACTIVITY, 
"3", "Activity")),
-                Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_DIAGRAM, "4", 
"Diagram")),
-                Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_ROUTES, "5", 
"Route")),
-                Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_ENDPOINTS, 
"6", "Endpoint")),
-                Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_HTTP, "7", 
"HTTP")),
+                Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_SOURCE, "2", 
"Source")),
+                Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_LOG, "3", 
"Log")),
+                Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_ACTIVITY, 
"4", "Activity")),
+                Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_DIAGRAM, "5", 
"Diagram")),
+                Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_ROUTES, "6", 
"Route")),
+                Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_ENDPOINTS, 
"7", "Endpoint")),
                 Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_INSPECT, "8", 
"Inspect")),
                 Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_ERRORS, "9", 
"Errors")),
                 Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_MORE, "0", 
TuiIcons.moreTabLabel())),
@@ -1615,10 +1603,6 @@ public class CamelMonitor extends CamelCommand {
         if (endpointCount > 0) {
             badgeTexts[TAB_ENDPOINTS] = "(" + endpointCount + ")";
         }
-        int httpCount = hasSelection ? sel.httpEndpoints.size() : 0;
-        if (httpCount > 0) {
-            badgeTexts[TAB_HTTP] = "(" + httpCount + ")";
-        }
         boolean hasTraces = hasSelection && 
!dataService.traces().get().isEmpty();
         if (hasTraces) {
             badgeTexts[TAB_HISTORY] = "(*)";
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
index 1aef9c262ee7..3f7151ea6c89 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
@@ -83,8 +83,8 @@ class McpFacade {
 
     // Tab name constants
     static final String[] TAB_NAMES = {
-            "Overview", "Log", "Activity", "Diagram", "Routes",
-            "Endpoints", "HTTP", "Inspect", "Errors", "More"
+            "Overview", "Source", "Log", "Activity", "Diagram",
+            "Routes", "Endpoints", "Inspect", "Errors", "More"
     };
 
     private final MonitorContext ctx;
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/PopupManager.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/PopupManager.java
index a880e3cbf78c..3992063df94f 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/PopupManager.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/PopupManager.java
@@ -77,6 +77,8 @@ class PopupManager {
     private final ListState morePopupState = new ListState();
     private final ScrollbarState moreScrollbarState = new ScrollbarState();
     private int lastMoreSelection;
+    private char lastShortcutLetter;
+    private int lastShortcutIndex = -1;
     private Line[] currentTabLabels;
 
     // Kill confirm
@@ -654,18 +656,43 @@ class PopupManager {
 
     int morePopupShortcut(KeyEvent ke) {
         List<TabRegistry.MoreTab> tabs = moreTabsSupplier.get();
+        List<Integer> matches = new ArrayList<>();
+        char pressedUpper = 0;
+
         for (int i = 0; i < tabs.size(); i++) {
             int idx = tabs.get(i).mnemonicIndex();
             if (idx < 0) {
                 continue;
             }
             char letter = tabs.get(i).displayName().charAt(idx);
-            // trigger on either case so Shift+letter works too
             if (ke.isChar(Character.toLowerCase(letter)) || 
ke.isChar(Character.toUpperCase(letter))) {
-                return i;
+                if (pressedUpper == 0) {
+                    pressedUpper = Character.toUpperCase(letter);
+                }
+                matches.add(i);
             }
         }
-        return -1;
+
+        if (matches.isEmpty()) {
+            return -1;
+        }
+        if (matches.size() == 1) {
+            lastShortcutLetter = pressedUpper;
+            lastShortcutIndex = matches.get(0);
+            return matches.get(0);
+        }
+
+        // cycle through duplicate mnemonics
+        if (pressedUpper == lastShortcutLetter && lastShortcutIndex >= 0) {
+            int pos = matches.indexOf(lastShortcutIndex);
+            int next = (pos + 1) % matches.size();
+            lastShortcutIndex = matches.get(next);
+            return matches.get(next);
+        }
+
+        lastShortcutLetter = pressedUpper;
+        lastShortcutIndex = matches.get(0);
+        return matches.get(0);
     }
 
     List<IntegrationInfo> getNonVanishingIntegrations() {
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
new file mode 100644
index 000000000000..e71a8d621e58
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
@@ -0,0 +1,679 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import dev.tamboui.layout.Constraint;
+import dev.tamboui.layout.Layout;
+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.tui.event.KeyEvent;
+import dev.tamboui.tui.event.MouseEvent;
+import dev.tamboui.tui.event.MouseEventKind;
+import dev.tamboui.widgets.block.Block;
+import dev.tamboui.widgets.block.BorderType;
+import dev.tamboui.widgets.block.Borders;
+import dev.tamboui.widgets.block.Title;
+import dev.tamboui.widgets.list.ListItem;
+import dev.tamboui.widgets.list.ListState;
+import dev.tamboui.widgets.list.ListWidget;
+import dev.tamboui.widgets.list.ScrollMode;
+import dev.tamboui.widgets.paragraph.Paragraph;
+import dev.tamboui.widgets.scrollbar.Scrollbar;
+import dev.tamboui.widgets.scrollbar.ScrollbarState;
+import org.apache.camel.catalog.CamelCatalog;
+import org.apache.camel.dsl.jbang.core.common.CatalogLoader;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+
+/**
+ * Source tab showing a file browser in the top panel and a source code viewer 
in the bottom panel. Replaces the HTTP
+ * tab as primary tab 7.
+ */
+class SourceTab extends AbstractTab {
+
+    private Path rootDir;
+    private Path currentDir;
+    private final ListState listState = new ListState();
+    private final ScrollbarState listScrollState = new ScrollbarState();
+    private List<FilesBrowser.FileEntry> entries = Collections.emptyList();
+    private final SourceViewer sourceViewer = new SourceViewer();
+    private boolean focusOnViewer;
+
+    private int leftPanelWidth = -1;
+    private final DragSplit hSplit = new DragSplit();
+    private Rect leftArea;
+    private Rect rightArea;
+
+    private final Map<String, CamelCatalog> catalogCache = new HashMap<>();
+
+    private static final Pattern YAML_URI_PATTERN = Pattern.compile(
+            
"^\\s*-?\\s*(?:uri|from|to|toD|wireTap|enrich|pollEnrich|deadLetterChannel):\\s*\"?([a-zA-Z][a-zA-Z0-9+.-]*:[^\"\\s]*)");
+    private static final Pattern YAML_EIP_PATTERN = Pattern.compile(
+            
"^\\s*-?\\s*(aggregate|bean|choice|circuitBreaker|claim[Cc]heck|convertBodyTo|convertHeaderTo|"
+                                                                    + 
"delay|dynamicRouter|enrich|filter|idempotentConsumer|"
+                                                                    + 
"intercept|kamelet|loadBalance|log|loop|marshal|multicast|"
+                                                                    + 
"onException|otherwise|pipeline|pollEnrich|process|"
+                                                                    + 
"recipientList|removeHeader|removeHeaders|removeProperty|"
+                                                                    + 
"resequence|rollback|routingSlip|saga|sample|"
+                                                                    + 
"script|serviceCall|setBody|setHeader|setProperty|"
+                                                                    + 
"sort|split|step|stop|threads|throttle|"
+                                                                    + 
"to|toD|transform|unmarshal|validate|when|wireTap)\\s*:");
+
+    SourceTab(MonitorContext ctx) {
+        super(ctx);
+    }
+
+    boolean isSourceViewerSearchActive() {
+        return sourceViewer.isSearchInputActive();
+    }
+
+    void handlePaste(String text) {
+        sourceViewer.handlePaste(text);
+    }
+
+    // ---- MonitorTab ----
+
+    @Override
+    public void onTabSelected() {
+        refreshFiles();
+    }
+
+    @Override
+    public void onIntegrationChanged() {
+        rootDir = null;
+        currentDir = null;
+        entries = Collections.emptyList();
+        sourceViewer.reset();
+        focusOnViewer = false;
+        leftPanelWidth = -1;
+        refreshFiles();
+    }
+
+    @Override
+    public boolean handleKeyEvent(KeyEvent ke) {
+        if (ke.isKey(dev.tamboui.tui.event.KeyCode.TAB)) {
+            if (sourceViewer.isVisible()) {
+                focusOnViewer = !focusOnViewer;
+            }
+            return true;
+        }
+
+        if (focusOnViewer && sourceViewer.isVisible()) {
+            boolean wasVisible = sourceViewer.isVisible();
+            if (sourceViewer.handleKeyEvent(ke)) {
+                if (wasVisible && !sourceViewer.isVisible()) {
+                    focusOnViewer = false;
+                }
+                return true;
+            }
+            if (ke.isCancel()) {
+                focusOnViewer = false;
+                return true;
+            }
+        }
+
+        if (!focusOnViewer) {
+            return handleFileListKey(ke);
+        }
+
+        return false;
+    }
+
+    @Override
+    public boolean handleMouseEvent(MouseEvent me, Rect area) {
+        if (hSplit.handleMouse(me, me.x())) {
+            if (hSplit.isDragging() && me.kind() == MouseEventKind.DRAG) {
+                leftPanelWidth = Math.max(15, Math.min(me.x() - area.x(), 
area.width() - 20));
+            }
+            return true;
+        }
+
+        if (leftArea != null && TuiHelper.contains(leftArea, me.x(), me.y())) {
+            focusOnViewer = false;
+            if (me.kind() == MouseEventKind.SCROLL_UP) {
+                listState.selectPrevious();
+                return true;
+            }
+            if (me.kind() == MouseEventKind.SCROLL_DOWN) {
+                listState.selectNext(entries.size());
+                return true;
+            }
+            if (me.isClick()) {
+                int innerTop = leftArea.top() + 1;
+                int clicked = listState.offset() + (me.y() - innerTop);
+                if (clicked >= 0 && clicked < entries.size()) {
+                    listState.select(clicked);
+                }
+                return true;
+            }
+            return true;
+        }
+
+        if (rightArea != null && TuiHelper.contains(rightArea, me.x(), 
me.y())) {
+            if (sourceViewer.isVisible()) {
+                focusOnViewer = true;
+                return sourceViewer.handleMouseEvent(me);
+            }
+            return true;
+        }
+
+        return false;
+    }
+
+    @Override
+    public boolean handleEscape() {
+        if (sourceViewer.isSearchInputActive()) {
+            return false;
+        }
+        if (focusOnViewer) {
+            focusOnViewer = false;
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public void navigateUp() {
+        listState.selectPrevious();
+    }
+
+    @Override
+    public void navigateDown() {
+        listState.selectNext(entries.size());
+    }
+
+    @Override
+    public void render(Frame frame, Rect area) {
+        if (ctx.selectedPid == null) {
+            renderNoSelection(frame, area);
+            return;
+        }
+
+        if (leftPanelWidth < 0) {
+            leftPanelWidth = Math.max(25, Math.min(35, area.width() * 25 / 
100));
+        }
+        leftPanelWidth = Math.max(15, Math.min(leftPanelWidth, area.width() - 
20));
+
+        List<Rect> chunks = Layout.horizontal()
+                .constraints(Constraint.length(leftPanelWidth), 
Constraint.fill())
+                .split(area);
+
+        leftArea = chunks.get(0);
+        rightArea = chunks.get(1);
+
+        int infoHeight = 6;
+        List<Rect> leftChunks = Layout.vertical()
+                .constraints(Constraint.fill(), Constraint.length(infoHeight))
+                .split(leftArea);
+
+        renderFileList(frame, leftChunks.get(0));
+        renderInfoPanel(frame, leftChunks.get(1));
+        hSplit.setBorderPos(rightArea.x());
+        renderSourcePanel(frame, rightArea);
+    }
+
+    @Override
+    public void renderFooter(List<Span> spans) {
+        if (focusOnViewer && sourceViewer.isVisible()) {
+            sourceViewer.renderFooter(spans);
+            TuiHelper.hint(spans, "Tab", "files");
+        } else {
+            TuiHelper.hint(spans, TuiIcons.HINT_SCROLL, "navigate");
+            TuiHelper.hint(spans, "Enter", "open");
+            if (currentDir != null && rootDir != null && 
!currentDir.equals(rootDir)) {
+                TuiHelper.hint(spans, "Bksp", "parent");
+            }
+            if (sourceViewer.isVisible()) {
+                TuiHelper.hint(spans, "Tab", "viewer");
+            }
+        }
+    }
+
+    @Override
+    public String description() {
+        return "Browse and view source files of the integration";
+    }
+
+    @Override
+    public String getHelpText() {
+        return """
+                # Source
+
+                Browse the source files of the selected integration and view 
their content
+                with syntax highlighting. The tab has a left-right split 
layout with a file
+                explorer on the left and a source viewer on the right.
+
+                ## File List (left panel)
+                - **Up/Down** — navigate files
+                - **Enter** — open file or directory
+                - **Backspace** — go to parent directory
+
+                ## Source Viewer (right panel)
+                - **Up/Down** — scroll through source code
+                - **Space** — cycle format (YAML/Java/XML) for Camel routes
+                - **i** — toggle inline Camel documentation for Camel source 
files
+                - **/** — search in source
+                - **h** — highlight text
+                - **n/N** — next/previous match
+                - **w** — toggle word wrap
+                - **Esc/c** — close source viewer
+
+                ## General
+                - **Tab** — toggle focus between file list and source viewer
+                - The focused panel title is highlighted; the unfocused panel 
dims
+                - Drag the split border with the mouse to resize panels
+                """;
+    }
+
+    @Override
+    public boolean isOverlayActive() {
+        return focusOnViewer && sourceViewer.isSearchInputActive();
+    }
+
+    @Override
+    public JsonObject getTableDataAsJson() {
+        JsonObject json = new JsonObject();
+        json.put("currentDir", currentDir != null ? currentDir.toString() : 
null);
+
+        var filesArray = new JsonArray();
+        for (FilesBrowser.FileEntry entry : entries) {
+            JsonObject file = new JsonObject();
+            file.put("name", entry.name());
+            file.put("directory", entry.directory());
+            if (!entry.directory()) {
+                file.put("size", entry.size());
+                file.put("sizeFormatted", 
FilesBrowser.formatFileSize(entry.size()));
+            }
+            file.put("path", entry.path());
+            filesArray.add(file);
+        }
+        json.put("files", filesArray);
+
+        Integer sel = listState.selected();
+        if (sel != null && sel < entries.size()) {
+            FilesBrowser.FileEntry selected = entries.get(sel);
+            json.put("selectedFile", selected.name());
+            json.put("selectedIndex", sel);
+            Path path = Path.of(selected.path());
+            json.put("isCamelSource", isCamelSourceFile(path));
+            try {
+                BasicFileAttributes attrs = Files.readAttributes(path, 
BasicFileAttributes.class);
+                if (!selected.directory()) {
+                    json.put("fileSize", attrs.size());
+                    json.put("fileSizeFormatted", 
FilesBrowser.formatFileSize(attrs.size()));
+                    int dot = selected.name().lastIndexOf('.');
+                    if (dot > 0) {
+                        json.put("fileType", selected.name().substring(dot + 
1));
+                    }
+                }
+                json.put("lastModified", attrs.lastModifiedTime().toInstant()
+                        .atZone(ZoneId.systemDefault()).format(DATE_FMT));
+            } catch (IOException e) {
+                // ignore
+            }
+        }
+        json.put("totalFiles", entries.size());
+        json.put("focusOnViewer", focusOnViewer);
+        json.put("viewerOpen", sourceViewer.isVisible());
+        return json;
+    }
+
+    // ---- Internals ----
+
+    private void refreshFiles() {
+        IntegrationInfo info = ctx.findSelectedIntegration();
+        if (info == null) {
+            return;
+        }
+        Path dir = FilesBrowser.resolveSourceDirectory(info);
+        if (dir == null || !Files.isDirectory(dir)) {
+            return;
+        }
+        if (rootDir == null) {
+            rootDir = dir;
+            currentDir = dir;
+        }
+        if (currentDir != null) {
+            loadDirectory(currentDir);
+        }
+    }
+
+    private boolean loadDirectory(Path dir) {
+        List<FilesBrowser.FileEntry> dirs = new ArrayList<>();
+        List<FilesBrowser.FileEntry> files = new ArrayList<>();
+        try (var stream = Files.list(dir)) {
+            stream.limit(200)
+                    .forEach(p -> {
+                        String name = p.getFileName().toString();
+                        if (Files.isDirectory(p) && !name.startsWith(".")) {
+                            dirs.add(new 
FilesBrowser.FileEntry(TuiIcons.FOLDER, name, -1, p.toString(), true));
+                        } else if (Files.isRegularFile(p)) {
+                            String emoji = TuiHelper.fileEmoji(p);
+                            long size = 0;
+                            try {
+                                size = Files.size(p);
+                            } catch (IOException e) {
+                                // ignore
+                            }
+                            files.add(new FilesBrowser.FileEntry(emoji, name, 
size, p.toString(), false));
+                        }
+                    });
+        } catch (IOException e) {
+            return false;
+        }
+        dirs.sort(Comparator.comparing(FilesBrowser.FileEntry::name, 
String.CASE_INSENSITIVE_ORDER));
+        files.sort(Comparator.comparing(FilesBrowser.FileEntry::name, 
String.CASE_INSENSITIVE_ORDER));
+
+        List<FilesBrowser.FileEntry> found = new ArrayList<>();
+        if (!dir.equals(rootDir)) {
+            found.add(new FilesBrowser.FileEntry(TuiIcons.FOLDER, "..", -1, 
dir.getParent().toString(), true));
+        }
+        found.addAll(dirs);
+        found.addAll(files);
+
+        if (found.isEmpty()) {
+            return false;
+        }
+        entries = found;
+        listState.select(0);
+        currentDir = dir;
+        return true;
+    }
+
+    private boolean handleFileListKey(KeyEvent ke) {
+        if (ke.isUp()) {
+            listState.selectPrevious();
+            return true;
+        }
+        if (ke.isDown()) {
+            listState.selectNext(entries.size());
+            return true;
+        }
+        if (ke.isPageUp()) {
+            for (int i = 0; i < 20; i++) {
+                listState.selectPrevious();
+            }
+            return true;
+        }
+        if (ke.isPageDown()) {
+            for (int i = 0; i < 20; i++) {
+                listState.selectNext(entries.size());
+            }
+            return true;
+        }
+        if (ke.isHome()) {
+            listState.select(0);
+            return true;
+        }
+        if (ke.isEnd()) {
+            listState.select(entries.size() - 1);
+            return true;
+        }
+        if (ke.isDeleteBackward()) {
+            if (currentDir != null && !currentDir.equals(rootDir)) {
+                loadDirectory(currentDir.getParent());
+            }
+            return true;
+        }
+        if (ke.isConfirm()) {
+            openSelectedEntry();
+            return true;
+        }
+        return false;
+    }
+
+    private void openSelectedEntry() {
+        Integer sel = listState.selected();
+        if (sel != null && sel < entries.size()) {
+            FilesBrowser.FileEntry entry = entries.get(sel);
+            if (entry.directory()) {
+                loadDirectory(Path.of(entry.path()));
+            } else {
+                Path filePath = Path.of(entry.path());
+                sourceViewer.loadFile(filePath);
+                if (isCamelSourceFile(filePath)) {
+                    
sourceViewer.setQuickDocProvider(this::provideCamelQuickDocs);
+                }
+                focusOnViewer = true;
+            }
+        }
+    }
+
+    private boolean isCamelSourceFile(Path path) {
+        String name = path.getFileName().toString().toLowerCase();
+        if (name.endsWith(".camel.yaml") || name.endsWith(".camel.yml")
+                || name.endsWith(".kamelet.yaml") || 
name.endsWith(".kamelet.yml")) {
+            return true;
+        }
+        if (name.endsWith(".yaml") || name.endsWith(".yml")) {
+            return TuiHelper.isCamelYaml(path);
+        }
+        if (name.endsWith(".xml")) {
+            return TuiHelper.isCamelXml(path);
+        }
+        return false;
+    }
+
+    private CamelCatalog getCatalog() {
+        IntegrationInfo info = ctx.findSelectedIntegration();
+        if (info == null || info.camelVersion == null) {
+            return null;
+        }
+        String version = info.camelVersion;
+        CamelCatalog cached = catalogCache.get(version);
+        if (cached != null) {
+            return cached;
+        }
+        try {
+            cached = CatalogLoader.loadCatalog(null, version, true);
+            if (cached != null) {
+                catalogCache.put(version, cached);
+            }
+            return cached;
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    private Map<Integer, List<String>> provideCamelQuickDocs(List<JsonObject> 
codeData) {
+        CamelCatalog catalog = getCatalog();
+        if (catalog == null || codeData.isEmpty()) {
+            return Map.of();
+        }
+
+        Map<Integer, List<String>> result = new LinkedHashMap<>();
+        for (int i = 0; i < codeData.size(); i++) {
+            String code = codeData.get(i).getString("code");
+            if (code == null) {
+                continue;
+            }
+
+            Matcher uriMatcher = YAML_URI_PATTERN.matcher(code);
+            if (uriMatcher.find()) {
+                String uri = uriMatcher.group(1);
+                if (uri.endsWith("\"")) {
+                    uri = uri.substring(0, uri.length() - 1);
+                }
+                RoutesTab.buildEndpointInlineDoc(result, codeData, catalog, 
uri, i);
+                continue;
+            }
+
+            Matcher eipMatcher = YAML_EIP_PATTERN.matcher(code);
+            if (eipMatcher.find()) {
+                String eipName = eipMatcher.group(1);
+                RoutesTab.buildEipInlineDoc(result, codeData, catalog, 
eipName, null, i);
+            }
+        }
+        return result;
+    }
+
+    private void renderFileList(Frame frame, Rect area) {
+        if (entries.isEmpty()) {
+            String noFilesMsg = rootDir == null ? "No source directory found" 
: "No files found";
+            frame.renderWidget(
+                    Paragraph.builder()
+                            .text(Text.from(Line.from(Span.styled("   " + 
noFilesMsg, Style.EMPTY.dim()))))
+                            .block(Block.builder()
+                                    
.borderType(BorderType.ROUNDED).borders(Borders.ALL)
+                                    .title(Title.from(Line.from(
+                                            Span.styled(" Files ",
+                                                    focusOnViewer ? 
Style.EMPTY.fg(Theme.accent()) : Theme.title()))))
+                                    .build())
+                            .build(),
+                    area);
+            return;
+        }
+
+        String panelTitle = " Files";
+        if (rootDir != null && currentDir != null && 
!currentDir.equals(rootDir)) {
+            panelTitle += ": " + rootDir.relativize(currentDir);
+        }
+        panelTitle += " ";
+
+        ListItem[] items = new ListItem[entries.size()];
+        for (int i = 0; i < entries.size(); i++) {
+            FilesBrowser.FileEntry entry = entries.get(i);
+            String label = " " + entry.emoji() + " " + entry.name();
+            if (entry.directory()) {
+                boolean dimDir = entry.name().startsWith(".") || 
"target".equals(entry.name());
+                Style dirStyle = dimDir ? Style.EMPTY.fg(Theme.accent()).dim() 
: Style.EMPTY.fg(Theme.accent());
+                items[i] = ListItem.from(Line.from(Span.styled(label, 
dirStyle)));
+            } else {
+                items[i] = ListItem.from(Line.from(Span.raw(label)));
+            }
+        }
+
+        Style titleStyle = focusOnViewer ? Style.EMPTY.fg(Theme.accent()) : 
Theme.title();
+        ListWidget list = ListWidget.builder()
+                .items(items)
+                .highlightStyle(focusOnViewer ? Theme.selectionBg().dim() : 
Theme.selectionBg())
+                .highlightSymbol("")
+                .scrollMode(ScrollMode.AUTO_SCROLL)
+                .block(Block.builder()
+                        .borderType(BorderType.ROUNDED).borders(Borders.ALL)
+                        .title(Title.from(Line.from(Span.styled(panelTitle, 
titleStyle))))
+                        .build())
+                .build();
+        frame.renderStatefulWidget(list, area, listState);
+
+        int visibleRows = area.height() - 2;
+        if (entries.size() > visibleRows) {
+            Rect scrollRect = new Rect(
+                    area.x() + area.width() - 1,
+                    area.y() + 1,
+                    1,
+                    visibleRows);
+            listScrollState.contentLength(entries.size());
+            listScrollState.viewportContentLength(visibleRows);
+            listScrollState.position(listState.offset());
+            frame.renderStatefulWidget(Scrollbar.builder().build(), 
scrollRect, listScrollState);
+        }
+    }
+
+    private static final DateTimeFormatter DATE_FMT = 
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+
+    private void renderInfoPanel(Frame frame, Rect area) {
+        List<Line> lines = new ArrayList<>();
+        Integer sel = listState.selected();
+        if (sel != null && sel < entries.size()) {
+            FilesBrowser.FileEntry entry = entries.get(sel);
+            Path path = Path.of(entry.path());
+            try {
+                BasicFileAttributes attrs = Files.readAttributes(path, 
BasicFileAttributes.class);
+                if (!entry.directory()) {
+                    lines.add(Line.from(
+                            Span.styled(" Size: ", Style.EMPTY.dim()),
+                            
Span.raw(FilesBrowser.formatFileSize(attrs.size()))));
+                }
+                String modified = attrs.lastModifiedTime().toInstant()
+                        .atZone(ZoneId.systemDefault()).format(DATE_FMT);
+                lines.add(Line.from(
+                        Span.styled(" Modified: ", Style.EMPTY.dim()),
+                        Span.raw(modified)));
+                if (!entry.directory()) {
+                    String ext = "";
+                    int dot = entry.name().lastIndexOf('.');
+                    if (dot > 0) {
+                        ext = entry.name().substring(dot + 1);
+                    }
+                    if (!ext.isEmpty()) {
+                        lines.add(Line.from(
+                                Span.styled(" Type: ", Style.EMPTY.dim()),
+                                Span.raw(ext)));
+                    }
+                }
+            } catch (IOException e) {
+                // ignore
+            }
+        }
+
+        frame.renderWidget(
+                Paragraph.builder()
+                        .text(Text.from(lines))
+                        .block(Block.builder()
+                                
.borderType(BorderType.ROUNDED).borders(Borders.ALL)
+                                .title(Title.from(Line.from(
+                                        Span.styled(" Info ", focusOnViewer ? 
Style.EMPTY.fg(Theme.accent()) : Theme.title()))))
+                                .build())
+                        .build(),
+                area);
+    }
+
+    private void renderSourcePanel(Frame frame, Rect area) {
+        Style sourceTitleStyle = focusOnViewer ? Theme.title() : 
Style.EMPTY.fg(Theme.accent());
+        if (sourceViewer.isVisible()) {
+            sourceViewer.setTitleStyle(sourceTitleStyle);
+            sourceViewer.setFocused(focusOnViewer);
+            sourceViewer.render(frame, area);
+        } else {
+            List<Line> lines = new ArrayList<>();
+            lines.add(Line.from(Span.raw("")));
+            lines.add(Line.from(Span.styled("   Select a file and press Enter 
to view source", Style.EMPTY.dim())));
+
+            frame.renderWidget(
+                    Paragraph.builder()
+                            .text(Text.from(lines))
+                            .block(Block.builder()
+                                    
.borderType(BorderType.ROUNDED).borders(Borders.ALL)
+                                    .title(Title.from(Line.from(
+                                            Span.styled(" Source ", 
sourceTitleStyle))))
+                                    .build())
+                            .build(),
+                    area);
+        }
+    }
+}
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 83bf0e67da04..cc8aadae919e 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
@@ -93,6 +93,8 @@ class SourceViewer {
     private QuickDocProvider quickDocProvider;
     private boolean quickDocEnabled;
     private Map<Integer, List<String>> quickDocEntries = 
Collections.emptyMap();
+    private Style titleStyle;
+    private boolean focused = true;
 
     private record CachedSource(
             List<String> lines, List<JsonObject> codeData,
@@ -103,6 +105,14 @@ class SourceViewer {
         return visible;
     }
 
+    void setTitleStyle(Style style) {
+        this.titleStyle = style;
+    }
+
+    void setFocused(boolean focused) {
+        this.focused = focused;
+    }
+
     void hide() {
         visible = false;
         onLineSelected = null;
@@ -193,7 +203,7 @@ class SourceViewer {
             onLineSelected = null;
             return true;
         }
-        if (isMarkdownFile && ke.isKey(KeyCode.TAB)) {
+        if (isMarkdownFile && ke.isChar(' ')) {
             markdownMode = !markdownMode;
             return true;
         }
@@ -213,7 +223,7 @@ class SourceViewer {
             }
             return true;
         }
-        if (currentRouteId != null && ke.isKey(KeyCode.TAB)) {
+        if (currentRouteId != null && ke.isChar(' ')) {
             String[] formats = { "yaml", "java", "xml" };
             int idx = 0;
             for (int i = 0; i < formats.length; i++) {
@@ -222,11 +232,7 @@ class SourceViewer {
                     break;
                 }
             }
-            if (ke.hasShift()) {
-                idx = (idx - 1 + formats.length) % formats.length;
-            } else {
-                idx = (idx + 1) % formats.length;
-            }
+            idx = (idx + 1) % formats.length;
             quickDocEntries = Collections.emptyMap();
             switchFormat(formats[idx]);
             return true;
@@ -475,7 +481,7 @@ class SourceViewer {
         if (markdownMode) {
             TuiHelper.hint(spans, "Esc/c", "close");
             TuiHelper.hint(spans, TuiIcons.HINT_SCROLL, "scroll");
-            TuiHelper.hint(spans, "Tab", "format");
+            TuiHelper.hint(spans, "Space", "format");
             TuiHelper.hint(spans, "PgUp/PgDn", "page");
             return;
         }
@@ -493,7 +499,7 @@ class SourceViewer {
         }
         TuiHelper.hint(spans, TuiIcons.HINT_SCROLL, "navigate");
         if (isMarkdownFile || currentRouteId != null) {
-            TuiHelper.hint(spans, "Tab", "format");
+            TuiHelper.hint(spans, "Space", "format");
         }
         search.renderSearchHints(spans);
         TuiHelper.hint(spans, "w", "wrap" + (wordWrap ? " [on]" : " [off]"));
@@ -755,9 +761,10 @@ class SourceViewer {
 
     private Title buildTitle() {
         String info = title != null ? title : "";
+        Style ts = titleStyle != null ? titleStyle : Style.EMPTY;
         if (isMarkdownFile) {
             List<Span> spans = new ArrayList<>();
-            spans.add(Span.raw(" Source [" + info + "]  "));
+            spans.add(Span.styled(" Source [" + info + "]  ", ts));
             String mdLabel = "Markdown*";
             if (markdownMode) {
                 spans.add(Span.styled(mdLabel, Style.EMPTY.bold()));
@@ -774,11 +781,11 @@ class SourceViewer {
             return Title.from(Line.from(spans));
         }
         if (currentRouteId == null) {
-            return Title.from(Line.from(List.of(Span.raw(" Source [" + info + 
"] "))));
+            return Title.from(Line.from(List.of(Span.styled(" Source [" + info 
+ "] ", ts))));
         }
 
         List<Span> spans = new ArrayList<>();
-        spans.add(Span.raw(" Source [" + info + "]  "));
+        spans.add(Span.styled(" Source [" + info + "]  ", ts));
 
         String[] formats = { "yaml", "java", "xml" };
         String[] labels = { "YAML", "Java", "XML" };
@@ -946,11 +953,11 @@ class SourceViewer {
         Line highlighted = SyntaxHighlighter.highlightLine(code, language);
 
         List<Span> spans = new ArrayList<>();
-        Style selBg = Theme.selectionBg();
+        Style selBg = focused ? Theme.selectionBg() : 
Theme.selectionBg().dim();
         if (isSelected) {
-            spans.add(Span.styled(">> ", Theme.label().bold()));
+            spans.add(Span.styled(">> ", focused ? Theme.label().bold() : 
Theme.label().dim()));
             if (!prefix.isEmpty()) {
-                spans.add(Span.styled(prefix, 
Theme.label().bold().patch(selBg)));
+                spans.add(Span.styled(prefix, (focused ? Theme.label().bold() 
: Theme.label().dim()).patch(selBg)));
             }
             for (Span s : highlighted.spans()) {
                 spans.add(Span.styled(s.content(), s.style().patch(selBg)));
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
index 0fdc0a31873c..5f52269fb948 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
@@ -29,12 +29,12 @@ class TabRegistry {
 
     // Tab indices
     static final int TAB_OVERVIEW = 0;
-    static final int TAB_LOG = 1;
-    static final int TAB_ACTIVITY = 2;
-    static final int TAB_DIAGRAM = 3;
-    static final int TAB_ROUTES = 4;
-    static final int TAB_ENDPOINTS = 5;
-    static final int TAB_HTTP = 6;
+    static final int TAB_SOURCE = 1;
+    static final int TAB_LOG = 2;
+    static final int TAB_ACTIVITY = 3;
+    static final int TAB_DIAGRAM = 4;
+    static final int TAB_ROUTES = 5;
+    static final int TAB_ENDPOINTS = 6;
     static final int TAB_HISTORY = 7;
     static final int TAB_ERRORS = 8;
     static final int TAB_MORE = 9;
@@ -75,6 +75,7 @@ class TabRegistry {
     private RouteControllerTab routeControllerTab;
     private EndpointsTab endpointsTab;
     private HttpTab httpTab;
+    private SourceTab sourceTab;
     private HealthTab healthTab;
     private HistoryTab historyTab;
     private CircuitBreakerTab circuitBreakerTab;
@@ -129,6 +130,7 @@ class TabRegistry {
         sqlTraceTab = new SqlTraceTab(ctx);
         endpointsTab = new EndpointsTab(ctx, dataService.metrics());
         httpTab = new HttpTab(ctx);
+        sourceTab = new SourceTab(ctx);
         healthTab = new HealthTab(ctx);
         historyTab = new HistoryTab(ctx, dataService.traces(), 
dataService.traceFilePositions());
         circuitBreakerTab = new CircuitBreakerTab(ctx, dataService.metrics());
@@ -165,6 +167,7 @@ class TabRegistry {
                 new MoreTab(TuiIcons.TAB_BROWSE, "Browse", "Bro&wse", 
browseTab, "Routing"),
                 new MoreTab(TuiIcons.TAB_CIRCUIT_BREAKER, "Circuit Breaker", 
"&Circuit Breaker", circuitBreakerTab, "Routing"),
                 new MoreTab(TuiIcons.TAB_CONSUMERS, "Consumers", "Co&nsumers", 
consumersTab, "Routing"),
+                new MoreTab(TuiIcons.TAB_HTTP, "HTTP", "&HTTP", httpTab, 
"Routing"),
                 new MoreTab(TuiIcons.TAB_INFLIGHT, "Inflight", "In&flight", 
inflightTab, "Routing"),
                 new MoreTab(TuiIcons.TAB_PRODUCERS, "Producers", "Prod&ucers", 
producersTab, "Routing"),
                 new MoreTab(
@@ -208,7 +211,7 @@ class TabRegistry {
             case TAB_DIAGRAM -> diagramTab;
             case TAB_ROUTES -> routesTab;
             case TAB_ENDPOINTS -> endpointsTab;
-            case TAB_HTTP -> httpTab;
+            case TAB_SOURCE -> sourceTab;
             case TAB_HISTORY -> historyTab;
             case TAB_ERRORS -> errorsTab;
             case TAB_MORE -> activeMoreTab;
@@ -301,7 +304,7 @@ class TabRegistry {
     void resetIntegrationTabState(DataRefreshService dataService, FilesBrowser 
filesBrowser) {
         diagramTab.onIntegrationChanged();
         routesTab.onIntegrationChanged();
-        httpTab.onIntegrationChanged();
+        sourceTab.onIntegrationChanged();
         logTab.onIntegrationChanged();
         historyTab.onIntegrationChanged();
         for (MoreTab mt : moreTabs) {
@@ -338,6 +341,10 @@ class TabRegistry {
         return httpTab;
     }
 
+    SourceTab sourceTab() {
+        return sourceTab;
+    }
+
     HealthTab healthTab() {
         return healthTab;
     }
@@ -447,14 +454,14 @@ class TabRegistry {
         List<TabEntry> entries = new ArrayList<>();
         entries.add(
                 new TabEntry(icon(TAB_OVERVIEW), "Overview", 
overviewTab.description(), "1", TAB_OVERVIEW, -1, overviewTab));
-        entries.add(new TabEntry(icon(TAB_LOG), "Log", logTab.description(), 
"2", TAB_LOG, -1, logTab));
+        entries.add(new TabEntry(icon(TAB_SOURCE), "Source", 
sourceTab.description(), "2", TAB_SOURCE, -1, sourceTab));
+        entries.add(new TabEntry(icon(TAB_LOG), "Log", logTab.description(), 
"3", TAB_LOG, -1, logTab));
         entries.add(
-                new TabEntry(icon(TAB_ACTIVITY), "Activity", 
activityTab.description(), "3", TAB_ACTIVITY, -1, activityTab));
-        entries.add(new TabEntry(icon(TAB_DIAGRAM), "Diagram", 
diagramTab.description(), "4", TAB_DIAGRAM, -1, diagramTab));
-        entries.add(new TabEntry(icon(TAB_ROUTES), "Routes", 
routesTab.description(), "5", TAB_ROUTES, -1, routesTab));
+                new TabEntry(icon(TAB_ACTIVITY), "Activity", 
activityTab.description(), "4", TAB_ACTIVITY, -1, activityTab));
+        entries.add(new TabEntry(icon(TAB_DIAGRAM), "Diagram", 
diagramTab.description(), "5", TAB_DIAGRAM, -1, diagramTab));
+        entries.add(new TabEntry(icon(TAB_ROUTES), "Routes", 
routesTab.description(), "6", TAB_ROUTES, -1, routesTab));
         entries.add(new TabEntry(
-                icon(TAB_ENDPOINTS), "Endpoints", endpointsTab.description(), 
"6", TAB_ENDPOINTS, -1, endpointsTab));
-        entries.add(new TabEntry(icon(TAB_HTTP), "HTTP", 
httpTab.description(), "7", TAB_HTTP, -1, httpTab));
+                icon(TAB_ENDPOINTS), "Endpoints", endpointsTab.description(), 
"7", TAB_ENDPOINTS, -1, endpointsTab));
         entries.add(new TabEntry(icon(TAB_HISTORY), "Inspect", 
historyTab.description(), "8", TAB_HISTORY, -1, historyTab));
         entries.add(new TabEntry(icon(TAB_ERRORS), "Errors", 
errorsTab.description(), "9", TAB_ERRORS, -1, errorsTab));
         for (int i = 0; i < moreTabs.size(); i++) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiHelper.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiHelper.java
index 16c5a4c624c4..d062c3f34f40 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiHelper.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiHelper.java
@@ -818,7 +818,7 @@ final class TuiHelper {
         return TuiIcons.CLIPBOARD;
     }
 
-    private static boolean isCamelYaml(Path path) {
+    static boolean isCamelYaml(Path path) {
         try {
             String content = Files.readString(path, StandardCharsets.UTF_8);
             for (String marker : CAMEL_YAML_MARKERS) {
@@ -832,7 +832,7 @@ final class TuiHelper {
         return false;
     }
 
-    private static boolean isCamelXml(Path path) {
+    static boolean isCamelXml(Path path) {
         try {
             String content = Files.readString(path, StandardCharsets.UTF_8);
             for (String marker : CAMEL_XML_MARKERS) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIcons.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIcons.java
index ad41009894f1..3e96996b5b6c 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIcons.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIcons.java
@@ -117,6 +117,7 @@ final class TuiIcons {
     static final String TAB_ROUTES = "🔀";
     static final String TAB_ENDPOINTS = ENDPOINT;
     static final String TAB_HTTP = ONLINE;
+    static final String TAB_SOURCE = COMPUTER;
     static final String TAB_HEALTH = DOCTOR;
     static final String TAB_INSPECT = GO_TO;
     static final String TAB_ERRORS = FAIL;
@@ -152,8 +153,8 @@ final class TuiIcons {
 
     /** Icons for {@link TabRegistry#TAB_OVERVIEW}..{@link 
TabRegistry#TAB_MORE} (in order). */
     static final List<String> PRIMARY_TAB_ICONS = List.of(
-            TAB_OVERVIEW, TAB_LOG, TAB_ACTIVITY, TAB_DIAGRAM, TAB_ROUTES,
-            TAB_ENDPOINTS, TAB_HTTP, TAB_INSPECT, TAB_ERRORS, TAB_MORE);
+            TAB_OVERVIEW, TAB_SOURCE, TAB_LOG, TAB_ACTIVITY, TAB_DIAGRAM,
+            TAB_ROUTES, TAB_ENDPOINTS, TAB_INSPECT, TAB_ERRORS, TAB_MORE);
 
     /** Marker placed immediately before a label's keyboard-shortcut letter 
(Windows-style mnemonic). */
     static final char MNEMONIC_MARKER = '&';
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TabBarRenderTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TabBarRenderTest.java
index 9f149007bae8..07b0e153bbd2 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TabBarRenderTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TabBarRenderTest.java
@@ -33,7 +33,7 @@ class TabBarRenderTest {
 
     @Test
     void primaryTabHeadersStartWithIconAndTwoSpaces() {
-        String[] names = { "Overview", "Log", "Diagram", "Route", "Endpoint", 
"HTTP", "Health", "Inspect", "Errors" };
+        String[] names = { "Overview", "Source", "Log", "Activity", "Diagram", 
"Route", "Endpoint", "Inspect", "Errors" };
         for (int i = 0; i < names.length; i++) {
             String header
                     = 
Line.from(TuiIcons.primaryTabHeader(TuiIcons.PRIMARY_TAB_ICONS.get(i), 
String.valueOf(i + 1), names[i]))
@@ -53,8 +53,8 @@ class TabBarRenderTest {
     @Test
     void routeAndTopLabelsHaveEqualWidth() {
         // toggling Top mode must not shift the tab bar
-        int route = Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_ROUTES, 
"4", "Route")).width();
-        int top = Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_ROUTES, 
"4", " Top ")).width();
+        int route = Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_ROUTES, 
"6", "Route")).width();
+        int top = Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_ROUTES, 
"6", " Top ")).width();
         assertEquals(route, top, "Route and Top tab cells must be the same 
width");
     }
 
@@ -79,12 +79,12 @@ class TabBarRenderTest {
     private static Line[] fullTabLabels(String routesLabel) {
         return new Line[] {
                 Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_OVERVIEW, 
"1", "Overview")),
-                Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_LOG, "2", 
"Log")),
-                Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_DIAGRAM, "3", 
"Diagram")),
-                Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_ROUTES, "4", 
routesLabel)),
-                Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_ENDPOINTS, 
"5", "Endpoint")),
-                Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_HTTP, "6", 
"HTTP")),
-                Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_HEALTH, "7", 
"Health")),
+                Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_SOURCE, "2", 
"Source")),
+                Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_LOG, "3", 
"Log")),
+                Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_ACTIVITY, 
"4", "Activity")),
+                Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_DIAGRAM, "5", 
"Diagram")),
+                Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_ROUTES, "6", 
routesLabel)),
+                Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_ENDPOINTS, 
"7", "Endpoint")),
                 Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_INSPECT, "8", 
"Inspect")),
                 Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_ERRORS, "9", 
"Errors")),
                 Line.from(TuiIcons.primaryTabHeader(TuiIcons.TAB_MORE, "0", 
TuiIcons.moreTabLabel())),
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistryTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistryTest.java
index 86d153cac9c6..6b384a904836 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistryTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistryTest.java
@@ -18,7 +18,6 @@ package org.apache.camel.dsl.jbang.core.commands.tui;
 
 import java.nio.file.Path;
 import java.util.List;
-import java.util.Set;
 import java.util.concurrent.atomic.AtomicReference;
 
 import dev.tamboui.text.CharWidth;
@@ -82,8 +81,8 @@ class TabRegistryTest {
     }
 
     @Test
-    void moreTabsHasTwentySixEntries() {
-        assertEquals(26, registry.moreTabs().size());
+    void moreTabsHasTwentySevenEntries() {
+        assertEquals(27, registry.moreTabs().size());
     }
 
     @Test
@@ -109,17 +108,22 @@ class TabRegistryTest {
         // MORE_SHORTCUTS array carried before the MoreTab refactor. A label 
edit that repoints a key must fail here.
         List<Character> shortcuts = 
registry.moreTabs().stream().map(TabRegistry.MoreTab::shortcut).toList();
         assertEquals(
-                List.of('W', 'C', 'N', 'F', 'U', 'Z', 'E', 'I', 'X', 'O', 'J', 
'K', 'Q', 'R', 'A', 'H', 'M', 'Y', 'P', 'S',
-                        'T', 'B', 'L', 'G', 'V', 'D'),
+                List.of('W', 'C', 'N', 'H', 'F', 'U', 'Z', 'E', 'I', 'X', 'O', 
'J', 'K', 'Q', 'R', 'A', 'H', 'M', 'Y',
+                        'P', 'S', 'T', 'B', 'L', 'G', 'V', 'D'),
                 shortcuts, "More tab shortcut letters must match the 
historical sequence");
     }
 
     @Test
-    void moreTabShortcutsAreUnique() {
-        // morePopupShortcut() returns the first matching tab, so a duplicated 
letter would silently shadow a later tab.
+    void moreTabShortcutsHaveAtMostTwoDuplicates() {
+        // Duplicate mnemonics are allowed; the popup cycles through them on 
repeated key press.
         List<Character> shortcuts = 
registry.moreTabs().stream().map(TabRegistry.MoreTab::shortcut).toList();
-        assertEquals(shortcuts.size(), Set.copyOf(shortcuts).size(),
-                "More tab shortcut letters must be unique: " + shortcuts);
+        java.util.Map<Character, Long> counts = shortcuts.stream()
+                .collect(java.util.stream.Collectors.groupingBy(c -> c, 
java.util.stream.Collectors.counting()));
+        for (var entry : counts.entrySet()) {
+            assertTrue(entry.getValue() <= 2,
+                    "More tab shortcut '" + entry.getKey() + "' appears " + 
entry.getValue()
+                                              + " times (max 2): " + 
shortcuts);
+        }
     }
 
     @Test
@@ -196,12 +200,12 @@ class TabRegistryTest {
     @Test
     void tabConstantsAreSequential() {
         assertEquals(0, TabRegistry.TAB_OVERVIEW, "TAB_OVERVIEW should be 0");
-        assertEquals(1, TabRegistry.TAB_LOG, "TAB_LOG should be 1");
-        assertEquals(2, TabRegistry.TAB_ACTIVITY, "TAB_ACTIVITY should be 2");
-        assertEquals(3, TabRegistry.TAB_DIAGRAM, "TAB_DIAGRAM should be 3");
-        assertEquals(4, TabRegistry.TAB_ROUTES, "TAB_ROUTES should be 4");
-        assertEquals(5, TabRegistry.TAB_ENDPOINTS, "TAB_ENDPOINTS should be 
5");
-        assertEquals(6, TabRegistry.TAB_HTTP, "TAB_HTTP should be 6");
+        assertEquals(1, TabRegistry.TAB_SOURCE, "TAB_SOURCE should be 1");
+        assertEquals(2, TabRegistry.TAB_LOG, "TAB_LOG should be 2");
+        assertEquals(3, TabRegistry.TAB_ACTIVITY, "TAB_ACTIVITY should be 3");
+        assertEquals(4, TabRegistry.TAB_DIAGRAM, "TAB_DIAGRAM should be 4");
+        assertEquals(5, TabRegistry.TAB_ROUTES, "TAB_ROUTES should be 5");
+        assertEquals(6, TabRegistry.TAB_ENDPOINTS, "TAB_ENDPOINTS should be 
6");
         assertEquals(7, TabRegistry.TAB_HISTORY, "TAB_HISTORY should be 7");
         assertEquals(8, TabRegistry.TAB_ERRORS, "TAB_ERRORS should be 8");
         assertEquals(9, TabRegistry.TAB_MORE, "TAB_MORE should be 9");
@@ -223,7 +227,7 @@ class TabRegistryTest {
         int[] tabs = {
                 TabRegistry.TAB_OVERVIEW, TabRegistry.TAB_LOG, 
TabRegistry.TAB_ACTIVITY,
                 TabRegistry.TAB_DIAGRAM, TabRegistry.TAB_ROUTES, 
TabRegistry.TAB_ENDPOINTS,
-                TabRegistry.TAB_HTTP, TabRegistry.TAB_HISTORY, 
TabRegistry.TAB_ERRORS,
+                TabRegistry.TAB_SOURCE, TabRegistry.TAB_HISTORY, 
TabRegistry.TAB_ERRORS,
                 TabRegistry.TAB_MORE
         };
         for (int i = 0; i < tabs.length; i++) {
@@ -242,7 +246,7 @@ class TabRegistryTest {
         assertTrue(TabRegistry.TAB_DIAGRAM >= 0, "TAB_DIAGRAM should be 
non-negative");
         assertTrue(TabRegistry.TAB_ROUTES >= 0, "TAB_ROUTES should be 
non-negative");
         assertTrue(TabRegistry.TAB_ENDPOINTS >= 0, "TAB_ENDPOINTS should be 
non-negative");
-        assertTrue(TabRegistry.TAB_HTTP >= 0, "TAB_HTTP should be 
non-negative");
+        assertTrue(TabRegistry.TAB_SOURCE >= 0, "TAB_SOURCE should be 
non-negative");
         assertTrue(TabRegistry.TAB_HISTORY >= 0, "TAB_HISTORY should be 
non-negative");
         assertTrue(TabRegistry.TAB_ERRORS >= 0, "TAB_ERRORS should be 
non-negative");
         assertTrue(TabRegistry.TAB_MORE >= 0, "TAB_MORE should be 
non-negative");
@@ -256,7 +260,7 @@ class TabRegistryTest {
         assertTrue(TabRegistry.TAB_DIAGRAM < TabRegistry.NUM_TABS);
         assertTrue(TabRegistry.TAB_ROUTES < TabRegistry.NUM_TABS);
         assertTrue(TabRegistry.TAB_ENDPOINTS < TabRegistry.NUM_TABS);
-        assertTrue(TabRegistry.TAB_HTTP < TabRegistry.NUM_TABS);
+        assertTrue(TabRegistry.TAB_SOURCE < TabRegistry.NUM_TABS);
         assertTrue(TabRegistry.TAB_HISTORY < TabRegistry.NUM_TABS);
         assertTrue(TabRegistry.TAB_ERRORS < TabRegistry.NUM_TABS);
         assertTrue(TabRegistry.TAB_MORE < TabRegistry.NUM_TABS);
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIconsTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIconsTest.java
index 87564bfae514..6ed452a76918 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIconsTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIconsTest.java
@@ -40,12 +40,12 @@ class TuiIconsTest {
         // Guards that PRIMARY_TAB_ICONS is indexed by the TAB_* constants: 
reordering the list must break this,
         // otherwise TabRegistry.icon(index) would attach the wrong emoji to 
Go-to entries.
         assertEquals(TuiIcons.TAB_OVERVIEW, 
TuiIcons.PRIMARY_TAB_ICONS.get(TabRegistry.TAB_OVERVIEW));
+        assertEquals(TuiIcons.TAB_SOURCE, 
TuiIcons.PRIMARY_TAB_ICONS.get(TabRegistry.TAB_SOURCE));
         assertEquals(TuiIcons.TAB_LOG, 
TuiIcons.PRIMARY_TAB_ICONS.get(TabRegistry.TAB_LOG));
         assertEquals(TuiIcons.TAB_ACTIVITY, 
TuiIcons.PRIMARY_TAB_ICONS.get(TabRegistry.TAB_ACTIVITY));
         assertEquals(TuiIcons.TAB_DIAGRAM, 
TuiIcons.PRIMARY_TAB_ICONS.get(TabRegistry.TAB_DIAGRAM));
         assertEquals(TuiIcons.TAB_ROUTES, 
TuiIcons.PRIMARY_TAB_ICONS.get(TabRegistry.TAB_ROUTES));
         assertEquals(TuiIcons.TAB_ENDPOINTS, 
TuiIcons.PRIMARY_TAB_ICONS.get(TabRegistry.TAB_ENDPOINTS));
-        assertEquals(TuiIcons.TAB_HTTP, 
TuiIcons.PRIMARY_TAB_ICONS.get(TabRegistry.TAB_HTTP));
         assertEquals(TuiIcons.TAB_INSPECT, 
TuiIcons.PRIMARY_TAB_ICONS.get(TabRegistry.TAB_HISTORY));
         assertEquals(TuiIcons.TAB_ERRORS, 
TuiIcons.PRIMARY_TAB_ICONS.get(TabRegistry.TAB_ERRORS));
         assertEquals(TuiIcons.TAB_MORE, 
TuiIcons.PRIMARY_TAB_ICONS.get(TabRegistry.TAB_MORE));

Reply via email to