diff --git a/ui/src/App.tsx b/ui/src/App.tsx index f51679de..114034d1 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -140,8 +140,10 @@ function boardRoutes() { } /> } /> } /> - } /> + } /> + } /> } /> + } /> } /> } /> diff --git a/ui/src/context/LiveUpdatesProvider.tsx b/ui/src/context/LiveUpdatesProvider.tsx index 25d0381e..fd9df800 100644 --- a/ui/src/context/LiveUpdatesProvider.tsx +++ b/ui/src/context/LiveUpdatesProvider.tsx @@ -256,7 +256,7 @@ function buildJoinRequestToast( title: `${label} wants to join`, body: "A new join request is waiting for approval.", tone: "info", - action: { label: "View inbox", href: "/inbox/new" }, + action: { label: "View inbox", href: "/inbox/unread" }, dedupeKey: `join-request:${entityId}`, }; } diff --git a/ui/src/lib/inbox.test.ts b/ui/src/lib/inbox.test.ts index 016ad9e3..ef23423f 100644 --- a/ui/src/lib/inbox.test.ts +++ b/ui/src/lib/inbox.test.ts @@ -220,11 +220,16 @@ describe("inbox helpers", () => { expect(issues).toHaveLength(2); }); - it("defaults the remembered inbox tab to new and persists all", () => { + it("defaults the remembered inbox tab to recent and persists all", () => { localStorage.clear(); - expect(loadLastInboxTab()).toBe("new"); + expect(loadLastInboxTab()).toBe("recent"); saveLastInboxTab("all"); expect(loadLastInboxTab()).toBe("all"); }); + + it("maps legacy new-tab storage to recent", () => { + localStorage.setItem("paperclip:inbox:last-tab", "new"); + expect(loadLastInboxTab()).toBe("recent"); + }); }); diff --git a/ui/src/lib/inbox.ts b/ui/src/lib/inbox.ts index 635991d4..e21dbabf 100644 --- a/ui/src/lib/inbox.ts +++ b/ui/src/lib/inbox.ts @@ -11,7 +11,7 @@ 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 type InboxTab = "recent" | "unread" | "all"; export interface InboxBadgeData { inbox: number; @@ -42,9 +42,11 @@ export function saveDismissedInboxItems(ids: Set) { export function loadLastInboxTab(): InboxTab { try { const raw = localStorage.getItem(INBOX_LAST_TAB_KEY); - return raw === "all" ? "all" : "new"; + if (raw === "all" || raw === "unread" || raw === "recent") return raw; + if (raw === "new") return "recent"; + return "recent"; } catch { - return "new"; + return "recent"; } } diff --git a/ui/src/pages/Inbox.tsx b/ui/src/pages/Inbox.tsx index 1a87b97a..4d7f3b0b 100644 --- a/ui/src/pages/Inbox.tsx +++ b/ui/src/pages/Inbox.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useState } from "react"; +import { 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"; @@ -44,7 +44,6 @@ import { ACTIONABLE_APPROVAL_STATUSES, getLatestFailedRunsByAgent, type InboxTab, - normalizeTimestamp, RECENT_ISSUES_LIMIT, saveLastInboxTab, sortIssuesByMostRecentActivity, @@ -245,8 +244,9 @@ export function Inbox() { const [allApprovalFilter, setAllApprovalFilter] = useState("all"); const { dismissed, dismiss } = useDismissedInboxItems(); - const pathSegment = location.pathname.split("/").pop() ?? "new"; - const tab: InboxTab = pathSegment === "all" ? "all" : "new"; + const pathSegment = location.pathname.split("/").pop() ?? "recent"; + const tab: InboxTab = + pathSegment === "all" || pathSegment === "unread" ? pathSegment : "recent"; const issueLinkState = useMemo( () => createIssueDetailLocationState( @@ -333,6 +333,10 @@ export function Inbox() { () => [...touchedIssuesRaw].sort(sortIssuesByMostRecentActivity).slice(0, RECENT_ISSUES_LIMIT), [touchedIssuesRaw], ); + const unreadTouchedIssues = useMemo( + () => touchedIssues.filter((issue) => issue.isUnreadForMe), + [touchedIssues], + ); const agentById = useMemo(() => { const map = new Map(); @@ -478,17 +482,22 @@ export function Inbox() { allCategoryFilter === "everything" || allCategoryFilter === "failed_runs"; const showAlertsCategory = allCategoryFilter === "everything" || allCategoryFilter === "alerts"; - const approvalsToRender = tab === "new" ? actionableApprovals : filteredAllApprovals; - const showTouchedSection = tab === "new" ? hasTouchedIssues : showTouchedCategory && hasTouchedIssues; + const approvalsToRender = tab === "unread" ? actionableApprovals : filteredAllApprovals; + const showTouchedSection = + tab === "all" + ? showTouchedCategory && hasTouchedIssues + : tab === "unread" + ? unreadTouchedIssues.length > 0 + : hasTouchedIssues; const showJoinRequestsSection = - tab === "new" ? hasJoinRequests : showJoinRequestsCategory && hasJoinRequests; + tab === "all" ? showJoinRequestsCategory && hasJoinRequests : hasJoinRequests; const showApprovalsSection = - tab === "new" + tab === "unread" ? actionableApprovals.length > 0 : showApprovalsCategory && filteredAllApprovals.length > 0; const showFailedRunsSection = - tab === "new" ? hasRunFailures : showFailedRunsCategory && hasRunFailures; - const showAlertsSection = tab === "new" ? hasAlerts : showAlertsCategory && hasAlerts; + tab === "all" ? showFailedRunsCategory && hasRunFailures : hasRunFailures; + const showAlertsSection = tab === "all" ? showAlertsCategory && hasAlerts : hasAlerts; const visibleSections = [ showFailedRunsSection ? "failed_runs" : null, @@ -511,13 +520,14 @@ export function Inbox() { return (
- navigate(`/inbox/${value === "all" ? "all" : "new"}`)}> + navigate(`/inbox/${value}`)}> @@ -572,9 +582,11 @@ export function Inbox() { )} @@ -584,7 +596,7 @@ export function Inbox() { {showSeparatorBefore("approvals") && }

- {tab === "new" ? "Approvals Needing Action" : "Approvals"} + {tab === "unread" ? "Approvals Needing Action" : "Approvals"}

{approvalsToRender.map((approval) => ( @@ -750,7 +762,7 @@ export function Inbox() { My Recent Issues
- {touchedIssues.map((issue) => { + {(tab === "unread" ? unreadTouchedIssues : touchedIssues).map((issue) => { const isUnread = issue.isUnreadForMe && !fadingOutIssues.has(issue.id); const isFading = fadingOutIssues.has(issue.id); return ( @@ -760,17 +772,18 @@ export function Inbox() { state={issueLinkState} className="flex min-w-0 cursor-pointer items-start gap-2 px-3 py-3 no-underline text-inherit transition-colors hover:bg-accent/50 sm:items-center sm:gap-3 sm:px-4" > - {/* Status icon - left column on mobile, inline on desktop */} - - - - - {/* Right column on mobile: title + metadata stacked */} - - {issue.title} + + + {issue.title} + + + {issue.lastExternalCommentAt + ? `commented ${timeAgo(issue.lastExternalCommentAt)}` + : `updated ${timeAgo(issue.updatedAt)}`} + - + {(isUnread || isFading) ? ( )} + {issue.identifier ?? issue.id.slice(0, 8)} - - · - - - {issue.lastExternalCommentAt - ? `commented ${timeAgo(issue.lastExternalCommentAt)}` - : `updated ${timeAgo(issue.updatedAt)}`} -