Skip to content

fix(settings): prevent IME composition cancellation in repository Display Name and Worktree Location inputs#4632

Merged
Jinwoo-H merged 2 commits into
stablyai:mainfrom
amondnet:fix/repo-display-name-ime-composition
Jun 4, 2026
Merged

fix(settings): prevent IME composition cancellation in repository Display Name and Worktree Location inputs#4632
Jinwoo-H merged 2 commits into
stablyai:mainfrom
amondnet:fix/repo-display-name-ime-composition

Conversation

@amondnet

@amondnet amondnet commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Summary

In Settings → Repository pane, typing Korean (Hangul) into the Display Name input caused syllables to decompose into bare jamo (가나다 → ㄱㅏㄴㅏㄷㅏ). The Worktree Location input had the same bug with the same root cause but manifested as dropped characters at fast typing speed.

Root cause: Both inputs were controlled inputs bound directly to the zustand store (value={repo.displayName}). updateRepo only writes to the store after an awaited IPC round-trip (repos:update). On every keystroke React synchronously restored the input to the stale store value, which silently killed the active IME composition (no compositionend fires). The async IPC echo then re-inserted the cancelled jamo as committed text.

Fix: Introduce a RepoSettingsDraftInput wrapper component that keeps keystrokes in local draft state so the input is always synchronously controlled. The store is still written on every keystroke in the background so the sidebar stays live. The draft resets only when the pane switches to a different repo, so async IPC echoes can never clobber newer draft text.

Applied to both the Display Name and Worktree Location inputs. Also linked the Display Name Label to its input via htmlFor/id (a11y + test locator).

Screenshots

No visual change — the fix is entirely behavioral (correct IME composition in Korean and other CJK input methods).

Testing

  • pnpm lint
  • pnpm typecheck
  • pnpm test
  • pnpm build
  • Added or updated high-quality tests that would catch regressions, or explained why tests were not needed

Unit tests (RepositoryPaneDraftInput.test.tsx, happy-dom, 7 cases):

  • Draft survives a stale store re-render after a keystroke
  • Draft survives a stale IPC echo arriving after newer keystrokes
  • Draft resets when the active repo changes (repo switch)
  • onUpdate fires on every keystroke (per-keystroke persistence)

Playwright e2e (tests/e2e/settings-display-name-ime.spec.ts):

  • Drives Blink's real IME pipeline via CDP Input.imeSetComposition / Input.insertText with an adaptive 2-set Korean IME emulator
  • The emulator detects page-clobbered composition the same way a real OS IME is cancelled
  • Fails with exactly Received: "ㄱㅏㄴㅏㄷㅏ" on the pre-fix code; passes with 가나다 on the fix
  • Slow per-key pacing (50 ms) to surface the IPC-echo race

AI Review Report

Reviewed the fix for correctness across the IME composition lifecycle. Key risks checked:

  • Race condition safety: The draft reset guard uses a ref comparison (repo.id) rather than a deep-equality check, which is fast and correct — repo identity is stable within a single pane view.
  • Double-write risk: onUpdate is called on every draft keystroke. Confirmed the IPC handler is idempotent and debounce is not needed given the existing async behavior.
  • Cross-platform: The fix is pure React state; no platform-specific keyboard or path handling is involved. Confirmed compatible with macOS, Linux, and Windows.
  • SSH use case: The Worktree Location input retains its existing path-change behavior; the draft only gates the controlled value, not the persistence path.
  • No regressions to other settings inputs: RepoSettingsDraftInput is scoped to the two affected fields; no other inputs in RepositoryPane were changed.

No flags raised that required code changes beyond the initial implementation.

Security Audit

  • No new IPC channels introduced; the fix sits entirely in the renderer.
  • onUpdate passes draft string values through the existing repos:update IPC handler — no change to input sanitization surface.
  • No secrets, auth tokens, or path traversal concerns in the changed files.
  • No new dependencies added.

Notes

  • The e2e spec requires CDP access (Electron in debug mode with --remote-debugging-port). The test file documents the required launch flags and will be skipped gracefully if CDP is unavailable.
  • test-results/ artifacts from local Playwright runs are intentionally not committed.

…play Name and Worktree Location inputs

Controlled inputs bound directly to the zustand store caused IME
composition to be cancelled on every keystroke because React re-rendered
from the stale store value before the async IPC round-trip completed.
On slow typing this yielded bare jamo decomposition (가나다 → ㄱㅏㄴㅏㄷㅏ);
on fast typing characters were silently dropped.

Introduce RepoSettingsDraftInput, a wrapper that keeps keystrokes in
local draft state so the input is always synchronously controlled. The
store is still written on every keystroke in the background so the
sidebar stays live. Draft resets only when the active repo changes, so
async IPC echoes cannot clobber newer draft text.

Apply RepoSettingsDraftInput to both the Display Name and the Worktree
Location inputs (identical store-bound binding, same root cause).

Also link the Display Name Label to its input via htmlFor/id for a11y
and test-locator clarity.

Tests added:
- RepositoryPaneDraftInput.test.tsx: unit tests (happy-dom) — draft
  survives stale store re-render and stale IPC echo; resets on repo
  switch; persists on every keystroke.
- tests/e2e/settings-display-name-ime.spec.ts: Playwright e2e driving
  Blink's real IME pipeline via CDP Input.imeSetComposition /
  Input.insertText with an adaptive 2-set Korean IME emulator.
  Fails with 'ㄱㅏㄴㅏㄷㅏ' on the pre-fix code; passes on the fix.

@Jinwoo-H Jinwoo-H 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. I pushed one maintainer follow-up so the draft input still accepts same-repo store updates that did not originate from the input, such as clearing Worktree Location via "Use Global," while continuing to ignore stale async echoes from prior keystrokes.

Verified with typecheck, lint, focused renderer tests, and the Electron IME e2e for Hangul composition. The e2e confirms both the visible Display Name input and persisted store value are 가나다.

@Jinwoo-H
Jinwoo-H merged commit e31f65f into stablyai:main Jun 4, 2026
1 check passed
thomaszdxsn pushed a commit to thomaszdxsn/orca that referenced this pull request Jun 15, 2026
…play Name and Worktree Location inputs (stablyai#4632)

* fix(settings): prevent IME composition cancellation in repository Display Name and Worktree Location inputs

Controlled inputs bound directly to the zustand store caused IME
composition to be cancelled on every keystroke because React re-rendered
from the stale store value before the async IPC round-trip completed.
On slow typing this yielded bare jamo decomposition (가나다 → ㄱㅏㄴㅏㄷㅏ);
on fast typing characters were silently dropped.

Introduce RepoSettingsDraftInput, a wrapper that keeps keystrokes in
local draft state so the input is always synchronously controlled. The
store is still written on every keystroke in the background so the
sidebar stays live. Draft resets only when the active repo changes, so
async IPC echoes cannot clobber newer draft text.

Apply RepoSettingsDraftInput to both the Display Name and the Worktree
Location inputs (identical store-bound binding, same root cause).

Also link the Display Name Label to its input via htmlFor/id for a11y
and test-locator clarity.

Tests added:
- RepositoryPaneDraftInput.test.tsx: unit tests (happy-dom) — draft
  survives stale store re-render and stale IPC echo; resets on repo
  switch; persists on every keystroke.
- tests/e2e/settings-display-name-ime.spec.ts: Playwright e2e driving
  Blink's real IME pipeline via CDP Input.imeSetComposition /
  Input.insertText with an adaptive 2-set Korean IME emulator.
  Fails with 'ㄱㅏㄴㅏㄷㅏ' on the pre-fix code; passes on the fix.

* fix(settings): preserve external repo setting updates

Co-authored-by: Orca <[email protected]>

---------

Co-authored-by: Jinwoo-H <[email protected]>
Co-authored-by: Orca <[email protected]>
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.

2 participants