diff --git a/apps/web/src/app/api/github/proxy/[..._path]/route.ts b/apps/web/src/app/api/github/proxy/[..._path]/route.ts
index 6dbf8e9a..077901f9 100644
--- a/apps/web/src/app/api/github/proxy/[..._path]/route.ts
+++ b/apps/web/src/app/api/github/proxy/[..._path]/route.ts
@@ -37,6 +37,11 @@ async function handler(req: NextRequest) {
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();
headers.set("Authorization", `Bearer ${token}`);
headers.set("Accept", "application/vnd.github.v3+json");
diff --git a/apps/web/src/app/api/github/repositories/route.ts b/apps/web/src/app/api/github/repositories/route.ts
index d4a4f063..54c0816a 100644
--- a/apps/web/src/app/api/github/repositories/route.ts
+++ b/apps/web/src/app/api/github/repositories/route.ts
@@ -8,7 +8,7 @@ import { GITHUB_INSTALLATION_ID_COOKIE } from "@/lib/auth";
/**
* 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) {
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
const appId = process.env.GITHUB_APP_ID;
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
- let repositories: Repository[];
+ let repositoryData: {
+ repositories: Repository[];
+ hasMore: boolean;
+ totalCount: number;
+ };
try {
- repositories = await getInstallationRepositories(installationToken);
+ repositoryData = await getInstallationRepositories(
+ installationToken,
+ page,
+ perPage,
+ );
} catch (error) {
console.error("Failed to fetch repositories:", error);
return NextResponse.json(
@@ -70,7 +91,7 @@ export async function GET(request: NextRequest) {
}
// Transform the response to include only the data we need
- const transformedRepos = repositories.map((repo) => ({
+ const transformedRepos = repositoryData.repositories.map((repo) => ({
id: repo.id,
name: repo.name,
full_name: repo.full_name,
@@ -81,7 +102,15 @@ export async function GET(request: NextRequest) {
permissions: repo.permissions,
}));
- return NextResponse.json({ repositories: transformedRepos });
+ return NextResponse.json({
+ repositories: transformedRepos,
+ pagination: {
+ page,
+ perPage,
+ hasMore: repositoryData.hasMore,
+ totalCount: repositoryData.totalCount,
+ },
+ });
} catch (error) {
console.error("Error fetching GitHub repositories:", error);
return NextResponse.json(
diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx
index 19675ded..c94f6705 100644
--- a/apps/web/src/app/page.tsx
+++ b/apps/web/src/app/page.tsx
@@ -6,18 +6,21 @@ import { ThreadProvider } from "@/providers/Thread";
import { ArtifactProvider } from "@/components/thread/artifact";
import { Toaster } from "@/components/ui/sonner";
import React from "react";
+import { GitHubAppProvider } from "@/providers/GitHubApp";
export default function DemoPage(): React.ReactNode {
return (
Loading (layout)...}>
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
);
}
diff --git a/apps/web/src/components/github/repo-branch-selectors/branch-selector.tsx b/apps/web/src/components/github/repo-branch-selectors/branch-selector.tsx
index 3c5c1fb0..8e3b1464 100644
--- a/apps/web/src/components/github/repo-branch-selectors/branch-selector.tsx
+++ b/apps/web/src/components/github/repo-branch-selectors/branch-selector.tsx
@@ -15,7 +15,7 @@ import { Button } from "@/components/ui/button";
import { Check, ChevronsUpDown } from "lucide-react";
import { cn } from "@/lib/utils";
import { useState, useEffect } from "react";
-import { useGitHubApp } from "@/hooks/useGitHubApp";
+import { useGitHubAppProvider } from "@/providers/GitHubApp";
import { GitBranch, Shield } from "lucide-react";
interface BranchSelectorProps {
@@ -39,8 +39,11 @@ export function BranchSelector({
selectedBranch,
setSelectedBranch,
selectedRepository,
+ branchesHasMore,
+ branchesLoadingMore,
+ loadMoreBranches,
defaultBranch,
- } = useGitHubApp();
+ } = useGitHubAppProvider();
// Auto-select default branch when repository changes and branches are loaded
useEffect(() => {
@@ -232,6 +235,21 @@ export function BranchSelector({
);
})}
+ {branchesHasMore && (
+
+ {
+ loadMoreBranches();
+ }}
+ disabled={branchesLoadingMore}
+ className="justify-center"
+ >
+ {branchesLoadingMore
+ ? "Loading more..."
+ : "Load more branches"}
+
+
+ )}
diff --git a/apps/web/src/components/github/repo-branch-selectors/repository-selector.tsx b/apps/web/src/components/github/repo-branch-selectors/repository-selector.tsx
index 0f4a5071..b3262287 100644
--- a/apps/web/src/components/github/repo-branch-selectors/repository-selector.tsx
+++ b/apps/web/src/components/github/repo-branch-selectors/repository-selector.tsx
@@ -16,7 +16,7 @@ import { Check, ChevronsUpDown } from "lucide-react";
import { cn } from "@/lib/utils";
import { useState } from "react";
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 { GitHubSVG } from "@/components/icons/github";
@@ -50,7 +50,10 @@ export function RepositorySelector({
isLoading,
error,
isInstalled,
- } = useGitHubApp();
+ repositoriesHasMore,
+ repositoriesLoadingMore,
+ loadMoreRepositories,
+ } = useGitHubAppProvider();
const handleSelect = (repositoryKey: string) => {
const repository = repositories.find(
@@ -210,6 +213,21 @@ export function RepositorySelector({
);
})}
+ {repositoriesHasMore && (
+
+ {
+ loadMoreRepositories();
+ }}
+ disabled={repositoriesLoadingMore}
+ className="justify-center"
+ >
+ {repositoriesLoadingMore
+ ? "Loading more..."
+ : "Load more repositories"}
+
+
+ )}
diff --git a/apps/web/src/components/github/repository-list.tsx b/apps/web/src/components/github/repository-list.tsx
index e80ac7b6..9bc0110f 100644
--- a/apps/web/src/components/github/repository-list.tsx
+++ b/apps/web/src/components/github/repository-list.tsx
@@ -1,6 +1,6 @@
"use client";
-import { useGitHubApp } from "@/hooks/useGitHubApp";
+import { useGitHubAppProvider } from "@/providers/GitHubApp";
import { InstallAppButton } from "./install-app-button";
import { Button } from "@/components/ui/button";
import { RefreshCw } from "lucide-react";
@@ -11,7 +11,7 @@ interface RepositoryListProps {
export function RepositoryList({ className = "" }: RepositoryListProps) {
const { isInstalled, isLoading, error, repositories, refreshRepositories } =
- useGitHubApp();
+ useGitHubAppProvider();
if (isLoading) {
return (
diff --git a/apps/web/src/components/tasks/use-task-plan.tsx b/apps/web/src/components/tasks/use-task-plan.tsx
index f2e6e624..e684d613 100644
--- a/apps/web/src/components/tasks/use-task-plan.tsx
+++ b/apps/web/src/components/tasks/use-task-plan.tsx
@@ -14,14 +14,6 @@ export function useTaskPlan() {
}
}, [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 = (
taskId: string,
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) => {
@@ -89,7 +78,6 @@ export function useTaskPlan() {
}),
};
});
- console.log(`Added new plan item to task ${taskId}: ${plan}`);
};
const handleDeletePlanItem = (taskId: string, planItemIndex: number) => {
@@ -116,13 +104,10 @@ export function useTaskPlan() {
}),
};
});
- console.log(`Deleted plan item ${planItemIndex} from task ${taskId}`);
};
return {
taskPlan,
- handleTaskChange,
- handleRevisionChange,
handleEditPlanItem,
handleAddPlanItem,
handleDeletePlanItem,
diff --git a/apps/web/src/components/thread/index.tsx b/apps/web/src/components/thread/index.tsx
index da523710..f9acf742 100644
--- a/apps/web/src/components/thread/index.tsx
+++ b/apps/web/src/components/thread/index.tsx
@@ -39,7 +39,7 @@ import {
useArtifactContext,
} from "./artifact";
import { GitHubOAuthButton } from "../github/github-oauth-button";
-import { useGitHubApp } from "@/hooks/useGitHubApp";
+import { useGitHubAppProvider } from "@/providers/GitHubApp";
import TaskList from "../task-list";
import { ConfigurationSidebar } from "../configuration-sidebar";
import { DEFAULT_CONFIG_KEY, useConfigStore } from "@/hooks/use-config-store";
@@ -100,12 +100,10 @@ export function Thread() {
const { push } = useRouter();
const [artifactContext, setArtifactContext] = useArtifactContext();
const [artifactOpen, closeArtifact] = useArtifactOpen();
- const { selectedRepository } = useGitHubApp();
+ const { selectedRepository } = useGitHubAppProvider();
const { getConfig } = useConfigStore();
const {
taskPlan,
- handleTaskChange,
- handleRevisionChange,
handleEditPlanItem,
handleAddPlanItem,
handleDeletePlanItem,
@@ -437,8 +435,8 @@ export function Thread() {
{taskPlan && (
{}}
+ onRevisionChange={() => {}}
onEditPlanItem={handleEditPlanItem}
onAddPlanItem={handleAddPlanItem}
onDeletePlanItem={handleDeletePlanItem}
diff --git a/apps/web/src/hooks/useGitHubApp.ts b/apps/web/src/hooks/useGitHubApp.ts
index a3040846..b5720a4e 100644
--- a/apps/web/src/hooks/useGitHubApp.ts
+++ b/apps/web/src/hooks/useGitHubApp.ts
@@ -1,33 +1,68 @@
-import { useState, useEffect, useRef, useCallback } from "react";
+import { useState, useEffect, useRef, useCallback, useMemo } from "react";
import { useQueryState } from "nuqs";
import { Repository, getRepositoryBranches, Branch } from "@/utils/github";
import type { TargetRepository } from "@open-swe/shared/open-swe/types";
interface UseGitHubAppReturn {
+ // Installation and general state
isInstalled: boolean | null;
isLoading: boolean;
error: string | null;
+
+ // Repository state and pagination
repositories: Repository[];
+ repositoriesPage: number;
+ repositoriesHasMore: boolean;
+ repositoriesLoadingMore: boolean;
refreshRepositories: () => Promise;
+ loadMoreRepositories: () => Promise;
+
+ // Repository selection
selectedRepository: TargetRepository | null;
setSelectedRepository: (repo: TargetRepository | null) => void;
+
+ // Branch state and pagination
branches: Branch[];
+ branchesPage: number;
+ branchesHasMore: boolean;
branchesLoading: boolean;
+ branchesLoadingMore: boolean;
branchesError: string | null;
+ loadMoreBranches: () => Promise;
+ fetchBranches: () => Promise;
+ setBranchesPage: (page: number) => void;
+ setBranches: (branches: Branch[]) => void;
+
+ // Branch selection
selectedBranch: string | null;
setSelectedBranch: (branch: string | null) => void;
refreshBranches: () => Promise;
+
+ // Repository metadata
defaultBranch: string | null;
}
export function useGitHubApp(): UseGitHubAppReturn {
+ // Installation and general state
const [isInstalled, setIsInstalled] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
+
+ // Repository state and pagination
const [repositories, setRepositories] = useState([]);
+ const [repositoriesPage, setRepositoriesPage] = useState(1);
+ const [repositoriesHasMore, setRepositoriesHasMore] = useState(false);
+ const [repositoriesLoadingMore, setRepositoriesLoadingMore] = useState(false);
+
+ // Branch state and pagination
const [branches, setBranches] = useState([]);
+ const [branchesPage, setBranchesPage] = useState(1);
+ const [branchesHasMore, setBranchesHasMore] = useState(false);
const [branchesLoading, setBranchesLoading] = useState(false);
+ const [branchesLoadingMore, setBranchesLoadingMore] = useState(false);
const [branchesError, setBranchesError] = useState(null);
+
+ // URL state management
const [selectedRepositoryParam, setSelectedRepositoryParam] =
useQueryState("repo");
const [selectedBranchParam, setSelectedBranchParam] = useQueryState("branch");
@@ -35,24 +70,34 @@ export function useGitHubApp(): UseGitHubAppReturn {
// Track if auto-selection has been attempted to prevent re-triggering
const hasAutoSelectedRef = useRef(false);
- const selectedRepository = selectedRepositoryParam
- ? (() => {
- try {
- // Parse "owner/repo" format instead of JSON
- const parts = selectedRepositoryParam.split("/");
- if (parts.length === 2) {
- return {
- owner: parts[0],
- repo: parts[1],
- branch: selectedBranchParam || undefined,
- } as TargetRepository;
- }
- return null;
- } catch {
- return null;
- }
- })()
- : null;
+ const selectedRepository = useMemo(() => {
+ if (!selectedRepositoryParam) return null;
+ try {
+ // Parse "owner/repo" format instead of JSON
+ const parts = selectedRepositoryParam.split("/");
+ if (parts.length === 2) {
+ return {
+ owner: parts[0],
+ repo: parts[1],
+ branch: selectedBranchParam || undefined,
+ } as TargetRepository;
+ }
+ return null;
+ } catch {
+ return null;
+ }
+ }, [selectedRepositoryParam, selectedBranchParam]);
+
+ useEffect(() => {
+ if (selectedRepository && !branchesLoading) {
+ fetchBranches();
+ // Reset branch pagination when repository changes
+ setBranchesPage(1);
+ } else if (!selectedRepository) {
+ setBranches([]);
+ setSelectedBranchParam(null);
+ }
+ }, [selectedRepository]);
const selectedBranch = selectedBranchParam;
@@ -62,6 +107,8 @@ export function useGitHubApp(): UseGitHubAppReturn {
if (!repo) {
setSelectedBranchParam(null);
setBranches([]);
+ setBranchesPage(1);
+ setBranchesHasMore(false);
}
},
[setSelectedRepositoryParam, setSelectedBranchParam],
@@ -71,16 +118,29 @@ export function useGitHubApp(): UseGitHubAppReturn {
setSelectedBranchParam(branch);
};
- const checkInstallation = async () => {
- setIsLoading(true);
+ const checkInstallation = async (
+ page: number = 1,
+ append: boolean = false,
+ ) => {
+ if (!append) setIsLoading(true);
+ if (append) setRepositoriesLoadingMore(true);
setError(null);
try {
- const response = await fetch("/api/github/repositories");
+ const response = await fetch(`/api/github/repositories?page=${page}`);
if (response.ok) {
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);
} else {
const errorData = await response.json();
@@ -95,56 +155,78 @@ export function useGitHubApp(): UseGitHubAppReturn {
setError("Failed to check GitHub App installation status");
setIsInstalled(false);
} finally {
- setIsLoading(false);
+ if (!append) setIsLoading(false);
+ if (append) setRepositoriesLoadingMore(false);
}
};
- const fetchBranches = useCallback(async () => {
- if (!selectedRepository) {
- setBranches([]);
- return;
- }
+ const fetchBranches = useCallback(
+ async (page: number = 1, append: boolean = false) => {
+ if (!selectedRepository) {
+ setBranches([]);
+ setBranchesPage(1);
+ setBranchesHasMore(false);
+ return;
+ }
- setBranchesLoading(true);
- setBranchesError(null);
+ if (!append) setBranchesLoading(true);
+ if (append) setBranchesLoadingMore(true);
+ setBranchesError(null);
- try {
- const branchData = await getRepositoryBranches(
- selectedRepository.owner,
- selectedRepository.repo,
- );
- setBranches(branchData || []);
- } 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 {
- setBranchesLoading(false);
+ try {
+ const branchData = await getRepositoryBranches(
+ selectedRepository.owner,
+ selectedRepository.repo,
+ page,
+ );
+
+ if (append) {
+ setBranches((prev) => {
+ // Avoid adding duplicates
+ const newBranches = branchData.branches.filter(
+ (branch) => !prev.some((b) => b.name === branch.name),
+ );
+ return [...prev, ...newBranches];
+ });
+ } 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(() => {
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
useEffect(() => {
if (
@@ -173,10 +255,16 @@ export function useGitHubApp(): UseGitHubAppReturn {
]);
const refreshRepositories = async () => {
+ // Reset pagination state on refresh
+ setRepositoriesPage(1);
+ setRepositoriesHasMore(false);
await checkInstallation();
};
const refreshBranches = async () => {
+ // Reset pagination state on refresh
+ setBranchesPage(1);
+ setBranchesHasMore(false);
await fetchBranches();
};
@@ -190,19 +278,41 @@ export function useGitHubApp(): UseGitHubAppReturn {
: null;
return {
+ // Installation and general state
isInstalled,
isLoading,
error,
+
+ // Repository state and pagination
repositories,
+ repositoriesPage,
+ repositoriesHasMore,
+ repositoriesLoadingMore,
refreshRepositories,
+ loadMoreRepositories,
+
+ // Repository selection
selectedRepository,
setSelectedRepository,
+
+ // Branch state and pagination
branches,
+ branchesPage,
+ branchesHasMore,
branchesLoading,
+ branchesLoadingMore,
branchesError,
+ loadMoreBranches,
+ fetchBranches,
+
+ // Branch selection
selectedBranch,
setSelectedBranch,
refreshBranches,
+ setBranchesPage,
+ setBranches,
+
+ // Repository metadata
defaultBranch,
};
}
diff --git a/apps/web/src/providers/GitHubApp.tsx b/apps/web/src/providers/GitHubApp.tsx
new file mode 100644
index 00000000..eb71cc00
--- /dev/null
+++ b/apps/web/src/providers/GitHubApp.tsx
@@ -0,0 +1,27 @@
+import { useGitHubApp } from "@/hooks/useGitHubApp";
+import { createContext, useContext, ReactNode } from "react";
+
+type GitHubAppContextType = ReturnType;
+
+const GitHubAppContext = createContext(
+ undefined,
+);
+
+export function GitHubAppProvider({ children }: { children: ReactNode }) {
+ const value = useGitHubApp();
+ return (
+
+ {children}
+
+ );
+}
+
+export function useGitHubAppProvider() {
+ const context = useContext(GitHubAppContext);
+ if (context === undefined) {
+ throw new Error(
+ "useGitHubAppProvider must be used within a GitHubAppProvider",
+ );
+ }
+ return context;
+}
diff --git a/apps/web/src/utils/github.ts b/apps/web/src/utils/github.ts
index 6f77f161..06e2a60c 100644
--- a/apps/web/src/utils/github.ts
+++ b/apps/web/src/utils/github.ts
@@ -1,4 +1,3 @@
-import { GITHUB_TOKEN_COOKIE } from "@open-swe/shared/constants";
import * as jwt from "jsonwebtoken";
function getBaseApiUrl(): string {
@@ -41,7 +40,7 @@ export async function getInstallationToken(
headers: {
Authorization: `Bearer ${jwtToken}`,
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(
installationToken: string,
-): Promise {
- const response = await fetch(
- "https://api.github.com/installation/repositories",
- {
- headers: {
- Authorization: `Bearer ${installationToken}`,
- Accept: "application/vnd.github.v3+json",
- "User-Agent": "YourAppName",
- },
+ page: number = 1,
+ perPage: number = 30,
+): Promise<{
+ repositories: Repository[];
+ hasMore: boolean;
+ totalCount: number;
+}> {
+ 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) {
const errorData = await response.json();
@@ -82,7 +88,12 @@ export async function getInstallationRepositories(
}
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(
owner: string,
repo: string,
-): Promise {
- const allBranches: Branch[] = [];
- let page = 1;
- const perPage = 100; // Maximum allowed by GitHub API
-
+ page: number = 1,
+ perPage: number = 30,
+): Promise<{ branches: Branch[]; hasMore: boolean; totalCount?: number }> {
// First, get repository info to ensure we have the default branch
const repoResponse = await fetch(
@@ -114,52 +123,65 @@ export async function getRepositoryBranches(
defaultBranch = repoData.default_branch;
}
- // Fetch all branches with pagination
- while (true) {
- const response = await fetch(
- `${getBaseApiUrl()}github/proxy/repos/${owner}/${repo}/branches?per_page=${perPage}&page=${page}`,
- {
- headers: {
- Accept: "application/vnd.github.v3+json",
- "User-Agent": "OpenSWE-Agent",
- },
+ // Fetch first 30 branches only
+ const response = await fetch(
+ `${getBaseApiUrl()}github/proxy/repos/${owner}/${repo}/branches?per_page=${perPage}&page=${page}`,
+ {
+ headers: {
+ Accept: "application/vnd.github.v3+json",
+ "User-Agent": "OpenSWE-Agent",
},
- );
+ },
+ );
- if (!response.ok) {
- const errorData = await response.json();
- throw new Error(`Failed to fetch branches: ${JSON.stringify(errorData)}`);
- }
+ if (!response.ok) {
+ const errorData = await response.json();
+ 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) {
- break;
- }
-
- allBranches.push(...data);
-
- // If we got less than the requested amount, we've reached the end
- if (data.length < perPage) {
- break;
- }
-
- page++;
+ if (branches.length === 0) {
+ return { branches: [], hasMore: false };
}
// Ensure default branch is at the beginning if it exists
- if (defaultBranch) {
- const defaultBranchIndex = allBranches.findIndex(
+ if (defaultBranch && page === 1) {
+ const defaultBranchIndex = branches.findIndex(
(branch) => branch.name === defaultBranch,
);
if (defaultBranchIndex > 0) {
- const defaultBranchData = allBranches[defaultBranchIndex];
- allBranches.splice(defaultBranchIndex, 1);
- allBranches.unshift(defaultBranchData);
+ const defaultBranchData = branches[defaultBranchIndex];
+ branches.splice(defaultBranchIndex, 1);
+ 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,
+ };
}
/**