import { useEffect, useMemo, useState } from "react"; import { useNavigate } from "react-router-dom"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { approvalsApi } from "../api/approvals"; import { dashboardApi } from "../api/dashboard"; import { issuesApi } from "../api/issues"; import { agentsApi } from "../api/agents"; import { heartbeatsApi } from "../api/heartbeats"; import { useCompany } from "../context/CompanyContext"; import { useBreadcrumbs } from "../context/BreadcrumbContext"; import { queryKeys } from "../lib/queryKeys"; import { StatusIcon } from "../components/StatusIcon"; import { PriorityIcon } from "../components/PriorityIcon"; import { EmptyState } from "../components/EmptyState"; import { ApprovalCard } from "../components/ApprovalCard"; import { StatusBadge } from "../components/StatusBadge"; import { timeAgo } from "../lib/timeAgo"; import { Button } from "@/components/ui/button"; import { Separator } from "@/components/ui/separator"; import { Inbox as InboxIcon, AlertTriangle, Clock, ExternalLink, ArrowUpRight, XCircle, } from "lucide-react"; import { Identity } from "../components/Identity"; import type { HeartbeatRun, Issue } from "@paperclip/shared"; const STALE_THRESHOLD_MS = 24 * 60 * 60 * 1000; // 24 hours const FAILED_RUN_STATUSES = new Set(["failed", "timed_out"]); const RUN_SOURCE_LABELS: Record = { timer: "Scheduled", assignment: "Assignment", on_demand: "Manual", automation: "Automation", }; function getStaleIssues(issues: Issue[]): Issue[] { const now = Date.now(); return issues .filter( (i) => ["in_progress", "todo"].includes(i.status) && now - new Date(i.updatedAt).getTime() > STALE_THRESHOLD_MS ) .sort( (a, b) => new Date(a.updatedAt).getTime() - new Date(b.updatedAt).getTime() ); } function getLatestFailedRunsByAgent(runs: HeartbeatRun[]): HeartbeatRun[] { const sorted = [...runs].sort( (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(), ); const latestByAgent = new Map(); for (const run of sorted) { if (!latestByAgent.has(run.agentId)) { latestByAgent.set(run.agentId, run); } } return Array.from(latestByAgent.values()).filter((run) => FAILED_RUN_STATUSES.has(run.status), ); } 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 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; } export function Inbox() { const { selectedCompanyId } = useCompany(); const { setBreadcrumbs } = useBreadcrumbs(); const navigate = useNavigate(); const queryClient = useQueryClient(); const [actionError, setActionError] = useState(null); const { data: agents } = useQuery({ queryKey: queryKeys.agents.list(selectedCompanyId!), queryFn: () => agentsApi.list(selectedCompanyId!), enabled: !!selectedCompanyId, }); useEffect(() => { setBreadcrumbs([{ label: "Inbox" }]); }, [setBreadcrumbs]); const { data: approvals, isLoading: isApprovalsLoading, error } = useQuery({ queryKey: queryKeys.approvals.list(selectedCompanyId!), queryFn: () => approvalsApi.list(selectedCompanyId!), enabled: !!selectedCompanyId, }); const { data: dashboard } = useQuery({ queryKey: queryKeys.dashboard(selectedCompanyId!), queryFn: () => dashboardApi.summary(selectedCompanyId!), enabled: !!selectedCompanyId, }); const { data: issues } = useQuery({ queryKey: queryKeys.issues.list(selectedCompanyId!), queryFn: () => issuesApi.list(selectedCompanyId!), enabled: !!selectedCompanyId, }); const { data: heartbeatRuns } = useQuery({ queryKey: queryKeys.heartbeats(selectedCompanyId!), queryFn: () => heartbeatsApi.list(selectedCompanyId!), enabled: !!selectedCompanyId, }); const staleIssues = issues ? getStaleIssues(issues) : []; 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 failedRuns = useMemo( () => getLatestFailedRunsByAgent(heartbeatRuns ?? []), [heartbeatRuns], ); const agentName = (id: string | null) => { if (!id) return null; return agentById.get(id) ?? null; }; const approveMutation = useMutation({ mutationFn: (id: string) => approvalsApi.approve(id), onSuccess: (_approval, id) => { 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: () => { queryClient.invalidateQueries({ queryKey: queryKeys.approvals.list(selectedCompanyId!) }); }, onError: (err) => { setActionError(err instanceof Error ? err.message : "Failed to reject"); }, }); if (!selectedCompanyId) { return ; } const actionableApprovals = (approvals ?? []).filter( (approval) => approval.status === "pending" || approval.status === "revision_requested", ); const hasActionableApprovals = actionableApprovals.length > 0; const hasRunFailures = failedRuns.length > 0; const showAggregateAgentError = !!dashboard && dashboard.agents.error > 0 && !hasRunFailures; const hasAlerts = !!dashboard && (showAggregateAgentError || (dashboard.costs.monthBudgetCents > 0 && dashboard.costs.monthUtilizationPercent >= 80)); const hasStale = staleIssues.length > 0; const hasContent = hasActionableApprovals || hasRunFailures || hasAlerts || hasStale; return (
{isApprovalsLoading &&

Loading...

} {error &&

{error.message}

} {actionError &&

{actionError}

} {!isApprovalsLoading && !hasContent && ( )} {/* Pending Approvals */} {hasActionableApprovals && (

Approvals

{actionableApprovals.map((approval) => ( a.id === approval.requestedByAgentId) ?? null : null} onApprove={() => approveMutation.mutate(approval.id)} onReject={() => rejectMutation.mutate(approval.id)} onOpen={() => navigate(`/approvals/${approval.id}`)} isPending={approveMutation.isPending || rejectMutation.isPending} /> ))}
)} {/* Failed Runs */} {hasRunFailures && ( <> {hasActionableApprovals && }

Failed Runs

{failedRuns.map((run) => { const issueId = readIssueIdFromRun(run); const issue = issueId ? issueById.get(issueId) ?? null : null; const sourceLabel = RUN_SOURCE_LABELS[run.invocationSource] ?? "Manual"; const displayError = runFailureMessage(run); const linkedAgentName = agentName(run.agentId); return (
{linkedAgentName ? : Agent {run.agentId.slice(0, 8)}}

{sourceLabel} run failed {timeAgo(run.createdAt)}

{displayError}
run {run.id.slice(0, 8)} {issue ? ( ) : ( {run.errorCode ? `code: ${run.errorCode}` : "No linked issue"} )}
); })}
)} {/* Alerts */} {hasAlerts && ( <> {(hasActionableApprovals || hasRunFailures) && }

Alerts

{showAggregateAgentError && (
navigate("/agents")} > {dashboard!.agents.error}{" "} {dashboard!.agents.error === 1 ? "agent has" : "agents have"} errors
)} {dashboard!.costs.monthBudgetCents > 0 && dashboard!.costs.monthUtilizationPercent >= 80 && (
navigate("/costs")} > Budget at{" "} {dashboard!.costs.monthUtilizationPercent}% {" "} utilization this month
)}
)} {/* Stale Work */} {hasStale && ( <> {(hasActionableApprovals || hasRunFailures || hasAlerts) && }

Stale Work

{staleIssues.map((issue) => (
navigate(`/issues/${issue.identifier ?? issue.id}`)} > {issue.identifier ?? issue.id.slice(0, 8)} {issue.title} {issue.assigneeAgentId && (() => { const name = agentName(issue.assigneeAgentId); return name ? : {issue.assigneeAgentId.slice(0, 8)}; })()} updated {timeAgo(issue.updatedAt)}
))}
)}
); }