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
617 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
The site no longer has g1t's own agents. g1t is where the coding agents you already use do their work, so the Agents mode is gone from the rail, with every page under /<workspace>/-/agents: personas, faces, skills, abilities, routines, computers, sessions and agent spend. Customize (skills, connectors, plugins, marketplaces), the context hub, model providers, the AI Gateway page, the Spend page and pill, Ask g1t, and the repository's Agents, Plans and agent settings pages go too, and their addresses now answer 404.5 * bypass form. Each form posts an `intent` to the page
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar6 * it is on.
7 */
The site no longer has g1t's own agents. g1t is where the coding agents you already use do their work, so the Agents mode is gone from the rail, with every page under /<workspace>/-/agents: personas, faces, skills, abilities, routines, computers, sessions and agent spend. Customize (skills, connectors, plugins, marketplaces), the context hub, model providers, the AI Gateway page, the Spend page and pill, Ask g1t, and the repository's Agents, Plans and agent settings pages go too, and their addresses now answer 404.8import { Bot, CircleCheck, CircleDot, CircleSlash, FileCode2, Lock, ShieldCheck } from "lucide-react";
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar9import { 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";
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar30import { RadioGroup, RadioOption } from "./ui/radio-group";
31import { 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.32import { 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.33import { wholeDollars } from "../lib/money";
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar34
35type Done = { ok: boolean; error?: string } | undefined;
36
37export const CARD = "rounded-xl border border-line bg-surface";
38export const LIST = "divide-y divide-line overflow-hidden rounded-xl border border-line bg-surface";
39const INPUT =
40 "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";
41const SMALL_BUTTON =
42 "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";
43/** A section's title, what it is for, and what can be done there. */
44export function SectionHeader({ title, about, actions }: { title: string; about: ReactNode; actions?: ReactNode }) {
45 return (
46 <div className="flex flex-wrap items-start justify-between gap-4">
47 <div className="min-w-0 max-w-2xl">
48 <h2 className="text-xl font-semibold tracking-tight">{title}</h2>
49 <p className="mt-1.5 text-sm text-muted">{about}</p>
50 </div>
51 {actions && <div className="flex shrink-0 flex-wrap items-center gap-2">{actions}</div>}
52 </div>
53 );
54}
55
56/**
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.57 * What a private repository's paid feature needs: the g1t plan, which
58 * Security and quality comes with at no price of its own, and who can
59 * 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 bar60 */
61export function ActivationPrompt({
62 workspace,
63 feature,
64 monthlyCents,
65 isOwner,
66}: {
67 workspace: string;
68 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.69 /** 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 bar70 monthlyCents: number | null;
71 isOwner: boolean;
72}) {
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.73 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 bar74 return (
75 <div className={`${CARD} flex flex-col gap-4 p-5 sm:flex-row sm:items-start`}>
76 <Lock size={18} className="mt-0.5 shrink-0 text-accent" />
77 <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.78 <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 bar79 <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.80 Security and quality has no price of its own. With the plan{price ? ` (${price} for the workspace)` : ""}, custom patterns,
81 validity checks, delegated bypass, code scanning, dependency review and the security overview are on for every private
82 repository in {workspace}, and their scans are charged at cost plus 20%, like everything g1t runs. Fixes by g1t's agent
83 are charged as agent usage. Public repositories have all of it free, and secret scanning, push protection, vulnerability
84 alerts and security updates stay free everywhere.
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar85 </p>
86 <div className="mt-3 flex flex-wrap items-center gap-3 text-sm">
87 {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.88 <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.89 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.90 </ButtonLink>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar91 ) : (
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.92 <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 bar93 )}
94 <a href="https://docs.g1t.sh/guides/security/pricing/" className="text-muted underline underline-offset-2 hover:text-fg">
95 What's free and what's paid
96 </a>
97 </div>
98 </div>
99 </div>
100 );
101}
102
103/** A labelled select that changes a filter in the address. */
104export function FilterSelect({
105 label,
106 value,
107 options,
108 onChange,
109}: {
110 label: string;
111 value: string;
112 options: [string, string][];
113 onChange: (value: string) => void;
114}) {
115 return (
116 <label className="flex min-w-0 flex-col gap-1 text-xs text-muted">
117 {label}
118 <Select value={value} onValueChange={onChange}>
119 <SelectTrigger size="sm" className="min-w-36" aria-label={label}>
120 <SelectValue />
121 </SelectTrigger>
122 <SelectContent>
123 {options.map(([option, text]) => (
124 <SelectItem key={option} value={option}>
125 {text}
126 </SelectItem>
127 ))}
128 </SelectContent>
129 </Select>
130 </label>
131 );
132}
133
134const STATE_ICON: Record<AlertState, ReactNode> = {
135 open: <CircleDot size={14} />,
136 dismissed: <CircleSlash size={14} />,
137 fixed: <CircleCheck size={14} />,
138};
139
140/** A code scanning alert in a list. */
141export function CodeAlertItem({ alert, base }: { alert: CodeAlert; base: string }) {
142 const where = alert.path ? `${alert.path}${alert.startLine ? `:${alert.startLine}` : ""}` : null;
143 return (
144 <li className="flex flex-col gap-2 px-4 py-3 sm:flex-row sm:items-start">
145 <span className={`mt-0.5 hidden shrink-0 sm:block ${alert.state === "open" ? "text-warn" : "text-muted"}`}>{STATE_ICON[alert.state]}</span>
146 <div className="min-w-0 grow">
147 <div className="flex flex-wrap items-center gap-2">
148 <Link to={`${base}/security/code-scanning/${alert.number}`} className="min-w-0 text-sm font-medium break-words hover:underline">
149 {alert.ruleName && alert.ruleName !== alert.ruleId ? alert.ruleName : shortRule(alert.ruleId)}
150 </Link>
151 <SeverityBadge severity={alert.severity} />
152 {alert.issue != null && (
153 <Link to={`${base}/issues/${alert.issue}`}>
154 <Badge tone="accent">
155 <Bot size={11} /> g1t on #{alert.issue}
156 </Badge>
157 </Link>
158 )}
159 </div>
160 <p className="mt-1 line-clamp-2 text-xs text-muted">{alert.message}</p>
161 <p className="mt-1.5 flex flex-wrap gap-x-3 gap-y-1 text-xs text-faint">
162 <span>#{alert.number}</span>
163 <span>{alert.tool}</span>
164 {where && <span className="truncate font-mono">{where}</span>}
165 <span>
166 {alert.state === "fixed" && alert.fixedAt ? (
167 <>
168 fixed <TimeAgo at={alert.fixedAt} />
169 </>
170 ) : (
171 <>
172 found <TimeAgo at={alert.createdAt} />
173 </>
174 )}
175 </span>
176 </p>
177 </div>
178 </li>
179 );
180}
181
182/** The last part of a dotted rule id: Semgrep's are long. */
183export function shortRule(ruleId: string): string {
184 const parts = ruleId.split(".");
185 return parts.length > 2 ? parts[parts.length - 1] : ruleId;
186}
187
188/** Open counts as compact words: "3 critical · 1 high". */
189export function countsLine(counts: SeverityCounts): string {
190 const parts = (["critical", "high", "medium", "low"] as Severity[]).filter((s) => counts[s] > 0).map((s) => `${counts[s]} ${s}`);
191 return parts.length ? parts.join(" · ") : "none open";
192}
193
194// The trend's three series, each its own hue, validated for the dark
195// surface (dataviz: categorical slots 1–3, dark steps).
196const SERIES = [
197 { key: "secretScanning", label: "Secrets", color: "#3987e5" },
198 { key: "codeScanning", label: "Code scanning", color: "#d95926" },
199 { key: "vulnerability", label: "Vulnerabilities", color: "#199e70" },
200] as const;
201
202/** Open alerts by type each day, as stacked bars with a legend and a table. */
203export function TrendChart({ points }: { points: TrendPoint[] }) {
204 const [hover, setHover] = useState<number | null>(null);
205 const max = trendMax(points);
206 const width = 640;
207 const height = 140;
208 const gap = 2;
209 const bar = Math.max(2, width / Math.max(points.length, 1) - gap);
210 const shown = hover != null ? points[hover] : points[points.length - 1];
211 return (
212 <figure className={`${CARD} p-4`}>
213 <figcaption className="flex flex-wrap items-baseline justify-between gap-2">
214 <span className="text-sm font-medium">Open alerts, by day</span>
215 <span className="text-xs text-muted" aria-live="polite">
216 {shown
217 ? `${shown.day}: ${SERIES.map((s) => `${shown[s.key]} ${s.label.toLowerCase()}`).join(", ")}`
218 : "No days yet"}
219 </span>
220 </figcaption>
221 <ul className="mt-2 flex flex-wrap gap-x-4 gap-y-1 text-xs text-muted" aria-label="Legend">
222 {SERIES.map((series) => (
223 <li key={series.key} className="flex items-center gap-1.5">
224 <span aria-hidden="true" className="size-2.5 rounded-sm" style={{ background: series.color }} />
225 {series.label}
226 </li>
227 ))}
228 </ul>
229 <svg
230 viewBox={`0 0 ${width} ${height}`}
231 preserveAspectRatio="none"
232 className="mt-3 h-36 w-full"
233 role="img"
234 aria-label="Open alerts by type each day; the table below has the numbers"
235 onMouseLeave={() => setHover(null)}
236 >
237 <line x1={0} x2={width} y1={height - 0.5} y2={height - 0.5} stroke="currentColor" className="text-line" />
238 {points.map((point, at) => {
239 const x = at * (bar + gap);
240 let y = height;
241 return (
242 <g key={point.day} onMouseEnter={() => setHover(at)}>
243 {/* A hit target the full height of the day. */}
244 <rect x={x} y={0} width={bar + gap} height={height} fill="transparent" />
245 {SERIES.map((series) => {
246 const value = point[series.key];
247 if (!value) return null;
248 const h = Math.max(1, (value / max) * (height - 8));
249 y -= h;
250 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} />;
251 return rect;
252 })}
253 <title>{`${point.day}: ${SERIES.map((s) => `${point[s.key]} ${s.label.toLowerCase()}`).join(", ")}`}</title>
254 </g>
255 );
256 })}
257 </svg>
258 <details className="mt-2">
259 <summary className="cursor-pointer text-xs text-muted hover:text-fg">Show as a table</summary>
260 <div className="mt-2 max-h-56 overflow-auto">
261 <table className="w-full text-xs tabular-nums">
262 <thead className="text-left text-muted">
263 <tr>
264 <th className="py-1 font-medium">Day</th>
265 {SERIES.map((series) => (
266 <th key={series.key} className="py-1 text-right font-medium">
267 {series.label}
268 </th>
269 ))}
270 </tr>
271 </thead>
272 <tbody>
273 {[...points].reverse().map((point) => (
274 <tr key={point.day} className="border-t border-line">
275 <td className="py-1">{point.day}</td>
276 {SERIES.map((series) => (
277 <td key={series.key} className="py-1 text-right">
278 {point[series.key]}
279 </td>
280 ))}
281 </tr>
282 ))}
283 </tbody>
284 </table>
285 </div>
286 </details>
287 </figure>
288 );
289}
290
291function On({ on, children }: { on: boolean; children?: ReactNode }) {
292 return on ? (
Merge branch 'worktree-agent-ad7c6d88d93adc817'293 <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 bar294 <CircleCheck size={13} aria-hidden="true" />
295 {children ?? "On"}
296 </span>
297 ) : (
298 <span className="text-faint">{children ? children : "Off"}</span>
299 );
300}
301
302/** Which repository has which feature on, and what is open in each. */
303export function CoverageTable({ repos, owner }: { repos: RepoCoverage[]; owner: string }) {
304 if (repos.length === 0) {
305 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.306 <Card asChild tone="plain" className="border-dashed px-4 py-6 text-sm text-muted">
307 <p>
308 No repository has been scanned yet. Each one is scanned on its next push to its default branch, or when its Security page
309 is first opened.
310 </p>
311 </Card>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar312 );
313 }
314 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.315 <div className={CARD}>
316 <Table className="min-w-[46rem]">
317 <TableHeader>
318 <TableRow>
319 <TableHead className="px-4">Repository</TableHead>
320 <TableHead>Open</TableHead>
321 <TableHead>Push protection</TableHead>
322 <TableHead>Custom patterns</TableHead>
323 <TableHead>Code scanning</TableHead>
324 <TableHead>Dependency review</TableHead>
325 <TableHead>Security updates</TableHead>
326 </TableRow>
327 </TableHeader>
328 <TableBody>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar329 {repos.map((repo) => {
330 const open = total(repo.secrets) + total(repo.code) + total(repo.vulnerabilities);
331 const critical = repo.secrets.critical + repo.code.critical + repo.vulnerabilities.critical;
332 const high = repo.secrets.high + repo.code.high + repo.vulnerabilities.high;
333 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.334 <TableRow key={repo.repoId}>
335 <TableCell className="px-4">
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar336 <Link to={`/${owner}/${repo.name}/security`} className="font-mono text-sm font-medium hover:underline">
337 {repo.name}
338 </Link>
339 {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.340 </TableCell>
341 <TableCell className="text-xs whitespace-nowrap">
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar342 {open === 0 ? (
Merge branch 'worktree-agent-ad7c6d88d93adc817'343 <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 bar344 <ShieldCheck size={13} /> None
345 </span>
346 ) : (
347 <span className="flex flex-wrap gap-1">
348 {critical > 0 && <Badge tone="danger">{critical} critical</Badge>}
349 {high > 0 && <Badge tone="warn">{high} high</Badge>}
350 {open - critical - high > 0 && <Badge>{open - critical - high} other</Badge>}
351 </span>
352 )}
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.353 </TableCell>
354 <TableCell className="text-xs">
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar355 <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.356 </TableCell>
357 <TableCell className="text-xs tabular-nums">
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar358 <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.359 </TableCell>
360 <TableCell className="text-xs">
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar361 {repo.codeScanningAt ? (
Merge branch 'worktree-agent-ad7c6d88d93adc817'362 <span className="text-success">
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar363 <TimeAgo at={repo.codeScanningAt} />
364 </span>
365 ) : (
366 <span className="text-faint">Not set up</span>
367 )}
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.368 </TableCell>
369 <TableCell className="text-xs">
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar370 <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.371 </TableCell>
372 <TableCell className="text-xs">
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar373 <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.374 </TableCell>
375 </TableRow>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar376 );
377 })}
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.378 </TableBody>
379 </Table>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar380 </div>
381 );
382}
383
384/** Pushing past push protection with a reason, or asking to. */
385export function BypassForm({ id, action, request }: { id: string; action: string; request: boolean }) {
386 const fetcher = useFetcher<Done & { requested?: boolean }>();
387 const [reason, setReason] = useState("");
388 if (fetcher.data?.ok) {
389 return (
Merge branch 'worktree-agent-ad7c6d88d93adc817'390 <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 bar391 {fetcher.data.requested
392 ? "Asked. The workspace's owners and the repository's admins were told; push again once one approves."
393 : "Bypassed. Push again and it goes through."}
394 </p>
395 );
396 }
397 return (
398 <fetcher.Form method="post" action={action} className="space-y-3">
399 <input type="hidden" name="intent" value="bypass" />
400 <input type="hidden" name="id" value={id} />
401 <fieldset>
402 <legend className="text-xs font-medium text-muted">Why does it need to go through?</legend>
403 <RadioGroup name="reason" value={reason} onValueChange={setReason} required className="mt-2 gap-3">
404 {BYPASS_REASONS.map((option) => (
405 <RadioOption key={option.reason} value={option.reason} label={option.label} description={option.about} />
406 ))}
407 </RadioGroup>
408 </fieldset>
409 <label className="block">
410 <span className="mb-1.5 block text-xs font-medium text-muted">Comment (optional)</span>
411 <textarea name="comment" rows={2} maxLength={500} className={INPUT} placeholder="What a reviewer should know." />
412 </label>
413 <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.414 <Button type="submit" disabled={fetcher.state !== "idle" || !reason}>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar415 {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.416 </Button>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar417 <span className="text-xs text-muted">Recorded with your name and reason, on the alert and in the audit log.</span>
418 </div>
419 {fetcher.data?.error && <p className="text-sm text-danger">{fetcher.data.error}</p>}
420 </fetcher.Form>
421 );
422}
423
424export type PatternDraft = {
425 id?: string;
426 name: string;
427 pattern: string;
428 before: string;
429 after: string;
430 testStrings: string;
431 published: boolean;
432};
433
434/** Where a test string matched, the match marked. */
435function TestResult({ text, found }: { text: string; found: [number, number] | null }) {
436 if (!found) {
437 return (
438 <li className="flex items-start gap-2 font-mono text-xs break-all">
439 <CircleSlash size={13} className="mt-0.5 shrink-0 text-faint" />
440 <span className="text-muted">{text}</span>
441 </li>
442 );
443 }
444 const chars = [...text];
445 return (
446 <li className="flex items-start gap-2 font-mono text-xs break-all">
Merge branch 'worktree-agent-ad7c6d88d93adc817'447 <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 bar448 <span>
449 {chars.slice(0, found[0]).join("")}
450 <mark className="rounded-sm bg-accent/25 text-fg">{chars.slice(found[0], found[1]).join("")}</mark>
451 {chars.slice(found[1]).join("")}
452 </span>
453 </li>
454 );
455}
456
457/**
458 * Making or changing a custom pattern: its regular expression, what comes
459 * before and after it, test strings, a dry run over the default branch,
460 * and saving it as a draft or published.
461 */
462export function PatternEditor({ draft, action, onDone }: { draft: PatternDraft; action: string; onDone?: () => void }) {
463 const save = useFetcher<{ ok: boolean; error?: string; saved?: SavedPattern }>();
464 const dry = useFetcher<{ ok: boolean; error?: string; dryRun?: DryRun }>();
465 const [form, setForm] = useState(draft);
466 useEffect(() => {
467 if (save.state === "idle" && save.data?.ok && onDone) onDone();
468 }, [save.state, save.data, onDone]);
469 const set = (key: keyof PatternDraft) => (event: { target: { value: string } }) => setForm({ ...form, [key]: event.target.value });
470 const fields = (publish: boolean) => ({
471 intent: "save_pattern",
472 id: form.id ?? "",
473 name: form.name,
474 pattern: form.pattern,
475 before: form.before,
476 after: form.after,
477 testStrings: form.testStrings,
478 publish: String(publish),
479 });
480 const tests = form.testStrings.split("\n").filter((line) => line.trim());
481 const saved = save.data?.saved;
482 return (
483 <div className="space-y-4">
484 <div className="grid gap-4 sm:grid-cols-2">
485 <label className="block">
486 <span className="mb-1.5 block text-xs font-medium text-muted">Name</span>
487 <input value={form.name} onChange={set("name")} maxLength={100} placeholder="Acme API key" className={INPUT} />
488 </label>
489 <label className="block sm:col-span-2">
490 <span className="mb-1.5 block text-xs font-medium text-muted">Secret format (regular expression)</span>
491 <input
492 value={form.pattern}
493 onChange={set("pattern")}
494 maxLength={1000}
495 spellCheck={false}
496 placeholder="acme_[a-z0-9]{32}"
497 className={`${INPUT} font-mono`}
498 />
499 </label>
500 <label className="block">
501 <span className="mb-1.5 block text-xs font-medium text-muted">Before the secret (optional)</span>
502 <input value={form.before} onChange={set("before")} spellCheck={false} placeholder="\A|[^0-9A-Za-z]" className={`${INPUT} font-mono`} />
503 </label>
504 <label className="block">
505 <span className="mb-1.5 block text-xs font-medium text-muted">After the secret (optional)</span>
506 <input value={form.after} onChange={set("after")} spellCheck={false} placeholder="\z|[^0-9A-Za-z]" className={`${INPUT} font-mono`} />
507 </label>
508 <label className="block sm:col-span-2">
509 <span className="mb-1.5 block text-xs font-medium text-muted">Test strings, one a line</span>
510 <textarea value={form.testStrings} onChange={set("testStrings")} rows={3} spellCheck={false} className={`${INPUT} font-mono`} />
511 </label>
512 </div>
513 <p className="text-xs text-muted">
514 Patterns use the Rust regex syntax, which runs in time linear in the text: no look-around or back-references. Before and
515 after default to a line edge or a character that is not a letter or digit.
516 </p>
517 {saved && tests.length > 0 && (
518 <ul className="space-y-1">
519 {tests.map((text, at) => (
520 <TestResult key={at} text={text} found={saved.tests[at] ?? null} />
521 ))}
522 </ul>
523 )}
524 <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.525 <Button
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar526 type="button"
527 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.528 onClick={() => save.submit(fields(true), { method: "post", action })}>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar529 {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.530 </Button>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar531 <button
532 type="button"
533 disabled={save.state !== "idle" || !form.name.trim() || !form.pattern.trim()}
534 onClick={() => save.submit(fields(false), { method: "post", action })}
535 className={SMALL_BUTTON}
536 >
537 {form.published ? "Unpublish, keep as draft" : "Save as draft"}
538 </button>
539 <button
540 type="button"
541 disabled={dry.state !== "idle" || !form.pattern.trim()}
542 onClick={() => dry.submit({ intent: "dry_run", pattern: form.pattern, before: form.before, after: form.after }, { method: "post", action })}
543 className={SMALL_BUTTON}
544 >
545 {dry.state !== "idle" ? "Running…" : "Dry run"}
546 </button>
547 {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.548 <Button
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar549 type="button"
550 disabled={save.state !== "idle"}
551 onClick={() => {
552 if (confirm(`Delete "${form.name}"? The alerts it found stay.`)) save.submit({ intent: "delete_pattern", id: form.id ?? "" }, { method: "post", action });
553 }}
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.554 variant="destructive"
555 size="inline"
556 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 bar557 >
558 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.559 </Button>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar560 )}
561 </div>
562 {save.data?.error && <p className="text-sm text-danger">{save.data.error}</p>}
Merge branch 'worktree-agent-ad7c6d88d93adc817'563 {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 bar564 {dry.data?.error && <p className="text-sm text-danger">{dry.data.error}</p>}
565 {dry.data?.dryRun && <DryRunResults dryRun={dry.data.dryRun} />}
566 </div>
567 );
568}
569
570function DryRunResults({ dryRun }: { dryRun: DryRun }) {
571 return (
572 <div className="space-y-3">
573 {dryRun.repos.map((repo) => (
574 <section key={repo.name} className={`${CARD} p-3`}>
575 <p className="text-sm">
576 <span className="font-mono font-medium">{repo.name}</span>
577 <span className="text-muted">
578 {" "}
579 · {repo.matches.length} {repo.matches.length === 1 ? "match" : "matches"} in {repo.filesScanned} files
580 {repo.truncated ? " (stopped early)" : ""}
581 </span>
582 </p>
583 {repo.matches.length > 0 && (
584 <ul className="mt-2 space-y-1">
585 {repo.matches.map((match, at) => (
586 <li key={at} className="font-mono text-xs break-all">
587 <span className="inline-flex items-center gap-1 text-fg-soft">
588 <FileCode2 size={12} />
589 {match.path}:{match.line}
590 </span>
591 <span className="text-faint"> {match.preview}</span>
592 </li>
593 ))}
594 </ul>
595 )}
596 </section>
597 ))}
598 </div>
599 );
600}
601
602/** Reads a pattern form's fields, as the pages' actions take them. */
603export function patternFields(form: FormData) {
604 const text = (key: string) => String(form.get(key) ?? "");
605 return {
606 id: text("id") || undefined,
607 name: text("name").trim(),
608 pattern: text("pattern"),
609 before: text("before").trim() || null,
610 after: text("after").trim() || null,
611 testStrings: text("testStrings")
612 .split("\n")
613 .map((line) => line.replace(/\r$/, ""))
614 .filter((line) => line.trim()),
615 publish: text("publish") === "true",
616 };
617}

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