Skip to content

fix(cli): make Shift+letter actually type a capital under modifyOtherKeys - #87785

Open
krunkosaurus wants to merge 2 commits into
NousResearch:mainfrom
krunkosaurus:fix/cli-modify-other-keys-insert-text
Open

fix(cli): make Shift+letter actually type a capital under modifyOtherKeys#87785
krunkosaurus wants to merge 2 commits into
NousResearch:mainfrom
krunkosaurus:fix/cli-modify-other-keys-insert-text

Conversation

@krunkosaurus

Copy link
Copy Markdown
Contributor

What does this PR do?

Completes #87511. That PR mapped ESC[27;2;<code>~ / ESC[<code>;2u to the uppercase character, but Shift+letter still leaks its escape sequence into the prompt — the mapping fixed what the key is, not what it types.

prompt_toolkit's VT100 parser reports every match as KeyPress(key=<table value>, data=<matched bytes>), and the default Keys.Any binding inserts event.data — the bytes, not the key. For the hundreds of Keys-valued entries in install_modify_other_keys_aliases() that is invisible: bindings match on key, and data is never read. For the character-valued entries it is the whole ballgame:

ANSI_SEQUENCES["\x1b[27;2;72~"] = "H"
  -> KeyPress(key="H", data="\x1b[27;2;72~")
  -> insert_text("\x1b[27;2;72~")

Verified against main at 7095e23, driving a real PromptSession over a pipe input:

typed submitted (main) submitted (this PR)
Hello World \x1b[27;2;72~ello \x1b[27;2;87~orld Hello World
Ctrl+A then X on bc Xbc Xbc (unchanged)

So the symptom in #87390 — capital letters printing [27;2;<code>~ on Ghostty — is still live on main for anyone on an allowlisted terminal. Reported independently by a Ghostty 1.3.2 user this morning.

Related Issue

Refs #87511, #87390, #86866, #87631, #87637.

Not a duplicate of #87637 (Cyrillic / all cased scripts): that PR extends the same mapping table and inherits the same gap — its assertions are also key-level, so it will report green while Cyrillic capitals still leak. This fix is orthogonal and makes both work.

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)

Changes Made

  • hermes_cli/pt_input_extras.py — new _install_literal_key_data_patch(): narrows KeyPress.data to the character for character-valued table entries. Called at the end of install_modify_other_keys_aliases(), so it covers Shift+letter, Shift+Space and the keypad digits. Idempotent, guarded by try/except and a class-level flag, and a no-op if prompt_toolkit's internals move.
  • tests/cli/test_modify_other_keys_insert_text.py — 14 tests.

Why the tests assert on submitted text

_parse()-style assertions compare KeyPress.key, which was already correct before this fix — that is precisely why this survived #87511's 160 tests, and why #87637 does not catch it either. These tests run a real PromptSession over create_pipe_input() and assert on the string the user would have submitted. 9 of the 14 fail on unpatched main; the 5 that pass are the guard tests (Ctrl combos, named keys, plain typing, bracketed paste, Shift+Enter aliases).

How to Test

  1. pytest tests/cli/test_modify_other_keys_insert_text.py -q → 14 passed.
  2. git stash the pt_input_extras.py change and re-run → 9 failed, 5 passed.
  3. On Ghostty / iTerm2 / WezTerm / kitty: launch the classic CLI and type a capital letter.

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix
  • I've run pytest tests/cli -q — 1214 passed, 3 failed, 9 skipped. The 3 failures (test_exit_watchdog_signal_arm x2, test_resume_quiet_stderr) reproduce identically on pristine main (1200 passed, same 3 failures).
  • I've added tests for my changes
  • I've tested on my platform: macOS 15 (Darwin 25.5.0), Ghostty 1.3.2, prompt_toolkit 3.0.52

Documentation & Housekeeping

  • Docstrings updated — the new helper documents the key vs data distinction
  • cli-config.yaml.example — N/A
  • CONTRIBUTING.md / AGENTS.md — N/A
  • Cross-platform impact considered: pure prompt_toolkit-layer change, no platform-specific code; affects every allowlisted terminal equally
  • Tool descriptions/schemas — N/A

🤖 Generated with Claude Code

…Keys

NousResearch#87511 mapped `ESC[27;2;<code>~` and `ESC[<code>;2u` to the uppercase
character, but the prompt still receives the raw escape sequence.

prompt_toolkit's VT100 parser reports every match as
`KeyPress(key=<table value>, data=<matched bytes>)`, and the default
`Keys.Any` binding inserts `event.data` — the bytes, not the key. For the
hundreds of `Keys`-valued entries that is invisible, because bindings match
on `key` and `data` is never read. For the character-valued entries added
by `install_modify_other_keys_aliases()` it is the whole ballgame: mapping
`ESC[27;2;72~` → `"H"` produces `KeyPress(key="H", data="\x1b[27;2;72~")`,
so `[27;2;72~` is what lands in the buffer.

Net effect: the mapping fixed what the key *is* but not what it *types*,
and Shift+letter is still unusable on Ghostty / iTerm2 / WezTerm / kitty —
the same symptom NousResearch#87390 reported. Verified against main at 7095e23:
typing `Hello World` submits `\x1b[27;2;72~ello \x1b[27;2;87~orld`.

Fix: narrow `KeyPress.data` to the character for character-valued table
entries (Shift+letter, Shift+Space, keypad digits). Stock prompt_toolkit
ships no character-valued entries and the parser's fallback path already
calls the handler with `key is data` for ordinary typing, so nothing else
changes shape. Ctrl/Alt combos, named keys, bracketed paste and the
Shift+Enter aliases are unaffected.

Tests assert on submitted prompt text via a real `PromptSession` over a
pipe input rather than on parsed keys — key-level assertions cannot catch
this class of bug, which is why it survived NousResearch#87511's 160 tests. 9 of the
14 new tests fail on unpatched main.

Refs NousResearch#87511, NousResearch#87390, NousResearch#86866, NousResearch#87631, NousResearch#87637.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
liuhao1024 added a commit to liuhao1024/hermes-agent that referenced this pull request Aug 16, 2026
Review follow-up on NousResearch#87637: prompt_toolkit's default Keys.Any binding
inserts event.data (the matched bytes), not the key, so the parse-level
key assertions cannot see what actually reaches the prompt buffer. Add an
xfail(strict=False) guard on the full observable (key AND data) for
Latin/Cyrillic/Greek — xfailed until the data-path fix in NousResearch#87785 lands,
green automatically once applied on top.
@alt-glitch alt-glitch added type/bug Something isn't working comp/cli CLI entry point, hermes_cli/, setup wizard P2 Medium — degraded but workaround exists labels Aug 16, 2026
@spfcraze

Copy link
Copy Markdown
Contributor

This was generated by AI during triage.

Summary:
_install_literal_key_data_patch() fetches Vt100Parser._call_handler outside the try/except that guards its imports, so a future prompt_toolkit that renames that private method raises AttributeError instead of returning False — and cli.py's lone except around all input-extras installs silently swallows the raise.
Problems:
In hermes_cli/pt_input_extras.py the imports at the top of _install_literal_key_data_patch sit inside try/except Exception: return False, and the idempotency flag uses getattr(..., False), but original_call_handler = Vt100Parser._call_handler runs between them, unguarded. The method exists in the pinned prompt_toolkit 3.0.52, so the patch works today; when a prompt_toolkit update moves or renames it, the fetch raises before the inner try, and cli.py wraps the whole input-extras block (five installers, including install_ignored_terminal_sequences) in a single except Exception: pass, so every installer after the raise is skipped silently with no error path.
Solution:
Fetch inside the guard with a default so a moved internal degrades to the same no-op the guard already returns for a missing module: original_call_handler = getattr(Vt100Parser, "_call_handler", None) guarded by if original_call_handler is None: return False before the assignment.

Evidence

no deterministic fact backs this claim — model belief, not executed or read evidence


Checked against 1a8dc77 — the tip of fix/cli-modify-other-keys-insert-text when this was written — and 86b2057, main at the same moment.

@Enough1122

Copy link
Copy Markdown
Contributor

AI code review — automated review for reference, author can ignore or act on any point.

fix(cli): make Shift+letter actually type a capital under modifyOtherKeys

  • hermes_cli/pt_input_extras.py _install_literal_key_data_patch(): the Vt100Parser._call_handler attribute access sits outside the guarded try/except — only the imports are protected. If a future prompt_toolkit upgrade renames or removes _call_handler, install_modify_other_keys_aliases() will raise AttributeError instead of degrading gracefully (it otherwise returns a changed-count). Consider fetching it inside the try, or getattr(Vt100Parser, "_call_handler", None) with a None-guard that returns False.
  • The idempotency flag _hermes_literal_key_data and the wrapper are both set on the class. If anything later replaces _call_handler (another patch, a plugin), the flag stays True while the wrapper is gone — flag and actual patch can drift out of sync. Minor, but re-checking wrapper identity before skipping would be more robust.
  • tests/cli/test_modify_other_keys_insert_text.py: the autouse fixture restores ANSI_SEQUENCES but does not undo the _call_handler monkey-patch or the _hermes_literal_key_data flag, so the patch leaks past the fixture for the rest of the process. Tests in this file depend on it, but any other test importing pt_input_extras in the same process would observe it too — restoring it in the fixture's teardown would keep the isolation airtight.

…ving

Review feedback from @spfcraze and @Enough1122, all three points valid:

- `Vt100Parser._call_handler` was fetched outside the guard, so a future
  prompt_toolkit that renames it would raise AttributeError through
  install_modify_other_keys_aliases() into cli.py's blanket
  `except Exception: pass`, silently skipping the installers that run
  after it. Fetch via getattr with a None-guard so a moved internal
  degrades to the same no-op as a missing module — which is what the
  docstring already promised. Pinned by a test that deletes the attribute.

- The idempotency marker lived on the class while the wrapper lived on
  the method, so the two could drift: if anything replaced
  `_call_handler`, the marker stayed True and the replacement was never
  wrapped. Move the marker onto the wrapper, where it cannot outlive
  what it describes.

- The test fixture restored ANSI_SEQUENCES but not the monkeypatch, so
  it leaked to the rest of the process. Restore it in teardown.

Also resolve Vt100Parser per call rather than at import.
tests/cli/test_bracketed_paste_timeout.py reloads
prompt_toolkit.input.vt100_parser, rebinding the module's class to a new
object, so a module-level import here held the pre-reload class and
patched something the parser no longer used — the new
marker-drift test passed alone and failed in the full suite.

tests/cli: 1217 passed, same 3 pre-existing failures as pristine main.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@krunkosaurus

Copy link
Copy Markdown
Contributor Author

Thanks both — all three points were valid, fixed in 472488f.

1. Unguarded _call_handler fetch. Correct, and it contradicted the docstring's own promise of degrading to a no-op. install_modify_other_keys_aliases() would have raised AttributeError into cli.py's blanket except Exception: pass, taking install_ignored_terminal_sequences() down with it silently. Now fetched via getattr(..., None) with a None-guard, and pinned by a test that deletes the attribute and asserts the install returns False without raising — so the hypothetical is now executed rather than argued.

2. Marker/wrapper drift. Also correct. Fixed by moving the marker onto the wrapper function instead of the class, so it cannot outlive what it describes: if anything later replaces _call_handler, the marker goes with it and the next install wraps the replacement rather than skipping on a stale flag.

3. Fixture leaked the monkeypatch. Fixed — teardown restores _call_handler alongside ANSI_SEQUENCES.

Writing the test for #2 turned up something worth flagging separately: it passed in isolation and failed in the full suite. tests/cli/test_bracketed_paste_timeout.py:53 calls importlib.reload() on prompt_toolkit.input.vt100_parser, which rebinds the module's class to a new object. Any test module that imports Vt100Parser at module level therefore holds the pre-reload class for the rest of the session and patches something the parser no longer uses. This file now resolves the class per call, the same way the installer does. Harmless here, but other suites touching that module may want to know.

tests/cli: 1217 passed, 9 skipped, with the same 3 failures that reproduce on pristine main (test_exit_watchdog_signal_arm x2, test_resume_quiet_stderr). 9 of the 17 tests in the new file still fail without the fix applied.

Investigated with Claude Code.

@krunkosaurus

Copy link
Copy Markdown
Contributor Author

Status update for reviewers — independent verification since the last push:

That makes four reports tracing to this one defect: #86866 (Shift+Space, Ctrl+K), #87390 (Ghostty capitals), #87631 (WezTerm Cyrillic), #88071 (Shift+Space again). The mapping PRs are all correct and still needed — they just cannot take effect until KeyPress.data carries the mapped character, which is the one thing this PR changes.

Investigated with Claude Code.

@jackulau

Copy link
Copy Markdown
Contributor

This PR closes a second issue that nobody has linked to it, and I think that is worth stating plainly because the title makes it look narrower than it is.

#90640 ("CLI prints raw CSI-u sequences for all numeric keypad keys", P2, filed today, no PR on it) is the same defect on a different symptom, and _install_literal_key_data_patch() fixes it as written. I fetched this branch's hermes_cli/pt_input_extras.py at head 472488f and ran that reporter's exact sequences through the real VT100 parser rather than reasoning about the diff:

                        main                          this branch
Numpad /  '\x1b[57410u'  ('/', '\x1b[57410u')    ->   ('/', '/')
Numpad 7  '\x1b[57406u'  ('7', '\x1b[57406u')    ->   ('7', '7')
Numpad +  '\x1b[57413u'  ('+', '\x1b[57413u')    ->   ('+', '+')
Numpad .  '\x1b[57409u'  ('.', '\x1b[57409u')    ->   ('.', '.')

Their report is ^[[57410u appearing in the prompt, which is exactly data being the matched bytes. Worth adding Fixes #90640 here: the keypad is arguably the more visible case, because kitty emits those CSI-u forms even in legacy mode (keypad keys have no legacy encoding), whereas Shift+letter only takes this path when modifyOtherKeys=2 is actually pushed.

For scale, on current main there are 123 character-valued entries in the table after all four installers run:

distinct characters:  *+,-./0123456789=ABCDEFGHIJKLMNOPQRSTUVWXYZ

Your patch is the only thing open that addresses all of them.

Overlap with #88109

#88109 ("stop Shift+Space from inserting raw CSI bytes") is the same root cause on a third symptom and touches the same function. It introduces bind_terminal_sequence_handlers() and converts only the two recognized Shift+Space payloads. Under your patch both are already correct:

'\x1b[27;2;32~'  ->  (' ', ' ')
'\x1b[32;2u'     ->  (' ', ' ')

So the special-case becomes redundant if this lands first, and if #88109 lands first this still has to touch the same lines. That is a sequencing decision for the maintainers rather than something either of you can resolve alone, but it should be made deliberately — I have left the same note there.

One thing I could not settle, offered as a question rather than a finding

Your patch narrows data for any single-character key value. That also reaches the non-first element of a tuple mapping, which prompt_toolkit blanks on purpose:

for i, k in enumerate(key):
    self._call_handler(k, insert_text if i == 0 else "")
    # "only pass data payload to first KeyPress (so that we won't insert it
    #  multiple times)"

Measured, unmodified Alt+a:

main         ->  [(Keys.Escape, '\x1b[27;3;97~'), ('a', '')]
this branch  ->  [(Keys.Escape, '\x1b[27;3;97~'), ('a', 'a')]

I tried to determine whether that produces a visible double-insert for an Alt+ combination that has no binding, and I could not build a KeyProcessor harness faithful enough to trust the answer — plain a inserted nothing in my rig, so the rig was wrong, not the patch. So this is an observation, not a claim that it regresses anything.

If it is easy for you to check: press an unbound Alt+<letter> under modifyOtherKeys=2 before and after, and see whether the letter now lands in the buffer. If it does, the fix is presumably to skip the narrowing when insert_text is already empty, which preserves prompt_toolkit's intent and costs nothing for the case this PR is actually about:

if insert_text and isinstance(key, str) and len(key) == 1:
    insert_text = key

Everything else here reads right to me, including fetching _call_handler defensively so a prompt_toolkit rename degrades to a no-op rather than raising through install_modify_other_keys_aliases() into cli.py's blanket except Exception: pass. That failure mode — one raise silently skipping the installers that run after it — is a real trap and I am glad it is called out in the docstring.

Not opening a competing PR; this one should land.

@hoohugokim

Copy link
Copy Markdown

Confirming this on Linux + Ghostty, and adding three datapoints: it still applies to today's main, it also fixes the keypad symptom (#90640) that @jackulau flagged above, and there's a user-side mitigation for anyone stuck until this lands.

Environment

  • Hermes Agent v0.20.4, git install, main @ 6f31cfad7
  • prompt_toolkit 3.0.52, Python 3.11.16
  • Ghostty 1.2.3, Pop!_OS 24.04 LTS (kernel 7.0.11), TERM=ghostty, TERM_PROGRAM=ghostty
  • Classic CLI (prompt_toolkit), not the Ink TUI

Symptom on hardware. Shift+letter inserted ^[[27;2;<code>~ into the prompt buffer — every capital untypeable, so the composed prompt was polluted with escape text. Ctrl+ and Alt+ combos were unaffected, which is the tell described in the PR body.

Verification. Applied this branch at 472488f onto 6f31cfad7 and drove a real PromptSession over create_pipe_input():

case bytes before after
Shift+Q (Ghostty sends the shifted cp) \x1b[27;2;81~ '\x1b[27;2;81~' 'Q'
Shift+Q (unshifted cp) \x1b[27;2;113~ '\x1b[27;2;113~' 'Q'
Shift+Q (Kitty CSI-u) \x1b[113;2u '\x1b[113;2u' 'Q'
typed sentence \x1b[27;2;72~ello \x1b[27;2;87~orld '\x1b[27;2;72~ello \x1b[27;2;87~orld' 'Hello World'
Shift+Space \x1b[27;2;32~ '\x1b[27;2;32~' ' '
keypad 8 \x1b[57407u '\x1b[57407u' '8'
keypad . \x1b[57409u '\x1b[57409u' '.'
keypad 8, NumLock twin \x1b[57407;129u '\x1b[57407;129u' '8'
plain typing hi there 'hi there' 'hi there'
Ctrl+B / Alt+f / Shift+Tab / Shift+Enter fires binding fires binding (unchanged)

All 26 Shift+letters type their capital under all three encodings (78/78); no regression in the
Keys-valued paths.

Still applies to current main. The GitHub mergeability shows UNKNOWN, so: git apply --check against 6f31cfad7 succeeds, hunks at offsets +26 and +106. No conflict with the lock-bit work that landed after this branch was cut (446da3ef5, 9ed06ca2b).

#90640 confirmed as a duplicate. The keypad rows above are the same defect — functional_map maps the PUA keypad codepoints to character values (57399 + d → str(d)), so they leak for exactly the reason Shift+letter does, and are fixed by the same patch. That issue has no PR of its own.

One TERM_PROGRAM detail worth documenting. Users trying to dodge this by changing their terminal's TERM will not succeed, and it costs a while to work out why: _terminal_supports_extended_enter_keys() gates the CSI >4;2m push on TERM_PROGRAM, which Ghostty sets regardless of the term config. Setting term = "ghostty" (so TERM=ghostty, no xterm- prefix) and fully restarting still pushes modifyOtherKeys and still leaks.

Mitigation until this lands, which also confirms the push is the trigger:

# ~/.hermes/config.yaml
display:
  cli_multiline_shortcuts: false

That gates both push sites (cli.py:20123, cli.py:8938), so the terminal stays in legacy
encoding and Shift+letter arrives as a plain character. Cost is the modified-Enter reporting this
mode exists for. Verified working here.

Minor suggestion. Worth tightening the existing helper too: _parse() in tests/cli/test_modify_other_keys_aliases.py returns [kp.key for kp in out] and discards kp.data, which is why 26 green Shift+letter assertions coexisted with a fully broken key. Returning (kp.key, kp.data) pairs there would have failed the moment #87511 landed, and it guards the whole character-valued half of the table rather than just the cases in this PR's new file.

Investigated with Claude Code.

@tireiron

Copy link
Copy Markdown

@krunkosaurus what is the plan / ETA for shipping this?

@krunkosaurus

Copy link
Copy Markdown
Contributor Author

@krunkosaurus what is the plan / ETA for shipping this?

Not up to me. I'm just a random dev

@tireiron

Copy link
Copy Markdown

@kshitijk4poor are you able to help get this merged?

@alt-glitch alt-glitch added the duplicate This issue or pull request already exists label Aug 21, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

Duplicate of #88097. Both PRs normalize Vt100Parser KeyPress.data for character-valued extended-key aliases so raw CSI bytes do not reach self-insert; #88097 is the earlier open, broader implementation.

@dosment

dosment commented Aug 21, 2026

Copy link
Copy Markdown

Independent confirmation from Fedora Linux + Ghostty: this is still reproducible on Hermes v0.20.5, and the KeyPress.data normalization fixes it.

Environment:

  • Fedora Linux 44, kernel 7.1.8-200.fc44.x86_64
  • Ghostty 1.3.1-4.fc44 (GTK/Wayland)
  • Hermes Agent v0.20.5, classic CLI, local source based on b2c4f1f
  • Python 3.11.16
  • prompt_toolkit 3.0.52

Observed with Shift+I:

bytes: ESC[27;2;73~
before: KeyPress(key="I", data="\x1b[27;2;73~")
after:  KeyPress(key="I", data="I")

The existing key-only regression test passed before the fix because it discarded KeyPress.data; prompt_toolkit's self-insert path uses data, so the raw sequence still reached the prompt.

I applied an equivalent local _call_handler normalization and verified:

  • focused CLI terminal-input suite: 252 passed, 1 skipped
  • real PTY launch under TERM=xterm-ghostty / TERM_PROGRAM=ghostty
  • Shift+I no longer rendered the raw [27;2;73~ sequence
  • normal /exit returned status 0 and emitted the explicit CSI >4;0m cleanup

This PR addresses the root cause seen on current Ghostty/Fedora hardware, not just a synthetic parser case.

@Observert

Copy link
Copy Markdown

Reproduced your diagnosis exactly and it's correct — this is in fact the gap that breaks Shift+letter on Ghostty.

On current main (v0.20.5, @ fc7523ca), after install_modify_other_keys_aliases() runs:

ANSI_SEQUENCES['\x1b[27;2;73~']   -> 'I'
parse('\x1b[27;2;73~')            -> KeyPress(key='I', data='\x1b[27;2;73~')

key is right (so the #87511 key-level tests pass), but the parser reports data as the raw matched bytes and the default Keys.Any binding inserts event.data. insert_text('\x1b[27;2;73~') is exactly why Shift+letters still type [27;2;73~ despite the mapping. Your _install_literal_key_data_patch() narrowing data to the character for character-valued entries is the right, minimal fix, and the submitted-text test approach is the correct way to catch it (the key-level assertions are precisely why this survived #87511's suite).

Interim data point for the thread: a level-1 workaround (CSI >4;2m -> CSI >4;1m, the root-cause-fix path from #87390) also resolves the symptom for the Shift+letter class because the terminal then never emits those sequences. Your data-path fix is the proper permanent solution — it also covers #87631 (non-Latin/Cyrillic) and #90640 (keypad), which level-1 alone doesn't fully address.

Signed off on your approach.

@krunkosaurus

Copy link
Copy Markdown
Contributor Author

@alt-glitch Thanks for the triage note. One small chronology correction: #87785 was opened on 2026-08-16 at 15:54 UTC, while #88097 was opened on 2026-08-17 at 03:09 UTC. This PR therefore predates #88097 by about 11 hours and 15 minutes, rather than #88097 being the earlier PR.

Regardless of which implementation the maintainers prefer, the underlying issue remains unresolved in the current v0.20.5 release, and both PRs are still open. I can still reproduce it locally in Ghostty: the parser correctly maps Shift+I to key='I', but leaves data as the raw \x1b[27;2;73~ sequence. Since prompt_toolkit's self-insert path uses data, the raw CSI bytes still reach the input buffer. Several other users have independently reproduced the same behavior, including the related Shift+Space and keypad cases.

It would be helpful to consolidate on one implementation or get maintainer direction on what should change before merge. I'm happy to rebase or adapt this PR if needed; I mainly want to make sure one of the fixes lands so affected users are not left with both repairs open and the bug still present.

@alt-glitch

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

Duplicate of open #88097: both patch Vt100Parser KeyPress.data so mapped character aliases cannot self-insert raw CSI bytes. #88097 remains the cleaner canonical implementation.

@alt-glitch alt-glitch removed the duplicate This issue or pull request already exists label Aug 23, 2026
obelisk-complex pushed a commit to obelisk-complex/hermes-agent that referenced this pull request Aug 24, 2026
…input

Under modifyOtherKeys level 2, Ghostty encodes Shift+key as ESC[27;2;<cp>~.
The alias table decoded the key (e.g. Shift+A -> 'A') but prompt_toolkit's
Vt100Parser passed the raw matched escape bytes as KeyPress.data, and the
default self-insert binding inserts event.data -- so the raw sequence landed
in the buffer (e.g. '[27;2;65~') instead of the character.

- install_literal_key_data_patch(): wrap Vt100Parser._call_handler so
  single-character string keys carry the decoded char as insert_text
  (type(key) is str guard leaves Keys enums untouched)
- map the 11 Ghostty-escaped shifted symbols (0x40-0x7F: @ [ \\ ] ^ _ ` { | } ~)
  tilde-form only -- codepoint is the already-shifted, layout-resolved text
- add buffer-level regression tests through a real Application + TextArea,
  incl. negative control (unpatched parser leaks raw bytes)

Refs upstream NousResearch/hermes-agent NousResearch#87390, NousResearch#86866, NousResearch#87785
obelisk-complex pushed a commit to obelisk-complex/hermes-agent that referenced this pull request Aug 25, 2026
…input

Under modifyOtherKeys level 2, Ghostty encodes Shift+key as ESC[27;2;<cp>~.
The alias table decoded the key (e.g. Shift+A -> 'A') but prompt_toolkit's
Vt100Parser passed the raw matched escape bytes as KeyPress.data, and the
default self-insert binding inserts event.data -- so the raw sequence landed
in the buffer (e.g. '[27;2;65~') instead of the character.

- install_literal_key_data_patch(): wrap Vt100Parser._call_handler so
  single-character string keys carry the decoded char as insert_text
  (type(key) is str guard leaves Keys enums untouched)
- map the 11 Ghostty-escaped shifted symbols (0x40-0x7F: @ [ \\ ] ^ _ ` { | } ~)
  tilde-form only -- codepoint is the already-shifted, layout-resolved text
- add buffer-level regression tests through a real Application + TextArea,
  incl. negative control (unpatched parser leaks raw bytes)

Refs upstream NousResearch/hermes-agent NousResearch#87390, NousResearch#86866, NousResearch#87785
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/cli CLI entry point, hermes_cli/, setup wizard P2 Medium — degraded but workaround exists type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants