Adjust inbox tab memory and badge counts

This commit is contained in:
Dotta
2026-03-11 07:42:19 -05:00
parent 21d2b075e7
commit a503d2c12c
8 changed files with 90 additions and 26 deletions

View File

@@ -1,8 +1,31 @@
// @vitest-environment node
import { describe, expect, it } from "vitest";
import { beforeEach, describe, expect, it } from "vitest";
import type { Approval, DashboardSummary, HeartbeatRun, Issue, JoinRequest } from "@paperclipai/shared";
import { computeInboxBadgeData, getUnreadTouchedIssues } from "./inbox";
import {
computeInboxBadgeData,
getUnreadTouchedIssues,
loadLastInboxTab,
saveLastInboxTab,
} from "./inbox";
const storage = new Map<string, string>();
Object.defineProperty(globalThis, "localStorage", {
value: {
getItem: (key: string) => storage.get(key) ?? null,
setItem: (key: string, value: string) => {
storage.set(key, value);
},
removeItem: (key: string) => {
storage.delete(key);
},
clear: () => {
storage.clear();
},
},
configurable: true,
});
function makeApproval(status: Approval["status"]): Approval {
return {
@@ -142,6 +165,10 @@ const dashboard: DashboardSummary = {
};
describe("inbox helpers", () => {
beforeEach(() => {
storage.clear();
});
it("counts the same inbox sources the badge uses", () => {
const result = computeInboxBadgeData({
approvals: [makeApproval("pending"), makeApproval("approved")],
@@ -152,7 +179,7 @@ describe("inbox helpers", () => {
makeRun("run-latest", "timed_out", "2026-03-11T01:00:00.000Z"),
makeRun("run-other-agent", "failed", "2026-03-11T02:00:00.000Z", "agent-2"),
],
touchedIssues: [makeIssue("1", true), makeIssue("2", false)],
unreadIssues: [makeIssue("1", true)],
dismissed: new Set<string>(),
});
@@ -172,7 +199,7 @@ describe("inbox helpers", () => {
joinRequests: [],
dashboard,
heartbeatRuns: [makeRun("run-1", "failed", "2026-03-11T00:00:00.000Z")],
touchedIssues: [],
unreadIssues: [],
dismissed: new Set<string>(["run:run-1", "alert:budget", "alert:agent-errors"]),
});
@@ -192,4 +219,12 @@ describe("inbox helpers", () => {
expect(getUnreadTouchedIssues(issues).map((issue) => issue.id)).toEqual(["1"]);
expect(issues).toHaveLength(2);
});
it("defaults the remembered inbox tab to new and persists all", () => {
localStorage.clear();
expect(loadLastInboxTab()).toBe("new");
saveLastInboxTab("all");
expect(loadLastInboxTab()).toBe("all");
});
});

View File

@@ -10,6 +10,8 @@ export const RECENT_ISSUES_LIMIT = 100;
export const FAILED_RUN_STATUSES = new Set(["failed", "timed_out"]);
export const ACTIONABLE_APPROVAL_STATUSES = new Set(["pending", "revision_requested"]);
export const DISMISSED_KEY = "paperclip:inbox:dismissed";
export const INBOX_LAST_TAB_KEY = "paperclip:inbox:last-tab";
export type InboxTab = "new" | "all";
export interface InboxBadgeData {
inbox: number;
@@ -30,7 +32,28 @@ export function loadDismissedInboxItems(): Set<string> {
}
export function saveDismissedInboxItems(ids: Set<string>) {
localStorage.setItem(DISMISSED_KEY, JSON.stringify([...ids]));
try {
localStorage.setItem(DISMISSED_KEY, JSON.stringify([...ids]));
} catch {
// Ignore localStorage failures.
}
}
export function loadLastInboxTab(): InboxTab {
try {
const raw = localStorage.getItem(INBOX_LAST_TAB_KEY);
return raw === "all" ? "all" : "new";
} catch {
return "new";
}
}
export function saveLastInboxTab(tab: InboxTab) {
try {
localStorage.setItem(INBOX_LAST_TAB_KEY, tab);
} catch {
// Ignore localStorage failures.
}
}
export function getLatestFailedRunsByAgent(runs: HeartbeatRun[]): HeartbeatRun[] {
@@ -80,14 +103,14 @@ export function computeInboxBadgeData({
joinRequests,
dashboard,
heartbeatRuns,
touchedIssues,
unreadIssues,
dismissed,
}: {
approvals: Approval[];
joinRequests: JoinRequest[];
dashboard: DashboardSummary | undefined;
heartbeatRuns: HeartbeatRun[];
touchedIssues: Issue[];
unreadIssues: Issue[];
dismissed: Set<string>;
}): InboxBadgeData {
const actionableApprovals = approvals.filter((approval) =>
@@ -96,7 +119,7 @@ export function computeInboxBadgeData({
const failedRuns = getLatestFailedRunsByAgent(heartbeatRuns).filter(
(run) => !dismissed.has(`run:${run.id}`),
).length;
const unreadTouchedIssues = getUnreadTouchedIssues(touchedIssues).length;
const unreadTouchedIssues = unreadIssues.length;
const agentErrorCount = dashboard?.agents.error ?? 0;
const monthBudgetCents = dashboard?.costs.monthBudgetCents ?? 0;
const monthUtilizationPercent = dashboard?.costs.monthUtilizationPercent ?? 0;