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 b3830a04924f CAMEL-23672: TUI - Diagram external toggle with three 
modes (off/edges/all) (#23809)
b3830a04924f is described below

commit b3830a04924f1b16b94cc5dd2c63b6962dc90354
Author: Claus Ibsen <[email protected]>
AuthorDate: Sat Jun 6 22:59:51 2026 +0200

    CAMEL-23672: TUI - Diagram external toggle with three modes (off/edges/all) 
(#23809)
    
    * CAMEL-23672: TUI - Diagram external toggle with three modes 
(off/edges/all)
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    * CAMEL-23672: TUI - Add tui_get_files MCP tool and tui_control + 
improvements
    
    Co-Authored-By: Claude <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    * CAMEL-23672: TUI - MCP diagram navigation, state reporting, and footer 
actions
    
    Add route/node parameters to tui_navigate for programmatic diagram
    navigation without arrow keys. Enrich tui_get_state with diagram context
    (mode, selected node, info panel stats). Expose footer keyboard shortcuts
    as structured JSON actions in tui_get_state, tui_navigate, and tui_send_keys
    responses so AI agents discover available actions without screen scraping.
    
    Co-Authored-By: Claude <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    * CAMEL-23672: TUI - ErrorsTab compact BHPV toggles and error count in title
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    * CAMEL-23672: TUI - Add tui_locate MCP tool for screen coordinate lookup
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    * CAMEL-23672: TUI - Add tui_draw_shape MCP tool for drawing shapes on 
screen
    
    Adds shape generators (box, highlight, underline, arrows, text) to 
DrawOverlay
    and registers tui_draw_shape MCP tool. Highlight mode preserves existing 
text
    and applies background color like a marker pen. Supports append mode for
    composing multiple shapes.
    
    Co-Authored-By: Claude <[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]>
---
 .../org/apache/camel/diagram/TopologyHelper.java   |  69 ++++++
 .../dsl/jbang/core/commands/tui/ActionsPopup.java  |  42 +++-
 .../dsl/jbang/core/commands/tui/CamelMonitor.java  | 245 +++++++++++++++++++++
 .../jbang/core/commands/tui/DiagramSupport.java    | 144 +++++++++++-
 .../dsl/jbang/core/commands/tui/DiagramTab.java    | 239 ++++++++++++++++++--
 .../dsl/jbang/core/commands/tui/DrawOverlay.java   |  97 +++++++-
 .../dsl/jbang/core/commands/tui/ErrorsTab.java     |   8 +-
 .../dsl/jbang/core/commands/tui/FilesBrowser.java  |  24 ++
 .../camel/dsl/jbang/core/commands/tui/LogTab.java  |   2 +-
 .../dsl/jbang/core/commands/tui/RoutesTab.java     |  11 +-
 .../jbang/core/commands/tui/SearchHighlighter.java |   4 +
 .../dsl/jbang/core/commands/tui/TuiCommand.java    |  47 +++-
 .../dsl/jbang/core/commands/tui/TuiMcpServer.java  | 224 ++++++++++++++++++-
 .../tui/diagram/TopologyDiagramWidget.java         |   3 +-
 .../tui/diagram/TopologyMinimapWidget.java         |  13 +-
 15 files changed, 1117 insertions(+), 55 deletions(-)

diff --git 
a/components/camel-diagram/src/main/java/org/apache/camel/diagram/TopologyHelper.java
 
b/components/camel-diagram/src/main/java/org/apache/camel/diagram/TopologyHelper.java
index ce48c0d105a5..d59a2a3bb1ad 100644
--- 
a/components/camel-diagram/src/main/java/org/apache/camel/diagram/TopologyHelper.java
+++ 
b/components/camel-diagram/src/main/java/org/apache/camel/diagram/TopologyHelper.java
@@ -17,7 +17,11 @@
 package org.apache.camel.diagram;
 
 import java.util.ArrayList;
+import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
 
 import org.apache.camel.diagram.TopologyLayoutEngine.TopologyEdgeInfo;
 import org.apache.camel.diagram.TopologyLayoutEngine.TopologyNodeInfo;
@@ -121,6 +125,71 @@ public final class TopologyHelper {
         }
     }
 
+    /**
+     * Expands external-type route-to-route edges into intermediary external 
nodes. For each shared external endpoint
+     * (e.g., kafka:foo used by both a producer and consumer route), the 
direct edge is replaced with a dashed external
+     * box and two edges passing through it.
+     */
+    public static void expandExternalEdges(List<TopologyNodeInfo> nodes, 
List<TopologyEdgeInfo> edges) {
+        // Only consider route nodes — exclude external-in/external-out nodes 
already added
+        Set<String> routeIds = nodes.stream()
+                .filter(n -> n.nodeType == null || 
!n.nodeType.startsWith("external"))
+                .map(n -> n.routeId)
+                .collect(Collectors.toSet());
+
+        // Group external edges by endpoint URI (only edges between two route 
nodes)
+        Map<String, List<TopologyEdgeInfo>> byEndpoint = new LinkedHashMap<>();
+        for (TopologyEdgeInfo edge : edges) {
+            if ("external".equals(edge.connectionType)
+                    && routeIds.contains(edge.fromRouteId) && 
routeIds.contains(edge.toRouteId)) {
+                byEndpoint.computeIfAbsent(edge.endpoint, k -> new 
ArrayList<>()).add(edge);
+            }
+        }
+        if (byEndpoint.isEmpty()) {
+            return;
+        }
+
+        int idx = 0;
+        for (Map.Entry<String, List<TopologyEdgeInfo>> entry : 
byEndpoint.entrySet()) {
+            String uri = entry.getKey();
+            List<TopologyEdgeInfo> group = entry.getValue();
+
+            // Create intermediary external node
+            TopologyNodeInfo extNode = new TopologyNodeInfo();
+            extNode.routeId = "ext-" + idx++;
+            extNode.from = uri;
+            extNode.nodeType = "external";
+            int colonIdx = uri.indexOf(':');
+            extNode.description = colonIdx > 0 ? uri.substring(colonIdx + 1) : 
uri;
+
+            // Determine scheme from URI
+            if (colonIdx > 0) {
+                extNode.fromScheme = uri.substring(0, colonIdx);
+            }
+
+            nodes.add(extNode);
+
+            // Replace each original edge with two edges through the 
intermediary node
+            for (TopologyEdgeInfo orig : group) {
+                edges.remove(orig);
+
+                TopologyEdgeInfo toExt = new TopologyEdgeInfo();
+                toExt.fromRouteId = orig.fromRouteId;
+                toExt.toRouteId = extNode.routeId;
+                toExt.endpoint = uri;
+                toExt.connectionType = "external";
+                edges.add(toExt);
+
+                TopologyEdgeInfo fromExt = new TopologyEdgeInfo();
+                fromExt.fromRouteId = extNode.routeId;
+                fromExt.toRouteId = orig.toRouteId;
+                fromExt.endpoint = uri;
+                fromExt.connectionType = "external";
+                edges.add(fromExt);
+            }
+        }
+    }
+
     public static void enrichWithMetrics(List<TopologyNodeInfo> nodes, 
JsonObject routeStructureJson) {
         if (routeStructureJson == null) {
             return;
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 afed843f228b..50b318011c04 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
@@ -83,12 +83,13 @@ class ActionsPopup {
         TAPE_INSTRUCTIONS,
         CAPTION,
         SHOW_KEYSTROKES,
+        SETUP_AI,
         MCP_INFO,
         MCP_LOG
     }
 
     private static final int[] GROUP_SIZES = { 5, 4, 5 };
-    private static final int MCP_GROUP_SIZE = 2;
+    private static final int MCP_GROUP_SIZE = 3;
 
     private final Supplier<Set<String>> runningNames;
     private final Supplier<List<IntegrationInfo>> integrations;
@@ -338,6 +339,7 @@ class ActionsPopup {
         // Group 4: MCP
         if (mcpEnabled) {
             labels.add("───");
+            labels.add("Setup AI...");
             labels.add("MCP Info");
             labels.add("MCP Log");
         }
@@ -564,6 +566,9 @@ class ActionsPopup {
                         doctorPopup.open();
                     } else if (action == Action.RUN_INFRA) {
                         openInfraBrowser();
+                    } else if (action == Action.SETUP_AI) {
+                        showActionsMenu = false;
+                        openSetupAI();
                     } else if (action == Action.MCP_INFO) {
                         showActionsMenu = false;
                         openMcpInfo();
@@ -767,6 +772,7 @@ class ActionsPopup {
         // Group 4: MCP
         if (mcpEnabled) {
             items.add(ListItem.from(divider).style(Style.EMPTY.dim()));
+            items.add(ListItem.from("  🧠 Setup AI..."));
             items.add(ListItem.from("  🤖 MCP Info"));
             items.add(ListItem.from("  📋 MCP Log"));
         }
@@ -1122,6 +1128,38 @@ class ActionsPopup {
         docViewerFromExampleBrowser = false;
     }
 
+    private void openSetupAI() {
+        docLines = null;
+        String url = "http://localhost:"; + mcpPort + "/mcp";
+        String client = mcpConnectedClient != null ? mcpConnectedClient.get() 
: null;
+        String status = client != null
+                ? "**Connected:** " + client + "\n\nYour AI agent is already 
connected and ready to use."
+                : "**Status:** Waiting for connection";
+        docContent = "# Setup AI Agent\n\n"
+                     + status + "\n\n"
+                     + "## Connect Claude Code\n\n"
+                     + "Run this command in your terminal:\n\n"
+                     + "    claude mcp add --transport http camel-tui " + url 
+ "\n\n"
+                     + "Then start a new Claude Code session. The TUI footer 
will turn green\n"
+                     + "when the AI agent connects.\n\n"
+                     + "## Alternative: .mcp.json\n\n"
+                     + "A `.mcp.json` file is auto-generated in the current 
directory while the\n"
+                     + "TUI runs with `--mcp`. AI agents that scan for 
`.mcp.json` will discover\n"
+                     + "the MCP server automatically.\n\n"
+                     + "## What the AI Can Do\n\n"
+                     + "Once connected, your AI agent can:\n\n"
+                     + "- See the TUI screen and follow your key presses\n"
+                     + "- Navigate tabs and select integrations\n"
+                     + "- Read route diagrams and health status\n"
+                     + "- Send test messages to endpoints\n"
+                     + "- Record VHS tapes for documentation\n\n"
+                     + "Try asking: *\"What's on my Camel TUI screen right 
now?\"*\n";
+        docTitle = "Setup AI";
+        docScroll = 0;
+        showDocViewer = true;
+        docViewerFromExampleBrowser = false;
+    }
+
     private void openMcpInfo() {
         docLines = null;
         String url = "http://localhost:"; + mcpPort + "/mcp";
@@ -1146,6 +1184,7 @@ class ActionsPopup {
                      + "| `tui_navigate` | Switch tabs and select integrations 
|\n"
                      + "| `tui_send_keys` | Send key presses to control the 
TUI |\n"
                      + "| `tui_wait_for_idle` | Waits for the screen to settle 
after an action |\n"
+                     + "| `tui_control` | Stop/start routes, restart, stop, or 
kill integration |\n"
                      + "| `tui_tape_start` | Start recording interactions as a 
VHS .tape file |\n"
                      + "| `tui_tape_stop` | Stop recording and return the tape 
content |\n\n"
                      + "## Setup for Claude Code\n\n"
@@ -2094,6 +2133,7 @@ class ActionsPopup {
             case TAPE_RECORDING -> toggleTapeRecording.run();
             case DOCTOR -> doctorPopup.open();
             case CAPTION -> captionOverlay.openInline();
+            case SETUP_AI -> openSetupAI();
             case MCP_INFO -> openMcpInfo();
             case MCP_LOG -> openMcpLog();
             default -> {
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 ec967232f378..1f935cbe9020 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
@@ -78,6 +78,7 @@ import 
org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
 import org.apache.camel.dsl.jbang.core.common.PathUtils;
 import org.apache.camel.dsl.jbang.core.common.RuntimeHelper;
 import org.apache.camel.dsl.jbang.core.common.VersionHelper;
+import org.apache.camel.util.json.JsonArray;
 import org.apache.camel.util.json.JsonObject;
 import org.apache.camel.util.json.Jsoner;
 import picocli.CommandLine;
@@ -1653,6 +1654,9 @@ public class CamelMonitor extends CamelCommand {
             }
             rightSpans.add(Span.styled(mcpLabel, labelStyle));
             rightSpans.add(Span.styled(suffix, suffixStyle));
+            if (client == null) {
+                rightSpans.add(Span.styled("  F2 → Setup AI", 
Style.EMPTY.dim()));
+            }
         }
 
         if (!rightSpans.isEmpty()) {
@@ -2426,6 +2430,73 @@ public class CamelMonitor extends CamelCommand {
         }
     }
 
+    JsonObject getFiles(String name, String file) {
+        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;
+        }
+        Path dir = FilesBrowser.resolveSourceDirectory(target);
+        if (dir == null || !Files.isDirectory(dir)) {
+            return null;
+        }
+        if (file != null && !file.isEmpty()) {
+            Path filePath = dir.resolve(file);
+            if (!Files.isRegularFile(filePath)) {
+                return null;
+            }
+            try {
+                String content = Files.readString(filePath, 
StandardCharsets.UTF_8);
+                JsonObject result = new JsonObject();
+                result.put("file", file);
+                result.put("directory", dir.toString());
+                result.put("size", 
FilesBrowser.formatFileSize(Files.size(filePath)));
+                result.put("type", FilesBrowser.fileType(filePath));
+                result.put("content", content);
+                return result;
+            } catch (IOException e) {
+                return null;
+            }
+        }
+        JsonArray files = new JsonArray();
+        try (var stream = Files.list(dir)) {
+            stream.filter(Files::isRegularFile)
+                    .sorted((a, b) -> 
a.getFileName().toString().compareToIgnoreCase(b.getFileName().toString()))
+                    .limit(99)
+                    .forEach(p -> {
+                        JsonObject entry = new JsonObject();
+                        entry.put("name", p.getFileName().toString());
+                        try {
+                            entry.put("size", 
FilesBrowser.formatFileSize(Files.size(p)));
+                        } catch (IOException e) {
+                            entry.put("size", "0 B");
+                        }
+                        entry.put("type", FilesBrowser.fileType(p));
+                        files.add(entry);
+                    });
+        } catch (IOException e) {
+            return null;
+        }
+        if (files.isEmpty()) {
+            return null;
+        }
+        JsonObject result = new JsonObject();
+        result.put("directory", dir.toString());
+        result.put("files", files);
+        result.put("totalFiles", files.size());
+        return result;
+    }
+
     int injectKeys(List<String> keys, int delayMs) {
         long fireAt = System.currentTimeMillis();
         int count = 0;
@@ -2539,6 +2610,137 @@ public class CamelMonitor extends CamelCommand {
         return diagramTab.getTopologyDataAsJson();
     }
 
+    String navigateDiagramToRoute(String routeId) {
+        navigateToTab("Diagram");
+        if (diagramTab.selectRoute(routeId)) {
+            return routeId;
+        }
+        return null;
+    }
+
+    String navigateDiagramToNode(String routeId, String nodeId) {
+        navigateToTab("Diagram");
+        if (diagramTab.selectNode(routeId, nodeId)) {
+            return nodeId;
+        }
+        return null;
+    }
+
+    JsonObject getDiagramState() {
+        return diagramTab.getDiagramStateAsJson();
+    }
+
+    JsonArray locateText(String search) {
+        Buffer buf = lastBuffer;
+        if (buf == null || search == null || search.isEmpty()) {
+            return new JsonArray();
+        }
+        String screen = ExportRequest.export(buf).text().toString();
+        String[] lines = screen.split("\n", -1);
+        int searchWidth = 0;
+        for (int i = 0; i < search.length();) {
+            int cp = search.codePointAt(i);
+            searchWidth += Math.max(1, CharWidth.of(cp));
+            i += Character.charCount(cp);
+        }
+        JsonArray matches = new JsonArray();
+        for (int y = 0; y < lines.length; y++) {
+            String line = lines[y];
+            int idx = line.indexOf(search);
+            while (idx >= 0) {
+                int visualCol = 0;
+                for (int i = 0; i < idx;) {
+                    int cp = line.codePointAt(i);
+                    visualCol += Math.max(1, CharWidth.of(cp));
+                    i += Character.charCount(cp);
+                }
+                JsonObject match = new JsonObject();
+                match.put("x", visualCol);
+                match.put("y", y);
+                match.put("width", searchWidth);
+                match.put("height", 1);
+                match.put("text", search);
+                matches.add(match);
+                idx = line.indexOf(search, idx + 1);
+            }
+            if (matches.size() >= 20) {
+                break;
+            }
+        }
+        return matches;
+    }
+
+    JsonObject locateNodes(List<String> nodeIds) {
+        return diagramTab.locateNodes(nodeIds);
+    }
+
+    JsonArray getFooterActionsAsJson() {
+        List<Span> spans = new ArrayList<>();
+        if (helpOverlay.isVisible()) {
+            helpOverlay.renderFooter(spans);
+        } else if (captionOverlay.isCaptionVisible()) {
+            captionOverlay.renderFooter(spans);
+        } else if (filesBrowser.isVisible()) {
+            filesBrowser.renderFooter(spans);
+        } else if (showSwitchPopup || showMorePopup) {
+            if (showSwitchPopup) {
+                hint(spans, "Up/Down", "select");
+                hint(spans, "Enter", "switch");
+                hint(spans, "Esc", "close");
+            } else {
+                hint(spans, "Up/Down", "select");
+                hint(spans, "Enter", "open");
+                hint(spans, "Esc", "close");
+            }
+        } else {
+            MonitorTab tab = activeTab();
+            if (tabsState.selected() == TAB_OVERVIEW) {
+                renderOverviewFooter(spans);
+            } else if (tab != null) {
+                tab.renderFooter(spans);
+                insertFKeyHints(spans);
+            }
+        }
+        JsonArray actions = new JsonArray();
+        for (int i = 0; i + 1 < spans.size(); i += 2) {
+            String key = spans.get(i).content().trim();
+            String rawLabel = spans.get(i + 1).content().trim();
+            // compact "show BHPV" pattern: key="show", then space, then 4 
single-letter spans, then trailing space
+            if ("show".equals(key) && i + 6 < spans.size()) {
+                for (int j = 0; j < 4; j++) {
+                    Span letter = spans.get(i + 2 + j);
+                    String ch = letter.content();
+                    boolean on = ch.equals(ch.toUpperCase());
+                    JsonObject toggle = new JsonObject();
+                    toggle.put("key", ch.toLowerCase());
+                    String label = switch (ch.toLowerCase()) {
+                        case "b" -> "body";
+                        case "h" -> "headers";
+                        case "p" -> "properties";
+                        case "v" -> "variables";
+                        default -> ch;
+                    };
+                    toggle.put("label", label);
+                    toggle.put("state", on ? "on" : "off");
+                    actions.add(toggle);
+                }
+                i += 5; // skip the 7-span group (loop adds 2, we consumed 5 
more)
+                continue;
+            }
+            JsonObject action = new JsonObject();
+            action.put("key", key);
+            int bracket = rawLabel.indexOf('[');
+            if (bracket > 0 && rawLabel.endsWith("]")) {
+                action.put("label", rawLabel.substring(0, bracket).trim());
+                action.put("state", rawLabel.substring(bracket + 1, 
rawLabel.length() - 1));
+            } else {
+                action.put("label", rawLabel);
+            }
+            actions.add(action);
+        }
+        return actions;
+    }
+
     void setLogLevel(String level) {
         logTab.setLogLevel(level);
     }
@@ -2568,6 +2770,49 @@ public class CamelMonitor extends CamelCommand {
         return RuntimeHelper.sendMessage(pid, endpoint, body, headers);
     }
 
+    String controlIntegration(String action) {
+        if (action == null || action.isBlank()) {
+            return "Error: action is required";
+        }
+        if (ctx.selectedPid == null) {
+            return "Error: no integration selected";
+        }
+        String name = selectedName();
+        return switch (action) {
+            case "stop-routes", "pause" -> {
+                if (isInfraSelected()) {
+                    yield "Error: cannot stop routes on infra service";
+                }
+                sendRouteCommand(ctx.selectedPid, "*", "stop");
+                yield "Routes stopped for " + name;
+            }
+            case "start-routes", "resume" -> {
+                if (isInfraSelected()) {
+                    yield "Error: cannot start routes on infra service";
+                }
+                sendRouteCommand(ctx.selectedPid, "*", "start");
+                yield "Routes started for " + name;
+            }
+            case "restart" -> {
+                if (isInfraSelected()) {
+                    yield "Error: cannot restart infra service";
+                }
+                restartSelectedProcess();
+                yield "Restarting " + name;
+            }
+            case "stop" -> {
+                stopSelectedProcess(false);
+                yield "Stopping " + name;
+            }
+            case "kill" -> {
+                stopSelectedProcess(true);
+                yield "Killed " + name;
+            }
+            default -> "Unknown action: " + action
+                       + ". Available: stop-routes, start-routes, restart, 
stop, kill";
+        };
+    }
+
     private record PendingKey(KeyEvent event, long fireAt) {
     }
 
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramSupport.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramSupport.java
index c31681f97321..ef5ddfea2941 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramSupport.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramSupport.java
@@ -79,6 +79,8 @@ class DiagramSupport {
     private String pendingSelectionRouteId;
     private int lastVisibleHeight;
     private int lastVisibleWidth;
+    private int lastAreaX;
+    private int lastAreaY;
 
     // Native widget rendering data
     private TopologyLayoutResult topologyLayout;
@@ -151,6 +153,15 @@ class DiagramSupport {
         return null;
     }
 
+    int findNodeIndexByRouteId(String routeId) {
+        for (int i = 0; i < nodeBoxes.size(); i++) {
+            if (routeId.equals(nodeBoxes.get(i).routeId())) {
+                return i;
+            }
+        }
+        return -1;
+    }
+
     TopologyLayoutNode getSelectedTopologyNode() {
         String routeId = getSelectedRouteId();
         if (routeId == null) {
@@ -244,6 +255,67 @@ class DiagramSupport {
         return result;
     }
 
+    JsonObject locateNodes(List<String> ids) {
+        if (ids == null || ids.isEmpty()) {
+            return null;
+        }
+        JsonArray matches = new JsonArray();
+        int minX = Integer.MAX_VALUE, minY = Integer.MAX_VALUE;
+        int maxX = Integer.MIN_VALUE, maxY = Integer.MIN_VALUE;
+
+        for (String id : ids) {
+            // search topology nodeBoxes by routeId
+            for (var nb : nodeBoxes) {
+                if (id.equals(nb.routeId())) {
+                    JsonObject m = nodeBoxToScreen(nb.startRow(), nb.endRow(), 
nb.startCol(), nb.endCol());
+                    m.put("node", id);
+                    matches.add(m);
+                    minX = Math.min(minX, m.getInteger("x"));
+                    minY = Math.min(minY, m.getInteger("y"));
+                    maxX = Math.max(maxX, m.getInteger("x") + 
m.getInteger("width"));
+                    maxY = Math.max(maxY, m.getInteger("y") + 
m.getInteger("height"));
+                    break;
+                }
+            }
+            // search eip nodeBoxes by nodeId
+            for (var nb : eipNodeBoxes) {
+                if (id.equals(nb.nodeId())) {
+                    JsonObject m = nodeBoxToScreen(nb.startRow(), nb.endRow(), 
nb.startCol(), nb.endCol());
+                    m.put("node", id);
+                    matches.add(m);
+                    minX = Math.min(minX, m.getInteger("x"));
+                    minY = Math.min(minY, m.getInteger("y"));
+                    maxX = Math.max(maxX, m.getInteger("x") + 
m.getInteger("width"));
+                    maxY = Math.max(maxY, m.getInteger("y") + 
m.getInteger("height"));
+                    break;
+                }
+            }
+        }
+        if (matches.isEmpty()) {
+            return null;
+        }
+        JsonObject result = new JsonObject();
+        result.put("matches", matches);
+        if (matches.size() > 1) {
+            JsonObject bounds = new JsonObject();
+            bounds.put("x", minX);
+            bounds.put("y", minY);
+            bounds.put("width", maxX - minX);
+            bounds.put("height", maxY - minY);
+            result.put("bounds", bounds);
+        }
+        return result;
+    }
+
+    private JsonObject nodeBoxToScreen(int startRow, int endRow, int startCol, 
int endCol) {
+        JsonObject m = new JsonObject();
+        m.put("x", lastAreaX + startCol - scrollX);
+        m.put("y", lastAreaY + startRow - scrollY);
+        m.put("width", endCol - startCol + 1);
+        m.put("height", endRow - startRow + 1);
+        return m;
+    }
+
     RouteDiagramLayoutEngine.LayoutRoute getRouteLayout(String routeId) {
         return routeLayouts.get(routeId);
     }
@@ -409,7 +481,7 @@ class DiagramSupport {
             ctx.runner.scheduler().execute(() -> {
                 try {
                     setTopologyMode(true);
-                    loadAllDiagramsInBackground(ctx, pid, false, false);
+                    loadAllDiagramsInBackground(ctx, pid, false, 0);
                 } finally {
                     endLoad();
                 }
@@ -640,7 +712,8 @@ class DiagramSupport {
         for (TopologyLayoutNode node : result.nodes) {
             int col = nodeW == 0 ? 0 : node.x * bw / nodeW;
             int row = node.y / 20;
-            boolean ext = "external-in".equals(node.nodeType) || 
"external-out".equals(node.nodeType);
+            boolean ext = "external-in".equals(node.nodeType) || 
"external-out".equals(node.nodeType)
+                    || "external".equals(node.nodeType);
             int contentLines;
             if (ext) {
                 contentLines = 1;
@@ -697,7 +770,10 @@ class DiagramSupport {
                 .constraints(Constraint.fill(), Constraint.length(1))
                 .split(vChunks.get(0));
 
-        frame.renderWidget(finalWidget, hChunks.get(0));
+        Rect widgetArea = hChunks.get(0);
+        frame.renderWidget(finalWidget, widgetArea);
+        lastAreaX = widgetArea.x();
+        lastAreaY = widgetArea.y();
 
         // Update nodeBoxes from widget
         List<TopologyAsciiRenderer.NodeBox> widgetBoxes = new ArrayList<>();
@@ -799,6 +875,15 @@ class DiagramSupport {
         return null;
     }
 
+    int findEipNodeIndexByNodeId(String nodeId) {
+        for (int i = 0; i < eipNodeBoxes.size(); i++) {
+            if (nodeId.equals(eipNodeBoxes.get(i).nodeId())) {
+                return i;
+            }
+        }
+        return -1;
+    }
+
     /**
      * Finds the route ID that the selected EIP node links to, by matching the 
node's endpoint URI against topology
      * edges and route "from" endpoints.
@@ -828,6 +913,10 @@ class DiagramSupport {
             if ("from".equals(type)) {
                 // "from" node: find route that sends TO this endpoint
                 if (currentRouteId.equals(edge.to.routeId) && 
!currentRouteId.equals(edge.from.routeId)) {
+                    String resolved = resolveThrough(edge.from.routeId, 
currentRouteId);
+                    if (resolved != null) {
+                        return resolved;
+                    }
                     return edge.from.routeId;
                 }
             } else {
@@ -835,6 +924,10 @@ class DiagramSupport {
                 if (currentRouteId.equals(edge.from.routeId) && 
!currentRouteId.equals(edge.to.routeId)) {
                     String targetFrom = stripQueryParams(edge.to.from);
                     if (baseUri.equals(targetFrom)) {
+                        String resolved = resolveThrough(edge.to.routeId, 
currentRouteId);
+                        if (resolved != null) {
+                            return resolved;
+                        }
                         return edge.to.routeId;
                     }
                 }
@@ -856,6 +949,30 @@ class DiagramSupport {
         return null;
     }
 
+    private String resolveThrough(String nodeId, String excludeRouteId) {
+        if (!"external".equals(findNodeType(nodeId))) {
+            return null;
+        }
+        for (TopologyLayoutEdge e : topologyEdges) {
+            if (nodeId.equals(e.from.routeId) && 
!excludeRouteId.equals(e.to.routeId)) {
+                return e.to.routeId;
+            }
+            if (nodeId.equals(e.to.routeId) && 
!excludeRouteId.equals(e.from.routeId)) {
+                return e.from.routeId;
+            }
+        }
+        return null;
+    }
+
+    private String findNodeType(String nodeId) {
+        for (TopologyLayoutNode n : topologyNodes) {
+            if (nodeId.equals(n.routeId)) {
+                return n.nodeType;
+            }
+        }
+        return null;
+    }
+
     static String getBaseUri(RouteDiagramLayoutEngine.NodeInfo info) {
         String uri = info.uri;
         if (uri == null) {
@@ -1130,7 +1247,10 @@ class DiagramSupport {
                 .constraints(Constraint.fill(), Constraint.length(1))
                 .split(vChunks.get(0));
 
-        frame.renderWidget(finalWidget, hChunks.get(0));
+        Rect widgetArea = hChunks.get(0);
+        frame.renderWidget(finalWidget, widgetArea);
+        lastAreaX = widgetArea.x();
+        lastAreaY = widgetArea.y();
 
         eipNodeBoxes = new ArrayList<>(finalWidget.getNodeBoxes());
         if (selectedEipNodeIndex < 0 && !eipNodeBoxes.isEmpty()) {
@@ -1567,7 +1687,10 @@ class DiagramSupport {
                 .constraints(Constraint.fill(), Constraint.length(1))
                 .split(vChunks.get(0));
 
-        frame.renderWidget(finalWidget, hChunks.get(0));
+        Rect widgetArea = hChunks.get(0);
+        frame.renderWidget(finalWidget, widgetArea);
+        lastAreaX = widgetArea.x();
+        lastAreaY = widgetArea.y();
 
         List<TopologyAsciiRenderer.NodeBox> widgetBoxes = new ArrayList<>();
         for (var nb : finalWidget.getNodeBoxes()) {
@@ -1692,7 +1815,10 @@ class DiagramSupport {
                 .constraints(Constraint.fill(), Constraint.length(1))
                 .split(vChunks.get(0));
 
-        frame.renderWidget(finalWidget, hChunks.get(0));
+        Rect widgetArea = hChunks.get(0);
+        frame.renderWidget(finalWidget, widgetArea);
+        lastAreaX = widgetArea.x();
+        lastAreaY = widgetArea.y();
 
         eipNodeBoxes = new ArrayList<>(finalWidget.getNodeBoxes());
         selectedEipNodeIndex = selIdx;
@@ -1720,8 +1846,9 @@ class DiagramSupport {
     }
 
     void loadAllDiagramsInBackground(
-            MonitorContext ctx, String pid, boolean metrics, boolean external) 
{
+            MonitorContext ctx, String pid, boolean metrics, int externalMode) 
{
         // Single IPC call: topology + route structures
+        boolean external = externalMode > 0;
         JsonObject topoJson = requestRouteTopology(ctx, pid, external, true);
 
         TopologyLayoutResult topoResult = null;
@@ -1735,6 +1862,9 @@ class DiagramSupport {
             if (external) {
                 TopologyHelper.addExternalEndpoints(nodes, edges, topoJson);
             }
+            if (externalMode == 2) {
+                TopologyHelper.expandExternalEdges(nodes, edges);
+            }
             if (!nodes.isEmpty()) {
                 TopologyLayoutEngine engine = new TopologyLayoutEngine();
                 topoResult = engine.layout(nodes, edges);
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTab.java
index aeea11c9205d..00265881eea2 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTab.java
@@ -35,6 +35,7 @@ import dev.tamboui.widgets.block.Block;
 import dev.tamboui.widgets.block.BorderType;
 import dev.tamboui.widgets.paragraph.Paragraph;
 import org.apache.camel.util.TimeUtils;
+import org.apache.camel.util.json.JsonArray;
 import org.apache.camel.util.json.JsonObject;
 
 import static org.apache.camel.dsl.jbang.core.commands.tui.MonitorContext.*;
@@ -45,7 +46,8 @@ class DiagramTab implements MonitorTab {
     private final DiagramSupport diagram = new DiagramSupport();
     private final SourceViewer sourceViewer = new SourceViewer();
     private boolean diagramMetrics = true;
-    private boolean showExternal;
+    private static final String[] EXTERNAL_LABELS = { " [off]", " [edges]", " 
[all]" };
+    private int externalMode;
     private boolean topologyMode = true;
     private String drillDownRouteId;
     private final Deque<String> routeNavigationStack = new ArrayDeque<>();
@@ -175,9 +177,9 @@ class DiagramTab implements MonitorTab {
             return true;
         }
 
-        // Toggle external systems
+        // Cycle external systems: off → edges → all → off
         if (diagram.isShowDiagram() && topologyMode && 
ke.isCharIgnoreCase('e')) {
-            showExternal = !showExternal;
+            externalMode = (externalMode + 1) % 3;
             diagram.endLoad();
             reloadDiagram();
             return true;
@@ -465,9 +467,10 @@ class DiagramTab implements MonitorTab {
             var topoNode = diagram.getSelectedTopologyNode();
             if (topoNode != null) {
                 boolean isInbound = "external-in".equals(topoNode.nodeType);
+                boolean isBridge = "external".equals(topoNode.nodeType);
+                String label = isBridge ? " External" : isInbound ? " Inbound" 
: " Outbound";
                 lines.add(Line.from(
-                        Span.styled(isInbound ? " Inbound" : " Outbound",
-                                Style.EMPTY.fg(Color.CYAN).bold())));
+                        Span.styled(label, 
Style.EMPTY.fg(Color.CYAN).bold())));
                 lines.add(Line.from(Span.raw("")));
                 lines.add(Line.from(
                         Span.styled(" URI: ", Style.EMPTY.dim()),
@@ -477,12 +480,14 @@ class DiagramTab implements MonitorTab {
                             Span.styled(" Path: ", Style.EMPTY.dim()),
                             Span.raw(topoNode.description)));
                 }
-                String connectedRoute = diagram.getConnectedRouteId(routeId);
-                if (connectedRoute != null) {
-                    lines.add(Line.from(Span.raw("")));
-                    lines.add(Line.from(
-                            Span.styled(isInbound ? " To route: " : " From 
route: ", Style.EMPTY.dim()),
-                            Span.styled(connectedRoute, 
Style.EMPTY.fg(Color.WHITE))));
+                if (!isBridge) {
+                    String connectedRoute = 
diagram.getConnectedRouteId(routeId);
+                    if (connectedRoute != null) {
+                        lines.add(Line.from(Span.raw("")));
+                        lines.add(Line.from(
+                                Span.styled(isInbound ? " To route: " : " From 
route: ", Style.EMPTY.dim()),
+                                Span.styled(connectedRoute, 
Style.EMPTY.fg(Color.WHITE))));
+                    }
                 }
                 if (topoNode.exchangesTotal > 0 || topoNode.exchangesFailed > 
0) {
                     lines.add(Line.from(Span.raw("")));
@@ -645,7 +650,7 @@ class DiagramTab implements MonitorTab {
             }
             hint(spans, "m", "metrics" + (diagramMetrics ? " [on]" : " 
[off]"));
             if (topologyMode) {
-                hint(spans, "e", "external" + (showExternal ? " [on]" : " 
[off]"));
+                hint(spans, "e", "external" + EXTERNAL_LABELS[externalMode]);
             }
             hint(spans, "n", "description" + (diagram.isShowDescription() ? " 
[on]" : " [off]"));
         }
@@ -675,7 +680,7 @@ class DiagramTab implements MonitorTab {
 
         String pid = ctx.selectedPid;
         boolean showMetrics = diagramMetrics;
-        boolean external = showExternal;
+        int external = externalMode;
 
         if (showPlaceholder) {
             diagram.setLoadingPlaceholder();
@@ -738,10 +743,15 @@ class DiagramTab implements MonitorTab {
 
                                 ## External Systems
 
-                                When external systems are enabled, the diagram 
shows a three-band layout:
-                                - **Top band** — external consumers sending 
messages INTO Camel
-                                - **Middle band** — the Camel routes and their 
internal connections
-                                - **Bottom band** — external producers where 
Camel sends messages OUT
+                                Press `e` to cycle through three external 
modes:
+
+                                - **off** — no external endpoints shown
+                                - **edges** — external endpoints that are 
truly outside Camel are shown
+                                  as dashed boxes in top/bottom bands. Routes 
sharing an external
+                                  endpoint (e.g. kafka) are connected with a 
direct arrow.
+                                - **all** — same as edges, but routes sharing 
an external endpoint
+                                  are connected through an intermediary dashed 
box showing the
+                                  endpoint name, instead of a direct arrow.
 
                                 External system boxes are drawn with dashed 
borders to distinguish
                                 them from route boxes. Dashed edges connect 
routes to external systems.
@@ -939,4 +949,199 @@ class DiagramTab implements MonitorTab {
         }
         return Math.max(1, String.valueOf(max).length());
     }
+
+    // ---- MCP programmatic navigation ----
+
+    boolean selectRoute(String routeId) {
+        int idx = diagram.findNodeIndexByRouteId(routeId);
+        if (idx < 0) {
+            return false;
+        }
+        diagram.setSelectedNodeIndex(idx);
+        diagram.scrollToSelectedNode();
+        return true;
+    }
+
+    boolean selectNode(String routeId, String nodeId) {
+        // Ensure we're on the Diagram tab in topology mode first
+        if (routeId != null) {
+            int routeIdx = diagram.findNodeIndexByRouteId(routeId);
+            if (routeIdx < 0) {
+                return false;
+            }
+            // Drill down into the route (mirrors Enter-key logic)
+            IntegrationInfo info = ctx.findSelectedIntegration();
+            if (info == null || info.routes.stream().noneMatch(r -> 
routeId.equals(r.routeId))) {
+                return false;
+            }
+            routeNavigationStack.clear();
+            drillDownRouteId = routeId;
+            topologyMode = false;
+            diagram.setTopologyMode(false);
+            diagram.selectFromNode(routeId);
+            diagram.resetScroll();
+            diagram.endLoad();
+            if (diagram.getRouteLayout(routeId) == null) {
+                reloadDiagram();
+            }
+        }
+        if (nodeId != null) {
+            int nodeIdx = diagram.findEipNodeIndexByNodeId(nodeId);
+            if (nodeIdx < 0) {
+                return false;
+            }
+            diagram.setSelectedEipNodeIndex(nodeIdx);
+            diagram.scrollToSelectedEipNode();
+        }
+        return true;
+    }
+
+    @Override
+    public SelectionContext getSelectionContext() {
+        if (!diagram.isShowDiagram()) {
+            return null;
+        }
+        if (topologyMode) {
+            var boxes = diagram.getNodeBoxes();
+            if (boxes.isEmpty()) {
+                return null;
+            }
+            List<String> items = new ArrayList<>();
+            for (var box : boxes) {
+                items.add(box.routeId());
+            }
+            return new SelectionContext(
+                    "topology-routes", items,
+                    diagram.getSelectedNodeIndex(), items.size(), "Topology 
routes");
+        } else {
+            var boxes = diagram.getEipNodeBoxes();
+            if (boxes.isEmpty()) {
+                return null;
+            }
+            List<String> items = new ArrayList<>();
+            for (var box : boxes) {
+                items.add(box.nodeId());
+            }
+            return new SelectionContext(
+                    "eip-nodes", items,
+                    diagram.getSelectedEipNodeIndex(), items.size(),
+                    "EIP nodes [" + drillDownRouteId + "]");
+        }
+    }
+
+    JsonObject getDiagramStateAsJson() {
+        if (!diagram.isShowDiagram()) {
+            return null;
+        }
+        JsonObject result = new JsonObject();
+        result.put("diagramMode", topologyMode ? "topology" : "route");
+
+        if (!topologyMode && drillDownRouteId != null) {
+            result.put("routeId", drillDownRouteId);
+            JsonArray stack = new JsonArray();
+            for (String s : routeNavigationStack) {
+                stack.add(s);
+            }
+            if (!stack.isEmpty()) {
+                result.put("navigationStack", stack);
+            }
+        }
+
+        // Selected node info
+        if (topologyMode) {
+            String routeId = diagram.getSelectedRouteId();
+            if (routeId != null) {
+                JsonObject node = new JsonObject();
+                node.put("routeId", routeId);
+                result.put("selectedRoute", node);
+            }
+        } else {
+            var eipBox = diagram.getSelectedEipNodeBox();
+            if (eipBox != null && eipBox.layoutNode() != null) {
+                JsonObject node = new JsonObject();
+                node.put("id", eipBox.nodeId());
+                node.put("type", eipBox.type());
+                String label = String.join("", 
eipBox.layoutNode().wrappedLines);
+                if (!label.isBlank()) {
+                    node.put("label", label);
+                }
+                if (eipBox.layoutNode().id != null) {
+                    node.put("processorId", eipBox.layoutNode().id);
+                }
+                String linkedRoute = 
diagram.findLinkedRouteId(drillDownRouteId);
+                if (linkedRoute != null) {
+                    node.put("linkedRoute", linkedRoute);
+                }
+                result.put("selectedNode", node);
+            }
+        }
+
+        // Info panel stats
+        IntegrationInfo info = ctx.findSelectedIntegration();
+        if (info != null) {
+            String routeId = topologyMode ? diagram.getSelectedRouteId() : 
drillDownRouteId;
+            if (routeId != null) {
+                RouteInfo route = null;
+                for (RouteInfo r : info.routes) {
+                    if (routeId.equals(r.routeId)) {
+                        route = r;
+                        break;
+                    }
+                }
+                if (route != null) {
+                    JsonObject ri = new JsonObject();
+                    ri.put("routeId", route.routeId);
+                    ri.put("from", route.from);
+                    ri.put("state", route.state);
+                    ri.put("uptime", route.uptime);
+                    ri.put("throughput", route.throughput);
+                    if (route.coverage != null) {
+                        ri.put("coverage", route.coverage);
+                    }
+                    ri.put("total", route.total);
+                    ri.put("failed", route.failed);
+                    ri.put("inflight", route.inflight);
+                    if (route.total > 0) {
+                        ri.put("meanTime", route.meanTime);
+                        ri.put("maxTime", route.maxTime);
+                        ri.put("minTime", route.minTime);
+                    }
+                    if (route.sinceLastCompleted != null) {
+                        ri.put("sinceLastSuccess", route.sinceLastCompleted);
+                    }
+                    if (route.sinceLastFailed != null) {
+                        ri.put("sinceLastFail", route.sinceLastFailed);
+                    }
+                    result.put("info", ri);
+                }
+            }
+
+            // EIP node stats (when drilled down)
+            if (!topologyMode) {
+                var eipBox = diagram.getSelectedEipNodeBox();
+                if (eipBox != null && eipBox.layoutNode() != null
+                        && eipBox.layoutNode().treeNode != null
+                        && eipBox.layoutNode().treeNode.info.stat != null) {
+                    var stat = eipBox.layoutNode().treeNode.info.stat;
+                    JsonObject ni = new JsonObject();
+                    ni.put("total", stat.exchangesTotal);
+                    ni.put("failed", stat.exchangesFailed);
+                    ni.put("inflight", stat.exchangesInflight);
+                    if (stat.exchangesTotal > 0) {
+                        ni.put("meanTime", stat.meanProcessingTime);
+                        ni.put("maxTime", stat.maxProcessingTime);
+                        ni.put("minTime", stat.minProcessingTime);
+                        ni.put("lastTime", stat.lastProcessingTime);
+                    }
+                    result.put("nodeInfo", ni);
+                }
+            }
+        }
+
+        return result;
+    }
+
+    JsonObject locateNodes(List<String> nodeIds) {
+        return diagram.locateNodes(nodeIds);
+    }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DrawOverlay.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DrawOverlay.java
index a8a45f7e1728..aeb651c92192 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DrawOverlay.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DrawOverlay.java
@@ -74,11 +74,106 @@ class DrawOverlay {
         for (DrawCell cell : cells) {
             if (cell.x >= 0 && cell.y >= 0
                     && cell.x < screenArea.width() && cell.y < 
screenArea.height()) {
-                buffer.setString(cell.x, cell.y, cell.symbol, cell.style);
+                if (cell.symbol == null) {
+                    // highlight mode: keep existing symbol, apply style as 
background
+                    var existing = buffer.get(cell.x, cell.y);
+                    if (existing != null && existing != 
dev.tamboui.buffer.Cell.CONTINUATION) {
+                        Style merged = 
existing.style().bg(cell.style.bg().orElse(Color.YELLOW));
+                        buffer.setString(cell.x, cell.y, existing.symbol(), 
merged);
+                    }
+                } else {
+                    buffer.setString(cell.x, cell.y, cell.symbol, cell.style);
+                }
             }
         }
     }
 
+    static List<DrawCell> generateShape(String shape, int x, int y, int width, 
int height, int length, Color color) {
+        return switch (shape) {
+            case "box" -> generateBox(x, y, width, height, color);
+            case "highlight" -> generateHighlight(x, y, width, height, color);
+            case "underline" -> generateUnderline(x, y, width, color);
+            case "arrow-down" -> generateArrow(x, y, length, 0, 1, color);
+            case "arrow-up" -> generateArrow(x, y, length, 0, -1, color);
+            case "arrow-right" -> generateArrow(x, y, length, 1, 0, color);
+            case "arrow-left" -> generateArrow(x, y, length, -1, 0, color);
+            default -> List.of();
+        };
+    }
+
+    private static List<DrawCell> generateBox(int x, int y, int w, int h, 
Color color) {
+        List<DrawCell> cells = new ArrayList<>();
+        Style s = Style.EMPTY.fg(color).bold();
+        cells.add(new DrawCell(x, y, "┌", s));
+        cells.add(new DrawCell(x + w - 1, y, "┐", s));
+        cells.add(new DrawCell(x, y + h - 1, "└", s));
+        cells.add(new DrawCell(x + w - 1, y + h - 1, "┘", s));
+        for (int i = 1; i < w - 1; i++) {
+            cells.add(new DrawCell(x + i, y, "─", s));
+            cells.add(new DrawCell(x + i, y + h - 1, "─", s));
+        }
+        for (int j = 1; j < h - 1; j++) {
+            cells.add(new DrawCell(x, y + j, "│", s));
+            cells.add(new DrawCell(x + w - 1, y + j, "│", s));
+        }
+        return cells;
+    }
+
+    private static List<DrawCell> generateHighlight(int x, int y, int w, int 
h, Color color) {
+        List<DrawCell> cells = new ArrayList<>();
+        Style s = Style.EMPTY.bg(color);
+        for (int row = 0; row < h; row++) {
+            for (int col = 0; col < w; col++) {
+                cells.add(new DrawCell(x + col, y + row, null, s));
+            }
+        }
+        return cells;
+    }
+
+    private static List<DrawCell> generateUnderline(int x, int y, int w, Color 
color) {
+        List<DrawCell> cells = new ArrayList<>();
+        Style s = Style.EMPTY.fg(color).bold();
+        for (int i = 0; i < w; i++) {
+            cells.add(new DrawCell(x + i, y, "─", s));
+        }
+        return cells;
+    }
+
+    private static List<DrawCell> generateArrow(int x, int y, int length, int 
dx, int dy, Color color) {
+        List<DrawCell> cells = new ArrayList<>();
+        Style s = Style.EMPTY.fg(color).bold();
+        String shaft = dy != 0 ? "│" : "─";
+        String head;
+        if (dx > 0) {
+            head = "▶";
+        } else if (dx < 0) {
+            head = "◀";
+        } else if (dy > 0) {
+            head = "▼";
+        } else {
+            head = "▲";
+        }
+        for (int i = 0; i < length - 1; i++) {
+            cells.add(new DrawCell(x + i * dx, y + i * dy, shaft, s));
+        }
+        cells.add(new DrawCell(x + (length - 1) * dx, y + (length - 1) * dy, 
head, s));
+        return cells;
+    }
+
+    static List<DrawCell> generateText(int x, int y, String text, Color color) 
{
+        List<DrawCell> cells = new ArrayList<>();
+        Style s = Style.EMPTY.fg(color).bold();
+        int col = x;
+        for (int i = 0; i < text.length();) {
+            int cp = text.codePointAt(i);
+            String ch = new String(Character.toChars(cp));
+            cells.add(new DrawCell(col, y, ch, s));
+            col += Math.max(1, dev.tamboui.text.CharWidth.of(cp));
+            i += Character.charCount(cp);
+        }
+        return cells;
+    }
+
     static Color parseColor(String name) {
         if (name == null || name.isBlank()) {
             return null;
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 c0adaf580826..806f071128ab 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
@@ -363,7 +363,8 @@ class ErrorsTab implements MonitorTab {
                         Constraint.fill())
                 .highlightStyle(Style.EMPTY.fg(Color.WHITE).bold().onBlue())
                 .highlightSpacing(Table.HighlightSpacing.ALWAYS)
-                .block(Block.builder().borderType(BorderType.ROUNDED).title(" 
Errors ").build())
+                .block(Block.builder().borderType(BorderType.ROUNDED)
+                        .title(" Errors (" + sorted.size() + ") sort:" + sort 
+ " ").build())
                 .build();
 
         frame.renderStatefulWidget(table, chunks.get(0), tableState);
@@ -416,10 +417,7 @@ class ErrorsTab implements MonitorTab {
         hint(spans, "s", "sort");
         hint(spans, "d", "diagram");
         hint(spans, "f", "handled [" + handledFilter + "]");
-        hint(spans, "p", "properties [" + (showProperties ? "on" : "off") + 
"]");
-        hint(spans, "v", "variables [" + (showVariables ? "on" : "off") + "]");
-        hint(spans, "h", "headers [" + (showHeaders ? "on" : "off") + "]");
-        hint(spans, "b", "body [" + (showBody ? "on" : "off") + "]");
+        hintShowBhpv(spans, showBody, showHeaders, showProperties, 
showVariables);
         hint(spans, "w", "wrap [" + (wordWrap ? "on" : "off") + "]");
     }
 
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FilesBrowser.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FilesBrowser.java
index bcdd965ee0b9..57605a020d9f 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FilesBrowser.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FilesBrowser.java
@@ -251,6 +251,30 @@ class FilesBrowser {
         return String.format("%.1f MB", bytes / (1024.0 * 1024));
     }
 
+    static String fileType(Path path) {
+        String name = path.getFileName().toString();
+        String lower = name.toLowerCase(Locale.ROOT);
+        if (lower.endsWith(".kamelet.yaml") || lower.endsWith(".kamelet.yml")) 
{
+            return "camel";
+        }
+        if (lower.endsWith(".yaml") || lower.endsWith(".yml")) {
+            return isCamelYaml(path) ? "camel" : "other";
+        }
+        if (lower.endsWith(".xml")) {
+            return isCamelXml(path) ? "camel" : "other";
+        }
+        if (lower.endsWith(".java")) {
+            return isCamelJava(path) ? "camel" : "java";
+        }
+        if (lower.endsWith(".properties") || lower.endsWith(".cfg")) {
+            return "config";
+        }
+        if (lower.startsWith("readme")) {
+            return "readme";
+        }
+        return "other";
+    }
+
     private static String fileEmoji(Path path) {
         String name = path.getFileName().toString();
         String lower = name.toLowerCase(Locale.ROOT);
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LogTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LogTab.java
index 28dec3649ef1..fd28125ec246 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LogTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LogTab.java
@@ -346,7 +346,7 @@ class LogTab implements MonitorTab {
         List<Line> visibleLines = allLines.subList(start, 
Math.min(allLines.size(), start + visibleHeight));
 
         int currentMatchLine = search.currentMatchLine();
-        if (currentMatchLine >= 0 || search.hasFindTerm()) {
+        if (currentMatchLine >= 0 || search.hasFindTerm() || 
search.hasHighlightTerm()) {
             List<Line> highlighted = new ArrayList<>(visibleLines.size());
             for (int i = 0; i < visibleLines.size(); i++) {
                 highlighted.add(search.applyHighlights(visibleLines.get(i), 
start + i, currentMatchLine));
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTab.java
index 49c3453de8df..88ab80b9d2c7 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTab.java
@@ -71,7 +71,8 @@ class RoutesTab implements MonitorTab {
     private final SourceViewer sourceViewer = new SourceViewer();
     private boolean diagramMetrics = true;
     private boolean showDescription;
-    private boolean showExternal;
+    private static final String[] EXTERNAL_LABELS = { " [off]", " [edges]", " 
[all]" };
+    private int externalMode;
     private boolean topologyMode = true;
     private String drillDownRouteId;
     private final Deque<String> routeNavigationStack = new ArrayDeque<>();
@@ -214,9 +215,9 @@ class RoutesTab implements MonitorTab {
             return true;
         }
 
-        // Toggle external systems (topology mode only)
+        // Cycle external systems: off → edges → all → off (topology mode only)
         if (diagram.isShowDiagram() && topologyMode && 
ke.isCharIgnoreCase('e')) {
-            showExternal = !showExternal;
+            externalMode = (externalMode + 1) % 3;
             diagram.endLoad();
             reloadDiagram();
             return true;
@@ -666,7 +667,7 @@ class RoutesTab implements MonitorTab {
             }
             hint(spans, "m", "metrics" + (diagramMetrics ? " [on]" : " 
[off]"));
             if (topologyMode) {
-                hint(spans, "e", "external" + (showExternal ? " [on]" : " 
[off]"));
+                hint(spans, "e", "external" + EXTERNAL_LABELS[externalMode]);
             }
             hint(spans, "n", "description" + (diagram.isShowDescription() ? " 
[on]" : " [off]"));
         } else {
@@ -1333,7 +1334,7 @@ class RoutesTab implements MonitorTab {
 
         String pid = ctx.selectedPid;
         boolean showMetrics = diagramMetrics;
-        boolean external = showExternal;
+        int external = externalMode;
 
         if (showPlaceholder) {
             diagram.setLoadingPlaceholder();
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SearchHighlighter.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SearchHighlighter.java
index 72a8d7379724..878d81b5f249 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SearchHighlighter.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SearchHighlighter.java
@@ -285,6 +285,10 @@ class SearchHighlighter {
         return findTerm != null;
     }
 
+    boolean hasHighlightTerm() {
+        return highlightTerm != null;
+    }
+
     void reset() {
         findInputActive = false;
         highlightInputActive = false;
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiCommand.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiCommand.java
index 28d64578814d..623720abfcbd 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiCommand.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiCommand.java
@@ -16,6 +16,9 @@
  */
 package org.apache.camel.dsl.jbang.core.commands.tui;
 
+import java.util.ArrayList;
+import java.util.List;
+
 import org.apache.camel.dsl.jbang.core.commands.CamelCommand;
 import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
 import picocli.CommandLine;
@@ -25,6 +28,28 @@ public class TuiCommand extends CamelCommand {
 
     private ClassLoader classLoader;
 
+    @CommandLine.Parameters(description = "Name or pid of running Camel 
integration", arity = "0..1")
+    String name;
+
+    @CommandLine.Option(names = { "--mcp" },
+                        description = "Enable embedded MCP server for AI agent 
access to the TUI")
+    boolean mcp;
+
+    @CommandLine.Option(names = { "--mcp-port" },
+                        description = "MCP server port (default: 
${DEFAULT-VALUE})",
+                        defaultValue = "8123")
+    int mcpPort = 8123;
+
+    @CommandLine.Option(names = { "--refresh" },
+                        description = "Refresh interval in milliseconds 
(default: ${DEFAULT-VALUE})",
+                        defaultValue = "100")
+    long refreshInterval = 100;
+
+    @CommandLine.Option(names = { "--record" },
+                        description = "Replay a .tape file inside the TUI and 
record to an Asciinema .cast file",
+                        arity = "0..1")
+    String record;
+
     public TuiCommand(CamelJBangMain main, ClassLoader classLoader) {
         super(main);
         this.classLoader = classLoader;
@@ -32,8 +57,26 @@ public class TuiCommand extends CamelCommand {
 
     @Override
     public Integer doCall() throws Exception {
-        // default to dashboard
+        List<String> args = new ArrayList<>();
+        if (name != null) {
+            args.add(name);
+        }
+        if (mcp) {
+            args.add("--mcp");
+        }
+        if (mcpPort != 8123) {
+            args.add("--mcp-port");
+            args.add(String.valueOf(mcpPort));
+        }
+        if (refreshInterval != 100) {
+            args.add("--refresh");
+            args.add(String.valueOf(refreshInterval));
+        }
+        if (record != null) {
+            args.add("--record");
+            args.add(record);
+        }
         CamelMonitor cmd = new CamelMonitor(getMain(), classLoader);
-        return new CommandLine(cmd).execute();
+        return new CommandLine(cmd).execute(args.toArray(String[]::new));
     }
 }
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 e0e90f8ed958..612be553db7e 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
@@ -32,6 +32,7 @@ import com.sun.net.httpserver.HttpExchange;
 import com.sun.net.httpserver.HttpServer;
 import dev.tamboui.buffer.Buffer;
 import dev.tamboui.export.ExportRequest;
+import dev.tamboui.style.Color;
 import org.apache.camel.util.json.JsonArray;
 import org.apache.camel.util.json.JsonObject;
 import org.apache.camel.util.json.Jsoner;
@@ -260,11 +261,18 @@ class TuiMcpServer {
         toolList.add(toolDef(
                 "tui_navigate",
                 "Navigates the TUI: switch tabs and/or select an integration. "
-                                + "Both parameters are optional — set 
whichever you want to change. "
-                                + "Tab names: Overview, Log, Routes, 
Consumers, Endpoints, HTTP, Health, Inspect, Circuit Breaker. "
+                                + "All parameters are optional — set whichever 
you want to change. "
+                                + "Tab names: Overview, Log, Diagram, Routes, 
Consumers, Endpoints, HTTP, Health, Inspect, Circuit Breaker. "
+                                + "Use 'route' to select a route in the 
Diagram topology, "
+                                + "and 'node' to drill down into a route and 
select a specific processor/EIP node. "
                                 + "Returns screen content and selection 
metadata after navigating.",
-                Map.of("tab", propDef("string", "Tab to switch to (e.g. 
'Routes', 'Health')"),
-                        "integration", propDef("string", "Integration name or 
PID to select"))));
+                Map.of("tab", propDef("string", "Tab to switch to (e.g. 
'Routes', 'Health', 'Diagram')"),
+                        "integration", propDef("string", "Integration name or 
PID to select"),
+                        "route", propDef("string",
+                                "Route ID to select in the Diagram tab 
topology (e.g. 'order-dispatcher')"),
+                        "node", propDef("string",
+                                "Processor/EIP node ID to select within a 
drilled-down route (e.g. 'multicast1'). "
+                                                  + "If 'route' is also 
provided, drills into that route first"))));
 
         toolList.add(toolDef(
                 "tui_send_keys",
@@ -347,6 +355,29 @@ class TuiMcpServer {
                                   + "The underlying content is unchanged since 
drawing is an overlay.",
                 Map.of()));
 
+        toolList.add(toolDef(
+                "tui_draw_shape",
+                "Draws a predefined shape on the TUI screen overlay. "
+                                  + "Much easier than constructing individual 
cells with tui_draw. "
+                                  + "Combine with tui_locate for precise 
positioning.",
+                Map.of("shape", propDef("string",
+                        "Shape to draw: box (rectangle border), highlight 
(background color on existing text like a marker pen), "
+                                                  + "underline (horizontal 
line), arrow-down, arrow-up, arrow-left, arrow-right, "
+                                                  + "text (draw text string at 
position)"),
+                        "x", propDef("integer", "X coordinate (column) of the 
shape origin"),
+                        "y", propDef("integer", "Y coordinate (row) of the 
shape origin"),
+                        "width", propDef("integer", "Width of the shape (for 
box, highlight, underline)"),
+                        "height", propDef("integer", "Height of the shape (for 
box, highlight). Defaults to 1."),
+                        "length", propDef("integer", "Length of arrows"),
+                        "text", propDef("string", "Text content to draw (for 
text shape)"),
+                        "color", propDef("string",
+                                "Color: red, green, blue, yellow, cyan, 
magenta, white, gray, black. Default: red for box/underline/arrow, yellow for 
highlight."),
+                        "duration",
+                        propDef("integer", "Auto-dismiss after this many 
seconds. If omitted, stays until cleared."),
+                        "append", propDef("boolean",
+                                "If true, add to existing drawing instead of 
replacing it. Default false.")),
+                List.of("shape", "x", "y")));
+
         // --- Structured data tools ---
 
         toolList.add(toolDef(
@@ -446,6 +477,39 @@ class TuiMcpServer {
                                   + "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."))));
+        toolList.add(toolDef(
+                "tui_control",
+                "Controls the selected integration: stop/start routes, 
restart, stop, or kill the process. "
+                               + "Actions: stop-routes (or pause) — suspend 
all routes; "
+                               + "start-routes (or resume) — resume all 
routes; "
+                               + "restart — gracefully restart the 
integration; "
+                               + "stop — gracefully stop the process; "
+                               + "kill — forcefully terminate the process.",
+                Map.of("action", propDef("string",
+                        "Control action: stop-routes, start-routes, pause, 
resume, restart, stop, or kill")),
+                List.of("action")));
+        toolList.add(toolDef(
+                "tui_get_files",
+                "Returns source files from the selected integration's 
directory. "
+                                 + "Without a file parameter, returns the list 
of files (name, size, type). "
+                                 + "With a file parameter, returns the file's 
content. "
+                                 + "Useful for reading route source code, 
configuration, and other integration files.",
+                Map.of("name", propDef("string",
+                        "Integration name. If omitted, uses the currently 
selected integration."),
+                        "file", propDef("string",
+                                "Filename to read. If omitted, returns the 
file list instead."))));
+        toolList.add(toolDef(
+                "tui_locate",
+                "Locates elements on the TUI screen and returns their exact 
screen coordinates (x, y, width, height). "
+                              + "Use 'text' to find text on screen with proper 
wide-character handling (emoji, CJK). "
+                              + "Use 'node' or 'nodes' to find diagram nodes 
by ID. "
+                              + "Returns coordinates suitable for tui_draw.",
+                Map.of("text", propDef("string",
+                        "Text to search for on screen. Returns all matches 
with screen coordinates."),
+                        "node", propDef("string",
+                                "Single diagram node ID to locate (routeId or 
nodeId)."),
+                        "nodes", propDef("array",
+                                "Array of diagram node IDs to locate. Returns 
individual rects plus combined bounds."))));
 
         JsonObject result = new JsonObject();
         result.put("tools", toolList);
@@ -492,6 +556,10 @@ class TuiMcpServer {
                 case "tui_filter" -> callFilter(args);
                 case "tui_toggle_trace_display" -> 
callToggleTraceDisplay(args);
                 case "tui_get_readme" -> callGetReadme(args);
+                case "tui_control" -> callControl(args);
+                case "tui_get_files" -> callGetFiles(args);
+                case "tui_locate" -> callLocate(args);
+                case "tui_draw_shape" -> callDrawShape(args);
                 default -> {
                     isError = true;
                     yield "Unknown tool: " + toolName;
@@ -533,6 +601,13 @@ class TuiMcpServer {
         }
     }
 
+    private void addFooterActions(JsonObject result) {
+        JsonArray actions = monitor.getFooterActionsAsJson();
+        if (actions != null && !actions.isEmpty()) {
+            result.put("actions", actions);
+        }
+    }
+
     private String callGetScreen(Map<String, Object> args) {
         Buffer buf = monitor.getLastBuffer();
         if (buf == null) {
@@ -593,6 +668,11 @@ class TuiMcpServer {
         result.put("keystrokesVisible", monitor.isKeystrokesVisible());
         result.put("captionVisible", monitor.isCaptionVisible());
         addSelectionContext(result);
+        addFooterActions(result);
+        JsonObject diagramState = monitor.getDiagramState();
+        if (diagramState != null) {
+            result.put("diagram", diagramState);
+        }
         return Jsoner.serialize(result);
     }
 
@@ -625,9 +705,11 @@ class TuiMcpServer {
         JsonObject result = new JsonObject();
         String tab = (String) args.get("tab");
         String integration = (String) args.get("integration");
+        String route = args.get("route") instanceof String s ? s : null;
+        String node = args.get("node") instanceof String s ? s : null;
 
-        if (tab == null && integration == null) {
-            result.put("error", "Provide at least one of: tab, integration");
+        if (tab == null && integration == null && route == null && node == 
null) {
+            result.put("error", "Provide at least one of: tab, integration, 
route, node");
             result.put("availableTabs", toJsonArray(monitor.getTabNames()));
             result.put("availableIntegrations", 
toJsonArray(monitor.getIntegrationNames()));
             return Jsoner.serialize(result);
@@ -661,6 +743,25 @@ class TuiMcpServer {
             }
         }
 
+        // Diagram route/node navigation (route selection in topology doesn't 
need render wait)
+        if (node == null && route != null) {
+            String selected = monitor.navigateDiagramToRoute(route);
+            if (selected != null) {
+                result.put("selectedRoute", route);
+            } else {
+                result.put("routeError", "Route not found in diagram: " + 
route);
+            }
+        }
+
+        // When drilling down with a node, we first drill into the route, then 
wait
+        // for render to populate the EIP node boxes, then select the node
+        if (node != null) {
+            // Drill into the route first (sets topologyMode=false)
+            if (route != null) {
+                monitor.navigateDiagramToNode(route, null);
+            }
+        }
+
         long beforeGen = monitor.getRenderGeneration();
         long deadline = System.currentTimeMillis() + 2000;
         while (System.currentTimeMillis() < deadline) {
@@ -674,11 +775,26 @@ class TuiMcpServer {
                 break;
             }
         }
+
+        // Now that the render has populated EIP node boxes, select the node
+        if (node != null) {
+            String selected = monitor.navigateDiagramToNode(null, node);
+            if (selected != null) {
+                result.put("selectedNode", node);
+                if (route != null) {
+                    result.put("drillDownRoute", route);
+                }
+            } else {
+                result.put("nodeError", "Node not found: " + node
+                                        + (route != null ? " in route " + 
route : ""));
+            }
+        }
         Buffer buf = monitor.getLastBuffer();
         if (buf != null) {
             result.put("screen", ExportRequest.export(buf).text().toString());
         }
         addSelectionContext(result);
+        addFooterActions(result);
         return Jsoner.serialize(result);
     }
 
@@ -742,6 +858,7 @@ class TuiMcpServer {
             result.put("screen", ExportRequest.export(buf).text().toString());
         }
         addSelectionContext(result);
+        addFooterActions(result);
         return Jsoner.serialize(result);
     }
 
@@ -1098,6 +1215,101 @@ class TuiMcpServer {
         return Jsoner.serialize(result);
     }
 
+    private String callControl(Map<String, Object> args) {
+        String action = (String) args.get("action");
+        if (action == null || action.isBlank()) {
+            return "Error: action is required";
+        }
+        return monitor.controlIntegration(action);
+    }
+
+    private String callGetFiles(Map<String, Object> args) {
+        String name = args.get("name") instanceof String s ? s : null;
+        String file = args.get("file") instanceof String s ? s : null;
+        JsonObject response = monitor.getFiles(name, file);
+        if (response == null) {
+            return name != null
+                    ? "No source files found for integration '" + name + "'"
+                    : "No source files found for the selected integration";
+        }
+        return Jsoner.serialize(response);
+    }
+
+    @SuppressWarnings("unchecked")
+    private String callLocate(Map<String, Object> args) {
+        String text = args.get("text") instanceof String s ? s : null;
+        String node = args.get("node") instanceof String s ? s : null;
+        List<String> nodes = args.get("nodes") instanceof List<?> list
+                ? ((List<Object>) list).stream().map(Object::toString).toList()
+                : null;
+
+        JsonObject result = new JsonObject();
+
+        if (text != null) {
+            JsonArray matches = monitor.locateText(text);
+            result.put("matches", matches);
+        } else if (node != null || nodes != null) {
+            List<String> ids = nodes != null ? nodes : List.of(node);
+            JsonObject located = monitor.locateNodes(ids);
+            if (located != null) {
+                result.put("matches", located.get("matches"));
+                if (located.containsKey("bounds")) {
+                    result.put("bounds", located.get("bounds"));
+                }
+            } else {
+                result.put("matches", new JsonArray());
+            }
+        } else {
+            result.put("error", "Provide 'text', 'node', or 'nodes' 
parameter");
+        }
+
+        return Jsoner.serialize(result);
+    }
+
+    private String callDrawShape(Map<String, Object> args) {
+        String shape = args.get("shape") instanceof String s ? s : null;
+        if (shape == null) {
+            return "Error: 'shape' is required";
+        }
+        int x = args.get("x") instanceof Number n ? n.intValue() : 0;
+        int y = args.get("y") instanceof Number n ? n.intValue() : 0;
+        int width = args.get("width") instanceof Number n ? n.intValue() : 0;
+        int height = args.get("height") instanceof Number n ? n.intValue() : 
Math.max(1, 0);
+        int length = args.get("length") instanceof Number n ? n.intValue() : 5;
+        String text = args.get("text") instanceof String s ? s : null;
+        String colorName = args.get("color") instanceof String s ? s : null;
+        int duration = args.get("duration") instanceof Number n ? n.intValue() 
: 0;
+
+        Color color = DrawOverlay.parseColor(colorName);
+        if (color == null) {
+            color = "highlight".equals(shape) ? Color.YELLOW : Color.RED;
+        }
+
+        if (height < 1) {
+            height = 1;
+        }
+
+        List<DrawOverlay.DrawCell> cells;
+        if ("text".equals(shape)) {
+            cells = DrawOverlay.generateText(x, y, text != null ? text : "", 
color);
+        } else {
+            cells = DrawOverlay.generateShape(shape, x, y, width, height, 
length, color);
+        }
+
+        if (cells.isEmpty() && !"text".equals(shape)) {
+            return "Unknown shape: " + shape
+                   + ". Use: box, highlight, underline, arrow-down, arrow-up, 
arrow-left, arrow-right, text";
+        }
+
+        boolean append = args.get("append") instanceof Boolean b && b;
+        if (append) {
+            monitor.appendDrawing(cells);
+        } else {
+            monitor.setDrawing(cells, duration);
+        }
+        return "Drew " + shape + " at (" + x + "," + y + ")";
+    }
+
     private static JsonArray toJsonArray(List<String> list) {
         JsonArray arr = new JsonArray();
         arr.addAll(list);
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/diagram/TopologyDiagramWidget.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/diagram/TopologyDiagramWidget.java
index d2187803f491..c53d2ec04205 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/diagram/TopologyDiagramWidget.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/diagram/TopologyDiagramWidget.java
@@ -388,7 +388,8 @@ public class TopologyDiagramWidget implements Widget {
     }
 
     private static boolean isExternal(TopologyLayoutNode node) {
-        return "external-in".equals(node.nodeType) || 
"external-out".equals(node.nodeType);
+        return "external-in".equals(node.nodeType) || 
"external-out".equals(node.nodeType)
+                || "external".equals(node.nodeType);
     }
 
     static List<String> wrapText(String text, int maxWidth) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/diagram/TopologyMinimapWidget.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/diagram/TopologyMinimapWidget.java
index f18ae2803244..0ebeeb90afd5 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/diagram/TopologyMinimapWidget.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/diagram/TopologyMinimapWidget.java
@@ -54,8 +54,10 @@ public class TopologyMinimapWidget implements Widget {
         }
 
         for (TopologyLayoutNode node : layout.nodes) {
+            if (node.nodeType != null && node.nodeType.startsWith("external")) 
{
+                continue;
+            }
             boolean isCurrent = node.routeId != null && 
node.routeId.equals(currentRouteId);
-            boolean isExternal = "external-in".equals(node.nodeType) || 
"external-out".equals(node.nodeType);
 
             int col = node.x * mapW / totalW;
             int row = node.y * mapH / totalH;
@@ -67,14 +69,7 @@ public class TopologyMinimapWidget implements Widget {
             col = Math.max(0, col);
             row = Math.max(0, row);
 
-            Style style;
-            if (isCurrent) {
-                style = CURRENT_STYLE;
-            } else if (isExternal) {
-                style = EXTERNAL_STYLE;
-            } else {
-                style = OTHER_STYLE;
-            }
+            Style style = isCurrent ? CURRENT_STYLE : OTHER_STYLE;
 
             drawMiniBox(buffer, area, col, row, nodeW, nodeH, style, 
isCurrent);
         }

Reply via email to