Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
import { getSessionService } from "@features/sessions/service/service";
import {
MagnifyingGlassMinus,
MagnifyingGlassPlus,
Stop,
Trash,
} from "@phosphor-icons/react";
import { Flex, Select, Text } from "@radix-ui/themes";
import type {
CommandCenterCellData,
StatusSummary,
} from "../hooks/useCommandCenterData";
import type { StatusSummary } from "../hooks/useCommandCenterData";
import {
type LayoutPreset,
useCommandCenterStore,
Expand Down Expand Up @@ -68,7 +63,6 @@ const LAYOUT_OPTIONS: {

interface CommandCenterToolbarProps {
summary: StatusSummary;
cells: CommandCenterCellData[];
}

function StatusSummaryText({ summary }: { summary: StatusSummary }) {
Expand All @@ -85,31 +79,14 @@ function StatusSummaryText({ summary }: { summary: StatusSummary }) {
);
}

export function CommandCenterToolbar({
summary,
cells,
}: CommandCenterToolbarProps) {
export function CommandCenterToolbar({ summary }: CommandCenterToolbarProps) {
const layout = useCommandCenterStore((s) => s.layout);
const setLayout = useCommandCenterStore((s) => s.setLayout);
const clearAll = useCommandCenterStore((s) => s.clearAll);
const zoom = useCommandCenterStore((s) => s.zoom);
const zoomIn = useCommandCenterStore((s) => s.zoomIn);
const zoomOut = useCommandCenterStore((s) => s.zoomOut);

const hasActiveAgents = summary.running > 0 || summary.waiting > 0;

const stopAll = () => {
const service = getSessionService();
for (const cell of cells) {
if (
cell.taskId &&
(cell.status === "running" || cell.status === "waiting")
) {
service.cancelPrompt(cell.taskId);
}
}
};

return (
<Flex
align="center"
Expand Down Expand Up @@ -163,17 +140,6 @@ export function CommandCenterToolbar({

<div className="flex-1" />

<button
type="button"
onClick={stopAll}
disabled={!hasActiveAgents}
className="flex items-center gap-1 rounded px-1.5 py-0.5 text-[12px] text-red-10 transition-colors hover:bg-red-3 hover:text-red-11 disabled:opacity-40 disabled:hover:bg-transparent disabled:hover:text-red-10"
title="Stop all agents"
>
<Stop size={12} weight="fill" />
Stop All
</button>

<button
type="button"
onClick={clearAll}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export function CommandCenterView() {

return (
<Flex direction="column" height="100%">
<CommandCenterToolbar summary={summary} cells={cells} />
<CommandCenterToolbar summary={summary} />
<Box className="min-h-0 flex-1">
<CommandCenterGrid layout={layout} cells={cells} />
</Box>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -324,4 +324,41 @@ describe("useAutofillCommandCenter", () => {
null,
]);
});

it("leaves hasAutofilled unset when there are no candidates yet", () => {
setQueries({ tasks: [], workspaces: {} });
renderHook(() => useAutofillCommandCenter());
expect(useCommandCenterStore.getState().hasAutofilled).toBe(false);
});

it("does not top up empty cells once the grid has been autofilled", () => {
useCommandCenterStore.setState({
cells: ["existing", null, null, null],
hasAutofilled: true,
});
setQueries({
tasks: [
makeTask({ id: "t1", updated_at: new Date(NOW - 100).toISOString() }),
makeTask({ id: "t2", updated_at: new Date(NOW - 200).toISOString() }),
],
workspaces: { t1: makeWorkspace("t1"), t2: makeWorkspace("t2") },
});
renderHook(() => useAutofillCommandCenter());
expect(useCommandCenterStore.getState().cells).toEqual([
"existing",
null,
null,
null,
]);
});

it("marks autofilled when the grid is already full so removals do not refill", () => {
useCommandCenterStore.setState({ cells: ["a", "b", "c", "d"] });
setQueries({
tasks: [makeTask({ id: "t1" })],
workspaces: { t1: makeWorkspace("t1") },
});
renderHook(() => useAutofillCommandCenter());
expect(useCommandCenterStore.getState().hasAutofilled).toBe(true);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ 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 { useEffect } from "react";
import { useCommandCenterStore } from "../stores/commandCenterStore";

// Window for "still in the current working session". Tasks last touched
Expand All @@ -24,24 +24,17 @@ export function useAutofillCommandCenter(): void {
const archivedTaskIds = useArchivedTaskIds();

const cells = useCommandCenterStore((s) => s.cells);
const hasAutofilled = useCommandCenterStore((s) => s.hasAutofilled);
const autofillCells = useCommandCenterStore((s) => s.autofillCells);

// Fires at most once per mount so clearing cells in-place doesn't
// immediately re-populate them. Navigating away and back remounts the
// view and lets autofill run again with the latest recent tasks.
const hasRunRef = useRef(false);

useEffect(() => {
if (hasRunRef.current) return;
// One-time bootstrap: the persisted `hasAutofilled` flag stops empty cells
// from being re-filled every time the Command Center remounts.
if (hasAutofilled) return;
if (!workspacesFetched || !workspaces) return;
if (!tasksFetched) return;

const emptySlots = cells.filter((id) => id == null).length;
if (emptySlots === 0) {
hasRunRef.current = true;
return;
}

const assignedIds = new Set(cells.filter((id): id is string => id != null));
const cutoff = Date.now() - RECENT_WINDOW_MS;
const candidates = tasks
Expand All @@ -56,12 +49,10 @@ export function useAutofillCommandCenter(): void {
.slice(0, emptySlots)
.map((task) => task.id);

if (candidates.length > 0) {
autofillCells(candidates);
}
hasRunRef.current = true;
autofillCells(candidates);
}, [
cells,
hasAutofilled,
workspaces,
workspacesFetched,
tasks,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,5 +75,28 @@ describe("commandCenterStore", () => {
null,
]);
});

it("sets hasAutofilled when it populates cells", () => {
useCommandCenterStore.getState().autofillCells(["t1"]);
expect(useCommandCenterStore.getState().hasAutofilled).toBe(true);
});

it("leaves hasAutofilled unset when there is nothing to fill", () => {
useCommandCenterStore.getState().autofillCells([]);
expect(useCommandCenterStore.getState().hasAutofilled).toBe(false);
});
});

describe("hasAutofilled", () => {
it("assigning a task marks the grid as curated", () => {
useCommandCenterStore.getState().assignTask(0, "t1");
expect(useCommandCenterStore.getState().hasAutofilled).toBe(true);
});

it("marks the grid as autofilled when it is already full", () => {
useCommandCenterStore.setState({ cells: ["a", "b", "c", "d"] });
useCommandCenterStore.getState().autofillCells([]);
expect(useCommandCenterStore.getState().hasAutofilled).toBe(true);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ interface CommandCenterStoreState {
activeCellIndex: number | null;
zoom: number;
creatingCells: number[];
// Persisted so autofill bootstraps the grid only once, not on every remount.
hasAutofilled: boolean;
}

interface CommandCenterStoreActions {
Expand All @@ -51,6 +53,7 @@ export const COMMAND_CENTER_INITIAL_STATE: CommandCenterStoreState = {
activeCellIndex: null,
zoom: 1,
creatingCells: [],
hasAutofilled: false,
};

type CommandCenterStore = CommandCenterStoreState & CommandCenterStoreActions;
Expand Down Expand Up @@ -117,21 +120,26 @@ export const useCommandCenterStore = create<CommandCenterStore>()(
cells,
activeTaskId: taskId,
creatingCells: state.creatingCells.filter((i) => i !== cellIndex),
// Manually placing a task counts as curating the grid.
hasAutofilled: true,
};
}),

autofillCells: (taskIds) =>
set((state) => {
// Grid already full: nothing to place, but the bootstrap is done.
if (state.cells.every((id) => id != null)) {
return { hasAutofilled: true };
}
if (taskIds.length === 0) return state;
if (state.cells.every((id) => id != null)) return state;
const cells: (string | null)[] = [...state.cells];
const queue = [...taskIds];
for (let i = 0; i < cells.length && queue.length > 0; i++) {
if (cells[i] == null) {
cells[i] = queue.shift() as string;
}
}
return { cells };
return { cells, hasAutofilled: true };
}),

removeTask: (cellIndex) =>
Expand Down Expand Up @@ -197,6 +205,7 @@ export const useCommandCenterStore = create<CommandCenterStore>()(
activeCellIndex: state.activeCellIndex,
zoom: state.zoom,
creatingCells: state.creatingCells,
hasAutofilled: state.hasAutofilled,
}),
},
),
Expand Down
Loading