New workspaces are told to connect a model, before anything fails
Walking g1t as a second account, the way a new user would: g1t's hosted models are open only to selected workspaces, so a new workspace's agents and workflow runners need a model provider of its own, and nothing said so until something failed. - Mission control's "Get started" said "Add agent credit" and ticked it off, since nothing is charged. A workspace that cannot reach a model now gets "Connect a model" there instead, linked to Integrations. - The Actions page says up front that workflows run once the workspace connects a model provider, with a link; a job that was refused for it links "Integrations" in its reason. - An issue showed no way to put g1t's agent on it at all, while its empty state said "Put g1t agents on it". It now shows where the agent would be and how to make it appear. - Integrations, Billing and the usage guide said g1t charges $0.10 a run on your own provider. Not while it is being built out; they now say so, as the models guide already did. - The quickstart gains a "Connect a model" step before putting an agent on an issue, and says git takes any username with a token.
| 32 | 32 | it, plus 20%. A small change costs a few cents. | |
| 33 | 33 | ||
| 34 | 34 | Work a workspace routes to [its own model providers](/guides/models/) is | |
| 35 | − | paid for at those providers instead, and each such run here is a flat $0.10 | |
| 36 | − | for the sandbox and orchestration. | |
| 35 | + | paid for at those providers instead, and each such run here will be a flat | |
| 36 | + | $0.10 for the sandbox and orchestration once pricing starts (nothing while | |
| 37 | + | g1t is being built out). | |
| 37 | 38 | ||
| 38 | 39 | The charge goes to the workspace that owns the repository, whoever | |
| 39 | 40 | assigned the issue. That is why only members of a workspace can put g1t | |
| 121 | 122 | - **g1t's agents, for any workspace with its own model provider:** connect | |
| 122 | 123 | an Anthropic key or endpoint under [Integrations](/guides/models/) and the | |
| 123 | 124 | workspace's agents, acceptance checks and merge queue work at once. Your | |
| 124 | − | provider bills you for the models; g1t charges $0.10 a run. | |
| 125 | + | provider bills you for the models; g1t charges nothing for now, and $0.10 | |
| 126 | + | a run once pricing starts. | |
| 125 | 127 | - **g1t's hosted models, for selected workspaces:** while payments are in | |
| 126 | 128 | test mode, g1t's own models are open only to workspaces it has opened | |
| 127 | 129 | them to. When payments go live, every workspace can use them, paid from |
| 34 | 34 | ||
| 35 | 35 | 3. **Push a repository** | |
| 36 | 36 | ||
| 37 | − | Pushing to a repository that does not exist creates it. When git asks for | |
| 38 | − | a password, give it your token. | |
| 37 | + | Pushing to a repository that does not exist creates it. When git asks, | |
| 38 | + | give it your username and, as the password, your token. | |
| 39 | 39 | ||
| 40 | 40 | ```sh | |
| 41 | 41 | git remote add g1t https://g1t.sh/<workspace>/my-project.git | |
| 57 | 57 | Add **acceptance checks**, commands that must pass, to hold the work to | |
| 58 | 58 | them. | |
| 59 | 59 | ||
| 60 | − | 5. **Put an agent on it** | |
| 60 | + | 5. **Connect a model** | |
| 61 | 61 | ||
| 62 | + | g1t's agents, and the runners your workflows use, need a model to work | |
| 63 | + | with. In your workspace's **Settings → Integrations**, connect your | |
| 64 | + | Anthropic or OpenAI key, or any compatible endpoint. Your provider bills | |
| 65 | + | you for the model; g1t itself costs nothing while it is being built out. | |
| 66 | + | Mission control and the issue page link there until it is done. See | |
| 67 | + | [model providers](/guides/models/). | |
| 68 | + | ||
| 69 | + | 6. **Put an agent on it** | |
| 70 | + | ||
| 62 | 71 | <Tabs> | |
| 63 | 72 | <TabItem label="g1t's agent"> | |
| 64 | − | On the issue, choose **Assign** and pick **g1t-agent**. It opens a pull | |
| 73 | + | On the issue, choose **Assign to g1t agent**. It opens a pull | |
| 65 | 74 | request, makes the change in a sandbox, and sees it through: checks and | |
| 66 | 75 | workflows, a review by a second agent, revisions until both pass. | |
| 67 | 76 | </TabItem> | |
| 79 | 88 | </TabItem> | |
| 80 | 89 | </Tabs> | |
| 81 | 90 | ||
| 82 | − | 6. **Merge** | |
| 91 | + | 7. **Merge** | |
| 83 | 92 | ||
| 84 | 93 | Open the pull request, read **Changes** and **Session**, and choose | |
| 85 | 94 | **Merge**, or turn on auto-merge in the repository's settings and let g1t |
| 1 | 1 | import { ArrowRight, Hand, Plus, Settings, Users } from "lucide-react"; | |
| 2 | + | import { env } from "cloudflare:workers"; | |
| 2 | 3 | import { type ReactNode, useEffect } from "react"; | |
| 3 | 4 | import { Link, data, useRevalidator, useRouteLoaderData } from "react-router"; | |
| 4 | 5 | ||
| 43 | 44 | const times: Record<string, number> = {}; | |
| 44 | 45 | const timed = <T,>(name: string, promise: Promise<T>) => | |
| 45 | 46 | promise.then((value) => ((times[name] = Date.now() - started), value)); | |
| 46 | − | const [repos, pulls, assigned] = await Promise.all([ | |
| 47 | + | // The workspace the sidebar shows, as the shell picks it. | |
| 48 | + | const workspace = viewer?.workspaces?.[0]?.slug ?? null; | |
| 49 | + | const [repos, pulls, assigned, models] = await Promise.all([ | |
| 47 | 50 | timed("repos", reposApi.list(viewer, { memberOnly: Boolean(viewer) })), | |
| 48 | 51 | timed("pulls", work.listActivePulls(viewer)), | |
| 49 | 52 | timed("assigned", work.listAssignedIssues(viewer)), | |
| 53 | + | // Whether its agents can reach a model: g1t's, or one it connected. | |
| 54 | + | workspace ? timed("models", env.RUNNER.modelAccess(workspace).catch(() => null)) : Promise.resolve(null), | |
| 50 | 55 | ]); | |
| 51 | 56 | // Each issue and pull request is shown under its repository. Most are in | |
| 52 | 57 | // the viewer's own, already listed; the rest are looked up once each, all | |
| 64 | 69 | return data({ | |
| 65 | 70 | viewer, | |
| 66 | 71 | repos, | |
| 72 | + | canRunAgents: models == null || models.hosted || models.own != null, | |
| 67 | 73 | assigned: assigned.flatMap((issue) => { | |
| 68 | 74 | const repo = known.get(issue.repoId); | |
| 69 | 75 | return repo ? [{ issue, repo }] : []; | |
| 241 | 247 | } | |
| 242 | 248 | ||
| 243 | 249 | export default function Home({ loaderData }: Route.ComponentProps) { | |
| 244 | − | const { viewer, repos, active, assigned } = loaderData; | |
| 250 | + | const { viewer, repos, active, assigned, canRunAgents } = loaderData; | |
| 245 | 251 | ||
| 246 | 252 | const needsYou = active.filter((item) => item.lifecycle?.stage === "needs_you"); | |
| 247 | 253 | const ready = active.filter((item) => item.lifecycle?.stage === "ready"); | |
| 279 | 285 | about: "Create one, or import one from GitHub by its address. Push to it with git as usual.", | |
| 280 | 286 | to: workspace ? `/new?workspace=${workspace}` : "/new", | |
| 281 | 287 | action: "Add", | |
| 282 | − | }, | |
| 283 | − | { | |
| 284 | − | // Nothing to pay while g1t is being built out. | |
| 285 | − | done: Boolean(shell?.free) || shell?.creditMicros == null || shell.creditMicros > 0, | |
| 286 | − | title: "Add agent credit", | |
| 287 | − | about: "g1t's agents are paid for from the workspace's credit, at what the model costs plus 20%.", | |
| 288 | − | to: workspace ? `/${workspace}/-/billing` : null, | |
| 289 | − | action: "Add credit", | |
| 290 | 288 | }, | |
| 289 | + | canRunAgents | |
| 290 | + | ? { | |
| 291 | + | // Nothing to pay while g1t is being built out. | |
| 292 | + | done: Boolean(shell?.free) || shell?.creditMicros == null || shell.creditMicros > 0, | |
| 293 | + | title: "Add agent credit", | |
| 294 | + | about: "g1t's agents are paid for from the workspace's credit, at what the model costs plus 20%.", | |
| 295 | + | to: workspace ? `/${workspace}/-/billing` : null, | |
| 296 | + | action: "Add credit", | |
| 297 | + | } | |
| 298 | + | : { | |
| 299 | + | done: false, | |
| 300 | + | title: "Connect a model", | |
| 301 | + | about: | |
| 302 | + | "Agents need a model to think with. Connect your Anthropic or OpenAI key, or any compatible endpoint; g1t itself costs nothing while it is being built out.", | |
| 303 | + | to: workspace ? `/${workspace}/-/integrations` : null, | |
| 304 | + | action: "Connect", | |
| 305 | + | }, | |
| 291 | 306 | { | |
| 292 | 307 | done: handedOff, | |
| 293 | 308 | title: "Hand off an outcome", |
| 61 | 61 | ); | |
| 62 | 62 | } | |
| 63 | 63 | ||
| 64 | + | /** Why a job did not run as asked, with "Integrations" linked where it says so. */ | |
| 65 | + | function Reason({ text, workspace }: { text: string; workspace: string }) { | |
| 66 | + | const [before, after] = text.split("Integrations"); | |
| 67 | + | return ( | |
| 68 | + | <p className="rounded-lg bg-raised px-3 py-2 text-sm text-muted"> | |
| 69 | + | {after === undefined ? ( | |
| 70 | + | text | |
| 71 | + | ) : ( | |
| 72 | + | <> | |
| 73 | + | {before} | |
| 74 | + | <Link to={`/${workspace}/-/integrations`} className="text-accent hover:underline"> | |
| 75 | + | Integrations | |
| 76 | + | </Link> | |
| 77 | + | {after} | |
| 78 | + | </> | |
| 79 | + | )} | |
| 80 | + | </p> | |
| 81 | + | ); | |
| 82 | + | } | |
| 83 | + | ||
| 64 | 84 | function JobView({ job, base }: { job: Job; base: string }) { | |
| 65 | 85 | const live = job.status !== "completed"; | |
| 66 | 86 | const log = useJobLog(`${base}/actions/jobs/${job.id}/log`, live); | |
| 76 | 96 | {job.startedAt && ` · ${duration(job.startedAt, job.finishedAt)}`} | |
| 77 | 97 | </span> | |
| 78 | 98 | </div> | |
| 79 | − | {job.reason && <p className="rounded-lg bg-raised px-3 py-2 text-sm text-muted">{job.reason}</p>} | |
| 99 | + | {job.reason && <Reason text={job.reason} workspace={base.split("/")[1]} />} | |
| 80 | 100 | {job.annotations.length > 0 && ( | |
| 81 | 101 | <ul className="space-y-2 rounded-xl border border-line bg-surface p-4 text-sm"> | |
| 82 | 102 | {job.annotations.map((note, index) => ( |
| 1 | 1 | import { AlertTriangle, FileCode2, GitBranch, Play, PlayCircle } from "lucide-react"; | |
| 2 | + | import { env } from "cloudflare:workers"; | |
| 2 | 3 | import { useEffect, useState } from "react"; | |
| 3 | 4 | import { Form, Link, useNavigation, useRevalidator, useSearchParams } from "react-router"; | |
| 4 | 5 | ||
| 18 | 19 | const viewer = getViewer(context); | |
| 19 | 20 | const repo = { namespace: params.owner, name: params.repo }; | |
| 20 | 21 | const selected = new URL(request.url).searchParams.get("workflow") ?? undefined; | |
| 21 | − | const [workflows, runs] = await Promise.all([ | |
| 22 | + | const member = roleIn(viewer, params.owner) != null; | |
| 23 | + | const [workflows, runs, models] = await Promise.all([ | |
| 22 | 24 | actions.workflows(repo, viewer), | |
| 23 | 25 | actions.runs(repo, viewer, { workflow: selected, limit: 50 }), | |
| 26 | + | // Jobs run on g1t's machines for workspaces whose agents can reach a | |
| 27 | + | // model; members are told before a run fails for it. | |
| 28 | + | member ? env.RUNNER.modelAccess(params.owner).catch(() => null) : Promise.resolve(null), | |
| 24 | 29 | ]); | |
| 25 | 30 | return { | |
| 26 | 31 | workflows: unwrap(workflows), | |
| 27 | 32 | runs: runs.ok ? runs.value : [], | |
| 28 | − | member: roleIn(viewer, params.owner) != null, | |
| 33 | + | member, | |
| 34 | + | runnable: models == null || models.hosted || models.own != null, | |
| 29 | 35 | selected: selected ?? null, | |
| 30 | 36 | }; | |
| 31 | 37 | } | |
| 246 | 252 | - run: npm test`; | |
| 247 | 253 | ||
| 248 | 254 | export default function Actions({ loaderData, actionData, params }: Route.ComponentProps) { | |
| 249 | − | const { workflows, runs, member, selected } = loaderData; | |
| 255 | + | const { workflows, runs, member, runnable, selected } = loaderData; | |
| 250 | 256 | const [search] = useSearchParams(); | |
| 251 | 257 | const base = `/${params.owner}/${params.repo}`; | |
| 252 | 258 | const workflow = workflows.find((w) => w.id === selected || w.path.endsWith(`/${selected}`)) ?? null; | |
| 270 | 276 | </a> | |
| 271 | 277 | </header> | |
| 272 | 278 | ||
| 279 | + | {member && !runnable && ( | |
| 280 | + | <div className="mb-6 flex flex-wrap items-center justify-between gap-3 rounded-xl bg-surface px-4 py-3 text-sm ring-1 ring-line"> | |
| 281 | + | <span className="flex items-center gap-2 text-muted"> | |
| 282 | + | <AlertTriangle size={15} className="shrink-0 text-warn" /> | |
| 283 | + | Workflows run on g1t's runners once this workspace connects a model provider for its agents. Free while g1t is | |
| 284 | + | being built out. | |
| 285 | + | </span> | |
| 286 | + | <Link to={`/${params.owner}/-/integrations`} className="text-accent hover:underline"> | |
| 287 | + | Connect a model | |
| 288 | + | </Link> | |
| 289 | + | </div> | |
| 290 | + | )} | |
| 291 | + | ||
| 273 | 292 | {workflows.length === 0 ? ( | |
| 274 | 293 | <div className="grid gap-6 lg:grid-cols-2"> | |
| 275 | 294 | <EmptyState title="No workflows yet"> |
| 478 | 478 | </Form> | |
| 479 | 479 | )} | |
| 480 | 480 | ||
| 481 | + | {open && !agentsEnabled && canManage && !assigned && !issue.queued && ( | |
| 482 | + | // Where g1t's agent would be, and what makes it appear. | |
| 483 | + | <div className="mt-3 rounded-lg border border-dashed border-line p-3 text-sm"> | |
| 484 | + | <p className="flex items-center gap-1.5 font-medium"> | |
| 485 | + | <Sparkles size={14} className="text-accent" /> | |
| 486 | + | g1t agent | |
| 487 | + | </p> | |
| 488 | + | <p className="mt-1 text-xs text-muted"> | |
| 489 | + | Connect a model provider and g1t's agent can take this issue: it opens a pull request and sees it | |
| 490 | + | through checks, review and fixes. g1t costs nothing while it is being built out. | |
| 491 | + | </p> | |
| 492 | + | <Link | |
| 493 | + | to={`/${params.owner}/-/integrations`} | |
| 494 | + | className="mt-2 inline-block text-xs text-accent hover:underline" | |
| 495 | + | > | |
| 496 | + | Connect a model | |
| 497 | + | </Link> | |
| 498 | + | </div> | |
| 499 | + | )} | |
| 500 | + | ||
| 481 | 501 | {viewer && canManage && open && ( | |
| 482 | 502 | <div className="mt-3 space-y-2"> | |
| 483 | 503 | {!issue.assignees.includes(viewer.username) && ( |
| 214 | 214 | Integrations | |
| 215 | 215 | </Link> | |
| 216 | 216 | , the provider bills you for the model and each run here is a flat{" "} | |
| 217 | − | {dollars(account.orchestrationFeeMicros)}. | |
| 217 | + | {dollars(account.orchestrationFeeMicros)} | |
| 218 | + | {status.free && " once pricing starts; nothing while g1t is being built out"}. | |
| 218 | 219 | </li> | |
| 219 | 220 | <li> | |
| 220 | 221 | Only members of <span className="font-mono text-fg">{slug}</span> can |
| 68 | 68 | hostedOpen: access.hosted, | |
| 69 | 69 | routes: routes.ok ? routes.value : [], | |
| 70 | 70 | feeMicros: account.ok ? account.value.orchestrationFeeMicros : 100_000, | |
| 71 | + | free: account.ok ? Boolean(account.value.status.free) : false, | |
| 71 | 72 | marginPercent: account.ok ? account.value.marginPercent : 20, | |
| 72 | 73 | }; | |
| 73 | 74 | } | |
| 174 | 175 | } | |
| 175 | 176 | ||
| 176 | 177 | export default function WorkspaceIntegrations({ loaderData, actionData }: Route.ComponentProps) { | |
| 177 | − | const { slug, role, connections, deliveries, repos: repoNames, adding, feeMicros, marginPercent, hostedOpen, routes } = | |
| 178 | + | const { slug, role, connections, deliveries, repos: repoNames, adding, feeMicros, free, marginPercent, hostedOpen, routes } = | |
| 178 | 179 | loaderData; | |
| 179 | 180 | const owner = role === "owner"; | |
| 180 | 181 | const busy = useNavigation().state === "submitting"; | |
| 224 | 225 | /> | |
| 225 | 226 | )} | |
| 226 | 227 | <p className="mt-3 text-xs text-faint"> | |
| 227 | − | On your own providers, their bills are yours and g1t charges {dollars(feeMicros)} a run for the | |
| 228 | − | sandbox and orchestration. Keys go only from g1t's model proxy to the provider: the agent's | |
| 228 | + | On your own providers, their bills are yours.{" "} | |
| 229 | + | {free | |
| 230 | + | ? `g1t charges nothing while it is being built out; once pricing starts, ${dollars(feeMicros)} a run for the sandbox and orchestration.` | |
| 231 | + | : `g1t charges ${dollars(feeMicros)} a run for the sandbox and orchestration.`}{" "} | |
| 232 | + | Keys go only from g1t's model proxy to the provider: the agent's | |
| 229 | 233 | sandbox holds a token that dies with the run. | |
| 230 | 234 | </p> | |
| 231 | 235 | {owner && <ModelCatalog slug={slug} adding={adding} />} |