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