From 5ef97d581cc382c49c775a316f1fbbae4cd4ad82 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 30 May 2026 01:36:49 -0700 Subject: [PATCH] fix: guard status bar account async state (#3555) --- .../src/components/status-bar/StatusBar.tsx | 70 +++++++++++++++---- 1 file changed, 58 insertions(+), 12 deletions(-) diff --git a/src/renderer/src/components/status-bar/StatusBar.tsx b/src/renderer/src/components/status-bar/StatusBar.tsx index a7b4cce58b6..96bcb24daae 100644 --- a/src/renderer/src/components/status-bar/StatusBar.tsx +++ b/src/renderer/src/components/status-bar/StatusBar.tsx @@ -588,6 +588,7 @@ function ClaudeSwitcherMenu({ activeAccountIdsByRuntime: { host: null, wsl: {} } }) const [isSwitching, setIsSwitching] = useState(false) + const mountedRef = useRef(true) const openSettingsPage = useAppStore((s) => s.openSettingsPage) const openSettingsTarget = useAppStore((s) => s.openSettingsTarget) const fetchSettings = useAppStore((s) => s.fetchSettings) @@ -609,9 +610,18 @@ function ClaudeSwitcherMenu({ }) const accountState = getClaudeStatusAccountsFromSettings(settings) ?? accounts + useEffect(() => { + mountedRef.current = true + return () => { + mountedRef.current = false + } + }, []) + const loadAccounts = useCallback(async () => { const next = await window.api.claudeAccounts.list() - setAccounts(next) + if (mountedRef.current) { + setAccounts(next) + } }, []) useEffect(() => { @@ -648,13 +658,19 @@ function ClaudeSwitcherMenu({ wslDistro: target.wslDistro }) recordFeatureInteraction('claude-account-switching') - setAccounts(next) + if (mountedRef.current) { + setAccounts(next) + } await fetchSettings() - setAccountsExpanded(false) + if (mountedRef.current) { + setAccountsExpanded(false) + } } catch (error) { console.error('Failed to switch Claude account from status bar:', error) } finally { - setIsSwitching(false) + if (mountedRef.current) { + setIsSwitching(false) + } } } @@ -1046,6 +1062,7 @@ function CodexSwitcherMenu({ }) const [isSwitching, setIsSwitching] = useState(false) const [reauthenticatingAccountId, setReauthenticatingAccountId] = useState(null) + const mountedRef = useRef(true) const accountsExpandedRef = useRef(accountsExpanded) // Why: Radix item selection is separate from the nested button click, so // propagation stops alone do not prevent the row switch action. @@ -1079,7 +1096,16 @@ function CodexSwitcherMenu({ const loadAccounts = useCallback(async () => { const next = await window.api.codexAccounts.list() - setAccounts(next) + if (mountedRef.current) { + setAccounts(next) + } + }, []) + + useEffect(() => { + mountedRef.current = true + return () => { + mountedRef.current = false + } }, []) useEffect(() => { @@ -1112,7 +1138,9 @@ function CodexSwitcherMenu({ wslDistro: target.wslDistro }) recordFeatureInteraction('codex-account-switching') - setAccounts(next) + if (mountedRef.current) { + setAccounts(next) + } await fetchSettings() const nextActiveAccountId = getCodexStatusActiveId(next, target) if (previousActiveAccountId !== nextActiveAccountId) { @@ -1124,12 +1152,16 @@ function CodexSwitcherMenu({ // for live Codex terminals. Keeping the switcher open and collapsing // back to the summary row lets the follow-up "restart open tabs" // prompt appear in the same flow instead of feeling detached. - setAccountsExpanded(false) + if (mountedRef.current) { + setAccountsExpanded(false) + } } } catch (error) { console.error('Failed to switch Codex account from status bar:', error) } finally { - setIsSwitching(false) + if (mountedRef.current) { + setIsSwitching(false) + } } } @@ -1141,15 +1173,19 @@ function CodexSwitcherMenu({ try { const next = await window.api.codexAccounts.reauthenticate({ accountId }) recordFeatureInteraction('codex-account-switching') - setAccounts(next) + if (mountedRef.current) { + setAccounts(next) + } await fetchSettings() - if (accountsExpandedRef.current) { + if (mountedRef.current && accountsExpandedRef.current) { await fetchInactiveCodexAccountUsage() } } catch (error) { console.error('Failed to re-authenticate Codex account from status bar:', error) } finally { - setReauthenticatingAccountId(null) + if (mountedRef.current) { + setReauthenticatingAccountId(null) + } } } @@ -1432,6 +1468,7 @@ function StatusBarInner({ floatingTerminalOpen }: StatusBarProps): React.JSX.Ele const petEnabled = useAppStore((s) => s.settings?.experimentalPet === true) const toggleStatusBarItem = useAppStore((s) => s.toggleStatusBarItem) const containerRef = useRef(null) + const mountedRef = useRef(true) const [isRefreshing, setIsRefreshing] = useState(false) const [menuOpen, setMenuOpen] = useState(false) const [menuPoint, setMenuPoint] = useState({ x: 0, y: 0 }) @@ -1439,6 +1476,13 @@ function StatusBarInner({ floatingTerminalOpen }: StatusBarProps): React.JSX.Ele const [containerWidth, setContainerWidth] = useState(900) const resizeObserverRef = useRef(null) + useEffect(() => { + mountedRef.current = true + return () => { + mountedRef.current = false + } + }, []) + useEffect(() => { const closeMenu = (): void => setMenuOpen(false) window.addEventListener(CLOSE_ALL_CONTEXT_MENUS_EVENT, closeMenu) @@ -1482,7 +1526,9 @@ function StatusBarInner({ floatingTerminalOpen }: StatusBarProps): React.JSX.Ele // appears (and a removed CLI's bar hides) without restarting Orca. await Promise.all([refreshRateLimits(), refreshDetectedAgents()]) } finally { - setIsRefreshing(false) + if (mountedRef.current) { + setIsRefreshing(false) + } } }, [isRefreshing, refreshRateLimits, refreshDetectedAgents])