Compare changes
Choose two branches to see what one has that the other does not, then open a pull request for it.
1 commit
8 files+116−260/8 viewed
| 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} />} |