Add IME cursor positioning and Synchronized Update Mode#866
Conversation
Enable CJK IME composition characters to appear at the correct cursor position by exposing a useCursor() hook and wrapping terminal output with Synchronized Update Mode (CSI ? 2026). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When cursor was positioned mid-screen via setCursorPosition, clear() would erase from the cursor position instead of the bottom, leaving stale lines on screen. Now clear() returns the cursor to the bottom before erasing, and resets cursor state afterward. Also fix misleading test name for cursorUp assertion. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move 5 side-effect-free cursor helpers (buildCursorSuffix, buildReturnToBottom, cursorPositionChanged, buildCursorOnlySequence, buildReturnToBottomPrefix) from log-update.ts into a dedicated cursor-helpers.ts module with 17 unit tests. This eliminates duplicated logic between createStandard and createIncremental. Also fix cursor-ime example import path (../src → ../../src). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move Synchronized Update wrapping from writeSynchronized() utility to onRender/writeToStdout/writeToStderr entry/exit points. This reduces the number of BSU/ESU pairs from N writes to 1 per cycle. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move Synchronized Update wrapping into throttledLog callback so that trailing throttle invocations are properly wrapped with BSU/ESU at actual write time, preventing tmux/Zellij from reading intermediate cursor positions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add cursorDirty flag so log-update only uses cursor position when setCursorPosition was actively called since the last render. This prevents stale cursor positions from persisting after a component using useCursor unmounts. Also add useEffect cleanup in useCursor as a secondary safety net, update write-synchronized tests to match current API, and add BSU/ESU throttle integration test. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move cursor position propagation from render phase to commit phase using useInsertionEffect, which runs before resetAfterCommit and does not execute for abandoned renders. Also expose isCursorDirty() from log-update so onRender can trigger throttledLog even when rendered output is unchanged but cursor position changed. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…Terminal Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…mports - Rename BSU/ESU constants to bsu/esu (naming-convention) - Merge duplicate React imports in cursor.tsx - Capitalize comments starting with lowercase - Fix prettier formatting and use .includes()/.has() over .some() - Add eslint-disable for unavoidable unsafe return/call in test stubs - Fix sync() to write cursor suffix when cursor is dirty Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Can you fix the merge conflicts? |
|
|
Addressed all review items and included merge-conflict resolution.
Validation:
|
|
|
Thanks for the catch. I rechecked the exact files/lines you listed on the latest PR head (
I ran:
Result: pass (no lint/prettier errors). I also re-ran full checks on the same head (including in a clean clone), and they pass as well:
Could you please re-run checks on the latest head and let me know if you still see a failure? |
Removed redundant line break in the documentation comment.
Moved string-width usage and example reference to the 'x' section for clarity.
|
Thanks for the thorough review and merge! |
Summary
useCursor()hook so apps can position the terminal cursor after each render — enabling CJK IME composition characters to appear at the correct locationCSI ? 2026 h/l) to prevent terminal multiplexers (tmux, Zellij) from reading intermediate cursor positions during renderingInspired by #846 (by @jedipunkz), which tackles the same problem. This PR takes a different implementation approach:
buildCursorSuffix/buildReturnToBottom) — cursor logic reused by bothcreateStandardandcreateIncrementalrenderers instead of being duplicatedDesign note:
setCursorPositionand concurrent mode safetysetCursorPositionis called in the component render body:Internally, the call only stores the position in a local ref — no external state is mutated during render. The actual propagation to
log-updatehappens in auseInsertionEffect, which:resetAfterCommit→onRender)This prevents cursor state from leaking across render boundaries while avoiding the 1-frame delay that
useEffectwould introduce.Design note: BSU/ESU and throttled writes
onRenderhas multiple output paths with different timing characteristics:onRender, so BSU/ESU wraps the entire blockthis.throttledLog): the trailing throttle invocation fires afteronRenderreturns, so wrapping BSU/ESU at theonRenderlevel would leave the trailing write unprotected. Instead, BSU/ESU is wrapped inside the throttle callback itself, ensuring every actualstream.write— whether leading or trailing — is synchronizedChanged files
New (4):
src/write-synchronized.tsshouldSynchronizeguardsrc/components/CursorContext.tssrc/hooks/use-cursor.tsuseCursor()hook — stores position in ref during render, propagates viauseInsertionEffectduring commitexamples/cursor-ime/cursor-ime.tsxModified (5):
src/log-update.tscursorDirtyflag,isCursorDirty()accessorsrc/components/App.tsxCursorContext, remove redundantcliCursor.hidesrc/ink.tsxsetCursorPosition; callthrottledLogwhen cursor is dirty even if output unchangedsrc/index.tsuseCursor,CursorPositionsrc/reconciler.tsTests (6):
test/log-update.tsxtest/cursor.tsxtest/write-synchronized.tsxtest/render.tsxclear outputassertion + BSU/ESU throttle integration testtest/cursor-helpers.tsxTest plan
npx xopasses (0 errors)npx tsc --noEmitpassesnpx ava— same 6 pre-existing failures asmaster, no new failuresnpx tsx examples/cursor-ime/cursor-ime.tsx— Korean IME cursor tracks inputReferences
🤖 Generated with Claude Code