This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch tui-mcp-CAMEL-23606 in repository https://gitbox.apache.org/repos/asf/camel.git
commit 4cb4ccd0c1396bf5dc22d964a5f57e3b8ea241d8 Author: Claus Ibsen <[email protected]> AuthorDate: Sun May 24 19:40:19 2026 +0200 CAMEL-23606: camel-tui - MCP key injection tool for AI-driven demo recording Co-Authored-By: Claude Opus 4.6 <[email protected]> --- .../dsl/jbang/core/commands/tui/ActionsPopup.java | 3 +- .../dsl/jbang/core/commands/tui/CamelMonitor.java | 90 ++++++++++++++++++++++ .../dsl/jbang/core/commands/tui/TuiMcpServer.java | 33 ++++++++ 3 files changed, 125 insertions(+), 1 deletion(-) diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java index bfce21ee00ef..e567ae83972c 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java @@ -700,7 +700,8 @@ class ActionsPopup { + "| `tui_get_events` | Returns recent key presses and navigation events |\n" + "| `tui_get_state` | Returns active tab, selected integration, etc. |\n" + "| `tui_show_caption` | Shows a message on the TUI screen |\n" - + "| `tui_navigate` | Switch tabs and select integrations |\n\n" + + "| `tui_navigate` | Switch tabs and select integrations |\n" + + "| `tui_send_keys` | Send key presses to control the TUI |\n\n" + "## Setup for Claude Code\n\n" + "Run this command to connect Claude Code to the TUI:\n\n" + " claude mcp add --transport http camel-tui " + url + "\n\n" 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 ce9cdb0e5248..c0ae386a3002 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 @@ -35,8 +35,10 @@ import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Optional; +import java.util.Queue; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; @@ -58,6 +60,7 @@ import dev.tamboui.tui.TuiRunner; import dev.tamboui.tui.event.Event; import dev.tamboui.tui.event.KeyCode; import dev.tamboui.tui.event.KeyEvent; +import dev.tamboui.tui.event.KeyModifiers; import dev.tamboui.tui.event.TickEvent; import dev.tamboui.widgets.Clear; import dev.tamboui.widgets.barchart.Bar; @@ -212,6 +215,7 @@ public class CamelMonitor extends CamelCommand { private boolean recording; private TuiEventLog eventLog; private TuiMcpServer mcpServer; + private final Queue<PendingKey> pendingKeys = new ConcurrentLinkedQueue<>(); private final List<KeyRecord> recentKeys = new ArrayList<>(); private final CaptionOverlay captionOverlay = new CaptionOverlay(); @@ -588,6 +592,12 @@ public class CamelMonitor extends CamelCommand { } if (event instanceof TickEvent) { long now = System.currentTimeMillis(); + PendingKey pk = pendingKeys.peek(); + if (pk != null && now >= pk.fireAt()) { + pendingKeys.poll(); + handleEvent(pk.event(), runner); + return true; + } actionsPopup.tick(now); captionOverlay.tick(now); if (recording && !recentKeys.isEmpty()) { @@ -3097,4 +3107,84 @@ public class CamelMonitor extends CamelCommand { .toList(); } + int injectKeys(List<String> keys, int delayMs) { + long fireAt = System.currentTimeMillis(); + int count = 0; + for (String key : keys) { + KeyEvent ke = parseKey(key); + if (ke != null) { + pendingKeys.add(new PendingKey(ke, fireAt)); + fireAt += delayMs; + count++; + } + } + return count; + } + + static KeyEvent parseKey(String key) { + if (key == null || key.isEmpty()) { + return null; + } + + boolean ctrl = false; + boolean shift = false; + String remainder = key; + while (remainder.contains("+")) { + int plus = remainder.indexOf('+'); + String mod = remainder.substring(0, plus).trim(); + remainder = remainder.substring(plus + 1).trim(); + if (mod.equalsIgnoreCase("Ctrl")) { + ctrl = true; + } else if (mod.equalsIgnoreCase("Shift")) { + shift = true; + } + } + + KeyModifiers mods = KeyModifiers.of(ctrl, false, shift); + + KeyCode code = switch (remainder.toLowerCase(Locale.ROOT)) { + case "enter", "return" -> KeyCode.ENTER; + case "esc", "escape" -> KeyCode.ESCAPE; + case "tab" -> KeyCode.TAB; + case "backspace" -> KeyCode.BACKSPACE; + case "delete", "del" -> KeyCode.DELETE; + case "up" -> KeyCode.UP; + case "down" -> KeyCode.DOWN; + case "left" -> KeyCode.LEFT; + case "right" -> KeyCode.RIGHT; + case "home" -> KeyCode.HOME; + case "end" -> KeyCode.END; + case "pageup", "pgup" -> KeyCode.PAGE_UP; + case "pagedown", "pgdn" -> KeyCode.PAGE_DOWN; + case "f1" -> KeyCode.F1; + case "f2" -> KeyCode.F2; + case "f3" -> KeyCode.F3; + case "f4" -> KeyCode.F4; + case "f5" -> KeyCode.F5; + case "f6" -> KeyCode.F6; + case "f7" -> KeyCode.F7; + case "f8" -> KeyCode.F8; + case "f9" -> KeyCode.F9; + case "f10" -> KeyCode.F10; + case "f11" -> KeyCode.F11; + case "f12" -> KeyCode.F12; + case "space" -> null; + default -> null; + }; + + if (code != null) { + return KeyEvent.ofKey(code, mods); + } + if ("space".equalsIgnoreCase(remainder)) { + return KeyEvent.ofChar(' ', mods); + } + if (remainder.length() == 1) { + return KeyEvent.ofChar(remainder.charAt(0), mods); + } + return null; + } + + private record PendingKey(KeyEvent event, long fireAt) { + } + } diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiMcpServer.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiMcpServer.java index 3cfd6e483b69..9c1e9378cc03 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 @@ -221,6 +221,17 @@ class TuiMcpServer { Map.of("tab", propDef("string", "Tab to switch to (e.g. 'Routes', 'Health')"), "integration", propDef("string", "Integration name or PID to select")))); + toolList.add(toolDef( + "tui_send_keys", + "Sends key presses to the TUI. Use this to control the TUI for recording demos. " + + "Keys are processed one per tick with the specified delay between them. " + + "Key names: Enter, Esc, Tab, Backspace, Delete, Up, Down, Left, Right, " + + "Home, End, PgUp, PgDn, Space, F1-F12, or any single character. " + + "Modifiers: Ctrl+x, Shift+x, Ctrl+Shift+x.", + Map.of("keys", propDef("array", "Array of key name strings to send"), + "delay", propDef("integer", "Delay in milliseconds between keys (default 150)")), + List.of("keys"))); + JsonObject result = new JsonObject(); result.put("tools", toolList); return result; @@ -247,6 +258,7 @@ class TuiMcpServer { case "tui_get_state" -> callGetState(); case "tui_show_caption" -> callShowCaption(args); case "tui_navigate" -> callNavigate(args); + case "tui_send_keys" -> callSendKeys(args); default -> { isError = true; yield "Unknown tool: " + toolName; @@ -376,6 +388,27 @@ class TuiMcpServer { return Jsoner.serialize(result); } + @SuppressWarnings("unchecked") + private String callSendKeys(Map<String, Object> args) { + Object keysArg = args.get("keys"); + if (!(keysArg instanceof List)) { + return "Error: keys must be an array of strings"; + } + List<String> keys = ((List<Object>) keysArg).stream() + .map(String::valueOf) + .toList(); + if (keys.isEmpty()) { + return "Error: keys array is empty"; + } + int delay = 150; + Object delayArg = args.get("delay"); + if (delayArg instanceof Number n) { + delay = Math.max(50, n.intValue()); + } + int sent = monitor.injectKeys(keys, delay); + return "Queued " + sent + " key(s) with " + delay + "ms delay"; + } + private static JsonArray toJsonArray(List<String> list) { JsonArray arr = new JsonArray(); arr.addAll(list);
