From 5862e82285d3b3f9c6110ee22d87fa156da7fa79 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 30 May 2026 01:57:29 -0700 Subject: [PATCH 001/889] fix: guard cli setup async state (#3562) --- .../FloatingTerminalOrchestrationDialog.tsx | 50 +++++++++++++---- .../FloatingTerminalPanel.tsx | 16 +++++- .../settings/WslCliRegistration.tsx | 53 ++++++++++++++----- 3 files changed, 95 insertions(+), 24 deletions(-) diff --git a/src/renderer/src/components/floating-terminal/FloatingTerminalOrchestrationDialog.tsx b/src/renderer/src/components/floating-terminal/FloatingTerminalOrchestrationDialog.tsx index 6e4d0044a32..4f0002fb601 100644 --- a/src/renderer/src/components/floating-terminal/FloatingTerminalOrchestrationDialog.tsx +++ b/src/renderer/src/components/floating-terminal/FloatingTerminalOrchestrationDialog.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useState } from 'react' +import { useCallback, useEffect, useRef, useState } from 'react' import { Check, Clipboard, Copy, Loader2 } from 'lucide-react' import { toast } from 'sonner' import { Button } from '@/components/ui/button' @@ -40,17 +40,35 @@ export function FloatingTerminalOrchestrationDialog({ const [cliLoading, setCliLoading] = useState(false) const [cliBusy, setCliBusy] = useState(false) const [skillBusy, setSkillBusy] = useState(false) + const mountedRef = useRef(true) + + useEffect(() => { + mountedRef.current = true + return () => { + mountedRef.current = false + } + }, []) + + const setCliStatusIfMounted = useCallback((status: CliInstallStatus | null): void => { + if (mountedRef.current) { + setCliStatus(status) + } + }, []) const refreshCliStatus = useCallback(async (): Promise => { setCliLoading(true) try { - setCliStatus(await window.api.cli.getInstallStatus()) + setCliStatusIfMounted(await window.api.cli.getInstallStatus()) } catch (error) { - toast.error(error instanceof Error ? error.message : 'Failed to load CLI status.') + if (mountedRef.current) { + toast.error(error instanceof Error ? error.message : 'Failed to load CLI status.') + } } finally { - setCliLoading(false) + if (mountedRef.current) { + setCliLoading(false) + } } - }, []) + }, [setCliStatusIfMounted]) useEffect(() => { if (open) { @@ -70,8 +88,11 @@ export function FloatingTerminalOrchestrationDialog({ setCliBusy(true) try { const next = await ensureOrcaCliAvailableForAgentSkillTerminal({ - onStatusChange: setCliStatus + onStatusChange: setCliStatusIfMounted }) + if (!mountedRef.current) { + return + } if (next) { notifyOrchestrationSetupStateChanged() onSetupStateChange() @@ -80,7 +101,9 @@ export function FloatingTerminalOrchestrationDialog({ toast.success('Registered the Orca CLI in PATH.') } } finally { - setCliBusy(false) + if (mountedRef.current) { + setCliBusy(false) + } } } @@ -88,8 +111,11 @@ export function FloatingTerminalOrchestrationDialog({ setSkillBusy(true) try { const nextCliStatus = await ensureOrcaCliAvailableForAgentSkillTerminal({ - onStatusChange: setCliStatus + onStatusChange: setCliStatusIfMounted }) + if (!mountedRef.current) { + return + } localStorage.setItem(ORCHESTRATION_ENABLED_STORAGE_KEY, '1') localStorage.removeItem(ORCHESTRATION_SETUP_DISMISSED_STORAGE_KEY) notifyOrchestrationSetupStateChanged() @@ -112,9 +138,13 @@ export function FloatingTerminalOrchestrationDialog({ onOpenChange(false) } } catch (error) { - toast.error(error instanceof Error ? error.message : 'Failed to copy skill command.') + if (mountedRef.current) { + toast.error(error instanceof Error ? error.message : 'Failed to copy skill command.') + } } finally { - setSkillBusy(false) + if (mountedRef.current) { + setSkillBusy(false) + } } } diff --git a/src/renderer/src/components/floating-terminal/FloatingTerminalPanel.tsx b/src/renderer/src/components/floating-terminal/FloatingTerminalPanel.tsx index 84ca4f83727..a6109500646 100644 --- a/src/renderer/src/components/floating-terminal/FloatingTerminalPanel.tsx +++ b/src/renderer/src/components/floating-terminal/FloatingTerminalPanel.tsx @@ -141,6 +141,7 @@ export function FloatingTerminalPanel({ const panelRef = useRef(null) const shortcutFocusFrameRef = useRef(null) const shortcutFocusTimeoutRef = useRef(null) + const mountedRef = useRef(true) const dragRef = useRef<{ pointerId: number startX: number @@ -344,6 +345,13 @@ export function FloatingTerminalPanel({ handleSaveDialogCancel() }, [handleSaveDialogCancel]) + useEffect(() => { + mountedRef.current = true + return () => { + mountedRef.current = false + } + }, []) + useEffect(() => { if (!open || normalizedInitialBoundsRef.current || typeof window === 'undefined') { return @@ -410,9 +418,13 @@ export function FloatingTerminalPanel({ } try { const status = await window.api.cli.getInstallStatus() - setShowOrchestrationSetup(!isOrcaCliAvailableOnPath(status)) + if (mountedRef.current) { + setShowOrchestrationSetup(!isOrcaCliAvailableOnPath(status)) + } } catch { - setShowOrchestrationSetup(true) + if (mountedRef.current) { + setShowOrchestrationSetup(true) + } } }, []) diff --git a/src/renderer/src/components/settings/WslCliRegistration.tsx b/src/renderer/src/components/settings/WslCliRegistration.tsx index c1d9f1eda47..a05224ecc5c 100644 --- a/src/renderer/src/components/settings/WslCliRegistration.tsx +++ b/src/renderer/src/components/settings/WslCliRegistration.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react' +import { useEffect, useRef, useState } from 'react' import { RefreshCw } from 'lucide-react' import { toast } from 'sonner' import type { CliInstallStatus } from '../../../../shared/cli-install-types' @@ -26,17 +26,32 @@ export function WslCliRegistration({ const [loading, setLoading] = useState(false) const [dialogOpen, setDialogOpen] = useState(false) const [busyAction, setBusyAction] = useState<'install' | 'remove' | null>(null) + const mountedRef = useRef(true) const { wslAvailable } = useWindowsTerminalCapabilities(currentPlatform === 'win32') const showWslCli = currentPlatform === 'win32' && wslAvailable + useEffect(() => { + mountedRef.current = true + return () => { + mountedRef.current = false + } + }, []) + const refreshStatus = async (): Promise => { setLoading(true) try { - setStatus(await window.api.cli.getWslInstallStatus()) + const next = await window.api.cli.getWslInstallStatus() + if (mountedRef.current) { + setStatus(next) + } } catch (error) { - toast.error(error instanceof Error ? error.message : 'Failed to load WSL CLI status.') + if (mountedRef.current) { + toast.error(error instanceof Error ? error.message : 'Failed to load WSL CLI status.') + } } finally { - setLoading(false) + if (mountedRef.current) { + setLoading(false) + } } } @@ -58,15 +73,22 @@ export function WslCliRegistration({ setBusyAction('install') try { const next = await window.api.cli.installWsl() + if (!mountedRef.current) { + return + } setStatus(next) setDialogOpen(false) toast.success(`Registered \`${next.commandName}\` in WSL.`) } catch (error) { - toast.error( - error instanceof Error ? error.message : `Failed to register \`${commandName}\` in WSL.` - ) + if (mountedRef.current) { + toast.error( + error instanceof Error ? error.message : `Failed to register \`${commandName}\` in WSL.` + ) + } } finally { - setBusyAction(null) + if (mountedRef.current) { + setBusyAction(null) + } } } @@ -74,15 +96,22 @@ export function WslCliRegistration({ setBusyAction('remove') try { const next = await window.api.cli.removeWsl() + if (!mountedRef.current) { + return + } setStatus(next) setDialogOpen(false) toast.success(`Removed \`${next.commandName}\` from WSL.`) } catch (error) { - toast.error( - error instanceof Error ? error.message : `Failed to remove \`${commandName}\` from WSL.` - ) + if (mountedRef.current) { + toast.error( + error instanceof Error ? error.message : `Failed to remove \`${commandName}\` from WSL.` + ) + } } finally { - setBusyAction(null) + if (mountedRef.current) { + setBusyAction(null) + } } } From 916624a40d5c46c4732f5bb1897a9f2764bbacdd Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 30 May 2026 01:57:53 -0700 Subject: [PATCH 002/889] perf: guard voice pane permission state without effect (#3563) --- src/renderer/src/components/settings/VoicePane.tsx | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/renderer/src/components/settings/VoicePane.tsx b/src/renderer/src/components/settings/VoicePane.tsx index dd057108c8e..1dd5dc5f0b1 100644 --- a/src/renderer/src/components/settings/VoicePane.tsx +++ b/src/renderer/src/components/settings/VoicePane.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from 'react' +import { useCallback, useEffect, useRef, useState } from 'react' import type { GlobalSettings } from '../../../../shared/types' import { getDefaultVoiceSettings } from '../../../../shared/constants' import type { SpeechModelManifest, SpeechModelState } from '../../../../shared/speech-types' @@ -34,11 +34,10 @@ export function VoicePane({ settings, updateSettings }: VoicePaneProps): React.J const [permissionPending, setPermissionPending] = useState(false) const mountedRef = useRef(true) - useEffect(() => { - mountedRef.current = true - return () => { - mountedRef.current = false - } + const handlePaneRef = useCallback((node: HTMLDivElement | null): void => { + // Why: the microphone permission prompt can resolve after Settings closes; + // the pane ref gives that completion a stale-write guard without an Effect. + mountedRef.current = node !== null }, []) useEffect(() => { @@ -116,7 +115,7 @@ export function VoicePane({ settings, updateSettings }: VoicePaneProps): React.J const selectedIsReady = selectedModelState?.status === 'ready' return ( -
+
From 90b7879f1c0a9dad11bbced05a8b553999a4b533 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 30 May 2026 02:01:50 -0700 Subject: [PATCH 003/889] fix: guard linear connect async state (#3564) --- src/renderer/src/components/TaskPage.tsx | 17 +++++++++++++++-- .../onboarding/IntegrationsStep.tsx | 19 ++++++++++++++++--- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/src/renderer/src/components/TaskPage.tsx b/src/renderer/src/components/TaskPage.tsx index 52f0f922dc0..42905687002 100644 --- a/src/renderer/src/components/TaskPage.tsx +++ b/src/renderer/src/components/TaskPage.tsx @@ -3170,6 +3170,14 @@ export default function TaskPage(): React.JSX.Element { 'idle' ) const [linearConnectError, setLinearConnectError] = useState(null) + const linearConnectMountedRef = useRef(true) + + useEffect(() => { + linearConnectMountedRef.current = true + return () => { + linearConnectMountedRef.current = false + } + }, []) const activeGithubTaskKind = getGitHubTaskKind(activeTaskPreset, appliedTaskSearch) const selectedGitHubRepoExternalLink = useMemo(() => { @@ -4259,6 +4267,9 @@ export default function TaskPage(): React.JSX.Element { setLinearConnectError(null) try { const result = await connectLinear(key) + if (!linearConnectMountedRef.current) { + return + } if (result.ok) { setLinearApiKeyDraft('') setLinearConnectState('idle') @@ -4268,8 +4279,10 @@ export default function TaskPage(): React.JSX.Element { setLinearConnectError(result.error) } } catch (error) { - setLinearConnectState('error') - setLinearConnectError(error instanceof Error ? error.message : 'Connection failed') + if (linearConnectMountedRef.current) { + setLinearConnectState('error') + setLinearConnectError(error instanceof Error ? error.message : 'Connection failed') + } } }, [connectLinear, linearApiKeyDraft]) diff --git a/src/renderer/src/components/onboarding/IntegrationsStep.tsx b/src/renderer/src/components/onboarding/IntegrationsStep.tsx index 322e236953c..8f38b6a675d 100644 --- a/src/renderer/src/components/onboarding/IntegrationsStep.tsx +++ b/src/renderer/src/components/onboarding/IntegrationsStep.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react' +import { useEffect, useRef, useState } from 'react' import { ExternalLink, Github, Loader2, Terminal } from 'lucide-react' import { LinearIcon } from '@/components/icons/LinearIcon' import { Button } from '@/components/ui/button' @@ -123,6 +123,14 @@ export function LinearRow(props: { compact?: boolean } = {}): React.JSX.Element const [apiKeyDraft, setApiKeyDraft] = useState('') const [connectState, setConnectState] = useState<'idle' | 'connecting' | 'error'>('idle') const [connectError, setConnectError] = useState(null) + const mountedRef = useRef(true) + + useEffect(() => { + mountedRef.current = true + return () => { + mountedRef.current = false + } + }, []) const workspaceCount = linearStatus.workspaces?.length ?? (linearStatus.connected ? 1 : 0) @@ -135,6 +143,9 @@ export function LinearRow(props: { compact?: boolean } = {}): React.JSX.Element setConnectError(null) try { const result = await connectLinear(apiKey) + if (!mountedRef.current) { + return + } if (result.ok) { setApiKeyDraft('') setConnectState('idle') @@ -144,8 +155,10 @@ export function LinearRow(props: { compact?: boolean } = {}): React.JSX.Element setConnectState('error') setConnectError(result.error) } catch (error) { - setConnectState('error') - setConnectError(error instanceof Error ? error.message : 'Connection failed') + if (mountedRef.current) { + setConnectState('error') + setConnectError(error instanceof Error ? error.message : 'Connection failed') + } } } From 4c1e17bce5e0f78662497909b902ffedaab7df59 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 30 May 2026 02:03:39 -0700 Subject: [PATCH 004/889] perf: guard Linear comment footer async state without effect (#3565) --- .../src/components/LinearItemDrawer.tsx | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/renderer/src/components/LinearItemDrawer.tsx b/src/renderer/src/components/LinearItemDrawer.tsx index 0e8e99e7585..e8dc2c36d7e 100644 --- a/src/renderer/src/components/LinearItemDrawer.tsx +++ b/src/renderer/src/components/LinearItemDrawer.tsx @@ -955,11 +955,10 @@ export function LinearIssueCommentFooter({ const textareaRef = useRef(null) const mountedRef = useRef(true) - useEffect(() => { - mountedRef.current = true - return () => { - mountedRef.current = false - } + const handleFooterRef = useCallback((node: HTMLDivElement | null): void => { + // Why: comment submission can resolve after the footer unmounts; the root + // ref keeps that completion from writing stale local state without an Effect. + mountedRef.current = node !== null }, []) const autoGrow = useCallback(() => { @@ -1016,7 +1015,10 @@ export function LinearIssueCommentFooter({ if (variant === 'linear-page') { return ( -
+