Skip to content

fix(tmux): submit quick action prompts via Enter keycode after paste#267

Merged
dhilgaertner merged 1 commit into
mainfrom
feature/crow-264-tmux-quick-action-enter
May 14, 2026
Merged

fix(tmux): submit quick action prompts via Enter keycode after paste#267
dhilgaertner merged 1 commit into
mainfrom
feature/crow-264-tmux-quick-action-enter

Conversation

@dhilgaertner
Copy link
Copy Markdown
Contributor

Summary

Quick action prompts (Merge PR, Address Changes, Fix Checks, Fix Conflicts) end with a trailing `\n` so they auto-submit in Claude Code's input. The Ghostty backend honored that contract by splitting on `\n` and emitting a synthetic Return keycode (36) — those events fire outside any bracketed-paste bracket, so the TUI saw them as real Enter keys.

The tmux backend shipped the entire string through `tmux load-buffer` + `tmux paste-buffer`. Claude Code's TUI enables bracketed-paste mode, so tmux wrapped the buffer in `\e[200~ … \e[201~` and the trailing `\n` was delivered as literal text — typed as a newline character in the input field, never as Enter. Prompt visible, never submitted.

Changes

  • `TmuxController.sendKeys(target:keys:)` — thin wrapper over `tmux send-keys -t <keys...>` reusing the existing `run` watchdog/timeout path.
  • `TmuxBackend.sendText` — strip the single trailing `\n` from the paste payload, then send a separate `Enter` via `sendKeys` after `paste-buffer` completes (outside the bracket). Internal newlines are preserved as-is; an empty-after-strip payload skips the paste entirely (some tmux builds reject empty `load-buffer` input).

Ghostty path is untouched.

Test plan

  • `make build` — green
  • `swift test` for the CrowTerminal package — all 24 tests pass, including `TmuxBackendTests.sendTextRoundTripsThroughBuffer`
  • Manual: with `experimentalTmuxBackend: true`, click Fix Conflicts / Merge PR on a PR session and confirm the prompt is submitted automatically
  • Manual regression: same flow with `experimentalTmuxBackend: false` (Ghostty) — behavior unchanged

Closes #264

Quick action prompts (Merge PR, Address Changes, Fix Checks, Fix
Conflicts) ended with `\n` so they would auto-submit in Claude Code's
input. The Ghostty backend honored that contract by splitting on `\n`
and emitting a synthetic Return keycode (36) after each segment — those
events fire outside any bracketed-paste bracket, so the TUI saw them as
real Enter keys.

The tmux backend, however, shipped the entire string through
`load-buffer` + `paste-buffer`. Claude Code's TUI enables bracketed-paste
mode, so tmux wrapped the buffer in `\e[200~…\e[201~` and the trailing
`\n` was delivered as literal text — typed as a newline character in the
input field, never as Enter. Prompt visible, never submitted.

Strip the trailing `\n` from the paste payload, then deliver a separate
`Enter` via `tmux send-keys` *after* `paste-buffer` finishes (outside
the bracket). Internal newlines are preserved as-is, matching Ghostty's
per-line semantics. Empty-after-strip payloads skip the paste entirely
since `load-buffer` rejects empty input on some tmux builds.

Closes #264
Copy link
Copy Markdown
Collaborator

@dgershman dgershman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code & Security Review

Critical Issues

None.

Security Review

Strengths:

  • No user-controlled data reaches shell execution — sendKeys passes ["Enter"] as a literal tmux key name, not raw shell input.
  • Buffer names are derived from UUID, no injection vector via tmux load-buffer / paste-buffer.
  • The existing run() watchdog/timeout path is correctly reused for the new sendKeys method, so a hung tmux server can't block the main actor.

Concerns:

  • None identified. The change stays within the existing trust boundary (Crow → tmux IPC over a private socket).

Code Quality

Well done:

  • The doc comment on sendText (TmuxBackend.swift:244-251) clearly explains the bracketed-paste root cause and references the Ghostty parity (keycode 36). Excellent for future maintainers.
  • Empty-payload guard (if !payload.isEmpty) correctly avoids sending an empty load-buffer, which some tmux builds reject — good defensive coding.
  • sendKeys in TmuxController is a thin, general-purpose wrapper (send-keys -t <target> <keys...>) that will be reusable for future key-event needs (e.g., C-c, Escape).
  • The defer { ctrl.deleteBuffer(...) } stays inside the if !payload.isEmpty block, so the buffer is only cleaned up when it was actually created.

Observations (non-blocking):

  • sendText only strips a single trailing \n (text.dropLast()). If a caller sends "hello\n\n", only one \n is stripped and the other is pasted as literal text inside the bracket. This matches the Ghostty backend's behavior (which splits on every \n and sends a keycode-36 for each), so multi-newline strings will behave differently between backends. Not a concern for the quick-action use case (always exactly one trailing \n), but worth noting if sendText is later used for multi-line submission. Current callers at registerTerminal (line 183: command + "\n") and crow send all append a single \n, so this is fine today.
  • The existing test sendTextRoundTripsThroughBuffer (TmuxBackendTests.swift:103) sends a payload without a trailing newline, so it exercises the paste-only path but not the new Enter-after-paste path. A test case like sendText(id: id, text: payload + "\n") would cover the new branch. Low risk given the simplicity, but would strengthen regression coverage.

Summary Table

Priority Issue
Green Consider adding a test case for the trailing-newline + Enter path
Green Document multi-newline behavior difference vs. Ghostty if sendText scope widens

Recommendation: Approve — clean, focused fix that correctly addresses the bracketed-paste issue (#264). The approach mirrors what the Ghostty backend already does, the code is well-commented, and there are no security concerns.

🤖 Reviewed by Crow via Claude Code

@dhilgaertner dhilgaertner merged commit 15551b3 into main May 14, 2026
3 checks passed
@dhilgaertner dhilgaertner deleted the feature/crow-264-tmux-quick-action-enter branch May 14, 2026 22:16
dgershman added a commit that referenced this pull request May 15, 2026
…#274)

Closes #272

## Summary

- Adds a 50ms delay between `paste-buffer` and `send-keys Enter` in
`TmuxBackend.sendText()` to prevent a timing race where the Enter
keystroke arrives before the TUI has finished processing the
bracketed-paste bracket-end sequence
- Only applies the delay when content was actually pasted (bare
Enter-only sends skip the delay)
- Adds two integration tests covering the trailing-newline paste+Enter
path and the bare-Enter edge case

## Context

PR #267 fixed quick-action prompts not submitting by splitting the
trailing `\n` into a separate `send-keys Enter` after the paste. The
same fix covers auto-respond prompts (same code path), but a timing race
between the two tmux CLI invocations can cause the Enter to be silently
dropped when the terminal has been idle — which is the typical state
when auto-respond fires.

## Test plan

- [ ] `swift build` compiles without errors
- [ ] `swift test` passes (existing + new `TmuxBackendTests`)
- [ ] Manual: create a tmux-backed session with a PR that has failing
CI, verify the auto-respond prompt is submitted automatically

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(tmux): quick action prompts not submitted — trailing \n inside bracketed paste

2 participants