feat: add pagination support for GitHub repositories and branches (#122)

* Apply patch

* Apply patch

* Apply patch

* Apply patch

* Apply patch

* Apply patch

* Apply patch

* Apply patch

* Apply patch

* Apply patch

* Apply patch

* Apply patch

* Apply patch

* Apply patch

* Apply patch

* cleanup & fix build

* fix: always get default branch for first page

* cr

* cr

* cr

* cr

* cr

* cr
This commit is contained in:
Brace Sproul 2025-06-11 17:36:23 -07:00 • committed by GitHub
parent 30881a5d92
commit 69b15a9f77
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 367 additions and 152 deletions

View file

@ -37,6 +37,11 @@ async function handler(req: NextRequest) {
const targetUrl = new URL(`${GITHUB_API_URL}/${path}`); const targetUrl = new URL(`${GITHUB_API_URL}/${path}`);
// Forward query parameters from the original request
req.nextUrl.searchParams.forEach((value, key) => {
targetUrl.searchParams.append(key, value);
});
const headers = new Headers(); const headers = new Headers();
headers.set("Authorization", `Bearer ${token}`); headers.set("Authorization", `Bearer ${token}`);
headers.set("Accept", "application/vnd.github.v3+json"); headers.set("Accept", "application/vnd.github.v3+json");

View file

@ -8,7 +8,7 @@ import { GITHUB_INSTALLATION_ID_COOKIE } from "@/lib/auth";
/** /**
* Fetches repositories accessible to the GitHub App installation * Fetches repositories accessible to the GitHub App installation
* Requires a valid GitHub installation ID in the cookies * Requires a valid GitHub installation ID in the cookies. Supports pagination via 'page' query parameter.
*/ */
export async function GET(request: NextRequest) { export async function GET(request: NextRequest) {
try { try {
@ -27,6 +27,19 @@ export async function GET(request: NextRequest) {
); );
} }
// Get pagination parameters from query string
const { searchParams } = new URL(request.url);
const page = parseInt(searchParams.get("page") || "1", 10);
const perPage = 30; // Fixed at 30 repositories per page
// Validate page parameter
if (page < 1 || isNaN(page)) {
return NextResponse.json(
{ error: "Invalid page parameter. Must be a positive integer." },
{ status: 400 },
);
}
// Get GitHub App credentials from environment variables // Get GitHub App credentials from environment variables
const appId = process.env.GITHUB_APP_ID; const appId = process.env.GITHUB_APP_ID;
const privateKey = process.env.GITHUB_APP_PRIVATE_KEY?.replace( const privateKey = process.env.GITHUB_APP_PRIVATE_KEY?.replace(
@ -58,9 +71,17 @@ export async function GET(request: NextRequest) {
} }
// Fetch repositories accessible to this installation // Fetch repositories accessible to this installation
let repositories: Repository[]; let repositoryData: {
repositories: Repository[];
hasMore: boolean;
totalCount: number;
};
try { try {
repositories = await getInstallationRepositories(installationToken); repositoryData = await getInstallationRepositories(
installationToken,
page,
perPage,
);
} catch (error) { } catch (error) {
console.error("Failed to fetch repositories:", error); console.error("Failed to fetch repositories:", error);
return NextResponse.json( return NextResponse.json(
@ -70,7 +91,7 @@ export async function GET(request: NextRequest) {
} }
// Transform the response to include only the data we need // Transform the response to include only the data we need
const transformedRepos = repositories.map((repo) => ({ const transformedRepos = repositoryData.repositories.map((repo) => ({
id: repo.id, id: repo.id,
name: repo.name, name: repo.name,
full_name: repo.full_name, full_name: repo.full_name,
@ -81,7 +102,15 @@ export async function GET(request: NextRequest) {
permissions: repo.permissions, permissions: repo.permissions,
})); }));
return NextResponse.json({ repositories: transformedRepos }); return NextResponse.json({
repositories: transformedRepos,
pagination: {
page,
perPage,
hasMore: repositoryData.hasMore,
totalCount: repositoryData.totalCount,
},
});
} catch (error) { } catch (error) {
console.error("Error fetching GitHub repositories:", error); console.error("Error fetching GitHub repositories:", error);
return NextResponse.json( return NextResponse.json(

View file

@ -6,18 +6,21 @@ import { ThreadProvider } from "@/providers/Thread";
import { ArtifactProvider } from "@/components/thread/artifact"; import { ArtifactProvider } from "@/components/thread/artifact";
import { Toaster } from "@/components/ui/sonner"; import { Toaster } from "@/components/ui/sonner";
import React from "react"; import React from "react";
import { GitHubAppProvider } from "@/providers/GitHubApp";
export default function DemoPage(): React.ReactNode { export default function DemoPage(): React.ReactNode {
return ( return (
<React.Suspense fallback={<div>Loading (layout)...</div>}> <React.Suspense fallback={<div>Loading (layout)...</div>}>
<Toaster /> <Toaster />
<ThreadProvider> <GitHubAppProvider>
<StreamProvider> <ThreadProvider>
<ArtifactProvider> <StreamProvider>
<Thread /> <ArtifactProvider>
</ArtifactProvider> <Thread />
</StreamProvider> </ArtifactProvider>
</ThreadProvider> </StreamProvider>
</ThreadProvider>
</GitHubAppProvider>
</React.Suspense> </React.Suspense>
); );
} }

View file

@ -15,7 +15,7 @@ import { Button } from "@/components/ui/button";
import { Check, ChevronsUpDown } from "lucide-react"; import { Check, ChevronsUpDown } from "lucide-react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { useGitHubApp } from "@/hooks/useGitHubApp"; import { useGitHubAppProvider } from "@/providers/GitHubApp";
import { GitBranch, Shield } from "lucide-react"; import { GitBranch, Shield } from "lucide-react";
interface BranchSelectorProps { interface BranchSelectorProps {
@ -39,8 +39,11 @@ export function BranchSelector({
selectedBranch, selectedBranch,
setSelectedBranch, setSelectedBranch,
selectedRepository, selectedRepository,
branchesHasMore,
branchesLoadingMore,
loadMoreBranches,
defaultBranch, defaultBranch,
} = useGitHubApp(); } = useGitHubAppProvider();
// Auto-select default branch when repository changes and branches are loaded // Auto-select default branch when repository changes and branches are loaded
useEffect(() => { useEffect(() => {
@ -232,6 +235,21 @@ export function BranchSelector({
); );
})} })}
</CommandGroup> </CommandGroup>
{branchesHasMore && (
<CommandGroup>
<CommandItem
onSelect={() => {
loadMoreBranches();
}}
disabled={branchesLoadingMore}
className="justify-center"
>
{branchesLoadingMore
? "Loading more..."
: "Load more branches"}
</CommandItem>
</CommandGroup>
)}
</CommandList> </CommandList>
</Command> </Command>
</PopoverContent> </PopoverContent>

View file

@ -16,7 +16,7 @@ import { Check, ChevronsUpDown } from "lucide-react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { useState } from "react"; import { useState } from "react";
import type { TargetRepository } from "@open-swe/shared/open-swe/types"; import type { TargetRepository } from "@open-swe/shared/open-swe/types";
import { useGitHubApp } from "@/hooks/useGitHubApp"; import { useGitHubAppProvider } from "@/providers/GitHubApp";
import type { Repository } from "@/utils/github"; import type { Repository } from "@/utils/github";
import { GitHubSVG } from "@/components/icons/github"; import { GitHubSVG } from "@/components/icons/github";
@ -50,7 +50,10 @@ export function RepositorySelector({
isLoading, isLoading,
error, error,
isInstalled, isInstalled,
} = useGitHubApp(); repositoriesHasMore,
repositoriesLoadingMore,
loadMoreRepositories,
} = useGitHubAppProvider();
const handleSelect = (repositoryKey: string) => { const handleSelect = (repositoryKey: string) => {
const repository = repositories.find( const repository = repositories.find(
@ -210,6 +213,21 @@ export function RepositorySelector({
); );
})} })}
</CommandGroup> </CommandGroup>
{repositoriesHasMore && (
<CommandGroup>
<CommandItem
onSelect={() => {
loadMoreRepositories();
}}
disabled={repositoriesLoadingMore}
className="justify-center"
>
{repositoriesLoadingMore
? "Loading more..."
: "Load more repositories"}
</CommandItem>
</CommandGroup>
)}
</CommandList> </CommandList>
</Command> </Command>
</PopoverContent> </PopoverContent>

View file

@ -1,6 +1,6 @@
"use client"; "use client";
import { useGitHubApp } from "@/hooks/useGitHubApp"; import { useGitHubAppProvider } from "@/providers/GitHubApp";
import { InstallAppButton } from "./install-app-button"; import { InstallAppButton } from "./install-app-button";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { RefreshCw } from "lucide-react"; import { RefreshCw } from "lucide-react";
@ -11,7 +11,7 @@ interface RepositoryListProps {
export function RepositoryList({ className = "" }: RepositoryListProps) { export function RepositoryList({ className = "" }: RepositoryListProps) {
const { isInstalled, isLoading, error, repositories, refreshRepositories } = const { isInstalled, isLoading, error, repositories, refreshRepositories } =
useGitHubApp(); useGitHubAppProvider();
if (isLoading) { if (isLoading) {
return ( return (

View file

@ -14,14 +14,6 @@ export function useTaskPlan() {
} }
}, [values?.plan]); }, [values?.plan]);
const handleTaskChange = (taskId: string) => {
console.log(`Switched to task: ${taskId}`);
};
const handleRevisionChange = (taskId: string, revisionIndex: number) => {
console.log(`Task ${taskId} switched to revision ${revisionIndex}`);
};
const handleEditPlanItem = ( const handleEditPlanItem = (
taskId: string, taskId: string,
planItemIndex: number, planItemIndex: number,
@ -52,9 +44,6 @@ export function useTaskPlan() {
}), }),
}; };
}); });
console.log(
`Edited plan item ${planItemIndex} in task ${taskId}: ${newPlan}`,
);
}; };
const handleAddPlanItem = (taskId: string, plan: string) => { const handleAddPlanItem = (taskId: string, plan: string) => {
@ -89,7 +78,6 @@ export function useTaskPlan() {
}), }),
}; };
}); });
console.log(`Added new plan item to task ${taskId}: ${plan}`);
}; };
const handleDeletePlanItem = (taskId: string, planItemIndex: number) => { const handleDeletePlanItem = (taskId: string, planItemIndex: number) => {
@ -116,13 +104,10 @@ export function useTaskPlan() {
}), }),
}; };
}); });
console.log(`Deleted plan item ${planItemIndex} from task ${taskId}`);
}; };
return { return {
taskPlan, taskPlan,
handleTaskChange,
handleRevisionChange,
handleEditPlanItem, handleEditPlanItem,
handleAddPlanItem, handleAddPlanItem,
handleDeletePlanItem, handleDeletePlanItem,

View file

@ -39,7 +39,7 @@ import {
useArtifactContext, useArtifactContext,
} from "./artifact"; } from "./artifact";
import { GitHubOAuthButton } from "../github/github-oauth-button"; import { GitHubOAuthButton } from "../github/github-oauth-button";
import { useGitHubApp } from "@/hooks/useGitHubApp"; import { useGitHubAppProvider } from "@/providers/GitHubApp";
import TaskList from "../task-list"; import TaskList from "../task-list";
import { ConfigurationSidebar } from "../configuration-sidebar"; import { ConfigurationSidebar } from "../configuration-sidebar";
import { DEFAULT_CONFIG_KEY, useConfigStore } from "@/hooks/use-config-store"; import { DEFAULT_CONFIG_KEY, useConfigStore } from "@/hooks/use-config-store";
@ -100,12 +100,10 @@ export function Thread() {
const { push } = useRouter(); const { push } = useRouter();
const [artifactContext, setArtifactContext] = useArtifactContext(); const [artifactContext, setArtifactContext] = useArtifactContext();
const [artifactOpen, closeArtifact] = useArtifactOpen(); const [artifactOpen, closeArtifact] = useArtifactOpen();
const { selectedRepository } = useGitHubApp(); const { selectedRepository } = useGitHubAppProvider();
const { getConfig } = useConfigStore(); const { getConfig } = useConfigStore();
const { const {
taskPlan, taskPlan,
handleTaskChange,
handleRevisionChange,
handleEditPlanItem, handleEditPlanItem,
handleAddPlanItem, handleAddPlanItem,
handleDeletePlanItem, handleDeletePlanItem,
@ -437,8 +435,8 @@ export function Thread() {
{taskPlan && ( {taskPlan && (
<TaskPlanView <TaskPlanView
taskPlan={taskPlan} taskPlan={taskPlan}
onTaskChange={handleTaskChange} onTaskChange={() => {}}
onRevisionChange={handleRevisionChange} onRevisionChange={() => {}}
onEditPlanItem={handleEditPlanItem} onEditPlanItem={handleEditPlanItem}
onAddPlanItem={handleAddPlanItem} onAddPlanItem={handleAddPlanItem}
onDeletePlanItem={handleDeletePlanItem} onDeletePlanItem={handleDeletePlanItem}

View file

@ -1,33 +1,68 @@
import { useState, useEffect, useRef, useCallback } from "react"; import { useState, useEffect, useRef, useCallback, useMemo } from "react";
import { useQueryState } from "nuqs"; import { useQueryState } from "nuqs";
import { Repository, getRepositoryBranches, Branch } from "@/utils/github"; import { Repository, getRepositoryBranches, Branch } from "@/utils/github";
import type { TargetRepository } from "@open-swe/shared/open-swe/types"; import type { TargetRepository } from "@open-swe/shared/open-swe/types";
interface UseGitHubAppReturn { interface UseGitHubAppReturn {
// Installation and general state
isInstalled: boolean | null; isInstalled: boolean | null;
isLoading: boolean; isLoading: boolean;
error: string | null; error: string | null;
// Repository state and pagination
repositories: Repository[]; repositories: Repository[];
repositoriesPage: number;
repositoriesHasMore: boolean;
repositoriesLoadingMore: boolean;
refreshRepositories: () => Promise<void>; refreshRepositories: () => Promise<void>;
loadMoreRepositories: () => Promise<void>;
// Repository selection
selectedRepository: TargetRepository | null; selectedRepository: TargetRepository | null;
setSelectedRepository: (repo: TargetRepository | null) => void; setSelectedRepository: (repo: TargetRepository | null) => void;
// Branch state and pagination
branches: Branch[]; branches: Branch[];
branchesPage: number;
branchesHasMore: boolean;
branchesLoading: boolean; branchesLoading: boolean;
branchesLoadingMore: boolean;
branchesError: string | null; branchesError: string | null;
loadMoreBranches: () => Promise<void>;
fetchBranches: () => Promise<void>;
setBranchesPage: (page: number) => void;
setBranches: (branches: Branch[]) => void;
// Branch selection
selectedBranch: string | null; selectedBranch: string | null;
setSelectedBranch: (branch: string | null) => void; setSelectedBranch: (branch: string | null) => void;
refreshBranches: () => Promise<void>; refreshBranches: () => Promise<void>;
// Repository metadata
defaultBranch: string | null; defaultBranch: string | null;
} }
export function useGitHubApp(): UseGitHubAppReturn { export function useGitHubApp(): UseGitHubAppReturn {
// Installation and general state
const [isInstalled, setIsInstalled] = useState<boolean | null>(null); const [isInstalled, setIsInstalled] = useState<boolean | null>(null);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
// Repository state and pagination
const [repositories, setRepositories] = useState<Repository[]>([]); const [repositories, setRepositories] = useState<Repository[]>([]);
const [repositoriesPage, setRepositoriesPage] = useState(1);
const [repositoriesHasMore, setRepositoriesHasMore] = useState(false);
const [repositoriesLoadingMore, setRepositoriesLoadingMore] = useState(false);
// Branch state and pagination
const [branches, setBranches] = useState<Branch[]>([]); const [branches, setBranches] = useState<Branch[]>([]);
const [branchesPage, setBranchesPage] = useState(1);
const [branchesHasMore, setBranchesHasMore] = useState(false);
const [branchesLoading, setBranchesLoading] = useState(false); const [branchesLoading, setBranchesLoading] = useState(false);
const [branchesLoadingMore, setBranchesLoadingMore] = useState(false);
const [branchesError, setBranchesError] = useState<string | null>(null); const [branchesError, setBranchesError] = useState<string | null>(null);
// URL state management
const [selectedRepositoryParam, setSelectedRepositoryParam] = const [selectedRepositoryParam, setSelectedRepositoryParam] =
useQueryState("repo"); useQueryState("repo");
const [selectedBranchParam, setSelectedBranchParam] = useQueryState("branch"); const [selectedBranchParam, setSelectedBranchParam] = useQueryState("branch");
@ -35,24 +70,34 @@ export function useGitHubApp(): UseGitHubAppReturn {
// Track if auto-selection has been attempted to prevent re-triggering // Track if auto-selection has been attempted to prevent re-triggering
const hasAutoSelectedRef = useRef(false); const hasAutoSelectedRef = useRef(false);
const selectedRepository = selectedRepositoryParam const selectedRepository = useMemo(() => {
? (() => { if (!selectedRepositoryParam) return null;
try { try {
// Parse "owner/repo" format instead of JSON // Parse "owner/repo" format instead of JSON
const parts = selectedRepositoryParam.split("/"); const parts = selectedRepositoryParam.split("/");
if (parts.length === 2) { if (parts.length === 2) {
return { return {
owner: parts[0], owner: parts[0],
repo: parts[1], repo: parts[1],
branch: selectedBranchParam || undefined, branch: selectedBranchParam || undefined,
} as TargetRepository; } as TargetRepository;
} }
return null; return null;
} catch { } catch {
return null; return null;
} }
})() }, [selectedRepositoryParam, selectedBranchParam]);
: null;
useEffect(() => {
if (selectedRepository && !branchesLoading) {
fetchBranches();
// Reset branch pagination when repository changes
setBranchesPage(1);
} else if (!selectedRepository) {
setBranches([]);
setSelectedBranchParam(null);
}
}, [selectedRepository]);
const selectedBranch = selectedBranchParam; const selectedBranch = selectedBranchParam;
@ -62,6 +107,8 @@ export function useGitHubApp(): UseGitHubAppReturn {
if (!repo) { if (!repo) {
setSelectedBranchParam(null); setSelectedBranchParam(null);
setBranches([]); setBranches([]);
setBranchesPage(1);
setBranchesHasMore(false);
} }
}, },
[setSelectedRepositoryParam, setSelectedBranchParam], [setSelectedRepositoryParam, setSelectedBranchParam],
@ -71,16 +118,29 @@ export function useGitHubApp(): UseGitHubAppReturn {
setSelectedBranchParam(branch); setSelectedBranchParam(branch);
}; };
const checkInstallation = async () => { const checkInstallation = async (
setIsLoading(true); page: number = 1,
append: boolean = false,
) => {
if (!append) setIsLoading(true);
if (append) setRepositoriesLoadingMore(true);
setError(null); setError(null);
try { try {
const response = await fetch("/api/github/repositories"); const response = await fetch(`/api/github/repositories?page=${page}`);
if (response.ok) { if (response.ok) {
const data = await response.json(); const data = await response.json();
setRepositories(data.repositories || []); const newRepositories = data.repositories || [];
if (append) {
setRepositories((prev) => [...prev, ...newRepositories]);
} else {
setRepositories(newRepositories);
}
setRepositoriesPage(data.pagination?.page || page);
setRepositoriesHasMore(data.pagination?.hasMore || false);
setIsInstalled(true); setIsInstalled(true);
} else { } else {
const errorData = await response.json(); const errorData = await response.json();
@ -95,56 +155,78 @@ export function useGitHubApp(): UseGitHubAppReturn {
setError("Failed to check GitHub App installation status"); setError("Failed to check GitHub App installation status");
setIsInstalled(false); setIsInstalled(false);
} finally { } finally {
setIsLoading(false); if (!append) setIsLoading(false);
if (append) setRepositoriesLoadingMore(false);
} }
}; };
const fetchBranches = useCallback(async () => { const fetchBranches = useCallback(
if (!selectedRepository) { async (page: number = 1, append: boolean = false) => {
setBranches([]); if (!selectedRepository) {
return; setBranches([]);
} setBranchesPage(1);
setBranchesHasMore(false);
return;
}
setBranchesLoading(true); if (!append) setBranchesLoading(true);
setBranchesError(null); if (append) setBranchesLoadingMore(true);
setBranchesError(null);
try { try {
const branchData = await getRepositoryBranches( const branchData = await getRepositoryBranches(
selectedRepository.owner, selectedRepository.owner,
selectedRepository.repo, selectedRepository.repo,
); page,
setBranches(branchData || []); );
} catch (err) {
const errorMessage = if (append) {
err instanceof Error ? err.message : "Failed to fetch branches"; setBranches((prev) => {
console.error( // Avoid adding duplicates
`Error fetching branches for ${selectedRepository.owner}/${selectedRepository.repo}:`, const newBranches = branchData.branches.filter(
err, (branch) => !prev.some((b) => b.name === branch.name),
); );
setBranchesError(errorMessage); return [...prev, ...newBranches];
} finally { });
setBranchesLoading(false); } else {
setBranches(branchData.branches);
}
setBranchesPage(page);
setBranchesHasMore(branchData.hasMore);
} catch (err) {
const errorMessage =
err instanceof Error ? err.message : "Failed to fetch branches";
console.error(
`Error fetching branches for ${selectedRepository.owner}/${selectedRepository.repo}:`,
err,
);
setBranchesError(errorMessage);
} finally {
if (!append) setBranchesLoading(false);
if (append) setBranchesLoadingMore(false);
}
},
[selectedRepository?.owner, selectedRepository?.repo],
);
// Load more functions
const loadMoreRepositories = useCallback(async () => {
if (repositoriesHasMore && !repositoriesLoadingMore) {
await checkInstallation(repositoriesPage + 1, true);
} }
}, [selectedRepository?.owner, selectedRepository?.repo]); }, [repositoriesHasMore, repositoriesLoadingMore, repositoriesPage]);
const loadMoreBranches = useCallback(async () => {
if (branchesHasMore && !branchesLoadingMore) {
await fetchBranches(branchesPage + 1, true);
}
}, [branchesHasMore, branchesLoadingMore, branchesPage, fetchBranches]);
useEffect(() => { useEffect(() => {
checkInstallation(); checkInstallation();
}, []); }, []);
useEffect(() => {
if (selectedRepository) {
fetchBranches();
} else {
setBranches([]);
setSelectedBranchParam(null);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
selectedRepository?.owner,
selectedRepository?.repo,
setSelectedBranchParam,
]);
// Auto-select first repository on initial page load // Auto-select first repository on initial page load
useEffect(() => { useEffect(() => {
if ( if (
@ -173,10 +255,16 @@ export function useGitHubApp(): UseGitHubAppReturn {
]); ]);
const refreshRepositories = async () => { const refreshRepositories = async () => {
// Reset pagination state on refresh
setRepositoriesPage(1);
setRepositoriesHasMore(false);
await checkInstallation(); await checkInstallation();
}; };
const refreshBranches = async () => { const refreshBranches = async () => {
// Reset pagination state on refresh
setBranchesPage(1);
setBranchesHasMore(false);
await fetchBranches(); await fetchBranches();
}; };
@ -190,19 +278,41 @@ export function useGitHubApp(): UseGitHubAppReturn {
: null; : null;
return { return {
// Installation and general state
isInstalled, isInstalled,
isLoading, isLoading,
error, error,
// Repository state and pagination
repositories, repositories,
repositoriesPage,
repositoriesHasMore,
repositoriesLoadingMore,
refreshRepositories, refreshRepositories,
loadMoreRepositories,
// Repository selection
selectedRepository, selectedRepository,
setSelectedRepository, setSelectedRepository,
// Branch state and pagination
branches, branches,
branchesPage,
branchesHasMore,
branchesLoading, branchesLoading,
branchesLoadingMore,
branchesError, branchesError,
loadMoreBranches,
fetchBranches,
// Branch selection
selectedBranch, selectedBranch,
setSelectedBranch, setSelectedBranch,
refreshBranches, refreshBranches,
setBranchesPage,
setBranches,
// Repository metadata
defaultBranch, defaultBranch,
}; };
} }

View file

@ -0,0 +1,27 @@
import { useGitHubApp } from "@/hooks/useGitHubApp";
import { createContext, useContext, ReactNode } from "react";
type GitHubAppContextType = ReturnType<typeof useGitHubApp>;
const GitHubAppContext = createContext<GitHubAppContextType | undefined>(
undefined,
);
export function GitHubAppProvider({ children }: { children: ReactNode }) {
const value = useGitHubApp();
return (
<GitHubAppContext.Provider value={value}>
{children}
</GitHubAppContext.Provider>
);
}
export function useGitHubAppProvider() {
const context = useContext(GitHubAppContext);
if (context === undefined) {
throw new Error(
"useGitHubAppProvider must be used within a GitHubAppProvider",
);
}
return context;
}

View file

@ -1,4 +1,3 @@
import { GITHUB_TOKEN_COOKIE } from "@open-swe/shared/constants";
import * as jwt from "jsonwebtoken"; import * as jwt from "jsonwebtoken";
function getBaseApiUrl(): string { function getBaseApiUrl(): string {
@ -41,7 +40,7 @@ export async function getInstallationToken(
headers: { headers: {
Authorization: `Bearer ${jwtToken}`, Authorization: `Bearer ${jwtToken}`,
Accept: "application/vnd.github.v3+json", Accept: "application/vnd.github.v3+json",
"User-Agent": "YourAppName", "User-Agent": "OpenSWE-Agent",
}, },
}, },
); );
@ -62,17 +61,24 @@ export async function getInstallationToken(
*/ */
export async function getInstallationRepositories( export async function getInstallationRepositories(
installationToken: string, installationToken: string,
): Promise<Repository[]> { page: number = 1,
const response = await fetch( perPage: number = 30,
"https://api.github.com/installation/repositories", ): Promise<{
{ repositories: Repository[];
headers: { hasMore: boolean;
Authorization: `Bearer ${installationToken}`, totalCount: number;
Accept: "application/vnd.github.v3+json", }> {
"User-Agent": "YourAppName", const url = new URL("https://api.github.com/installation/repositories");
}, url.searchParams.set("page", page.toString());
url.searchParams.set("per_page", perPage.toString());
const response = await fetch(url.toString(), {
headers: {
Authorization: `Bearer ${installationToken}`,
Accept: "application/vnd.github.v3+json",
"User-Agent": "YourAppName",
}, },
); });
if (!response.ok) { if (!response.ok) {
const errorData = await response.json(); const errorData = await response.json();
@ -82,7 +88,12 @@ export async function getInstallationRepositories(
} }
const data = await response.json(); const data = await response.json();
return data.repositories;
return {
repositories: data.repositories,
hasMore: data.repositories.length === perPage,
totalCount: data.total_count,
};
} }
/** /**
@ -91,11 +102,9 @@ export async function getInstallationRepositories(
export async function getRepositoryBranches( export async function getRepositoryBranches(
owner: string, owner: string,
repo: string, repo: string,
): Promise<Branch[]> { page: number = 1,
const allBranches: Branch[] = []; perPage: number = 30,
let page = 1; ): Promise<{ branches: Branch[]; hasMore: boolean; totalCount?: number }> {
const perPage = 100; // Maximum allowed by GitHub API
// First, get repository info to ensure we have the default branch // First, get repository info to ensure we have the default branch
const repoResponse = await fetch( const repoResponse = await fetch(
@ -114,52 +123,65 @@ export async function getRepositoryBranches(
defaultBranch = repoData.default_branch; defaultBranch = repoData.default_branch;
} }
// Fetch all branches with pagination // Fetch first 30 branches only
while (true) { const response = await fetch(
const response = await fetch( `${getBaseApiUrl()}github/proxy/repos/${owner}/${repo}/branches?per_page=${perPage}&page=${page}`,
`${getBaseApiUrl()}github/proxy/repos/${owner}/${repo}/branches?per_page=${perPage}&page=${page}`, {
{ headers: {
headers: { Accept: "application/vnd.github.v3+json",
Accept: "application/vnd.github.v3+json", "User-Agent": "OpenSWE-Agent",
"User-Agent": "OpenSWE-Agent",
},
}, },
); },
);
if (!response.ok) { if (!response.ok) {
const errorData = await response.json(); const errorData = await response.json();
throw new Error(`Failed to fetch branches: ${JSON.stringify(errorData)}`); throw new Error(`Failed to fetch branches: ${JSON.stringify(errorData)}`);
} }
const data = await response.json(); const data = await response.json();
const branches: Branch[] = Array.isArray(data) ? data : [];
if (!Array.isArray(data) || data.length === 0) { if (branches.length === 0) {
break; return { branches: [], hasMore: false };
}
allBranches.push(...data);
// If we got less than the requested amount, we've reached the end
if (data.length < perPage) {
break;
}
page++;
} }
// Ensure default branch is at the beginning if it exists // Ensure default branch is at the beginning if it exists
if (defaultBranch) { if (defaultBranch && page === 1) {
const defaultBranchIndex = allBranches.findIndex( const defaultBranchIndex = branches.findIndex(
(branch) => branch.name === defaultBranch, (branch) => branch.name === defaultBranch,
); );
if (defaultBranchIndex > 0) { if (defaultBranchIndex > 0) {
const defaultBranchData = allBranches[defaultBranchIndex]; const defaultBranchData = branches[defaultBranchIndex];
allBranches.splice(defaultBranchIndex, 1); branches.splice(defaultBranchIndex, 1);
allBranches.unshift(defaultBranchData); branches.unshift(defaultBranchData);
} else {
// Need to fetch default branch
const defaultBranchResponse = await fetch(
`${getBaseApiUrl()}github/proxy/repos/${owner}/${repo}/branches/${defaultBranch}`,
{
headers: {
Accept: "application/vnd.github.v3+json",
"User-Agent": "OpenSWE-Agent",
},
},
);
if (!defaultBranchResponse.ok) {
const errorData = await defaultBranchResponse.json();
throw new Error(
`Failed to fetch default branch: ${JSON.stringify(errorData)}`,
);
}
const defaultBranchData = await defaultBranchResponse.json();
branches.unshift(defaultBranchData);
} }
} }
return allBranches; return {
branches,
hasMore: branches.length >= perPage,
totalCount: branches.length,
};
} }
/** /**