From eb033a221f57ba36bb70e4fcdce388fceddfd379 Mon Sep 17 00:00:00 2001 From: Dotta Date: Fri, 6 Mar 2026 07:27:35 -0600 Subject: [PATCH] feat(ui): add dismiss buttons to inbox errors and failures Failed runs, alerts, and stale work items can now be dismissed via an X button that appears on hover. Dismissed items are stored in localStorage and filtered from the inbox view and item count. Co-Authored-By: Claude Opus 4.6 --- ui/src/pages/Inbox.tsx | 183 +++++++++++++++++++++++++++++------------ 1 file changed, 131 insertions(+), 52 deletions(-) diff --git a/ui/src/pages/Inbox.tsx b/ui/src/pages/Inbox.tsx index b11afecf..58bc734b 100644 --- a/ui/src/pages/Inbox.tsx +++ b/ui/src/pages/Inbox.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { Link, useLocation, useNavigate } from "@/lib/router"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { approvalsApi } from "../api/approvals"; @@ -34,6 +34,7 @@ import { Clock, ArrowUpRight, XCircle, + X, UserCheck, RotateCcw, } from "lucide-react"; @@ -63,6 +64,36 @@ type SectionKey = | "alerts" | "stale_work"; +const DISMISSED_KEY = "paperclip:inbox:dismissed"; + +function loadDismissed(): Set { + try { + const raw = localStorage.getItem(DISMISSED_KEY); + return raw ? new Set(JSON.parse(raw)) : new Set(); + } catch { + return new Set(); + } +} + +function saveDismissed(ids: Set) { + localStorage.setItem(DISMISSED_KEY, JSON.stringify([...ids])); +} + +function useDismissedItems() { + const [dismissed, setDismissed] = useState>(loadDismissed); + + const dismiss = useCallback((id: string) => { + setDismissed((prev) => { + const next = new Set(prev); + next.add(id); + saveDismissed(next); + return next; + }); + }, []); + + return { dismissed, dismiss }; +} + const RUN_SOURCE_LABELS: Record = { timer: "Scheduled", assignment: "Assignment", @@ -123,10 +154,12 @@ function FailedRunCard({ run, issueById, agentName: linkedAgentName, + onDismiss, }: { run: HeartbeatRun; issueById: Map; agentName: string | null; + onDismiss: () => void; }) { const queryClient = useQueryClient(); const navigate = useNavigate(); @@ -165,6 +198,14 @@ function FailedRunCard({ return (
+
{issue ? ( (null); const [allCategoryFilter, setAllCategoryFilter] = useState("everything"); const [allApprovalFilter, setAllApprovalFilter] = useState("all"); + const { dismissed, dismiss } = useDismissedItems(); const pathSegment = location.pathname.split("/").pop() ?? "new"; const tab: InboxTab = pathSegment === "all" ? "all" : "new"; @@ -326,7 +368,10 @@ export function Inbox() { enabled: !!selectedCompanyId, }); - const staleIssues = issues ? getStaleIssues(issues) : []; + const staleIssues = useMemo( + () => (issues ? getStaleIssues(issues) : []).filter((i) => !dismissed.has(`stale:${i.id}`)), + [issues, dismissed], + ); const assignedToMeIssues = useMemo( () => [...assignedToMeIssuesRaw].sort( @@ -348,8 +393,8 @@ export function Inbox() { }, [issues]); const failedRuns = useMemo( - () => getLatestFailedRunsByAgent(heartbeatRuns ?? []), - [heartbeatRuns], + () => getLatestFailedRunsByAgent(heartbeatRuns ?? []).filter((r) => !dismissed.has(`run:${r.id}`)), + [heartbeatRuns, dismissed], ); const allApprovals = useMemo( @@ -435,11 +480,12 @@ export function Inbox() { } const hasRunFailures = failedRuns.length > 0; - const showAggregateAgentError = !!dashboard && dashboard.agents.error > 0 && !hasRunFailures; + const showAggregateAgentError = !!dashboard && dashboard.agents.error > 0 && !hasRunFailures && !dismissed.has("alert:agent-errors"); const showBudgetAlert = !!dashboard && dashboard.costs.monthBudgetCents > 0 && - dashboard.costs.monthUtilizationPercent >= 80; + dashboard.costs.monthUtilizationPercent >= 80 && + !dismissed.has("alert:budget"); const hasAlerts = showAggregateAgentError || showBudgetAlert; const hasStale = staleIssues.length > 0; const hasJoinRequests = joinRequests.length > 0; @@ -700,6 +746,7 @@ export function Inbox() { run={run} issueById={issueById} agentName={agentName(run.agentId)} + onDismiss={() => dismiss(`run:${run.id}`)} /> ))}
@@ -716,29 +763,49 @@ export function Inbox() {
{showAggregateAgentError && ( - - - - {dashboard!.agents.error}{" "} - {dashboard!.agents.error === 1 ? "agent has" : "agents have"} errors - - +
+ + + + {dashboard!.agents.error}{" "} + {dashboard!.agents.error === 1 ? "agent has" : "agents have"} errors + + + +
)} {showBudgetAlert && ( - - - - Budget at{" "} - {dashboard!.costs.monthUtilizationPercent}%{" "} - utilization this month - - +
+ + + + Budget at{" "} + {dashboard!.costs.monthUtilizationPercent}%{" "} + utilization this month + + + +
)}
@@ -754,33 +821,45 @@ export function Inbox() {
{staleIssues.map((issue) => ( - - - - - - {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)} - - + + + + + + {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)} + + + +
))}