Skip to content

fix(tui): uppercase shift+letter input from kitty keyboard protocol - #37687

Open
liuhao1024 wants to merge 1 commit into
NousResearch:mainfrom
liuhao1024:fix/tui-shift-capitalization
Open

liuhao1024 wants to merge 1 commit into
NousResearch:mainfrom
liuhao1024:fix/tui-shift-capitalization

Conversation

@liuhao1024

Copy link
Copy Markdown
Contributor

What does this PR do?

Fixes Shift+letter capitalization in hermes --tui when the kitty keyboard protocol is active. Previously, holding Shift+A would insert a instead of A because the composer's useInput handler inserted the raw input character without checking the shift modifier.

Related Issue

Fixes #37680

Type of Change

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

Changes Made

  • ui-tui/src/components/textInput.tsx: Add shift-uppercase guard before applyPrintableInsert — when k.shift is true and the input is a single ASCII letter (a-z), uppercase it before insertion. Multi-char paste, bracketed input, and non-ASCII characters pass through unchanged.

How to Test

  1. Open hermes --tui in Ghostty (or any terminal with kitty keyboard protocol support)
  2. Type Shift+A — should insert A, not a
  3. Type Shift+Z — should insert Z, not z
  4. Paste text (Ctrl+V or bracketed paste) — should remain unchanged
  5. Type normal lowercase letters — should remain lowercase
  6. Run cd ui-tui && npm test -- --run — all textInput-related tests should pass (1 pre-existing failure in virtualHeights.test.ts is unrelated)

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(scope):, feat(scope):, etc.)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix/feature (no unrelated commits)
  • I've run pytest tests/ -q and all tests pass
  • I've added tests for my changes (required for bug fixes, strongly encouraged for features)
  • I've tested on my platform: macOS

Documentation & Housekeeping

  • I've updated relevant documentation (README, docs/, docstrings) — or N/A
  • I've updated cli-config.yaml.example if I added/changed config keys — or N/A
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows — or N/A
  • I've considered cross-platform impact (Windows, macOS) per the compatibility guide — or N/A
  • I've updated tool descriptions/schemas if I changed tool behavior — or N/A

Code Intelligence

  • Analyzed: ui-tui/src/components/textInput.tsx (useInput handler, line 1111-1152)
  • Blast radius: LOW — single guard in input handler, only affects kitty keyboard protocol terminals
  • Related patterns: k.shift already used for selection (line 960) and newline insertion (line 974); this extends it to printable character uppercase

When the kitty keyboard protocol is active, Shift+A sends \x1b[97;2u
which hermes-ink reduces to input='a' + key.shift=true. The composer's
useInput handler inserted the lowercase character without checking the
shift flag, so Shift+A produced 'a' instead of 'A'.

Add a guard before applyPrintableInsert that uppercases single ASCII
letters when k.shift is true. Multi-char paste, bracketed input, and
non-ASCII characters pass through unchanged.

Fixes NousResearch#37680
@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/tui Terminal UI (ui-tui/ + tui_gateway/) labels Jun 2, 2026
@teknium1

Copy link
Copy Markdown
Collaborator

Thanks for the focused TUI fix. The premise remains reproducible from the current input pipeline: ui-tui/packages/hermes-ink/src/ink/parse-keypress.ts:537-541 lowercases printable CSI-u keycodes, ui-tui/packages/hermes-ink/src/ink/events/input-event.ts:108-118 passes that lowercase name as input, and ui-tui/src/components/textInput.tsx:1135-1177 inserts it without applying key.shift.

Problems

  • This PR adds no regression test. ui-tui/src/__tests__/textInputBurstInput.test.ts:3-39 tests applyPrintableInsert directly, but the new behavior is in the enclosing TextInput useInput callback.

Suggested changes

  • Add coverage that routes \x1b[97;2u through the composer input path and verifies A is committed, while lowercase and paste input retain their current behavior.

Automated hermes-sweeper review.

@teknium1 teknium1 added sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 13, 2026
@kshitijk4poor

Copy link
Copy Markdown
Contributor

Update: the CLI-side variant of this bug is fixed by #87511, which maps all Shift+letter combos under modifyOtherKeys level 2 in hermes_cli/pt_input_extras.py.

This PR (#37687) fixes the TUI side (ui-tui/src/components/textInput.tsx), which is a separate code path (TypeScript/Ink vs Python/prompt_toolkit). Both PRs are needed — they fix different surfaces of the same underlying problem (issue #37680).

If this PR is merged, both the CLI and TUI will correctly uppercase Shift+letter input on terminals with modifyOtherKeys/Kitty keyboard protocol active.

@liuhao1024

Copy link
Copy Markdown
Contributor Author

Thanks for the cross-link — good to have the two-surface picture spelled out. Confirming the split: this PR fixes the TUI/Ink text input (ui-tui/src/components/textInput.tsx), #87511 fixed the CLI/prompt_toolkit side (hermes_cli/pt_input_extras.py), both against #37680. No overlap in code paths, so both are needed; nothing to change on this branch from your update.

counterposition added a commit to counterposition/hermes-agent that referenced this pull request Aug 24, 2026
Under enhanced key reporting, Shift+letter arrives as an escape
sequence, and hermes-ink rebuilds typed text from the parsed key name
(input-event.ts uses `name` when ctrl is clear). keycodeToName()
unconditionally lowercased printable ASCII and dropped the shift bit,
so every Shift+letter decoded as lowercase: the composer silently
lost capitalization.

This became a default-path regression for Ghostty when the Ink TUI
stopped pushing the kitty keyboard protocol for it (45f1126 on the
maintained fork; upstream equivalent of the cli.py Ghostty exception),
leaving xterm modifyOtherKeys level 2 as the only enhanced mode — and
Ghostty re-encodes every Shift+letter under it. The same decoder gap
was reported for the classic CLI in NousResearch#87390/NousResearch#87631; NousResearch#37687 patches the
composer instead, which leaves other parsed-key consumers broken.

Fix at the decoder: keycodeToName() takes the shift modifier and
capitalizes letters when set (shift && !ctrl at both call sites).
Terminals disagree on which codepoint they report (kitty CSI-u sends
the base key, ESC[97;2u; xterm modifyOtherKeys usually the shifted
result, ESC[27;2;65~), so an uppercase report is preserved and a
lowercase one promoted. Control chords keep the lowercase name because
input-event.ts takes input = name verbatim when ctrl is set, and all
chord matching is case-folded.

Co-Authored-By: ox-alpha <noreply@nousresearch.com>
counterposition added a commit to counterposition/hermes-agent that referenced this pull request Sep 1, 2026
Under enhanced key reporting, Shift+letter arrives as an escape
sequence, and hermes-ink rebuilds typed text from the parsed key name
(input-event.ts uses `name` when ctrl is clear). keycodeToName()
unconditionally lowercased printable ASCII and dropped the shift bit,
so every Shift+letter decoded as lowercase: the composer silently
lost capitalization.

This became a default-path regression for Ghostty when the Ink TUI
stopped pushing the kitty keyboard protocol for it (45f1126 on the
maintained fork; upstream equivalent of the cli.py Ghostty exception),
leaving xterm modifyOtherKeys level 2 as the only enhanced mode — and
Ghostty re-encodes every Shift+letter under it. The same decoder gap
was reported for the classic CLI in NousResearch#87390/NousResearch#87631; NousResearch#37687 patches the
composer instead, which leaves other parsed-key consumers broken.

Fix at the decoder: keycodeToName() takes the shift modifier and
capitalizes letters when set (shift && !ctrl at both call sites).
Terminals disagree on which codepoint they report (kitty CSI-u sends
the base key, ESC[97;2u; xterm modifyOtherKeys usually the shifted
result, ESC[27;2;65~), so an uppercase report is preserved and a
lowercase one promoted. Control chords keep the lowercase name because
input-event.ts takes input = name verbatim when ctrl is set, and all
chord matching is case-folded.

Co-Authored-By: ox-alpha <noreply@nousresearch.com>
counterposition added a commit to counterposition/hermes-agent that referenced this pull request Sep 5, 2026
Under enhanced key reporting, Shift+letter arrives as an escape
sequence, and hermes-ink rebuilds typed text from the parsed key name
(input-event.ts uses `name` when ctrl is clear). keycodeToName()
unconditionally lowercased printable ASCII and dropped the shift bit,
so every Shift+letter decoded as lowercase: the composer silently
lost capitalization.

This became a default-path regression for Ghostty when the Ink TUI
stopped pushing the kitty keyboard protocol for it (45f1126 on the
maintained fork; upstream equivalent of the cli.py Ghostty exception),
leaving xterm modifyOtherKeys level 2 as the only enhanced mode — and
Ghostty re-encodes every Shift+letter under it. The same decoder gap
was reported for the classic CLI in NousResearch#87390/NousResearch#87631; NousResearch#37687 patches the
composer instead, which leaves other parsed-key consumers broken.

Fix at the decoder: keycodeToName() takes the shift modifier and
capitalizes letters when set (shift && !ctrl at both call sites).
Terminals disagree on which codepoint they report (kitty CSI-u sends
the base key, ESC[97;2u; xterm modifyOtherKeys usually the shifted
result, ESC[27;2;65~), so an uppercase report is preserved and a
lowercase one promoted. Control chords keep the lowercase name because
input-event.ts takes input = name verbatim when ctrl is set, and all
chord matching is case-folded.

Co-Authored-By: ox-alpha <noreply@nousresearch.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/tui Terminal UI (ui-tui/ + tui_gateway/) P2 Medium — degraded but workaround exists sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: TUI drops shift+letter capitalization

4 participants