feat: Better support for pagination in gh branch dropdown (#430)

* feat: Better support for pagination in gh branch dropdown

* cr

* cr
This commit is contained in:
Brace Sproul 2025-07-16 11:49:46 -07:00 • committed by GitHub
parent 7a06b37bd8
commit 7e5a5a7947
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 172 additions and 20 deletions

View file

@ -18,6 +18,8 @@ import { useState, useEffect } from "react";
import { useGitHubAppProvider } from "@/providers/GitHubApp";
import { GitBranch, Shield } from "lucide-react";
import { TargetRepository } from "@open-swe/shared/open-swe/types";
import { Branch } from "@/utils/github";
import { toast } from "sonner";
interface BranchSelectorProps {
disabled?: boolean;
@ -27,6 +29,21 @@ interface BranchSelectorProps {
streamTargetRepository?: TargetRepository;
}
const findDefaultBranch = (
branches: Branch[],
defaultBranch: string | null,
) => {
const actualDefaultBranch = defaultBranch
? branches.find((branch) => branch.name === defaultBranch)
: null;
return actualDefaultBranch
? actualDefaultBranch.name
: branches.length > 0
? branches[0].name
: null;
};
export function BranchSelector({
disabled = false,
placeholder = "Select a branch...",
@ -35,6 +52,8 @@ export function BranchSelector({
streamTargetRepository,
}: BranchSelectorProps) {
const [open, setOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
const [isSearching, setIsSearching] = useState(false);
const {
branches,
branchesLoading,
@ -45,6 +64,7 @@ export function BranchSelector({
branchesHasMore,
branchesLoadingMore,
loadMoreBranches,
searchForBranch,
defaultBranch,
} = useGitHubAppProvider();
@ -61,19 +81,16 @@ export function BranchSelector({
selectedBranch &&
branches.some((branch) => branch.name === selectedBranch);
// Auto-select default branch if no branch is selected OR if the selected branch doesn't exist in this repo
if (!selectedBranch || !currentBranchExists) {
// Try to find the repository's actual default branch first
const actualDefaultBranch = defaultBranch
? branches.find((branch) => branch.name === defaultBranch)
: null;
if (actualDefaultBranch) {
setSelectedBranch(actualDefaultBranch.name);
} else if (branches.length > 0) {
// If default branch doesn't exist in branches list, select the first available branch
setSelectedBranch(branches[0].name);
}
if (selectedBranch && !currentBranchExists) {
searchForBranch(selectedBranch).then((b) => {
if (b) {
// branch was found after search. can return early
return;
}
setSelectedBranch(findDefaultBranch(branches, defaultBranch));
});
} else if (!selectedBranch || !currentBranchExists) {
setSelectedBranch(findDefaultBranch(branches, defaultBranch));
}
}
}, [
@ -92,6 +109,20 @@ export function BranchSelector({
setOpen(false);
};
const handleSearchForBranch = async () => {
if (!searchQuery.trim() || !selectedRepository) return;
setIsSearching(true);
try {
const foundBranch = await searchForBranch(searchQuery.trim());
if (!foundBranch) {
toast.warning(`Branch "${searchQuery.trim()}" not found`);
}
} finally {
setIsSearching(false);
}
};
if (!selectedRepository) {
return (
<Button
@ -106,7 +137,7 @@ export function BranchSelector({
);
}
if (branchesLoading) {
if (branchesLoading && !branches.length) {
return (
<Button
variant="outline"
@ -198,9 +229,32 @@ export function BranchSelector({
</PopoverTrigger>
<PopoverContent className="w-[340px] p-0">
<Command>
<CommandInput placeholder="Search branches..." />
<CommandInput
placeholder="Search branches..."
value={searchQuery}
onValueChange={setSearchQuery}
/>
<CommandList>
<CommandEmpty>No branches found.</CommandEmpty>
<CommandEmpty>
<div className="flex flex-col items-center gap-2 py-4">
<span className="text-muted-foreground text-sm">
No branches found.
</span>
{searchQuery.trim() && (
<Button
variant="outline"
size="sm"
onClick={handleSearchForBranch}
disabled={isSearching}
className="text-xs"
>
{isSearching
? "Searching..."
: `Search for "${searchQuery.trim()}"`}
</Button>
)}
</div>
</CommandEmpty>
<CommandGroup>
{branches
.slice()
@ -244,6 +298,26 @@ export function BranchSelector({
);
})}
</CommandGroup>
{/* Show this search button if there is a search query, and there are some results. this is for
cases when some results do show, just not the exact result the user is looking for */}
{searchQuery.trim() &&
branches.some((branch) =>
branch.name.toLowerCase().includes(searchQuery.toLowerCase()),
) && (
<div className="px-2 py-1">
<Button
variant="outline"
size="sm"
onClick={handleSearchForBranch}
disabled={isSearching}
className="w-full text-xs"
>
{isSearching
? "Searching..."
: `Search for "${searchQuery.trim()}"`}
</Button>
</div>
)}
{branchesHasMore && (
<CommandGroup>
<CommandItem

View file

@ -1,6 +1,11 @@
import { useState, useEffect, useRef, useCallback, useMemo } from "react";
import { useQueryState } from "nuqs";
import { Repository, getRepositoryBranches, Branch } from "@/utils/github";
import {
Repository,
getRepositoryBranches,
Branch,
searchBranch,
} from "@/utils/github";
import { getRepository } from "@/utils/github";
import type { TargetRepository } from "@open-swe/shared/open-swe/types";
import {
@ -88,6 +93,7 @@ interface UseGitHubAppReturn {
selectedBranch: string | null;
setSelectedBranch: (branch: string | null) => void;
refreshBranches: () => Promise<void>;
searchForBranch: (branchName: string) => Promise<Branch | null>;
// Repository metadata
defaultBranch: string | null;
@ -294,6 +300,39 @@ export function useGitHubApp(): UseGitHubAppReturn {
}
}, [branchesHasMore, branchesLoadingMore, branchesPage, fetchBranches]);
const searchForBranch = useCallback(
async (branchName: string): Promise<Branch | null> => {
if (!selectedRepository) {
return null;
}
try {
const branch = await searchBranch(
selectedRepository.owner,
selectedRepository.repo,
branchName,
);
if (branch) {
// Add the found branch to the existing branches list if it's not already there
setBranches((prev) => {
const exists = prev.some((b) => b.name === branch.name);
if (!exists) {
return [...prev, branch];
}
return prev;
});
}
return branch;
} catch (error) {
console.error(`Error searching for branch ${branchName}:`, error);
return null;
}
},
[selectedRepository?.owner, selectedRepository?.repo],
);
// Refresh repositories when installation changes
useEffect(() => {
if (currentInstallationId) {
@ -483,6 +522,7 @@ export function useGitHubApp(): UseGitHubAppReturn {
selectedBranch,
setSelectedBranch,
refreshBranches,
searchForBranch,
setBranchesPage,
setBranches,

View file

@ -187,7 +187,10 @@ export async function getRepositoryBranches(
/**
* Fetches a specific repository using OAuth access token
*/
export async function getRepository(owner: string, repo: string) {
export async function getRepository(
owner: string,
repo: string,
): Promise<Repository> {
const response = await fetch(
`${getBaseApiUrl()}github/proxy/repos/${owner}/${repo}`,
{
@ -203,8 +206,43 @@ export async function getRepository(owner: string, repo: string) {
throw new Error(`Failed to fetch repository: ${JSON.stringify(errorData)}`);
}
const data = await response.json();
return data;
return response.json();
}
/**
* Searches for a specific branch by name in a repository
*/
export async function searchBranch(
owner: string,
repo: string,
branchName: string,
): Promise<Branch | null> {
try {
const response = await fetch(
`${getBaseApiUrl()}github/proxy/repos/${owner}/${repo}/branches/${encodeURIComponent(branchName)}`,
{
headers: {
Accept: "application/vnd.github.v3+json",
"User-Agent": "OpenSWE-Agent",
},
},
);
if (!response.ok) {
if (response.status === 404) {
return null; // Branch not found
}
const errorData = await response.json();
throw new Error(
`Failed to search for branch: ${JSON.stringify(errorData)}`,
);
}
return response.json();
} catch (error) {
console.error(`Error searching for branch ${branchName}:`, error);
return null;
}
}
/**