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 bebbafa9c769 CAMEL-23672: TUI - Files popup, source viewer 
enhancements, and history improvements (#23802)
bebbafa9c769 is described below

commit bebbafa9c769ac17d501426653d5639383c569f3
Author: Claus Ibsen <[email protected]>
AuthorDate: Fri Jun 5 23:44:09 2026 +0200

    CAMEL-23672: TUI - Files popup, source viewer enhancements, and history 
improvements (#23802)
    
    * CAMEL-23672: camel-jbang - Update F1 help for TUI diagram info panel
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    * CAMEL-23672: camel-jbang - TUI BHPV change indicators in diagram info 
panel and detail view
    
    Add Changed: line with BHPV indicators to diagram info panel.
    Highlight section headers (Body, Headers, Properties, Variables) in
    yellow when changed vs green when unchanged, both in the diagram info
    panel and the table detail view. Shorten Processor label to Proc.
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    * CAMEL-23672: camel-jbang - TUI highlight individual changed keys in yellow
    
    When headers/properties/variables change between steps, highlight the
    specific keys that were added or modified in yellow while unchanged
    keys stay cyan. Applied in both table detail view and diagram info panel.
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    * CAMEL-23672: camel-jbang - TUI History badge counts by breadcrumbId
    
    Use breadcrumbId header to correlate related exchanges so parent and
    child exchanges from multicast/splitter count as one logical exchange
    in the tab badge. Falls back to exchangeId when no breadcrumbId present.
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    * CAMEL-23672: TUI - Align F2 sub-popup Y position with actions menu
    
    Co-Authored-By: Claude <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    * CAMEL-23672: TUI - Show book emoji and direct 'd' key for integration docs
    
    Move doc viewing from F2 menu item to a direct 'd' keybinding in the
    overview tab. Show a book emoji next to integrations that have a
    README file. The footer shows 'd docs' hint when the selected
    integration has documentation.
    
    Co-Authored-By: Claude <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    * CAMEL-23672: TUI - Add tui_get_readme MCP tool for integration docs
    
    Allows AI agents to retrieve the README/documentation content from a
    running integration via the MCP protocol. Accepts an optional name
    parameter; defaults to the currently selected integration.
    
    Co-Authored-By: Claude <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    * CAMEL-23672: TUI - Improve source view highlight readability
    
    Change selected line background from dark gray to dark navy blue for
    better contrast with red syntax tokens. Extend the highlight to fill
    the full viewport width so the selection bar doesn't stop at end of text.
    
    Co-Authored-By: Claude <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    * CAMEL-23672: TUI - Add files popup and source viewer enhancements
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    * CAMEL-23672: TUI - Detect kamelet yaml files as Camel emoji
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    ---------
    
    Signed-off-by: Claus Ibsen <[email protected]>
    Co-authored-by: Claude Opus 4.6 <[email protected]>
---
 .../dsl/jbang/core/commands/tui/ActionsPopup.java  |  59 +++-
 .../dsl/jbang/core/commands/tui/CamelMonitor.java  | 333 +++++++++++++++++-
 .../dsl/jbang/core/commands/tui/ErrorsTab.java     |  25 +-
 .../dsl/jbang/core/commands/tui/HistoryTab.java    | 156 ++++++---
 .../dsl/jbang/core/commands/tui/OverviewTab.java   |  15 +-
 .../dsl/jbang/core/commands/tui/SourceViewer.java  | 386 +++++++++++++++++++--
 .../dsl/jbang/core/commands/tui/TuiMcpServer.java  |  24 ++
 7 files changed, 895 insertions(+), 103 deletions(-)

diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
index 6dcfcc91f5c0..afed843f228b 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
@@ -73,7 +73,7 @@ class ActionsPopup {
         RUN_EXAMPLE,
         RUN_FOLDER,
         RUN_INFRA,
-        SHOW_DOCS,
+        BROWSE_FILES,
         DOCTOR,
         RESET_STATS,
         RESET_SCREEN,
@@ -100,6 +100,7 @@ class ActionsPopup {
     private final Runnable burstCallback;
     private Runnable resetStatsAction;
     private Runnable resetScreenAction;
+    private Runnable browseFilesAction;
     private final Supplier<Boolean> tapeRecordingActive;
     private MonitorContext ctx;
     private boolean mcpEnabled;
@@ -195,6 +196,10 @@ class ActionsPopup {
         this.resetScreenAction = resetScreenAction;
     }
 
+    void setBrowseFilesAction(Runnable browseFilesAction) {
+        this.browseFilesAction = browseFilesAction;
+    }
+
     void setMcpEnabled(
             boolean enabled, int port, Supplier<String> connectedClient, 
Supplier<List<TuiMcpServer.LogEntry>> activityLog) {
         this.mcpEnabled = enabled;
@@ -316,7 +321,7 @@ class ActionsPopup {
         labels.add("Run an example...");
         labels.add("Run from folder...");
         labels.add("Run Dev/Infra Service...");
-        labels.add("Show Integration Doc");
+        labels.add("Browse Files...");
         labels.add("───");
         // Group 2: Diagnostics
         labels.add("Run Doctor");
@@ -535,8 +540,6 @@ class ActionsPopup {
                         openExampleBrowser();
                     } else if (action == Action.RUN_FOLDER) {
                         openFolderInput();
-                    } else if (action == Action.SHOW_DOCS) {
-                        openDocPicker();
                     } else if (action == Action.SCREENSHOT) {
                         showActionsMenu = false;
                         screenshotAction.run();
@@ -549,6 +552,13 @@ class ActionsPopup {
                     } else if (action == Action.TAPE_INSTRUCTIONS) {
                         showActionsMenu = false;
                         openTapeInstructions();
+                    } else if (action == Action.BROWSE_FILES) {
+                        if (ctx != null && ctx.selectedPid != null && 
!ctx.isInfraSelected()) {
+                            showActionsMenu = false;
+                            if (browseFilesAction != null) {
+                                browseFilesAction.run();
+                            }
+                        }
                     } else if (action == Action.DOCTOR) {
                         showActionsMenu = false;
                         doctorPopup.open();
@@ -737,7 +747,10 @@ class ActionsPopup {
         items.add(ListItem.from("  🐪 Run an example..."));
         items.add(ListItem.from("  📂 Run from folder..."));
         items.add(ListItem.from("  🔧 Run Dev/Infra Service..."));
-        items.add(ListItem.from("  📖 Show Integration Doc"));
+        boolean hasSelection = ctx != null && ctx.selectedPid != null && 
!ctx.isInfraSelected();
+        items.add(hasSelection
+                ? ListItem.from("  📁 Browse Files...")
+                : ListItem.from("  📁 Browse 
Files...").style(Style.EMPTY.dim()));
         items.add(ListItem.from(divider).style(Style.EMPTY.dim()));
         // Group 2: Diagnostics
         items.add(ListItem.from("  🩺 Run Doctor"));
@@ -775,10 +788,10 @@ class ActionsPopup {
             return;
         }
         int popupW = Math.min(100, area.width() - 4);
-        int popupH = Math.min(exampleCatalog.size() + 10, Math.min(22, 
area.height() - 6));
+        int popupH = Math.min(exampleCatalog.size() + 10, Math.min(22, 
area.height() - 4));
         int x = area.left() + Math.max(0, (area.width() - popupW) / 2);
-        int y = area.top() + Math.max(0, (area.height() - popupH) / 2);
-        Rect popup = new Rect(x, y, Math.min(popupW, area.width()), 
Math.min(popupH, area.height()));
+        int y = area.top() + 2;
+        Rect popup = new Rect(x, y, Math.min(popupW, area.width()), 
Math.min(popupH, area.height() - 2));
 
         frame.renderWidget(Clear.INSTANCE, popup);
 
@@ -897,10 +910,10 @@ class ActionsPopup {
             return;
         }
         int popupW = Math.min(60, area.width() - 4);
-        int popupH = Math.min(docPickerIntegrations.size() + 2, Math.min(15, 
area.height() - 6));
+        int popupH = Math.min(docPickerIntegrations.size() + 2, Math.min(15, 
area.height() - 4));
         int x = area.left() + Math.max(0, (area.width() - popupW) / 2);
-        int y = area.top() + Math.max(0, (area.height() - popupH) / 2);
-        Rect popup = new Rect(x, y, Math.min(popupW, area.width()), 
Math.min(popupH, area.height()));
+        int y = area.top() + 2;
+        Rect popup = new Rect(x, y, Math.min(popupW, area.width()), 
Math.min(popupH, area.height() - 2));
 
         frame.renderWidget(Clear.INSTANCE, popup);
         List<ListItem> items = new ArrayList<>();
@@ -944,6 +957,11 @@ class ActionsPopup {
         docPickerState.select(0);
     }
 
+    void openDoc(IntegrationInfo info) {
+        showActionsMenu = false;
+        loadDocFromIntegration(info);
+    }
+
     private void loadDocFromSelectedIntegration() {
         Integer sel = docPickerState.selected();
         if (sel == null || docPickerIntegrations == null || sel >= 
docPickerIntegrations.size()) {
@@ -1177,6 +1195,15 @@ class ActionsPopup {
         if (folder.isEmpty()) {
             return;
         }
+        // resolve ~ to home directory
+        if (folder.startsWith("~")) {
+            folder = System.getProperty("user.home") + folder.substring(1);
+        }
+        Path dirPath = Path.of(folder);
+        if (!Files.isDirectory(dirPath)) {
+            setNotification("Directory does not exist: " + folder, true);
+            return;
+        }
         folderHistory.remove(folder);
         folderHistory.add(0, folder);
         if (folderHistory.size() > 20) {
@@ -1184,7 +1211,7 @@ class ActionsPopup {
         }
         selectedFolder = folder;
         showFolderInput = false;
-        String displayName = Path.of(folder).getFileName().toString();
+        String displayName = dirPath.getFileName().toString();
         runOptionsForm.open(displayName, displayName, false, true);
     }
 
@@ -1644,10 +1671,10 @@ class ActionsPopup {
         }
         refreshInfraRunningState();
         int popupW = Math.min(100, area.width() - 4);
-        int popupH = Math.min(infraCatalog.size() + 2, Math.min(22, 
area.height() - 6));
+        int popupH = Math.min(infraCatalog.size() + 2, Math.min(22, 
area.height() - 4));
         int x = area.left() + Math.max(0, (area.width() - popupW) / 2);
-        int y = area.top() + Math.max(0, (area.height() - popupH) / 2);
-        Rect popup = new Rect(x, y, Math.min(popupW, area.width()), 
Math.min(popupH, area.height()));
+        int y = area.top() + 2;
+        Rect popup = new Rect(x, y, Math.min(popupW, area.width()), 
Math.min(popupH, area.height() - 2));
 
         frame.renderWidget(Clear.INSTANCE, popup);
 
@@ -1708,7 +1735,7 @@ class ActionsPopup {
         int popupW = 42;
         int popupH = hasMultiImpl ? 8 : 6;
         int x = area.left() + Math.max(0, (area.width() - popupW) / 2);
-        int y = area.top() + Math.max(0, (area.height() - popupH) / 2);
+        int y = area.top() + 2;
         Rect popup = new Rect(x, y, Math.min(popupW, area.width()), 
Math.min(popupH, area.height()));
 
         frame.renderWidget(Clear.INSTANCE, popup);
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 5c837bd1e96a..cf8f472d3f03 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
@@ -279,6 +279,16 @@ public class CamelMonitor extends CamelCommand {
     private boolean showSwitchPopup;
     private final ListState switchPopupState = new ListState();
 
+    // "Files" popup state
+    record FileEntry(String emoji, String name, long size, String path) {
+    }
+
+    private boolean showFilesPopup;
+    private String filesPopupTitle;
+    private final ListState filesPopupState = new ListState();
+    private List<FileEntry> fileEntries = Collections.emptyList();
+    private final SourceViewer overviewSourceViewer = new SourceViewer();
+
     // "More" dropdown state
     private boolean showMorePopup;
     private final ListState morePopupState = new ListState();
@@ -319,6 +329,7 @@ public class CamelMonitor extends CamelCommand {
         ctx = new MonitorContext(data, infraData);
         actionsPopup.setContext(ctx);
         actionsPopup.setResetStatsAction(this::resetStats);
+        actionsPopup.setBrowseFilesAction(this::openFilesPopup);
         logTab = new LogTab(ctx);
         diagramTab = new DiagramTab(ctx);
         routesTab = new RoutesTab(ctx);
@@ -439,6 +450,41 @@ public class CamelMonitor extends CamelCommand {
             if (actionsPopup.isVisible()) {
                 return actionsPopup.handleKeyEvent(ke);
             }
+            // "Files" popup
+            if (showFilesPopup) {
+                if (overviewSourceViewer.isVisible()) {
+                    if (overviewSourceViewer.handleKeyEvent(ke)) {
+                        return true;
+                    }
+                }
+                if (ke.isCancel()) {
+                    if (overviewSourceViewer.isVisible()) {
+                        overviewSourceViewer.hide();
+                    } else {
+                        showFilesPopup = false;
+                    }
+                    return true;
+                }
+                if (!overviewSourceViewer.isVisible()) {
+                    if (ke.isUp()) {
+                        filesPopupState.selectPrevious();
+                        return true;
+                    }
+                    if (ke.isDown()) {
+                        filesPopupState.selectNext(fileEntries.size());
+                        return true;
+                    }
+                    if (ke.isConfirm()) {
+                        Integer sel = filesPopupState.selected();
+                        if (sel != null && sel < fileEntries.size()) {
+                            FileEntry entry = fileEntries.get(sel);
+                            
overviewSourceViewer.loadFile(Path.of(entry.path()));
+                        }
+                        return true;
+                    }
+                }
+                return true;
+            }
             // "More" tab popup
             if (showMorePopup) {
                 if (ke.isCancel()) {
@@ -758,6 +804,17 @@ public class CamelMonitor extends CamelCommand {
                 restartSelectedProcess();
                 return true;
             }
+            if (tab == TAB_OVERVIEW && ke.isChar('d') && ctx.selectedPid != 
null && !isInfraSelected()) {
+                IntegrationInfo selInfo = findSelectedIntegration();
+                if (selInfo != null && selInfo.readmeFiles != null && 
!selInfo.readmeFiles.isEmpty()) {
+                    actionsPopup.openDoc(selInfo);
+                    return true;
+                }
+            }
+            if (tab == TAB_OVERVIEW && ke.isChar('f') && ctx.selectedPid != 
null && !isInfraSelected()) {
+                openFilesPopup();
+                return true;
+            }
             // Delegate remaining keys to active tab
             if (activeTab != null && activeTab.handleKeyEvent(ke)) {
                 return true;
@@ -776,6 +833,10 @@ public class CamelMonitor extends CamelCommand {
                 logTab.handlePaste(pe.text());
                 return true;
             }
+            if (overviewSourceViewer.isSearchInputActive()) {
+                overviewSourceViewer.handlePaste(pe.text());
+                return true;
+            }
         }
         if (event instanceof TickEvent) {
             long now = System.currentTimeMillis();
@@ -943,6 +1004,10 @@ public class CamelMonitor extends CamelCommand {
         circuitBreakerTab.onIntegrationChanged();
         inflightTab.onIntegrationChanged();
 
+        showFilesPopup = false;
+        fileEntries = Collections.emptyList();
+        overviewSourceViewer.reset();
+
         // Preload diagram data in background so it's ready when the user 
switches tabs
         routesTab.preloadDiagram();
         diagramTab.preloadDiagram();
@@ -1092,7 +1157,17 @@ public class CamelMonitor extends CamelCommand {
         long healthDownCount = hasSelection
                 ? sel.healthChecks.stream().filter(hc -> 
"DOWN".equals(hc.state)).count() : 0;
         long historyCount = hasSelection
-                ? historyTab.historyEntries.stream().map(e -> 
e.exchangeId).distinct().count()
+                ? historyTab.historyEntries.stream()
+                        .map(e -> {
+                            if (e.headers != null) {
+                                Object bid = e.headers.get("breadcrumbId");
+                                if (bid != null) {
+                                    return bid.toString();
+                                }
+                            }
+                            return e.exchangeId;
+                        })
+                        .distinct().count()
                 : 0;
         boolean hasTraces = hasSelection && !traces.get().isEmpty();
         int httpCount = hasSelection ? sel.httpEndpoints.size() : 0;
@@ -1204,6 +1279,10 @@ public class CamelMonitor extends CamelCommand {
         if (showSwitchPopup) {
             renderSwitchPopup(frame, area);
         }
+        // Render "Files" popup overlay when visible
+        if (showFilesPopup) {
+            renderFilesPopup(frame, area);
+        }
     }
 
     private void renderMorePopup(Frame frame, Rect area) {
@@ -1303,6 +1382,211 @@ public class CamelMonitor extends CamelCommand {
         frame.renderStatefulWidget(list, popup, switchPopupState);
     }
 
+    private void openFilesPopup() {
+        IntegrationInfo info = findSelectedIntegration();
+        if (info == null) {
+            return;
+        }
+        Path dir = resolveSourceDirectory(info);
+        if (dir == null || !Files.isDirectory(dir)) {
+            return;
+        }
+        List<FileEntry> entries = new ArrayList<>();
+        try (var stream = Files.list(dir)) {
+            stream.filter(Files::isRegularFile)
+                    .limit(99)
+                    .forEach(p -> {
+                        String name = p.getFileName().toString();
+                        String emoji = fileEmoji(p);
+                        long size = 0;
+                        try {
+                            size = Files.size(p);
+                        } catch (IOException e) {
+                            // ignore
+                        }
+                        entries.add(new FileEntry(emoji, name, size, 
p.toString()));
+                    });
+        } catch (IOException e) {
+            return;
+        }
+        if (entries.isEmpty()) {
+            return;
+        }
+        entries.sort(Comparator.comparing(FileEntry::name, 
String.CASE_INSENSITIVE_ORDER));
+        fileEntries = entries;
+        filesPopupTitle = info.name != null ? info.name : "?";
+        filesPopupState.select(0);
+        showFilesPopup = true;
+        overviewSourceViewer.reset();
+    }
+
+    private void renderFilesPopup(Frame frame, Rect area) {
+        if (overviewSourceViewer.isVisible()) {
+            frame.renderWidget(Clear.INSTANCE, area);
+            overviewSourceViewer.render(frame, area);
+            return;
+        }
+        if (fileEntries.isEmpty()) {
+            showFilesPopup = false;
+            return;
+        }
+
+        int nameWidth = fileEntries.stream().mapToInt(e -> 
e.name().length()).max().orElse(10);
+        int sizeWidth = fileEntries.stream().mapToInt(e -> 
formatFileSize(e.size()).length()).max().orElse(4);
+        int itemWidth = 4 + nameWidth + 2 + sizeWidth + 2;
+        int popupW = Math.min(area.width() - 4, Math.max(30, itemWidth + 4));
+        int popupH = Math.min(area.height() - 4, fileEntries.size() + 2);
+
+        int x = area.left() + Math.max(0, (area.width() - popupW) / 2);
+        int y = area.top() + 2;
+        Rect popup = new Rect(x, y, Math.min(popupW, area.width()), 
Math.min(popupH, area.height() - 2));
+
+        frame.renderWidget(Clear.INSTANCE, popup);
+
+        ListItem[] items = new ListItem[fileEntries.size()];
+        for (int i = 0; i < fileEntries.size(); i++) {
+            FileEntry entry = fileEntries.get(i);
+            String sizeStr = formatFileSize(entry.size());
+            String label = String.format("  %s %-" + nameWidth + "s  %s", 
entry.emoji(), entry.name(), sizeStr);
+            items[i] = ListItem.from(label);
+        }
+
+        ListWidget list = ListWidget.builder()
+                .items(items)
+                .highlightStyle(Style.EMPTY.fg(Color.WHITE).bold().onBlue())
+                .highlightSymbol("")
+                .scrollMode(ScrollMode.NONE)
+                .block(Block.builder()
+                        .borderType(BorderType.ROUNDED)
+                        .title(Title.from(Line
+                                .from(Span.styled(" Files: " + filesPopupTitle 
+ " ", Style.EMPTY.fg(Color.YELLOW).bold()))))
+                        .build())
+                .build();
+        frame.renderStatefulWidget(list, popup, filesPopupState);
+    }
+
+    private static final String[] CAMEL_YAML_MARKERS = {
+            "- from:", "- route:",
+            "- routeTemplate:", "- route-template:",
+            "- templatedRoute:", "- templated-route:",
+            "- routeConfiguration:", "- route-configuration:",
+            "- rest:", "- beans:"
+    };
+
+    private static final String[] CAMEL_XML_MARKERS = {
+            "<route", "<routes", "<routeTemplate", "<routeTemplates",
+            "<templatedRoute", "<templatedRoutes",
+            "<rest", "<rests", "<routeConfiguration",
+            "<beans", "<blueprint", "<camel"
+    };
+
+    private static String fileEmoji(Path path) {
+        String name = path.getFileName().toString();
+        String lower = name.toLowerCase(Locale.ROOT);
+        if (lower.endsWith(".kamelet.yaml") || lower.endsWith(".kamelet.yml")) 
{
+            return "🐪";
+        }
+        if (lower.endsWith(".yaml") || lower.endsWith(".yml")) {
+            return isCamelYaml(path) ? "🐪" : "📋";
+        }
+        if (lower.endsWith(".xml")) {
+            return isCamelXml(path) ? "🐪" : "📋";
+        }
+        if (lower.endsWith(".java")) {
+            return isCamelJava(path) ? "🐪" : "☕";
+        }
+        if (lower.endsWith(".properties") || lower.endsWith(".cfg")) {
+            return "📄";
+        }
+        if (lower.startsWith("readme")) {
+            return "📖";
+        }
+        return "📋";
+    }
+
+    private static boolean isCamelYaml(Path path) {
+        try {
+            String content = Files.readString(path, StandardCharsets.UTF_8);
+            for (String marker : CAMEL_YAML_MARKERS) {
+                if (content.contains(marker)) {
+                    return true;
+                }
+            }
+        } catch (IOException e) {
+            // ignore
+        }
+        return false;
+    }
+
+    private static boolean isCamelXml(Path path) {
+        try {
+            String content = Files.readString(path, StandardCharsets.UTF_8);
+            for (String marker : CAMEL_XML_MARKERS) {
+                if (content.contains(marker)) {
+                    return true;
+                }
+            }
+        } catch (IOException e) {
+            // ignore
+        }
+        return false;
+    }
+
+    private static boolean isCamelJava(Path path) {
+        try {
+            String content = Files.readString(path, StandardCharsets.UTF_8);
+            return content.contains("RouteBuilder")
+                    || content.contains("EndpointRouteBuilder");
+        } catch (IOException e) {
+            // ignore
+        }
+        return false;
+    }
+
+    private static Path resolveSourceDirectory(IntegrationInfo info) {
+        for (ConfigurationTab.ConfigProperty cp : info.configProperties) {
+            if ("camel.main.routesIncludePattern".equals(cp.key) && cp.value 
!= null) {
+                for (String part : cp.value.split(",")) {
+                    part = part.trim();
+                    if (part.startsWith("file:")) {
+                        String filePath = part.substring("file:".length());
+                        // strip query params like ?optional=true
+                        int q = filePath.indexOf('?');
+                        if (q > 0) {
+                            filePath = filePath.substring(0, q);
+                        }
+                        // source-dir pattern: file:/path/to/folder/** → use 
/path/to/folder directly
+                        if (filePath.endsWith("/**")) {
+                            Path dir = Path.of(filePath.substring(0, 
filePath.length() - 3));
+                            if (Files.isDirectory(dir)) {
+                                return dir;
+                            }
+                        }
+                        // individual file: file:/tmp/example/foo.yaml → use 
parent dir
+                        Path parent = Path.of(filePath).getParent();
+                        if (parent != null && Files.isDirectory(parent)) {
+                            return parent;
+                        }
+                    }
+                }
+            }
+        }
+        if (info.directory != null && !info.directory.isEmpty()) {
+            return Path.of(info.directory);
+        }
+        return null;
+    }
+
+    private static String formatFileSize(long bytes) {
+        if (bytes < 1024) {
+            return bytes + " B";
+        }
+        if (bytes < 1024 * 1024) {
+            return String.format("%.1f KB", bytes / 1024.0);
+        }
+        return String.format("%.1f MB", bytes / (1024.0 * 1024));
+    }
+
     private List<IntegrationInfo> getNonVanishingIntegrations() {
         return data.get().stream()
                 .filter(i -> !i.vanishing && i.name != null)
@@ -1641,7 +1925,15 @@ public class CamelMonitor extends CamelCommand {
             return;
         }
 
-        if (showSwitchPopup) {
+        if (showFilesPopup) {
+            if (overviewSourceViewer.isVisible()) {
+                overviewSourceViewer.renderFooter(spans);
+            } else {
+                hint(spans, "Up/Down", "navigate");
+                hint(spans, "Enter", "open");
+                hint(spans, "Esc", "close");
+            }
+        } else if (showSwitchPopup) {
             hint(spans, "Up/Down", "select");
             hint(spans, "Enter", "switch");
             hint(spans, "Esc", "close");
@@ -1738,6 +2030,12 @@ public class CamelMonitor extends CamelCommand {
         if (ctx.selectedPid != null && !isInfraSelected()) {
             IntegrationInfo selInfo = findSelectedIntegration();
             if (selInfo != null) {
+                if (selInfo.readmeFiles != null && 
!selInfo.readmeFiles.isEmpty()) {
+                    hint(spans, "d", "docs");
+                }
+                if (selInfo.directory != null && !selInfo.directory.isEmpty()) 
{
+                    hint(spans, "f", "files");
+                }
                 hint(spans, "p", selInfo.routeStarted > 0 ? "stop routes" : 
"start routes");
             }
         }
@@ -2779,6 +3077,37 @@ public class CamelMonitor extends CamelCommand {
                 .toList();
     }
 
+    JsonObject getReadme(String name) {
+        List<IntegrationInfo> integrations = data.get();
+        IntegrationInfo target = null;
+        if (name != null && !name.isEmpty()) {
+            for (IntegrationInfo info : integrations) {
+                if (!info.vanishing && name.equals(info.name)) {
+                    target = info;
+                    break;
+                }
+            }
+        } else {
+            target = ctx != null ? ctx.findSelectedIntegration() : null;
+        }
+        if (target == null) {
+            return null;
+        }
+        if (target.readmeFiles == null || target.readmeFiles.isEmpty()) {
+            return null;
+        }
+        try {
+            Path outputFile = ctx.getOutputFile(target.pid);
+            Files.deleteIfExists(outputFile);
+            JsonObject action = new JsonObject();
+            action.put("action", "readme");
+            PathUtils.writeTextSafely(action.toJson(), 
ctx.getActionFile(target.pid));
+            return MonitorContext.pollJsonResponse(outputFile, 5000);
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
     int injectKeys(List<String> keys, int delayMs) {
         long fireAt = System.currentTimeMillis();
         int count = 0;
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTab.java
index e2c17215d367..c0adaf580826 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTab.java
@@ -468,16 +468,16 @@ class ErrorsTab implements MonitorTab {
 
         // exchange properties, variables, headers, body
         if (showProperties && !ei.properties.isEmpty()) {
-            HistoryTab.addKvLines(lines, " Exchange Properties:", 
ei.properties, ei.propertyTypes);
+            HistoryTab.addKvLines(lines, " Exchange Properties:", 
ei.properties, ei.propertyTypes, false, null);
         }
         if (showVariables && !ei.variables.isEmpty()) {
-            HistoryTab.addKvLines(lines, " Exchange Variables:", ei.variables, 
ei.variableTypes);
+            HistoryTab.addKvLines(lines, " Exchange Variables:", ei.variables, 
ei.variableTypes, false, null);
         }
         if (showHeaders && !ei.headers.isEmpty()) {
-            HistoryTab.addKvLines(lines, " Headers:", ei.headers, 
ei.headerTypes);
+            HistoryTab.addKvLines(lines, " Headers:", ei.headers, 
ei.headerTypes, false, null);
         }
         if (showBody) {
-            HistoryTab.addBodyLines(lines, ei.body, ei.bodyType);
+            HistoryTab.addBodyLines(lines, ei.body, ei.bodyType, false);
         }
 
         int[] scroll = { detailScroll };
@@ -822,20 +822,31 @@ class ErrorsTab implements MonitorTab {
                 step is shown with a selection highlight. Use `Left/Right` to 
scroll
                 the diagram horizontally if it extends beyond the screen.
 
-                Press `Esc` to close the diagram and return to the detail view.
+                **Info Panel** — An info panel on the left side of the diagram 
shows
+                metadata for the selected error: exchange ID, route, node, 
elapsed
+                time, thread, handled status, and the full exception. It also 
shows
+                body, headers, properties, and variables respecting the 
`b/h/p/v`
+                toggles. Press `i` to cycle the panel size: narrow (35 chars),
+                wide (half screen), or full (entire area). In wide mode, the 
minimap
+                and tree preview are hidden. Word wrap (`w`) is also supported.
+
+                Press `d` to close the diagram and return to the detail view.
+                Press `Esc` to navigate back one route in drill-down mode.
 
                 ## Keys
 
                 - `Up/Down` — select error (list) / navigate path steps 
(diagram)
                 - `Enter` — view error details
-                - `d` — show error diagram (in detail view)
+                - `d` — toggle error diagram (open and close)
+                - `Esc` — back to list / back one route in diagram drill-down
+                - `i` — cycle info panel size (narrow / wide / full) in diagram
                 - `h` — toggle headers
                 - `b` — toggle body
                 - `p` — toggle properties
                 - `v` — toggle variables
+                - `w` — toggle word wrap
                 - `s` — cycle sort column
                 - `S` — reverse sort order
-                - `Esc` — back to list / close diagram
                 """;
     }
 
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTab.java
index b0a3120083c8..ab15cb065f48 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTab.java
@@ -683,6 +683,9 @@ class HistoryTab implements MonitorTab {
         Map<String, Object> headers = null;
         Map<String, Object> properties = null;
         Map<String, Object> variables = null;
+        Map<String, Object> prevHeaders = null;
+        Map<String, Object> prevProperties = null;
+        Map<String, Object> prevVariables = null;
 
         if (!diagramTraceSteps.isEmpty() && stepIdx >= 0 && stepIdx < 
diagramTraceSteps.size()) {
             TraceEntry e = diagramTraceSteps.get(stepIdx);
@@ -701,6 +704,12 @@ class HistoryTab implements MonitorTab {
             headers = e.headers;
             properties = e.exchangeProperties;
             variables = e.exchangeVariables;
+            if (stepIdx > 0) {
+                TraceEntry p = diagramTraceSteps.get(stepIdx - 1);
+                prevHeaders = p.headers;
+                prevProperties = p.exchangeProperties;
+                prevVariables = p.exchangeVariables;
+            }
         } else if (!diagramHistorySteps.isEmpty() && stepIdx >= 0 && stepIdx < 
diagramHistorySteps.size()) {
             HistoryEntry e = diagramHistorySteps.get(stepIdx);
             exchangeId = e.exchangeId;
@@ -718,6 +727,12 @@ class HistoryTab implements MonitorTab {
             headers = e.headers;
             properties = e.exchangeProperties;
             variables = e.exchangeVariables;
+            if (stepIdx > 0) {
+                HistoryEntry p = diagramHistorySteps.get(stepIdx - 1);
+                prevHeaders = p.headers;
+                prevProperties = p.exchangeProperties;
+                prevVariables = p.exchangeVariables;
+            }
         }
 
         if (exchangeId == null) {
@@ -750,8 +765,8 @@ class HistoryTab implements MonitorTab {
                 Span.raw(nodeId != null ? nodeId : "")));
         if (processor != null) {
             lines.add(Line.from(
-                    Span.styled(" Processor:", 
Style.EMPTY.fg(Color.YELLOW).bold()),
-                    Span.raw(" " + processor.strip())));
+                    Span.styled(" Proc:     ", 
Style.EMPTY.fg(Color.YELLOW).bold()),
+                    Span.raw(processor.strip())));
         }
         if (elapsed >= 0) {
             lines.add(Line.from(
@@ -774,6 +789,25 @@ class HistoryTab implements MonitorTab {
                     Span.styled("Failed", 
Style.EMPTY.fg(Color.LIGHT_RED).bold())));
         }
 
+        // Compute BHPV change indicators
+        String changes = "";
+        if (!diagramTraceSteps.isEmpty() && stepIdx > 0 && stepIdx < 
diagramTraceSteps.size()) {
+            changes = computeTraceChanges(diagramTraceSteps.get(stepIdx - 1), 
diagramTraceSteps.get(stepIdx));
+        } else if (!diagramHistorySteps.isEmpty() && stepIdx > 0 && stepIdx < 
diagramHistorySteps.size()) {
+            changes = computeHistoryChanges(diagramHistorySteps.get(stepIdx - 
1), diagramHistorySteps.get(stepIdx));
+        }
+        boolean bodyChanged = changes.length() > 0 && changes.charAt(0) == 'B';
+        boolean headersChanged = changes.length() > 1 && changes.charAt(1) == 
'H';
+        boolean propsChanged = changes.length() > 2 && changes.charAt(2) == 
'P';
+        boolean varsChanged = changes.length() > 3 && changes.charAt(3) == 'V';
+
+        List<Span> changeSpans = new ArrayList<>();
+        changeSpans.add(Span.styled(" Changed:  ", 
Style.EMPTY.fg(Color.YELLOW).bold()));
+        if (!changes.isBlank()) {
+            changeSpans.addAll(buildChangeSpans(changes));
+        }
+        lines.add(Line.from(changeSpans));
+
         boolean isTraceMode = !diagramTraceSteps.isEmpty();
         boolean showBody = isTraceMode ? showTraceBody : showHistoryBody;
         boolean showHeaders = isTraceMode ? showTraceHeaders : 
showHistoryHeaders;
@@ -787,9 +821,10 @@ class HistoryTab implements MonitorTab {
         }
 
         if (showBody && body != null) {
+            Style headerStyle = bodyChanged ? 
Style.EMPTY.fg(Color.YELLOW).bold() : Style.EMPTY.fg(Color.GREEN).bold();
             lines.add(Line.from(Span.raw("")));
             lines.add(Line.from(
-                    Span.styled(" Body", Style.EMPTY.fg(Color.GREEN).bold()),
+                    Span.styled(" Body", headerStyle),
                     bodyType != null ? Span.styled(" (" + bodyType + ")", 
Style.EMPTY.dim()) : Span.raw("")));
             for (String line : body.split("\n")) {
                 lines.add(Line.from(Span.raw(" " + line)));
@@ -797,36 +832,24 @@ class HistoryTab implements MonitorTab {
         }
 
         if (showHeaders && headers != null && !headers.isEmpty()) {
+            Style sectionStyle = headersChanged ? 
Style.EMPTY.fg(Color.YELLOW).bold() : Style.EMPTY.fg(Color.GREEN).bold();
             lines.add(Line.from(Span.raw("")));
-            lines.add(Line.from(Span.styled(" Headers", 
Style.EMPTY.fg(Color.GREEN).bold())));
-            for (var entry : headers.entrySet()) {
-                String val = entry.getValue() != null ? 
entry.getValue().toString() : "null";
-                lines.add(Line.from(
-                        Span.styled(" " + entry.getKey(), 
Style.EMPTY.fg(Color.CYAN)),
-                        Span.raw(" = " + val)));
-            }
+            lines.add(Line.from(Span.styled(" Headers", sectionStyle)));
+            addInfoKvLines(lines, headers, headersChanged, prevHeaders);
         }
 
         if (showProps && properties != null && !properties.isEmpty()) {
+            Style sectionStyle = propsChanged ? 
Style.EMPTY.fg(Color.YELLOW).bold() : Style.EMPTY.fg(Color.GREEN).bold();
             lines.add(Line.from(Span.raw("")));
-            lines.add(Line.from(Span.styled(" Properties", 
Style.EMPTY.fg(Color.GREEN).bold())));
-            for (var entry : properties.entrySet()) {
-                String val = entry.getValue() != null ? 
entry.getValue().toString() : "null";
-                lines.add(Line.from(
-                        Span.styled(" " + entry.getKey(), 
Style.EMPTY.fg(Color.CYAN)),
-                        Span.raw(" = " + val)));
-            }
+            lines.add(Line.from(Span.styled(" Properties", sectionStyle)));
+            addInfoKvLines(lines, properties, propsChanged, prevProperties);
         }
 
         if (showVars && variables != null && !variables.isEmpty()) {
+            Style sectionStyle = varsChanged ? 
Style.EMPTY.fg(Color.YELLOW).bold() : Style.EMPTY.fg(Color.GREEN).bold();
             lines.add(Line.from(Span.raw("")));
-            lines.add(Line.from(Span.styled(" Variables", 
Style.EMPTY.fg(Color.GREEN).bold())));
-            for (var entry : variables.entrySet()) {
-                String val = entry.getValue() != null ? 
entry.getValue().toString() : "null";
-                lines.add(Line.from(
-                        Span.styled(" " + entry.getKey(), 
Style.EMPTY.fg(Color.CYAN)),
-                        Span.raw(" = " + val)));
-            }
+            lines.add(Line.from(Span.styled(" Variables", sectionStyle)));
+            addInfoKvLines(lines, variables, varsChanged, prevVariables);
         }
 
         boolean wordWrap = !diagramTraceSteps.isEmpty() ? traceWordWrap : 
historyWordWrap;
@@ -839,6 +862,23 @@ class HistoryTab implements MonitorTab {
         frame.renderWidget(pb.build(), area);
     }
 
+    private static void addInfoKvLines(
+            List<Line> lines, Map<String, Object> map,
+            boolean sectionChanged, Map<String, Object> prevMap) {
+        for (var entry : map.entrySet()) {
+            String val = entry.getValue() != null ? 
entry.getValue().toString() : "null";
+            boolean keyChanged = sectionChanged && prevMap != null
+                    && (!prevMap.containsKey(entry.getKey())
+                            || !Objects.equals(prevMap.get(entry.getKey()), 
entry.getValue()));
+            Style keyStyle = keyChanged ? Style.EMPTY.fg(Color.YELLOW) : 
Style.EMPTY.fg(Color.CYAN);
+            Style valStyle = keyChanged ? Style.EMPTY.fg(Color.YELLOW) : 
Style.EMPTY;
+            lines.add(Line.from(
+                    Span.styled(" " + entry.getKey(), keyStyle),
+                    Span.raw(" = "),
+                    Span.styled(val, valStyle)));
+        }
+    }
+
     // ---- Diagram loading ----
 
     private void loadDiagramForCurrentView() {
@@ -1081,21 +1121,30 @@ class HistoryTab implements MonitorTab {
         }
 
         TraceEntry entry = steps.get(sel);
+        TraceEntry prev = sel > 0 ? steps.get(sel - 1) : null;
+        String changes = computeTraceChanges(prev, entry);
+        boolean bodyChanged = changes.length() > 0 && changes.charAt(0) == 'B';
+        boolean headersChanged = changes.length() > 1 && changes.charAt(1) == 
'H';
+        boolean propsChanged = changes.length() > 2 && changes.charAt(2) == 
'P';
+        boolean varsChanged = changes.length() > 3 && changes.charAt(3) == 'V';
         List<Line> lines = new ArrayList<>();
 
         addExchangeInfoLines(lines, entry.exchangeId, entry.routeId, 
entry.nodeId, entry.nodeLabel,
                 entry.location, entry.elapsed, entry.threadName, entry.failed);
         if (showTraceProperties) {
-            addKvLines(lines, " Exchange Properties:", 
entry.exchangeProperties, entry.exchangePropertyTypes);
+            addKvLines(lines, " Exchange Properties:", 
entry.exchangeProperties, entry.exchangePropertyTypes,
+                    propsChanged, prev != null ? prev.exchangeProperties : 
null);
         }
         if (showTraceVariables) {
-            addKvLines(lines, " Exchange Variables:", entry.exchangeVariables, 
entry.exchangeVariableTypes);
+            addKvLines(lines, " Exchange Variables:", entry.exchangeVariables, 
entry.exchangeVariableTypes,
+                    varsChanged, prev != null ? prev.exchangeVariables : null);
         }
         if (showTraceHeaders) {
-            addKvLines(lines, " Headers:", entry.headers, entry.headerTypes);
+            addKvLines(lines, " Headers:", entry.headers, entry.headerTypes,
+                    headersChanged, prev != null ? prev.headers : null);
         }
         if (showTraceBody) {
-            addBodyLines(lines, entry.body, entry.bodyType);
+            addBodyLines(lines, entry.body, entry.bodyType, bodyChanged);
         }
         addExceptionLines(lines, entry.exception);
 
@@ -1343,21 +1392,30 @@ class HistoryTab implements MonitorTab {
         }
 
         HistoryEntry entry = current.get(sel);
+        HistoryEntry prev = sel > 0 ? current.get(sel - 1) : null;
+        String changes = computeHistoryChanges(prev, entry);
+        boolean bodyChanged = changes.length() > 0 && changes.charAt(0) == 'B';
+        boolean headersChanged = changes.length() > 1 && changes.charAt(1) == 
'H';
+        boolean propsChanged = changes.length() > 2 && changes.charAt(2) == 
'P';
+        boolean varsChanged = changes.length() > 3 && changes.charAt(3) == 'V';
         List<Line> lines = new ArrayList<>();
 
         addExchangeInfoLines(lines, entry.exchangeId, entry.routeId, 
entry.nodeId, entry.nodeLabel,
                 entry.location, entry.elapsed, entry.threadName, entry.failed);
         if (showHistoryProperties) {
-            addKvLines(lines, " Exchange Properties:", 
entry.exchangeProperties, entry.exchangePropertyTypes);
+            addKvLines(lines, " Exchange Properties:", 
entry.exchangeProperties, entry.exchangePropertyTypes,
+                    propsChanged, prev != null ? prev.exchangeProperties : 
null);
         }
         if (showHistoryVariables) {
-            addKvLines(lines, " Exchange Variables:", entry.exchangeVariables, 
entry.exchangeVariableTypes);
+            addKvLines(lines, " Exchange Variables:", entry.exchangeVariables, 
entry.exchangeVariableTypes,
+                    varsChanged, prev != null ? prev.exchangeVariables : null);
         }
         if (showHistoryHeaders) {
-            addKvLines(lines, " Headers:", entry.headers, entry.headerTypes);
+            addKvLines(lines, " Headers:", entry.headers, entry.headerTypes,
+                    headersChanged, prev != null ? prev.headers : null);
         }
         if (showHistoryBody) {
-            addBodyLines(lines, entry.body, entry.bodyType);
+            addBodyLines(lines, entry.body, entry.bodyType, bodyChanged);
         }
         addExceptionLines(lines, entry.exception);
 
@@ -1813,11 +1871,13 @@ class HistoryTab implements MonitorTab {
 
     static void addKvLines(
             List<Line> lines, String section,
-            Map<String, Object> map, Map<String, String> types) {
+            Map<String, Object> map, Map<String, String> types,
+            boolean changed, Map<String, Object> prevMap) {
         if (map == null || map.isEmpty()) {
             return;
         }
-        lines.add(Line.from(Span.styled(section, 
Style.EMPTY.fg(Color.GREEN).bold())));
+        Style headerStyle = changed ? Style.EMPTY.fg(Color.YELLOW).bold() : 
Style.EMPTY.fg(Color.GREEN).bold();
+        lines.add(Line.from(Span.styled(section, headerStyle)));
         for (Map.Entry<String, Object> entry : map.entrySet()) {
             String type = types != null ? types.get(entry.getKey()) : null;
             String typeLabel;
@@ -1835,23 +1895,29 @@ class HistoryTab implements MonitorTab {
                 // ignore
             }
             val = stripControlChars(val);
+            boolean keyChanged = changed && prevMap != null
+                    && (!prevMap.containsKey(entry.getKey())
+                            || !Objects.equals(prevMap.get(entry.getKey()), 
entry.getValue()));
+            Style keyStyle = keyChanged ? Style.EMPTY.fg(Color.YELLOW) : 
Style.EMPTY.fg(Color.CYAN);
+            Style valStyle = keyChanged ? Style.EMPTY.fg(Color.YELLOW) : 
Style.EMPTY;
             lines.add(Line.from(
                     Span.styled("   " + typeLabel, Style.EMPTY.dim()),
-                    Span.styled(entry.getKey(), Style.EMPTY.fg(Color.CYAN)),
+                    Span.styled(entry.getKey(), keyStyle),
                     Span.raw(" = "),
-                    Span.raw(val)));
+                    Span.styled(val, valStyle)));
         }
         lines.add(Line.from(Span.raw("")));
     }
 
-    static void addBodyLines(List<Line> lines, String body, String bodyType) {
+    static void addBodyLines(List<Line> lines, String body, String bodyType, 
boolean changed) {
+        Style headerStyle = changed ? Style.EMPTY.fg(Color.YELLOW).bold() : 
Style.EMPTY.fg(Color.GREEN).bold();
         if (body != null) {
             if (bodyType != null) {
                 lines.add(Line.from(
-                        Span.styled(" Body: ", 
Style.EMPTY.fg(Color.GREEN).bold()),
+                        Span.styled(" Body: ", headerStyle),
                         Span.styled("(" + bodyType + ")", Style.EMPTY.dim())));
             } else {
-                lines.add(Line.from(Span.styled(" Body:", 
Style.EMPTY.fg(Color.GREEN).bold())));
+                lines.add(Line.from(Span.styled(" Body:", headerStyle)));
             }
             try {
                 body = Jsoner.unescape(body);
@@ -1863,7 +1929,7 @@ class HistoryTab implements MonitorTab {
                 lines.add(Line.from(Span.raw("   " + stripControlChars(bl))));
             }
         } else {
-            lines.add(Line.from(Span.styled(" Body is null", 
Style.EMPTY.fg(Color.GREEN).bold())));
+            lines.add(Line.from(Span.styled(" Body is null", headerStyle)));
         }
         lines.add(Line.from(Span.raw("")));
     }
@@ -2161,6 +2227,15 @@ class HistoryTab implements MonitorTab {
                 large routes. This is the same minimap available on the Routes 
and
                 Diagram tabs.
 
+                **Info Panel** — An info panel on the left side of the diagram 
shows
+                trace metadata for the current step: exchange ID, route, node,
+                processor, elapsed time, thread, and direction. It also shows 
body,
+                headers, properties, and variables respecting the same 
`b/h/p/v`
+                toggles as the table view. Press `i` to cycle the panel size:
+                narrow (35 chars), wide (half screen), or full (entire area).
+                In wide mode, the minimap and tree preview are hidden to give 
more
+                space. Word wrap (`w`) is also supported.
+
                 Press `d` to close the diagram and return to the table.
                 Press `Esc` to navigate back one route in drill-down mode.
 
@@ -2170,6 +2245,7 @@ class HistoryTab implements MonitorTab {
                 - `Enter` — view exchange details
                 - `d` — toggle route diagram (open and close)
                 - `Esc` — back to list / back one route in diagram drill-down
+                - `i` — cycle info panel size (narrow / wide / full) in diagram
                 - `n` — toggle description mode
                 - `g` — toggle waterfall view
                 - `h` — toggle headers
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java
index d5d09d4d3a04..979eae7e9842 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java
@@ -215,12 +215,17 @@ class OverviewTab implements MonitorTab {
 
                 String sinceLastDisplay = formatSinceLast(info);
 
+                boolean hasDoc = info.readmeFiles != null && 
!info.readmeFiles.isEmpty();
                 String nameText = "🐪 " + (info.name != null ? info.name : "");
-                Line nameLine = info.devMode
-                        ? Line.from(
-                                Span.styled(nameText, 
Style.EMPTY.fg(Color.CYAN)),
-                                Span.styled(" [dev]", 
Style.EMPTY.fg(Color.YELLOW).dim()))
-                        : Line.from(Span.styled(nameText, 
Style.EMPTY.fg(Color.CYAN)));
+                List<Span> nameSpans = new ArrayList<>();
+                nameSpans.add(Span.styled(nameText, 
Style.EMPTY.fg(Color.CYAN)));
+                if (info.devMode) {
+                    nameSpans.add(Span.styled(" [dev]", 
Style.EMPTY.fg(Color.YELLOW).dim()));
+                }
+                if (hasDoc) {
+                    nameSpans.add(Span.styled(" 📖", Style.EMPTY));
+                }
+                Line nameLine = Line.from(nameSpans);
                 rows.add(Row.from(
                         Cell.from(info.pid),
                         Cell.from(nameLine),
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 21e502932ca3..273a0a0d5c17 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
@@ -24,9 +24,14 @@ import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.function.IntConsumer;
+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.Color;
+import dev.tamboui.style.Overflow;
 import dev.tamboui.style.Style;
 import dev.tamboui.terminal.Frame;
 import dev.tamboui.text.Line;
@@ -36,6 +41,7 @@ import dev.tamboui.tui.event.KeyCode;
 import dev.tamboui.tui.event.KeyEvent;
 import dev.tamboui.widgets.block.Block;
 import dev.tamboui.widgets.block.BorderType;
+import dev.tamboui.widgets.input.TextInputState;
 import dev.tamboui.widgets.paragraph.Paragraph;
 import dev.tamboui.widgets.scrollbar.Scrollbar;
 import dev.tamboui.widgets.scrollbar.ScrollbarState;
@@ -69,6 +75,24 @@ class SourceViewer {
     private final AtomicBoolean loading = new AtomicBoolean(false);
     private IntConsumer onLineSelected;
     private final Map<String, CachedSource> sourceCache = new 
ConcurrentHashMap<>();
+    private boolean wordWrap;
+
+    // Find mode
+    private boolean findInputActive;
+    private boolean highlightInputActive;
+    private TextInputState searchInputState = new TextInputState("");
+    private String findTerm;
+    private Pattern findPattern;
+    private int findMatchIndex = -1;
+    private List<Integer> findMatches = Collections.emptyList();
+
+    // Highlight mode
+    private String highlightTerm;
+    private Pattern highlightPattern;
+
+    private static final Style HIGHLIGHT_STYLE = 
Style.EMPTY.fg(Color.BLACK).bg(Color.YELLOW);
+    private static final Style FIND_MATCH_STYLE = 
Style.EMPTY.fg(Color.BLACK).bg(Color.YELLOW);
+    private static final Style FIND_CURRENT_STYLE = 
Style.EMPTY.fg(Color.BLACK).bg(Color.LIGHT_GREEN);
 
     private record CachedSource(
             List<String> lines, List<JsonObject> codeData,
@@ -95,6 +119,15 @@ class SourceViewer {
         pendingScroll = false;
         onLineSelected = null;
         sourceCache.clear();
+        wordWrap = false;
+        findInputActive = false;
+        highlightInputActive = false;
+        findTerm = null;
+        findPattern = null;
+        findMatchIndex = -1;
+        findMatches = Collections.emptyList();
+        highlightTerm = null;
+        highlightPattern = null;
     }
 
     void setOnLineSelected(IntConsumer callback) {
@@ -105,11 +138,49 @@ class SourceViewer {
         if (!visible) {
             return false;
         }
-        if (ke.isChar('c') || ke.isCancel()) {
+        if (findInputActive || highlightInputActive) {
+            return handleSearchInput(ke);
+        }
+        if (ke.isCancel()) {
+            if (findTerm != null) {
+                findTerm = null;
+                findPattern = null;
+                findMatches = Collections.emptyList();
+                findMatchIndex = -1;
+                return true;
+            }
             visible = false;
             onLineSelected = null;
             return true;
         }
+        if (ke.isChar('c')) {
+            visible = false;
+            onLineSelected = null;
+            return true;
+        }
+        if (ke.isChar('/')) {
+            findInputActive = true;
+            searchInputState = new TextInputState("");
+            return true;
+        }
+        if (ke.isChar('h')) {
+            highlightInputActive = true;
+            searchInputState = new TextInputState("");
+            return true;
+        }
+        if (ke.isChar('n') && findTerm != null) {
+            navigateToNextMatch();
+            return true;
+        }
+        if (ke.isChar('N') && findTerm != null) {
+            navigateToPrevMatch();
+            return true;
+        }
+        if (ke.isChar('w')) {
+            wordWrap = !wordWrap;
+            scrollX = 0;
+            return true;
+        }
         if (ke.isKey(KeyCode.UP) && ke.hasCtrl()) {
             scrollY = Math.max(0, scrollY - 1);
         } else if (ke.isKey(KeyCode.DOWN) && ke.hasCtrl()) {
@@ -128,9 +199,9 @@ class SourceViewer {
             if (!lines.isEmpty()) {
                 selectedLine = Math.min(lines.size() - 1, selectedLine + page);
             }
-        } else if (ke.isLeft()) {
+        } else if (!wordWrap && ke.isLeft()) {
             scrollX = Math.max(0, scrollX - 1);
-        } else if (ke.isRight()) {
+        } else if (!wordWrap && ke.isRight()) {
             scrollX++;
         } else if (ke.isHome()) {
             selectedLine = 0;
@@ -153,6 +224,53 @@ class SourceViewer {
         return true;
     }
 
+    private boolean handleSearchInput(KeyEvent ke) {
+        if (ke.isKey(KeyCode.ESCAPE)) {
+            findInputActive = false;
+            highlightInputActive = false;
+            return true;
+        }
+        if (ke.isConfirm()) {
+            String text = searchInputState.text().trim();
+            if (findInputActive) {
+                if (text.isEmpty()) {
+                    findTerm = null;
+                    findPattern = null;
+                    findMatches = Collections.emptyList();
+                    findMatchIndex = -1;
+                } else {
+                    findTerm = text;
+                    findPattern = Pattern.compile(Pattern.quote(text), 
Pattern.CASE_INSENSITIVE);
+                    buildFindMatches();
+                    jumpToNearestMatch();
+                }
+                findInputActive = false;
+            } else if (highlightInputActive) {
+                if (text.isEmpty()) {
+                    highlightTerm = null;
+                    highlightPattern = null;
+                } else {
+                    highlightTerm = text;
+                    highlightPattern = Pattern.compile(Pattern.quote(text), 
Pattern.CASE_INSENSITIVE);
+                }
+                highlightInputActive = false;
+            }
+            return true;
+        }
+        FormHelper.handleTextInput(ke, searchInputState);
+        return true;
+    }
+
+    boolean isSearchInputActive() {
+        return findInputActive || highlightInputActive;
+    }
+
+    void handlePaste(String text) {
+        if (findInputActive || highlightInputActive) {
+            FormHelper.handlePaste(text, searchInputState);
+        }
+    }
+
     void render(Frame frame, Rect area) {
         Block block = Block.builder()
                 .borderType(BorderType.ROUNDED)
@@ -186,41 +304,127 @@ class SourceViewer {
         }
         scrollY = Math.min(scrollY, maxScroll);
 
-        int cursorWidth = 3;
-        int maxLineWidth = 
lines.stream().mapToInt(String::length).max().orElse(0) + cursorWidth;
-        int maxHScroll = Math.max(0, maxLineWidth - inner.width());
-        scrollX = Math.min(scrollX, maxHScroll);
+        int hSkip = wordWrap ? 0 : scrollX;
+        if (!wordWrap) {
+            int cursorWidth = 3;
+            int maxLineWidth = 
lines.stream().mapToInt(String::length).max().orElse(0) + cursorWidth;
+            int maxHScroll = Math.max(0, maxLineWidth - inner.width());
+            scrollX = Math.min(scrollX, maxHScroll);
+        }
+
+        int currentMatchLine = findMatchIndex >= 0 && findMatchIndex < 
findMatches.size()
+                ? findMatches.get(findMatchIndex) : -1;
 
         int end = Math.min(scrollY + visibleLines, lines.size());
         List<Line> visible = new ArrayList<>();
         for (int i = scrollY; i < end; i++) {
             String raw = lines.get(i);
             boolean isSelected = (i == selectedLine);
-            visible.add(highlightSourceLine(raw, scrollX, isSelected));
+            Line line = highlightSourceLine(raw, hSkip, isSelected, 
inner.width());
+            if (highlightPattern != null || findPattern != null) {
+                line = applySearchHighlights(line, i, currentMatchLine);
+            }
+            visible.add(line);
         }
-        
frame.renderWidget(Paragraph.builder().text(Text.from(visible)).build(), inner);
+
+        List<Rect> hChunks = Layout.horizontal()
+                .constraints(Constraint.fill(), Constraint.length(1))
+                .split(inner);
+
+        Overflow overflow = wordWrap ? Overflow.WRAP_WORD : Overflow.CLIP;
+        
frame.renderWidget(Paragraph.builder().text(Text.from(visible)).overflow(overflow).build(),
 hChunks.get(0));
 
         if (lines.size() > visibleLines) {
             
vScrollState.contentLength(lines.size()).viewportContentLength(visibleLines).position(scrollY);
-            frame.renderStatefulWidget(Scrollbar.builder().build(), inner, 
vScrollState);
+            frame.renderStatefulWidget(Scrollbar.builder().build(), 
hChunks.get(1), vScrollState);
         }
-        if (maxHScroll > 0) {
-            
hScrollState.contentLength(maxLineWidth).viewportContentLength(inner.width()).position(scrollX);
-            frame.renderStatefulWidget(Scrollbar.horizontal(), inner, 
hScrollState);
+        if (!wordWrap) {
+            int cursorWidth = 3;
+            int maxLineWidth = 
lines.stream().mapToInt(String::length).max().orElse(0) + cursorWidth;
+            int maxHScroll = Math.max(0, maxLineWidth - inner.width());
+            if (maxHScroll > 0) {
+                
hScrollState.contentLength(maxLineWidth).viewportContentLength(inner.width()).position(scrollX);
+                frame.renderStatefulWidget(Scrollbar.horizontal(), inner, 
hScrollState);
+            }
         }
     }
 
     void renderFooter(List<Span> spans) {
-        MonitorContext.hint(spans, "Esc/c", "close");
+        if (findInputActive) {
+            spans.add(Span.styled(" /", MonitorContext.HINT_KEY_STYLE));
+            spans.add(Span.raw(searchInputState.text() + "█  "));
+            MonitorContext.hint(spans, "Enter", "search");
+            MonitorContext.hintLast(spans, "Esc", "cancel");
+            return;
+        }
+        if (highlightInputActive) {
+            spans.add(Span.styled(" h:", MonitorContext.HINT_KEY_STYLE));
+            spans.add(Span.raw(searchInputState.text() + "█  "));
+            MonitorContext.hint(spans, "Enter", "set");
+            MonitorContext.hintLast(spans, "Esc", "cancel");
+            return;
+        }
+        if (findTerm != null) {
+            MonitorContext.hint(spans, "Esc", "clear find");
+            MonitorContext.hint(spans, "n", "next");
+            MonitorContext.hint(spans, "N", "prev");
+            String pos = findMatches.isEmpty()
+                    ? "0/0"
+                    : (findMatchIndex + 1) + "/" + findMatches.size();
+            spans.add(Span.styled("  /", MonitorContext.HINT_KEY_STYLE));
+            spans.add(Span.raw("\"" + findTerm + "\" [" + pos + "]  "));
+        } else {
+            MonitorContext.hint(spans, "Esc/c", "close");
+        }
         MonitorContext.hint(spans, "↑↓", "navigate");
-        MonitorContext.hint(spans, "Ctrl+↑↓", "scroll");
-        MonitorContext.hint(spans, "←→", "horizontal");
+        MonitorContext.hint(spans, "/", "find");
+        MonitorContext.hint(spans, "h", "highlight" + (highlightTerm != null ? 
" [" + highlightTerm + "]" : ""));
+        MonitorContext.hint(spans, "w", "wrap" + (wordWrap ? " [on]" : " 
[off]"));
+        if (!wordWrap) {
+            MonitorContext.hint(spans, "←→", "horizontal");
+        }
         MonitorContext.hint(spans, "PgUp/PgDn", "page");
         if (onLineSelected != null) {
             MonitorContext.hint(spans, "Enter", "select node");
         }
     }
 
+    /**
+     * Load source for a route, scrolling to the given source line number.
+     */
+    void loadFile(Path filePath) {
+        String fileName = filePath.getFileName().toString();
+        try {
+            List<String> rawLines = java.nio.file.Files.readAllLines(filePath, 
java.nio.charset.StandardCharsets.UTF_8);
+            int lineNumWidth = String.valueOf(rawLines.size()).length();
+            List<String> result = new ArrayList<>();
+            List<JsonObject> codeLines = new ArrayList<>();
+            for (int i = 0; i < rawLines.size(); i++) {
+                int lineNum = i + 1;
+                String code = rawLines.get(i);
+                result.add(String.format("%" + lineNumWidth + "d  %s", 
lineNum, code));
+                JsonObject jo = new JsonObject();
+                jo.put("line", lineNum);
+                jo.put("code", code);
+                codeLines.add(jo);
+            }
+            title = fileName;
+            language = SyntaxHighlighter.detectLanguage(fileName);
+            lines = result;
+            codeData = codeLines;
+            selectedLine = findLicenseHeaderEnd(codeLines);
+            scrollY = 0;
+            scrollX = 0;
+            pendingScroll = true;
+            visible = true;
+        } catch (java.io.IOException e) {
+            title = fileName;
+            lines = List.of("(Failed to read file: " + e.getMessage() + ")");
+            codeData = Collections.emptyList();
+            visible = true;
+        }
+    }
+
     /**
      * Load source for a route, scrolling to the given source line number.
      */
@@ -406,7 +610,7 @@ class SourceViewer {
         });
     }
 
-    private Line highlightSourceLine(String raw, int hSkip, boolean 
isSelected) {
+    private Line highlightSourceLine(String raw, int hSkip, boolean 
isSelected, int viewportWidth) {
         int prefixEnd = 0;
         while (prefixEnd < raw.length() && (raw.charAt(prefixEnd) == ' ' || 
Character.isDigit(raw.charAt(prefixEnd)))) {
             prefixEnd++;
@@ -418,11 +622,11 @@ class SourceViewer {
         Line highlighted = SyntaxHighlighter.highlightLine(code, language);
 
         List<Span> spans = new ArrayList<>();
-        Style selBg = Style.EMPTY.bg(Color.DARK_GRAY);
+        Style selBg = Style.EMPTY.bg(Color.rgb(30, 45, 70));
         if (isSelected) {
             spans.add(Span.styled(">> ", Style.EMPTY.fg(Color.YELLOW).bold()));
             if (!prefix.isEmpty()) {
-                spans.add(Span.styled(prefix, 
Style.EMPTY.fg(Color.YELLOW).bold().bg(Color.DARK_GRAY)));
+                spans.add(Span.styled(prefix, 
Style.EMPTY.fg(Color.YELLOW).bold().patch(selBg)));
             }
             for (Span s : highlighted.spans()) {
                 spans.add(Span.styled(s.content(), s.style().patch(selBg)));
@@ -437,24 +641,140 @@ class SourceViewer {
 
         Line full = Line.from(spans);
 
-        if (hSkip <= 0) {
-            return full;
+        if (hSkip > 0) {
+            List<Span> scrolled = new ArrayList<>();
+            int skipped = 0;
+            for (Span span : full.spans()) {
+                String content = span.content();
+                if (skipped >= hSkip) {
+                    scrolled.add(span);
+                } else if (skipped + content.length() > hSkip) {
+                    int offset = hSkip - skipped;
+                    scrolled.add(Span.styled(content.substring(offset), 
span.style()));
+                    skipped = hSkip;
+                } else {
+                    skipped += content.length();
+                }
+            }
+            full = scrolled.isEmpty() ? Line.from(List.of(Span.raw(""))) : 
Line.from(scrolled);
+        }
+
+        if (isSelected && viewportWidth > 0) {
+            int contentWidth = full.width();
+            if (contentWidth < viewportWidth) {
+                List<Span> padded = new ArrayList<>(full.spans());
+                padded.add(Span.styled(" ".repeat(viewportWidth - 
contentWidth), selBg));
+                full = Line.from(padded);
+            }
+        }
+
+        return full;
+    }
+
+    private void buildFindMatches() {
+        List<Integer> matches = new ArrayList<>();
+        for (int i = 0; i < lines.size(); i++) {
+            if (findPattern.matcher(lines.get(i)).find()) {
+                matches.add(i);
+            }
+        }
+        findMatches = matches;
+    }
+
+    private void jumpToNearestMatch() {
+        if (findMatches.isEmpty()) {
+            findMatchIndex = -1;
+            return;
+        }
+        for (int i = 0; i < findMatches.size(); i++) {
+            if (findMatches.get(i) >= selectedLine) {
+                findMatchIndex = i;
+                scrollToMatch();
+                return;
+            }
+        }
+        findMatchIndex = 0;
+        scrollToMatch();
+    }
+
+    private void navigateToNextMatch() {
+        if (findMatches.isEmpty()) {
+            return;
         }
-        List<Span> scrolled = new ArrayList<>();
-        int skipped = 0;
-        for (Span span : full.spans()) {
+        findMatchIndex = (findMatchIndex + 1) % findMatches.size();
+        scrollToMatch();
+    }
+
+    private void navigateToPrevMatch() {
+        if (findMatches.isEmpty()) {
+            return;
+        }
+        findMatchIndex = findMatchIndex <= 0 ? findMatches.size() - 1 : 
findMatchIndex - 1;
+        scrollToMatch();
+    }
+
+    private void scrollToMatch() {
+        if (findMatchIndex >= 0 && findMatchIndex < findMatches.size()) {
+            selectedLine = findMatches.get(findMatchIndex);
+        }
+    }
+
+    private Line applySearchHighlights(Line line, int lineIndex, int 
currentMatchLine) {
+        String fullText = line.rawContent();
+        if (fullText.isEmpty()) {
+            return line;
+        }
+
+        List<int[]> ranges = new ArrayList<>();
+        List<Style> rangeStyles = new ArrayList<>();
+        if (highlightPattern != null) {
+            Matcher m = highlightPattern.matcher(fullText);
+            while (m.find()) {
+                ranges.add(new int[] { m.start(), m.end() });
+                rangeStyles.add(HIGHLIGHT_STYLE);
+            }
+        }
+        if (findPattern != null) {
+            boolean isCurrentLine = lineIndex == currentMatchLine;
+            Matcher m = findPattern.matcher(fullText);
+            while (m.find()) {
+                ranges.add(new int[] { m.start(), m.end() });
+                rangeStyles.add(isCurrentLine ? FIND_CURRENT_STYLE : 
FIND_MATCH_STYLE);
+            }
+        }
+        if (ranges.isEmpty()) {
+            return line;
+        }
+
+        List<Span> original = line.spans();
+        List<Span> result = new ArrayList<>();
+        int charPos = 0;
+        for (Span span : original) {
             String content = span.content();
-            if (skipped >= hSkip) {
-                scrolled.add(span);
-            } else if (skipped + content.length() > hSkip) {
-                int offset = hSkip - skipped;
-                scrolled.add(Span.styled(content.substring(offset), 
span.style()));
-                skipped = hSkip;
-            } else {
-                skipped += content.length();
+            Style baseStyle = span.style();
+            int spanStart = charPos;
+            int spanEnd = charPos + content.length();
+            int cursor = 0;
+            for (int r = 0; r < ranges.size(); r++) {
+                int matchStart = ranges.get(r)[0];
+                int matchEnd = ranges.get(r)[1];
+                if (matchEnd <= spanStart || matchStart >= spanEnd) {
+                    continue;
+                }
+                int localStart = Math.max(0, matchStart - spanStart);
+                int localEnd = Math.min(content.length(), matchEnd - 
spanStart);
+                if (localStart > cursor) {
+                    result.add(Span.styled(content.substring(cursor, 
localStart), baseStyle));
+                }
+                result.add(Span.styled(content.substring(localStart, 
localEnd), rangeStyles.get(r)));
+                cursor = localEnd;
+            }
+            if (cursor < content.length()) {
+                result.add(Span.styled(content.substring(cursor), baseStyle));
             }
+            charPos = spanEnd;
         }
-        return scrolled.isEmpty() ? Line.from(List.of(Span.raw(""))) : 
Line.from(scrolled);
+        return Line.from(result);
     }
 
     static int findLicenseHeaderEnd(List<JsonObject> codeLines) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiMcpServer.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiMcpServer.java
index f97427bbd2f2..e0e90f8ed958 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiMcpServer.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiMcpServer.java
@@ -439,6 +439,13 @@ class TuiMcpServer {
                                 "If provided, forces the section on (true) or 
off (false). "
                                                       + "If omitted, toggles 
the current state.")),
                 List.of("section")));
+        toolList.add(toolDef(
+                "tui_get_readme",
+                "Returns the README/documentation content from a running 
integration. "
+                                  + "Useful for understanding what the 
integration does, its configuration, and usage. "
+                                  + "If no name is provided, returns the 
README for the currently selected integration.",
+                Map.of("name", propDef("string",
+                        "Integration name. If omitted, uses the currently 
selected integration."))));
 
         JsonObject result = new JsonObject();
         result.put("tools", toolList);
@@ -484,6 +491,7 @@ class TuiMcpServer {
                 case "tui_set_log_level" -> callSetLogLevel(args);
                 case "tui_filter" -> callFilter(args);
                 case "tui_toggle_trace_display" -> 
callToggleTraceDisplay(args);
+                case "tui_get_readme" -> callGetReadme(args);
                 default -> {
                     isError = true;
                     yield "Unknown tool: " + toolName;
@@ -1074,6 +1082,22 @@ class TuiMcpServer {
         return result;
     }
 
+    private String callGetReadme(Map<String, Object> args) {
+        String name = args.get("name") instanceof String s ? s : null;
+        JsonObject response = monitor.getReadme(name);
+        if (response == null) {
+            return name != null
+                    ? "No README found for integration '" + name + "'"
+                    : "No README found for the selected integration";
+        }
+        JsonObject result = new JsonObject();
+        String content = response.getString("content");
+        String file = response.getStringOrDefault("file", "README");
+        result.put("file", file);
+        result.put("content", content != null ? content : "");
+        return Jsoner.serialize(result);
+    }
+
     private static JsonArray toJsonArray(List<String> list) {
         JsonArray arr = new JsonArray();
         arr.addAll(list);

Reply via email to