fix(input): make sure a prompt sent through the API actually leaves the composer

Claude Code 2.1.277 takes typed text the moment its composer paints but
ignores Enter for the first 30 to 50 seconds after it (measured 2026-09-19
through the input route: an Enter at 28 s stranded the prompt, one at 51 s
submitted it). The text+Enter pair `sendInput` sends 50 ms apart therefore
left every programmatic prompt sitting unsent, and every waiter burned its
timeout on a turn that never started.

Server: `SubmitVerifier` (session-submit-verifier.ts), armed from
`writeViaMux` for every mux write that carried a carriage return, reads the
pane on a 2 s to 60 s schedule and re-sends Enter only while the last
composer line (the CLI's own prompt glyph) still holds the head of what was
sent. An empty composer, other text, or no composer line at all ends it; a
newer write replaces the schedule.

Skill: `sendwait` gets the same loop (`_composer_text`, no-break space
stripped by its bytes for BSD sed) for servers that predate this, and the
preamble version moves to 1.30.1 so seeded agents pick up the fresh copy.
SKILL.md's heredoc and the plugin mirror are regenerated.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer
2026-09-19 11:38:59 +02:00
parent 3cdb4bf42e
commit 19ffe9b7a8
11 changed files with 616 additions and 94 deletions
+32 -1
View File
@@ -110,6 +110,7 @@ import {
import { DEFAULT_TMUX_HISTORY_LIMIT } from './config/terminal-history.js';
import { EXEC_TIMEOUT_MS } from './config/exec-timeout.js';
import { getCli } from './config/cli-registry/registry.js';
import { SubmitVerifier } from './session-submit-verifier.js';
import { compileVersionRegex } from './config/cli-registry/patterns.js';
import { resolveSessionCliVersion } from './utils/cli-resolver.js';
import {
@@ -502,6 +503,8 @@ export class Session extends EventEmitter {
private _trustDialogAttempts = 0; // Keystrokes sent at the trust dialog
private _lastTrustDialogScanAt = 0; // Throttle for the trust-dialog screen read
private _trustDialogTimer: NodeJS.Timeout | null = null; // Re-read after a keystroke (see below)
/** Re-sends Enter while a programmatic prompt still sits in the composer (session-submit-verifier.ts). */
private _submitVerifier: SubmitVerifier | null = null;
private _interactiveStartedAt = 0; // When the interactive pane launched (bounds that scan)
private _taskTracker: TaskTracker;
@@ -3190,6 +3193,9 @@ export class Session extends EventEmitter {
}
private _clearAllTimers(): void {
// Stop re-sending Enter for a prompt this session will never take now
this._submitVerifier?.cancel();
this._submitVerifier = null;
// Clear the workspace-trust follow-up read
if (this._trustDialogTimer) {
clearTimeout(this._trustDialogTimer);
@@ -3721,7 +3727,10 @@ export class Session extends EventEmitter {
const submittedPrompt = this._trackSubmit(data, options);
if (this._mux && this._muxSession) {
const sent = await this._mux.sendInput(this.id, data);
if (sent) this._emitSubmittedPrompt(submittedPrompt);
if (sent) {
this._emitSubmittedPrompt(submittedPrompt);
this._verifySubmitted(data);
}
return sent;
}
// Fallback to PTY write
@@ -3733,6 +3742,28 @@ export class Session extends EventEmitter {
return false;
}
/**
* Arm the composer check for a write that carried Enter (session-submit-verifier.ts):
* Claude Code 2.1.277+ ignores Enter for the first 30-50 s after the composer paints,
* so the pair `sendInput` just sent can leave the text stranded. Only a mux session
* can read its pane, only text can be stranded, and the glyph is the CLI's own.
*/
private _verifySubmitted(data: string): void {
if (!data.includes('\r') || !this._mux?.capturePaneText || !this._muxSession) return;
const text = data.replace(/[\r\n]/g, '').trimEnd();
if (!text) return;
this._submitVerifier ??= new SubmitVerifier({
capture: () =>
this._isStopped || !this._mux || !this._muxSession
? null
: this._mux.capturePaneText?.(this._muxSession.muxName),
sendEnter: () => this._mux?.sendInput(this.id, '\r'),
glyph: () => getCli(this.mode)?.capabilities.workDetect?.promptGlyph ?? '❯',
log: (m) => console.log(`[Session ${this.id.slice(0, 8)}] ${m}`),
});
this._submitVerifier.arm(text);
}
/** Current PTY dimensions — used to skip no-op resizes that trigger Ink redraws */
private _ptyCols = 120;
private _ptyRows = 40;