Skip to content
4 changes: 4 additions & 0 deletions apps/code/src/main/platform-adapters/electron-context-menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ function toElectronItem(item: ContextMenuItem): MenuItemConstructorOptions {
enabled: action.enabled ?? true,
accelerator: action.accelerator,
};
if (action.checked !== undefined) {
options.type = "checkbox";
options.checked = action.checked;
}
if (action.icon) {
options.icon = resizeIcon(action.icon);
}
Expand Down
2 changes: 2 additions & 0 deletions apps/code/src/main/services/context-menu/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const taskContextMenuInput = z.object({
isSuspended: z.boolean().optional(),
isInCommandCenter: z.boolean().optional(),
hasEmptyCommandCenterCell: z.boolean().optional(),
isRevisit: z.boolean().optional(),
});

export const archivedTaskContextMenuInput = z.object({
Expand Down Expand Up @@ -42,6 +43,7 @@ const taskAction = z.discriminatedUnion("type", [
z.object({ type: z.literal("archive-prior") }),
z.object({ type: z.literal("delete") }),
z.object({ type: z.literal("add-to-command-center") }),
z.object({ type: z.literal("toggle-revisit") }),
z.object({ type: z.literal("external-app"), action: externalAppAction }),
]);

Expand Down
7 changes: 7 additions & 0 deletions apps/code/src/main/services/context-menu/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,12 +111,18 @@ export class ContextMenuService {
isSuspended,
isInCommandCenter,
hasEmptyCommandCenterCell,
isRevisit,
} = input;
const { apps, lastUsedAppId } = await this.getExternalAppsData();
const hasPath = worktreePath || folderPath;

return this.showMenu<TaskAction>([
this.item(isPinned ? "Unpin" : "Pin", { type: "pin" }),
this.item(
"Revisit",
{ type: "toggle-revisit" },
{ checked: isRevisit ?? false },
),
this.item("Rename", { type: "rename" }),
...(worktreePath
? [
Expand Down Expand Up @@ -336,6 +342,7 @@ export class ContextMenuService {
enabled: def.enabled,
accelerator: def.accelerator,
icon: def.icon,
checked: def.checked,
click,
};
}
Expand Down
2 changes: 2 additions & 0 deletions apps/code/src/main/services/context-menu/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export interface ActionItemDef<T> {
enabled?: boolean;
icon?: string;
confirm?: ConfirmOptions;
/** Renders the item as a native checkbox menu entry that shows a check mark when true. */
checked?: boolean;
}

export interface SubmenuItemDef<T> {
Expand Down
8 changes: 8 additions & 0 deletions apps/code/src/renderer/constants/keyboard-shortcuts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export const SHORTCUTS = {
FIND_IN_CONVERSATION: "mod+f",
BLUR: "escape",
SUBMIT_BLUR: "mod+enter",
TOGGLE_REVISIT: "mod+shift+m",
} as const;

export type ShortcutCategory = "general" | "navigation" | "panels" | "editor";
Expand Down Expand Up @@ -160,6 +161,13 @@ export const KEYBOARD_SHORTCUTS: KeyboardShortcut[] = [
category: "panels",
context: "Task detail",
},
{
id: "toggle-revisit",
keys: SHORTCUTS.TOGGLE_REVISIT,
description: "Toggle revisit task",
category: "panels",
context: "Task detail",
},
{
id: "paste-as-file",
keys: SHORTCUTS.PASTE_AS_FILE,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,23 @@ import {
} from "@features/sessions/stores/sessionStore";
import type { Plan } from "@features/sessions/types";
import { useSettingsStore } from "@features/settings/stores/settingsStore";
import { useRevisitStore } from "@features/task-detail/stores/revisitStore";
import { useIsWorkspaceCloudRun } from "@features/workspace/hooks/useWorkspace";
import { useAutoFocusOnTyping } from "@hooks/useAutoFocusOnTyping";
import { Pause, Spinner, Warning } from "@phosphor-icons/react";
import { Box, Button, ContextMenu, Flex, Text } from "@radix-ui/themes";
import { SHORTCUTS } from "@renderer/constants/keyboard-shortcuts";
import { toast } from "@renderer/utils/toast";
import type { Task, TaskRunStatus } from "@shared/types";
import { ANALYTICS_EVENTS } from "@shared/types/analytics";
import {
type AcpMessage,
isJsonRpcNotification,
isJsonRpcResponse,
} from "@shared/types/session-events";
import { track } from "@utils/analytics";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useHotkeys } from "react-hotkeys-hook";
import { getSessionService } from "../service/service";
import { flattenSelectOptions } from "../stores/sessionStore";
import {
Expand Down Expand Up @@ -95,6 +100,25 @@ function resolveAllowAlwaysUpgradeMode(
return undefined;
}

function useRevisitShortcut(taskId: string | undefined) {
useHotkeys(
SHORTCUTS.TOGGLE_REVISIT,
(e) => {
if (!taskId) return;
e.preventDefault();
const store = useRevisitStore.getState();
const wasMarked = store.revisitTaskIds.has(taskId);
store.toggle(taskId);
track(ANALYTICS_EVENTS.TASK_REVISIT_TOGGLED, {
task_id: taskId,
enabled: !wasMarked,
});
},
{ enableOnFormTags: true, enableOnContentEditable: true },
[taskId],
);
}

export function SessionView({
events,
taskId,
Expand Down Expand Up @@ -124,6 +148,7 @@ export function SessionView({
isActiveSession = true,
hideInput = false,
}: SessionViewProps) {
useRevisitShortcut(taskId);
const showRawLogs = useShowRawLogs();
const { setShowRawLogs } = useSessionViewActions();
const pendingPermissions = usePendingPermissionsForTask(taskId);
Expand Down
17 changes: 17 additions & 0 deletions apps/code/src/renderer/features/sidebar/components/SidebarMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
import { useOnboardingStore } from "@features/onboarding/stores/onboardingStore";
import { getSessionService } from "@features/sessions/service/service";
import { useSetupStore } from "@features/setup/stores/setupStore";
import { useRevisitStore } from "@features/task-detail/stores/revisitStore";
import {
archiveTaskImperative,
useArchiveTask,
Expand All @@ -19,9 +20,11 @@ import { useTaskContextMenu } from "@hooks/useTaskContextMenu";
import { ScrollArea, Separator } from "@posthog/quill";
import { Box, Flex } from "@radix-ui/themes";
import type { Task } from "@shared/types";
import { ANALYTICS_EVENTS } from "@shared/types/analytics";
import { useNavigationStore } from "@stores/navigationStore";
import { useRendererWindowFocusStore } from "@stores/rendererWindowFocusStore";
import { useQueryClient } from "@tanstack/react-query";
import { track } from "@utils/analytics";
import { logger } from "@utils/logger";
import { toast } from "@utils/toast";
import { memo, useCallback, useEffect, useRef } from "react";
Expand Down Expand Up @@ -62,6 +65,7 @@ function SidebarMenuComponent() {
useTaskContextMenu();
const { archiveTask } = useArchiveTask();
const { togglePin } = usePinnedTasks();
const toggleRevisit = useRevisitStore((s) => s.toggle);

const hasCompletedSetup = useOnboardingStore(
(state) => state.hasCompletedSetup,
Expand Down Expand Up @@ -172,15 +176,28 @@ function SidebarMenuComponent() {
(id) => id == null || !taskMap.has(id),
);

const isRevisit = useRevisitStore.getState().revisitTaskIds.has(taskId);

showContextMenu(task, e, {
worktreePath: workspace?.worktreePath ?? undefined,
folderPath: workspace?.folderPath ?? undefined,
isPinned,
isSuspended: taskData?.isSuspended,
isInCommandCenter,
hasEmptyCommandCenterCell,
isRevisit,
onTogglePin: () => togglePin(taskId),
onArchivePrior: handleArchivePrior,
onToggleRevisit: () => {
const wasMarked = useRevisitStore
.getState()
.revisitTaskIds.has(taskId);
toggleRevisit(taskId);
track(ANALYTICS_EVENTS.TASK_REVISIT_TOGGLED, {
task_id: taskId,
enabled: !wasMarked,
});
},
onAddToCommandCenter: () => {
const cells = useCommandCenterStore.getState().cells;
const idx = cells.findIndex((id) => id == null || !taskMap.has(id));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { PointerSensor } from "@dnd-kit/dom";
import type { DragDropEvents } from "@dnd-kit/react";
import { DragDropProvider } from "@dnd-kit/react";
import { useFolders } from "@features/folders/hooks/useFolders";
import { useRevisitStore } from "@features/task-detail/stores/revisitStore";
import { useMeQuery } from "@hooks/useMeQuery";
import {
FunnelSimple as FunnelSimpleIcon,
Expand All @@ -20,8 +21,10 @@ import {
import { Flex, Text } from "@radix-ui/themes";
import builderHog from "@renderer/assets/images/hedgehogs/builder-hog-03.png";
import { useWorkspace } from "@renderer/features/workspace/hooks/useWorkspace";
import { ANALYTICS_EVENTS } from "@shared/types/analytics";
import { normalizeRepoKey } from "@shared/utils/repo";
import { useNavigationStore } from "@stores/navigationStore";
import { track } from "@utils/analytics";
import { getRelativeDateGroup } from "@utils/time";
import { motion } from "framer-motion";
import { Fragment, useCallback, useEffect, useMemo } from "react";
Expand Down Expand Up @@ -102,6 +105,7 @@ function TaskRow({
workspace?.mode ??
(task.taskRunEnvironment === "cloud" ? "cloud" : undefined);
const { prState, hasDiff } = useTaskPrStatus(task);
const isRevisit = useRevisitStore((s) => s.revisitTaskIds.has(task.id));

return (
<TaskItem
Expand All @@ -116,6 +120,7 @@ function TaskRow({
isGenerating={task.isGenerating}
isUnread={task.isUnread}
isPinned={task.isPinned}
isRevisit={isRevisit}
needsPermission={task.needsPermission}
taskRunStatus={task.taskRunStatus}
prState={prState}
Expand All @@ -137,10 +142,14 @@ function TaskFilterMenu() {
const sortMode = useSidebarStore((state) => state.sortMode);
const showAllUsers = useSidebarStore((state) => state.showAllUsers);
const showInternal = useSidebarStore((state) => state.showInternal);
const showRevisitOnly = useSidebarStore((state) => state.showRevisitOnly);
const setOrganizeMode = useSidebarStore((state) => state.setOrganizeMode);
const setSortMode = useSidebarStore((state) => state.setSortMode);
const setShowAllUsers = useSidebarStore((state) => state.setShowAllUsers);
const setShowInternal = useSidebarStore((state) => state.setShowInternal);
const setShowRevisitOnly = useSidebarStore(
(state) => state.setShowRevisitOnly,
);
const { data: currentUser } = useMeQuery();
const isStaff = currentUser?.is_staff === true;

Expand Down Expand Up @@ -185,6 +194,29 @@ function TaskFilterMenu() {
<DropdownMenuRadioItem value="updated">Updated</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>

<DropdownMenuSeparator />

<MenuLabel>Filter</MenuLabel>
<DropdownMenuRadioGroup
value={showRevisitOnly ? "revisit" : "all"}
onValueChange={(value) => {
const next = value === "revisit";
const previous = showRevisitOnly ? "revisit" : "all";
if (previous === value) return;
setShowRevisitOnly(next);
track(ANALYTICS_EVENTS.TASK_REVISIT_LIST_FILTER_CHANGED, {
filter_name: "revisit_only",
value,
previous_value: previous,
});
}}
>
<DropdownMenuRadioItem value="all">All tasks</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="revisit">
Revisit only
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>

{import.meta.env.DEV && (
<>
<DropdownMenuSeparator />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ interface TaskItemProps {
isGenerating?: boolean;
isUnread?: boolean;
isPinned?: boolean;
isRevisit?: boolean;
isSuspended?: boolean;
needsPermission?: boolean;
taskRunStatus?: TaskRunStatus;
Expand Down Expand Up @@ -233,6 +234,7 @@ export function TaskItem({
isGenerating,
isUnread,
isPinned = false,
isRevisit = false,
needsPermission = false,
taskRunStatus,
prState,
Expand Down Expand Up @@ -276,6 +278,12 @@ export function TaskItem({
<PrStatusIcon prState={prState} hasDiff={hasDiff} />
) : isPinned ? (
<PushPin size={ICON_SIZE} className="text-accent-11" />
) : isRevisit ? (
<Tooltip content="Marked for revisit" side="right">
<span className="flex items-center justify-center">
<ChatCircle size={ICON_SIZE} weight="fill" className="text-yellow-10" />
</span>
</Tooltip>
) : (
<ChatCircle size={ICON_SIZE} className="text-gray-10" />
);
Expand Down
17 changes: 13 additions & 4 deletions apps/code/src/renderer/features/sidebar/hooks/useSidebarData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ import { useArchivedTaskIds } from "@features/archive/hooks/useArchivedTaskIds";
import { useProvisioningStore } from "@features/provisioning/stores/provisioningStore";
import { useSessions } from "@features/sessions/stores/sessionStore";
import { useSuspendedTaskIds } from "@features/suspension/hooks/useSuspendedTaskIds";
import { useRevisitStore } from "@features/task-detail/stores/revisitStore";
import { useTaskSummaries, useTasks } from "@features/tasks/hooks/useTasks";
import { useWorkspaces } from "@features/workspace/hooks/useWorkspace";
import type { Schemas } from "@renderer/api/generated";
import type { Task, TaskRunStatus } from "@shared/types";
import { useEffect, useMemo, useRef } from "react";
import { useSidebarStore } from "../stores/sidebarStore";
import type { SortMode } from "../types";
import { applyRevisitFilter } from "../utils/applyRevisitFilter";
import {
type TaskGroup as GenericTaskGroup,
getRepositoryInfo,
Expand Down Expand Up @@ -91,6 +93,8 @@ export function useSidebarData({
}: UseSidebarDataProps): SidebarData {
const showAllUsers = useSidebarStore((state) => state.showAllUsers);
const showInternal = useSidebarStore((state) => state.showInternal);
const showRevisitOnly = useSidebarStore((state) => state.showRevisitOnly);
const revisitTaskIds = useRevisitStore((state) => state.revisitTaskIds);
const { data: workspaces, isFetched: isWorkspacesFetched } = useWorkspaces();
const archivedTaskIds = useArchivedTaskIds();
const suspendedTaskIds = useSuspendedTaskIds();
Expand Down Expand Up @@ -256,14 +260,19 @@ export function useSidebarData({
workspaces,
]);

const filteredTaskData = useMemo(
() => applyRevisitFilter(taskData, showRevisitOnly, revisitTaskIds),
[taskData, showRevisitOnly, revisitTaskIds],
);

const pinnedTasks = useMemo(() => {
const pinned = taskData.filter((task) => task.isPinned);
const pinned = filteredTaskData.filter((task) => task.isPinned);
return sortTasks(pinned, sortMode);
}, [taskData, sortMode]);
}, [filteredTaskData, sortMode]);

const unpinnedTasks = useMemo(
() => taskData.filter((task) => !task.isPinned),
[taskData],
() => filteredTaskData.filter((task) => !task.isPinned),
[filteredTaskData],
);

const sortedUnpinnedTasks = useMemo(
Expand Down
Loading