///
import { useEffect, useMemo, useRef, useState } from "react";
import { Check, GitBranch, Tag } from "lucide-react";
import { hydrateIsland } from "@/client/hydrate";
import { buttonClasses } from "@/client/components/ui/button";
type RefItem = {
name: string;
displayName: string;
};
type RefApiResponse = {
branches?: RefItem[];
tags?: RefItem[];
};
export type RefPickerProps = {
owner: string;
repo: string;
currentRef: string;
};
function decodeSafe(value: string): string {
try {
return decodeURIComponent(value);
} catch {
return value;
}
}
function formatRefLabel(ref: string): string {
return /^[0-9a-f]{40}$/i.test(ref) ? `${ref.slice(0, 7)}...` : ref || "...";
}
function buildRefHref(currentUrl: string, nextRef: string): string {
const url = new URL(currentUrl);
url.searchParams.set("ref", nextRef);
return url.toString();
}
function RefPickerSection({
title,
kind,
currentRef,
currentUrl,
items,
query,
}: {
title: string;
kind: "branch" | "tag";
currentRef: string;
currentUrl: string | null;
items: RefItem[];
query: string;
}) {
const filtered = items.filter((item) => item.displayName.toLowerCase().includes(query));
if (!filtered.length) {
return null;
}
const Icon = kind === "branch" ? GitBranch : Tag;
return (
<>
{title}
{filtered.map((item) => {
const raw = decodeSafe(item.name);
const isCurrent = raw === currentRef;
const href = currentUrl ? buildRefHref(currentUrl, raw) : null;
return isCurrent ? (
{item.displayName}
) : href ? (
{item.displayName}
) : (
{item.displayName}
);
})}
>
);
}
export function RefPickerIsland({ owner, repo, currentRef }: RefPickerProps) {
const rootRef = useRef(null);
const filterRef = useRef(null);
const [currentUrl, setCurrentUrl] = useState(null);
const [open, setOpen] = useState(false);
const [query, setQuery] = useState("");
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [data, setData] = useState({ branches: [], tags: [] });
useEffect(() => {
setCurrentUrl(window.location.href);
}, []);
useEffect(() => {
let cancelled = false;
async function loadRefs() {
setLoading(true);
setError(null);
try {
const response = await fetch(`/${owner}/${repo}/api/refs`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const nextData = (await response.json()) as RefApiResponse;
if (!cancelled) {
setData({
branches: nextData.branches || [],
tags: nextData.tags || [],
});
}
} catch (err) {
if (!cancelled) {
setError(err instanceof Error ? err.message : "Failed to load refs");
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
}
void loadRefs();
return () => {
cancelled = true;
};
}, [owner, repo]);
useEffect(() => {
if (!open) {
return;
}
const timeout = window.setTimeout(() => {
filterRef.current?.focus();
filterRef.current?.select();
}, 0);
function onPointerDown(event: MouseEvent) {
if (!rootRef.current?.contains(event.target as Node)) {
setOpen(false);
}
}
function onKeyDown(event: KeyboardEvent) {
if (event.key === "Escape") {
setOpen(false);
}
}
document.addEventListener("mousedown", onPointerDown);
document.addEventListener("keydown", onKeyDown);
return () => {
window.clearTimeout(timeout);
document.removeEventListener("mousedown", onPointerDown);
document.removeEventListener("keydown", onKeyDown);
};
}, [open]);
const branches = data.branches || [];
const tags = data.tags || [];
const queryLower = query.trim().toLowerCase();
const hasCurrentRef = useMemo(
() => [...branches, ...tags].some((item) => decodeSafe(item.name) === currentRef),
[branches, currentRef, tags]
);
const showCurrentChip =
!hasCurrentRef && currentRef && formatRefLabel(currentRef).includes(queryLower);
return (
{
event.preventDefault();
setOpen((value) => !value);
}}
>
{formatRefLabel(currentRef)}
setQuery(event.target.value)}
className="w-full rounded-lg border border-zinc-300 bg-white px-2 py-1.5 text-sm dark:border-zinc-700 dark:bg-zinc-900"
autoComplete="off"
/>
{loading ? (
Loading...
) : null}
{!loading && error ? (
{error}
) : null}
{!loading && !error ? (
<>
{showCurrentChip ? (
<>
Current
{/^[0-9a-f]{40}$/i.test(currentRef)
? `Commit: ${formatRefLabel(currentRef)}`
: currentRef}
>
) : null}
{!showCurrentChip &&
!branches.some((item) => item.displayName.toLowerCase().includes(queryLower)) &&
!tags.some((item) => item.displayName.toLowerCase().includes(queryLower)) ? (
No refs
) : null}
>
) : null}
);
}
export function initRefPicker() {
hydrateIsland("ref-picker", RefPickerIsland);
}