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 0375ee87ac4772a504752b23c3e01586343017da Author: Claus Ibsen <[email protected]> AuthorDate: Sun May 24 19:19:41 2026 +0200 CAMEL-23606: camel-tui - MCP caption tool and inline caption mode Adds tui_show_caption MCP tool so AI agents can display messages on the TUI screen with the typewriter animation. Also adds Ctrl+Shift+T inline caption mode where keystrokes render directly on screen without a dialog. - tui_show_caption: text parameter (required), triggers caption display - Ctrl+Shift+T: live typing mode with blinking cursor, finishes on Enter/Esc or 3s idle timeout, then hold+fade as usual - MCP Info guide updated with tui_show_caption in tools table - toolDef helper now supports required fields in JSON Schema Co-Authored-By: Claude Opus 4.6 <[email protected]> --- .../dsl/jbang/core/commands/tui/ActionsPopup.java | 6 +- .../dsl/jbang/core/commands/tui/CamelMonitor.java | 8 ++ .../jbang/core/commands/tui/CaptionOverlay.java | 98 +++++++++++++++++++++- .../dsl/jbang/core/commands/tui/TuiMcpServer.java | 26 ++++++ 4 files changed, 134 insertions(+), 4 deletions(-) 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 46022d003015..0a2d768614e0 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 @@ -698,7 +698,8 @@ class ActionsPopup { + "|------|-------------|\n" + "| `tui_get_screen` | Returns the current screen content as text |\n" + "| `tui_get_events` | Returns recent key presses and navigation events |\n" - + "| `tui_get_state` | Returns active tab, selected integration, etc. |\n\n" + + "| `tui_get_state` | Returns active tab, selected integration, etc. |\n" + + "| `tui_show_caption` | Shows a message on the TUI screen |\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" @@ -718,7 +719,8 @@ class ActionsPopup { + "- \"What's on my Camel TUI screen right now?\"\n" + "- \"What tab am I on and what integration is selected?\"\n" + "- \"What keys did I press in the last minute?\"\n" - + "- \"What color is the throughput chart?\"\n"; + + "- \"What color is the throughput chart?\"\n" + + "- \"Show me a message on the TUI screen\"\n"; docTitle = "MCP Info"; docScroll = 0; showDocViewer = true; 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 7018f442d50d..e11639e0a850 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 @@ -347,6 +347,10 @@ public class CamelMonitor extends CamelCommand { captionOverlay.handleKeyEvent(ke); return true; } + if (ke.hasCtrl() && ke.hasShift() && ke.isChar('t')) { + captionOverlay.openInline(); + return true; + } if (ke.hasCtrl() && ke.isChar('t')) { captionOverlay.openInput(); return true; @@ -3052,4 +3056,8 @@ public class CamelMonitor extends CamelCommand { return (int) list.stream().filter(i -> !i.vanishing).count(); } + void showCaption(String text) { + captionOverlay.showCaption(text); + } + } diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CaptionOverlay.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CaptionOverlay.java index 6574e4eb640d..24050181509a 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CaptionOverlay.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CaptionOverlay.java @@ -44,10 +44,15 @@ class CaptionOverlay { private static final long CHAR_DELAY_MS = 50; private static final long HOLD_DURATION_MS = 3000; private static final long FADE_DURATION_MS = 1000; + private static final long INLINE_IDLE_TIMEOUT_MS = 3000; private boolean showInput; private TextInputState inputState; + private boolean inlineMode; + private StringBuilder inlineBuffer; + private long inlineLastKeystroke; + private String captionText; private long captionStartTime; private long captionFullyTypedTime; @@ -56,12 +61,16 @@ class CaptionOverlay { return showInput; } + boolean isInlineMode() { + return inlineMode; + } + boolean isCaptionVisible() { return captionText != null; } boolean isVisible() { - return showInput || captionText != null; + return showInput || inlineMode || captionText != null; } void openInput() { @@ -69,9 +78,26 @@ class CaptionOverlay { inputState = new TextInputState(""); } + void openInline() { + inlineMode = true; + inlineBuffer = new StringBuilder(); + inlineLastKeystroke = System.currentTimeMillis(); + captionText = ""; + captionStartTime = System.currentTimeMillis(); + captionFullyTypedTime = 0; + } + + void showCaption(String text) { + captionText = text; + captionStartTime = System.currentTimeMillis(); + captionFullyTypedTime = 0; + } + void close() { showInput = false; inputState = null; + inlineMode = false; + inlineBuffer = null; captionText = null; captionFullyTypedTime = 0; } @@ -107,6 +133,22 @@ class CaptionOverlay { } return true; } + if (inlineMode) { + if (ke.isCancel() || ke.isConfirm()) { + finishInline(); + } else if (ke.isDeleteBackward()) { + if (!inlineBuffer.isEmpty()) { + inlineBuffer.deleteCharAt(inlineBuffer.length() - 1); + captionText = inlineBuffer.toString(); + inlineLastKeystroke = System.currentTimeMillis(); + } + } else if (ke.code() == KeyCode.CHAR) { + inlineBuffer.append(ke.character()); + captionText = inlineBuffer.toString(); + inlineLastKeystroke = System.currentTimeMillis(); + } + return true; + } if (captionText != null) { captionText = null; captionFullyTypedTime = 0; @@ -116,7 +158,12 @@ class CaptionOverlay { } void tick(long now) { - if (captionText == null) { + if (inlineMode && !inlineBuffer.isEmpty() + && now - inlineLastKeystroke > INLINE_IDLE_TIMEOUT_MS) { + finishInline(); + return; + } + if (captionText == null || inlineMode) { return; } int totalChars = captionText.length(); @@ -132,11 +179,27 @@ class CaptionOverlay { } } + private void finishInline() { + inlineMode = false; + if (inlineBuffer.isEmpty()) { + captionText = null; + inlineBuffer = null; + return; + } + captionText = inlineBuffer.toString(); + inlineBuffer = null; + captionFullyTypedTime = System.currentTimeMillis(); + } + void render(Frame frame, Rect area) { if (showInput) { renderInput(frame, area); return; } + if (inlineMode) { + renderInline(frame, area); + return; + } if (captionText != null) { renderCaption(frame, area); } @@ -146,6 +209,9 @@ class CaptionOverlay { if (showInput) { hint(spans, "Enter", "show"); hintLast(spans, "Esc", "cancel"); + } else if (inlineMode) { + hint(spans, "Enter", "finish"); + hintLast(spans, "Esc", "cancel"); } else if (captionText != null) { hintLast(spans, "any key", "dismiss"); } @@ -180,6 +246,34 @@ class CaptionOverlay { frame.renderStatefulWidget(textInput, inputArea, inputState); } + private void renderInline(Frame frame, Rect area) { + Style style = Style.EMPTY.fg(Color.WHITE).bold(); + String text = inlineBuffer != null ? inlineBuffer.toString() : ""; + boolean cursorVisible = (System.currentTimeMillis() / 500) % 2 == 0; + String display = text + (cursorVisible ? "▌" : " "); + + String[] parts = display.split("\\\\n", -1); + List<Line> lines = new ArrayList<>(); + int maxWidth = 0; + for (String part : parts) { + lines.add(Line.from(Span.styled(" " + part + " ", style))); + maxWidth = Math.max(maxWidth, part.length() + 4); + } + + int captionW = Math.min(Math.max(maxWidth, 6), area.width() - 2); + int captionH = lines.size(); + int captionX = area.left() + Math.max(0, (area.width() - captionW) / 2); + int captionY = area.top() + Math.max(0, (area.height() - captionH) / 2); + Rect captionArea = new Rect( + captionX, captionY, Math.min(captionW, area.width()), + Math.min(captionH, area.height())); + + frame.renderWidget(Clear.INSTANCE, captionArea); + frame.renderWidget(Paragraph.builder() + .text(Text.from(lines.toArray(Line[]::new))) + .build(), captionArea); + } + private void renderCaption(Frame frame, Rect area) { long now = System.currentTimeMillis(); long elapsed = now - captionStartTime; 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 6b694e036b03..0d3fd0854fd9 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 @@ -206,6 +206,13 @@ class TuiMcpServer { "Returns the current TUI navigation state: active tab, selected integration, " + "and integration count.", Map.of())); + toolList.add(toolDef( + "tui_show_caption", + "Shows a caption message on the TUI screen with a typewriter animation. " + + "Use this to display messages to the user. " + + "Supports \\n for newlines.", + Map.of("text", propDef("string", "The caption text to display")), + List.of("text"))); JsonObject result = new JsonObject(); result.put("tools", toolList); @@ -231,6 +238,7 @@ class TuiMcpServer { case "tui_get_screen" -> callGetScreen(args); case "tui_get_events" -> callGetEvents(args); case "tui_get_state" -> callGetState(); + case "tui_show_caption" -> callShowCaption(args); default -> { isError = true; yield "Unknown tool: " + toolName; @@ -316,6 +324,15 @@ class TuiMcpServer { return Jsoner.serialize(result); } + private String callShowCaption(Map<String, Object> args) { + String text = (String) args.get("text"); + if (text == null || text.isBlank()) { + return "Error: text is required"; + } + monitor.showCaption(text); + return "Caption displayed: " + text; + } + // --- JSON-RPC helpers --- private void sendResult(HttpExchange exchange, JsonObject request, JsonObject result) throws IOException { @@ -350,6 +367,10 @@ class TuiMcpServer { // --- Tool definition helpers --- private JsonObject toolDef(String name, String description, Map<String, JsonObject> properties) { + return toolDef(name, description, properties, List.of()); + } + + private JsonObject toolDef(String name, String description, Map<String, JsonObject> properties, List<String> required) { JsonObject schema = new JsonObject(); schema.put("type", "object"); if (!properties.isEmpty()) { @@ -357,6 +378,11 @@ class TuiMcpServer { props.putAll(properties); schema.put("properties", props); } + if (!required.isEmpty()) { + JsonArray req = new JsonArray(); + req.addAll(required); + schema.put("required", req); + } JsonObject tool = new JsonObject(); tool.put("name", name);
