Prototype: hand ctrl-r to a shell-native wrapper widget via raw keypress (CORE-3807) - #15519
Prototype: hand ctrl-r to a shell-native wrapper widget via raw keypress (CORE-3807)#15519warp-agent-staging[bot] wants to merge 25 commits into
Conversation
…for all shells CORE-3807. Alternative mechanism to the foreground-command handoff in #15513.
…ess (CORE-3807) Alternative to the foreground-command handoff in PR #15513. Instead of submitting a fake command to get the shell to run the user's ^R widget (fzf/atuin), this writes a private key sequence (ctrl-x ctrl-r) to the pty. Each shell's bootstrap installs a wrapper on that sequence which re-invokes whatever is actually bound to ^R from inside a genuine readline/zle/bind context, then reports the resulting line buffer back over a new ExternalCtrlRRawKeypressSelection DCS hook. Nothing is ever submitted to the shell as a real command, so there is no foreground command to hide from history/output and no in-band marker to race. Client-side: - New RawKeypressCtrlRHandoff feature flag (dogfood-enabled). - New ExternalCtrlRRawKeypressSelection DCS hook end-to-end (event, handler, ansi dispatch). - Block gets a raw_keypress_forward_active flag that plugs into is_active_and_long_running(), so the forwarded TUI renders in the PromptAndCommand grid exactly like a real long-running command. - TerminalView::maybe_trigger_raw_keypress_ctrl_r_handoff hands ctrl-r off when the session's shell reports the external_ctrl_r_raw_keypress shell_plugins tag; Workspace wires it in ahead of opening command search. Shell bootstrap (bash/zsh/fish): detect the user's real ^R binding at bootstrap time (once), install the wrapper on ctrl-x ctrl-r only -- the user's actual ^R binding is never touched or replaced, so re-sourcing the bootstrap (e.g. a subshell) cannot corrupt or recurse it. Report external_ctrl_r_raw_keypress via shell_plugins when installed. Also fixes two pre-existing, independent bugs in bash's warp_bootstrapped() that were blocking end-to-end verification: the non-MSYS2 Bootstrapped JSON payload never actually included shell_plugins despite computing it, and "$shell_plugins" (bash array in scalar context) only expanded to the array's first element. Matches the fix already in #15518, which was found and opened independently; see PR description.
|
This PR was generated with Warp. Comment |
There was a problem hiding this comment.
Overview
Adds an alternative ctrl-r handoff that invokes the user's own history widget through a private key sequence, as a parallel option to #15513. One finding needs a product decision before this direction can be adopted.
Concerns
- Invoking the user's real atuin widget preserves atuin's
enter_acceptbehavior, which is on by default: fish's_atuin_searchcallscommandline -f execute, bash's__atuin_historyreaches__atuin_accept_line/eval, and zsh reacheszle accept-line. The selected command therefore runs, which contradicts the contract that a selection is restored into Warp's editor unexecuted, and the wrapper's later DCS and buffer clear cannot reliably undo an execution the widget already requested. This is not a local wrapper fix: it needs a decision between rejecting execution-capable configurations, reintroducing a tool-specific non-executing path, or accepting the changed behavior — and that choice is the crux of whether this mechanism or #15513's can meet the requirement.
Verdict
Checks: build pass, tests pass, CI red (required-results gate skips validation jobs on drafts), visual proof present
Found: 0 critical, 1 important, 0 suggestions, 0 nits
Separately identified implementation-level defects are being addressed directly and are not part of this review.
Responding as wilson: Open session · View in factory
…flag bail-out 1. Replace the private key sequence (ctrl-x ctrl-r) with Alt-] (ESC ]). "\C-x\C-r" collides with bash's built-in re-read-init-file. Alt-] was verified empirically (via direct bind/bindkey queries in real PTY sessions) to be genuinely unbound in bash (emacs, vi-insert, vi-command), zsh (emacs, viins, vicmd), and fish (default, insert). 2. Fix keymap coverage in all three shells. An unqualified bind/bindkey only installs into whichever keymap is current at that moment: bash's plain `bind -x` and fish's plain `bind` both only reached one keymap, silently stranding vi-mode users (confirmed for fish: fzf/atuin bind ctrl-r in "insert" mode too, which was never wrapped). Detection and wrapper installation are now done per-keymap (bash: emacs/vi-insert/vi-command; zsh: emacs/viins/vicmd; fish: default/insert), each re-invoking that keymap's own binding, so a tool bound in multiple keymaps (or only some of them, like atuin's vicmd fallback to "/") gets exactly the coverage it actually has. 3. Add a bail-out timer so raw_keypress_forward_active cannot stay set indefinitely if the wrapper is absent, rebound, or hangs -- this mechanism runs whatever the user has bound to ctrl-r and cannot assume it returns. A 5s timeout now force-ends the handoff and restores the input editor if no completion hook arrives. Verified: real PTY sessions (bash, zsh, fish) confirm per-keymap install and the new sequence work correctly, including a fish vi-insert-mode fzf run that previously produced no wrapper at all. Computer-use confirmed both a working top-level fish vi-mode handoff and the timeout bail-out recovering automatically after a deliberately hung wrapper.
…, occupied-key detection, regression tests - Report the raw-keypress ctrl-r plugin tag per keymap (not just once session-wide) in bash/zsh/fish, so a keymap lacking a real wrapper doesn't wrongly borrow another keymap's capability claim. - Check each keymap for a pre-existing Alt-] binding before installing the wrapper/fallback there, so a user's own binding is never silently clobbered. - Add RawKeypressCtrlRHandoffId to distinguish handoff attempts, so a stale bail-out timer from an earlier handoff can't tear down a newer one; add a post-timeout cooldown to guard against a late completion hook from a still-alive old wrapper. - Add regression tests covering plugin-tag gating, keyseq forwarding, stale-timeout rejection, cooldown, and selection application/ rejection for the raw-keypress ctrl-r handoff lifecycle.
… list The classify function only excluded the two zsh built-ins it named (history-incremental-search-backward and its pattern variant), missing zsh's *other* per-keymap ^R defaults: redisplay in viins and redo in vicmd (both confirmed live in stock zsh with 'bindkey -v' and no third-party tool installed). Either was accepted as if a real history tool had bound it, installing a live wrapper around a builtin, and that keymap's ctrl-r would report whatever was in the buffer as a 'selection' instead of falling through to Warp's command search. $widgets[<name>] is zsh's own classification (builtin, completion:..., or user:<function> only for a zle -N-registered widget, which is how every third-party ctrl-r tool installs itself). Requiring that classification instead of naming individual defaults rejects all of zsh's own ^R bindings, not just the ones a comment happened to name. Verified live: atuin-style setup (real widget in emacs/viins, '/' in vicmd) now correctly wraps emacs/viins and falls back in vicmd instead of wrapping zsh's own 'redo'; the pre-existing occupied-Alt-] and fzf (bound in all three keymaps) cases are unaffected.
Closes the completion-ID gap flagged in review: completions from the shell were previously verified only by session_id, so a stale completion from a superseded handoff attempt could be misapplied to a newer one. - Warp now pastes the handoff id (via bracketed paste, to avoid triggering per-character keybinding side effects) immediately before the private key sequence, and rejects a completion whose echoed token doesn't match the pending handoff. - bash/zsh/fish wrapper widgets capture the pasted token from the line buffer before invoking the user's real ctrl-r binding, and echo it back in the completion hook alongside the selection. - Adds Rust unit tests covering token match/mismatch. - Adds a shell-side PTY test matrix (app/assets/bundled/bootstrap/tests/) that drives real bash/zsh/fish to cover keymap/mode classification (including the zsh builtin-default fix from the previous round), the occupied-Alt-] case, the fallback path, and the full token/selection round trip through a real wrapper widget.
Root-causes the typed-filter-characters bug observed in computer-use verification: at the idle prompt the handoff always starts from, the input editor is focused. should_write_typed_chars_to_pty already exempted the handoff from its "block must have started" check, but nothing moved focus off the input editor when the handoff began, so a TypedCharacters event never reached TerminalView at all -- the still-focused input editor consumed it first. Control keys (arrows, Enter) were unaffected since they route via global keybindings independent of literal focus, which is why the wrapper widget appeared to work but couldn't be filtered by typing. maybe_trigger_raw_keypress_ctrl_r_handoff now calls redetermine_global_focus right after marking the block as forwarding, mirroring the same call end_raw_keypress_ctrl_r_handoff already makes on teardown (and the same pattern used elsewhere for the analogous long-running-command case). Adds a unit test that fails without the fix: focuses the input box to simulate the idle prompt, triggers the handoff, and asserts focus has moved off the input editor and that typed characters reach the pty instead of the input buffer.
…ermine_global_focus redetermine_global_focus has several early-return guards (open context menu, open modal, active onboarding callout) that are irrelevant here -- this handoff can only ever trigger while the input editor already holds keyboard focus, since its keybinding requires the Input context. Call focus_terminal directly so none of those guards can silently no-op the focus change.
… normal pauses The 5-second inactivity timeout fired during ordinary interactive use of the wrapper widget: a user pausing to read the filtered list before pressing Enter easily exceeds 5 seconds without touching a key. When the bail-out fires mid-read, it force-clears forwarding and restores focus to the (empty) input editor -- the Enter the user then presses to select an entry no longer reaches the pty at all, and instead submits the empty input editor buffer as a command, leaving a stray empty block behind. Verified via a real zsh + fzf PTY harness that three back-to-back handoffs in the same session complete correctly at bot speed (shell-side completion protocol is not at fault), which narrowed this down to the timeout firing during human-paced interaction. Bumped the timeout from 5s to 30s and added a regression test pinning the exact mechanism: once the (matching) timeout fires, a subsequent keystroke must route to the input editor, not the pty. Also committing the real-dispatch diagnostic test added while investigating the earlier typed-character routing question, which is stronger than the existing focus-only test and pins the (sound) tree-omission mechanism.
…submitting an empty command The 5s->30s timeout change made the premature bail-out rarer, not impossible: a user who reads a long history list, or glances away, can still exceed even a generous timeout without touching a key. When the bail-out fires, restoring focus to the (empty) input editor, an Enter the user believes is still going to the wrapper widget instead submits that empty buffer as a real command, leaving a stray empty block behind -- independent of how long the timeout is. Add a short-lived guard armed only on the bail-out path (not on normal completion, since a successful completion means the widget worked correctly): Block::arm_raw_keypress_bailout_guard()/consume_raw_keypress_bailout_guard(). Input::try_execute_command_from_source consumes it before executing an empty command, suppressing exactly one stray Enter without affecting a later, deliberate empty Enter (the guard self-expires after 2s and is one-shot). Added a regression test exercising the guard directly.
…the widget is alive Replaces the time-based bail-out with an evidence-based one. The 5s/30s timeout only ever needed to catch one failure mode -- nothing ever listening for the handoff at all (missing/rebound binding, dead shell) -- but it was reasoning about that from elapsed time, so a user legitimately reading the wrapper widget's already-painted output could still exceed it and trigger the same stray-empty-command symptom the stray-Enter guard was added for. Added Event::RawKeypressCtrlRHandoffOutputObserved, emitted from TerminalModel::on_finish_byte_processing the moment pty output is processed for the active block while a handoff is pending -- proof the wrapper widget actually started painting. Wired through ModelEventDispatcher/ModelEvent to TerminalView::note_raw_keypress_ctrl_r_handoff_output_observed, which cancels the pending bail-out timer outright (aborts it, does not reschedule). Once cancelled, there is no deadline at all for the rest of the handoff, no matter how long the user then reads or thinks -- the same as any other interactive program running in the terminal (a hung 'vim' isn't rescued either, and Ctrl-C still forwards). The timeout constant now only needs to bound time-to-first-output, so it's back down to 5s. The new event variant is only ever emitted while raw_keypress_forward_active is set, so it's inert for every other pty-output path. The stray-Enter guard from the previous commit is unaffected and still applies (bail-outs should become rare, not impossible). Added a regression test pinning the exact cancel-not-reschedule behavior. cargo test -p warp --lib raw_keypress: 12/12 passing. format/clippy clean.
…tion with a DCS 'started' signal; un-gate the feature flag Two fixes from a live-pass review of 2649198: 1. on_finish_byte_processing cancelled the bail-out timer on any non-empty pty output while forwarding was active. The shell's line editor echoes the pasted handoff token, so that echo alone cancelled the timer - meaning the one scenario the timer exists for (nothing listening for the handoff) is exactly the scenario where cancellation misfired, leaving the token stranded on the line with no way to recover short of ctrl-C. Replaced it with an explicit 'started' DCS hook (ExternalCtrlRRawKeypressStarted) that each shell wrapper sends the moment it captures the token and before invoking the real widget. This is positive evidence the widget ran; it can't be faked by an echo of our own paste. The hook is validated against the pending handoff's session_id and token before it cancels the timer, and fallback (immediate-report) paths don't send it since they don't need to. 2. app/Cargo.toml's default feature list included raw_keypress_ctrl_r_handoff, which unconditionally enables FeatureFlag::RawKeypressCtrlRHandoff for every channel/binary (independent of DOGFOOD_FLAGS), making the flag ungated. Removed it from default; the flag now only turns on where DOGFOOD_FLAGS applies it, as originally intended. Extended raw_keypress_ctrl_r_matrix.py to assert the started hook fires (with the matching token) before the selection hook on every real-widget round trip, and that fallback paths never emit it.
…on durable The 'started' signal cancelled the bail-out timer outright, but note_raw_keypress_ctrl_r_handoff_activity unconditionally re-armed a fresh timer on every keystroke forwarded to the pty, with no way to know the timer had already been permanently cancelled. So typing a filter query after the wrapper widget had already proven it was alive would still tear the handoff down 5 seconds later: type to filter, pause before Enter, and the selection is lost with the input editor left empty. This is the flow everybody actually uses, so the 'no deadline once cancelled' guarantee did not hold in practice. Added PendingRawKeypressCtrlRHandoff::started, set once a matching started report is validated. note_raw_keypress_ctrl_r_handoff_activity is now a no-op once started is set, so activity can never resurrect a timer that the started signal already cancelled. Added raw_keypress_ctrl_r_handoff_started_is_durable_against_activity, pinning that a forwarded keystroke after the started signal does not re-arm the timer.
Only advertise external_ctrl_r_raw_keypress when Alt-] is safely claimed in every relevant keymap/mode; otherwise preserve the user's binding and withhold the session-wide tag. Extend the bash/zsh/fish PTY matrix to assert tag gating and drive the occupied-map failure path. Trim narrative comments and correct the stale handoff-ID docs to reflect the echoed, validated token protocol.
…leanup Add a matrix case that occupies Alt-] in fish's vi insert mode (via fish_vi_key_bindings), asserting the tag is withheld and a real handoff into that mode produces no hook. Verified by mutation: reverting each of the four else-branches added last round (bash vi-insert, zsh emacs, fish default, fish insert) makes its corresponding new assertion fail, confirming none of them are vacuous. Also removed the remaining historical/call-site narration from the RAW_KEYPRESS_CTRL_R_HANDOFF_KEYSEQ and raw_keypress_ctrl_r_handoff_payload doc comments in view.rs.
The vi-insert collision case occupied insert while leaving default free, but only asserted the occupied mode. Without checking that default still gets a real wrapper, the case does not distinguish a partial collision from one where every mode is occupied, which is the situation the tag gate exists for. Also restores the rationale for rejecting bash's own C-x C-r on the key-sequence const, and describes the whole payload the trigger function returns rather than only the bracketed-paste wrapping.
… Alt-]' assertion Verifying the assertion added in 9101000 (as requested) found it wasn't actually load-bearing: it sent 'bind -M default ... && echo MARKER' and checked session.buf for MARKER, but an interactive shell echoes back a typed command as you type it regardless of whether it goes on to execute. Removing the production binding entirely (mutation test) still left session.buf containing the literal typed text 'echo MARKER', so the assertion passed unconditionally -- it could not have caught the regression it was added for. Confirmed by mutation: with the fish 'bind -M default ...' install commented out, the assertion still passed while 'bind -M default \x1b\x5d' itself reported 'No binding found'. Fixed by checking a file-redirected bind listing instead of session.buf, matching the pattern the pre-existing occupied-mode checks in this file already use for exactly this reason. Also added a second, independent assertion that actually drives a handoff in default mode while insert is occupied and checks for a real DCS hook, since that path is immune to the same class of bug by construction (a typed command can never fake a hook payload). Re-verified by mutation: with the same install removed, both the fixed listing-based assertion and the new hook-based assertion now correctly fail (5 failures total, matching every default-mode behavior the removed install was responsible for), and both pass again once restored. Matrix: 41/41 (was 40). Full build, cargo clippy --all-targets --tests, ./script/format, and cargo test --lib raw_keypress (14/14) all clean.
Replace six session.buf marker checks with file-redirected bind listings, since interactive shells echo the typed command itself into the transcript and make marker assertions unfalsifiable. Covers bash emacs wrapper and vi-command fallback, zsh emacs wrapper and viins builtin fallback, and fish default wrapper and preset fallback. Mutation-tested each assertion in the appropriate direction and confirmed it fails with real bind-listing detail; restored all shell scripts and re-ran the matrix at 41/41.
The explanation was repeated verbatim in the bash, zsh and fish real-widget checks. Keep it where it first applies and cross-reference it from the other two.
Keep the non-obvious rationale for why the free mode is asserted at all, and drop the restated echo-trap explanation and the narration of neighbouring cases.
…ress-handoff Resolves the modify/delete conflict on app/src/terminal/model/ansi/mod.rs, whose logic moved to crates/warp_terminal/src/model/ansi/mod.rs on master: reapplied the ExternalCtrlRRawKeypressSelection/Started dispatch arms at the new location and removed the old file. Also fixes a silent (non-conflicting) breakage the merge introduced: TerminalModel::external_ctrl_r_raw_keypress_selection/started still called event_proxy.send_terminal_event(), which master repointed at the narrower warp_terminal::event::Event type. Every sibling ansi::Handler method in this file already reports through send_app_event() for exactly this class of app-level event; switched these two to match. Adds two Performer-level dispatch tests (crates/warp_terminal/src/model/ansi/mod_tests.rs) that drive the real hex-encoded hook bytes through Processor::parse_bytes and assert each reaches its own Handler method, proving the relocated match arms still fire correctly and aren't swapped.
There was a problem hiding this comment.
Overview
Prototype handing ctrl-r to a shell-native wrapper widget via a raw keypress on a private key sequence. Two findings need a human decision before this is mergeable; the implementation itself reviewed clean.
Concerns
shell_integration_tests::test_ssh_wrapper_into_zshfails deterministically on this branch. The remote zsh receivesInitShell, transitions toScriptExecution, then never emitsBootstrapped— the block sits executing with empty output until the 20s assertion times out. It failed 9/9 on Linux and again on macOS, whiletest_ssh_wrapper_into_bashpassed in the same run against the same host and an unrelated PR passed the same test within ~90s, so the SSH/IAP layer was healthy. Not reproduced locally: the test VM is unreachable from our sandboxes, and a local PTY harness fed the same assembled script ran clean, so the trigger appears to need the real host (namedubuntu-14-04, whose stock zsh is 5.0.2).- The shell-side installation is not gated by the feature flag.
FeatureFlag::RawKeypressCtrlRHandoffgates only whether the client triggers a handoff; the Alt-]classification andbindkey/zle -Ninstallation inzsh_body.sh,bash_body.shandfish.shruns on every bootstrap of those shells regardless, and the cargo feature is not indefault. So a prototype that is compiled out still modifies every user's shell bootstrap. That is a design decision rather than a bug, and it is also the most likely reason the failure above is reachable at all.
Verdict
Checks: build pass, tests pass (11199/11199 unit), CI red (one integration test), visual proof present
Found: 0 critical, 2 important, 0 suggestions, 0 nits
Responding as wilson: Open session · View in factory
…cal assignment Root-caused #15519 CI's real (non-environmental) failure: shell_integration_tests::test_ssh_wrapper_into_zsh hung the remote zsh session forever, never reaching warp_bootstrapped, because the SSH test VM (ubuntu-14-04) runs zsh 5.0.2. On that version, `local var=$(cmd)` -- unlike plain `var=$(cmd)` -- applies filename generation to the unquoted substituted value; bindkey's own unbound-key output (`"^[]" undefined-key`) crashes that glob attempt with "not valid in this context: undefined-key", aborting the entire sourced bootstrap script right there. Everything after it, including the final warp_bootstrapped call, never runs. Reproduced against a real zsh 5.0.2 (Ubuntu 14.04's stock build, via Docker) by sourcing the exact assembled bootstrap script: it hit the identical error message and never emitted the Bootstrapped hook, both in a plain interactive session and in the login-shell (`-l -g`) mode the real SSH wrapper uses. Isolated it down to __warp_raw_keypress_ctrl_r_keyseq_free's `local result=$(bindkey ...)` line; the sibling __warp_classify_raw_keypress_ctrl_r_binding function is unaffected since its value comes through a parameter expansion, not a bare command substitution. Fix: quote the command substitution. Verified against the real zsh 5.0.2 container that this alone resolves both the local and login-shell repro (Bootstrapped hook now fires in both), and against the existing 41/41 raw_keypress_ctrl_r_matrix.py suite on the sandbox's zsh 5.9 for no regression (the suite doesn't itself cover zsh 5.0.2, since CI doesn't have one available outside the real SSH test VM).
The comment attributed the crash to filename generation, but the observed error is zsh's invalid-identifier-in-declaration error, not a glob failure: 'local undefined-key' reproduces 'not valid in this context: undefined-key' exactly, whereas a failed glob reports 'no matches found'. The cause is word-splitting making local declare each word.
|
Updating my review above: the first finding is root-caused and fixed, so only the second still needs a decision.
Reproduced in an I swept the rest of this branch's shell code for the same pattern and found no second instance. The second finding stands unchanged: the shell-side installation still runs on every bootstrap regardless of the feature flag, and that remains a design decision for you. It is also what made this bug reachable in the first place — a prototype that is compiled out still edited every zsh user's bootstrap. Responding as wilson: Open session · View in factory |
The live PTY matrix only ever exercises whatever zsh is installed in the sandbox running it (5.9 here), which can't reproduce the bug the previous commits fixed: on zsh <5.1 (e.g. 5.0.2, the CI SSH test VM's actual version), an unquoted 'local x=$(cmd)' word-splits the substitution's output before 'local' sees it, and bindkey's two-word unbound-key output then makes 'local' try to declare 'undefined-key' as a second local, which isn't a valid identifier -- aborting the rest of bootstrap. Add a static check that the fix's quoting doesn't regress, since no zsh available to this matrix can catch it at runtime. Independently reproduced and verified the fix (before: real zsh 5.0.2 via Docker never emits the Bootstrapped hook; after: it does, in ~2s) against a real Ubuntu 14.04 container's stock zsh package, corroborating the fix already on this branch.



Description
Prototype: alternative to the foreground-command handoff in #15513 for handing Ctrl-R off to a shell's native history widget (fzf/atuin), per CORE-3807.
Mechanism: instead of writing a fake foreground command for the shell to run the widget, Warp writes a per-handoff token (via bracketed paste) followed by a private key sequence (Alt-]) to the pty. Each shell's bootstrap detects whatever is actually bound to Ctrl-R in each of its keymaps (once, at bootstrap time) and installs a wrapper on that private sequence, in that same keymap, which re-invokes the real binding from inside a genuine readline/zle/bind context -- exactly as if the user had pressed Ctrl-R themselves. The wrapper captures the pasted token, then reads back the resulting line buffer and reports both to Warp over a new
ExternalCtrlRRawKeypressSelectionDCS hook, clearing the shell's own buffer so Warp owns the command from there. Warp only applies a completion whose token matches the handoff it started (see "Third review pass" below). Nothing is ever submitted to the shell as a real command -- there's no foreground command to hide from history/output, and no in-band marker to race.Update (sixth review pass): the "Fourth review pass" defect below (typed filter characters never reaching the picker) does not reproduce on current
HEAD-- it was a stale build plus the 5-second bail-out firing during slow, screenshot-paced manual testing, not the windowing-layer bug the fourth pass diagnosed. The tree-omission mechanism that hides the input editor during a handoff is sound and is now pinned by a test that drives a realPresenterdispatch cycle. A real, separate defect was found in a second ctrl-r attempt in the same pane, root-caused to that timeout being too aggressive for ordinary interactive use. The first fifth-pass fix merely lengthened it (5s -> 30s), which made the defect rarer rather than removing it. The final sixth-pass design replaces the guess about elapsed time with evidence about state: the 5-second timer now only bounds time-to-first-output and is cancelled outright -- not rescheduled -- as soon as any pty output proves the wrapper widget started, so there is no deadline at all for the rest of a live handoff. A separate one-shot guard prevents a stray Enter after a genuine bail-out from ever submitting an empty command. See "Fifth review pass" and "Sixth review pass" below. This prototype is no longer blocked on the fourth pass's diagnosis.The user's actual Ctrl-R binding is never touched or replaced (only shadowed on a separate, otherwise-unbound sequence), so re-sourcing the bootstrap (e.g. a subshell) cannot corrupt or recurse it.
Client-side changes:
RawKeypressCtrlRHandofffeature flag (dogfood-enabled; not a default cargo feature, see "Seventh review pass" below).ExternalCtrlRRawKeypressSelectionandExternalCtrlRRawKeypressStartedDCS hooks, end to end (event, handler, ansi dispatch), carrying a per-handoff completion token.Blockgets araw_keypress_forward_activeflag that plugs intois_active_and_long_running(), so the forwarded TUI renders in thePromptAndCommandgrid exactly like a real long-running foreground command -- this was the biggest open risk going in, see Testing below.TerminalView::maybe_trigger_raw_keypress_ctrl_r_handoffhands Ctrl-R off when the active session's shell reports theexternal_ctrl_r_raw_keypressshell_pluginstag;Workspacewires it in ahead of opening command search.RAW_KEYPRESS_CTRL_R_HANDOFF_TIMEOUT) bounds only time-to-first-evidence: each shell wrapper reportsExternalCtrlRRawKeypressStartedover DCS the moment it captures the paste and before invoking the real widget, and that (session/token-validated) signal cancels the timer outright -- not rescheduled -- so there is no deadline at all for the rest of that handoff. If no start signal ever arrives, the timer force-restores the editor. A separate short, one-shot guard suppresses an empty Enter immediately after that rare bail-out so a keystroke meant for the torn-down wrapper can never submit an empty command. See "Fixes from review", "Fifth review pass", "Sixth review pass", and "Seventh review pass".Note on a duplicate fix: while root-causing why detection wasn't triggering, I independently found and fixed two bugs in bash's
warp_bootstrapped()-- the non-MSYS2BootstrappedJSON payload never actually includedshell_pluginsdespite computing it, and"$shell_plugins"(a bash array in scalar context) only expands to the array's first element. This is already fixed on its own PR, #15518 (open, passed review), which the requester split out so it can land independently. My fix here is byte-for-byte the same change, kept on this branch only so this prototype is independently testable before #15518 lands; this diff should be dropped/rebased once #15518 merges, same as #15513 already does.Fixes from review
Review found three blocking issues, all now fixed and re-verified:
\C-x\C-rcollides with bash's built-inre-read-init-file. Replaced the private sequence with Alt-] (ESC ]), verified genuinely unbound (via directbind/bindkeyqueries in real PTY sessions, not just by inspection) in bash (emacs, vi-insert, vi-command), zsh (emacs, viins, vicmd), and fish (default, insert).bind/bindkeyonly installs into whichever keymap is current at that moment. Bash's plainbind -xand fish's plainbindboth only reached one keymap, so a vi-mode user's session could advertise the handoff tag while the wrapper was absent from the keymap they were actually in (confirmed for fish: fzf/atuin bind ctrl-r in "insert" mode too, which was never wrapped, silently stranding the keyboard). Detection and installation are now done per-keymap in all three shells (bash: emacs/vi-insert/vi-command; zsh: emacs/viins/vicmd; fish: default/insert), each re-invoking that specific keymap's own binding -- so a tool bound in only some keymaps (e.g. atuin falls back to/in zsh's vicmd, not ctrl-r) gets exactly the coverage it actually has, no more and no less.raw_keypress_forward_activeset indefinitely, hiding the input editor with no way back except closing the tab. Added a 5-second timer that force-ends the handoff and restores the input editor if no completion hook arrives, independent of whether the wrapper ever runs.Also investigated and explicitly out of scope per the requester's direction: atuin's
enter_accept = true(its default) executes the selected command from inside the widget itself before this mechanism's completion hook can intervene, since this design's whole premise is running the user's real widget -- the widget is what executes. This is a product-level trade-off between the two prototypes (see Comparison below), not a client-side bug to patch around.Second review pass: 4 more findings, all fixed
shell_pluginsremains one flag for the whole session), so the fix is shell-side: every keymap where Alt-] is free now gets something bound to it -- a real wrapper if ctrl-r resolves to a re-invokable tool there, otherwise an empty-completion fallback that reports back immediately. That guarantees the single flag is safe to advertise, because every keymap responds fast and deterministically instead of ever silently waiting out the 5-second bail-out. Verified live for the exact case this needs to cover: a zsh vi user with a widget bound to ctrl-r in emacs/viins but not vicmd (atuin's own default, which binds/instead) -- vicmd gets the fallback and reports back immediately, no stall.RawKeypressCtrlRHandoffId, a monotonic id minted per handoff attempt and captured in the timer closure; the timeout path now only tears down the handoff whose id it matches -- a stale timer is a no-op. At the time, this did not cover the completion path the same way, since the completion hook only carriedsession_id; that gap is closed below.app/src/terminal/view_tests.rs,App::testharness, exercisingTerminalViewdirectly) covering: plugin-tag gating, keyseq forwarding on trigger, a stale timer being ignored while the matching timer still tears the handoff down and starts the cooldown, the cooldown refusing a new handoff, a selection tagged with the wrong session being ignored, and a matching selection ending the handoff and populating the input buffer. These are unit-level and Rust-only; the shell-side keymap/occupied-key logic now has automated coverage too (see "Third review pass" below).bindkey -v, no third-party tool) that ctrl-r defaults toredisplayin viins andredoin vicmd, neither of which was excluded. Either would have been accepted as if a real history tool had bound it, installing a live wrapper around a shell builtin and reporting whatever was in the buffer as a "selection" instead of falling through to Warp's own command search. Fixed by checking zsh's own$widgets[<name>]classification (user:<function>only for azle -N-registered widget, which is how every third-party ctrl-r tool installs itself) instead of naming individual defaults -- this rejects all of zsh's built-in ctrl-r bindings, not just the ones a comment happened to enumerate. Bash and fish were already immune to this class of bug (bash's classify only acceptsbind -xbindings; fish's explicitly skipsbind --presetlines), so this was zsh-specific. Verified live: the atuin-style vicmd case now correctly falls back instead of wrappingredo; the occupied-Alt-] and fzf-in-all-three-keymaps cases are unaffected.Third review pass: completion-ID gap closed, shell harness shipped, fresh captures
ExternalCtrlRSelectionpattern.maybe_trigger_raw_keypress_ctrl_r_handoffnow pastes the handoff id (decimal digits, wrapped in bracketed-paste markers so the shell's line editor inserts it as literal text rather than interpreting the digits as an editing command, e.g. bash/zsh's own Alt-digit numeric argument) immediately before the private key sequence. Each shell's wrapper captures that pasted text as the token before invoking the user's real binding (the buffer holds the token at that point; after the widget runs it holds the selection instead), and echoes both back in theExternalCtrlRRawKeypressSelectionhook's newtokenfield.apply_raw_keypress_ctrl_r_selectionnow requires bothsession_idandtokento match the pending handoff before applying a completion; a stale or forged token is ignored and the handoff stays pending. This assumes the shell has bracketed-paste support (readline 8.1+/bash 5.1+, zsh 5.1+, fish all support this) -- the same baseline Warp's regular paste feature already assumes, so no new limitation is introduced. Cancellation is still not fully guaranteed: the token check means a completion from a superseded handoff can no longer be misapplied, but if the old wrapper widget is still alive past the cooldown window, it could still consume the new handoff's paste+Alt-] as ordinary input to whatever it's doing (wasting that attempt on the 5s timeout rather than reaching a wrapper that can answer it). That's a UX robustness gap, distinct from the correctness fix above.raw_keypress_ctrl_r_handoff_selection_ignored_for_wrong_tokencovers a completion with the right session but a mismatched token; the existing wrong-session and matching-selection tests were updated for the newapply_raw_keypress_ctrl_r_selection(session_id, token, selection, ctx)signature. All 7 tests pass (cargo test -p warp --lib raw_keypress_ctrl_r_handoff).app/assets/bundled/bootstrap/tests/raw_keypress_ctrl_r_matrix.py,run-matrix.sh), addressing the "not included in this diff" gap noted under Testing in earlier rounds. It drives real, unmodified bash/zsh/fish on a PTY, sourcing the actual bundled bootstrap scripts (#includeexpanded exactly as in production), and covers: keymap/mode classification (including the zsh builtin-default case from finding 5 above --redisplay/redomust not be classified as real widgets), the occupied-Alt-] case (a pre-existing user binding is left untouched), the fallback path (immediate token+empty-selection report), and the full completion-token round trip through a real wrapper widget (fzf-stylebind -x/zle -N/bindwidgets). 18/18 passing.Fourth review pass: filter typing is blocked in the native text-input path
User-facing result: filter typing does not reach the picker, so this mechanism is not usable as it stands. In repeated fresh-build GUI tests, Ctrl-R can render the inline fzf picker and control keys can interact with it, but printable filter text is captured by Warp's input editor instead. The picker eventually exits through the 5-second bail-out, and the buffered text can later be submitted as a literal shell command. This must be fixed before this prototype is a viable alternative to #15513.
Evidence
Temporary, unconditional runtime logging was added at the entry points of
TerminalView::typed_characters_on_terminalandTerminalView::keydown_on_terminal, plus the raw-keypress handoff trigger, end, and completion paths. In a fresh build, pressing Ctrl-R and typing all six characters ofmarkerproduced only these lifecycle events:There were zero
typed_characters_on_terminaland zerokeydown_on_terminallog lines for all six characters. Because those logs were unconditional, the characters did not reach either TerminalView handler at all; this is not an inference from the resulting UI. The missing handler calls also explain whynote_raw_keypress_ctrl_r_handoff_activitynever restarted the inactivity timer and the 5-second timeout fired on schedule.Ruled out
d715fd06and441c8333move focus to the terminal when the handoff starts, withraw_keypress_ctrl_r_handoff_moves_focus_off_input_so_typed_chars_reach_ptyproving thatfocus_terminalupdates Warp's focused view and that a directly-invoked typed-character handler forwards to the pty. The live GUI defect remains after this fix.should_write_typed_chars_to_ptyalready exempts an active raw-keypress handoff from the normalactive_block().started()requirement. The unconditional handler logs prove that this predicate is never consulted for real typed characters, because the handler is never entered.raw_keypress_forward_activeoverride and its pty-write branch: execution never reaches them.Narrowed location / next lead
The remaining defect is below TerminalView's view-action layer, in native printable-text targeting.
TerminalAction::TypedCharactersreachesTerminalView::typed_characters_on_terminalthrough Warp's responder-chain/keybinding system, which is the layerctx.focus()andfocus_terminal()update. NativeEvent::TypedCharacters, however, travels through the rendered element tree:RichTextElement::dispatch_eventincrates/editor/src/render/element/mod.rsconsumes it, whileDispatchedEvent::at_z_indexincrates/warpui_core/src/event.rspassesTypedCharactersthrough without consulting view focus. The best continuation point is therefore the platform/window event bridge that creates and targetsEvent::TypedCharacters, particularlycrates/warpui/src/windowing/winit/event_loop/mod.rsandcrates/warpui/src/platform/mac/window.rs. Fixing that path can affect every text-input surface in the app, so it should be separate follow-up work if this prototype is selected.The completion-token protocol, stale-completion rejection, shell keymap/occupied-key/fallback logic, zsh builtin-widget classification, and shipped 18/18 PTY shell matrix are unaffected by this diagnosis and stand on their own.
This diagnosis turned out to be wrong -- see "Fifth review pass" immediately below.
Fifth review pass: fourth pass's diagnosis corrected; real second-handoff defect found and fixed
The fourth pass's windowing-layer diagnosis does not hold up. Re-examined empirically rather than by further code reading:
TerminalView::render(is_input_box_visible) genuinely omits the input editor'sChildViewfrom the element tree during a handoff -- it isn't merely hidden/zero-height. Traced the full dispatch path for a nativeEvent::TypedCharacters(macOSinsertText:->warp_handle_insert_text-> the same genericAppContext::dispatch_eventevery other event uses ->Presenter::dispatch_event, rooted atroot_view_id) and confirmed there is no focus-keyed shortcut anywhere in that path;RichTextElement::dispatch_event(the actual consumer) has no focus check at all, so tree presence is the only gate. Addedraw_keypress_ctrl_r_handoff_typed_characters_via_real_dispatch_reach_pty_not_input, which drives a realPresenter::invalidate+build_scenecycle and a realwarpui::Event::TypedCharactersthroughctx.simulate_window_event(not a direct call totyped_characters_on_terminal) during an active handoff -- it passes: the character reaches the pty and the input editor's buffer stays empty. A fresh live re-test on currentHEADconfirmed this in the real GUI too: typed characters reach fzf's own filter line, the selection lands in Warp's input box unexecuted, and Warp's editor is correctly absent while the widget is up. Stale build, an open CLI-agent input session (has_active_cli_agent_input_session, the oneis_input_box_visibleoverride that isn't exercised by any unit test and would have kept the editor in the tree regardless of the handoff), and nested shells were all explicitly ruled out as alternative explanations. The fourth pass's symptom is best explained as a stale build combined with the bail-out below firing during slow, screenshot-paced manual typing, which hands the keyboard back mid-test and looks exactly like characters going to the wrong place.A real, separate defect: a second ctrl-r press in the same pane was unreliable. It opens the picker and filters correctly, but Enter did not deliver the selection into the input box, and each attempt left a stray, anomalous empty block behind. Root-caused to
RAW_KEYPRESS_CTRL_R_HANDOFF_TIMEOUT(5 seconds): it restarts on every keystroke forwarded to the pty, but a user reading the filtered list before pressing Enter is ordinary interactive behavior, not inactivity, and easily exceeds 5 seconds without touching a key. When the bail-out fires mid-read, it force-clearsraw_keypress_forward_activeand restores focus to the (empty) input editor; the Enter the user then presses to select an entry no longer reaches the pty at all -- it submits the empty input editor buffer as a command instead, leaving the stray empty block. Verified the shell side is not at fault first: a real zsh + fzf PTY harness driving three back-to-back handoffs at bot speed (no injected delay) completed all three correctly with matching tokens and selections, which narrowed the defect to timing rather than the completion protocol.First fix (superseded in the sixth pass): bumped
RAW_KEYPRESS_CTRL_R_HANDOFF_TIMEOUTfrom 5s to 30s and addedraw_keypress_ctrl_r_handoff_timeout_restores_normal_typed_character_routing, pinning the exact mechanism: once the (matching) timeout fires, a subsequent keystroke routes to the input editor, not the pty (this is what a stray post-bail-out Enter actually hits). Also committedraw_keypress_ctrl_r_handoff_typed_characters_via_real_dispatch_reach_pty_not_inputfrom the investigation above, since it's a strictly stronger regression test than the existing focus-only one and pins the (sound) tree-omission mechanism. Lengthening the timeout made the defect rarer, not impossible, so this was not accepted as the final design; see the sixth pass immediately below.Sixth review pass: evidence-based timer cancellation, plus a hard guard against stray Enter
A longer timeout is still a booby trap. A user can read a long history list or glance at another window for longer than 30 seconds; if the timer still fires after the widget is already alive, the same harmful outcome remains: focus silently moves back to the empty input editor, and the user's Enter -- meant to select a history entry -- submits an empty command instead. So the final design stops treating time as the signal once the widget has proved it is alive.
The 5-second timeout now only bounds time-to-first-output. Added
Event::RawKeypressCtrlRHandoffOutputObserved, emitted fromTerminalModel::on_finish_byte_processingthe moment a non-empty pty-read chunk arrives for the active block whileraw_keypress_forward_activeis set. This variant is inert for every other pty-output path: it is never emitted unless this feature's forwarding flag is active.ModelEventDispatchermaps it toModelEvent::RawKeypressCtrlRHandoffOutputObserved, andTerminalView::note_raw_keypress_ctrl_r_handoff_output_observedaborts and removes the pending bail-out timer -- it does not reschedule it. Once that happens, there is no deadline at all for the rest of that handoff, no matter how long the user then reads the widget's output or thinks before pressing a key. The failure mode the timer exists to catch -- nothing ever listening for the handoff at all (missing/rebound binding, dead shell) -- is now ruled out by observed output. A real widget (fzf/atuin) paints within milliseconds, so the timer is back to 5 seconds and only covers the genuinely never-started case. Trade-off accepted explicitly: if a widget paints once and then genuinely hangs, Warp does not auto-rescue it -- the same behavior as any other frozen interactive terminal program; Ctrl-C still forwards to the pty, and the tab can still be closed.A genuine bail-out can no longer turn a stray Enter into an empty command. Added a short-lived, one-shot guard on
Block, armed only byon_raw_keypress_ctrl_r_handoff_timeout(never on normal completion).Input::try_execute_command_from_sourceconsumes it before executing an empty command: if active, exactly that one empty submission is suppressed; the guard self-expires after 2 seconds and is always consumed on the first check, so a later, deliberate empty Enter is unaffected. This is independent of the output-based cancellation above: bail-outs should now be rare, but their failure path is safe even when they do occur.No mouse/activity broadening was added. It would be motion without effect under this design: before any output has arrived there is no widget on screen to scroll or click; after output arrives there is no timer left to restart.
Added
raw_keypress_ctrl_r_handoff_output_observed_cancels_bailout_timer_outright(asserts output observation leaves the handoff pending but removes the timer permanently) andraw_keypress_bailout_guard_suppresses_stray_empty_enter(asserts the guarded empty Enter does not execute, while the next unguarded empty Enter does).cargo test -p warp --lib raw_keypress: 12/12 passing;cargo test -p warp --lib -- model_events terminal_model: 55/55 passing.This pass's "any output cancels the timer" design turned out to be flawed -- see "Seventh review pass" immediately below.
Seventh review pass: the output-inference cancellation was itself unsafe; the feature flag was found to be ungated
Case found in a live pass on
2649198c: with the wrapper deliberately unbound, Ctrl-R left the pasted handoff token sitting on the line with no widget, and the pane never recovered -- not after 10 seconds, not after another 60. Only a manual Ctrl-C freed it. Root cause:on_finish_byte_processingcancelled the bail-out timer on any non-empty pty output for the active block while forwarding was active, and the shell's own line editor echoes back the token Warp just pasted. That echo is output, so the timer was cancelled by Warp's own bytes coming back -- meaning the one scenario the timer exists for (nothing listening for the handoff at all) is precisely the scenario where its cancellation misfired. Two other live scenarios from the sixth pass -- a 40-second untouched pause before selecting, and two immediate back-to-back handoffs in the same pane -- worked correctly under that design; only the unbound-wrapper case broke.Fix: stop inferring liveness from output entirely; get an unambiguous signal instead. Each shell wrapper now sends a new
ExternalCtrlRRawKeypressStartedDCS hook (carrying the samesession_id/tokenas the completion hook) the moment it captures the pasted token and before invoking the real widget -- in every keymap's real-widget wrapper in bash, zsh, and fish (the fallback/immediate-report paths don't send it, since they report completion instantly and have nothing to signal early). This is positive evidence the widget actually ran; unlike raw pty output, it can't be faked by an echo of Warp's own paste, and any change to the token or paste framing can't silently re-break it the way pattern-matching the echoed bytes would.TerminalModel::external_ctrl_r_raw_keypress_startedemitsEvent::ExternalCtrlRRawKeypressStarted;TerminalView::note_raw_keypress_ctrl_r_handoff_startedvalidates the reportedsession_idandtokenagainst the pending handoff (same pattern as completion) before cancelling the timer outright -- an unsolicited or stale report is ignored. The oldon_finish_byte_processingoutput-inference block andEvent::RawKeypressCtrlRHandoffOutputObservedare removed entirely.Separately, the feature flag was found to be ungated.
app/Cargo.toml'sdefaultcargo feature list includedraw_keypress_ctrl_r_handoffsince this PR's first commit.app/src/features.rs::enabled_features()unconditionally enablesFeatureFlag::RawKeypressCtrlRHandoffbehind#[cfg(feature = "raw_keypress_ctrl_r_handoff")], with no channel check -- so having it indefaultmeant the flag was compiled on for every channel and binary, making theDOGFOOD_FLAGSentry vestigial and this prototype effectively shipped to all users, not gated at all. Fixed by removing it fromdefault(the bareraw_keypress_ctrl_r_handoff = []feature declaration and theDOGFOOD_FLAGSentry are unchanged, so the flag is applied only whereChannelState::additional_features()/DOGFOOD_FLAGSturns it on, as originally intended).Updated tests: replaced
raw_keypress_ctrl_r_handoff_output_observed_cancels_bailout_timer_outrightwithraw_keypress_ctrl_r_handoff_started_cancels_bailout_timer_outrightandraw_keypress_ctrl_r_handoff_started_ignored_for_wrong_session_or_token(mismatched session or token must not cancel the timer). Extended the shell-side PTY matrix (raw_keypress_ctrl_r_matrix.py) to assert, for every real-widget round trip in bash/zsh/fish, that the started hook is observed (with the matching token) strictly before the selection hook, and that fallback paths never emit it.cargo test -p warp --lib raw_keypress: 13/13 passing;cargo test -p warp --lib dcs_hooks: 13/13 passing; shell matrix: 24/24 passing (18 previous + 6 new started-hook assertions);cargo test -p warp --lib terminal::: no regressions from this change (three unrelated, pre-existing failures reproduce identically onorigin/core-3807-raw-keypress-handoffwithout this commit --decorations::tests::test_decorations_with_multibyte_charsandinput::tests::test_histignorespace_support_in_zshfail deterministically on the prior commit too, andsecrets::tests::test_secret_redaction_unobfuscated_secret_remains_after_byte_processingpasses in isolation and only flakes under full-suite parallelism).A live pass on the seventh pass's commit found that the cancellation was not durable -- see "Eighth review pass" immediately below.
Eighth review pass: the started signal's timer cancellation wasn't durable against ordinary typing
Case found in a live pass on
d18b3886: with the wrapper genuinely bound, idle waiting worked (60+ seconds, no timeout), but typing a filter query and then pausing before Enter tore the handoff down 5 seconds later, losing the selection and leaving the input editor empty. Reproduced 5/5. Root cause, from the app log:note_raw_keypress_ctrl_r_handoff_activityunconditionally re-arms a fresh 5-second timer on every keystroke forwarded to the pty, with no way to know the timer had already been permanently cancelled by the started signal --PendingRawKeypressCtrlRHandoffcarried no flag recording that fact. So the seventh pass's "once cancelled, there is no deadline at all for the rest of the handoff" claim was true only for a handoff nobody types into: the moment a user filters and then pauses, the last forwarded keystroke's re-armed timer fires five seconds later, and the already-arrived completion (or the one still to come) is discarded as stale.Fix: track
startedon the pending handoff and make the activity path a no-op once it's set. AddedPendingRawKeypressCtrlRHandoff::started, set bynote_raw_keypress_ctrl_r_handoff_startedat the same point it validates the report and cancels the timer.note_raw_keypress_ctrl_r_handoff_activitynow checks this flag first and returns immediately if set, so a forwarded keystroke can never resurrect a timer the started signal already cancelled outright. Before this fix, the reasoning for not broadening the activity set to mouse/scroll events ("nothing on screen before output arrives, so there's nothing to broaden to") missed that the activity path doesn't just fail to extend the deadline -- it actively resurrects one that had already been cancelled; cancellation isn't durable while anything can still re-arm it.Added
raw_keypress_ctrl_r_handoff_started_is_durable_against_activity, which pins the durability directly: after a matching started report cancels the timer, a subsequentnote_raw_keypress_ctrl_r_handoff_activitycall (the same path a forwarded filter keystroke takes) must leave it cancelled.cargo test -p warp --lib raw_keypress: 14/14 passing.cargo clippy -p warp --all-targets --tests --no-depsand./script/format: clean.Secondary observation from the same live pass, investigated but not changed: pressing Up-arrow while the picker was open closed it without selecting or cancelling. Traced
TerminalAction::Up(app/src/terminal/view/init.rs, bound on plainupin theTerminalcontext) toTerminalView::terminal_up: when no block is selected, it already forwards the arrow-up escape sequence to the pty wheneveris_long_running()is true, whichraw_keypress_forward_activemakes true during a handoff -- the code path that should carry it to the wrapper widget looks correct.terminal_up/terminal_downdo not callnote_raw_keypress_ctrl_r_handoff_activitythe waykeydown_on_terminal/typed_characters_on_terminal/control_sequence_on_terminaldo, which is a real gap but a much narrower one (it would only have mattered before this pass's durability fix, and only during the pre-started window). I could not reproduce the picker actually closing from code inspection alone -- ifself.selected_blockswas non-empty at the time (e.g. a block selected from an earlier action in the same session),terminal_uptakes the block-navigation branch instead of forwarding to the pty at all, which would explain the symptom, but I don't have confirmation that was the case here. Flagging for a follow-up live check rather than guessing at a fix blind.Ninth review pass: a partial Alt-] collision could still enable the handoff in the wrong keymap, plus comment-rule cleanup
An adversarial review of the live-tested tip found two issues before this could go back to the requester:
1. The single session-wide capability tag could be advertised even when one keymap/mode was occupied. Each shell added
external_ctrl_r_raw_keypresstoshell_pluginsindependently, inside the per-keymapif <keyseq is free>block -- so if any keymap succeeded, the tag went out even if another keymap already had a real user binding on Alt-] (left correctly untouched, per the second review pass). Since the client only receives one flag for the whole session, it cannot tell a keymap where the wrapper was installed from one where the user's own binding lives on Alt-]. Reproduction: bind Alt-] to a custom widget in one keymap while another stays free; bootstrap advertises the tag from the free keymap; switching to the occupied keymap and pressing ctrl-r makes Warp paste the token and Alt-] into the user's unrelated binding instead of a ctrl-r wrapper, with no completion hook to end the forwarding state until the 5s bail-out. This is the same shape of defect that cost several rounds on #15513 -- detection advertising more than invocation can actually deliver.Fix (the conservative option, chosen over negotiating a different free sequence per session): withhold the tag entirely unless every relevant keymap/mode safely claimed Alt-] (with either a real wrapper or an empty-completion fallback). Each shell now tracks a single
all_keymaps_safe/all_modes_safeflag across the per-keymap loop, set to false the moment any keymap is occupied; the tag is only appended once, after the loop, when the flag is still true. A keymap that was occupied is still left completely alone (unchanged from the second review pass) -- what changes is that the session now advertises no capability at all rather than a capability that doesn't hold for every keymap the user might be in. Extendedraw_keypress_ctrl_r_matrix.py's existing occupied-keymap case in all three shells to assert theBootstrappedhook'sshell_pluginswithholds the tag, and to actually switch into the occupied keymap/mode and send a real handoff sequence, asserting noExternalCtrlRRawKeypress*hook fires (the private sequence reaches only the user's own binding). Also added the mirror assertion -- tag present -- to the existing real-widget and fallback cases, since those already have every keymap safely claimed and hadn't been asserting on the tag before.2. Several comments violated this repo's comment rules (
AGENTS.md's Comments section /warp-comments): narrative essays duplicated near-verbatim across the three shell scripts, historical references to "an earlier version" of the timer or "a previous version (5s, later 30s)" of the timeout, and -- most importantly -- a doc comment onRawKeypressCtrlRHandoffIdthat was flatly false: it said "the shell has no way to echo this back", which stopped being true back in the third review pass once the token protocol was added, and never got corrected. A stale comment asserting a safety invariant that the code no longer has is exactly how that invariant gets silently regressed later. Rewrote it to state plainly that the decimal value is echoed as the token and validated by bothnote_raw_keypress_ctrl_r_handoff_startedandapply_raw_keypress_ctrl_r_selection. Trimmed theraw_keypress_ctrl_r_handoff_payloadandRAW_KEYPRESS_CTRL_R_HANDOFF_TIMEOUTdoc comments inview.rsand the near-duplicate design essays at the top of the ctrl-r sections inbash_body.sh/zsh_body.sh/fish.shdown to the non-obvious "why" each one actually needs, deleting the historical/revision narrative and per-shell restatements of the same rationale.cargo test -p warp --lib raw_keypress: 14/14 passing (unchanged by this pass -- no Rust behavior changed, only comments).cargo clippy -p warp --all-targets --tests --no-depsand./script/format: clean. Shell matrix: 36/36 passing (24 previous + 12 new: tag-present assertions on the 3 successful cases, tag-withheld plus active-attempt-produces-no-hook assertions on the 3 occupied cases), across bash/zsh/fish.Re-review of the ninth pass found the production fix correct but one of its three shells under-covered -- see "Tenth review pass" immediately below.
Tenth review pass: fish's vi insert-mode collision was fixed but untested, plus the last comment residue
An adversarial re-review confirmed bash and zsh's
all_keymaps_safegating and the correctedRawKeypressCtrlRHandoffIddoc, and hand-verified that fish'sinsert-mode branch (fish_vi_key_bindingsloaded, Alt-] already bound ininsert) does correctly preserve the binding and withhold the tag. Two gaps remained:1. The fish
insert-mode fix had no matrix coverage. All three committed fish cases only ever occupieddefault; none calledfish_vi_key_bindingsand occupiedinsert. Measured concretely: deleting theelse if bind -M insert …guard infish.shstill left the matrix at 36/36 -- the exact "a passing test that doesn't test the defect" failure mode this whole round was about. Added a fourth fish case (occupied-insert) that loadsfish_vi_key_bindings, binds a custom Alt-] handler ininsertonly, and asserts: the binding is preserved, theBootstrappedtag is withheld, and a real handoff attempted after switching intoinsert(ESCtheni, which deterministically lands in insert mode regardless of the starting mode) produces noStarted/Selectionhook. Then, per the ask, verified the check itself: reverted theinsert-modeelse ifguard locally and confirmed the new tag-withheld assertion fails; restored it and confirmed the full suite passes again. Applied the identical revert-and-check to the other threeall_keymaps_safe/all_modes_safebranches added in the ninth pass (bash vi-insert, zsh emacs, fish default) -- each one's corresponding assertion fails when itselsebranch is removed and passes once restored, so none of the nine-pass or ten-pass tag-gating assertions are vacuous. Shell matrix: 39/39 (36 previous + 3 new).2. Cleared the last comment-rule residue in
view.rs.RAW_KEYPRESS_CTRL_R_HANDOFF_KEYSEQ's doc still referenced "an earlier choice of ctrl-x ctrl-r," andraw_keypress_ctrl_r_handoff_payload's doc still named its caller (TerminalView::maybe_trigger_raw_keypress_ctrl_r_handoff) and narrated the trivial 4-line body that follows it. Removed the historical comparison and the caller reference, keeping only the non-obvious "why" (Alt-] verified unbound in every keymap; bracketed-paste avoids the digits being read as an editing command).cargo test -p warp --lib raw_keypress: 14/14 passing (unchanged -- no Rust behavior changed).cargo clippy -p warp --all-targets --tests --no-depsand./script/format: clean.Linked Issue
Comparison with #15513
Both prototypes solve the same problem; the difference is where the "real Ctrl-R" gets invoked:
fc -rl ... | fzf, etc.), reimplementing the picker invocation per shell/tool, and needs to reconcile with Warp's block/history/output machinery around a fake command's lifecycle.warp_run_external_ctrl_r_widgetentries from its own verification video, because atuin records via its own preexec hook independent of shell history exclusion.bind -x/zlewidget/bindfunction (which is how fzf and atuin both install themselves on bash >= 4, zsh, and fish); a readline macro (fzf on bash < 4) or bare function name can't be re-invoked this way and falls back to Warp's own command search. And, per "Fixes from review" above, atuin's defaultenter_accept = trueexecutes the command from inside the widget, which this mechanism cannot intercept -- a real limitation Shell widget handoff: hand ctrl-r and ctrl-t to fzf/atuin (CORE-3807) #15513 doesn't share for that specific configuration.HEAD-- the fourth pass's diagnosis was a stale build plus the bail-out firing during slow manual testing, not a windowing-layer defect. A real, separate defect (a second ctrl-r attempt in the same pane) was found and fixed with evidence-based timer cancellation: once any pty output proves the wrapper is alive, the timer is gone permanently for that handoff. The separate stray-Enter guard makes the rare genuine bail-out safe too.Testing
./script/runcargo clippy -p warp -p warp_features --lib --tests -- -D warnings: clean (the workspace-widecargo clippy --workspacetarget fails on a pre-existing, unrelated issue inwarp_completeron master, confirmed by reproducing it with this branch's changes stashed).cargo test -p warp --lib dcs_hooks: 13/13 passing, including the newtokenfield on theExternalCtrlRRawKeypressSelectionhook and the newExternalCtrlRRawKeypressStartedhook (seventh pass).cargo test -p warp --lib raw_keypress: 14/14 passing (includes the wrong-token, focus-routing, real-dispatch, post-timeout-routing, started-hook-cancels-timer-outright, started-hook-ignored-for-wrong-session-or-token, started-hook-durable-against-activity, and stray-empty-Enter guard regression tests; seventh pass replaced the flawed output-observed test with the started-hook ones, eighth pass added the durability test).cargo test -p warp --lib -- model_events terminal_model: passing (covers the broader shared event plumbing touched by the started-hook signal).cargo test -p warp --lib terminal::: no regressions introduced by the seventh through tenth passes (see the seventh-pass section for the three unrelated, pre-existing failures reproduced against the prior commit; the ninth and tenth passes changed no Rust behavior beyond comments).cargo clippy -p warp --all-targets --tests -- -D warningsand./script/format: clean.app/assets/bundled/bootstrap/tests/run-matrix.sh): 39/39 passing (18 original keymap/fallback/round-trip cases, 6 seventh-pass started-hook-ordering assertions, 12 ninth-pass capability-tag-gating assertions, and 3 tenth-pass fish vi-insert-collision assertions), across bash/zsh/fish. Every tag-gating assertion added in the ninth and tenth passes was confirmed non-vacuous by reverting its corresponding productionelsebranch and observing the matrix fail.HEAD(fifth pass) confirmed typed filtering reaches the picker, the selection lands in Warp's input box unexecuted, and the input editor is correctly absent while the widget is up. Zsh vicmd fallback and bail-out-timeout recovery were re-confirmed working in earlier rounds.Screenshots / Videos
The third video/screenshot entries are from round 3 (before the focus-routing investigation in round 4) and show the core flow, vicmd fallback, and timeout recovery working, along with the typed-filter symptom later root-caused above. The earlier captures predate the token protocol entirely -- kept as an honest record of the investigation, not as proof of current behavior.
Computer-use video recordings (3)
Testing Ctrl+R history search in Warp prototype: Recording the flow of pressing Ctrl+R at an idle prompt in Warp, filtering history with 'marker', navigating with arrow keys, and pressing Enter to select an entry.
Testing Warp ctrl-r timeout bail-out: Demonstrates pressing Ctrl+R in bash after binding the escape sequence to a never-returning sleep command, showing whether Warp's terminal becomes unresponsive and then automatically recovers within a few seconds.
Ctrl-R raw keypress handoff verification (bash+fzf, zsh vicmd fallback, timeout recovery): Testing Warp's Ctrl-R handoff feature: inline fzf widget in bash, vicmd fallback responsiveness in zsh, and timeout recovery of the input editor when the handoff never returns.
Computer-use screenshots (10)
Step 4: Pressing Ctrl+R at idle terminal prompt opened Warp's Command Search modal - dark rounded panel with title "Command Search", "I'm looking for..." label with a "history" pill, "Example queries" showing "history: git checkout", and a search box with magnifying glass icon and placeholder "Search your history, workflows, and more"
Inline fzf-style history widget appearing directly in the Warp terminal grid after pressing Ctrl+R at an empty bash prompt, showing numbered history entries, a highlighted selected entry, a "174/174 +S" status line, and a "> " filter prompt at the bottom.
Inline fzf-style widget filtered by typing "echo" in the query prompt, showing 58/174 matching history entries with "echo" occurrences highlighted in green, and the query "echo" visible at the "> " prompt at the bottom.
Fish shell in vi insert mode ([I] prompt indicator) immediately after pressing Ctrl+R at an empty prompt — no inline fzf history widget or Warp Command Search modal appeared; the prompt remains empty and unchanged. (Before the keymap-coverage fix.)
Warp Settings > Features > Session section showing "Default shell for new sessions" dropdown set to "Fish" (options were Default, Zsh, Bash x2, Fish, Custom).
Test 1 (after the keymap-coverage fix): pressing Ctrl+R in a genuine top-level fish session (confirmed via
echo $FISH_VERSION= 3.7.0) now renders the inline fzf-style history widget directly in the terminal grid.![Test 2: Immediately after pressing Ctrl+R in bash (with the broken "\e]" binding active), the normal Warp input/prompt box has disappeared from below the last command block - the terminal appears to hang/become unresponsive right after Ctrl+R.](https://staging.warp.dev/api/v1/agent/artifacts/01a03781-2651-7a5d-959a-9c8dd961eac4/download)
Test 2 (bail-out timer): immediately after pressing Ctrl+R with the wrapper deliberately bound to a never-returning command, the input box disappears.
Test 2: after waiting ~7 seconds, the input box reappears on its own -- the 5-second bail-out timer fired with no user action.
Test 2 final: typed input works normally again, confirming full recovery.
Agent Mode
Computer-use video recordings (1)
View video recording - Live verification of PR #15519 (raw-keypress ctrl-r handoff, tip e0e7486): the full four-case session — filter then pause 18s and select, 63s idle then select, never-started 5s bail-out and recovery, and up-arrow behaviour inside the picker.