fix: stop mutating refs during render for React Doctor

Replace render-time ref writes in TasksPane sticky auto-expand and
active project skill runtime identity caching with setState-during-render
so static analysis can pass without changing behavior.
This commit is contained in:
Jinjing
2026-07-30 11:21:28 -07:00
parent 7b8f4881b6
commit 5488255c33
2 changed files with 17 additions and 16 deletions
@@ -1,4 +1,4 @@
import { useRef } from 'react'
import { useState } from 'react'
import { Github, Gitlab } from 'lucide-react'
import type { GlobalSettings, TaskProvider } from '../../../../shared/types'
import {
@@ -103,17 +103,17 @@ export function TasksPane({ settings, updateSettings }: TasksPaneProps): React.J
// Warn only about started-then-stalled setup; untouched providers are the default.
const stalledVisible = getStalledVisibleTaskProviders(TASK_PROVIDERS, readinessByProvider)
// Sticky across rechecks so expanded Linear install terminals are not unmounted.
const previousAutoExpandedRef = useRef<TaskProvider | null>(null)
// Claim during render (not an effect): a layout effect elsewhere can force a
// sync re-render before passive effects flush and collapse the open card.
// useState (not a ref write) keeps render pure for React Doctor.
const [previousAutoExpanded, setPreviousAutoExpanded] = useState<TaskProvider | null>(null)
const autoExpandedProvider = resolveStickyAutoExpandedTaskProvider({
providers: TASK_PROVIDERS,
readinessByProvider,
previousAutoExpanded: previousAutoExpandedRef.current
previousAutoExpanded
})
// Claim during render, not in an effect: a layout effect elsewhere can force a
// sync re-render before passive effects flush, and that render would see an
// unclaimed slot and collapse the open card.
if (autoExpandedProvider !== null && previousAutoExpandedRef.current === null) {
previousAutoExpandedRef.current = autoExpandedProvider
if (autoExpandedProvider !== null && previousAutoExpanded === null) {
setPreviousAutoExpanded(autoExpandedProvider)
}
const toggleProvider = (provider: TaskProvider): void => {
@@ -1,4 +1,4 @@
import { useMemo, useRef } from 'react'
import { useMemo, useState } from 'react'
import { useShallow } from 'zustand/react/shallow'
import type { ProjectExecutionRuntimeResolution } from '../../../shared/project-execution-runtime'
import type { SkillDiscoveryTarget } from '../../../shared/skills'
@@ -86,14 +86,15 @@ export function useActiveProjectSkillRuntime(): ActiveProjectSkillRuntime {
}
}, [currentPlatform, runtimeState, windowsCapabilities])
const stableRef = useRef(resolved)
if (
activeProjectSkillRuntimeIdentity(stableRef.current) !==
activeProjectSkillRuntimeIdentity(resolved)
) {
stableRef.current = resolved
// Content-equal runtimes keep one reference so effect keys do not thrash.
// Adjust during render (not a ref write) when serialized identity changes.
const [stable, setStable] = useState(resolved)
const stableIdentity = activeProjectSkillRuntimeIdentity(stable)
const resolvedIdentity = activeProjectSkillRuntimeIdentity(resolved)
if (stableIdentity !== resolvedIdentity) {
setStable(resolved)
}
return stableRef.current
return stableIdentity === resolvedIdentity ? stable : resolved
}
function getCurrentPlatform(): NodeJS.Platform {