diff --git a/src/renderer/src/components/automations/use-automations-page-local-state.ts b/src/renderer/src/components/automations/use-automations-page-local-state.ts index f9a64c8dde7..25f4fa30444 100644 --- a/src/renderer/src/components/automations/use-automations-page-local-state.ts +++ b/src/renderer/src/components/automations/use-automations-page-local-state.ts @@ -138,6 +138,8 @@ export function useAutomationsPageLocalState(store: AutomationsPageStoreState) { scheduleWarning: null }) const draftRef = useRef(draft) + // Keep async editor actions on the latest draft before they can run. + // react-doctor-disable-next-line react-doctor/no-ref-current-in-render draftRef.current = draft return { diff --git a/src/renderer/src/components/floating-terminal/use-floating-terminal-panel-local-state.ts b/src/renderer/src/components/floating-terminal/use-floating-terminal-panel-local-state.ts index e4b6cd01405..f910b6432f2 100644 --- a/src/renderer/src/components/floating-terminal/use-floating-terminal-panel-local-state.ts +++ b/src/renderer/src/components/floating-terminal/use-floating-terminal-panel-local-state.ts @@ -54,6 +54,8 @@ export function useFloatingTerminalPanelLocalState() { ) const doubleTapDetectorRef = useRef(null) if (!doubleTapDetectorRef.current) { + // The detector must exist before event handlers are published. + // react-doctor-disable-next-line react-doctor/no-ref-current-in-render doubleTapDetectorRef.current = new ModifierDoubleTapDetector() } const shortcutFocusFrameRef = useRef(null) diff --git a/src/renderer/src/components/status-bar/use-workspace-space-manager-projection.ts b/src/renderer/src/components/status-bar/use-workspace-space-manager-projection.ts index 4c3a9dfede2..02363fe33bb 100644 --- a/src/renderer/src/components/status-bar/use-workspace-space-manager-projection.ts +++ b/src/renderer/src/components/status-bar/use-workspace-space-manager-projection.ts @@ -89,12 +89,15 @@ export function useWorkspaceSpaceManagerProjection(args: { // Why: these ids are local UI state derived from the latest scan rows. Repair // them before commit so stale selections cannot flash after a scan changes. if (inspectedWorktreeId !== nextInspectedWorktreeId) { + // react-doctor-disable-next-line react-doctor/no-prop-callback-in-render setInspectedWorktreeId(nextInspectedWorktreeId) } if (nextSelectedIds !== selectedIds) { + // react-doctor-disable-next-line react-doctor/no-prop-callback-in-render setSelectedIds(nextSelectedIds) } if (treemapZoomWorktreeId !== nextTreemapZoomWorktreeId) { + // react-doctor-disable-next-line react-doctor/no-prop-callback-in-render setTreemapZoomWorktreeId(nextTreemapZoomWorktreeId) } diff --git a/src/renderer/src/components/terminal-pane/use-terminal-pane-foundation.ts b/src/renderer/src/components/terminal-pane/use-terminal-pane-foundation.ts index 1f75a595807..dedd69cbd41 100644 --- a/src/renderer/src/components/terminal-pane/use-terminal-pane-foundation.ts +++ b/src/renderer/src/components/terminal-pane/use-terminal-pane-foundation.ts @@ -57,9 +57,12 @@ export function useTerminalPaneFoundation( const panePtyBindingsRef = useRef>(new Map()) const replayingPanesRef = useRef>(new Map()) const isActiveRef = useRef(isActive) + // Event/PTY callbacks need current visibility before passive effects run. + // react-doctor-disable-next-line react-doctor/no-ref-current-in-render isActiveRef.current = isActive const isRendererVisible = isVisible && isWorktreeActive const isVisibleRef = useRef(isRendererVisible) + // react-doctor-disable-next-line react-doctor/no-ref-current-in-render isVisibleRef.current = isRendererVisible const { nativeChatTranscriptIsLocalReadable, @@ -91,6 +94,8 @@ export function useTerminalPaneFoundation( }, []) const [searchOpen, setSearchOpen] = useState(false) const searchOpenRef = useRef(false) + // Keyboard callbacks must observe current search state immediately. + // react-doctor-disable-next-line react-doctor/no-ref-current-in-render searchOpenRef.current = searchOpen const searchStateRef = useRef({ query: '', caseSensitive: false, regex: false }) const [pendingCloseConfirmation, setPendingCloseConfirmation] = useState<{ diff --git a/src/renderer/src/components/terminal-pane/use-terminal-pane-startup-actions.ts b/src/renderer/src/components/terminal-pane/use-terminal-pane-startup-actions.ts index d77a074b705..e97e1323dcc 100644 --- a/src/renderer/src/components/terminal-pane/use-terminal-pane-startup-actions.ts +++ b/src/renderer/src/components/terminal-pane/use-terminal-pane-startup-actions.ts @@ -158,6 +158,8 @@ export function useTerminalPaneStartupActions(controller: TerminalPaneStoreContr }, [issueCommandSplit, tabId, consumeTabIssueCommandSplit]) const settingsRef = useRef(settings) + // Startup callbacks can run before the next effect commit. + // react-doctor-disable-next-line react-doctor/no-ref-current-in-render settingsRef.current = settings const openLinksInAppPreferencePromiseRef = useRef | null>(null) const requestOpenLinksInAppPreference = useCallback( @@ -192,8 +194,12 @@ export function useTerminalPaneStartupActions(controller: TerminalPaneStoreContr ) const effectiveMacOptionAsAlt = useEffectiveMacOptionAsAlt(settings?.terminalMacOptionAsAlt) const macOptionAsAltRef = useRef(effectiveMacOptionAsAlt) + // Keyboard listeners need the current preference without a render lag. + // react-doctor-disable-next-line react-doctor/no-ref-current-in-render macOptionAsAltRef.current = effectiveMacOptionAsAlt const onPtyExitRef = useRef(onPtyExit) + // PTY teardown may call this ref before passive effects flush. + // react-doctor-disable-next-line react-doctor/no-ref-current-in-render onPtyExitRef.current = onPtyExit const systemPrefersDark = useSystemPrefersDark() const dispatchNotification = useNotificationDispatch(worktreeId) diff --git a/src/renderer/src/components/terminal-pane/use-terminal-pane-title-state.ts b/src/renderer/src/components/terminal-pane/use-terminal-pane-title-state.ts index f29df113d87..dc36176738b 100644 --- a/src/renderer/src/components/terminal-pane/use-terminal-pane-title-state.ts +++ b/src/renderer/src/components/terminal-pane/use-terminal-pane-title-state.ts @@ -20,6 +20,8 @@ export function useTerminalPaneTitleState(controller: TerminalPaneFoundation) { } = controller const [paneTitles, setPaneTitles] = useState>({}) const paneTitlesRef = useRef>({}) + // Rename handlers read this ref synchronously during the render that changes it. + // react-doctor-disable-next-line react-doctor/no-ref-current-in-render paneTitlesRef.current = paneTitles const removedTitleLeafIdsRef = useRef>(new Set()) const clearedScrollbackLeafIdsRef = useRef>(new Set()) diff --git a/src/renderer/src/components/use-task-page-github-list-state.ts b/src/renderer/src/components/use-task-page-github-list-state.ts index c5a0e71cf92..7a5e6b31253 100644 --- a/src/renderer/src/components/use-task-page-github-list-state.ts +++ b/src/renderer/src/components/use-task-page-github-list-state.ts @@ -89,7 +89,10 @@ export function useTaskPageGitHubListState(model: TaskPageProviderStateModel) { const [currentPage, setCurrentPage] = useState(0) const pagesRef = useRef(pages) const currentPageRef = useRef(currentPage) + // Fetch callbacks need the latest paging window immediately after render. + // react-doctor-disable-next-line react-doctor/no-ref-current-in-render pagesRef.current = pages + // react-doctor-disable-next-line react-doctor/no-ref-current-in-render currentPageRef.current = currentPage const githubResumeConsumedRef = useRef(false) const githubResumeContextRef = useRef('') diff --git a/src/renderer/src/components/use-task-page-github-quiet-refresh.ts b/src/renderer/src/components/use-task-page-github-quiet-refresh.ts index 99844495fec..50267617a13 100644 --- a/src/renderer/src/components/use-task-page-github-quiet-refresh.ts +++ b/src/renderer/src/components/use-task-page-github-quiet-refresh.ts @@ -17,6 +17,8 @@ export function useTaskPageGitHubQuietRefreshPrelude(model: TaskPageGitHubLandin queryKey: githubWorkItemMutationQueryKey, generation: 0 }) + // Revalidation may run before a passive effect observes a new key. + // react-doctor-disable-next-line react-doctor/no-ref-current-in-render quietRevalidateScopeRef.current = advanceTaskPageQuietRevalidateScope( quietRevalidateScopeRef.current, githubWorkItemMutationQueryKey diff --git a/src/renderer/src/components/use-task-page-store-bindings.ts b/src/renderer/src/components/use-task-page-store-bindings.ts index 7c4004cd8af..ab46452d3ba 100644 --- a/src/renderer/src/components/use-task-page-store-bindings.ts +++ b/src/renderer/src/components/use-task-page-store-bindings.ts @@ -71,6 +71,8 @@ export function useTaskPageStoreBindings() { const checkJiraConnection = useAppStore((s) => s.checkJiraConnection) const providerRuntimeContextKey = getProviderRuntimeContextKey(settings) const providerRuntimeContextKeyRef = useRef(providerRuntimeContextKey) + // Submit handlers must fence against the current provider context immediately. + // react-doctor-disable-next-line react-doctor/no-ref-current-in-render providerRuntimeContextKeyRef.current = providerRuntimeContextKey const linearStatusCurrent = linearStatusContextKey === providerRuntimeContextKey const jiraStatusCurrent = jiraStatusContextKey === providerRuntimeContextKey diff --git a/src/renderer/src/components/use-worktree-jump-palette-local-state.ts b/src/renderer/src/components/use-worktree-jump-palette-local-state.ts index f854a53b511..5bfd941c8fd 100644 --- a/src/renderer/src/components/use-worktree-jump-palette-local-state.ts +++ b/src/renderer/src/components/use-worktree-jump-palette-local-state.ts @@ -1,9 +1,6 @@ import { useDeferredValue, useMemo, useRef, useState } from 'react' import type { WorktreePaletteRequestGuard } from '@/lib/worktree-palette-create-action' -import { - EMPTY_PALETTE_FILTER, - type PaletteFilterState -} from '@/components/cmd-j/palette-filter' +import { EMPTY_PALETTE_FILTER, type PaletteFilterState } from '@/components/cmd-j/palette-filter' import { parseCmdJTaskSourceUrl } from '@/lib/worktree-palette-task-url-match' import { getWorktreePaletteCreateActionState } from '@/lib/worktree-palette-create-action' import type { CmdJActiveGroupSnapshot } from '@/components/cmd-j/quick-action-context' @@ -20,6 +17,8 @@ export function useWorktreeJumpPaletteLocalState({ const [query, setQuery] = useState('') const deferredQuery = useDeferredValue(query) const liveQueryRef = useRef(query) + // Keyboard handlers must see the current query before effects flush. + // react-doctor-disable-next-line react-doctor/no-ref-current-in-render liveQueryRef.current = query const taskSourceUrl = useMemo(() => parseCmdJTaskSourceUrl(query), [query]) const paletteSearchQuery = taskSourceUrl ? query.trim() : deferredQuery.trim()