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 9cb00776336b CAMEL-26864: camel-jbang - tui_get_diagram returns the 
diagram of the Diagram tab as JSON (#29661)
9cb00776336b is described below

commit 9cb00776336bfb16bbf1f98dae224e50175c698d
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu Nov 12 07:38:28 2026 +0100

    CAMEL-26864: camel-jbang - tui_get_diagram returns the diagram of the 
Diagram tab as JSON (#29661)
    
    * CAMEL-26864: camel-jbang - tui_get_diagram returns the diagram of the 
Diagram tab as JSON
    
    It returned "No diagram available" since the text diagram was removed. It 
now returns what the tab draws, at its level: the groups of the architecture or 
the routes of the topology with their links, or the steps of a route with their 
parent and children in order. Each box says where it is drawn (row, col, 
height, width in characters), so an agent can sketch the diagram in any way it 
likes.
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    * CAMEL-26864: camel-jbang - a shorter description of tui_get_diagram, so 
the prompt of the AI panel stays within its budget with the other PRs of 
CAMEL-26864
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    ---------
    
    Signed-off-by: Claus Ibsen <[email protected]>
    Co-authored-by: Claude Opus 5.5 <[email protected]>
---
 .../jbang/core/commands/tui/ArchitectureView.java  |  34 +++++
 .../dsl/jbang/core/commands/tui/DiagramJson.java   | 164 +++++++++++++++++++++
 .../dsl/jbang/core/commands/tui/DiagramTab.java    |  22 ++-
 .../core/commands/tui/TuiToolDefinitions.java      |   4 +-
 .../jbang/core/commands/tui/TuiToolRegistry.java   |   2 +-
 .../jbang/core/commands/tui/DiagramJsonTest.java   | 131 ++++++++++++++++
 6 files changed, 353 insertions(+), 4 deletions(-)

diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ArchitectureView.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ArchitectureView.java
index 1cddd092eec4..d3bc17e738c4 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ArchitectureView.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ArchitectureView.java
@@ -58,6 +58,8 @@ import 
org.apache.camel.dsl.jbang.core.commands.ai.ProjectOverview;
 import org.apache.camel.dsl.jbang.core.commands.tui.diagram.RouteDensity;
 import 
org.apache.camel.dsl.jbang.core.commands.tui.diagram.TopologyDiagramWidget;
 import 
org.apache.camel.dsl.jbang.core.commands.tui.diagram.TopologyDiagramWidget.NodeLine;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
 
 import static org.apache.camel.dsl.jbang.core.commands.tui.TuiHelper.hint;
 
@@ -422,6 +424,38 @@ final class ArchitectureView {
 
     // ---- for the Diagram tab ----
 
+    /**
+     * The groups as the Diagram tab draws them, for tui_get_diagram: the 
boxes and links, each group with its name and
+     * routes. Null while the architecture is not shown.
+     */
+    JsonObject getDataAsJson() {
+        Capabilities caps = capabilities;
+        JsonObject json = active && !suspended ? DiagramJson.boxes(support) : 
null;
+        if (json == null || caps == null) {
+            return json;
+        }
+        for (Object o : (JsonArray) json.get("nodes")) {
+            JsonObject n = (JsonObject) o;
+            String id = (String) n.remove("routeId");
+            n.put("id", id);
+            Group g = caps.group(id);
+            if (g != null) {
+                n.put("name", g.name());
+                n.put("routes", new JsonArray(g.routes()));
+                if (!g.entryPoints().isEmpty()) {
+                    n.put("entryPoints", new JsonArray(g.entryPoints()));
+                }
+                if (!g.systems().isEmpty()) {
+                    n.put("systems", new JsonArray(g.systems()));
+                }
+                if (g.text() != null && !g.text().isBlank()) {
+                    n.put("text", g.text().strip());
+                }
+            }
+        }
+        return json;
+    }
+
     /** Whether the utility routes are shown; the topology follows the same 
setting. */
     boolean isShowUtility() {
         return showUtility;
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramJson.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramJson.java
new file mode 100644
index 000000000000..49435d9ee950
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramJson.java
@@ -0,0 +1,164 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.camel.diagram.RouteDiagramLayoutEngine.LayoutNode;
+import org.apache.camel.diagram.RouteDiagramLayoutEngine.LayoutRoute;
+import org.apache.camel.diagram.RouteDiagramLayoutEngine.NodeInfo;
+import org.apache.camel.diagram.RouteDiagramLayoutEngine.TreeNode;
+import org.apache.camel.diagram.TopologyAsciiRenderer;
+import 
org.apache.camel.dsl.jbang.core.commands.tui.diagram.RouteDiagramWidget.EipNodeBox;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+
+/**
+ * The diagram the Diagram tab draws, as data an agent can read or sketch in 
any way it likes (tui_get_diagram): its
+ * boxes, where each one is drawn (in characters, the top left box at 0,0) and 
how they connect.
+ */
+final class DiagramJson {
+
+    private DiagramJson() {
+    }
+
+    /**
+     * The boxes and links of a graph of boxes: the topology of the routes, or 
the architecture of their groups. Each
+     * node is as tui_get_topology has it, with where it is drawn.
+     */
+    static JsonObject boxes(DiagramSupport support) {
+        JsonObject json = support.getTopologyDataAsJson();
+        if (json == null) {
+            return null;
+        }
+        List<int[]> at = new ArrayList<>();
+        Map<String, int[]> byId = new HashMap<>();
+        for (TopologyAsciiRenderer.NodeBox b : support.getNodeBoxes()) {
+            int[] box = { b.startRow(), b.startCol(), b.endRow() - 
b.startRow() + 1, b.endCol() - b.startCol() + 1 };
+            at.add(box);
+            byId.putIfAbsent(b.routeId(), box);
+        }
+        int[] origin = origin(at);
+        for (Object o : (JsonArray) json.get("nodes")) {
+            JsonObject n = (JsonObject) o;
+            putBox(n, byId.get(n.getString("routeId")), origin);
+        }
+        return json;
+    }
+
+    /**
+     * The steps of a route, in the order they are drawn: each with its id, 
type and label, the step it is in
+     * ({@code parent}) and the steps in it in order ({@code children}), where 
it is drawn, and its numbers.
+     */
+    static JsonObject route(LayoutRoute route, List<EipNodeBox> drawn) {
+        if (route == null) {
+            return null;
+        }
+        List<int[]> at = new ArrayList<>();
+        Map<LayoutNode, int[]> byNode = new HashMap<>();
+        for (EipNodeBox b : drawn) {
+            int[] box = { b.startRow(), b.startCol(), b.endRow() - 
b.startRow() + 1, b.endCol() - b.startCol() + 1 };
+            at.add(box);
+            byNode.putIfAbsent(b.layoutNode(), box);
+        }
+        int[] origin = origin(at);
+        JsonArray nodes = new JsonArray();
+        for (LayoutNode ln : route.nodes) {
+            if ("route".equals(ln.type)) {
+                continue;
+            }
+            JsonObject n = new JsonObject();
+            n.put("id", ln.id);
+            n.put("type", ln.type);
+            String label = ln.wrappedLines != null ? String.join(" ", 
ln.wrappedLines).strip() : "";
+            if (!label.isEmpty()) {
+                n.put("label", label);
+            }
+            TreeNode tn = ln.treeNode;
+            if (tn != null) {
+                addStep(n, tn);
+            }
+            putBox(n, byNode.get(ln), origin);
+            nodes.add(n);
+        }
+        JsonObject json = new JsonObject();
+        json.put("routeId", route.routeId);
+        if (route.source != null) {
+            json.put("source", route.source);
+        }
+        json.put("nodes", nodes);
+        json.put("totalNodes", nodes.size());
+        return json;
+    }
+
+    private static void addStep(JsonObject n, TreeNode tn) {
+        NodeInfo info = tn.info;
+        if (tn.parent != null && tn.parent.info != null && 
!"route".equals(tn.parent.info.type)) {
+            n.put("parent", tn.parent.info.id);
+        }
+        if (!tn.children.isEmpty()) {
+            JsonArray children = new JsonArray();
+            tn.children.forEach(c -> children.add(c.info.id));
+            n.put("children", children);
+        }
+        if (info.uri != null) {
+            n.put("uri", info.uri);
+            n.put("remote", info.remote);
+        }
+        if (info.description != null) {
+            n.put("description", info.description);
+        }
+        if (info.line > 0) {
+            n.put("line", info.line);
+        }
+        if (info.stat != null) {
+            n.put("exchangesTotal", info.stat.exchangesTotal);
+            n.put("exchangesFailed", info.stat.exchangesFailed);
+            n.put("meanProcessingTime", info.stat.meanProcessingTime);
+        }
+        if (!info.bodyTypes.isEmpty()) {
+            n.put("bodyTypes", new JsonArray(info.bodyTypes));
+        }
+        if (!info.problems.isEmpty()) {
+            n.put("problems", new JsonArray(info.problems));
+        }
+    }
+
+    /** The top left of the drawing: the first row and column with a box. */
+    private static int[] origin(List<int[]> boxes) {
+        int row = Integer.MAX_VALUE;
+        int col = Integer.MAX_VALUE;
+        for (int[] b : boxes) {
+            row = Math.min(row, b[0]);
+            col = Math.min(col, b[1]);
+        }
+        return boxes.isEmpty() ? new int[] { 0, 0 } : new int[] { row, col };
+    }
+
+    /** Where a box is drawn: row and col from the top left box, and its size; 
nothing for a box not drawn (folded). */
+    private static void putBox(JsonObject n, int[] box, int[] origin) {
+        if (box != null) {
+            n.put("row", box[0] - origin[0]);
+            n.put("col", box[1] - origin[1]);
+            n.put("height", box[2]);
+            n.put("width", box[3]);
+        }
+    }
+}
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 44200a466296..8af2ca74aff3 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
@@ -19,6 +19,7 @@ package org.apache.camel.dsl.jbang.core.commands.tui;
 import java.util.ArrayList;
 import java.util.LinkedHashSet;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
 
@@ -761,7 +762,26 @@ class DiagramTab extends AbstractDiagramTab {
 
     @Override
     public JsonObject getTableDataAsJson() {
-        return null;
+        return getDiagramAsJson();
+    }
+
+    /**
+     * What the tab draws now, as data an agent can read or sketch 
(tui_get_diagram): the groups of the architecture,
+     * the topology of the routes, or the steps of a route, each box with 
where it is drawn.
+     */
+    JsonObject getDiagramAsJson() {
+        Level level = level();
+        JsonObject json = switch (level) {
+            case ARCHITECTURE -> architecture.getDataAsJson();
+            case TOPOLOGY -> diagram.isShowDiagram() ? 
DiagramJson.boxes(diagram) : null;
+            case ROUTE -> drillDownRouteId != null
+                    ? 
DiagramJson.route(diagram.getRouteLayout(drillDownRouteId), 
diagram.getEipNodeBoxes()) : null;
+        };
+        if (json != null) {
+            json.put("tab", "Diagram");
+            json.put("level", level.name().toLowerCase(Locale.ROOT));
+        }
+        return json;
     }
 
     JsonObject getTopologyDataAsJson() {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolDefinitions.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolDefinitions.java
index 465fb7370674..a1a6570bd2f7 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolDefinitions.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolDefinitions.java
@@ -366,8 +366,8 @@ final class TuiToolDefinitions {
                 List.of("theme")), r::callSetTheme));
         tools.add(tool(toolDef(
                 "tui_get_diagram",
-                "Returns the route topology diagram as text. "
-                                   + "Shows the ASCII/Unicode art diagram of 
routes and their connections.",
+                "The Diagram tab's diagram as JSON: level, boxes (row, col, 
height, width) and links, "
+                                   + "or a route's steps.",
                 Map.of()), args -> r.callGetDiagram(), READ_ONLY));
         tools.add(tool(toolDef(
                 "tui_get_history",
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java
index bd1277247f67..7817fd1cd633 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java
@@ -1223,7 +1223,7 @@ class TuiToolRegistry {
     String callGetDiagram() {
         JsonObject data = facade.getDiagramData();
         if (data == null) {
-            return "No diagram available. Navigate to the Diagram tab first.";
+            return "No diagram yet: the Diagram tab draws one when it is open 
(tui_navigate tab=Diagram).";
         }
         return Jsoner.serialize(data);
     }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramJsonTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramJsonTest.java
new file mode 100644
index 000000000000..96c5059903eb
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramJsonTest.java
@@ -0,0 +1,131 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
+
+import dev.tamboui.buffer.Buffer;
+import dev.tamboui.layout.Rect;
+import org.apache.camel.diagram.RouteDiagramLayoutEngine;
+import org.apache.camel.diagram.RouteDiagramLayoutEngine.LayoutRoute;
+import org.apache.camel.diagram.RouteDiagramLayoutEngine.NodeInfo;
+import org.apache.camel.diagram.RouteDiagramLayoutEngine.RouteInfo;
+import org.apache.camel.diagram.TopologyLayoutEngine.TopologyEdgeInfo;
+import org.apache.camel.diagram.TopologyLayoutEngine.TopologyNodeInfo;
+import org.apache.camel.dsl.jbang.core.commands.tui.diagram.RouteDiagramWidget;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * CAMEL-26864: tui_get_diagram returned "No diagram available" since the text 
diagram was removed. It returns the
+ * diagram the Diagram tab draws as JSON: the boxes, where each is drawn, and 
how they connect.
+ */
+class DiagramJsonTest {
+
+    @Test
+    void theTopologyHasItsBoxesAndWhereTheyAreDrawn() {
+        DiagramTab tab = tab();
+        TopologyEdgeInfo edge = new TopologyEdgeInfo();
+        edge.fromRouteId = "hello";
+        edge.toRouteId = "seven";
+        edge.endpoint = "direct://seven";
+        tab.diagram.showCustomTopology(List.of(node("hello"), node("seven")), 
List.of(edge), Map.of(), null);
+
+        JsonObject json = tab.getDiagramAsJson();
+
+        assertThat(json.getString("level")).isEqualTo("topology");
+        JsonArray nodes = (JsonArray) json.get("nodes");
+        assertThat(nodes).hasSize(2);
+        JsonObject hello = (JsonObject) nodes.stream().filter(n -> 
"hello".equals(((JsonObject) n).getString("routeId")))
+                .findFirst().orElseThrow();
+        JsonObject seven = (JsonObject) nodes.stream().filter(n -> 
"seven".equals(((JsonObject) n).getString("routeId")))
+                .findFirst().orElseThrow();
+        assertThat(hello.getInteger("width")).isPositive();
+        assertThat(hello.getInteger("height")).isPositive();
+        assertThat(seven.getInteger("row")).as("seven is drawn below hello, 
which sends to it")
+                .isGreaterThan(hello.getInteger("row"));
+        JsonObject e = (JsonObject) ((JsonArray) json.get("edges")).get(0);
+        assertThat(e.getString("from")).isEqualTo("hello");
+        assertThat(e.getString("to")).isEqualTo("seven");
+    }
+
+    @Test
+    void aRouteHasItsStepsInOrderWithTheirBranches() {
+        RouteInfo route = new RouteInfo();
+        route.routeId = "hello";
+        route.nodes.add(step("route", "hello", 0));
+        route.nodes.add(step("from", "hello", 0));
+        route.nodes.add(step("setBody", "setBody1", 1));
+        route.nodes.add(step("choice", "choice1", 1));
+        route.nodes.add(step("when", "when1", 2));
+        route.nodes.add(step("to", "to1", 3));
+        route.nodes.add(step("otherwise", "otherwise1", 2));
+        route.nodes.add(step("log", "log1", 3));
+        RouteDiagramLayoutEngine engine = new RouteDiagramLayoutEngine();
+        LayoutRoute layout = engine.layoutRoute(route, 0);
+        RouteDiagramWidget widget = new RouteDiagramWidget(layout, 
engine.getNodeWidth(), -1, 0, 0, false, Map.of());
+        widget.render(new Rect(0, 0, 200, 60), Buffer.empty(new Rect(0, 0, 
200, 60)));
+
+        JsonObject json = DiagramJson.route(layout, widget.getNodeBoxes());
+
+        assertThat(json.getString("routeId")).isEqualTo("hello");
+        JsonArray nodes = (JsonArray) json.get("nodes");
+        assertThat(nodes.stream().map(n -> ((JsonObject) 
n).getString("id")).toList())
+                .containsExactly("hello", "setBody1", "choice1", "when1", 
"to1", "otherwise1", "log1");
+        JsonObject from = (JsonObject) nodes.get(0);
+        assertThat(from.getString("type")).isEqualTo("from");
+        assertThat(from.getInteger("row")).isZero();
+        JsonObject choice = (JsonObject) nodes.get(2);
+        assertThat(((JsonArray) 
choice.get("children")).stream().map(Object::toString).toList())
+                .containsExactly("when1", "otherwise1");
+        JsonObject to = (JsonObject) nodes.get(4);
+        assertThat(to.getString("parent")).isEqualTo("when1");
+        
assertThat(to.getInteger("row")).isGreaterThan(choice.getInteger("row"));
+        assertThat(to.getInteger("width")).isPositive();
+    }
+
+    private static DiagramTab tab() {
+        IntegrationInfo info = new IntegrationInfo();
+        info.pid = "1234";
+        info.name = "hello";
+        MonitorContext ctx = new MonitorContext(new 
AtomicReference<>(List.of(info)), new AtomicReference<>(List.of()));
+        ctx.selectedPid = "1234";
+        return new DiagramTab(ctx);
+    }
+
+    private static TopologyNodeInfo node(String id) {
+        TopologyNodeInfo n = new TopologyNodeInfo();
+        n.routeId = id;
+        n.from = "";
+        n.nodeType = "route";
+        return n;
+    }
+
+    private static NodeInfo step(String type, String id, int level) {
+        NodeInfo n = new NodeInfo();
+        n.type = type;
+        n.id = id;
+        n.code = type;
+        n.level = level;
+        return n;
+    }
+}

Reply via email to