Skip to content

[Bug]: Cmd/Option+Arrow during IME composition moves the composing character to the cursor destination (Korean & Japanese, macOS) #12871

Closed

Description

@ethznn

Operating system

macOS

Orca version

v1.4.173

Details

Short summary

In the integrated terminal, pressing a cursor-movement chord (Cmd+←/→, Option+←/→) while an IME composition is active moves the cursor and takes the in-progress (preedit) character along with it. The composing character ends up committed at the cursor's destination instead of at the position where it was being typed.

With the Korean 2-Set IME the character is relocated. With the Japanese Kotoeri (Romaji) IME the relocated preedit additionally overwrites the character already at the destination, so previously committed text is lost.

What happened?

Typing 가나 다라 마바사 with the macOS Korean 2-Set input source (the final syllable 사 still composing), then pressing:

  • Option+← (move back one word) produces 가나 '사'다라 마바
  • Cmd+← (move to line start) produces '사'가나 다라 마바

(quotes mark the relocated syllable — they are not actually typed)

The composing 사 follows the cursor jump and is inserted at the destination.

Japanese reproduces it too. With committed text 가나 다라さ on the line and a new さ still in preedit, pressing Cmd+← produces 'さ'나 다라さ — the preedit jumped to the line start and replaced the first character (가 is gone).

How can we reproduce it?

  1. macOS, integrated terminal, with a TUI input line active (reproduced with the Claude Code CLI input).
  2. Select the Korean 2-Set input source.
  3. Type 가나 다라 마바사 and stop — do not press Space/Enter, so 사 is still composing.
  4. Press Option+← or Cmd+←.
  5. Observe the composing syllable move to wherever the cursor lands (가나 '사'다라 마바 / '사'가나 다라 마바).

Japanese variant:

  1. Same terminal, some committed text already on the line.
  2. Select Japanese – Romaji (Kotoeri) and type sa so さ is in preedit.
  3. Press Cmd+←.
  4. The preedit さ jumps to the line start and overwrites the character that was there.

Expected behavior

A cursor-movement chord during composition should behave like native macOS text fields and Terminal.app: the active composition is committed in place first (or the chord is ignored), and only then does the cursor move. The preedit must never travel with the cursor or overwrite existing text.

Actual behavior

The movement chord reaches the terminal while the composition is still open, so the composition session survives the cursor move and later commits at the new cursor position. In the Japanese case the relocated preedit replaces the glyph at the destination cell.

Observations that may help locate the cause

  • A bare arrow key during composition behaves differently: it commits the pending syllable in place (as expected). Only modifier+arrow chords leak through while the composition stays active, which suggests the chord path (shortcut/keyboard resolver → escape sequence to the PTY) is evaluated without waiting for the pending composition, while the bare-key path is composition-aware.
  • The preedit is rendered at the cursor cell, so once the cursor moves mid-composition the preedit visually re-anchors to the new position, and the eventual commit lands there. Possibly related rendering context: [Bug]: Korean IME preedit overlay hides the character right after the cursor when composing mid-line (macOS) #12545 (preedit overlay anchored to the cursor cell).
  • Reproduced with both macOS built-in Korean 2-Set and Kotoeri Romaji, so it is IME-generic rather than Korean-specific. Vietnamese/Chinese IMEs are likely affected as well but were not tested.

Suggested fix direction

On keydown of a cursor-movement chord while a composition is active, finalize the composition at its current position before forwarding the movement escape sequence to the PTY (mirroring what already happens for bare arrow keys), or alternatively swallow the chord entirely during composition.

The pending #12732 sequences shortcut bytes behind an open composition and lists Cmd+←/→ and Alt+←/→ among the affected chords, so it looks like it would resolve this symptom as well — this report adds the user-visible reproduction for those chords (relocation, plus the Japanese overwrite variant) in case it is useful as a verification scenario. Happy to help test a build, and I am preparing regression tests that pin these two scenarios.

Environment

  • macOS 26.5 (Apple Silicon)
  • Orca v1.4.173, local workspace, integrated terminal
  • IMEs: macOS Korean 2-Set (com.apple.inputmethod.Korean.2SetKorean), Japanese Kotoeri Romaji (com.apple.inputmethod.Kotoeri.RomajiTyping.Japanese)
  • Reproduced inside a Claude Code CLI session (raw-mode TUI input line).

Activity

  1. nwparker commented on Aug 8, 2026

    @nwparker
    Contributor

    Still open — this one isn't fixed, and the fix path you pointed at needs rethinking.

    We landed a large IME change on main (17b3dff) that removes the first-party composition layer and hands composition back to xterm. #12732 was building on sendTerminalInputAfterComposition / deferredNewlineSender, and both are gone, so it can't land as written.

    Your diagnosis still holds though, and it's the useful part: bare arrows are composition-aware while modifier+arrow chords are not. That asymmetry is in the chord path, independent of the deferral machinery we removed. The replacement is to have the chord path consult composition state directly — isImeOwnedKeyboardEvent in @/lib/ime-composition-keyboard-event is what the rest of the surfaces now use for this.

    Worth flagging one thing we learned the hard way: gate on event.code, not event.key. A CJK input source rewrites key while code keeps the physical key, and that alone caused two separate issues (#12171, #13033).

    Yes please to the regression tests — the Japanese overwrite variant especially. Recorded traces beat hand-written event shapes here; real IMEs produce sequences nobody guesses right.

  2. ethznn commented on Aug 8, 2026

    @ethznn
    ContributorAuthor

    Recorded evidence update: on current main (d64ccc71b) this no longer reproduces on macOS — in any of the five scenarios I recorded.

    Following your note, I captured real-IME traces (DOM events + terminal.onData + PTY-side bytes, boundary-probe format) plus screenshot series on stock macOS, and manually re-verified by typing on a real keyboard (Korean and Japanese) in the same dev build, for: the two Korean relocation cases (Option+←, Cmd+← with 사 composing), Cmd+→, the Japanese overwrite variant (Kotoeri さ preedit over committed text, Cmd+←), and a paired ABC negative.

    Findings:

    • Korean: the chord commits the preedit in place — compositionend fires with the syllable, and the PTY receives …마바사 before the movement byte (onData chunk is "사\x01"). Screenshots at keydown+1 frame and +300ms are pixel-identical: no relocation, no transient glitch.
    • Japanese (Kotoeri): the chord is swallowed whole by the IME — no compositionend, no movement byte reaches the PTY, the preedit stays at the cursor, and the destination glyph survives. No overwrite.
    • Event shape (answering the gating question): during composition every modifier+arrow keydown arrives as keyCode: 229, isComposing: true, with key/code unrewritten — so isImeOwnedKeyboardEvent already matches these events as-is.

    Your structural point stands, though: nothing in the chord path consults composition state deliberately, so the current correct ordering is a property of the OS-IME + xterm-owned composition interplay rather than anything pinned. That seems worth locking down — I'm preparing a tests-only PR that transcribes these traces into regression tests (the Korean relocation cases, the Japanese variant you asked for, and the negative). If you suspect remaining gaps on shapes I haven't recorded — Linux ibus/fcitx5, Windows, phrase-level Chinese — point me at them and I'll record those too.

  3. ethznn commented on Aug 8, 2026

    @ethznn
    ContributorAuthor

    Update on where the tests went: they are on #12732 rather than a standalone PR — the chord rework landed there, and the fixtures belong with the behavior they verify. Two commits on ethznn:test/ime-chord-traces-v3, offered there for cherry-pick: the in-app trace fixtures pooled into that PR's replay rig, and a native macOS e2e spec asserting pty byte order for the Korean, Japanese and non-IME cases. Happy to split them into their own PR instead if you would rather tests land separately.

    One measurement worth surfacing here, because it widens this issue rather than closing it. The relocation symptom this was filed for is gone on current main, but the other half — the chord being swallowed outright during composition — is IME-general rather than a Kotoeri quirk. Across ten states (Kotoeri converted / live-converted / unconverted single / unconverted multi, Simplified Pinyin with and without a candidate window, Traditional Zhuyin), the OS delivers the chord keydown (ArrowLeft, keyCode 229, isComposing: true) at window capture and then never delivers its keyup at any level, so nothing downstream can recover the press. 2-Set Korean is the exception: it ends the composition on the chord itself, and the keyup arrives with isComposing: false. Verified with synthesized keys and re-checked by hand on a real keyboard. Traces and the full table are on #12732.

  4. ethznn commented on Aug 8, 2026

    @ethznn
    ContributorAuthor

    Closing the loop on my last note here: the Pinyin and Zhuyin cases I flagged as affected are covered by the fix on #12732 as of 84517d74b — the Command chord now resolves on the Command key's own release, which is delivered while the composition is still live. Verified by hand as well as in the rig: with a Pinyin preedit up, Cmd+← then commit puts \x01 on the pty and the cursor at line start. Nothing further needed from my side here; the remaining discussion is on the PR.

  5. added a commit that references this issue on Aug 15, 2026
    a663f1b
  6. added a commit that references this issue on Oct 15, 2026
    fb1df36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

os:macosIssues affecting macOS

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions