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

davsclaus pushed a commit to branch feature/tui-mcp-draw
in repository https://gitbox.apache.org/repos/asf/camel.git

commit b8d4214a3c16304e60b4eb8c4181c1001369f48f
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Jun 2 22:26:24 2026 +0200

    camel-jbang: Add tui_draw MCP tool for on-screen drawing overlay
    
    Add tui_draw and tui_draw_clear MCP tools that let AI agents draw
    characters at specific screen coordinates as an overlay on the TUI.
    Useful for highlighting areas, annotating the screen, or drawing
    emoji art. All cells sent in a single call to avoid chatty networking.
    Supports auto-dismiss via duration parameter.
    
    Co-Authored-By: Claude <[email protected]>
---
 .../dsl/jbang/core/commands/tui/CamelMonitor.java  |  21 ++++
 .../dsl/jbang/core/commands/tui/DrawOverlay.java   | 106 +++++++++++++++++++++
 .../dsl/jbang/core/commands/tui/TuiMcpServer.java  |  99 +++++++++++++++++++
 3 files changed, 226 insertions(+)

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 e22a456156f0..737b0dc326db 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
@@ -225,6 +225,7 @@ public class CamelMonitor extends CamelCommand {
     private final Queue<PendingKey> pendingKeys = new 
ConcurrentLinkedQueue<>();
     private final List<KeyRecord> recentKeys = new ArrayList<>();
     private final CaptionOverlay captionOverlay = new CaptionOverlay();
+    private final DrawOverlay drawOverlay = new DrawOverlay();
     private final HelpOverlay helpOverlay = new HelpOverlay();
 
     private final ActionsPopup actionsPopup = new ActionsPopup(
@@ -781,6 +782,7 @@ public class CamelMonitor extends CamelCommand {
                 return true;
             }
             actionsPopup.tick(now);
+            drawOverlay.tick(now);
             captionOverlay.tick(now);
             if (recording && !recentKeys.isEmpty()) {
                 long cutoff = now - 2000;
@@ -956,6 +958,9 @@ public class CamelMonitor extends CamelCommand {
         renderTabs(frame, mainChunks.get(2));
         // mainChunks.get(3) is the empty spacer row between tabs and content
         renderContent(frame, mainChunks.get(4));
+        if (drawOverlay.isVisible()) {
+            drawOverlay.render(frame, mainChunks.get(4));
+        }
         if (showKillConfirm) {
             renderKillConfirm(frame, mainChunks.get(4));
         }
@@ -2675,6 +2680,22 @@ public class CamelMonitor extends CamelCommand {
         captionOverlay.showCaption(text, durationSeconds);
     }
 
+    boolean isDrawVisible() {
+        return drawOverlay.isVisible();
+    }
+
+    void setDrawing(List<DrawOverlay.DrawCell> cells, int durationSeconds) {
+        drawOverlay.setDrawing(cells, durationSeconds);
+    }
+
+    void appendDrawing(List<DrawOverlay.DrawCell> cells) {
+        drawOverlay.appendDrawing(cells);
+    }
+
+    void clearDrawing() {
+        drawOverlay.clear();
+    }
+
     String navigateToTab(String tabName) {
         for (int i = 0; i < TAB_NAMES.length; i++) {
             if (TAB_NAMES[i].equalsIgnoreCase(tabName)) {
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
new file mode 100644
index 000000000000..a8a45f7e1728
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DrawOverlay.java
@@ -0,0 +1,106 @@
+/*
+ * 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.List;
+
+import dev.tamboui.buffer.Buffer;
+import dev.tamboui.layout.Rect;
+import dev.tamboui.style.Color;
+import dev.tamboui.style.Style;
+import dev.tamboui.terminal.Frame;
+
+class DrawOverlay {
+
+    record DrawCell(int x, int y, String symbol, Style style) {
+    }
+
+    private List<DrawCell> cells;
+    private long autoDismissTime;
+
+    boolean isVisible() {
+        return cells != null && !cells.isEmpty();
+    }
+
+    void setDrawing(List<DrawCell> newCells, int durationSeconds) {
+        this.cells = new ArrayList<>(newCells);
+        if (durationSeconds > 0) {
+            this.autoDismissTime = System.currentTimeMillis() + 
(durationSeconds * 1000L);
+        } else {
+            this.autoDismissTime = 0;
+        }
+    }
+
+    void appendDrawing(List<DrawCell> newCells) {
+        if (this.cells == null) {
+            this.cells = new ArrayList<>(newCells);
+        } else {
+            this.cells.addAll(newCells);
+        }
+    }
+
+    void clear() {
+        cells = null;
+        autoDismissTime = 0;
+    }
+
+    void tick(long now) {
+        if (autoDismissTime > 0 && now > autoDismissTime) {
+            clear();
+        }
+    }
+
+    void render(Frame frame, Rect area) {
+        if (cells == null || cells.isEmpty()) {
+            return;
+        }
+        Buffer buffer = frame.buffer();
+        Rect screenArea = buffer.area();
+        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);
+            }
+        }
+    }
+
+    static Color parseColor(String name) {
+        if (name == null || name.isBlank()) {
+            return null;
+        }
+        return switch (name.toLowerCase().trim()) {
+            case "red" -> Color.RED;
+            case "green" -> Color.GREEN;
+            case "blue" -> Color.BLUE;
+            case "yellow" -> Color.YELLOW;
+            case "cyan" -> Color.CYAN;
+            case "magenta" -> Color.MAGENTA;
+            case "white" -> Color.WHITE;
+            case "gray", "grey" -> Color.GRAY;
+            case "dark_gray", "dark_grey", "darkgray", "darkgrey" -> 
Color.DARK_GRAY;
+            case "light_red", "lightred" -> Color.LIGHT_RED;
+            case "light_green", "lightgreen" -> Color.LIGHT_GREEN;
+            case "light_blue", "lightblue" -> Color.LIGHT_BLUE;
+            case "light_yellow", "lightyellow" -> Color.LIGHT_YELLOW;
+            case "light_cyan", "lightcyan" -> Color.LIGHT_CYAN;
+            case "light_magenta", "lightmagenta" -> Color.LIGHT_MAGENTA;
+            case "black" -> Color.BLACK;
+            default -> null;
+        };
+    }
+}
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 87f38387ccea..a1dc64ae002f 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
@@ -318,6 +318,34 @@ class TuiMcpServer {
                 Map.of("seconds", propDef("integer",
                         "Number of seconds to sleep (1-30)")),
                 List.of("seconds")));
+        toolList.add(toolDef(
+                "tui_draw",
+                "Draws characters at specific screen coordinates as an overlay 
on top of the TUI. "
+                            + "Use this to highlight areas, annotate the 
screen for the human, "
+                            + "draw shapes, or create fun emoji art. "
+                            + "All cells are sent in a single call to avoid 
chatty networking. "
+                            + "Coordinates are 0-based and match the screen 
grid from tui_get_screen. "
+                            + "Characters can be any unicode including emoji. "
+                            + "The drawing overlays on top of existing content 
without modifying it. "
+                            + "Use with tui_show_caption to explain what you 
drew.",
+                Map.of("cells", propDef("array",
+                        "Array of cell objects to draw. Each cell has: "
+                                                 + "x (integer, column), y 
(integer, row), "
+                                                 + "char (string, character to 
draw), "
+                                                 + "fg (string, optional 
foreground color: red/green/blue/yellow/cyan/magenta/white/gray/black), "
+                                                 + "bg (string, optional 
background color, same values), "
+                                                 + "bold (boolean, optional)"),
+                        "duration", propDef("integer",
+                                "Auto-dismiss drawing after this many seconds. 
"
+                                                       + "If omitted, drawing 
stays until cleared with tui_draw_clear or replaced by another tui_draw call."),
+                        "append", propDef("boolean",
+                                "If true, add cells to the existing drawing 
instead of replacing it. Default false.")),
+                List.of("cells")));
+        toolList.add(toolDef(
+                "tui_draw_clear",
+                "Clears the drawing overlay and restores the screen to its 
normal state. "
+                                  + "The underlying content is unchanged since 
drawing is an overlay.",
+                Map.of()));
 
         JsonObject result = new JsonObject();
         result.put("tools", toolList);
@@ -350,6 +378,8 @@ class TuiMcpServer {
                 case "tui_tape_start" -> callTapeStart(args);
                 case "tui_tape_stop" -> callTapeStop(args);
                 case "tui_sleep" -> callSleep(args);
+                case "tui_draw" -> callDraw(args);
+                case "tui_draw_clear" -> callDrawClear();
                 default -> {
                     isError = true;
                     yield "Unknown tool: " + toolName;
@@ -745,6 +775,75 @@ class TuiMcpServer {
         return "Slept for " + seconds + "s";
     }
 
+    @SuppressWarnings("unchecked")
+    private String callDraw(Map<String, Object> args) {
+        Object cellsArg = args.get("cells");
+        if (!(cellsArg instanceof List)) {
+            return "Error: cells must be an array";
+        }
+        List<Object> cellsList = (List<Object>) cellsArg;
+        if (cellsList.isEmpty()) {
+            return "Error: cells array is empty";
+        }
+
+        List<DrawOverlay.DrawCell> drawCells = new ArrayList<>();
+        for (Object item : cellsList) {
+            if (!(item instanceof Map)) {
+                continue;
+            }
+            Map<String, Object> cell = (Map<String, Object>) item;
+
+            int x = cell.get("x") instanceof Number n ? n.intValue() : -1;
+            int y = cell.get("y") instanceof Number n ? n.intValue() : -1;
+            String ch = cell.get("char") instanceof String s ? s : " ";
+            if (x < 0 || y < 0) {
+                continue;
+            }
+
+            dev.tamboui.style.Style style = dev.tamboui.style.Style.EMPTY;
+            dev.tamboui.style.Color fg = DrawOverlay.parseColor(
+                    cell.get("fg") instanceof String s ? s : null);
+            dev.tamboui.style.Color bg = DrawOverlay.parseColor(
+                    cell.get("bg") instanceof String s ? s : null);
+            if (fg != null) {
+                style = style.fg(fg);
+            }
+            if (bg != null) {
+                style = style.bg(bg);
+            }
+            if (Boolean.TRUE.equals(cell.get("bold"))) {
+                style = style.bold();
+            }
+
+            drawCells.add(new DrawOverlay.DrawCell(x, y, ch, style));
+        }
+
+        if (drawCells.isEmpty()) {
+            return "Error: no valid cells in array";
+        }
+
+        boolean append = Boolean.TRUE.equals(args.get("append"));
+        int duration = 0;
+        if (args.get("duration") instanceof Number n) {
+            duration = n.intValue();
+        }
+
+        if (append) {
+            monitor.appendDrawing(drawCells);
+        } else {
+            monitor.setDrawing(drawCells, duration);
+        }
+
+        return "Drawing " + drawCells.size() + " cell(s)"
+               + (append ? " (appended)" : "")
+               + (duration > 0 ? ", auto-dismiss in " + duration + "s" : "");
+    }
+
+    private String callDrawClear() {
+        monitor.clearDrawing();
+        return "Drawing cleared";
+    }
+
     private static JsonArray toJsonArray(List<String> list) {
         JsonArray arr = new JsonArray();
         arr.addAll(list);

Reply via email to