From c121f4d4a7ce0d1adf82bb297f1b0b0295a9aece Mon Sep 17 00:00:00 2001 From: dotta Date: Tue, 17 Mar 2026 16:10:26 -0500 Subject: [PATCH 1/9] Fix inbox recent visibility Co-Authored-By: Paperclip --- ui/src/lib/inbox.test.ts | 61 +++++++++++++++++++++++++++++++++++++++ ui/src/lib/inbox.ts | 41 ++++++++++++++++++++++++++ ui/src/pages/Inbox.tsx | 62 +++++++++++++++++++++++----------------- 3 files changed, 138 insertions(+), 26 deletions(-) diff --git a/ui/src/lib/inbox.test.ts b/ui/src/lib/inbox.test.ts index 9b7edb06..0576dcf2 100644 --- a/ui/src/lib/inbox.test.ts +++ b/ui/src/lib/inbox.test.ts @@ -3,12 +3,14 @@ import { beforeEach, describe, expect, it } from "vitest"; import type { Approval, DashboardSummary, HeartbeatRun, Issue, JoinRequest } from "@paperclipai/shared"; import { + getApprovalsForTab, computeInboxBadgeData, getRecentTouchedIssues, getUnreadTouchedIssues, loadLastInboxTab, RECENT_ISSUES_LIMIT, saveLastInboxTab, + shouldShowInboxSection, } from "./inbox"; const storage = new Map(); @@ -46,6 +48,19 @@ function makeApproval(status: Approval["status"]): Approval { }; } +function makeApprovalWithTimestamps( + id: string, + status: Approval["status"], + updatedAt: string, +): Approval { + return { + ...makeApproval(status), + id, + createdAt: new Date(updatedAt), + updatedAt: new Date(updatedAt), + }; +} + function makeJoinRequest(id: string): JoinRequest { return { id, @@ -231,6 +246,52 @@ describe("inbox helpers", () => { expect(issues).toHaveLength(2); }); + it("shows recent approvals in updated order and unread approvals as actionable only", () => { + const approvals = [ + makeApprovalWithTimestamps("approval-approved", "approved", "2026-03-11T02:00:00.000Z"), + makeApprovalWithTimestamps("approval-pending", "pending", "2026-03-11T01:00:00.000Z"), + makeApprovalWithTimestamps( + "approval-revision", + "revision_requested", + "2026-03-11T03:00:00.000Z", + ), + ]; + + expect(getApprovalsForTab(approvals, "recent", "all").map((approval) => approval.id)).toEqual([ + "approval-revision", + "approval-approved", + "approval-pending", + ]); + expect(getApprovalsForTab(approvals, "unread", "all").map((approval) => approval.id)).toEqual([ + "approval-revision", + "approval-pending", + ]); + expect(getApprovalsForTab(approvals, "all", "resolved").map((approval) => approval.id)).toEqual([ + "approval-approved", + ]); + }); + + it("can include sections on recent without forcing them to be unread", () => { + expect( + shouldShowInboxSection({ + tab: "recent", + hasItems: true, + showOnRecent: true, + showOnUnread: false, + showOnAll: false, + }), + ).toBe(true); + expect( + shouldShowInboxSection({ + tab: "unread", + hasItems: true, + showOnRecent: true, + showOnUnread: false, + showOnAll: false, + }), + ).toBe(false); + }); + it("limits recent touched issues before unread badge counting", () => { const issues = Array.from({ length: RECENT_ISSUES_LIMIT + 5 }, (_, index) => { const issue = makeIssue(String(index + 1), index < 3); diff --git a/ui/src/lib/inbox.ts b/ui/src/lib/inbox.ts index 88447a98..9edba8ac 100644 --- a/ui/src/lib/inbox.ts +++ b/ui/src/lib/inbox.ts @@ -12,6 +12,7 @@ export const ACTIONABLE_APPROVAL_STATUSES = new Set(["pending", "revision_reques export const DISMISSED_KEY = "paperclip:inbox:dismissed"; export const INBOX_LAST_TAB_KEY = "paperclip:inbox:last-tab"; export type InboxTab = "recent" | "unread" | "all"; +export type InboxApprovalFilter = "all" | "actionable" | "resolved"; export interface InboxBadgeData { inbox: number; @@ -104,6 +105,46 @@ export function getUnreadTouchedIssues(issues: Issue[]): Issue[] { return issues.filter((issue) => issue.isUnreadForMe); } +export function getApprovalsForTab( + approvals: Approval[], + tab: InboxTab, + filter: InboxApprovalFilter, +): Approval[] { + const sortedApprovals = [...approvals].sort( + (a, b) => normalizeTimestamp(b.updatedAt) - normalizeTimestamp(a.updatedAt), + ); + + if (tab === "recent") return sortedApprovals; + if (tab === "unread") { + return sortedApprovals.filter((approval) => ACTIONABLE_APPROVAL_STATUSES.has(approval.status)); + } + if (filter === "all") return sortedApprovals; + + return sortedApprovals.filter((approval) => { + const isActionable = ACTIONABLE_APPROVAL_STATUSES.has(approval.status); + return filter === "actionable" ? isActionable : !isActionable; + }); +} + +export function shouldShowInboxSection({ + tab, + hasItems, + showOnRecent, + showOnUnread, + showOnAll, +}: { + tab: InboxTab; + hasItems: boolean; + showOnRecent: boolean; + showOnUnread: boolean; + showOnAll: boolean; +}): boolean { + if (!hasItems) return false; + if (tab === "recent") return showOnRecent; + if (tab === "unread") return showOnUnread; + return showOnAll; +} + export function computeInboxBadgeData({ approvals, joinRequests, diff --git a/ui/src/pages/Inbox.tsx b/ui/src/pages/Inbox.tsx index a00cc9b8..bf580f33 100644 --- a/ui/src/pages/Inbox.tsx +++ b/ui/src/pages/Inbox.tsx @@ -43,10 +43,13 @@ import { PageTabBar } from "../components/PageTabBar"; import type { HeartbeatRun, Issue, JoinRequest } from "@paperclipai/shared"; import { ACTIONABLE_APPROVAL_STATUSES, + getApprovalsForTab, getLatestFailedRunsByAgent, getRecentTouchedIssues, + InboxApprovalFilter, type InboxTab, saveLastInboxTab, + shouldShowInboxSection, } from "../lib/inbox"; import { useDismissedInboxItems } from "../hooks/useInboxBadge"; @@ -57,7 +60,6 @@ type InboxCategoryFilter = | "approvals" | "failed_runs" | "alerts"; -type InboxApprovalFilter = "all" | "actionable" | "resolved"; type SectionKey = | "issues_i_touched" | "join_requests" @@ -362,10 +364,7 @@ export function Inbox() { }, [heartbeatRuns]); const allApprovals = useMemo( - () => - [...(approvals ?? [])].sort( - (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(), - ), + () => getApprovalsForTab(approvals ?? [], "recent", "all"), [approvals], ); @@ -374,14 +373,10 @@ export function Inbox() { [allApprovals], ); - const filteredAllApprovals = useMemo(() => { - if (allApprovalFilter === "all") return allApprovals; - - return allApprovals.filter((approval) => { - const isActionable = ACTIONABLE_APPROVAL_STATUSES.has(approval.status); - return allApprovalFilter === "actionable" ? isActionable : !isActionable; - }); - }, [allApprovals, allApprovalFilter]); + const approvalsToRender = useMemo( + () => getApprovalsForTab(approvals ?? [], tab, allApprovalFilter), + [approvals, tab, allApprovalFilter], + ); const agentName = (id: string | null) => { if (!id) return null; @@ -516,21 +511,36 @@ export function Inbox() { allCategoryFilter === "everything" || allCategoryFilter === "failed_runs"; const showAlertsCategory = allCategoryFilter === "everything" || allCategoryFilter === "alerts"; - const approvalsToRender = tab === "all" ? filteredAllApprovals : actionableApprovals; - const showTouchedSection = - tab === "all" - ? showTouchedCategory && hasTouchedIssues - : tab === "unread" - ? unreadTouchedIssues.length > 0 - : hasTouchedIssues; + const showTouchedSection = shouldShowInboxSection({ + tab, + hasItems: tab === "unread" ? unreadTouchedIssues.length > 0 : hasTouchedIssues, + showOnRecent: hasTouchedIssues, + showOnUnread: unreadTouchedIssues.length > 0, + showOnAll: showTouchedCategory && hasTouchedIssues, + }); const showJoinRequestsSection = tab === "all" ? showJoinRequestsCategory && hasJoinRequests : tab === "unread" && hasJoinRequests; - const showApprovalsSection = tab === "all" - ? showApprovalsCategory && filteredAllApprovals.length > 0 - : actionableApprovals.length > 0; - const showFailedRunsSection = - tab === "all" ? showFailedRunsCategory && hasRunFailures : tab === "unread" && hasRunFailures; - const showAlertsSection = tab === "all" ? showAlertsCategory && hasAlerts : tab === "unread" && hasAlerts; + const showApprovalsSection = shouldShowInboxSection({ + tab, + hasItems: approvalsToRender.length > 0, + showOnRecent: approvalsToRender.length > 0, + showOnUnread: actionableApprovals.length > 0, + showOnAll: showApprovalsCategory && approvalsToRender.length > 0, + }); + const showFailedRunsSection = shouldShowInboxSection({ + tab, + hasItems: hasRunFailures, + showOnRecent: hasRunFailures, + showOnUnread: hasRunFailures, + showOnAll: showFailedRunsCategory && hasRunFailures, + }); + const showAlertsSection = shouldShowInboxSection({ + tab, + hasItems: hasAlerts, + showOnRecent: hasAlerts, + showOnUnread: hasAlerts, + showOnAll: showAlertsCategory && hasAlerts, + }); const visibleSections = [ showFailedRunsSection ? "failed_runs" : null, From 22ae70649bc97f04b648453f0ba6bde35f4fb371 Mon Sep 17 00:00:00 2001 From: dotta Date: Tue, 17 Mar 2026 16:19:00 -0500 Subject: [PATCH 2/9] Mix approvals into inbox activity feed Co-Authored-By: Paperclip --- ui/src/lib/inbox.test.ts | 28 +++- ui/src/lib/inbox.ts | 50 +++++++ ui/src/pages/Inbox.tsx | 302 ++++++++++++++++++++++++--------------- 3 files changed, 260 insertions(+), 120 deletions(-) diff --git a/ui/src/lib/inbox.test.ts b/ui/src/lib/inbox.test.ts index 0576dcf2..4821b6cf 100644 --- a/ui/src/lib/inbox.test.ts +++ b/ui/src/lib/inbox.test.ts @@ -3,8 +3,9 @@ import { beforeEach, describe, expect, it } from "vitest"; import type { Approval, DashboardSummary, HeartbeatRun, Issue, JoinRequest } from "@paperclipai/shared"; import { - getApprovalsForTab, computeInboxBadgeData, + getApprovalsForTab, + getInboxWorkItems, getRecentTouchedIssues, getUnreadTouchedIssues, loadLastInboxTab, @@ -271,6 +272,31 @@ describe("inbox helpers", () => { ]); }); + it("mixes approvals into the inbox feed by most recent activity", () => { + const newerIssue = makeIssue("1", true); + newerIssue.lastExternalCommentAt = new Date("2026-03-11T04:00:00.000Z"); + + const olderIssue = makeIssue("2", false); + olderIssue.lastExternalCommentAt = new Date("2026-03-11T02:00:00.000Z"); + + const approval = makeApprovalWithTimestamps( + "approval-between", + "pending", + "2026-03-11T03:00:00.000Z", + ); + + expect( + getInboxWorkItems({ + issues: [olderIssue, newerIssue], + approvals: [approval], + }).map((item) => item.kind === "issue" ? `issue:${item.issue.id}` : `approval:${item.approval.id}`), + ).toEqual([ + "issue:1", + "approval:approval-between", + "issue:2", + ]); + }); + it("can include sections on recent without forcing them to be unread", () => { expect( shouldShowInboxSection({ diff --git a/ui/src/lib/inbox.ts b/ui/src/lib/inbox.ts index 9edba8ac..b9a74f72 100644 --- a/ui/src/lib/inbox.ts +++ b/ui/src/lib/inbox.ts @@ -13,6 +13,17 @@ export const DISMISSED_KEY = "paperclip:inbox:dismissed"; export const INBOX_LAST_TAB_KEY = "paperclip:inbox:last-tab"; export type InboxTab = "recent" | "unread" | "all"; export type InboxApprovalFilter = "all" | "actionable" | "resolved"; +export type InboxWorkItem = + | { + kind: "issue"; + timestamp: number; + issue: Issue; + } + | { + kind: "approval"; + timestamp: number; + approval: Approval; + }; export interface InboxBadgeData { inbox: number; @@ -126,6 +137,45 @@ export function getApprovalsForTab( }); } +export function approvalActivityTimestamp(approval: Approval): number { + const updatedAt = normalizeTimestamp(approval.updatedAt); + if (updatedAt > 0) return updatedAt; + return normalizeTimestamp(approval.createdAt); +} + +export function getInboxWorkItems({ + issues, + approvals, +}: { + issues: Issue[]; + approvals: Approval[]; +}): InboxWorkItem[] { + return [ + ...issues.map((issue) => ({ + kind: "issue" as const, + timestamp: issueLastActivityTimestamp(issue), + issue, + })), + ...approvals.map((approval) => ({ + kind: "approval" as const, + timestamp: approvalActivityTimestamp(approval), + approval, + })), + ].sort((a, b) => { + const timestampDiff = b.timestamp - a.timestamp; + if (timestampDiff !== 0) return timestampDiff; + + if (a.kind === "issue" && b.kind === "issue") { + return sortIssuesByMostRecentActivity(a.issue, b.issue); + } + if (a.kind === "approval" && b.kind === "approval") { + return approvalActivityTimestamp(b.approval) - approvalActivityTimestamp(a.approval); + } + + return a.kind === "approval" ? -1 : 1; + }); +} + export function shouldShowInboxSection({ tab, hasItems, diff --git a/ui/src/pages/Inbox.tsx b/ui/src/pages/Inbox.tsx index bf580f33..7339dc1b 100644 --- a/ui/src/pages/Inbox.tsx +++ b/ui/src/pages/Inbox.tsx @@ -14,11 +14,11 @@ import { queryKeys } from "../lib/queryKeys"; import { createIssueDetailLocationState } from "../lib/issueDetailBreadcrumb"; import { EmptyState } from "../components/EmptyState"; import { PageSkeleton } from "../components/PageSkeleton"; -import { ApprovalCard } from "../components/ApprovalCard"; import { IssueRow } from "../components/IssueRow"; import { PriorityIcon } from "../components/PriorityIcon"; import { StatusIcon } from "../components/StatusIcon"; import { StatusBadge } from "../components/StatusBadge"; +import { defaultTypeIcon, typeIcon, typeLabel } from "../components/ApprovalPayload"; import { timeAgo } from "../lib/timeAgo"; import { Button } from "@/components/ui/button"; import { Separator } from "@/components/ui/separator"; @@ -40,16 +40,17 @@ import { } from "lucide-react"; import { Identity } from "../components/Identity"; import { PageTabBar } from "../components/PageTabBar"; -import type { HeartbeatRun, Issue, JoinRequest } from "@paperclipai/shared"; +import type { Approval, HeartbeatRun, Issue, JoinRequest } from "@paperclipai/shared"; import { ACTIONABLE_APPROVAL_STATUSES, getApprovalsForTab, + getInboxWorkItems, getLatestFailedRunsByAgent, getRecentTouchedIssues, InboxApprovalFilter, - type InboxTab, saveLastInboxTab, shouldShowInboxSection, + type InboxTab, } from "../lib/inbox"; import { useDismissedInboxItems } from "../hooks/useInboxBadge"; @@ -61,9 +62,8 @@ type InboxCategoryFilter = | "failed_runs" | "alerts"; type SectionKey = - | "issues_i_touched" + | "work_items" | "join_requests" - | "approvals" | "failed_runs" | "alerts"; @@ -84,6 +84,10 @@ function runFailureMessage(run: HeartbeatRun): string { return firstNonEmptyLine(run.error) ?? firstNonEmptyLine(run.stderrExcerpt) ?? "Run exited with an error."; } +function approvalStatusLabel(status: Approval["status"]): string { + return status.replaceAll("_", " "); +} + function readIssueIdFromRun(run: HeartbeatRun): string | null { const context = run.contextSnapshot; if (!context) return null; @@ -235,6 +239,93 @@ function FailedRunCard({ ); } +function ApprovalInboxRow({ + approval, + requesterName, + onApprove, + onReject, + isPending, +}: { + approval: Approval; + requesterName: string | null; + onApprove: () => void; + onReject: () => void; + isPending: boolean; +}) { + const Icon = typeIcon[approval.type] ?? defaultTypeIcon; + const label = typeLabel[approval.type] ?? approval.type; + const showResolutionButtons = + approval.type !== "budget_override_required" && + ACTIONABLE_APPROVAL_STATUSES.has(approval.status); + + return ( +
+
+ + + + + + + {label} + + + {approvalStatusLabel(approval.status)} + {requesterName ? requested by {requesterName} : null} + updated {timeAgo(approval.updatedAt)} + + + + {showResolutionButtons ? ( +
+ + +
+ ) : null} +
+ {showResolutionButtons ? ( +
+ + +
+ ) : null} +
+ ); +} + export function Inbox() { const { selectedCompanyId } = useCompany(); const { setBreadcrumbs } = useBreadcrumbs(); @@ -336,6 +427,10 @@ export function Inbox() { () => touchedIssues.filter((issue) => issue.isUnreadForMe), [touchedIssues], ); + const issuesToRender = useMemo( + () => (tab === "unread" ? unreadTouchedIssues : touchedIssues), + [tab, touchedIssues, unreadTouchedIssues], + ); const agentById = useMemo(() => { const map = new Map(); @@ -363,20 +458,27 @@ export function Inbox() { return ids; }, [heartbeatRuns]); - const allApprovals = useMemo( - () => getApprovalsForTab(approvals ?? [], "recent", "all"), - [approvals], - ); - - const actionableApprovals = useMemo( - () => allApprovals.filter((approval) => ACTIONABLE_APPROVAL_STATUSES.has(approval.status)), - [allApprovals], - ); - const approvalsToRender = useMemo( () => getApprovalsForTab(approvals ?? [], tab, allApprovalFilter), [approvals, tab, allApprovalFilter], ); + const showJoinRequestsCategory = + allCategoryFilter === "everything" || allCategoryFilter === "join_requests"; + const showTouchedCategory = + allCategoryFilter === "everything" || allCategoryFilter === "issues_i_touched"; + const showApprovalsCategory = + allCategoryFilter === "everything" || allCategoryFilter === "approvals"; + const showFailedRunsCategory = + allCategoryFilter === "everything" || allCategoryFilter === "failed_runs"; + const showAlertsCategory = allCategoryFilter === "everything" || allCategoryFilter === "alerts"; + const workItemsToRender = useMemo( + () => + getInboxWorkItems({ + issues: tab === "all" && !showTouchedCategory ? [] : issuesToRender, + approvals: tab === "all" && !showApprovalsCategory ? [] : approvalsToRender, + }), + [approvalsToRender, issuesToRender, showApprovalsCategory, showTouchedCategory, tab], + ); const agentName = (id: string | null) => { if (!id) return null; @@ -500,33 +602,9 @@ export function Inbox() { !dismissed.has("alert:budget"); const hasAlerts = showAggregateAgentError || showBudgetAlert; const hasJoinRequests = joinRequests.length > 0; - const hasTouchedIssues = touchedIssues.length > 0; - - const showJoinRequestsCategory = - allCategoryFilter === "everything" || allCategoryFilter === "join_requests"; - const showTouchedCategory = - allCategoryFilter === "everything" || allCategoryFilter === "issues_i_touched"; - const showApprovalsCategory = allCategoryFilter === "everything" || allCategoryFilter === "approvals"; - const showFailedRunsCategory = - allCategoryFilter === "everything" || allCategoryFilter === "failed_runs"; - const showAlertsCategory = allCategoryFilter === "everything" || allCategoryFilter === "alerts"; - - const showTouchedSection = shouldShowInboxSection({ - tab, - hasItems: tab === "unread" ? unreadTouchedIssues.length > 0 : hasTouchedIssues, - showOnRecent: hasTouchedIssues, - showOnUnread: unreadTouchedIssues.length > 0, - showOnAll: showTouchedCategory && hasTouchedIssues, - }); + const showWorkItemsSection = workItemsToRender.length > 0; const showJoinRequestsSection = tab === "all" ? showJoinRequestsCategory && hasJoinRequests : tab === "unread" && hasJoinRequests; - const showApprovalsSection = shouldShowInboxSection({ - tab, - hasItems: approvalsToRender.length > 0, - showOnRecent: approvalsToRender.length > 0, - showOnUnread: actionableApprovals.length > 0, - showOnAll: showApprovalsCategory && approvalsToRender.length > 0, - }); const showFailedRunsSection = shouldShowInboxSection({ tab, hasItems: hasRunFailures, @@ -545,9 +623,8 @@ export function Inbox() { const visibleSections = [ showFailedRunsSection ? "failed_runs" : null, showAlertsSection ? "alerts" : null, - showApprovalsSection ? "approvals" : null, showJoinRequestsSection ? "join_requests" : null, - showTouchedSection ? "issues_i_touched" : null, + showWorkItemsSection ? "work_items" : null, ].filter((key): key is SectionKey => key !== null); const allLoaded = @@ -653,29 +730,72 @@ export function Inbox() { /> )} - {showApprovalsSection && ( + {showWorkItemsSection && ( <> - {showSeparatorBefore("approvals") && } + {showSeparatorBefore("work_items") && }
-

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

-
- {approvalsToRender.map((approval) => ( - a.id === approval.requestedByAgentId) ?? null - : null - } - onApprove={() => approveMutation.mutate(approval.id)} - onReject={() => rejectMutation.mutate(approval.id)} - detailLink={`/approvals/${approval.id}`} - isPending={approveMutation.isPending || rejectMutation.isPending} - /> - ))} +
+ {workItemsToRender.map((item) => { + if (item.kind === "approval") { + return ( + approveMutation.mutate(item.approval.id)} + onReject={() => rejectMutation.mutate(item.approval.id)} + isPending={approveMutation.isPending || rejectMutation.isPending} + /> + ); + } + + const issue = item.issue; + const isUnread = issue.isUnreadForMe && !fadingOutIssues.has(issue.id); + const isFading = fadingOutIssues.has(issue.id); + return ( + + + + + + + + + {issue.identifier ?? issue.id.slice(0, 8)} + + {liveIssueIds.has(issue.id) && ( + + + + + + + Live + + + )} + + )} + mobileMeta={ + issue.lastExternalCommentAt + ? `commented ${timeAgo(issue.lastExternalCommentAt)}` + : `updated ${timeAgo(issue.updatedAt)}` + } + unreadState={isUnread ? "visible" : isFading ? "fading" : "hidden"} + onMarkRead={() => markReadMutation.mutate(issue.id)} + trailingMeta={ + issue.lastExternalCommentAt + ? `commented ${timeAgo(issue.lastExternalCommentAt)}` + : `updated ${timeAgo(issue.updatedAt)}` + } + /> + ); + })}
@@ -816,62 +936,6 @@ export function Inbox() { )} - {showTouchedSection && ( - <> - {showSeparatorBefore("issues_i_touched") && } -
-
- {(tab === "unread" ? unreadTouchedIssues : touchedIssues).map((issue) => { - const isUnread = issue.isUnreadForMe && !fadingOutIssues.has(issue.id); - const isFading = fadingOutIssues.has(issue.id); - return ( - - - - - - - - - {issue.identifier ?? issue.id.slice(0, 8)} - - {liveIssueIds.has(issue.id) && ( - - - - - - - Live - - - )} - - )} - mobileMeta={ - issue.lastExternalCommentAt - ? `commented ${timeAgo(issue.lastExternalCommentAt)}` - : `updated ${timeAgo(issue.updatedAt)}` - } - unreadState={isUnread ? "visible" : isFading ? "fading" : "hidden"} - onMarkRead={() => markReadMutation.mutate(issue.id)} - trailingMeta={ - issue.lastExternalCommentAt - ? `commented ${timeAgo(issue.lastExternalCommentAt)}` - : `updated ${timeAgo(issue.updatedAt)}` - } - /> - ); - })} -
-
- - )}
); } From bfb1960703e29cf9d99caa5b61142064bf5ebb60 Mon Sep 17 00:00:00 2001 From: dotta Date: Tue, 17 Mar 2026 17:12:41 -0500 Subject: [PATCH 3/9] fix: show only 'v' in sidebar with full version on hover tooltip The full version string was pushing the sidebar too wide. Now displays just "v" with the full version (e.g. "v1.2.3") shown on hover via title attribute, for both mobile and desktop sidebar layouts. Fixes PAP-533 Co-Authored-By: Paperclip --- ui/src/components/Layout.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/src/components/Layout.tsx b/ui/src/components/Layout.tsx index 8d2e3e6f..c60d6880 100644 --- a/ui/src/components/Layout.tsx +++ b/ui/src/components/Layout.tsx @@ -298,7 +298,7 @@ export function Layout() { Documentation {health?.version && ( - v{health.version} + v )}