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

Share the editor settings and Claude Code deploy permissions1import {
2 AlertTriangle,
Integrations are a directory of connectors, for a workspace and for each person3 ArrowLeft,
Share the editor settings and Claude Code deploy permissions4 CheckCircle2,
5 ChevronRight,
6 Cpu,
7 Siren,
8 Ticket,
9 X,
10} from "lucide-react";
11import { env } from "cloudflare:workers";
12import type { ReactNode } from "react";
Merge branch 'worktree-agent-a985c01f43538af01'13import { Form, Link } from "react-router";
Share the editor settings and Claude Code deploy permissions14
15import {
16 type Connection,
17 type ConnectionConfig,
18 type Delivery,
19 MODEL_TASKS,
20 type ModelRoute,
21 type Provider,
22 type ProviderKind,
23 PROVIDERS,
AI Gateway: OpenAI's format, open models, and your own providers24 gatewayPatterns,
Share the editor settings and Claude Code deploy permissions25} from "@g1t/contracts";
26
27import type { Route } from "./+types/integrations";
Social cards for every page: og.g1t.sh28import { page } from "../../lib/meta";
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put29import { trialClosed } from "../../lib/trial";
AI Gateway: OpenAI's format, open models, and your own providers30import { GatewayModelsField, MODEL_CATALOG, ModelCatalog, ModelProviderFields, ProviderMark, ProviderTiles, Routing } from "../../components/model-providers";
31import { parseGatewayModels } from "../../lib/gateway";
Integrations are a directory of connectors, for a workspace and for each person32import { integrationsSection, sectionKind } from "../../lib/integration-sections";
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 { money } from "../../lib/money";
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.34import { CopyLine, ErrorText, Field, Input, SubmitButton, TimeAgo } from "../../components/ui";
35import { Avatar } from "../../components/ui/avatar";
36import { Card } from "../../components/ui/card";
Workspace names and icons, and a component kit for every control37import { CheckboxOption } from "../../components/ui/checkbox";
38import { Combobox } from "../../components/ui/combobox";
Share the editor settings and Claude Code deploy permissions39import { billing, integrations, repos } from "../../lib/services.server";
40import { assertSameOrigin, getViewer, requireUser, roleIn, unwrap } from "../../lib/session.server";
41
Social cards for every page: og.g1t.sh42export function meta({ params, ...args }: Route.MetaArgs) {
Integrations are a directory of connectors, for a workspace and for each person43 const kind = sectionKind(params.section);
44 return page(args, { title: `${kind ? KIND_INFO[kind].title : "Integrations"} · ${params.owner} · g1t` });
Share the editor settings and Claude Code deploy permissions45}
46
47const isProvider = (value: string | null): value is Provider => value != null && value in PROVIDERS;
48
49export async function loader({ params, context, request }: Route.LoaderArgs) {
50 const viewer = getViewer(context);
51 const role = roleIn(viewer, params.owner);
Integrations are a directory of connectors, for a workspace and for each person52 // One kind of connection's page: the directory at `-/integrations` finds them.
53 const kind = sectionKind(params.section);
54 if (!role || !kind) throw new Response(null, { status: 404 });
Share the editor settings and Claude Code deploy permissions55 const slug = params.owner.toLowerCase();
56 const [connections, listed, account, access, routes] = await Promise.all([
57 integrations.list(slug, viewer),
58 repos.list(viewer, { namespace: slug }),
59 billing.account(slug, viewer),
60 env.RUNNER.modelAccess(slug),
61 integrations.routes(slug, viewer),
62 ]);
63 const all = unwrap(connections);
64 // What each alert source has sent lately, to see it is wired up.
65 const deliveries: Record<string, Delivery[]> = {};
66 await Promise.all(
67 all
68 .filter((connection) => connection.kind === "alerts")
69 .map(async (connection) => {
70 const found = await integrations.deliveries(slug, viewer, connection.id);
71 deliveries[connection.id] = found.ok ? found.value.slice(0, 5) : [];
72 }),
73 );
74 const adding = new URL(request.url).searchParams.get("add");
75 return {
76 slug,
77 role,
Integrations are a directory of connectors, for a workspace and for each person78 kind,
Share the editor settings and Claude Code deploy permissions79 connections: all,
80 deliveries,
81 repos: listed.map((repo) => `${repo.namespace}/${repo.name}`),
82 adding: isProvider(adding) ? adding : null,
83 hostedOpen: access.hosted,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily84 hostedPreview: access.preview,
Share the editor settings and Claude Code deploy permissions85 trial: access.trial,
86 routes: routes.ok ? routes.value : [],
87 free: account.ok ? Boolean(account.value.status.free) : false,
88 marginPercent: account.ok ? account.value.marginPercent : 20,
89 };
90}
91
92function text(form: FormData, name: string): string | undefined {
93 const value = String(form.get(name) ?? "").trim();
94 return value || undefined;
95}
96
97/** The settings a form describes, for the provider it is for. */
98function configFrom(form: FormData): ConnectionConfig {
99 const keys = text(form, "keys");
100 return {
101 repo: text(form, "repo"),
102 assign: form.get("assign") === "on",
103 label: text(form, "label"),
104 writeBack: form.get("writeBack") !== "off",
105 organization: text(form, "organization"),
106 site: text(form, "site"),
107 email: text(form, "email"),
108 keys: keys ? keys.split(/[\s,]+/).filter(Boolean) : [],
109 baseUrl: text(form, "baseUrl"),
110 authHeader: text(form, "authHeader"),
111 model: text(form, "model"),
AI Gateway: OpenAI's format, open models, and your own providers112 // Only a model provider's form has the field; empty means none.
113 gatewayModels: form.has("gatewayModels") ? parseGatewayModels(String(form.get("gatewayModels"))) : undefined,
Share the editor settings and Claude Code deploy permissions114 };
115}
116
117export async function action({ request, params, context }: Route.ActionArgs) {
118 assertSameOrigin(request);
119 const user = requireUser(context, request);
120 const slug = params.owner.toLowerCase();
121 const form = await request.formData();
122 const intent = form.get("intent");
123 const id = String(form.get("id") ?? "");
124 if (intent === "disconnect") {
125 const removed = await integrations.disconnect(user, slug, id);
126 return { error: removed.ok ? null : removed.error.message };
127 }
128 if (intent === "test") {
129 const tested = await integrations.test(user, slug, id);
130 return tested.ok ? { tested: { id, ...tested.value } } : { error: tested.error.message };
131 }
132 if (intent === "routes") {
133 const routes: ModelRoute[] = [];
134 for (const task of MODEL_TASKS) {
135 const choice = String(form.get(`route-${task}`) ?? "");
136 if (!choice) continue;
137 if (choice === "g1t") routes.push({ task, connectionId: null, model: null });
Merge branch 'model-routing'138 else if (choice.startsWith("g1t::")) routes.push({ task, connectionId: null, model: choice.slice("g1t::".length) || null });
Share the editor settings and Claude Code deploy permissions139 else {
140 const [connectionId, model] = choice.split("::");
141 routes.push({ task, connectionId, model: model || null });
142 }
143 }
144 const saved = await integrations.setRoutes(user, slug, routes);
145 return saved.ok ? { routed: true } : { error: saved.error.message };
146 }
AI Gateway: OpenAI's format, open models, and your own providers147 if (intent === "gateway") {
148 // A model provider's AI Gateway models, and a new key if one was given:
149 // its other settings as they are.
150 const listed = await integrations.list(slug, user);
151 const connection = listed.ok ? listed.value.find((c) => c.id === id) : undefined;
152 if (!connection) return { error: listed.ok ? "No such integration." : listed.error.message };
153 const updated = await integrations.update(user, slug, id, {
154 config: { ...connection.config, gatewayModels: parseGatewayModels(String(form.get("gatewayModels") ?? "")) },
155 secret: text(form, "secret"),
156 });
157 return updated.ok ? { updated: id } : { error: updated.error.message };
158 }
Share the editor settings and Claude Code deploy permissions159 if (intent === "update") {
160 const updated = await integrations.update(user, slug, id, {
161 signingSecret: text(form, "signingSecret"),
162 secret: text(form, "secret"),
163 });
Merge branch 'worktree-agent-a985c01f43538af01'164 return updated.ok ? { updated: id } : { error: updated.error.message };
Share the editor settings and Claude Code deploy permissions165 }
166 const provider = String(form.get("provider") ?? "");
167 if (!isProvider(provider)) return { error: "Choose what to connect." };
168 const connected = await integrations.connect(user, slug, {
169 provider,
170 name: text(form, "name"),
171 config: configFrom(form),
172 secret: text(form, "secret"),
173 signingSecret: text(form, "signingSecret"),
174 });
175 return connected.ok ? { connected: connected.value } : { error: connected.error.message, provider };
176}
177
178const KIND_INFO: Record<ProviderKind, { title: string; icon: ReactNode; blurb: string }> = {
179 models: {
180 title: "Model providers",
181 icon: <Cpu size={16} />,
182 blurb: "Connect as many as you use, then choose which model does which work, and so who pays for it.",
183 },
184 alerts: {
185 title: "Alerts",
186 icon: <Siren size={16} />,
187 blurb: "Problems your monitoring finds become issues, and agents can start on them before anyone looks.",
188 },
189 tracker: {
190 title: "Trackers",
191 icon: <Ticket size={16} />,
192 blurb: "Tickets agents can read when work mentions them, that people can import as issues, and that hear back when the work lands.",
193 },
194};
195
196const PROVIDER_BLURB: Partial<Record<Provider, string>> = {
197 sentry: "New errors open issues, with the stack trace. Resolved in Sentry when the fix merges.",
198 datadog: "Monitors that trigger open issues. Recoveries are noted on them.",
199 webhook: "Anything that can send signed JSON: PagerDuty, Grafana, your own scripts.",
200 jira: "Agents read TECH-1234 when work mentions it. Import tickets; they hear back.",
201 linear: "Agents read ENG-42 when work mentions it. Import issues; they hear back.",
202};
203
204export default function WorkspaceIntegrations({ loaderData, actionData }: Route.ComponentProps) {
Integrations are a directory of connectors, for a workspace and for each person205 const { slug, role, kind, connections, deliveries, repos: repoNames, adding, free, marginPercent, hostedOpen, hostedPreview, trial, routes } =
Share the editor settings and Claude Code deploy permissions206 loaderData;
207 const owner = role === "owner";
208 const modelConnections = connections.filter((connection) => connection.kind === "models");
209 const justConnected = actionData && "connected" in actionData ? actionData.connected : null;
210 const tested = (actionData && "tested" in actionData ? actionData.tested : null) ?? null;
211 const error = (actionData && "error" in actionData ? actionData.error : null) ?? null;
Merge branch 'worktree-agent-a985c01f43538af01'212 const updated = (actionData && "updated" in actionData ? actionData.updated : null) ?? null;
Share the editor settings and Claude Code deploy permissions213
214 return (
215 <div>
Integrations are a directory of connectors, for a workspace and for each person216 <Link to={`/${slug}/-/integrations`} className="inline-flex items-center gap-1.5 text-sm text-muted hover:text-fg">
217 <ArrowLeft size={14} /> All integrations
218 </Link>
Share the editor settings and Claude Code deploy permissions219 {justConnected && <Connected connected={justConnected} />}
220 <div className="mt-4">
221 <ErrorText>{error}</ErrorText>
222 </div>
223
224 {/* Models -------------------------------------------------------------- */}
Integrations are a directory of connectors, for a workspace and for each person225 {kind === "models" && (
226 <Section kind="models">
Share the editor settings and Claude Code deploy permissions227 {!hostedOpen && modelConnections.length === 0 && (
228 <div className="mb-4 flex items-center gap-3 rounded-xl border border-warn/30 bg-warn/5 p-4">
229 <span className="inline-flex size-8 shrink-0 items-center justify-center rounded-lg bg-warn/10 text-warn ring-1 ring-warn/30">
230 <AlertTriangle size={16} />
231 </span>
232 <p className="text-sm text-muted">
233 <span className="font-medium text-fg">Choose how your agents reach a model.</span>{" "}
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily234 {hostedPreview
235 ? `During the preview, g1t's hosted models run only for a few invited workspaces, and ${slug} is not one of them yet.`
236 : (trialClosed(trial, slug) ?? `g1t's hosted models are not open to ${slug}.`)}{" "}
Share the editor settings and Claude Code deploy permissions237 Connect a provider of your own below and your agents start at once, billed by that provider.
238 </p>
239 </div>
240 )}
Merge branch 'worktree-agent-a985c01f43538af01'241 <Connections list={modelConnections} owner={owner} tested={tested} updated={updated} deliveries={deliveries} />
Share the editor settings and Claude Code deploy permissions242 {modelConnections.length === 0 && hostedOpen && (
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.243 <Card asChild className="flex items-center gap-2 px-4 py-3 text-sm text-muted">
244 <p>
245 <CheckCircle2 size={15} className="shrink-0 text-accent" />
246 {trial?.open
247 ? trial.granted
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.248 ? `All work runs on g1t's models, paid by ${slug}'s trial credit first: ${money(Math.max(0, trial.limitMicros - trial.usedMicros))} of ${money(trial.limitMicros)} left. Connect a provider of your own for more, or to choose models.`
249 : `All work runs on g1t's models. ${slug} gets ${money(trial.limitMicros)} of trial credit the first time its agents work. Connect a provider of your own for more, or to choose models.`
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.250 : free
251 ? "All work runs on g1t's models, free while g1t is being built out. Connect a provider of your own to choose models and pay for them there."
252 : "All work runs on g1t's models, charged to your AI credit at the provider's price plus the agent rate. Auto picks the model for each job; choose one per kind of work below, or connect a provider of your own and pay for its models there."}
253 </p>
254 </Card>
Share the editor settings and Claude Code deploy permissions255 )}
Merge branch 'model-routing'256 {(modelConnections.length > 0 || hostedOpen) && (
Share the editor settings and Claude Code deploy permissions257 <Routing
Merge branch 'worktree-agent-a985c01f43538af01'258 // Started again from what is saved whenever that changes, such as a provider disconnected.
259 key={JSON.stringify([routes, modelConnections.map((c) => c.id)])}
Share the editor settings and Claude Code deploy permissions260 connections={modelConnections}
261 routes={routes}
262 hostedOpen={hostedOpen}
263 owner={owner}
264 saved={actionData != null && "routed" in actionData}
265 />
266 )}
267 <p className="mt-3 text-xs text-faint">
268 On your own providers, their bills are yours.{" "}
269 {free
Merge branch 'model-routing'270 ? `g1t charges nothing while it is being built out; once pricing starts, each run's sandbox time at cost plus ${marginPercent}%, and the agent rate on the tokens it used.`
271 : `g1t charges each run's sandbox time at cost plus ${marginPercent}%, and the agent rate on the tokens it used, shown on Usage as "Agent rate, your own model key".`}{" "}
Share the editor settings and Claude Code deploy permissions272 Keys go only from g1t's model proxy to the provider: the agent's
273 sandbox holds a token that dies with the run.
274 </p>
275 {owner && <ModelCatalog slug={slug} adding={adding} />}
276 {adding && PROVIDERS[adding].kind === "models" && owner && (
277 <AddForm
278 provider={adding}
279 slug={slug}
280 repos={repoNames}
281 error={actionData && "provider" in actionData ? error : null}
282 />
283 )}
284 </Section>
Integrations are a directory of connectors, for a workspace and for each person285 )}
Share the editor settings and Claude Code deploy permissions286
287 {/* Alerts -------------------------------------------------------------- */}
Integrations are a directory of connectors, for a workspace and for each person288 {kind === "alerts" && (
Share the editor settings and Claude Code deploy permissions289 <Section kind="alerts">
290 <Connections
291 list={connections.filter((c) => c.kind === "alerts")}
292 owner={owner}
293 tested={tested}
Merge branch 'worktree-agent-a985c01f43538af01'294 updated={updated}
Share the editor settings and Claude Code deploy permissions295 deliveries={deliveries}
296 />
297 {owner && <Choices kind="alerts" slug={slug} adding={adding} />}
298 {adding && PROVIDERS[adding].kind === "alerts" && owner && (
299 <AddForm
300 provider={adding}
301 slug={slug}
302 repos={repoNames}
303 error={actionData && "provider" in actionData ? error : null}
304 />
305 )}
306 </Section>
Integrations are a directory of connectors, for a workspace and for each person307 )}
Share the editor settings and Claude Code deploy permissions308
309 {/* Trackers ------------------------------------------------------------ */}
Integrations are a directory of connectors, for a workspace and for each person310 {kind === "tracker" && (
Share the editor settings and Claude Code deploy permissions311 <Section kind="tracker">
312 <Connections
313 list={connections.filter((c) => c.kind === "tracker")}
314 owner={owner}
315 tested={tested}
Merge branch 'worktree-agent-a985c01f43538af01'316 updated={updated}
Share the editor settings and Claude Code deploy permissions317 deliveries={deliveries}
318 />
319 {owner && <Choices kind="tracker" slug={slug} adding={adding} />}
320 {adding && PROVIDERS[adding].kind === "tracker" && owner && (
321 <AddForm
322 provider={adding}
323 slug={slug}
324 repos={repoNames}
325 error={actionData && "provider" in actionData ? error : null}
326 />
327 )}
328 </Section>
Integrations are a directory of connectors, for a workspace and for each person329 )}
Share the editor settings and Claude Code deploy permissions330
331 {!owner && <p className="mt-8 text-sm text-muted">An owner of {slug} can add and remove integrations.</p>}
332 </div>
333 );
334}
335
Integrations are a directory of connectors, for a workspace and for each person336/** One kind of connection's page: its heading, then what is connected and what to connect. */
337function Section({ kind, children }: { kind: ProviderKind; children: ReactNode }) {
Share the editor settings and Claude Code deploy permissions338 const info = KIND_INFO[kind];
339 return (
Integrations are a directory of connectors, for a workspace and for each person340 <section className="mt-4">
341 <h1 className="flex items-center gap-2 text-2xl font-semibold tracking-tight [&_svg]:size-5">
Share the editor settings and Claude Code deploy permissions342 <span className="text-muted">{info.icon}</span>
343 {info.title}
Integrations are a directory of connectors, for a workspace and for each person344 </h1>
345 <p className="mt-1.5 mb-6 border-b border-line pb-6 text-sm text-muted">{info.blurb}</p>
Share the editor settings and Claude Code deploy permissions346 {children}
347 </section>
348 );
349}
350
351function Connections({
352 list,
353 owner,
354 tested,
Merge branch 'worktree-agent-a985c01f43538af01'355 updated,
Share the editor settings and Claude Code deploy permissions356 deliveries,
357}: {
358 list: Connection[];
359 owner: boolean;
360 tested: { id: string; ok: boolean; message: string } | null;
Merge branch 'worktree-agent-a985c01f43538af01'361 /** The connection whose secret was just saved. */
362 updated: string | null;
Share the editor settings and Claude Code deploy permissions363 deliveries: Record<string, Delivery[]>;
364}) {
365 if (list.length === 0) return null;
366 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.367 <Card asChild divided className="mb-4 overflow-hidden">
368 <ul>
369 {list.map((connection) => (
370 <li key={connection.id} className="p-4">
371 <ConnectionRow
372 connection={connection}
373 owner={owner}
374 tested={tested}
375 updated={updated === connection.id}
376 deliveries={deliveries[connection.id] ?? []}
377 />
378 </li>
379 ))}
380 </ul>
381 </Card>
Share the editor settings and Claude Code deploy permissions382 );
383}
384
385/** Just the host of an address, which is what tells connections apart. */
386function host(url: string | undefined): string | undefined {
387 if (!url) return undefined;
388 try {
389 return new URL(url).host;
390 } catch {
391 return url;
392 }
393}
394
395function ConnectionRow({
396 connection,
397 owner,
398 tested,
Merge branch 'worktree-agent-a985c01f43538af01'399 updated,
Share the editor settings and Claude Code deploy permissions400 deliveries,
401}: {
402 connection: Connection;
403 owner: boolean;
404 tested: { id: string; ok: boolean; message: string } | null;
Merge branch 'worktree-agent-a985c01f43538af01'405 updated: boolean;
Share the editor settings and Claude Code deploy permissions406 deliveries: Delivery[];
407}) {
408 const { config } = connection;
409 const facts = [
410 PROVIDERS[connection.provider].label,
411 config.repo && `issues in ${config.repo}`,
412 config.assign && "agents start at once",
413 config.organization,
414 host(config.site),
415 host(config.baseUrl),
416 config.model && `default ${config.model}`,
417 connection.models.length > 0 && `${connection.models.length} models`,
418 config.keys?.length ? config.keys.join(", ") : null,
419 connection.secretHint && `key ${connection.secretHint}`,
420 ].filter(Boolean);
AI Gateway: OpenAI's format, open models, and your own providers421 const gateway = connection.kind === "models" ? gatewayPatterns(connection.provider, config) : null;
Share the editor settings and Claude Code deploy permissions422 const waitingForSecret = connection.provider === "sentry" && !deliveries.length && !connection.lastUsedAt;
423 return (
424 <div>
425 <div className="flex items-center gap-3">
426 <ProviderMark provider={connection.provider} />
427 <div className="min-w-0 grow">
428 <p className="truncate text-sm font-medium">{connection.name}</p>
429 <p className="truncate text-xs text-muted">{facts.join(" · ")}</p>
430 </div>
431 {connection.lastUsedAt && (
432 <span className="hidden shrink-0 text-xs text-faint sm:inline">
433 Used <TimeAgo at={connection.lastUsedAt} />
434 </span>
435 )}
436 {owner && (
437 <Form method="post" className="flex shrink-0 gap-2">
438 <input type="hidden" name="id" value={connection.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.439 <SubmitButton variant="outline" name="intent" value="test" match={{ id: connection.id }} pending="Testing…">
Share the editor settings and Claude Code deploy permissions440 Test
Merge branch 'worktree-agent-a985c01f43538af01'441 </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.442 <SubmitButton variant="outline" name="intent" value="disconnect" match={{ id: connection.id }} pending="Disconnecting…" aria-label="Disconnect">
Share the editor settings and Claude Code deploy permissions443 <X size={14} />
Merge branch 'worktree-agent-a985c01f43538af01'444 </SubmitButton>
Share the editor settings and Claude Code deploy permissions445 </Form>
446 )}
447 </div>
448 {tested?.id === connection.id && (
Merge branch 'worktree-agent-ad7c6d88d93adc817'449 <p className={`mt-3 text-sm ${tested.ok ? "text-success" : "text-danger"}`}>{tested.message}</p>
Share the editor settings and Claude Code deploy permissions450 )}
451 {connection.lastError && (
452 <p className="mt-3 flex items-start gap-2 text-xs text-warn">
453 <AlertTriangle size={13} className="mt-0.5 shrink-0" />
454 {connection.lastError}
455 </p>
456 )}
AI Gateway: OpenAI's format, open models, and your own providers457 {gateway && (
458 <div className="mt-3 flex flex-wrap items-center gap-1.5 text-xs text-muted">
459 <span className="text-faint">AI Gateway</span>
460 {gateway.length === 0 ? (
461 <span>no models</span>
462 ) : (
463 gateway.map((pattern) => (
464 <code key={pattern} className="rounded border border-line bg-bg px-1.5 py-0.5 font-mono text-[0.75rem] text-fg">
465 {pattern}
466 </code>
467 ))
468 )}
469 </div>
470 )}
471 {owner && gateway && (
472 <details className="group mt-3" open={updated || undefined}>
473 <summary className="inline-flex cursor-pointer list-none items-center gap-1 text-xs text-muted hover:text-fg">
474 <ChevronRight size={12} className="transition-transform group-open:rotate-90" />
475 Change its AI Gateway models or key
476 </summary>
477 <Form method="post" className="mt-3 max-w-2xl space-y-3">
478 <input type="hidden" name="intent" value="gateway" />
479 <input type="hidden" name="id" value={connection.id} />
480 <GatewayModelsField provider={connection.provider} value={gateway} />
481 <Field label="Replace the key" hint="Write-only: g1t never shows it again. Leave it empty to keep the key you have.">
482 <Input name="secret" type="password" placeholder={connection.secretHint ? `Now ${connection.secretHint}` : "Paste a key"} autoComplete="new-password" className="max-w-sm" />
483 </Field>
484 <div className="flex 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.485 <SubmitButton variant="outline" match={{ intent: "gateway", id: connection.id }} pending="Saving…">
AI Gateway: OpenAI's format, open models, and your own providers486 Save
487 </SubmitButton>
488 {updated && <span className="text-sm text-success">Saved.</span>}
489 </div>
490 </Form>
491 </details>
492 )}
Share the editor settings and Claude Code deploy permissions493 {connection.webhookUrl && (
494 <div className="mt-3">
495 <p className="mb-1.5 text-xs text-faint">Where it sends alerts</p>
496 <CopyLine text={connection.webhookUrl} />
497 </div>
498 )}
499 {owner && waitingForSecret && (
500 <Form method="post" className="mt-3 flex flex-wrap items-end gap-2">
501 <input type="hidden" name="intent" value="update" />
502 <input type="hidden" name="id" value={connection.id} />
503 <div className="min-w-64 grow">
504 <Field label="Client secret" hint="From the internal integration's page in Sentry, once it has this address. Leave it if it is already saved.">
505 <Input name="signingSecret" type="password" placeholder="Paste to replace" />
506 </Field>
507 </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.508 <SubmitButton variant="outline" match={{ intent: "update", id: connection.id }} pending="Saving…">
Share the editor settings and Claude Code deploy permissions509 Save
Merge branch 'worktree-agent-a985c01f43538af01'510 </SubmitButton>
511 {updated && <span className="pb-2 text-sm text-muted">Saved.</span>}
Share the editor settings and Claude Code deploy permissions512 </Form>
513 )}
514 {deliveries.length > 0 && (
515 <ul className="mt-3 space-y-1 text-xs">
516 {deliveries.map((delivery) => (
517 <li key={delivery.id} className="flex items-center gap-2 text-muted">
518 <span
519 className={`size-1.5 shrink-0 rounded-full ${
520 delivery.outcome === "refused"
521 ? "bg-danger"
522 : delivery.outcome === "ignored"
523 ? "bg-faint"
Merge branch 'worktree-agent-ad7c6d88d93adc817'524 : "bg-success"
Share the editor settings and Claude Code deploy permissions525 }`}
526 />
527 <span className="font-mono text-faint">{delivery.event}</span>
528 <span className="min-w-0 truncate">{delivery.detail}</span>
529 {delivery.issue && (
530 <Link
531 to={`/${delivery.issue.replace("#", "/issues/")}`}
532 className="shrink-0 font-mono text-fg hover:underline"
533 >
534 {delivery.issue}
535 </Link>
536 )}
537 <span className="ml-auto shrink-0 text-faint">
538 <TimeAgo at={delivery.receivedAt} />
539 </span>
540 </li>
541 ))}
542 </ul>
543 )}
544 </div>
545 );
546}
547
548/** What a provider is for, in a line. */
549function blurb(provider: Provider): string {
550 return PROVIDER_BLURB[provider] ?? MODEL_CATALOG[provider]?.blurb ?? "";
551}
552
553function Choices({ kind, slug, adding }: { kind: ProviderKind; slug: string; adding: Provider | null }) {
554 const providers = (Object.keys(PROVIDERS) as Provider[]).filter((p) => PROVIDERS[p].kind === kind);
555 return (
556 <div className="mt-3">
557 <ProviderTiles providers={providers} slug={slug} adding={adding} blurb={blurb} />
558 </div>
559 );
560}
561
562function RepoField({ repos, required, hint }: { repos: string[]; required?: boolean; hint: string }) {
563 return (
564 <Field label="Repository" hint={hint}>
Workspace names and icons, and a component kit for every control565 <Combobox
Share the editor settings and Claude Code deploy permissions566 name="repo"
567 defaultValue={repos[0] ?? ""}
Workspace names and icons, and a component kit for every control568 aria-label="Repository"
569 placeholder={repos.length === 0 ? "No repositories yet" : "Choose a repository"}
570 searchPlaceholder="Find a repository"
571 emptyText="No repository by that name."
572 options={[
573 ...(required ? [] : [{ value: "", label: "None" }]),
574 ...repos.map((repo) => ({ value: repo, label: repo, icon: <Avatar name={repo.split("/").pop() ?? repo} size={16} square /> })),
575 ]}
576 className="font-mono text-[0.8125rem]"
577 />
Share the editor settings and Claude Code deploy permissions578 </Field>
579 );
580}
581
582function AssignField() {
583 return (
Workspace names and icons, and a component kit for every control584 <CheckboxOption
585 name="assign"
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent586 label={<span className="font-medium">Assign each new issue to g1t</span>}
Workspace names and icons, and a component kit for every control587 description={
588 <span className="text-muted">
589 It opens a pull request, gets reviewed, and lands through your merge rules, before anyone has to look.
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent590 Agents run only where g1t is enabled.
Share the editor settings and Claude Code deploy permissions591 </span>
Workspace names and icons, and a component kit for every control592 }
593 />
Share the editor settings and Claude Code deploy permissions594 );
595}
596
597function AddForm({
598 provider,
599 slug,
600 repos,
601 error,
602}: {
603 provider: Provider;
604 slug: string;
605 repos: string[];
606 error: string | null;
607}) {
608 const fields: Partial<Record<Provider, ReactNode>> = {
609 sentry: (
610 <>
611 <Field label="Organization" hint="Its slug, as in acme.sentry.io.">
612 <Input name="organization" required placeholder="acme" />
613 </Field>
614 <RepoField repos={repos} required hint="Where issues are opened." />
615 <Field label="Auth token" hint="The internal integration's token, with Issue & Event read and write. Lets g1t read stack traces and resolve issues. Optional, but recommended.">
616 <Input name="secret" type="password" />
617 </Field>
618 <Field label="Client secret" hint="You get this from Sentry after giving it this connection's address; you can add it on the next step.">
619 <Input name="signingSecret" type="password" />
620 </Field>
621 <AssignField />
622 </>
623 ),
624 datadog: (
625 <>
626 <RepoField repos={repos} required hint="Where issues are opened." />
627 <AssignField />
628 </>
629 ),
630 webhook: (
631 <>
632 <RepoField repos={repos} required hint="Where issues are opened." />
633 <AssignField />
634 </>
635 ),
636 jira: (
637 <>
638 <Field label="Site" hint="Your Jira's address.">
639 <Input name="site" type="url" required placeholder="https://acme.atlassian.net" />
640 </Field>
641 <Field label="Email" hint="The account the API token belongs to.">
642 <Input name="email" type="email" required autoComplete="off" />
643 </Field>
644 <Field label="API token" hint="From id.atlassian.com → Security → API tokens.">
645 <Input name="secret" type="password" required />
646 </Field>
647 <Field label="Project keys" hint="Optional. The projects this answers for, such as TECH, OPS. Empty answers for any key.">
648 <Input name="keys" placeholder="TECH, OPS" />
649 </Field>
650 </>
651 ),
652 linear: (
653 <>
654 <Field label="API key" hint="From Linear → Settings → Security & access → Personal API keys.">
655 <Input name="secret" type="password" required placeholder="lin_api_…" />
656 </Field>
657 <Field label="Team keys" hint="Optional. Such as ENG. Empty answers for any key.">
658 <Input name="keys" placeholder="ENG" />
659 </Field>
660 </>
661 ),
662 };
663 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.664 <Card asChild className="mt-4 scroll-mt-20 p-5">
665 <section id="add">
666 <div className="flex items-center gap-3">
667 <ProviderMark provider={provider} />
668 <div className="grow">
669 <h3 className="font-medium">
670 Connect {provider.endsWith("_endpoint") ? `an ${PROVIDERS[provider].label}` : PROVIDERS[provider].label}
671 </h3>
672 <p className="text-xs text-muted">{blurb(provider)}</p>
673 </div>
674 <Link
675 to={`/${slug}/-/integrations/${integrationsSection(PROVIDERS[provider].kind)}`}
676 className="rounded-md p-1.5 text-faint hover:bg-raised hover:text-fg"
677 aria-label="Close"
678 >
679 <X size={16} />
680 </Link>
Share the editor settings and Claude Code deploy permissions681 </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.682 <Form method="post" className="mt-5 grid max-w-xl gap-4">
683 <input type="hidden" name="provider" value={provider} />
684 {fields[provider] ?? <ModelProviderFields provider={provider} />}
685 <ErrorText>{error}</ErrorText>
686 <div>
687 <SubmitButton match={{ provider }} pending="Connecting…">
688 Connect
689 </SubmitButton>
690 </div>
691 </Form>
692 </section>
693 </Card>
Share the editor settings and Claude Code deploy permissions694 );
695}
696
697/** What to do next, right after connecting: shown once. */
698function Connected({ connected }: { connected: { connection: Connection; signingSecret: string | null } }) {
699 const { connection, signingSecret } = connected;
700 const url = connection.webhookUrl;
701 return (
Merge branch 'worktree-agent-ad7c6d88d93adc817'702 <div className="mt-6 rounded-xl border border-success-dim/60 bg-success/5 p-5">
Share the editor settings and Claude Code deploy permissions703 <p className="flex items-center gap-2 font-medium">
Merge branch 'worktree-agent-ad7c6d88d93adc817'704 <CheckCircle2 size={16} className="text-success" />
Share the editor settings and Claude Code deploy permissions705 {connection.name} is connected.
706 </p>
707 {connection.provider === "sentry" && url && (
708 <ol className="mt-3 list-decimal space-y-2 pl-5 text-sm text-muted">
709 <li>
710 In Sentry, open Settings → Developer Settings → Custom Integrations, and create an internal
711 integration.
712 </li>
713 <li>
714 Set its webhook URL to <CopyLine text={url} />
715 </li>
716 <li>Turn on Alert Rule Action, and under Webhooks, tick issue.</li>
717 <li>Give it Issue &amp; Event read and write, save, and paste its client secret below, on the connection.</li>
718 </ol>
719 )}
720 {(connection.provider === "datadog" || connection.provider === "webhook") && url && signingSecret && (
721 <div className="mt-3 space-y-3 text-sm text-muted">
722 <p>This secret is shown once. Copy it now.</p>
723 <CopyLine text={signingSecret} />
724 {connection.provider === "datadog" ? (
725 <>
726 <p>
727 In Datadog, open Integrations → Webhooks and add one named <code>g1t</code> with this URL:
728 </p>
729 <CopyLine text={url} />
730 <p>Custom headers:</p>
731 <CopyLine text={`{"Authorization": "Bearer ${signingSecret}"}`} />
732 <p>Payload:</p>
733 <CopyLine
734 text={`{"id": "$ALERT_ID", "title": "$EVENT_TITLE", "body": "$EVENT_MSG", "url": "$LINK", "status": "$ALERT_TRANSITION", "priority": "$PRIORITY"}`}
735 />
736 <p>
737 Then mention <code>@webhook-g1t</code> in any monitor's message.
738 </p>
739 </>
740 ) : (
741 <>
742 <p>Send JSON with at least a title to:</p>
743 <CopyLine text={url} />
744 <p>
745 with <code>Authorization: Bearer &lt;secret&gt;</code>, or an HMAC-SHA256 of the body in{" "}
746 <code>X-G1t-Signature</code>. An <code>id</code> keeps repeats on one issue.
747 </p>
748 </>
749 )}
750 </div>
751 )}
752 {connection.kind === "tracker" && (
753 <p className="mt-2 text-sm text-muted">
754 Agents now read tickets that work mentions, and you can import one from any repository's new
755 issue page. Use Test to check the token.
756 </p>
757 )}
758 {connection.kind === "models" && (
759 <p className="mt-2 text-sm text-muted">
760 {connection.lastError
761 ? `It was saved, but the check failed: ${connection.lastError}`
762 : connection.models.length > 0
763 ? `It offers ${connection.models.length} models. Choose which work goes to it under “Which model does which work”.`
764 : "Choose which work goes to it under “Which model does which work”."}
765 </p>
766 )}
767 </div>
768 );
769}

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