Skip to content
118 linesCodeBlameRaw
1import { Bot, ExternalLink, User } from "lucide-react";
2import type { ReactElement } from "react";
3import { siClaude, siCursor, siDependabot, siGithubactions, siGithubcopilot, siGooglegemini, siRenovate } from "simple-icons";
4
5import { type CodingAgent, type CodingAgentId, codingAgentById } from "@g1t/contracts/coding-agents";
6
7import { cn } from "../lib/cn";
8
9import { 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. */
12const 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 */
28export 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. */
49export 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 */
58export 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 */
92export 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&apos;t one any account has confirmed. If they&apos;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&apos;ll show as you.
113 </p>
114 </div>
115 </HoverCardContent>
116 </HoverCard>
117 );
118}