Skip to content
647 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.

Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1/**
2 * The security suite's pieces, shared by a project's Security sections and
3 * the workspace's: the activation prompt, filters, code scanning alerts,
4 * the trend chart and coverage table, the custom pattern editor, and the
5 * bypass and "Fix with g1t" forms. Each form posts an `intent` to the page
6 * it is on.
7 */
8import { Bot, CircleCheck, CircleDot, CircleSlash, FileCode2, Lock, ShieldCheck, Sparkles } from "lucide-react";
9import { type ReactNode, useEffect, useState } from "react";
10import { Link, useFetcher } from "react-router";
11
12import {
13 type AlertState,
14 BYPASS_REASONS,
15 type CodeAlert,
16 type DryRun,
17 type RepoCoverage,
18 type SavedPattern,
19 type Severity,
20 type SeverityCounts,
21 type TrendPoint,
22} from "@g1t/contracts";
23
24import { total, trendMax } from "../lib/security-suite";
25import { SeverityBadge } from "./security";
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.26import { ButtonLink, TimeAgo } from "./ui";
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar27import { Badge } 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.28import { Button } from "./ui/button";
29import { Card } from "./ui/card";
Merge branch 'worktree-agent-ae1299e92e4462012'30import { Hint } from "./ui/hint";
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar31import { RadioGroup, RadioOption } from "./ui/radio-group";
32import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
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.33import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "./ui/table";
Money is written one way. A single formatter turns millionths of a dollar into dollars, rounding half up on whole micros rather than on a float, so the same sum reads the same on every page: under a cent reads <$0.01 on a total and exactly nothing is $0.00, while the statement's lines, a session's receipt, the price book and an agent's effort costs carry up to four places where the fraction of a cent is the point; the six formatters that each rounded their own way are gone. This month is the calendar month in UTC from its first day to today everywhere, and billing counts the month's not-yet-closed usage in any range that reaches into the current month, so the top bar's pill, Spend, Home and Usage ask for the same days and get the same figure; Home now reads the usage report the others read instead of adding up statements. Usage's pending sentence says what of that usage the close will charge after the discount and included usage, which the billing API returns as pending_charged_micros. A reconciliation test holds the pill, Spend, Home and Usage to one number for one month. The usage and billing guide says how amounts are written and what this month means.34import { wholeDollars } from "../lib/money";
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar35
36type Done = { ok: boolean; error?: string } | undefined;
37
38export const CARD = "rounded-xl border border-line bg-surface";
39export const LIST = "divide-y divide-line overflow-hidden rounded-xl border border-line bg-surface";
40const INPUT =
41 "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";
42const SMALL_BUTTON =
43 "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";
44/** A section's title, what it is for, and what can be done there. */
45export function SectionHeader({ title, about, actions }: { title: string; about: ReactNode; actions?: ReactNode }) {
46 return (
47 <div className="flex flex-wrap items-start justify-between gap-4">
48 <div className="min-w-0 max-w-2xl">
49 <h2 className="text-xl font-semibold tracking-tight">{title}</h2>
50 <p className="mt-1.5 text-sm text-muted">{about}</p>
51 </div>
52 {actions && <div className="flex shrink-0 flex-wrap items-center gap-2">{actions}</div>}
53 </div>
54 );
55}
56
57/**
Pricing says it plainly: models at the provider's price, the agent rate for what g1t runs around every model call (the gateway, secrets, routing, context and pass-through to your own provider, so your own keys too), everything else at cost plus 20%, your own runners free, no seats; Security and quality comes with the plan with no separate fee, and live activations end. Each agent has an effort setting, Auto to Max, with what a typical task has cost at each level, and Spend's Spend less, keep quality suggests a lower level only when the agent's own past work shows quality held, to apply or dismiss. The pricing, spend and agents guides say how.58 * What a private repository's paid feature needs: the g1t plan, which
59 * Security and quality comes with at no price of its own, and who can
60 * start it. No pressure: what stays free is said too.
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar61 */
62export function ActivationPrompt({
63 workspace,
64 feature,
65 monthlyCents,
66 isOwner,
67}: {
68 workspace: string;
69 feature: string;
Pricing says it plainly: models at the provider's price, the agent rate for what g1t runs around every model call (the gateway, secrets, routing, context and pass-through to your own provider, so your own keys too), everything else at cost plus 20%, your own runners free, no seats; Security and quality comes with the plan with no separate fee, and live activations end. Each agent has an effort setting, Auto to Max, with what a typical task has cost at each level, and Spend's Spend less, keep quality suggests a lower level only when the agent's own past work shows quality held, to apply or dismiss. The pricing, spend and agents guides say how.70 /** The g1t plan's monthly price, from billing; null when it could not be read. */
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar71 monthlyCents: number | null;
72 isOwner: boolean;
73}) {
Money is written one way. A single formatter turns millionths of a dollar into dollars, rounding half up on whole micros rather than on a float, so the same sum reads the same on every page: under a cent reads <$0.01 on a total and exactly nothing is $0.00, while the statement's lines, a session's receipt, the price book and an agent's effort costs carry up to four places where the fraction of a cent is the point; the six formatters that each rounded their own way are gone. This month is the calendar month in UTC from its first day to today everywhere, and billing counts the month's not-yet-closed usage in any range that reaches into the current month, so the top bar's pill, Spend, Home and Usage ask for the same days and get the same figure; Home now reads the usage report the others read instead of adding up statements. Usage's pending sentence says what of that usage the close will charge after the discount and included usage, which the billing API returns as pending_charged_micros. A reconciliation test holds the pill, Spend, Home and Usage to one number for one month. The usage and billing guide says how amounts are written and what this month means.74 const price = monthlyCents == null ? null : `${wholeDollars(monthlyCents * 10_000)} a month`;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar75 return (
76 <div className={`${CARD} flex flex-col gap-4 p-5 sm:flex-row sm:items-start`}>
77 <Lock size={18} className="mt-0.5 shrink-0 text-accent" />
78 <div className="min-w-0 grow">
Pricing says it plainly: models at the provider's price, the agent rate for what g1t runs around every model call (the gateway, secrets, routing, context and pass-through to your own provider, so your own keys too), everything else at cost plus 20%, your own runners free, no seats; Security and quality comes with the plan with no separate fee, and live activations end. Each agent has an effort setting, Auto to Max, with what a typical task has cost at each level, and Spend's Spend less, keep quality suggests a lower level only when the agent's own past work shows quality held, to apply or dismiss. The pricing, spend and agents guides say how.79 <p className="font-medium">{feature} on private repositories comes with the g1t plan</p>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar80 <p className="mt-1.5 text-sm text-muted">
Pricing says it plainly: models at the provider's price, the agent rate for what g1t runs around every model call (the gateway, secrets, routing, context and pass-through to your own provider, so your own keys too), everything else at cost plus 20%, your own runners free, no seats; Security and quality comes with the plan with no separate fee, and live activations end. Each agent has an effort setting, Auto to Max, with what a typical task has cost at each level, and Spend's Spend less, keep quality suggests a lower level only when the agent's own past work shows quality held, to apply or dismiss. The pricing, spend and agents guides say how.81 Security and quality has no price of its own. With the plan{price ? ` (${price} for the workspace)` : ""}, custom patterns,
82 validity checks, delegated bypass, code scanning, dependency review and the security overview are on for every private
83 repository in {workspace}, and their scans are charged at cost plus 20%, like everything g1t runs. Fixes by g1t's agent
84 are charged as agent usage. Public repositories have all of it free, and secret scanning, push protection, vulnerability
85 alerts and security updates stay free everywhere.
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar86 </p>
87 <div className="mt-3 flex flex-wrap items-center gap-3 text-sm">
88 {isOwner ? (
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 <ButtonLink to={`/${workspace}/-/billing`}>
Pricing says it plainly: models at the provider's price, the agent rate for what g1t runs around every model call (the gateway, secrets, routing, context and pass-through to your own provider, so your own keys too), everything else at cost plus 20%, your own runners free, no seats; Security and quality comes with the plan with no separate fee, and live activations end. Each agent has an effort setting, Auto to Max, with what a typical task has cost at each level, and Spend's Spend less, keep quality suggests a lower level only when the agent's own past work shows quality held, to apply or dismiss. The pricing, spend and agents guides say how.90 Start the plan
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.91 </ButtonLink>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar92 ) : (
Pricing says it plainly: models at the provider's price, the agent rate for what g1t runs around every model call (the gateway, secrets, routing, context and pass-through to your own provider, so your own keys too), everything else at cost plus 20%, your own runners free, no seats; Security and quality comes with the plan with no separate fee, and live activations end. Each agent has an effort setting, Auto to Max, with what a typical task has cost at each level, and Spend's Spend less, keep quality suggests a lower level only when the agent's own past work shows quality held, to apply or dismiss. The pricing, spend and agents guides say how.93 <span className="text-muted">An owner of {workspace} can start the plan in Billing.</span>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar94 )}
95 <a href="https://docs.g1t.sh/guides/security/pricing/" className="text-muted underline underline-offset-2 hover:text-fg">
96 What's free and what's paid
97 </a>
98 </div>
99 </div>
100 </div>
101 );
102}
103
104/** A labelled select that changes a filter in the address. */
105export function FilterSelect({
106 label,
107 value,
108 options,
109 onChange,
110}: {
111 label: string;
112 value: string;
113 options: [string, string][];
114 onChange: (value: string) => void;
115}) {
116 return (
117 <label className="flex min-w-0 flex-col gap-1 text-xs text-muted">
118 {label}
119 <Select value={value} onValueChange={onChange}>
120 <SelectTrigger size="sm" className="min-w-36" aria-label={label}>
121 <SelectValue />
122 </SelectTrigger>
123 <SelectContent>
124 {options.map(([option, text]) => (
125 <SelectItem key={option} value={option}>
126 {text}
127 </SelectItem>
128 ))}
129 </SelectContent>
130 </Select>
131 </label>
132 );
133}
134
135const STATE_ICON: Record<AlertState, ReactNode> = {
136 open: <CircleDot size={14} />,
137 dismissed: <CircleSlash size={14} />,
138 fixed: <CircleCheck size={14} />,
139};
140
141/** A code scanning alert in a list. */
142export function CodeAlertItem({ alert, base }: { alert: CodeAlert; base: string }) {
143 const where = alert.path ? `${alert.path}${alert.startLine ? `:${alert.startLine}` : ""}` : null;
144 return (
145 <li className="flex flex-col gap-2 px-4 py-3 sm:flex-row sm:items-start">
146 <span className={`mt-0.5 hidden shrink-0 sm:block ${alert.state === "open" ? "text-warn" : "text-muted"}`}>{STATE_ICON[alert.state]}</span>
147 <div className="min-w-0 grow">
148 <div className="flex flex-wrap items-center gap-2">
149 <Link to={`${base}/security/code-scanning/${alert.number}`} className="min-w-0 text-sm font-medium break-words hover:underline">
150 {alert.ruleName && alert.ruleName !== alert.ruleId ? alert.ruleName : shortRule(alert.ruleId)}
151 </Link>
152 <SeverityBadge severity={alert.severity} />
153 {alert.issue != null && (
154 <Link to={`${base}/issues/${alert.issue}`}>
155 <Badge tone="accent">
156 <Bot size={11} /> g1t on #{alert.issue}
157 </Badge>
158 </Link>
159 )}
160 </div>
161 <p className="mt-1 line-clamp-2 text-xs text-muted">{alert.message}</p>
162 <p className="mt-1.5 flex flex-wrap gap-x-3 gap-y-1 text-xs text-faint">
163 <span>#{alert.number}</span>
164 <span>{alert.tool}</span>
165 {where && <span className="truncate font-mono">{where}</span>}
166 <span>
167 {alert.state === "fixed" && alert.fixedAt ? (
168 <>
169 fixed <TimeAgo at={alert.fixedAt} />
170 </>
171 ) : (
172 <>
173 found <TimeAgo at={alert.createdAt} />
174 </>
175 )}
176 </span>
177 </p>
178 </div>
179 </li>
180 );
181}
182
183/** The last part of a dotted rule id: Semgrep's are long. */
184export function shortRule(ruleId: string): string {
185 const parts = ruleId.split(".");
186 return parts.length > 2 ? parts[parts.length - 1] : ruleId;
187}
188
189/** Open counts as compact words: "3 critical · 1 high". */
190export function countsLine(counts: SeverityCounts): string {
191 const parts = (["critical", "high", "medium", "low"] as Severity[]).filter((s) => counts[s] > 0).map((s) => `${counts[s]} ${s}`);
192 return parts.length ? parts.join(" · ") : "none open";
193}
194
195// The trend's three series, each its own hue, validated for the dark
196// surface (dataviz: categorical slots 1–3, dark steps).
197const SERIES = [
198 { key: "secretScanning", label: "Secrets", color: "#3987e5" },
199 { key: "codeScanning", label: "Code scanning", color: "#d95926" },
200 { key: "vulnerability", label: "Vulnerabilities", color: "#199e70" },
201] as const;
202
203/** Open alerts by type each day, as stacked bars with a legend and a table. */
204export function TrendChart({ points }: { points: TrendPoint[] }) {
205 const [hover, setHover] = useState<number | null>(null);
206 const max = trendMax(points);
207 const width = 640;
208 const height = 140;
209 const gap = 2;
210 const bar = Math.max(2, width / Math.max(points.length, 1) - gap);
211 const shown = hover != null ? points[hover] : points[points.length - 1];
212 return (
213 <figure className={`${CARD} p-4`}>
214 <figcaption className="flex flex-wrap items-baseline justify-between gap-2">
215 <span className="text-sm font-medium">Open alerts, by day</span>
216 <span className="text-xs text-muted" aria-live="polite">
217 {shown
218 ? `${shown.day}: ${SERIES.map((s) => `${shown[s.key]} ${s.label.toLowerCase()}`).join(", ")}`
219 : "No days yet"}
220 </span>
221 </figcaption>
222 <ul className="mt-2 flex flex-wrap gap-x-4 gap-y-1 text-xs text-muted" aria-label="Legend">
223 {SERIES.map((series) => (
224 <li key={series.key} className="flex items-center gap-1.5">
225 <span aria-hidden="true" className="size-2.5 rounded-sm" style={{ background: series.color }} />
226 {series.label}
227 </li>
228 ))}
229 </ul>
230 <svg
231 viewBox={`0 0 ${width} ${height}`}
232 preserveAspectRatio="none"
233 className="mt-3 h-36 w-full"
234 role="img"
235 aria-label="Open alerts by type each day; the table below has the numbers"
236 onMouseLeave={() => setHover(null)}
237 >
238 <line x1={0} x2={width} y1={height - 0.5} y2={height - 0.5} stroke="currentColor" className="text-line" />
239 {points.map((point, at) => {
240 const x = at * (bar + gap);
241 let y = height;
242 return (
243 <g key={point.day} onMouseEnter={() => setHover(at)}>
244 {/* A hit target the full height of the day. */}
245 <rect x={x} y={0} width={bar + gap} height={height} fill="transparent" />
246 {SERIES.map((series) => {
247 const value = point[series.key];
248 if (!value) return null;
249 const h = Math.max(1, (value / max) * (height - 8));
250 y -= h;
251 const rect = <rect key={series.key} x={x} y={y} width={bar} height={Math.max(0, h - gap)} rx={Math.min(2, bar / 2)} fill={series.color} opacity={hover == null || hover === at ? 1 : 0.55} />;
252 return rect;
253 })}
254 <title>{`${point.day}: ${SERIES.map((s) => `${point[s.key]} ${s.label.toLowerCase()}`).join(", ")}`}</title>
255 </g>
256 );
257 })}
258 </svg>
259 <details className="mt-2">
260 <summary className="cursor-pointer text-xs text-muted hover:text-fg">Show as a table</summary>
261 <div className="mt-2 max-h-56 overflow-auto">
262 <table className="w-full text-xs tabular-nums">
263 <thead className="text-left text-muted">
264 <tr>
265 <th className="py-1 font-medium">Day</th>
266 {SERIES.map((series) => (
267 <th key={series.key} className="py-1 text-right font-medium">
268 {series.label}
269 </th>
270 ))}
271 </tr>
272 </thead>
273 <tbody>
274 {[...points].reverse().map((point) => (
275 <tr key={point.day} className="border-t border-line">
276 <td className="py-1">{point.day}</td>
277 {SERIES.map((series) => (
278 <td key={series.key} className="py-1 text-right">
279 {point[series.key]}
280 </td>
281 ))}
282 </tr>
283 ))}
284 </tbody>
285 </table>
286 </div>
287 </details>
288 </figure>
289 );
290}
291
292function On({ on, children }: { on: boolean; children?: ReactNode }) {
293 return on ? (
Merge branch 'worktree-agent-ad7c6d88d93adc817'294 <span className="inline-flex items-center gap-1 text-success">
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar295 <CircleCheck size={13} aria-hidden="true" />
296 {children ?? "On"}
297 </span>
298 ) : (
299 <span className="text-faint">{children ? children : "Off"}</span>
300 );
301}
302
303/** Which repository has which feature on, and what is open in each. */
304export function CoverageTable({ repos, owner }: { repos: RepoCoverage[]; owner: string }) {
305 if (repos.length === 0) {
306 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.307 <Card asChild tone="plain" className="border-dashed px-4 py-6 text-sm text-muted">
308 <p>
309 No repository has been scanned yet. Each one is scanned on its next push to its default branch, or when its Security page
310 is first opened.
311 </p>
312 </Card>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar313 );
314 }
315 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.316 <div className={CARD}>
317 <Table className="min-w-[46rem]">
318 <TableHeader>
319 <TableRow>
320 <TableHead className="px-4">Repository</TableHead>
321 <TableHead>Open</TableHead>
322 <TableHead>Push protection</TableHead>
323 <TableHead>Custom patterns</TableHead>
324 <TableHead>Code scanning</TableHead>
325 <TableHead>Dependency review</TableHead>
326 <TableHead>Security updates</TableHead>
327 </TableRow>
328 </TableHeader>
329 <TableBody>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar330 {repos.map((repo) => {
331 const open = total(repo.secrets) + total(repo.code) + total(repo.vulnerabilities);
332 const critical = repo.secrets.critical + repo.code.critical + repo.vulnerabilities.critical;
333 const high = repo.secrets.high + repo.code.high + repo.vulnerabilities.high;
334 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.335 <TableRow key={repo.repoId}>
336 <TableCell className="px-4">
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar337 <Link to={`/${owner}/${repo.name}/security`} className="font-mono text-sm font-medium hover:underline">
338 {repo.name}
339 </Link>
340 {repo.private && <span className="ml-2 text-xs text-faint">private</span>}
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.341 </TableCell>
342 <TableCell className="text-xs whitespace-nowrap">
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar343 {open === 0 ? (
Merge branch 'worktree-agent-ad7c6d88d93adc817'344 <span className="inline-flex items-center gap-1 text-success">
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar345 <ShieldCheck size={13} /> None
346 </span>
347 ) : (
348 <span className="flex flex-wrap gap-1">
349 {critical > 0 && <Badge tone="danger">{critical} critical</Badge>}
350 {high > 0 && <Badge tone="warn">{high} high</Badge>}
351 {open - critical - high > 0 && <Badge>{open - critical - high} other</Badge>}
352 </span>
353 )}
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.354 </TableCell>
355 <TableCell className="text-xs">
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar356 <On on>On</On>
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.357 </TableCell>
358 <TableCell className="text-xs tabular-nums">
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar359 <On on={repo.customPatterns > 0}>{repo.customPatterns > 0 ? `${repo.customPatterns}` : "None"}</On>
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.360 </TableCell>
361 <TableCell className="text-xs">
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar362 {repo.codeScanningAt ? (
Merge branch 'worktree-agent-ad7c6d88d93adc817'363 <span className="text-success">
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar364 <TimeAgo at={repo.codeScanningAt} />
365 </span>
366 ) : (
367 <span className="text-faint">Not set up</span>
368 )}
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.369 </TableCell>
370 <TableCell className="text-xs">
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar371 <On on={repo.dependencyReview && repo.lockfiles > 0}>{repo.lockfiles === 0 ? "No lockfiles" : undefined}</On>
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.372 </TableCell>
373 <TableCell className="text-xs">
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar374 <On on={repo.securityUpdates} />
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.375 </TableCell>
376 </TableRow>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar377 );
378 })}
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.379 </TableBody>
380 </Table>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar381 </div>
382 );
383}
384
385/** "Fix with g1t": an issue for g1t, as the person asking. */
386export function FixWithG1t({ id, action, issue, base }: { id: string; action: string; issue: number | null; base: string }) {
387 const fetcher = useFetcher<{ ok: boolean; error?: string; issue?: number; message?: string | null }>();
388 const opened = fetcher.data?.issue ?? issue;
389 if (opened != null) {
390 return (
391 <Link to={`${base}/issues/${opened}`} className={`${SMALL_BUTTON} inline-flex items-center gap-1.5`}>
392 <Bot size={13} /> g1t is on #{opened}
393 </Link>
394 );
395 }
396 return (
397 <span className="flex flex-col items-end gap-1">
Merge branch 'worktree-agent-ae1299e92e4462012'398 <Hint label="Opens an issue assigned to g1t; its run is charged as agent usage">
399 <button
400 type="button"
401 disabled={fetcher.state !== "idle"}
402 onClick={() => fetcher.submit({ intent: "fix", id }, { method: "post", action })}
403 className={`${SMALL_BUTTON} inline-flex items-center gap-1.5`}
404 >
405 <Sparkles size={13} />
406 {fetcher.state !== "idle" ? "Opening…" : "Fix with g1t"}
407 </button>
408 </Hint>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar409 {fetcher.data?.error && <span className="text-xs text-danger">{fetcher.data.error}</span>}
410 </span>
411 );
412}
413
414/** Pushing past push protection with a reason, or asking to. */
415export function BypassForm({ id, action, request }: { id: string; action: string; request: boolean }) {
416 const fetcher = useFetcher<Done & { requested?: boolean }>();
417 const [reason, setReason] = useState("");
418 if (fetcher.data?.ok) {
419 return (
Merge branch 'worktree-agent-ad7c6d88d93adc817'420 <p className="rounded-md border border-success/30 bg-success/5 px-3 py-2 text-sm text-success">
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar421 {fetcher.data.requested
422 ? "Asked. The workspace's owners and the repository's admins were told; push again once one approves."
423 : "Bypassed. Push again and it goes through."}
424 </p>
425 );
426 }
427 return (
428 <fetcher.Form method="post" action={action} className="space-y-3">
429 <input type="hidden" name="intent" value="bypass" />
430 <input type="hidden" name="id" value={id} />
431 <fieldset>
432 <legend className="text-xs font-medium text-muted">Why does it need to go through?</legend>
433 <RadioGroup name="reason" value={reason} onValueChange={setReason} required className="mt-2 gap-3">
434 {BYPASS_REASONS.map((option) => (
435 <RadioOption key={option.reason} value={option.reason} label={option.label} description={option.about} />
436 ))}
437 </RadioGroup>
438 </fieldset>
439 <label className="block">
440 <span className="mb-1.5 block text-xs font-medium text-muted">Comment (optional)</span>
441 <textarea name="comment" rows={2} maxLength={500} className={INPUT} placeholder="What a reviewer should know." />
442 </label>
443 <div className="flex flex-wrap items-center gap-3">
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.444 <Button type="submit" disabled={fetcher.state !== "idle" || !reason}>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar445 {fetcher.state !== "idle" ? "Sending…" : request ? "Ask to bypass" : "Bypass push protection"}
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.446 </Button>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar447 <span className="text-xs text-muted">Recorded with your name and reason, on the alert and in the audit log.</span>
448 </div>
449 {fetcher.data?.error && <p className="text-sm text-danger">{fetcher.data.error}</p>}
450 </fetcher.Form>
451 );
452}
453
454export type PatternDraft = {
455 id?: string;
456 name: string;
457 pattern: string;
458 before: string;
459 after: string;
460 testStrings: string;
461 published: boolean;
462};
463
464/** Where a test string matched, the match marked. */
465function TestResult({ text, found }: { text: string; found: [number, number] | null }) {
466 if (!found) {
467 return (
468 <li className="flex items-start gap-2 font-mono text-xs break-all">
469 <CircleSlash size={13} className="mt-0.5 shrink-0 text-faint" />
470 <span className="text-muted">{text}</span>
471 </li>
472 );
473 }
474 const chars = [...text];
475 return (
476 <li className="flex items-start gap-2 font-mono text-xs break-all">
Merge branch 'worktree-agent-ad7c6d88d93adc817'477 <CircleCheck size={13} className="mt-0.5 shrink-0 text-success" />
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar478 <span>
479 {chars.slice(0, found[0]).join("")}
480 <mark className="rounded-sm bg-accent/25 text-fg">{chars.slice(found[0], found[1]).join("")}</mark>
481 {chars.slice(found[1]).join("")}
482 </span>
483 </li>
484 );
485}
486
487/**
488 * Making or changing a custom pattern: its regular expression, what comes
489 * before and after it, test strings, a dry run over the default branch,
490 * and saving it as a draft or published.
491 */
492export function PatternEditor({ draft, action, onDone }: { draft: PatternDraft; action: string; onDone?: () => void }) {
493 const save = useFetcher<{ ok: boolean; error?: string; saved?: SavedPattern }>();
494 const dry = useFetcher<{ ok: boolean; error?: string; dryRun?: DryRun }>();
495 const [form, setForm] = useState(draft);
496 useEffect(() => {
497 if (save.state === "idle" && save.data?.ok && onDone) onDone();
498 }, [save.state, save.data, onDone]);
499 const set = (key: keyof PatternDraft) => (event: { target: { value: string } }) => setForm({ ...form, [key]: event.target.value });
500 const fields = (publish: boolean) => ({
501 intent: "save_pattern",
502 id: form.id ?? "",
503 name: form.name,
504 pattern: form.pattern,
505 before: form.before,
506 after: form.after,
507 testStrings: form.testStrings,
508 publish: String(publish),
509 });
510 const tests = form.testStrings.split("\n").filter((line) => line.trim());
511 const saved = save.data?.saved;
512 return (
513 <div className="space-y-4">
514 <div className="grid gap-4 sm:grid-cols-2">
515 <label className="block">
516 <span className="mb-1.5 block text-xs font-medium text-muted">Name</span>
517 <input value={form.name} onChange={set("name")} maxLength={100} placeholder="Acme API key" className={INPUT} />
518 </label>
519 <label className="block sm:col-span-2">
520 <span className="mb-1.5 block text-xs font-medium text-muted">Secret format (regular expression)</span>
521 <input
522 value={form.pattern}
523 onChange={set("pattern")}
524 maxLength={1000}
525 spellCheck={false}
526 placeholder="acme_[a-z0-9]{32}"
527 className={`${INPUT} font-mono`}
528 />
529 </label>
530 <label className="block">
531 <span className="mb-1.5 block text-xs font-medium text-muted">Before the secret (optional)</span>
532 <input value={form.before} onChange={set("before")} spellCheck={false} placeholder="\A|[^0-9A-Za-z]" className={`${INPUT} font-mono`} />
533 </label>
534 <label className="block">
535 <span className="mb-1.5 block text-xs font-medium text-muted">After the secret (optional)</span>
536 <input value={form.after} onChange={set("after")} spellCheck={false} placeholder="\z|[^0-9A-Za-z]" className={`${INPUT} font-mono`} />
537 </label>
538 <label className="block sm:col-span-2">
539 <span className="mb-1.5 block text-xs font-medium text-muted">Test strings, one a line</span>
540 <textarea value={form.testStrings} onChange={set("testStrings")} rows={3} spellCheck={false} className={`${INPUT} font-mono`} />
541 </label>
542 </div>
543 <p className="text-xs text-muted">
544 Patterns use the Rust regex syntax, which runs in time linear in the text: no look-around or back-references. Before and
545 after default to a line edge or a character that is not a letter or digit.
546 </p>
547 {saved && tests.length > 0 && (
548 <ul className="space-y-1">
549 {tests.map((text, at) => (
550 <TestResult key={at} text={text} found={saved.tests[at] ?? null} />
551 ))}
552 </ul>
553 )}
554 <div className="flex flex-wrap items-center gap-2">
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.555 <Button
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar556 type="button"
557 disabled={save.state !== "idle" || !form.name.trim() || !form.pattern.trim()}
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.558 onClick={() => save.submit(fields(true), { method: "post", action })}>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar559 {save.state !== "idle" ? "Saving…" : form.published ? "Save" : "Publish"}
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.560 </Button>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar561 <button
562 type="button"
563 disabled={save.state !== "idle" || !form.name.trim() || !form.pattern.trim()}
564 onClick={() => save.submit(fields(false), { method: "post", action })}
565 className={SMALL_BUTTON}
566 >
567 {form.published ? "Unpublish, keep as draft" : "Save as draft"}
568 </button>
569 <button
570 type="button"
571 disabled={dry.state !== "idle" || !form.pattern.trim()}
572 onClick={() => dry.submit({ intent: "dry_run", pattern: form.pattern, before: form.before, after: form.after }, { method: "post", action })}
573 className={SMALL_BUTTON}
574 >
575 {dry.state !== "idle" ? "Running…" : "Dry run"}
576 </button>
577 {form.id && (
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.578 <Button
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar579 type="button"
580 disabled={save.state !== "idle"}
581 onClick={() => {
582 if (confirm(`Delete "${form.name}"? The alerts it found stay.`)) save.submit({ intent: "delete_pattern", id: form.id ?? "" }, { method: "post", action });
583 }}
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.584 variant="destructive"
585 size="inline"
586 className="ml-auto px-2.5 py-1 text-xs"
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar587 >
588 Delete
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.589 </Button>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar590 )}
591 </div>
592 {save.data?.error && <p className="text-sm text-danger">{save.data.error}</p>}
Merge branch 'worktree-agent-ad7c6d88d93adc817'593 {save.data?.ok && (saved ? <p className="text-sm text-success">Saved{saved.pattern.state === "published" ? " and published: the history is scanned again for it" : " as a draft"}.</p> : <p className="text-sm text-success">Deleted. The alerts it found stay.</p>)}
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar594 {dry.data?.error && <p className="text-sm text-danger">{dry.data.error}</p>}
595 {dry.data?.dryRun && <DryRunResults dryRun={dry.data.dryRun} />}
596 </div>
597 );
598}
599
600function DryRunResults({ dryRun }: { dryRun: DryRun }) {
601 return (
602 <div className="space-y-3">
603 {dryRun.repos.map((repo) => (
604 <section key={repo.name} className={`${CARD} p-3`}>
605 <p className="text-sm">
606 <span className="font-mono font-medium">{repo.name}</span>
607 <span className="text-muted">
608 {" "}
609 · {repo.matches.length} {repo.matches.length === 1 ? "match" : "matches"} in {repo.filesScanned} files
610 {repo.truncated ? " (stopped early)" : ""}
611 </span>
612 </p>
613 {repo.matches.length > 0 && (
614 <ul className="mt-2 space-y-1">
615 {repo.matches.map((match, at) => (
616 <li key={at} className="font-mono text-xs break-all">
617 <span className="inline-flex items-center gap-1 text-fg-soft">
618 <FileCode2 size={12} />
619 {match.path}:{match.line}
620 </span>
621 <span className="text-faint"> {match.preview}</span>
622 </li>
623 ))}
624 </ul>
625 )}
626 </section>
627 ))}
628 </div>
629 );
630}
631
632/** Reads a pattern form's fields, as the pages' actions take them. */
633export function patternFields(form: FormData) {
634 const text = (key: string) => String(form.get(key) ?? "");
635 return {
636 id: text("id") || undefined,
637 name: text("name").trim(),
638 pattern: text("pattern"),
639 before: text("before").trim() || null,
640 after: text("after").trim() || null,
641 testStrings: text("testStrings")
642 .split("\n")
643 .map((line) => line.replace(/\r$/, ""))
644 .filter((line) => line.trim()),
645 publish: text("publish") === "true",
646 };
647}

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