fix(input): keep the line breaks of a multi-line mux prompt, pasted as one block

TmuxManager.sendInput() removed every `\n` before the text reached tmux, so a
multi-line prompt sent with `useMux` (or a multi-line respawn prompt) arrived as
one line with its lines run together: `"echo A\necho B\r"` ran `echo Aecho B`.

The text half of a submit has gone in as one bracketed paste
(buildPasteTextCommands) since the Codex paste-burst fix, and inside the paste
markers Claude Code and Codex insert a pasted line break into the composer
instead of submitting. So the `\n` is now kept (`\r\n` loses only its `\r`) and
the existing paste carries the lines; the Enter stays a separate tmux call.
Multi-line text without an Enter is pasted too rather than typed with
`send-keys -l`, where every line break would reach the pane as a key.
Single-line input is unchanged.

The test-mode gate moves from the top of sendInput() into inputRunner(), as
readPaneRows() does, so a test subclass can record the commands sendInput()
would run instead of pinning the branch shapes on the source.

SubmitVerifier still compares the composer's last glyph line with the prompt
minus its newlines, so it does not reliably recognise a multi-line paste and
may not re-press an Enter that Claude Code ignored. `agent send` and the TUI
therefore keep refusing or joining multi-line input; the docs that described
the stripping now describe the paste and that limit.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claudia[bot]andClaude Opus 5.5 committed 2026-10-21 03:51:31 +02:00
1 parent 39077281d1
commit e3de354461
19 files changed
+164 -56

No files matched your search

+1 -1
View File
@@ -119,7 +119,7 @@ Spawn commands are pinned as literal strings in `test/cli-registry-spawn-golden.
### Input delivery and WS resilience
**Input**: `session.writeViaMux()` for programmatic/curl input — the text as one tmux bracketed paste (`paste-buffer -p`, so Codex cannot fold the Enter into a paste burst) + `send-keys Enter`. A single character is typed with `send-keys -l` instead (`submitsAsKeys()`): Claude Code's and Codex's selection dialogs ignore a pasted `3`, and the Enter then confirms the highlighted option. Single-line only (fire-and-once). Interactive **browser** input goes through a durable **exactly-once** layer: each frame carries a stable `clientId` + monotonic per-session `seq`, persisted to localStorage until the server ACKs (`{t:'ia',seq}` over WS, or HTTP 2xx), so a dropped link/reconnect can't lose or double-deliver a prompt. A duplicate is ACKed as `{t:'ia',seq,dup:true,last}` so a client whose persisted counter rolled back below the server's watermark can lift itself out instead of typing into a silently dead terminal (`docs/reliable-input-delivery.md`). **WS resilience** (#149): the upgrade URL carries `cid = clientId + ':' + perTabNonce`, and `ws-connection-registry.ts` supersedes only same-TAB reconnects (two tabs on one session coexist; input frames keep the bare `clientId` for seq dedup); reconnects back off exponentially (attempts preserved across `_connectWs`), and the header connection chip renders from a real `_wsState` lifecycle (`connecting`/`connected`/`fallback`/`reconnecting`/`disconnected`).
**Input**: `session.writeViaMux()` for programmatic/curl input — the text as one tmux bracketed paste (`paste-buffer -p`, so Codex cannot fold the Enter into a paste burst) + `send-keys Enter`. A single character is typed with `send-keys -l` instead (`submitsAsKeys()`): Claude Code's and Codex's selection dialogs ignore a pasted `3`, and the Enter then confirms the highlighted option. A multi-line prompt keeps its line breaks inside that one paste, with or without the Enter (fire-and-once). Interactive **browser** input goes through a durable **exactly-once** layer: each frame carries a stable `clientId` + monotonic per-session `seq`, persisted to localStorage until the server ACKs (`{t:'ia',seq}` over WS, or HTTP 2xx), so a dropped link/reconnect can't lose or double-deliver a prompt. A duplicate is ACKed as `{t:'ia',seq,dup:true,last}` so a client whose persisted counter rolled back below the server's watermark can lift itself out instead of typing into a silently dead terminal (`docs/reliable-input-delivery.md`). **WS resilience** (#149): the upgrade URL carries `cid = clientId + ':' + perTabNonce`, and `ws-connection-registry.ts` supersedes only same-TAB reconnects (two tabs on one session coexist; input frames keep the bare `clientId` for seq dedup); reconnects back off exponentially (attempts preserved across `_connectWs`), and the header connection chip renders from a real `_wsState` lifecycle (`connecting`/`connected`/`fallback`/`reconnecting`/`disconnected`).
### Per-session env overrides: exact-key allowlist and CLAUDE_CONFIG_DIR
+1 -1
View File
@@ -183,7 +183,7 @@ curl -u admin:$PASS -X POST http://127.0.0.1:3000/api/v1/sessions \
-H 'Content-Type: application/json' \
-d '{"workingDir":"/home/me/project","mode":"claude"}'
# Send a prompt (single-line only, and it must end with \r: Enter is sent only
# Send a prompt (it must end with \r: Enter is sent only
# when the input contains a carriage return; without it the text sits on the
# session's prompt unsubmitted)
curl -u admin:$PASS -X POST http://127.0.0.1:3000/api/v1/sessions/$ID/input \
+1 -1
View File
@@ -78,7 +78,7 @@ input for the session, and came back from localStorage on each reload.
- `_sendInputAsync()` splits a paste over the frame limit into in-limit frames
(`CodemanInputLimit.split`, constants.js, never cutting a surrogate pair). They
go out in seq order, so the PTY sees one contiguous stream. A paste over
`PASTE_MAX_CHARS` (1 MiB), or an oversized `useMux` write (line-oriented, never
`PASTE_MAX_CHARS` (1 MiB), or an oversized `useMux` write (one paste plus its own Enter, never
split), is refused with a toast and never queued.
- The WebSocket answers an oversized sequenced frame with
`{t:'ia', seq, err:'too_large', max}`; the client drops it with a toast. A
+7 -5
View File
@@ -125,11 +125,13 @@ These are set in every managed session. Read them rather than hardcoding anythin
Read these before writing any code. Each one has cost somebody an afternoon.
1. **Input is single line and must end with `\r`.** Enter fires only when the payload
contains a carriage return. Without it the text sits unsubmitted on the prompt, the
request still succeeds, and a combined wait burns its full timeout on a turn that never
started. Embedded newlines are stripped rather than rejected, so `"echo A\necho B\r"` runs
the joined `echo Aecho B`. One line per call.
1. **Input must end with `\r`.** Enter fires only when the payload contains a carriage
return. Without it the text sits unsubmitted on the prompt, the request still succeeds,
and a combined wait burns its full timeout on a turn that never started. Embedded
newlines are kept: the text goes in as one paste and the Enter after it submits all of
it, so `"echo A\necho B\r"` runs both commands in a shell. The check that re-presses a
stranded Enter cannot reliably recognise a multi-line paste, so a long multi-line
prompt to a just-started agent is still safer in a file it is told to read.
2. **Make input idempotent.** Send a stable `clientId` and a monotonic per-session `seq`. The
server deduplicates, so a retry after a dropped connection cannot double-deliver.
3. **Auth.** With `CODEMAN_PASSWORD` set, use HTTP Basic or the session cookie. A missing
+4 -3
View File
@@ -125,9 +125,10 @@ Sending prompts over the API has one rule that catches everyone: **the payload m
`\r`** or Enter is never sent. The request still succeeds, the text sits unsubmitted in the
composer, and any wait burns its whole timeout on a turn that never started.
Input is also **single line**. Embedded newlines are stripped rather than rejected, so
`"echo A\necho B\r"` runs the joined `echo Aecho B`. Put multi-line content in a file and
tell the agent to read it.
Embedded newlines are kept: multi-line input goes in as one paste and the Enter after it
submits all of it, so `"echo A\necho B\r"` runs both commands in a shell. The check that
re-presses a stranded Enter cannot reliably recognise a multi-line paste, so long
multi-line content is still safer in a file the agent is told to read.
See [Driving Codeman From An Agent](Driving-Codeman-From-An-Agent).