diff --git a/src/components/Editor/SourceEditor.tsx b/src/components/Editor/SourceEditor.tsx index 55e0908bb..144e48518 100644 --- a/src/components/Editor/SourceEditor.tsx +++ b/src/components/Editor/SourceEditor.tsx @@ -79,14 +79,14 @@ export function SourceEditor({ hidden = false, readOnly = false }: SourceEditorP const setCursorInfoRef = useRef(setCursorInfo); const setSelectedTextRef = useRef(setSelectedText); const cursorInfoRef = useRef(cursorInfo); - // Sync "latest value" refs after commit (read only from the CodeMirror listener/effects) — concurrent-safe (#1063). - useEffect(() => { - hiddenRef.current = hidden; - setContentRef.current = setContent; - setCursorInfoRef.current = setCursorInfo; - setSelectedTextRef.current = setSelectedText; - cursorInfoRef.current = cursorInfo; - }); + // Latest-value refs synced during render: read by CodeMirror's update listener, a delayed focus/restore setTimeout, and an interval poll — all of which can fire before a passive effect would flush, so they need pre-commit freshness (#1063). + /* eslint-disable react-hooks/refs */ + hiddenRef.current = hidden; + setContentRef.current = setContent; + setCursorInfoRef.current = setCursorInfo; + setSelectedTextRef.current = setSelectedText; + cursorInfoRef.current = cursorInfo; + /* eslint-enable react-hooks/refs */ // Use editor store for global settings const wordWrap = useUIStore((state) => state.wordWrap); diff --git a/src/components/Editor/TiptapEditor.tsx b/src/components/Editor/TiptapEditor.tsx index 49f528254..23fecad67 100644 --- a/src/components/Editor/TiptapEditor.tsx +++ b/src/components/Editor/TiptapEditor.tsx @@ -180,15 +180,15 @@ export function TiptapEditorInner({ hidden = false, readOnly = false, preview = const contentRef = useRef(content); const editorRef = useRef(null); const flushToStoreRef = useRef<((editor: TiptapEditor) => void) | null>(null); - // Sync "latest value" refs after commit (read only from callbacks/effects) — concurrent-safe (#1063). - useEffect(() => { - cursorInfoRef.current = cursorInfo; - preserveLineBreaksRef.current = preserveLineBreaks; - hardBreakStyleOnSaveRef.current = hardBreakStyleOnSave; - hiddenRef.current = hidden; - previewRef.current = preview; - contentRef.current = content; - }); + // Latest-value refs synced during render: a deferred init parse (setTimeout) + the unmount-flush read these and need the latest committed value before effects run (#1063). + /* eslint-disable react-hooks/refs */ + cursorInfoRef.current = cursorInfo; + preserveLineBreaksRef.current = preserveLineBreaks; + hardBreakStyleOnSaveRef.current = hardBreakStyleOnSave; + hiddenRef.current = hidden; + previewRef.current = preview; + contentRef.current = content; + /* eslint-enable react-hooks/refs */ const extensions = useMemo( () => createTiptapExtensions({ tabId: activeTabId, lintEnabled }), diff --git a/src/hooks/useGenieInvocation.ts b/src/hooks/useGenieInvocation.ts index 2662e47c3..ea5604291 100644 --- a/src/hooks/useGenieInvocation.ts +++ b/src/hooks/useGenieInvocation.ts @@ -487,7 +487,7 @@ export function useGenieInvocation() { [runGenie] ); - // eslint-disable-next-line react-hooks/refs -- latest-value ref read only by the async MCP bridge listener (#1063) + // eslint-disable-next-line react-hooks/refs -- render-synced so the synchronous MCP bridge handler sees the latest invokeGenie before passive effects run (#1063) invokeGenieRef.current = invokeGenie; const invokeFreeform = useCallback( diff --git a/src/hooks/useTabDragOut.ts b/src/hooks/useTabDragOut.ts index c6301a4e0..cb511e0b8 100644 --- a/src/hooks/useTabDragOut.ts +++ b/src/hooks/useTabDragOut.ts @@ -14,7 +14,7 @@ * @module hooks/useTabDragOut */ -import { useCallback, useEffect, useRef, useState, type PointerEvent as ReactPointerEvent, type RefObject } from "react"; +import { useCallback, useRef, useState, type PointerEvent as ReactPointerEvent, type RefObject } from "react"; /** Vertical distance (px) outside the tab bar to trigger drag-out. */ const DRAG_OUT_THRESHOLD = 40; @@ -150,17 +150,17 @@ export function useTabDragOut({ tabBarRef, onDragOut, onReorder, onDragMove }: U } }, []); - // Latest-value refs read only from the document drag listeners; synced after commit (#1063). + // Latest-value refs read by synchronous document pointer listeners during a drag, so they must be render-synced (fresh before a pointer event fires), not passive (#1063). const onDragOutRef = useRef(onDragOut); const onReorderRef = useRef(onReorder); const onDragMoveRef = useRef(onDragMove); const stableBarRef = useRef(tabBarRef); - useEffect(() => { - onDragOutRef.current = onDragOut; - onReorderRef.current = onReorder; - onDragMoveRef.current = onDragMove; - stableBarRef.current = tabBarRef; - }); + /* eslint-disable react-hooks/refs */ + onDragOutRef.current = onDragOut; + onReorderRef.current = onReorder; + onDragMoveRef.current = onDragMove; + stableBarRef.current = tabBarRef; + /* eslint-enable react-hooks/refs */ // Detach document listeners and reset state /* v8 ignore start -- @preserve reason: cleanupRef empty-function initializer and reset are uncovered; drag cleanup not triggered in unit tests */