Skip to content

fix(tmux): submit text as a bracketed paste so Codex does not swallow Enter - #628

Merged
Ark0N merged 2 commits into
Ark0N:masterfrom
Randalix:fix/codex-submit-paste
Oct 16, 2026
Merged

Ark0N merged 2 commits into
Ark0N:masterfrom
Randalix:fix/codex-submit-paste

Conversation

@Randalix

Copy link
Copy Markdown
Contributor

Closes #627

sendInput() now puts the text of a text+Enter pair into the pane as one bracketed paste (set-buffer + paste-buffer -p -d) instead of typing it with send-keys -l, then sends Enter 50 ms later as before. Codex no longer folds that Enter into a paste burst, so long prompts stop sitting unsubmitted in its composer. Measurements are in #627.

What changed

  • buildPasteTextCommands(tmux, target, text) in src/tmux-manager.ts returns the three commands of one paste: set fills a buffer of its own (codeman-submit-<pid>-<seq>), paste pastes it bracketed into the target and deletes it, and discard deletes it when the paste failed.
  • pasteText(run, cmds) runs them. If the paste fails (pane or server gone), it deletes the buffer and rethrows the paste's error. tmux never trims named buffers by itself (buffer-limit only applies to automatic ones), and on my fork one stayed behind on 2026-10-11 after a paste into a session that had just been deleted (can't find pane).
  • A trailing ; is sent as \;. tmux reads an argument that ends in ; as a command separator: on master send-keys -l 'x;' types x and a lone ; types nothing; with set-buffer a lone ; fails outright (no data specified). Checked against tmux 3.5a for x;, ;, x;; and x\;, all four come out of show-buffer unchanged.
  • Comments and docs that said send-keys -l for this path: writeViaMux(), the SubmitVerifier header, two comments in the input route, CLAUDE.md, docs/architecture-invariants.md, docs/cron-guide.md.

What stays as it was

  • Text without \r is still typed with send-keys -l. There is no Enter to fold, and the text lands in the composer either way.
  • sendInputToPane() has no callers in src/, so I left it alone.
  • Without bracketed paste requested by the pane, paste-buffer -p sends the plain text, the same bytes send-keys -l sent. I haven't measured an SSH remote case, where the paste goes into the local pane that runs ssh; if the markers don't get through there, it behaves like master.
  • Slash commands still run when pasted. Checked on 2026-09-25: /cost in Claude Code and /status in Codex both executed.
  • SubmitVerifier is unchanged. Claude Code 2.1.299 shows long text as [Pasted text #1] whether it was typed or pasted, and Codex shows [Pasted Content N chars], so the verifier can't match a long prompt in either CLI on master or with this change. I noted it in the buildPasteTextCommands doc comment and did not touch the verifier.

Tests

test/tmux-submit-paste.test.ts (18 tests):

  • command shape, a fresh buffer per call, no send-keys in the paste commands
  • the text through a real shell into a fake tmux that prints its argv: quotes, $(), tmux format strings, a leading -, non-ASCII, 1500 chars, the trailing-; cases
  • pasteText(): a failed paste deletes the buffer and reports the paste's error, also when the delete fails too; a failed set-buffer stops before the paste
  • a wiring test that reads sendInput()'s source, since sendInput() returns before touching tmux under vitest; it fails on master's send-keys -l

Full npm test: 11276 passed, 12 skipped, 0 failed (568 files passed, 1 skipped). The first run had one red, frontend-public-tooling > check:public-assets, because this worktree had never run postinstall and had no spreadsheet vendor assets; after node scripts/prepare-spreadsheet-assets.mjs that file passes too. tsc, eslint on the changed files, prettier and check:frontend-syntax are green.

Live

Throwaway Codex pane (codex-cli 0.162.1) on its own tmux socket, see #627: 98 chars 4/4 and 1505 chars 3/3 submitted with the paste, against 3/4 and 0/3 on master.

Through the input route: codeman agent send of a 1506-char prompt to a Codex worker on my fork's server, three times. The rollout has three user messages of exactly 1506 characters and three task_complete. That server runs the same paste commands, but without the discard branch, which is new in this PR and covered by the unit tests above.

No changeset.

🤖 Generated with Claude Code

… Enter

sendInput() typed the text of a text+Enter pair with `send-keys -l` and
sent Enter 50 ms later. Codex detects an unmarked paste from a fast
character burst and folds an Enter inside that window into the paste as a
newline, so the prompt stays in the composer unsubmitted. Measured with
codex-cli 0.162.1 in a throwaway tmux pane: 1505 chars stuck 3/3 as
`[Pasted Content 1506 chars]`, a 98-char line stuck 1/4. SubmitVerifier
re-presses Enter for the short case, but it cannot match the long one: the
composer shows the placeholder instead of the prompt's head.

The text now goes in via set-buffer + `paste-buffer -p -d` (markers only
when the pane requested bracketed paste), followed by the separate Enter as
before. Same pane, same texts: 4/4 and 3/3 submitted. Each send gets its
own named buffer; a paste that fails (pane or server gone) deletes it,
since tmux never trims named buffers on its own.

Also escapes a trailing `;` in the text, which tmux treats as a command
separator (`send-keys -l 'x;'` typed `x`, a lone `;` typed nothing).

sendInputToPane() has no callers in src/ and is left as it was.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@Ark0N

Ark0N commented Oct 14, 2026

Copy link
Copy Markdown
Owner

Thanks @Randalix for this, and for the careful measurements in #627. The PR puts the text half of a text+Enter submit into the pane as one bracketed paste, so Codex can no longer fold the Enter into a paste burst.

The mechanism checks out. tmux adds the markers only when the pane asked for them, the per-call buffer and the discard path are careful, and the trailing-; escape is right (I re-ran your four cases on tmux 3.4). I also confirmed that Claude Code 2.1.299 renders a pasted prompt exactly like a typed one from 60 to 1200 characters, so SubmitVerifier sees the same composer either way, and /cost still runs through your exact sequence.

One thing needs a change before merge:

  1. Single-key dialog answers sent with Enter now confirm the highlighted option (src/tmux-manager.ts:3835). Claude Code's and Codex's selection dialogs drop a bracketed paste. On Claude Code 2.1.299 and codex-cli 0.162.0, a typed 3 on a numbered menu selects option 3; a pasted 3 does nothing. Replaying buildPasteTextCommands + pasteText + 50 ms + Enter with 4\r on Claude's "Using 3rd-party platforms" list (4 is "Go back") confirmed option 1. So codeman agent send <id> 3, MCP send_prompt with 3, or {"input":"3\r"} on the input route now answer a permission prompt or an AskUserQuestion list with option 1 instead of the key that was sent. Please keep send-keys -l when textPart is a single character and paste anything longer. A lone character is not a burst: on codex-cli 0.162.0 a typed single character + Enter 50 ms later submitted from the composer 4 out of 4 times, so this keeps your Codex fix intact. Put that rule in a small exported helper with a unit test.

Two doc lines still describe the old path:

  1. src/tmux-manager.ts:10: the @fileoverview still says "Reliable input: send-keys -l sends literal text in a single command".
  2. docs/orchestrator-loop-architecture.md:247: writeViaMux() → tmux send-keys -l "prompt..." + Enter.

Optional, and fine as a follow-up: the send-keys -l branch for text without \r (src/tmux-manager.ts:3850) and sendInputToPane() still drop a trailing ;. Your escape would fix both.

Once the single-character guard and its test are in, this is ready to merge, and I will add the changeset at release.

Claude Code's and Codex's selection dialogs ignore a bracketed paste, so
after the previous commit a pasted `3` + Enter confirmed the highlighted
option instead of option 3. sendInput() now types a single character with
`send-keys -l` as master did (submitsAsKeys()). One character cannot form
the burst Codex reads as a paste, so the Codex fix is unaffected.

The trailing-`;` escape moves into escapeTmuxTrailingSemicolon() and now
also covers every `send-keys -l` call in sendInput() and
sendInputToPane(); tmux 3.5a types `x;` for `send-keys -l 'x\;'`.

Docs: the @fileoverview lines and orchestrator-loop-architecture.md still
described `send-keys -l`; CLAUDE.md, architecture-invariants.md and
cron-guide.md name the single-character exception.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@Randalix

Copy link
Copy Markdown
Contributor Author

Thanks, that dialog case was a real hole. Pushed as a separate commit on top so the diff stays readable:

  1. Single character stays typed. submitsAsKeys() returns true for one code point, and sendInput() then uses send-keys -l + 50 ms + Enter like master did. One character can't form the burst Codex reads as a paste, so the Codex fix doesn't need it. Tests pin 3, y, ;, ä and an emoji as typed, and 10, yes, /cost and a 1500-char prompt as pasted. A wiring test checks that the single-character branch uses send-keys -l and the other one pasteText().
  2. Doc lines. @fileoverview (both lines that still described send-keys) and orchestrator-loop-architecture.md:247. I also added the single-character exception where the first commit described the paste path: CLAUDE.md, architecture-invariants.md and cron-guide.md.
  3. Trailing ;. I took the optional one too. The escape is now escapeTmuxTrailingSemicolon(), and all four send-keys -l calls in sendInput() and sendInputToPane() go through it, as does set-buffer. Checked on tmux 3.5a: send-keys -l 'x\;' types x;, and ;, x;; and a; b arrive unchanged.

tmux-submit-paste.test.ts has 36 tests now (12 of the new assertions fail against the previous commit). The full npm test run is green (11294 passed).

I couldn't repeat your menu repro here: on Claude Code 2.1.300 the "Teach auto mode" menu came up once and then not again in a fresh folder. The guard sends single characters down master's old path, which is the one you measured as selecting correctly.

@Ark0N
Ark0N merged commit 8084cae into Ark0N:master Oct 16, 2026
2 checks passed
Ark0N pushed a commit that referenced this pull request Oct 16, 2026
One release changeset for #616, #617, #621, #622, #623, #624, #625, #628, #632, #634, #635 and #642. The changesets carried by #616, #617 and #642 are folded into it and removed (#617's was minor; 1.41.2 is a patch release).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@Ark0N

Ark0N commented Oct 16, 2026

Copy link
Copy Markdown
Owner

Merged and shipped in 1.42.0, closing #627. Thanks @Randalix!

Every point from the review is in: single characters stay typed, the doc lines are fixed, and you took the optional trailing-; escape too. I re-ran it live: in Claude Code 2.1.301 a 768-char prompt ending in ; submits whole and a 3 on an AskUserQuestion list still picks option 3, and in codex 0.162.0 a 1467-char prompt now submits where master left it sitting in the composer. Cleaning up the named buffer when a paste fails and testing the argv through a real shell were nice touches.

Two small follow-ups if you feel like it, neither blocking: the sendInput wiring tests are still source greps (a pure "input to tmux commands" planner would make them behavioural), and sendInputToPane() has no callers today but still types text+Enter with send-keys -l, so it would bring the Codex paste-burst problem back if it ever gets one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Codex: a prompt sent through the input route is pasted but never submitted (Enter folded into the paste burst)

2 participants