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 { queryKeys } from "../lib/queryKeys"; import { armIssueDetailInboxQuickArchive, createIssueDetailLocationState, createIssueDetailPath, } from "../lib/issueDetailBreadcrumb"; import { hasBlockingShortcutDialog, isKeyboardShortcutTextInputTarget } from "../lib/keyboardShortcuts"; import { EmptyState } from "../components/EmptyState"; import { PageSkeleton } from "../components/PageSkeleton"; 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 { Identity } from "../components/Identity"; import { approvalLabel, defaultTypeIcon, typeIcon } from "../components/ApprovalPayload"; import { pickTextColorForPillBg } from "@/lib/color-contrast"; import { timeAgo } from "../lib/timeAgo"; import { formatAssigneeUserLabel } from "../lib/assignees"; 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 { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Inbox as InboxIcon, AlertTriangle, XCircle, X, RotateCcw, UserPlus, Columns3, Search, } 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, getAvailableInboxIssueColumns, getApprovalsForTab, getInboxWorkItems, getInboxKeyboardSelectionIndex, getLatestFailedRunsByAgent, getRecentTouchedIssues, isMineInboxTab, loadInboxIssueColumns, normalizeInboxIssueColumns, resolveIssueWorkspaceName, resolveInboxSelectionIndex, saveInboxIssueColumns, InboxApprovalFilter, type InboxIssueColumn, saveLastInboxTab, shouldShowInboxSection, type InboxTab, type InboxWorkItem, } from "../lib/inbox"; import { useDismissedInboxItems, useReadInboxItems } from "../hooks/useInboxBadge"; type InboxCategoryFilter = | "everything" | "issues_i_touched" | "join_requests" | "approvals" | "failed_runs" | "alerts"; type SectionKey = | "work_items" | "alerts"; 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; const trailingIssueColumns: InboxIssueColumn[] = ["assignee", "project", "workspace", "labels", "updated"]; const inboxIssueColumnLabels: Record = { status: "Status", id: "ID", assignee: "Assignee", project: "Project", workspace: "Workspace", labels: "Tags", updated: "Last updated", }; const inboxIssueColumnDescriptions: Record = { status: "Issue state chip on the left edge.", id: "Ticket identifier like PAP-1009.", assignee: "Assigned agent or board user.", project: "Linked project pill with its color.", workspace: "Execution or project workspace used for the issue.", labels: "Issue labels and tags.", updated: "Latest visible activity time.", }; export function InboxIssueMetaLeading({ issue, isLive, showStatus = true, showIdentifier = true, }: { issue: Issue; isLive: boolean; showStatus?: boolean; showIdentifier?: boolean; }) { return ( <> {showStatus ? ( ) : null} {showIdentifier ? ( {issue.identifier ?? issue.id.slice(0, 8)} ) : null} {isLive && ( Live )} ); } function issueActivityText(issue: Issue): string { return `Updated ${timeAgo(issue.lastActivityAt ?? issue.lastExternalCommentAt ?? issue.updatedAt)}`; } function issueTrailingGridTemplate(columns: InboxIssueColumn[]): string { return columns .map((column) => { if (column === "assignee") return "minmax(7.5rem, 9.5rem)"; if (column === "project") return "minmax(6.5rem, 8.5rem)"; if (column === "workspace") return "minmax(9rem, 12rem)"; if (column === "labels") return "minmax(8rem, 10rem)"; return "minmax(6rem, 7rem)"; }) .join(" "); } export function InboxIssueTrailingColumns({ issue, columns, projectName, projectColor, workspaceName, assigneeName, currentUserId, }: { issue: Issue; columns: InboxIssueColumn[]; projectName: string | null; projectColor: string | null; workspaceName: string | null; assigneeName: string | null; currentUserId: string | null; }) { const activityText = timeAgo(issue.lastActivityAt ?? issue.lastExternalCommentAt ?? issue.updatedAt); const userLabel = formatAssigneeUserLabel(issue.assigneeUserId, currentUserId) ?? "User"; return ( {columns.map((column) => { if (column === "assignee") { if (issue.assigneeAgentId) { return ( ); } if (issue.assigneeUserId) { return ( {userLabel} ); } return ( Unassigned ); } if (column === "project") { if (projectName) { const accentColor = projectColor ?? "#64748b"; return ( {projectName} ); } return ( No project ); } if (column === "labels") { if ((issue.labels ?? []).length > 0) { return ( {(issue.labels ?? []).slice(0, 2).map((label) => ( {label.name} ))} {(issue.labels ?? []).length > 2 ? ( +{(issue.labels ?? []).length - 2} ) : null} ); } return