fix: guard daemon session async state (#3543)

This commit is contained in:
Neil
2026-05-30 01:05:56 -07:00
committed by GitHub
parent 9872e9989f
commit 706ecd7434
2 changed files with 48 additions and 13 deletions
@@ -1,4 +1,4 @@
import React, { useCallback, useState } from 'react'
import React, { useCallback, useEffect, useRef, useState } from 'react'
import { LoaderCircle } from 'lucide-react'
import { toast } from 'sonner'
import { Button } from '../ui/button'
@@ -39,6 +39,22 @@ export type DaemonActionsApi = {
export function useDaemonActions(callbacks?: DaemonActionCallbacks): DaemonActionsApi {
const [pending, setPending] = useState<PendingConfirm>(null)
const [busyKind, setBusyKind] = useState<DaemonActionKind | null>(null)
const mountedRef = useRef(true)
useEffect(() => {
mountedRef.current = true
return () => {
mountedRef.current = false
}
}, [])
const clearPendingAction = useCallback((): void => {
if (!mountedRef.current) {
return
}
setBusyKind(null)
setPending(null)
}, [])
const runRestart = useCallback(async () => {
setBusyKind('restart')
@@ -54,11 +70,12 @@ export function useDaemonActions(callbacks?: DaemonActionCallbacks): DaemonActio
description: err instanceof Error ? err.message : undefined
})
} finally {
setBusyKind(null)
setPending(null)
callbacks?.onRestartSettled?.()
clearPendingAction()
if (mountedRef.current) {
callbacks?.onRestartSettled?.()
}
}
}, [callbacks])
}, [callbacks, clearPendingAction])
const runKillAll = useCallback(async () => {
setBusyKind('killAll')
@@ -77,16 +94,19 @@ export function useDaemonActions(callbacks?: DaemonActionCallbacks): DaemonActio
toast.error(`${remainingCount} session${remainingCount === 1 ? '' : 's'} refused to exit.`)
}
} catch (err) {
callbacks?.onKillAllError?.()
if (mountedRef.current) {
callbacks?.onKillAllError?.()
}
toast.error('Couldn’t kill sessions.', {
description: err instanceof Error ? err.message : undefined
})
} finally {
setBusyKind(null)
setPending(null)
callbacks?.onKillAllSettled?.()
clearPendingAction()
if (mountedRef.current) {
callbacks?.onKillAllSettled?.()
}
}
}, [callbacks])
}, [callbacks, clearPendingAction])
const runConfirmed = useCallback(() => {
if (pending === 'restart') {
@@ -695,6 +695,7 @@ export function ResourceUsageStatusSegment({
// somewhere stable for keyboard users.
const popoverBodyRef = useRef<HTMLDivElement | null>(null)
const popoverBodyFocusFrameRef = useRef<number | null>(null)
const mountedRef = useRef(true)
const cancelPopoverBodyFocusFrame = useCallback((): void => {
if (popoverBodyFocusFrameRef.current === null) {
@@ -706,6 +707,13 @@ export function ResourceUsageStatusSegment({
useEffect(() => cancelPopoverBodyFocusFrame, [cancelPopoverBodyFocusFrame])
useEffect(() => {
mountedRef.current = true
return () => {
mountedRef.current = false
}
}, [])
const setPopoverBodyNode = useCallback(
(node: HTMLDivElement | null): void => {
// Why: the queued post-kill focus is only valid while the popover body exists.
@@ -719,16 +727,23 @@ export function ResourceUsageStatusSegment({
const refreshSessions = useCallback(async () => {
if (runtimeEnvironmentActive) {
setSessions([])
setSessionsError(false)
if (mountedRef.current) {
setSessions([])
setSessionsError(false)
}
return
}
try {
const result = await window.api.pty.listSessions()
if (!mountedRef.current) {
return
}
setSessions(result)
setSessionsError(false)
} catch {
setSessionsError(true)
if (mountedRef.current) {
setSessionsError(true)
}
}
}, [runtimeEnvironmentActive])