From 8ee7610806c574b8acfa3f1bb505d588d8f50a07 Mon Sep 17 00:00:00 2001 From: Charles Vien Date: Tue, 7 Apr 2026 19:20:16 -0700 Subject: [PATCH 1/3] Prevent permission request from stealing focus across cells --- .../action-selector/useActionSelectorState.ts | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/apps/code/src/renderer/components/action-selector/useActionSelectorState.ts b/apps/code/src/renderer/components/action-selector/useActionSelectorState.ts index 811cb117b..ee8b4d90b 100644 --- a/apps/code/src/renderer/components/action-selector/useActionSelectorState.ts +++ b/apps/code/src/renderer/components/action-selector/useActionSelectorState.ts @@ -12,6 +12,17 @@ function needsCustomInput(option: SelectorOption): boolean { return option.customInput === true || isOtherOption(option.id); } +function isUserInInteractiveElement(): boolean { + const el = document.activeElement; + return ( + el instanceof HTMLElement && + (el.tagName === "INPUT" || + el.tagName === "TEXTAREA" || + el.tagName === "SELECT" || + el.getAttribute("contenteditable") === "true") + ); +} + interface UseActionSelectorStateProps { options: SelectorOption[]; multiSelect: boolean; @@ -76,7 +87,9 @@ export function useActionSelectorState({ isEditing && selectedOption && needsCustomInput(selectedOption); useEffect(() => { - containerRef.current?.focus(); + if (!isUserInInteractiveElement()) { + containerRef.current?.focus(); + } }, []); useEffect(() => { @@ -107,7 +120,7 @@ export function useActionSelectorState({ }, [activeStep, checkedOptions, customInput, onStepAnswer]); const restoreStepAnswer = useCallback( - (step: number) => { + (step: number, { autoFocus = true }: { autoFocus?: boolean } = {}) => { const saved = stepAnswers.get(step); if (saved) { setCheckedOptions(new Set(saved.selectedIds)); @@ -121,13 +134,17 @@ export function useActionSelectorState({ } setSelectedIndex(0); setIsEditing(false); - containerRef.current?.focus(); + if (autoFocus) { + containerRef.current?.focus(); + } }, [initialSelections, stepAnswers], ); useEffect(() => { - restoreStepAnswer(activeStep); + restoreStepAnswer(activeStep, { + autoFocus: !isUserInInteractiveElement(), + }); }, [activeStep, restoreStepAnswer]); useEffect(() => { From 537154892e5692596b8ff9d366aa45b840827005 Mon Sep 17 00:00:00 2001 From: Charles Vien Date: Tue, 7 Apr 2026 20:59:52 -0700 Subject: [PATCH 2/3] Scope focus-steal guard to different grid cells --- .../action-selector/useActionSelectorState.ts | 30 ++++++++++++------- .../components/CommandCenterGrid.tsx | 1 + 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/apps/code/src/renderer/components/action-selector/useActionSelectorState.ts b/apps/code/src/renderer/components/action-selector/useActionSelectorState.ts index ee8b4d90b..d78a366fc 100644 --- a/apps/code/src/renderer/components/action-selector/useActionSelectorState.ts +++ b/apps/code/src/renderer/components/action-selector/useActionSelectorState.ts @@ -1,3 +1,4 @@ +import type React from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { filterOtherOptions, @@ -12,15 +13,24 @@ function needsCustomInput(option: SelectorOption): boolean { return option.customInput === true || isOtherOption(option.id); } -function isUserInInteractiveElement(): boolean { +function isInteractiveElementInDifferentCell( + containerRef: React.RefObject, +): boolean { const el = document.activeElement; - return ( - el instanceof HTMLElement && - (el.tagName === "INPUT" || - el.tagName === "TEXTAREA" || - el.tagName === "SELECT" || - el.getAttribute("contenteditable") === "true") - ); + if (!(el instanceof HTMLElement)) return false; + + const isInteractive = + el.tagName === "INPUT" || + el.tagName === "TEXTAREA" || + el.tagName === "SELECT" || + el.getAttribute("contenteditable") === "true"; + if (!isInteractive) return false; + + const activeCell = el.closest("[data-grid-cell]"); + const ownCell = containerRef.current?.closest("[data-grid-cell]"); + if (!activeCell || !ownCell) return true; + + return activeCell !== ownCell; } interface UseActionSelectorStateProps { @@ -87,7 +97,7 @@ export function useActionSelectorState({ isEditing && selectedOption && needsCustomInput(selectedOption); useEffect(() => { - if (!isUserInInteractiveElement()) { + if (!isInteractiveElementInDifferentCell(containerRef)) { containerRef.current?.focus(); } }, []); @@ -143,7 +153,7 @@ export function useActionSelectorState({ useEffect(() => { restoreStepAnswer(activeStep, { - autoFocus: !isUserInInteractiveElement(), + autoFocus: !isInteractiveElementInDifferentCell(containerRef), }); }, [activeStep, restoreStepAnswer]); diff --git a/apps/code/src/renderer/features/command-center/components/CommandCenterGrid.tsx b/apps/code/src/renderer/features/command-center/components/CommandCenterGrid.tsx index 729129f09..6fc4b0e6a 100644 --- a/apps/code/src/renderer/features/command-center/components/CommandCenterGrid.tsx +++ b/apps/code/src/renderer/features/command-center/components/CommandCenterGrid.tsx @@ -103,6 +103,7 @@ function GridCell({ // biome-ignore lint/a11y/useKeyWithClickEvents lint/a11y/noStaticElementInteractions: click delegates focus to ActionSelector within
Date: Tue, 7 Apr 2026 21:03:03 -0700 Subject: [PATCH 3/3] Skip restoreStepAnswer when only callback ref changed --- .../components/action-selector/useActionSelectorState.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/code/src/renderer/components/action-selector/useActionSelectorState.ts b/apps/code/src/renderer/components/action-selector/useActionSelectorState.ts index d78a366fc..057be94d2 100644 --- a/apps/code/src/renderer/components/action-selector/useActionSelectorState.ts +++ b/apps/code/src/renderer/components/action-selector/useActionSelectorState.ts @@ -28,6 +28,9 @@ function isInteractiveElementInDifferentCell( const activeCell = el.closest("[data-grid-cell]"); const ownCell = containerRef.current?.closest("[data-grid-cell]"); + + // Outside a grid (single-task mode): block focus steal from any interactive element. + // Inside a grid: only block when the interactive element is in a different cell. if (!activeCell || !ownCell) return true; return activeCell !== ownCell; @@ -72,6 +75,7 @@ export function useActionSelectorState({ () => new Map(), ); const containerRef = useRef(null); + const prevActiveStepRef = useRef(currentStep); const activeStep = internalStep; const hasSteps = steps !== undefined && steps.length > 1; @@ -152,6 +156,8 @@ export function useActionSelectorState({ ); useEffect(() => { + if (activeStep === prevActiveStepRef.current) return; + prevActiveStepRef.current = activeStep; restoreStepAnswer(activeStep, { autoFocus: !isInteractiveElementInDifferentCell(containerRef), });