Skip to content

Commit 08e2882

Browse files
committed
Add task search and sidebar context menu integration to command center
1 parent e9a68ca commit 08e2882

File tree

5 files changed

+87
-30
lines changed

5 files changed

+87
-30
lines changed

apps/code/src/main/services/context-menu/schemas.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ export const taskContextMenuInput = z.object({
66
folderPath: z.string().optional(),
77
isPinned: z.boolean().optional(),
88
isSuspended: z.boolean().optional(),
9+
isInCommandCenter: z.boolean().optional(),
10+
hasEmptyCommandCenterCell: z.boolean().optional(),
911
});
1012

1113
export const archivedTaskContextMenuInput = z.object({
@@ -40,6 +42,7 @@ const taskAction = z.discriminatedUnion("type", [
4042
z.object({ type: z.literal("archive") }),
4143
z.object({ type: z.literal("archive-prior") }),
4244
z.object({ type: z.literal("delete") }),
45+
z.object({ type: z.literal("add-to-command-center") }),
4346
z.object({ type: z.literal("external-app"), action: externalAppAction }),
4447
]);
4548

apps/code/src/main/services/context-menu/service.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,14 @@ export class ContextMenuService {
104104
async showTaskContextMenu(
105105
input: TaskContextMenuInput,
106106
): Promise<TaskContextMenuResult> {
107-
const { worktreePath, folderPath, isPinned, isSuspended } = input;
107+
const {
108+
worktreePath,
109+
folderPath,
110+
isPinned,
111+
isSuspended,
112+
isInCommandCenter,
113+
hasEmptyCommandCenterCell,
114+
} = input;
108115
const { apps, lastUsedAppId } = await this.getExternalAppsData();
109116
const hasPath = worktreePath || folderPath;
110117

@@ -126,6 +133,16 @@ export class ContextMenuService {
126133
...this.externalAppItems<TaskAction>(apps, lastUsedAppId),
127134
]
128135
: []),
136+
...(!isInCommandCenter
137+
? [
138+
this.separator(),
139+
this.item(
140+
"Add to Command Center",
141+
{ type: "add-to-command-center" as const },
142+
{ enabled: hasEmptyCommandCenterCell ?? true },
143+
),
144+
]
145+
: []),
129146
this.separator(),
130147
this.item("Archive", { type: "archive" }),
131148
this.item(

apps/code/src/renderer/features/command-center/components/TaskSelector.tsx

Lines changed: 42 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
import { Combobox } from "@components/ui/combobox/Combobox";
12
import { Plus } from "@phosphor-icons/react";
2-
import { Popover, Separator } from "@radix-ui/themes";
3+
import { Popover } from "@radix-ui/themes";
34
import { useNavigationStore } from "@stores/navigationStore";
45
import { type ReactNode, useCallback } from "react";
56
import { useAvailableTasks } from "../hooks/useAvailableTasks";
@@ -42,42 +43,54 @@ export function TaskSelector({
4243
}, [onOpenChange, onNewTask, navigateToTaskInput]);
4344

4445
return (
45-
<Popover.Root open={open} onOpenChange={onOpenChange}>
46+
<Combobox.Root
47+
open={open}
48+
onOpenChange={onOpenChange}
49+
value=""
50+
onValueChange={handleSelect}
51+
size="1"
52+
>
4653
<Popover.Trigger>{children}</Popover.Trigger>
47-
<Popover.Content
54+
<Combobox.Content
55+
items={availableTasks}
56+
getValue={(task) => task.title}
4857
side="bottom"
4958
align="center"
5059
sideOffset={4}
51-
style={{ padding: 4, minWidth: 240, maxHeight: 300 }}
60+
style={{ minWidth: 240 }}
5261
>
53-
<button
54-
type="button"
55-
onClick={handleNewTask}
56-
className="flex w-full items-center gap-1.5 rounded-sm px-2 py-1.5 text-left text-[12px] text-gray-12 transition-colors hover:bg-gray-3"
57-
>
58-
<Plus size={12} />
59-
New task
60-
</button>
61-
<Separator size="4" className="my-1" />
62-
<div className="overflow-y-auto" style={{ maxHeight: 240 }}>
63-
{availableTasks.length === 0 ? (
64-
<div className="px-2 py-3 text-center font-mono text-[11px] text-gray-10">
65-
No available tasks
66-
</div>
67-
) : (
68-
availableTasks.map((task) => (
69-
<button
62+
{({ filtered, hasMore, moreCount }) => (
63+
<>
64+
<Combobox.Input placeholder="Search tasks..." />
65+
<Combobox.Empty>No matching tasks</Combobox.Empty>
66+
{filtered.map((task) => (
67+
<Combobox.Item
7068
key={task.id}
69+
value={task.id}
70+
textValue={task.title}
71+
>
72+
{task.title}
73+
</Combobox.Item>
74+
))}
75+
{hasMore && (
76+
<div className="combobox-label">
77+
{moreCount} more {moreCount === 1 ? "task" : "tasks"} — type to
78+
filter
79+
</div>
80+
)}
81+
<Combobox.Footer>
82+
<button
7183
type="button"
72-
onClick={() => handleSelect(task.id)}
73-
className="flex w-full items-center rounded-sm px-2 py-1.5 text-left text-[12px] text-gray-12 transition-colors hover:bg-gray-3"
84+
className="combobox-footer-button"
85+
onClick={handleNewTask}
7486
>
75-
<span className="min-w-0 flex-1 truncate">{task.title}</span>
87+
<Plus size={11} weight="bold" />
88+
New task
7689
</button>
77-
))
78-
)}
79-
</div>
80-
</Popover.Content>
81-
</Popover.Root>
90+
</Combobox.Footer>
91+
</>
92+
)}
93+
</Combobox.Content>
94+
</Combobox.Root>
8295
);
8396
}

apps/code/src/renderer/features/sidebar/components/SidebarMenu.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ function SidebarMenuComponent() {
7676
}
7777

7878
const commandCenterCells = useCommandCenterStore((s) => s.cells);
79+
const assignTaskToCommandCenter = useCommandCenterStore((s) => s.assignTask);
7980
const commandCenterActiveCount = commandCenterCells.filter(
8081
(taskId) => taskId != null && taskMap.has(taskId),
8182
).length;
@@ -137,13 +138,25 @@ function SidebarMenuComponent() {
137138
if (task) {
138139
const workspace = workspaces[taskId];
139140
const taskData = allSidebarTasks.find((t) => t.id === taskId);
141+
const isInCommandCenter = commandCenterCells.includes(taskId);
142+
const firstEmptyIndex = commandCenterCells.indexOf(null);
143+
const hasEmptyCommandCenterCell = firstEmptyIndex !== -1;
144+
140145
showContextMenu(task, e, {
141146
worktreePath: workspace?.worktreePath ?? undefined,
142147
folderPath: workspace?.folderPath ?? undefined,
143148
isPinned,
144149
isSuspended: taskData?.isSuspended,
150+
isInCommandCenter,
151+
hasEmptyCommandCenterCell,
145152
onTogglePin: () => togglePin(taskId),
146153
onArchivePrior: handleArchivePrior,
154+
onAddToCommandCenter: () => {
155+
if (firstEmptyIndex !== -1) {
156+
assignTaskToCommandCenter(firstEmptyIndex, taskId);
157+
navigateToCommandCenter();
158+
}
159+
},
147160
});
148161
}
149162
};

apps/code/src/renderer/hooks/useTaskContextMenu.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,11 @@ export function useTaskContextMenu() {
2828
folderPath?: string;
2929
isPinned?: boolean;
3030
isSuspended?: boolean;
31+
isInCommandCenter?: boolean;
32+
hasEmptyCommandCenterCell?: boolean;
3133
onTogglePin?: () => void;
3234
onArchivePrior?: (taskId: string) => void;
35+
onAddToCommandCenter?: () => void;
3336
},
3437
) => {
3538
event.preventDefault();
@@ -40,8 +43,11 @@ export function useTaskContextMenu() {
4043
folderPath,
4144
isPinned,
4245
isSuspended,
46+
isInCommandCenter,
47+
hasEmptyCommandCenterCell,
4348
onTogglePin,
4449
onArchivePrior,
50+
onAddToCommandCenter,
4551
} = options ?? {};
4652

4753
try {
@@ -51,6 +57,8 @@ export function useTaskContextMenu() {
5157
folderPath,
5258
isPinned,
5359
isSuspended,
60+
isInCommandCenter,
61+
hasEmptyCommandCenterCell,
5462
});
5563

5664
if (!result.action) return;
@@ -86,6 +94,9 @@ export function useTaskContextMenu() {
8694
hasWorktree: !!worktreePath,
8795
});
8896
break;
97+
case "add-to-command-center":
98+
onAddToCommandCenter?.();
99+
break;
89100
case "external-app": {
90101
const effectivePath = worktreePath ?? folderPath;
91102
if (effectivePath) {

0 commit comments

Comments
 (0)