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.
| Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running | 1 | import { Mail, ShieldCheck } from "lucide-react"; |
| 2 | import { Form } from "react-router"; | |
| 3 | ||
| 4 | import { type AccountEmails, type SecurityEvent, securityEventLabel } from "@g1t/contracts"; | |
| 5 | ||
| 6 | import { addressActions, backupChoices } from "../lib/emails"; | |
| 7 | import type { EmailActionData } from "../lib/emails.server"; | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 8 | import { ErrorText, Field, Input, SubmitButton, TimeAgo } from "./ui"; |
| 9 | import { Badge } from "./ui/badge"; | |
| Chat controls, public profiles, shadcn selects, and no Docs tab in a project | 10 | import { SelectField } from "./ui/select"; |
| Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running | 11 | import { SwitchCard } from "./ui/switch"; |
| 12 | ||
| 13 | /** One small form posting one intent about one address. */ | |
| Merge branch 'worktree-agent-a985c01f43538af01' | 14 | function AddressButton({ intent, email, label, pending }: { intent: string; email: string; label: string; pending: string }) { |
| Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running | 15 | return ( |
| 16 | <Form method="post"> | |
| 17 | <input type="hidden" name="intent" value={intent} /> | |
| 18 | <input type="hidden" name="email" value={email} /> | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 19 | <SubmitButton variant="outline" pending={pending} match={{ intent, email }}> |
| Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running | 20 | {label} |
| Merge branch 'worktree-agent-a985c01f43538af01' | 21 | </SubmitButton> |
| Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running | 22 | </Form> |
| 23 | ); | |
| 24 | } | |
| 25 | ||
| 26 | /** What a waiting change does, for the line that asks for the password. */ | |
| 27 | function describe(intent: string): string { | |
| 28 | switch (intent) { | |
| 29 | case "add-email": | |
| 30 | return "adding"; | |
| 31 | case "remove-email": | |
| 32 | return "removing"; | |
| 33 | case "primary-email": | |
| 34 | return "making primary"; | |
| 35 | default: | |
| 36 | return "for"; | |
| 37 | } | |
| 38 | } | |
| 39 | ||
| 40 | /** Asks for the password before a sensitive change goes through. */ | |
| Merge main (membership, two-factor, GitHub repo roles) into tokens | 41 | export function ConfirmItIsYou({ pending, hasPassword }: { pending: NonNullable<EmailActionData>["reauth"]; hasPassword: boolean }) { |
| Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running | 42 | if (!pending) return null; |
| 43 | return ( | |
| 44 | <div role="alert" className="rounded-xl border border-accent/40 bg-surface p-4"> | |
| 45 | <p className="flex items-center gap-2 text-sm font-medium"> | |
| 46 | <ShieldCheck size={16} className="text-accent" /> Confirm it is you | |
| 47 | </p> | |
| 48 | {hasPassword ? ( | |
| 49 | <Form method="post" className="mt-3 space-y-3"> | |
| 50 | <p className="text-sm text-muted"> | |
| 51 | {pending.message} | |
| 52 | {pending.fields.email ? ( | |
| 53 | <> | |
| 54 | {" "} | |
| 55 | ({describe(pending.intent)} <span className="text-fg">{pending.fields.email}</span>) | |
| 56 | </> | |
| 57 | ) : null} | |
| 58 | </p> | |
| 59 | <input type="hidden" name="intent" value={pending.intent} /> | |
| 60 | {Object.entries(pending.fields).map(([name, value]) => ( | |
| 61 | <input key={name} type="hidden" name={name} value={value} /> | |
| 62 | ))} | |
| 63 | <div className="flex flex-col gap-3 sm:flex-row sm:items-end"> | |
| 64 | <div className="grow"> | |
| 65 | <Field label="Password"> | |
| 66 | <Input name="password" type="password" autoComplete="current-password" required autoFocus /> | |
| 67 | </Field> | |
| 68 | </div> | |
| Merge branch 'worktree-agent-a985c01f43538af01' | 69 | <SubmitButton pending="Confirming…" match={{ intent: pending.intent }}> |
| 70 | Confirm | |
| 71 | </SubmitButton> | |
| Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running | 72 | </div> |
| 73 | <p className="text-xs text-faint">You will not be asked again for 10 minutes.</p> | |
| 74 | </Form> | |
| 75 | ) : ( | |
| 76 | <p className="mt-2 text-sm text-muted"> | |
| 77 | Your account signs in with GitHub only. Sign out, sign in with GitHub again, and make this change within 10 | |
| 78 | minutes. Or set a password with Forgot your password on the sign-in page. | |
| 79 | </p> | |
| 80 | )} | |
| 81 | </div> | |
| 82 | ); | |
| 83 | } | |
| 84 | ||
| Merge branch 'worktree-agent-a985c01f43538af01' | 85 | /** The answer to one form's post, under that form: what went wrong, or that it worked. */ |
| 86 | function Answer({ actionData }: { actionData: EmailActionData | undefined }) { | |
| 87 | return ( | |
| 88 | <> | |
| 89 | <ErrorText>{actionData?.emailError}</ErrorText> | |
| 90 | {actionData?.emailNotice && ( | |
| 91 | <p role="status" className="mt-2 text-sm text-muted"> | |
| 92 | {actionData.emailNotice} | |
| 93 | </p> | |
| 94 | )} | |
| 95 | </> | |
| 96 | ); | |
| 97 | } | |
| 98 | ||
| 99 | /** | |
| 100 | * A person's email addresses, in their account settings. `intent` is the | |
| 101 | * form that last posted, so its answer shows beside it. | |
| 102 | */ | |
| Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running | 103 | export function EmailsSection({ |
| 104 | data, | |
| 105 | actionData, | |
| Merge branch 'worktree-agent-a985c01f43538af01' | 106 | intent, |
| Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running | 107 | hasPassword, |
| 108 | }: { | |
| 109 | data: AccountEmails | null; | |
| 110 | actionData: EmailActionData | undefined; | |
| Merge branch 'worktree-agent-a985c01f43538af01' | 111 | intent?: string; |
| Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running | 112 | hasPassword: boolean; |
| 113 | }) { | |
| 114 | if (!data) return null; | |
| Merge branch 'worktree-agent-a985c01f43538af01' | 115 | const answerFor = (one: string) => (intent === one ? actionData : undefined); |
| 116 | const elsewhere = intent === "backup-email" || intent === "email-privacy"; | |
| Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running | 117 | const backups = backupChoices(data.emails); |
| 118 | const backup = data.emails.find((email) => email.backup); | |
| 119 | const full = data.emails.length >= data.limit; | |
| 120 | return ( | |
| 121 | <section id="emails" className="scroll-mt-20"> | |
| 122 | {actionData?.reauth && ( | |
| 123 | <div className="mb-4"> | |
| 124 | <ConfirmItIsYou pending={actionData.reauth} hasPassword={hasPassword} /> | |
| 125 | </div> | |
| 126 | )} | |
| 127 | ||
| 128 | <ul className="divide-y divide-line rounded-md border border-line"> | |
| 129 | {data.emails.map((email) => { | |
| 130 | const can = addressActions(email, data.emails); | |
| 131 | return ( | |
| 132 | <li key={email.email} className="flex flex-col gap-3 px-4 py-3 sm:flex-row sm:items-center"> | |
| 133 | <div className="flex min-w-0 grow items-start gap-3"> | |
| 134 | <Mail size={16} className="mt-0.5 shrink-0 text-faint" /> | |
| 135 | <div className="min-w-0"> | |
| 136 | <p className="truncate text-sm">{email.email}</p> | |
| 137 | <p className="mt-1 flex flex-wrap items-center gap-1.5 text-xs text-faint"> | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 138 | {email.primary && <Badge size="md">Primary</Badge>} |
| 139 | {email.backup && <Badge size="md">Backup</Badge>} | |
| Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running | 140 | {email.verified ? ( |
| 141 | <span> | |
| 142 | Confirmed {email.verifiedAt ? <TimeAgo at={email.verifiedAt} /> : null} | |
| 143 | </span> | |
| 144 | ) : ( | |
| 145 | <span className="text-warn">Unconfirmed: check your inbox for the link</span> | |
| 146 | )} | |
| 147 | </p> | |
| 148 | </div> | |
| 149 | </div> | |
| 150 | <div className="flex shrink-0 flex-wrap gap-2 sm:justify-end"> | |
| Merge branch 'worktree-agent-a985c01f43538af01' | 151 | {can.resend && <AddressButton intent="resend-email" email={email.email} label="Resend link" pending="Sending…" />} |
| 152 | {can.makePrimary && <AddressButton intent="primary-email" email={email.email} label="Make primary" pending="Saving…" />} | |
| 153 | {can.remove && <AddressButton intent="remove-email" email={email.email} label="Remove" pending="Removing…" />} | |
| Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running | 154 | </div> |
| 155 | </li> | |
| 156 | ); | |
| 157 | })} | |
| 158 | </ul> | |
| 159 | ||
| Merge branch 'worktree-agent-a985c01f43538af01' | 160 | {/* Keyed on the list, so an address once added leaves the field empty. */} |
| 161 | <Form key={data.emails.length} method="post" className="mt-4"> | |
| Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running | 162 | <input type="hidden" name="intent" value="add-email" /> |
| 163 | <div className="flex flex-col gap-3 sm:flex-row sm:items-end"> | |
| 164 | <div className="grow"> | |
| 165 | <Field label="Add an email address"> | |
| 166 | <Input name="email" type="email" autoComplete="email" placeholder="you@example.com" required disabled={full} /> | |
| 167 | </Field> | |
| 168 | </div> | |
| Merge branch 'worktree-agent-a985c01f43538af01' | 169 | <SubmitButton pending="Adding…" match={{ intent: "add-email" }} disabled={full}> |
| Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running | 170 | Add |
| Merge branch 'worktree-agent-a985c01f43538af01' | 171 | </SubmitButton> |
| Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running | 172 | </div> |
| 173 | <p className="mt-1.5 text-xs text-faint"> | |
| 174 | {full ? `You have ${data.limit} addresses, the most an account can have.` : "g1t sends it a link to confirm it."} | |
| 175 | </p> | |
| 176 | </Form> | |
| Merge branch 'worktree-agent-a985c01f43538af01' | 177 | <Answer actionData={elsewhere ? undefined : actionData} /> |
| Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running | 178 | |
| 179 | <div className="mt-8"> | |
| 180 | <h2 className="font-medium">Backup address</h2> | |
| 181 | <p className="mt-1 text-sm text-muted"> | |
| 182 | Security notices, such as a new address or a changed password, go to your primary and to this address. | |
| 183 | </p> | |
| 184 | <Form method="post" className="mt-3 flex flex-col gap-3 sm:flex-row sm:items-end"> | |
| 185 | <input type="hidden" name="intent" value="backup-email" /> | |
| Chat controls, public profiles, shadcn selects, and no Docs tab in a project | 186 | <div className="block grow"> |
| 187 | <SelectField | |
| Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running | 188 | name="backup" |
| Chat controls, public profiles, shadcn selects, and no Docs tab in a project | 189 | aria-label="Backup address" |
| Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running | 190 | defaultValue={backup?.email ?? ""} |
| 191 | disabled={backups.length === 0} | |
| Chat controls, public profiles, shadcn selects, and no Docs tab in a project | 192 | className="h-auto py-2" |
| 193 | options={[{ value: "", label: "Primary address only" }, ...backups.map((email) => ({ value: email.email, label: email.email }))]} | |
| 194 | /> | |
| 195 | </div> | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 196 | <SubmitButton variant="outline" pending="Saving…" match={{ intent: "backup-email" }} disabled={backups.length === 0}> |
| Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running | 197 | Save |
| Merge branch 'worktree-agent-a985c01f43538af01' | 198 | </SubmitButton> |
| Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running | 199 | </Form> |
| Merge branch 'worktree-agent-a985c01f43538af01' | 200 | <Answer actionData={answerFor("backup-email")} /> |
| Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running | 201 | {backups.length === 0 && ( |
| 202 | <p className="mt-2 text-xs text-faint">Add and confirm a second address to choose a backup.</p> | |
| 203 | )} | |
| 204 | </div> | |
| 205 | ||
| 206 | <div className="mt-8"> | |
| 207 | <h2 className="font-medium">Privacy</h2> | |
| 208 | <Form method="post" className="mt-3 space-y-3"> | |
| 209 | <input type="hidden" name="intent" value="email-privacy" /> | |
| 210 | <SwitchCard name="private" defaultChecked={data.privateEmail} title="Keep my email address private"> | |
| 211 | Merges and other commits g1t makes for you on the web, or an agent makes for you, use{" "} | |
| People wear their own avatars wherever g1t knows them only by name, and a workspace is never shown as a person. The activity feeds, recent changes, home's most active faces, issue and pull request authors, assignees, comments and the audit log drew a person from their username alone, so everyone got their letter avatar instead of the picture they uploaded. An avatar given no picture now draws the letter at once and lays the person's own picture over it once it loads, from /-/avatar/user/<username> (routes/avatar-user.ts): a redirect to the picture on the usercontent origin, or not found, read from identity's people only, so a workspace's icon can never stand in for someone. Kept five minutes, public like the profile. Agents, the product tour's people, "someone" and workspaces' square avatars are never looked up (image null), so an account with the same name never lends them its face. | 212 | <span className="font-mono text-fg wrap-anywhere">{data.noreply}</span> instead of your primary address. |
| Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running | 213 | </SwitchCard> |
| 214 | <SwitchCard name="block" defaultChecked={data.blockPrivatePushes} title="Block pushes that expose my email"> | |
| 215 | While your address is private, a push is refused if a commit in it has one of your confirmed | |
| 216 | addresses as its author or committer. Commit with your noreply address instead. | |
| 217 | </SwitchCard> | |
| 218 | <div className="flex flex-wrap items-center justify-between gap-3"> | |
| 219 | <p className="text-xs text-faint"> | |
| People wear their own avatars wherever g1t knows them only by name, and a workspace is never shown as a person. The activity feeds, recent changes, home's most active faces, issue and pull request authors, assignees, comments and the audit log drew a person from their username alone, so everyone got their letter avatar instead of the picture they uploaded. An avatar given no picture now draws the letter at once and lays the person's own picture over it once it loads, from /-/avatar/user/<username> (routes/avatar-user.ts): a redirect to the picture on the usercontent origin, or not found, read from identity's people only, so a workspace's icon can never stand in for someone. Kept five minutes, public like the profile. Agents, the product tour's people, "someone" and workspaces' square avatars are never looked up (image null), so an account with the same name never lends them its face. | 220 | Commits g1t makes for you now carry <span className="font-mono wrap-anywhere">{data.commitEmail}</span>. |
| Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running | 221 | </p> |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 222 | <SubmitButton variant="outline" pending="Saving…" match={{ intent: "email-privacy" }}> |
| Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running | 223 | Save |
| Merge branch 'worktree-agent-a985c01f43538af01' | 224 | </SubmitButton> |
| Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running | 225 | </div> |
| 226 | </Form> | |
| Merge branch 'worktree-agent-a985c01f43538af01' | 227 | <Answer actionData={answerFor("email-privacy")} /> |
| Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running | 228 | </div> |
| 229 | </section> | |
| 230 | ); | |
| 231 | } | |
| 232 | ||
| 233 | /** What happened to the account's security, newest first. */ | |
| 234 | export function SecurityLogSection({ log }: { log: SecurityEvent[] }) { | |
| 235 | return ( | |
| 236 | <section id="security-log" className="scroll-mt-20"> | |
| 237 | {log.length === 0 ? ( | |
| 238 | <p className="text-sm text-faint">Nothing yet.</p> | |
| 239 | ) : ( | |
| 240 | <ul className="divide-y divide-line rounded-md border border-line"> | |
| 241 | {log.map((event, at) => ( | |
| 242 | <li key={`${event.createdAt}:${at}`} className="flex flex-col gap-1 px-4 py-3 sm:flex-row sm:items-center sm:gap-4"> | |
| 243 | <p className="min-w-0 grow truncate text-sm"> | |
| 244 | {securityEventLabel(event)} | |
| 245 | {event.byStaff && <span className="text-muted"> · by g1t staff{event.reason ? `: ${event.reason}` : ""}</span>} | |
| 246 | </p> | |
| 247 | <p className="shrink-0 text-xs text-faint"> | |
| 248 | <TimeAgo at={event.createdAt} /> | |
| 249 | </p> | |
| 250 | </li> | |
| 251 | ))} | |
| 252 | </ul> | |
| 253 | )} | |
| 254 | </section> | |
| 255 | ); | |
| 256 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.