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/05/24 21:03:13 UTC
(camel) 04/09: CAMEL-23606: camel-tui - MCP key injection tool for AI-driven demo recording
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 <cl...@gmail.com>
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 <no...@anthropic.com>
---
.../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);