mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-18 03:30:39 +09:00
Persist non-issue inbox dismissals
This commit is contained in:
parent
1de5fb9316
commit
5640d29ab0
23 changed files with 13623 additions and 54 deletions
8
ui/src/api/inboxDismissals.ts
Normal file
8
ui/src/api/inboxDismissals.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import type { InboxDismissal } from "@paperclipai/shared";
|
||||
import { api } from "./client";
|
||||
|
||||
export const inboxDismissalsApi = {
|
||||
list: (companyId: string) => api.get<InboxDismissal[]>(`/companies/${companyId}/inbox-dismissals`),
|
||||
dismiss: (companyId: string, itemKey: string) =>
|
||||
api.post<InboxDismissal>(`/companies/${companyId}/inbox-dismissals`, { itemKey }),
|
||||
};
|
||||
|
|
@ -15,4 +15,5 @@ export { dashboardApi } from "./dashboard";
|
|||
export { heartbeatsApi } from "./heartbeats";
|
||||
export { instanceSettingsApi } from "./instanceSettings";
|
||||
export { sidebarBadgesApi } from "./sidebarBadges";
|
||||
export { inboxDismissalsApi } from "./inboxDismissals";
|
||||
export { companySkillsApi } from "./companySkills";
|
||||
|
|
|
|||
|
|
@ -1,17 +1,19 @@
|
|||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { accessApi } from "../api/access";
|
||||
import { ApiError } from "../api/client";
|
||||
import { inboxDismissalsApi } from "../api/inboxDismissals";
|
||||
import { approvalsApi } from "../api/approvals";
|
||||
import { dashboardApi } from "../api/dashboard";
|
||||
import { heartbeatsApi } from "../api/heartbeats";
|
||||
import { issuesApi } from "../api/issues";
|
||||
import { queryKeys } from "../lib/queryKeys";
|
||||
import {
|
||||
buildInboxDismissedAtByKey,
|
||||
computeInboxBadgeData,
|
||||
getRecentTouchedIssues,
|
||||
loadDismissedInboxItems,
|
||||
saveDismissedInboxItems,
|
||||
loadDismissedInboxAlerts,
|
||||
saveDismissedInboxAlerts,
|
||||
loadReadInboxItems,
|
||||
saveReadInboxItems,
|
||||
READ_ITEMS_KEY,
|
||||
|
|
@ -19,13 +21,13 @@ import {
|
|||
|
||||
const INBOX_ISSUE_STATUSES = "backlog,todo,in_progress,in_review,blocked,done";
|
||||
|
||||
export function useDismissedInboxItems() {
|
||||
const [dismissed, setDismissed] = useState<Set<string>>(loadDismissedInboxItems);
|
||||
export function useDismissedInboxAlerts() {
|
||||
const [dismissed, setDismissed] = useState<Set<string>>(loadDismissedInboxAlerts);
|
||||
|
||||
useEffect(() => {
|
||||
const handleStorage = (event: StorageEvent) => {
|
||||
if (event.key !== "paperclip:inbox:dismissed") return;
|
||||
setDismissed(loadDismissedInboxItems());
|
||||
setDismissed(loadDismissedInboxAlerts());
|
||||
};
|
||||
window.addEventListener("storage", handleStorage);
|
||||
return () => window.removeEventListener("storage", handleStorage);
|
||||
|
|
@ -35,7 +37,7 @@ export function useDismissedInboxItems() {
|
|||
setDismissed((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.add(id);
|
||||
saveDismissedInboxItems(next);
|
||||
saveDismissedInboxAlerts(next);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
|
@ -43,6 +45,63 @@ export function useDismissedInboxItems() {
|
|||
return { dismissed, dismiss };
|
||||
}
|
||||
|
||||
export function useInboxDismissals(companyId: string | null | undefined) {
|
||||
const queryClient = useQueryClient();
|
||||
const queryKey = companyId
|
||||
? queryKeys.inboxDismissals(companyId)
|
||||
: ["inbox-dismissals", "__disabled__"] as const;
|
||||
|
||||
const { data: dismissals = [] } = useQuery({
|
||||
queryKey,
|
||||
queryFn: () => inboxDismissalsApi.list(companyId!),
|
||||
enabled: !!companyId,
|
||||
});
|
||||
|
||||
const dismissMutation = useMutation({
|
||||
mutationFn: ({ itemKey }: { itemKey: string }) => inboxDismissalsApi.dismiss(companyId!, itemKey),
|
||||
onMutate: async ({ itemKey }) => {
|
||||
if (!companyId) return { previous: [] as typeof dismissals };
|
||||
await queryClient.cancelQueries({ queryKey });
|
||||
const previous = queryClient.getQueryData<typeof dismissals>(queryKey) ?? [];
|
||||
const now = new Date();
|
||||
queryClient.setQueryData(queryKey, [
|
||||
{
|
||||
id: `optimistic:${itemKey}`,
|
||||
companyId,
|
||||
userId: "me",
|
||||
itemKey,
|
||||
dismissedAt: now,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
},
|
||||
...previous.filter((dismissal) => dismissal.itemKey !== itemKey),
|
||||
]);
|
||||
return { previous };
|
||||
},
|
||||
onError: (_error, _variables, context) => {
|
||||
if (!context) return;
|
||||
queryClient.setQueryData(queryKey, context.previous);
|
||||
},
|
||||
onSettled: () => {
|
||||
if (!companyId) return;
|
||||
queryClient.invalidateQueries({ queryKey });
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.sidebarBadges(companyId) });
|
||||
},
|
||||
});
|
||||
|
||||
const dismissedAtByKey = useMemo(
|
||||
() => buildInboxDismissedAtByKey(dismissals),
|
||||
[dismissals],
|
||||
);
|
||||
|
||||
return {
|
||||
dismissals,
|
||||
dismissedAtByKey,
|
||||
dismiss: (itemKey: string) => dismissMutation.mutate({ itemKey }),
|
||||
isPending: dismissMutation.isPending,
|
||||
};
|
||||
}
|
||||
|
||||
export function useReadInboxItems() {
|
||||
const [readItems, setReadItems] = useState<Set<string>>(loadReadInboxItems);
|
||||
|
||||
|
|
@ -77,7 +136,8 @@ export function useReadInboxItems() {
|
|||
}
|
||||
|
||||
export function useInboxBadge(companyId: string | null | undefined) {
|
||||
const { dismissed } = useDismissedInboxItems();
|
||||
const { dismissed: dismissedAlerts } = useDismissedInboxAlerts();
|
||||
const { dismissedAtByKey } = useInboxDismissals(companyId);
|
||||
|
||||
const { data: approvals = [] } = useQuery({
|
||||
queryKey: queryKeys.approvals.list(companyId!),
|
||||
|
|
@ -134,8 +194,9 @@ export function useInboxBadge(companyId: string | null | undefined) {
|
|||
dashboard,
|
||||
heartbeatRuns,
|
||||
mineIssues,
|
||||
dismissed,
|
||||
dismissedAlerts,
|
||||
dismissedAtByKey,
|
||||
}),
|
||||
[approvals, joinRequests, dashboard, heartbeatRuns, mineIssues, dismissed],
|
||||
[approvals, joinRequests, dashboard, heartbeatRuns, mineIssues, dismissedAlerts, dismissedAtByKey],
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import type {
|
|||
} from "@paperclipai/shared";
|
||||
import {
|
||||
DEFAULT_INBOX_ISSUE_COLUMNS,
|
||||
buildInboxDismissedAtByKey,
|
||||
computeInboxBadgeData,
|
||||
getAvailableInboxIssueColumns,
|
||||
getApprovalsForTab,
|
||||
|
|
@ -19,6 +20,7 @@ import {
|
|||
getInboxKeyboardSelectionIndex,
|
||||
getRecentTouchedIssues,
|
||||
getUnreadTouchedIssues,
|
||||
isInboxEntityDismissed,
|
||||
isMineInboxTab,
|
||||
loadInboxIssueColumns,
|
||||
loadLastInboxTab,
|
||||
|
|
@ -286,7 +288,8 @@ describe("inbox helpers", () => {
|
|||
makeRun("run-other-agent", "failed", "2026-03-11T02:00:00.000Z", "agent-2"),
|
||||
],
|
||||
mineIssues: [makeIssue("1", true)],
|
||||
dismissed: new Set<string>(),
|
||||
dismissedAlerts: new Set<string>(),
|
||||
dismissedAtByKey: new Map<string, number>(),
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
|
|
@ -306,7 +309,8 @@ describe("inbox helpers", () => {
|
|||
dashboard,
|
||||
heartbeatRuns: [makeRun("run-1", "failed", "2026-03-11T00:00:00.000Z")],
|
||||
mineIssues: [],
|
||||
dismissed: new Set<string>(["run:run-1", "alert:budget", "alert:agent-errors"]),
|
||||
dismissedAlerts: new Set<string>(["alert:budget", "alert:agent-errors"]),
|
||||
dismissedAtByKey: new Map<string, number>([["run:run-1", new Date("2026-03-11T00:00:00.000Z").getTime()]]),
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
|
|
@ -326,7 +330,7 @@ describe("inbox helpers", () => {
|
|||
dashboard,
|
||||
heartbeatRuns: [],
|
||||
mineIssues: [makeIssue("1", false), makeIssue("2", false), makeIssue("3", true)],
|
||||
dismissed: new Set<string>(),
|
||||
dismissedAtByKey: new Map(),
|
||||
});
|
||||
|
||||
expect(result.mineIssues).toBe(1);
|
||||
|
|
@ -334,6 +338,35 @@ describe("inbox helpers", () => {
|
|||
expect(result.inbox).toBe(3);
|
||||
});
|
||||
|
||||
it("resurfaces non-issue items when they change after dismissal", () => {
|
||||
const dismissedAtByKey = buildInboxDismissedAtByKey([
|
||||
{
|
||||
id: "dismissal-1",
|
||||
companyId: "company-1",
|
||||
userId: "user-1",
|
||||
itemKey: "approval:approval-1",
|
||||
dismissedAt: new Date("2026-03-11T01:00:00.000Z"),
|
||||
createdAt: new Date("2026-03-11T01:00:00.000Z"),
|
||||
updatedAt: new Date("2026-03-11T01:00:00.000Z"),
|
||||
},
|
||||
]);
|
||||
|
||||
expect(
|
||||
isInboxEntityDismissed(
|
||||
dismissedAtByKey,
|
||||
"approval:approval-1",
|
||||
new Date("2026-03-11T00:30:00.000Z"),
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
isInboxEntityDismissed(
|
||||
dismissedAtByKey,
|
||||
"approval:approval-1",
|
||||
new Date("2026-03-11T01:30:00.000Z"),
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps read issues in the touched list but excludes them from unread counts", () => {
|
||||
const issues = [makeIssue("1", true), makeIssue("2", false)];
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,11 @@
|
|||
import type { Approval, DashboardSummary, HeartbeatRun, Issue, JoinRequest } from "@paperclipai/shared";
|
||||
import type {
|
||||
Approval,
|
||||
DashboardSummary,
|
||||
HeartbeatRun,
|
||||
InboxDismissal,
|
||||
Issue,
|
||||
JoinRequest,
|
||||
} from "@paperclipai/shared";
|
||||
|
||||
export const RECENT_ISSUES_LIMIT = 100;
|
||||
export const FAILED_RUN_STATUSES = new Set(["failed", "timed_out"]);
|
||||
|
|
@ -43,16 +50,19 @@ export interface InboxBadgeData {
|
|||
alerts: number;
|
||||
}
|
||||
|
||||
export function loadDismissedInboxItems(): Set<string> {
|
||||
export function loadDismissedInboxAlerts(): Set<string> {
|
||||
try {
|
||||
const raw = localStorage.getItem(DISMISSED_KEY);
|
||||
return raw ? new Set(JSON.parse(raw)) : new Set();
|
||||
if (!raw) return new Set();
|
||||
const parsed = JSON.parse(raw);
|
||||
if (!Array.isArray(parsed)) return new Set();
|
||||
return new Set(parsed.filter((value): value is string => typeof value === "string" && value.startsWith("alert:")));
|
||||
} catch {
|
||||
return new Set();
|
||||
}
|
||||
}
|
||||
|
||||
export function saveDismissedInboxItems(ids: Set<string>) {
|
||||
export function saveDismissedInboxAlerts(ids: Set<string>) {
|
||||
try {
|
||||
localStorage.setItem(DISMISSED_KEY, JSON.stringify([...ids]));
|
||||
} catch {
|
||||
|
|
@ -60,6 +70,22 @@ export function saveDismissedInboxItems(ids: Set<string>) {
|
|||
}
|
||||
}
|
||||
|
||||
export function buildInboxDismissedAtByKey(dismissals: InboxDismissal[]): Map<string, number> {
|
||||
return new Map(
|
||||
dismissals.map((dismissal) => [dismissal.itemKey, normalizeTimestamp(dismissal.dismissedAt)]),
|
||||
);
|
||||
}
|
||||
|
||||
export function isInboxEntityDismissed(
|
||||
dismissedAtByKey: ReadonlyMap<string, number>,
|
||||
itemKey: string,
|
||||
activityAt: string | Date | null | undefined,
|
||||
): boolean {
|
||||
const dismissedAt = dismissedAtByKey.get(itemKey);
|
||||
if (dismissedAt == null) return false;
|
||||
return dismissedAt >= normalizeTimestamp(activityAt);
|
||||
}
|
||||
|
||||
export function loadReadInboxItems(): Set<string> {
|
||||
try {
|
||||
const raw = localStorage.getItem(READ_ITEMS_KEY);
|
||||
|
|
@ -342,25 +368,27 @@ export function computeInboxBadgeData({
|
|||
dashboard,
|
||||
heartbeatRuns,
|
||||
mineIssues,
|
||||
dismissed,
|
||||
dismissedAlerts,
|
||||
dismissedAtByKey,
|
||||
}: {
|
||||
approvals: Approval[];
|
||||
joinRequests: JoinRequest[];
|
||||
dashboard: DashboardSummary | undefined;
|
||||
heartbeatRuns: HeartbeatRun[];
|
||||
mineIssues: Issue[];
|
||||
dismissed: Set<string>;
|
||||
dismissedAlerts: Set<string>;
|
||||
dismissedAtByKey: ReadonlyMap<string, number>;
|
||||
}): InboxBadgeData {
|
||||
const actionableApprovals = approvals.filter(
|
||||
(approval) =>
|
||||
ACTIONABLE_APPROVAL_STATUSES.has(approval.status) &&
|
||||
!dismissed.has(`approval:${approval.id}`),
|
||||
!isInboxEntityDismissed(dismissedAtByKey, `approval:${approval.id}`, approval.updatedAt),
|
||||
).length;
|
||||
const failedRuns = getLatestFailedRunsByAgent(heartbeatRuns).filter(
|
||||
(run) => !dismissed.has(`run:${run.id}`),
|
||||
(run) => !isInboxEntityDismissed(dismissedAtByKey, `run:${run.id}`, run.createdAt),
|
||||
).length;
|
||||
const visibleJoinRequests = joinRequests.filter(
|
||||
(jr) => !dismissed.has(`join:${jr.id}`),
|
||||
(jr) => !isInboxEntityDismissed(dismissedAtByKey, `join:${jr.id}`, jr.updatedAt ?? jr.createdAt),
|
||||
).length;
|
||||
const visibleMineIssues = mineIssues.filter((issue) => issue.isUnreadForMe).length;
|
||||
const agentErrorCount = dashboard?.agents.error ?? 0;
|
||||
|
|
@ -369,11 +397,11 @@ export function computeInboxBadgeData({
|
|||
const showAggregateAgentError =
|
||||
agentErrorCount > 0 &&
|
||||
failedRuns === 0 &&
|
||||
!dismissed.has("alert:agent-errors");
|
||||
!dismissedAlerts.has("alert:agent-errors");
|
||||
const showBudgetAlert =
|
||||
monthBudgetCents > 0 &&
|
||||
monthUtilizationPercent >= 80 &&
|
||||
!dismissed.has("alert:budget");
|
||||
!dismissedAlerts.has("alert:budget");
|
||||
const alerts = Number(showAggregateAgentError) + Number(showBudgetAlert);
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -105,6 +105,7 @@ export const queryKeys = {
|
|||
},
|
||||
dashboard: (companyId: string) => ["dashboard", companyId] as const,
|
||||
sidebarBadges: (companyId: string) => ["sidebar-badges", companyId] as const,
|
||||
inboxDismissals: (companyId: string) => ["inbox-dismissals", companyId] as const,
|
||||
activity: (companyId: string) => ["activity", companyId] as const,
|
||||
costs: (companyId: string, from?: string, to?: string) =>
|
||||
["costs", companyId, from, to] as const,
|
||||
|
|
|
|||
|
|
@ -86,6 +86,7 @@ import {
|
|||
getInboxKeyboardSelectionIndex,
|
||||
getLatestFailedRunsByAgent,
|
||||
getRecentTouchedIssues,
|
||||
isInboxEntityDismissed,
|
||||
isMineInboxTab,
|
||||
loadInboxIssueColumns,
|
||||
normalizeInboxIssueColumns,
|
||||
|
|
@ -99,7 +100,7 @@ import {
|
|||
type InboxTab,
|
||||
type InboxWorkItem,
|
||||
} from "../lib/inbox";
|
||||
import { useDismissedInboxItems, useReadInboxItems } from "../hooks/useInboxBadge";
|
||||
import { useDismissedInboxAlerts, useInboxDismissals, useReadInboxItems } from "../hooks/useInboxBadge";
|
||||
|
||||
type InboxCategoryFilter =
|
||||
| "everything"
|
||||
|
|
@ -826,7 +827,8 @@ export function Inbox() {
|
|||
const [allCategoryFilter, setAllCategoryFilter] = useState<InboxCategoryFilter>("everything");
|
||||
const [allApprovalFilter, setAllApprovalFilter] = useState<InboxApprovalFilter>("all");
|
||||
const [visibleIssueColumns, setVisibleIssueColumns] = useState<InboxIssueColumn[]>(loadInboxIssueColumns);
|
||||
const { dismissed, dismiss } = useDismissedInboxItems();
|
||||
const { dismissed: dismissedAlerts, dismiss: dismissAlert } = useDismissedInboxAlerts();
|
||||
const { dismissedAtByKey, dismiss: dismissInboxItem } = useInboxDismissals(selectedCompanyId);
|
||||
const { readItems, markRead: markItemRead, markUnread: markItemUnread } = useReadInboxItems();
|
||||
|
||||
const pathSegment = location.pathname.split("/").pop() ?? "mine";
|
||||
|
|
@ -1033,8 +1035,11 @@ export function Inbox() {
|
|||
const currentUserId = session?.user.id ?? session?.session.userId ?? null;
|
||||
|
||||
const failedRuns = useMemo(
|
||||
() => getLatestFailedRunsByAgent(heartbeatRuns ?? []).filter((r) => !dismissed.has(`run:${r.id}`)),
|
||||
[heartbeatRuns, dismissed],
|
||||
() =>
|
||||
getLatestFailedRunsByAgent(heartbeatRuns ?? []).filter(
|
||||
(r) => !isInboxEntityDismissed(dismissedAtByKey, `run:${r.id}`, r.createdAt),
|
||||
),
|
||||
[heartbeatRuns, dismissedAtByKey],
|
||||
);
|
||||
const liveIssueIds = useMemo(() => {
|
||||
const ids = new Set<string>();
|
||||
|
|
@ -1049,10 +1054,12 @@ export function Inbox() {
|
|||
const approvalsToRender = useMemo(() => {
|
||||
let filtered = getApprovalsForTab(approvals ?? [], tab, allApprovalFilter);
|
||||
if (tab === "mine") {
|
||||
filtered = filtered.filter((a) => !dismissed.has(`approval:${a.id}`));
|
||||
filtered = filtered.filter(
|
||||
(a) => !isInboxEntityDismissed(dismissedAtByKey, `approval:${a.id}`, a.updatedAt),
|
||||
);
|
||||
}
|
||||
return filtered;
|
||||
}, [approvals, tab, allApprovalFilter, dismissed]);
|
||||
}, [approvals, tab, allApprovalFilter, dismissedAtByKey]);
|
||||
const showJoinRequestsCategory =
|
||||
allCategoryFilter === "everything" || allCategoryFilter === "join_requests";
|
||||
const showTouchedCategory =
|
||||
|
|
@ -1069,9 +1076,13 @@ export function Inbox() {
|
|||
|
||||
const joinRequestsForTab = useMemo(() => {
|
||||
if (tab === "all" && !showJoinRequestsCategory) return [];
|
||||
if (tab === "mine") return joinRequests.filter((jr) => !dismissed.has(`join:${jr.id}`));
|
||||
if (tab === "mine") {
|
||||
return joinRequests.filter(
|
||||
(jr) => !isInboxEntityDismissed(dismissedAtByKey, `join:${jr.id}`, jr.updatedAt ?? jr.createdAt),
|
||||
);
|
||||
}
|
||||
return joinRequests;
|
||||
}, [joinRequests, tab, showJoinRequestsCategory, dismissed]);
|
||||
}, [joinRequests, tab, showJoinRequestsCategory, dismissedAtByKey]);
|
||||
|
||||
const workItemsToRender = useMemo(
|
||||
() =>
|
||||
|
|
@ -1385,14 +1396,18 @@ export function Inbox() {
|
|||
const handleArchiveNonIssue = useCallback((key: string) => {
|
||||
setArchivingNonIssueIds((prev) => new Set(prev).add(key));
|
||||
setTimeout(() => {
|
||||
dismiss(key);
|
||||
if (key.startsWith("alert:")) {
|
||||
dismissAlert(key);
|
||||
} else {
|
||||
dismissInboxItem(key);
|
||||
}
|
||||
setArchivingNonIssueIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.delete(key);
|
||||
return next;
|
||||
});
|
||||
}, 200);
|
||||
}, [dismiss]);
|
||||
}, [dismissAlert, dismissInboxItem]);
|
||||
|
||||
const nonIssueUnreadState = (key: string): NonIssueUnreadState => {
|
||||
if (!canArchiveFromTab) return null;
|
||||
|
|
@ -1575,12 +1590,16 @@ export function Inbox() {
|
|||
}
|
||||
|
||||
const hasRunFailures = failedRuns.length > 0;
|
||||
const showAggregateAgentError = !!dashboard && dashboard.agents.error > 0 && !hasRunFailures && !dismissed.has("alert:agent-errors");
|
||||
const showAggregateAgentError =
|
||||
!!dashboard &&
|
||||
dashboard.agents.error > 0 &&
|
||||
!hasRunFailures &&
|
||||
!dismissedAlerts.has("alert:agent-errors");
|
||||
const showBudgetAlert =
|
||||
!!dashboard &&
|
||||
dashboard.costs.monthBudgetCents > 0 &&
|
||||
dashboard.costs.monthUtilizationPercent >= 80 &&
|
||||
!dismissed.has("alert:budget");
|
||||
!dismissedAlerts.has("alert:budget");
|
||||
const hasAlerts = showAggregateAgentError || showBudgetAlert;
|
||||
const showWorkItemsSection = filteredWorkItems.length > 0;
|
||||
const showAlertsSection = shouldShowInboxSection({
|
||||
|
|
@ -1891,7 +1910,7 @@ export function Inbox() {
|
|||
issueById={issueById}
|
||||
agentName={agentName(item.run.agentId)}
|
||||
issueLinkState={issueLinkState}
|
||||
onDismiss={() => dismiss(runKey)}
|
||||
onDismiss={() => dismissInboxItem(runKey)}
|
||||
onRetry={() => retryRunMutation.mutate(item.run)}
|
||||
isRetrying={retryingRunIds.has(item.run.id)}
|
||||
unreadState={nonIssueUnreadState(runKey)}
|
||||
|
|
@ -2049,7 +2068,7 @@ export function Inbox() {
|
|||
</Link>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => dismiss("alert:agent-errors")}
|
||||
onClick={() => dismissAlert("alert:agent-errors")}
|
||||
className="rounded-md p-1 text-muted-foreground opacity-0 transition-opacity hover:bg-accent hover:text-foreground group-hover/alert:opacity-100"
|
||||
aria-label="Dismiss"
|
||||
>
|
||||
|
|
@ -2072,7 +2091,7 @@ export function Inbox() {
|
|||
</Link>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => dismiss("alert:budget")}
|
||||
onClick={() => dismissAlert("alert:budget")}
|
||||
className="rounded-md p-1 text-muted-foreground opacity-0 transition-opacity hover:bg-accent hover:text-foreground group-hover/alert:opacity-100"
|
||||
aria-label="Dismiss"
|
||||
>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue