Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

170 linesCodeBlameRaw
1import { User } from "lucide-react";
2import { Link } from "react-router";
3
4import { codingAgentById } from "@g1t/contracts/coding-agents";
5
6import { type CommitPerson, type ShownCommit, profileHref, shownName } from "../lib/commit-people";
7import { cn } from "../lib/cn";
8
9import { AgentBadge, AgentCard, AgentMark, AuthorCard } from "./coding-agent";
10import { UserCard } from "./user-card";
11import { Avatar } from "./ui/avatar";
12
13/**
14 * A person on a commit, as an avatar: theirs and their card when the
15 * commit is matched to an account (or is g1t's); a coding agent's mark
16 * and card; else a silhouette with a card that says they're not on g1t.
17 */
18export function CommitAvatar({ person, size = 20 }: { person: CommitPerson; size?: number }) {
19 if (person.kind === "user" || person.kind === "g1t") {
20 const href = profileHref(person);
21 const avatar = <Avatar name={shownName(person)} image={person.avatar} size={size} />;
22 return (
23 <UserCard username={person.username}>
24 {href ? (
25 // Its name beside it is the link a keyboard reaches; this is for the pointer.
26 <Link to={href} tabIndex={-1} aria-hidden="true" className="relative z-10 inline-flex shrink-0 rounded-full">
27 {avatar}
28 </Link>
29 ) : (
30 <span className="relative z-10 inline-flex shrink-0">{avatar}</span>
31 )}
32 </UserCard>
33 );
34 }
35 const agent = person.kind === "agent" ? codingAgentById(person.agent) : null;
36 if (agent) {
37 return (
38 <AgentCard agentId={agent.id}>
39 <span className="relative z-10 inline-flex shrink-0 rounded-full">
40 <AgentMark agent={agent} size={size} />
41 </span>
42 </AgentCard>
43 );
44 }
45 const silhouette = (
46 <span aria-hidden="true" className="inline-flex shrink-0 items-center justify-center rounded-full bg-line text-faint" style={{ width: size, height: size }}>
47 <User size={Math.round(size * 0.62)} strokeWidth={2.25} />
48 </span>
49 );
50 // A deleted account: a plain silhouette, no card.
51 if (person.kind === "ghost") return silhouette;
52 // Nobody on g1t: the silhouette, with a card that says so.
53 return (
54 <AuthorCard name={person.name}>
55 <span className="relative z-10 inline-flex shrink-0 rounded-full">{silhouette}</span>
56 </AuthorCard>
57 );
58}
59
60/**
61 * A commit's author and co-authors, as overlapping avatars: the author's
62 * on top, at most `max`.
63 */
64export function CommitAvatars({ commit, size = 20, max = 3 }: { commit: Pick<ShownCommit, "author" | "coAuthors">; size?: number; max?: number }) {
65 const people = [commit.author, ...commit.coAuthors].slice(0, max);
66 if (people.length === 1) return <CommitAvatar person={commit.author} size={size} />;
67 return (
68 <span className="inline-flex shrink-0 items-center">
69 {people.map((person, index) => (
70 <span
71 key={`${index}:${person.username ?? person.name}`}
72 className="inline-flex rounded-full ring-2 ring-bg"
73 style={{ marginLeft: index === 0 ? 0 : -Math.round(size * 0.35), zIndex: people.length - index }}
74 >
75 <CommitAvatar person={person} size={size} />
76 </span>
77 ))}
78 </span>
79 );
80}
81
82/**
83 * A person on a commit, by name: their username, linked to their profile
84 * with their card, for an account; g1t with its label and card; ghost for
85 * a deleted account; else the name written on the commit, as text.
86 */
87export function CommitName({ person, className }: { person: CommitPerson; className?: string }) {
88 if (person.kind === "g1t") {
89 return (
90 <UserCard username="g1t">
91 <span tabIndex={0} className="relative z-10 inline-flex items-baseline gap-1 rounded outline-none focus-visible:ring-2 focus-visible:ring-accent">
92 <span className={className}>g1t</span>
93 <span className="rounded border border-line px-1 text-[0.625rem] leading-[1.35] font-medium text-muted">bot</span>
94 </span>
95 </UserCard>
96 );
97 }
98 const agent = person.kind === "agent" ? codingAgentById(person.agent) : null;
99 if (agent) {
100 return (
101 <AgentCard agentId={agent.id}>
102 <span tabIndex={0} className="relative z-10 inline-flex items-baseline gap-1 rounded outline-none focus-visible:ring-2 focus-visible:ring-accent">
103 <span className={className}>{agent.name}</span>
104 <AgentBadge agent={agent} />
105 </span>
106 </AgentCard>
107 );
108 }
109 const href = profileHref(person);
110 if (!href) {
111 if (person.kind === "ghost") return <span className={className}>{shownName(person)}</span>;
112 return (
113 <AuthorCard name={person.name}>
114 <span tabIndex={0} className={cn("relative z-10 rounded outline-none focus-visible:ring-2 focus-visible:ring-accent", className)}>
115 {shownName(person)}
116 </span>
117 </AuthorCard>
118 );
119 }
120 return (
121 <UserCard username={person.username}>
122 <Link to={href} className={cn("relative z-10 hover:underline", className)}>
123 {shownName(person)}
124 </Link>
125 </UserCard>
126 );
127}
128
129/**
130 * Who made a commit, in words: the author, and its co-authors after them
131 * ("ada and grace", "ada, grace and 2 others").
132 */
133export function CommitNames({
134 commit,
135 className,
136 all,
137}: {
138 commit: Pick<ShownCommit, "author" | "coAuthors">;
139 className?: string;
140 /** Name every co-author, rather than counting past the first. */
141 all?: boolean;
142}) {
143 const others = commit.coAuthors;
144 if (others.length === 0) return <CommitName person={commit.author} className={className} />;
145 if (all && others.length > 1) {
146 const people = [commit.author, ...others];
147 return (
148 <>
149 {people.map((person, index) => (
150 <span key={index}>
151 {index > 0 && (index === people.length - 1 ? " and " : ", ")}
152 <CommitName person={person} className={className} />
153 </span>
154 ))}
155 </>
156 );
157 }
158 if (others.length === 1) {
159 return (
160 <>
161 <CommitName person={commit.author} className={className} /> and <CommitName person={others[0]!} className={className} />
162 </>
163 );
164 }
165 return (
166 <>
167 <CommitName person={commit.author} className={className} /> and {others.length} others
168 </>
169 );
170}