Skip to content

feat(tui): configurable additive voice-input keybinding (APP-4988)#14307

Open
warp-dev-github-integration[bot] wants to merge 2 commits into
masterfrom
factory/tui-voice-keybinding
Open

feat(tui): configurable additive voice-input keybinding (APP-4988)#14307
warp-dev-github-integration[bot] wants to merge 2 commits into
masterfrom
factory/tui-voice-keybinding

Conversation

@warp-dev-github-integration

@warp-dev-github-integration warp-dev-github-integration Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Description

Adds a configurable TUI voice-input key under agents.voice.voice_input_toggle_key while preserving the existing hardcoded ctrl-s trigger. The configured key is an additional tap-to-start trigger; none (the default) leaves ctrl-s unchanged. Bare Alt/Control/Shift/Super modifier events are converted into standalone keystrokes; Fn remains unsupported and push-to-talk/release handling remains out of scope.

Linked Issue

Testing

  • Added settings round-trip/default and standalone modifier keymap/event-conversion regressions.
  • Added TUI binding tests for the preserved ctrl-s trigger and an additional configured modifier trigger.
  • cargo fmt --all -- --check
  • CARGO_BUILD_JOBS=1 cargo test -p warpui_core standalone_modifier --no-fail-fast
  • CARGO_BUILD_JOBS=1 cargo test -p warpui_core --features tui standalone_modifier_keys_map_to_modifier_only_keystrokes --no-fail-fast
  • Full warp/warp_tui compilation and focused TUI test were attempted with low-memory settings but rustc was terminated by the sandbox with SIGKILL/OOM before diagnostics.

Screenshots / Videos

The required real-TUI visual verification could not be captured because the full TUI build was terminated by the sandbox memory limit; no UI artifact is included.

Agent Mode

  • Warp Agent Mode - This PR was created via Warp's AI Agent Mode

CHANGELOG-IMPROVEMENT: Add a configurable TUI voice-input trigger while preserving the default ctrl-s hotkey.

Conversation: https://staging.warp.dev/conversation/97cba722-4fd4-4c78-b399-994a7f6997e0
Run: https://oz.staging.warp.dev/runs/019f99b9-9775-7c5f-9e5d-9c63381f5987

Co-Authored-By: Oz oz-agent@warp.dev
This PR was generated with Oz.

Spec for a configurable TUI keybinding to start voice mode, mirroring the
GUI's "activate voice input" setting. Adds a TUI-surface enum setting
`agents.voice.voice_input_toggle_key` (reusing the GUI's VoiceInputToggleKey
type, default None), wires the selected key to the existing
`tui:session:start_voice_input` tap action (replacing the hardcoded ctrl-s
default), and maps `KeyCode::Modifier(_)` in the TUI input-conversion layer so
bare-modifier options (Alt/Control/Shift, Super best-effort) can fire. Fn is
unsupported (no crossterm KeyCode). Hold-to-toggle / push-to-talk
(REPORT_EVENT_TYPES + key-release handling) is explicitly out of scope and
recorded as future work. Centered on 15 exhaustive, checkable validation
criteria.

This PR carries the committed spec; the implementation phase adds code to
this same branch/PR.

Co-Authored-By: Warp <agent@warp.dev>
Preserve ctrl-s while adding a TUI settings-driven modifier trigger for voice input, including standalone modifier event conversion and regression coverage.

Co-Authored-By: Oz <oz-agent@warp.dev>
@warp-dev-github-integration warp-dev-github-integration Bot changed the title Spec: TUI configurable voice-input keybinding (APP-4988) feat(tui): configurable additive voice-input keybinding (APP-4988) Jul 25, 2026
@warp-dev-github-integration
warp-dev-github-integration Bot marked this pull request as ready for review July 25, 2026 15:52
@oz-for-oss

oz-for-oss Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

@warp-dev-github-integration[bot]

I'm starting a first review of this pull request.

You can view the conversation on Warp.

I completed the review and no human review was requested for this pull request.

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

@oz-for-oss oz-for-oss Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Overview

This PR adds a TUI-local voice-input setting, registers an additive configured binding alongside the existing ctrl-s binding, and maps crossterm standalone modifier key events into modifier-only keystrokes.

Concerns

  • This is a user-facing TUI behavior change, but the PR description explicitly says no terminal transcript, render snapshot, or ./script/run-tui capture is attached. Repo guidance requires TUI visual evidence for this kind of change; attach a TUI capture showing the configured key entering the voice-input Listening state and the default none/ctrl-s behavior still working.
  • TuiVoiceInputToggleKey::keystroke() currently forwards the GUI Fn keystroke even though Fn is documented as unsupported/no-op for the TUI, so fn can register an unusable binding instead of leaving no active trigger.

Verdict

Found: 0 critical, 2 important, 0 suggestions

Request changes

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

Comment on lines +19 to +21
pub fn keystroke(self) -> Option<warpui::keymap::Keystroke> {
self.0.keystroke()
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ [IMPORTANT] Fn is documented as unsupported for the TUI, but this wrapper forwards the GUI enum's keystroke() and registers a synthetic fn trigger. Filter Fn to None so agents.voice.voice_input_toggle_key = "fn" leaves no active binding instead of showing an unusable one.

Suggested change
pub fn keystroke(self) -> Option<warpui::keymap::Keystroke> {
self.0.keystroke()
}
pub fn keystroke(self) -> Option<warpui::keymap::Keystroke> {
match self.0 {
VoiceInputToggleKey::Fn => None,
key => key.keystroke(),
}
}

@warp-dev-github-integration warp-dev-github-integration 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

This PR adds a configurable, additive TUI voice-input keybinding (agents.voice.voice_input_toggle_key, default none) that registers an additional tap-to-start trigger alongside the preserved hardcoded ctrl-s, and maps crossterm standalone modifier events (KeyCode::Modifier) into modifier-only keystrokes so Alt/Control/Shift (and Super, best-effort) can fire. The design matches the committed spec's chosen approach (tap-to-start, additive): the binding name tui:session:start_voice_input and ctrl-s baseline are preserved, the voice lifecycle and Kitty flags are untouched, and a merge_setting_schema change unions x-warp-surfaces for the shared GUI/TUI TOML path. I verified the GUI's VoiceInputToggleKey::keystroke() byte-equals the event-conversion output for each modifier (Control→{ctrl:true,key:""}, Alt/Shift/Super likewise), so the configured binding matches the converted KeyDown; focused warpui_core tests pass and cargo fmt --check + clippy -p warpui_core --features tui --tests -- -D warnings are clean.

Concerns

  • Missing full build + TUI visual proof (environment OOM, not an author omission). This is a user-facing TUI change, so the spec's criterion #15 requires a rendered-screen capture (./script/run-tui or a cloud dogfood build) showing the configured key entering the Listening state and the none/ctrl-s baseline still working. The PR body documents, and I independently confirmed, that the full warp/warp_tui compile is terminated by the sandbox's ~4GB memory limit (rustc SIGKILL/OOM), so neither the full build nor the TUI visual proof could be captured here. This is the only outstanding verification gap and is caused by the environment, not the author — a higher-memory environment is needed to run the full build and capture the TUI rendered-screen proof before merge. Per the foreman's environment caveat, this is routed to a human rather than an unwinnable author-rework loop.
  • Fn registers a dead binding instead of no binding. TuiVoiceInputToggleKey::keystroke() forwards the GUI enum's keystroke(), which returns Some(Keystroke { key: "fn", .. }) for Fn, so agents.voice.voice_input_toggle_key = "fn" registers a binding with a synthetic fn keystroke rather than leaving no active trigger. It is inert today (no crossterm event produces key: "fn"), so the spec's intent — Fn is a no-op — holds, but this deviates from spec criterion #7's "registers no keystroke" and leaves a latent dead binding. A one-line filter (Fn => None) makes the code match the documented behavior.
  • Add a chord-vs-bare-modifier regression test (spec criterion #11). The highest-risk side effect called out in the spec is a bare-modifier KeyDown matching an unintended binding; the guard is that chord KeyDowns carry a non-empty key and so can't match the standalone-modifier binding. The new tests cover the modifier→keystroke mapping and the constructed-keystroke matcher path, but none assert that a chord like ctrl-c or ctrl-enter does not match the control_left voice binding. A small chord-non-match test in event_conversion_tests or keymap_tests would lock in that invariant against future changes to key_name/key_modifiers.

Verdict

Found: 0 critical, 0 important, 2 suggestions

Approve (with the environment-OOM build/visual-proof limitation flagged for the human merge gate, and the two non-blocking suggestions below)

Review run

https://oz.staging.warp.dev/runs/019f99fd-4411-7565-9e89-eb9b30926d62

Comment on lines +19 to +21
pub fn keystroke(self) -> Option<warpui::keymap::Keystroke> {
self.0.keystroke()
}

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.

💡 [SUGGESTION] Fn is documented as unsupported in the TUI, but this forwards the GUI enum's keystroke(), which returns Some(Keystroke { key: "fn", .. }) for Fn — so voice_input_toggle_key = "fn" registers a synthetic (inert) fn binding instead of no active trigger. Filter Fn to None so the TUI matches spec criterion #7 (“registers no keystroke”) and avoids a latent dead binding.

Suggested change
pub fn keystroke(self) -> Option<warpui::keymap::Keystroke> {
self.0.keystroke()
}
pub fn keystroke(self) -> Option<warpui::keymap::Keystroke> {
match self.0 {
VoiceInputToggleKey::Fn => None,
key => key.keystroke(),
}
}

Comment on lines +107 to +148
let modifiers = [
(
ModifierKeyCode::LeftControl,
KeyModifiers::CONTROL,
"control",
),
(
ModifierKeyCode::RightControl,
KeyModifiers::CONTROL,
"control",
),
(ModifierKeyCode::LeftAlt, KeyModifiers::ALT, "alt"),
(ModifierKeyCode::RightAlt, KeyModifiers::ALT, "alt"),
(ModifierKeyCode::LeftShift, KeyModifiers::SHIFT, "shift"),
(ModifierKeyCode::RightShift, KeyModifiers::SHIFT, "shift"),
(ModifierKeyCode::LeftSuper, KeyModifiers::SUPER, "super"),
(ModifierKeyCode::RightSuper, KeyModifiers::SUPER, "super"),
];

for (modifier, expected_flags, name) in modifiers {
let Some(TuiEvent::KeyDown { keystroke, .. }) =
key(KeyCode::Modifier(modifier), KeyModifiers::empty())
else {
panic!("expected standalone modifier KeyDown for {name}");
};
assert_eq!(keystroke.key, "");
assert_eq!(
(
keystroke.ctrl,
keystroke.alt,
keystroke.shift,
keystroke.cmd
),
(
expected_flags.contains(KeyModifiers::CONTROL),
expected_flags.contains(KeyModifiers::ALT),
expected_flags.contains(KeyModifiers::SHIFT),
expected_flags.contains(KeyModifiers::SUPER),
),
"unexpected flags for {name}",
);
}

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.

💡 [SUGGESTION] This locks in the modifier→keystroke mapping, but the spec's highest-risk side effect (criterion #11) is a bare-modifier KeyDown matching an unintended binding. Consider adding a companion test asserting that a chord KeyDown (e.g. KeyCode::Char('c') with KeyModifiers::CONTROL) produces a keystroke with a non-empty key and does not equal the standalone-control keystroke, so the chord-vs-bare-modifier invariant is guarded against future changes to key_name/key_modifiers.

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.

2 participants