From 6ef7c4960ec0b45ddfaa0efe85b438c9db498d51 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 30 May 2026 11:26:38 -0700 Subject: [PATCH] fix: guard terminal mobile driver action from root ref (#3552) --- .../terminal-pane/MobileDriverOverlay.tsx | 37 ++++++++++++++----- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/src/renderer/src/components/terminal-pane/MobileDriverOverlay.tsx b/src/renderer/src/components/terminal-pane/MobileDriverOverlay.tsx index 1ae7be990ee..49c762947ee 100644 --- a/src/renderer/src/components/terminal-pane/MobileDriverOverlay.tsx +++ b/src/renderer/src/components/terminal-pane/MobileDriverOverlay.tsx @@ -1,4 +1,4 @@ -import { useEffect, useId, useRef, useState, type ReactElement } from 'react' +import { useCallback, useEffect, useId, useRef, useState, type ReactElement } from 'react' import { Button } from '@/components/ui/button' import { cn } from '@/lib/utils' import type { DriverState } from '@/lib/pane-manager/mobile-driver-state' @@ -33,14 +33,11 @@ export function MobileDriverOverlay({ createMobileDriverOverlayCollapseState(driverClientId) ) const [actionPending, setActionPending] = useState(false) - const mountedRef = useRef(true) + const mountedRef = useRef(false) - useEffect( - () => () => { - mountedRef.current = false - }, - [] - ) + const setOverlayRootRef = useCallback((node: HTMLDivElement | null): void => { + mountedRef.current = node !== null + }, []) const currentCollapseState = getMobileDriverOverlayCollapseState(collapseState, driverClientId) // Why: a new mobile actor must be loud even if the prior driver was collapsed. @@ -77,6 +74,7 @@ export function MobileDriverOverlay({ actionPending={actionPending} onAction={handleAction} tone="held" + rootRef={setOverlayRootRef} rootClassName={rootClassName} /> ) @@ -88,6 +86,7 @@ export function MobileDriverOverlay({ actionPending={actionPending} onAction={handleAction} onExpand={() => setCollapseState(createMobileDriverOverlayCollapseState(driverClientId))} + rootRef={setOverlayRootRef} rootClassName={rootClassName} /> ) @@ -103,6 +102,7 @@ export function MobileDriverOverlay({ onAction={handleAction} onCollapse={() => setCollapseState({ driverClientId, collapsed: true })} tone="driving" + rootRef={setOverlayRootRef} rootClassName={rootClassName} /> ) @@ -117,6 +117,7 @@ type LoudOverlayProps = { onAction: () => void | Promise onCollapse?: () => void tone: 'driving' | 'held' + rootRef?: (node: HTMLDivElement | null) => void rootClassName?: string } @@ -129,12 +130,20 @@ function LoudOverlay({ onAction, onCollapse, tone, + rootRef: outerRootRef, rootClassName }: LoudOverlayProps): ReactElement { const titleId = useId() const bodyId = useId() const rootRef = useRef(null) const actionRef = useRef(null) + const setRootRef = useCallback( + (node: HTMLDivElement | null): void => { + rootRef.current = node + outerRootRef?.(node) + }, + [outerRootRef] + ) // Why: focus the recovery action on mount only when the user isn't already // typing into another input (composer, command palette, settings field). // Unconditional autoFocus yanks focus on every overlay mount, so a phone @@ -150,7 +159,7 @@ function LoudOverlay({ // lock UI must not add a pane-wide scrim or blur over the live stream. return (
void | Promise onExpand: () => void + rootRef?: (node: HTMLDivElement | null) => void rootClassName?: string } -function LockChip({ actionPending, onAction, onExpand, rootClassName }: ChipProps): ReactElement { +function LockChip({ + actionPending, + onAction, + onExpand, + rootRef, + rootClassName +}: ChipProps): ReactElement { return (