mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-10-06 20:32:12 +00:00
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:
parent
30881a5d92
commit
69b15a9f77
11 changed files with 367 additions and 152 deletions
|
|
@ -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");
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
27
apps/web/src/providers/GitHubApp.tsx
Normal file
27
apps/web/src/providers/GitHubApp.tsx
Normal 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;
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue