From 2a20e90a026936d0d5b96823d74e2e4fe13a166f Mon Sep 17 00:00:00 2001 From: Ogulcan Celik Date: Thu, 23 Jul 2026 01:49:12 +0300 Subject: [PATCH] fix: preserve physical escape on windows refs #1736 --- docs/next/CHANGELOG.md | 1 + src/client/input/windows_vti.rs | 67 ++++++++++++++++++++++++++++++--- 2 files changed, 63 insertions(+), 5 deletions(-) diff --git a/docs/next/CHANGELOG.md b/docs/next/CHANGELOG.md index d9f789ec..5ce4b9d0 100644 --- a/docs/next/CHANGELOG.md +++ b/docs/next/CHANGELOG.md @@ -7,6 +7,7 @@ ### Fixed - OMP integration install, status, and uninstall now respect `PI_CONFIG_DIR` when `PI_CODING_AGENT_DIR` is not set, and installation refuses extension-directory collisions with Pi. (#1696) +- Physical Escape key records on native Windows now bypass raw VT report framing, so pane applications receive Escape immediately and reliably. (#1736) ## [0.7.5] - 2026-07-21 diff --git a/src/client/input/windows_vti.rs b/src/client/input/windows_vti.rs index 3b29e790..5742b825 100644 --- a/src/client/input/windows_vti.rs +++ b/src/client/input/windows_vti.rs @@ -389,6 +389,7 @@ impl WindowsInputMapper { key_down = key.key_down, repeat_count = key.repeat_count, virtual_key_code = key.virtual_key_code, + virtual_scan_code = key.virtual_scan_code, unicode = key.unicode, control_key_state = key.control_key_state, "windows input trace: console key record" @@ -452,8 +453,11 @@ impl WindowsInputMapper { return false; } + // Physical Escape carries a scan code. Scan-code-zero Escape can + // introduce raw VT reports and must stay in the framer. let bare_escape = modifiers.is_empty() - && (key.virtual_key_code == 0x1b || (key.virtual_key_code == 0 && key.unicode == 0x1b)); + && ((key.virtual_key_code == 0x1b && key.virtual_scan_code == 0) + || (key.virtual_key_code == 0 && key.unicode == 0x1b)); let ctrl_bracket = key.virtual_key_code == 0xdb && key.unicode == 0x1b && modifiers == crossterm::event::KeyModifiers::CONTROL; @@ -1338,9 +1342,11 @@ mod tests { } #[test] - fn vti_escape_key_record_flushes_to_escape_after_idle() { + fn vti_scan_code_zero_escape_key_record_flushes_after_idle() { let mut translator = WindowsInputTranslator::default(); - assert!(translator.translate(key_vk(0x1b, 0)).is_empty()); + assert!(translator + .translate(key_vk_with_scan_unicode(0x1b, 0, '\0', 0)) + .is_empty()); assert_eq!( translator.idle(), vec![crate::protocol::ClientInputEvent::Key { @@ -1351,6 +1357,19 @@ mod tests { ); } + #[test] + fn vti_physical_escape_key_record_is_immediately_semantic() { + let mut translator = WindowsInputTranslator::default(); + assert_eq!( + translator.translate(key_vk_with_scan_unicode(0x1b, 0x01, '\x1b', 0)), + vec![crate::protocol::ClientInputEvent::Key { + code: crate::protocol::ClientKeyCode::Esc, + modifiers: 0, + kind: crate::protocol::ClientKeyKind::Press, + }] + ); + } + #[test] fn vti_repeated_escape_record_stays_semantic() { assert_eq!( @@ -1752,8 +1771,8 @@ mod tests { } #[test] - fn vti_escape_key_record_without_unicode_starts_mouse_sequence() { - let records = [key_vk(0x1b, 0)] + fn vti_scan_code_zero_escape_starts_mouse_sequence() { + let records = [key_vk_with_scan_unicode(0x1b, 0, '\0', 0)] .into_iter() .chain("[<35;48;26M".chars().map(key_char)); @@ -1768,6 +1787,44 @@ mod tests { ); } + #[test] + fn vti_nonzero_scan_escape_is_immediately_semantic() { + let mut translator = WindowsInputTranslator::default(); + assert_eq!( + translator.translate(key_vk_with_scan_unicode(0x1b, 0x02, '\0', 0)), + vec![crate::protocol::ClientInputEvent::Key { + code: crate::protocol::ClientKeyCode::Esc, + modifiers: 0, + kind: crate::protocol::ClientKeyKind::Press, + }] + ); + } + + #[test] + fn vti_win32_input_mode_physical_escape_is_immediately_semantic() { + let records = win32_input_mode_encoded_record(WindowsKeyRecord { + key_down: true, + repeat_count: 1, + virtual_key_code: 0x1b, + virtual_scan_code: 0x01, + unicode: 0x1b, + control_key_state: 0, + }); + let mut translator = WindowsInputTranslator::default(); + + assert_eq!( + records + .into_iter() + .flat_map(|record| translator.translate(record)) + .collect::>(), + vec![crate::protocol::ClientInputEvent::Key { + code: crate::protocol::ClientKeyCode::Esc, + modifiers: 0, + kind: crate::protocol::ClientKeyKind::Press, + }] + ); + } + #[test] fn vti_win32_input_mode_encoded_mouse_sequence() { assert_eq!(