Repository navigation
Fix Zhuyin candidate-window arrow navigation (#3691), with guardrails - #3850
austinywang wants to merge 5 commits into
Conversation
Add the issue #3691 regression matrix before changing production code so the first commit demonstrates the current post-revert behavior. Korean, Japanese, Pinyin, Cangjie, Latin/ABC, and Zhuyin without marked text all continue to forward navigation keys, while Zhuyin Up/Down during marked text still leak into Ghostty. Constraint: Issue #3691 guardrails require a two-commit structure with failing Zhuyin tests first Constraint: Production code is intentionally untouched in this commit Confidence: high Scope-risk: narrow Directive: Do not broaden these tests into generic IME suppression; they are guarding against the reverted #3694 shape Tested: ./scripts/test-unit.sh test -only-testing:cmuxTests/CJKIMEMarkedSelectionTests (expected failure: only testZhuyinDownArrowDuringCompositionOpensCandidates and testZhuyinUpArrowDuringCompositionMovesCandidateSelection) Not-tested: Manual IME repro on tagged dev build not yet captured in this commit
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
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:
📝 WalkthroughWalkthroughRoutes Up/Down arrow events to AppKit's IME handler only when Apple Traditional Zhuyin is active and marked text exists; integrates into keyDown, adds a test-only NSTextInputContext.handleEvent swizzle, and expands tests covering multiple IMEs to prevent regressions. ChangesZhuyin Arrow-Key Candidate Window Routing
Sequence Diagram(s)sequenceDiagram
participant User
participant GhosttyNSView
participant NSTextInputContext
participant Shell
User->>GhosttyNSView: keyDown (Up/Down)
GhosttyNSView->>GhosttyNSView: shouldRouteKeyToZhuyinCandidateInsteadOfTerminal?
alt predicate true
GhosttyNSView->>NSTextInputContext: handleEvent(event)
Note right of NSTextInputContext: IME handles candidate navigation
GhosttyNSView->>GhosttyNSView: syncPreedit(clearIfNeeded:)
GhosttyNSView-->>User: return (do not forward to Shell)
else predicate false
GhosttyNSView->>GhosttyNSView: interpretKeyEvents / forward
GhosttyNSView->>Shell: forwarded key bytes
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 14 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (14 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
Greptile SummaryFixes #3691 by intercepting plain Down/Up arrow keys during active Apple Zhuyin composition (
Confidence Score: 5/5Safe to merge; the Zhuyin routing is tightly scoped to a single input source in active composition, all other IME paths and the normal terminal forwarding path are unchanged. The change adds six independent guard conditions before any key is diverted, none of which affect non-Zhuyin input sources or keys outside Down/Up during active composition. The regression test suite explicitly covers Korean, Japanese, Pinyin, Cangjie, and Latin/ABC sources to confirm they are unaffected. The two minor issues found do not affect correctness under normal use. The Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[keyDown: Down or Up event] --> B{markedText active?}
B -- No --> C[Normal path: interpretKeyEvents with translationEvent]
B -- Yes --> D{Exact Apple Zhuyin source\nplain modifiers\nDown or Up?}
D -- No --> C
D -- Yes --> E[interpretKeyEvents with original event\ncapture commandSelector via doCommand]
E --> F{accumulatedText non-empty?\ni.e. IME committed text}
F -- Yes --> G[Fall through to normal\naccumulated-text send path]
F -- No --> H{shouldExpandZhuyinCandidates?\nDown + moveDown: + unchanged composition}
H -- Yes --> I[requestZhuyinCandidateExpansion\nNSTextInputContext.handleEvent Space]
H -- No --> J[syncPreedit + early return\nkey consumed by AppKit]
I --> J
C --> K[keyboard layout change check]
K --> L[syncPreedit]
L --> M{shouldSuppressGhosttyKeyForwarding?}
M -- Yes --> N[return: IME consumed event]
M -- No --> O[Send key event to Ghostty terminal]
Reviews (10): Last reviewed commit: "Prevent wrong-event Zhuyin expansion fal..." | Re-trigger Greptile |
| private var appleTraditionalZhuyinInputSourceId: String { | ||
| "com.apple.inputmethod.TCIM.Zhuyin" | ||
| } |
There was a problem hiding this comment.
The
appleTraditionalZhuyinInputSourceId property is a computed var, so it allocates a fresh String on every access. isAppleTraditionalZhuyinInputSource calls it on each invocation, and the value never changes. Promoting this to a private static let makes the intent — a compile-time constant — explicit and avoids the repeated allocation.
| private var appleTraditionalZhuyinInputSourceId: String { | |
| "com.apple.inputmethod.TCIM.Zhuyin" | |
| } | |
| private static let appleTraditionalZhuyinInputSourceId = "com.apple.inputmethod.TCIM.Zhuyin" |
There was a problem hiding this comment.
Fixed in 737a272 by changing the Zhuyin source identifier to a private static let.
— Claude Code
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@Sources/GhosttyTerminalView.swift`:
- Around line 7517-7520: The call to
shouldRouteKeyToZhuyinCandidateInsteadOfTerminal currently passes
translationEvent (whose modifiers may be rewritten by
ghostty_surface_key_translation_mods) causing modified arrows to be
misclassified; change the call to pass the original key event (the unmodified
event variable in scope) instead of translationEvent so the Zhuyin “plain arrow”
gate uses original modifiers; keep the rest of the logic and only swap the
argument to the original event to avoid suppressing legitimate forwarded keys.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: a18ce62f-417d-4d80-bf97-5242f427b647
📒 Files selected for processing (3)
Sources/GhosttyNSView+IMEComposition.swiftSources/GhosttyTerminalView.swiftcmuxTests/CJKIMEMarkedSelectionTests.swift
a7c982f to
efd9507
Compare
efd9507 to
737a272
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@cmuxTests/CJKIMEMarkedSelectionTests.swift`:
- Around line 85-97: The test's terminalNavigationKeyCases uses empty text which
causes the production keyDown early-return (via ghostty_surface_key) and skips
Zhuyin routing; update ForwardedKeyCase entries in terminalNavigationKeyCases so
their text fields contain the real NSEvent function-key Unicode scalars for
navigation keys (i.e., use the NSEvent.charactersIgnoringModifiers values rather
than ""), for example set Up to the function-key scalar (e.g., "\u{F700}") and
similarly populate Left, Right, Down, PageUp, PageDown, Home, End with their
corresponding function-key scalars while keeping Space as " ", so the synthetic
events exercise the same keyDown path that reaches the Zhuyin routing logic.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 23d15c1d-9a1e-41db-890a-658a34f83f8a
📒 Files selected for processing (4)
Sources/GhosttyNSView+IMEComposition.swiftSources/GhosttyTerminalView.swiftcmuxTests/CJKIMEInputTests.swiftcmuxTests/CJKIMEMarkedSelectionTests.swift
737a272 to
c5833bc
Compare
c5833bc to
ed9dd5e
Compare
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)
cmuxTests/CJKIMEMarkedSelectionTests.swift (1)
59-97:⚠️ Potential issue | 🟠 Major | ⚡ Quick winAdd
.numericPadmodifier to arrow key fixtures only.Arrow keys (Left, Right, Up, Down) should include
.numericPadin their synthetic events per AppKit behavior. However, Home/End/PageUp/PageDown do not carry.numericPadper Apple's official API—only arrow keys are distinguished. The current fixtures omit.numericPadfor all navigation keys, so tests will not catch regressions in the production "normalized modifier flags" logic that strips.numericPad.Apply the fix to arrow keys only:
Left,Right,Up,Down: addmodifiers: [.numericPad]PageUp,PageDown,Home,End: keep as-is (no.numericPad)Space: keep as-is- Lines 142-145, 212-216: add
modifiers: [.numericPad]when callingkeyEvent()for arrow keys- Lines 294-299: change
modifiers: [.shift]tomodifiers: [.shift, .numericPad]for shifted arrow tests🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@cmuxTests/CJKIMEMarkedSelectionTests.swift` around lines 59 - 97, The arrow-key fixtures need the .numericPad modifier added so synthetic events match AppKit: update the terminalNavigationKeyCases entries for "Left", "Right", "Up", and "Down" (the ForwardedKeyCase instances used by terminalNavigationKeyCases) to generate key events with modifiers: [.numericPad] when calling keyEvent(...), leave "PageUp", "PageDown", "Home", "End", and "Space" unchanged, and also update any tests that call keyEvent(...) for shifted arrow key scenarios (the shifted-arrow cases) to use modifiers: [.shift, .numericPad] instead of just [.shift]; locate changes around usages of keyEvent(...) and the terminalNavigationKeyCases array.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@cmuxTests/CJKIMEMarkedSelectionTests.swift`:
- Around line 59-97: The arrow-key fixtures need the .numericPad modifier added
so synthetic events match AppKit: update the terminalNavigationKeyCases entries
for "Left", "Right", "Up", and "Down" (the ForwardedKeyCase instances used by
terminalNavigationKeyCases) to generate key events with modifiers: [.numericPad]
when calling keyEvent(...), leave "PageUp", "PageDown", "Home", "End", and
"Space" unchanged, and also update any tests that call keyEvent(...) for shifted
arrow key scenarios (the shifted-arrow cases) to use modifiers: [.shift,
.numericPad] instead of just [.shift]; locate changes around usages of
keyEvent(...) and the terminalNavigationKeyCases array.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 2a51012b-2439-442c-a59d-a83c1ec74330
📒 Files selected for processing (4)
Sources/GhosttyNSView+IMEComposition.swiftSources/GhosttyTerminalView.swiftcmuxTests/CJKIMEInputTests.swiftcmuxTests/CJKIMEMarkedSelectionTests.swift
ed9dd5e to
475b52e
Compare
475b52e to
ba23271
Compare
ba23271 to
2c9695e
Compare
Apple Zhuyin reports plain Down during marked text as moveDown: without changing composition, so simply consuming the arrow or asking AppKit's app-candidate panel does not show the IME candidate menu. Keep the #3691 intercept exact, then replay the IME candidate expansion key into NSTextInputContext only for that unchanged Down case. This also keeps the Zhuyin candidate-arrow test cleanup symmetrical by restoring the debug expansion handler after the commit-from-interpretKeyEvents regression case. Constraint: #3691 scope is exact Apple Zhuyin, active marked text, plain Down/Up only; Up remains consume-only for candidate navigation Rejected: private AppKit candidate presentation selectors | dogfood still showed no Zhuyin candidate menu Rejected: broad IME routing/keyUp state/window reroute | reverted in #3849 after breaking other IMEs Confidence: medium Scope-risk: narrow Directive: Do not widen this beyond Apple Zhuyin marked-text Down without per-IME shell-forwarding regression guards and manual IME dogfood Tested: git diff --check Tested: ./scripts/test-unit.sh test -only-testing:cmuxTests/CJKIMEMarkedSelectionTests Tested: ./scripts/reload.sh --tag issue-3691-zhuyin-candidate-arrows Not-tested: latest local manual IME dogfood; user/maintainer should verify Apple Zhuyin Down opens the candidate window in the tagged app
2c9695e to
18bf468
Compare
|
Addressed the latest Cursor Bugbot finding in amended commit |
The PR branch was behind main after CI review. Merging origin/main brings the current app, test, and workflow changes into this branch before final CI validation, without intentionally changing the Zhuyin candidate-arrow implementation beyond the clean auto-merge. Constraint: iterate-pr workflow requires validating the PR against the latest base branch Confidence: medium Scope-risk: moderate Directive: Treat this as a base-sync merge only; do not use it to infer new Zhuyin behavior changes Tested: git diff --check HEAD^..HEAD Not-tested: Local unit/UI tests skipped per user instruction; post-push CI will validate
Synthetic navigation events already use AppKit function-key characters. Add the matching numericPad modifier for arrow keys only so the Zhuyin regression tests exercise the same modifier normalization path as real arrow-key NSEvents. Constraint: CodeRabbit review noted arrow keys carry numericPad while PageUp, PageDown, Home, End, and Space do not Confidence: high Scope-risk: narrow Directive: Keep these fixtures aligned with real NSEvent shape; do not add numericPad to non-arrow navigation keys without AppKit evidence Tested: git diff --check Not-tested: Local unit/UI tests skipped per user instruction; CI will validate after push
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit c7c66ce. Configure here.
The candidate-expansion path should only send the synthetic Space event to AppKit. If NSEvent cannot create that event, return false instead of replaying the original Down arrow and accidentally asking the IME to navigate twice. Constraint: Cursor Bugbot flagged the original-event fallback on the current PR head Rejected: Keep the original Down fallback | it silently changes expansion failure into a second navigation event Confidence: high Scope-risk: narrow Directive: Candidate expansion must fail closed; do not substitute terminal or arrow events for the synthetic Space request Tested: git diff --check Not-tested: Local unit/UI tests skipped per user instruction; CI will validate after push
|
Current head
The high-priority entries still reported by the feedback helper are old CodeRabbit summary comments without current inline unresolved threads; the corresponding inline findings are resolved/outdated on the latest head. |
|
Closing — issue is already fixed. See #3691. |

Summary
Fixes #3691 for the narrow Apple Zhuyin path:
com.apple.inputmethod.TCIM.Zhuyin, active marked text, plain Down/Up only.The previous pushed attempt stopped the shell leak but did not show the candidate menu. That path asked private AppKit candidate-panel selectors to present candidates; Austin dogfooded it and Down still did nothing. This revision removes that non-working presenter path. The current flow first gives the original Down/Up to AppKit text input; if exact plain Apple Zhuyin Down reports
moveDown:, produces no committed text, and leaves marked text unchanged, it replays a plain Space key event into the activeNSTextInputContextto trigger Zhuyin's candidate-list expansion. Up remains consume-only for navigating an already-open list.This replaces PR #3694, which was reverted by #3849 after broad IME/window-level routing broke arrows globally for IME users. This PR does not recreate the reverted framework: no window reroute, no keyUp suppression set, no broad IME predicate, and no PageUp/PageDown/Home/End/Space suppression case list. McBopomofo/OpenVanilla remain out of scope.
#3691 Guardrails Confirmation
Citing the mandatory "Guardrails for the next fix attempt" section from #3691:
TCIM.Zhuyin: satisfied. The source predicate is an exact case-insensitive compare againstcom.apple.inputmethod.TCIM.Zhuyin.hasMarkedText() == true; Zhuyin outside composition falls through to Ghostty.kVK_DownArrowandkVK_UpArrow. Candidate expansion is narrower: Down only, after unchangedmoveDown:handling.AppDelegate.performKeyEquivalentchanges; the hook lives in the focusedGhosttyNSView.keyDownpath.keyUpis untouched and noSet<UInt16>lifecycle was added.216da0338) is test-only. Commit 2 (2c9695e3) lands the fix.Implementation note: the issue guardrails are satisfied. The later implementation sketch said not to synthesize Space; dogfood proved the private candidate-panel path did not show Apple Zhuyin's menu. The Space replay here is therefore intentionally narrower than #3694: it is only an input-context event after exact Down + Apple Zhuyin + active marked text + unchanged
moveDown:no-op, and it does not add any of #3694's broad state/routing machinery.Manual Repro Capture
Pre-fix tagged current-main build:
./scripts/reload.sh --tag repro-3691-main --launchfrom post-revert base behavior.cmux DEV repro-3691-main.cat -v, selectedcom.apple.inputmethod.TCIM.Zhuyin.cat -vprinted^[[B.cat -vprinted^[[A.Latest branch dogfood before
2c9695e3: Down no longer visibly leaked to the shell, but the expected Zhuyin candidate menu still did not appear. This revision replaces that failed presenter path with input-context candidate expansion.Regression-Guard Test Diff
Each forwarding guard synthesizes Left/Right/Up/Down/PageUp/PageDown/Home/End/Space with empty modifiers and asserts the key reaches Ghostty.
Zhuyin-Specific Test Diff
The positive helper asserts the original arrow goes through AppKit text input, composition remains active, no bare Up/Down reaches Ghostty, and only plain Down asks the input context to expand candidates via a plain Space event. Modified arrows and synchronous committed candidate text do not use the expansion fallback.
Verification
Commit 1 red proof:
./scripts/test-unit.sh test -only-testing:cmuxTests/CJKIMEMarkedSelectionTestsPost-fix focused proof:
./scripts/test-unit.sh test -only-testing:cmuxTests/CJKIMEMarkedSelectionTests2c9695e3: 20 tests, 0 failures.Passing
CJKIMEMarkedSelectionTestsnames:testAttributedSubstringReturnsMarkedTextSegmenttestCangjieArrowKeysAlwaysReachShelltestDoesNotSuppressCommittedIMEInsertTexttestDoesNotSuppressNormalTerminalKeyWhenIMEDidNothingtestJapaneseInputSourceArrowKeysAlwaysReachShelltestKeyDownDoesNotForwardWhenZhuyinStartsMarkedTexttestKoreanInputSourceArrowKeysAlwaysReachShelltestNonIMELayoutArrowKeysAlwaysReachShelltestSelectedRangeReturnsEmptyRangeAfterCompositionEndstestSelectedRangeReturnsEmptyRangeWithoutSelectionOrMarkedTexttestSelectedRangeTracksMarkedTextSelectiontestSimplifiedChinesePinyinArrowKeysAlwaysReachShelltestSuppressesTerminalForwardingWhenZhuyinMarkedTextChangestestSuppressesTerminalForwardingWhenZhuyinStartsMarkedTexttestTraditionalChineseZhuyinMarkedTextSelectionAndSubstringtestZhuyinArrowKeysOutsideCompositionReachShelltestZhuyinCandidateArrowCommitFromInterpretKeyEventsReachesShelltestZhuyinDownArrowDuringCompositionOpensCandidatestestZhuyinModifiedCandidateArrowsDuringCompositionReachShelltestZhuyinUpArrowDuringCompositionMovesCandidateSelectionNeighboring file run:
./scripts/test-unit.sh testwith the XCTestCase selectors fromcmuxTests/CJKIMEInputTests.swiftwas run earlier on this branch.Static checks:
git diff --checkpassed.isInputMethodSource,shouldRouteTextInputKeyEquivalentToKeyDown,shouldKeepIMECompositionCommandInsideTextInput,shouldOpenZhuyinCandidatesWithSyntheticSpace,imeSuppressedKeyUpKeyCodes,zhuyinCandidateOpenRequested,debugTextInputEventHandler,shouldAllowDeferredNumpadIMEFallback,shouldRememberZhuyinCandidateInteraction, orisTraditionalZhuyinInputSource.Retest Plan for yoonkeee@gmail.com
After CI/nightly is available, send the build to
yoonkeee@gmail.com(ko-KR, M4 Pro, macOS 26.4) and ask them to verify Korean 2-Set Hangul with Left/Right/Up/Down/PageUp/PageDown/Home/End/Space in an active terminal session. Expected result: all keys still reach the shell when not composing.Additional manual dogfood for this branch:
Note
Medium Risk
Touches the core
keyDown/IME event path and synthesizes an extra input-context event, so regressions could affect key handling during composition, though the behavior is tightly gated to Apple Zhuyin + marked text + plain Up/Down and is covered by new tests.Overview
Fixes the narrow Apple Traditional Zhuyin IME case where plain Up/Down arrows during active marked text should control the candidate UI instead of leaking to the terminal.
GhosttyTerminalView.keyDownnow detects this gated scenario, routes the original arrow event throughinterpretKeyEvents, observes the resultingdoCommandselector, and if Down results in no composition/commit change, sends a synthetic Space event to theNSTextInputContextto request candidate-list expansion (with a DEBUG hook for tests).Adds helper predicates in
GhosttyNSView+IMECompositionand expandsCJKIMEMarkedSelectionTestswith regression guards ensuring navigation keys still reach the shell for other input sources, plus Zhuyin-specific tests for arrow suppression/expansion behavior, modified-arrow passthrough, and commit-on-interpret handling.Reviewed by Cursor Bugbot for commit 22da525. Bugbot is set up for automated code reviews on this repo. Configure here.