import { useAgent } from "agents/react"; import { useCallback, useEffect, useState } from "react"; import { createRoot } from "react-dom/client"; import type { StoredEvent } from "./github-types"; import type { RepoState } from "./server"; import "./styles.css"; // Event type icons and colors const eventConfig: Record< string, { icon: string; color: string; label: string } > = { push: { icon: "commit", color: "#2ea043", label: "Push" }, pull_request: { icon: "git_pull_request", color: "#8957e5", label: "PR" }, issues: { icon: "error_outline", color: "#f85149", label: "Issue" }, issue_comment: { icon: "chat_bubble_outline", color: "#768390", label: "Comment" }, star: { icon: "star", color: "#e3b341", label: "Star" }, fork: { icon: "call_split", color: "#57ab5a", label: "Fork" }, release: { icon: "local_offer", color: "#388bfd", label: "Release" }, ping: { icon: "notifications", color: "#768390", label: "Ping" } }; function formatTimeAgo(timestamp: string): string { const now = new Date(); const then = new Date(timestamp); const diffMs = now.getTime() - then.getTime(); const diffSecs = Math.floor(diffMs / 1000); const diffMins = Math.floor(diffSecs / 60); const diffHours = Math.floor(diffMins / 60); const diffDays = Math.floor(diffHours / 24); if (diffSecs < 60) return "just now"; if (diffMins < 60) return `${diffMins}m ago`; if (diffHours < 24) return `${diffHours}h ago`; if (diffDays < 7) return `${diffDays}d ago`; return then.toLocaleDateString(); } function EventCard({ event }: { event: StoredEvent }) { const config = eventConfig[event.type] || { icon: "help_outline", color: "#768390", label: event.type }; return (
{config.icon}
{config.label} {event.action && ` (${event.action})`} {formatTimeAgo(event.timestamp)}
{event.title}
{event.description}
{event.actor.avatar_url && ( {event.actor.login} )} {event.actor.login}
); } function StatsBar({ stats }: { stats: RepoState["stats"] }) { return (
star {stats.stars.toLocaleString()} Stars
call_split {stats.forks.toLocaleString()} Forks
error_outline {stats.openIssues.toLocaleString()} Open Issues
); } function App() { const [repoInput, setRepoInput] = useState("cloudflare/agents"); const [connectedRepo, setConnectedRepo] = useState(null); const [agentName, setAgentName] = useState(null); const [events, setEvents] = useState([]); const [repoState, setRepoState] = useState(null); const [isConnecting, setIsConnecting] = useState(false); const [error, setError] = useState(null); const [filter, setFilter] = useState("all"); // Connect to the agent when agentName is set const agent = useAgent({ agent: "repo-agent", name: agentName || undefined, onStateUpdate: (state) => { setRepoState(state); }, onOpen: async () => { setIsConnecting(false); setError(null); // Fetch initial events try { const events = await agent.call("getEvents", [50]); setEvents(events); } catch (err) { console.error("Failed to fetch events:", err); } }, onError: (err) => { setError(`Connection error: ${err}`); setIsConnecting(false); } }); // Refresh events periodically useEffect(() => { if (!agentName) return; const interval = setInterval(async () => { try { const events = await agent.call("getEvents", [50]); setEvents(events); } catch (err) { console.error("Failed to refresh events:", err); } }, 10000); // Every 10 seconds return () => clearInterval(interval); }, [agentName, agent]); const handleConnect = useCallback(async () => { if (!repoInput.includes("/")) { setError("Please enter a valid repo name (owner/repo)"); return; } setIsConnecting(true); setError(null); try { // Get the sanitized agent name const response = await fetch( `/api/agent-name?repo=${encodeURIComponent(repoInput)}` ); const data = (await response.json()) as { agentName: string }; setAgentName(data.agentName); setConnectedRepo(repoInput); } catch (err) { setError(`Failed to connect: ${err}`); setIsConnecting(false); } }, [repoInput]); const handleClearEvents = useCallback(async () => { try { await agent.call("clearEvents"); setEvents([]); } catch (err) { console.error("Failed to clear events:", err); } }, [agent]); // Filter events const filteredEvents = filter === "all" ? events : events.filter((e) => e.type === filter); // Get unique event types for filter const eventTypes = Array.from(new Set(events.map((e) => e.type))); return (

webhook GitHub Webhook Dashboard

Real-time repository activity monitor

setRepoInput(e.target.value)} placeholder="owner/repo (e.g., cloudflare/agents)" className="repo-input" onKeyDown={(e) => e.key === "Enter" && handleConnect()} />
{error &&
{error}
} {connectedRepo && (
check_circle Connected to {connectedRepo} {!repoState?.webhookConfigured && ( hourglass_empty Waiting for webhook events... )}
)}
{repoState?.webhookConfigured && ( <>

history Recent Events

{filteredEvents.length === 0 ? (
inbox

No events yet

Events will appear here when webhooks are received

) : ( filteredEvents.map((event) => ( )) )}
)} {!connectedRepo && (

settings Setup Instructions

  1. Deploy this worker to get your webhook URL
  2. Go to your GitHub repository → Settings →{" "} Webhooks
  3. Click Add webhook
  4. Set Payload URL to:{" "} {window.location.origin}/webhooks/github/owner/repo
  5. Set Content type to: application/json
  6. Set your webhook secret (must match{" "} GITHUB_WEBHOOK_SECRET in your worker)
  7. Select events you want to receive (or choose "Send me everything")
  8. Enter your repository name above and click Connect
)}
); } const root = createRoot(document.getElementById("root")!); root.render();