feat: add localStorage persistence for repository selection (#127)

* Apply patch

* Apply patch

* Apply patch

* fix: add getRepository func

* cr

* cr
This commit is contained in:
Brace Sproul 2025-06-11 18:07:59 -07:00 • committed by GitHub
parent 001d38228a
commit 38fc7f9cbd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 138 additions and 1 deletions

View file

@ -1,8 +1,47 @@
import { useState, useEffect, useRef, useCallback, useMemo } from "react";
import { useQueryState } from "nuqs";
import { Repository, getRepositoryBranches, Branch } from "@/utils/github";
import { getRepository } from "@/utils/github";
import type { TargetRepository } from "@open-swe/shared/open-swe/types";
const SELECTED_REPO_STORAGE_KEY = "selected-repository";
const saveRepositoryToLocalStorage = (repo: TargetRepository | null) => {
try {
if (repo) {
localStorage.setItem(SELECTED_REPO_STORAGE_KEY, JSON.stringify(repo));
} else {
localStorage.removeItem(SELECTED_REPO_STORAGE_KEY);
}
} catch (error) {
console.warn("Failed to save repository to localStorage:", error);
}
};
const getRepositoryFromLocalStorage = (): TargetRepository | null => {
try {
const stored = localStorage.getItem(SELECTED_REPO_STORAGE_KEY);
if (stored) {
const parsed = JSON.parse(stored);
if (
parsed &&
typeof parsed.owner === "string" &&
typeof parsed.repo === "string"
) {
return {
owner: parsed.owner,
repo: parsed.repo,
// Don't restore branch from localStorage
};
}
}
return null;
} catch (error) {
console.warn("Failed to retrieve repository from localStorage:", error);
return null;
}
};
interface UseGitHubAppReturn {
// Installation and general state
isInstalled: boolean | null;
@ -70,6 +109,9 @@ export function useGitHubApp(): UseGitHubAppReturn {
// Track if auto-selection has been attempted to prevent re-triggering
const hasAutoSelectedRef = useRef(false);
// Track if we've attempted to load from localStorage
const hasCheckedLocalStorageRef = useRef(false);
const selectedRepository = useMemo(() => {
if (!selectedRepositoryParam) return null;
try {
@ -104,6 +146,9 @@ export function useGitHubApp(): UseGitHubAppReturn {
const setSelectedRepository = useCallback(
(repo: TargetRepository | null) => {
setSelectedRepositoryParam(repo ? `${repo.owner}/${repo.repo}` : null);
// Persist to localStorage whenever repository is selected
saveRepositoryToLocalStorage(repo);
if (!repo) {
setSelectedBranchParam(null);
setBranches([]);
@ -227,6 +272,72 @@ export function useGitHubApp(): UseGitHubAppReturn {
checkInstallation();
}, []);
useEffect(() => {
if (
!hasCheckedLocalStorageRef.current &&
!selectedRepository &&
!isLoading &&
!error &&
isInstalled === true &&
repositories.length > 0
) {
hasCheckedLocalStorageRef.current = true;
const storedRepo = getRepositoryFromLocalStorage();
if (storedRepo) {
const existsInResponse = repositories.some(
(repo) => repo.full_name === `${storedRepo.owner}/${storedRepo.repo}`,
);
if (existsInResponse) {
setSelectedRepository(storedRepo);
hasAutoSelectedRef.current = true;
} else {
const fetchSpecificRepo = async () => {
try {
const specificRepo = await getRepository(
storedRepo.owner,
storedRepo.repo,
);
if (specificRepo) {
setSelectedRepository(storedRepo);
hasAutoSelectedRef.current = true;
} else {
const firstRepo = repositories[0];
const targetRepo = {
owner: firstRepo.full_name.split("/")[0],
repo: firstRepo.full_name.split("/")[1],
};
setSelectedRepository(targetRepo);
saveRepositoryToLocalStorage(targetRepo);
hasAutoSelectedRef.current = true;
}
} catch (error) {
console.warn("Failed to fetch specific repository:", error);
const firstRepo = repositories[0];
const targetRepo = {
owner: firstRepo.full_name.split("/")[0],
repo: firstRepo.full_name.split("/")[1],
};
setSelectedRepository(targetRepo);
saveRepositoryToLocalStorage(targetRepo);
hasAutoSelectedRef.current = true;
}
};
fetchSpecificRepo();
}
}
}
}, [
repositories,
selectedRepository,
isLoading,
error,
isInstalled,
setSelectedRepository,
]);
// Auto-select first repository on initial page load
useEffect(() => {
if (
@ -235,7 +346,8 @@ export function useGitHubApp(): UseGitHubAppReturn {
!isLoading && // Not loading repositories
!error && // No error occurred
isInstalled === true && // GitHub App is installed
repositories.length > 0 // Repositories are available
repositories.length > 0 && // Repositories are available
hasCheckedLocalStorageRef.current // Only after localStorage check is complete
) {
const firstRepo = repositories[0];
const targetRepo = {
@ -243,6 +355,7 @@ export function useGitHubApp(): UseGitHubAppReturn {
repo: firstRepo.full_name.split("/")[1],
};
setSelectedRepository(targetRepo);
saveRepositoryToLocalStorage(targetRepo);
hasAutoSelectedRef.current = true;
}
}, [
@ -252,6 +365,7 @@ export function useGitHubApp(): UseGitHubAppReturn {
error,
isInstalled,
setSelectedRepository,
hasCheckedLocalStorageRef.current,
]);
const refreshRepositories = async () => {

View file

@ -184,6 +184,29 @@ export async function getRepositoryBranches(
};
}
/**
* Fetches a specific repository using OAuth access token
*/
export async function getRepository(owner: string, repo: string) {
const response = await fetch(
`${getBaseApiUrl()}github/proxy/repos/${owner}/${repo}`,
{
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 repository: ${JSON.stringify(errorData)}`);
}
const data = await response.json();
return data;
}
/**
* Repository interface representing GitHub repository data
*/