Skip to content

fix(terminal): forward Ctrl+<letter> to PTY under non-Latin IME#4917

Open
wnduqrla wants to merge 1 commit into
superset-sh:mainfrom
wnduqrla:fix/non-latin-ime-ctrl-letter-forwarding
Open

fix(terminal): forward Ctrl+<letter> to PTY under non-Latin IME#4917
wnduqrla wants to merge 1 commit into
superset-sh:mainfrom
wnduqrla:fix/non-latin-ime-ctrl-letter-forwarding

Conversation

@wnduqrla
Copy link
Copy Markdown

@wnduqrla wnduqrla commented May 25, 2026

Problem

When a non-Latin IME (e.g. Korean 2-set, Japanese, Chinese) is active, physical letter keys are mapped to non-ASCII glyphs by the OS. Pressing Ctrl+O while Korean input is active yields:

  • event.code = "KeyO" ✅ (always layout-agnostic)
  • event.key = "ㅐ" ❌ (Korean glyph, charCode 4432)
  • event.ctrlKey = true

xterm derives the Ctrl+ control byte from event.key. Since "ㅐ" is not a Latin letter, xterm silently drops the chord — the byte is never written to the PTY.

This breaks any TUI app shortcut that uses a bare Ctrl+letter while a non-Latin IME is active. Confirmed with:

  • Claude Code Ctrl+O (toggle transcript) and Ctrl+K (clear input line)

Root cause

createTerminalKeyEventHandler returns true to let xterm handle Ctrl+letter chords, but xterm's own key encoder uses event.key to compute the control byte and fails silently when the value is non-ASCII.

Fix

Intercept the chord before xterm runs: when ctrlKey is set, no other modifier is held, event.code is a letter key (Key[A-Z]), and event.key is non-ASCII (charCodeAt(0) > 127), compute the correct control byte from event.code and write it directly to the PTY via terminal.input().

charCode = letter.charCodeAt(0) - 64   // 'A'→1, 'O'→15, 'K'→11, …

event.code is always physical-key-position based and unaffected by IME state, so the correct byte is sent regardless of active input source.

Relation to prior work

Testing

Added 4 unit tests in terminal-key-event-handler.test.ts:

Test Asserts
Korean IME + Ctrl+O → \x0f sent
Korean IME + Ctrl+K → \x0b sent
Latin IME + Ctrl+O → xterm handles normally
Korean IME + Ctrl+Shift+O → not intercepted (modifier guard)

Reproduction

  1. Open Superset Desktop with a terminal pane
  2. Switch input source to Korean (or any non-Latin IME)
  3. Run claude (Claude Code CLI) in the terminal
  4. Press Ctrl+O → transcript toggle does not fire (bug)
  5. Apply this patch → Ctrl+O works correctly ✅

Open in Stage

Summary by cubic

Fixes Ctrl+letter chords being dropped under non‑Latin IMEs by deriving the control byte from event.code and sending it to the PTY. Restores TUI shortcuts like Ctrl+O and Ctrl+K when Korean/Japanese/Chinese input is active.

  • Bug Fixes
    • Intercepts bare Ctrl+letter when event.code is Key[A-Z] and event.key is non‑ASCII; computes A→\x01 … Z→\x1a and writes via terminal.input().
    • Does not intercept when event.key is ASCII (Latin IME) or when Shift/Alt/Meta is pressed; lets xterm handle normally.
    • Adds tests for Korean IME Ctrl+O/K success, Latin IME passthrough, and modifier guard.

Written for commit 40238a1. Summary will update on new commits. Review in cubic

Summary by CodeRabbit

  • Bug Fixes

    • Fixed Ctrl+ keyboard shortcut handling when using non-Latin input methods in the terminal.
  • Tests

    • Added test suite for keyboard event handling with non-Latin input method support, including edge cases with modifier key combinations.

Korean (and other non-Latin) IME maps physical letter keys to non-ASCII
glyphs: pressing O while Korean 2-set is active yields event.key="ㅐ"
instead of "o". xterm derives the Ctrl+<letter> control byte from
event.key, so it silently drops every Ctrl+<letter> chord typed while
such an IME is active — breaking TUI app shortcuts like Claude Code's
Ctrl+O (toggle transcript) and Ctrl+K (clear input).

The fix intercepts these chords in createTerminalKeyEventHandler before
xterm runs: when ctrlKey is set, no other modifier is held, event.code
is a letter key, and event.key is non-ASCII (charCode > 127), compute
the control byte from the physical key position (event.code) and write
it directly to the PTY via terminal.input(). event.code is always
layout- and IME-agnostic, so the correct byte is sent regardless of
which input source is active.

Relates to superset-sh#3365, extends the event.code approach from superset-sh#3391 to the
xterm key-encoder path.
@stage-review
Copy link
Copy Markdown

stage-review Bot commented May 25, 2026

Ready to review this PR? Stage has broken it down into 2 individual chapters for you:

Title
1 Intercept Ctrl+letter chords for non-Latin IMEs
2 Verify IME control byte forwarding with tests
Open in Stage

Chapters generated by Stage for commit 40238a1 on May 25, 2026 8:59am UTC.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 25, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0ea15f01-906d-4785-a788-2a89c3f47467

📥 Commits

Reviewing files that changed from the base of the PR and between 80fc9c3 and 40238a1.

📒 Files selected for processing (2)
  • apps/desktop/src/renderer/lib/terminal/terminal-key-event-handler.test.ts
  • apps/desktop/src/renderer/lib/terminal/terminal-key-event-handler.ts

📝 Walkthrough

Walkthrough

This PR adds a workaround for Ctrl+ key combinations when using non-Latin IMEs like Korean. The handler detects non-ASCII characters in event.key, maps the physical key to a control byte (A→1 through Z→26), sends it to the terminal, and includes test coverage for Korean IME, ASCII-only input, and modifier combinations.

Changes

Non-Latin IME Ctrl+ Fix

Layer / File(s) Summary
Terminal key event handler for non-Latin IME
apps/desktop/src/renderer/lib/terminal/terminal-key-event-handler.ts, apps/desktop/src/renderer/lib/terminal/terminal-key-event-handler.test.ts
Handler now intercepts keydown events for Ctrl+ when non-Latin IME is active (non-ASCII event.key), derives control bytes from event.code, prevents default, and sends to terminal. Tests verify Ctrl+O and Ctrl+K send \x0f and \x0b respectively with Korean IME, and confirm events pass through for ASCII keys and Ctrl+Shift combinations.

🎯 2 (Simple) | ⏱️ ~10 minutes

A rabbit hops through keys with glee,
Non-Latin IME, now handled with care,
Ctrl-bytes flow swift, both tested fair,
Korean input now plays its part,
In the terminal's beating heart! 🐰✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: fixing Ctrl+letter forwarding to PTY under non-Latin IME, which matches the core problem and solution in the changeset.
Description check ✅ Passed The description comprehensively covers the problem, root cause, fix approach, testing, and reproduction steps. While it doesn't strictly follow the provided template structure, the content is complete, detailed, and demonstrates clear intent.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@capy-ai
Copy link
Copy Markdown

capy-ai Bot commented May 25, 2026

Capy auto-review is paused for this organization because the monthly auto-review limit has been reached. Increase the limit or turn it off in billing settings to resume automatic reviews.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 25, 2026

Greptile Summary

This PR fixes Ctrl+letter shortcuts in xterm being silently dropped when a non-Latin IME (Korean, Japanese, Chinese) is active, because xterm derives the control byte from event.key which is a non-ASCII glyph under those IMEs. The fix intercepts bare Ctrl+letter keydown events before they reach xterm and computes the correct byte from event.code, which is always layout-agnostic.

  • Adds an intercept block in createTerminalKeyEventHandler that fires only when ctrlKey is set with no other modifiers, event.code matches Key[A-Z], and event.key is non-ASCII — leaving all Latin-IME and multi-modifier chords on the existing xterm path.
  • Adds four unit tests covering the Korean IME Ctrl+O/K cases, the Latin pass-through, and the Shift-modifier guard.

Confidence Score: 4/5

Safe to merge; the intercept is tightly scoped and does not affect any existing Latin-IME or multi-modifier path.

The fix is logically correct and well-placed in the handler chain. Two minor rough edges exist: wasUserInput is passed as false while the adjacent translateLineEditChord path uses true (risks missing viewport scroll-to-bottom on Ctrl+letter with Korean IME), and there is no isComposing guard, which could cause the intercept to fire during an active composition sequence that xterm itself would have suppressed.

apps/desktop/src/renderer/lib/terminal/terminal-key-event-handler.ts — the new IME intercept block has both issues.

Important Files Changed

Filename Overview
apps/desktop/src/renderer/lib/terminal/terminal-key-event-handler.ts Adds IME intercept block for bare Ctrl+letter when event.key is non-ASCII; correct placement in handler chain but passes wasUserInput=false inconsistently with adjacent translateLineEditChord path and lacks an isComposing guard
apps/desktop/src/renderer/lib/terminal/terminal-key-event-handler.test.ts Four new unit tests cover Korean IME Ctrl+O and Ctrl+K, Latin pass-through, and Shift modifier guard; missing coverage for isComposing edge case

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[KeyboardEvent] --> B{resolveHotkeyFromEvent?}
    B -- yes --> C[return false: host handles hotkey]
    B -- no --> D{translateLineEditChord?}
    D -- yes --> E[terminal.input translation true, return false]
    D -- no --> F{shouldSelectAllShortcut?}
    F -- yes --> G[terminal.selectAll, return false]
    F -- no --> H{shouldBubbleClipboardShortcut?}
    H -- yes --> I[return false: browser paste pipeline]
    H -- no --> J{keydown AND ctrlKey AND NOT meta/alt/shift?}
    J -- no --> M[return true: xterm handles]
    J -- yes --> K{code matches Key A-Z AND key.charCodeAt 0 > 127?}
    K -- no --> M
    K -- yes --> L[Compute byte from code, terminal.input charCode false, return false]
    style L fill:#d4edda,stroke:#28a745
    style E fill:#d4edda,stroke:#28a745
Loading
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
apps/desktop/src/renderer/lib/terminal/terminal-key-event-handler.ts:94
**Possible double-interception when `isComposing` is true**

If an IME composition is in progress (e.g. the user is mid-way through composing a Korean syllable) and presses Ctrl+letter, `isComposing` may be `true`. xterm's own handler skips events where `isComposing` is true, so it would naturally ignore that event. The new block doesn't guard for this state, meaning it would still call `terminal.input()` and `preventDefault()` — potentially swallowing an event that should have been ignored and interrupting IME composition unexpectedly. Adding `&& !event.isComposing` to the outer condition would match xterm's own behavior and avoid this edge case.

### Issue 2 of 2
apps/desktop/src/renderer/lib/terminal/terminal-key-event-handler.ts:97
**`wasUserInput: false` is inconsistent with the rest of the handler**

The `translateLineEditChord` path directly above (line 54) calls `terminal.input(translation, true)`. When `wasUserInput` is `true`, xterm scrolls the viewport to the bottom before delivering the bytes — the expected behavior for a real keypress. Passing `false` here means a user who has scrolled up and then presses Ctrl+K (or any Ctrl+letter) while Korean IME is active won't have the viewport snapped to the current prompt, so the shell's response appears off-screen. Since this block only fires on a genuine `keydown`, `true` is the correct value.

```suggestion
			terminal.input(String.fromCharCode(charCode), true);
```

Reviews (1): Last reviewed commit: "fix(terminal): forward Ctrl+<letter> to ..." | Re-trigger Greptile

!event.shiftKey
) {
const codeMatch = event.code.match(/^Key([A-Z])$/);
if (codeMatch && event.key.charCodeAt(0) > 127) {
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.

P2 Possible double-interception when isComposing is true

If an IME composition is in progress (e.g. the user is mid-way through composing a Korean syllable) and presses Ctrl+letter, isComposing may be true. xterm's own handler skips events where isComposing is true, so it would naturally ignore that event. The new block doesn't guard for this state, meaning it would still call terminal.input() and preventDefault() — potentially swallowing an event that should have been ignored and interrupting IME composition unexpectedly. Adding && !event.isComposing to the outer condition would match xterm's own behavior and avoid this edge case.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/renderer/lib/terminal/terminal-key-event-handler.ts
Line: 94

Comment:
**Possible double-interception when `isComposing` is true**

If an IME composition is in progress (e.g. the user is mid-way through composing a Korean syllable) and presses Ctrl+letter, `isComposing` may be `true`. xterm's own handler skips events where `isComposing` is true, so it would naturally ignore that event. The new block doesn't guard for this state, meaning it would still call `terminal.input()` and `preventDefault()` — potentially swallowing an event that should have been ignored and interrupting IME composition unexpectedly. Adding `&& !event.isComposing` to the outer condition would match xterm's own behavior and avoid this edge case.

How can I resolve this? If you propose a fix, please make it concise.

if (codeMatch && event.key.charCodeAt(0) > 127) {
event.preventDefault();
const charCode = codeMatch[1].charCodeAt(0) - 64; // A→1 … Z→26
terminal.input(String.fromCharCode(charCode), false);
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.

P2 wasUserInput: false is inconsistent with the rest of the handler

The translateLineEditChord path directly above (line 54) calls terminal.input(translation, true). When wasUserInput is true, xterm scrolls the viewport to the bottom before delivering the bytes — the expected behavior for a real keypress. Passing false here means a user who has scrolled up and then presses Ctrl+K (or any Ctrl+letter) while Korean IME is active won't have the viewport snapped to the current prompt, so the shell's response appears off-screen. Since this block only fires on a genuine keydown, true is the correct value.

Suggested change
terminal.input(String.fromCharCode(charCode), false);
terminal.input(String.fromCharCode(charCode), true);
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/renderer/lib/terminal/terminal-key-event-handler.ts
Line: 97

Comment:
**`wasUserInput: false` is inconsistent with the rest of the handler**

The `translateLineEditChord` path directly above (line 54) calls `terminal.input(translation, true)`. When `wasUserInput` is `true`, xterm scrolls the viewport to the bottom before delivering the bytes — the expected behavior for a real keypress. Passing `false` here means a user who has scrolled up and then presses Ctrl+K (or any Ctrl+letter) while Korean IME is active won't have the viewport snapped to the current prompt, so the shell's response appears off-screen. Since this block only fires on a genuine `keydown`, `true` is the correct value.

```suggestion
			terminal.input(String.fromCharCode(charCode), true);
```

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 2 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/desktop/src/renderer/lib/terminal/terminal-key-event-handler.ts">

<violation number="1" location="apps/desktop/src/renderer/lib/terminal/terminal-key-event-handler.ts:97">
P2: Mark this forwarded control byte as user input so xterm applies normal keypress behavior (including scroll-to-bottom and user-input side effects) consistently with the other keyboard translation path.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

if (codeMatch && event.key.charCodeAt(0) > 127) {
event.preventDefault();
const charCode = codeMatch[1].charCodeAt(0) - 64; // A→1 … Z→26
terminal.input(String.fromCharCode(charCode), false);
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.

P2: Mark this forwarded control byte as user input so xterm applies normal keypress behavior (including scroll-to-bottom and user-input side effects) consistently with the other keyboard translation path.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/renderer/lib/terminal/terminal-key-event-handler.ts, line 97:

<comment>Mark this forwarded control byte as user input so xterm applies normal keypress behavior (including scroll-to-bottom and user-input side effects) consistently with the other keyboard translation path.</comment>

<file context>
@@ -76,6 +76,29 @@ export function createTerminalKeyEventHandler(
+			if (codeMatch && event.key.charCodeAt(0) > 127) {
+				event.preventDefault();
+				const charCode = codeMatch[1].charCodeAt(0) - 64; // A→1 … Z→26
+				terminal.input(String.fromCharCode(charCode), false);
+				return false;
+			}
</file context>
Suggested change
terminal.input(String.fromCharCode(charCode), false);
terminal.input(String.fromCharCode(charCode), true);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant