diff --git a/apps/code/src/renderer/features/inbox/components/ReportCard.tsx b/apps/code/src/renderer/features/inbox/components/ReportCard.tsx index bf018b8f3..9c4b70c36 100644 --- a/apps/code/src/renderer/features/inbox/components/ReportCard.tsx +++ b/apps/code/src/renderer/features/inbox/components/ReportCard.tsx @@ -4,7 +4,8 @@ import { inboxStatusAccentCss, inboxStatusLabel, } from "@features/inbox/utils/inboxSort"; -import { Flex, Text } from "@radix-ui/themes"; +import { UserIcon } from "@phosphor-icons/react"; +import { Flex, Text, Tooltip } from "@radix-ui/themes"; import type { SignalReport } from "@shared/types"; import { motion } from "framer-motion"; import type { KeyboardEvent, MouseEvent } from "react"; @@ -118,6 +119,20 @@ export function ReportCard({ {statusLabel} + {report.is_suggested_reviewer && ( + + + + + + )} {/* Summary is outside the title row so wrapped lines align with title text (bullet + gap), not the card edge */} diff --git a/apps/code/src/renderer/features/inbox/components/SignalsToolbar.tsx b/apps/code/src/renderer/features/inbox/components/SignalsToolbar.tsx index 0b8d4d4d3..799eb75a9 100644 --- a/apps/code/src/renderer/features/inbox/components/SignalsToolbar.tsx +++ b/apps/code/src/renderer/features/inbox/components/SignalsToolbar.tsx @@ -1,14 +1,22 @@ import { useInboxSignalsFilterStore } from "@features/inbox/stores/inboxSignalsFilterStore"; +import { + inboxStatusAccentCss, + inboxStatusLabel, +} from "@features/inbox/utils/inboxSort"; import { CalendarPlus, Check, Clock, FunnelSimple as FunnelSimpleIcon, + ListNumbers, MagnifyingGlass, TrendUp, } from "@phosphor-icons/react"; import { Box, Flex, Popover, Text, TextField } from "@radix-ui/themes"; -import type { SignalReportOrderingField } from "@shared/types"; +import type { + SignalReportOrderingField, + SignalReportStatus, +} from "@shared/types"; interface SignalsToolbarProps { totalCount: number; @@ -21,12 +29,21 @@ interface SignalsToolbarProps { type SortOption = { label: string; - field: Extract; + field: Extract< + SignalReportOrderingField, + "priority" | "created_at" | "total_weight" + >; direction: "asc" | "desc"; icon: React.ReactNode; }; const sortOptions: SortOption[] = [ + { + label: "Priority", + field: "priority", + direction: "asc", + icon: , + }, { label: "Strongest signal", field: "total_weight", @@ -47,6 +64,14 @@ const sortOptions: SortOption[] = [ }, ]; +const FILTERABLE_STATUSES: SignalReportStatus[] = [ + "ready", + "pending_input", + "in_progress", + "candidate", + "potential", +]; + export function SignalsToolbar({ totalCount, filteredCount, @@ -60,6 +85,8 @@ export function SignalsToolbar({ const sortField = useInboxSignalsFilterStore((s) => s.sortField); const sortDirection = useInboxSignalsFilterStore((s) => s.sortDirection); const setSort = useInboxSignalsFilterStore((s) => s.setSort); + const statusFilter = useInboxSignalsFilterStore((s) => s.statusFilter); + const toggleStatus = useInboxSignalsFilterStore((s) => s.toggleStatus); const countLabel = isSearchActive ? `${filteredCount} of ${totalCount}` @@ -103,10 +130,12 @@ export function SignalsToolbar({ ) : null} - void; + statusFilter: SignalReportStatus[]; + onToggleStatus: (status: SignalReportStatus) => void; }) { const itemClassName = "flex w-full items-center justify-between rounded-sm px-1 py-1 text-left text-[13px] text-gray-12 transition-colors hover:bg-gray-3"; @@ -144,7 +177,7 @@ function SortMenu({ + ); + })} + + diff --git a/apps/code/src/renderer/features/inbox/stores/inboxSignalsFilterStore.ts b/apps/code/src/renderer/features/inbox/stores/inboxSignalsFilterStore.ts index 4aae45062..a186b420f 100644 --- a/apps/code/src/renderer/features/inbox/stores/inboxSignalsFilterStore.ts +++ b/apps/code/src/renderer/features/inbox/stores/inboxSignalsFilterStore.ts @@ -1,23 +1,37 @@ -import type { SignalReportOrderingField } from "@shared/types"; +import type { + SignalReportOrderingField, + SignalReportStatus, +} from "@shared/types"; import { create } from "zustand"; import { persist } from "zustand/middleware"; type SignalSortField = Extract< SignalReportOrderingField, - "created_at" | "total_weight" + "priority" | "created_at" | "total_weight" >; type SignalSortDirection = "asc" | "desc"; +const DEFAULT_STATUS_FILTER: SignalReportStatus[] = [ + "ready", + "pending_input", + "in_progress", + "candidate", + "potential", +]; + interface InboxSignalsFilterState { sortField: SignalSortField; sortDirection: SignalSortDirection; searchQuery: string; + statusFilter: SignalReportStatus[]; } interface InboxSignalsFilterActions { setSort: (field: SignalSortField, direction: SignalSortDirection) => void; setSearchQuery: (query: string) => void; + setStatusFilter: (statuses: SignalReportStatus[]) => void; + toggleStatus: (status: SignalReportStatus) => void; } type InboxSignalsFilterStore = InboxSignalsFilterState & @@ -26,17 +40,28 @@ type InboxSignalsFilterStore = InboxSignalsFilterState & export const useInboxSignalsFilterStore = create()( persist( (set) => ({ - sortField: "total_weight", - sortDirection: "desc", + sortField: "priority", + sortDirection: "asc", searchQuery: "", + statusFilter: DEFAULT_STATUS_FILTER, setSort: (sortField, sortDirection) => set({ sortField, sortDirection }), setSearchQuery: (searchQuery) => set({ searchQuery }), + setStatusFilter: (statusFilter) => set({ statusFilter }), + toggleStatus: (status) => + set((state) => { + const current = state.statusFilter; + const next = current.includes(status) + ? current.filter((s) => s !== status) + : [...current, status]; + return { statusFilter: next.length > 0 ? next : current }; + }), }), { name: "inbox-signals-filter-storage", partialize: (state) => ({ sortField: state.sortField, sortDirection: state.sortDirection, + statusFilter: state.statusFilter, }), }, ), diff --git a/apps/code/src/renderer/features/inbox/utils/filterReports.ts b/apps/code/src/renderer/features/inbox/utils/filterReports.ts index 56c74e6a5..43331f2a8 100644 --- a/apps/code/src/renderer/features/inbox/utils/filterReports.ts +++ b/apps/code/src/renderer/features/inbox/utils/filterReports.ts @@ -1,4 +1,8 @@ -import type { SignalReport, SignalReportOrderingField } from "@shared/types"; +import type { + SignalReport, + SignalReportOrderingField, + SignalReportStatus, +} from "@shared/types"; export function filterReportsBySearch( reports: SignalReport[], @@ -16,13 +20,22 @@ export function filterReportsBySearch( } /** - * Comma-separated `ordering` for the signal report list API: semantic `status` rank - * then the toolbar field (matches default inbox UX). + * Build a comma-separated status filter string for the API from an array of statuses. + */ +export function buildStatusFilterParam(statuses: SignalReportStatus[]): string { + return statuses.join(","); +} + +/** + * Comma-separated `ordering` for the signal report list API: + * 1. Status rank (ready first — semantic server-side rank, always applied) + * 2. Suggested reviewer (current user's reports first) + * 3. Toolbar-selected field (priority, total_weight, created_at, etc.) */ export function buildSignalReportListOrdering( field: SignalReportOrderingField, direction: "asc" | "desc", ): string { - const secondary = direction === "desc" ? `-${field}` : field; - return `status,${secondary}`; + const fieldKey = direction === "desc" ? `-${field}` : field; + return `status,-is_suggested_reviewer,${fieldKey}`; }