diff --git a/bridged/src/main/java/dev/ltms/bridged/herdr/AgentControl.java b/bridged/src/main/java/dev/ltms/bridged/herdr/AgentControl.java
index 3104545..31a9a49 100644
--- a/bridged/src/main/java/dev/ltms/bridged/herdr/AgentControl.java
+++ b/bridged/src/main/java/dev/ltms/bridged/herdr/AgentControl.java
@@ -18,6 +18,17 @@ import java.util.Map;
*/
public final class AgentControl {
+ /**
+ * The keystroke that submits a prompt in the Claude Code TUI: a carriage return (Enter).
+ * It must be delivered as its own {@code agent.send} call — herdr delivers a message's
+ * text as a bracketed paste, and a {@code "\r"} appended to that same text is swallowed as
+ * literal newline content, not a submit. Sent as a separate keystroke event it lands outside
+ * the paste and submits. (A bare {@code "\n"} inserts a newline either way.) Verified live
+ * against Claude Code v2.1.210: an injected task stayed unsubmitted with {@code "text\r"} in
+ * one call, and submitted the instant a standalone {@code "\r"} was sent.
+ */
+ static final String SUBMIT_KEY = "\r";
+
private final HerdrClient herdr;
public AgentControl(HerdrClient herdr) {
@@ -53,9 +64,15 @@ public final class AgentControl {
return Agent.from(result.get("agent"));
}
- /** Deliver {@code text} to an agent (its next prompt input). */
+ /**
+ * Deliver {@code text} to an agent as its next prompt and submit it — two keystroke
+ * events: the message (a bracketed paste, so any embedded newlines are preserved verbatim),
+ * then a standalone {@link #SUBMIT_KEY} (Enter) that actually submits it. Without the second
+ * event the text just sits in the worker's input box, never processed (see {@link #SUBMIT_KEY}).
+ */
public void send(String target, String text) {
herdr.call("agent.send", Map.of("target", target, "text", text));
+ herdr.call("agent.send", Map.of("target", target, "text", SUBMIT_KEY));
}
/**
diff --git a/bridged/src/test/java/dev/ltms/bridged/herdr/AgentControlTest.java b/bridged/src/test/java/dev/ltms/bridged/herdr/AgentControlTest.java
new file mode 100644
index 0000000..b32bea3
--- /dev/null
+++ b/bridged/src/test/java/dev/ltms/bridged/herdr/AgentControlTest.java
@@ -0,0 +1,40 @@
+package dev.ltms.bridged.herdr;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/** Unit-level behaviour of {@link AgentControl} over a fake herdr. */
+class AgentControlTest {
+
+ /** The {@code text} of every agent.send, in call order. */
+ @SuppressWarnings("unchecked")
+ private static List sendTexts(FakeHerdr herdr) {
+ return herdr.calls.stream()
+ .filter(c -> c.method().equals("agent.send"))
+ .map(c -> ((Map) c.params()).get("text").toString())
+ .toList();
+ }
+
+ @Test
+ void sendDeliversThePayloadThenAStandaloneSubmitKey() {
+ FakeHerdr herdr = new FakeHerdr();
+ new AgentControl(herdr).send("term_x", "do the thing");
+
+ // The Enter must be its own event — appended to the paste it would be swallowed as text.
+ assertEquals(List.of("do the thing", "\r"), sendTexts(herdr),
+ "payload paste first, then a separate carriage-return keystroke to submit it");
+ }
+
+ @Test
+ void sendPreservesEmbeddedNewlinesAndSubmitsOnlyOnce() {
+ FakeHerdr herdr = new FakeHerdr();
+ new AgentControl(herdr).send("term_x", "line1\nline2");
+
+ assertEquals(List.of("line1\nline2", "\r"), sendTexts(herdr),
+ "multiline content is delivered verbatim; a single trailing Enter submits it");
+ }
+}
diff --git a/bridged/src/test/java/dev/ltms/bridged/inject/InjectorTest.java b/bridged/src/test/java/dev/ltms/bridged/inject/InjectorTest.java
index 7d0797d..6653cf8 100644
--- a/bridged/src/test/java/dev/ltms/bridged/inject/InjectorTest.java
+++ b/bridged/src/test/java/dev/ltms/bridged/inject/InjectorTest.java
@@ -25,12 +25,18 @@ class InjectorTest {
private final FakeHerdr herdr = new FakeHerdr();
private final Injector injector = new Injector(new AgentControl(herdr));
- /** Text of every agent.send, in order. */
+ /**
+ * The logical messages delivered, in order. AgentControl.send emits each delivery as two
+ * agent.send calls — the payload, then a standalone Enter keystroke ({@code "\r"}) to submit
+ * it; these tests assert delivery ordering/gating, not the submit event, so drop the bare
+ * carriage returns.
+ */
@SuppressWarnings("unchecked")
private List sent() {
return herdr.calls.stream()
.filter(c -> c.method().equals("agent.send"))
.map(c -> ((Map) c.params()).get("text").toString())
+ .filter(t -> !t.equals("\r"))
.toList();
}
@@ -170,7 +176,9 @@ class InjectorTest {
@SuppressWarnings("unchecked")
Map p = (Map) c.params();
return p.get("text").toString();
- }).toList());
+ })
+ .filter(t -> !t.equals("\r")) // drop the standalone submit keystroke
+ .toList());
}
@Test