import { type ReactNode, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Link, useLocation, useNavigate } from "@/lib/router"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { INBOX_MINE_ISSUE_STATUS_FILTER } from "@paperclipai/shared"; import { approvalsApi } from "../api/approvals"; import { accessApi } from "../api/access"; import { authApi } from "../api/auth"; import { ApiError } from "../api/client"; import { dashboardApi } from "../api/dashboard"; import { executionWorkspacesApi } from "../api/execution-workspaces"; import { issuesApi } from "../api/issues"; import { agentsApi } from "../api/agents"; import { heartbeatsApi } from "../api/heartbeats"; import { instanceSettingsApi } from "../api/instanceSettings"; import { projectsApi } from "../api/projects"; import { useCompany } from "../context/CompanyContext"; import { useBreadcrumbs } from "../context/BreadcrumbContext"; import { useGeneralSettings } from "../context/GeneralSettingsContext"; import { useSidebar } from "../context/SidebarContext"; import { queryKeys } from "../lib/queryKeys"; import { applyIssueFilters, countActiveIssueFilters, type IssueFilterState, } from "../lib/issue-filters"; import { armIssueDetailInboxQuickArchive, createIssueDetailLocationState, createIssueDetailPath, rememberIssueDetailLocationState, withIssueDetailHeaderSeed, } from "../lib/issueDetailBreadcrumb"; import { hasBlockingShortcutDialog, isKeyboardShortcutTextInputTarget, shouldBlurPageSearchOnEnter, shouldBlurPageSearchOnEscape, } from "../lib/keyboardShortcuts"; import { EmptyState } from "../components/EmptyState"; import { PageSkeleton } from "../components/PageSkeleton"; import { InboxIssueMetaLeading, InboxIssueTrailingColumns, IssueColumnPicker, issueActivityText, issueTrailingColumns, } from "../components/IssueColumns"; import { IssueFiltersPopover } from "../components/IssueFiltersPopover"; import { IssueRow } from "../components/IssueRow"; import { SwipeToArchive } from "../components/SwipeToArchive"; import { StatusIcon } from "../components/StatusIcon"; import { cn } from "../lib/utils"; import { StatusBadge } from "../components/StatusBadge"; import { approvalLabel, defaultTypeIcon, typeIcon } from "../components/ApprovalPayload"; import { timeAgo } from "../lib/timeAgo"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Separator } from "@/components/ui/separator"; import { Tabs } from "@/components/ui/tabs"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Inbox as InboxIcon, AlertTriangle, Check, ChevronRight, Layers, XCircle, X, RotateCcw, UserPlus, Search, ListTree, } from "lucide-react"; import { Input } from "@/components/ui/input"; import { PageTabBar } from "../components/PageTabBar"; import type { Approval, HeartbeatRun, Issue, JoinRequest } from "@paperclipai/shared"; import { ACTIONABLE_APPROVAL_STATUSES, DEFAULT_INBOX_ISSUE_COLUMNS, buildInboxNesting, getAvailableInboxIssueColumns, getApprovalsForTab, getArchivedInboxSearchIssues, getInboxWorkItems, getInboxKeyboardSelectionIndex, getLatestFailedRunsByAgent, matchesInboxIssueSearch, getRecentTouchedIssues, groupInboxWorkItems, isInboxEntityDismissed, isMineInboxTab, loadInboxFilterPreferences, loadInboxIssueColumns, loadInboxNesting, loadInboxWorkItemGroupBy, normalizeInboxIssueColumns, resolveInboxNestingEnabled, resolveIssueWorkspaceName, resolveInboxSelectionIndex, saveInboxFilterPreferences, saveInboxIssueColumns, saveInboxNesting, saveInboxWorkItemGroupBy, type InboxApprovalFilter, type InboxCategoryFilter, type InboxFilterPreferences, type InboxIssueColumn, saveLastInboxTab, shouldShowInboxSection, type InboxTab, type InboxWorkItem, type InboxWorkItemGroupBy, } from "../lib/inbox"; import { useDismissedInboxAlerts, useInboxDismissals, useReadInboxItems } from "../hooks/useInboxBadge"; export { InboxIssueMetaLeading, InboxIssueTrailingColumns } from "../components/IssueColumns"; type SectionKey = | "work_items" | "alerts"; /** A flat navigation entry for keyboard j/k traversal that includes expanded children. */ type NavEntry = | { type: "top"; index: number; item: InboxWorkItem } | { type: "child"; parentIndex: number; issue: Issue }; type InboxGroupedSection = { key: string; label: string | null; displayItems: InboxWorkItem[]; childrenByIssueId: Map; isArchivedSearch: boolean; }; function buildGroupedInboxSections( items: InboxWorkItem[], groupBy: InboxWorkItemGroupBy, nestingEnabled: boolean, options?: { keyPrefix?: string; isArchivedSearch?: boolean }, ): InboxGroupedSection[] { const keyPrefix = options?.keyPrefix ?? ""; const isArchivedSearch = options?.isArchivedSearch ?? false; return groupInboxWorkItems(items, groupBy).map((group) => { const nestedGroup = nestingEnabled && group.items.some((item) => item.kind === "issue") ? buildInboxNesting(group.items) : { displayItems: group.items, childrenByIssueId: new Map() }; return { key: `${keyPrefix}${group.key}`, label: group.label, displayItems: nestedGroup.displayItems, childrenByIssueId: nestedGroup.childrenByIssueId, isArchivedSearch, }; }); } function firstNonEmptyLine(value: string | null | undefined): string | null { if (!value) return null; const line = value.split("\n").map((chunk) => chunk.trim()).find(Boolean); return line ?? null; } function runFailureMessage(run: HeartbeatRun): string { return firstNonEmptyLine(run.error) ?? firstNonEmptyLine(run.stderrExcerpt) ?? "Run exited with an error."; } function approvalStatusLabel(status: Approval["status"]): string { return status.replaceAll("_", " "); } function readIssueIdFromRun(run: HeartbeatRun): string | null { const context = run.contextSnapshot; if (!context) return null; const issueId = context["issueId"]; if (typeof issueId === "string" && issueId.length > 0) return issueId; const taskId = context["taskId"]; if (typeof taskId === "string" && taskId.length > 0) return taskId; return null; } type NonIssueUnreadState = "visible" | "fading" | "hidden" | null; export function FailedRunInboxRow({ run, issueById, agentName: linkedAgentName, issueLinkState, onDismiss, onRetry, isRetrying, unreadState = null, onMarkRead, onArchive, archiveDisabled, selected = false, className, }: { run: HeartbeatRun; issueById: Map; agentName: string | null; issueLinkState: unknown; onDismiss: () => void; onRetry: () => void; isRetrying: boolean; unreadState?: NonIssueUnreadState; onMarkRead?: () => void; onArchive?: () => void; archiveDisabled?: boolean; selected?: boolean; className?: string; }) { const issueId = readIssueIdFromRun(run); const issue = issueId ? issueById.get(issueId) ?? null : null; const displayError = runFailureMessage(run); const showUnreadSlot = unreadState !== null; const showUnreadDot = unreadState === "visible" || unreadState === "fading"; return (
{showUnreadSlot ? ( {showUnreadDot ? ( ) : onArchive ? ( ) : ( ) : null} {!showUnreadSlot &&
{!showUnreadSlot && ( )}
); } function ApprovalInboxRow({ approval, requesterName, onApprove, onReject, isPending, unreadState = null, onMarkRead, onArchive, archiveDisabled, selected = false, className, }: { approval: Approval; requesterName: string | null; onApprove: () => void; onReject: () => void; isPending: boolean; unreadState?: NonIssueUnreadState; onMarkRead?: () => void; onArchive?: () => void; archiveDisabled?: boolean; selected?: boolean; className?: string; }) { const Icon = typeIcon[approval.type] ?? defaultTypeIcon; const label = approvalLabel(approval.type, approval.payload as Record | null); const showResolutionButtons = approval.type !== "budget_override_required" && ACTIONABLE_APPROVAL_STATUSES.has(approval.status); const showUnreadSlot = unreadState !== null; const showUnreadDot = unreadState === "visible" || unreadState === "fading"; return (
{showUnreadSlot ? ( {showUnreadDot ? ( ) : onArchive ? ( ) : ( ) : null} {!showUnreadSlot &&
{showResolutionButtons ? (
) : null}
); } function JoinRequestInboxRow({ joinRequest, onApprove, onReject, isPending, unreadState = null, onMarkRead, onArchive, archiveDisabled, selected = false, className, }: { joinRequest: JoinRequest; onApprove: () => void; onReject: () => void; isPending: boolean; unreadState?: NonIssueUnreadState; onMarkRead?: () => void; onArchive?: () => void; archiveDisabled?: boolean; selected?: boolean; className?: string; }) { const label = joinRequest.requestType === "human" ? "Human join request" : `Agent join request${joinRequest.agentName ? `: ${joinRequest.agentName}` : ""}`; const showUnreadSlot = unreadState !== null; const showUnreadDot = unreadState === "visible" || unreadState === "fading"; return (
{showUnreadSlot ? ( {showUnreadDot ? ( ) : onArchive ? ( ) : ( ) : null}
{!showUnreadSlot &&
); } export function Inbox() { const { selectedCompanyId } = useCompany(); const { setBreadcrumbs } = useBreadcrumbs(); const { isMobile } = useSidebar(); const navigate = useNavigate(); const location = useLocation(); const queryClient = useQueryClient(); const [actionError, setActionError] = useState(null); const { keyboardShortcutsEnabled } = useGeneralSettings(); const { data: experimentalSettings } = useQuery({ queryKey: queryKeys.instance.experimentalSettings, queryFn: () => instanceSettingsApi.getExperimental(), retry: false, }); const [searchQuery, setSearchQuery] = useState(""); const [filterPreferences, setFilterPreferences] = useState( () => loadInboxFilterPreferences(selectedCompanyId), ); const [groupBy, setGroupBy] = useState(() => loadInboxWorkItemGroupBy()); const [visibleIssueColumns, setVisibleIssueColumns] = useState(loadInboxIssueColumns); const { dismissed: dismissedAlerts, dismiss: dismissAlert } = useDismissedInboxAlerts(); const { dismissedAtByKey, dismiss: dismissInboxItem } = useInboxDismissals(selectedCompanyId); const { readItems, markRead: markItemRead, markUnread: markItemUnread } = useReadInboxItems(); const { allCategoryFilter, allApprovalFilter, issueFilters } = filterPreferences; const pathSegment = location.pathname.split("/").pop() ?? "mine"; const tab: InboxTab = pathSegment === "mine" || pathSegment === "recent" || pathSegment === "all" || pathSegment === "unread" ? pathSegment : "mine"; const canArchiveFromTab = isMineInboxTab(tab); const issueLinkState = useMemo( () => createIssueDetailLocationState( "Inbox", `${location.pathname}${location.search}${location.hash}`, "inbox", ), [location.pathname, location.search, location.hash], ); const { data: session } = useQuery({ queryKey: queryKeys.auth.session, queryFn: () => authApi.getSession(), }); const { data: agents } = useQuery({ queryKey: queryKeys.agents.list(selectedCompanyId!), queryFn: () => agentsApi.list(selectedCompanyId!), enabled: !!selectedCompanyId, }); const { data: projects } = useQuery({ queryKey: queryKeys.projects.list(selectedCompanyId!), queryFn: () => projectsApi.list(selectedCompanyId!), enabled: !!selectedCompanyId, }); const { data: labels } = useQuery({ queryKey: queryKeys.issues.labels(selectedCompanyId!), queryFn: () => issuesApi.listLabels(selectedCompanyId!), enabled: !!selectedCompanyId, }); const isolatedWorkspacesEnabled = experimentalSettings?.enableIsolatedWorkspaces === true; const { data: executionWorkspaces = [] } = useQuery({ queryKey: selectedCompanyId ? queryKeys.executionWorkspaces.list(selectedCompanyId) : ["execution-workspaces", "__disabled__"], queryFn: () => executionWorkspacesApi.list(selectedCompanyId!), enabled: !!selectedCompanyId && isolatedWorkspacesEnabled, }); useEffect(() => { setBreadcrumbs([{ label: "Inbox" }]); }, [setBreadcrumbs]); useEffect(() => { saveLastInboxTab(tab); setSelectedIndex(-1); setSearchQuery(""); }, [tab]); const previousSelectedCompanyIdRef = useRef(selectedCompanyId); useEffect(() => { if (previousSelectedCompanyIdRef.current !== selectedCompanyId) { previousSelectedCompanyIdRef.current = selectedCompanyId; setFilterPreferences(loadInboxFilterPreferences(selectedCompanyId)); } }, [selectedCompanyId]); const { data: approvals, isLoading: isApprovalsLoading, error: approvalsError, } = useQuery({ queryKey: queryKeys.approvals.list(selectedCompanyId!), queryFn: () => approvalsApi.list(selectedCompanyId!), enabled: !!selectedCompanyId, }); const { data: joinRequests = [], isLoading: isJoinRequestsLoading, } = useQuery({ queryKey: queryKeys.access.joinRequests(selectedCompanyId!), queryFn: async () => { try { return await accessApi.listJoinRequests(selectedCompanyId!, "pending_approval"); } catch (err) { if (err instanceof ApiError && (err.status === 403 || err.status === 401)) { return []; } throw err; } }, enabled: !!selectedCompanyId, retry: false, }); const { data: dashboard, isLoading: isDashboardLoading } = useQuery({ queryKey: queryKeys.dashboard(selectedCompanyId!), queryFn: () => dashboardApi.summary(selectedCompanyId!), enabled: !!selectedCompanyId, }); const { data: issues, isLoading: isIssuesLoading } = useQuery({ queryKey: [...queryKeys.issues.list(selectedCompanyId!), "with-routine-executions"], queryFn: () => issuesApi.list(selectedCompanyId!, { includeRoutineExecutions: true }), enabled: !!selectedCompanyId, }); const { data: mineIssuesRaw = [], isLoading: isMineIssuesLoading, } = useQuery({ queryKey: [...queryKeys.issues.listMineByMe(selectedCompanyId!), "with-routine-executions"], queryFn: () => issuesApi.list(selectedCompanyId!, { touchedByUserId: "me", inboxArchivedByUserId: "me", status: INBOX_MINE_ISSUE_STATUS_FILTER, includeRoutineExecutions: true, }), enabled: !!selectedCompanyId, }); const { data: touchedIssuesRaw = [], isLoading: isTouchedIssuesLoading, } = useQuery({ queryKey: [...queryKeys.issues.listTouchedByMe(selectedCompanyId!), "with-routine-executions"], queryFn: () => issuesApi.list(selectedCompanyId!, { touchedByUserId: "me", status: INBOX_MINE_ISSUE_STATUS_FILTER, includeRoutineExecutions: true, }), enabled: !!selectedCompanyId, }); const { data: heartbeatRuns, isLoading: isRunsLoading } = useQuery({ queryKey: queryKeys.heartbeats(selectedCompanyId!), queryFn: () => heartbeatsApi.list(selectedCompanyId!), enabled: !!selectedCompanyId, }); const { data: liveRuns } = useQuery({ queryKey: queryKeys.liveRuns(selectedCompanyId!), queryFn: () => heartbeatsApi.liveRunsForCompany(selectedCompanyId!), enabled: !!selectedCompanyId, refetchInterval: 5000, }); const currentUserId = session?.user.id ?? session?.session.userId ?? null; const mineIssues = useMemo(() => getRecentTouchedIssues(mineIssuesRaw), [mineIssuesRaw]); const touchedIssues = useMemo(() => getRecentTouchedIssues(touchedIssuesRaw), [touchedIssuesRaw]); const visibleMineIssues = useMemo( () => applyIssueFilters(mineIssues, issueFilters, currentUserId, true), [mineIssues, issueFilters, currentUserId], ); const visibleTouchedIssues = useMemo( () => applyIssueFilters(touchedIssues, issueFilters, currentUserId, true), [touchedIssues, issueFilters, currentUserId], ); const unreadTouchedIssues = useMemo( () => visibleTouchedIssues.filter((issue) => issue.isUnreadForMe), [visibleTouchedIssues], ); const issuesToRender = useMemo( () => { if (tab === "mine") return visibleMineIssues; if (tab === "unread") return unreadTouchedIssues; return visibleTouchedIssues; }, [tab, visibleMineIssues, visibleTouchedIssues, unreadTouchedIssues], ); const agentById = useMemo(() => { const map = new Map(); for (const agent of agents ?? []) map.set(agent.id, agent.name); return map; }, [agents]); const issueById = useMemo(() => { const map = new Map(); for (const issue of issues ?? []) map.set(issue.id, issue); return map; }, [issues]); const projectById = useMemo(() => { const map = new Map(); for (const project of projects ?? []) { map.set(project.id, { name: project.name, color: project.color }); } return map; }, [projects]); const projectWorkspaceById = useMemo(() => { const map = new Map(); for (const project of projects ?? []) { for (const workspace of project.workspaces ?? []) { map.set(workspace.id, { name: workspace.name }); } } return map; }, [projects]); const defaultProjectWorkspaceIdByProjectId = useMemo(() => { const map = new Map(); for (const project of projects ?? []) { const defaultWorkspaceId = project.executionWorkspacePolicy?.defaultProjectWorkspaceId ?? project.primaryWorkspace?.id ?? null; if (defaultWorkspaceId) map.set(project.id, defaultWorkspaceId); } return map; }, [projects]); const executionWorkspaceById = useMemo(() => { const map = new Map(); for (const workspace of executionWorkspaces) { map.set(workspace.id, { name: workspace.name, mode: workspace.mode, projectWorkspaceId: workspace.projectWorkspaceId ?? null, }); } return map; }, [executionWorkspaces]); const visibleIssueColumnSet = useMemo(() => new Set(visibleIssueColumns), [visibleIssueColumns]); const availableIssueColumns = useMemo( () => getAvailableInboxIssueColumns(isolatedWorkspacesEnabled), [isolatedWorkspacesEnabled], ); const availableIssueColumnSet = useMemo(() => new Set(availableIssueColumns), [availableIssueColumns]); const visibleTrailingIssueColumns = useMemo( () => issueTrailingColumns.filter((column) => visibleIssueColumnSet.has(column) && availableIssueColumnSet.has(column)), [availableIssueColumnSet, visibleIssueColumnSet], ); const failedRuns = useMemo( () => getLatestFailedRunsByAgent(heartbeatRuns ?? []).filter( (r) => !isInboxEntityDismissed(dismissedAtByKey, `run:${r.id}`, r.createdAt), ), [heartbeatRuns, dismissedAtByKey], ); const liveIssueIds = useMemo(() => { const ids = new Set(); for (const run of liveRuns ?? []) { if (run.issueId) ids.add(run.issueId); } return ids; }, [liveRuns]); const approvalsToRender = useMemo(() => { let filtered = getApprovalsForTab(approvals ?? [], tab, allApprovalFilter); if (tab === "mine") { filtered = filtered.filter( (a) => !isInboxEntityDismissed(dismissedAtByKey, `approval:${a.id}`, a.updatedAt), ); } return filtered; }, [approvals, tab, allApprovalFilter, dismissedAtByKey]); const showJoinRequestsCategory = allCategoryFilter === "everything" || allCategoryFilter === "join_requests"; const showTouchedCategory = allCategoryFilter === "everything" || allCategoryFilter === "issues_i_touched"; const showApprovalsCategory = allCategoryFilter === "everything" || allCategoryFilter === "approvals"; const showFailedRunsCategory = allCategoryFilter === "everything" || allCategoryFilter === "failed_runs"; const showAlertsCategory = allCategoryFilter === "everything" || allCategoryFilter === "alerts"; const failedRunsForTab = useMemo(() => { if (tab === "all" && !showFailedRunsCategory) return []; return failedRuns; }, [failedRuns, tab, showFailedRunsCategory]); const joinRequestsForTab = useMemo(() => { if (tab === "all" && !showJoinRequestsCategory) return []; if (tab === "mine") { return joinRequests.filter( (jr) => !isInboxEntityDismissed(dismissedAtByKey, `join:${jr.id}`, jr.updatedAt ?? jr.createdAt), ); } return joinRequests; }, [joinRequests, tab, showJoinRequestsCategory, dismissedAtByKey]); const workItemsToRender = useMemo( () => getInboxWorkItems({ issues: tab === "all" && !showTouchedCategory ? [] : issuesToRender, approvals: tab === "all" && !showApprovalsCategory ? [] : approvalsToRender, failedRuns: failedRunsForTab, joinRequests: joinRequestsForTab, }), [approvalsToRender, issuesToRender, showApprovalsCategory, showTouchedCategory, tab, failedRunsForTab, joinRequestsForTab], ); const filteredWorkItems = useMemo(() => { const q = searchQuery.trim().toLowerCase(); if (!q) return workItemsToRender; return workItemsToRender.filter((item) => { if (item.kind === "issue") { return matchesInboxIssueSearch(item.issue, q, { isolatedWorkspacesEnabled, executionWorkspaceById, projectWorkspaceById, defaultProjectWorkspaceIdByProjectId, }); } if (item.kind === "approval") { const a = item.approval; const label = approvalLabel(a.type, a.payload as Record | null); if (label.toLowerCase().includes(q)) return true; if (a.type.toLowerCase().includes(q)) return true; return false; } if (item.kind === "failed_run") { const run = item.run; const name = agentById.get(run.agentId); if (name?.toLowerCase().includes(q)) return true; const msg = runFailureMessage(run); if (msg.toLowerCase().includes(q)) return true; const issueId = readIssueIdFromRun(run); if (issueId) { const issue = issueById.get(issueId); if (issue?.title.toLowerCase().includes(q)) return true; if (issue?.identifier?.toLowerCase().includes(q)) return true; } return false; } if (item.kind === "join_request") { const jr = item.joinRequest; if (jr.agentName?.toLowerCase().includes(q)) return true; if (jr.capabilities?.toLowerCase().includes(q)) return true; return false; } return false; }); }, [ workItemsToRender, searchQuery, agentById, defaultProjectWorkspaceIdByProjectId, executionWorkspaceById, issueById, isolatedWorkspacesEnabled, projectWorkspaceById, ]); const archivedSearchIssues = useMemo( () => tab === "mine" ? getArchivedInboxSearchIssues({ visibleIssues: visibleMineIssues, searchableIssues: visibleTouchedIssues, query: searchQuery, isolatedWorkspacesEnabled, executionWorkspaceById, projectWorkspaceById, defaultProjectWorkspaceIdByProjectId, }) : [], [ defaultProjectWorkspaceIdByProjectId, executionWorkspaceById, isolatedWorkspacesEnabled, projectWorkspaceById, searchQuery, tab, visibleMineIssues, visibleTouchedIssues, ], ); const archivedSearchIssueIds = useMemo( () => new Set(archivedSearchIssues.map((issue) => issue.id)), [archivedSearchIssues], ); // --- Parent-child nesting for inbox issues --- const [nestingPreferenceEnabled, setNestingPreferenceEnabled] = useState(() => loadInboxNesting()); const nestingEnabled = resolveInboxNestingEnabled(nestingPreferenceEnabled, isMobile); const toggleNesting = useCallback(() => { setNestingPreferenceEnabled((prev) => { const next = !prev; saveInboxNesting(next); return next; }); }, []); const [collapsedInboxParents, setCollapsedInboxParents] = useState>(new Set()); const groupedSections = useMemo(() => [ ...buildGroupedInboxSections(filteredWorkItems, groupBy, nestingEnabled), ...buildGroupedInboxSections( getInboxWorkItems({ issues: archivedSearchIssues, approvals: [] }), groupBy, nestingEnabled, { keyPrefix: "archived-search:", isArchivedSearch: true }, ), ], [archivedSearchIssues, filteredWorkItems, groupBy, nestingEnabled]); const totalVisibleWorkItems = useMemo( () => groupedSections.reduce((count, group) => count + group.displayItems.length, 0), [groupedSections], ); const toggleInboxParentCollapse = useCallback((parentId: string) => { setCollapsedInboxParents((prev) => { const next = new Set(prev); if (next.has(parentId)) next.delete(parentId); else next.add(parentId); return next; }); }, []); // Build flat navigation list including expanded children for keyboard traversal const flatNavItems = useMemo((): NavEntry[] => { const entries: NavEntry[] = []; let topIndex = 0; for (const group of groupedSections) { for (const item of group.displayItems) { entries.push({ type: "top", index: topIndex, item }); if (item.kind === "issue") { const children = group.childrenByIssueId.get(item.issue.id); const isExpanded = children?.length && !collapsedInboxParents.has(item.issue.id); if (isExpanded) { for (const child of children) { entries.push({ type: "child", parentIndex: topIndex, issue: child }); } } } topIndex += 1; } } return entries; }, [groupedSections, collapsedInboxParents]); const agentName = (id: string | null) => { if (!id) return null; return agentById.get(id) ?? null; }; const setIssueColumns = useCallback((next: InboxIssueColumn[]) => { const normalized = normalizeInboxIssueColumns(next); setVisibleIssueColumns(normalized); saveInboxIssueColumns(normalized); }, []); const toggleIssueColumn = useCallback((column: InboxIssueColumn, enabled: boolean) => { if (enabled) { setIssueColumns([...visibleIssueColumns, column]); return; } setIssueColumns(visibleIssueColumns.filter((value) => value !== column)); }, [setIssueColumns, visibleIssueColumns]); const updateFilterPreferences = useCallback( (updater: (previous: InboxFilterPreferences) => InboxFilterPreferences) => { setFilterPreferences((previous) => { const next = updater(previous); saveInboxFilterPreferences(selectedCompanyId, next); return next; }); }, [selectedCompanyId], ); const updateIssueFilters = useCallback((patch: Partial) => { updateFilterPreferences((previous) => ({ ...previous, issueFilters: { ...previous.issueFilters, ...patch }, })); }, [updateFilterPreferences]); const updateAllCategoryFilter = useCallback((value: InboxCategoryFilter) => { updateFilterPreferences((previous) => ({ ...previous, allCategoryFilter: value })); }, [updateFilterPreferences]); const updateAllApprovalFilter = useCallback((value: InboxApprovalFilter) => { updateFilterPreferences((previous) => ({ ...previous, allApprovalFilter: value })); }, [updateFilterPreferences]); const updateGroupBy = useCallback((nextGroupBy: InboxWorkItemGroupBy) => { setGroupBy(nextGroupBy); saveInboxWorkItemGroupBy(nextGroupBy); }, []); const approveMutation = useMutation({ mutationFn: (id: string) => approvalsApi.approve(id), onSuccess: (_approval, id) => { setActionError(null); queryClient.invalidateQueries({ queryKey: queryKeys.approvals.list(selectedCompanyId!) }); navigate(`/approvals/${id}?resolved=approved`); }, onError: (err) => { setActionError(err instanceof Error ? err.message : "Failed to approve"); }, }); const rejectMutation = useMutation({ mutationFn: (id: string) => approvalsApi.reject(id), onSuccess: () => { setActionError(null); queryClient.invalidateQueries({ queryKey: queryKeys.approvals.list(selectedCompanyId!) }); }, onError: (err) => { setActionError(err instanceof Error ? err.message : "Failed to reject"); }, }); const approveJoinMutation = useMutation({ mutationFn: (joinRequest: JoinRequest) => accessApi.approveJoinRequest(selectedCompanyId!, joinRequest.id), onSuccess: () => { setActionError(null); queryClient.invalidateQueries({ queryKey: queryKeys.access.joinRequests(selectedCompanyId!) }); queryClient.invalidateQueries({ queryKey: queryKeys.sidebarBadges(selectedCompanyId!) }); queryClient.invalidateQueries({ queryKey: queryKeys.agents.list(selectedCompanyId!) }); queryClient.invalidateQueries({ queryKey: queryKeys.companies.all }); }, onError: (err) => { setActionError(err instanceof Error ? err.message : "Failed to approve join request"); }, }); const rejectJoinMutation = useMutation({ mutationFn: (joinRequest: JoinRequest) => accessApi.rejectJoinRequest(selectedCompanyId!, joinRequest.id), onSuccess: () => { setActionError(null); queryClient.invalidateQueries({ queryKey: queryKeys.access.joinRequests(selectedCompanyId!) }); queryClient.invalidateQueries({ queryKey: queryKeys.sidebarBadges(selectedCompanyId!) }); }, onError: (err) => { setActionError(err instanceof Error ? err.message : "Failed to reject join request"); }, }); const [retryingRunIds, setRetryingRunIds] = useState>(new Set()); const retryRunMutation = useMutation({ mutationFn: async (run: HeartbeatRun) => { const payload: Record = {}; const context = run.contextSnapshot as Record | null; if (context) { if (typeof context.issueId === "string" && context.issueId) payload.issueId = context.issueId; if (typeof context.taskId === "string" && context.taskId) payload.taskId = context.taskId; if (typeof context.taskKey === "string" && context.taskKey) payload.taskKey = context.taskKey; } const result = await agentsApi.wakeup(run.agentId, { source: "on_demand", triggerDetail: "manual", reason: "retry_failed_run", payload, }); if (!("id" in result)) { throw new Error(result.message ?? "Retry was skipped."); } return { newRun: result, originalRun: run }; }, onMutate: (run) => { setRetryingRunIds((prev) => new Set(prev).add(run.id)); }, onSuccess: ({ newRun, originalRun }) => { queryClient.invalidateQueries({ queryKey: queryKeys.heartbeats(originalRun.companyId) }); queryClient.invalidateQueries({ queryKey: queryKeys.heartbeats(originalRun.companyId, originalRun.agentId) }); navigate(`/agents/${originalRun.agentId}/runs/${newRun.id}`); }, onSettled: (_data, _error, run) => { if (!run) return; setRetryingRunIds((prev) => { const next = new Set(prev); next.delete(run.id); return next; }); }, }); const [fadingOutIssues, setFadingOutIssues] = useState>(new Set()); const [showMarkAllReadConfirm, setShowMarkAllReadConfirm] = useState(false); const [archivingIssueIds, setArchivingIssueIds] = useState>(new Set()); const [fadingNonIssueItems, setFadingNonIssueItems] = useState>(new Set()); const [archivingNonIssueIds, setArchivingNonIssueIds] = useState>(new Set()); const [selectedIndex, setSelectedIndex] = useState(-1); const listRef = useRef(null); const invalidateInboxIssueQueries = () => { if (!selectedCompanyId) return; queryClient.invalidateQueries({ queryKey: queryKeys.issues.listMineByMe(selectedCompanyId) }); queryClient.invalidateQueries({ queryKey: queryKeys.issues.listTouchedByMe(selectedCompanyId) }); queryClient.invalidateQueries({ queryKey: queryKeys.issues.listUnreadTouchedByMe(selectedCompanyId) }); queryClient.invalidateQueries({ queryKey: queryKeys.sidebarBadges(selectedCompanyId) }); }; const archiveIssueMutation = useMutation({ mutationFn: (id: string) => issuesApi.archiveFromInbox(id), onMutate: async (id) => { setActionError(null); setArchivingIssueIds((prev) => new Set(prev).add(id)); // Cancel in-flight refetches so they don't overwrite our optimistic update const queryKeys_ = [ [...queryKeys.issues.listMineByMe(selectedCompanyId!), "with-routine-executions"], [...queryKeys.issues.listTouchedByMe(selectedCompanyId!), "with-routine-executions"], queryKeys.issues.listUnreadTouchedByMe(selectedCompanyId!), ]; await Promise.all(queryKeys_.map((qk) => queryClient.cancelQueries({ queryKey: qk }))); // Snapshot previous data for rollback const previousData = queryKeys_.map((qk) => [qk, queryClient.getQueryData(qk)] as const); // Optimistically remove the issue from all inbox query caches for (const qk of queryKeys_) { queryClient.setQueryData(qk, (old: unknown) => { if (!Array.isArray(old)) return old; return old.filter((issue: { id: string }) => issue.id !== id); }); } return { previousData }; }, onError: (err, id, context) => { setActionError(err instanceof Error ? err.message : "Failed to archive issue"); setArchivingIssueIds((prev) => { const next = new Set(prev); next.delete(id); return next; }); // Restore previous query data on failure if (context?.previousData) { for (const [qk, data] of context.previousData) { queryClient.setQueryData(qk, data); } } }, onSettled: (_data, error, id) => { // Clean up archiving state and refetch to sync with server setArchivingIssueIds((prev) => { const next = new Set(prev); next.delete(id); return next; }); invalidateInboxIssueQueries(); }, }); const markReadMutation = useMutation({ mutationFn: (id: string) => issuesApi.markRead(id), onMutate: (id) => { setFadingOutIssues((prev) => new Set(prev).add(id)); }, onSuccess: () => { invalidateInboxIssueQueries(); }, onSettled: (_data, _error, id) => { setTimeout(() => { setFadingOutIssues((prev) => { const next = new Set(prev); next.delete(id); return next; }); }, 300); }, }); const markAllReadMutation = useMutation({ mutationFn: async (issueIds: string[]) => { await Promise.all(issueIds.map((issueId) => issuesApi.markRead(issueId))); }, onMutate: (issueIds) => { setFadingOutIssues((prev) => { const next = new Set(prev); for (const issueId of issueIds) next.add(issueId); return next; }); }, onSuccess: () => { invalidateInboxIssueQueries(); }, onSettled: (_data, _error, issueIds) => { setTimeout(() => { setFadingOutIssues((prev) => { const next = new Set(prev); for (const issueId of issueIds) next.delete(issueId); return next; }); }, 300); }, }); const markUnreadMutation = useMutation({ mutationFn: (id: string) => issuesApi.markUnread(id), onSuccess: () => { invalidateInboxIssueQueries(); }, }); const handleMarkNonIssueRead = useCallback((key: string) => { setFadingNonIssueItems((prev) => new Set(prev).add(key)); markItemRead(key); setTimeout(() => { setFadingNonIssueItems((prev) => { const next = new Set(prev); next.delete(key); return next; }); }, 300); }, [markItemRead]); const handleArchiveNonIssue = useCallback((key: string) => { setArchivingNonIssueIds((prev) => new Set(prev).add(key)); setTimeout(() => { if (key.startsWith("alert:")) { dismissAlert(key); } else { dismissInboxItem(key); } setArchivingNonIssueIds((prev) => { const next = new Set(prev); next.delete(key); return next; }); }, 200); }, [dismissAlert, dismissInboxItem]); const nonIssueUnreadState = (key: string): NonIssueUnreadState => { if (!canArchiveFromTab) return null; const isRead = readItems.has(key); const isFading = fadingNonIssueItems.has(key); if (isFading) return "fading"; if (!isRead) return "visible"; return "hidden"; }; const getWorkItemKey = useCallback((item: InboxWorkItem): string => { if (item.kind === "issue") return `issue:${item.issue.id}`; if (item.kind === "approval") return `approval:${item.approval.id}`; if (item.kind === "failed_run") return `run:${item.run.id}`; return `join:${item.joinRequest.id}`; }, []); // Keep selection valid when the list shape changes, but do not auto-select on initial load. useEffect(() => { setSelectedIndex((prev) => resolveInboxSelectionIndex(prev, flatNavItems.length)); }, [flatNavItems.length]); // Use refs for keyboard handler to avoid stale closures const kbStateRef = useRef({ workItems: groupedSections, flatNavItems, selectedIndex, canArchive: canArchiveFromTab, archivedSearchIssueIds, archivingIssueIds, archivingNonIssueIds, fadingOutIssues, readItems, }); kbStateRef.current = { workItems: groupedSections, flatNavItems, selectedIndex, canArchive: canArchiveFromTab, archivedSearchIssueIds, archivingIssueIds, archivingNonIssueIds, fadingOutIssues, readItems, }; const kbActionsRef = useRef({ archiveIssue: (id: string) => archiveIssueMutation.mutate(id), archiveNonIssue: handleArchiveNonIssue, markRead: (id: string) => markReadMutation.mutate(id), markUnreadIssue: (id: string) => markUnreadMutation.mutate(id), markNonIssueRead: handleMarkNonIssueRead, markNonIssueUnread: markItemUnread, navigate, }); kbActionsRef.current = { archiveIssue: (id: string) => archiveIssueMutation.mutate(id), archiveNonIssue: handleArchiveNonIssue, markRead: (id: string) => markReadMutation.mutate(id), markUnreadIssue: (id: string) => markUnreadMutation.mutate(id), markNonIssueRead: handleMarkNonIssueRead, markNonIssueUnread: markItemUnread, navigate, }; // Keyboard shortcuts (mail-client style) — single stable listener using refs useEffect(() => { if (!keyboardShortcutsEnabled) return; const handleKeyDown = (e: KeyboardEvent) => { if (e.defaultPrevented) return; // Don't capture when typing in inputs/textareas or with modifier keys const target = e.target; if ( !(target instanceof HTMLElement) || isKeyboardShortcutTextInputTarget(target) || hasBlockingShortcutDialog(document) || e.metaKey || e.ctrlKey || e.altKey ) { return; } const st = kbStateRef.current; const act = kbActionsRef.current; // Keyboard shortcuts are only active on the "mine" tab if (!st.canArchive) return; const navItems = st.flatNavItems; const navCount = navItems.length; if (navCount === 0) return; /** Resolve the nav entry at selectedIndex to an issue (for child entries) or work item. */ const resolveNavEntry = (idx: number): { issue?: Issue; item?: InboxWorkItem } => { const entry = navItems[idx]; if (!entry) return {}; if (entry.type === "child") return { issue: entry.issue }; return { item: entry.item }; }; switch (e.key) { case "j": { e.preventDefault(); setSelectedIndex((prev) => getInboxKeyboardSelectionIndex(prev, navCount, "next")); break; } case "k": { e.preventDefault(); setSelectedIndex((prev) => getInboxKeyboardSelectionIndex(prev, navCount, "previous")); break; } case "a": case "y": { if (st.selectedIndex < 0 || st.selectedIndex >= navCount) return; e.preventDefault(); const { issue, item } = resolveNavEntry(st.selectedIndex); if (issue) { if (!st.archivedSearchIssueIds.has(issue.id) && !st.archivingIssueIds.has(issue.id)) act.archiveIssue(issue.id); } else if (item) { if (item.kind === "issue") { if (!st.archivedSearchIssueIds.has(item.issue.id) && !st.archivingIssueIds.has(item.issue.id)) { act.archiveIssue(item.issue.id); } } else { const key = getWorkItemKey(item); if (!st.archivingNonIssueIds.has(key)) act.archiveNonIssue(key); } } break; } case "U": { if (st.selectedIndex < 0 || st.selectedIndex >= navCount) return; e.preventDefault(); const { issue, item } = resolveNavEntry(st.selectedIndex); if (issue) { act.markUnreadIssue(issue.id); } else if (item) { if (item.kind === "issue") act.markUnreadIssue(item.issue.id); else act.markNonIssueUnread(getWorkItemKey(item)); } break; } case "r": { if (st.selectedIndex < 0 || st.selectedIndex >= navCount) return; e.preventDefault(); const { issue, item } = resolveNavEntry(st.selectedIndex); if (issue) { if (issue.isUnreadForMe && !st.fadingOutIssues.has(issue.id)) act.markRead(issue.id); } else if (item) { if (item.kind === "issue") { if (item.issue.isUnreadForMe && !st.fadingOutIssues.has(item.issue.id)) act.markRead(item.issue.id); } else { const key = getWorkItemKey(item); if (!st.readItems.has(key)) act.markNonIssueRead(key); } } break; } case "Enter": { if (st.selectedIndex < 0 || st.selectedIndex >= navCount) return; e.preventDefault(); const { issue, item } = resolveNavEntry(st.selectedIndex); if (issue) { const pathId = issue.identifier ?? issue.id; const detailState = armIssueDetailInboxQuickArchive(withIssueDetailHeaderSeed(issueLinkState, issue)); rememberIssueDetailLocationState(pathId, detailState); act.navigate(createIssueDetailPath(pathId), { state: detailState }); } else if (item) { if (item.kind === "issue") { const pathId = item.issue.identifier ?? item.issue.id; const detailState = armIssueDetailInboxQuickArchive( withIssueDetailHeaderSeed(issueLinkState, item.issue), ); rememberIssueDetailLocationState(pathId, detailState); act.navigate(createIssueDetailPath(pathId), { state: detailState }); } else if (item.kind === "approval") { act.navigate(`/approvals/${item.approval.id}`); } else if (item.kind === "failed_run") { act.navigate(`/agents/${item.run.agentId}/runs/${item.run.id}`); } } break; } default: return; } }; window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); }, [getWorkItemKey, issueLinkState, keyboardShortcutsEnabled]); // Scroll selected item into view useEffect(() => { if (selectedIndex < 0 || !listRef.current) return; const rows = listRef.current.querySelectorAll("[data-inbox-item]"); const row = rows[selectedIndex]; if (row) row.scrollIntoView({ block: "nearest" }); }, [selectedIndex]); if (!selectedCompanyId) { return ; } const hasRunFailures = failedRuns.length > 0; const showAggregateAgentError = !!dashboard && dashboard.agents.error > 0 && !hasRunFailures && !dismissedAlerts.has("alert:agent-errors"); const showBudgetAlert = !!dashboard && dashboard.costs.monthBudgetCents > 0 && dashboard.costs.monthUtilizationPercent >= 80 && !dismissedAlerts.has("alert:budget"); const hasAlerts = showAggregateAgentError || showBudgetAlert; const showWorkItemsSection = totalVisibleWorkItems > 0; const showAlertsSection = shouldShowInboxSection({ tab, hasItems: hasAlerts, showOnMine: hasAlerts, showOnRecent: hasAlerts, showOnUnread: hasAlerts, showOnAll: showAlertsCategory && hasAlerts, }); const visibleSections = [ showAlertsSection ? "alerts" : null, showWorkItemsSection ? "work_items" : null, ].filter((key): key is SectionKey => key !== null); const allLoaded = !isJoinRequestsLoading && !isApprovalsLoading && !isDashboardLoading && !isIssuesLoading && !isMineIssuesLoading && !isTouchedIssuesLoading && !isRunsLoading; const showSeparatorBefore = (key: SectionKey) => visibleSections.indexOf(key) > 0; const markAllReadIssues = (tab === "mine" ? visibleMineIssues : unreadTouchedIssues) .filter((issue) => issue.isUnreadForMe && !fadingOutIssues.has(issue.id) && !archivingIssueIds.has(issue.id)); const unreadIssueIds = markAllReadIssues .map((issue) => issue.id); const canMarkAllRead = unreadIssueIds.length > 0; const activeIssueFilterCount = countActiveIssueFilters(issueFilters, true); return (
{/* Search — full-width row on mobile, inline on desktop */}
setSearchQuery(e.target.value)} onKeyDown={(e) => { if (shouldBlurPageSearchOnEnter({ key: e.key, isComposing: e.nativeEvent.isComposing, })) { e.currentTarget.blur(); return; } if (shouldBlurPageSearchOnEscape({ key: e.key, isComposing: e.nativeEvent.isComposing, currentValue: e.currentTarget.value, })) { e.currentTarget.blur(); } }} className="h-8 w-full pl-8 text-xs" data-page-search-target="true" />
navigate(`/inbox/${value}`)}>
setSearchQuery(e.target.value)} onKeyDown={(e) => { if (shouldBlurPageSearchOnEnter({ key: e.key, isComposing: e.nativeEvent.isComposing, })) { e.currentTarget.blur(); return; } if (shouldBlurPageSearchOnEscape({ key: e.key, isComposing: e.nativeEvent.isComposing, currentValue: e.currentTarget.value, })) { e.currentTarget.blur(); } }} className="h-8 w-[220px] pl-8 text-xs" data-page-search-target="true" />
({ id: project.id, name: project.name }))} labels={labels?.map((label) => ({ id: label.id, name: label.name, color: label.color }))} currentUserId={currentUserId} enableRoutineVisibilityFilter buttonVariant="outline" iconOnly workspaces={isolatedWorkspacesEnabled ? executionWorkspaces.filter((w) => w.mode === "isolated_workspace" && w.status === "active").map((w) => ({ id: w.id, name: w.name })) : undefined} />
{([ ["none", "None"], ["type", "Type"], ] as const).map(([value, label]) => ( ))}
setIssueColumns(DEFAULT_INBOX_ISSUE_COLUMNS)} title="Choose which inbox columns stay visible" iconOnly /> {canMarkAllRead && ( <> Mark all as read? This will mark {unreadIssueIds.length} unread {unreadIssueIds.length === 1 ? "item" : "items"} as read. )}
{tab === "all" && (
{showApprovalsCategory && ( )}
)} {approvalsError &&

{approvalsError.message}

} {actionError &&

{actionError}

} {!allLoaded && visibleSections.length === 0 && ( )} {allLoaded && visibleSections.length === 0 && ( )} {showWorkItemsSection && ( <> {showSeparatorBefore("work_items") && }
{(() => { // Pre-compute flat nav index for each top-level item and child issue. let flatIdx = 0; const topFlatIndex = new Map(); const childFlatIndex = new Map(); for (const group of groupedSections) { for (const topItem of group.displayItems) { const itemKey = `${group.key}:${getWorkItemKey(topItem)}`; topFlatIndex.set(itemKey, flatIdx); flatIdx++; if (topItem.kind === "issue") { const children = group.childrenByIssueId.get(topItem.issue.id); const isExpanded = children?.length && !collapsedInboxParents.has(topItem.issue.id); if (isExpanded) { for (const child of children) { childFlatIndex.set(child.id, flatIdx); flatIdx++; } } } } } const renderInboxIssue = ({ issue, depth, selected, hasChildren = false, isExpanded = false, childCount = 0, collapseParentId = null, allowArchive = canArchiveFromTab, }: { issue: Issue; depth: number; selected: boolean; hasChildren?: boolean; isExpanded?: boolean; childCount?: number; collapseParentId?: string | null; allowArchive?: boolean; }) => { const isUnread = issue.isUnreadForMe && !fadingOutIssues.has(issue.id); const isFading = fadingOutIssues.has(issue.id); const isArchiving = archivingIssueIds.has(issue.id); const project = issue.projectId ? projectById.get(issue.projectId) ?? null : null; return ( {nestingEnabled ? ( depth === 0 && hasChildren && collapseParentId ? ( ) : ( ) ) : null} {depth > 0 ? : null} } titleSuffix={hasChildren && !isExpanded && depth === 0 ? ( ({childCount} sub-task{childCount !== 1 ? "s" : ""}) ) : undefined} mobileMeta={issueActivityText(issue).toLowerCase()} mobileLeading={ depth === 0 && hasChildren && collapseParentId ? ( ) : undefined } unreadState={isUnread ? "visible" : isFading ? "fading" : "hidden"} onMarkRead={() => markReadMutation.mutate(issue.id)} onArchive={allowArchive ? () => archiveIssueMutation.mutate(issue.id) : undefined} archiveDisabled={isArchiving || archiveIssueMutation.isPending} desktopTrailing={ visibleTrailingIssueColumns.length > 0 ? ( ) : undefined } /> ); }; let previousTimestamp = Number.POSITIVE_INFINITY; return groupedSections.flatMap((group, groupIndex) => { const elements: ReactNode[] = []; if (group.isArchivedSearch && (groupIndex === 0 || !groupedSections[groupIndex - 1]?.isArchivedSearch)) { elements.push(
Archived
, ); } if (group.label) { elements.push(
0 && "border-t border-border", )} > {group.label}
, ); } for (let index = 0; index < group.displayItems.length; index += 1) { const item = group.displayItems[index]!; const navIdx = topFlatIndex.get(`${group.key}:${getWorkItemKey(item)}`) ?? 0; const wrapItem = (key: string, isSelected: boolean, child: ReactNode) => (
setSelectedIndex(navIdx)} > {child}
); const todayCutoff = Date.now() - 24 * 60 * 60 * 1000; const showTodayDivider = groupBy === "none" && item.timestamp > 0 && item.timestamp < todayCutoff && previousTimestamp >= todayCutoff; previousTimestamp = item.timestamp > 0 ? item.timestamp : previousTimestamp; if (showTodayDivider) { elements.push(
Earlier
, ); } const isSelected = selectedIndex === navIdx; if (item.kind === "approval") { const approvalKey = `approval:${item.approval.id}`; const isArchiving = archivingNonIssueIds.has(approvalKey); const row = ( approveMutation.mutate(item.approval.id)} onReject={() => rejectMutation.mutate(item.approval.id)} isPending={approveMutation.isPending || rejectMutation.isPending} unreadState={nonIssueUnreadState(approvalKey)} onMarkRead={() => handleMarkNonIssueRead(approvalKey)} onArchive={canArchiveFromTab ? () => handleArchiveNonIssue(approvalKey) : undefined} archiveDisabled={isArchiving} className={ isArchiving ? "pointer-events-none -translate-x-4 scale-[0.98] opacity-0 transition-all duration-200 ease-out" : "transition-all duration-200 ease-out" } /> ); elements.push(wrapItem(approvalKey, isSelected, canArchiveFromTab ? ( handleArchiveNonIssue(approvalKey)} > {row} ) : row)); continue; } if (item.kind === "failed_run") { const runKey = `run:${item.run.id}`; const isArchiving = archivingNonIssueIds.has(runKey); const row = ( dismissInboxItem(runKey)} onRetry={() => retryRunMutation.mutate(item.run)} isRetrying={retryingRunIds.has(item.run.id)} unreadState={nonIssueUnreadState(runKey)} onMarkRead={() => handleMarkNonIssueRead(runKey)} onArchive={canArchiveFromTab ? () => handleArchiveNonIssue(runKey) : undefined} archiveDisabled={isArchiving} className={ isArchiving ? "pointer-events-none -translate-x-4 scale-[0.98] opacity-0 transition-all duration-200 ease-out" : "transition-all duration-200 ease-out" } /> ); elements.push(wrapItem(runKey, isSelected, canArchiveFromTab ? ( handleArchiveNonIssue(runKey)} > {row} ) : row)); continue; } if (item.kind === "join_request") { const joinKey = `join:${item.joinRequest.id}`; const isArchiving = archivingNonIssueIds.has(joinKey); const row = ( approveJoinMutation.mutate(item.joinRequest)} onReject={() => rejectJoinMutation.mutate(item.joinRequest)} isPending={approveJoinMutation.isPending || rejectJoinMutation.isPending} unreadState={nonIssueUnreadState(joinKey)} onMarkRead={() => handleMarkNonIssueRead(joinKey)} onArchive={canArchiveFromTab ? () => handleArchiveNonIssue(joinKey) : undefined} archiveDisabled={isArchiving} className={ isArchiving ? "pointer-events-none -translate-x-4 scale-[0.98] opacity-0 transition-all duration-200 ease-out" : "transition-all duration-200 ease-out" } /> ); elements.push(wrapItem(joinKey, isSelected, canArchiveFromTab ? ( handleArchiveNonIssue(joinKey)} > {row} ) : row)); continue; } const issue = item.issue; const childIssues = group.childrenByIssueId.get(issue.id) ?? []; const hasChildren = childIssues.length > 0; const isExpanded = hasChildren && !collapsedInboxParents.has(issue.id); const canArchiveIssue = canArchiveFromTab && !group.isArchivedSearch; const parentRow = renderInboxIssue({ issue, depth: 0, selected: isSelected, hasChildren, isExpanded, childCount: childIssues.length, collapseParentId: issue.id, allowArchive: canArchiveIssue, }); elements.push(wrapItem(`issue:${issue.id}`, isSelected, canArchiveIssue ? ( archiveIssueMutation.mutate(issue.id)} > {parentRow} ) : parentRow)); if (isExpanded) { for (const child of childIssues) { const childNavIdx = childFlatIndex.get(child.id) ?? -1; const isChildSelected = selectedIndex === childNavIdx; const childRow = renderInboxIssue({ issue: child, depth: 1, selected: isChildSelected, allowArchive: canArchiveIssue, }); const isChildArchiving = archivingIssueIds.has(child.id); elements.push(
setSelectedIndex(childNavIdx)} > {canArchiveIssue ? ( archiveIssueMutation.mutate(child.id)} > {childRow} ) : childRow}
, ); } } } return elements; }); })()}
)} {showAlertsSection && ( <> {showSeparatorBefore("alerts") && }

Alerts

{showAggregateAgentError && (
{dashboard!.agents.error}{" "} {dashboard!.agents.error === 1 ? "agent has" : "agents have"} errors
)} {showBudgetAlert && (
Budget at{" "} {dashboard!.costs.monthUtilizationPercent}%{" "} utilization this month
)}
)}
); }