From 5e7b2e24cacf102a0c9b04e4f585edaa59aa0ab2 Mon Sep 17 00:00:00 2001 From: Siddharth Jain Date: Fri, 8 May 2026 14:26:39 +0530 Subject: [PATCH 1/9] feat: add per-task Revisit toggle and sidebar filter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #2104 Adds a per-task "Revisit" annotation so users can mark tasks they want to come back to later. - Inline Revisit Switch above the chat input on the right, with tooltip "Come back to revisit the task later (⇧⌘M)" and Cmd+Shift+M shortcut. - Sidebar chat-bubble fills yellow on tasks marked for revisit. - Task list filter dropdown gains "All tasks / Revisit only" radio so users can focus on tasks they flagged. - State persists per-device via a Zustand localStorage store. - Two PostHog events captured (also fire when toggled via shortcut): - "Task revisit toggled" { task_id, enabled } - "Task revisit list filter changed" { filter_name, value, previous_value } Generated-By: PostHog Code Task-Id: c63c2352-b983-4807-8b6c-029bb97ab9f5 --- .../renderer/constants/keyboard-shortcuts.ts | 8 +++ .../sessions/components/SessionView.tsx | 61 ++++++++++++++++++- .../sidebar/components/TaskListView.tsx | 32 ++++++++++ .../sidebar/components/items/TaskItem.tsx | 8 +++ .../features/sidebar/hooks/useSidebarData.ts | 19 ++++-- .../features/sidebar/stores/sidebarStore.ts | 8 +++ .../task-detail/stores/revisitStore.ts | 56 +++++++++++++++++ apps/code/src/shared/types/analytics.ts | 20 ++++++ 8 files changed, 207 insertions(+), 5 deletions(-) create mode 100644 apps/code/src/renderer/features/task-detail/stores/revisitStore.ts diff --git a/apps/code/src/renderer/constants/keyboard-shortcuts.ts b/apps/code/src/renderer/constants/keyboard-shortcuts.ts index a209ed436..650341d2c 100644 --- a/apps/code/src/renderer/constants/keyboard-shortcuts.ts +++ b/apps/code/src/renderer/constants/keyboard-shortcuts.ts @@ -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"; @@ -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, diff --git a/apps/code/src/renderer/features/sessions/components/SessionView.tsx b/apps/code/src/renderer/features/sessions/components/SessionView.tsx index c542a1fcc..1f19398d6 100644 --- a/apps/code/src/renderer/features/sessions/components/SessionView.tsx +++ b/apps/code/src/renderer/features/sessions/components/SessionView.tsx @@ -1,5 +1,6 @@ import { isOtherOption } from "@components/action-selector/constants"; import { PermissionSelector } from "@components/permissions/PermissionSelector"; +import { Tooltip } from "@components/ui/Tooltip"; import { PromptInput, type EditorHandle as PromptInputHandle, @@ -16,18 +17,26 @@ 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 { Box, Button, ContextMenu, Flex, Switch, Text } from "@radix-ui/themes"; +import { + formatHotkey, + 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 { @@ -95,6 +104,53 @@ function resolveAllowAlwaysUpgradeMode( return undefined; } +function RevisitToggleInline({ taskId }: { taskId: string }) { + const isRevisit = useRevisitStore((s) => s.revisitTaskIds.has(taskId)); + const setRevisit = useRevisitStore((s) => s.setRevisit); + + const applyChange = useCallback( + (next: boolean) => { + setRevisit(taskId, next); + track(ANALYTICS_EVENTS.TASK_REVISIT_TOGGLED, { + task_id: taskId, + enabled: next, + }); + }, + [taskId, setRevisit], + ); + + useHotkeys( + SHORTCUTS.TOGGLE_REVISIT, + (e) => { + e.preventDefault(); + applyChange(!isRevisit); + }, + { enableOnFormTags: true, enableOnContentEditable: true }, + [isRevisit, applyChange], + ); + + return ( + + + + + Revisit + + + + + + ); +} + export function SessionView({ events, taskId, @@ -613,6 +669,9 @@ export function SessionView({ : { maxWidth: CHAT_CONTENT_MAX_WIDTH } } > + {taskId ? ( + + ) : null} s.revisitTaskIds.has(task.id)); return ( 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; @@ -185,6 +194,29 @@ function TaskFilterMenu() { Updated + + + Filter + { + 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, + }); + }} + > + All tasks + + Revisit only + + + {import.meta.env.DEV && ( <> diff --git a/apps/code/src/renderer/features/sidebar/components/items/TaskItem.tsx b/apps/code/src/renderer/features/sidebar/components/items/TaskItem.tsx index eb604baeb..ad802e018 100644 --- a/apps/code/src/renderer/features/sidebar/components/items/TaskItem.tsx +++ b/apps/code/src/renderer/features/sidebar/components/items/TaskItem.tsx @@ -29,6 +29,7 @@ interface TaskItemProps { isGenerating?: boolean; isUnread?: boolean; isPinned?: boolean; + isRevisit?: boolean; isSuspended?: boolean; needsPermission?: boolean; taskRunStatus?: TaskRunStatus; @@ -233,6 +234,7 @@ export function TaskItem({ isGenerating, isUnread, isPinned = false, + isRevisit = false, needsPermission = false, taskRunStatus, prState, @@ -276,6 +278,12 @@ export function TaskItem({ ) : isPinned ? ( + ) : isRevisit ? ( + + + + + ) : ( ); diff --git a/apps/code/src/renderer/features/sidebar/hooks/useSidebarData.ts b/apps/code/src/renderer/features/sidebar/hooks/useSidebarData.ts index c0f166b02..178c0d829 100644 --- a/apps/code/src/renderer/features/sidebar/hooks/useSidebarData.ts +++ b/apps/code/src/renderer/features/sidebar/hooks/useSidebarData.ts @@ -2,6 +2,7 @@ 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"; @@ -91,6 +92,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(); @@ -256,14 +259,22 @@ export function useSidebarData({ workspaces, ]); + const filteredTaskData = useMemo( + () => + showRevisitOnly + ? taskData.filter((task) => revisitTaskIds.has(task.id)) + : taskData, + [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( diff --git a/apps/code/src/renderer/features/sidebar/stores/sidebarStore.ts b/apps/code/src/renderer/features/sidebar/stores/sidebarStore.ts index b87d80c2f..97fed84a8 100644 --- a/apps/code/src/renderer/features/sidebar/stores/sidebarStore.ts +++ b/apps/code/src/renderer/features/sidebar/stores/sidebarStore.ts @@ -14,6 +14,7 @@ interface SidebarStoreState { sortMode: "updated" | "created"; showAllUsers: boolean; showInternal: boolean; + showRevisitOnly: boolean; } interface SidebarStoreActions { @@ -32,6 +33,7 @@ interface SidebarStoreActions { setSortMode: (mode: SidebarStoreState["sortMode"]) => void; setShowAllUsers: (showAllUsers: boolean) => void; setShowInternal: (showInternal: boolean) => void; + setShowRevisitOnly: (showRevisitOnly: boolean) => void; } type SidebarStore = SidebarStoreState & SidebarStoreActions; @@ -50,6 +52,7 @@ export const useSidebarStore = create()( sortMode: "updated", showAllUsers: false, showInternal: false, + showRevisitOnly: false, setOpen: (open) => set({ open, hasUserSetOpen: true }), setOpenAuto: (open) => set((state) => (state.hasUserSetOpen ? state : { open })), @@ -100,6 +103,7 @@ export const useSidebarStore = create()( setSortMode: (sortMode) => set({ sortMode }), setShowAllUsers: (showAllUsers) => set({ showAllUsers }), setShowInternal: (showInternal) => set({ showInternal }), + setShowRevisitOnly: (showRevisitOnly) => set({ showRevisitOnly }), }), { name: "sidebar-storage", @@ -114,6 +118,7 @@ export const useSidebarStore = create()( sortMode: state.sortMode, showAllUsers: state.showAllUsers, showInternal: state.showInternal, + showRevisitOnly: state.showRevisitOnly, }), merge: (persisted, current) => { const persistedState = persisted as { @@ -127,6 +132,7 @@ export const useSidebarStore = create()( sortMode?: SidebarStoreState["sortMode"]; showAllUsers?: boolean; showInternal?: boolean; + showRevisitOnly?: boolean; }; return { ...current, @@ -145,6 +151,8 @@ export const useSidebarStore = create()( sortMode: persistedState.sortMode ?? current.sortMode, showAllUsers: persistedState.showAllUsers ?? current.showAllUsers, showInternal: persistedState.showInternal ?? current.showInternal, + showRevisitOnly: + persistedState.showRevisitOnly ?? current.showRevisitOnly, }; }, }, diff --git a/apps/code/src/renderer/features/task-detail/stores/revisitStore.ts b/apps/code/src/renderer/features/task-detail/stores/revisitStore.ts new file mode 100644 index 000000000..c80c19e61 --- /dev/null +++ b/apps/code/src/renderer/features/task-detail/stores/revisitStore.ts @@ -0,0 +1,56 @@ +import { create } from "zustand"; +import { persist } from "zustand/middleware"; + +interface RevisitStoreState { + revisitTaskIds: Set; +} + +interface RevisitStoreActions { + isRevisit: (taskId: string) => boolean; + setRevisit: (taskId: string, on: boolean) => void; + toggle: (taskId: string) => void; +} + +type RevisitStore = RevisitStoreState & RevisitStoreActions; + +export const useRevisitStore = create()( + persist( + (set, get) => ({ + revisitTaskIds: new Set(), + isRevisit: (taskId) => get().revisitTaskIds.has(taskId), + setRevisit: (taskId, on) => + set((state) => { + const next = new Set(state.revisitTaskIds); + if (on) { + next.add(taskId); + } else { + next.delete(taskId); + } + return { revisitTaskIds: next }; + }), + toggle: (taskId) => + set((state) => { + const next = new Set(state.revisitTaskIds); + if (next.has(taskId)) { + next.delete(taskId); + } else { + next.add(taskId); + } + return { revisitTaskIds: next }; + }), + }), + { + name: "revisit-tasks-storage", + partialize: (state) => ({ + revisitTaskIds: Array.from(state.revisitTaskIds), + }), + merge: (persisted, current) => { + const persistedState = persisted as { revisitTaskIds?: string[] }; + return { + ...current, + revisitTaskIds: new Set(persistedState.revisitTaskIds ?? []), + }; + }, + }, + ), +); diff --git a/apps/code/src/shared/types/analytics.ts b/apps/code/src/shared/types/analytics.ts index 8679f18ad..f1e1cce4d 100644 --- a/apps/code/src/shared/types/analytics.ts +++ b/apps/code/src/shared/types/analytics.ts @@ -331,6 +331,18 @@ export interface SetupSkippedProperties { entry_point: "during_scan" | "after_done"; } +// Task revisit + sidebar filter events +export interface TaskRevisitToggledProperties { + task_id: string; + enabled: boolean; +} + +export interface TaskRevisitListFilterChangedProperties { + filter_name: string; + value: string; + previous_value?: string; +} + // Subscription / billing events export interface SubscriptionStartedProperties { plan_key: string; @@ -433,6 +445,10 @@ export const ANALYTICS_EVENTS = { // Subscription events SUBSCRIPTION_STARTED: "Subscription started", SUBSCRIPTION_CANCELLED: "Subscription cancelled", + + // Task revisit + sidebar filter events + TASK_REVISIT_TOGGLED: "Task revisit toggled", + TASK_REVISIT_LIST_FILTER_CHANGED: "Task revisit list filter changed", } as const; // Event property mapping @@ -520,4 +536,8 @@ export type EventPropertyMap = { // Subscription events [ANALYTICS_EVENTS.SUBSCRIPTION_STARTED]: SubscriptionStartedProperties; [ANALYTICS_EVENTS.SUBSCRIPTION_CANCELLED]: SubscriptionCancelledProperties; + + // Task revisit + sidebar filter events + [ANALYTICS_EVENTS.TASK_REVISIT_TOGGLED]: TaskRevisitToggledProperties; + [ANALYTICS_EVENTS.TASK_REVISIT_LIST_FILTER_CHANGED]: TaskRevisitListFilterChangedProperties; }; From accc4c9f6a165fe64cc6b9886e51eea8158100a7 Mon Sep 17 00:00:00 2001 From: Siddharth Jain Date: Fri, 8 May 2026 14:46:04 +0530 Subject: [PATCH 2/9] test: add revisit store and revisit filter tests - revisitStore: toggle, setRevisit (idempotent), persist + rehydrate. - applyRevisitFilter: pure helper extracted from useSidebarData so the filter narrowing can be tested without mocking the full hook surface. Generated-By: PostHog Code Task-Id: c63c2352-b983-4807-8b6c-029bb97ab9f5 --- .../features/sidebar/hooks/useSidebarData.ts | 6 +- .../sidebar/utils/applyRevisitFilter.test.ts | 70 ++++++++++++++++ .../sidebar/utils/applyRevisitFilter.ts | 10 +++ .../task-detail/stores/revisitStore.test.ts | 80 +++++++++++++++++++ 4 files changed, 162 insertions(+), 4 deletions(-) create mode 100644 apps/code/src/renderer/features/sidebar/utils/applyRevisitFilter.test.ts create mode 100644 apps/code/src/renderer/features/sidebar/utils/applyRevisitFilter.ts create mode 100644 apps/code/src/renderer/features/task-detail/stores/revisitStore.test.ts diff --git a/apps/code/src/renderer/features/sidebar/hooks/useSidebarData.ts b/apps/code/src/renderer/features/sidebar/hooks/useSidebarData.ts index 178c0d829..f6d0fa318 100644 --- a/apps/code/src/renderer/features/sidebar/hooks/useSidebarData.ts +++ b/apps/code/src/renderer/features/sidebar/hooks/useSidebarData.ts @@ -10,6 +10,7 @@ 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, @@ -260,10 +261,7 @@ export function useSidebarData({ ]); const filteredTaskData = useMemo( - () => - showRevisitOnly - ? taskData.filter((task) => revisitTaskIds.has(task.id)) - : taskData, + () => applyRevisitFilter(taskData, showRevisitOnly, revisitTaskIds), [taskData, showRevisitOnly, revisitTaskIds], ); diff --git a/apps/code/src/renderer/features/sidebar/utils/applyRevisitFilter.test.ts b/apps/code/src/renderer/features/sidebar/utils/applyRevisitFilter.test.ts new file mode 100644 index 000000000..9599a50bc --- /dev/null +++ b/apps/code/src/renderer/features/sidebar/utils/applyRevisitFilter.test.ts @@ -0,0 +1,70 @@ +import { describe, expect, it } from "vitest"; +import type { TaskData } from "../hooks/useSidebarData"; +import { applyRevisitFilter } from "./applyRevisitFilter"; + +function makeTask(overrides: Partial = {}): TaskData { + return { + id: "task-1", + title: "Test task", + createdAt: 1, + lastActivityAt: 1, + isGenerating: false, + isUnread: false, + isPinned: false, + needsPermission: false, + repository: null, + isSuspended: false, + folderPath: null, + cloudPrUrl: null, + branchName: null, + linkedBranch: null, + ...overrides, + }; +} + +describe("applyRevisitFilter", () => { + it("returns input unchanged when showRevisitOnly is false", () => { + const tasks = [makeTask({ id: "a" }), makeTask({ id: "b" })]; + const result = applyRevisitFilter(tasks, false, new Set(["a"])); + expect(result).toBe(tasks); + }); + + it("filters to only tasks marked for revisit when showRevisitOnly is true", () => { + const tasks = [ + makeTask({ id: "a" }), + makeTask({ id: "b" }), + makeTask({ id: "c" }), + ]; + const result = applyRevisitFilter(tasks, true, new Set(["a", "c"])); + expect(result.map((t) => t.id)).toEqual(["a", "c"]); + }); + + it("returns empty array when showRevisitOnly is true and no tasks are marked", () => { + const tasks = [makeTask({ id: "a" }), makeTask({ id: "b" })]; + const result = applyRevisitFilter(tasks, true, new Set()); + expect(result).toEqual([]); + }); + + it("preserves pinned tasks that are also marked for revisit", () => { + const tasks = [ + makeTask({ id: "a", isPinned: true }), + makeTask({ id: "b", isPinned: true }), + makeTask({ id: "c", isPinned: false }), + ]; + const result = applyRevisitFilter(tasks, true, new Set(["a", "c"])); + expect(result.map((t) => t.id)).toEqual(["a", "c"]); + expect(result.find((t) => t.id === "a")?.isPinned).toBe(true); + }); + + it("returns empty array for empty input regardless of filter", () => { + expect(applyRevisitFilter([], true, new Set(["a"]))).toEqual([]); + expect(applyRevisitFilter([], false, new Set(["a"]))).toEqual([]); + }); + + it("does not mutate the input array", () => { + const tasks = [makeTask({ id: "a" }), makeTask({ id: "b" })]; + const snapshot = [...tasks]; + applyRevisitFilter(tasks, true, new Set(["a"])); + expect(tasks).toEqual(snapshot); + }); +}); diff --git a/apps/code/src/renderer/features/sidebar/utils/applyRevisitFilter.ts b/apps/code/src/renderer/features/sidebar/utils/applyRevisitFilter.ts new file mode 100644 index 000000000..6e9646e54 --- /dev/null +++ b/apps/code/src/renderer/features/sidebar/utils/applyRevisitFilter.ts @@ -0,0 +1,10 @@ +import type { TaskData } from "../hooks/useSidebarData"; + +export function applyRevisitFilter( + tasks: TaskData[], + showRevisitOnly: boolean, + revisitTaskIds: Set, +): TaskData[] { + if (!showRevisitOnly) return tasks; + return tasks.filter((task) => revisitTaskIds.has(task.id)); +} diff --git a/apps/code/src/renderer/features/task-detail/stores/revisitStore.test.ts b/apps/code/src/renderer/features/task-detail/stores/revisitStore.test.ts new file mode 100644 index 000000000..e52b81dc7 --- /dev/null +++ b/apps/code/src/renderer/features/task-detail/stores/revisitStore.test.ts @@ -0,0 +1,80 @@ +import { beforeEach, describe, expect, it } from "vitest"; +import { useRevisitStore } from "./revisitStore"; + +describe("revisitStore", () => { + beforeEach(() => { + localStorage.clear(); + useRevisitStore.setState({ revisitTaskIds: new Set() }); + }); + + it("starts with an empty set", () => { + expect(useRevisitStore.getState().revisitTaskIds.size).toBe(0); + expect(useRevisitStore.getState().isRevisit("task-1")).toBe(false); + }); + + it("setRevisit(true) marks a task and isRevisit returns true", () => { + useRevisitStore.getState().setRevisit("task-1", true); + expect(useRevisitStore.getState().isRevisit("task-1")).toBe(true); + expect(useRevisitStore.getState().revisitTaskIds.has("task-1")).toBe(true); + }); + + it("setRevisit(false) removes a task", () => { + useRevisitStore.getState().setRevisit("task-1", true); + useRevisitStore.getState().setRevisit("task-1", false); + expect(useRevisitStore.getState().isRevisit("task-1")).toBe(false); + }); + + it("setRevisit(true) is idempotent", () => { + useRevisitStore.getState().setRevisit("task-1", true); + useRevisitStore.getState().setRevisit("task-1", true); + expect(useRevisitStore.getState().revisitTaskIds.size).toBe(1); + }); + + it("toggle flips state on and off", () => { + useRevisitStore.getState().toggle("task-1"); + expect(useRevisitStore.getState().isRevisit("task-1")).toBe(true); + useRevisitStore.getState().toggle("task-1"); + expect(useRevisitStore.getState().isRevisit("task-1")).toBe(false); + }); + + it("tracks multiple tasks independently", () => { + useRevisitStore.getState().setRevisit("task-1", true); + useRevisitStore.getState().setRevisit("task-2", true); + useRevisitStore.getState().setRevisit("task-1", false); + expect(useRevisitStore.getState().isRevisit("task-1")).toBe(false); + expect(useRevisitStore.getState().isRevisit("task-2")).toBe(true); + }); + + it("persists marked tasks to localStorage as an array", () => { + useRevisitStore.getState().setRevisit("task-1", true); + useRevisitStore.getState().setRevisit("task-2", true); + const raw = localStorage.getItem("revisit-tasks-storage"); + expect(raw).toBeTruthy(); + const persisted = JSON.parse(raw as string); + expect(persisted.state.revisitTaskIds).toEqual( + expect.arrayContaining(["task-1", "task-2"]), + ); + expect(persisted.state.revisitTaskIds).toHaveLength(2); + }); + + it("rehydrates marked tasks from localStorage as a Set", async () => { + localStorage.setItem( + "revisit-tasks-storage", + JSON.stringify({ + state: { revisitTaskIds: ["task-1", "task-2"] }, + version: 0, + }), + ); + await useRevisitStore.persist.rehydrate(); + const state = useRevisitStore.getState(); + expect(state.revisitTaskIds).toBeInstanceOf(Set); + expect(state.isRevisit("task-1")).toBe(true); + expect(state.isRevisit("task-2")).toBe(true); + expect(state.isRevisit("task-3")).toBe(false); + }); + + it("rehydrates with empty set when no persisted state exists", async () => { + await useRevisitStore.persist.rehydrate(); + expect(useRevisitStore.getState().revisitTaskIds.size).toBe(0); + }); +}); From 1ca80dc100479ed48105b41d00f18fbbca01ee6d Mon Sep 17 00:00:00 2001 From: Siddharth J Date: Fri, 8 May 2026 14:57:01 +0530 Subject: [PATCH 3/9] Update apps/code/src/renderer/features/task-detail/stores/revisitStore.ts Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- .../features/task-detail/stores/revisitStore.ts | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/apps/code/src/renderer/features/task-detail/stores/revisitStore.ts b/apps/code/src/renderer/features/task-detail/stores/revisitStore.ts index c80c19e61..919cba544 100644 --- a/apps/code/src/renderer/features/task-detail/stores/revisitStore.ts +++ b/apps/code/src/renderer/features/task-detail/stores/revisitStore.ts @@ -28,16 +28,7 @@ export const useRevisitStore = create()( } return { revisitTaskIds: next }; }), - toggle: (taskId) => - set((state) => { - const next = new Set(state.revisitTaskIds); - if (next.has(taskId)) { - next.delete(taskId); - } else { - next.add(taskId); - } - return { revisitTaskIds: next }; - }), + toggle: (taskId) => get().setRevisit(taskId, !get().revisitTaskIds.has(taskId)), }), { name: "revisit-tasks-storage", From 58a19efdb0b373c2439d3ad9461310b603f5d5fa Mon Sep 17 00:00:00 2001 From: Siddharth Jain Date: Fri, 8 May 2026 22:22:56 +0530 Subject: [PATCH 4/9] test: parameterise revisit store and filter tests Consolidate setRevisit, rehydrate, and applyRevisitFilter cases into it.each tables per team convention. Same coverage, fewer redundant arrange/act/assert blocks. Generated-By: PostHog Code Task-Id: c63c2352-b983-4807-8b6c-029bb97ab9f5 --- .../sidebar/utils/applyRevisitFilter.test.ts | 120 +++++++++------ .../task-detail/stores/revisitStore.test.ts | 144 ++++++++++++------ 2 files changed, 177 insertions(+), 87 deletions(-) diff --git a/apps/code/src/renderer/features/sidebar/utils/applyRevisitFilter.test.ts b/apps/code/src/renderer/features/sidebar/utils/applyRevisitFilter.test.ts index 9599a50bc..b6b02a3b4 100644 --- a/apps/code/src/renderer/features/sidebar/utils/applyRevisitFilter.test.ts +++ b/apps/code/src/renderer/features/sidebar/utils/applyRevisitFilter.test.ts @@ -23,48 +23,80 @@ function makeTask(overrides: Partial = {}): TaskData { } describe("applyRevisitFilter", () => { - it("returns input unchanged when showRevisitOnly is false", () => { - const tasks = [makeTask({ id: "a" }), makeTask({ id: "b" })]; - const result = applyRevisitFilter(tasks, false, new Set(["a"])); - expect(result).toBe(tasks); - }); - - it("filters to only tasks marked for revisit when showRevisitOnly is true", () => { - const tasks = [ - makeTask({ id: "a" }), - makeTask({ id: "b" }), - makeTask({ id: "c" }), - ]; - const result = applyRevisitFilter(tasks, true, new Set(["a", "c"])); - expect(result.map((t) => t.id)).toEqual(["a", "c"]); - }); - - it("returns empty array when showRevisitOnly is true and no tasks are marked", () => { - const tasks = [makeTask({ id: "a" }), makeTask({ id: "b" })]; - const result = applyRevisitFilter(tasks, true, new Set()); - expect(result).toEqual([]); - }); - - it("preserves pinned tasks that are also marked for revisit", () => { - const tasks = [ - makeTask({ id: "a", isPinned: true }), - makeTask({ id: "b", isPinned: true }), - makeTask({ id: "c", isPinned: false }), - ]; - const result = applyRevisitFilter(tasks, true, new Set(["a", "c"])); - expect(result.map((t) => t.id)).toEqual(["a", "c"]); - expect(result.find((t) => t.id === "a")?.isPinned).toBe(true); - }); - - it("returns empty array for empty input regardless of filter", () => { - expect(applyRevisitFilter([], true, new Set(["a"]))).toEqual([]); - expect(applyRevisitFilter([], false, new Set(["a"]))).toEqual([]); - }); - - it("does not mutate the input array", () => { - const tasks = [makeTask({ id: "a" }), makeTask({ id: "b" })]; - const snapshot = [...tasks]; - applyRevisitFilter(tasks, true, new Set(["a"])); - expect(tasks).toEqual(snapshot); - }); + it.each<{ + name: string; + tasks: TaskData[]; + showRevisitOnly: boolean; + revisitIds: string[]; + expectedIds: string[]; + /** When true, the filter is expected to short-circuit and return the input by reference. */ + expectSameRef?: boolean; + }>([ + { + name: "returns input unchanged when showRevisitOnly is false", + tasks: [makeTask({ id: "a" }), makeTask({ id: "b" })], + showRevisitOnly: false, + revisitIds: ["a"], + expectedIds: ["a", "b"], + expectSameRef: true, + }, + { + name: "filters to only tasks marked for revisit when showRevisitOnly is true", + tasks: [ + makeTask({ id: "a" }), + makeTask({ id: "b" }), + makeTask({ id: "c" }), + ], + showRevisitOnly: true, + revisitIds: ["a", "c"], + expectedIds: ["a", "c"], + }, + { + name: "returns empty array when showRevisitOnly is true and no tasks are marked", + tasks: [makeTask({ id: "a" }), makeTask({ id: "b" })], + showRevisitOnly: true, + revisitIds: [], + expectedIds: [], + }, + { + name: "preserves pinned tasks that are also marked for revisit", + tasks: [ + makeTask({ id: "a", isPinned: true }), + makeTask({ id: "b", isPinned: true }), + makeTask({ id: "c", isPinned: false }), + ], + showRevisitOnly: true, + revisitIds: ["a", "c"], + expectedIds: ["a", "c"], + }, + { + name: "empty input with filter on returns empty", + tasks: [], + showRevisitOnly: true, + revisitIds: ["a"], + expectedIds: [], + }, + { + name: "empty input with filter off returns empty", + tasks: [], + showRevisitOnly: false, + revisitIds: ["a"], + expectedIds: [], + expectSameRef: true, + }, + ])( + "$name", + ({ tasks, showRevisitOnly, revisitIds, expectedIds, expectSameRef }) => { + const snapshot = [...tasks]; + const result = applyRevisitFilter( + tasks, + showRevisitOnly, + new Set(revisitIds), + ); + expect(result.map((t) => t.id)).toEqual(expectedIds); + if (expectSameRef) expect(result).toBe(tasks); + // Filter must never mutate the input array. + expect(tasks).toEqual(snapshot); + }, + ); }); diff --git a/apps/code/src/renderer/features/task-detail/stores/revisitStore.test.ts b/apps/code/src/renderer/features/task-detail/stores/revisitStore.test.ts index e52b81dc7..679862c5c 100644 --- a/apps/code/src/renderer/features/task-detail/stores/revisitStore.test.ts +++ b/apps/code/src/renderer/features/task-detail/stores/revisitStore.test.ts @@ -12,22 +12,66 @@ describe("revisitStore", () => { expect(useRevisitStore.getState().isRevisit("task-1")).toBe(false); }); - it("setRevisit(true) marks a task and isRevisit returns true", () => { - useRevisitStore.getState().setRevisit("task-1", true); - expect(useRevisitStore.getState().isRevisit("task-1")).toBe(true); - expect(useRevisitStore.getState().revisitTaskIds.has("task-1")).toBe(true); - }); - - it("setRevisit(false) removes a task", () => { - useRevisitStore.getState().setRevisit("task-1", true); - useRevisitStore.getState().setRevisit("task-1", false); - expect(useRevisitStore.getState().isRevisit("task-1")).toBe(false); - }); - - it("setRevisit(true) is idempotent", () => { - useRevisitStore.getState().setRevisit("task-1", true); - useRevisitStore.getState().setRevisit("task-1", true); - expect(useRevisitStore.getState().revisitTaskIds.size).toBe(1); + describe("setRevisit", () => { + it.each<{ + name: string; + ops: Array<["setRevisit", string, boolean]>; + expected: { id: string; revisit: boolean }[]; + expectedSize: number; + }>([ + { + name: "setRevisit(true) marks a task", + ops: [["setRevisit", "task-1", true]], + expected: [{ id: "task-1", revisit: true }], + expectedSize: 1, + }, + { + name: "setRevisit(false) on unmarked task is a no-op", + ops: [["setRevisit", "task-1", false]], + expected: [{ id: "task-1", revisit: false }], + expectedSize: 0, + }, + { + name: "setRevisit(false) removes a previously marked task", + ops: [ + ["setRevisit", "task-1", true], + ["setRevisit", "task-1", false], + ], + expected: [{ id: "task-1", revisit: false }], + expectedSize: 0, + }, + { + name: "setRevisit(true) is idempotent", + ops: [ + ["setRevisit", "task-1", true], + ["setRevisit", "task-1", true], + ], + expected: [{ id: "task-1", revisit: true }], + expectedSize: 1, + }, + { + name: "tracks multiple tasks independently", + ops: [ + ["setRevisit", "task-1", true], + ["setRevisit", "task-2", true], + ["setRevisit", "task-1", false], + ], + expected: [ + { id: "task-1", revisit: false }, + { id: "task-2", revisit: true }, + ], + expectedSize: 1, + }, + ])("$name", ({ ops, expected, expectedSize }) => { + for (const [, taskId, on] of ops) { + useRevisitStore.getState().setRevisit(taskId, on); + } + const state = useRevisitStore.getState(); + expect(state.revisitTaskIds.size).toBe(expectedSize); + for (const { id, revisit } of expected) { + expect(state.isRevisit(id)).toBe(revisit); + } + }); }); it("toggle flips state on and off", () => { @@ -37,14 +81,6 @@ describe("revisitStore", () => { expect(useRevisitStore.getState().isRevisit("task-1")).toBe(false); }); - it("tracks multiple tasks independently", () => { - useRevisitStore.getState().setRevisit("task-1", true); - useRevisitStore.getState().setRevisit("task-2", true); - useRevisitStore.getState().setRevisit("task-1", false); - expect(useRevisitStore.getState().isRevisit("task-1")).toBe(false); - expect(useRevisitStore.getState().isRevisit("task-2")).toBe(true); - }); - it("persists marked tasks to localStorage as an array", () => { useRevisitStore.getState().setRevisit("task-1", true); useRevisitStore.getState().setRevisit("task-2", true); @@ -57,24 +93,46 @@ describe("revisitStore", () => { expect(persisted.state.revisitTaskIds).toHaveLength(2); }); - it("rehydrates marked tasks from localStorage as a Set", async () => { - localStorage.setItem( - "revisit-tasks-storage", - JSON.stringify({ - state: { revisitTaskIds: ["task-1", "task-2"] }, - version: 0, - }), - ); - await useRevisitStore.persist.rehydrate(); - const state = useRevisitStore.getState(); - expect(state.revisitTaskIds).toBeInstanceOf(Set); - expect(state.isRevisit("task-1")).toBe(true); - expect(state.isRevisit("task-2")).toBe(true); - expect(state.isRevisit("task-3")).toBe(false); - }); - - it("rehydrates with empty set when no persisted state exists", async () => { - await useRevisitStore.persist.rehydrate(); - expect(useRevisitStore.getState().revisitTaskIds.size).toBe(0); + describe("rehydrate", () => { + it.each<{ + name: string; + seed: string[] | null; + expectedSize: number; + checks: Array<[string, boolean]>; + }>([ + { + name: "from persisted ids restores a Set", + seed: ["task-1", "task-2"], + expectedSize: 2, + checks: [ + ["task-1", true], + ["task-2", true], + ["task-3", false], + ], + }, + { + name: "with no persisted state yields an empty Set", + seed: null, + expectedSize: 0, + checks: [["task-1", false]], + }, + ])("$name", async ({ seed, expectedSize, checks }) => { + if (seed) { + localStorage.setItem( + "revisit-tasks-storage", + JSON.stringify({ + state: { revisitTaskIds: seed }, + version: 0, + }), + ); + } + await useRevisitStore.persist.rehydrate(); + const state = useRevisitStore.getState(); + expect(state.revisitTaskIds).toBeInstanceOf(Set); + expect(state.revisitTaskIds.size).toBe(expectedSize); + for (const [id, revisit] of checks) { + expect(state.isRevisit(id)).toBe(revisit); + } + }); }); }); From c1356c0ea6620fadf3e9f0f67bdb68135df488f2 Mon Sep 17 00:00:00 2001 From: Siddharth Jain Date: Fri, 8 May 2026 22:47:47 +0530 Subject: [PATCH 5/9] refactor: move revisit toggle into sidebar right-click menu, rename to "Mark as unread" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the persistent toggle above the chat input with a right-click menu item on each sidebar task ("Mark as unread" / "Unmark as unread"). Behavior is unchanged — yellow chat-bubble icon, sidebar filter, persisted per-device — but the chrome above the chat input is gone. - Add `mark-as-unread` action to the task context menu schema + service. - `useTaskContextMenu` accepts `isMarkedAsUnread` + `onToggleMarkAsUnread`. - `SidebarMenu` reads + toggles via `useRevisitStore`, fires analytics. - Drop `RevisitToggleInline` from `SessionView`; keep the keyboard shortcut (Cmd+Shift+M) via a small `useMarkAsUnreadShortcut` hook so the active task can still be toggled without the right-click flow. - Update copy: tooltip on the yellow icon now reads "Marked as unread"; filter dropdown uses "Marked as unread only"; shortcut sheet entry is "Mark as unread"; shortcut constant renamed to `TOGGLE_MARK_AS_UNREAD`. - Rename analytics events: - `TASK_REVISIT_TOGGLED` -> `TASK_MARK_AS_UNREAD_TOGGLED` ("Task mark as unread toggled") - `TASK_REVISIT_LIST_FILTER_CHANGED` -> `TASK_UNREAD_LIST_FILTER_CHANGED` ("Task unread list filter changed") Internal store name (`revisitStore`) is kept to limit churn; only the user-facing surface and event names change. Generated-By: PostHog Code Task-Id: c63c2352-b983-4807-8b6c-029bb97ab9f5 --- .../src/main/services/context-menu/schemas.ts | 2 + .../src/main/services/context-menu/service.ts | 4 ++ .../renderer/constants/keyboard-shortcuts.ts | 8 +-- .../sessions/components/SessionView.tsx | 63 +++++-------------- .../sidebar/components/SidebarMenu.tsx | 19 ++++++ .../sidebar/components/TaskListView.tsx | 14 ++--- .../sidebar/components/items/TaskItem.tsx | 2 +- .../src/renderer/hooks/useTaskContextMenu.ts | 8 +++ apps/code/src/shared/types/analytics.ts | 18 +++--- 9 files changed, 69 insertions(+), 69 deletions(-) diff --git a/apps/code/src/main/services/context-menu/schemas.ts b/apps/code/src/main/services/context-menu/schemas.ts index 2db37c6e9..306c437b0 100644 --- a/apps/code/src/main/services/context-menu/schemas.ts +++ b/apps/code/src/main/services/context-menu/schemas.ts @@ -8,6 +8,7 @@ export const taskContextMenuInput = z.object({ isSuspended: z.boolean().optional(), isInCommandCenter: z.boolean().optional(), hasEmptyCommandCenterCell: z.boolean().optional(), + isMarkedAsUnread: z.boolean().optional(), }); export const archivedTaskContextMenuInput = z.object({ @@ -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("mark-as-unread") }), z.object({ type: z.literal("external-app"), action: externalAppAction }), ]); diff --git a/apps/code/src/main/services/context-menu/service.ts b/apps/code/src/main/services/context-menu/service.ts index 24d3dbc62..3b6b34038 100644 --- a/apps/code/src/main/services/context-menu/service.ts +++ b/apps/code/src/main/services/context-menu/service.ts @@ -111,12 +111,16 @@ export class ContextMenuService { isSuspended, isInCommandCenter, hasEmptyCommandCenterCell, + isMarkedAsUnread, } = input; const { apps, lastUsedAppId } = await this.getExternalAppsData(); const hasPath = worktreePath || folderPath; return this.showMenu([ this.item(isPinned ? "Unpin" : "Pin", { type: "pin" }), + this.item(isMarkedAsUnread ? "Unmark as unread" : "Mark as unread", { + type: "mark-as-unread", + }), this.item("Rename", { type: "rename" }), ...(worktreePath ? [ diff --git a/apps/code/src/renderer/constants/keyboard-shortcuts.ts b/apps/code/src/renderer/constants/keyboard-shortcuts.ts index 650341d2c..616134880 100644 --- a/apps/code/src/renderer/constants/keyboard-shortcuts.ts +++ b/apps/code/src/renderer/constants/keyboard-shortcuts.ts @@ -24,7 +24,7 @@ export const SHORTCUTS = { FIND_IN_CONVERSATION: "mod+f", BLUR: "escape", SUBMIT_BLUR: "mod+enter", - TOGGLE_REVISIT: "mod+shift+m", + TOGGLE_MARK_AS_UNREAD: "mod+shift+m", } as const; export type ShortcutCategory = "general" | "navigation" | "panels" | "editor"; @@ -162,9 +162,9 @@ export const KEYBOARD_SHORTCUTS: KeyboardShortcut[] = [ context: "Task detail", }, { - id: "toggle-revisit", - keys: SHORTCUTS.TOGGLE_REVISIT, - description: "Toggle revisit task", + id: "toggle-mark-as-unread", + keys: SHORTCUTS.TOGGLE_MARK_AS_UNREAD, + description: "Mark as unread", category: "panels", context: "Task detail", }, diff --git a/apps/code/src/renderer/features/sessions/components/SessionView.tsx b/apps/code/src/renderer/features/sessions/components/SessionView.tsx index 1f19398d6..052419d4d 100644 --- a/apps/code/src/renderer/features/sessions/components/SessionView.tsx +++ b/apps/code/src/renderer/features/sessions/components/SessionView.tsx @@ -1,6 +1,5 @@ import { isOtherOption } from "@components/action-selector/constants"; import { PermissionSelector } from "@components/permissions/PermissionSelector"; -import { Tooltip } from "@components/ui/Tooltip"; import { PromptInput, type EditorHandle as PromptInputHandle, @@ -21,11 +20,8 @@ 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, Switch, Text } from "@radix-ui/themes"; -import { - formatHotkey, - SHORTCUTS, -} from "@renderer/constants/keyboard-shortcuts"; +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"; @@ -104,50 +100,23 @@ function resolveAllowAlwaysUpgradeMode( return undefined; } -function RevisitToggleInline({ taskId }: { taskId: string }) { - const isRevisit = useRevisitStore((s) => s.revisitTaskIds.has(taskId)); - const setRevisit = useRevisitStore((s) => s.setRevisit); - - const applyChange = useCallback( - (next: boolean) => { - setRevisit(taskId, next); - track(ANALYTICS_EVENTS.TASK_REVISIT_TOGGLED, { - task_id: taskId, - enabled: next, - }); - }, - [taskId, setRevisit], - ); - +function useMarkAsUnreadShortcut(taskId: string | undefined) { + const toggle = useRevisitStore((s) => s.toggle); + const revisitTaskIds = useRevisitStore((s) => s.revisitTaskIds); useHotkeys( - SHORTCUTS.TOGGLE_REVISIT, + SHORTCUTS.TOGGLE_MARK_AS_UNREAD, (e) => { + if (!taskId) return; e.preventDefault(); - applyChange(!isRevisit); + const wasMarked = revisitTaskIds.has(taskId); + toggle(taskId); + track(ANALYTICS_EVENTS.TASK_MARK_AS_UNREAD_TOGGLED, { + task_id: taskId, + enabled: !wasMarked, + }); }, { enableOnFormTags: true, enableOnContentEditable: true }, - [isRevisit, applyChange], - ); - - return ( - - - - - Revisit - - - - - + [taskId, toggle, revisitTaskIds], ); } @@ -180,6 +149,7 @@ export function SessionView({ isActiveSession = true, hideInput = false, }: SessionViewProps) { + useMarkAsUnreadShortcut(taskId); const showRawLogs = useShowRawLogs(); const { setShowRawLogs } = useSessionViewActions(); const pendingPermissions = usePendingPermissionsForTask(taskId); @@ -669,9 +639,6 @@ export function SessionView({ : { maxWidth: CHAT_CONTENT_MAX_WIDTH } } > - {taskId ? ( - - ) : null} s.toggle); const hasCompletedSetup = useOnboardingStore( (state) => state.hasCompletedSetup, @@ -172,6 +176,10 @@ function SidebarMenuComponent() { (id) => id == null || !taskMap.has(id), ); + const isMarkedAsUnread = useRevisitStore + .getState() + .revisitTaskIds.has(taskId); + showContextMenu(task, e, { worktreePath: workspace?.worktreePath ?? undefined, folderPath: workspace?.folderPath ?? undefined, @@ -179,8 +187,19 @@ function SidebarMenuComponent() { isSuspended: taskData?.isSuspended, isInCommandCenter, hasEmptyCommandCenterCell, + isMarkedAsUnread, onTogglePin: () => togglePin(taskId), onArchivePrior: handleArchivePrior, + onToggleMarkAsUnread: () => { + const wasMarked = useRevisitStore + .getState() + .revisitTaskIds.has(taskId); + toggleMarkAsUnread(taskId); + track(ANALYTICS_EVENTS.TASK_MARK_AS_UNREAD_TOGGLED, { + task_id: taskId, + enabled: !wasMarked, + }); + }, onAddToCommandCenter: () => { const cells = useCommandCenterStore.getState().cells; const idx = cells.findIndex((id) => id == null || !taskMap.has(id)); diff --git a/apps/code/src/renderer/features/sidebar/components/TaskListView.tsx b/apps/code/src/renderer/features/sidebar/components/TaskListView.tsx index d89d8b015..db95e35fb 100644 --- a/apps/code/src/renderer/features/sidebar/components/TaskListView.tsx +++ b/apps/code/src/renderer/features/sidebar/components/TaskListView.tsx @@ -198,22 +198,22 @@ function TaskFilterMenu() { Filter { - const next = value === "revisit"; - const previous = showRevisitOnly ? "revisit" : "all"; + const next = value === "marked_as_unread"; + const previous = showRevisitOnly ? "marked_as_unread" : "all"; if (previous === value) return; setShowRevisitOnly(next); - track(ANALYTICS_EVENTS.TASK_REVISIT_LIST_FILTER_CHANGED, { - filter_name: "revisit_only", + track(ANALYTICS_EVENTS.TASK_UNREAD_LIST_FILTER_CHANGED, { + filter_name: "marked_as_unread_only", value, previous_value: previous, }); }} > All tasks - - Revisit only + + Marked as unread only diff --git a/apps/code/src/renderer/features/sidebar/components/items/TaskItem.tsx b/apps/code/src/renderer/features/sidebar/components/items/TaskItem.tsx index ad802e018..738bfaa87 100644 --- a/apps/code/src/renderer/features/sidebar/components/items/TaskItem.tsx +++ b/apps/code/src/renderer/features/sidebar/components/items/TaskItem.tsx @@ -279,7 +279,7 @@ export function TaskItem({ ) : isPinned ? ( ) : isRevisit ? ( - + diff --git a/apps/code/src/renderer/hooks/useTaskContextMenu.ts b/apps/code/src/renderer/hooks/useTaskContextMenu.ts index c57a2725d..e2ec2fb3a 100644 --- a/apps/code/src/renderer/hooks/useTaskContextMenu.ts +++ b/apps/code/src/renderer/hooks/useTaskContextMenu.ts @@ -29,9 +29,11 @@ export function useTaskContextMenu() { isSuspended?: boolean; isInCommandCenter?: boolean; hasEmptyCommandCenterCell?: boolean; + isMarkedAsUnread?: boolean; onTogglePin?: () => void; onArchivePrior?: (taskId: string) => void; onAddToCommandCenter?: () => void; + onToggleMarkAsUnread?: () => void; }, ) => { event.preventDefault(); @@ -44,9 +46,11 @@ export function useTaskContextMenu() { isSuspended, isInCommandCenter, hasEmptyCommandCenterCell, + isMarkedAsUnread, onTogglePin, onArchivePrior, onAddToCommandCenter, + onToggleMarkAsUnread, } = options ?? {}; try { @@ -58,6 +62,7 @@ export function useTaskContextMenu() { isSuspended, isInCommandCenter, hasEmptyCommandCenterCell, + isMarkedAsUnread, }); if (!result.action) return; @@ -92,6 +97,9 @@ export function useTaskContextMenu() { case "add-to-command-center": onAddToCommandCenter?.(); break; + case "mark-as-unread": + onToggleMarkAsUnread?.(); + break; case "external-app": { const effectivePath = worktreePath ?? folderPath; if (effectivePath) { diff --git a/apps/code/src/shared/types/analytics.ts b/apps/code/src/shared/types/analytics.ts index f1e1cce4d..57afe8fd6 100644 --- a/apps/code/src/shared/types/analytics.ts +++ b/apps/code/src/shared/types/analytics.ts @@ -331,13 +331,13 @@ export interface SetupSkippedProperties { entry_point: "during_scan" | "after_done"; } -// Task revisit + sidebar filter events -export interface TaskRevisitToggledProperties { +// Mark-as-unread + sidebar filter events +export interface TaskMarkAsUnreadToggledProperties { task_id: string; enabled: boolean; } -export interface TaskRevisitListFilterChangedProperties { +export interface TaskUnreadListFilterChangedProperties { filter_name: string; value: string; previous_value?: string; @@ -446,9 +446,9 @@ export const ANALYTICS_EVENTS = { SUBSCRIPTION_STARTED: "Subscription started", SUBSCRIPTION_CANCELLED: "Subscription cancelled", - // Task revisit + sidebar filter events - TASK_REVISIT_TOGGLED: "Task revisit toggled", - TASK_REVISIT_LIST_FILTER_CHANGED: "Task revisit list filter changed", + // Mark-as-unread + sidebar filter events + TASK_MARK_AS_UNREAD_TOGGLED: "Task mark as unread toggled", + TASK_UNREAD_LIST_FILTER_CHANGED: "Task unread list filter changed", } as const; // Event property mapping @@ -537,7 +537,7 @@ export type EventPropertyMap = { [ANALYTICS_EVENTS.SUBSCRIPTION_STARTED]: SubscriptionStartedProperties; [ANALYTICS_EVENTS.SUBSCRIPTION_CANCELLED]: SubscriptionCancelledProperties; - // Task revisit + sidebar filter events - [ANALYTICS_EVENTS.TASK_REVISIT_TOGGLED]: TaskRevisitToggledProperties; - [ANALYTICS_EVENTS.TASK_REVISIT_LIST_FILTER_CHANGED]: TaskRevisitListFilterChangedProperties; + // Mark-as-unread + sidebar filter events + [ANALYTICS_EVENTS.TASK_MARK_AS_UNREAD_TOGGLED]: TaskMarkAsUnreadToggledProperties; + [ANALYTICS_EVENTS.TASK_UNREAD_LIST_FILTER_CHANGED]: TaskUnreadListFilterChangedProperties; }; From 33b8bd27de74c3e3644b5c90f4d16a7e8bcdf65d Mon Sep 17 00:00:00 2001 From: Siddharth Jain Date: Fri, 8 May 2026 22:56:11 +0530 Subject: [PATCH 6/9] refactor: rename mark-as-unread back to revisit, keep right-click placement MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reverts the user-facing copy from "Mark as unread" to "Revisit" since the unread phrasing collided with the existing isUnread state (green dot on tasks with new activity since last viewed). The right-click menu placement stays — there is no persistent toggle above the chat. - Context menu item: "Mark for revisit" / "Unmark for revisit" - Schema action type: mark-as-unread -> toggle-revisit - Schema input flag: isMarkedAsUnread -> isRevisit - Hook callback: onToggleMarkAsUnread -> onToggleRevisit - Tooltip on the yellow chat-bubble icon: "Marked for revisit" - Filter dropdown radio: "Revisit only" - Shortcut constant: TOGGLE_REVISIT (Cmd+Shift+M kept) - Shortcut sheet entry: "Toggle revisit task" - SessionView shortcut hook: useRevisitShortcut - Analytics events: - TASK_REVISIT_TOGGLED ("Task revisit toggled") - TASK_REVISIT_LIST_FILTER_CHANGED ("Task revisit list filter changed") Generated-By: PostHog Code Task-Id: c63c2352-b983-4807-8b6c-029bb97ab9f5 --- .../src/main/services/context-menu/schemas.ts | 4 ++-- .../src/main/services/context-menu/service.ts | 6 +++--- .../renderer/constants/keyboard-shortcuts.ts | 8 ++++---- .../sessions/components/SessionView.tsx | 8 ++++---- .../sidebar/components/SidebarMenu.tsx | 14 ++++++-------- .../sidebar/components/TaskListView.tsx | 14 +++++++------- .../sidebar/components/items/TaskItem.tsx | 2 +- .../src/renderer/hooks/useTaskContextMenu.ts | 14 +++++++------- apps/code/src/shared/types/analytics.ts | 18 +++++++++--------- 9 files changed, 43 insertions(+), 45 deletions(-) diff --git a/apps/code/src/main/services/context-menu/schemas.ts b/apps/code/src/main/services/context-menu/schemas.ts index 306c437b0..ba0275599 100644 --- a/apps/code/src/main/services/context-menu/schemas.ts +++ b/apps/code/src/main/services/context-menu/schemas.ts @@ -8,7 +8,7 @@ export const taskContextMenuInput = z.object({ isSuspended: z.boolean().optional(), isInCommandCenter: z.boolean().optional(), hasEmptyCommandCenterCell: z.boolean().optional(), - isMarkedAsUnread: z.boolean().optional(), + isRevisit: z.boolean().optional(), }); export const archivedTaskContextMenuInput = z.object({ @@ -43,7 +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("mark-as-unread") }), + z.object({ type: z.literal("toggle-revisit") }), z.object({ type: z.literal("external-app"), action: externalAppAction }), ]); diff --git a/apps/code/src/main/services/context-menu/service.ts b/apps/code/src/main/services/context-menu/service.ts index 3b6b34038..d791430ad 100644 --- a/apps/code/src/main/services/context-menu/service.ts +++ b/apps/code/src/main/services/context-menu/service.ts @@ -111,15 +111,15 @@ export class ContextMenuService { isSuspended, isInCommandCenter, hasEmptyCommandCenterCell, - isMarkedAsUnread, + isRevisit, } = input; const { apps, lastUsedAppId } = await this.getExternalAppsData(); const hasPath = worktreePath || folderPath; return this.showMenu([ this.item(isPinned ? "Unpin" : "Pin", { type: "pin" }), - this.item(isMarkedAsUnread ? "Unmark as unread" : "Mark as unread", { - type: "mark-as-unread", + this.item(isRevisit ? "Unmark for revisit" : "Mark for revisit", { + type: "toggle-revisit", }), this.item("Rename", { type: "rename" }), ...(worktreePath diff --git a/apps/code/src/renderer/constants/keyboard-shortcuts.ts b/apps/code/src/renderer/constants/keyboard-shortcuts.ts index 616134880..650341d2c 100644 --- a/apps/code/src/renderer/constants/keyboard-shortcuts.ts +++ b/apps/code/src/renderer/constants/keyboard-shortcuts.ts @@ -24,7 +24,7 @@ export const SHORTCUTS = { FIND_IN_CONVERSATION: "mod+f", BLUR: "escape", SUBMIT_BLUR: "mod+enter", - TOGGLE_MARK_AS_UNREAD: "mod+shift+m", + TOGGLE_REVISIT: "mod+shift+m", } as const; export type ShortcutCategory = "general" | "navigation" | "panels" | "editor"; @@ -162,9 +162,9 @@ export const KEYBOARD_SHORTCUTS: KeyboardShortcut[] = [ context: "Task detail", }, { - id: "toggle-mark-as-unread", - keys: SHORTCUTS.TOGGLE_MARK_AS_UNREAD, - description: "Mark as unread", + id: "toggle-revisit", + keys: SHORTCUTS.TOGGLE_REVISIT, + description: "Toggle revisit task", category: "panels", context: "Task detail", }, diff --git a/apps/code/src/renderer/features/sessions/components/SessionView.tsx b/apps/code/src/renderer/features/sessions/components/SessionView.tsx index 052419d4d..4aa1a80bf 100644 --- a/apps/code/src/renderer/features/sessions/components/SessionView.tsx +++ b/apps/code/src/renderer/features/sessions/components/SessionView.tsx @@ -100,17 +100,17 @@ function resolveAllowAlwaysUpgradeMode( return undefined; } -function useMarkAsUnreadShortcut(taskId: string | undefined) { +function useRevisitShortcut(taskId: string | undefined) { const toggle = useRevisitStore((s) => s.toggle); const revisitTaskIds = useRevisitStore((s) => s.revisitTaskIds); useHotkeys( - SHORTCUTS.TOGGLE_MARK_AS_UNREAD, + SHORTCUTS.TOGGLE_REVISIT, (e) => { if (!taskId) return; e.preventDefault(); const wasMarked = revisitTaskIds.has(taskId); toggle(taskId); - track(ANALYTICS_EVENTS.TASK_MARK_AS_UNREAD_TOGGLED, { + track(ANALYTICS_EVENTS.TASK_REVISIT_TOGGLED, { task_id: taskId, enabled: !wasMarked, }); @@ -149,7 +149,7 @@ export function SessionView({ isActiveSession = true, hideInput = false, }: SessionViewProps) { - useMarkAsUnreadShortcut(taskId); + useRevisitShortcut(taskId); const showRawLogs = useShowRawLogs(); const { setShowRawLogs } = useSessionViewActions(); const pendingPermissions = usePendingPermissionsForTask(taskId); diff --git a/apps/code/src/renderer/features/sidebar/components/SidebarMenu.tsx b/apps/code/src/renderer/features/sidebar/components/SidebarMenu.tsx index 625b6cdd3..06079de44 100644 --- a/apps/code/src/renderer/features/sidebar/components/SidebarMenu.tsx +++ b/apps/code/src/renderer/features/sidebar/components/SidebarMenu.tsx @@ -65,7 +65,7 @@ function SidebarMenuComponent() { useTaskContextMenu(); const { archiveTask } = useArchiveTask(); const { togglePin } = usePinnedTasks(); - const toggleMarkAsUnread = useRevisitStore((s) => s.toggle); + const toggleRevisit = useRevisitStore((s) => s.toggle); const hasCompletedSetup = useOnboardingStore( (state) => state.hasCompletedSetup, @@ -176,9 +176,7 @@ function SidebarMenuComponent() { (id) => id == null || !taskMap.has(id), ); - const isMarkedAsUnread = useRevisitStore - .getState() - .revisitTaskIds.has(taskId); + const isRevisit = useRevisitStore.getState().revisitTaskIds.has(taskId); showContextMenu(task, e, { worktreePath: workspace?.worktreePath ?? undefined, @@ -187,15 +185,15 @@ function SidebarMenuComponent() { isSuspended: taskData?.isSuspended, isInCommandCenter, hasEmptyCommandCenterCell, - isMarkedAsUnread, + isRevisit, onTogglePin: () => togglePin(taskId), onArchivePrior: handleArchivePrior, - onToggleMarkAsUnread: () => { + onToggleRevisit: () => { const wasMarked = useRevisitStore .getState() .revisitTaskIds.has(taskId); - toggleMarkAsUnread(taskId); - track(ANALYTICS_EVENTS.TASK_MARK_AS_UNREAD_TOGGLED, { + toggleRevisit(taskId); + track(ANALYTICS_EVENTS.TASK_REVISIT_TOGGLED, { task_id: taskId, enabled: !wasMarked, }); diff --git a/apps/code/src/renderer/features/sidebar/components/TaskListView.tsx b/apps/code/src/renderer/features/sidebar/components/TaskListView.tsx index db95e35fb..d89d8b015 100644 --- a/apps/code/src/renderer/features/sidebar/components/TaskListView.tsx +++ b/apps/code/src/renderer/features/sidebar/components/TaskListView.tsx @@ -198,22 +198,22 @@ function TaskFilterMenu() { Filter { - const next = value === "marked_as_unread"; - const previous = showRevisitOnly ? "marked_as_unread" : "all"; + const next = value === "revisit"; + const previous = showRevisitOnly ? "revisit" : "all"; if (previous === value) return; setShowRevisitOnly(next); - track(ANALYTICS_EVENTS.TASK_UNREAD_LIST_FILTER_CHANGED, { - filter_name: "marked_as_unread_only", + track(ANALYTICS_EVENTS.TASK_REVISIT_LIST_FILTER_CHANGED, { + filter_name: "revisit_only", value, previous_value: previous, }); }} > All tasks - - Marked as unread only + + Revisit only diff --git a/apps/code/src/renderer/features/sidebar/components/items/TaskItem.tsx b/apps/code/src/renderer/features/sidebar/components/items/TaskItem.tsx index 738bfaa87..ad802e018 100644 --- a/apps/code/src/renderer/features/sidebar/components/items/TaskItem.tsx +++ b/apps/code/src/renderer/features/sidebar/components/items/TaskItem.tsx @@ -279,7 +279,7 @@ export function TaskItem({ ) : isPinned ? ( ) : isRevisit ? ( - + diff --git a/apps/code/src/renderer/hooks/useTaskContextMenu.ts b/apps/code/src/renderer/hooks/useTaskContextMenu.ts index e2ec2fb3a..6f731ee61 100644 --- a/apps/code/src/renderer/hooks/useTaskContextMenu.ts +++ b/apps/code/src/renderer/hooks/useTaskContextMenu.ts @@ -29,11 +29,11 @@ export function useTaskContextMenu() { isSuspended?: boolean; isInCommandCenter?: boolean; hasEmptyCommandCenterCell?: boolean; - isMarkedAsUnread?: boolean; + isRevisit?: boolean; onTogglePin?: () => void; onArchivePrior?: (taskId: string) => void; onAddToCommandCenter?: () => void; - onToggleMarkAsUnread?: () => void; + onToggleRevisit?: () => void; }, ) => { event.preventDefault(); @@ -46,11 +46,11 @@ export function useTaskContextMenu() { isSuspended, isInCommandCenter, hasEmptyCommandCenterCell, - isMarkedAsUnread, + isRevisit, onTogglePin, onArchivePrior, onAddToCommandCenter, - onToggleMarkAsUnread, + onToggleRevisit, } = options ?? {}; try { @@ -62,7 +62,7 @@ export function useTaskContextMenu() { isSuspended, isInCommandCenter, hasEmptyCommandCenterCell, - isMarkedAsUnread, + isRevisit, }); if (!result.action) return; @@ -97,8 +97,8 @@ export function useTaskContextMenu() { case "add-to-command-center": onAddToCommandCenter?.(); break; - case "mark-as-unread": - onToggleMarkAsUnread?.(); + case "toggle-revisit": + onToggleRevisit?.(); break; case "external-app": { const effectivePath = worktreePath ?? folderPath; diff --git a/apps/code/src/shared/types/analytics.ts b/apps/code/src/shared/types/analytics.ts index 57afe8fd6..f1e1cce4d 100644 --- a/apps/code/src/shared/types/analytics.ts +++ b/apps/code/src/shared/types/analytics.ts @@ -331,13 +331,13 @@ export interface SetupSkippedProperties { entry_point: "during_scan" | "after_done"; } -// Mark-as-unread + sidebar filter events -export interface TaskMarkAsUnreadToggledProperties { +// Task revisit + sidebar filter events +export interface TaskRevisitToggledProperties { task_id: string; enabled: boolean; } -export interface TaskUnreadListFilterChangedProperties { +export interface TaskRevisitListFilterChangedProperties { filter_name: string; value: string; previous_value?: string; @@ -446,9 +446,9 @@ export const ANALYTICS_EVENTS = { SUBSCRIPTION_STARTED: "Subscription started", SUBSCRIPTION_CANCELLED: "Subscription cancelled", - // Mark-as-unread + sidebar filter events - TASK_MARK_AS_UNREAD_TOGGLED: "Task mark as unread toggled", - TASK_UNREAD_LIST_FILTER_CHANGED: "Task unread list filter changed", + // Task revisit + sidebar filter events + TASK_REVISIT_TOGGLED: "Task revisit toggled", + TASK_REVISIT_LIST_FILTER_CHANGED: "Task revisit list filter changed", } as const; // Event property mapping @@ -537,7 +537,7 @@ export type EventPropertyMap = { [ANALYTICS_EVENTS.SUBSCRIPTION_STARTED]: SubscriptionStartedProperties; [ANALYTICS_EVENTS.SUBSCRIPTION_CANCELLED]: SubscriptionCancelledProperties; - // Mark-as-unread + sidebar filter events - [ANALYTICS_EVENTS.TASK_MARK_AS_UNREAD_TOGGLED]: TaskMarkAsUnreadToggledProperties; - [ANALYTICS_EVENTS.TASK_UNREAD_LIST_FILTER_CHANGED]: TaskUnreadListFilterChangedProperties; + // Task revisit + sidebar filter events + [ANALYTICS_EVENTS.TASK_REVISIT_TOGGLED]: TaskRevisitToggledProperties; + [ANALYTICS_EVENTS.TASK_REVISIT_LIST_FILTER_CHANGED]: TaskRevisitListFilterChangedProperties; }; From 4593582e0b35aaf83b666033dca6c8bbe0256a38 Mon Sep 17 00:00:00 2001 From: Siddharth Jain Date: Fri, 8 May 2026 23:02:03 +0530 Subject: [PATCH 7/9] feat: render Revisit context-menu item as a native checkbox toggle Adds checkbox-style menu items to the platform context-menu primitive and uses it for the per-task Revisit toggle. The right-click menu now shows a single "Revisit" entry with a check mark when the task is marked, instead of swapping the label between "Mark for revisit" and "Unmark for revisit". Mirrors the on/off feel of the original switch that lived above the chat input. - Add `checked?: boolean` to `ContextMenuAction` and `ActionItemDef`. - Pass through to Electron's `MenuItemConstructorOptions` as `type: "checkbox"` + `checked`. - Use `checked: isRevisit` for the Revisit menu item; label is now just "Revisit". Generated-By: PostHog Code Task-Id: c63c2352-b983-4807-8b6c-029bb97ab9f5 --- .../src/main/platform-adapters/electron-context-menu.ts | 4 ++++ apps/code/src/main/services/context-menu/service.ts | 9 ++++++--- apps/code/src/main/services/context-menu/types.ts | 2 ++ packages/platform/src/context-menu.ts | 2 ++ 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/apps/code/src/main/platform-adapters/electron-context-menu.ts b/apps/code/src/main/platform-adapters/electron-context-menu.ts index 4a38374c4..df4852d82 100644 --- a/apps/code/src/main/platform-adapters/electron-context-menu.ts +++ b/apps/code/src/main/platform-adapters/electron-context-menu.ts @@ -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); } diff --git a/apps/code/src/main/services/context-menu/service.ts b/apps/code/src/main/services/context-menu/service.ts index d791430ad..fbc243822 100644 --- a/apps/code/src/main/services/context-menu/service.ts +++ b/apps/code/src/main/services/context-menu/service.ts @@ -118,9 +118,11 @@ export class ContextMenuService { return this.showMenu([ this.item(isPinned ? "Unpin" : "Pin", { type: "pin" }), - this.item(isRevisit ? "Unmark for revisit" : "Mark for revisit", { - type: "toggle-revisit", - }), + this.item( + "Revisit", + { type: "toggle-revisit" }, + { checked: isRevisit ?? false }, + ), this.item("Rename", { type: "rename" }), ...(worktreePath ? [ @@ -340,6 +342,7 @@ export class ContextMenuService { enabled: def.enabled, accelerator: def.accelerator, icon: def.icon, + checked: def.checked, click, }; } diff --git a/apps/code/src/main/services/context-menu/types.ts b/apps/code/src/main/services/context-menu/types.ts index c76c93d9d..86b3fc99b 100644 --- a/apps/code/src/main/services/context-menu/types.ts +++ b/apps/code/src/main/services/context-menu/types.ts @@ -13,6 +13,8 @@ export interface ActionItemDef { 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 { diff --git a/packages/platform/src/context-menu.ts b/packages/platform/src/context-menu.ts index 1bb9f3909..deb1f93e0 100644 --- a/packages/platform/src/context-menu.ts +++ b/packages/platform/src/context-menu.ts @@ -4,6 +4,8 @@ export interface ContextMenuAction { enabled?: boolean; accelerator?: string; submenu?: ContextMenuItem[]; + /** When defined, renders as a native checkbox item with a tick when `checked` is true. */ + checked?: boolean; click: () => void | Promise; } From 056d72127930c59bbebe7a1cc0a0222eb6d5fdf1 Mon Sep 17 00:00:00 2001 From: Siddharth Jain Date: Fri, 8 May 2026 23:10:02 +0530 Subject: [PATCH 8/9] fix: address SessionView re-renders and archive cleanup for revisit store - useRevisitShortcut no longer subscribes to revisitTaskIds; reads via useRevisitStore.getState() inside the keypress handler. Prevents the whole SessionView from re-rendering whenever any task's revisit state is toggled elsewhere. - archiveTaskImperative now clears the archived task from the revisit store alongside the existing pin/terminal/command-center cleanup, so the persisted revisitTaskIds Set does not grow unboundedly and an unarchived task does not silently come back marked for revisit. Generated-By: PostHog Code Task-Id: c63c2352-b983-4807-8b6c-029bb97ab9f5 --- .../features/sessions/components/SessionView.tsx | 9 ++++----- .../src/renderer/features/tasks/hooks/useArchiveTask.ts | 2 ++ 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/apps/code/src/renderer/features/sessions/components/SessionView.tsx b/apps/code/src/renderer/features/sessions/components/SessionView.tsx index 4aa1a80bf..54e09cc75 100644 --- a/apps/code/src/renderer/features/sessions/components/SessionView.tsx +++ b/apps/code/src/renderer/features/sessions/components/SessionView.tsx @@ -101,22 +101,21 @@ function resolveAllowAlwaysUpgradeMode( } function useRevisitShortcut(taskId: string | undefined) { - const toggle = useRevisitStore((s) => s.toggle); - const revisitTaskIds = useRevisitStore((s) => s.revisitTaskIds); useHotkeys( SHORTCUTS.TOGGLE_REVISIT, (e) => { if (!taskId) return; e.preventDefault(); - const wasMarked = revisitTaskIds.has(taskId); - toggle(taskId); + 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, toggle, revisitTaskIds], + [taskId], ); } diff --git a/apps/code/src/renderer/features/tasks/hooks/useArchiveTask.ts b/apps/code/src/renderer/features/tasks/hooks/useArchiveTask.ts index 6552a87b2..805309640 100644 --- a/apps/code/src/renderer/features/tasks/hooks/useArchiveTask.ts +++ b/apps/code/src/renderer/features/tasks/hooks/useArchiveTask.ts @@ -1,6 +1,7 @@ import { useCommandCenterStore } from "@features/command-center/stores/commandCenterStore"; import { getSessionService } from "@features/sessions/service/service"; import { pinnedTasksApi } from "@features/sidebar/hooks/usePinnedTasks"; +import { useRevisitStore } from "@features/task-detail/stores/revisitStore"; import { useTerminalStore } from "@features/terminal/stores/terminalStore"; import { workspaceApi } from "@features/workspace/hooks/useWorkspace"; import { trpc, trpcClient } from "@renderer/trpc"; @@ -37,6 +38,7 @@ export async function archiveTaskImperative( pinnedTasksApi.unpin(taskId); useTerminalStore.getState().clearTerminalStatesForTask(taskId); useCommandCenterStore.getState().removeTaskById(taskId); + useRevisitStore.getState().setRevisit(taskId, false); queryClient.setQueryData( trpc.archive.archivedTaskIds.queryKey(), From 40a096a20dd1cb02811b097e8088d76afc7753d1 Mon Sep 17 00:00:00 2001 From: Siddharth Jain Date: Fri, 8 May 2026 23:18:15 +0530 Subject: [PATCH 9/9] fix: clear revisit state when deleting a task Mirrors the unpin/clear-revisit cleanup that already runs on archive, so the persisted revisit-tasks-storage Set does not accumulate IDs of deleted tasks. Generated-By: PostHog Code Task-Id: c63c2352-b983-4807-8b6c-029bb97ab9f5 --- apps/code/src/renderer/features/tasks/hooks/useTasks.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/code/src/renderer/features/tasks/hooks/useTasks.ts b/apps/code/src/renderer/features/tasks/hooks/useTasks.ts index dfd203e3c..e6b21520d 100644 --- a/apps/code/src/renderer/features/tasks/hooks/useTasks.ts +++ b/apps/code/src/renderer/features/tasks/hooks/useTasks.ts @@ -1,4 +1,5 @@ import { pinnedTasksApi } from "@features/sidebar/hooks/usePinnedTasks"; +import { useRevisitStore } from "@features/task-detail/stores/revisitStore"; import { workspaceApi } from "@features/workspace/hooks/useWorkspace"; import { useAuthenticatedMutation } from "@hooks/useAuthenticatedMutation"; import { useAuthenticatedQuery } from "@hooks/useAuthenticatedQuery"; @@ -249,6 +250,7 @@ export function useDeleteTask() { } pinnedTasksApi.unpin(taskId); + useRevisitStore.getState().setRevisit(taskId, false); await mutation.mutateAsync(taskId);