From aa6e47bbd0b1f802dcd9104bb380fb60444d3302 Mon Sep 17 00:00:00 2001 From: Harsh Date: Thu, 12 Mar 2026 05:13:21 +0530 Subject: [PATCH] feat: add dedicated all-projects portfolio view Treat the all-projects dashboard as a project overview surface with per-project metrics and orchestrator access, while keeping the detailed kanban lanes for single-project workflows. --- packages/web/src/components/Dashboard.tsx | 220 +++++++++++++++++++--- 1 file changed, 193 insertions(+), 27 deletions(-) diff --git a/packages/web/src/components/Dashboard.tsx b/packages/web/src/components/Dashboard.tsx index 2322e5b92..391f0aa54 100644 --- a/packages/web/src/components/Dashboard.tsx +++ b/packages/web/src/components/Dashboard.tsx @@ -46,6 +46,7 @@ export function Dashboard({ const [rateLimitDismissed, setRateLimitDismissed] = useState(false); const [globalPauseDismissed, setGlobalPauseDismissed] = useState(false); const showSidebar = projects.length > 1; + const allProjectsView = showSidebar && projectId === undefined; const grouped = useMemo(() => { const zones: Record = { @@ -72,6 +73,35 @@ export function Dashboard({ .sort((a, b) => mergeScore(a) - mergeScore(b)); }, [sessions]); + const projectOverviews = useMemo(() => { + if (!allProjectsView) return []; + + return projects.map((project) => { + const projectSessions = sessions.filter((session) => session.projectId === project.id); + const counts: Record = { + merge: 0, + respond: 0, + review: 0, + pending: 0, + working: 0, + done: 0, + }; + + for (const session of projectSessions) { + counts[getAttentionLevel(session)]++; + } + + return { + project, + orchestrator: + orchestrators.find((orchestrator) => orchestrator.projectId === project.id) ?? null, + sessionCount: projectSessions.length, + openPRCount: projectSessions.filter((session) => session.pr?.state === "open").length, + counts, + }; + }); + }, [allProjectsView, orchestrators, projects, sessions]); + const handleSend = async (sessionId: string, message: string) => { const res = await fetch(`/api/sessions/${encodeURIComponent(sessionId)}/send`, { method: "POST", @@ -152,31 +182,7 @@ export function Dashboard({ - {orchestrators.length > 0 && ( -
- {orchestrators.map((orchestrator) => ( - - - {orchestrators.length > 1 - ? `${orchestrator.projectName} orchestrator` - : "orchestrator"} - - - - - ))} -
- )} + {!allProjectsView && } {globalPause && !globalPauseDismissed && ( @@ -252,7 +258,9 @@ export function Dashboard({ )} - {hasKanbanSessions && ( + {allProjectsView && } + + {!allProjectsView && hasKanbanSessions && (
{KANBAN_LEVELS.map((level) => grouped[level].length > 0 ? ( @@ -272,7 +280,7 @@ export function Dashboard({
)} - {grouped.done.length > 0 && ( + {!allProjectsView && grouped.done.length > 0 && (
+ + orchestrator + + + + + ); + } + + return ( +
+ + + {orchestrators.length} orchestrators + + + + + +
+ ); +} + +function ProjectOverviewGrid({ + overviews, +}: { + overviews: Array<{ + project: ProjectInfo; + orchestrator: DashboardOrchestratorLink | null; + sessionCount: number; + openPRCount: number; + counts: Record; + }>; +}) { + return ( +
+ {overviews.map(({ project, orchestrator, sessionCount, openPRCount, counts }) => ( +
+
+
+

+ {project.name} +

+
+ {sessionCount} active session{sessionCount !== 1 ? "s" : ""} + {openPRCount > 0 ? ` ยท ${openPRCount} open PR${openPRCount !== 1 ? "s" : ""}` : ""} +
+
+ + Open project + +
+ +
+ + + + + +
+ +
+
+ {orchestrator ? "Per-project orchestrator available" : "No running orchestrator"} +
+ {orchestrator ? ( + + + orchestrator + + ) : null} +
+
+ ))} +
+ ); +} + +function ProjectMetric({ label, value, tone }: { label: string; value: number; tone: string }) { + return ( +
+
+ {label} +
+
+ {value} +
+
+ ); +} + function StatusLine({ stats }: { stats: DashboardStats }) { if (stats.totalSessions === 0) { return no sessions;