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