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

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 81c86d4cbff99e8742deea9ef65c2a0e5e832644
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Sep 8 15:56:28 2026 +0200

    Camel TUI: Support pasting text into the AI panel (F8) and the shell panel 
(F6)
    
    Paste events were only routed to the actions popup and a few tab inputs, so 
a
    model name could not be pasted after /model in the AI panel, and pasting 
into
    the embedded shell did nothing. Route PasteEvent to the open shell (which
    holds input focus) and to the AI panel, with the same precedence as key 
events.
    
    The AI panel inserts the text at the cursor and collapses line breaks to 
spaces
    so a multi-line paste stays one prompt; the shell forwards the bytes to the
    virtual terminal with line breaks sent as carriage returns like a real 
terminal.
    
    Co-Authored-By: Claude Fable 5.1 <[email protected]>
    Claude-Session: https://claude.ai/code/session_01DHRH7agh6Fh1ExD9e261H6
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../modules/ROOT/pages/camel-jbang-tui.adoc        |  7 ++++
 .../camel/dsl/jbang/core/commands/tui/AiPanel.java | 23 +++++++++++
 .../dsl/jbang/core/commands/tui/CamelMonitor.java  |  9 +++++
 .../dsl/jbang/core/commands/tui/ShellPanel.java    | 24 +++++++++++
 .../dsl/jbang/core/commands/tui/AiPanelTest.java   | 47 ++++++++++++++++++++++
 .../jbang/core/commands/tui/ShellPanelTest.java    | 15 +++++++
 6 files changed, 125 insertions(+)

diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
index 06713c524c54..43b25261142f 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
@@ -573,6 +573,13 @@ respectively. Use *↑*/*↓* on the input line to walk 
previous entries. Limits
 `100` each; set to `0` to disable recall and persistence). Shell history is 
stored in
 `~/.camel/tui-shell.history`; AI prompt history in 
`~/.camel/tui-ai-prompt.history`.
 
+=== Pasting
+
+Both panels accept text pasted from the terminal (for example a model name 
such as
+`qwen3.6:35b-a3b` after `/model`). In the AI prompt, line breaks in the pasted 
text are collapsed
+to spaces so the paste stays a single prompt you can review before pressing 
*Enter*. In the shell,
+line breaks are sent as *Enter*, so a multi-line paste runs line by line as in 
a regular terminal.
+
 == Keyboard Shortcuts
 
 === Global (All Tabs)
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java
index 482663d0a05f..0a0e15112ba6 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java
@@ -600,6 +600,29 @@ class AiPanel {
         return historySearchActive;
     }
 
+    /**
+     * Inserts pasted text at the cursor. Line breaks are collapsed to single 
spaces so a multi-line paste still forms
+     * one prompt that can be reviewed and submitted with Enter, rather than 
submitting on the first newline. While the
+     * reverse-i-search is active the text is appended to the search term 
instead.
+     */
+    void handlePaste(String text) {
+        if (!visible || text == null || text.isEmpty() || 
providerSwitchPopup.isVisible()) {
+            return;
+        }
+        String flat = text.replace("\r\n", "\n").replace('\r', 
'\n').replace('\n', ' ');
+        if (historySearchActive) {
+            searchTerm.append(flat);
+            performSearch(searchIndex >= 0 ? searchIndex : 
promptHistory.size() - 1);
+            return;
+        }
+        if (promptHistory != null) {
+            promptHistory.resetNavigation();
+        }
+        completionMatches = null;
+        inputBuffer.insert(cursorPos, flat);
+        cursorPos += flat.length();
+    }
+
     String searchTermForTesting() {
         return searchTerm.toString();
     }
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 d8bf3bb6d5a2..4de42a3ca044 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
@@ -1439,10 +1439,19 @@ public class CamelMonitor extends CamelCommand {
     }
 
     private boolean handlePasteEvent(PasteEvent pe) {
+        // Same precedence as key events: an open shell holds input focus, 
then popups, then the AI panel
+        if (shellPanel.isOpen()) {
+            shellPanel.handlePaste(pe.text());
+            return true;
+        }
         if (actionsPopup.isVisible()) {
             actionsPopup.handlePaste(pe.text());
             return true;
         }
+        if (aiPanel.isOpen()) {
+            aiPanel.handlePaste(pe.text());
+            return true;
+        }
         if (tabRegistry.httpTab().isProbeMode()) {
             tabRegistry.httpTab().handlePaste(pe.text());
             return true;
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ShellPanel.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ShellPanel.java
index 6af96449f142..31b5de33a7e2 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ShellPanel.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ShellPanel.java
@@ -192,6 +192,30 @@ class ShellPanel {
         return true;
     }
 
+    /**
+     * Forwards pasted text to the shell as if typed. Line breaks are sent as 
carriage returns, which is what a real
+     * terminal emulator sends for Enter, so multi-line pastes run line by 
line like they would in a normal terminal.
+     */
+    void handlePaste(String text) {
+        if (!visible || text == null || text.isEmpty()) {
+            return;
+        }
+        scrollOffset = 0;
+        if (virtualTerminal != null) {
+            try {
+                virtualTerminal.processInputBytes(encodePaste(text));
+            } catch (IOException e) {
+                LOG.log(Level.DEBUG, "Terminal I/O error forwarding pasted 
text", e);
+            } catch (ArrayIndexOutOfBoundsException e) {
+                LOG.log(Level.DEBUG, "Buffer resize race during paste 
forwarding", e);
+            }
+        }
+    }
+
+    static byte[] encodePaste(String text) {
+        return text.replace("\r\n", "\r").replace('\n', 
'\r').getBytes(StandardCharsets.UTF_8);
+    }
+
     boolean handleMouseEvent(MouseEvent me) {
         if (!visible || lastArea == null) {
             return false;
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelTest.java
index 13f9a9189c50..41dee94aa795 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelTest.java
@@ -728,6 +728,53 @@ class AiPanelTest {
         assertEquals("/clear-history", panel.inputBufferForTesting());
     }
 
+    // ---- paste tests ----
+
+    @Test
+    void pasteInsertsTextAtCursor() {
+        AiPanel panel = new AiPanel();
+        panel.open();
+        type(panel, "/model ");
+
+        panel.handlePaste("qwen3.6:35b-a3b");
+
+        assertEquals("/model qwen3.6:35b-a3b", panel.inputBufferForTesting());
+    }
+
+    @Test
+    void pasteInsertsInTheMiddleOfTheBuffer() {
+        AiPanel panel = new AiPanel();
+        panel.open();
+        type(panel, "ac");
+        panel.handleKeyEvent(KeyEvent.ofKey(KeyCode.LEFT, KeyModifiers.NONE));
+
+        panel.handlePaste("b");
+        type(panel, "d");
+
+        // The cursor advances past the pasted text so typing continues right 
after it.
+        assertEquals("abdc", panel.inputBufferForTesting());
+    }
+
+    @Test
+    void pasteCollapsesLineBreaksToSpaces() {
+        AiPanel panel = new AiPanel();
+        panel.open();
+
+        panel.handlePaste("why is\nthe route\r\nstopped?");
+
+        // A multi-line paste becomes a single prompt instead of submitting on 
the first newline.
+        assertEquals("why is the route stopped?", 
panel.inputBufferForTesting());
+    }
+
+    @Test
+    void pasteIsIgnoredWhileClosed() {
+        AiPanel panel = new AiPanel();
+
+        panel.handlePaste("ignored");
+
+        assertEquals("", panel.inputBufferForTesting());
+    }
+
     private static void type(AiPanel panel, String text) {
         for (char ch : text.toCharArray()) {
             panel.handleKeyEvent(KeyEvent.ofChar(ch));
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ShellPanelTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ShellPanelTest.java
index 0f127481e01d..4366c4e0d776 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ShellPanelTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ShellPanelTest.java
@@ -248,6 +248,21 @@ class ShellPanelTest {
         
assertFalse(spans.get(2).style().effectiveModifiers().contains(Modifier.REVERSED));
     }
 
+    // ---- encodePaste tests ----
+
+    @Test
+    void encodePastePlainText() {
+        byte[] result = ShellPanel.encodePaste("ollama run qwen3.6:35b-a3b");
+        assertArrayEquals("ollama run 
qwen3.6:35b-a3b".getBytes(StandardCharsets.UTF_8), result);
+    }
+
+    @Test
+    void encodePasteSendsLineBreaksAsCarriageReturns() {
+        byte[] result = ShellPanel.encodePaste("ls\npwd\r\ndate\n");
+        // Each line break becomes the single \r a real terminal sends for 
Enter, never \r\r
+        assertArrayEquals("ls\rpwd\rdate\r".getBytes(StandardCharsets.UTF_8), 
result);
+    }
+
     // ---- encodeKeyEvent tests ----
 
     @Test

Reply via email to