fix: guard terminal mobile driver action from root ref (#3552)

This commit is contained in:
Neil
2026-05-30 14:26:38 -04:00
committed by GitHub
parent e88a0f477d
commit 6ef7c4960e
@@ -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<void>
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<HTMLDivElement>(null)
const actionRef = useRef<HTMLButtonElement>(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 (
<div
ref={rootRef}
ref={setRootRef}
role="dialog"
aria-live="assertive"
aria-labelledby={titleId}
@@ -203,12 +212,20 @@ type ChipProps = {
actionPending: boolean
onAction: () => void | Promise<void>
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 (
<div
ref={rootRef}
className={cn(
'absolute right-2 top-2 z-50 flex items-center gap-1.5 rounded-full border border-border bg-card px-2 py-1 text-xs font-medium text-card-foreground shadow-xs',
rootClassName