Skip to content

Prototype: hand ctrl-r to a shell-native wrapper widget via raw keypress (CORE-3807) - #15519

Open
warp-agent-staging[bot] wants to merge 25 commits into
masterfrom
core-3807-raw-keypress-handoff
Open

Prototype: hand ctrl-r to a shell-native wrapper widget via raw keypress (CORE-3807)#15519
warp-agent-staging[bot] wants to merge 25 commits into
masterfrom
core-3807-raw-keypress-handoff

Conversation

@warp-agent-staging

@warp-agent-staging warp-agent-staging Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

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 ExternalCtrlRRawKeypressSelection DCS 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 real Presenter dispatch 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:

  • New RawKeypressCtrlRHandoff feature flag (dogfood-enabled; not a default cargo feature, see "Seventh review pass" below).
  • New ExternalCtrlRRawKeypressSelection and ExternalCtrlRRawKeypressStarted DCS hooks, end to end (event, handler, ansi dispatch), carrying a per-handoff completion token.
  • 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 foreground command -- this was the biggest open risk going in, see Testing below.
  • TerminalView::maybe_trigger_raw_keypress_ctrl_r_handoff hands Ctrl-R off when the active session's shell reports the external_ctrl_r_raw_keypress shell_plugins tag; Workspace wires it in ahead of opening command search.
  • A 5-second bail-out timer (RAW_KEYPRESS_CTRL_R_HANDOFF_TIMEOUT) bounds only time-to-first-evidence: each shell wrapper reports ExternalCtrlRRawKeypressStarted over 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-MSYS2 Bootstrapped JSON payload never actually included shell_plugins despite 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:

  1. \C-x\C-r collides with bash's built-in re-read-init-file. Replaced the private sequence with Alt-] (ESC ]), verified genuinely unbound (via direct bind/bindkey queries in real PTY sessions, not just by inspection) in bash (emacs, vi-insert, vi-command), zsh (emacs, viins, vicmd), and fish (default, insert).
  2. Keymap coverage gap. 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, 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.
  3. No bail-out if the wrapper never returns. This mechanism runs whatever the user has bound to ctrl-r and cannot assume it returns -- a missing/rebound/hung wrapper left raw_keypress_forward_active set 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

  1. The single, session-wide capability tag could silently stall a keymap with no real wrapper. The client still cannot tell which keymap is active when ctrl-r is pressed (no client-side change here -- shell_plugins remains 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.
  2. A pre-existing user binding on Alt-] would have been silently clobbered. Each shell now checks, per keymap, whether Alt-] is already bound before installing anything there; if the user already owns it, that keymap is left alone entirely (no wrapper, no fallback, no capability claimed). Verified live in all three shells: a custom Alt-] binding installed in one keymap (bash emacs, zsh emacs, fish default) is left untouched while the other keymaps still get Warp's wrapper.
  3. The bail-out timer didn't distinguish handoff attempts. A second ctrl-r press after a timeout fired -- but before the underlying wrapper actually stopped consuming pty input -- could have its own bail-out timer torn down by the first handoff's late-arriving timer. Added 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 carried session_id; that gap is closed below.
  4. No regression coverage for the above. Added 6 client-side unit tests (app/src/terminal/view_tests.rs, App::test harness, exercising TerminalView directly) 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).
  5. (Found and fixed during this pass's own verification, not from review.) While live-verifying Search history by more than just command #1 above, found that zsh's classify function only excluded the two named zsh built-ins it knew ctrl-r could default to, missing zsh's other per-keymap defaults -- confirmed live in stock zsh (bindkey -v, no third-party tool) that ctrl-r defaults to redisplay in viins and redo in 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 a zle -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 accepts bind -x bindings; fish's explicitly skips bind --preset lines), so this was zsh-specific. Verified live: the atuin-style vicmd case now correctly falls back instead of wrapping redo; the occupied-Alt-] and fzf-in-all-three-keymaps cases are unaffected.

Third review pass: completion-ID gap closed, shell harness shipped, fresh captures

  1. The completion-ID gap (item 3 above) is now closed with a per-handoff token, mirroring Shell widget handoff: hand ctrl-r and ctrl-t to fzf/atuin (CORE-3807) #15513's ExternalCtrlRSelection pattern. maybe_trigger_raw_keypress_ctrl_r_handoff now 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 the ExternalCtrlRRawKeypressSelection hook's new token field. apply_raw_keypress_ctrl_r_selection now requires both session_id and token to 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.
  2. Rust unit tests extended for the token check. raw_keypress_ctrl_r_handoff_selection_ignored_for_wrong_token covers a completion with the right session but a mismatched token; the existing wrong-session and matching-selection tests were updated for the new apply_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).
  3. A shell-side PTY test matrix is now shipped in this diff (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 (#include expanded exactly as in production), and covers: keymap/mode classification (including the zsh builtin-default case from finding 5 above -- redisplay/redo must 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-style bind -x/zle -N/bind widgets). 18/18 passing.
  4. Fresh computer-use verification against this round's changes flagged a typed-filter-characters symptom, investigated in the fourth pass below. Bash+fzf, zsh vicmd fallback, and the bail-out timeout all otherwise behaved as expected in this pass (see "Fourth review pass" for the full, more careful re-investigation of the typed-character symptom that superseded this pass's initial read of it as a timing quirk).

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_terminal and TerminalView::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 of marker produced only these lifecycle events:

  • the handoff triggered;
  • exactly five seconds later, the timeout ended it;
  • the shell then reported an empty completion after the timeout's Ctrl-C unblocked the wrapper.

There were zero typed_characters_on_terminal and zero keydown_on_terminal log 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 why note_raw_keypress_ctrl_r_handoff_activity never restarted the inactivity timer and the 5-second timeout fired on schedule.

Ruled out

  • View-level focus. The first diagnosed bug was real: the trigger path left focus on the idle input editor. Commits d715fd06 and 441c8333 move focus to the terminal when the handoff starts, with raw_keypress_ctrl_r_handoff_moves_focus_off_input_so_typed_chars_reach_pty proving that focus_terminal updates Warp's focused view and that a directly-invoked typed-character handler forwards to the pty. The live GUI defect remains after this fix.
  • The second input gate. should_write_typed_chars_to_pty already exempts an active raw-keypress handoff from the normal active_block().started() requirement. The unconditional handler logs prove that this predicate is never consulted for real typed characters, because the handler is never entered.
  • The override/routing logic inside TerminalView. The same evidence rules out the raw_keypress_forward_active override and its pty-write branch: execution never reaches them.
  • Timing or a computer-use typing artifact. The symptom reproduced in bash and fish across multiple fresh builds. Both fast bursts and slow per-character typing reproduced it; fast typing can briefly update the widget, while slower typing crosses back into Warp's input path, but the diagnostic run establishes that the real characters bypass TerminalView's handlers.

Narrowed location / next lead

The remaining defect is below TerminalView's view-action layer, in native printable-text targeting. TerminalAction::TypedCharacters reaches TerminalView::typed_characters_on_terminal through Warp's responder-chain/keybinding system, which is the layer ctx.focus() and focus_terminal() update. Native Event::TypedCharacters, however, travels through the rendered element tree: RichTextElement::dispatch_event in crates/editor/src/render/element/mod.rs consumes it, while DispatchedEvent::at_z_index in crates/warpui_core/src/event.rs passes TypedCharacters through without consulting view focus. The best continuation point is therefore the platform/window event bridge that creates and targets Event::TypedCharacters, particularly crates/warpui/src/windowing/winit/event_loop/mod.rs and crates/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's ChildView from the element tree during a handoff -- it isn't merely hidden/zero-height. Traced the full dispatch path for a native Event::TypedCharacters (macOS insertText: -> warp_handle_insert_text -> the same generic AppContext::dispatch_event every other event uses -> Presenter::dispatch_event, rooted at root_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. Added raw_keypress_ctrl_r_handoff_typed_characters_via_real_dispatch_reach_pty_not_input, which drives a real Presenter::invalidate + build_scene cycle and a real warpui::Event::TypedCharacters through ctx.simulate_window_event (not a direct call to typed_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 current HEAD confirmed 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 one is_input_box_visible override 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-clears raw_keypress_forward_active 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 -- 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_TIMEOUT from 5s to 30s and added raw_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 committed raw_keypress_ctrl_r_handoff_typed_characters_via_real_dispatch_reach_pty_not_input from 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 from TerminalModel::on_finish_byte_processing the moment a non-empty pty-read chunk arrives for the active block while raw_keypress_forward_active is set. This variant is inert for every other pty-output path: it is never emitted unless this feature's forwarding flag is active. ModelEventDispatcher maps it to ModelEvent::RawKeypressCtrlRHandoffOutputObserved, and TerminalView::note_raw_keypress_ctrl_r_handoff_output_observed aborts 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 by on_raw_keypress_ctrl_r_handoff_timeout (never on normal completion). Input::try_execute_command_from_source consumes 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) and raw_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_processing cancelled 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 ExternalCtrlRRawKeypressStarted DCS hook (carrying the same session_id/token as 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_started emits Event::ExternalCtrlRRawKeypressStarted; TerminalView::note_raw_keypress_ctrl_r_handoff_started validates the reported session_id and token against the pending handoff (same pattern as completion) before cancelling the timer outright -- an unsolicited or stale report is ignored. The old on_finish_byte_processing output-inference block and Event::RawKeypressCtrlRHandoffOutputObserved are removed entirely.

Separately, the feature flag was found to be ungated. app/Cargo.toml's default cargo feature list included raw_keypress_ctrl_r_handoff since this PR's first commit. app/src/features.rs::enabled_features() unconditionally enables FeatureFlag::RawKeypressCtrlRHandoff behind #[cfg(feature = "raw_keypress_ctrl_r_handoff")], with no channel check -- so having it in default meant the flag was compiled on for every channel and binary, making the DOGFOOD_FLAGS entry vestigial and this prototype effectively shipped to all users, not gated at all. Fixed by removing it from default (the bare raw_keypress_ctrl_r_handoff = [] feature declaration and the DOGFOOD_FLAGS entry are unchanged, so the flag is applied only where ChannelState::additional_features()/DOGFOOD_FLAGS turns it on, as originally intended).

Updated tests: replaced raw_keypress_ctrl_r_handoff_output_observed_cancels_bailout_timer_outright with raw_keypress_ctrl_r_handoff_started_cancels_bailout_timer_outright and raw_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 on origin/core-3807-raw-keypress-handoff without this commit -- decorations::tests::test_decorations_with_multibyte_chars and input::tests::test_histignorespace_support_in_zsh fail deterministically on the prior commit too, and secrets::tests::test_secret_redaction_unobfuscated_secret_remains_after_byte_processing passes 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_activity unconditionally 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 -- PendingRawKeypressCtrlRHandoff carried 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 started on the pending handoff and make the activity path a no-op once it's set. Added PendingRawKeypressCtrlRHandoff::started, set by note_raw_keypress_ctrl_r_handoff_started at the same point it validates the report and cancels the timer. note_raw_keypress_ctrl_r_handoff_activity now 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 subsequent note_raw_keypress_ctrl_r_handoff_activity call (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-deps and ./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 plain up in the Terminal context) to TerminalView::terminal_up: when no block is selected, it already forwards the arrow-up escape sequence to the pty whenever is_long_running() is true, which raw_keypress_forward_active makes true during a handoff -- the code path that should carry it to the wrapper widget looks correct. terminal_up/terminal_down do not call note_raw_keypress_ctrl_r_handoff_activity the way keydown_on_terminal/typed_characters_on_terminal/control_sequence_on_terminal do, 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 -- if self.selected_blocks was non-empty at the time (e.g. a block selected from an earlier action in the same session), terminal_up takes 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_keypress to shell_plugins independently, inside the per-keymap if <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_safe flag 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. Extended raw_keypress_ctrl_r_matrix.py's existing occupied-keymap case in all three shells to assert the Bootstrapped hook's shell_plugins withholds the tag, and to actually switch into the occupied keymap/mode and send a real handoff sequence, asserting no ExternalCtrlRRawKeypress* 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 on RawKeypressCtrlRHandoffId that 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 both note_raw_keypress_ctrl_r_handoff_started and apply_raw_keypress_ctrl_r_selection. Trimmed the raw_keypress_ctrl_r_handoff_payload and RAW_KEYPRESS_CTRL_R_HANDOFF_TIMEOUT doc comments in view.rs and the near-duplicate design essays at the top of the ctrl-r sections in bash_body.sh/zsh_body.sh/fish.sh down 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-deps and ./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_safe gating and the corrected RawKeypressCtrlRHandoffId doc, and hand-verified that fish's insert-mode branch (fish_vi_key_bindings loaded, Alt-] already bound in insert) 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 occupied default; none called fish_vi_key_bindings and occupied insert. Measured concretely: deleting the else if bind -M insert … guard in fish.sh still 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 loads fish_vi_key_bindings, binds a custom Alt-] handler in insert only, and asserts: the binding is preserved, the Bootstrapped tag is withheld, and a real handoff attempted after switching into insert (ESC then i, which deterministically lands in insert mode regardless of the starting mode) produces no Started/Selection hook. Then, per the ask, verified the check itself: reverted the insert-mode else if guard 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 three all_keymaps_safe/all_modes_safe branches added in the ninth pass (bash vi-insert, zsh emacs, fish default) -- each one's corresponding assertion fails when its else branch 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," and raw_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-deps and ./script/format: clean.

Linked Issue

  • CORE-3807
  • Screenshots/recording below

Comparison with #15513

Both prototypes solve the same problem; the difference is where the "real Ctrl-R" gets invoked:

  • Shell widget handoff: hand ctrl-r and ctrl-t to fzf/atuin (CORE-3807) #15513 submits a synthetic foreground command that runs the tool's own shell script (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.
  • This PR re-invokes whatever is actually bound to Ctrl-R, from inside a real key-binding context, so it works for any tool the user has already bound to Ctrl-R (not just fzf/atuin specifically) with the same ~20 lines of shell code per shell. It also sidesteps history-exclusion entirely: since nothing is ever executed as a command, there's no helper invocation to hide from history and nothing to falsely verify by its absence -- unlike Shell widget handoff: hand ctrl-r and ctrl-t to fzf/atuin (CORE-3807) #15513, whose atuin history list still shows warp_run_external_ctrl_r_widget entries from its own verification video, because atuin records via its own preexec hook independent of shell history exclusion.
  • Running the normal command path (as Shell widget handoff: hand ctrl-r and ctrl-t to fzf/atuin (CORE-3807) #15513 does) means the existing block lifecycle ends the mode on its own, so there's no new way to strand the user; this mechanism had to add its own bail-out (see above) to get an equivalent guarantee, since it doesn't go through that lifecycle.
  • Trade-offs: only works for bindings installed via bind -x/zle widget/bind function (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 default enter_accept = true executes 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.
  • Correction, per the fifth and sixth review passes above: live filtering into the picker does work end to end on current 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

  • I have manually tested my changes locally with ./script/run
  • cargo clippy -p warp -p warp_features --lib --tests -- -D warnings: clean (the workspace-wide cargo clippy --workspace target fails on a pre-existing, unrelated issue in warp_completer on master, confirmed by reproducing it with this branch's changes stashed).
  • cargo test -p warp --lib dcs_hooks: 13/13 passing, including the new token field on the ExternalCtrlRRawKeypressSelection hook and the new ExternalCtrlRRawKeypressStarted hook (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 warnings and ./script/format: clean.
  • Shell-side PTY test matrix, shipped in this diff (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 production else branch and observing the matrix fail.
  • A separate, ad hoc real zsh + fzf PTY run (not shipped in this diff) drove three back-to-back handoffs at bot speed and confirmed the shell-side completion protocol is not at fault for the second-handoff defect fixed in the fifth pass.
  • End-to-end via computer-use: a fresh live re-test on current 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)

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 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.

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.
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.

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.
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"
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 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.
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.
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).
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 pressing Ctrl+R in a genuine top-level fish session (confirmed via echo $FISH_VERSION = 3.7.0), an inline fzf-style history widget appears directly in the terminal grid, showing numbered history entries 1-6 with a "6/6 +S" status line and a "> " filter prompt at the bottom.
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 Warp input/prompt box has reappeared (folder icon, ~, and blinking cursor visible), suggesting the terminal automatically recovered from the Ctrl+R hang without any user intervention.
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: "hello" typed successfully and visible (underlined in red, indicating spellcheck/unknown-word styling, not an error) in Warp's normal input box, confirming the terminal fully recovered from the Ctrl+R hang and is responsive to keyboard input.
Test 2 final: typed input works normally again, confirming full recovery.

Agent Mode

  • Warp Agent Mode - This PR was created via Warp's AI 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.

…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.
@warp-agent-staging

Copy link
Copy Markdown
Contributor Author

This PR was generated with Warp.

Comment @warp-factory on this PR to send it follow-up work.

View run View conversation View on Slack

@warp-agent-staging warp-agent-staging Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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_accept behavior, which is on by default: fish's _atuin_search calls commandline -f execute, bash's __atuin_history reaches __atuin_accept_line/eval, and zsh reaches zle 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

warp-agent-staging Bot and others added 20 commits August 25, 2026 06:02
…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.
@warp-agent-staging
warp-agent-staging Bot marked this pull request as ready for review August 26, 2026 13:22

@warp-agent-staging warp-agent-staging Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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_zsh fails deterministically on this branch. The remote zsh receives InitShell, transitions to ScriptExecution, then never emits Bootstrapped — the block sits executing with empty output until the 20s assertion times out. It failed 9/9 on Linux and again on macOS, while test_ssh_wrapper_into_bash passed 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 (named ubuntu-14-04, whose stock zsh is 5.0.2).
  • The shell-side installation is not gated by the feature flag. FeatureFlag::RawKeypressCtrlRHandoff gates only whether the client triggers a handoff; the Alt-] classification and bindkey/zle -N installation in zsh_body.sh, bash_body.sh and fish.sh runs on every bootstrap of those shells regardless, and the cargo feature is not in default. 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.
@warp-agent-staging

Copy link
Copy Markdown
Contributor Author

Updating my review above: the first finding is root-caused and fixed, so only the second still needs a decision.

__warp_raw_keypress_ctrl_r_keyseq_free used an unquoted local result=$(bindkey ...). On zsh <5.1 — Ubuntu 14.04's stock 5.0.2, which is what the test host runs — local var=$(cmd) word-splits the value and declares each word, so bindkey's ordinary "^[]" undefined-key output made local try to declare undefined-key. That is not a valid identifier, so the sourced script aborted before reaching warp_bootstrapped — which is exactly the CI signature: InitShell, then no Bootstrapped, then a 20s timeout on empty output. Modern zsh does not split, which is why the shell matrix never caught it.

Reproduced in an ubuntu:14.04 container against that zsh with master as a negative control, and confirmed fixed there. I also verified the stated mechanism directly rather than inferring it: local undefined-key reproduces not valid in this context: undefined-key exactly, while a failed glob reports no matches found — so this is word-splitting, not filename generation. Fixed in 1ccd207aa; shell matrix 41/41.

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.
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.

0 participants