fix(viewer): prevent IME composition interruption in search inputs#517
Conversation
The viewer's five search inputs (graph, memories, lessons, actions, crystals) destroy and recreate their input DOM via innerHTML on every keystroke, which interrupts active IME composition sessions and makes non-Latin input (Chinese, Japanese, Korean) unusable. Additionally, the viewer's CSP includes script-src-attr 'none', which silently blocks the inline oninput=/onchange= handlers on the lessons, actions, and crystals panels. Those three search/filter controls have been non-functional under the strict CSP. This patch: 1. Adds a bindImeSafeSearch helper that guards on both an explicit compositionstart/compositionend flag and event.isComposing. compositionend triggers an immediate commit and sets a justCommitted one-shot flag to suppress the redundant trailing input event that browsers dispatch after compositionend. 2. Adds captureSearchFocus/restoreSearchFocus helpers to preserve focus and cursor position across innerHTML rebuilds, so multi-word IME input doesn't require clicking back into the search box after each commit. 3. Migrates all five search inputs to addEventListener via the new helpers, removing the CSP-blocked inline handlers on lessons, actions, and crystals. The actions panel's status filter <select> is also migrated for the same reason. 4. Unifies debounce delay to 200ms across all five panels. Verified via: - 8/8 jsdom + synthetic CompositionEvent regression cases (ASCII debounce, IME composing suppresses input, compositionend immediate commit + justCommitted suppression, post-IME ASCII resumes, fast typing coalesces, composition cancel returns to idle). - 8/8 manual browser cases on Windows + Chrome (Chinese pinyin commit, multi-word IME without re-focusing, English regression, all five panels, zero CSP violations in DevTools, cursor position retained). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
@jonathanzhan1975 is attempting to deploy a commit to the rohitg00's projects Team on Vercel. A member of the Team first needs to authorize it. |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughThis PR adds IME-safe input handling to prevent premature searches during text composition. Three helper functions manage composition detection, debounced search, and focus/selection preservation. All five search tabs are then updated to use these utilities while maintaining caret position across re-renders. ChangesIME-safe search input handling
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Merged + shipping in next release. Thanks @jonathanzhan1975 for the IME composition guard — clean fix for CJK users hitting mid-character interruption on the viewer search inputs. |
Quality + integration wave. Bundles 11 PRs since v0.9.20: Contributor feature: - #237 OpenCode plugin with 22 auto-capture hooks (@cl0ckt0wer) Bug fixes (9): - #516 memory_recall endpoint + format/token_budget (@serhiizghama, closes #507/#440) - #461 env-file AGENTMEMORY_DROP_STALE_INDEX flag honored (@honor2030, closes #456) - #487 Windows hook path quoting (@honor2030, closes #477) - #517 viewer IME composition guard (@jonathanzhan1975) - #472 chunk large sessions for LLM context window (@efenex) - #473 surface lessons in smart-search + diagnose tally (@efenex) - #486 declare all Hermes plugin hooks (@honor2030) - #500 rebuildIndex non-blocking on boot (@efenex) - #504 batched embed in rebuildIndex (25h -> 3h) (@efenex) - #491 cli skip onboarding without tty (@honor2030) Upstream-installer revert: - #546 drop --next workaround now that iii-hq/iii#1660 shipped 1067/1067 tests pass across 95 files.
Problem
The viewer's five search inputs — graph (
#graph-search), memories (#mem-search), and the lessons/actions/crystals filters — destroy and recreate the focused input element viainnerHTMLon every keystroke. This interrupts any active IME composition session, making non-Latin input (Chinese pinyin, Japanese kana-kanji, Korean hangul) effectively impossible: the candidate window keeps closing before the user can commit a character.While investigating I also noticed that the viewer's CSP includes:
(see
src/viewer/document.ts/buildViewerCsp). This silently blocks the inlineoninput="..."handlers on the lessons/actions/crystals panels and theonchange="..."on the actions status<select>. Under the strict CSP, those three search controls and the status filter have been non-functional — the inline handlers never run. The user just sees a search box that does nothing.Fix
This PR adds two small helpers and migrates all five search inputs to use them. No framework, no render-layer refactor, no behavior change for ASCII users.
bindImeSafeSearch(input, ms, onSearch)Guards on both an explicit
compositionstart/compositionendflag andevent.isComposing, because engine-specific event ordering differs between Chromium/Firefox/Safari and you can't rely on either signal alone (per MDN InputEvent.isComposing and the compositionend spec).compositionendtriggers an immediate commit (so the user sees the filter result without a 200ms delay after every IME selection). A one-shotjustCommittedflag suppresses the redundant trailinginputevent that browsers dispatch aftercompositionend, avoiding a duplicate render.captureSearchFocus(ids)/restoreSearchFocus(focus)Records
activeElement.id,selectionStart,selectionEndimmediately before eachinnerHTML = html, then restores focus and cursor position after re-binding listeners. The guard checksactiveElement.idis in the managed list before saving, so we never steal focus from anywhere else.Without this, even after fixing the IME interrupt, multi-word Chinese input still required clicking back into the search box after every committed word.
Migrated controls
All five search inputs and the actions status
<select>are migrated toaddEventListenervia the helpers above, with a unified 200ms debounce.#graph-searchaddEventListener('input', debounce(..., 150))bindImeSafeSearch(..., 200, ...)+ focus restore#mem-searchaddEventListener('input', debounce(..., 200))bindImeSafeSearch(..., 200, ...)+ focus restore.search-input→#lessons-searchoninput=(CSP-blocked)bindImeSafeSearch(..., 200, ...)+ focus restore.search-input→#actions-searchoninput=(CSP-blocked)bindImeSafeSearch(..., 200, ...)+ focus restore<select>→#actions-status-filteronchange=(CSP-blocked)addEventListener('change', ...).search-input→#crystals-searchoninput=(CSP-blocked)bindImeSafeSearch(..., 200, ...)+ focus restoreThe patch carries an
// IME_SAFE_SEARCH_V2marker comment for quickgrepverification.Verification
Synthetic regression (Node + jsdom + synthetic
CompositionEvent/InputEvent): 8/8 passjustCommittedsuppresses the trailing input event (single commit)Manual browser (Windows 11 + Chrome): 8/8 pass
abc→ cursor betweenaandb→ typeX→ resultaXbc)script-src-attrCSP violations after the fix (previously had violations on every lessons/actions/crystals interaction)<select>change handler binds correctlyWhat this PR explicitly does not do
innerHTML-based)Risk surface
el.focus()after each managed render. This is gated onactiveElement.idmatching a managed input, so it can't steal focus from elsewhere on the page.setSelectionRangeis wrapped intry/catchto defend against future input types that might reject it (e.g.,type="number"doesn't support it).setTimeout(..., 0)to clearjustCommittedruns as a microtask after the next event-loop tick, which is the same tick where the trailing post-compositionendinputevent fires — so the suppression window is tight but reliable across the browsers I tested.🤖 Generated with Claude Code
Summary by CodeRabbit