diff --git a/packages/core/src/sidebar/buildSidebarData.ts b/packages/core/src/sidebar/buildSidebarData.ts index 43edfc52b2..39bb00e14e 100644 --- a/packages/core/src/sidebar/buildSidebarData.ts +++ b/packages/core/src/sidebar/buildSidebarData.ts @@ -1,3 +1,4 @@ +import type { WorkspaceMode } from "@posthog/shared"; import type { Task, TaskRunStatus } from "@posthog/shared/domain-types"; import { getRepositoryInfo } from "./groupTasks"; import type { TaskData } from "./sidebarData.types"; @@ -116,6 +117,7 @@ export interface TaskWorkspace { folderPath?: string | null; branchName?: string | null; linkedBranch?: string | null; + mode?: WorkspaceMode; } export interface TaskTimestamp { @@ -174,6 +176,9 @@ export function deriveTaskData( folderId: workspace?.folderId || undefined, taskRunStatus: session?.cloudStatus ?? task.latest_run?.status ?? undefined, taskRunEnvironment: task.latest_run?.environment ?? undefined, + workspaceMode: + workspace?.mode ?? + (task.latest_run?.environment === "cloud" ? "cloud" : undefined), originProduct, slackThreadUrl, folderPath: workspace?.folderPath ?? null, @@ -183,6 +188,27 @@ export function deriveTaskData( }; } +export const ALL_WORKSPACE_MODES: readonly WorkspaceMode[] = [ + "worktree", + "local", + "cloud", +]; + +/** + * Keeps tasks whose workspace mode is in `enabledModes`. Tasks without a known + * mode always pass so an unclassified task never silently disappears. + */ +export function filterByWorkspaceMode( + tasks: TaskData[], + enabledModes: readonly WorkspaceMode[], +): TaskData[] { + if (enabledModes.length >= ALL_WORKSPACE_MODES.length) return tasks; + return tasks.filter( + (task) => + task.workspaceMode == null || enabledModes.includes(task.workspaceMode), + ); +} + function getSortValue(task: TaskData, sortMode: SortMode): number { return sortMode === "updated" ? task.lastActivityAt : task.createdAt; } diff --git a/packages/core/src/sidebar/filterByWorkspaceMode.test.ts b/packages/core/src/sidebar/filterByWorkspaceMode.test.ts new file mode 100644 index 0000000000..4089df17e7 --- /dev/null +++ b/packages/core/src/sidebar/filterByWorkspaceMode.test.ts @@ -0,0 +1,49 @@ +import { describe, expect, it } from "vitest"; +import { ALL_WORKSPACE_MODES, filterByWorkspaceMode } from "./buildSidebarData"; +import type { TaskData } from "./sidebarData.types"; + +const task = (overrides: Partial): TaskData => ({ + id: "t", + title: "t", + createdAt: 0, + lastActivityAt: 0, + isGenerating: false, + isUnread: false, + isPinned: false, + needsPermission: false, + repository: null, + isSuspended: false, + folderPath: null, + cloudPrUrl: null, + branchName: null, + linkedBranch: null, + ...overrides, +}); + +describe("filterByWorkspaceMode", () => { + const worktree = task({ id: "w", workspaceMode: "worktree" }); + const local = task({ id: "l", workspaceMode: "local" }); + const cloud = task({ id: "c", workspaceMode: "cloud" }); + const unknown = task({ id: "u", workspaceMode: undefined }); + const tasks = [worktree, local, cloud, unknown]; + + it("returns all tasks when every mode is enabled", () => { + expect(filterByWorkspaceMode(tasks, ALL_WORKSPACE_MODES)).toEqual(tasks); + }); + + it("keeps only tasks whose mode is enabled, plus unknown-mode tasks", () => { + expect(filterByWorkspaceMode(tasks, ["local"])).toEqual([local, unknown]); + }); + + it("keeps multiple enabled modes", () => { + expect(filterByWorkspaceMode(tasks, ["worktree", "cloud"])).toEqual([ + worktree, + cloud, + unknown, + ]); + }); + + it("keeps only unknown-mode tasks when nothing is enabled", () => { + expect(filterByWorkspaceMode(tasks, [])).toEqual([unknown]); + }); +}); diff --git a/packages/core/src/sidebar/sidebarData.types.ts b/packages/core/src/sidebar/sidebarData.types.ts index cf03c1fc6e..6efc20f4e3 100644 --- a/packages/core/src/sidebar/sidebarData.types.ts +++ b/packages/core/src/sidebar/sidebarData.types.ts @@ -1,3 +1,4 @@ +import type { WorkspaceMode } from "@posthog/shared"; import type { TaskRunStatus } from "@posthog/shared/domain-types"; import type { TaskGroup as GenericTaskGroup, @@ -18,6 +19,7 @@ export interface TaskData { folderId?: string; taskRunStatus?: TaskRunStatus; taskRunEnvironment?: "local" | "cloud"; + workspaceMode?: WorkspaceMode; originProduct?: string; slackThreadUrl?: string; folderPath: string | null; diff --git a/packages/ui/src/features/sidebar/components/TasksHeader.tsx b/packages/ui/src/features/sidebar/components/TasksHeader.tsx index ebb6f84e69..5fd6726311 100644 --- a/packages/ui/src/features/sidebar/components/TasksHeader.tsx +++ b/packages/ui/src/features/sidebar/components/TasksHeader.tsx @@ -7,6 +7,7 @@ import { useHostTRPCClient } from "@posthog/host-router/react"; import { Button, DropdownMenu, + DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuRadioGroup, DropdownMenuRadioItem, @@ -14,6 +15,7 @@ import { DropdownMenuTrigger, MenuLabel, } from "@posthog/quill"; +import type { WorkspaceMode } from "@posthog/shared"; import { useMeQuery } from "@posthog/ui/features/auth/useMeQuery"; import { useFolders } from "@posthog/ui/features/folders/useFolders"; import { useSidebarStore } from "@posthog/ui/features/sidebar/sidebarStore"; @@ -25,6 +27,12 @@ import { useState } from "react"; const log = logger.scope("tasks-header"); +const TASK_TYPE_OPTIONS: { value: WorkspaceMode; label: string }[] = [ + { value: "worktree", label: "Worktree" }, + { value: "local", label: "Local" }, + { value: "cloud", label: "Cloud" }, +]; + function AddFolderButton() { const trpcClient = useHostTRPCClient(); const { addFolder } = useFolders(); @@ -82,6 +90,8 @@ function TaskFilterMenu() { const setSortMode = useSidebarStore((state) => state.setSortMode); const setShowAllUsers = useSidebarStore((state) => state.setShowAllUsers); const setShowInternal = useSidebarStore((state) => state.setShowInternal); + const taskTypeFilter = useSidebarStore((state) => state.taskTypeFilter); + const toggleTaskType = useSidebarStore((state) => state.toggleTaskType); const { data: currentUser } = useMeQuery(); const isStaff = currentUser?.is_staff === true; @@ -126,6 +136,20 @@ function TaskFilterMenu() { Updated + + + Task type + {TASK_TYPE_OPTIONS.map((option) => ( + toggleTaskType(option.value)} + > + {option.label} + + ))} + {import.meta.env.DEV && ( <> diff --git a/packages/ui/src/features/sidebar/sidebarStore.ts b/packages/ui/src/features/sidebar/sidebarStore.ts index 37f18f4bd9..d2f5506094 100644 --- a/packages/ui/src/features/sidebar/sidebarStore.ts +++ b/packages/ui/src/features/sidebar/sidebarStore.ts @@ -1,3 +1,5 @@ +import { ALL_WORKSPACE_MODES } from "@posthog/core/sidebar/buildSidebarData"; +import type { WorkspaceMode } from "@posthog/shared"; import { create } from "zustand"; import { persist } from "zustand/middleware"; import { SIDEBAR_MIN_WIDTH } from "./constants"; @@ -14,6 +16,7 @@ interface SidebarStoreState { sortMode: "updated" | "created"; showAllUsers: boolean; showInternal: boolean; + taskTypeFilter: WorkspaceMode[]; } interface SidebarStoreActions { @@ -32,6 +35,7 @@ interface SidebarStoreActions { setSortMode: (mode: SidebarStoreState["sortMode"]) => void; setShowAllUsers: (showAllUsers: boolean) => void; setShowInternal: (showInternal: boolean) => void; + toggleTaskType: (mode: WorkspaceMode) => void; } type SidebarStore = SidebarStoreState & SidebarStoreActions; @@ -50,6 +54,7 @@ export const useSidebarStore = create()( sortMode: "updated", showAllUsers: false, showInternal: false, + taskTypeFilter: [...ALL_WORKSPACE_MODES], setOpen: (open) => set({ open, hasUserSetOpen: true }), setOpenAuto: (open) => set((state) => (state.hasUserSetOpen ? state : { open })), @@ -100,6 +105,12 @@ export const useSidebarStore = create()( setSortMode: (sortMode) => set({ sortMode }), setShowAllUsers: (showAllUsers) => set({ showAllUsers }), setShowInternal: (showInternal) => set({ showInternal }), + toggleTaskType: (mode) => + set((state) => ({ + taskTypeFilter: state.taskTypeFilter.includes(mode) + ? state.taskTypeFilter.filter((m) => m !== mode) + : [...state.taskTypeFilter, mode], + })), }), { name: "sidebar-storage", @@ -114,6 +125,7 @@ export const useSidebarStore = create()( sortMode: state.sortMode, showAllUsers: state.showAllUsers, showInternal: state.showInternal, + taskTypeFilter: state.taskTypeFilter, }), merge: (persisted, current) => { const persistedState = persisted as { @@ -127,6 +139,7 @@ export const useSidebarStore = create()( sortMode?: SidebarStoreState["sortMode"]; showAllUsers?: boolean; showInternal?: boolean; + taskTypeFilter?: WorkspaceMode[]; }; return { ...current, @@ -145,6 +158,8 @@ export const useSidebarStore = create()( sortMode: persistedState.sortMode ?? current.sortMode, showAllUsers: persistedState.showAllUsers ?? current.showAllUsers, showInternal: persistedState.showInternal ?? current.showInternal, + taskTypeFilter: + persistedState.taskTypeFilter ?? current.taskTypeFilter, }; }, }, diff --git a/packages/ui/src/features/sidebar/useSidebarData.ts b/packages/ui/src/features/sidebar/useSidebarData.ts index c05be96909..0340ccd69f 100644 --- a/packages/ui/src/features/sidebar/useSidebarData.ts +++ b/packages/ui/src/features/sidebar/useSidebarData.ts @@ -1,6 +1,7 @@ import { deriveTaskData, type FullTask, + filterByWorkspaceMode, filterVisibleTasks, narrowFullTask, partitionAndSortTasks, @@ -51,6 +52,7 @@ export function useSidebarData({ const organizeMode = useSidebarStore((state) => state.organizeMode); const sortMode = useSidebarStore((state) => state.sortMode); const folderOrder = useSidebarStore((state) => state.folderOrder); + const taskTypeFilter = useSidebarStore((state) => state.taskTypeFilter); const summaryIds = useMemo( () => @@ -166,9 +168,14 @@ export function useSidebarData({ ], ); + const filteredTaskData = useMemo( + () => filterByWorkspaceMode(taskData, taskTypeFilter), + [taskData, taskTypeFilter], + ); + const { pinnedTasks, sortedUnpinnedTasks, totalCount } = useMemo( - () => partitionAndSortTasks(taskData, sortMode), - [taskData, sortMode], + () => partitionAndSortTasks(filteredTaskData, sortMode), + [filteredTaskData, sortMode], ); const { flatTasks, hasMore } = useMemo(