Skip to content

Bug korean ibus input still duplicated or droppe - #11011

Open
AmethystLiang wants to merge 5 commits into
mainfrom
bug-korean-ibus-input-still-duplicated-or-droppe
Open

Bug korean ibus input still duplicated or droppe#11011
AmethystLiang wants to merge 5 commits into
mainfrom
bug-korean-ibus-input-still-duplicated-or-droppe

Conversation

@AmethystLiang

@AmethystLiang AmethystLiang commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Caution

This PR must be further verified on an independent Linux/X11 machine with a real IBus Hangul input method before merge. The current implementation is an experimental, best-effort reconciliation queue/loop that repeatedly tries to merge asynchronous composition, keypress, input, and textarea observations. The same-host results are encouraging, but we are not yet sure that this model is generally valid for every native IME event order.
It can also cause regression in other language (e.g. Chinese, Japanese) and regression on other platforms (macOS, Windows)

Summary

Fixes #9862.

The investigation localized the corruption to xterm's composition reconciliation:

  • On the failing build, DOM composition*/input(insertText) commits were correct.
  • terminal.onData() already contained the same dropped or duplicated text later observed at the PTY, so Orca's PTY forwarding was not the failing boundary.
  • Observed IBus sequences included a deferred finalizer seeing stale textarea state, a final consonant moving into the next syllable without a separate insertText, and both the timer and input(insertText) path emitting the same commit.

The xterm dependency patch now:

  • keeps FIFO records for pending composition finalizations instead of one global sending flag;
  • scopes deferred work to the composition transaction that created it;
  • reconciles composition, keypress, input, and textarea observations before emitting;
  • preserves final-consonant transfer and avoids suppressing legitimate repeated commits.

This PR also adds:

  • focused xterm composition regression coverage;
  • deterministic Electron -> terminal.onData() -> PTY exact-byte scenarios;
  • synthetic repeated Japanese and Chinese conversion coverage;
  • an isolated Linux/X11/IBus Hangul native runner using the issue's xdotool sequences;
  • a dedicated pull-request/scheduled workflow that archives boundary evidence.

The underlying xterm work was also ported upstream as xtermjs/xterm.js#6060.

What has been verified

The Codex session records contain the following same-host results from Ubuntu/X11 with IBus 1.5.29, ibus-hangul 1.5.5, and Electron 43.1.0:

Scenario Baseline Orca Patched Orca GNOME Terminal control
한abc글 exact bytes 0/30 30/30 30/30
Full Korean sentence stress case 0/30 30/30 30/30

For the patched exact-byte run:

  • the DOM recorded the expected Hangul commits;
  • terminal.onData() emitted exactly 30 copies of 한abc글\r;
  • the PTY received ed959c616263eab8800a on all 30 trials.

The recorded automated checks also report:

  • focused Vitest: 12/12 passed;
  • changed-file oxlint passed;
  • web TypeScript check passed;
  • max-lines ratchet passed with no bypass;
  • git diff --check was clean;
  • deterministic scenarios passed for retained/propagated Hangul commits, rapid boundaries, overlapping observations, stale timers/finalizers, no-keydown insertion, screen-reader suffixes, final-consonant transfer, and repeated Japanese/Chinese conversions.

What is not yet verified

  • The current PR has not yet produced an independent native Linux/IBus result on a separate machine. At the time this description was written, the visible GitHub check only covered community-PR tracking.
  • The overlap-merging and pending-finalization loop is heuristic. Passing the observed event sequences does not prove that it handles every Chromium/IBus timing or candidate shape.
  • Native Japanese, Simplified/Traditional Chinese, non-two-set Hangul layouts, macOS IMEs, and Windows IMEs were not exercised.
  • macOS, Windows, WSL, SSH, and Windows-over-SSH paths received deterministic/static review only, not native end-to-end validation.
  • A separate adversarial review found no concrete cross-language, platform, queue-growth, or ordinary-input performance regression, but its final signed synthesis stalled twice in the remote model service. We should not represent that as a completed independent approval.

Testing

The dedicated workflow is intended to run, in order:

  1. deterministic terminal IME boundary tests under Electron/Xvfb;
  2. native IBus Hangul exact-byte and sentence tests in an isolated X11/D-Bus session;
  3. evidence upload from test-results/.

Before merge, please run the native suite on a separate Linux/X11 machine and inspect the DOM, terminal.onData(), and PTY evidence rather than accepting retries alone as proof.

AI Review Report

Codex was used for reproduction, boundary tracing, implementation, test generation, and an adversarial static/test review. The evidence above separates observed native results from synthetic tests and static reasoning; it does not claim universal IME correctness.

Security Audit

The native runner starts its own Xvfb/D-Bus/IBus process group, records owned PIDs, and terminates only that group. The workflow has read-only repository permissions. Automated review comments about workflow credential persistence and runner hardening still need to be resolved separately.

Notes

This should be treated as an experimental fix with useful regression infrastructure, not as a fully validated general solution. Independent Linux verification is the next required confidence step.

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

Failed to post review comments.

We encountered an issue with GitHub. Use @coderabbitai full review to retry the review.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 83e082df-c171-45c1-b2b1-1bcb0587f780

📥 Commits

Reviewing files that changed from the base of the PR and between 7aa6838 and a767606.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (11)
  • .github/workflows/terminal-ime-e2e.yml
  • config/patches/@xterm__xterm@6.1.0-beta.287.patch
  • config/scripts/run-terminal-ibus-hangul-e2e.mjs
  • config/scripts/terminal-ime-e2e-workflow.test.mjs
  • package.json
  • src/renderer/src/components/terminal-pane/terminal-ime-xterm-composition-deduplication.test.ts
  • tests/e2e/terminal-ibus-hangul-native.spec.ts
  • tests/e2e/terminal-ime-boundary-probe.ts
  • tests/e2e/terminal-ime-byte-reader.ts
  • tests/e2e/terminal-ime-exact-byte.spec.ts
  • tests/e2e/terminal-ime-observed-event-sequences.ts
🚧 Files skipped from review as they are similar to previous changes (7)
  • config/scripts/terminal-ime-e2e-workflow.test.mjs
  • tests/e2e/terminal-ime-exact-byte.spec.ts
  • tests/e2e/terminal-ime-observed-event-sequences.ts
  • tests/e2e/terminal-ime-boundary-probe.ts
  • config/scripts/run-terminal-ibus-hangul-e2e.mjs
  • src/renderer/src/components/terminal-pane/terminal-ime-xterm-composition-deduplication.test.ts
  • package.json
⏰ Context from checks skipped due to timeout. (17)
  • GitHub Check: e2e / e2e 8-of-10
  • GitHub Check: e2e / e2e 1-of-10
  • GitHub Check: e2e / e2e 3-of-10
  • GitHub Check: e2e / e2e 10-of-10
  • GitHub Check: e2e / e2e 2-of-10
  • GitHub Check: e2e / e2e 4-of-10
  • GitHub Check: e2e / e2e 7-of-10
  • GitHub Check: e2e / e2e 5-of-10
  • GitHub Check: e2e / e2e 9-of-10
  • GitHub Check: e2e / e2e 6-of-10
  • GitHub Check: e2e / ssh docker watcher isolation
  • GitHub Check: Windows terminal restart regressions
  • GitHub Check: Greptile Review
  • GitHub Check: crash-survival (packaged build)
  • GitHub Check: Linux X11 terminal IME
  • GitHub Check: golden e2e mac experiment
  • GitHub Check: golden e2e linux experiment
🧰 Additional context used
📓 Path-based instructions (4)
**/*

📄 CodeRabbit inference engine (AGENTS.md)

Do not use vague names such as helpers, utils, common, misc, or shared-stuff for files, folders, or modules; use concrete domain names and split responsibilities when necessary.

Files:

  • config/patches/@xterm__xterm@6.1.0-beta.287.patch
  • tests/e2e/terminal-ibus-hangul-native.spec.ts
  • tests/e2e/terminal-ime-byte-reader.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx}: Comments must be concise, limited to non-obvious information, and preferably one line; do not explain obvious code or walk through implementation details.
Keep platform-dependent behavior behind runtime checks for macOS, Linux, and Windows. Use platform-aware keyboard shortcuts, CmdOrCtrl for Electron accelerators, platform-specific shortcut labels, and path utilities such as path.join instead of hardcoded separators.
Git commands must remain compatible with Git 2.25 across native, WSL, and SSH hosts. Check subcommand and option introduction versions, provide safe fallbacks, use host-scoped GitCapabilityCache with narrow unsupported-error handling, test caching/concurrency/host isolation, maintain real-binary compatibility CI, and preserve global options such as -c before subcommands.
Source-control and review changes must support GitLab and other supported providers; keep provider-specific behavior behind explicit checks and avoid GitHub-only names for generic review concepts.

Files:

  • tests/e2e/terminal-ibus-hangul-native.spec.ts
  • tests/e2e/terminal-ime-byte-reader.ts
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Never add max-lines disable directives, including eslint-disable max-lines, oxlint-disable max-lines, or line-specific variants.

Files:

  • tests/e2e/terminal-ibus-hangul-native.spec.ts
  • tests/e2e/terminal-ime-byte-reader.ts
**/*.{ts,tsx,js,jsx,sh,ps1}

📄 CodeRabbit inference engine (AGENTS.md)

Use the user's gh CLI rate limit carefully by batching requests and avoiding unnecessary calls; code, commands, and scripts must work on macOS, Linux, and Windows.

Files:

  • tests/e2e/terminal-ibus-hangul-native.spec.ts
  • tests/e2e/terminal-ime-byte-reader.ts
🧠 Learnings (1)
📚 Learning: 2026-07-22T18:28:59.997Z
Learnt from: OrcaWin
Repo: stablyai/orca PR: 9925
File: src/main/ipc/runtime-environment-shared-control-support.ts:4-7
Timestamp: 2026-07-22T18:28:59.997Z
Learning: In TypeScript, identifiers brought in via `import type` can be referenced inside purely type-level expressions (e.g., `ReturnType<typeof importedIdentifier>` or other `typeof` queries that are used only in a type position). Because the usage occurs entirely in the type space, it should not be treated as requiring a runtime import. In code review, do not require converting that `import type` to a value import when the identifier is only used within type queries/types.

Applied to files:

  • tests/e2e/terminal-ibus-hangul-native.spec.ts
  • tests/e2e/terminal-ime-byte-reader.ts
🪛 ast-grep (0.45.0)
tests/e2e/terminal-ibus-hangul-native.spec.ts

[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { execFileSync } from 'node:child_process'
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)

tests/e2e/terminal-ime-byte-reader.ts

[warning] 68-68: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(${reader.resultPrefix}:(\\d+):([0-9a-f]+), 'g')
Note: [CWE-1333] Inefficient Regular Expression Complexity

(regexp-from-variable)

🪛 Betterleaks (1.7.0)
config/patches/@xterm__xterm@6.1.0-beta.287.patch

[high] 6-6: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)


[high] 7-7: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

🪛 zizmor (1.28.0)
.github/workflows/terminal-ime-e2e.yml

[warning] 60-60: ad-hoc installation of packages (adhoc-packages): installs a package outside of a lockfile

(adhoc-packages)

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
📝 Walkthrough

Walkthrough

Adds expanded IME composition de-duplication tests and new Playwright helpers for boundary tracing, exact-byte capture, and synthetic event sequences. Adds native IBus Hangul tests with isolated X11/D-Bus process management and evidence collection. Adds a GitHub Actions workflow that builds and runs deterministic and native tests on Ubuntu, uploads results, and validates workflow configuration and process cleanup patterns.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. 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 is concise and matches the main change: Korean IBus input duplication/drop issues in the integrated terminal.
Description check ✅ Passed The description is mostly complete and includes substantive summary, testing, review, security, and notes sections.
Linked Issues check ✅ Passed The changes address #9862 with xterm reconciliation fixes plus Linux/X11 IBus Hangul regression tests and native runner coverage.
Out of Scope Changes check ✅ Passed No clearly unrelated changes are evident; the added workflow, helpers, and tests all support the Korean IBus fix.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

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: 4

🧹 Nitpick comments (7)
src/renderer/src/components/terminal-pane/terminal-ime-xterm-composition-deduplication.test.ts (1)

208-229: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider asserting observable output alongside xterm internals.

These two tests rely solely on private xterm fields (_compositionHelper._compositionPosition, _pendingCompositionFinalizations) that are only meaningful with the local @xterm/xterm patch; the beta version bump or patch rebase will silently break them (or worse, pass vacuously if the field disappears and toHaveLength throws vs. changes shape). Adding an emitted-based expectation in the deferred-position test would keep the behavioral contract covered even if internals move.

Also applies to: 293-305

tests/e2e/terminal-ime-byte-reader.ts (1)

69-73: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Dedupe by sequence number before counting.

matchAll over the whole scrollback can yield the same PREFIX:n: line twice (e.g. redraw/reflow), making results.length reach expectedLineCount with duplicated entries.

♻️ Dedupe by sequence
-        results = [...terminal.matchAll(resultPattern)]
-          .sort((left, right) => Number(left[1]) - Number(right[1]))
-          .map((match) => match[2])
+        const bySequence = new Map<number, string>()
+        for (const match of terminal.matchAll(resultPattern)) {
+          bySequence.set(Number(match[1]), match[2])
+        }
+        results = [...bySequence.entries()]
+          .sort(([left], [right]) => left - right)
+          .map(([, hex]) => hex)
config/scripts/run-terminal-ibus-hangul-e2e.mjs (1)

283-285: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Validate the inner-session argument and surface failures as an exit code.

process.argv[3] is assumed present; when missing, path.join(undefined, ...) throws a confusing TypeError. A rejection from either branch also becomes an unhandled rejection rather than a clean non-zero exit.

♻️ Suggested hardening
-const insideSession = process.argv[2] === insideSessionFlag
-const exitCode = insideSession ? await runInsideSession(process.argv[3]) : await runOuter()
-process.exitCode = exitCode
+const insideSession = process.argv[2] === insideSessionFlag
+try {
+  if (insideSession && !process.argv[3]) {
+    throw new Error(`${insideSessionFlag} requires an evidence directory argument`)
+  }
+  process.exitCode = insideSession ? await runInsideSession(process.argv[3]) : await runOuter()
+} catch (error) {
+  console.error(`[terminal-ime] ${error instanceof Error ? error.message : error}`)
+  process.exitCode = 1
+}
tests/e2e/terminal-ibus-hangul-native.spec.ts (2)

27-28: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

A 1 ms default key delay is likely to make the native run flaky.

xdotool type --delay 1 sends keystrokes faster than IBus/Hangul engine round-trips in CI, which is exactly the timing window where drops/duplication become nondeterministic and hard to distinguish from the bug under test. A higher default (e.g. 20-30 ms) keeps the suite meaningful while remaining fast at 5 repetitions; the env override still allows stress runs at 1 ms.

Also applies to: 73-83


155-180: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Both tests inherit whatever IME mode the previous one left behind.

The exact-byte test toggles Hangul twice (net no-op), so the sentence test only passes because of that accidental symmetry. Explicitly (re)selecting Hangul mode at the start of each scenario — e.g. inside focusNativeTerminalWindow after ibus engine hangul — removes the ordering dependency in this serial describe.

config/scripts/terminal-ime-e2e-workflow.test.mjs (2)

51-67: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Source-substring assertions on the runner are formatting-brittle.

These match exact prettier output of run-terminal-ibus-hangul-e2e.mjs; a reflow (e.g. the ibus arg array wrapping onto multiple lines) fails the test without any behavior change. Prefer whitespace-tolerant regexes for the positive checks, keeping only the killall/pkill/--replace negative guards as literals.


25-49: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the located steps exist, and match the native step loosely.

If the install step is renamed or restructured, installRun is undefined and toContain fails with an unhelpful message. runs.indexOf(...) also requires the run block to be byte-identical.

♻️ Proposed change
     const installRun = runs.find((run) => run.includes('apt-get install'))
 
+    expect(installRun).toBeDefined()
     expect(installRun).toContain('ibus-hangul')
-    const nativeIndex = runs.indexOf('pnpm run test:e2e:terminal-ime-native')
+    const nativeIndex = runs.findIndex((run) => run.includes('test:e2e:terminal-ime-native'))

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 71ef897d-a7da-4d1f-ad0c-daa4b485cf0e

📥 Commits

Reviewing files that changed from the base of the PR and between 0ab5f49 and 14e5ae7.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (11)
  • .github/workflows/terminal-ime-e2e.yml
  • config/patches/@xterm__xterm@6.1.0-beta.287.patch
  • config/scripts/run-terminal-ibus-hangul-e2e.mjs
  • config/scripts/terminal-ime-e2e-workflow.test.mjs
  • package.json
  • src/renderer/src/components/terminal-pane/terminal-ime-xterm-composition-deduplication.test.ts
  • tests/e2e/terminal-ibus-hangul-native.spec.ts
  • tests/e2e/terminal-ime-boundary-probe.ts
  • tests/e2e/terminal-ime-byte-reader.ts
  • tests/e2e/terminal-ime-exact-byte.spec.ts
  • tests/e2e/terminal-ime-observed-event-sequences.ts

Comment thread .github/workflows/terminal-ime-e2e.yml
Comment thread config/scripts/run-terminal-ibus-hangul-e2e.mjs
Comment thread tests/e2e/terminal-ime-boundary-probe.ts
Comment thread tests/e2e/terminal-ime-observed-event-sequences.ts
@OrcaWin
OrcaWin force-pushed the bug-korean-ibus-input-still-duplicated-or-droppe branch from 14e5ae7 to 84bf20b Compare July 28, 2026 06:33

@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.

🧹 Nitpick comments (4)
config/patches/@xterm__xterm@6.1.0-beta.287.patch (3)

28-81: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Repeated instanceof CompositionHelper guards suggest the field type may not need to be widened, or the checks are redundant dead branches.

_handleTextAreaBlur, the compositionend listener, the dispose registration, _keyPress, and _inputEvent all gate new behavior behind this._compositionHelper instanceof CompositionHelper. Since _compositionHelper is only ever constructed via this._instantiationService.createInstance(CompositionHelper, ...), the instanceof check should always be true, and the else/fallback branches (e.g. the else { this._compositionHelper!.compositionend(); } in the listener) become unreachable dead code.

If _compositionHelper's declared type is a broader interface intentionally (e.g. to support future/alternate implementations), please note that in a comment; otherwise consider dropping the repeated runtime checks in favor of the concrete type, which would simplify all five call sites.


330-337: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Newly added early-return guard in _finalizeComposition looks unreachable.

private _finalizeComposition(waitForPropagation: boolean, endData: string = ''): void {
    const wasComposing = this._isComposing;
    ...
    if (waitForPropagation && !wasComposing) {
      return;
    }

_finalizeComposition(true, ...) is only invoked from compositionend(), which already returns early unless this._isComposing is true — so wasComposing will always be true on that path. All other callers (keydown(), blur()) pass waitForPropagation=false. This makes the guard dead code as currently wired.


425-465: 🩺 Stability & Availability | 🔵 Trivial | 🏗️ Heavy lift

Complex textarea/keypress/input reconciliation heuristic has no accompanying unit test in this diff.

_sendPendingComposition, _getPendingTextareaInput, _updatePostCompositionInputExpectation, and _getCompositionInput implement an intricate reconciliation between deferred textarea snapshots and observed keypress/input candidates (including a nextCompositionStart interruption case for back-to-back compositions). This is exactly the kind of logic prone to subtle regressions across browsers/IMEs, and none of the files selected for this "Additional reviewed files" cohort exercise it directly.

Per the PR objectives, this is covered by terminal-ime-xterm-composition-deduplication.test.ts and the native IBus Hangul e2e in other layers of the stack — worth double-checking those specifically exercise: (a) rapid back-to-back compositions where nextCompositionStart gets set mid-flush, and (b) the pending.inputData || pending.keypressData precedence over the textarea-derived candidate.

.github/workflows/terminal-ime-e2e.yml (1)

58-61: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Pin node-gyp through the repo lockfile.

This workflow installs node-gyp@11.5.0 globally, while the repo tracks node-gyp@12.3.0 under pnpm. Add the required version as a workspace/tooling dependency and invoke it via pnpm exec, or at least keep the pinned global package audited/integrity-checked.

Source: Linters/SAST tools


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ab04e538-9c5b-4205-a305-61b9138337cb

📥 Commits

Reviewing files that changed from the base of the PR and between 14e5ae7 and 84bf20b.

📒 Files selected for processing (4)
  • .github/workflows/terminal-ime-e2e.yml
  • config/patches/@xterm__xterm@6.1.0-beta.287.patch
  • config/scripts/run-terminal-ibus-hangul-e2e.mjs
  • config/scripts/terminal-ime-e2e-workflow.test.mjs
🚧 Files skipped from review as they are similar to previous changes (2)
  • config/scripts/terminal-ime-e2e-workflow.test.mjs
  • config/scripts/run-terminal-ibus-hangul-e2e.mjs

@OrcaWin
OrcaWin force-pushed the bug-korean-ibus-input-still-duplicated-or-droppe branch from 84bf20b to 7312a49 Compare July 28, 2026 06:42

@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: 3

🧹 Nitpick comments (2)
tests/e2e/terminal-ime-byte-reader.ts (1)

55-55: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Quote the terminal command for its actual shell.

JSON.stringify does not shell-escape reader.scriptPath: POSIX shells still expand $ and backticks, while Windows command parsing differs. Build the command with platform-specific quoting behind a process.platform check.

As per coding guidelines, “Keep platform-dependent behavior behind runtime checks for macOS, Linux, and Windows.”

Source: Coding guidelines

config/patches/@xterm__xterm@6.1.0-beta.287.patch (1)

106-350: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Composition-transaction rewrite (CompositionHelper.ts) looks internally consistent with the bundle changes.

The new transaction model (_pendingComposition, _compositionTransactionId, _isAwaitingCompositionEnd, keypress()/input() reconciliation, blur()/dispose()) is mirrored correctly in both lib/xterm.js and lib/xterm.mjs, and CoreBrowserTerminal.ts gates the new API surface behind instanceof CompositionHelper consistently in both bundles. One minor observation: in _finalizeComposition(waitForPropagation, endData), the guard if (waitForPropagation && !wasComposing) return; appears unreachable given current call sites — waitForPropagation=true is only passed from compositionend(), which already returns early unless this._isComposing was true, so wasComposing will always be true in that branch today. Not a correctness bug, just dead defensive code that could be simplified or left with a comment explaining it's for future-proofing.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 401e1e71-a4b3-4b55-80b8-776ea401de1b

📥 Commits

Reviewing files that changed from the base of the PR and between 84bf20b and 7312a49.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (11)
  • .github/workflows/terminal-ime-e2e.yml
  • config/patches/@xterm__xterm@6.1.0-beta.287.patch
  • config/scripts/run-terminal-ibus-hangul-e2e.mjs
  • config/scripts/terminal-ime-e2e-workflow.test.mjs
  • package.json
  • src/renderer/src/components/terminal-pane/terminal-ime-xterm-composition-deduplication.test.ts
  • tests/e2e/terminal-ibus-hangul-native.spec.ts
  • tests/e2e/terminal-ime-boundary-probe.ts
  • tests/e2e/terminal-ime-byte-reader.ts
  • tests/e2e/terminal-ime-exact-byte.spec.ts
  • tests/e2e/terminal-ime-observed-event-sequences.ts
🚧 Files skipped from review as they are similar to previous changes (7)
  • tests/e2e/terminal-ime-observed-event-sequences.ts
  • package.json
  • config/scripts/terminal-ime-e2e-workflow.test.mjs
  • config/scripts/run-terminal-ibus-hangul-e2e.mjs
  • tests/e2e/terminal-ime-boundary-probe.ts
  • tests/e2e/terminal-ime-exact-byte.spec.ts
  • src/renderer/src/components/terminal-pane/terminal-ime-xterm-composition-deduplication.test.ts

Comment on lines +53 to +55
function runXdotool(...args: string[]): void {
execFileSync('xdotool', args, { stdio: 'pipe' })
}

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

site:nodejs.org/api child_process execFileSync timeout behavior

💡 Result:

In Node.js, the timeout option for child_process.execFileSync specifies the maximum number of milliseconds the child process is allowed to run [1][2]. If the process runs longer than this duration, the parent process sends the signal specified by the killSignal property, which defaults to SIGTERM [1][2]. Important behavioral details regarding this timeout include: 1. Method Blocking: Because execFileSync is a synchronous method, it blocks the Node.js event loop until the child process has fully closed [1][2]. Even after the timeout has been triggered and the kill signal sent, the method will not return until the child process has completely exited [1][2]. 2. Signal Handling: If the child process intercepts and handles the kill signal (e.g., SIGTERM) and does not exit, the parent process will continue to wait indefinitely until the child process eventually exits [1][2]. 3. Error Handling: If the child process times out, or if it exits with a non-zero exit code, execFileSync will throw an Error [1][2]. This error object contains the full result of the underlying child_process.spawnSync call, providing access to the captured output and exit information [1][2].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and inspect the relevant e2e test without running repository code.
if command -v fd >/dev/null 2>&1; then
  file="$(fd 'terminal-ibus-hangul-native\.spec\.ts$' . | head -n1 || true)"
else
  file="$(find . -type f -name 'terminal-ibus-hangul-native.spec.ts' | head -n1 || true)"
fi

if [ -z "${file:-}" ]; then
  echo "Relevant file not found"
  exit 0
fi

echo "FILE=$file"
wc -l "$file"
echo "--- overview ---"
ast-grep outline "$file" --view compact || true
echo "--- relevant lines ---"
sed -n '1,110p' "$file" | nl -ba
echo "--- execFileSync usages ---"
rg -n "execFileSync|runXdotool|runIbus|timeout:|killSignal:" "$file"

Repository: stablyai/orca

Length of output: 488


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="./tests/e2e/terminal-ibus-hangul-native.spec.ts"
echo "--- lines 1-110 ---"
awk '{printf "%5d  %s\n", NR, $0}' "$file" | sed -n '1,120p'

echo "--- execFileSync usages ---"
grep -nE "execFileSync|runXdotool|runIbus|timeout:|killSignal:" "$file" || true

echo "--- file metadata ---"
stat "$file"

Repository: stablyai/orca

Length of output: 5678


Bound synchronous native commands.

xdotool windowfocus --sync and the ibus engine commands invoke blocking execFileSync calls without a timeout. Use a shared execFileSync wrapper with a finite timeout so a stalled D-Bus/X11 command cannot occupy the full E2E workflow timeout.

Also applies to lines 65-67.

🧰 Tools
🪛 ast-grep (0.44.1)

[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { execFileSync } from 'node:child_process'
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)

Comment on lines +154 to +158
test.describe('Native IBus Hangul terminal input @headful', () => {
test.skip(
process.env.ORCA_E2E_NATIVE_IBUS_HANGUL !== '1',
'Run through config/scripts/run-terminal-ibus-hangul-e2e.mjs'
)

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate file =="
fd -a 'terminal-ibus-hangul-native.spec.ts' . || true

echo "== relevant file excerpt =="
if [ -f tests/e2e/terminal-ibus-hangul-native.spec.ts ]; then
  sed -n '1,230p' tests/e2e/terminal-ibus-hangul-native.spec.ts | cat -n
fi

echo "== referenced runner script =="
fd -a 'run-terminal-ibus-hangul-e2e.mjs' . || true
if [ -f config/scripts/run-terminal-ibus-hangul-e2e.mjs ]; then
  sed -n '1,240p' config/scripts/run-terminal-ibus-hangul-e2e.mjs | cat -n
fi

echo "== usage of ORCA_E2E_NATIVE_IBUS_HANGUL =="
rg -n "ORCA_E2E_NATIVE_IBUS_HANGUL|xdo|xdotool|ibus" . --glob '!node_modules/**' --glob '!dist/**' --glob '!build/**' || true

Repository: stablyai/orca

Length of output: 23939


Skip this Linux-native suite outside Linux at the spec level.

tests/e2e/terminal-ibus-hangul-native.spec.ts can enter this describe when ORCA_E2E_NATIVE_IBUS_HANGUL=1 is set on macOS or Windows, but focusNativeTerminalWindow() immediately runs xdotool/ibus before the runner’s Linux check runs.

Proposed fix
 test.skip(
-  process.env.ORCA_E2E_NATIVE_IBUS_HANGUL !== '1',
+  process.platform !== 'linux' || process.env.ORCA_E2E_NATIVE_IBUS_HANGUL !== '1',
   'Run through config/scripts/run-terminal-ibus-hangul-e2e.mjs'
 )
📝 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
test.describe('Native IBus Hangul terminal input @headful', () => {
test.skip(
process.env.ORCA_E2E_NATIVE_IBUS_HANGUL !== '1',
'Run through config/scripts/run-terminal-ibus-hangul-e2e.mjs'
)
test.describe('Native IBus Hangul terminal input `@headful`', () => {
test.skip(
process.platform !== 'linux' || process.env.ORCA_E2E_NATIVE_IBUS_HANGUL !== '1',
'Run through config/scripts/run-terminal-ibus-hangul-e2e.mjs'
)
🧰 Tools
🪛 ast-grep (0.44.1)

[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { execFileSync } from 'node:child_process'
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)

Source: Coding guidelines

Comment on lines +38 to +40
process.stdout.write(resultPrefix + ':' + receivedLineCount + ':' + line.toString('hex') + '\\n')
if (receivedLineCount === expectedLineCount) {
process.exit(0)

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

site:nodejs.org/api process.exit stdout writes may be truncated

💡 Result:

Calling process.exit in Node.js can cause output written to process.stdout and process.stderr to be truncated or lost [1][2]. This occurs because writes to these streams are often asynchronous and may span multiple ticks of the Node.js event loop [1][2]. When process.exit is invoked, it forces the process to terminate immediately, interrupting any pending asynchronous I/O operations before they can complete [1][2]. To avoid this issue, it is generally recommended to avoid calling process.exit explicitly [1]. Instead, you should allow the process to exit naturally by ensuring no additional work is pending in the event loop [1]. If a specific exit code is required, you can set the process.exitCode property and allow the process to finish gracefully [1][2]. If termination is necessary due to an error, throwing an uncaught error is considered a safer practice than calling process.exit [1][2].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and inspect the referenced test file around the reported lines.
if [ -f tests/e2e/terminal-ime-byte-reader.ts ]; then
  echo "== file exists =="
  wc -l tests/e2e/terminal-ime-byte-reader.ts
  echo "== lines 1-120 =="
  cat -n tests/e2e/terminal-ime-byte-reader.ts | sed -n '1,120p'
else
  echo "referenced file missing"
  fd -a 'terminal-ime-byte-reader\.ts' .
fi

echo "== related process.exit writes in referenced file =="
rg -n "process\.exit|stdout\.write|resultPrefix|receivedLineCount" tests/e2e/terminal-ime-byte-reader.ts || true

Repository: stablyai/orca

Length of output: 4492


🌐 Web query:

Node.js process.stdout.write callback synchronous false process.exit callback

💡 Result:

In Node.js, process.stdout.write does not guarantee synchronous completion, and its behavior—whether it behaves synchronously or asynchronously—depends on the underlying file descriptor, such as whether it is connected to a TTY, a file, or a pipe [1][2]. Regarding your query: 1. Asynchronous Behavior and Callbacks: While process.stdout.write(chunk, [encoding], [callback]) accepts an optional callback, this callback indicates only that the data has been handed off to the operating system; it does not guarantee that the data has been fully flushed or received by the destination [1][3]. 2. Process Exit: Calling process.exit forces the process to terminate immediately [4][5]. If there are asynchronous I/O operations pending—including writes to process.stdout—they will be abandoned, which can lead to truncated or missing output [4][5][6]. 3. Synchronicity: Writes to process.stdout are generally synchronous when the output is a file or a TTY (on POSIX systems), but they are often asynchronous when the output is a pipe [1][2]. Because this behavior is platform-dependent and dynamic, relying on the synchronous nature of a write is not recommended [2][6]. Recommendation: To ensure all data is written before a process exits, avoid calling process.exit explicitly [4][5]. Instead, allow the Node.js process to exit naturally by letting the event loop empty [4][5]. If you must exit with a specific code, use process.exitCode and ensure no further work is scheduled [4][5]. For cases where you must explicitly handle cleanup, you can monitor the stream's state (such as writableLength), but note that there is no universal "fireproof" API in Node.js to guarantee that stdout/stderr have been fully flushed to a pipe before process termination [3][7].

Citations:


🌐 Web query:

Node.js fs writeFileSync stdout write close drain process.exit documentation

💡 Result:

In Node.js, calling process.exit forces the process to terminate as quickly as possible, which can lead to truncated output or lost data if there are pending asynchronous I/O operations, including writes to process.stdout or process.stderr [1][2]. Key technical considerations: 1. Asynchronous nature of stdio: While writes to process.stdout may appear synchronous when directed to a terminal (TTY), they are frequently asynchronous when piped to other processes or files [3][4][5]. Because process.exit terminates the event loop immediately, it does not wait for these pending asynchronous operations to complete [1][2]. 2. Limitations of write callbacks: Using a callback with process.stdout.write does not guarantee that the data has been fully flushed to the underlying OS buffers before process.exit is called [6]. The process will still exit abruptly if the event loop is stopped by process.exit [3]. 3. The drain event: The 'drain' event is used for backpressure management—it signals that a writable stream's buffer has been emptied and it is safe to resume writing after a previous write call returned false [3]. It is not a mechanism for ensuring all data is flushed before an exit, and it should not be relied upon to keep the process alive [3]. 4. Recommended practice: The official Node.js documentation advises against calling process.exit explicitly [1][2]. Instead, developers should: - Use process.exitCode to set the desired exit status [1][2]. - Allow the process to exit naturally by ensuring no further work is scheduled on the event loop; this allows pending I/O operations and streams to flush gracefully [3][1][2]. - If an error must terminate the process, throwing an uncaught error is considered safer than using process.exit [1][2].

Citations:


Flush the final byte record before terminating.

process.exit(0) exits before any pending stdout writes from the PTY shell may complete, so the last expected line can be lost. Set process.exitCode = 0 and remove the explicit process.exit(0) so the reader can exit naturally after the final stream write completes.

@Jinwoo-H
Jinwoo-H force-pushed the bug-korean-ibus-input-still-duplicated-or-droppe branch from 7312a49 to 7aa6838 Compare July 28, 2026 19:56

@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.

🧹 Nitpick comments (1)
config/patches/@xterm__xterm@6.1.0-beta.287.patch (1)

44-46: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Repeated instanceof CompositionHelper guards suggest the interface is missing the new members.

blur(), dispose(), keypress(), input(), and the CompositionEvent-accepting overload of compositionend() are all guarded by instanceof CompositionHelper at four separate call sites. Since _compositionHelper is always constructed via this._instantiationService.createInstance(CompositionHelper, ...), these branches likely only exist for TypeScript type-narrowing against a narrower ICompositionHelper interface. Adding these methods to that interface would remove the repeated runtime checks and the risk that a future call site forgets the guard (silently falling back to old, now-incomplete behavior).

♻️ Suggested direction
-  private _handleTextAreaBlur(): void {
-    if (this._compositionHelper instanceof CompositionHelper) {
-      this._compositionHelper.blur();
-    }
+  private _handleTextAreaBlur(): void {
+    this._compositionHelper?.blur();

(assuming blur/dispose/keypress/input/compositionend(ev) are added to ICompositionHelper)

Also applies to: 56-61, 69-73, 82-84, 92-100


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 3bc2e4a0-6940-4733-bd8a-1fe441d8813b

📥 Commits

Reviewing files that changed from the base of the PR and between 7312a49 and 7aa6838.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (11)
  • .github/workflows/terminal-ime-e2e.yml
  • config/patches/@xterm__xterm@6.1.0-beta.287.patch
  • config/scripts/run-terminal-ibus-hangul-e2e.mjs
  • config/scripts/terminal-ime-e2e-workflow.test.mjs
  • package.json
  • src/renderer/src/components/terminal-pane/terminal-ime-xterm-composition-deduplication.test.ts
  • tests/e2e/terminal-ibus-hangul-native.spec.ts
  • tests/e2e/terminal-ime-boundary-probe.ts
  • tests/e2e/terminal-ime-byte-reader.ts
  • tests/e2e/terminal-ime-exact-byte.spec.ts
  • tests/e2e/terminal-ime-observed-event-sequences.ts
🚧 Files skipped from review as they are similar to previous changes (7)
  • package.json
  • tests/e2e/terminal-ime-exact-byte.spec.ts
  • config/scripts/terminal-ime-e2e-workflow.test.mjs
  • tests/e2e/terminal-ime-observed-event-sequences.ts
  • config/scripts/run-terminal-ibus-hangul-e2e.mjs
  • src/renderer/src/components/terminal-pane/terminal-ime-xterm-composition-deduplication.test.ts
  • tests/e2e/terminal-ime-boundary-probe.ts

AmethystLiang and others added 5 commits July 28, 2026 13:18
- Add deterministic xterm boundary tests and a Linux/X11 native IBus Hangul E2E runner
- Patch xterm composition handling and expand unit coverage for commit deduplication and retention
- Wire a scheduled GitHub Action to install IME tooling, run the new suites, and archive evidence
@OrcaWin
OrcaWin force-pushed the bug-korean-ibus-input-still-duplicated-or-droppe branch from 7aa6838 to a767606 Compare July 28, 2026 20:21
Comment on lines +36 to +43
}
})

function nativeRepetitions(): number {
const parsed = Number(process.env.ORCA_E2E_NATIVE_IBUS_REPETITIONS ?? DEFAULT_REPETITIONS)
return Number.isInteger(parsed) && parsed > 0
? Math.min(parsed, MAX_REPETITIONS)
: DEFAULT_REPETITIONS

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 IBUS_ENABLE_SYNC_MODE=1 disables the async timing the original bug depended on

The original defect (dropped/duplicated Hangul) was caused by asynchronous IBus event ordering — deferred finalizers seeing stale state and multiple paths racing to emit the same commit. Forcing IBUS_ENABLE_SYNC_MODE=1 on both the outer session (in run-terminal-ibus-hangul-e2e.mjs) and the Electron app eliminates that async interleaving entirely. This means the native suite validates the new queue logic against a sequential event stream, not the actual race-prone stream real users encounter. A test run that passes under sync mode cannot confirm the fix handles the asynchronous case that caused the original issue.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines 567 to 579
})

it('preserves composition-first order when keypress overlaps its suffix', async () => {
it('keeps a following keypress even when it matches the composition suffix', async () => {
const { emitted, terminal, textarea } = openTerminal()
startComposition(textarea, '가한')
await nextEventLoop()

textarea.dispatchEvent(new CompositionEvent('compositionend', { bubbles: true }))
dispatchCompositionEvent(textarea, 'compositionend')
dispatchKeypress(textarea, '한')
await nextEventLoop()

expect(emitted.join('')).toBe('가한')
expect(emitted.join('')).toBe('가한한')
terminal.dispose()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Deliberate removal of suffix/prefix overlap deduplication may regress non-Korean platforms

Three existing tests were renamed and their expected output was inverted:

  • 'keeps a following keypress even when it matches the composition suffix': previously '가한', now '가한한'
  • 'keeps a following keypress even when it matches the composition prefix': previously '한a', now '한a한'
  • Ordering of composition text vs following keypress: previously '한a', now 'a한'

The removed suppression logic existed to handle macOS and Windows IMEs that fire a real keypress event whose text is the committed composition text (i.e. the suffix). Emitting both means users on those platforms could see duplicated characters after every IME commit. The PR description acknowledges this risk explicitly, but the test suite that validates macOS/Windows/WSL paths is described as "deterministic/static review only." Consider whether the overlap detection can be scoped to the Hangul-specific path (e.g. guarded by isComposing === false on the following keypress) to preserve safety for other IMEs.

Comment on lines +30 to +34
- name: Checkout
uses: actions/checkout@v6
with:
persist-credentials: false

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 security GitHub Actions steps reference mutable tags rather than immutable commit SHAs

actions/checkout@v6, actions/setup-node@v6, pnpm/action-setup@v6, and actions/upload-artifact@v7 all use floating version tags. A compromised or updated tag could silently swap in malicious code on future workflow runs. Pinning each action to a full commit SHA (e.g. actions/checkout@<sha>) is the standard supply-chain hardening practice for third-party GitHub Actions.

Comment on lines +110 to +116
'한abc글',
dispatchObservedIbusHangulMixedSequence,
(trace) => {
const commits = trace.dom
.filter((event) => event.type === 'input' && event.inputType === 'insertText')
.map((event) => event.data)
expect(commits).toEqual(expect.arrayContaining(['한', '글']))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Linux-only guard prevents detecting regressions on other platforms

test.skip(process.platform !== 'linux', ...) means the deterministic exact-byte scenarios — which now embed the new suffix/prefix-pass-through behavior — never run on macOS or Windows CI. If the new composition reconciliation emits extra characters for macOS IME sequences, the exact-byte suite will not catch it. Consider whether any platform-agnostic subset of these scenarios can run unconditionally, or add a separate macOS run in the workflow.

@greptile-apps

greptile-apps Bot commented Jul 28, 2026

Copy link
Copy Markdown

Greptile Summary

This PR patches the vendored xterm.js bundle and adds a matching test infrastructure to fix duplicated/dropped Korean (Hangul) IBus input on Linux/X11. The core change replaces a single global _isSendingComposition flag with a per-transaction FIFO queue that reconciles composition*, input(insertText), keypress, and textarea observations before emitting to terminal.onData().

  • xterm patch: Rewrites the composition reconciliation loop in the minified lib/xterm.js bundle; the patch also deliberately removes the suffix/prefix keypress overlap-deduplication that previously suppressed keypresses matching composition text — a change that fixes Hangul final-consonant transfer but may emit extra characters for macOS/Windows IMEs.
  • Native test infrastructure: Adds an isolated Xvfb/D-Bus/IBus session runner, a deterministic exact-byte Playwright suite, a DOM boundary probe, and a GitHub Actions workflow — all gated behind IBUS_ENABLE_SYNC_MODE=1, which disables the async IBus timing the original bug depended on.

Confidence Score: 3/5

This is an experimental fix with real unknowns: the test suite forces synchronous IBus mode, which does not replicate the async race that caused the original bug; and three tests were deliberately changed to allow keypresses that were previously suppressed, introducing a concrete risk of duplicated characters on macOS and Windows IMEs.

The xterm composition logic change is embedded entirely in a minified bundle, making the reconciliation queue and FIFO loop unverifiable at the source level. The native test suite gates on IBUS_ENABLE_SYNC_MODE=1 rather than the default async mode — so passing 30/30 trials under that setup says less than it appears. The deliberate removal of suffix/prefix keypress deduplication — necessary for Hangul final-consonant transfer — directly changes behavior for code paths used by other IMEs on other platforms, and no automated cross-platform validation exists for those paths. These concerns compound rather than cancel each other.

Files Needing Attention: config/patches/@xterm__xterm@6.1.0-beta.287.patch (unreadable minified diff), tests/e2e/terminal-ibus-hangul-native.spec.ts (IBUS_ENABLE_SYNC_MODE concern), src/renderer/src/components/terminal-pane/terminal-ime-xterm-composition-deduplication.test.ts (inverted suffix/prefix deduplication expectations)

Security Review

  • Supply chain — floating GitHub Actions tags (.github/workflows/terminal-ime-e2e.yml): actions/checkout@v6, actions/setup-node@v6, pnpm/action-setup@v6, and actions/upload-artifact@v7 all use mutable version tags rather than pinned commit SHAs. A tag hijack could inject arbitrary code into the workflow. The workflow is permissions: contents: read only, which limits blast radius, but pinning to SHAs is still recommended.
  • No secrets, credentials, or tokens are introduced. The native runner starts its own Xvfb/D-Bus/IBus process group and terminates only that group via SIGTERM/SIGKILL on the owned process group ID.

Important Files Changed

Filename Overview
config/patches/@xterm__xterm@6.1.0-beta.287.patch Core composition logic fix embedded in a minified bundle — entirely unreadable at the source level; changes to FIFO per-transaction state and the reconciliation loop cannot be algorithmically reviewed
src/renderer/src/components/terminal-pane/terminal-ime-xterm-composition-deduplication.test.ts Comprehensive new unit tests for Korean/CJK composition; three existing tests deliberately inverted their expected output to remove suffix/prefix keypress deduplication — a behavioral change that may regress macOS/Windows IMEs
tests/e2e/terminal-ibus-hangul-native.spec.ts New native IBus Hangul e2e spec, but forces IBUS_ENABLE_SYNC_MODE=1 which eliminates the async timing that caused the original bug; tests skipped unless ORCA_E2E_NATIVE_IBUS_HANGUL=1
config/scripts/run-terminal-ibus-hangul-e2e.mjs Well-structured isolated X11/D-Bus/IBus session runner with proper process-group cleanup; minor bug where evidence.ibusGroupBeforeCleanup is overwritten in the finally block
.github/workflows/terminal-ime-e2e.yml New workflow for Linux/X11 IME testing with correct read-only permissions; all four third-party actions use floating version tags instead of pinned SHAs — supply chain risk
tests/e2e/terminal-ime-exact-byte.spec.ts Deterministic exact-byte Hangul/Japanese/Chinese scenarios; skipped on non-Linux which prevents catching regressions on macOS/Windows where the removed deduplication may matter
tests/e2e/terminal-ime-boundary-probe.ts Clean DOM event and onData trace probe; accesses internal pane state via window globals but is test-only and isolated to the probe install/dispose pattern
tests/e2e/terminal-ime-byte-reader.ts Writes a CJS hex-reporter script to the worktree and sends it to the terminal PTY for exact-byte verification; uses JSON.stringify for path quoting and cleans up via rmSync in finally
tests/e2e/terminal-ime-observed-event-sequences.ts Synthetic IBus event sequence replay using DOM APIs; textarea.value is never updated to reflect the first composition commit in the retained variant, which may not faithfully replicate real IBus sequences
config/scripts/terminal-ime-e2e-workflow.test.mjs Vitest-based workflow structural test that validates apt-get packages, step ordering, and runner script contents by reading live files
package.json Adds test:e2e:terminal-ime-native script pointing to the new runner

Sequence Diagram

sequenceDiagram
    participant IBus as IBus (native)
    participant Chromium as Chromium/X11
    participant xterm as xterm.js patch
    participant Queue as FIFO Pending Queue
    participant Terminal as terminal.onData()
    participant PTY as PTY

    IBus->>Chromium: compositionstart
    Chromium->>xterm: compositionstart event
    xterm->>Queue: create composition transaction

    IBus->>Chromium: compositionupdate(한)
    Chromium->>xterm: compositionupdate + input(insertCompositionText)
    xterm->>Queue: update current transaction

    IBus->>Chromium: compositionend (deferred finalizer)
    Chromium->>xterm: compositionend
    xterm->>Queue: enqueue pending finalization (scoped to transaction)

    note over IBus,xterm: Async: finalizer may see stale textarea state
    Chromium->>xterm: input(insertText, 한)
    xterm->>Queue: reconcile: match insertText to pending transaction
    Queue->>Terminal: emit 한 (exactly once)

    IBus->>Chromium: next compositionstart (글)
    Chromium->>xterm: compositionstart
    xterm->>Queue: new transaction (old transaction finalized)

    IBus->>Chromium: compositionend(글) + insertText(글)
    Chromium->>xterm: compositionend + input(insertText, 글)
    xterm->>Queue: reconcile: emit 글
    Queue->>Terminal: emit 글

    Terminal->>PTY: forward 한abc글\r
Loading

Comments Outside Diff (2)

  1. config/scripts/run-terminal-ibus-hangul-e2e.mjs

    P2 The finally block re-captures and overwrites evidence.ibusGroupBeforeCleanup, which was already populated in the try block (line 169). Only the later snapshot (taken just before cleanup, after the tests finish) survives into the JSON artifact. The earlier snapshot — which reflects the live IBus group during the test run — is silently lost. Using a distinct field name preserves both captures for debugging.

  2. config/patches/@xterm__xterm@6.1.0-beta.287.patch, line 1-5 (link)

    P2 Patch applies only to the minified bundle, making the composition logic change unreadable

    The diff modifies lib/xterm.js (a single-line minified bundle). All of the FIFO queue, per-transaction state, and reconciliation loop changes are embedded in this unreadable form. There is no corresponding diff of the TypeScript source or a human-readable re-write of the patched functions, so the composition logic cannot be reviewed for correctness at the algorithmic level. Upstream Fix dropped and duplicated IME composition commits xtermjs/xterm.js#6060 reportedly contains the readable source; linking to and including a summary of the source-level diff in the PR description would significantly improve reviewability.

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Reviews (1): Last reviewed commit: "fix(terminal): reject stale IME composit..." | Re-trigger Greptile

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][Linux-only]: Korean/IBus input still duplicated or dropped in the integrated terminal on v1.4.149 (regression of #8861)

2 participants