You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by da...@apache.org on 2026/10/15 11:46:00 UTC

(camel) branch main updated (71f919ddac78 -> ce1a91857c3e)

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

davsclaus pushed a change to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


    from 71f919ddac78 chore(deps): Bump org.apache.fory:fory-core from 1.7.6 to 1.7.7
     new aa49df83f203 chore: camel-jbang - a selected box in the diagram has a solid frame
     new fa6f59a1afdb chore: camel-jbang - the F-key hints no longer split a field typed in the footer
     new ce1a91857c3e chore: camel-jbang - camel get endpoint and the Endpoint tab show the scope of an endpoint

The 3 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |   8 ++
 .../dsl/jbang/core/commands/ai/ToolConfirms.java   |  27 +----
 .../jbang/core/commands/process/ListEndpoint.java  |  11 +-
 .../camel/dsl/jbang/core/common/EndpointScope.java |  85 +++++++++++++
 .../core/commands/process/ListEndpointTest.java    |  66 ++++++++++
 .../dsl/jbang/core/common/EndpointScopeTest.java   |  53 ++++++++
 .../dsl/jbang/core/commands/tui/CamelMonitor.java  |   4 +-
 .../dsl/jbang/core/commands/tui/EndpointInfo.java  |   2 +
 .../dsl/jbang/core/commands/tui/EndpointsTab.java  |  19 ++-
 .../dsl/jbang/core/commands/tui/FooterHints.java   |  16 +++
 .../dsl/jbang/core/commands/tui/SourceTab.java     |  19 ++-
 .../dsl/jbang/core/commands/tui/StatusParser.java  |   3 +
 .../commands/tui/diagram/RouteDiagramWidget.java   |   5 +-
 .../tui/diagram/TopologyDiagramWidget.java         |   5 +-
 .../src/main/resources/tui/help/diagram.md         |  19 +--
 .../src/main/resources/tui/help/endpoints.md       |  12 +-
 .../core/commands/tui/EndpointsTabRenderTest.java  |  26 ++++
 .../jbang/core/commands/tui/FooterHintsTest.java   |  30 +++++
 .../core/commands/tui/SourceViewerEditTest.java    |  31 +++++
 .../jbang/core/commands/tui/StatusParserTest.java  |  23 ++++
 .../tui/diagram/DashedBoxSelectionTest.java        | 134 +++++++++++++++++++++
 21 files changed, 540 insertions(+), 58 deletions(-)
 create mode 100644 dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/EndpointScope.java
 create mode 100644 dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/EndpointScopeTest.java
 create mode 100644 dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/diagram/DashedBoxSelectionTest.java


(camel) 02/03: chore: camel-jbang - the F-key hints no longer split a field typed in the footer

Posted by da...@apache.org.
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

commit fa6f59a1afdbcfbbba42fa0401450ee565f166e1
Author: Claus Ibsen <cl...@gmail.com>
AuthorDate: Thu Oct 15 12:55:52 2026 +0200

    chore: camel-jbang - the F-key hints no longer split a field typed in the footer
    
    The F-keys every screen has (F2 actions, F10 run) go into a footer after
    the screen's first hint. While a term is typed (a table filter, find or
    highlight in the source viewer) the footer starts with the field instead,
    so they landed between the field and its own Enter and Esc, and inside
    the term when the caret was in its middle.
    
    While a screen takes typed text they now go after the field's keys. The
    Source tab also leaves Tab out of that footer, as it does the other keys
    of the viewer while a term is typed.
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <no...@anthropic.com>
    Signed-off-by: Claus Ibsen <cl...@gmail.com>
---
 .../dsl/jbang/core/commands/tui/CamelMonitor.java  |  4 +--
 .../dsl/jbang/core/commands/tui/FooterHints.java   | 16 +++++++++++
 .../dsl/jbang/core/commands/tui/SourceTab.java     | 19 +++++++------
 .../jbang/core/commands/tui/FooterHintsTest.java   | 30 +++++++++++++++++++++
 .../core/commands/tui/SourceViewerEditTest.java    | 31 ++++++++++++++++++++++
 5 files changed, 88 insertions(+), 12 deletions(-)

diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
index 13ee96c2dfc0..d3da8b8cdae6 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
@@ -2531,9 +2531,9 @@ public class CamelMonitor extends CamelCommand {
     private int fKeyStart;
 
     private int insertFKeyHints(List<Span> spans) {
-        int insertPos = Math.min(2, spans.size());
-        List<Span> fKeySpans = new ArrayList<>();
         MonitorTab tab = tabRegistry.activeTab();
+        int insertPos = FooterHints.fKeyPosition(spans, tab != null && tab.isTextInputActive());
+        List<Span> fKeySpans = new ArrayList<>();
         // F1 opens the help of every tab, but is seldom pressed: the Overview, the first screen, names it
         if (tab == tabRegistry.overviewTab() && tab.getHelpText() != null) {
             hint(fKeySpans, "F1", "help");
diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FooterHints.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FooterHints.java
index 2dd5a9ed9b14..5961975063c0 100644
--- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FooterHints.java
+++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FooterHints.java
@@ -30,6 +30,22 @@ final class FooterHints {
     private FooterHints() {
     }
 
+    /**
+     * Where the F-key hints go in a screen's footer: after its first hint, its most important one. While text is typed
+     * the footer is the field with its own keys (Enter, Esc): the F-keys go after those, as after the first two spans
+     * they would part the field from its keys, or cut it in two when the caret is inside it. The field's last key has
+     * no gap after it, so one is added.
+     */
+    static int fKeyPosition(List<Span> spans, boolean typing) {
+        if (!typing) {
+            return Math.min(2, spans.size());
+        }
+        if (!spans.isEmpty() && !spans.get(spans.size() - 1).content().endsWith("  ")) {
+            spans.add(Span.raw("  "));
+        }
+        return spans.size();
+    }
+
     /**
      * @param  spans       the footer: the screen's hints, with the F-key hints inserted at {@code fKeyStart}; the hints
      *                     dropped are removed from it
diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
index 1d0fbcdc8cd3..18c068bc338d 100644
--- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
+++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
@@ -469,17 +469,16 @@ class SourceTab extends AbstractTab {
         }
         if (focusOnViewer && sourceViewer.isVisible()) {
             sourceViewer.renderFooter(spans);
-            if (!sourceViewer.isEditMode()) {
+            // also after a jump, which moves the focus to the viewer; not while a term is typed into find or
+            // highlight, as the footer is then that field with its keys
+            if (!sourceViewer.isEditMode() && !sourceViewer.isTextInputActive()) {
                 TuiHelper.hint(spans, "Tab", "files");
-                // also after a jump, which moves the focus to the viewer; not while g is typed into search
-                if (!sourceViewer.isTextInputActive()) {
-                    if (!routeIndex.isEmpty()) {
-                        TuiHelper.hint(spans, "g", "go to route");
-                    }
-                    TuiHelper.hint(spans, "Ctrl+G", "go to");
-                    if (endpointOnSelectedLine() != null) {
-                        TuiHelper.hint(spans, "u", "usages");
-                    }
+                if (!routeIndex.isEmpty()) {
+                    TuiHelper.hint(spans, "g", "go to route");
+                }
+                TuiHelper.hint(spans, "Ctrl+G", "go to");
+                if (endpointOnSelectedLine() != null) {
+                    TuiHelper.hint(spans, "u", "usages");
                 }
             }
         } else {
diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/FooterHintsTest.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/FooterHintsTest.java
index e974a8dc180f..c453ffb7f3fe 100644
--- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/FooterHintsTest.java
+++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/FooterHintsTest.java
@@ -127,6 +127,36 @@ class FooterHintsTest {
         assertThat(keys(spans)).containsExactly("Enter", "Esc");
     }
 
+    @Test
+    void theFKeysGoAfterTheFirstHintOfAScreen() {
+        List<Span> spans = new ArrayList<>();
+        hint(spans, "Esc", "back");
+        hint(spans, "/", "find");
+
+        assertThat(FooterHints.fKeyPosition(spans, false)).isEqualTo(2);
+        assertThat(spans).hasSize(4);
+    }
+
+    @Test
+    void whileTextIsTypedTheFKeysGoAfterTheFieldAndItsKeys() {
+        // as a filter draws it with the caret inside the term: "ord|ers", where the caret splits the field in three
+        List<Span> spans = new ArrayList<>();
+        spans.add(Span.styled(" /", Theme.label().bold()));
+        spans.add(Span.raw("ord"));
+        spans.add(Span.styled("e", Style.EMPTY.reversed()));
+        spans.add(Span.raw("rs"));
+        spans.add(Span.raw("  "));
+        hint(spans, "Enter", "filter");
+        TuiHelper.hintLast(spans, "Esc", "cancel");
+
+        int position = FooterHints.fKeyPosition(spans, true);
+
+        // after Esc, which has no gap after it as the last key: one is added
+        assertThat(position).isEqualTo(spans.size());
+        assertThat(spans.get(position - 2).content()).isEqualTo(" cancel");
+        assertThat(spans.get(position - 1).content()).isEqualTo("  ");
+    }
+
     private static List<String> keys(List<Span> spans) {
         return spans.stream().filter(s -> Theme.hintKey().equals(s.style())).map(s -> s.content().strip()).toList();
     }
diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewerEditTest.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewerEditTest.java
index d3f92c2f3a3d..138f00b9f163 100644
--- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewerEditTest.java
+++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewerEditTest.java
@@ -456,6 +456,37 @@ class SourceViewerEditTest {
         assertThat(Files.readString(md, StandardCharsets.UTF_8)).endsWith("!");
     }
 
+    @Test
+    void sourceTabFooterIsTheFieldWhileATermIsTyped() {
+        IntegrationInfo info = new IntegrationInfo();
+        info.pid = "1234";
+        info.name = "test-app";
+        info.directory = tempDir.toString();
+        MonitorContext ctx = new MonitorContext(
+                new AtomicReference<>(List.of(info)), new AtomicReference<>(List.of()));
+        ctx.selectedPid = "1234";
+        SourceTab tab = new SourceTab(ctx);
+        tab.onTabSelected();
+
+        // the file opened, the viewer's footer offers Tab back to the files
+        assertThat(tab.handleKeyEvent(KeyEvent.ofKey(KeyCode.ENTER, KeyModifiers.NONE))).isTrue();
+        assertThat(footer(tab)).contains(" Tab ");
+
+        // h opens the highlight field: the footer is the field and its keys, not the viewer's navigation
+        tab.handleKeyEvent(KeyEvent.ofChar('h', KeyModifiers.NONE));
+        tab.handleKeyEvent(KeyEvent.ofChar('a', KeyModifiers.NONE));
+        assertThat(tab.isTextInputActive()).isTrue();
+        assertThat(footer(tab)).contains(" h:", "a█", " Enter ", " Esc ").doesNotContain(" Tab ");
+    }
+
+    private static String footer(SourceTab tab) {
+        List<Span> spans = new ArrayList<>();
+        tab.renderFooter(spans);
+        StringBuilder sb = new StringBuilder();
+        spans.forEach(s -> sb.append(s.content()));
+        return sb.toString();
+    }
+
     @Test
     void sourceTabForwardsKeysToViewerWhileEditing() throws Exception {
         IntegrationInfo info = new IntegrationInfo();


(camel) 01/03: chore: camel-jbang - a selected box in the diagram has a solid frame

Posted by da...@apache.org.
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

commit aa49df83f203b3e541dbfa649a889b86d980dab0
Author: Claus Ibsen <cl...@gmail.com>
AuthorDate: Thu Oct 15 12:47:03 2026 +0200

    chore: camel-jbang - a selected box in the diagram has a solid frame
    
    The box of a remote system is drawn dashed, in the route diagram as in the
    topology and the architecture view, and so is the error handling box.
    Selected, its frame took the selection colour like any other, and the
    dashes broke up on it: the vertical ones did not line up with the
    corners.
    
    A selected box now has a solid frame, and goes back to dashes when the
    cursor moves on; the system name in its border, its colour and the Info
    panel still say it is remote. The help of the Diagram tab says the
    edges of the integration are dashed.
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <no...@anthropic.com>
    Signed-off-by: Claus Ibsen <cl...@gmail.com>
---
 .../commands/tui/diagram/RouteDiagramWidget.java   |   5 +-
 .../tui/diagram/TopologyDiagramWidget.java         |   5 +-
 .../src/main/resources/tui/help/diagram.md         |  19 +--
 .../tui/diagram/DashedBoxSelectionTest.java        | 134 +++++++++++++++++++++
 4 files changed, 150 insertions(+), 13 deletions(-)

diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/diagram/RouteDiagramWidget.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/diagram/RouteDiagramWidget.java
index cd6e0156e356..8574a4cd1215 100644
--- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/diagram/RouteDiagramWidget.java
+++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/diagram/RouteDiagramWidget.java
@@ -347,8 +347,9 @@ public class RouteDiagramWidget implements Widget {
             borderStyle = borderStyle.patch(selectionStyle());
         }
 
-        char hChar = external ? DASH_H : H;
-        char vChar = external ? DASH_V : V;
+        // a selected box has a solid frame like any other: dashes on the selection colour break up, the vertical ones most
+        char hChar = external && !selected ? DASH_H : H;
+        char vChar = external && !selected ? DASH_V : V;
 
         // Top border
         setChar(buffer, area, row, col, TL, borderStyle);
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 1a5e5fc36616..2cd851d5cd3e 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
@@ -409,8 +409,9 @@ public class TopologyDiagramWidget implements Widget {
 
         boolean highlighted = !ext && node.routeId != null && highlightRouteIds.contains(node.routeId);
 
-        char hChar = dashedBox ? DASH_H : H;
-        char vChar = dashedBox ? DASH_V : V;
+        // a selected box has a solid frame like any other: dashes on the selection colour break up, the vertical ones most
+        char hChar = dashedBox && !selected ? DASH_H : H;
+        char vChar = dashedBox && !selected ? DASH_V : V;
         Style borderStyle;
         if (highlighted) {
             Color hlColor = highlightFailed ? highlightFailColor() : highlightOkColor();
diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/diagram.md b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/diagram.md
index a37b69d387d6..fc89f6e876ed 100644
--- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/diagram.md
+++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/diagram.md
@@ -122,15 +122,16 @@ business view (`b`), marked with `✦`; the technical view (`n`)
 keeps the expression. The Info panel of such a node shows the
 label, why the route decides there, and the code.
 
-The endpoints of remote systems are the edges of the integration
-and have their own color, in the route diagram as in the topology.
-The input of a route from a remote system shows where it comes
-from in the top border of its box, `AMQP ──▶`. Where a message is
-sent to a remote system and no route of the integration consumes
-it, the bottom border names where it leaves to: `──▶ AMQP`. A
-hand-off to another route of the integration shows `↵ route`
-beside the box. A route that starts itself on a schedule (timer,
-cron, quartz, scheduler) shows `↻ ──▶` at its input.
+The endpoints of remote systems are the edges of the integration:
+they have their own color and a dashed frame (solid while
+selected), in the route diagram as in the topology. The input of
+a route from a remote system shows where it comes from in the top
+border of its box, `AMQP ──▶`. Where a message is sent to a remote
+system and no route of the integration consumes it, the bottom
+border names where it leaves to: `──▶ AMQP`. A hand-off to another
+route of the integration shows `↵ route` beside the box. A route
+that starts itself on a schedule (timer, cron, quartz, scheduler)
+shows `↻ ──▶` at its input.
 
 ## Route Diagram
 
diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/diagram/DashedBoxSelectionTest.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/diagram/DashedBoxSelectionTest.java
new file mode 100644
index 000000000000..79209fcdb096
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/diagram/DashedBoxSelectionTest.java
@@ -0,0 +1,134 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui.diagram;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import dev.tamboui.buffer.Buffer;
+import dev.tamboui.layout.Rect;
+import org.apache.camel.diagram.RouteDiagramLayoutEngine;
+import org.apache.camel.diagram.TopologyLayoutEngine;
+import org.apache.camel.diagram.TopologyLayoutEngine.TopologyEdgeInfo;
+import org.apache.camel.diagram.TopologyLayoutEngine.TopologyLayoutResult;
+import org.apache.camel.diagram.TopologyLayoutEngine.TopologyNodeInfo;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * The box of a remote system is drawn dashed, in the route diagram as in the topology, but a selected box has a solid
+ * frame like any other: dashes on the selection colour break up.
+ */
+class DashedBoxSelectionTest {
+
+    @Test
+    void aRemoteEndpointInTheRouteDiagramIsSolidWhileSelected() {
+        RouteDiagramLayoutEngine.RouteInfo route = new RouteDiagramLayoutEngine.RouteInfo();
+        route.routeId = "checkout";
+        route.nodes.add(node("from", "checkout", "from[file:orders]", "file:orders", 0, true));
+        route.nodes.add(node("to", "to1", "to[direct:charge]", "direct:charge", 1, false));
+        RouteDiagramLayoutEngine.LayoutRoute layout = new RouteDiagramLayoutEngine().layoutRoute(route, 0);
+        int nodeWidth = RouteDiagramLayoutEngine.DEFAULT_BOX_WIDTH * RouteDiagramLayoutEngine.SCALE;
+
+        // nothing selected: the file box is dashed, the box of the local step solid
+        RouteDiagramWidget widget = new RouteDiagramWidget(layout, nodeWidth, -1, 0, 0, false, Map.of(), false, Map.of());
+        Buffer buffer = render(widget.getTotalCols(), widget.getTotalRows(), (area, b) -> widget.render(area, b));
+        RouteDiagramWidget.EipNodeBox file = widget.getNodeBoxes().get(0);
+        RouteDiagramWidget.EipNodeBox to = widget.getNodeBoxes().get(1);
+        assertThat(frame(buffer, file.startRow(), file.endRow(), file.startCol(), file.endCol())).isEqualTo("┆┄");
+        assertThat(frame(buffer, to.startRow(), to.endRow(), to.startCol(), to.endCol())).isEqualTo("│─");
+
+        // the file box selected: solid
+        RouteDiagramWidget selected = new RouteDiagramWidget(layout, nodeWidth, 0, 0, 0, false, Map.of(), false, Map.of());
+        buffer = render(selected.getTotalCols(), selected.getTotalRows(), (area, b) -> selected.render(area, b));
+        file = selected.getNodeBoxes().get(0);
+        assertThat(frame(buffer, file.startRow(), file.endRow(), file.startCol(), file.endCol())).isEqualTo("│─");
+    }
+
+    @Test
+    void anExternalSystemInTheTopologyIsSolidWhileSelected() {
+        TopologyNodeInfo checkout = new TopologyNodeInfo();
+        checkout.routeId = "checkout";
+        checkout.from = "file:orders";
+        checkout.nodeType = "route";
+        TopologyNodeInfo files = new TopologyNodeInfo();
+        files.routeId = "in-checkout-0";
+        files.from = "file:orders";
+        files.nodeType = "external-in";
+        TopologyEdgeInfo reads = new TopologyEdgeInfo();
+        reads.fromRouteId = "in-checkout-0";
+        reads.toRouteId = "checkout";
+        reads.endpoint = "file:orders";
+        reads.connectionType = "external";
+        TopologyLayoutEngine engine = new TopologyLayoutEngine();
+        TopologyLayoutResult layout = engine.layout(
+                new ArrayList<>(List.of(checkout, files)), new ArrayList<>(List.of(reads)));
+
+        TopologyDiagramWidget widget = new TopologyDiagramWidget(layout, engine.getNodeWidth(), -1, 0, 0, false, false);
+        Buffer buffer = render(widget.getTotalCols(), widget.getTotalRows(), (area, b) -> widget.render(area, b));
+        int index = indexOf(widget.getNodeBoxes(), "in-checkout-0");
+        TopologyDiagramWidget.NodeBox box = widget.getNodeBoxes().get(index);
+        assertThat(frame(buffer, box.startRow(), box.endRow(), box.startCol(), box.endCol())).isEqualTo("┆┄");
+
+        TopologyDiagramWidget selected
+                = new TopologyDiagramWidget(layout, engine.getNodeWidth(), index, 0, 0, false, false);
+        buffer = render(selected.getTotalCols(), selected.getTotalRows(), (area, b) -> selected.render(area, b));
+        box = selected.getNodeBoxes().get(index);
+        assertThat(frame(buffer, box.startRow(), box.endRow(), box.startCol(), box.endCol())).isEqualTo("│─");
+    }
+
+    private interface Renderer {
+        void render(Rect area, Buffer buffer);
+    }
+
+    private static Buffer render(int cols, int rows, Renderer renderer) {
+        Rect area = new Rect(0, 0, Math.max(100, cols), Math.max(20, rows));
+        Buffer buffer = Buffer.empty(area);
+        renderer.render(area, buffer);
+        return buffer;
+    }
+
+    /** The left side of a box below its top corner, then the top side right of its corner. */
+    private static String frame(Buffer buffer, int top, int bottom, int left, int right) {
+        assertThat(bottom).isGreaterThan(top + 1);
+        assertThat(right).isGreaterThan(left + 1);
+        return buffer.get(left, top + 1).symbol() + buffer.get(right - 1, top).symbol();
+    }
+
+    private static int indexOf(List<TopologyDiagramWidget.NodeBox> boxes, String routeId) {
+        for (int i = 0; i < boxes.size(); i++) {
+            if (routeId.equals(boxes.get(i).routeId())) {
+                return i;
+            }
+        }
+        throw new AssertionError("no box for " + routeId + " in " + boxes);
+    }
+
+    private static RouteDiagramLayoutEngine.NodeInfo node(
+            String type, String id, String code, String uri, int level, boolean remote) {
+        RouteDiagramLayoutEngine.NodeInfo n = new RouteDiagramLayoutEngine.NodeInfo();
+        n.type = type;
+        n.id = id;
+        n.code = code;
+        n.uri = uri;
+        n.level = level;
+        n.remote = remote;
+        return n;
+    }
+}


(camel) 03/03: chore: camel-jbang - camel get endpoint and the Endpoint tab show the scope of an endpoint

Posted by da...@apache.org.
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

commit ce1a91857c3e0a30e96e9fce106b2057e7768c6a
Author: Claus Ibsen <cl...@gmail.com>
AuthorDate: Thu Oct 15 13:18:08 2026 +0200

    chore: camel-jbang - camel get endpoint and the Endpoint tab show the scope of an endpoint
    
    Besides remote, a component can now be marked internal (CAMEL-25652):
    Camel plumbing such as direct, seda, log, timer and mock that stays inside
    the integration. That makes three kinds of endpoint, which exclude each
    other: remote (an external system), internal, and local (neither, such as
    exec, bean or class, which can run code or a process on this host).
    
    camel get endpoint and the Endpoint tab show them in a SCOPE column
    instead of REMOTE; the JSON of camel get endpoint keeps remote and adds
    internal, and tui_get_table adds scope. Remote comes from the runtime,
    internal from the catalog by the scheme of the URI, through the new
    EndpointScope, which the tool confirms of camel ask and the TUI now use
    as well. A component the catalog does not know is never internal.
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <no...@anthropic.com>
    Signed-off-by: Claus Ibsen <cl...@gmail.com>
---
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |  8 ++
 .../dsl/jbang/core/commands/ai/ToolConfirms.java   | 27 ++-----
 .../jbang/core/commands/process/ListEndpoint.java  | 11 ++-
 .../camel/dsl/jbang/core/common/EndpointScope.java | 85 ++++++++++++++++++++++
 .../core/commands/process/ListEndpointTest.java    | 66 +++++++++++++++++
 .../dsl/jbang/core/common/EndpointScopeTest.java   | 53 ++++++++++++++
 .../dsl/jbang/core/commands/tui/EndpointInfo.java  |  2 +
 .../dsl/jbang/core/commands/tui/EndpointsTab.java  | 19 ++++-
 .../dsl/jbang/core/commands/tui/StatusParser.java  |  3 +
 .../src/main/resources/tui/help/endpoints.md       | 12 +--
 .../core/commands/tui/EndpointsTabRenderTest.java  | 26 +++++++
 .../jbang/core/commands/tui/StatusParserTest.java  | 23 ++++++
 12 files changed, 302 insertions(+), 33 deletions(-)

diff --git a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index 147092569b76..5d1d302fc766 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -159,6 +159,14 @@ local model gets a core set of tools (`--tools=auto`, the default), a hosted mod
 of `--max-iterations` is now 25 instead of 10, and `--model llama3.2` now selects that model also when a
 model better at tool calling, such as `qwen3.6`, is installed.
 
+=== camel-jbang - camel get endpoint shows the scope of an endpoint
+
+`camel get endpoint` replaces the `REMOTE` column with `SCOPE`, which tells three kinds of endpoint apart:
+`remote` (an external system, as before), `internal` (Camel plumbing that stays inside the integration, such as
+`direct`, `seda` or `log`, as the catalog marks its component internal) and `local` (neither, such as `exec` or
+`bean`). Scripts that read the `REMOTE` column of the table must read `SCOPE` instead. The `--json` output keeps
+`remote` and adds `internal`.
+
 === camel-jpa, camel-hazelcast - an exchange marked rollback only is not consumed
 
 When a route marks the exchange rollback only (for example with `markRollbackOnly()` or `markRollbackOnlyLast()`)
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolConfirms.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolConfirms.java
index 2d56e29b9de6..019b506a08c2 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolConfirms.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolConfirms.java
@@ -30,9 +30,7 @@ import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
 
-import org.apache.camel.catalog.CamelCatalog;
-import org.apache.camel.catalog.DefaultCamelCatalog;
-import org.apache.camel.tooling.model.ComponentModel;
+import org.apache.camel.dsl.jbang.core.common.EndpointScope;
 import org.apache.camel.util.URISupport;
 
 /**
@@ -66,27 +64,12 @@ public final class ToolConfirms {
     }
 
     /**
-     * Whether a component stays inside the integration, as the catalog marks it internal: its routes, in-memory state
-     * and log, and no file, network, process, or code or resource named in its URI, by any option. This is narrower
-     * than remote=false, which exec, class, bean, language and the components that load a template or a stylesheet by
-     * URI are too. The catalog is the CLI's own, so it has the flag; a component it does not know (a custom one, or one
-     * newer than the CLI) is not internal.
+     * Whether a component stays inside the integration, as the catalog marks it internal ({@link EndpointScope}): a
+     * message to it needs no confirm. This is narrower than remote=false, which exec, class, bean, language and the
+     * components that load a template or a stylesheet by URI are too.
      */
     public static boolean isInternal(String scheme) {
-        if (!scheme.matches("[a-z0-9-]+")) {
-            return false;
-        }
-        try {
-            ComponentModel model = Catalog.INSTANCE.componentModel(scheme);
-            return model != null && model.isInternal();
-        } catch (RuntimeException e) {
-            return false;
-        }
-    }
-
-    // loaded on the first send, not when a call only needs the SQL or language checks
-    private static final class Catalog {
-        static final CamelCatalog INSTANCE = new DefaultCamelCatalog(true);
+        return EndpointScope.isInternal(scheme);
     }
 
     /**
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListEndpoint.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListEndpoint.java
index b50d83401420..ebf926f93dec 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListEndpoint.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListEndpoint.java
@@ -28,6 +28,7 @@ import com.github.freva.asciitable.Column;
 import com.github.freva.asciitable.HorizontalAlign;
 import com.github.freva.asciitable.OverflowBehaviour;
 import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.apache.camel.dsl.jbang.core.common.EndpointScope;
 import org.apache.camel.dsl.jbang.core.common.ProcessHelper;
 import org.apache.camel.dsl.jbang.core.common.TerminalWidthHelper;
 import org.apache.camel.support.PatternHelper;
@@ -127,6 +128,7 @@ public class ListEndpoint extends ProcessWatchCommand {
                                 row.endpoint = o.getString("uri");
                                 row.stub = o.getBooleanOrDefault("stub", false);
                                 row.remote = o.getBooleanOrDefault("remote", true);
+                                row.scope = EndpointScope.of(row.endpoint, row.remote);
                                 row.direction = o.getString("direction");
                                 row.total = o.getString("hits");
                                 row.meanBodySize = o.getLongOrDefault("meanBodySize", -1L);
@@ -190,6 +192,7 @@ public class ListEndpoint extends ProcessWatchCommand {
                 jo.put("total", r.total);
                 jo.put("stub", r.stub);
                 jo.put("remote", r.remote);
+                jo.put("internal", EndpointScope.INTERNAL.equals(r.scope));
                 jo.put("uri", r.endpoint);
                 if (r.meanBodySize >= 0) {
                     jo.put("minBodySize", r.minBodySize);
@@ -207,8 +210,8 @@ public class ListEndpoint extends ProcessWatchCommand {
         }
         boolean hasSize = rows.stream().anyMatch(r -> r.meanBodySize >= 0 || r.meanHeadersSize >= 0);
         // Flexible column: URI (90/140)
-        // Fixed columns: PID(8)+NAME(30)+AGE(8)+DIR(3)+TOTAL(5)+STUB(4)+REMOTE(6) ~= 64
-        int fixedW = 64;
+        // Fixed columns: PID(8)+NAME(30)+AGE(8)+DIR(3)+TOTAL(5)+STUB(4)+SCOPE(8) ~= 66
+        int fixedW = 66;
         if (hasSize) {
             // BODY-SIZE(9)+HDR-SIZE(8) = 17
             fixedW += 17;
@@ -229,7 +232,7 @@ public class ListEndpoint extends ProcessWatchCommand {
                 new Column().header("DIR").with(r -> r.direction),
                 new Column().header("TOTAL").with(r -> r.total),
                 new Column().header("STUB").dataAlign(HorizontalAlign.CENTER).with(r -> r.stub ? "x" : ""),
-                new Column().header("REMOTE").dataAlign(HorizontalAlign.CENTER).with(r -> r.remote ? "x" : ""),
+                new Column().header("SCOPE").dataAlign(HorizontalAlign.LEFT).with(r -> r.scope),
                 new Column().header("BODY-SIZE").visible(hasSize).dataAlign(HorizontalAlign.RIGHT)
                         .with(r -> sizeToString(r.meanBodySize)),
                 new Column().header("HDR-SIZE").visible(hasSize).dataAlign(HorizontalAlign.RIGHT)
@@ -307,6 +310,8 @@ public class ListEndpoint extends ProcessWatchCommand {
         String total;
         boolean stub;
         boolean remote;
+        /** remote, internal or local ({@link EndpointScope}) */
+        String scope;
         long meanBodySize = -1;
         long meanHeadersSize = -1;
         long minBodySize = -1;
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/EndpointScope.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/EndpointScope.java
new file mode 100644
index 000000000000..60479ced1ad8
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/EndpointScope.java
@@ -0,0 +1,85 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.common;
+
+import org.apache.camel.catalog.CamelCatalog;
+import org.apache.camel.catalog.DefaultCamelCatalog;
+import org.apache.camel.tooling.model.ComponentModel;
+
+/**
+ * How far an endpoint reaches, as {@code camel get endpoint} and the Endpoint tab show it: {@link #REMOTE}, an external
+ * system; {@link #INTERNAL}, Camel plumbing that stays inside the integration; or {@link #LOCAL}, neither. The three
+ * exclude each other, as an internal component is never remote.
+ */
+public final class EndpointScope {
+
+    /** An external system, over the network or a file system: the endpoint says so at runtime. */
+    public static final String REMOTE = "remote";
+    /** Stays inside the integration (direct, seda, log, mock): the catalog marks its component internal. */
+    public static final String INTERNAL = "internal";
+    /** Neither: the component can run code or a process, or read a file or a resource, on this host. */
+    public static final String LOCAL = "local";
+
+    private EndpointScope() {
+    }
+
+    /**
+     * The scope of an endpoint.
+     *
+     * @param uri    the endpoint URI
+     * @param remote whether the endpoint is remote, as the runtime reports it
+     */
+    public static String of(String uri, boolean remote) {
+        if (remote) {
+            return REMOTE;
+        }
+        return isInternal(scheme(uri)) ? INTERNAL : LOCAL;
+    }
+
+    /** The component of an endpoint URI: what comes before its first colon. */
+    public static String scheme(String uri) {
+        if (uri == null) {
+            return null;
+        }
+        int colon = uri.indexOf(':');
+        return colon > 0 ? uri.substring(0, colon) : uri;
+    }
+
+    /**
+     * Whether a component stays inside the integration, as the catalog marks it internal: its routes, in-memory state
+     * and log, and no file, network, process, or code or resource named in its URI, by any option. This is narrower
+     * than remote=false, which exec, class, bean, language and the components that load a template or a stylesheet by
+     * URI are too. The catalog is the CLI's own, so it has the flag; a component it does not know (a custom one, or one
+     * newer than the CLI) is not internal.
+     */
+    public static boolean isInternal(String scheme) {
+        if (scheme == null || !scheme.matches("[a-z0-9-]+")) {
+            return false;
+        }
+        try {
+            ComponentModel model = Catalog.INSTANCE.componentModel(scheme);
+            return model != null && model.isInternal();
+        } catch (RuntimeException e) {
+            return false;
+        }
+    }
+
+    // loaded when a scope is first asked for, not when the CLI starts
+    private static final class Catalog {
+        static final CamelCatalog INSTANCE = new DefaultCamelCatalog(true);
+    }
+}
diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListEndpointTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListEndpointTest.java
index e2605a1340f3..c5d57b255db7 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListEndpointTest.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListEndpointTest.java
@@ -22,6 +22,7 @@ import java.util.stream.Stream;
 import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
 import org.apache.camel.util.json.JsonArray;
 import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.mockito.MockedStatic;
@@ -132,6 +133,71 @@ class ListEndpointTest extends ProcessCommandTestSupport {
         }
     }
 
+    @Test
+    void testScopeTellsRemoteInternalAndLocalApart() throws Exception {
+        JsonObject root = buildEndpointStatus("myApp",
+                endpoint("kafka:orders", "in", "3", true),
+                endpoint("log:payment", "out", "3", false),
+                endpoint("exec:wc", "out", "1", false));
+        writeStatusFile(TEST_PID, root);
+
+        ListEndpoint command = new ListEndpoint(new CamelJBangMain().withPrinter(printer));
+        command.sort = "pid";
+
+        try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+            ProcessHandle ph = mockProcessHandle(TEST_PID);
+            ProcessHandle currentHandle = mockCurrentHandle();
+            mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+            mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> Stream.of(ph));
+
+            assertEquals(0, command.doCall());
+            String output = printer.getOutput();
+            assertTrue(output.contains("SCOPE"), output);
+            assertFalse(output.contains("REMOTE"), output);
+            assertTrue(lineOf(output, "kafka:orders").contains("remote"), output);
+            assertTrue(lineOf(output, "log:payment").contains("internal"), output);
+            assertTrue(lineOf(output, "exec:wc").contains("local"), output);
+        }
+    }
+
+    @Test
+    void testJsonOutputKeepsRemoteAndAddsInternal() throws Exception {
+        JsonObject root = buildEndpointStatus("myApp",
+                endpoint("log:payment", "out", "3", false),
+                endpoint("exec:wc", "out", "1", false));
+        writeStatusFile(TEST_PID, root);
+
+        ListEndpoint command = new ListEndpoint(new CamelJBangMain().withPrinter(printer));
+        command.sort = "pid";
+        command.jsonOutput = true;
+
+        try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+            ProcessHandle ph = mockProcessHandle(TEST_PID);
+            ProcessHandle currentHandle = mockCurrentHandle();
+            mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+            mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> Stream.of(ph));
+
+            assertEquals(0, command.doCall());
+            JsonArray rows = (JsonArray) Jsoner.deserialize(printer.getOutput().trim());
+            JsonObject log = (JsonObject) rows.get(0);
+            JsonObject exec = (JsonObject) rows.get(1);
+            assertEquals("log:payment", log.getString("uri"));
+            assertEquals(false, log.get("remote"));
+            assertEquals(true, log.get("internal"));
+            assertEquals(false, exec.get("internal"));
+        }
+    }
+
+    private static String lineOf(String output, String uri) {
+        return output.lines().filter(l -> l.contains(uri)).findFirst().orElse("");
+    }
+
+    private static JsonObject endpoint(String uri, String direction, String hits, boolean remote) {
+        JsonObject ep = endpoint(uri, direction, hits);
+        ep.put("remote", remote);
+        return ep;
+    }
+
     private static JsonObject buildEndpointStatus(String contextName, JsonObject... endpoints) {
         JsonArray epArr = new JsonArray();
         Collections.addAll(epArr, endpoints);
diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/EndpointScopeTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/EndpointScopeTest.java
new file mode 100644
index 000000000000..636a132548f7
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/EndpointScopeTest.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.common;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class EndpointScopeTest {
+
+    @Test
+    void remoteAsTheRuntimeSaysElseInternalOrLocalByTheCatalog() {
+        assertThat(EndpointScope.of("kafka://orders", true)).isEqualTo(EndpointScope.REMOTE);
+        assertThat(EndpointScope.of("file://orders?noop=true", true)).isEqualTo(EndpointScope.REMOTE);
+        // Camel plumbing that stays inside the integration
+        assertThat(EndpointScope.of("direct://charge", false)).isEqualTo(EndpointScope.INTERNAL);
+        assertThat(EndpointScope.of("log:payment", false)).isEqualTo(EndpointScope.INTERNAL);
+        assertThat(EndpointScope.of("timer://tick?period=1000", false)).isEqualTo(EndpointScope.INTERNAL);
+        // not remote, yet it runs a process or code on this host
+        assertThat(EndpointScope.of("exec://wc", false)).isEqualTo(EndpointScope.LOCAL);
+        assertThat(EndpointScope.of("bean:greeter", false)).isEqualTo(EndpointScope.LOCAL);
+    }
+
+    @Test
+    void aComponentTheCatalogDoesNotKnowIsNeverInternal() {
+        assertThat(EndpointScope.of("my-custom://thing", false)).isEqualTo(EndpointScope.LOCAL);
+        assertThat(EndpointScope.of(null, false)).isEqualTo(EndpointScope.LOCAL);
+        assertThat(EndpointScope.isInternal("Direct")).isFalse();
+        assertThat(EndpointScope.isInternal("../direct")).isFalse();
+    }
+
+    @Test
+    void theSchemeIsWhatComesBeforeTheFirstColon() {
+        assertThat(EndpointScope.scheme("seda://queue?size=10")).isEqualTo("seda");
+        assertThat(EndpointScope.scheme("log:info")).isEqualTo("log");
+        assertThat(EndpointScope.scheme("direct")).isEqualTo("direct");
+        assertThat(EndpointScope.scheme(null)).isNull();
+    }
+}
diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointInfo.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointInfo.java
index 21bef41c3e1c..214e2b6a6034 100644
--- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointInfo.java
+++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointInfo.java
@@ -24,6 +24,8 @@ class EndpointInfo {
     long hits;
     boolean stub;
     boolean remote;
+    /** remote, internal or local, as EndpointScope tells them apart */
+    String scope;
     long minBodySize = -1;
     long maxBodySize = -1;
     long meanBodySize = -1;
diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java
index 4b64ba726e07..c2a5483c2a0a 100644
--- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java
+++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java
@@ -44,6 +44,7 @@ import dev.tamboui.widgets.table.Cell;
 import dev.tamboui.widgets.table.Row;
 import dev.tamboui.widgets.table.Table;
 import org.apache.camel.catalog.CamelCatalog;
+import org.apache.camel.dsl.jbang.core.common.EndpointScope;
 import org.apache.camel.tooling.model.BaseOptionModel;
 import org.apache.camel.tooling.model.ComponentModel;
 import org.apache.camel.util.json.JsonArray;
@@ -255,7 +256,7 @@ class EndpointsTab extends AbstractTableTab {
                 cells.add(rightCell(FlowHelper.sizeToString(ep.meanHeadersSize), 10));
             }
             cells.add(centerCell(ep.stub ? TuiIcons.CHECK : "", 6));
-            cells.add(centerCell(ep.remote ? TuiIcons.CHECK : "", 8));
+            cells.add(scopeCell(ep.scope));
             cells.add(Cell.from(ep.uri != null ? TuiHelper.displayUri(ep.uri) : ""));
             rows.add(Row.from(cells));
         }
@@ -275,7 +276,7 @@ class EndpointsTab extends AbstractTableTab {
             headerCells.add(rightCell(sortLabel("HDR", "hdr"), 10, sortStyle("hdr")));
         }
         headerCells.add(centerCell("STUB", 6, Style.EMPTY.bold()));
-        headerCells.add(centerCell("REMOTE", 8, Style.EMPTY.bold()));
+        headerCells.add(Cell.from(Span.styled("SCOPE", Style.EMPTY.bold())));
         headerCells.add(Cell.from(Span.styled(sortLabel("URI", "uri"), sortStyle("uri"))));
 
         List<Constraint> widths = new ArrayList<>();
@@ -747,6 +748,19 @@ class EndpointsTab extends AbstractTableTab {
         return DocHelper.loadHelpText("endpoints");
     }
 
+    /** The scope of an endpoint: remote in the colour the diagrams draw the edges of the integration in. */
+    private static Cell scopeCell(String scope) {
+        if (scope == null) {
+            return Cell.from("");
+        }
+        Style style = switch (scope) {
+            case EndpointScope.REMOTE -> Style.EMPTY.fg(Theme.diagramExternal());
+            case EndpointScope.INTERNAL -> Theme.muted();
+            default -> Style.EMPTY;
+        };
+        return Cell.from(Span.styled(scope, style));
+    }
+
     @Override
     public JsonObject getTableDataAsJson() {
         IntegrationInfo info = ctx.findSelectedIntegration();
@@ -764,6 +778,7 @@ class EndpointsTab extends AbstractTableTab {
             row.put("routeId", ei.routeId);
             row.put("hits", ei.hits);
             row.put("remote", ei.remote);
+            row.put("scope", ei.scope);
             row.put("stub", ei.stub);
             if (ei.meanBodySize >= 0) {
                 row.put("meanBodySize", ei.meanBodySize);
diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParser.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParser.java
index 777a76008d72..bd714eeee0fd 100644
--- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParser.java
+++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParser.java
@@ -28,6 +28,7 @@ import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
 
+import org.apache.camel.dsl.jbang.core.common.EndpointScope;
 import org.apache.camel.dsl.jbang.core.common.ProcessHelper;
 import org.apache.camel.dsl.jbang.core.common.VersionHelper;
 import org.apache.camel.util.TimeUtils;
@@ -562,6 +563,8 @@ final class StatusParser {
                         int idx = ep.uri.indexOf(':');
                         ep.component = idx > 0 ? ep.uri.substring(0, idx) : ep.uri;
                     }
+                    // here on the refresh thread, as the first look at the catalog loads it
+                    ep.scope = EndpointScope.of(ep.uri, ep.remote);
                     info.endpoints.add(ep);
                 }
             }
diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/endpoints.md b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/endpoints.md
index 35ea3cffc357..68d24c3f8440 100644
--- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/endpoints.md
+++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/endpoints.md
@@ -14,17 +14,17 @@ identifies both the component and the specific resource.
 - **BODY** — Average message body size in bytes passing through this endpoint. Helps identify which endpoints handle large payloads
 - **HDR** — Average message headers size in bytes. Large headers may indicate excessive metadata being passed
 - **STUB** — Marked `x` if the endpoint has been replaced by a stub component (useful during testing to avoid connecting to real external systems)
-- **REMOTE** — Marked `x` if the endpoint connects to an external system outside the JVM (e.g., `kafka`, `http`, `ftp`). Local endpoints like `seda`, `direct`, `log` are not remote
+- **SCOPE** — How far the endpoint reaches: `remote`, an external system outside the JVM (e.g., `kafka`, `http`, `ftp`, `file`); `internal`, Camel plumbing that stays inside the integration (`direct`, `seda`, `log`, `timer`, `mock`); or `local`, neither: it can run code or a process, or read a file or a resource, on this host (`exec`, `bean`, `class`, `language`, the template components)
 - **URI** — Full endpoint URI with parameters (e.g., `timer://hello?period=2000`)
 
 ## Example Screen
 
 ```
- COMPONENT  ROUTE          DIR  TOTAL  BODY  HDR  STUB  REMOTE  URI
- timer      timer-to-log   in   21     0     0                  timer://hello?period=2000
- timer      timer-to-seda  in   14     0     0                  timer://pump?period=3000
- seda       seda-consumer  in   14     14    0                  seda://queue
- seda       timer-to-seda  out  14     14    0                  seda://queue
+ COMPONENT  ROUTE          DIR  TOTAL  BODY  HDR  STUB  SCOPE     URI
+ timer      timer-to-log   in   21     0     0           internal  timer://hello?period=2000
+ timer      timer-to-seda  in   14     0     0           internal  timer://pump?period=3000
+ seda       seda-consumer  in   14     14    0           internal  seda://queue
+ seda       timer-to-seda  out  14     14    0           internal  seda://queue
 ```
 
 Notice that `seda://queue` appears twice — once as `in` (the
diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTabRenderTest.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTabRenderTest.java
index 5c99b27db1a6..5355400b4c7c 100644
--- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTabRenderTest.java
+++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTabRenderTest.java
@@ -27,9 +27,12 @@ import dev.tamboui.terminal.Frame;
 import dev.tamboui.text.Span;
 import dev.tamboui.tui.event.KeyEvent;
 import dev.tamboui.tui.event.KeyModifiers;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
+import static org.assertj.core.api.Assertions.assertThat;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 /**
@@ -204,6 +207,29 @@ class EndpointsTabRenderTest {
         assertTrue(bar.views().stream().anyMatch(v -> v.label().equals("Detail")), "View bar should list Detail");
     }
 
+    @Test
+    void theScopeColumnTellsRemoteInternalAndLocalApart() {
+        addEndpoint("kafka", "kafka://orders", "in", "intake", 3).scope = "remote";
+        addEndpoint("direct", "direct://charge", "out", "intake", 3).scope = "internal";
+        addEndpoint("exec", "exec://wc", "out", "report", 1).scope = "local";
+
+        EndpointsTab tab = new EndpointsTab(ctx, new MetricsCollector());
+        String rendered = TuiTestHelper.renderToString(tab, 140, 30);
+
+        assertThat(rendered).contains("SCOPE").doesNotContain("REMOTE");
+        assertThat(line(rendered, "kafka://orders")).contains("remote");
+        assertThat(line(rendered, "direct://charge")).contains("internal");
+        assertThat(line(rendered, "exec://wc")).contains("local");
+        // and for the tui_get_table tool
+        JsonArray rows = (JsonArray) tab.getTableDataAsJson().get("rows");
+        assertThat(rows).extracting(r -> ((JsonObject) r).getString("scope"))
+                .containsExactlyInAnyOrder("remote", "internal", "local");
+    }
+
+    private static String line(String rendered, String uri) {
+        return rendered.lines().filter(l -> l.contains(uri)).findFirst().orElse("");
+    }
+
     // ---- Helper methods ----
 
     private EndpointInfo addEndpoint(String component, String uri, String direction, String routeId, long hits) {
diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParserTest.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParserTest.java
index d2b290c910e5..fd1177b48885 100644
--- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParserTest.java
+++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParserTest.java
@@ -17,6 +17,7 @@
 package org.apache.camel.dsl.jbang.core.commands.tui;
 
 import java.util.LinkedHashMap;
+import java.util.List;
 import java.util.Map;
 
 import org.apache.camel.util.json.JsonArray;
@@ -52,6 +53,28 @@ class StatusParserTest {
         assertEquals(5, info.state);
     }
 
+    @Test
+    void anEndpointGetsItsScopeFromTheRemoteFlagAndTheCatalog() {
+        JsonArray endpoints = new JsonArray();
+        for (String[] e : new String[][] {
+                { "kafka://orders", "true" }, { "log://payment", "false" },
+                { "exec://wc", "false" } }) {
+            JsonObject ep = new JsonObject();
+            ep.put("uri", e[0]);
+            ep.put("remote", Boolean.parseBoolean(e[1]));
+            endpoints.add(ep);
+        }
+        JsonObject holder = new JsonObject();
+        holder.put("endpoints", endpoints);
+        JsonObject root = new JsonObject();
+        root.put("context", new JsonObject());
+        root.put("endpoints", holder);
+
+        IntegrationInfo info = StatusParser.parseIntegration(ProcessHandle.current(), root);
+
+        assertEquals(List.of("remote", "internal", "local"), info.endpoints.stream().map(ep -> ep.scope).toList());
+    }
+
     @Test
     void aFailedReloadIsReadAsCamelPsReadsIt() {
         JsonObject root = new JsonObject();