fix(windows): preserve native key input through conpty - #2081
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughWindows key metadata is added to terminal input models and wire events, preserved during Win32 translation, used for repeat routing and retained-selection handling, and encoded through a generalized ConPTY fallback. ChangesWindows key provenance
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Win32Input
participant WindowsVti
participant ClientInputEvent
participant App
participant FocusedPane
Win32Input->>WindowsVti: provide key record
WindowsVti->>ClientInputEvent: emit WindowsKey with provenance
ClientInputEvent->>App: deliver key metadata
App->>App: derive native identity and classify repeats
App->>FocusedPane: forward routed key event
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
Greptile SummaryThe PR preserves native Windows key records through semantic routing and forwards unconsumed physical input to legacy ConPTY panes.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains; the prior compatibility issue is resolved by bumping the wire protocol and all corresponding advertised protocol values to 19.
|
| Filename | Overview |
|---|---|
| src/client/input/windows_vti.rs | Preserves eligible physical Win32 key records while keeping IME, Ctrl+Break, Alt-code, and semantic-only paths separate. |
| src/input/model.rs | Extends semantic terminal keys with optional native Windows provenance and synchronizes record direction with event kind. |
| src/app/mod.rs | Uses scan-code-based identities to route native repeats and releases to the pane that received the original press. |
| src/platform/windows.rs | Encodes preserved native key records for ConPTY and retains the semantic Shift+Enter fallback. |
| src/protocol/wire.rs | Adds the serialized Windows key event and bumps the incompatible wire protocol change to version 19. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Windows KEY_EVENT_RECORD] --> B[Semantic key plus native record]
B --> C{Consumed by Herdr routing?}
C -->|Yes| D[Stop]
C -->|No| E{Legacy Windows ConPTY pane?}
E -->|Yes| F[Encode original Win32 record]
E -->|No| G[Use existing semantic input path]
Reviews (10): Last reviewed commit: "fix(windows): preserve suppressed native..." | Re-trigger Greptile
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/protocol/wire.rs (1)
1133-1154: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick winFix the Windows key test fixture or assertion.
record.key_downis initialized tofalse, but the test expectsSome(true).to_raw_input_event()preserves the record unchanged, so this test fails deterministically. Setkey_down: truefor a press event, or assertSome(false)if release metadata is intentional.Proposed fix
let record = crate::input::WindowsKeyRecord { - key_down: false, + key_down: true,
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 6a72c09d-fd13-4c85-8fd0-4c4b3761614c
📒 Files selected for processing (4)
docs/next/api/herdr-api.schema.jsonsrc/protocol/wire.rstests/api_ping.rstests/cli/sessions.rs
e65fdee to
07cbcd1
Compare
ogulcancelik
left a comment
There was a problem hiding this comment.
requesting changes because key repeats can lose the final release. for example, holding escape could send “escape down” but never “escape up,” leaving it stuck in the app. please fix and cover this case.
Fixed in Grouped Windows repeats are now split into one native record per press/repeat event. This means holding Escape for example keeps the native key down and repeat events, while also making sure that the final native key up reaches the application. This also works if focus is lost while the key is still held. Added coverage for holding Escape through repeats, releasing it normally and losing focus while it is held. One note on the protocol version: I intentionally kept this at 19 even though v0.7.5 uses 17 and master already moved to 18. The new Depending how you fold that in a release, that can be pushed back down to 18 to keep the guidance for one-bump-per-release true. |
82c5881 to
664ef47
Compare
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/app/mod.rs (1)
215-231: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider a brief comment explaining the scan-code bit-packing scheme.
0xf0000 + scan_code + (enhanced_bit << 16)deliberately lands in the valid Unicode scalar range up to0x10FFFF(Plane 15 private-use area) so it never collides with a realchar::from_u32result while still fittingKeyCode::Char. This is a neat but non-obvious trick; a short comment would save future readers from re-deriving why0xf0000and the range bound were chosen.💬 Example comment
const ENHANCED_KEY: u32 = 0x0100; + // Pack scan_code + enhanced-key bit into a scalar in 0xF0000..=0x10FFFF + // (Unicode Plane 15/16 private-use range) so it can round-trip through + // `KeyCode::Char` without ever colliding with a real typed character. let native_code = key.windows_record.and_then(|record| {
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: db98d550-b8ae-4bfa-a500-81164352cef7
📒 Files selected for processing (13)
docs/next/api/herdr-api.schema.jsonsrc/app/input/clipboard.rssrc/app/mod.rssrc/client/input/windows_vti.rssrc/input/mod.rssrc/input/model.rssrc/input/parse.rssrc/pane/terminal.rssrc/platform/windows.rssrc/protocol/wire.rstests/api_ping.rstests/cli/sessions.rstests/support/mod.rs
ogulcancelik
left a comment
There was a problem hiding this comment.
the escape repeat/release fix looks good, but requesting changes because standalone printable keys in default win32 input mode still lose native provenance. the key-down becomes a text event, while the physical release has no tracked press and is dropped. this leaves the pr’s broader physical-input guarantee incomplete and makes behavior depend on how windows groups repeats. please preserve printable physical records outside actual text/ime, paste, and vt framing, and cover a printable press/repeat/release cycle.
664ef47 to
d201018
Compare
Fixed in This sits where Herdr decodes Win32 input. Physical key events are no longer reduced to plain text before routing. Herdr can still handle them as normal keybindings, but when it forwards an unhandled key to ConPTY, the original Windows record is still available. Text and IME input, paste and VT framing keep their existing paths. The tests now cover:
The separate count-one case is why I also removed the protocol 19 commits. I was trying to tackle a lot of edge cases with local reviewers and those kept tripping up over the explicit release repo rule. There's no real right or wrong here imo, Greptile's note on mismatched client vs server is correct, but release rule makes sense, too. So can bring it back or not, should mostly be fine either way. Note that I haven't tested this explicitly for more than a few minutes locally. @coderabbitai full review |
|
✅ Action performedFull review finished. |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/app/mod.rs (1)
1857-1937: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winConsider adding a state-invariant assertion to this identity/state test.
This test exercises
pressed_key_identity(an identity refactor) together with adversarial workspace state (mid-hold focus change, then focus loss). Per path instructions, identity/state refactors should validate withAppState::assert_invariants_for_test()(orWorkspace::assert_invariants_for_test()) in adversarial test state, in addition to the existing byte-output assertions.As per path instructions: "For identity or state refactors, use
AppState::assert_invariants_for_test()orWorkspace::assert_invariants_for_test()with adversarial test state where applicable."Source: Path instructions
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: f258bcac-865c-4781-b764-3bfc11bca743
📒 Files selected for processing (9)
src/app/input/clipboard.rssrc/app/mod.rssrc/client/input/windows_vti.rssrc/input/mod.rssrc/input/model.rssrc/input/parse.rssrc/pane/terminal.rssrc/platform/windows.rssrc/protocol/wire.rs
Generic A direct check that the release and focus-loss paths clear all held-key ownership does make sense, so I that is added in |
Carry physical Windows key records through the existing semantic routing path. The pane send-text Escape case remains separate because it has no native record to preserve. refs herdrdev#2077 discussion: herdrdev#1934
Treat nonzero scan codes as physical input before printable records are reduced to text. This keeps actual text, paste, and VT framing on their existing paths while preserving press, grouped repeats, and release. refs herdrdev#2077
Treat a native press for an already-owned physical key as a repeat. Windows may emit a held key as separate count-one records, so this keeps subsequent downs and the final release routed to the pane that received the first press. refs herdrdev#2077
ccc75b4 to
676a577
Compare
|
i pushed |
Tested fb8827e on native Windows and everything looks right.
Looks good from my side for now, I'll keep running it locally for a bit. edit:
Only thing I'm missing is numpad buttons |
|
Superseded by #2142. We preserved the core Windows implementation and credited you as co-author, then refactored it around a shared input model and lease state machine so the final change is easier to review as one maintainer-owned commit. Thank you for the implementation, repeated Windows testing, and patience through the review. If you have time, please verify the exact #2142 head on Windows as well. |
Summary
On Windows, Herdr receives the full native record for each physical key, but currently reduces it to a cross-platform key and modifiers before routing it. When Herdr later reconstructs input for ConPTY, details such as the scan code, control state, and repeat count are already gone. This is why physical keys such as Escape can become unreliable in native Windows applications.
This keeps the original Windows record attached to Herdr's semantic key:
Herdr's semantic routing remains authoritative. At the pane boundary, unconsumed physical input for a legacy Windows ConPTY pane is encoded from the original record instead of reconstructed from the reduced key.
What this fixes
What this does not fix
pane send-textEscape reproduction from Escape can still consume the following key in native Windows panes #1929. It contains bytes, not a physical key recordpane send-keys, Kitty keyboard,modifyOtherKeys, API input, paste, IME/text input, remote input, and POSIX panes keep their existing behaviorBackground: Discussion #1934.
Note on earlier fixes
2a20e90should stay because it fixes how physical Escape reaches Herdr in the first place.Whether the Shift+Enter workaround from #1909 should stay depends on whether
pane send-keysis expected to preserve modified keys on Windows.Either way, the whole PR #1909 cannot be reverted as-is because the code in this PR still relies on the part that respects the keyboard format requested by the application.
Validation
just check(129 Windows-focused tests and 21 client transport tests)