From 31427883ecbfd30425876bf04e29029d4d8cc5a6 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 10 Oct 2025 12:45:11 +0000 Subject: [PATCH] feat: Add High Scores view and update version Co-authored-by: meta.alex.r --- package-lock.json | 4 +- src/renderer/App.tsx | 2 + src/renderer/components/Sidebar.tsx | 3 +- src/renderer/views/HighScoresView.tsx | 600 ++++++++++++++++++++++++++ 4 files changed, 606 insertions(+), 3 deletions(-) create mode 100644 src/renderer/views/HighScoresView.tsx diff --git a/package-lock.json b/package-lock.json index 0b1ebfef..643fc824 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "bottleneck", - "version": "0.1.8", + "version": "0.1.10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "bottleneck", - "version": "0.1.8", + "version": "0.1.10", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index faf04480..3ad4806b 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -24,6 +24,7 @@ const IssueDetailView = lazy(() => import("./views/IssueDetailView")); const CursorView = lazy(() => import("./views/CursorView")); const DevinView = lazy(() => import("./views/DevinView")); const ChatGPTView = lazy(() => import("./views/ChatGPTView")); +const HighScoresView = lazy(() => import("./views/HighScoresView")); PerfLogger.mark("App.tsx module loaded"); @@ -185,6 +186,7 @@ function App() { path="/issues/:owner/:repo/:number" element={} /> + } /> } /> } /> } /> diff --git a/src/renderer/components/Sidebar.tsx b/src/renderer/components/Sidebar.tsx index fc84bd9a..8b37f4fe 100644 --- a/src/renderer/components/Sidebar.tsx +++ b/src/renderer/components/Sidebar.tsx @@ -1,7 +1,7 @@ import { useState, useMemo, useEffect, useRef } from "react"; import type { MouseEvent as ReactMouseEvent } from "react"; import { useLocation, useNavigate } from "react-router-dom"; -import { GitPullRequest, GitBranch, Settings, AlertCircle, SatelliteDish } from "lucide-react"; +import { GitPullRequest, GitBranch, Settings, AlertCircle, SatelliteDish, Trophy } from "lucide-react"; import { CursorIcon } from "./icons/CursorIcon"; import { DevinIcon } from "./icons/DevinIcon"; import { ChatGPTIcon } from "./icons/ChatGPTIcon"; @@ -26,6 +26,7 @@ const NAV_ITEMS: SidebarNavItem[] = [ { path: "/pulls", icon: GitPullRequest, label: "Pull Requests" }, { path: "/issues", icon: AlertCircle, label: "Issues" }, { path: "/branches", icon: GitBranch, label: "Branches" }, + { path: "/high-scores", icon: Trophy, label: "High Scores" }, { icon: SatelliteDish, label: "Async Agents", diff --git a/src/renderer/views/HighScoresView.tsx b/src/renderer/views/HighScoresView.tsx new file mode 100644 index 00000000..eb110867 --- /dev/null +++ b/src/renderer/views/HighScoresView.tsx @@ -0,0 +1,600 @@ +import { useMemo } from "react"; +import { Trophy, TrendingUp, Users, Zap, Target, Clock, MessageSquare, Award, Bot } from "lucide-react"; +import { usePRStore } from "../stores/prStore"; +import { useUIStore } from "../stores/uiStore"; +import { cn } from "../utils/cn"; +import { detectAgentName } from "../utils/agentIcons"; +import { AgentIcon } from "../components/AgentIcon"; + +interface StatCard { + title: string; + value: string | number; + subtitle?: string; + icon: React.ComponentType<{ className?: string }>; + trend?: { + value: number; + isPositive: boolean; + }; + color: string; +} + +interface ContributorStat { + login: string; + avatar_url: string; + prsCreated: number; + prsMerged: number; + mergeRate: number; + avgTimeToMerge: number; + totalComments: number; + isAgent: boolean; + agentType?: string; +} + +interface AgentStat { + name: string; + type: string; + prsRaised: number; + prsMerged: number; + mergeRate: number; + avgLinesChanged: number; +} + +export default function HighScoresView() { + const { pullRequests, selectedRepo } = usePRStore(); + const { theme } = useUIStore(); + + const stats = useMemo(() => { + if (!selectedRepo) return null; + + const repoKey = `${selectedRepo.owner}/${selectedRepo.name}`; + const repoPRs = Array.from(pullRequests.values()).filter(pr => + `${pr.base.repo.owner.login}/${pr.base.repo.name}` === repoKey + ); + + if (repoPRs.length === 0) return null; + + // Calculate overall metrics + const totalPRs = repoPRs.length; + const mergedPRs = repoPRs.filter(pr => pr.merged).length; + const openPRs = repoPRs.filter(pr => pr.state === "open").length; + const mergeRate = totalPRs > 0 ? (mergedPRs / totalPRs) * 100 : 0; + + // Calculate average time to merge (for merged PRs) + const mergedPRsWithTime = repoPRs.filter(pr => pr.merged && pr.merged_at && pr.created_at); + const avgTimeToMerge = mergedPRsWithTime.length > 0 + ? mergedPRsWithTime.reduce((acc, pr) => { + const created = new Date(pr.created_at).getTime(); + const merged = new Date(pr.merged_at!).getTime(); + return acc + (merged - created); + }, 0) / mergedPRsWithTime.length / (1000 * 60 * 60 * 24) // Convert to days + : 0; + + // Calculate total lines changed + const totalAdditions = repoPRs.reduce((acc, pr) => acc + (pr.additions || 0), 0); + const totalDeletions = repoPRs.reduce((acc, pr) => acc + (pr.deletions || 0), 0); + const totalComments = repoPRs.reduce((acc, pr) => acc + (pr.comments || 0), 0); + + // Calculate contributor stats + const contributorMap = new Map(); + + repoPRs.forEach(pr => { + const login = pr.user.login; + const agentType = detectAgentName(login, pr.head.ref, pr.title); + const isAgent = !!agentType; + + if (!contributorMap.has(login)) { + contributorMap.set(login, { + login, + avatar_url: pr.user.avatar_url, + prsCreated: 0, + prsMerged: 0, + mergeRate: 0, + avgTimeToMerge: 0, + totalComments: 0, + isAgent, + agentType: agentType || undefined, + }); + } + + const contributor = contributorMap.get(login)!; + contributor.prsCreated++; + contributor.totalComments += pr.comments || 0; + + if (pr.merged) { + contributor.prsMerged++; + } + }); + + // Calculate merge rates and average time for each contributor + contributorMap.forEach((contributor, login) => { + contributor.mergeRate = contributor.prsCreated > 0 + ? (contributor.prsMerged / contributor.prsCreated) * 100 + : 0; + + const contributorMergedPRs = repoPRs.filter(pr => + pr.user.login === login && pr.merged && pr.merged_at && pr.created_at + ); + + if (contributorMergedPRs.length > 0) { + contributor.avgTimeToMerge = contributorMergedPRs.reduce((acc, pr) => { + const created = new Date(pr.created_at).getTime(); + const merged = new Date(pr.merged_at!).getTime(); + return acc + (merged - created); + }, 0) / contributorMergedPRs.length / (1000 * 60 * 60 * 24); + } + }); + + const contributors = Array.from(contributorMap.values()); + const humanContributors = contributors.filter(c => !c.isAgent); + const agentContributors = contributors.filter(c => c.isAgent); + + // Calculate agent stats + const agentStats: AgentStat[] = agentContributors.map(agent => { + const agentPRs = repoPRs.filter(pr => pr.user.login === agent.login); + const avgLinesChanged = agentPRs.length > 0 + ? agentPRs.reduce((acc, pr) => acc + (pr.additions || 0) + (pr.deletions || 0), 0) / agentPRs.length + : 0; + + return { + name: agent.login, + type: agent.agentType || 'unknown', + prsRaised: agent.prsCreated, + prsMerged: agent.prsMerged, + mergeRate: agent.mergeRate, + avgLinesChanged, + }; + }); + + // Top performers + const topContributors = humanContributors + .sort((a, b) => b.prsCreated - a.prsCreated) + .slice(0, 5); + + const fastestMergers = contributors + .filter(c => c.avgTimeToMerge > 0) + .sort((a, b) => a.avgTimeToMerge - b.avgTimeToMerge) + .slice(0, 3); + + const mostActiveReviewers = contributors + .filter(c => c.totalComments > 0) + .sort((a, b) => b.totalComments - a.totalComments) + .slice(0, 3); + + return { + totalPRs, + mergedPRs, + openPRs, + mergeRate, + avgTimeToMerge, + totalAdditions, + totalDeletions, + totalComments, + contributors, + humanContributors, + agentContributors, + agentStats, + topContributors, + fastestMergers, + mostActiveReviewers, + }; + }, [pullRequests, selectedRepo]); + + if (!selectedRepo) { + return ( +
+
+ +

Select a repository to view high scores

+
+
+ ); + } + + if (!stats) { + return ( +
+
+ +

No pull request data available

+
+
+ ); + } + + const overallStats: StatCard[] = [ + { + title: "Merge Success Rate", + value: `${stats.mergeRate.toFixed(1)}%`, + subtitle: `${stats.mergedPRs}/${stats.totalPRs} PRs merged`, + icon: Target, + color: "text-green-500", + }, + { + title: "Avg Time to Merge", + value: stats.avgTimeToMerge > 0 ? `${stats.avgTimeToMerge.toFixed(1)}d` : "N/A", + subtitle: "Days from creation to merge", + icon: Clock, + color: "text-blue-500", + }, + { + title: "Total Velocity", + value: `${stats.totalAdditions + stats.totalDeletions}`, + subtitle: `+${stats.totalAdditions} -${stats.totalDeletions} lines`, + icon: TrendingUp, + color: "text-purple-500", + }, + { + title: "Collaboration Score", + value: stats.totalComments, + subtitle: `${(stats.totalComments / Math.max(stats.totalPRs, 1)).toFixed(1)} comments/PR`, + icon: MessageSquare, + color: "text-orange-500", + }, + { + title: "Active Contributors", + value: stats.contributors.length, + subtitle: `${stats.humanContributors.length} human, ${stats.agentContributors.length} AI`, + icon: Users, + color: "text-cyan-500", + }, + ]; + + return ( +
+
+ {/* Header */} +
+ +
+

+ High Scores +

+

+ Engineering metrics for {selectedRepo.full_name} +

+
+
+ + {/* Overall Stats Grid */} +
+ {overallStats.map((stat, index) => ( +
+
+ + {stat.trend && ( + + {stat.trend.isPositive ? "+" : ""}{stat.trend.value}% + + )} +
+
+ {stat.value} +
+
+ {stat.title} +
+ {stat.subtitle && ( +
+ {stat.subtitle} +
+ )} +
+ ))} +
+ + {/* Top Contributors */} +
+
+
+ +

+ Top Contributors +

+
+
+ {stats.topContributors.map((contributor, index) => ( +
+
+ {index + 1} +
+ {contributor.login} +
+
+ {contributor.login} +
+
+ {contributor.prsCreated} PRs • {contributor.mergeRate.toFixed(0)}% merged +
+
+
+ ))} +
+
+ +
+
+ +

+ Fastest Mergers +

+
+
+ {stats.fastestMergers.map((contributor, index) => ( +
+
+ {index + 1} +
+ {contributor.login} +
+
+ {contributor.login} +
+
+ {contributor.avgTimeToMerge.toFixed(1)} days avg +
+
+
+ ))} +
+
+
+ + {/* Agent Performance Section */} + {stats.agentStats.length > 0 && ( +
+
+ +

+ Agent Performance +

+
+
+ {stats.agentStats.map((agent) => ( +
+
+ +
+
+ {agent.name} +
+
+ {agent.type} +
+
+
+
+
+ + PRs Raised + + + {agent.prsRaised} + +
+
+ + PRs Merged + + + {agent.prsMerged} + +
+
+ + Merge Rate + + = 80 ? "text-green-500" : + agent.mergeRate >= 60 ? "text-yellow-500" : "text-red-500" + )}> + {agent.mergeRate.toFixed(0)}% + +
+
+ + Avg Lines/PR + + + {agent.avgLinesChanged.toFixed(0)} + +
+
+
+ ))} +
+
+ )} + + {/* Most Active Reviewers */} +
+
+ +

+ Most Active Reviewers +

+
+
+ {stats.mostActiveReviewers.map((reviewer, index) => ( +
+
+ {index + 1} +
+ {reviewer.login} +
+
+ {reviewer.login} +
+
+ {reviewer.totalComments} comments +
+
+
+ ))} +
+
+
+
+ ); +} \ No newline at end of file