Skip to content

fix(terminal): commit Sogou Shift latin in the Electron TUI - #22026

Open
JasonJarvan wants to merge 9 commits into
stablyai:mainfrom
JasonJarvan:fix/windows-sogou-ime-shift
Open

JasonJarvan wants to merge 9 commits into
stablyai:mainfrom
JasonJarvan:fix/windows-sogou-ime-shift

Conversation

@JasonJarvan

@JasonJarvan JasonJarvan commented Sep 21, 2026 •

Copy link
Copy Markdown

ELI5

On Windows, you type pinyin in an Electron terminal, then tap Shift to send those letters as English. In Sogou that text disappeared. It now commits as Latin, the same way Web UI and ordinary inputs already did.

What Changed

Before: Sogou Shift-to-English (key=Process or key=Shift, code=ShiftLeft) was swallowed before xterm's CompositionHelper. xterm then treated the empty compositionend as a cancel, so the later insertText never reached the PTY.

After: physical Shift reaches CompositionHelper while composing. Idle Shift stays suppressed so kitty does not encode a bare modifier. While that IME keydown still owes a commit, an empty compositionend waits for the delayed latin input instead of flushing an empty textarea.

Why

#11946 stopped Microsoft Pinyin Shift from turning into Enter. Sogou still lost the commit entirely: Windows 229 suppression plus swallowing composing key=Shift as a standalone modifier, then an empty-end flush while the key is held.

Letting only composing Shift through is smaller than unblocking every Shift keydown (which would let kitty report idle modifiers) or turning off all Windows 229 handling. Waiting only when a held IME keydown still owes a commit keeps ordinary cancel (Backspace over the whole preedit) intact.

Linked Issue

Fixes #22021

Related: #11946 (Microsoft Pinyin Shift became a newline; different IME, different failure).

Visual Proof

N/A — this is terminal input routing, not a renderer layout change. The Electron TUI vs Web UI difference is covered by synthetic composition replay: Process/229 and ordinary Shift while composing, empty compositionend, then delayed insertText while the key is held.

Testing

  • I manually tested these changes locally
  • Automated tests added/updated, or explained why not below

Verified on Windows with native Sogou in the Electron TUI: pinyin then a single Shift commits latin and switches IME. Search/plain inputs were already fine.

pnpm vitest run --config config/vitest.config.ts
  src/renderer/src/components/terminal-pane/terminal-ime-xterm-composition-cancel.test.ts
  src/renderer/src/components/terminal-pane/xterm-bypass-policy-interrupt.test.ts
  src/renderer/src/components/terminal-pane/xterm-bypass-policy-non-mac.test.ts
Test Files  3 passed (3)
Tests       66 passed (66)

CI should cover Linux/macOS.

AI Disclosure

Cursor Grok 4.6 was used to diagnose the Electron TUI event sequence, implement the bypass-policy and CompositionHelper changes, regenerate the xterm patches, and write tests.

Review

Please focus on:

  • whether Windows letter-229 suppression stays for the preedit-diff race;
  • whether idle Shift remains blocked from kitty CSI-u;
  • whether the held-Process wait can swallow a real cancel;
  • whether the xterm source patch plus regenerated bundle/lockfile stay in lockstep.

Agent skill upstream boundary

  • Not applicable, or this change follows docs/reference/agent-skill-sharing-upstream-boundary.md and copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Checklist

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

@coderabbitai

coderabbitai Bot commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: 784d829a-ffb1-42a2-bba5-3772f8c32bac

📥 Commits

Reviewing files that changed from the base of the PR and between ce3b3c5 and 6630665.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (5)
  • config/patches/@xterm__xterm@6.1.0-beta.303.patch
  • config/patches/xterm-src/@xterm__xterm@6.1.0-beta.303.src.patch
  • docs/reference/ime-regression-checklist.md
  • src/renderer/src/components/terminal-pane/xterm-bypass-policy-interrupt.test.ts
  • src/renderer/src/components/terminal-pane/xterm-bypass-policy.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 7 remain after this review.


📝 Walkthrough

Walkthrough

The patch updates xterm IME handling with deferred composition transactions, delayed insertText reconciliation, lifecycle events, cancellation, disposal, and themed preedit rendering. It wires these behaviors into CoreBrowserTerminal and updates SortedList deletion tracking. Patch regeneration now uses normalized relative paths and isolates Git repository settings. Windows IME policy and tests now allow Sogou physical Shift events and verify delayed Latin commits.

Priority: ➖ Normal

Severity of issue fixed: Medium

Merge Risk: 🟡 Moderate · up to 66306

A Windows Sogou Shift keyup can still produce unintended terminal input, and composition lifecycle consumers miss the pending-session closure event. Resolve these IME behaviors before merging.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 24 functions across 10 files. (2 skipped:… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely identifies the main change: fixing Sogou Shift-to-English Latin input in the Electron terminal UI.
Description check ✅ Passed The description covers the required core sections, including user impact, implementation details, rationale, linked issue, visual-proof rationale, testing, AI disclosure, review focus, and checklist i…
Linked Issues check ✅ Passed Issue #22021 requires Sogou Windows Shift-to-English input to preserve the preedit and delayed Latin insertText without extra terminal output. The PR passes Process/229 physical Shift events to `Com…
Out of Scope Changes check ✅ Passed The changes remain within issue #22021. The xterm IME changes, terminal bypass policy, composition tracker, and regression tests implement or verify the required input sequence. The Git environment an…
Full details: Docstring Coverage

Explanation

Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 24 functions across 10 files. (2 skipped: 2 unsupported.)


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.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to GitHub limitations.

⚠️ Outside diff range comments (3)

🟠 Major · Clear Git repository-location variables before Git commands. · regenerate-xterm-patches.mjs:169-176

config/scripts/regenerate-xterm-patches.mjs:169-176
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Clear Git repository-location variables before Git commands.

run inherits GIT_DIR, GIT_WORK_TREE, GIT_INDEX_FILE, and related variables. The new git rm --cached . at Line 239 can then target the caller repository instead of root. The following hard reset can fail or modify the unintended checkout. Build a sanitized environment for every git invocation.

Based on learnings: strip Git repository-location environment variables from subprocesses that target a specific cwd.

Source: Learnings

🟠 Major · Clear the IME commit debt when cancellation… · `@xterm__xterm`@6.1.0-beta.303.src.patch:800-802

config/patches/xterm-src/@xterm__xterm@6.1.0-beta.303.src.patch:800-802
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Clear the IME commit debt when cancellation settles the transaction.

The held-Shift path leaves _imeKeydownAwaitingCommit set after _cancelComposition(). If Escape cancels the pending composition and the delayed insertText then arrives, input() calls _claimImeKeydownCommit() and forwards the canceled text to the PTY.

Clear the flag during cancellation.

Proposed fix
     this._pendingComposition = undefined;
     this._isAwaitingCompositionEnd = false;
     this._isComposing = false;
+    this._imeKeydownAwaitingCommit = false;
🟡 Minor · Handle the delayed Sogou commit in screen… · `@xterm__xterm`@6.1.0-beta.303.src.patch:1051-1058

config/patches/xterm-src/@xterm__xterm@6.1.0-beta.303.src.patch:1051-1058
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Handle the delayed Sogou commit in screen reader mode. Screen reader mode bypasses CompositionHelper.input, so the Process/229 + Shift path can leave _pendingComposition waiting after the empty compositionend. The delayed insertText can then be dropped by xterm’s held-key guard, and later input can use stale composition state. Route insertText to the helper when a pending composition finalization exists, including screen reader mode. Clear _imeKeydownAwaitingCommit when that pending branch consumes the text.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: cc27b466-4ce6-4013-ba90-9bf5d0238088

📥 Commits

Reviewing files that changed from the base of the PR and between 4aaa6c7 and 74b1a83.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (9)
  • config/patches/@xterm__xterm@6.1.0-beta.303.patch
  • config/patches/xterm-src/@xterm__xterm@6.1.0-beta.303.src.patch
  • config/scripts/regenerate-xterm-patches.mjs
  • config/scripts/regenerate-xterm-patches.test.mjs
  • config/scripts/xterm-patch-text.mjs
  • docs/reference/ime-regression-checklist.md
  • src/renderer/src/components/terminal-pane/terminal-ime-xterm-composition-cancel.test.ts
  • src/renderer/src/components/terminal-pane/xterm-bypass-policy-non-mac.test.ts
  • src/renderer/src/components/terminal-pane/xterm-bypass-policy.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review.

Comment thread config/scripts/xterm-patch-text.mjs Outdated
@JasonJarvan

Copy link
Copy Markdown
Author

Addressed the actionable review notes on e217314:

  • GIT_DIR / caller checkout: run('git', …) and pnpmDiffEnvironment now strip GIT_DIR, GIT_WORK_TREE, GIT_INDEX_FILE, and the other repository-location variables so git rm --cached / git reset stay inside the xterm work dir.
  • Cancel then delayed insertText: _cancelComposition() clears _imeKeydownAwaitingCommit. Added a replay that Shift-commits, Escapes, then delivers the delayed insertText and expects no PTY output.
  • commonParent root: POSIX /left vs /right now returns / instead of ''; Windows drive-only share returns C:\.

Not changing in this PR:

  • SortedList / WidthCache / preedit rendering: those hunks were already in main's xterm source patch (#19367 and earlier IME work). This PR regenerates the whole patch file, so the bundle text still contains them; they are not new product changes for #22021.
  • Docstring coverage: Orca's rule is concise comments only. The new helpers already have one-line whys; adding JSDoc to hit an 80% bot threshold would be noise.
  • Screen reader insertText: _inputEvent already skips CompositionHelper.input when screenReaderMode is on (emoji doubling). That bypass predates this bug. #22021 is Electron TUI Sogou Shift, not NVDA/JAWS. Happy to take it as a follow-up if you want that path covered too.

@pullfrog pullfrog Bot left a comment

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.

Important

Two concrete risks worth resolving before merge. Both are conditional, but each has real fallout: a kitty release CSI-u leaked to the PTY, and a possible duplicated commit. Details inline.

Reviewed changes

  • IME bypass policy — isImeModeToggleShiftKey lets any code starting with Shift skip shouldSuppressTerminalImeKeyboardEvent, so Sogou's Process/229 Shift keydown reaches xterm's CompositionHelper. Platform-agnostic, not gated on event type, key, keyCode, or isComposing.
  • xterm source patch — sets _imeKeydownAwaitingCommit for a Shift keydown while composing, adds _isImeModeToggleShift and _shouldWaitForHeldImeCommit, makes the composition finalizer wait (leaving _pendingComposition open) while that held keydown still owes a commit, and guards _deferPreeditResync with !this._imeKeydownAwaitingCommit.
  • Patch regeneration script — Windows-only fixes: * -text in the upstream checkout's .git/info/attributes before a re-reset (CRLF smudging), toPosix for the version-stamp comparison, git diff --no-index run from a computed commonParent cwd via posixRelative, and -c core.autocrlf=false in the test's patch replay.
  • Tests — two integration tests (Shift/Process/229 keydown, empty compositionend, delayed insertText → emits s once) plus a bypass-policy unit test asserting Shift keydown/keyup are not suppressed.
  • Docs — new #22021 row in the IME regression checklist.

ℹ️ Nitpicks

  • The keyCode === 16 || 17 || 18 _isImeModeToggleShift block (config/patches/xterm-src/@xterm__xterm@6.1.0-beta.303.src.patch:444-446) is unreachable through Orca's wiring: shouldSuppressTerminalModifierKeyboardEvent returns false for key:'Shift' at terminal-pane-pane-input.ts:161 before CompositionHelper.keydown runs. The new integration tests only exercise it by dispatching directly on the textarea, so that coverage overstates real reachability.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

Comment thread src/renderer/src/components/terminal-pane/xterm-bypass-policy.ts Outdated
+ }
+ if (this._pendingComposition === pending) {
+ if (this._shouldWaitForHeldImeCommit(pending)) {
+ return;

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.

_shouldWaitForHeldImeCommit's early return leaves _imeKeydownAwaitingCommit set, and nothing on the flush path clears it. When the textarea is non-empty at compositionend, _updatePostCompositionInputExpectation sets expectsPostCompositionInput false, the finalizer flushes the preedit, and a later insertText is then claimed by _claimImeKeydownCommit and sent again.

Technical details
# Sticky `_imeKeydownAwaitingCommit` can duplicate a commit or swallow a cancel

## Affected sites
- `config/patches/xterm-src/@xterm__xterm@6.1.0-beta.303.src.patch:635-636` — early return leaves `_pendingComposition` open with no timer.
- `...:920-921` — `_deferPreeditResync` refuses to cancel while the flag is set, so a key that empties the preedit later in the same composition no longer clears it.
- `_sendPendingComposition` / `_settlePendingComposition` (pre-existing) never reset the flag; `_claimImeKeydownCommit` (`:520-531`) then claims a later `insertText`.

## Sequence (double commit)
1. `compositionstart`; `compositionupdate('s')`; textarea `'s'`.
2. Shift `Process`/`ShiftLeft`/229 keydown → `_imeKeydownAwaitingCommit = true`.
3. Empty `compositionend` while the textarea still holds `'s'` → `expectsPostCompositionInput = false`, finalizer flushes `'s'`, `_pendingComposition` cleared, flag still true.
4. IME's delayed `insertText 's'` → `input()` finds no pending → `_claimImeKeydownCommit` sees the flag and sends `'s'` a second time.

## Required outcome
- Once a pending composition has been sent or settled, the held-key debt is discharged and a later `insertText` must not be re-sent.

## Suggested approach
- Clear `_imeKeydownAwaitingCommit` in `_sendPendingComposition` / `_settlePendingComposition` (one source of truth), or set it only on the empty-`compositionend` path that actually waits.

## Open questions for the human
- Does the captured Sogou trace ever leave the textarea non-empty at `compositionend`? If not, this is latent rather than live.

@pullfrog

pullfrog Bot commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

Run failed. View the logs →

Pullfrog  | Rerun failed job ➔ | View workflow run | via Pullfrog | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@pullfrog pullfrog Bot left a comment

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.

Important

The release-event half of the prior kitty finding is only partly closed. The new modifier guard keys off event.key, so a Sogou keyup with key:'Process' and a Shift* code still reaches xterm's kitty encoder — the same leak the guard was added to stop. Details inline.

Reviewed changes

  • Shift modifier policy — extracted shouldSuppressTerminalModifierKeyboardEvent into an options-taking function that lets a composing Shift keydown reach CompositionHelper while keeping idle Shift and non-idle Shift releases suppressed.
  • IME-commit-debt lifecycle — _imeKeydownAwaitingCommit is now cleared in _cancelComposition and whenever the deferred textarea diff observes a change, with an integration test proving Escape cancels a held-Process composition without emitting latin.
  • Patch-regeneration hardening — withoutGitRepositoryLocation strips GIT_DIR/GIT_WORK_TREE/GIT_INDEX_FILE/etc. from every git spawn, commonParent takes a path implementation and returns the Windows drive root, and pnpmDiffEnvironment reuses the sanitizer; both are unit-tested.
  • Bundle/lockfile/docs — regenerated the bundle patch and pnpm-lock.yaml patch hash, and refreshed the #22021 checklist row to cover the key=Shift shape.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

Comment thread src/renderer/src/components/terminal-pane/xterm-bypass-policy.ts

@pullfrog pullfrog Bot left a comment

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.

ℹ️ No critical issues — a couple of minor observations inline.

Reviewed changes

  • Post-compositionend Shift window — added isImeShiftCommitGuardActive() to the IME composition tracker, armed for 250 ms after every compositionend (and covering a live composition), with unit tests for arming, survival across the delayed insertText, and expiry.
  • Modifier policy — threaded imeShiftCommitGuardActive into shouldSuppressTerminalModifierKeyboardEvent, so a key:'Shift' keydown inside that window reaches CompositionHelper even when isComposing is already false.
  • xterm patch — any Shift-coded keydown (_isImeModeToggleShift) now arms _imeKeydownAwaitingCommit, covering the post-end Shift shape.
  • Tests/docs — added an integration test for ordinary Shift arriving after an empty compositionend and refreshed the #22021 checklist row and regenerated bundle patch/pnpm-lock.yaml hash.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

if (
shouldSuppressTerminalModifierKeyboardEvent(ev, {
compositionActive: windowsComposing.compositionActive,
imeShiftCommitGuardActive: true

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.

The pane-handler stub hardcodes imeShiftCommitGuardActive: true, so these integration tests pass even if terminal-pane-pane-input.ts stopped passing the tracker's isImeShiftCommitGuardActive() — the one piece of wiring this commit adds. The policy unit tests cover the guard logic, but nothing exercises the tracker-to-pane call; deriving the flag from a parameter (or driving it through the real tracker in one test) would close that gap.

Comment thread src/renderer/src/components/terminal-pane/terminal-ime-composition-tracker.ts Outdated

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to GitHub limitations.

⚠️ Outside diff range comments (1)

🟡 Minor · End the prior pending session when a new… · `@xterm__xterm`@6.1.0-beta.303.src.patch:595-596

config/patches/xterm-src/@xterm__xterm@6.1.0-beta.303.src.patch:595-596
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

End the prior pending session when a new composition starts.

compositionstart() only records nextCompositionStart for an existing pending transaction. It does not call _endPendingCompositionSession(). The pending-reconciliation event is intended for this force-end path, not for initial pending creation. Calling it at lines 595-596 would end the session before reconciliation and cause current consumers to discard the pending event.

Suggested fix
     if (this._pendingComposition) {
       this._pendingComposition.nextCompositionStart = this._compositionPosition.start;
+      this._endPendingCompositionSession(this._pendingComposition);
     }

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: 7d21efaf-dc42-4e61-8704-6699d94c55d3

📥 Commits

Reviewing files that changed from the base of the PR and between 3779e76 and ce3b3c5.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (9)
  • config/patches/@xterm__xterm@6.1.0-beta.303.patch
  • config/patches/xterm-src/@xterm__xterm@6.1.0-beta.303.src.patch
  • docs/reference/ime-regression-checklist.md
  • src/renderer/src/components/terminal-pane/terminal-ime-composition-tracker.test.ts
  • src/renderer/src/components/terminal-pane/terminal-ime-composition-tracker.ts
  • src/renderer/src/components/terminal-pane/terminal-ime-xterm-composition-cancel.test.ts
  • src/renderer/src/components/terminal-pane/terminal-pane-pane-input.ts
  • src/renderer/src/components/terminal-pane/xterm-bypass-policy-interrupt.test.ts
  • src/renderer/src/components/terminal-pane/xterm-bypass-policy.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/reference/ime-regression-checklist.md

Included review availability: Your plan provides up to 10 included reviews per hour; 8 remain after this review.

+ // Nothing is forwarded for a keydown the IME consumed, so it is left owing its commit; any
+ // other keydown either forwards its own text or produces none, and clears the debt.
+ this._imeKeydownAwaitingCommit = ev.keyCode === 229;
+ this._imeKeydownAwaitingCommit = ev.keyCode === 229 || this._isImeModeToggleShift(ev);

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

sed -n '420,535p' config/patches/xterm-src/@xterm__xterm@6.1.0-beta.303.src.patch
sed -n '950,990p' config/patches/xterm-src/@xterm__xterm@6.1.0-beta.303.src.patch
sed -n '1010,1070p' config/patches/xterm-src/@xterm__xterm@6.1.0-beta.303.src.patch

Repository: stablyai/orca

Length of output: 8574


🏁 Script executed:

#!/bin/bash
set -eu
file='config/patches/xterm-src/@xterm__xterm@6.1.0-beta.303.src.patch'
printf '%s\n' '--- CompositionHelper keydown/input/reconciliation ---'
sed -n '400,570p' "$file"
sed -n '760,880p' "$file"
printf '%s\n' '--- CoreBrowserTerminal event wiring ---'
rg -n -C 12 'compositionHelper\.(keydown|input|keypress|textarea|blur)|_inputEvent|_keyDown|insertText' 'config/patches/@xterm__xterm@6.1.0-beta.303.patch' "$file"

Repository: stablyai/orca

Length of output: 45546


🏁 Script executed:

#!/bin/bash
set -eu
file='config/patches/xterm-src/@xterm__xterm@6.1.0-beta.303.src.patch'
core='config/patches/@xterm__xterm@6.1.0-beta.303.patch'
printf '%s\n' '--- CompositionHelper state and reconciliation ---'
rg -n --no-heading --fixed-strings '_imeKeydownAwaitingCommit' "$file"
sed -n '780,875p' "$file"
printf '%s\n' '--- CoreBrowserTerminal keydown and input wiring ---'
sed -n '95,140p' "$core"
sed -n '1010,1065p' "$core"

Repository: stablyai/orca

Length of output: 8234


Prevent idle Shift from creating IME commit debt.

CoreBrowserTerminal._keyDown calls CompositionHelper.keydown for every textarea keydown. An idle IME-toggle Shift can set _imeKeydownAwaitingCommit. Textarea reconciliation clears this flag only when the textarea value changes, so a later unrelated insertText can be claimed as the pending IME commit.

Gate the Shift case on active composition or pending finalization.

Suggested fix
-    this._imeKeydownAwaitingCommit = ev.keyCode === 229 || this._isImeModeToggleShift(ev);
+    this._imeKeydownAwaitingCommit =
+      ev.keyCode === 229 ||
+      ((this._isComposing || this.hasPendingCompositionFinalization) &&
+        this._isImeModeToggleShift(ev));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
+ this._imeKeydownAwaitingCommit = ev.keyCode === 229 || this._isImeModeToggleShift(ev);
this._imeKeydownAwaitingCommit =
ev.keyCode === 229 ||
((this._isComposing || this.hasPendingCompositionFinalization) &&
this._isImeModeToggleShift(ev));

@JasonJarvan

Copy link
Copy Markdown
Author

Native Sogou on Windows confirmed this follow-up: IME/composition flags are not trustworthy after empty compositionend, so gating Shift keydown on isComposing or a post-end window still drops the held-key insertText. The custom handler now always delivers Shift keydown; CompositionHelper consumes it so kitty cannot encode a bare modifier, and Shift keyup stays suppressed.

@pullfrog pullfrog Bot left a comment

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.

Important

The new unconditional Shift pass-through arms the IME-commit claim on an idle Shift keydown, and a following bypassed Shift+<non-ASCII> keydown leaves it armed, so the character is emitted twice on Windows/Linux non-Latin layouts. Details inline.

Reviewed changes

  • Made shouldSuppressTerminalModifierKeyboardEvent pass every key:'Shift' keydown unconditionally, so the kitty guard now lives in xterm: CompositionHelper.keydown consumes any Shift-coded keydown before _keyboardService.evaluateKeyDown runs.
  • Armed _imeKeydownAwaitingCommit for any Shift-coded keydown, not just composing ones, and kept the finalizer wait for the held Sogou commit.
  • Dropped the composition/guard gating from the modifier policy (its options are now ignored) and refreshed the #22021 checklist row.
  • Regenerated the source patch, the compiled bundle patch, and the pnpm-lock.yaml patch hash in lockstep (bundle now carries _isImeModeToggleShift).

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

+ // Nothing is forwarded for a keydown the IME consumed, so it is left owing its commit; any
+ // other keydown either forwards its own text or produces none, and clears the debt.
+ this._imeKeydownAwaitingCommit = ev.keyCode === 229;
+ this._imeKeydownAwaitingCommit = ev.keyCode === 229 || this._isImeModeToggleShift(ev);

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.

Arming the claim for an idle (non-composing) Shift keydown leaves it armed when the next keydown is bypassed by shouldBypassXtermKeyboardEvent (Shift + a single non-ASCII printable). That keydown never reaches CompositionHelper.keydown to clear the flag, so the input event's _claimImeKeydownCommit re-sends the glyph _keyPress already emitted — every Shift+<non-ASCII> keystroke (uppercase Cyrillic/Greek, etc.) types twice on Windows/Linux.

Technical details
# Idle-Shift claim survives a bypassed keydown and double-emits

## Affected sites
- `config/patches/xterm-src/@xterm__xterm@6.1.0-beta.303.src.patch:456` — `_imeKeydownAwaitingCommit = ev.keyCode === 229 || this._isImeModeToggleShift(ev)` arms the claim for an idle, non-composing Shift keydown.
- `src/renderer/src/components/terminal-pane/xterm-bypass-policy.ts:315` — every `key:'Shift'` keydown now passes the modifier policy.
- `src/renderer/src/components/terminal-pane/xterm-bypass-policy.ts:346-357` — a Shift + single non-ASCII printable keydown is bypassed before `CompositionHelper.keydown`, so nothing clears the claim.
- `node_modules/@xterm/xterm/src/browser/CoreBrowserTerminal.ts:869-871` — the bypass makes `_keyDown` return at the custom-handler check, never calling `CompositionHelper.keydown`.
- `CoreBrowserTerminal.ts:1031-1035` (`_keyPress` emits and sets `_keyPressHandled`) and the patched `_inputEvent` first branch (calls `_compositionHelper.input(ev.data)` before the `_keyPressHandled` dedup).

## Sequence
1. Idle `Shift` keydown → not suppressed by the IME policy (`isImeModeToggleShiftKey`) or the modifier policy → `CompositionHelper.keydown` sets `_imeKeydownAwaitingCommit = true`.
2. `Shift+Ф` keydown → `shouldBypassXtermKeyboardEvent` returns true → `_keyDown` returns before `CompositionHelper.keydown`; claim stays true.
3. `keypress` → `_keyPress` emits `Ф`, sets `_keyPressHandled = true`.
4. `input` `insertText:'Ф'` → `_inputEvent` first branch → `_claimImeKeydownCommit` sees the stale claim and emits `Ф` again.

## Required outcome
- A Shift keydown that does not begin an IME commit must not leave a claim a later `input` event consumes, so `Shift+<non-ASCII>` emits once. macOS is already shielded by the native-text forwarder; Windows/Linux are exposed.

## Suggested approach
- Arm `_imeKeydownAwaitingCommit` only for a Shift keydown while composing/finalizing (as the previous revision did), or clear the claim whenever a keydown is bypassed. Add an integration test that types `Shift` + a non-ASCII letter and asserts one emission (`terminal-ime-xterm-consumed-key-commit.test.ts:233` only covers the non-bypassed next keydown).

Comment thread src/renderer/src/components/terminal-pane/xterm-bypass-policy.ts Outdated

@nwparker nwparker left a comment

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.

Thanks for staying on this — the diagnosis in #22589 and your native-Sogou follow-up are exactly the evidence this area needs, and I want this to land. Two things block it for me, and one of them is the note Pullfrog left last that is still open.

1. The imeShiftCommitGuard is dead code

shouldSuppressTerminalModifierKeyboardEvent(event, _options) never reads _options. Since the guard became unconditional in 6630665, nothing consumes it:

  • XtermModifierKeyboardOptions is declared and never read;
  • terminal-pane-pane-input.ts computes imeShiftCommitGuardActive: imeCompositionTracker.isImeShiftCommitGuardActive() and throws it away;
  • isImeShiftCommitGuardActive, imeShiftCommitGuardUntil, its three reset sites and its tracker tests have no consumer at all.

Either delete the whole guard, or restore the gating it was written for. As it stands the tests that cover it pass whatever the policy does, which is worse than no coverage. This is the same thing Pullfrog flagged on xterm-bypass-policy.ts:304.

2. The Shift carve-out is not platform-gated, and it is wider than Shift-to-English

The comment says "Windows keeps letter 229 suppression … but ShiftLeft/ShiftRight must pass", but isImeModeToggleShiftKey early-returns before isMac / isLinux are consulted, and before the isComposing and keyCode === 229 branches. I ran the matrix on your branch:

event main this branch
Shift keydown, isComposing: true — mac / linux / win suppressed passes
Process/229 ShiftLeft keydown, composing — mac / linux / win suppressed passes
Process/229 ShiftRight keyup, composing — mac / linux / win suppressed passes

So this changes macOS and Linux, not only Windows, and it drops the 229 keyup suppression as well as the keydown. The keyup half is what Pullfrog raised against xterm-bypass-policy-non-mac.test.ts:256; the new test added at :207 asserts the keyup now passes rather than resolving whether it should. With REPORT_EVENT_TYPES negotiated, that release reaches KittyKeyboard and encodes a modifier release CSI-u for a press the TUI never saw.

If the real finding is "IME/composition flags are not trustworthy after an empty compositionend" — and I believe you, that matches what #22589 measured independently — then the narrow version is: gate on isWindows, keep the carve-out to keydown, and leave the keyup suppressed. If the keyup genuinely must pass too, that needs its own stated reason and a test for what the TUI receives, not just that the policy returns false.

Not blocking, but worth pinning

shouldSuppressTerminalModifierKeyboardEvent now returns false for an idle Shift keydown on every platform. You say CompositionHelper consumes it before the kitty encoder runs, so no bare modifier press is emitted — that is the right mechanism, but it now lives entirely in the vendored xterm patch. A policy-level test that pins "idle Shift keydown under REPORT_ALL_KEYS_AS_ESCAPE_CODES produces no CSI-u" would stop a later patch regeneration from quietly undoing it.

Context

I have a related fix up at #22607 for the Linux half of the same family — Sogou on fcitx opening no composition session at all, so candidate Space and digits leak to the PTY. It touches xterm-bypass-policy.ts and terminal-pane-pane-input.ts too, but only inside the isLinux branch and the Linux candidate state, so I do not expect a conflict beyond imports. Happy to rebase behind you if yours lands first — you have the native Windows Sogou hardware and that is the scarcer resource here.

@JasonJarvan

Copy link
Copy Markdown
Author

Both review blockers are addressed on 30c05ec. I did not restore the composition gate.

1. Dead imeShiftCommitGuard

Deleted it, including XtermModifierKeyboardOptions and the tracker state and tests. Restoring the gate brings back a failure already confirmed with native Sogou: after an empty compositionend, isComposing and the tracker are often already false, and gating Shift keydown on them drops the held-key insertText. Shift keydown stays unconditional. CompositionHelper.keydown still consumes that keydown, so kitty does not encode a bare modifier press.

2. Windows keydown only

isImeModeToggleShiftKey now applies only when the event is a Windows keydown (!isMac && !isLinux). A Shift-coded Process/229 keyup is suppressed again, on Windows as well as macOS and Linux, including the composing case in your matrix. macOS and Linux Shift keydown while composing is suppressed again, matching main.

The modifier policy still delivers key === 'Shift' keydown on every platform and still swallows that keyup. The kitty leak was the key === 'Process' keyup, which never matched the modifier-key set.

The kitty check

Not only a policy return value. With kitty REPORT_ALL_KEYS_AS_ESCAPE_CODES negotiated, an idle Shift keydown emits no bytes. With REPORT_EVENT_TYPES negotiated, a Shift-coded Process/229 keyup emits no release.

Still out of this PR

Matching your note on #22589:

One Pullfrog note is not in this commit: an idle Shift can leave _imeKeydownAwaitingCommit set, and a following bypassed Shift+<non-ASCII> keydown can emit the glyph twice. I did not clear that claim here. The claim still has to arm when Shift arrives after compositionend, which is the sequence native Sogou required, and changing it means regenerating the xterm patch. Say if you want that in this PR rather than a follow-up.

Tests: the four terminal IME and bypass files, 85 passed.

@pullfrog pullfrog Bot left a comment

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.

ℹ️ No new issues in this delta — the commits close the two blocking items from the human review and the two related prior threads.

Reviewed changes

  • Windows-only Shift carve-out — shouldSuppressTerminalImeKeyboardEvent now lets a Shift*-coded keydown past IME suppression only when isWindows and only for keydown, so macOS and Linux return to the previous policy and the Shift-coded keyup falls through to the existing 229 suppression.
  • Kitty release leak closed — a Sogou Process/229 ShiftRight keyup is suppressed again, with a pane-level test asserting the PTY receives no release CSI-u.
  • Dead IME-Shift guard removed — imeShiftCommitGuard state, isImeShiftCommitGuardActive, XtermModifierKeyboardOptions, and the ignored _options parameter are deleted, reverting terminal-pane-pane-input.ts and terminal-ime-composition-tracker.ts to their base shape.
  • Idle-Shift invariant pinned — an integration test asserts an idle Shift keydown emits no kitty CSI-u under REPORT_ALL_KEYS_AS_ESCAPE_CODES, and the #22021 checklist row now documents the Windows-scoped behavior.

Pullfrog  | Fix it ➔ | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

JasonJarvan and others added 7 commits September 24, 2026 01:05
Windows Sogou reports Shift-to-English as Process/229 on ShiftLeft during composition, then an empty compositionend and delayed insertText. Let that Shift reach CompositionHelper and wait for the held-key commit instead of dropping it.

Fixes stablyai#22021

Co-authored-by: Cursor <cursoragent@cursor.com>
Clear _imeKeydownAwaitingCommit in _cancelComposition so Escape cannot later claim a delayed insertText. Isolate cwd-targeted git from GIT_DIR, and return the filesystem root from commonParent when that is the only shared path.

Co-authored-by: Cursor <cursoragent@cursor.com>
…e modifiers

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Native Sogou reports Shift-to-English after compositionend with flags we cannot trust, so IME-state gates drop the held-key insertText. Always let Shift keydown reach CompositionHelper, which consumes it so kitty cannot encode a bare modifier.

Fixes stablyai#22021

Co-authored-by: Cursor <cursoragent@cursor.com>
The post-composition Shift guard was unread after Shift keydown became unconditional, and the IME early return also let a Shift-coded keyup through on macOS and Linux.

Delete the guard instead of restoring it. Native Sogou reports composition flags too late, so gating the keydown drops the latin commit. Keep delivering every Shift keydown; CompositionHelper still consumes it. Suppress Shift-coded keyup, including Process/229, and leave macOS and Linux on the previous IME policy.
The IME policy already gated its Shift carve-out to Windows keydown, but
the modifier policy did not: every platform stopped swallowing a Shift
keydown, so macOS and Linux began relying on the vendored
CompositionHelper patch consuming it to avoid a kitty press CSI-u. Only
Sogou needs the keydown delivered, so gate that half on Windows too and
leave the other platforms on the pane-level swallow they had before.

Regenerate the xterm bundle patch and lockfile hash: the committed bundle
was not what the pinned upstream build produces from the committed source
patch. The patched source tree is byte-identical either way — the source
patch only re-anchors its hunks.

Co-authored-by: JasonJarvan <JasonJarvan@users.noreply.github.com>
@nwparker
nwparker force-pushed the fix/windows-sogou-ime-shift branch from 30c05ec to cc28cca Compare September 24, 2026 08:22
@nwparker

Copy link
Copy Markdown
Contributor

Picked this up to get it over the line, and you had already done most of what I asked — so first, credit where it is due, and an apology for briefing my review as if you had gone quiet.

Blocker 1 (the dead guard): you resolved it in 30c05ec, choosing to delete, which is what the evidence supported. I verified by grep rather than taking the commit message for it — imeShiftCommitGuard, XtermModifierKeyboardOptions, isImeShiftCommitGuardActive, its reset sites and its tests are all gone, with nothing unreferenced left behind.

Blocker 2: you fixed half, and the half you fixed is right. shouldSuppressTerminalImeKeyboardEvent is now Windows-only and keydown-only, with the Shift-coded Process/229 keyup still suppressed on all three platforms and a test asserting no kitty release under REPORT_EVENT_TYPES. That is exactly the shape I was asking for.

The half I have added: shouldSuppressTerminalModifierKeyboardEvent was still ungated — return !(event.type === 'keydown' && event.key === 'Shift'), unconditional across platforms. So macOS and Linux stopped swallowing Shift at the pane level and began relying on the vendored CompositionHelper patch to consume it instead, which is the fragility your own third reviewer note was about, made load-bearing on two platforms that have no Sogou at all. It now takes { isMac, isLinux } and the carve-out is Windows-only. macOS and Linux are byte-identical to main again: the pane handler swallows Shift before xterm, so _imeKeydownAwaitingCommit is never even set there. Added a test covering both platforms, composing and idle.

Blocker 3 you covered better than I asked. I suggested a policy-level assertion; you drove a real Terminal with kitty enabled, negotiated \x1b[=8u, asserted the reply, then dispatched an idle Shift and asserted nothing was emitted. That exercises the vendored patch itself, so a regeneration that quietly undid the consume would fail it. Good.

Also rebased onto current main, which now carries #22607 — a Linux-only candidate-key guard in the same two files. The two are genuinely independent (isLinux && versus isWindows &&) and both survive; I checked rather than trusting the clean apply.

One thing still to settle: the branch's pnpm-lock.yaml records patch hash fb8f4c30… while the regenerated patch computes to something else, so the lockfile is stale relative to its own patch. main is in sync at 1,362,743 bytes, so this is specific to this branch rather than a repo-level problem. The xterm patch sync job will confirm once CI finishes.

Speaking of which — your CI had never run. The workflow runs sat unapproved since you opened this, so only the automated reviewer ever executed. I have approved them.

Native Windows Sogou verification is still owed and I am not asking you for it — one of our Windows machines has Sogou installed and I will drive that myself. Thank you for the original diagnosis; the runtime instrumentation in the linked report is what made this tractable.

@pullfrog pullfrog Bot left a comment

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.

ℹ️ No new issues in this delta. The two blocking items from the human review are addressed, and the stale xterm bundle/lockfile pair is back in lockstep. I'm not approving only because two earlier comments on this PR remain open and are untouched by these commits.

This run re-reviewed 5d5c029 and cc28cca, the two commits pushed after the previous Pullfrog review (30c05ec).

Reviewed changes

  • Gated the IME Shift carve-out to Windows keydown — shouldSuppressTerminalImeKeyboardEvent passes a Shift*-coded keydown only when isWindows and only for keydown, so macOS/Linux return to the previous policy and a Shift-coded Process/229 keyup falls through to the 229 suppression again.
  • Scoped the modifier policy to Windows — shouldSuppressTerminalModifierKeyboardEvent now takes {isMac, isLinux} and passes a Windows key:'Shift' keydown while swallowing it on macOS/Linux; the dead imeShiftCommitGuard state, option type, and tracker tests are deleted.
  • Re-regenerated the xterm bundle patch and pnpm-lock.yaml patch hash — the committed bundle now matches the pinned build; sha256(bundle patch) equals the lockfile hash and every dependent resolution key, and pnpm install --frozen-lockfile succeeds.
  • Added platform-matrix and idle-Shift tests — Windows Shift keydown delivered, Shift-coded 229 keyup suppressed, macOS/Linux unchanged, and an idle Shift keydown under REPORT_ALL_KEYS_AS_ESCAPE_CODES emits no CSI-u. All 71 tests in the three affected suites pass once the current patched bundle is installed.

Pullfrog  | Fix it ➔ | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@JasonJarvan

Copy link
Copy Markdown
Author

Native Windows Sogou smoke on the dev client at 30c05ec: type a pinyin preedit, then one Shift. The Latin letters commit and the IME switches to English. Confirmed by the reporter just now.

stablyai#22301 added src/shared/agent-session-context-usage.ts and src/shared/agent-session-context-usage-schema.ts to the route. It touched nothing under mobile/, so mains pin stayed at 4219 while the closure measures 4221.
@JasonJarvan

Copy link
Copy Markdown
Author

The mobile web app bundle failure is fixed in 087a738.

SESSION_ROUTE_MODULES was still 4219 while the session route closure measured 4221. #22301 added src/shared/agent-session-context-usage.ts and src/shared/agent-session-context-usage-schema.ts. That change touched nothing under mobile/, so the pin was never moved. This commit sets it to 4221. Locally, pnpm test config/scripts/mobile-web-app-session-terminal-closure.test.mjs passed 5/5, and pnpm run check:code-quality:changed passed.

The other red job on that run, test / tests node 24 3/8, was the palette matcher performance budget (about 186ms against a 180ms limit). That is timing jitter, not this change. verify failed because those two jobs failed.

@pullfrog pullfrog Bot left a comment

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.

Important

The new commit re-pins a module-count constant that main has already deleted, so it now conflicts with main on that file. Rebase onto main and keep main's version rather than carrying the repin.

This run re-reviewed the single commit pushed since the previous Pullfrog review (cc28cca).

Reviewed changes

  • Re-pinned the session-route module census — bumped SESSION_ROUTE_MODULES from 4219 to 4221 for #22301's two src/shared context-usage modules, adding a docstring paragraph that records the +2.
  • No product code in the delta — the commit is test-only and leaves the terminal IME path reviewed in earlier runs untouched.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

Comment thread config/scripts/mobile-web-app-session-terminal-closure.test.mjs Outdated
@JasonJarvan

Copy link
Copy Markdown
Author

Withdrawing 087a738. Reverted in 5207f82.

That pin change is the mobile web session closure count, not this terminal IME fix, and it was not part of the review. The branch is back to the previous SESSION_ROUTE_MODULES value.

@pullfrog pullfrog Bot left a comment

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.

ℹ️ No new issues. The delta is a clean revert of the previous test-only commit and clears the modify/delete conflict this review flagged; the branch still conflicts with main on pnpm-lock.yaml (noted in the prior run), so a rebase is still required before merge.

Reviewed changes

  • Reverted the mobile session-closure repin — 5207f82 reverts 087a738, so the SESSION_ROUTE_MODULES 4219 → 4221 bump and its #22301 docstring paragraph are gone and config/scripts/mobile-web-app-session-terminal-closure.test.mjs leaves the PR diff, resolving the modify/delete conflict against main.
  • No product code in the delta — the terminal IME path is byte-for-byte the code reviewed in runs 5 and 6.

The two earlier open findings (idle-Shift claim surviving a bypassed keydown, and the sticky _imeKeydownAwaitingCommit on the non-empty-textarea flush path) are untouched by this revert and remain open.

Pullfrog  | Fix it ➔ | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

This branch has not been deployed

No deployments
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.

[Bug]: Windows Sogou Shift-to-English drops pinyin in the Electron TUI

2 participants