Skip to content
Open
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
13 changes: 13 additions & 0 deletions apps/code/src/main/services/context-menu/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ export const taskContextMenuInput = z.object({
hasEmptyCommandCenterCell: z.boolean().optional(),
});

export const bulkTaskContextMenuInput = z.object({
taskCount: z.number().int().positive(),
});
Comment on lines +13 to +15
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 The schema uses positive() (≥ 1), but the context menu label renders "Archive ${taskCount} tasks" — if taskCount is 1 that reads "Archive 1 tasks". Changing to .min(2) also makes the schema self-document that this endpoint is intentionally bulk-only.

Suggested change
export const bulkTaskContextMenuInput = z.object({
taskCount: z.number().int().positive(),
});
export const bulkTaskContextMenuInput = z.object({
taskCount: z.number().int().min(2),
});
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/code/src/main/services/context-menu/schemas.ts
Line: 13-15

Comment:
The schema uses `positive()` (≥ 1), but the context menu label renders `"Archive ${taskCount} tasks"` — if `taskCount` is 1 that reads "Archive 1 tasks". Changing to `.min(2)` also makes the schema self-document that this endpoint is intentionally bulk-only.

```suggestion
export const bulkTaskContextMenuInput = z.object({
  taskCount: z.number().int().min(2),
});
```

How can I resolve this? If you propose a fix, please make it concise.


export const archivedTaskContextMenuInput = z.object({
taskTitle: z.string(),
});
Expand Down Expand Up @@ -45,6 +49,10 @@ const taskAction = z.discriminatedUnion("type", [
z.object({ type: z.literal("external-app"), action: externalAppAction }),
]);

const bulkTaskAction = z.discriminatedUnion("type", [
z.object({ type: z.literal("archive") }),
]);

const archivedTaskAction = z.discriminatedUnion("type", [
z.object({ type: z.literal("restore") }),
z.object({ type: z.literal("delete") }),
Expand Down Expand Up @@ -72,6 +80,9 @@ const splitDirection = z.enum(["left", "right", "up", "down"]);
export const taskContextMenuOutput = z.object({
action: taskAction.nullable(),
});
export const bulkTaskContextMenuOutput = z.object({
action: bulkTaskAction.nullable(),
});
export const archivedTaskContextMenuOutput = z.object({
action: archivedTaskAction.nullable(),
});
Expand All @@ -87,6 +98,7 @@ export const splitContextMenuOutput = z.object({
});

export type TaskContextMenuInput = z.infer<typeof taskContextMenuInput>;
export type BulkTaskContextMenuInput = z.infer<typeof bulkTaskContextMenuInput>;
export type ArchivedTaskContextMenuInput = z.infer<
typeof archivedTaskContextMenuInput
>;
Expand All @@ -96,6 +108,7 @@ export type FileContextMenuInput = z.infer<typeof fileContextMenuInput>;

export type ExternalAppAction = z.infer<typeof externalAppAction>;
export type TaskAction = z.infer<typeof taskAction>;
export type BulkTaskAction = z.infer<typeof bulkTaskAction>;
export type ArchivedTaskAction = z.infer<typeof archivedTaskAction>;
export type FolderAction = z.infer<typeof folderAction>;
export type TabAction = z.infer<typeof tabAction>;
Expand Down
23 changes: 23 additions & 0 deletions apps/code/src/main/services/context-menu/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import type {
ArchivedTaskAction,
ArchivedTaskContextMenuInput,
ArchivedTaskContextMenuResult,
BulkTaskAction,
BulkTaskContextMenuInput,
ConfirmDeleteArchivedTaskInput,
ConfirmDeleteArchivedTaskResult,
ConfirmDeleteTaskInput,
Expand Down Expand Up @@ -160,6 +162,27 @@ export class ContextMenuService {
]);
}

async showBulkTaskContextMenu(
input: BulkTaskContextMenuInput,
): Promise<{ action: BulkTaskAction | null }> {
const { taskCount } = input;
const label = `Archive ${taskCount} tasks`;
return this.showMenu<BulkTaskAction>([
this.item(
label,
{ type: "archive" },
{
confirm: {
title: "Archive Tasks",
message: `Archive ${taskCount} tasks?`,
detail: "You can unarchive them later.",
confirmLabel: "Archive",
},
},
),
]);
}

async showArchivedTaskContextMenu(
input: ArchivedTaskContextMenuInput,
): Promise<ArchivedTaskContextMenuResult> {
Expand Down
7 changes: 7 additions & 0 deletions apps/code/src/main/trpc/routers/context-menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { MAIN_TOKENS } from "../../di/tokens";
import {
archivedTaskContextMenuInput,
archivedTaskContextMenuOutput,
bulkTaskContextMenuInput,
bulkTaskContextMenuOutput,
confirmDeleteArchivedTaskInput,
confirmDeleteArchivedTaskOutput,
confirmDeleteTaskInput,
Expand Down Expand Up @@ -46,6 +48,11 @@ export const contextMenuRouter = router({
.output(taskContextMenuOutput)
.mutation(({ input }) => getService().showTaskContextMenu(input)),

showBulkTaskContextMenu: publicProcedure
.input(bulkTaskContextMenuInput)
.output(bulkTaskContextMenuOutput)
.mutation(({ input }) => getService().showBulkTaskContextMenu(input)),

showArchivedTaskContextMenu: publicProcedure
.input(archivedTaskContextMenuInput)
.output(archivedTaskContextMenuOutput)
Expand Down
21 changes: 21 additions & 0 deletions apps/code/src/renderer/features/sidebar/components/MainSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,16 @@ import { useWorkspaces } from "@features/workspace/hooks/useWorkspace";
import { Box } from "@radix-ui/themes";
import { useEffect } from "react";
import { useSidebarStore } from "../stores/sidebarStore";
import { useTaskSelectionStore } from "../stores/taskSelectionStore";
import { Sidebar, SidebarContent } from "./index";

function isEditableTarget(target: EventTarget | null): boolean {
if (!(target instanceof HTMLElement)) return false;
if (target.isContentEditable) return true;
const tag = target.tagName;
return tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT";
}

export function MainSidebar() {
const { data: workspaces = {}, isFetched } = useWorkspaces();
const hasCompletedOnboarding = useOnboardingStore(
Expand All @@ -19,6 +27,19 @@ export function MainSidebar() {
}
}, [isFetched, workspaces, hasCompletedOnboarding, setOpenAuto]);

useEffect(() => {
const handler = (e: KeyboardEvent) => {
if (e.key !== "Escape") return;
if (isEditableTarget(e.target)) return;
const { selectedTaskIds, clearSelection } =
useTaskSelectionStore.getState();
if (selectedTaskIds.length === 0) return;
clearSelection();
};
window.addEventListener("keydown", handler);
return () => window.removeEventListener("keydown", handler);
}, []);

return (
<Box flexShrink="0" className="shrink-0">
<Sidebar>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ interface SidebarItemProps {
label: React.ReactNode;
subtitle?: React.ReactNode;
isActive?: boolean;
isSelected?: boolean;
isDimmed?: boolean;
draggable?: boolean;
onDragStart?: (e: React.DragEvent) => void;
onClick?: () => void;
onClick?: (e: React.MouseEvent) => void;
onDoubleClick?: () => void;
onContextMenu?: (e: React.MouseEvent) => void;
action?: SidebarItemAction;
Expand All @@ -28,6 +29,7 @@ export function SidebarItem({
label,
subtitle,
isActive,
isSelected,
draggable,
onDragStart,
onClick,
Expand Down Expand Up @@ -69,9 +71,10 @@ export function SidebarItem({
type="button"
className={cn(
"group focus-visible:-outline-offset-2 flex w-full text-left text-[13px] leading-snug transition-colors focus-visible:outline-2 focus-visible:outline-accent-8",
"cursor-default disabled:opacity-100 data-active:bg-fill-selected",
"cursor-default disabled:opacity-100 data-active:bg-fill-selected data-selected:bg-(--gray-3)",
)}
data-active={isActive || undefined}
data-selected={(isSelected && !isActive) || undefined}
draggable={draggable}
onDragStart={onDragStart}
style={{
Expand Down
Loading
Loading