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

Project settings say who created the project, by name1/**
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";
Project settings say who created the project, by name6import { useState } from "react";
Merge branch 'worktree-agent-a985c01f43538af01'7import { Form, Link } from "react-router";
Project settings say who created the project, by name8
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";
Project settings say who created the project, by name14import { CheckboxOption } from "./ui/checkbox";
Merge branch 'worktree-agent-ae1299e92e4462012'15import { Hint } from "./ui/hint";
Project settings say who created the project, by name16import { 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 ],
Project settings say who created the project, by name33 },
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 ],
Project settings say who created the project, by name58 },
The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens.59 { title: "Checks and the queue", events: ["checks.completed", "workflow.completed", "queue.changed"] },
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb9760 {
Merge checks: statuses and check runs on every commit61 title: "Statuses and check runs",
62 events: [
63 "status.created",
64 "check_run.created",
65 "check_run.completed",
66 "check_run.rerequested",
67 "check_run.requested_action",
68 "check_suite.completed",
69 "check_suite.rerequested",
70 ],
71 },
72 {
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb9773 title: "Deployments",
74 events: ["deployment.created", "deployment_status.created", "deployment.succeeded", "deployment.failed"],
75 },
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.76 { title: "Sessions", events: ["session.appended"] },
@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.77 { 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 bar78 {
79 title: "Teams",
80 events: [
81 "team.created",
82 "team.edited",
83 "team.deleted",
84 "team.member_added",
85 "team.member_role_changed",
86 "team.member_removed",
87 "team.repo_added",
88 "team.repo_role_changed",
89 "team.repo_removed",
90 ],
91 },
Project settings say who created the project, by name92];
93
94function StatusDot({ status }: { status: string | null }) {
95 const color =
Merge branch 'worktree-agent-ad7c6d88d93adc817'96 status === "delivered" ? "bg-success" : status === "failed" ? "bg-danger" : status === "pending" ? "bg-warn" : "bg-faint";
Project settings say who created the project, by name97 return <span aria-hidden="true" className={`size-2 shrink-0 rounded-full ${color}`} />;
98}
99
100function describeEvents(events: string[]): string {
101 if (events.includes("*")) return "Every event";
102 return events.length === 1 ? events[0] : `${events.length} events`;
103}
104
105/** JSON, indented for reading, or the text as it is. */
106function pretty(text: string | null): string {
107 if (!text) return "";
108 try {
109 return JSON.stringify(JSON.parse(text), null, 2);
110 } catch {
111 return text;
112 }
113}
114
115function DeliveryRow({ delivery, manage }: { delivery: HookDelivery; manage: boolean }) {
116 return (
117 <details className="group border-t border-line first:border-t-0">
118 <summary className="flex cursor-pointer list-none items-center gap-3 px-4 py-2.5 text-sm hover:bg-raised/40">
119 <ChevronRight size={14} className="shrink-0 text-faint transition-transform group-open:rotate-90" />
120 <StatusDot status={delivery.status} />
121 <span className="font-mono text-[0.8125rem]">{delivery.event}</span>
122 <span className="min-w-0 truncate text-xs text-muted">
123 {delivery.responseStatus != null
124 ? `${delivery.responseStatus}`
125 : delivery.error
126 ? delivery.error
127 : "Not yet sent"}
128 {delivery.attempts > 1 && ` · ${delivery.attempts} attempts`}
129 {delivery.status === "pending" && delivery.nextAttemptAt && " · will try again"}
130 </span>
131 <span className="ml-auto shrink-0 font-mono text-xs text-faint">
132 {delivery.durationMs != null && `${delivery.durationMs}ms · `}
133 <TimeAgo at={delivery.createdAt} />
134 </span>
135 </summary>
136 <div className="grid gap-4 border-t border-line bg-bg/40 p-4 lg:grid-cols-2">
137 <div className="min-w-0">
138 <p className="mb-1.5 text-xs font-medium text-muted">Request</p>
139 <pre className="max-h-80 overflow-auto rounded-lg bg-surface p-3 font-mono text-xs ring-1 ring-line">
140 <code>{pretty(delivery.payload)}</code>
141 </pre>
142 </div>
143 <div className="min-w-0">
144 <p className="mb-1.5 text-xs font-medium text-muted">
145 Response{delivery.responseStatus != null && ` · ${delivery.responseStatus}`}
146 </p>
147 <pre className="max-h-80 overflow-auto rounded-lg bg-surface p-3 font-mono text-xs ring-1 ring-line">
148 <code>{delivery.error ?? (pretty(delivery.responseBody) || "No body.")}</code>
149 </pre>
150 {manage && (
151 <Form method="post" className="mt-3">
152 <input type="hidden" name="intent" value="redeliver" />
153 <input type="hidden" name="delivery" value={delivery.id} />
Merge branch 'worktree-agent-a985c01f43538af01'154 <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.155 variant="outline"
Merge branch 'worktree-agent-a985c01f43538af01'156 match={{ intent: "redeliver", delivery: delivery.id }}
157 pending="Redelivering…"
158 >
Project settings say who created the project, by name159 <RotateCw size={14} />
160 Redeliver
Merge branch 'worktree-agent-a985c01f43538af01'161 </SubmitButton>
Project settings say who created the project, by name162 </Form>
163 )}
164 </div>
165 </div>
166 </details>
167 );
168}
169
170function HookRow({ hook, open, deliveries, manage }: { hook: Hook; open: boolean; deliveries: HookDelivery[]; manage: boolean }) {
171 return (
172 <li className="border-t border-line first:border-t-0">
173 <div className="flex items-center gap-3 px-4 py-3">
174 <StatusDot status={hook.active ? hook.lastStatus : null} />
175 <div className="min-w-0 grow">
176 <p className="truncate font-mono text-[0.8125rem]">{hook.url}</p>
177 <p className="truncate text-xs text-muted">
178 {describeEvents(hook.events)}
179 {!hook.active && " · paused"}
180 {hook.lastDeliveredAt && (
181 <>
182 {" · last sent "}
183 <TimeAgo at={hook.lastDeliveredAt} />
184 </>
185 )}
186 {` · secret ${hook.secretHint}`}
187 </p>
188 </div>
189 <Link
190 to={open ? "?" : `?hook=${hook.id}`}
191 preventScrollReset
192 className="shrink-0 rounded-md px-2.5 py-1.5 text-xs text-muted transition-colors hover:bg-raised hover:text-fg"
193 >
194 {open ? "Hide deliveries" : "Deliveries"}
195 </Link>
196 {manage && (
197 <Form method="post" className="flex shrink-0 gap-1">
198 <input type="hidden" name="id" value={hook.id} />
Merge branch 'worktree-agent-a985c01f43538af01'199 <IconButton intent="ping" id={hook.id} label="Send a ping">
Project settings say who created the project, by name200 <Send size={14} />
201 </IconButton>
202 <input type="hidden" name="active" value={hook.active ? "false" : "true"} />
Merge branch 'worktree-agent-a985c01f43538af01'203 <IconButton intent="toggle" id={hook.id} label={hook.active ? "Pause" : "Resume"}>
Project settings say who created the project, by name204 {hook.active ? <Pause size={14} /> : <Play size={14} />}
205 </IconButton>
Merge branch 'worktree-agent-a985c01f43538af01'206 <IconButton intent="delete" id={hook.id} label="Delete">
Project settings say who created the project, by name207 <Trash2 size={14} />
208 </IconButton>
209 </Form>
210 )}
211 </div>
212 {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.213 <Card tone="plain" radius="lg" className="mx-4 mb-4 overflow-hidden">
Project settings say who created the project, by name214 {deliveries.length === 0 ? (
215 <p className="px-4 py-3 text-sm text-muted">Nothing sent yet.</p>
216 ) : (
217 deliveries.map((delivery) => <DeliveryRow key={delivery.id} delivery={delivery} manage={manage} />)
218 )}
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.219 </Card>
Project settings say who created the project, by name220 )}
221 </li>
222 );
223}
224
Merge branch 'worktree-agent-a985c01f43538af01'225/** One webhook's action, as an icon: a spinner in its place while that action goes. */
226function IconButton({ intent, id, label, children }: { intent: string; id: string; label: string; children: React.ReactNode }) {
Project settings say who created the project, by name227 return (
Merge branch 'worktree-agent-ae1299e92e4462012'228 <Hint label={label}>
229 <SubmitButton
230 icon
231 name="intent"
232 value={intent}
233 match={{ id }}
234 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.235 variant="ghost"
236 size="inline"
237 className="p-2"
Merge branch 'worktree-agent-ae1299e92e4462012'238 >
239 {children}
240 </SubmitButton>
241 </Hint>
Project settings say who created the project, by name242 );
243}
244
245function AddWebhook() {
246 const [which, setWhich] = useState<"all" | "some">("all");
247 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.248 <Card asChild className="space-y-4 p-5">
249 <Form method="post">
250 <input type="hidden" name="intent" value="create" />
251 <p className="font-medium">Add a webhook</p>
252 <Field label="Payload URL" hint="An HTTPS address on the public internet. g1t sends it a ping as soon as you add it.">
253 <Input name="url" type="url" required placeholder="https://example.com/g1t/events" />
254 </Field>
255 <fieldset>
256 <legend className="mb-1.5 text-sm font-medium text-muted">Which events</legend>
257 <RadioGroup
258 name="which"
259 value={which}
260 onValueChange={(value) => setWhich(value as typeof which)}
261 aria-label="Which events"
262 className="flex flex-wrap gap-x-6 gap-y-2"
263 >
264 <RadioOption value="all" label="Everything, including events added later" />
265 <RadioOption value="some" label="Let me choose" />
266 </RadioGroup>
267 {which === "some" && (
268 <div className="mt-3 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
269 {GROUPS.map((group) => (
270 <div key={group.title}>
271 <p className="mb-1.5 text-xs font-medium text-faint">{group.title}</p>
272 <div className="space-y-1">
273 {group.events
274 .filter((event) => (EVENT_TYPES as readonly string[]).includes(event))
275 .map((event) => (
276 <CheckboxOption
277 key={event}
278 name="event"
279 value={event}
280 label={event}
281 className="items-center"
282 labelClassName="font-mono text-[0.8125rem]"
283 />
284 ))}
285 </div>
Project settings say who created the project, by name286 </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.287 ))}
288 </div>
289 )}
290 </fieldset>
291 <Field label="Secret" hint="Optional. Deliveries are signed with it. Leave it empty and g1t makes one, shown once.">
292 <Input name="secret" type="password" autoComplete="off" />
293 </Field>
294 <SubmitButton match={{ intent: "create" }} pending="Adding…">
295 Add webhook
296 </SubmitButton>
297 </Form>
298 </Card>
Project settings say who created the project, by name299 );
300}
301
302export function WebhooksPanel({
303 data,
304 action,
305 manage,
306 scope,
307}: {
308 data: WebhooksData;
309 action: WebhooksAction | undefined;
310 manage: boolean;
311 /** Says whose events these are. */
312 scope: string;
313}) {
314 const created = action?.created;
315 return (
316 <div className="max-w-4xl space-y-6">
317 {created && (
Merge branch 'worktree-agent-ad7c6d88d93adc817'318 <div className="space-y-3 rounded-xl border border-success-dim/60 bg-success/5 p-5 text-sm">
Project settings say who created the project, by name319 <p className="font-medium">Added. g1t sent it a ping.</p>
320 {created.secret ? (
321 <>
322 <p className="text-muted">
323 Every delivery is signed with this secret in <code>X-G1t-Signature-256</code>. It is shown this once.
324 </p>
325 <CopyLine text={created.secret} />
326 </>
327 ) : (
328 <p className="text-muted">Deliveries are signed with the secret you gave.</p>
329 )}
330 </div>
331 )}
332 {action?.sent && (
333 <p className="text-sm text-muted">
334 Sent {action.sent.event}:{" "}
335 {action.sent.status === "delivered" ? (
Merge branch 'worktree-agent-ad7c6d88d93adc817'336 <span className="text-success">delivered, {action.sent.responseStatus}</span>
Project settings say who created the project, by name337 ) : (
338 <span className="text-danger">{action.sent.error ?? `answered ${action.sent.responseStatus}`}</span>
339 )}
340 .
341 </p>
342 )}
343 <ErrorText>{action?.error}</ErrorText>
344
345 {data.hooks.length === 0 ? (
346 <EmptyState title="No webhooks yet">
The webhooks empty state is a sentence, without an icon squeezed to a dot on phones347 Add one and {scope} sends its events to your address as they happen, signed, and retried until it answers.
Project settings say who created the project, by name348 </EmptyState>
349 ) : (
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.350 <Card asChild className="overflow-hidden">
351 <ul>
352 {data.hooks.map((hook) => (
353 <HookRow
354 key={hook.id}
355 hook={hook}
356 open={data.open === hook.id}
357 deliveries={data.open === hook.id ? data.deliveries : []}
358 manage={manage}
359 />
360 ))}
361 </ul>
362 </Card>
Project settings say who created the project, by name363 )}
364
Merge branch 'worktree-agent-a985c01f43538af01'365 {/* Keyed to the webhook just added, so the form starts empty for the next. */}
366 {manage && <AddWebhook key={created?.hook.id ?? "new"} />}
Project settings say who created the project, by name367 <p className="text-xs text-faint">
368 Each delivery is an HTTPS POST of JSON with <code>X-G1t-Event</code>, <code>X-G1t-Delivery</code> and{" "}
369 <code>X-G1t-Signature-256</code>. One that is not answered with a 2xx is tried again after 1 minute, 5 minutes,
370 30 minutes, 2 hours and 5 hours.{" "}
371 <a href="https://docs.g1t.sh/guides/webhooks/" className="text-muted underline underline-offset-4">
372 How to check the signature
373 </a>
374 .
375 </p>
376 </div>
377 );
378}

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