diff --git a/package-lock.json b/package-lock.json index 0b1ebfef..4ac53fb1 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": { @@ -17,7 +17,7 @@ "@tanstack/react-query": "^5.17.9", "clsx": "^2.1.0", "date-fns": "^3.2.0", - "dotenv": "^17.2.2", + "dotenv": "^17.2.3", "electron-store": "^8.1.0", "electron-updater": "^6.6.2", "lucide-react": "^0.312.0", @@ -5333,9 +5333,9 @@ } }, "node_modules/dotenv": { - "version": "17.2.2", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.2.tgz", - "integrity": "sha512-Sf2LSQP+bOlhKWWyhFsn0UsfdK/kCWRv1iuA2gXAwt3dyNabr6QSj00I2V10pidqz69soatm9ZwZvpQMTIOd5Q==", + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", + "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", "license": "BSD-2-Clause", "engines": { "node": ">=12" diff --git a/package.json b/package.json index 763f06c0..cb422ea9 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "@tanstack/react-query": "^5.17.9", "clsx": "^2.1.0", "date-fns": "^3.2.0", - "dotenv": "^17.2.2", + "dotenv": "^17.2.3", "electron-store": "^8.1.0", "electron-updater": "^6.6.2", "lucide-react": "^0.312.0", @@ -132,4 +132,4 @@ "target": "nsis" } } -} \ No newline at end of file +} diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index faf04480..7fdb616f 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"); @@ -180,6 +181,7 @@ function App() { element={} /> } /> + } /> } /> { - const { data } = await this.octokit.repos.listForAuthenticatedUser({ - page, - per_page: perPage, - sort: "updated", - }); + async getRepositories(): Promise { + const repositories: Repository[] = []; + let page = 1; + const perPage = 100; // GitHub's max per page - return data.map((repo) => ({ - id: repo.id, - owner: repo.owner.login, - name: repo.name, - full_name: repo.full_name, - description: repo.description, - default_branch: repo.default_branch || "main", - private: repo.private, - clone_url: repo.clone_url, - updated_at: repo.updated_at, - pushed_at: repo.pushed_at, - stargazers_count: repo.stargazers_count, - open_issues_count: repo.open_issues_count, - })); + while (true) { + const { data } = await this.octokit.repos.listForAuthenticatedUser({ + page, + per_page: perPage, + sort: "updated", + visibility: "all", // Explicitly include both public and private repos + }); + + if (data.length === 0) break; + + repositories.push(...data.map((repo) => ({ + id: repo.id, + owner: repo.owner.login, + name: repo.name, + full_name: repo.full_name, + description: repo.description, + default_branch: repo.default_branch || "main", + private: repo.private, + clone_url: repo.clone_url, + updated_at: repo.updated_at, + pushed_at: repo.pushed_at, + stargazers_count: repo.stargazers_count, + open_issues_count: repo.open_issues_count, + }))); + + // If we got less than a full page, we're done + if (data.length < perPage) break; + + page++; + } + + console.log(`Fetched ${repositories.length} repositories (public + private)`); + return repositories; } async getPullRequests( diff --git a/src/renderer/views/HighScoresView.tsx b/src/renderer/views/HighScoresView.tsx new file mode 100644 index 00000000..7d76e34b --- /dev/null +++ b/src/renderer/views/HighScoresView.tsx @@ -0,0 +1,484 @@ +import React, { useMemo } from "react"; +import { usePRStore } from "../stores/prStore"; +import { useUIStore } from "../stores/uiStore"; +import { cn } from "../utils/cn"; +import { + Trophy, + Users, + GitPullRequest, + GitMerge, + Clock, + TrendingUp, + Code, + MessageSquare, + CheckCircle, + AlertCircle, + BarChart3, + Zap +} from "lucide-react"; + +interface StatCardProps { + title: string; + value: string | number; + subtitle?: string; + icon: React.ComponentType<{ className?: string }>; + trend?: "up" | "down" | "neutral"; + color?: "blue" | "green" | "yellow" | "red" | "purple"; +} + +function StatCard({ title, value, subtitle, icon: Icon, trend, color = "blue" }: StatCardProps) { + const { theme } = useUIStore(); + + const colorClasses = { + blue: "bg-blue-50 border-blue-200 text-blue-700 dark:bg-blue-900/20 dark:border-blue-800 dark:text-blue-300", + green: "bg-green-50 border-green-200 text-green-700 dark:bg-green-900/20 dark:border-green-800 dark:text-green-300", + yellow: "bg-yellow-50 border-yellow-200 text-yellow-700 dark:bg-yellow-900/20 dark:border-yellow-800 dark:text-yellow-300", + red: "bg-red-50 border-red-200 text-red-700 dark:bg-red-900/20 dark:border-red-800 dark:text-red-300", + purple: "bg-purple-50 border-purple-200 text-purple-700 dark:bg-purple-900/20 dark:border-purple-800 dark:text-purple-300", + }; + + const trendIcon = { + up: , + down: , + neutral: , + }; + + return ( +
+
+
+
+ +
+
+

{title}

+

{value}

+ {subtitle &&

{subtitle}

} +
+
+ {trend && ( +
+ {trendIcon[trend]} +
+ )} +
+
+ ); +} + +interface AgentStats { + name: string; + prsRaised: number; + prsMerged: number; + mergeRate: number; + avgTimeToMerge: number; + totalLinesChanged: number; + avgPrSize: number; +} + +function AgentCard({ agent }: { agent: AgentStats }) { + const { theme } = useUIStore(); + + return ( +
+
+

{agent.name}

+
+ {agent.mergeRate.toFixed(1)}% merge rate +
+
+ +
+
+
PRs Raised
+
{agent.prsRaised}
+
+
+
PRs Merged
+
{agent.prsMerged}
+
+
+
Avg Time to Merge
+
{agent.avgTimeToMerge}h
+
+
+
Avg PR Size
+
{agent.avgPrSize} lines
+
+
+ +
+
Total Lines Changed
+
{agent.totalLinesChanged.toLocaleString()}
+
+
+ ); +} + +export default function HighScoresView() { + const { pullRequests, selectedRepo } = usePRStore(); + const { theme } = useUIStore(); + + const stats = useMemo(() => { + if (!selectedRepo) { + return { + totalPRs: 0, + mergedPRs: 0, + openPRs: 0, + draftPRs: 0, + avgTimeToMerge: 0, + totalContributors: 0, + humanContributors: 0, + agentContributors: 0, + avgPrSize: 0, + totalLinesChanged: 0, + avgCommentsPerPR: 0, + mergeRate: 0, + avgReviewTime: 0, + mostActiveContributor: "", + mostProductiveAgent: "", + agents: [] as AgentStats[], + }; + } + + const repoPRs = Array.from(pullRequests.values()).filter((pr) => { + const baseOwner = pr.base?.repo?.owner?.login; + const baseName = pr.base?.repo?.name; + return baseOwner === selectedRepo.owner && baseName === selectedRepo.name; + }); + + const totalPRs = repoPRs.length; + const mergedPRs = repoPRs.filter(pr => pr.merged).length; + const openPRs = repoPRs.filter(pr => pr.state === "open" && !pr.merged).length; + const draftPRs = repoPRs.filter(pr => pr.draft).length; + + // Calculate average time to merge + 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); + const merged = new Date(pr.merged_at!); + return acc + (merged.getTime() - created.getTime()) / (1000 * 60 * 60); // hours + }, 0) / mergedPRsWithTime.length + : 0; + + // Calculate contributors + const contributors = new Set(repoPRs.map(pr => pr.user.login)); + const totalContributors = contributors.size; + + // Separate human and agent contributors + const agentPatterns = ['cursor-ai', 'devin-ai', 'chatgpt-ai', 'claude-ai', 'copilot-ai']; + const humanContributors = Array.from(contributors).filter(login => + !agentPatterns.some(pattern => login.toLowerCase().includes(pattern)) + ); + const agentContributors = Array.from(contributors).filter(login => + agentPatterns.some(pattern => login.toLowerCase().includes(pattern)) + ); + + // Calculate PR size and lines changed + const prsWithSize = repoPRs.filter(pr => pr.additions !== undefined && pr.deletions !== undefined); + const avgPrSize = prsWithSize.length > 0 + ? prsWithSize.reduce((acc, pr) => acc + (pr.additions || 0) + (pr.deletions || 0), 0) / prsWithSize.length + : 0; + + const totalLinesChanged = prsWithSize.reduce((acc, pr) => + acc + (pr.additions || 0) + (pr.deletions || 0), 0 + ); + + // Calculate average comments per PR + const avgCommentsPerPR = totalPRs > 0 + ? repoPRs.reduce((acc, pr) => acc + pr.comments, 0) / totalPRs + : 0; + + const mergeRate = totalPRs > 0 ? (mergedPRs / totalPRs) * 100 : 0; + + // Calculate most active contributor + const contributorCounts = new Map(); + repoPRs.forEach(pr => { + const count = contributorCounts.get(pr.user.login) || 0; + contributorCounts.set(pr.user.login, count + 1); + }); + const mostActiveContributor = contributorCounts.size > 0 + ? Array.from(contributorCounts.entries()).sort((a, b) => b[1] - a[1])[0][0] + : ""; + + // Calculate agent stats + const agentStats = new Map(); + + repoPRs.forEach(pr => { + const isAgent = agentPatterns.some(pattern => + pr.user.login.toLowerCase().includes(pattern) + ); + + if (isAgent) { + const agentName = pr.user.login; + const current = agentStats.get(agentName) || { + prsRaised: 0, + prsMerged: 0, + totalLinesChanged: 0, + mergeTimes: [], + }; + + current.prsRaised++; + if (pr.merged) { + current.prsMerged++; + if (pr.merged_at && pr.created_at) { + const created = new Date(pr.created_at); + const merged = new Date(pr.merged_at); + current.mergeTimes.push((merged.getTime() - created.getTime()) / (1000 * 60 * 60)); + } + } + + if (pr.additions !== undefined && pr.deletions !== undefined) { + current.totalLinesChanged += pr.additions + pr.deletions; + } + + agentStats.set(agentName, current); + } + }); + + const agents: AgentStats[] = Array.from(agentStats.entries()).map(([name, stats]) => ({ + name: name.replace('-ai', '').toUpperCase(), + prsRaised: stats.prsRaised, + prsMerged: stats.prsMerged, + mergeRate: stats.prsRaised > 0 ? (stats.prsMerged / stats.prsRaised) * 100 : 0, + avgTimeToMerge: stats.mergeTimes.length > 0 + ? stats.mergeTimes.reduce((a, b) => a + b, 0) / stats.mergeTimes.length + : 0, + totalLinesChanged: stats.totalLinesChanged, + avgPrSize: stats.prsRaised > 0 ? stats.totalLinesChanged / stats.prsRaised : 0, + })); + + const mostProductiveAgent = agents.length > 0 + ? agents.sort((a, b) => b.prsMerged - a.prsMerged)[0].name + : ""; + + // Calculate average review time (simplified - using time between creation and last update) + const avgReviewTime = totalPRs > 0 + ? repoPRs.reduce((acc, pr) => { + const created = new Date(pr.created_at); + const updated = new Date(pr.updated_at); + return acc + (updated.getTime() - created.getTime()) / (1000 * 60 * 60); // hours + }, 0) / totalPRs + : 0; + + return { + totalPRs, + mergedPRs, + openPRs, + draftPRs, + avgTimeToMerge, + totalContributors, + humanContributors: humanContributors.length, + agentContributors: agentContributors.length, + avgPrSize: Math.round(avgPrSize), + totalLinesChanged, + avgCommentsPerPR: Math.round(avgCommentsPerPR * 10) / 10, + mergeRate: Math.round(mergeRate * 10) / 10, + avgReviewTime: Math.round(avgReviewTime * 10) / 10, + mostActiveContributor, + mostProductiveAgent, + agents: agents.sort((a, b) => b.prsMerged - a.prsMerged), + }; + }, [pullRequests, selectedRepo]); + + if (!selectedRepo) { + return ( +
+
+ +

No Repository Selected

+

Select a repository to view high scores and statistics.

+
+
+ ); + } + + return ( +
+
+ {/* Header */} +
+
+ +

High Scores

+
+

+ Engineering insights for {selectedRepo.owner}/{selectedRepo.name} +

+
+ + {/* Key Metrics Grid */} +
+ + 70 ? "up" : stats.mergeRate < 50 ? "down" : "neutral"} + /> + 72 ? "down" : "neutral"} + /> + +
+ + {/* Secondary Metrics */} +
+ + + +
+ + {/* Status Breakdown */} +
+ + + +
+ + {/* Top Performers */} +
+

+ + Top Performers +

+
+
+

+ + Most Active Contributor +

+
+ {stats.mostActiveContributor || "N/A"} +
+

+ Highest number of PRs created +

+
+ +
+

+ + Most Productive Agent +

+
+ {stats.mostProductiveAgent || "N/A"} +
+

+ Highest number of merged PRs +

+
+
+
+ + {/* Agents Section */} + {stats.agents.length > 0 && ( +
+

+ + AI Agents Performance +

+
+ {stats.agents.map((agent) => ( + + ))} +
+
+ )} +
+
+ ); +} \ No newline at end of file