From 0f7bf659cba0931e98ee1dcf689e685af3da5854 Mon Sep 17 00:00:00 2001 From: Danny Gershman Date: Thu, 14 May 2026 22:15:58 -0400 Subject: [PATCH] fix(tmux): add delay between paste and Enter for auto-respond prompts (#272) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When auto-respond detects failing CI and sends a prompt to Claude Code, the paste-buffer and send-keys Enter commands can race: the TUI hasn't finished processing the bracket-end sequence before Enter arrives, causing the keystroke to be silently dropped. Add a 50ms delay between paste and Enter to give the TUI time to process, matching the pattern that works reliably for quick actions. 🤖 Generated with Claude Code, orchestrated by Crow Co-Authored-By: Claude Crow-Session: F9344639-17E5-4B0E-81C0-ED59F8B0872A --- .../Sources/CrowTerminal/TmuxBackend.swift | 22 +++++++++--- .../CrowTerminalTests/TmuxBackendTests.swift | 36 +++++++++++++++++++ 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/Packages/CrowTerminal/Sources/CrowTerminal/TmuxBackend.swift b/Packages/CrowTerminal/Sources/CrowTerminal/TmuxBackend.swift index a82ff1f..ad7168e 100644 --- a/Packages/CrowTerminal/Sources/CrowTerminal/TmuxBackend.swift +++ b/Packages/CrowTerminal/Sources/CrowTerminal/TmuxBackend.swift @@ -245,10 +245,16 @@ public final class TmuxBackend { /// Quirk: Claude Code's TUI enables bracketed-paste mode, which wraps /// `paste-buffer` output in `\e[200~…\e[201~`. A trailing `\n` inside the /// bracket is treated as literal text, not as Enter — so prompts that - /// rely on `\n` to submit (quick actions: Merge PR, Fix Conflicts, …) get - /// pasted but never submitted (#264). Strip the trailing newline before - /// pasting and deliver a separate `Enter` via `send-keys` afterwards, - /// mirroring what `GhosttySurfaceView.writeText` does with keycode 36. + /// rely on `\n` to submit (quick actions, auto-respond) get pasted but + /// never submitted (#264). Strip the trailing newline before pasting and + /// deliver a separate `Enter` via `send-keys` afterwards, mirroring what + /// `GhosttySurfaceView.writeText` does with keycode 36. + /// + /// A 50ms delay between the paste and the Enter keystroke gives the TUI + /// time to process the bracket-end sequence (`\e[201~`). Without this, + /// auto-respond prompts (which fire when the terminal has been idle) can + /// race: the Enter arrives before the TUI finishes handling the paste, + /// causing it to be silently dropped (#272). public func sendText(id: UUID, text: String) throws { guard let windowIndex = bindings[id] else { throw TmuxBackendError.unknownTerminal(id) @@ -259,13 +265,21 @@ public final class TmuxBackend { let endsWithNewline = text.hasSuffix("\n") let payload = endsWithNewline ? String(text.dropLast()) : text + var didPaste = false if !payload.isEmpty { let bufferName = "crow-\(id.uuidString)" try ctrl.loadBufferFromStdin(name: bufferName, data: Data(payload.utf8)) defer { ctrl.deleteBuffer(name: bufferName) } try ctrl.pasteBuffer(name: bufferName, target: target) + didPaste = true } if endsWithNewline { + // Give the TUI time to process the paste bracket-end before + // the Enter key arrives. Only needed when we actually pasted + // content — a bare "\n" (Enter-only) needs no delay. + if didPaste { + Thread.sleep(forTimeInterval: 0.05) + } try ctrl.sendKeys(target: target, keys: ["Enter"]) } } catch { diff --git a/Packages/CrowTerminal/Tests/CrowTerminalTests/TmuxBackendTests.swift b/Packages/CrowTerminal/Tests/CrowTerminalTests/TmuxBackendTests.swift index d4ef8d8..77a024c 100644 --- a/Packages/CrowTerminal/Tests/CrowTerminalTests/TmuxBackendTests.swift +++ b/Packages/CrowTerminal/Tests/CrowTerminalTests/TmuxBackendTests.swift @@ -125,6 +125,42 @@ struct TmuxBackendTests { #expect(binding.windowIndex >= 0) } + @Test func sendTextWithTrailingNewlineDoesNotThrow() throws { + let backend = makeBackend() + defer { backend.shutdown() } + + let id = UUID() + _ = try backend.registerTerminal( + id: id, + name: "newline-test", + cwd: NSHomeDirectory(), + command: nil, + trackReadiness: false + ) + + // Trailing \n triggers the paste + separate Enter path (#264/#272). + // Verify the full code path executes without throwing. + let payload = "AUTO-RESPOND-TEST-\(UUID().uuidString)\n" + try backend.sendText(id: id, text: payload) + } + + @Test func sendTextEmptyWithNewlineDoesNotThrow() throws { + let backend = makeBackend() + defer { backend.shutdown() } + + let id = UUID() + _ = try backend.registerTerminal( + id: id, + name: "bare-enter", + cwd: NSHomeDirectory(), + command: nil, + trackReadiness: false + ) + + // Edge case: bare "\n" should only send Enter (no paste). + try backend.sendText(id: id, text: "\n") + } + @Test func retryReadinessEmitsTimedOutWhenSentinelMissing() async throws { let backend = makeBackend() defer { backend.shutdown() }