From 8799a69a8459044e728d1ec7689f71e302a5bfea Mon Sep 17 00:00:00 2001 From: Richard Solomou Date: Tue, 19 May 2026 03:24:54 +0000 Subject: [PATCH 1/2] feat(command-center): autofill empty cells with recent active tasks When the user opens the Command Center and no tasks are attached, populate empty cells with their tasks updated in the past 2 hours, ordered by most recent activity. Generated-By: PostHog Code Task-Id: 427c51ec-695b-4203-9ac2-bb79ea575422 --- .../components/CommandCenterView.tsx | 3 + .../hooks/useAutofillCommandCenter.ts | 61 +++++++++++++++ .../stores/commandCenterStore.test.ts | 74 +++++++++++++++++++ .../stores/commandCenterStore.ts | 13 ++++ 4 files changed, 151 insertions(+) create mode 100644 apps/code/src/renderer/features/command-center/hooks/useAutofillCommandCenter.ts create mode 100644 apps/code/src/renderer/features/command-center/stores/commandCenterStore.test.ts diff --git a/apps/code/src/renderer/features/command-center/components/CommandCenterView.tsx b/apps/code/src/renderer/features/command-center/components/CommandCenterView.tsx index 2c1853149..0e844a523 100644 --- a/apps/code/src/renderer/features/command-center/components/CommandCenterView.tsx +++ b/apps/code/src/renderer/features/command-center/components/CommandCenterView.tsx @@ -3,6 +3,7 @@ import { useSetHeaderContent } from "@hooks/useSetHeaderContent"; import { Lightning } from "@phosphor-icons/react"; import { Box, Flex, Text } from "@radix-ui/themes"; import { useEffect, useMemo } from "react"; +import { useAutofillCommandCenter } from "../hooks/useAutofillCommandCenter"; import { useCommandCenterData } from "../hooks/useCommandCenterData"; import { useCommandCenterStore } from "../stores/commandCenterStore"; import { CommandCenterGrid } from "./CommandCenterGrid"; @@ -13,6 +14,8 @@ export function CommandCenterView() { const { cells, summary } = useCommandCenterData(); const { markAsViewed } = useTaskViewed(); + useAutofillCommandCenter(); + const visibleTaskIdsKey = cells .map((c) => c.taskId) .filter(Boolean) diff --git a/apps/code/src/renderer/features/command-center/hooks/useAutofillCommandCenter.ts b/apps/code/src/renderer/features/command-center/hooks/useAutofillCommandCenter.ts new file mode 100644 index 000000000..5d1d92ea8 --- /dev/null +++ b/apps/code/src/renderer/features/command-center/hooks/useAutofillCommandCenter.ts @@ -0,0 +1,61 @@ +import { useArchivedTaskIds } from "@features/archive/hooks/useArchivedTaskIds"; +import { useTasks } from "@features/tasks/hooks/useTasks"; +import { useWorkspaces } from "@features/workspace/hooks/useWorkspace"; +import type { Task } from "@shared/types"; +import { useEffect, useRef } from "react"; +import { useCommandCenterStore } from "../stores/commandCenterStore"; + +const RECENT_WINDOW_MS = 2 * 60 * 60 * 1000; + +function getLastActivity(task: Task): number { + const taskTime = new Date(task.updated_at).getTime(); + const runTime = task.latest_run?.updated_at + ? new Date(task.latest_run.updated_at).getTime() + : 0; + return Math.max(taskTime, runTime); +} + +export function useAutofillCommandCenter(): void { + const { data: tasks = [] } = useTasks(); + const { data: workspaces, isFetched: workspacesFetched } = useWorkspaces(); + const archivedTaskIds = useArchivedTaskIds(); + + const cells = useCommandCenterStore((s) => s.cells); + const autofillCells = useCommandCenterStore((s) => s.autofillCells); + + const hasRunRef = useRef(false); + + useEffect(() => { + if (hasRunRef.current) return; + if (!workspacesFetched || !workspaces) return; + + if (!cells.every((id) => id == null)) { + hasRunRef.current = true; + return; + } + + const cutoff = Date.now() - RECENT_WINDOW_MS; + const candidates = tasks + .filter( + (task) => + !archivedTaskIds.has(task.id) && + !!workspaces[task.id] && + getLastActivity(task) >= cutoff, + ) + .sort((a, b) => getLastActivity(b) - getLastActivity(a)) + .slice(0, cells.length) + .map((task) => task.id); + + if (candidates.length > 0) { + autofillCells(candidates); + } + hasRunRef.current = true; + }, [ + cells, + workspaces, + workspacesFetched, + tasks, + archivedTaskIds, + autofillCells, + ]); +} diff --git a/apps/code/src/renderer/features/command-center/stores/commandCenterStore.test.ts b/apps/code/src/renderer/features/command-center/stores/commandCenterStore.test.ts new file mode 100644 index 000000000..02f6592b4 --- /dev/null +++ b/apps/code/src/renderer/features/command-center/stores/commandCenterStore.test.ts @@ -0,0 +1,74 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +vi.mock("@utils/electronStorage", () => ({ + electronStorage: { + getItem: () => null, + setItem: () => {}, + removeItem: () => {}, + }, +})); + +import { useCommandCenterStore } from "./commandCenterStore"; + +describe("commandCenterStore", () => { + beforeEach(() => { + useCommandCenterStore.setState({ + layout: "2x2", + cells: [null, null, null, null], + activeTaskId: null, + activeCellIndex: null, + zoom: 1, + creatingCells: [], + }); + }); + + describe("autofillCells", () => { + it("fills empty cells from index 0", () => { + useCommandCenterStore.getState().autofillCells(["t1", "t2"]); + expect(useCommandCenterStore.getState().cells).toEqual([ + "t1", + "t2", + null, + null, + ]); + }); + + it("does nothing when any cell is already populated", () => { + useCommandCenterStore.setState({ cells: [null, "existing", null, null] }); + useCommandCenterStore.getState().autofillCells(["t1", "t2"]); + expect(useCommandCenterStore.getState().cells).toEqual([ + null, + "existing", + null, + null, + ]); + }); + + it("ignores empty task list", () => { + useCommandCenterStore.getState().autofillCells([]); + expect(useCommandCenterStore.getState().cells).toEqual([ + null, + null, + null, + null, + ]); + }); + + it("caps fill at the number of cells", () => { + useCommandCenterStore + .getState() + .autofillCells(["t1", "t2", "t3", "t4", "t5", "t6"]); + expect(useCommandCenterStore.getState().cells).toEqual([ + "t1", + "t2", + "t3", + "t4", + ]); + }); + + it("does not set activeTaskId", () => { + useCommandCenterStore.getState().autofillCells(["t1"]); + expect(useCommandCenterStore.getState().activeTaskId).toBeNull(); + }); + }); +}); 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 77c36fc61..890cc3d6a 100644 --- a/apps/code/src/renderer/features/command-center/stores/commandCenterStore.ts +++ b/apps/code/src/renderer/features/command-center/stores/commandCenterStore.ts @@ -33,6 +33,7 @@ interface CommandCenterStoreActions { setActiveTask: (taskId: string | null) => void; setActiveCell: (cellIndex: number | null) => void; assignTask: (cellIndex: number, taskId: string) => void; + autofillCells: (taskIds: string[]) => void; removeTask: (cellIndex: number) => void; removeTaskById: (taskId: string) => void; clearAll: () => void; @@ -115,6 +116,18 @@ export const useCommandCenterStore = create()( }; }), + autofillCells: (taskIds) => + set((state) => { + if (!state.cells.every((id) => id == null)) return state; + if (taskIds.length === 0) return state; + const cells: (string | null)[] = [...state.cells]; + const limit = Math.min(cells.length, taskIds.length); + for (let i = 0; i < limit; i++) { + cells[i] = taskIds[i]; + } + return { cells }; + }), + removeTask: (cellIndex) => set((state) => { const cells = [...state.cells]; From 791cbaf4f9066337c373de7d9277296d968cb709 Mon Sep 17 00:00:00 2001 From: Richard Solomou Date: Tue, 19 May 2026 03:44:57 +0000 Subject: [PATCH 2/2] fix(command-center): guard autofill on tasks fetch and parameterize tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address Greptile review on PR #2212: - The hook only waited on workspaces. useTasks() defaults data to [], so if workspaces were cached and resolved first the effect ran with an empty tasks array, found zero candidates, set hasRunRef true and bailed — the later tasks response could never trigger the autofill. Add a tasksFetched guard so both sources must be loaded. - Collapse the autofillCells test trio into an it.each table per repo convention. Generated-By: PostHog Code Task-Id: 427c51ec-695b-4203-9ac2-bb79ea575422 --- .../hooks/useAutofillCommandCenter.ts | 4 +- .../stores/commandCenterStore.test.ts | 55 +++++++------------ 2 files changed, 23 insertions(+), 36 deletions(-) diff --git a/apps/code/src/renderer/features/command-center/hooks/useAutofillCommandCenter.ts b/apps/code/src/renderer/features/command-center/hooks/useAutofillCommandCenter.ts index 5d1d92ea8..18538f5db 100644 --- a/apps/code/src/renderer/features/command-center/hooks/useAutofillCommandCenter.ts +++ b/apps/code/src/renderer/features/command-center/hooks/useAutofillCommandCenter.ts @@ -16,7 +16,7 @@ function getLastActivity(task: Task): number { } export function useAutofillCommandCenter(): void { - const { data: tasks = [] } = useTasks(); + const { data: tasks = [], isFetched: tasksFetched } = useTasks(); const { data: workspaces, isFetched: workspacesFetched } = useWorkspaces(); const archivedTaskIds = useArchivedTaskIds(); @@ -28,6 +28,7 @@ export function useAutofillCommandCenter(): void { useEffect(() => { if (hasRunRef.current) return; if (!workspacesFetched || !workspaces) return; + if (!tasksFetched) return; if (!cells.every((id) => id == null)) { hasRunRef.current = true; @@ -55,6 +56,7 @@ export function useAutofillCommandCenter(): void { workspaces, workspacesFetched, tasks, + tasksFetched, archivedTaskIds, autofillCells, ]); diff --git a/apps/code/src/renderer/features/command-center/stores/commandCenterStore.test.ts b/apps/code/src/renderer/features/command-center/stores/commandCenterStore.test.ts index 02f6592b4..c9fa694eb 100644 --- a/apps/code/src/renderer/features/command-center/stores/commandCenterStore.test.ts +++ b/apps/code/src/renderer/features/command-center/stores/commandCenterStore.test.ts @@ -23,14 +23,26 @@ describe("commandCenterStore", () => { }); describe("autofillCells", () => { - it("fills empty cells from index 0", () => { - useCommandCenterStore.getState().autofillCells(["t1", "t2"]); - expect(useCommandCenterStore.getState().cells).toEqual([ - "t1", - "t2", - null, - null, - ]); + it.each([ + { + name: "fills empty cells from index 0", + input: ["t1", "t2"], + expectedCells: ["t1", "t2", null, null], + }, + { + name: "ignores empty task list", + input: [], + expectedCells: [null, null, null, null], + }, + { + name: "caps fill at the number of cells", + input: ["t1", "t2", "t3", "t4", "t5", "t6"], + expectedCells: ["t1", "t2", "t3", "t4"], + }, + ])("$name and leaves activeTaskId null", ({ input, expectedCells }) => { + useCommandCenterStore.getState().autofillCells(input); + expect(useCommandCenterStore.getState().cells).toEqual(expectedCells); + expect(useCommandCenterStore.getState().activeTaskId).toBeNull(); }); it("does nothing when any cell is already populated", () => { @@ -43,32 +55,5 @@ describe("commandCenterStore", () => { null, ]); }); - - it("ignores empty task list", () => { - useCommandCenterStore.getState().autofillCells([]); - expect(useCommandCenterStore.getState().cells).toEqual([ - null, - null, - null, - null, - ]); - }); - - it("caps fill at the number of cells", () => { - useCommandCenterStore - .getState() - .autofillCells(["t1", "t2", "t3", "t4", "t5", "t6"]); - expect(useCommandCenterStore.getState().cells).toEqual([ - "t1", - "t2", - "t3", - "t4", - ]); - }); - - it("does not set activeTaskId", () => { - useCommandCenterStore.getState().autofillCells(["t1"]); - expect(useCommandCenterStore.getState().activeTaskId).toBeNull(); - }); }); });