fix: resolve keyboard freeze via sync render path and stable useAppState selectors (#266)
* fix: resolve keyboard freeze via sync render path and stable useAppState selectors Two compounding React 19 defects caused keyboard input to freeze after MCP notifications or rapid state updates: Defect 2 (ink.tsx): The render() path used async updateContainer, which batches updates across scheduler ticks. Keyboard events dispatched mid-render drained faster than React processed them, causing input to appear frozen. Fixed by switching to updateContainerSync + flushSyncWork (same pattern already used in the unmount path). Defect 4 (AppState.tsx): useAppState and useAppStateMaybeOutsideOfProvider used React Compiler _c cache invalidation tied to selector identity. Inline arrow selectors (new reference each render) invalidated the cache every cycle, producing a new `get` function. useSyncExternalStore treats a new `get` as a tearing signal, re-syncing state and re-rendering — causing a loop that starved the input handler. Fixed with useRef + useCallback(fn, []) to give useSyncExternalStore a permanently stable snapshot reference. Note: AppState.tsx is React Compiler output. The _c bypass for these two hooks is intentional — compiler cache invalidation on inline selectors is the root cause of the tearing loop. All 200 tests pass. Build and smoke test verified. Closes #77, #220, #228 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: update selector refs during render instead of useLayoutEffect Addresses review feedback on PR #266. The previous useLayoutEffect approach updated selectorRef.current after the render phase, meaning a changed selector (e.g. s => s.tasks[attachment.taskId] when taskId changes) would still read stale data during the render it changed in. Fix: assign selectorRef.current and storeRef.current directly during render before useSyncExternalStore calls get(). Ref mutation during render is safe here — it's synchronous and happens before the snapshot is read. get() identity stays stable via useCallback(fn, []) so useSyncExternalStore never sees a new subscription function and won't trigger re-render loops. This is the standard pattern used by zustand and jotai for selector stability. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user