mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-15 02:49:33 +01:00
Merge pull request #670: fix(input): keep the line breaks of a multi-line mux prompt, pasted as one block
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
b67018b318
39 files changed
+618
-135
No files matched your search
@@ -434,6 +434,12 @@ about a hundred characters or more is taken as a paste by Claude Code, its `\r`
|
||||
becomes a newline, and the prompt stays unsent (measured on 2.1.283). Any other
|
||||
input (escape sequences, a bracketed-paste frame, a line feed, a bare `\r`) keeps
|
||||
the raw write, and an explicit `"useMux": false` forces it.
|
||||
With `"useMux": true`, a line feed is kept: the text goes in as one bracketed paste
|
||||
(`\r\n` read as `\n`, leading and trailing line breaks dropped), then Enter if the
|
||||
input carried a `\r`; a pane that has not enabled bracketed paste receives each line
|
||||
break as a carriage return and runs the lines as they arrive. 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.
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$API/api/v1/sessions/$SID/input" \
|
||||
|
||||
@@ -123,7 +123,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
|
||||
|
||||
@@ -1054,7 +1054,7 @@ Mapping is `ctrlByteFor()` (`code & 0x1f` over @A-Z[\]^_ and a-z, plus Ctrl+Spac
|
||||
|
||||
⚠️ **Delivery is a hand-built bracketed-paste frame** (`\x1b[200~` + the text with newlines mapped to `\r` + `\x1b[201~`, byte-identical to what `terminal.paste()` would emit) through `_sendInputAsync` WITHOUT `{ useMux: true }`, plus a SEPARATE Enter 120 ms later WITH it (codex drops keys that share a PTY read with a bracketed paste). Not `terminal.paste()`, for two reasons: xterm's `bracketedPasteMode` mirror is false for every session after a tab switch or reload (`terminal.reset()` in the replay re-clones the DEC modes, the tmux capture carries no `?2004h`, and tmux never forwards the pane's DECSET to a client after attach), so a paste through xterm would go out unbracketed and the CLI would submit at the first `\r`; and xterm's onData is where the local-echo paste branch flushes pending overlay text AHEAD of the block, text the composer has already taken and erased from the PTY, so the frame goes straight to the wire with the composer as the prompt's only owner. The unconditional frame is safe for a CLI that never enabled DECSET 2004 because tmux does the gating (measured against a live pane: markers stripped for a `cat -v` pane, forwarded intact to a process that had emitted `?2004h`).
|
||||
|
||||
⚠️ The frame must never take the mux fallback: `TmuxManager.sendInput()` strips every `\r` and `\n`, which welds the lines together and submits them.
|
||||
⚠️ The frame must never take the mux fallback: `TmuxManager.sendInput()` strips every `\r`, the frame's line separator, so the lines weld into one, pasted inside a second pair of markers, and get submitted.
|
||||
|
||||
⚠️ The size guard sits on the `MAX_INPUT_LENGTH` boundary (64 KiB of UTF-16 code units): `ws-routes.ts` drops a longer frame WITHOUT an ACK, which would wedge the durable queue, so `_composerMaxLength` is derived from that limit minus both markers and an oversized prompt stays a draft with a toast.
|
||||
|
||||
|
||||
+4
-3
@@ -207,9 +207,10 @@ trailing carriage return:
|
||||
| `typed` | `session.writeViaMux()` — the text as a tmux bracketed paste + Enter (a single character is typed, so a dialog answer selects) | Default; like a human pasting the prompt and pressing Enter. |
|
||||
| `paste` | `session.write()` — writes directly to the PTY/mux | Bulk paste-style delivery. |
|
||||
|
||||
> ⚠️ **Single-line only — enforced.** Like all programmatic input in Codeman,
|
||||
> multi-line delivery would be silently corrupted (Ink-based TUIs treat a
|
||||
> newline as submit; typed mode fuses lines). So newlines are **rejected**: the
|
||||
> ⚠️ **Single-line only — enforced.** A job fires unattended, and the submit
|
||||
> check that re-presses an ignored Enter cannot reliably recognise a multi-line
|
||||
> paste in the composer, so a stranded prompt would go unnoticed (in paste mode
|
||||
> the newlines reach the CLI as keys). So newlines are **rejected**: the
|
||||
> schema and the form refuse a multi-line `promptText`, and at fire time a
|
||||
> prompt file whose content is multi-line (after stripping trailing newlines)
|
||||
> fails the run with a clear `errorMessage`. Put multi-line instructions in a
|
||||
|
||||
+13
-11
@@ -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 \
|
||||
@@ -294,8 +294,8 @@ if ! jq -e '.data.wait.matched' <<<"$R" >/dev/null; then
|
||||
fi
|
||||
|
||||
# 3. Send the prompt AND register the wait in one call, so the answer cannot be
|
||||
# the previous turn's idle state. Single line only, ending in \r (otherwise
|
||||
# Enter is never sent and this wait times out on a turn that never started).
|
||||
# the previous turn's idle state. It must end in \r (otherwise Enter is never
|
||||
# sent and this wait times out on a turn that never started).
|
||||
W=$("${CURL[@]}" -X POST "$API/api/v1/sessions/$SID/input" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"input":"Run the test suite and summarize the failures\r","useMux":true,
|
||||
@@ -414,14 +414,16 @@ Every one of these has cost somebody real time.
|
||||
shipped bugs more than once.
|
||||
- **`text/plain` bodies stay raw.** Auto-parsing them as JSON enabled
|
||||
simple-request CSRF, so it is deliberate. Send `application/json`.
|
||||
- **Prompts are single-line and must end with `\r`.** The server splits your text
|
||||
and Enter into two separate tmux writes (Ink needs them apart), but it sends the
|
||||
Enter **only when the input contains a carriage return**. Without it your text
|
||||
sits on the prompt unsubmitted, which is the single most common "the wait
|
||||
endpoints don't work" report: the wait runs its full timeout on a turn that never
|
||||
started. Newlines inside the string are stripped rather than rejected, so
|
||||
`"echo A\necho B\r"` runs the single joined command `echo Aecho B`: send one line
|
||||
per call.
|
||||
- **Prompts must end with `\r`.** The server splits your text and Enter into two
|
||||
separate tmux writes (Ink needs them apart), but it sends the Enter **only when
|
||||
the input contains a carriage return**. Without it your text sits on the prompt
|
||||
unsubmitted, which is the single most common "the wait endpoints don't work"
|
||||
report: the wait runs its full timeout on a turn that never started. With
|
||||
`useMux`, newlines inside the string 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.
|
||||
- **`wait-output`'s `from=now` is not "printed after you asked".** tmux repaints
|
||||
the visible screen on attach, on resize, and on any TUI redraw, and a repaint
|
||||
arrives as ordinary output, so text already on screen can satisfy a fresh wait.
|
||||
|
||||
@@ -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
|
||||
|
||||
+3
-3
@@ -136,9 +136,9 @@ instead of failing silently; `Enter` and `1-9` keep working.
|
||||
|
||||
### `p` sends exactly one line
|
||||
|
||||
The composer is a single line by design, ending in a carriage return: that is the
|
||||
input contract every Codeman path follows, because multi-line text breaks the
|
||||
agent's own composer. Pasted newlines become spaces rather than being rejected, so
|
||||
The composer is a single line by design, ending in a carriage return, because the
|
||||
server's submit check cannot reliably recognise a multi-line paste: a prompt it cannot
|
||||
see on the composer line is not re-submitted when the agent ignored the first Enter. Pasted newlines become spaces rather than being rejected, so
|
||||
a paste cannot silently run a different command than the one you read.
|
||||
|
||||
## Answering approvals
|
||||
|
||||
@@ -51,10 +51,11 @@ or a server restart shifts later runs slightly. That drift is accepted rather th
|
||||
|
||||
## Prompts are single line
|
||||
|
||||
This is the rule people trip over. Programmatic input into an agent session is single line
|
||||
everywhere in Codeman, because the terminal UIs these CLIs use treat a newline as submit. A
|
||||
multi-line prompt would be silently mangled, so it is **rejected** instead: the form refuses
|
||||
it, and a prompt file whose contents are multi-line fails the run with a clear message.
|
||||
This is the rule people trip over. A cron job fires with nobody watching, and the check that
|
||||
re-presses an Enter the agent ignored cannot reliably recognise a multi-line paste in the
|
||||
composer, so a multi-line prompt could sit there unsent. In `paste` mode the newlines would
|
||||
reach the agent as keys. So a multi-line prompt is **rejected**: the form refuses it, and a
|
||||
prompt file whose contents are multi-line fails the run with a clear message.
|
||||
|
||||
For anything longer than a sentence, put the instructions in a file and make the prompt tell
|
||||
the agent to read it:
|
||||
|
||||
@@ -125,11 +125,14 @@ 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. With
|
||||
`useMux`, 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
|
||||
|
||||
@@ -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.
|
||||
With `useMux`, 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).
|
||||
|
||||
|
||||
Reference in new issue
Block a user