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

Fixes from the page sweep1/**
2 * A repository's or a workspace's webhooks: the addresses events are sent
3 * to, adding one, and every delivery with what was sent and what came back.
4 */
The webhooks empty state is a sentence, without an icon squeezed to a dot on phones5import { ChevronRight, Pause, Play, RotateCw, Send, Trash2 } from "lucide-react";
Fixes from the page sweep6import { useState } from "react";
Merge branch 'worktree-agent-a985c01f43538af01'7import { Form, Link } from "react-router";
Fixes from the page sweep8
9import { EVENT_TYPES, type Hook, type HookDelivery } from "@g1t/contracts";
10
11import type { WebhooksAction, WebhooksData } from "../lib/webhooks.server";
Merge branch 'worktree-agent-a985c01f43538af01'12import { CopyLine, EmptyState, ErrorText, Field, Input, SubmitButton, TimeAgo } from "./ui";
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.13import { Card } from "./ui/card";
Fixes from the page sweep14import { CheckboxOption } from "./ui/checkbox";
Merge branch 'worktree-agent-ae1299e92e4462012'15import { Hint } from "./ui/hint";
Fixes from the page sweep16import { RadioGroup, RadioOption } from "./ui/radio-group";
17
18/** The events, in groups people recognise. */
19const GROUPS: { title: string; events: string[] }[] = [
20 { title: "Code", events: ["git.push", "repo.created", "repo.forked"] },
21 {
22 title: "Issues",
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts23 events: [
24 "issue.opened",
25 "issue.updated",
26 "issue.assigned",
27 "issue.closed",
28 "issue.reopened",
29 "comment.created",
30 "comment.edited",
31 "comment.deleted",
32 ],
Fixes from the page sweep33 },
34 {
35 title: "Pull requests",
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts36 events: [
37 "pull.opened",
38 "pull.ready",
39 "pull.converted_to_draft",
40 "pull.updated",
41 "pull.merge_requested",
42 "pull.merged",
43 "pull.closed",
44 "pull.reopened",
45 ],
46 },
47 {
48 title: "Releases",
49 events: [
50 "release.created",
51 "release.published",
52 "release.released",
53 "release.prereleased",
54 "release.edited",
55 "release.unpublished",
56 "release.deleted",
57 ],
Fixes from the page sweep58 },
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb9759 {
Merge branch 'worktree-agent-ab8033ab973f8d99a'60 title: "Checks and the queue",
61 events: ["checks.completed", "workflow.completed", "queue.changed", "integration.failed", "integration.passed"],
62 },
63 {
Merge checks: statuses and check runs on every commit64 title: "Statuses and check runs",
65 events: [
66 "status.created",
67 "check_run.created",
68 "check_run.completed",
69 "check_run.rerequested",
70 "check_run.requested_action",
71 "check_suite.completed",
72 "check_suite.rerequested",
73 ],
74 },
75 {
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb9776 title: "Deployments",
77 events: ["deployment.created", "deployment_status.created", "deployment.succeeded", "deployment.failed"],
78 },
Merge branch 'worktree-agent-ab8033ab973f8d99a'79 { title: "Sessions and decisions", events: ["session.appended", "decision.recorded"] },
@g1t welcomes you when you join a workspace, and that is the one introduction any agent makes. Identity now says when a person becomes a member, workspace.member_joined, by invitation, by an invite code, or by creating the workspace, and webhooks can hear it; the agents service hears it too, and for anyone who joined, never the creator, the workspace's @g1t opens your direct message and says in its own voice who it is, what to ask it, how agents answer, and where things are, once per person per workspace and charged to @g1t like any reply, with a fixed welcome when no model can be used. The agents, workspaces, authentication and webhooks guides say so.80 { title: "Access", events: ["workspace.member_joined", "repo.collaborator_added", "repo.collaborator_removed", "repo.collaborator_role_changed"] },
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar81 {
82 title: "Teams",
83 events: [
84 "team.created",
85 "team.edited",
86 "team.deleted",
87 "team.member_added",
88 "team.member_role_changed",
89 "team.member_removed",
90 "team.repo_added",
91 "team.repo_role_changed",
92 "team.repo_removed",
93 ],
94 },
Fixes from the page sweep95];
96
97function StatusDot({ status }: { status: string | null }) {
98 const color =
Merge branch 'worktree-agent-ad7c6d88d93adc817'99 status === "delivered" ? "bg-success" : status === "failed" ? "bg-danger" : status === "pending" ? "bg-warn" : "bg-faint";
Fixes from the page sweep100 return <span aria-hidden="true" className={`size-2 shrink-0 rounded-full ${color}`} />;
101}
102
103function describeEvents(events: string[]): string {
104 if (events.includes("*")) return "Every event";
105 return events.length === 1 ? events[0] : `${events.length} events`;
106}
107
108/** JSON, indented for reading, or the text as it is. */
109function pretty(text: string | null): string {
110 if (!text) return "";
111 try {
112 return JSON.stringify(JSON.parse(text), null, 2);
113 } catch {
114 return text;
115 }
116}
117
118function DeliveryRow({ delivery, manage }: { delivery: HookDelivery; manage: boolean }) {
119 return (
120 <details className="group border-t border-line first:border-t-0">
121 <summary className="flex cursor-pointer list-none items-center gap-3 px-4 py-2.5 text-sm hover:bg-raised/40">
122 <ChevronRight size={14} className="shrink-0 text-faint transition-transform group-open:rotate-90" />
123 <StatusDot status={delivery.status} />
124 <span className="font-mono text-[0.8125rem]">{delivery.event}</span>
125 <span className="min-w-0 truncate text-xs text-muted">
126 {delivery.responseStatus != null
127 ? `${delivery.responseStatus}`
128 : delivery.error
129 ? delivery.error
130 : "Not yet sent"}
131 {delivery.attempts > 1 && ` · ${delivery.attempts} attempts`}
132 {delivery.status === "pending" && delivery.nextAttemptAt && " · will try again"}
133 </span>
134 <span className="ml-auto shrink-0 font-mono text-xs text-faint">
135 {delivery.durationMs != null && `${delivery.durationMs}ms · `}
136 <TimeAgo at={delivery.createdAt} />
137 </span>
138 </summary>
139 <div className="grid gap-4 border-t border-line bg-bg/40 p-4 lg:grid-cols-2">
140 <div className="min-w-0">
141 <p className="mb-1.5 text-xs font-medium text-muted">Request</p>
142 <pre className="max-h-80 overflow-auto rounded-lg bg-surface p-3 font-mono text-xs ring-1 ring-line">
143 <code>{pretty(delivery.payload)}</code>
144 </pre>
145 </div>
146 <div className="min-w-0">
147 <p className="mb-1.5 text-xs font-medium text-muted">
148 Response{delivery.responseStatus != null && ` · ${delivery.responseStatus}`}
149 </p>
150 <pre className="max-h-80 overflow-auto rounded-lg bg-surface p-3 font-mono text-xs ring-1 ring-line">
151 <code>{delivery.error ?? (pretty(delivery.responseBody) || "No body.")}</code>
152 </pre>
153 {manage && (
154 <Form method="post" className="mt-3">
155 <input type="hidden" name="intent" value="redeliver" />
156 <input type="hidden" name="delivery" value={delivery.id} />
Merge branch 'worktree-agent-a985c01f43538af01'157 <SubmitButton
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.158 variant="outline"
Merge branch 'worktree-agent-a985c01f43538af01'159 match={{ intent: "redeliver", delivery: delivery.id }}
160 pending="Redelivering…"
161 >
Fixes from the page sweep162 <RotateCw size={14} />
163 Redeliver
Merge branch 'worktree-agent-a985c01f43538af01'164 </SubmitButton>
Fixes from the page sweep165 </Form>
166 )}
167 </div>
168 </div>
169 </details>
170 );
171}
172
173function HookRow({ hook, open, deliveries, manage }: { hook: Hook; open: boolean; deliveries: HookDelivery[]; manage: boolean }) {
174 return (
175 <li className="border-t border-line first:border-t-0">
176 <div className="flex items-center gap-3 px-4 py-3">
177 <StatusDot status={hook.active ? hook.lastStatus : null} />
178 <div className="min-w-0 grow">
179 <p className="truncate font-mono text-[0.8125rem]">{hook.url}</p>
180 <p className="truncate text-xs text-muted">
181 {describeEvents(hook.events)}
182 {!hook.active && " · paused"}
183 {hook.lastDeliveredAt && (
184 <>
185 {" · last sent "}
186 <TimeAgo at={hook.lastDeliveredAt} />
187 </>
188 )}
189 {` · secret ${hook.secretHint}`}
190 </p>
191 </div>
192 <Link
193 to={open ? "?" : `?hook=${hook.id}`}
194 preventScrollReset
195 className="shrink-0 rounded-md px-2.5 py-1.5 text-xs text-muted transition-colors hover:bg-raised hover:text-fg"
196 >
197 {open ? "Hide deliveries" : "Deliveries"}
198 </Link>
199 {manage && (
200 <Form method="post" className="flex shrink-0 gap-1">
201 <input type="hidden" name="id" value={hook.id} />
Merge branch 'worktree-agent-a985c01f43538af01'202 <IconButton intent="ping" id={hook.id} label="Send a ping">
Fixes from the page sweep203 <Send size={14} />
204 </IconButton>
205 <input type="hidden" name="active" value={hook.active ? "false" : "true"} />
Merge branch 'worktree-agent-a985c01f43538af01'206 <IconButton intent="toggle" id={hook.id} label={hook.active ? "Pause" : "Resume"}>
Fixes from the page sweep207 {hook.active ? <Pause size={14} /> : <Play size={14} />}
208 </IconButton>
Merge branch 'worktree-agent-a985c01f43538af01'209 <IconButton intent="delete" id={hook.id} label="Delete">
Fixes from the page sweep210 <Trash2 size={14} />
211 </IconButton>
212 </Form>
213 )}
214 </div>
215 {open && (
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.216 <Card tone="plain" radius="lg" className="mx-4 mb-4 overflow-hidden">
Fixes from the page sweep217 {deliveries.length === 0 ? (
218 <p className="px-4 py-3 text-sm text-muted">Nothing sent yet.</p>
219 ) : (
220 deliveries.map((delivery) => <DeliveryRow key={delivery.id} delivery={delivery} manage={manage} />)
221 )}
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.222 </Card>
Fixes from the page sweep223 )}
224 </li>
225 );
226}
227
Merge branch 'worktree-agent-a985c01f43538af01'228/** One webhook's action, as an icon: a spinner in its place while that action goes. */
229function IconButton({ intent, id, label, children }: { intent: string; id: string; label: string; children: React.ReactNode }) {
Fixes from the page sweep230 return (
Merge branch 'worktree-agent-ae1299e92e4462012'231 <Hint label={label}>
232 <SubmitButton
233 icon
234 name="intent"
235 value={intent}
236 match={{ id }}
237 aria-label={label}
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.238 variant="ghost"
239 size="inline"
240 className="p-2"
Merge branch 'worktree-agent-ae1299e92e4462012'241 >
242 {children}
243 </SubmitButton>
244 </Hint>
Fixes from the page sweep245 );
246}
247
248function AddWebhook() {
249 const [which, setWhich] = useState<"all" | "some">("all");
250 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.251 <Card asChild className="space-y-4 p-5">
252 <Form method="post">
253 <input type="hidden" name="intent" value="create" />
254 <p className="font-medium">Add a webhook</p>
255 <Field label="Payload URL" hint="An HTTPS address on the public internet. g1t sends it a ping as soon as you add it.">
256 <Input name="url" type="url" required placeholder="https://example.com/g1t/events" />
257 </Field>
258 <fieldset>
259 <legend className="mb-1.5 text-sm font-medium text-muted">Which events</legend>
260 <RadioGroup
261 name="which"
262 value={which}
263 onValueChange={(value) => setWhich(value as typeof which)}
264 aria-label="Which events"
265 className="flex flex-wrap gap-x-6 gap-y-2"
266 >
267 <RadioOption value="all" label="Everything, including events added later" />
268 <RadioOption value="some" label="Let me choose" />
269 </RadioGroup>
270 {which === "some" && (
271 <div className="mt-3 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
272 {GROUPS.map((group) => (
273 <div key={group.title}>
274 <p className="mb-1.5 text-xs font-medium text-faint">{group.title}</p>
275 <div className="space-y-1">
276 {group.events
277 .filter((event) => (EVENT_TYPES as readonly string[]).includes(event))
278 .map((event) => (
279 <CheckboxOption
280 key={event}
281 name="event"
282 value={event}
283 label={event}
284 className="items-center"
285 labelClassName="font-mono text-[0.8125rem]"
286 />
287 ))}
288 </div>
Fixes from the page sweep289 </div>
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.290 ))}
291 </div>
292 )}
293 </fieldset>
294 <Field label="Secret" hint="Optional. Deliveries are signed with it. Leave it empty and g1t makes one, shown once.">
295 <Input name="secret" type="password" autoComplete="off" />
296 </Field>
297 <SubmitButton match={{ intent: "create" }} pending="Adding…">
298 Add webhook
299 </SubmitButton>
300 </Form>
301 </Card>
Fixes from the page sweep302 );
303}
304
305export function WebhooksPanel({
306 data,
307 action,
308 manage,
309 scope,
310}: {
311 data: WebhooksData;
312 action: WebhooksAction | undefined;
313 manage: boolean;
314 /** Says whose events these are. */
315 scope: string;
316}) {
317 const created = action?.created;
318 return (
319 <div className="max-w-4xl space-y-6">
320 {created && (
Merge branch 'worktree-agent-ad7c6d88d93adc817'321 <div className="space-y-3 rounded-xl border border-success-dim/60 bg-success/5 p-5 text-sm">
Fixes from the page sweep322 <p className="font-medium">Added. g1t sent it a ping.</p>
323 {created.secret ? (
324 <>
325 <p className="text-muted">
326 Every delivery is signed with this secret in <code>X-G1t-Signature-256</code>. It is shown this once.
327 </p>
328 <CopyLine text={created.secret} />
329 </>
330 ) : (
331 <p className="text-muted">Deliveries are signed with the secret you gave.</p>
332 )}
333 </div>
334 )}
335 {action?.sent && (
336 <p className="text-sm text-muted">
337 Sent {action.sent.event}:{" "}
338 {action.sent.status === "delivered" ? (
Merge branch 'worktree-agent-ad7c6d88d93adc817'339 <span className="text-success">delivered, {action.sent.responseStatus}</span>
Fixes from the page sweep340 ) : (
341 <span className="text-danger">{action.sent.error ?? `answered ${action.sent.responseStatus}`}</span>
342 )}
343 .
344 </p>
345 )}
346 <ErrorText>{action?.error}</ErrorText>
347
348 {data.hooks.length === 0 ? (
349 <EmptyState title="No webhooks yet">
The webhooks empty state is a sentence, without an icon squeezed to a dot on phones350 Add one and {scope} sends its events to your address as they happen, signed, and retried until it answers.
Fixes from the page sweep351 </EmptyState>
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 <Card asChild className="overflow-hidden">
354 <ul>
355 {data.hooks.map((hook) => (
356 <HookRow
357 key={hook.id}
358 hook={hook}
359 open={data.open === hook.id}
360 deliveries={data.open === hook.id ? data.deliveries : []}
361 manage={manage}
362 />
363 ))}
364 </ul>
365 </Card>
Fixes from the page sweep366 )}
367
Merge branch 'worktree-agent-a985c01f43538af01'368 {/* Keyed to the webhook just added, so the form starts empty for the next. */}
369 {manage && <AddWebhook key={created?.hook.id ?? "new"} />}
Fixes from the page sweep370 <p className="text-xs text-faint">
371 Each delivery is an HTTPS POST of JSON with <code>X-G1t-Event</code>, <code>X-G1t-Delivery</code> and{" "}
372 <code>X-G1t-Signature-256</code>. One that is not answered with a 2xx is tried again after 1 minute, 5 minutes,
373 30 minutes, 2 hours and 5 hours.{" "}
374 <a href="https://docs.g1t.sh/guides/webhooks/" className="text-muted underline underline-offset-4">
375 How to check the signature
376 </a>
377 .
378 </p>
379 </div>
380 );
381}

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