mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-04 14:39:42 +02:00
Merge the input-delivery fix from master
fix(input): deliver API prompts through tmux so their Enter is not lost
This commit is contained in:
@@ -437,6 +437,26 @@ export function parseBody<T>(schema: z.ZodType<T>, body: unknown, errorMessage?:
|
||||
return result.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether an input body is a plain prompt: printable text followed by exactly one
|
||||
* carriage return, and nothing else.
|
||||
*
|
||||
* That is the shape a script, a bot or a curl call sends to submit a prompt, and the
|
||||
* one that must NOT be written into the pane in one piece. Measured on Claude Code
|
||||
* 2.1.283 (2026-09-28): a direct write of `<text>\r` arrives as a single burst, and a
|
||||
* burst of about a hundred characters or more is taken as a paste, so its trailing
|
||||
* `\r` lands as a NEWLINE in the composer and the prompt sits there unsent. A later
|
||||
* bare `\r` written the same way does not recover it; a tmux `send-keys Enter` does.
|
||||
* Short bursts (tens of characters) submit, which is why the failure looked random.
|
||||
*
|
||||
* Anything with another control character (escape sequences, a bracketed-paste frame,
|
||||
* a line feed, a tab, C1 controls) is raw terminal input and keeps the direct write.
|
||||
*/
|
||||
export function isPlainPromptInput(input: string): boolean {
|
||||
// eslint-disable-next-line no-control-regex -- matching control characters is the point
|
||||
return /^[^\x00-\x1f\x7f-\x9f]+\r$/.test(input);
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist session state and broadcast a SessionUpdated event.
|
||||
* Replaces the repeated two-line pattern across route handlers.
|
||||
|
||||
@@ -91,6 +91,7 @@ import {
|
||||
findSessionOrFail,
|
||||
getAuthUser,
|
||||
isAdmin,
|
||||
isPlainPromptInput,
|
||||
isWorkingDirAllowed,
|
||||
ownerFor,
|
||||
parseBody,
|
||||
@@ -1835,10 +1836,18 @@ export function registerSessionRoutes(
|
||||
// the wrong recovery — wait longer, when the truth is "restart the worker".
|
||||
let delivered = false;
|
||||
|
||||
// A plain prompt (`<text>\r`) goes through the mux even when the caller did not
|
||||
// ask for it: written straight into the pane it arrives as one burst, and Claude
|
||||
// Code takes a long burst as a paste whose `\r` becomes a newline, so the prompt
|
||||
// sat unsent (see isPlainPromptInput). The mux path types the text, presses Enter
|
||||
// separately and arms the SubmitVerifier. An explicit `useMux: false` keeps the
|
||||
// raw write for a caller that really wants it.
|
||||
const autoMux = useMux === undefined && isPlainPromptInput(inputStr);
|
||||
|
||||
if (duplicate) {
|
||||
// Redelivery of an already-applied input: skip the write, but still honor the
|
||||
// wait, since the caller's question ("tell me when this settles") is unanswered.
|
||||
} else if (useMux && waitPromise) {
|
||||
} else if ((useMux || autoMux) && waitPromise) {
|
||||
// The response is already staying open for the wait, so the tmux write can be
|
||||
// awaited here. This is the ONE path where a writeViaMux failure is observable.
|
||||
const ok = await session.writeViaMux(inputStr, { fromUser: true }).catch(() => false);
|
||||
@@ -1849,6 +1858,16 @@ export function registerSessionRoutes(
|
||||
delivered = session.write(inputStr, { fromUser: true });
|
||||
if (!delivered) undoOnFailure();
|
||||
}
|
||||
} else if (autoMux) {
|
||||
// Awaited, unlike the explicit useMux branch below. This shape also reaches here
|
||||
// from the browser's POST fallback, which sends its frames one at a time and
|
||||
// waits for each 2xx; answering only once Enter has gone out is what keeps the
|
||||
// next keystroke from overtaking it.
|
||||
const ok = await session.writeViaMux(inputStr, { fromUser: true }).catch(() => false);
|
||||
if (!ok) {
|
||||
console.warn(`[Server] writeViaMux failed for session ${id}, falling back to direct write`);
|
||||
if (!session.write(inputStr, { fromUser: true })) undoOnFailure();
|
||||
}
|
||||
} else if (useMux) {
|
||||
// Fire-and-forget: don't block the HTTP response on a tmux child process.
|
||||
// Fallback to a direct write on failure. Unchanged from before send-and-wait.
|
||||
|
||||
Reference in New Issue
Block a user