Skip to content

Commit e97132e

Browse files
authored
feat: Add multi-agent command center grid view (#1212)
Inspired by https://x.com/karpathy/status/2031616709560610993?s=46 This is exactly how I use Ghostty w/ Claude Code and if I could do this in Code + now we have skills support... it would be magical to say the least. I could follow this up with tab support in each of the panels so you can interact with shells for each and stuff? LMK thoughts code folks! If you guys are onboard and we manage to merge this, I'd love to demo this on Monday because I think this would drive some internal adoption. 1. Add command center grid view with configurable layouts (1x1 through 3x3) and zoom controls 2. Support drag-and-drop task assignment onto grid cells with swap behavior for occupied cells 3. Add per-cell session views with status badges, toolbar with stop-all/clear actions and agent summary 4. Extract useSessionCallbacks and useSessionConnection hooks from TaskDetail for reuse 5. Add compact mode to SessionView/ConversationView for tighter grid rendering 6. Wire command center into MainLayout as a new "command-center" view type <img width="1862" height="1237" alt="Screenshot 2026-03-11 at 10 48 08 PM" src="https://github.com/user-attachments/assets/bb8bb4e8-cab6-4357-b3d7-24cf4d082b23" />
1 parent 0453233 commit e97132e

28 files changed

Lines changed: 1386 additions & 295 deletions

apps/code/src/renderer/components/MainLayout.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { KeyboardShortcutsSheet } from "@components/KeyboardShortcutsSheet";
55

66
import { ArchivedTasksView } from "@features/archive/components/ArchivedTasksView";
77
import { CommandMenu } from "@features/command/components/CommandMenu";
8+
import { CommandCenterView } from "@features/command-center/components/CommandCenterView";
89
import { InboxView } from "@features/inbox/components/InboxView";
910
import { RightSidebar, RightSidebarContent } from "@features/right-sidebar";
1011
import { FolderSettingsView } from "@features/settings/components/FolderSettingsView";
@@ -75,6 +76,8 @@ export function MainLayout() {
7576
{view.type === "inbox" && <InboxView />}
7677

7778
{view.type === "archived" && <ArchivedTasksView />}
79+
80+
{view.type === "command-center" && <CommandCenterView />}
7881
</Box>
7982

8083
{view.type === "task-detail" && view.data && (
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import { useCallback, useEffect, useState } from "react";
2+
import type { CommandCenterCellData } from "../hooks/useCommandCenterData";
3+
import {
4+
getGridDimensions,
5+
type LayoutPreset,
6+
useCommandCenterStore,
7+
} from "../stores/commandCenterStore";
8+
import { CommandCenterPanel } from "./CommandCenterPanel";
9+
10+
interface CommandCenterGridProps {
11+
layout: LayoutPreset;
12+
cells: CommandCenterCellData[];
13+
}
14+
15+
function useTaskDragActive() {
16+
const [active, setActive] = useState(false);
17+
18+
useEffect(() => {
19+
const onDragStart = (e: DragEvent) => {
20+
if (e.dataTransfer?.types.includes("text/x-task-id")) {
21+
setActive(true);
22+
}
23+
};
24+
const onDragEnd = () => setActive(false);
25+
const onDrop = () => setActive(false);
26+
const onDragLeave = (e: DragEvent) => {
27+
if (!e.relatedTarget) setActive(false);
28+
};
29+
30+
document.addEventListener("dragstart", onDragStart);
31+
document.addEventListener("dragend", onDragEnd);
32+
document.addEventListener("drop", onDrop);
33+
document.addEventListener("dragleave", onDragLeave);
34+
return () => {
35+
document.removeEventListener("dragstart", onDragStart);
36+
document.removeEventListener("dragend", onDragEnd);
37+
document.removeEventListener("drop", onDrop);
38+
document.removeEventListener("dragleave", onDragLeave);
39+
};
40+
}, []);
41+
42+
return active;
43+
}
44+
45+
function GridCell({
46+
cell,
47+
zoom,
48+
isDragActive,
49+
}: {
50+
cell: CommandCenterCellData;
51+
zoom: number;
52+
isDragActive: boolean;
53+
}) {
54+
const [isDragOver, setIsDragOver] = useState(false);
55+
56+
const handleDragOver = useCallback((e: React.DragEvent) => {
57+
if (e.dataTransfer.types.includes("text/x-task-id")) {
58+
e.preventDefault();
59+
e.dataTransfer.dropEffect = "copy";
60+
setIsDragOver(true);
61+
}
62+
}, []);
63+
64+
const handleDragLeave = useCallback(() => {
65+
setIsDragOver(false);
66+
}, []);
67+
68+
const handleDrop = useCallback(
69+
(e: React.DragEvent) => {
70+
e.preventDefault();
71+
setIsDragOver(false);
72+
const taskId = e.dataTransfer.getData("text/x-task-id");
73+
if (taskId) {
74+
useCommandCenterStore.getState().assignTask(cell.cellIndex, taskId);
75+
}
76+
},
77+
[cell.cellIndex],
78+
);
79+
80+
return (
81+
<div className="relative overflow-hidden bg-gray-1">
82+
<div
83+
className="h-full w-full origin-top-left"
84+
style={{
85+
zoom: zoom !== 1 ? zoom : undefined,
86+
}}
87+
>
88+
<CommandCenterPanel cell={cell} />
89+
</div>
90+
{isDragActive && (
91+
// biome-ignore lint/a11y/noStaticElementInteractions: transparent overlay to capture drag events over session content
92+
<div
93+
className="absolute inset-0"
94+
style={{
95+
outline: isDragOver ? "2px solid var(--accent-9)" : undefined,
96+
outlineOffset: "-2px",
97+
}}
98+
onDragOver={handleDragOver}
99+
onDragLeave={handleDragLeave}
100+
onDrop={handleDrop}
101+
/>
102+
)}
103+
</div>
104+
);
105+
}
106+
107+
export function CommandCenterGrid({ layout, cells }: CommandCenterGridProps) {
108+
const { cols, rows } = getGridDimensions(layout);
109+
const zoom = useCommandCenterStore((s) => s.zoom);
110+
const isDragActive = useTaskDragActive();
111+
112+
return (
113+
<div
114+
className="h-full bg-gray-6"
115+
style={{
116+
display: "grid",
117+
gridTemplateColumns: `repeat(${cols}, 1fr)`,
118+
gridTemplateRows: `repeat(${rows}, 1fr)`,
119+
gap: "1px",
120+
}}
121+
>
122+
{cells.map((cell) => (
123+
<GridCell
124+
key={cell.cellIndex}
125+
cell={cell}
126+
zoom={zoom}
127+
isDragActive={isDragActive}
128+
/>
129+
))}
130+
</div>
131+
);
132+
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { ArrowsOut, Plus, X } from "@phosphor-icons/react";
2+
import { Flex, Text } from "@radix-ui/themes";
3+
import type { Task } from "@shared/types";
4+
import { useNavigationStore } from "@stores/navigationStore";
5+
import { useCallback, useState } from "react";
6+
import type { CommandCenterCellData } from "../hooks/useCommandCenterData";
7+
import { useCommandCenterStore } from "../stores/commandCenterStore";
8+
import { CommandCenterSessionView } from "./CommandCenterSessionView";
9+
import { StatusBadge } from "./StatusBadge";
10+
import { TaskSelector } from "./TaskSelector";
11+
12+
interface CommandCenterPanelProps {
13+
cell: CommandCenterCellData;
14+
}
15+
16+
function EmptyCell({ cellIndex }: { cellIndex: number }) {
17+
const [selectorOpen, setSelectorOpen] = useState(false);
18+
19+
return (
20+
<Flex align="center" justify="center" height="100%">
21+
<TaskSelector
22+
cellIndex={cellIndex}
23+
open={selectorOpen}
24+
onOpenChange={setSelectorOpen}
25+
>
26+
<button
27+
type="button"
28+
onClick={() => setSelectorOpen(true)}
29+
className="flex items-center gap-1.5 rounded-md border border-gray-7 border-dashed px-3 py-1.5 font-mono text-[11px] text-gray-10 transition-colors hover:border-gray-9 hover:text-gray-12"
30+
>
31+
<Plus size={12} />
32+
Add task
33+
</button>
34+
</TaskSelector>
35+
</Flex>
36+
);
37+
}
38+
39+
function PopulatedCell({
40+
cell,
41+
}: {
42+
cell: CommandCenterCellData & { task: Task };
43+
}) {
44+
const navigateToTask = useNavigationStore((s) => s.navigateToTask);
45+
const removeTask = useCommandCenterStore((s) => s.removeTask);
46+
47+
const handleExpand = useCallback(() => {
48+
navigateToTask(cell.task);
49+
}, [navigateToTask, cell.task]);
50+
51+
const handleRemove = useCallback(() => {
52+
removeTask(cell.cellIndex);
53+
}, [removeTask, cell.cellIndex]);
54+
55+
return (
56+
<Flex direction="column" height="100%">
57+
<Flex
58+
align="center"
59+
gap="2"
60+
px="2"
61+
py="1"
62+
className="shrink-0 border-gray-6 border-b"
63+
>
64+
<Text
65+
size="1"
66+
weight="medium"
67+
className="min-w-0 flex-1 truncate font-mono text-[11px]"
68+
title={cell.task.title}
69+
>
70+
{cell.task.title}
71+
</Text>
72+
<Flex align="center" gap="1" className="shrink-0">
73+
<StatusBadge status={cell.status} />
74+
{cell.repoName && (
75+
<span className="rounded bg-gray-3 px-1 py-0.5 font-mono text-[9px] text-gray-10">
76+
{cell.repoName}
77+
</span>
78+
)}
79+
<button
80+
type="button"
81+
onClick={handleExpand}
82+
className="flex h-5 w-5 items-center justify-center rounded text-gray-10 transition-colors hover:bg-gray-4 hover:text-gray-12"
83+
title="Open task"
84+
>
85+
<ArrowsOut size={12} />
86+
</button>
87+
<button
88+
type="button"
89+
onClick={handleRemove}
90+
className="flex h-5 w-5 items-center justify-center rounded text-gray-10 transition-colors hover:bg-gray-4 hover:text-gray-12"
91+
title="Remove from grid"
92+
>
93+
<X size={12} />
94+
</button>
95+
</Flex>
96+
</Flex>
97+
98+
<Flex direction="column" className="min-h-0 flex-1">
99+
<CommandCenterSessionView taskId={cell.task.id} task={cell.task} />
100+
</Flex>
101+
</Flex>
102+
);
103+
}
104+
105+
export function CommandCenterPanel({ cell }: CommandCenterPanelProps) {
106+
if (!cell.taskId || !cell.task) {
107+
return <EmptyCell cellIndex={cell.cellIndex} />;
108+
}
109+
110+
return (
111+
<PopulatedCell cell={cell as CommandCenterCellData & { task: Task }} />
112+
);
113+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { useDraftStore } from "@features/message-editor/stores/draftStore";
2+
import { SessionView } from "@features/sessions/components/SessionView";
3+
import { useSessionCallbacks } from "@features/sessions/hooks/useSessionCallbacks";
4+
import { useSessionConnection } from "@features/sessions/hooks/useSessionConnection";
5+
import { useSessionViewState } from "@features/sessions/hooks/useSessionViewState";
6+
import { Flex } from "@radix-ui/themes";
7+
import type { Task } from "@shared/types";
8+
import { useEffect } from "react";
9+
10+
interface CommandCenterSessionViewProps {
11+
taskId: string;
12+
task: Task;
13+
}
14+
15+
export function CommandCenterSessionView({
16+
taskId,
17+
task,
18+
}: CommandCenterSessionViewProps) {
19+
const { requestFocus } = useDraftStore((s) => s.actions);
20+
21+
const {
22+
session,
23+
repoPath,
24+
isCloud,
25+
isRunning,
26+
hasError,
27+
events,
28+
isPromptPending,
29+
promptStartedAt,
30+
isInitializing,
31+
cloudBranch,
32+
readOnlyMessage,
33+
errorTitle,
34+
errorMessage,
35+
} = useSessionViewState(taskId, task);
36+
37+
useSessionConnection({ taskId, task, session, repoPath, isCloud });
38+
39+
const {
40+
handleSendPrompt,
41+
handleCancelPrompt,
42+
handleRetry,
43+
handleNewSession,
44+
handleBashCommand,
45+
} = useSessionCallbacks({ taskId, task, session, repoPath });
46+
47+
useEffect(() => {
48+
requestFocus(taskId);
49+
}, [taskId, requestFocus]);
50+
51+
return (
52+
<Flex direction="column" height="100%">
53+
<SessionView
54+
events={events}
55+
taskId={taskId}
56+
isRunning={isRunning}
57+
isPromptPending={isCloud ? null : isPromptPending}
58+
promptStartedAt={isCloud ? undefined : promptStartedAt}
59+
onSendPrompt={handleSendPrompt}
60+
onBashCommand={isCloud ? undefined : handleBashCommand}
61+
onCancelPrompt={handleCancelPrompt}
62+
repoPath={repoPath}
63+
cloudBranch={cloudBranch}
64+
hasError={hasError}
65+
errorTitle={errorTitle}
66+
errorMessage={errorMessage}
67+
onRetry={isCloud ? undefined : handleRetry}
68+
onNewSession={isCloud ? undefined : handleNewSession}
69+
isInitializing={isInitializing}
70+
readOnlyMessage={readOnlyMessage}
71+
compact
72+
/>
73+
</Flex>
74+
);
75+
}

0 commit comments

Comments
 (0)