import { Trans, useLingui } from "@lingui/react/macro"; import { Button, Input, Spinner } from "@rakazo/ui-web"; import { Check, ExternalLink, GitBranch, Lock, RefreshCw, Search, X } from "lucide-react"; import { useCallback, useEffect, useId, useMemo, useState } from "react"; import { rpc } from "../lib/rpc"; export interface GithubRepoItem { name: string; fullName: string; url: string; description?: string | null; isPrivate: boolean; updatedAt?: string; } export interface GithubRepoPickerProps { value: string; onChange: (url: string, repo?: { name: string; fullName: string }) => void; onSuggestProjectName?: (name: string) => void; onSuggestWorkspacePath?: (path: string) => void; disabled?: boolean; } function GithubMark({ className }: { className?: string }) { return ( ); } export function GithubRepoPicker({ value, onChange, onSuggestProjectName, onSuggestWorkspacePath, disabled = false, }: GithubRepoPickerProps) { const { t } = useLingui(); const searchId = useId(); const manualId = useId(); const [connected, setConnected] = useState(null); const [username, setUsername] = useState(undefined); const [repos, setRepos] = useState([]); const [loading, setLoading] = useState(false); const [searchQuery, setSearchQuery] = useState(""); const [showManualInput, setShowManualInput] = useState(false); const fetchRepos = useCallback(async () => { setLoading(true); try { const res = await rpc.spaces.listGithubRepos({ limit: 100 }); setConnected(res.connected); setUsername(res.username); setRepos(res.repos); if (!res.connected && !value) { setShowManualInput(true); } } catch { setConnected(false); setShowManualInput(true); } finally { setLoading(false); } }, [value]); useEffect(() => { void fetchRepos(); }, [fetchRepos]); const filteredRepos = useMemo(() => { if (!searchQuery.trim()) return repos; const q = searchQuery.trim().toLowerCase(); return repos.filter( (r) => r.name.toLowerCase().includes(q) || r.fullName.toLowerCase().includes(q) || (r.description && r.description.toLowerCase().includes(q)), ); }, [repos, searchQuery]); const handleSelectRepo = (repo: GithubRepoItem) => { onChange(repo.url, { name: repo.name, fullName: repo.fullName }); if (onSuggestProjectName) { onSuggestProjectName(repo.name); } if (onSuggestWorkspacePath) { onSuggestWorkspacePath(`~/Projects/${repo.name}`); } }; const selectedRepo = useMemo(() => { if (!value) return null; return ( repos.find( (r) => r.url.toLowerCase() === value.toLowerCase() || r.url.replace(/\.git$/, "").toLowerCase() === value.replace(/\.git$/, "").toLowerCase(), ) ?? null ); }, [repos, value]); return (
GitHub repository (optional)
{connected ? (
{username ? `@${username}` : t`Connected`}
) : connected === false ? (
) : null}
{showManualInput || connected === false ? (
onChange(e.target.value)} placeholder="https://github.com/organization/repository" spellCheck={false} className="text-ui-body-sm" /> {value ? ( ) : null}
) : null} {connected && !showManualInput ? (
{/* Active selection banner if a repo is currently selected */} {selectedRepo ? (
{selectedRepo.isPrivate ? ( ) : ( )} {selectedRepo.fullName}
) : null} {/* Mini-search input */}
setSearchQuery(e.target.value)} placeholder={t`Search repositories...`} disabled={disabled || loading} className="pl-8 h-8 text-ui-body-sm" /> {loading ? (
) : null}
{/* Repositories list */}
{loading && repos.length === 0 ? (
) : filteredRepos.length === 0 ? (
{searchQuery ? ( No matching repositories ) : ( No repositories found )}
) : ( filteredRepos.map((repo) => { const isSelected = value.toLowerCase() === repo.url.toLowerCase() || value.replace(/\.git$/, "").toLowerCase() === repo.url.replace(/\.git$/, "").toLowerCase(); return ( ); }) )}
) : null}
); }