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.

g1t.sh
863 linesCodeBlameRaw

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.

Merge branch 'worktree-agent-a1b995daa94e4e1b7'1/**
2 * Security, as a project's page shows it: open alerts by severity, the
3 * secrets found in pushes and history, vulnerable dependencies with the
4 * security update g1t opened for each, and what happened to every alert.
5 * The page posts the intents in `routes/repo/security.tsx`'s action.
6 */
7import {
8 Bot,
9 CircleAlert,
10 CircleCheck,
11 CircleDot,
12 CircleSlash,
13 ExternalLink,
14 GitBranch,
15 GitMerge,
16 GitPullRequest,
17 GitPullRequestClosed,
18 History,
19 KeyRound,
20 Loader,
21 Package,
22 ShieldAlert,
23 ShieldCheck,
24} from "lucide-react";
25import { type ReactNode, useEffect, useRef, useState } from "react";
26import { Link, useFetcher } from "react-router";
27
28import {
29 type AlertActivity,
30 type AlertState,
31 DEPENDENCY_DISMISS_REASONS,
32 type DismissReason,
33 SECRET_DISMISS_REASONS,
34 SEVERITIES,
35 type PullStatus,
36 type SecretFinding,
37 type SecurityUpdate,
38 type Severity,
39 type SeverityCounts,
40 type Vulnerability,
41 dismissLabel,
42} from "@g1t/contracts";
43
44import {
45 type ActivityEntry,
46 type PackageGroup,
47 UPDATE_STATES,
48 alertActivity,
49 groupByPackage,
50 highestFix,
51 latestUpdate,
52 splitSecrets,
53 worstSeverity,
54} from "../lib/security-alerts";
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.55import { TimeAgo } from "./ui";
56import { Avatar } from "./ui/avatar";
Merge branch 'worktree-agent-a1b995daa94e4e1b7'57import { Badge, type BadgeTone } from "./ui/badge";
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.58import { Button } from "./ui/button";
59import { Card } from "./ui/card";
Merge branch 'worktree-agent-ae1299e92e4462012'60import { Hint } from "./ui/hint";
Merge branch 'worktree-agent-a1b995daa94e4e1b7'61import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "./ui/dialog";
62import { RadioGroup, RadioOption } from "./ui/radio-group";
63
64type Done = { ok: boolean; error?: string } | undefined;
65
66const SEVERITY: Record<Severity, { label: string; tone: BadgeTone }> = {
67 critical: { label: "Critical", tone: "danger" },
68 high: { label: "High", tone: "warn" },
69 medium: { label: "Medium", tone: "merged" },
70 low: { label: "Low", tone: "info" },
71 unknown: { label: "Unrated", tone: "neutral" },
72};
73
74
75export function SeverityBadge({ severity }: { severity: Severity }) {
76 return <Badge tone={SEVERITY[severity].tone}>{SEVERITY[severity].label}</Badge>;
77}
78
79/** Open alerts by severity, one tile each. */
80export function SeverityCountsGrid({ counts }: { counts: SeverityCounts }) {
81 return (
82 <div className="grid grid-cols-2 gap-3 sm:grid-cols-5">
83 {SEVERITIES.map((severity) => (
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.84 <Card key={severity} className="px-4 py-3">
Merge branch 'worktree-agent-a1b995daa94e4e1b7'85 <p className="text-xs text-muted">{SEVERITY[severity].label}</p>
86 <p className={`mt-1 text-2xl font-semibold tabular-nums ${counts[severity] > 0 && severity === "critical" ? "text-danger" : ""}`}>
87 {counts[severity]}
88 </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.89 </Card>
Merge branch 'worktree-agent-a1b995daa94e4e1b7'90 ))}
91 </div>
92 );
93}
94
95/** A compact row of severity counts, for a list of projects. */
96export function SeverityCountsInline({ counts }: { counts: SeverityCounts }) {
97 const shown = SEVERITIES.filter((severity) => counts[severity] > 0);
98 if (shown.length === 0) {
99 return (
Merge branch 'worktree-agent-ad7c6d88d93adc817'100 <span className="inline-flex items-center gap-1 text-xs text-success">
Merge branch 'worktree-agent-a1b995daa94e4e1b7'101 <ShieldCheck size={13} />
102 Nothing open
103 </span>
104 );
105 }
106 return (
107 <span className="flex flex-wrap gap-1.5">
108 {shown.map((severity) => (
109 <Badge key={severity} tone={SEVERITY[severity].tone}>
110 {counts[severity]} {SEVERITY[severity].label.toLowerCase()}
111 </Badge>
112 ))}
113 </span>
114 );
115}
116
117const STATE_FILTERS: { state: AlertState; label: string; icon: ReactNode }[] = [
118 { state: "open", label: "Open", icon: <CircleDot size={14} /> },
119 { state: "dismissed", label: "Dismissed", icon: <CircleSlash size={14} /> },
120 { state: "fixed", label: "Fixed", icon: <CircleCheck size={14} /> },
121];
122
123/** Open, Dismissed and Fixed, with how many alerts each holds. */
124export function StateFilter({
125 counts,
126 value,
127 onChange,
128}: {
129 counts: Record<AlertState, number>;
130 value: AlertState;
131 onChange: (state: AlertState) => void;
132}) {
133 return (
134 <div role="group" aria-label="Filter alerts" className="flex flex-wrap items-center gap-x-4 gap-y-1 text-sm">
135 {STATE_FILTERS.map(({ state, label, icon }) => (
136 <button
137 key={state}
138 type="button"
139 aria-pressed={value === state}
140 onClick={() => onChange(state)}
141 className={`inline-flex items-center gap-1.5 transition-colors ${
142 value === state ? "font-medium text-fg" : "text-muted hover:text-fg"
143 }`}
144 >
145 {icon}
146 {counts[state]} {label}
147 </button>
148 ))}
149 </div>
150 );
151}
152
153const TEXTAREA =
154 "w-full rounded-md border border-line bg-bg px-3 py-2 text-sm outline-none transition-colors placeholder:text-faint hover:border-line-strong focus:border-accent-dim";
155
156const SMALL_BUTTON =
157 "rounded-md border border-line px-2.5 py-1 text-xs font-medium text-muted transition-colors hover:border-line-strong hover:text-fg disabled:opacity-50";
158
159/** Dismiss an alert with one of `reasons` and an optional comment. */
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar160export function DismissDialog({
Merge branch 'worktree-agent-a1b995daa94e4e1b7'161 id,
162 title,
163 detail,
164 reasons,
165 note,
166 action,
167 defaultReason,
168 trigger = "Dismiss",
169}: {
170 id: string;
171 title: string;
172 /** What the alert is, in a line of code type. */
173 detail: string;
174 reasons: { reason: DismissReason; label: string; about: string }[];
175 note?: string;
176 action: string;
177 defaultReason?: DismissReason;
178 trigger?: string;
179}) {
180 const fetcher = useFetcher<Done>();
181 const [open, setOpen] = useState(false);
182 const [reason, setReason] = useState<string>(defaultReason ?? "");
183 useEffect(() => {
184 if (fetcher.state === "idle" && fetcher.data?.ok) setOpen(false);
185 }, [fetcher.state, fetcher.data]);
186 return (
187 <Dialog
188 open={open}
189 onOpenChange={(next) => {
190 setOpen(next);
191 if (next) setReason(defaultReason ?? "");
192 }}
193 >
194 <DialogTrigger className={SMALL_BUTTON}>{trigger}</DialogTrigger>
195 <DialogContent>
196 <DialogHeader>
197 <DialogTitle>{title}</DialogTitle>
198 <DialogDescription>
199 Say why it can stay. The alert keeps your name, the reason and your comment, and anyone with access can reopen it.
200 </DialogDescription>
201 </DialogHeader>
202 <fetcher.Form method="post" action={action} className="space-y-4">
203 <input type="hidden" name="intent" value="dismiss" />
204 <input type="hidden" name="id" value={id} />
205 <p className="font-mono text-xs break-all text-muted">{detail}</p>
206 <fieldset>
207 <legend className="text-xs font-medium text-muted">Reason</legend>
208 <RadioGroup name="reason" value={reason} onValueChange={setReason} required className="mt-2 gap-3">
209 {reasons.map((option) => (
210 <RadioOption key={option.reason} value={option.reason} label={option.label} description={option.about} />
211 ))}
212 </RadioGroup>
213 </fieldset>
214 {note && <p className="rounded-md border border-warn/30 bg-warn/5 px-3 py-2 text-xs text-warn">{note}</p>}
215 <label className="block">
216 <span className="mb-1.5 block text-xs font-medium text-muted">Comment (optional)</span>
217 <textarea name="comment" rows={3} maxLength={500} placeholder="What someone reading this later should know." className={TEXTAREA} />
218 </label>
219 <div className="flex justify-end">
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.220 <Button
Merge branch 'worktree-agent-a1b995daa94e4e1b7'221 type="submit"
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 disabled={fetcher.state !== "idle" || !reason}>
Merge branch 'worktree-agent-a1b995daa94e4e1b7'223 {fetcher.state !== "idle" ? "Dismissing…" : "Dismiss alert"}
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.224 </Button>
Merge branch 'worktree-agent-a1b995daa94e4e1b7'225 </div>
226 {fetcher.data?.error && <p className="text-sm text-danger">{fetcher.data.error}</p>}
227 </fetcher.Form>
228 </DialogContent>
229 </Dialog>
230 );
231}
232
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar233export function ReopenButton({ id, action }: { id: string; action: string }) {
Merge branch 'worktree-agent-a1b995daa94e4e1b7'234 const fetcher = useFetcher<Done>();
235 return (
236 <span className="flex flex-col items-end gap-1">
237 <button
238 type="button"
239 disabled={fetcher.state !== "idle"}
240 onClick={() => fetcher.submit({ intent: "reopen", id }, { method: "post", action })}
241 className={SMALL_BUTTON}
242 >
243 {fetcher.state !== "idle" ? "Reopening…" : "Reopen"}
244 </button>
245 {fetcher.data?.error && <span className="text-xs text-danger">{fetcher.data.error}</span>}
246 </span>
247 );
248}
249
250/** What happened to an alert, oldest first, folded away until asked for. */
251function ActivityLog({ entries, base, open }: { entries: ActivityEntry[]; base: string; open?: boolean }) {
252 if (entries.length === 0) return null;
253 return (
254 <details className="group mt-2" open={open}>
255 <summary className="inline-flex cursor-pointer list-none items-center gap-1.5 text-xs text-muted hover:text-fg [&::-webkit-details-marker]:hidden">
256 <History size={12} />
257 <span className="group-open:hidden">Show activity ({entries.length})</span>
258 <span className="hidden group-open:inline">Hide activity</span>
259 </summary>
260 <ol className="mt-2 space-y-2 border-l border-line pl-3">
261 {entries.map((entry) => (
262 <li key={entry.key} className="text-xs">
263 <p className="flex flex-wrap items-center gap-x-1.5 gap-y-1 text-muted">
264 {entry.actor ? (
265 <>
266 <Avatar name={entry.actor} size={16} />
267 <span className="font-medium text-fg">{entry.actor}</span>
268 </>
269 ) : (
270 <CircleDot size={12} className="text-faint" />
271 )}
272 <span>{entry.text}</span>
273 {entry.ref && (
274 <Link
275 to={`${base}/${entry.ref.kind === "pull" ? "pull" : "issues"}/${entry.ref.number}`}
276 className="font-medium text-fg-soft hover:text-fg hover:underline"
277 >
278 #{entry.ref.number}
279 </Link>
280 )}
281 {entry.reason && <Badge>{dismissLabel(entry.reason)}</Badge>}
282 <span className="text-faint">
283 <TimeAgo at={entry.at} />
284 </span>
285 </p>
286 {entry.comment && <p className="mt-1 text-fg-soft wrap-anywhere">“{entry.comment}”</p>}
287 </li>
288 ))}
289 </ol>
290 </details>
291 );
292}
293
294function useFocus<T extends HTMLElement>(focused: boolean) {
295 const ref = useRef<T>(null);
296 useEffect(() => {
297 if (focused) ref.current?.scrollIntoView({ block: "center" });
298 }, [focused]);
299 return ref;
300}
301
302const FOCUSED = "bg-accent/5 ring-1 ring-accent/40 ring-inset";
303
304function secretBadge(finding: SecretFinding): { label: string; tone: BadgeTone; about: string } {
305 if (finding.state === "dismissed") {
306 return {
307 label: "Dismissed",
308 tone: "neutral",
309 about: finding.status === "allowed" ? "Pushes carrying it go through." : "Dismissed.",
310 };
311 }
Merge branch 'worktree-agent-ad7c6d88d93adc817'312 if (finding.state === "fixed") return { label: "Revoked", tone: "success", about: "Revoked or rotated." };
Merge branch 'worktree-agent-a1b995daa94e4e1b7'313 if (finding.status === "blocked") {
314 return { label: "Push blocked", tone: finding.testValue ? "neutral" : "warn", about: "A push carrying it was refused, so it never landed." };
315 }
316 return {
317 label: "In history",
318 tone: finding.testValue ? "neutral" : "danger",
319 about: "In the repository's history. Rotate it with whoever issued it, then dismiss it as revoked.",
320 };
321}
322
323function SecretItem({
324 finding,
325 activity,
326 base,
327 action,
328 focused,
329 canDismiss,
330}: {
331 finding: SecretFinding;
332 activity: AlertActivity[];
333 base: string;
334 action: string;
335 focused: boolean;
336 canDismiss: boolean;
337}) {
338 const ref = useFocus<HTMLLIElement>(focused);
339 const badge = secretBadge(finding);
340 const landed = finding.source === "history" || finding.status === "open";
341 const where = `${finding.path}:${finding.line}`;
342 return (
343 <li ref={ref} id={finding.id} className={`flex flex-col gap-3 px-4 py-3 sm:flex-row sm:items-start ${focused ? FOCUSED : ""}`}>
344 <KeyRound size={15} className="mt-0.5 hidden shrink-0 text-muted sm:block" />
345 <div className="min-w-0 grow">
346 <div className="flex flex-wrap items-center gap-2">
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar347 <Link to={`${base}/security/secret-scanning/${finding.id}`} className="text-sm font-medium first-letter:uppercase hover:underline">
348 {finding.label}
349 </Link>
Merge branch 'worktree-agent-ae1299e92e4462012'350 <Hint label={badge.about}>
351 <Badge tone={badge.tone}>{badge.label}</Badge>
352 </Hint>
353 {finding.testValue && (
354 <Hint label={finding.testValue}>
355 <Badge>Likely test value</Badge>
356 </Hint>
357 )}
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar358 {finding.validity === "active" && (
Merge branch 'worktree-agent-ae1299e92e4462012'359 <Hint label="Its issuer says it still works">
360 <Badge tone="danger">Active</Badge>
361 </Hint>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar362 )}
Merge branch 'worktree-agent-ae1299e92e4462012'363 {finding.validity === "inactive" && (
364 <Hint label="Its issuer refused it: revoked or expired">
365 <Badge>Inactive</Badge>
366 </Hint>
367 )}
368 {finding.bypass && (
369 <Hint label={`Bypassed by ${finding.bypass.by}`}>
370 <Badge tone="warn">Bypassed</Badge>
371 </Hint>
372 )}
Merge branch 'worktree-agent-a1b995daa94e4e1b7'373 {finding.state === "dismissed" && finding.dismissedReason && <Badge>{dismissLabel(finding.dismissedReason)}</Badge>}
374 </div>
375 <p className="mt-1 truncate font-mono text-xs">
376 {landed ? (
377 <Link to={`${base}/blob/${finding.commit}/${finding.path}#L${finding.line}`} className="text-fg-soft hover:text-fg hover:underline">
378 {where}
379 </Link>
380 ) : (
381 <span className="text-fg-soft">{where}</span>
382 )}
383 <span className="text-faint"> · {finding.preview}</span>
384 </p>
385 {finding.testValue && <p className="mt-1 text-xs text-muted">{finding.testValue}</p>}
386 <p className="mt-1.5 flex flex-wrap gap-x-3 gap-y-1 text-xs text-faint">
387 <span>
388 {finding.source === "push" ? "in a push" : "in history"}
389 {finding.foundBy && <> by {finding.foundBy}</>}, commit{" "}
390 {landed ? (
391 <Link to={`${base}/commit/${finding.commit}`} className="font-mono hover:text-fg">
392 {finding.commit.slice(0, 7)}
393 </Link>
394 ) : (
395 <span className="font-mono">{finding.commit.slice(0, 7)}</span>
396 )}
397 </span>
398 <span>
399 found <TimeAgo at={finding.foundAt} />
400 </span>
401 </p>
402 <ActivityLog entries={alertActivity(finding, activity)} base={base} open={focused && finding.state !== "open"} />
403 </div>
404 {canDismiss && (
405 <div className="flex shrink-0 items-center gap-1.5">
406 {finding.state === "open" ? (
407 <DismissDialog
408 id={finding.id}
409 title={`Dismiss ${finding.label}`}
410 detail={`${where} · ${finding.preview}`}
411 reasons={SECRET_DISMISS_REASONS}
412 note={
413 finding.status === "blocked" && !finding.testValue
414 ? "Dismissing it lets the same push through, unless you dismiss it as revoked."
415 : undefined
416 }
417 action={action}
418 />
419 ) : (
420 <ReopenButton id={finding.id} action={action} />
421 )}
422 </div>
423 )}
424 </li>
425 );
426}
427
428function Empty({ title, children }: { title: string; children: ReactNode }) {
429 return (
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.430 <Card tone="plain" className="border-dashed px-6 py-10 text-center">
Merge branch 'worktree-agent-ad7c6d88d93adc817'431 <ShieldCheck size={22} className="mx-auto text-success" />
Merge branch 'worktree-agent-a1b995daa94e4e1b7'432 <p className="mt-2 font-medium">{title}</p>
433 <p className="mt-1 text-sm text-muted">{children}</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.434 </Card>
Merge branch 'worktree-agent-a1b995daa94e4e1b7'435 );
436}
437
438const LIST = "divide-y divide-line overflow-hidden rounded-xl border border-line bg-surface";
439
440/** The secret alerts in one state: on Open, real ones first, then likely test values. */
441export function SecretsList({
442 secrets,
443 state,
444 activity,
445 base,
446 action,
447 focus,
448 canDismiss,
449}: {
450 /** Already filtered to `state`. */
451 secrets: SecretFinding[];
452 state: AlertState;
453 activity: AlertActivity[];
454 base: string;
455 action: string;
456 focus: string | null;
457 /** Whether the viewer may dismiss and reopen secret alerts (Admin). */
458 canDismiss: boolean;
459}) {
460 const item = (finding: SecretFinding) => (
461 <SecretItem
462 key={finding.id}
463 finding={finding}
464 activity={activity}
465 base={base}
466 action={action}
467 focused={finding.id === focus}
468 canDismiss={canDismiss}
469 />
470 );
471 if (secrets.length === 0) {
472 return state === "open" ? (
473 <Empty title="No open secret alerts">
474 Pushes that add a key or a token are refused before they land, and the history is scanned once in the background.
475 </Empty>
476 ) : (
477 <Empty title={state === "dismissed" ? "No dismissed secret alerts" : "No revoked secrets"}>
478 {state === "dismissed"
479 ? "Alerts someone dismissed as a false positive, a test value or accepted are listed here."
480 : "Secrets dismissed as revoked are listed here."}
481 </Empty>
482 );
483 }
484 if (state !== "open") return <ul className={LIST}>{secrets.map(item)}</ul>;
485 const { real, tests } = splitSecrets(secrets);
486 return (
487 <div className="space-y-5">
488 {real.length > 0 ? (
489 <ul className={LIST}>{real.map(item)}</ul>
490 ) : (
491 <Empty title="No secrets that look real">Only likely test values are open, and they never block a push.</Empty>
492 )}
493 {tests.length > 0 && (
494 <section>
495 <h4 className="text-sm font-medium">Likely test values</h4>
496 <p className="mt-0.5 mb-2 text-xs text-muted">
497 These look made for tests or documentation. They never block a push and are not counted as critical; dismiss them
498 to clear the list.
499 </p>
500 <ul className={LIST}>{tests.map(item)}</ul>
501 </section>
502 )}
503 </div>
504 );
505}
506
507/** Where the legacy upgrade issue for a package stands, as the page loads it. */
508export type UpgradeFix = {
509 number: number;
510 state: "open" | "closed";
511 /** The newest pull request for the issue, if any. */
512 pull: { number: number; status: PullStatus; agent: string | null } | null;
513 resolvedBy: number | null;
514};
515
516/** A security update's pull request as the page loads it. */
517export type PullInfo = { number: number; status: PullStatus; title: string };
518
519function FixLink({ issue, fix, base }: { issue: number; fix: UpgradeFix | undefined; base: string }) {
520 const pull = fix?.pull;
521 return (
522 <span className="flex flex-wrap items-center gap-x-2 gap-y-1 text-xs">
523 <span className="text-muted">Upgrade issue</span>
524 <Link to={`${base}/issues/${issue}`} className="inline-flex items-center gap-1 text-fg-soft hover:text-fg">
Merge branch 'worktree-agent-ad7c6d88d93adc817'525 {fix?.state === "closed" ? <CircleCheck size={12} className="text-merged" /> : <CircleDot size={12} className="text-success" />}#{issue}
Merge branch 'worktree-agent-a1b995daa94e4e1b7'526 </Link>
527 {pull && (
528 <Link to={`${base}/pull/${pull.number}`} className="inline-flex items-center gap-1 text-muted hover:text-fg">
529 {pull.agent ? <Bot size={12} /> : <GitPullRequest size={12} />}#{pull.number} {pull.status === "draft" ? "in progress" : pull.status}
530 </Link>
531 )}
532 </span>
533 );
534}
535
536const PULL_ICON: Record<PullStatus, ReactNode> = {
537 draft: <GitPullRequest size={13} className="text-muted" />,
Merge branch 'worktree-agent-ad7c6d88d93adc817'538 open: <GitPullRequest size={13} className="text-success" />,
Merge branch 'worktree-agent-a1b995daa94e4e1b7'539 merged: <GitMerge size={13} className="text-merged" />,
540 closed: <GitPullRequestClosed size={13} className="text-danger" />,
541};
542
Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them.543/** Where g1t's security update for a package stands. */
544function UpdateStatus({ update, name, pulls, base }: {
A desktop release goes out with what can be built. Linux is built on g1t's machines in every release; Windows and macOS each wait on a repository variable: a machine registered for them, or for Windows a cross build on Linux once aka.ms and download.visualstudio.microsoft.com are workflow-only domains, which is where the first release stopped. A platform with no machine is skipped and the download page says it is coming, with a word for the visitor whose platform this release lacks. The guide and CONTRIBUTING say so.545 update: SecurityUpdate;
546 name: string;
547 pulls: Record<number, PullInfo>;
548 base: string;
549}) {
Merge branch 'worktree-agent-a1b995daa94e4e1b7'550 const meta = UPDATE_STATES[update.state];
551 const pull = update.pull != null ? pulls[update.pull] : undefined;
552 const about: Record<SecurityUpdate["state"], ReactNode> = {
553 requested: <>A sandbox is raising {name} to {update.target}.</>,
554 open: <>It raises {name} to {update.target} and lands through your branch's required checks.</>,
555 merged: <>{name} was raised to {update.target}.</>,
556 closed: <>The pull request was closed without merging.</>,
557 superseded: <>A newer update replaced it, or the package is no longer vulnerable, so g1t closed it.</>,
558 needs_code: (
559 <>
560 Raising {name} to {update.target} needs code changes, so g1t opened an issue to make them
561 {update.issue != null && (
562 <>
563 :{" "}
564 <Link to={`${base}/issues/${update.issue}`} className="font-medium text-fg-soft hover:text-fg hover:underline">
565 #{update.issue}
566 </Link>
567 </>
568 )}
569 .
570 </>
571 ),
572 failed: <>{update.error ?? "g1t could not make the change."}</>,
573 };
574 return (
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.575 <Card tone="plain" radius="lg" className="mt-2.5 bg-bg/40 px-3 py-2 text-xs sm:ml-7">
Merge branch 'worktree-agent-a1b995daa94e4e1b7'576 <div className="flex flex-wrap items-center gap-x-2.5 gap-y-1.5">
577 {update.state === "requested" ? (
578 <Loader size={13} className="animate-spin text-info motion-reduce:animate-none" />
579 ) : update.state === "failed" || update.state === "needs_code" ? (
580 <CircleAlert size={13} className={update.state === "failed" ? "text-danger" : "text-warn"} />
581 ) : (
582 PULL_ICON[pull?.status ?? (update.state === "merged" ? "merged" : update.state === "open" ? "open" : "closed")]
583 )}
584 <Badge tone={meta.tone}>{meta.label}</Badge>
585 {update.pull != null && (
586 <Link to={`${base}/pull/${update.pull}`} className="font-medium text-fg-soft hover:text-fg hover:underline">
587 #{update.pull}
588 {pull && <span className="font-normal text-muted"> {pull.status === "draft" ? "draft" : pull.status}</span>}
589 </Link>
590 )}
591 {update.branch && (
592 <span className="inline-flex min-w-0 items-center gap-1 font-mono text-muted">
593 <GitBranch size={12} className="shrink-0" />
594 <span className="truncate">{update.branch}</span>
595 </span>
596 )}
597 <span className="text-faint">
598 <TimeAgo at={update.updatedAt} />
599 </span>
600 </div>
601 <p className={`mt-1 ${update.state === "failed" ? "text-danger" : "text-muted"} wrap-anywhere`}>{about[update.state]}</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.602 </Card>
Merge branch 'worktree-agent-a1b995daa94e4e1b7'603 );
604}
605
606export function VulnerabilityList({
607 vulnerabilities,
608 state,
609 activity,
610 fixes,
611 pulls,
612 upkeep,
613 base,
614 action,
615 focus,
616 canDismiss,
617}: {
618 /** Already filtered to `state`. */
619 vulnerabilities: Vulnerability[];
620 state: AlertState;
621 activity: AlertActivity[];
622 fixes: Record<number, UpgradeFix>;
623 pulls: Record<number, PullInfo>;
624 /** Whether security updates are on. */
625 upkeep: boolean;
626 base: string;
627 action: string;
628 focus: string | null;
629 /** Whether the viewer may dismiss and reopen dependency alerts (Write). */
630 canDismiss: boolean;
631}) {
632 const packages = groupByPackage(vulnerabilities);
633 if (packages.length === 0) {
634 return state === "open" ? (
635 <Empty title="No known vulnerabilities">
636 Every package the lockfiles resolve is checked against the OSV database on each push to the default branch, and daily.
637 </Empty>
638 ) : (
639 <Empty title={state === "dismissed" ? "No dismissed dependency alerts" : "Nothing fixed yet"}>
640 {state === "dismissed"
641 ? "Alerts someone dismissed, with their reason, are listed here."
642 : "Alerts whose package was upgraded, or is no longer vulnerable, are listed here."}
643 </Empty>
644 );
645 }
646 return (
647 <ul className={LIST}>
648 {packages.map((group) => (
649 <PackageItem
650 key={group.key}
651 group={group}
652 activity={activity}
653 fixes={fixes}
654 pulls={pulls}
655 upkeep={upkeep}
656 base={base}
657 action={action}
658 focus={focus}
659 canDismiss={canDismiss}
660 />
661 ))}
662 </ul>
663 );
664}
665
666function PackageItem({
667 group,
668 activity,
669 fixes,
670 pulls,
671 upkeep,
672 base,
673 action,
674 focus,
675 canDismiss,
676}: {
677 group: PackageGroup;
678 activity: AlertActivity[];
679 fixes: Record<number, UpgradeFix>;
680 pulls: Record<number, PullInfo>;
681 upkeep: boolean;
682 base: string;
683 action: string;
684 focus: string | null;
685 canDismiss: boolean;
686}) {
687 const versions = [...new Set(group.vulns.map((vuln) => vuln.version))];
688 const manifests = [...new Set(group.vulns.map((vuln) => vuln.manifest))];
689 const target = highestFix(group.vulns);
690 const update = latestUpdate(group.vulns);
691 const issue = update ? null : (group.vulns.find((vuln) => vuln.issue != null)?.issue ?? null);
692 return (
693 <li className="px-4 py-3">
694 <div className="flex items-start gap-3">
695 <Package size={15} className="mt-0.5 hidden shrink-0 text-muted sm:block" />
696 <div className="min-w-0 grow">
697 <div className="flex flex-wrap items-center gap-2">
698 <span className="font-mono text-sm font-medium break-all">{group.name}</span>
699 <span className="font-mono text-xs text-muted">{versions.join(", ")}</span>
700 <Badge>{group.ecosystem}</Badge>
701 <SeverityBadge severity={worstSeverity(group.vulns)} />
702 </div>
703 <p className="mt-1 text-xs text-faint wrap-anywhere">
704 {target ? <>Fixed in {target}</> : "No patched version"} · locked in{" "}
705 <span className="font-mono">{manifests.join(", ")}</span>
706 </p>
707 </div>
708 </div>
Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them.709 {update && <UpdateStatus update={update} name={group.name} pulls={pulls} base={base} />}
Merge branch 'worktree-agent-a1b995daa94e4e1b7'710 {issue != null && (
711 <div className="mt-2 sm:ml-7">
712 <FixLink issue={issue} fix={fixes[issue]} base={base} />
713 </div>
714 )}
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.715 <Card asChild tone="plain" radius="lg" className="mt-2.5 space-y-px overflow-hidden sm:ml-7">
716 <ul>
717 {group.vulns.map((vuln) => (
718 <AdvisoryItem
719 key={vuln.id}
720 vuln={vuln}
721 showManifest={manifests.length > 1}
722 activity={activity}
723 upkeep={upkeep}
724 base={base}
725 action={action}
726 focused={vuln.id === focus}
727 canDismiss={canDismiss}
728 />
729 ))}
730 </ul>
731 </Card>
Merge branch 'worktree-agent-a1b995daa94e4e1b7'732 </li>
733 );
734}
735
736function AdvisoryItem({
737 vuln,
738 showManifest,
739 activity,
740 upkeep,
741 base,
742 action,
743 focused,
744 canDismiss,
745}: {
746 vuln: Vulnerability;
747 showManifest: boolean;
748 activity: AlertActivity[];
749 upkeep: boolean;
750 base: string;
751 action: string;
752 focused: boolean;
753 canDismiss: boolean;
754}) {
755 const ref = useFocus<HTMLLIElement>(focused);
756 const osv = `https://osv.dev/vulnerability/${vuln.osvId}`;
757 const dismiss = (reason?: DismissReason, trigger?: string) => (
758 <DismissDialog
759 id={vuln.id}
760 title={`Dismiss ${vuln.advisory}`}
761 detail={`${vuln.package} ${vuln.version} · ${vuln.manifest}`}
762 reasons={DEPENDENCY_DISMISS_REASONS}
763 action={action}
764 defaultReason={reason}
765 trigger={trigger}
766 />
767 );
768 return (
769 <li ref={ref} id={vuln.id} className={`bg-bg/30 px-3 py-2.5 not-first:border-t not-first:border-line ${focused ? FOCUSED : ""}`}>
770 <div className="flex flex-col gap-2 sm:flex-row sm:items-start">
771 <div className="min-w-0 grow text-xs">
772 <p className="flex flex-wrap items-center gap-x-2 gap-y-1">
773 <a href={osv} target="_blank" rel="noreferrer" className="inline-flex items-center gap-1 font-mono text-fg-soft hover:text-fg">
774 {vuln.advisory}
775 <ExternalLink size={10} />
776 </a>
777 <SeverityBadge severity={vuln.severity} />
778 {vuln.fixedVersion && <span className="text-faint">fixed in {vuln.fixedVersion}</span>}
779 {showManifest && <span className="font-mono text-faint">{vuln.manifest}</span>}
780 {vuln.state === "dismissed" && vuln.dismissedReason && <Badge>{dismissLabel(vuln.dismissedReason)}</Badge>}
781 </p>
782 <p className="mt-1 text-muted wrap-anywhere">{vuln.summary}</p>
783 </div>
784 {canDismiss && vuln.state !== "fixed" && (
785 <div className="flex shrink-0 items-center gap-1.5">
786 {vuln.state === "open" ? dismiss() : <ReopenButton id={vuln.id} action={action} />}
787 </div>
788 )}
789 </div>
790 {vuln.state === "open" && !vuln.fixedVersion && (
791 <div className="mt-2 rounded-md border border-warn/30 bg-warn/5 px-3 py-2 text-xs">
792 <p className="flex items-center gap-1.5 font-medium text-warn">
793 <ShieldAlert size={13} />
794 No patched version available
795 </p>
796 <p className="mt-1 text-muted">
797 No release fixes this yet. Dependencies are checked again daily
Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them.798 {upkeep
799 ? ", and g1t opens a pull request when a fix is published."
800 : "; turn on security updates and g1t opens a pull request when a fix is published."}
Merge branch 'worktree-agent-a1b995daa94e4e1b7'801 </p>
802 <div className="mt-2 flex flex-wrap items-center gap-2">
803 <a href={osv} target="_blank" rel="noreferrer" className="inline-flex items-center gap-1 text-fg-soft hover:text-fg hover:underline">
804 Read the advisory
805 <ExternalLink size={10} />
806 </a>
807 {canDismiss && dismiss("tolerable_risk", "Dismiss as tolerable risk")}
808 </div>
809 </div>
810 )}
811 <ActivityLog entries={alertActivity(vuln, activity)} base={base} open={focused && vuln.state !== "open"} />
812 </li>
813 );
814}
815
816export function ScanSummary({
817 scan,
818}: {
819 scan: { history: string; commitsScanned: number; historyFinishedAt: string | null; dependenciesScannedAt: string | null; dependenciesError: string | null; lockfiles: string[] };
820}) {
821 const history =
822 scan.history === "done" ? (
823 <>
824 History scanned: {scan.commitsScanned.toLocaleString()} commits
825 {scan.historyFinishedAt && (
826 <>
827 , <TimeAgo at={scan.historyFinishedAt} />
828 </>
829 )}
830 </>
831 ) : scan.history === "stopped" ? (
832 "History scan paused: the workspace reached its spending limit"
833 ) : scan.history === "running" ? (
834 `Scanning history: ${scan.commitsScanned.toLocaleString()} commits so far`
835 ) : (
836 "History scan queued"
837 );
838 return (
839 <div className="flex flex-wrap gap-x-5 gap-y-1 text-xs text-muted">
840 <span className="inline-flex items-center gap-1.5">
841 <KeyRound size={12} />
842 {history}
843 </span>
844 <span className="inline-flex items-center gap-1.5">
845 <Package size={12} />
846 {scan.dependenciesScannedAt ? (
847 <>
848 Dependencies read <TimeAgo at={scan.dependenciesScannedAt} />
849 {scan.lockfiles.length > 0 ? <> from {scan.lockfiles.join(", ")}</> : " (no lockfiles found)"}
850 </>
851 ) : (
852 "Dependencies not read yet"
853 )}
854 </span>
855 {scan.dependenciesError && (
856 <span className="inline-flex items-center gap-1.5 text-warn">
857 <ShieldAlert size={12} />
858 {scan.dependenciesError}
859 </span>
860 )}
861 </div>
862 );
863}

This file's history is long; its oldest lines are credited to the oldest commit read.