Cancel tmux copy-mode before paste-buffer in TmuxBackend.sendText#488
Conversation
Crow's bundled crow-tmux.conf keeps `mouse on` so wheel scrollback works through the cockpit (#452). The side effect is that tmux's default WheelUpPane puts the pane into copy-mode, where `paste-buffer` does not deliver content into the underlying shell. Programmatic sends — Manager paste, auto-respond prompts, quick actions — silently fail whenever the user happened to scroll first. Fix the send path: pre-cancel copy-mode via `if-shell -F '#{pane_in_mode}' 'send-keys -X cancel'` before paste-buffer. Added as a named helper `TmuxController.cancelCopyModeIfActive(target:)` so the intent reads locally and is easy to extend. Also pin WheelUpPane to `copy-mode -e` in crow-tmux.conf so the pane auto-exits copy-mode once wheel-down scrolls past the bottom of history. Matches tmux ≥3.0 defaults; making it explicit insulates Crow from future upstream changes and minimizes time-in-copy-mode. Tests: two real-tmux integration tests in TmuxBackendTests — one forces the pane into copy-mode before sendText and asserts pane_in_mode flips back to 0; one sanity-checks that the if-shell guard is a no-op on a normal pane (no false-failure on every send). Follow-up (deferred to a separate PR): the keystroke-trap leg of #486 — auto-exit copy-mode on a printable keyDown in GhosttySurfaceView. That needs a tmux control-mode `%mode-changed` parser the codebase doesn't have yet; orthogonal to the paste fix this PR closes. 🐦⬛ Generated with Claude Code, orchestrated by Crow Co-Authored-By: Claude <[email protected]> Crow-Session: 0E27BE27-E0BC-45A4-BFA1-593A2D112BE2
dhilgaertner
left a comment
There was a problem hiding this comment.
Code & Security Review
Critical Issues
None.
Security Review
Strengths:
- No injection surface in the new
if-shellcommand.targetis built only fromTmuxBackend.cockpitSessionName(a constant) and an integerwindowIndex; even though it's string-interpolated into thesend-keys -t <target> -X cancelargument, there is no attacker-controlled input. - The
if-shell -F '#{pane_in_mode}'guard is semantically correct: with-F, tmux treats"0"(the not-in-mode value) as false, so thesend-keys -X cancelonly fires when the pane is actually in a mode — avoiding thesend-keys -X"not in a mode" error in the common path. crow-tmux.confWheelUpPanerebind faithfully mirrors the tmux ≥3.0 default and the existingDoubleClick1Pane/TripleClick1Panebrace-block pattern already in the file (copy-mode -et===copy-mode -e -t =, consistent with the file's-t =convention).
Concerns:
- None.
Code Quality
Yellow — copy-mode cancel is skipped on the bare-Enter (Enter-only) send path (TmuxBackend.swift:340-358)
cancelCopyModeIfActive(target:) is called inside the if !payload.isEmpty { ... } block. When sendText is invoked with text == \"\n\" (payload empties to "", didPaste stays false), the cancel is skipped and the Enter is delivered via ctrl.sendKeys(target:, keys: [\"Enter\"]). A plain send-keys Enter into a pane that's in copy-mode is routed through the copy-mode key table (default emacs: copy-selection-and-cancel), so it exits copy-mode but never delivers a carriage return to the underlying shell — the same silent-drop failure mode this PR fixes for paste.
This contradicts the new doc comment's claim that "every programmatic send into a pane the user has scrolled ... is silently dropped" is fixed — the Enter-only submit path still is. crow send --terminal <id> \"\n\" (and TmuxBackendTests.swift:225 exercises exactly this bare-\n shape) hits it. Auto-respond / quick actions append text+\n (non-empty payload), so they're covered; pure-Enter submits are not.
Recommendation: hoist cancelCopyModeIfActive(target:) above the paste/Enter split so it runs for any delivery into the pane (it's one cheap if-shell call), or duplicate it before the sendKeys([\"Enter\"]). Then the doc comment's contract holds for all send shapes.
Green — cosmetic: the new WheelUpPane line spells out if-shell while the neighboring MouseDown1Pane/DoubleClick1Pane bindings use the if alias. Harmless; consider matching the surrounding style for grep-consistency.
Tests
sendTextCancelsCopyModeBeforePasteandsendTextOnNormalPaneIsUnaffectedByCancelGuardare well-scoped real-tmux integration tests covering the in-mode and no-op-guard paths. A test for the bare-Enter-into-copy-mode case would have caught the gap above.- I did not re-run the suite: a local build needs the GhosttyKit xcframework (
make ghostty) and the integration tests require a real tmux. Relying on the PR's stated "all 63 tests pass" for the green run.
Summary Table
| Color | Meaning | Verdict effect |
|---|---|---|
| Red | Must fix | Request changes |
| Yellow | Should fix | Request changes |
| Green | Consider | Approve allowed |
Recommendation: Request Changes — driven by [0 Red, 1 Yellow, 1 Green] findings. The core paste fix is correct and well-tested; addressing the Enter-only path (or narrowing the doc comment's claim) closes it out in this round trip.
Reviewer (PR #488) caught that cancelCopyModeIfActive was nested inside the `if !payload.isEmpty` block, so the bare-Enter path (text == "\n", payload empties to "", didPaste stays false) skipped the cancel. A plain `send-keys Enter` into a copy-mode pane is routed through the copy-mode key table (emacs `copy-selection-and-cancel`, vi `cancel`) — exits copy-mode but never delivers a CR to the shell. Same silent-drop failure mode the paste path had before this PR. Hoist the cancel above the paste/Enter split so it runs for every send shape (paste-only, paste+Enter, Enter-only). Updated the doc comment to spell out the bare-Enter path too. Add a regression test sendTextBareEnterCancelsCopyMode that forces the pane into copy-mode then verifies `sendText(id, "\n")` leaves it. Also cosmetic: switch the new WheelUpPane binding from `if-shell` to the `if` alias to match the surrounding MouseDown1Pane / DoubleClick1Pane style in crow-tmux.conf. 🐦⬛ Generated with Claude Code, orchestrated by Crow Co-Authored-By: Claude <[email protected]> Crow-Session: 0E27BE27-E0BC-45A4-BFA1-593A2D112BE2
dhilgaertner
left a comment
There was a problem hiding this comment.
Code & Security Review
Critical Issues
None.
Security Review
Strengths:
- No command-injection surface.
TmuxController.cancelCopyModeIfActiveinterpolates onlytarget, which is derived from the constantcockpitSessionName("crow-cockpit") and an integerwindowIndex— never from user input. The user'stextreaches tmux vialoadBufferFromStdin(stdin) andpaste-buffer, not via argv, so the payload can't influence command parsing. - The
if-shell -F '#{pane_in_mode}'guard meanssend-keys -X cancelonly fires when the pane is actually in a mode, avoiding the error tmux raises otherwise.
Code Quality
- Correct hoisting of the cancel (
TmuxBackend.swift:347). PlacingcancelCopyModeIfActiveabove theif !payload.isEmpty/endsWithNewlinesplit is the key correctness point: it covers the bare-\npath wheresend-keys Enterwould otherwise be eaten by the copy-mode key table (emacscopy-selection-and-cancel/ vicancel) and never reach the shell. The newsendTextBareEnterCancelsCopyModetest locks this in. - Ordering is safe. Each
ctrl.run(...)is a separate synchronous tmux client invocation, so the server has processed the cancel beforepaste-bufferis issued — no race between cancel and paste. crow-tmux.confWheelUpPane rebind mirrors the existing MouseDown/DoubleClick/TripleClick binding structure and matches the tmux ≥3.0 default; pinning it is reasonable insurance. Note it only takes effect for tmux servers started after this change (config is read at server start / reload), which is expected behavior, not a regression.- Doc comments are thorough and accurate about the failure mode and the fix.
Green (consider only):
sendTextCancelsCopyModeBeforePasteassertspane_in_modeflips back to0, which proves the cancel ran, but doesn't assert the pasted token actually landed in the pane (e.g. via acapture-panecheck). The cancel behavior is the regression being fixed, so this is fine; an end-to-end content assertion would just make the test name's "...BeforePaste" promise airtight. Covered by the manual test plan.- Minor: the inline comment block at
TmuxBackend.sendText(lines ~341-346) has an unbalanced(where ...parenthesis. Cosmetic. - The keystroke-trap leg of #486 (auto-exit copy-mode on printable keyDown) is intentionally deferred with a clear rationale and a planned follow-up. The paste/auto-respond/quick-action symptom named in the ticket is fixed here.
Summary Table
| Color | Meaning | Verdict effect |
|---|---|---|
| Red | Must fix | Request changes |
| Yellow | Should fix | Request changes |
| Green | Consider | Approve allowed |
Recommendation: Approve — driven by [0 Red, 0 Yellow, 3 Green] findings. Tight, well-tested, well-documented fix with no security or correctness blockers.
Note: I could not run the build/tests locally because GhosttyKit.xcframework is not present in this checkout (requires make ghostty); the review is by inspection. The PR reports all 63 tests passing.
Closes #486
Summary
Crow's bundled
crow-tmux.confkeepsmouse onso wheel scrollback works through the cockpit (#452). The side effect is that tmux's defaultWheelUpPaneputs the pane into copy-mode, wherepaste-bufferdoes not deliver content into the underlying shell. Programmatic sends — Manager paste, auto-respond prompts, quick actions — silently fail whenever the user happened to scroll first.This PR fixes the send path: pre-cancel copy-mode before
paste-bufferviatmux if-shell -F '#{pane_in_mode}' 'send-keys -X cancel'. Theif-shellguard keeps it a no-op in the common case (pane not in a mode).Changes
TmuxController.cancelCopyModeIfActive(target:)— new helper wrapping theif-shellguard so the intent reads locally.TmuxBackend.sendText— calls the helper afterloadBufferFromStdinand beforepasteBuffer. Doc comment extended to document why.crow-tmux.conf— explicitWheelUpPanerebind usingcopy-mode -e. Matches tmux ≥3.0 defaults but pinning it insulates Crow from upstream changes and minimizes time-in-copy-mode (the-eauto-exits when wheel-down scrolls past the bottom of history).TmuxBackendTests:sendTextCancelsCopyModeBeforePasteforces the pane into copy-mode via a side-channel controller, then assertspane_in_modeflips back to0aftersendText.sendTextOnNormalPaneIsUnaffectedByCancelGuardsanity-checks theif-shellno-op path so a normal-state pane doesn't fail.Deferred (intentional)
The keystroke-trap leg of #486 — auto-exit copy-mode on a printable keyDown in
GhosttySurfaceView— is not addressed here. It needs a tmux control-mode%mode-changedparser the codebase doesn't have yet; building that infrastructure is orthogonal to the paste fix this PR closes. I'll file a follow-up issue against this PR.The user-typing trap is still present in this PR (scroll up, then start typing → keystrokes eaten until Escape). The Manager-paste / auto-respond / quick-action paste bug — the symptom the ticket title actually names — is fixed.
Test plan
make ghosttythenarch -arm64 swift test --package-path Packages/CrowTerminal→ all 63 tests in 9 suites pass.crow send --session 00000000-0000-0000-0000-000000000000 --terminal <uuid> "test\n"from another shell — text lands in the prompt and submits.🐦⬛ Generated with Claude Code, orchestrated by Crow