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 5140bf2119..ad61b6daf3 100644 --- a/apps/code/src/renderer/features/command-center/components/CommandCenterGrid.tsx +++ b/apps/code/src/renderer/features/command-center/components/CommandCenterGrid.tsx @@ -1,3 +1,4 @@ +import { FOCUSABLE_SELECTOR } from "@utils/overlay"; import { useCallback, useEffect, useRef, useState } from "react"; import type { CommandCenterCellData } from "../hooks/useCommandCenterData"; import { @@ -46,34 +47,43 @@ function GridCell({ cell, zoom, isDragActive, - activeTaskId, + isActive, }: { cell: CommandCenterCellData; zoom: number; isDragActive: boolean; - activeTaskId: string | null; + isActive: boolean; }) { const cellRef = useRef(null); const [isDragOver, setIsDragOver] = useState(false); const setActiveTask = useCommandCenterStore((s) => s.setActiveTask); - const isActive = !!cell.taskId && activeTaskId === cell.taskId; + const setActiveCell = useCommandCenterStore((s) => s.setActiveCell); - const handleCellClick = useCallback(() => { + const markActive = useCallback(() => { + setActiveCell(cell.cellIndex); setActiveTask(cell.taskId); - const selection = window.getSelection(); - if (selection && !selection.isCollapsed) return; - const actionSelector = - cellRef.current?.querySelector("[tabindex='0']"); - actionSelector?.focus(); - }, [cell.taskId, setActiveTask]); + }, [cell.cellIndex, cell.taskId, setActiveCell, setActiveTask]); - const handleCellPointerDownCapture = useCallback(() => { - setActiveTask(cell.taskId); - }, [cell.taskId, setActiveTask]); - - const handleCellFocusCapture = useCallback(() => { - setActiveTask(cell.taskId); - }, [cell.taskId, setActiveTask]); + const handleCellClick = useCallback( + (e: React.MouseEvent) => { + markActive(); + const target = e.target as HTMLElement; + // Don't redirect focus when the click already lands on a real control, + // or when it bubbled in from a portaled popover whose DOM target is + // outside this cell. Either way the click is targeting something that + // owns its own focus. + if ( + !e.currentTarget.contains(target) || + target.closest(FOCUSABLE_SELECTOR) + ) { + return; + } + const selection = window.getSelection(); + if (selection && !selection.isCollapsed) return; + cellRef.current?.querySelector("[tabindex='0']")?.focus(); + }, + [markActive], + ); const handleDragOver = useCallback((e: React.DragEvent) => { if (e.dataTransfer.types.includes("text/x-task-id")) { @@ -106,8 +116,8 @@ function GridCell({ data-grid-cell className="relative overflow-hidden bg-gray-1" onClick={handleCellClick} - onPointerDownCapture={handleCellPointerDownCapture} - onFocusCapture={handleCellFocusCapture} + onPointerDownCapture={markActive} + onFocusCapture={markActive} >
s.zoom); - const activeTaskId = useCommandCenterStore((s) => s.activeTaskId); + const activeCellIndex = useCommandCenterStore((s) => s.activeCellIndex); const isDragActive = useTaskDragActive(); return ( @@ -157,7 +167,7 @@ export function CommandCenterGrid({ layout, cells }: CommandCenterGridProps) { cell={cell} zoom={zoom} isDragActive={isDragActive} - activeTaskId={activeTaskId} + isActive={activeCellIndex === cell.cellIndex} /> ))}
diff --git a/apps/code/src/renderer/features/command-center/stores/commandCenterStore.ts b/apps/code/src/renderer/features/command-center/stores/commandCenterStore.ts index 7e8b354ce2..77c36fc619 100644 --- a/apps/code/src/renderer/features/command-center/stores/commandCenterStore.ts +++ b/apps/code/src/renderer/features/command-center/stores/commandCenterStore.ts @@ -23,6 +23,7 @@ interface CommandCenterStoreState { layout: LayoutPreset; cells: (string | null)[]; activeTaskId: string | null; + activeCellIndex: number | null; zoom: number; creatingCells: number[]; } @@ -30,6 +31,7 @@ interface CommandCenterStoreState { interface CommandCenterStoreActions { setLayout: (preset: LayoutPreset) => void; setActiveTask: (taskId: string | null) => void; + setActiveCell: (cellIndex: number | null) => void; assignTask: (cellIndex: number, taskId: string) => void; removeTask: (cellIndex: number) => void; removeTaskById: (taskId: string) => void; @@ -70,6 +72,7 @@ export const useCommandCenterStore = create()( layout: "2x2", cells: [null, null, null, null], activeTaskId: null, + activeCellIndex: null, zoom: 1, creatingCells: [], @@ -82,6 +85,10 @@ export const useCommandCenterStore = create()( ) ? state.activeTaskId : null, + activeCellIndex: + state.activeCellIndex !== null && state.activeCellIndex < newCount + ? state.activeCellIndex + : null, layout: preset, cells: resizeCells(state.cells, newCount), creatingCells: state.creatingCells.filter((i) => i < newCount), @@ -90,6 +97,8 @@ export const useCommandCenterStore = create()( setActiveTask: (taskId) => set({ activeTaskId: taskId }), + setActiveCell: (cellIndex) => set({ activeCellIndex: cellIndex }), + assignTask: (cellIndex, taskId) => set((state) => { if (cellIndex < 0 || cellIndex >= state.cells.length) return state; @@ -136,6 +145,7 @@ export const useCommandCenterStore = create()( clearAll: () => set((state) => ({ activeTaskId: null, + activeCellIndex: null, cells: state.cells.map(() => null), creatingCells: [], })), @@ -165,6 +175,7 @@ export const useCommandCenterStore = create()( layout: state.layout, cells: state.cells, activeTaskId: state.activeTaskId, + activeCellIndex: state.activeCellIndex, zoom: state.zoom, creatingCells: state.creatingCells, }), diff --git a/apps/code/src/renderer/features/task-detail/components/TaskInput.tsx b/apps/code/src/renderer/features/task-detail/components/TaskInput.tsx index 22f6dcf38a..c75b7df024 100644 --- a/apps/code/src/renderer/features/task-detail/components/TaskInput.tsx +++ b/apps/code/src/renderer/features/task-detail/components/TaskInput.tsx @@ -36,6 +36,7 @@ import { toast } from "@renderer/utils/toast"; import { useNavigationStore } from "@stores/navigationStore"; import { useQuery } from "@tanstack/react-query"; import { getFilePath } from "@utils/getFilePath"; +import { FOCUSABLE_SELECTOR } from "@utils/overlay"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { usePreviewConfig } from "../hooks/usePreviewConfig"; import { useTaskCreation } from "../hooks/useTaskCreation"; @@ -480,10 +481,18 @@ export function TaskInput({ editorRef.current?.focus(); }, []); + const handleContainerClick = useCallback((e: React.MouseEvent) => { + if (!e.currentTarget.contains(e.target as Node)) return; + if ((e.target as HTMLElement).closest(FOCUSABLE_SELECTOR)) return; + editorRef.current?.focus(); + }, []); + return ( // biome-ignore lint/a11y/noStaticElementInteractions: drag-and-drop container + // biome-ignore lint/a11y/useKeyWithClickEvents: click delegates focus to the editor; keyboard users tab into it directly