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 permissions | 1 | import { |
| 2 | AlertTriangle, | |
| Integrations are a directory of connectors, for a workspace and for each person | 3 | ArrowLeft, |
| Share the editor settings and Claude Code deploy permissions | 4 | CheckCircle2, |
| 5 | ChevronRight, | |
| 6 | Cpu, | |
| 7 | Siren, | |
| 8 | Ticket, | |
| 9 | X, | |
| 10 | } from "lucide-react"; | |
| 11 | import { env } from "cloudflare:workers"; | |
| 12 | import type { ReactNode } from "react"; | |
| Merge branch 'worktree-agent-a985c01f43538af01' | 13 | import { Form, Link } from "react-router"; |
| Share the editor settings and Claude Code deploy permissions | 14 | |
| 15 | import { | |
| 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 providers | 24 | gatewayPatterns, |
| Share the editor settings and Claude Code deploy permissions | 25 | } from "@g1t/contracts"; |
| 26 | ||
| 27 | import type { Route } from "./+types/integrations"; | |
| Social cards for every page: og.g1t.sh | 28 | import { page } from "../../lib/meta"; |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 29 | import { trialClosed } from "../../lib/trial"; |
| AI Gateway: OpenAI's format, open models, and your own providers | 30 | import { GatewayModelsField, MODEL_CATALOG, ModelCatalog, ModelProviderFields, ProviderMark, ProviderTiles, Routing } from "../../components/model-providers"; |
| 31 | import { parseGatewayModels } from "../../lib/gateway"; | |
| Integrations are a directory of connectors, for a workspace and for each person | 32 | import { 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. | 33 | import { 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. | 34 | import { CopyLine, ErrorText, Field, Input, SubmitButton, TimeAgo } from "../../components/ui"; |
| 35 | import { Avatar } from "../../components/ui/avatar"; | |
| 36 | import { Card } from "../../components/ui/card"; | |
| Workspace names and icons, and a component kit for every control | 37 | import { CheckboxOption } from "../../components/ui/checkbox"; |
| 38 | import { Combobox } from "../../components/ui/combobox"; | |
| Share the editor settings and Claude Code deploy permissions | 39 | import { billing, integrations, repos } from "../../lib/services.server"; |
| 40 | import { assertSameOrigin, getViewer, requireUser, roleIn, unwrap } from "../../lib/session.server"; | |
| 41 | ||
| Social cards for every page: og.g1t.sh | 42 | export function meta({ params, ...args }: Route.MetaArgs) { |
| Integrations are a directory of connectors, for a workspace and for each person | 43 | 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 permissions | 45 | } |
| 46 | ||
| 47 | const isProvider = (value: string | null): value is Provider => value != null && value in PROVIDERS; | |
| 48 | ||
| 49 | export 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 person | 52 | // 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 permissions | 55 | 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 person | 78 | kind, |
| Share the editor settings and Claude Code deploy permissions | 79 | 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 daily | 84 | hostedPreview: access.preview, |
| Share the editor settings and Claude Code deploy permissions | 85 | 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 | ||
| 92 | function 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. */ | |
| 98 | function 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 providers | 112 | // 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 permissions | 114 | }; |
| 115 | } | |
| 116 | ||
| 117 | export 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 permissions | 139 | 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 providers | 147 | 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 permissions | 159 | 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 permissions | 165 | } |
| 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 | ||
| 178 | const 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 | ||
| 196 | const 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 | ||
| 204 | export default function WorkspaceIntegrations({ loaderData, actionData }: Route.ComponentProps) { | |
| Integrations are a directory of connectors, for a workspace and for each person | 205 | const { slug, role, kind, connections, deliveries, repos: repoNames, adding, free, marginPercent, hostedOpen, hostedPreview, trial, routes } = |
| Share the editor settings and Claude Code deploy permissions | 206 | 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 permissions | 213 | |
| 214 | return ( | |
| 215 | <div> | |
| Integrations are a directory of connectors, for a workspace and for each person | 216 | <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 permissions | 219 | {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 person | 225 | {kind === "models" && ( |
| 226 | <Section kind="models"> | |
| Share the editor settings and Claude Code deploy permissions | 227 | {!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 daily | 234 | {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 permissions | 237 | 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 permissions | 242 | {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 permissions | 255 | )} |
| Merge branch 'model-routing' | 256 | {(modelConnections.length > 0 || hostedOpen) && ( |
| Share the editor settings and Claude Code deploy permissions | 257 | <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 permissions | 260 | 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 permissions | 272 | 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 person | 285 | )} |
| Share the editor settings and Claude Code deploy permissions | 286 | |
| 287 | {/* Alerts -------------------------------------------------------------- */} | |
| Integrations are a directory of connectors, for a workspace and for each person | 288 | {kind === "alerts" && ( |
| Share the editor settings and Claude Code deploy permissions | 289 | <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 permissions | 295 | 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 person | 307 | )} |
| Share the editor settings and Claude Code deploy permissions | 308 | |
| 309 | {/* Trackers ------------------------------------------------------------ */} | |
| Integrations are a directory of connectors, for a workspace and for each person | 310 | {kind === "tracker" && ( |
| Share the editor settings and Claude Code deploy permissions | 311 | <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 permissions | 317 | 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 person | 329 | )} |
| Share the editor settings and Claude Code deploy permissions | 330 | |
| 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 person | 336 | /** One kind of connection's page: its heading, then what is connected and what to connect. */ |
| 337 | function Section({ kind, children }: { kind: ProviderKind; children: ReactNode }) { | |
| Share the editor settings and Claude Code deploy permissions | 338 | const info = KIND_INFO[kind]; |
| 339 | return ( | |
| Integrations are a directory of connectors, for a workspace and for each person | 340 | <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 permissions | 342 | <span className="text-muted">{info.icon}</span> |
| 343 | {info.title} | |
| Integrations are a directory of connectors, for a workspace and for each person | 344 | </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 permissions | 346 | {children} |
| 347 | </section> | |
| 348 | ); | |
| 349 | } | |
| 350 | ||
| 351 | function Connections({ | |
| 352 | list, | |
| 353 | owner, | |
| 354 | tested, | |
| Merge branch 'worktree-agent-a985c01f43538af01' | 355 | updated, |
| Share the editor settings and Claude Code deploy permissions | 356 | 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 permissions | 363 | 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 permissions | 382 | ); |
| 383 | } | |
| 384 | ||
| 385 | /** Just the host of an address, which is what tells connections apart. */ | |
| 386 | function 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 | ||
| 395 | function ConnectionRow({ | |
| 396 | connection, | |
| 397 | owner, | |
| 398 | tested, | |
| Merge branch 'worktree-agent-a985c01f43538af01' | 399 | updated, |
| Share the editor settings and Claude Code deploy permissions | 400 | 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 permissions | 406 | 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 providers | 421 | const gateway = connection.kind === "models" ? gatewayPatterns(connection.provider, config) : null; |
| Share the editor settings and Claude Code deploy permissions | 422 | 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 permissions | 440 | 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 permissions | 443 | <X size={14} /> |
| Merge branch 'worktree-agent-a985c01f43538af01' | 444 | </SubmitButton> |
| Share the editor settings and Claude Code deploy permissions | 445 | </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 permissions | 450 | )} |
| 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 providers | 457 | {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 providers | 486 | 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 permissions | 493 | {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 permissions | 509 | 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 permissions | 512 | </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 permissions | 525 | }`} |
| 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. */ | |
| 549 | function blurb(provider: Provider): string { | |
| 550 | return PROVIDER_BLURB[provider] ?? MODEL_CATALOG[provider]?.blurb ?? ""; | |
| 551 | } | |
| 552 | ||
| 553 | function 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 | ||
| 562 | function 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 control | 565 | <Combobox |
| Share the editor settings and Claude Code deploy permissions | 566 | name="repo" |
| 567 | defaultValue={repos[0] ?? ""} | |
| Workspace names and icons, and a component kit for every control | 568 | 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 permissions | 578 | </Field> |
| 579 | ); | |
| 580 | } | |
| 581 | ||
| 582 | function AssignField() { | |
| 583 | return ( | |
| Workspace names and icons, and a component kit for every control | 584 | <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-agent | 586 | label={<span className="font-medium">Assign each new issue to g1t</span>} |
| Workspace names and icons, and a component kit for every control | 587 | 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-agent | 590 | Agents run only where g1t is enabled. |
| Share the editor settings and Claude Code deploy permissions | 591 | </span> |
| Workspace names and icons, and a component kit for every control | 592 | } |
| 593 | /> | |
| Share the editor settings and Claude Code deploy permissions | 594 | ); |
| 595 | } | |
| 596 | ||
| 597 | function 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 permissions | 681 | </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 permissions | 694 | ); |
| 695 | } | |
| 696 | ||
| 697 | /** What to do next, right after connecting: shown once. */ | |
| 698 | function 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 permissions | 703 | <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 permissions | 705 | {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 & 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 <secret></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.