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);

Reply via email to