Sidebar: pending-review count badge on the Review inbox item

After dispatching an agent run, its proposal goes to the review inbox, but the board
gave no signal — users thought "nothing happened". The Review inbox nav item now shows
an amber count of held actions awaiting review (polled), so waiting work is always visible.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-16 21:35:09 +03:30
parent 9c18ddfc8f
commit b398e68c8b
+36 -2
View File
@@ -1,4 +1,4 @@
import type { ReactNode } from 'react'
import { useEffect, useState, type ReactNode } from 'react'
import { Link, useLocation } from 'react-router'
import {
BookMarked,
@@ -21,12 +21,39 @@ import {
} from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Separator } from '@/components/ui/separator'
import { api } from '@/lib/api'
import { cn } from '@/lib/utils'
import { useAuth } from '@/store/auth'
/** Polls the count of held actions awaiting review, so the nav can flag work is waiting. */
function usePendingReviewCount(organizationId: string | null): number {
const [count, setCount] = useState(0)
useEffect(() => {
if (!organizationId) return
let cancelled = false
const tick = async () => {
try {
const items = await api.get<unknown[]>(`/api/governance/reviews?organizationId=${organizationId}&status=Pending`)
if (!cancelled) setCount(items.length)
} catch {
// leave the last known count on a transient failure
}
}
void tick()
const id = setInterval(tick, 12000)
return () => {
cancelled = true
clearInterval(id)
}
}, [organizationId])
return count
}
export function AppShell({ children }: { children: ReactNode }) {
const email = useAuth((s) => s.email)
const logout = useAuth((s) => s.logout)
const organizationId = useAuth((s) => s.organizationId)
const reviewCount = usePendingReviewCount(organizationId)
return (
<div className="flex min-h-screen text-foreground">
@@ -59,7 +86,7 @@ export function AppShell({ children }: { children: ReactNode }) {
<NavItem icon={LayoutDashboard} label="Board" to="/" color="#38bdf8" />
<NavItem icon={Sparkles} label="Team" to="/team" color="#38bdf8" />
<NavItem icon={Inbox} label="Cartable" to="/cartable" color="#38bdf8" />
<NavItem icon={ShieldCheck} label="Review inbox" to="/reviews" color="#38bdf8" />
<NavItem icon={ShieldCheck} label="Review inbox" to="/reviews" color="#38bdf8" badge={reviewCount} />
<NavSection label="Organization" />
<NavItem icon={Boxes} label="Structure" to="/structure" color="#34d399" />
@@ -115,12 +142,14 @@ function NavItem({
to,
muted,
color,
badge,
}: {
icon: LucideIcon
label: string
to?: string
muted?: boolean
color?: string
badge?: number
}) {
const location = useLocation()
const active = to ? location.pathname === to : false
@@ -142,6 +171,11 @@ function NavItem({
<Icon className="size-3.5" />
</span>
{label}
{!!badge && badge > 0 && (
<span className="ml-auto grid h-5 min-w-5 place-items-center rounded-full bg-amber-400 px-1.5 text-[11px] font-semibold text-amber-950">
{badge}
</span>
)}
{muted && <span className="ml-auto text-[10px] uppercase tracking-wide opacity-70">soon</span>}
</>
)