diff --git a/src/cron/cron-service.ts b/src/cron/cron-service.ts index 7b0a6599..b1d33cd6 100644 --- a/src/cron/cron-service.ts +++ b/src/cron/cron-service.ts @@ -536,11 +536,11 @@ export class CronService { } /** - * Resolves the prompt text and enforces the single-line constraint: prompt - * delivery rides writeViaMux/PTY writes where a newline is Enter, so a - * multi-line prompt would be silently corrupted (typed mode fuses lines, - * paste mode submits the first line and dribbles the rest in as separate - * messages). Rather than mangle an unattended agent's instructions, fail the + * Resolves the prompt text and enforces the single-line constraint: a job + * fires unattended, and the submit check that re-presses an ignored Enter + * cannot recognise a multi-line paste in the composer (typed mode), so a + * stranded prompt would go unnoticed; in paste mode the newlines reach the + * CLI as keys. Rather than risk an unattended agent's instructions, fail the * run with a clear error. A prompt FILE may end with trailing newline(s) * (every editor writes one) — those are stripped before the check. */ diff --git a/src/tmux-manager.ts b/src/tmux-manager.ts index 8364cbeb..53cd3c89 100644 --- a/src/tmux-manager.ts +++ b/src/tmux-manager.ts @@ -4019,7 +4019,9 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer { /** * Send input to a specific pane within a tmux session. - * Uses the same literal text approach as sendInput() but targets a specific pane. + * Types the text literally with `send-keys -l`, line breaks stripped, then Enter + * when the input carried a `\r`. Unlike sendInput(), it neither pastes nor keeps + * the line breaks of multi-line text. */ sendInputToPane(muxName: string, paneTarget: string, input: string): boolean { if (IS_TEST_MODE) return true; diff --git a/src/tui/tui-client.ts b/src/tui/tui-client.ts index a78bf453..c4f035af 100644 --- a/src/tui/tui-client.ts +++ b/src/tui/tui-client.ts @@ -704,9 +704,9 @@ export class TuiClient { * * Two hard rules from CLAUDE.md, both enforced here so no caller can get them * wrong: the payload must END with `\r` or the server never issues Enter and - * the text sits unsubmitted, and embedded newlines are stripped rather than - * sent (the server would paste them as one block, which its submit check - * cannot reliably recognise in the composer). + * the text sits unsubmitted, and embedded newlines become spaces rather than + * being sent (the server would paste them as one block, which its submit + * check cannot reliably recognise in the composer). * The `clientId`/`seq` pair makes delivery exactly-once, so a retry after a * dropped connection cannot type the prompt twice. */ diff --git a/test/tui/tui-client.test.ts b/test/tui/tui-client.test.ts index 59ab9844..8b78fdb8 100644 --- a/test/tui/tui-client.test.ts +++ b/test/tui/tui-client.test.ts @@ -327,7 +327,7 @@ describe('TuiClient.sendInput', () => { expect(JSON.parse(recorded[0].body)).toMatchObject({ input: 'hello world\r', useMux: true }); }); - it('collapses embedded newlines into spaces (multi-line breaks Ink)', async () => { + it('collapses embedded newlines into spaces (a multi-line paste cannot be confirmed as submitted)', async () => { await client().sendInput('abc', 'echo A\necho B\r\nline three '); expect(JSON.parse(recorded[0].body).input).toBe('echo A echo B line three\r'); });