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

Integrations: your own model provider, alerts that open issues, tickets agents read1import {
2 AlertTriangle,
Integrations are a directory of connectors, for a workspace and for each person3 ArrowLeft,
Integrations: your own model provider, alerts that open issues, tickets agents read4 CheckCircle2,
5 ChevronRight,
6 Cpu,
7 Siren,
8 Ticket,
9 X,
10} from "lucide-react";
Models per workspace: several providers, routed by kind of work11import { env } from "cloudflare:workers";
Integrations: your own model provider, alerts that open issues, tickets agents read12import type { ReactNode } from "react";
Merge branch 'worktree-agent-a985c01f43538af01'13import { Form, Link } from "react-router";
Integrations: your own model provider, alerts that open issues, tickets agents read14
15import {
16 type Connection,
17 type ConnectionConfig,
18 type Delivery,
Models per workspace: several providers, routed by kind of work19 MODEL_TASKS,
20 type ModelRoute,
Integrations: your own model provider, alerts that open issues, tickets agents read21 type Provider,
22 type ProviderKind,
23 PROVIDERS,
AI Gateway: OpenAI's format, open models, and your own providers24 gatewayPatterns,
Integrations: your own model provider, alerts that open issues, tickets agents read25} 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";
Merge branch 'worktree-agent-a985c01f43538af01'33import { Avatar, CopyLine, ErrorText, Field, Input, SubmitButton, TimeAgo } from "../../components/ui";
Workspace names and icons, and a component kit for every control34import { CheckboxOption } from "../../components/ui/checkbox";
35import { Combobox } from "../../components/ui/combobox";
Integrations: your own model provider, alerts that open issues, tickets agents read36import { billing, integrations, repos } from "../../lib/services.server";
37import { assertSameOrigin, getViewer, requireUser, roleIn, unwrap } from "../../lib/session.server";
38
Social cards for every page: og.g1t.sh39export function meta({ params, ...args }: Route.MetaArgs) {
Integrations are a directory of connectors, for a workspace and for each person40 const kind = sectionKind(params.section);
41 return page(args, { title: `${kind ? KIND_INFO[kind].title : "Integrations"} · ${params.owner} · g1t` });
Integrations: your own model provider, alerts that open issues, tickets agents read42}
43
44const isProvider = (value: string | null): value is Provider => value != null && value in PROVIDERS;
45
46export async function loader({ params, context, request }: Route.LoaderArgs) {
47 const viewer = getViewer(context);
48 const role = roleIn(viewer, params.owner);
Integrations are a directory of connectors, for a workspace and for each person49 // One kind of connection's page: the directory at `-/integrations` finds them.
50 const kind = sectionKind(params.section);
51 if (!role || !kind) throw new Response(null, { status: 404 });
Integrations: your own model provider, alerts that open issues, tickets agents read52 const slug = params.owner.toLowerCase();
Models per workspace: several providers, routed by kind of work53 const [connections, listed, account, access, routes] = await Promise.all([
Integrations: your own model provider, alerts that open issues, tickets agents read54 integrations.list(slug, viewer),
55 repos.list(viewer, { namespace: slug }),
56 billing.account(slug, viewer),
Models per workspace: several providers, routed by kind of work57 env.RUNNER.modelAccess(slug),
58 integrations.routes(slug, viewer),
Integrations: your own model provider, alerts that open issues, tickets agents read59 ]);
60 const all = unwrap(connections);
61 // What each alert source has sent lately, to see it is wired up.
62 const deliveries: Record<string, Delivery[]> = {};
63 await Promise.all(
64 all
65 .filter((connection) => connection.kind === "alerts")
66 .map(async (connection) => {
67 const found = await integrations.deliveries(slug, viewer, connection.id);
68 deliveries[connection.id] = found.ok ? found.value.slice(0, 5) : [];
69 }),
70 );
71 const adding = new URL(request.url).searchParams.get("add");
72 return {
73 slug,
74 role,
Integrations are a directory of connectors, for a workspace and for each person75 kind,
Integrations: your own model provider, alerts that open issues, tickets agents read76 connections: all,
77 deliveries,
78 repos: listed.map((repo) => `${repo.namespace}/${repo.name}`),
79 adding: isProvider(adding) ? adding : null,
Models per workspace: several providers, routed by kind of work80 hostedOpen: access.hosted,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily81 hostedPreview: access.preview,
A free allowance on g1t's models, so anyone can try its agents82 trial: access.trial,
Models per workspace: several providers, routed by kind of work83 routes: routes.ok ? routes.value : [],
New workspaces are told to connect a model, before anything fails84 free: account.ok ? Boolean(account.value.status.free) : false,
Integrations: your own model provider, alerts that open issues, tickets agents read85 marginPercent: account.ok ? account.value.marginPercent : 20,
86 };
87}
88
89function text(form: FormData, name: string): string | undefined {
90 const value = String(form.get(name) ?? "").trim();
91 return value || undefined;
92}
93
94/** The settings a form describes, for the provider it is for. */
95function configFrom(form: FormData): ConnectionConfig {
96 const keys = text(form, "keys");
97 return {
98 repo: text(form, "repo"),
99 assign: form.get("assign") === "on",
100 label: text(form, "label"),
101 writeBack: form.get("writeBack") !== "off",
102 organization: text(form, "organization"),
103 site: text(form, "site"),
104 email: text(form, "email"),
105 keys: keys ? keys.split(/[\s,]+/).filter(Boolean) : [],
106 baseUrl: text(form, "baseUrl"),
107 authHeader: text(form, "authHeader"),
108 model: text(form, "model"),
AI Gateway: OpenAI's format, open models, and your own providers109 // Only a model provider's form has the field; empty means none.
110 gatewayModels: form.has("gatewayModels") ? parseGatewayModels(String(form.get("gatewayModels"))) : undefined,
Integrations: your own model provider, alerts that open issues, tickets agents read111 };
112}
113
114export async function action({ request, params, context }: Route.ActionArgs) {
115 assertSameOrigin(request);
116 const user = requireUser(context, request);
117 const slug = params.owner.toLowerCase();
118 const form = await request.formData();
119 const intent = form.get("intent");
120 const id = String(form.get("id") ?? "");
121 if (intent === "disconnect") {
122 const removed = await integrations.disconnect(user, slug, id);
123 return { error: removed.ok ? null : removed.error.message };
124 }
125 if (intent === "test") {
126 const tested = await integrations.test(user, slug, id);
127 return tested.ok ? { tested: { id, ...tested.value } } : { error: tested.error.message };
128 }
Models per workspace: several providers, routed by kind of work129 if (intent === "routes") {
130 const routes: ModelRoute[] = [];
131 for (const task of MODEL_TASKS) {
132 const choice = String(form.get(`route-${task}`) ?? "");
133 if (!choice) continue;
134 if (choice === "g1t") routes.push({ task, connectionId: null, model: null });
Merge branch 'model-routing'135 else if (choice.startsWith("g1t::")) routes.push({ task, connectionId: null, model: choice.slice("g1t::".length) || null });
Models per workspace: several providers, routed by kind of work136 else {
137 const [connectionId, model] = choice.split("::");
138 routes.push({ task, connectionId, model: model || null });
139 }
140 }
141 const saved = await integrations.setRoutes(user, slug, routes);
142 return saved.ok ? { routed: true } : { error: saved.error.message };
143 }
AI Gateway: OpenAI's format, open models, and your own providers144 if (intent === "gateway") {
145 // A model provider's AI Gateway models, and a new key if one was given:
146 // its other settings as they are.
147 const listed = await integrations.list(slug, user);
148 const connection = listed.ok ? listed.value.find((c) => c.id === id) : undefined;
149 if (!connection) return { error: listed.ok ? "No such integration." : listed.error.message };
150 const updated = await integrations.update(user, slug, id, {
151 config: { ...connection.config, gatewayModels: parseGatewayModels(String(form.get("gatewayModels") ?? "")) },
152 secret: text(form, "secret"),
153 });
154 return updated.ok ? { updated: id } : { error: updated.error.message };
155 }
Integrations: your own model provider, alerts that open issues, tickets agents read156 if (intent === "update") {
157 const updated = await integrations.update(user, slug, id, {
158 signingSecret: text(form, "signingSecret"),
159 secret: text(form, "secret"),
160 });
Merge branch 'worktree-agent-a985c01f43538af01'161 return updated.ok ? { updated: id } : { error: updated.error.message };
Integrations: your own model provider, alerts that open issues, tickets agents read162 }
163 const provider = String(form.get("provider") ?? "");
164 if (!isProvider(provider)) return { error: "Choose what to connect." };
165 const connected = await integrations.connect(user, slug, {
166 provider,
167 name: text(form, "name"),
168 config: configFrom(form),
169 secret: text(form, "secret"),
170 signingSecret: text(form, "signingSecret"),
171 });
172 return connected.ok ? { connected: connected.value } : { error: connected.error.message, provider };
173}
174
175const KIND_INFO: Record<ProviderKind, { title: string; icon: ReactNode; blurb: string }> = {
176 models: {
Model providers: gateway tokens for endpoints, tidier rows, and the docs177 title: "Model providers",
Integrations: your own model provider, alerts that open issues, tickets agents read178 icon: <Cpu size={16} />,
Model providers: gateway tokens for endpoints, tidier rows, and the docs179 blurb: "Connect as many as you use, then choose which model does which work, and so who pays for it.",
Integrations: your own model provider, alerts that open issues, tickets agents read180 },
181 alerts: {
182 title: "Alerts",
183 icon: <Siren size={16} />,
184 blurb: "Problems your monitoring finds become issues, and agents can start on them before anyone looks.",
185 },
186 tracker: {
187 title: "Trackers",
188 icon: <Ticket size={16} />,
189 blurb: "Tickets agents can read when work mentions them, that people can import as issues, and that hear back when the work lands.",
190 },
191};
192
A catalogue of model providers, and settings that feel like settings193const PROVIDER_BLURB: Partial<Record<Provider, string>> = {
Integrations: your own model provider, alerts that open issues, tickets agents read194 sentry: "New errors open issues, with the stack trace. Resolved in Sentry when the fix merges.",
195 datadog: "Monitors that trigger open issues. Recoveries are noted on them.",
196 webhook: "Anything that can send signed JSON: PagerDuty, Grafana, your own scripts.",
197 jira: "Agents read TECH-1234 when work mentions it. Import tickets; they hear back.",
198 linear: "Agents read ENG-42 when work mentions it. Import issues; they hear back.",
199};
200
201function dollars(micros: number): string {
202 return `$${(micros / 1_000_000).toFixed(2)}`;
203}
204
205export default function WorkspaceIntegrations({ loaderData, actionData }: Route.ComponentProps) {
Integrations are a directory of connectors, for a workspace and for each person206 const { slug, role, kind, connections, deliveries, repos: repoNames, adding, free, marginPercent, hostedOpen, hostedPreview, trial, routes } =
Models per workspace: several providers, routed by kind of work207 loaderData;
Integrations: your own model provider, alerts that open issues, tickets agents read208 const owner = role === "owner";
Models per workspace: several providers, routed by kind of work209 const modelConnections = connections.filter((connection) => connection.kind === "models");
Integrations: your own model provider, alerts that open issues, tickets agents read210 const justConnected = actionData && "connected" in actionData ? actionData.connected : null;
211 const tested = (actionData && "tested" in actionData ? actionData.tested : null) ?? null;
212 const error = (actionData && "error" in actionData ? actionData.error : null) ?? null;
Merge branch 'worktree-agent-a985c01f43538af01'213 const updated = (actionData && "updated" in actionData ? actionData.updated : null) ?? null;
Integrations: your own model provider, alerts that open issues, tickets agents read214
215 return (
A catalogue of model providers, and settings that feel like settings216 <div>
Integrations are a directory of connectors, for a workspace and for each person217 <Link to={`/${slug}/-/integrations`} className="inline-flex items-center gap-1.5 text-sm text-muted hover:text-fg">
218 <ArrowLeft size={14} /> All integrations
219 </Link>
Integrations: your own model provider, alerts that open issues, tickets agents read220 {justConnected && <Connected connected={justConnected} />}
221 <div className="mt-4">
222 <ErrorText>{error}</ErrorText>
223 </div>
224
225 {/* Models -------------------------------------------------------------- */}
Integrations are a directory of connectors, for a workspace and for each person226 {kind === "models" && (
227 <Section kind="models">
Models per workspace: several providers, routed by kind of work228 {!hostedOpen && modelConnections.length === 0 && (
229 <div className="mb-4 flex items-center gap-3 rounded-xl border border-warn/30 bg-warn/5 p-4">
230 <span className="inline-flex size-8 shrink-0 items-center justify-center rounded-lg bg-warn/10 text-warn ring-1 ring-warn/30">
231 <AlertTriangle size={16} />
232 </span>
233 <p className="text-sm text-muted">
A free allowance on g1t's models, so anyone can try its agents234 <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 daily235 {hostedPreview
236 ? `During the preview, g1t's hosted models run only for a few invited workspaces, and ${slug} is not one of them yet.`
237 : (trialClosed(trial, slug) ?? `g1t's hosted models are not open to ${slug}.`)}{" "}
A free allowance on g1t's models, so anyone can try its agents238 Connect a provider of your own below and your agents start at once, billed by that provider.
Models per workspace: several providers, routed by kind of work239 </p>
240 </div>
241 )}
Merge branch 'worktree-agent-a985c01f43538af01'242 <Connections list={modelConnections} owner={owner} tested={tested} updated={updated} deliveries={deliveries} />
A catalogue of model providers, and settings that feel like settings243 {modelConnections.length === 0 && hostedOpen && (
244 <p className="flex items-center gap-2 rounded-xl border border-line bg-surface px-4 py-3 text-sm text-muted">
Merge branch 'worktree-agent-ad7c6d88d93adc817'245 <CheckCircle2 size={15} className="shrink-0 text-accent" />
A free allowance on g1t's models, so anyone can try its agents246 {trial?.open
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put247 ? trial.granted
248 ? `All work runs on g1t's models, paid by ${slug}'s trial credit first: ${dollars(Math.max(0, trial.limitMicros - trial.usedMicros))} of ${dollars(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 ${dollars(trial.limitMicros)} of trial credit the first time its agents work. Connect a provider of your own for more, or to choose models.`
A free allowance on g1t's models, so anyone can try its agents250 : 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."
Merge branch 'model-routing'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."}
A catalogue of model providers, and settings that feel like settings253 </p>
254 )}
Merge branch 'model-routing'255 {(modelConnections.length > 0 || hostedOpen) && (
Models per workspace: several providers, routed by kind of work256 <Routing
Merge branch 'worktree-agent-a985c01f43538af01'257 // Started again from what is saved whenever that changes, such as a provider disconnected.
258 key={JSON.stringify([routes, modelConnections.map((c) => c.id)])}
Models per workspace: several providers, routed by kind of work259 connections={modelConnections}
260 routes={routes}
261 hostedOpen={hostedOpen}
262 owner={owner}
263 saved={actionData != null && "routed" in actionData}
264 />
A catalogue of model providers, and settings that feel like settings265 )}
Integrations: your own model provider, alerts that open issues, tickets agents read266 <p className="mt-3 text-xs text-faint">
New workspaces are told to connect a model, before anything fails267 On your own providers, their bills are yours.{" "}
268 {free
Merge branch 'model-routing'269 ? `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.`
270 : `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".`}{" "}
New workspaces are told to connect a model, before anything fails271 Keys go only from g1t's model proxy to the provider: the agent's
Models per workspace: several providers, routed by kind of work272 sandbox holds a token that dies with the run.
Integrations: your own model provider, alerts that open issues, tickets agents read273 </p>
A catalogue of model providers, and settings that feel like settings274 {owner && <ModelCatalog slug={slug} adding={adding} />}
Integrations: connect forms open under the tile that was chosen275 {adding && PROVIDERS[adding].kind === "models" && owner && (
276 <AddForm
277 provider={adding}
278 slug={slug}
279 repos={repoNames}
280 error={actionData && "provider" in actionData ? error : null}
281 />
282 )}
Integrations: your own model provider, alerts that open issues, tickets agents read283 </Section>
Integrations are a directory of connectors, for a workspace and for each person284 )}
Integrations: your own model provider, alerts that open issues, tickets agents read285
286 {/* Alerts -------------------------------------------------------------- */}
Integrations are a directory of connectors, for a workspace and for each person287 {kind === "alerts" && (
Integrations: your own model provider, alerts that open issues, tickets agents read288 <Section kind="alerts">
289 <Connections
290 list={connections.filter((c) => c.kind === "alerts")}
291 owner={owner}
292 tested={tested}
Merge branch 'worktree-agent-a985c01f43538af01'293 updated={updated}
Integrations: your own model provider, alerts that open issues, tickets agents read294 deliveries={deliveries}
295 />
296 {owner && <Choices kind="alerts" slug={slug} adding={adding} />}
Integrations: connect forms open under the tile that was chosen297 {adding && PROVIDERS[adding].kind === "alerts" && owner && (
298 <AddForm
299 provider={adding}
300 slug={slug}
301 repos={repoNames}
302 error={actionData && "provider" in actionData ? error : null}
303 />
304 )}
Integrations: your own model provider, alerts that open issues, tickets agents read305 </Section>
Integrations are a directory of connectors, for a workspace and for each person306 )}
Integrations: your own model provider, alerts that open issues, tickets agents read307
308 {/* Trackers ------------------------------------------------------------ */}
Integrations are a directory of connectors, for a workspace and for each person309 {kind === "tracker" && (
Integrations: your own model provider, alerts that open issues, tickets agents read310 <Section kind="tracker">
311 <Connections
312 list={connections.filter((c) => c.kind === "tracker")}
313 owner={owner}
314 tested={tested}
Merge branch 'worktree-agent-a985c01f43538af01'315 updated={updated}
Integrations: your own model provider, alerts that open issues, tickets agents read316 deliveries={deliveries}
317 />
318 {owner && <Choices kind="tracker" slug={slug} adding={adding} />}
Integrations: connect forms open under the tile that was chosen319 {adding && PROVIDERS[adding].kind === "tracker" && owner && (
320 <AddForm
321 provider={adding}
322 slug={slug}
323 repos={repoNames}
324 error={actionData && "provider" in actionData ? error : null}
325 />
326 )}
Integrations: your own model provider, alerts that open issues, tickets agents read327 </Section>
Integrations are a directory of connectors, for a workspace and for each person328 )}
Integrations: your own model provider, alerts that open issues, tickets agents read329
330 {!owner && <p className="mt-8 text-sm text-muted">An owner of {slug} can add and remove integrations.</p>}
331 </div>
332 );
Models per workspace: several providers, routed by kind of work333}
334
Integrations are a directory of connectors, for a workspace and for each person335/** One kind of connection's page: its heading, then what is connected and what to connect. */
336function Section({ kind, children }: { kind: ProviderKind; children: ReactNode }) {
Integrations: your own model provider, alerts that open issues, tickets agents read337 const info = KIND_INFO[kind];
338 return (
Integrations are a directory of connectors, for a workspace and for each person339 <section className="mt-4">
340 <h1 className="flex items-center gap-2 text-2xl font-semibold tracking-tight [&_svg]:size-5">
Integrations: your own model provider, alerts that open issues, tickets agents read341 <span className="text-muted">{info.icon}</span>
342 {info.title}
Integrations are a directory of connectors, for a workspace and for each person343 </h1>
344 <p className="mt-1.5 mb-6 border-b border-line pb-6 text-sm text-muted">{info.blurb}</p>
Integrations: your own model provider, alerts that open issues, tickets agents read345 {children}
346 </section>
347 );
348}
349
350function Connections({
351 list,
352 owner,
353 tested,
Merge branch 'worktree-agent-a985c01f43538af01'354 updated,
Integrations: your own model provider, alerts that open issues, tickets agents read355 deliveries,
356}: {
357 list: Connection[];
358 owner: boolean;
359 tested: { id: string; ok: boolean; message: string } | null;
Merge branch 'worktree-agent-a985c01f43538af01'360 /** The connection whose secret was just saved. */
361 updated: string | null;
Integrations: your own model provider, alerts that open issues, tickets agents read362 deliveries: Record<string, Delivery[]>;
363}) {
364 if (list.length === 0) return null;
365 return (
366 <ul className="mb-4 divide-y divide-line overflow-hidden rounded-xl border border-line bg-surface">
367 {list.map((connection) => (
368 <li key={connection.id} className="p-4">
369 <ConnectionRow
370 connection={connection}
371 owner={owner}
372 tested={tested}
Merge branch 'worktree-agent-a985c01f43538af01'373 updated={updated === connection.id}
Integrations: your own model provider, alerts that open issues, tickets agents read374 deliveries={deliveries[connection.id] ?? []}
375 />
376 </li>
377 ))}
378 </ul>
379 );
380}
381
Model providers: gateway tokens for endpoints, tidier rows, and the docs382/** Just the host of an address, which is what tells connections apart. */
383function host(url: string | undefined): string | undefined {
384 if (!url) return undefined;
385 try {
386 return new URL(url).host;
387 } catch {
388 return url;
389 }
390}
391
Integrations: your own model provider, alerts that open issues, tickets agents read392function ConnectionRow({
393 connection,
394 owner,
395 tested,
Merge branch 'worktree-agent-a985c01f43538af01'396 updated,
Integrations: your own model provider, alerts that open issues, tickets agents read397 deliveries,
398}: {
399 connection: Connection;
400 owner: boolean;
401 tested: { id: string; ok: boolean; message: string } | null;
Merge branch 'worktree-agent-a985c01f43538af01'402 updated: boolean;
Integrations: your own model provider, alerts that open issues, tickets agents read403 deliveries: Delivery[];
404}) {
405 const { config } = connection;
406 const facts = [
407 PROVIDERS[connection.provider].label,
408 config.repo && `issues in ${config.repo}`,
409 config.assign && "agents start at once",
410 config.organization,
Model providers: gateway tokens for endpoints, tidier rows, and the docs411 host(config.site),
412 host(config.baseUrl),
Models per workspace: several providers, routed by kind of work413 config.model && `default ${config.model}`,
414 connection.models.length > 0 && `${connection.models.length} models`,
Integrations: your own model provider, alerts that open issues, tickets agents read415 config.keys?.length ? config.keys.join(", ") : null,
416 connection.secretHint && `key ${connection.secretHint}`,
417 ].filter(Boolean);
AI Gateway: OpenAI's format, open models, and your own providers418 const gateway = connection.kind === "models" ? gatewayPatterns(connection.provider, config) : null;
Integrations: your own model provider, alerts that open issues, tickets agents read419 const waitingForSecret = connection.provider === "sentry" && !deliveries.length && !connection.lastUsedAt;
420 return (
421 <div>
Model providers: gateway tokens for endpoints, tidier rows, and the docs422 <div className="flex items-center gap-3">
Integrations: your own model provider, alerts that open issues, tickets agents read423 <ProviderMark provider={connection.provider} />
424 <div className="min-w-0 grow">
425 <p className="truncate text-sm font-medium">{connection.name}</p>
426 <p className="truncate text-xs text-muted">{facts.join(" · ")}</p>
427 </div>
428 {connection.lastUsedAt && (
Model providers: gateway tokens for endpoints, tidier rows, and the docs429 <span className="hidden shrink-0 text-xs text-faint sm:inline">
Integrations: your own model provider, alerts that open issues, tickets agents read430 Used <TimeAgo at={connection.lastUsedAt} />
431 </span>
432 )}
433 {owner && (
Model providers: gateway tokens for endpoints, tidier rows, and the docs434 <Form method="post" className="flex shrink-0 gap-2">
Integrations: your own model provider, alerts that open issues, tickets agents read435 <input type="hidden" name="id" value={connection.id} />
Merge branch 'worktree-agent-a985c01f43538af01'436 <SubmitButton variant="quiet" name="intent" value="test" match={{ id: connection.id }} pending="Testing…">
Integrations: your own model provider, alerts that open issues, tickets agents read437 Test
Merge branch 'worktree-agent-a985c01f43538af01'438 </SubmitButton>
439 <SubmitButton variant="quiet" name="intent" value="disconnect" match={{ id: connection.id }} pending="Disconnecting…" aria-label="Disconnect">
Integrations: your own model provider, alerts that open issues, tickets agents read440 <X size={14} />
Merge branch 'worktree-agent-a985c01f43538af01'441 </SubmitButton>
Integrations: your own model provider, alerts that open issues, tickets agents read442 </Form>
443 )}
444 </div>
445 {tested?.id === connection.id && (
Merge branch 'worktree-agent-ad7c6d88d93adc817'446 <p className={`mt-3 text-sm ${tested.ok ? "text-success" : "text-danger"}`}>{tested.message}</p>
Integrations: your own model provider, alerts that open issues, tickets agents read447 )}
448 {connection.lastError && (
449 <p className="mt-3 flex items-start gap-2 text-xs text-warn">
450 <AlertTriangle size={13} className="mt-0.5 shrink-0" />
451 {connection.lastError}
452 </p>
453 )}
AI Gateway: OpenAI's format, open models, and your own providers454 {gateway && (
455 <div className="mt-3 flex flex-wrap items-center gap-1.5 text-xs text-muted">
456 <span className="text-faint">AI Gateway</span>
457 {gateway.length === 0 ? (
458 <span>no models</span>
459 ) : (
460 gateway.map((pattern) => (
461 <code key={pattern} className="rounded border border-line bg-bg px-1.5 py-0.5 font-mono text-[0.75rem] text-fg">
462 {pattern}
463 </code>
464 ))
465 )}
466 </div>
467 )}
468 {owner && gateway && (
469 <details className="group mt-3" open={updated || undefined}>
470 <summary className="inline-flex cursor-pointer list-none items-center gap-1 text-xs text-muted hover:text-fg">
471 <ChevronRight size={12} className="transition-transform group-open:rotate-90" />
472 Change its AI Gateway models or key
473 </summary>
474 <Form method="post" className="mt-3 max-w-2xl space-y-3">
475 <input type="hidden" name="intent" value="gateway" />
476 <input type="hidden" name="id" value={connection.id} />
477 <GatewayModelsField provider={connection.provider} value={gateway} />
478 <Field label="Replace the key" hint="Write-only: g1t never shows it again. Leave it empty to keep the key you have.">
479 <Input name="secret" type="password" placeholder={connection.secretHint ? `Now ${connection.secretHint}` : "Paste a key"} autoComplete="new-password" className="max-w-sm" />
480 </Field>
481 <div className="flex items-center gap-3">
482 <SubmitButton variant="quiet" match={{ intent: "gateway", id: connection.id }} pending="Saving…">
483 Save
484 </SubmitButton>
485 {updated && <span className="text-sm text-success">Saved.</span>}
486 </div>
487 </Form>
488 </details>
489 )}
Integrations: your own model provider, alerts that open issues, tickets agents read490 {connection.webhookUrl && (
491 <div className="mt-3">
492 <p className="mb-1.5 text-xs text-faint">Where it sends alerts</p>
493 <CopyLine text={connection.webhookUrl} />
494 </div>
495 )}
496 {owner && waitingForSecret && (
497 <Form method="post" className="mt-3 flex flex-wrap items-end gap-2">
498 <input type="hidden" name="intent" value="update" />
499 <input type="hidden" name="id" value={connection.id} />
500 <div className="min-w-64 grow">
501 <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.">
502 <Input name="signingSecret" type="password" placeholder="Paste to replace" />
503 </Field>
504 </div>
Merge branch 'worktree-agent-a985c01f43538af01'505 <SubmitButton variant="quiet" match={{ intent: "update", id: connection.id }} pending="Saving…">
Integrations: your own model provider, alerts that open issues, tickets agents read506 Save
Merge branch 'worktree-agent-a985c01f43538af01'507 </SubmitButton>
508 {updated && <span className="pb-2 text-sm text-muted">Saved.</span>}
Integrations: your own model provider, alerts that open issues, tickets agents read509 </Form>
510 )}
511 {deliveries.length > 0 && (
512 <ul className="mt-3 space-y-1 text-xs">
513 {deliveries.map((delivery) => (
514 <li key={delivery.id} className="flex items-center gap-2 text-muted">
515 <span
516 className={`size-1.5 shrink-0 rounded-full ${
517 delivery.outcome === "refused"
518 ? "bg-danger"
519 : delivery.outcome === "ignored"
520 ? "bg-faint"
Merge branch 'worktree-agent-ad7c6d88d93adc817'521 : "bg-success"
Integrations: your own model provider, alerts that open issues, tickets agents read522 }`}
523 />
524 <span className="font-mono text-faint">{delivery.event}</span>
525 <span className="min-w-0 truncate">{delivery.detail}</span>
526 {delivery.issue && (
527 <Link
528 to={`/${delivery.issue.replace("#", "/issues/")}`}
529 className="shrink-0 font-mono text-fg hover:underline"
530 >
531 {delivery.issue}
532 </Link>
533 )}
534 <span className="ml-auto shrink-0 text-faint">
535 <TimeAgo at={delivery.receivedAt} />
536 </span>
537 </li>
538 ))}
539 </ul>
540 )}
541 </div>
542 );
543}
544
A catalogue of model providers, and settings that feel like settings545/** What a provider is for, in a line. */
546function blurb(provider: Provider): string {
547 return PROVIDER_BLURB[provider] ?? MODEL_CATALOG[provider]?.blurb ?? "";
548}
549
Integrations: your own model provider, alerts that open issues, tickets agents read550function Choices({ kind, slug, adding }: { kind: ProviderKind; slug: string; adding: Provider | null }) {
551 const providers = (Object.keys(PROVIDERS) as Provider[]).filter((p) => PROVIDERS[p].kind === kind);
552 return (
A catalogue of model providers, and settings that feel like settings553 <div className="mt-3">
554 <ProviderTiles providers={providers} slug={slug} adding={adding} blurb={blurb} />
Integrations: your own model provider, alerts that open issues, tickets agents read555 </div>
556 );
557}
558
559function RepoField({ repos, required, hint }: { repos: string[]; required?: boolean; hint: string }) {
560 return (
561 <Field label="Repository" hint={hint}>
Workspace names and icons, and a component kit for every control562 <Combobox
Integrations: your own model provider, alerts that open issues, tickets agents read563 name="repo"
564 defaultValue={repos[0] ?? ""}
Workspace names and icons, and a component kit for every control565 aria-label="Repository"
566 placeholder={repos.length === 0 ? "No repositories yet" : "Choose a repository"}
567 searchPlaceholder="Find a repository"
568 emptyText="No repository by that name."
569 options={[
570 ...(required ? [] : [{ value: "", label: "None" }]),
571 ...repos.map((repo) => ({ value: repo, label: repo, icon: <Avatar name={repo.split("/").pop() ?? repo} size={16} square /> })),
572 ]}
573 className="font-mono text-[0.8125rem]"
574 />
Integrations: your own model provider, alerts that open issues, tickets agents read575 </Field>
576 );
577}
578
579function AssignField() {
580 return (
Workspace names and icons, and a component kit for every control581 <CheckboxOption
582 name="assign"
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent583 label={<span className="font-medium">Assign each new issue to g1t</span>}
Workspace names and icons, and a component kit for every control584 description={
585 <span className="text-muted">
586 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-agent587 Agents run only where g1t is enabled.
Integrations: your own model provider, alerts that open issues, tickets agents read588 </span>
Workspace names and icons, and a component kit for every control589 }
590 />
Integrations: your own model provider, alerts that open issues, tickets agents read591 );
592}
593
594function AddForm({
595 provider,
596 slug,
597 repos,
598 error,
599}: {
600 provider: Provider;
601 slug: string;
602 repos: string[];
603 error: string | null;
604}) {
A catalogue of model providers, and settings that feel like settings605 const fields: Partial<Record<Provider, ReactNode>> = {
Integrations: your own model provider, alerts that open issues, tickets agents read606 sentry: (
607 <>
608 <Field label="Organization" hint="Its slug, as in acme.sentry.io.">
609 <Input name="organization" required placeholder="acme" />
610 </Field>
611 <RepoField repos={repos} required hint="Where issues are opened." />
612 <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.">
613 <Input name="secret" type="password" />
614 </Field>
615 <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.">
616 <Input name="signingSecret" type="password" />
617 </Field>
618 <AssignField />
619 </>
620 ),
621 datadog: (
622 <>
623 <RepoField repos={repos} required hint="Where issues are opened." />
624 <AssignField />
625 </>
626 ),
627 webhook: (
628 <>
629 <RepoField repos={repos} required hint="Where issues are opened." />
630 <AssignField />
631 </>
632 ),
633 jira: (
634 <>
635 <Field label="Site" hint="Your Jira's address.">
636 <Input name="site" type="url" required placeholder="https://acme.atlassian.net" />
637 </Field>
638 <Field label="Email" hint="The account the API token belongs to.">
639 <Input name="email" type="email" required autoComplete="off" />
640 </Field>
641 <Field label="API token" hint="From id.atlassian.com → Security → API tokens.">
642 <Input name="secret" type="password" required />
643 </Field>
644 <Field label="Project keys" hint="Optional. The projects this answers for, such as TECH, OPS. Empty answers for any key.">
645 <Input name="keys" placeholder="TECH, OPS" />
646 </Field>
647 </>
648 ),
649 linear: (
650 <>
651 <Field label="API key" hint="From Linear → Settings → Security & access → Personal API keys.">
652 <Input name="secret" type="password" required placeholder="lin_api_…" />
653 </Field>
654 <Field label="Team keys" hint="Optional. Such as ENG. Empty answers for any key.">
655 <Input name="keys" placeholder="ENG" />
656 </Field>
657 </>
658 ),
659 };
660 return (
Integrations: connect forms open under the tile that was chosen661 <section id="add" className="mt-4 scroll-mt-20 rounded-xl border border-line bg-surface p-5">
Integrations: your own model provider, alerts that open issues, tickets agents read662 <div className="flex items-center gap-3">
663 <ProviderMark provider={provider} />
664 <div className="grow">
Integrations: connect forms open under the tile that was chosen665 <h3 className="font-medium">
Models per workspace: several providers, routed by kind of work666 Connect {provider.endsWith("_endpoint") ? `an ${PROVIDERS[provider].label}` : PROVIDERS[provider].label}
Integrations: connect forms open under the tile that was chosen667 </h3>
A catalogue of model providers, and settings that feel like settings668 <p className="text-xs text-muted">{blurb(provider)}</p>
Integrations: your own model provider, alerts that open issues, tickets agents read669 </div>
Integrations are a directory of connectors, for a workspace and for each person670 <Link
671 to={`/${slug}/-/integrations/${integrationsSection(PROVIDERS[provider].kind)}`}
672 className="rounded-md p-1.5 text-faint hover:bg-raised hover:text-fg"
673 aria-label="Close"
674 >
Integrations: your own model provider, alerts that open issues, tickets agents read675 <X size={16} />
676 </Link>
677 </div>
678 <Form method="post" className="mt-5 grid max-w-xl gap-4">
679 <input type="hidden" name="provider" value={provider} />
A catalogue of model providers, and settings that feel like settings680 {fields[provider] ?? <ModelProviderFields provider={provider} />}
Integrations: your own model provider, alerts that open issues, tickets agents read681 <ErrorText>{error}</ErrorText>
682 <div>
Merge branch 'worktree-agent-a985c01f43538af01'683 <SubmitButton match={{ provider }} pending="Connecting…">
684 Connect
685 </SubmitButton>
Integrations: your own model provider, alerts that open issues, tickets agents read686 </div>
687 </Form>
688 </section>
689 );
690}
691
692/** What to do next, right after connecting: shown once. */
693function Connected({ connected }: { connected: { connection: Connection; signingSecret: string | null } }) {
694 const { connection, signingSecret } = connected;
695 const url = connection.webhookUrl;
696 return (
Merge branch 'worktree-agent-ad7c6d88d93adc817'697 <div className="mt-6 rounded-xl border border-success-dim/60 bg-success/5 p-5">
Integrations: your own model provider, alerts that open issues, tickets agents read698 <p className="flex items-center gap-2 font-medium">
Merge branch 'worktree-agent-ad7c6d88d93adc817'699 <CheckCircle2 size={16} className="text-success" />
Integrations: your own model provider, alerts that open issues, tickets agents read700 {connection.name} is connected.
701 </p>
702 {connection.provider === "sentry" && url && (
703 <ol className="mt-3 list-decimal space-y-2 pl-5 text-sm text-muted">
704 <li>
705 In Sentry, open Settings → Developer Settings → Custom Integrations, and create an internal
706 integration.
707 </li>
708 <li>
709 Set its webhook URL to <CopyLine text={url} />
710 </li>
711 <li>Turn on Alert Rule Action, and under Webhooks, tick issue.</li>
712 <li>Give it Issue &amp; Event read and write, save, and paste its client secret below, on the connection.</li>
713 </ol>
714 )}
715 {(connection.provider === "datadog" || connection.provider === "webhook") && url && signingSecret && (
716 <div className="mt-3 space-y-3 text-sm text-muted">
717 <p>This secret is shown once. Copy it now.</p>
718 <CopyLine text={signingSecret} />
719 {connection.provider === "datadog" ? (
720 <>
721 <p>
722 In Datadog, open Integrations → Webhooks and add one named <code>g1t</code> with this URL:
723 </p>
724 <CopyLine text={url} />
725 <p>Custom headers:</p>
726 <CopyLine text={`{"Authorization": "Bearer ${signingSecret}"}`} />
727 <p>Payload:</p>
728 <CopyLine
729 text={`{"id": "$ALERT_ID", "title": "$EVENT_TITLE", "body": "$EVENT_MSG", "url": "$LINK", "status": "$ALERT_TRANSITION", "priority": "$PRIORITY"}`}
730 />
731 <p>
732 Then mention <code>@webhook-g1t</code> in any monitor's message.
733 </p>
734 </>
735 ) : (
736 <>
737 <p>Send JSON with at least a title to:</p>
738 <CopyLine text={url} />
739 <p>
740 with <code>Authorization: Bearer &lt;secret&gt;</code>, or an HMAC-SHA256 of the body in{" "}
741 <code>X-G1t-Signature</code>. An <code>id</code> keeps repeats on one issue.
742 </p>
743 </>
744 )}
745 </div>
746 )}
747 {connection.kind === "tracker" && (
748 <p className="mt-2 text-sm text-muted">
749 Agents now read tickets that work mentions, and you can import one from any repository's new
750 issue page. Use Test to check the token.
751 </p>
752 )}
753 {connection.kind === "models" && (
754 <p className="mt-2 text-sm text-muted">
Models per workspace: several providers, routed by kind of work755 {connection.lastError
756 ? `It was saved, but the check failed: ${connection.lastError}`
757 : connection.models.length > 0
758 ? `It offers ${connection.models.length} models. Choose which work goes to it under “Which model does which work”.`
759 : "Choose which work goes to it under “Which model does which work”."}
Integrations: your own model provider, alerts that open issues, tickets agents read760 </p>
761 )}
762 </div>
763 );
764}

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