Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Coding agents show as themselves on commits and contributors: Claude, Codex, Copilot, Gemini, Jules, Cursor, Devin, Aider, Dependabot, Renovate and GitHub Actions are known by the address they write on their commits (never by name), shown with their maker's mark, their own name and an agent or bot label, and a card saying what each is. Anyone else not on g1t gets a card too, saying so and how to claim the commits by adding the address. Contributor lists are worked out again (stats version 2) so an agent counts once. | 1 | import { Bot, ExternalLink, User } from "lucide-react"; |
| 2 | import type { ReactElement } from "react"; | |
| 3 | import { siClaude, siCursor, siDependabot, siGithubactions, siGithubcopilot, siGooglegemini, siRenovate } from "simple-icons"; | |
| 4 | ||
| 5 | import { type CodingAgent, type CodingAgentId, codingAgentById } from "@g1t/contracts/coding-agents"; | |
| 6 | ||
| 7 | import { cn } from "../lib/cn"; | |
| 8 | ||
| 9 | import { HoverCard, HoverCardContent, HoverCardTrigger } from "./ui/hover-card"; | |
| 10 | ||
| 11 | /** Each agent's mark (from Simple Icons) where its maker publishes one there; the rest show a bot. */ | |
| 12 | const MARKS: Partial<Record<CodingAgentId, string>> = { | |
| 13 | claude: siClaude.path, | |
| 14 | copilot: siGithubcopilot.path, | |
| 15 | gemini: siGooglegemini.path, | |
| 16 | jules: siGooglegemini.path, | |
| 17 | cursor: siCursor.path, | |
| 18 | dependabot: siDependabot.path, | |
| 19 | renovate: siRenovate.path, | |
| 20 | "github-actions": siGithubactions.path, | |
| 21 | }; | |
| 22 | ||
| 23 | /** | |
| 24 | * A coding agent's avatar: its maker's mark on its colour, or on the | |
| 25 | * page's own (light on dark, dark on light) when its mark is black or | |
| 26 | * it has none. | |
| 27 | */ | |
| 28 | export function AgentMark({ agent, size = 20, className }: { agent: CodingAgent; size?: number; className?: string }) { | |
| 29 | const path = MARKS[agent.id]; | |
| 30 | const glyph = Math.round(size * 0.58); | |
| 31 | return ( | |
| 32 | <span | |
| 33 | aria-hidden="true" | |
| 34 | className={cn("inline-flex shrink-0 items-center justify-center rounded-full", agent.color ? "text-white" : "bg-fg text-bg", className)} | |
| 35 | style={{ width: size, height: size, ...(agent.color ? { backgroundColor: `#${agent.color}` } : {}) }} | |
| 36 | > | |
| 37 | {path ? ( | |
| 38 | <svg viewBox="0 0 24 24" fill="currentColor" width={glyph} height={glyph}> | |
| 39 | <path d={path} /> | |
| 40 | </svg> | |
| 41 | ) : ( | |
| 42 | <Bot size={glyph} strokeWidth={2.25} /> | |
| 43 | )} | |
| 44 | </span> | |
| 45 | ); | |
| 46 | } | |
| 47 | ||
| 48 | /** "agent" or "bot", beside an agent's name. */ | |
| 49 | export function AgentBadge({ agent }: { agent: CodingAgent }) { | |
| 50 | return <span className="rounded border border-line px-1 text-[0.625rem] leading-[1.35] font-medium text-muted">{agent.kind}</span>; | |
| 51 | } | |
| 52 | ||
| 53 | /** | |
| 54 | * A coding agent's card, over its name or avatar (`children`, one element | |
| 55 | * that takes a ref): what it is, who makes it, and that g1t knows it by | |
| 56 | * the address on its commits rather than as an account. | |
| 57 | */ | |
| 58 | export function AgentCard({ agentId, children }: { agentId: string | null | undefined; children: ReactElement }) { | |
| 59 | const agent = codingAgentById(agentId); | |
| 60 | if (!agent) return children; | |
| 61 | return ( | |
| 62 | <HoverCard> | |
| 63 | <HoverCardTrigger asChild>{children}</HoverCardTrigger> | |
| 64 | <HoverCardContent aria-label={`About ${agent.name}`}> | |
| 65 | <div className="space-y-3"> | |
| 66 | <div className="flex items-start gap-3"> | |
| 67 | <AgentMark agent={agent} size={48} /> | |
| 68 | <div className="min-w-0 pt-0.5"> | |
| 69 | <p className="flex items-baseline gap-1.5 leading-tight"> | |
| 70 | <span className="font-semibold">{agent.name}</span> | |
| 71 | <AgentBadge agent={agent} /> | |
| 72 | </p> | |
| 73 | <p className="mt-0.5 text-xs text-faint">by {agent.maker}</p> | |
| 74 | </div> | |
| 75 | </div> | |
| 76 | <p className="leading-relaxed text-fg/90">{agent.about}</p> | |
| 77 | <p className="text-[0.8125rem] text-muted">Not an account on g1t: known by the address on its commits.</p> | |
| 78 | <a href={agent.url} target="_blank" rel="noreferrer" className="inline-flex items-center gap-1 text-[0.8125rem] font-medium text-accent hover:underline"> | |
| 79 | About {agent.name} | |
| 80 | <ExternalLink size={12} /> | |
| 81 | </a> | |
| 82 | </div> | |
| 83 | </HoverCardContent> | |
| 84 | </HoverCard> | |
| 85 | ); | |
| 86 | } | |
| 87 | ||
| 88 | /** | |
| 89 | * The card of an author nobody on g1t owns: the name on their commits, and | |
| 90 | * how to have them shown as you when they are yours. | |
| 91 | */ | |
| 92 | export function AuthorCard({ name, children }: { name: string; children: ReactElement }) { | |
| 93 | return ( | |
| 94 | <HoverCard> | |
| 95 | <HoverCardTrigger asChild>{children}</HoverCardTrigger> | |
| 96 | <HoverCardContent aria-label={`About ${name}`}> | |
| 97 | <div className="space-y-3"> | |
| 98 | <div className="flex items-start gap-3"> | |
| 99 | <span aria-hidden="true" className="inline-flex size-12 shrink-0 items-center justify-center rounded-full bg-line text-faint"> | |
| 100 | <User size={28} strokeWidth={2.25} /> | |
| 101 | </span> | |
| 102 | <div className="min-w-0 pt-0.5"> | |
| 103 | <p className="leading-tight font-semibold wrap-anywhere">{name}</p> | |
| 104 | <p className="mt-0.5 text-xs text-faint">Not on g1t</p> | |
| 105 | </div> | |
| 106 | </div> | |
| 107 | <p className="text-[0.8125rem] leading-relaxed text-muted"> | |
| 108 | The name on these commits. Their address isn't one any account has confirmed. If they're yours, add that address under{" "} | |
| 109 | <a href="/settings/emails" className="font-medium text-fg hover:underline"> | |
| 110 | Settings → Emails | |
| 111 | </a>{" "} | |
| 112 | and they'll show as you. | |
| 113 | </p> | |
| 114 | </div> | |
| 115 | </HoverCardContent> | |
| 116 | </HoverCard> | |
| 117 | ); | |
| 118 | } |