import "./BranchGroupCard.css"; import { useCallback, useEffect, useMemo, useState } from "react"; import { CheckCircle2, ChevronDown, ChevronRight, CircleDashed, ExternalLink, GitBranch, GitPullRequest, Loader2 } from "lucide-react"; import type { BranchGroupSummary } from "../api"; import { apiGetBranchGroup, apiPromoteBranchGroup } from "../api"; import { subscribeSse } from "../sse-bus"; interface BranchGroupCardProps { groupId: string; projectId?: string; } export function BranchGroupCard({ groupId, projectId }: BranchGroupCardProps) { const [group, setGroup] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [promoting, setPromoting] = useState(false); const [collapsed, setCollapsed] = useState(false); const loadGroup = useCallback(async () => { try { const response = await apiGetBranchGroup(groupId, projectId); setGroup(response.group); setError(null); } catch (loadError) { const message = loadError instanceof Error ? loadError.message : "Failed to load branch group"; setError(message); } finally { setLoading(false); } }, [groupId, projectId]); useEffect(() => { setLoading(true); void loadGroup(); }, [loadGroup]); useEffect(() => { if (typeof window === "undefined" || typeof window.matchMedia !== "function") { return; } const mediaQuery = window.matchMedia("(max-width: 768px)"); const syncCollapsed = (matches: boolean) => { setCollapsed(matches); }; syncCollapsed(mediaQuery.matches); const onMediaChange = (event: MediaQueryListEvent) => { syncCollapsed(event.matches); }; if (typeof mediaQuery.addEventListener === "function") { mediaQuery.addEventListener("change", onMediaChange); return () => mediaQuery.removeEventListener("change", onMediaChange); } mediaQuery.addListener(onMediaChange); return () => mediaQuery.removeListener(onMediaChange); }, []); useEffect(() => { const query = projectId ? `?projectId=${encodeURIComponent(projectId)}` : ""; return subscribeSse(`/api/events${query}`, { events: { "task:updated": () => { void loadGroup(); }, }, onReconnect: () => { void loadGroup(); }, }); }, [loadGroup, projectId]); const completionText = useMemo(() => { if (!group) return ""; return `${group.completion.landed} of ${group.completion.total} members finished`; }, [group]); const onPromote = useCallback(async () => { setPromoting(true); try { await apiPromoteBranchGroup(groupId, projectId); await loadGroup(); } finally { setPromoting(false); } }, [groupId, loadGroup, projectId]); if (loading) { return
Loading branch group…
; } if (error || !group) { return
{error ?? "Branch group unavailable"}
; } const completionPercent = group.completion.total > 0 ? (group.completion.landed / group.completion.total) * 100 : 0; const complete = group.completion.complete; return (
{group.branchName}
Group {group.id}
{completionText}
{!collapsed && (
    {group.members.map((member) => (
  • {member.taskId} · {member.title} {member.landed ? : }
  • ))}
)} {!collapsed && complete && (
{group.prUrl && ( PR #{group.prNumber ?? "—"} )} {group.autoMerge ? ( Auto-merge enabled ) : ( )}
)}
); }