Skip to content

g1t/apps/web/app/routes/repo/pull.tsx

1,432 lines63,093 bytesCodeBlameRaw

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.

Acceptance checks in sandboxes, line comments and review verdicts1import { env } from "cloudflare:workers";
Issues and pull requests replace intents and attempts2import {
3 Bot,
4 ChevronRight,
5 FileDiff,
Pull requests from branches6 GitBranch,
Issues and pull requests replace intents and attempts7 GitCommitHorizontal,
Acceptance checks in sandboxes, line comments and review verdicts8 CircleCheck,
9 CircleSlash,
Issues and pull requests replace intents and attempts10 GitMerge,
Agents as a team: lifecycle, merge queue, billing and a new shell11 Layers,
12 GitPullRequestArrow,
13 Hand,
14 Loader,
15 Radar,
16 Sparkles,
Issues and pull requests replace intents and attempts17 MessageSquare,
Pull requests say where they are live; the project menu shows what is coming18 ArrowUpRight,
Issues and pull requests replace intents and attempts19 MessagesSquare,
Pull requests say where they are live; the project menu shows what is coming20 Rocket,
Project dependencies: addresses, preview stacks, Affects, and agents who know21 Network,
Issues and pull requests replace intents and attempts22 StickyNote,
23 User,
24 Wrench,
25} from "lucide-react";
Fast pages, required checks on the branch, self-hosted runners, honest incidents26import { Suspense, useEffect } from "react";
27import { Await, Form, Link, redirect, useNavigation, useRevalidator } from "react-router";
Issues and pull requests replace intents and attempts28
Pull requests say where they are live; the project menu shows what is coming29import {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look30 type Capability,
Pull requests say where they are live; the project menu shows what is coming31 type Comparison,
32 type Deployment,
Agents and memory, checks and conflicts, profiles, slug renames, custom domains33 type Job,
Pull requests say where they are live; the project menu shows what is coming34 type LiveApp,
35 type SessionEntry,
36 type Viewer,
37 pullComparison,
38} from "@g1t/contracts";
Issues and pull requests replace intents and attempts39
40import type { Route } from "./+types/pull";
Social cards for every page: og.g1t.sh41import { excerpt, page } from "../../lib/meta";
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent42import { openedBy } from "../../lib/opened-by";
Issues and pull requests replace intents and attempts43import { DiffView } from "../../components/diff-view";
Agents as a team: lifecycle, merge queue, billing and a new shell44import { LifecyclePanel } from "../../components/lifecycle";
Agents and memory, checks and conflicts, profiles, slug renames, custom domains45import { AgentPanel } from "../../components/agents";
Issues and pull requests replace intents and attempts46import { Markdown } from "../../components/markdown";
47import {
48 Avatar,
49 Button,
Agents as a team: lifecycle, merge queue, billing and a new shell50 ButtonLink,
Issues and pull requests replace intents and attempts51 CopyLine,
52 EmptyState,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look53 ComputeNote,
Issues and pull requests replace intents and attempts54 ErrorText,
55 Textarea,
56 TimeAgo,
57} from "../../components/ui";
Workspace names and icons, and a component kit for every control58import { CheckboxOption } from "../../components/ui/checkbox";
GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs59import { WorkflowStatuses } from "../../components/actions";
Fast pages, required checks on the branch, self-hosted runners, honest incidents60import { AddCiPrompt } from "../../components/add-ci";
Agents as a team: lifecycle, merge queue, billing and a new shell61import {
62 CommentForm,
63 CommentList,
64 IssueIcon,
65 PeoplePicker,
Agents and memory, checks and conflicts, profiles, slug renames, custom domains66 PersonLink,
Agents as a team: lifecycle, merge queue, billing and a new shell67 PullState,
68 TimelineItem,
69 verdicts,
70} from "../../components/work";
Catching up with main takes seconds when the two sides touched different files71import { CatchUpProgress, ChecksSection, ConflictsSection, MergeabilityRow, runIdOf } from "../../components/merge-box";
72import { CATCH_UP_TIMEOUT_MS } from "../../lib/catch-up";
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put73import { notFound } from "../../lib/not-found.server";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look74import { computeNoteFor } from "../../lib/compute.server";
Agents and memory, checks and conflicts, profiles, slug renames, custom domains75import { actions, deployments, identity, projects, repos, work } from "../../lib/services.server";
Issues and pull requests replace intents and attempts76import { assertSameOrigin, getViewer, requireUser } from "../../lib/session.server";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look77import { accessTo, refusal } from "../../lib/access.server";
Issues and pull requests replace intents and attempts78
79const REFRESH_MS = 4000;
80const EMPTY_COMPARISON: Comparison = { base: null, head: "", files: [], truncated: false };
81const TABS = ["conversation", "session", "changes"] as const;
82type Tab = (typeof TABS)[number];
83
Social cards for every page: og.g1t.sh84export function meta({ loaderData, params, ...args }: Route.MetaArgs) {
85 const pull = loaderData?.pull;
86 const title = pull ? `${pull.title} · Pull request #${pull.number} · ` : "";
87 const state = { draft: "Draft", open: "Open", merged: "Merged", closed: "Closed" }[pull?.status ?? "open"];
88 const body = excerpt(pull?.body);
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent89 const opener = pull ? openedBy(pull) : null;
Social cards for every page: og.g1t.sh90 return page(args, {
91 title: `${title}${params.owner}/${params.repo} · g1t`,
92 description: pull
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent93 ? `${state} pull request #${pull.number} on ${params.owner}/${params.repo} by ${opener!.name}${opener!.requestedBy ? `, requested by ${opener!.requestedBy}` : ""}.${body ? ` ${body}` : ""}`
Social cards for every page: og.g1t.sh94 : null,
95 // The card shows the title and the state.
96 version: pull ? [pull.title, pull.status] : undefined,
97 type: "article",
98 });
Issues and pull requests replace intents and attempts99}
100
101export async function loader({ params, context, request }: Route.LoaderArgs) {
102 const viewer = getViewer(context);
103 const path = { namespace: params.owner, name: params.repo };
104 const number = Number(params.number);
105 const asked = new URL(request.url).searchParams.get("tab");
106 const tab: Tab = TABS.find((name) => name === asked) ?? "conversation";
107
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look108 // Members of the workspace pick reviewers and assignees from its people;
109 // what the viewer may do here goes by their role on the repository.
Agents as a team: lifecycle, merge queue, billing and a new shell110 const member = (viewer?.workspaces ?? []).some(
111 (membership) => membership.slug === params.owner,
112 );
Fast pages, required checks on the branch, self-hosted runners, honest incidents113 // Everything starts at once. Only what the viewer's role decides waits
114 // for the repository, which the layout is looking up in this request too
115 // (lib/access.server.ts), and only what the pull request decides waits
116 // for it.
Project dependencies: addresses, preview stacks, Affects, and agents who know117 const ref = { workspace: params.owner, slug: params.repo };
Fast pages, required checks on the branch, self-hosted runners, honest incidents118 const access = accessTo(context, params);
119 const pullFound = work.getPull(path, number, viewer);
120 const deps = projects.dependencies(params.owner, params.repo, viewer);
121 // Awaited below, unless the pull request is missing first.
122 deps.catch(() => null);
123 const [{ can }, found, repo, settings, agentsEnabled, members, computeNote, session, deployed] = await Promise.all([
124 access,
125 pullFound,
Issues and pull requests replace intents and attempts126 repos.get(path, viewer),
Agents as a team: lifecycle, merge queue, billing and a new shell127 work.getSettings(path, viewer),
g1t's agents only for listed workspaces, whatever the state of billing128 env.RUNNER.enabled(viewer, path),
Agents as a team: lifecycle, merge queue, billing and a new shell129 // A member picks reviewers and assignees from the workspace's people.
130 member ? identity.listMembers(params.owner, viewer) : null,
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent131 // Before someone asks g1t for something: whether the plan lets it start.
Fast pages, required checks on the branch, self-hosted runners, honest incidents132 access.then(({ can }) => (can.run ? computeNoteFor(params.owner, "agent") : null)),
133 tab === "session" ? work.readSession(path, number, viewer) : null,
134 // Its preview, for people with a role here: beside the rest, not after.
135 access.then(({ insider }) => (insider ? deployments.list(ref, viewer).catch(() => null) : null)),
Issues and pull requests replace intents and attempts136 ]);
137 if (!found.ok) {
138 // Issues and pull requests share numbers; this one may be an issue.
139 const issue = await work.getIssue(path, number, viewer);
140 if (issue.ok) throw redirect(`/${params.owner}/${params.repo}/issues/${number}`);
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put141 throw notFound("pull");
Issues and pull requests replace intents and attempts142 }
143 const { pull } = found.value;
Pull requests from branches144 const range = pullComparison(pull);
Agents and memory, checks and conflicts, profiles, slug renames, custom domains145 // The jobs of each workflow run on its head, to list checks job by job.
146 // Read as the viewer: a run they cannot see is listed by its status alone.
Fast pages, required checks on the branch, self-hosted runners, honest incidents147 // Streamed: the checks show by status first, then job by job.
Agents and memory, checks and conflicts, profiles, slug renames, custom domains148 const runIds = [...new Set((found.value.statuses ?? []).map(runIdOf).filter((id) => id != null))].slice(0, 10);
Fast pages, required checks on the branch, self-hosted runners, honest incidents149 const workflowJobs =
150 tab === "conversation" && pull.status === "open" && runIds.length > 0
151 ? Promise.all(runIds.map((id) => actions.run(path, viewer, id).catch(() => null))).then((runs) => {
152 const jobs: Record<string, Job[]> = {};
153 for (const run of runs) if (run?.ok) jobs[run.value.run.id] = run.value.jobs;
154 return jobs;
155 })
156 .catch(() => ({}) as Record<string, Job[]>)
157 : Promise.resolve({} as Record<string, Job[]>);
158 // No checks at all on an open pull request: whether that is because the
159 // repository has no workflows, to offer a starter one.
160 const unchecked =
161 tab === "conversation" &&
162 pull.status === "open" &&
163 (found.value.statuses ?? []).length === 0 &&
164 (found.value.requiredChecks ?? []).length === 0;
165 const [comparison, used, noChecks] = await Promise.all([
166 tab === "changes" ? repos.compare(range.repoId, viewer, range.base, range.head) : null,
167 deps,
168 unchecked
169 ? actions
170 .workflows(path, viewer)
171 .then((found) => found.ok && found.value.length === 0)
172 .catch(() => false)
173 : false,
Issues and pull requests replace intents and attempts174 ]);
Fast pages, required checks on the branch, self-hosted runners, honest incidents175 const affects = used.ok ? used.value.usedBy : [];
Issues and pull requests replace intents and attempts176 return {
177 ...found.value,
Agents and memory, checks and conflicts, profiles, slug renames, custom domains178 workflowJobs,
Issues and pull requests replace intents and attempts179 tab,
180 session: session?.ok ? session.value : [],
181 // An empty comparison if it could not be made.
182 comparison: comparison && (comparison.ok ? comparison.value : EMPTY_COMPARISON),
183 viewer,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look184 // Write and up can merge.
185 canMerge: can.merge,
186 // Triage and up manage anyone's pull request; its author, their own.
187 canManage: can.triage || (viewer != null && viewer.id === pull.author.id),
188 // Telling its agent things, and re-running checks, spend compute: Write and up.
189 canRun: can.run,
Agents as a team: lifecycle, merge queue, billing and a new shell190 // A catch-up is pushed as the viewer: a fork takes pushes only from
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look191 // whoever opened it, a branch from anyone who can push.
192 canUpdate: pull.fork ? viewer?.id === pull.author.id : can.push,
Agents as a team: lifecycle, merge queue, billing and a new shell193 agentsEnabled,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look194 computeNote,
Agents as a team: lifecycle, merge queue, billing and a new shell195 members: members?.ok ? members.value.map((person) => person.username) : [],
196 requireUpToDate: settings.ok && settings.value.requireUpToDate,
197 mergeQueue: settings.ok && settings.value.mergeQueue,
198 requiredApprovals: settings.ok ? settings.value.requiredApprovals : 0,
199 canIgnoreChecks: !settings.ok || settings.value.allowIgnoringChecks,
Fast pages, required checks on the branch, self-hosted runners, honest incidents200 noChecks,
201 // Who may open the pull request that adds CI: anyone who can push.
202 canAddCi: can.push,
203 // Who may choose the required checks.
204 canProtect: can.manage_protection,
Issues and pull requests replace intents and attempts205 defaultBranch: repo.ok ? repo.value.defaultBranch : "main",
Fast pages, required checks on the branch, self-hosted runners, honest incidents206 affects,
207 // Streamed: the conversation shows first, the preview card after.
208 ...deploymentOf(deployed?.ok ? deployed.value : null, number, params.owner, affects, viewer),
Issues and pull requests replace intents and attempts209 };
210}
211
Pull requests say where they are live; the project menu shows what is coming212/**
213 * Where this pull request is live: its preview (or its latest build, while
214 * one is going or after one failed), and the previews of the projects
215 * that use this one, built against it.
216 */
Fast pages, required checks on the branch, self-hosted runners, honest incidents217function deploymentOf(
Pull requests say where they are live; the project menu shows what is coming218 list: { deployments: Deployment[]; live: LiveApp[] } | null,
219 number: number,
220 owner: string,
221 affects: { slug: string; name: string }[],
222 viewer: Viewer,
Fast pages, required checks on the branch, self-hosted runners, honest incidents223): { preview: LiveApp | null; build: Deployment | null; stacked: Promise<Stacked[]> } {
224 const preview = list?.live.find((app) => app.kind === "preview" && app.number === number) ?? null;
225 const build = list?.deployments.find((d) => d.kind === "preview" && d.number === number) ?? null;
Pull requests say where they are live; the project menu shows what is coming226 const branch = preview?.branch ?? build?.branch ?? null;
Fast pages, required checks on the branch, self-hosted runners, honest incidents227 // Streamed: another lookup per project, shown in the card once known.
228 const stacked: Promise<Stacked[]> = branch
229 ? Promise.all(
230 affects.slice(0, 5).map(async (project) => {
231 const theirs = await deployments.list({ workspace: owner, slug: project.slug }, viewer).catch(() => null);
232 const app = theirs?.ok ? theirs.value.live.find((a) => a.kind === "preview" && a.branch === branch) : undefined;
233 return app ? { name: project.name, slug: project.slug, url: app.url } : null;
234 }),
235 ).then((found) => found.filter((entry) => entry != null))
236 : Promise.resolve([]);
Pull requests say where they are live; the project menu shows what is coming237 return { preview, build, stacked };
238}
239
Fast pages, required checks on the branch, self-hosted runners, honest incidents240/** A project that uses this one, with its preview built against this change. */
241type Stacked = { name: string; slug: string; url: string };
242
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look243const PULL_NEEDS: Record<string, Capability> = {
244 stack: "run",
245 "agent-review": "run",
246 "rerun-workflow": "run",
Fast pages, required checks on the branch, self-hosted runners, honest incidents247 "rerun-failed": "run",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look248 message: "run",
249 merge: "merge",
250 unqueue: "merge",
251};
252
Issues and pull requests replace intents and attempts253export async function action({ request, params, context }: Route.ActionArgs) {
254 assertSameOrigin(request);
255 const user = requireUser(context, request);
256 const form = await request.formData();
257 const path = { namespace: params.owner, name: params.repo };
258 const number = Number(params.number);
259 const action = form.get("action");
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look260 // What each form needs beyond reading; work checks the rest.
261 const needed = typeof action === "string" ? PULL_NEEDS[action] : undefined;
262 const refused = needed ? await refusal(context, params, needed) : null;
263 if (refused) return { error: refused, action: String(action) };
Acceptance checks in sandboxes, line comments and review verdicts264 const verdict = form.get("verdict");
265 const line = Number(form.get("line"));
Agents as a team: lifecycle, merge queue, billing and a new shell266 /** Names ticked in a people picker, plus those typed beside it. */
267 const picked = (field: string) => [
268 ...form.getAll(field).map(String),
269 ...String(form.get("others") ?? "").split(/[\s,]+/),
270 ];
Project dependencies: addresses, preview stacks, Affects, and agents who know271 // The projects that use this one, built against this pull request's preview.
272 if (action === "stack") {
273 const built = await deployments.stack(user, { workspace: params.owner, slug: params.repo }, String(form.get("branch") ?? ""));
274 return built.ok
275 ? { action, notice: `Building ${built.value.join(", ")} against this preview. They appear on their Deployments pages.` }
276 : { action, error: built.error.message };
277 }
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent278 // Asking g1t for its review records the request, then starts it.
Agents as a team: lifecycle, merge queue, billing and a new shell279 if (action === "agent-review") {
280 const asked = await work.updatePull(user, path, number, {
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent281 reviewers: [...form.getAll("reviewer").map(String), "g1t"],
Agents as a team: lifecycle, merge queue, billing and a new shell282 });
283 if (!asked.ok) return { error: asked.error.message, action };
284 }
Catching up with main takes seconds when the two sides touched different files285 // Catching up: merged and pushed in seconds when the two sides changed
286 // different files; otherwise handed to a sandbox, which takes a minute.
287 if (action === "update") {
288 const caught = await work.catchUpPull(user, path, number);
289 if (!caught.ok) return { action, error: caught.error.message };
290 const update = caught.value;
291 if (update.outcome !== "needs_agent") {
292 return {
293 action,
294 updated: { commit: update.commit, already: update.outcome === "up_to_date", at: Date.now() },
295 };
296 }
297 const started = await env.RUNNER.update(user, path, number);
298 if (!started.ok) return { action, error: started.error.message };
299 return { action, agent: { update, startedAt: Date.now() } };
300 }
Agents and memory, checks and conflicts, profiles, slug renames, custom domains301 // A workflow run's failed jobs, run again. Who may is the actions service's call.
302 if (action === "rerun-workflow") {
303 const rerun = await actions.rerun(user, path, String(form.get("run") ?? ""), true);
304 return rerun.ok ? null : { error: rerun.error.message, action };
305 }
Fast pages, required checks on the branch, self-hosted runners, honest incidents306 // Every failed run on its head, run again: Mission control's quick action.
307 if (action === "rerun-failed") {
308 const found = await work.getPull(path, number, user);
309 if (!found.ok) return { error: found.error.message, action };
310 const failed = [
311 ...new Set(
312 (found.value.statuses ?? [])
313 .filter((status) => status.state === "failure" || status.state === "error")
314 .map(runIdOf)
315 .filter((id) => id != null),
316 ),
317 ];
318 if (failed.length === 0) return { error: "No workflow run failed on its latest commit.", action };
319 const reruns = await Promise.all(failed.map((id) => actions.rerun(user, path, id, true)));
320 const refused = reruns.find((rerun) => !rerun.ok);
321 return refused && !refused.ok ? { error: refused.error.message, action } : null;
322 }
Issues and pull requests replace intents and attempts323 const result =
324 action === "merge"
Acceptance checks in sandboxes, line comments and review verdicts325 ? await work.mergePull(user, path, number, {
326 keepIssueOpen: form.get("keepIssueOpen") === "on",
327 ignoreChecks: form.get("ignoreChecks") === "on",
328 })
Agents as a team: lifecycle, merge queue, billing and a new shell329 : action === "unqueue"
330 ? await work.removeFromQueue(user, path, number)
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request331 : action === "message"
332 ? await work.messageAgent(user, path, number, String(form.get("body") ?? ""))
Issues and pull requests replace intents and attempts333 : action === "close"
334 ? await work.closePull(user, path, number)
Fast pages, required checks on the branch, self-hosted runners, honest incidents335 : action === "agent-review"
Agents as a team: lifecycle, merge queue, billing and a new shell336 ? await env.RUNNER.review(user, path, number)
337 : action === "reviewers"
338 ? await work.updatePull(user, path, number, { reviewers: picked("reviewer") })
339 : action === "assign"
340 ? await work.updatePull(user, path, number, { assignees: picked("assignee") })
Acceptance checks in sandboxes, line comments and review verdicts341 : action === "comment"
342 ? await work.addComment(user, path, number, {
343 body: String(form.get("body") ?? ""),
344 path: String(form.get("path") ?? "") || undefined,
345 line: line > 0 ? line : undefined,
346 verdict:
347 verdict === "approve" || verdict === "request_changes" ? verdict : undefined,
348 })
349 : await work.readyPull(user, path, number, String(form.get("summary") ?? ""));
Issues and pull requests replace intents and attempts350 return result.ok ? null : { error: result.error.message, action };
351}
352
Pull requests: title and tabs across the full width, with counts and the size of the change353/** How many, beside a tab's name, when there are any. */
354function TabCount({ n }: { n: number }) {
355 if (n <= 0) return null;
356 return (
357 <span className="rounded-full bg-raised px-1.5 py-px text-xs tabular-nums text-muted ring-1 ring-line">
358 {n.toLocaleString("en-US")}
359 </span>
360 );
361}
362
Issues and pull requests replace intents and attempts363function TabLink({
364 to,
365 active,
366 children,
367}: {
368 to: string;
369 active: boolean;
370 children: React.ReactNode;
371}) {
372 return (
373 <Link
374 to={to}
375 preventScrollReset
376 className={
Fast pages, required checks on the branch, self-hosted runners, honest incidents377 "-mb-px flex items-center gap-2 border-b-2 px-3 pb-2.5 text-sm transition-colors " +
Issues and pull requests replace intents and attempts378 (active
379 ? "border-accent font-medium text-fg"
380 : "border-transparent text-muted hover:text-fg")
381 }
382 >
383 {children}
384 </Link>
385 );
386}
387
388/** One step of the session, on the timeline's rail. */
389function Entry({ entry, agent }: { entry: SessionEntry; agent: string }) {
390 const isTool = entry.kind === "tool_call" || entry.kind === "tool_result";
391 const Icon =
392 entry.kind === "prompt"
393 ? User
394 : entry.kind === "note"
395 ? StickyNote
396 : isTool
397 ? Wrench
398 : Bot;
399 return (
400 <li className="relative pl-10">
401 <span
402 className={`absolute top-0.5 left-0 flex size-7 items-center justify-center rounded-full border bg-bg ${
403 entry.kind === "prompt"
404 ? "border-accent/50 text-accent"
405 : "border-line text-faint"
406 }`}
407 >
408 <Icon size={14} />
409 </span>
410 {isTool ? (
411 <details className="group rounded-lg border border-line bg-surface">
412 <summary className="flex cursor-pointer list-none items-center gap-2 px-3 py-1.5 text-sm">
413 <ChevronRight
414 size={14}
415 className="text-faint transition-transform group-open:rotate-90"
416 />
417 <span className="font-mono text-xs text-accent">
418 {entry.tool ?? "tool"}
419 </span>
420 <span className="truncate font-mono text-xs text-muted">
421 {entry.kind === "tool_result" ? "→ " : ""}
422 {entry.text.split("\n")[0]}
423 </span>
424 </summary>
425 <pre className="overflow-x-auto border-t border-line p-3 font-mono text-xs whitespace-pre-wrap text-muted">
Fix a hydration mismatch in output that starts with a blank line426 <code>{entry.text}</code>
Issues and pull requests replace intents and attempts427 </pre>
428 </details>
429 ) : (
430 <div>
431 <p className="text-xs font-medium text-faint">
432 {entry.kind === "prompt"
433 ? "Prompt"
434 : entry.kind === "note"
435 ? "Note"
436 : agent}
437 </p>
438 {entry.kind === "message" ? (
439 <div className="mt-1">
440 <Markdown source={entry.text} />
441 </div>
Agents as a team: lifecycle, merge queue, billing and a new shell442 ) : entry.kind === "prompt" ? (
443 <div className="mt-1.5 max-h-[32rem] overflow-y-auto rounded-xl border border-line bg-surface p-4">
444 <Markdown source={entry.text} />
445 </div>
Issues and pull requests replace intents and attempts446 ) : (
Agents as a team: lifecycle, merge queue, billing and a new shell447 <p className="mt-1 text-[0.9375rem] leading-relaxed wrap-break-word whitespace-pre-wrap">
Issues and pull requests replace intents and attempts448 {entry.text}
449 </p>
450 )}
451 </div>
452 )}
453 {entry.commit && (
454 <p className="mt-1.5 flex items-center gap-1 font-mono text-xs text-faint">
455 <GitCommitHorizontal size={12} />
456 {entry.commit.slice(0, 7)}
457 </p>
458 )}
459 </li>
460 );
461}
462
Agents as a team: lifecycle, merge queue, billing and a new shell463/** The box at the foot of the conversation saying what stands before a merge. */
464function StatusBox({ children }: { children: React.ReactNode }) {
465 return (
466 <div className="flex gap-3">
467 <span className="hidden w-8 shrink-0 sm:block" />
468 <section className="min-w-0 grow divide-y divide-line overflow-hidden rounded-xl border border-line-strong bg-surface">
469 {children}
470 </section>
471 </div>
472 );
473}
474
475function StatusRow({
476 icon,
477 title,
478 children,
479}: {
480 icon: React.ReactNode;
481 title: string;
482 children?: React.ReactNode;
483}) {
484 return (
485 <div className="flex gap-3 px-4 py-3 text-sm">
486 <span className="mt-0.5 shrink-0">{icon}</span>
487 <div className="min-w-0">
488 <p className="font-medium">{title}</p>
489 {children && <div className="mt-0.5 text-muted">{children}</div>}
490 </div>
491 </div>
492 );
493}
494
Issues and pull requests replace intents and attempts495export default function PullPage({ loaderData, actionData, params }: Route.ComponentProps) {
496 const {
497 pull,
498 issue,
499 comments,
Acceptance checks in sandboxes, line comments and review verdicts500 checks,
Agents as a team: lifecycle, merge queue, billing and a new shell501 overlaps,
502 behind,
503 reviewPending,
504 lifecycle,
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request505 messages,
GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs506 statuses = [],
Fast pages, required checks on the branch, self-hosted runners, honest incidents507 requiredChecks = [],
508 noChecks,
509 canAddCi,
510 canProtect,
Agents and memory, checks and conflicts, profiles, slug renames, custom domains511 mergeable = "unknown",
512 conflicts = [],
513 earlierChecks = [],
514 workflowJobs,
Project dependencies: addresses, preview stacks, Affects, and agents who know515 affects,
516 preview,
Pull requests say where they are live; the project menu shows what is coming517 build,
518 stacked,
Agents as a team: lifecycle, merge queue, billing and a new shell519 landing,
520 stalled,
521 requireUpToDate,
522 mergeQueue,
523 requiredApprovals,
524 canIgnoreChecks,
525 canUpdate,
526 agentsEnabled,
527 members,
Issues and pull requests replace intents and attempts528 tab,
529 session,
530 comparison,
531 viewer,
532 canMerge,
533 canManage,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look534 canRun,
Issues and pull requests replace intents and attempts535 defaultBranch,
536 } = loaderData;
537 const base = `/${params.owner}/${params.repo}`;
538 const here = `${base}/pull/${pull.number}`;
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent539 // g1t, on a pull request it made: the person who asked for it is a line below.
540 const opener = openedBy(pull);
Fast pages, required checks on the branch, self-hosted runners, honest incidents541 const checksSection = (jobs: Record<string, Job[]>) => (
542 <ChecksSection
543 run={checks}
544 required={requiredChecks}
545 statuses={statuses}
546 jobs={jobs}
547 pull={pull}
548 base={base}
549 earlier={earlierChecks}
550 canRerunWorkflows={canMerge}
551 settingsUrl={canProtect ? `${base}/settings/branches` : null}
552 error={actionData?.action === "rerun-failed" || actionData?.action === "rerun-workflow" ? actionData.error : null}
553 />
554 );
Pull requests from branches555 const remote = pull.fork
556 ? `https://g1t.sh/${pull.fork.namespace}/${pull.fork.name}.git`
557 : `https://g1t.sh/${params.owner}/${params.repo}.git`;
Issues and pull requests replace intents and attempts558 const active = pull.status === "draft" || pull.status === "open";
559
Acceptance checks in sandboxes, line comments and review verdicts560 // Follow an agent at work, or checks in progress, without a manual reload.
Issues and pull requests replace intents and attempts561 const revalidator = useRevalidator();
562 const working = pull.status === "draft";
Fast pages, required checks on the branch, self-hosted runners, honest incidents563 const checking = statuses.some((status) => status.state === "pending");
Acceptance checks in sandboxes, line comments and review verdicts564 const reviews = verdicts(comments);
Fast pages, required checks on the branch, self-hosted runners, honest incidents565 // What stands between this pull request and a merge, if anything: a
566 // required check that has not passed, or the merge queue taking it out.
567 const unchecked = requiredChecks.some((check) => check.state !== "success") || checks?.status === "failed";
568 const requiredFailed = requiredChecks.some((check) => check.state === "failure") || checks?.status === "failed";
Agents as a team: lifecycle, merge queue, billing and a new shell569 // Pull requests for other issues changing the same files will conflict;
570 // ones for the same issue are alternatives, and expected to.
571 const collisions = overlaps.filter((other) => other.issue == null || other.issue !== pull.issue);
572 const review = {
573 changesUrl: here + "?tab=changes",
574 // Nobody reviews their own pull request.
575 canJudge: active && viewer != null && viewer.id !== pull.author.id,
576 };
577 // Everyone whose review was asked for, then anyone who reviewed unasked.
578 const reviewerNames = [
579 ...new Set([...pull.reviewers, ...reviews.map(({ reviewer }) => reviewer)]),
580 ];
Catching up with main takes seconds when the two sides touched different files581 // A catch-up: the click shows at once; the answer says whether it is
582 // done already or a sandbox is on it.
583 const navigation = useNavigation();
584 const catchUpPending = navigation.state !== "idle" && navigation.formData?.get("action") === "update";
585 const catchUp = actionData?.action === "update" ? actionData : null;
586 const caughtUp = catchUp && "updated" in catchUp ? catchUp.updated : null;
587 const agentCatchUp = catchUp && "agent" in catchUp ? catchUp.agent : null;
588 // Pushed already: followed until the pull request's head is the merge,
589 // so its checks show starting again, for a minute at most.
590 const settling =
591 caughtUp != null && !caughtUp.already && pull.headCommit !== caughtUp.commit && Date.now() - caughtUp.at < 60_000;
592 // Followed until it is pushed, or for so long, never longer.
593 const catchingUp =
594 agentCatchUp != null && behind && Date.now() - agentCatchUp.startedAt < CATCH_UP_TIMEOUT_MS + REFRESH_MS;
Agents as a team: lifecycle, merge queue, billing and a new shell595 // g1t is taking a step of its own accord, so the page will change.
596 const moving =
597 lifecycle != null && lifecycle.stage !== "ready" && lifecycle.stage !== "needs_you";
Agents and memory, checks and conflicts, profiles, slug renames, custom domains598 // Whether it merges cleanly is being worked out, so the box will change.
599 const probing = active && mergeable === "checking";
600 const conflicting = active && mergeable === "conflicting";
Issues and pull requests replace intents and attempts601 useEffect(() => {
Catching up with main takes seconds when the two sides touched different files602 if (!working && !checking && !reviewPending && !catchingUp && !settling && !moving && !landing && !probing) return;
Issues and pull requests replace intents and attempts603 const timer = setInterval(() => {
604 if (document.visibilityState === "visible") revalidator.revalidate();
605 }, REFRESH_MS);
606 return () => clearInterval(timer);
Catching up with main takes seconds when the two sides touched different files607 }, [working, checking, reviewPending, catchingUp, settling, moving, landing, probing, revalidator]);
Agents and memory, checks and conflicts, profiles, slug renames, custom domains608 // Why the merge button cannot be pressed, if it cannot.
609 const mergeBlocked = conflicting
610 ? "Resolve the conflicts first."
611 : probing
612 ? "Waiting to find out whether it merges cleanly."
613 : behind && requireUpToDate
614 ? `This repository requires it to be up to date with ${defaultBranch} first.`
Fast pages, required checks on the branch, self-hosted runners, honest incidents615 : unchecked && !canIgnoreChecks
616 ? `The checks ${defaultBranch} requires have to pass first.`
617 : null;
Issues and pull requests replace intents and attempts618
619 return (
Agents as a team: lifecycle, merge queue, billing and a new shell620 // The changes get the whole width; people and settings are a tab away.
Pull requests: title and tabs across the full width, with counts and the size of the change621 <div>
622 {/* The title and the tabs run the full width; what is happening on
623 the pull request sits at the top of Conversation. */}
624 <header>
Issues and pull requests replace intents and attempts625 <h2 className="text-2xl font-semibold tracking-tight text-balance">
626 {pull.title} <span className="font-normal text-faint">#{pull.number}</span>
627 </h2>
628 <div className="mt-3 flex flex-wrap items-center gap-x-3 gap-y-2 text-sm text-muted">
629 <PullState status={pull.status} />
630 <span className="flex items-center gap-2">
Agents as a team: lifecycle, merge queue, billing and a new shell631 <Avatar
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent632 name={(pull.status === "merged" && pull.mergedBy) || opener.name}
Agents as a team: lifecycle, merge queue, billing and a new shell633 size={18}
634 />
Issues and pull requests replace intents and attempts635 <span>
Agents and memory, checks and conflicts, profiles, slug renames, custom domains636 <PersonLink
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent637 name={(pull.status === "merged" && pull.mergedBy) || opener.name}
Agents and memory, checks and conflicts, profiles, slug renames, custom domains638 className="font-medium text-fg hover:underline"
639 />{" "}
Pull requests from branches640 {pull.status === "merged" ? "merged" : "wants to merge"}
641 {pull.branch && (
642 <>
643 {" "}
644 <span className="font-mono text-fg">{pull.branch}</span>
645 </>
646 )}{" "}
647 into <span className="font-mono text-fg">{defaultBranch}</span>
Issues and pull requests replace intents and attempts648 </span>
649 </span>
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent650 {opener.requestedBy && (
651 <span className="text-xs">
652 requested by{" "}
653 <PersonLink name={opener.requestedBy} className="font-medium text-fg-soft hover:underline" />
654 </span>
655 )}
656 {/* A pull request from a branch was made by its author, not an agent;
657 one g1t made already says so. */}
658 {!pull.branch && !opener.requestedBy && (
Pull requests from branches659 <span className="flex items-center gap-1.5 font-mono text-xs">
660 <Bot size={14} />
661 {pull.agent}
662 {pull.runtime === "hosted" && <span className="text-faint">on g1t</span>}
663 </span>
664 )}
Issues and pull requests replace intents and attempts665 </div>
666
Acceptance checks in sandboxes, line comments and review verdicts667 {reviews.length > 0 && (
668 <p className="mt-3 flex flex-wrap items-center gap-x-4 gap-y-1 text-sm">
669 {reviews.map(({ reviewer, verdict }) => (
670 <span
671 key={reviewer}
672 className={`flex items-center gap-1.5 ${
673 verdict === "approve" ? "text-accent" : "text-danger"
674 }`}
675 >
676 {verdict === "approve" ? <CircleCheck size={15} /> : <CircleSlash size={15} />}
677 {verdict === "approve" ? "Approved by" : "Changes requested by"}{" "}
678 <span className="font-medium">{reviewer}</span>
679 </span>
680 ))}
681 </p>
682 )}
683
Fast pages, required checks on the branch, self-hosted runners, honest incidents684 <nav className="mt-6 flex items-end gap-1 overflow-x-auto border-b border-line">
Issues and pull requests replace intents and attempts685 <TabLink to={here} active={tab === "conversation"}>
686 <MessageSquare size={15} />
687 Conversation
Pull requests: title and tabs across the full width, with counts and the size of the change688 <TabCount n={comments.length} />
Issues and pull requests replace intents and attempts689 </TabLink>
690 <TabLink to={here + "?tab=session"} active={tab === "session"}>
691 <MessagesSquare size={15} />
692 Session
693 </TabLink>
694 <TabLink to={here + "?tab=changes"} active={tab === "changes"}>
695 <FileDiff size={15} />
Pull requests: title and tabs across the full width, with counts and the size of the change696 Files changed
697 <TabCount n={pull.files.length} />
Issues and pull requests replace intents and attempts698 </TabLink>
Pull requests: title and tabs across the full width, with counts and the size of the change699 {pull.files.length > 0 && (
700 <span className="ml-auto hidden pb-2.5 font-mono text-xs sm:inline">
701 <span className="text-accent">+{pull.files.reduce((sum, file) => sum + file.additions, 0).toLocaleString("en-US")}</span>{" "}
702 <span className="text-danger">−{pull.files.reduce((sum, file) => sum + file.deletions, 0).toLocaleString("en-US")}</span>
703 </span>
704 )}
Issues and pull requests replace intents and attempts705 </nav>
Pull requests: title and tabs across the full width, with counts and the size of the change706 </header>
707 <div className={`mt-6 grid gap-8 ${tab === "changes" ? "" : "lg:grid-cols-[1fr_19rem]"}`}>
708 <div className="min-w-0">
709 {/* What is happening on it now: its stage, its agent, what people told
710 the agent, and what else touches the same files. On the conversation,
711 so the tabs sit right under the title. */}
712 {tab === "conversation" && (
713 <div>
714 {issue && (
715 <Link
716 to={`${base}/issues/${issue.number}`}
717 className="mt-4 flex items-center gap-2.5 rounded-xl border border-line bg-surface px-4 py-2.5 text-sm transition-colors hover:border-line-strong"
718 >
719 <IssueIcon issue={issue} />
720 <span className="text-muted">
721 {pull.status === "merged" && issue.resolvedBy === pull.number
722 ? "Resolved"
723 : "For issue"}
724 </span>
725 <span className="truncate font-medium">{issue.title}</span>
726 <span className="text-faint">#{issue.number}</span>
727 {issue.pullCount > 1 && (
728 <span className="ml-auto shrink-0 text-xs text-faint">
729 1 of {issue.pullCount} pull requests
Pull requests show their tabs right under the title; agent panels move into Conversation730 </span>
Pull requests: title and tabs across the full width, with counts and the size of the change731 )}
732 </Link>
733 )}
734 {pull.status === "merged" && (
735 <p className="mt-4 flex items-center gap-2.5 rounded-xl border border-merged/40 bg-merged/5 px-4 py-2.5 text-sm">
736 <GitMerge size={16} className="shrink-0 text-merged" />
Agents as a team: lifecycle, merge queue, billing and a new shell737 <span>
Pull requests: title and tabs across the full width, with counts and the size of the change738 Merged into <span className="font-mono">{defaultBranch}</span> by{" "}
739 <span className="font-medium">{pull.mergedBy}</span>{" "}
740 {/* Not an account: the repository's settings said to merge it. */}
741 {pull.mergedBy === "g1t" && "automatically, once it was ready, "}
742 {pull.mergedAt && <TimeAgo at={pull.mergedAt} />}
743 {pull.headCommit && (
744 <span className="font-mono text-muted"> · {pull.headCommit.slice(0, 7)}</span>
Agents as a team: lifecycle, merge queue, billing and a new shell745 )}
746 </span>
Pull requests: title and tabs across the full width, with counts and the size of the change747 </p>
748 )}
749 {pull.supersededBy != null && (
750 <p className="mt-4 rounded-xl border border-line bg-surface px-4 py-2.5 text-sm text-muted">
751 Closed because{" "}
752 <Link
753 to={`${base}/pull/${pull.supersededBy}`}
754 className="font-medium text-fg hover:underline"
755 >
756 #{pull.supersededBy}
757 </Link>{" "}
758 was merged for this issue instead.
759 </p>
760 )}
761 {lifecycle && <LifecyclePanel lifecycle={lifecycle} />}
762 {/* The agent on it: who, doing what this minute, for how long, at what cost. */}
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step763 <AgentPanel
764 owner={params.owner}
765 repo={params.repo}
766 number={pull.number}
767 stage={lifecycle?.stage ?? null}
768 confidence={pull.confidence ?? null}
769 />
Pull requests: title and tabs across the full width, with counts and the size of the change770
771 {/* Steering: while its agent works, people can tell it things. */}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look772 {canRun &&
Pull requests: title and tabs across the full width, with counts and the size of the change773 pull.runtime === "hosted" &&
774 (working || ["working", "revising", "catching_up", "answering"].includes(lifecycle?.stage ?? "")) && (
775 <Form method="post" className="mt-4 rounded-2xl bg-surface p-4 ring-1 ring-merged/30">
776 <p className="flex items-center gap-2 text-sm font-medium">
777 <Sparkles size={15} className="text-merged" />
778 Message the agent
779 </p>
780 <p className="mt-1 text-xs text-muted">
781 A correction, a hint, a change of plan. It reads it at its next step, without
782 starting over.
783 </p>
784 <div className="mt-3 flex gap-2">
785 <input type="hidden" name="action" value="message" />
786 <input
787 name="body"
788 required
789 autoComplete="off"
790 data-1p-ignore
791 placeholder="Keep the old flag working too…"
792 className="h-9 min-w-0 grow rounded-md bg-bg px-3 text-sm ring-1 ring-line outline-none placeholder:text-faint focus:ring-merged/60"
Agents as a team: lifecycle, merge queue, billing and a new shell793 />
Pull requests: title and tabs across the full width, with counts and the size of the change794 <Button type="submit">Send</Button>
795 </div>
796 </Form>
Agents as a team: lifecycle, merge queue, billing and a new shell797 )}
Pull requests: title and tabs across the full width, with counts and the size of the change798 {messages.length > 0 && (
799 <ul className="mt-3 space-y-1.5">
800 {messages.map((message) => (
801 <li key={message.id} className="flex items-start gap-2 text-sm">
802 <Avatar name={message.author} size={18} />
803 <span className="min-w-0 grow">
804 <span className="font-medium">
805 {message.fromNumber != null ? `The agent on #${message.fromNumber}` : message.author}
806 </span>{" "}
807 <span className="text-muted">
808 {message.kind === "question"
809 ? "asked:"
810 : message.kind === "handoff"
811 ? "handed over:"
812 : message.kind === "answer"
813 ? "answered:"
814 : "to the agent:"}
815 </span>{" "}
816 {message.body}
817 {message.answer && (
818 <span className="mt-1 block border-l-2 border-merged/40 pl-2 text-muted">
819 {message.declined ? "Declined: " : "Answer: "}
820 {message.answer}
821 </span>
822 )}
823 </span>
824 <span className={`shrink-0 text-xs ${message.deliveredAt ? "text-accent" : "text-faint"}`}>
825 {message.deliveredAt ? "read by the agent" : "waiting for its next step"}
826 </span>
827 </li>
828 ))}
829 </ul>
830 )}
831 {collisions.length > 0 && active && (
832 <div className="mt-4 rounded-xl border border-line bg-surface px-4 py-3 text-sm">
833 <p className="flex items-center gap-2.5 font-medium">
834 <Radar size={16} className="shrink-0 text-info" />
835 Other work is changing the same files
836 </p>
837 <ul className="mt-2 space-y-1.5">
838 {collisions.map((other) => (
839 <li key={other.number} className="flex flex-wrap items-baseline gap-x-2 text-muted">
840 <Link
841 to={`${base}/pull/${other.number}`}
842 className="font-medium text-fg hover:underline"
843 >
844 {other.title} <span className="font-normal text-faint">#{other.number}</span>
845 </Link>
846 <span className="font-mono text-xs">{other.paths.join(", ")}</span>
847 </li>
848 ))}
849 </ul>
850 <p className="mt-2 text-xs text-faint">
851 Whichever merges second will have to catch up, and may conflict.
852 </p>
853 </div>
854 )}
855 </div>
856 )}
857 <div className="mt-5">
858 {comparison ? (
859 <DiffView
860 comparison={comparison}
861 review={{
862 comments: comments.filter((comment) => comment.path),
863 canComment: Boolean(viewer),
864 }}
865 />
866 ) : tab === "session" ? (
867 session.length === 0 ? (
868 <EmptyState title="Nothing recorded yet">
869 The agent's prompts, reasoning and tool calls appear here as it
870 works.
871 </EmptyState>
872 ) : (
873 <ol className="relative space-y-5 before:absolute before:top-2 before:bottom-2 before:left-3.25 before:w-px before:bg-line">
874 {session.map((entry) => (
875 <Entry key={entry.seq} entry={entry} agent={pull.agent} />
876 ))}
877 </ol>
878 )
879 ) : (
880 <div className="space-y-4">
881 <TimelineItem
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent882 author={opener.name}
Pull requests: title and tabs across the full width, with counts and the size of the change883 at={pull.createdAt}
884 action={
885 <span>
886 opened this pull request
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent887 {opener.requestedBy ? (
Pull requests: title and tabs across the full width, with counts and the size of the change888 <>
889 {" "}
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent890 for{" "}
891 <PersonLink name={opener.requestedBy} className="font-medium text-fg-soft hover:underline" />
892 </>
893 ) : !pull.branch && (
894 <>
895 {" "}
Pull requests: title and tabs across the full width, with counts and the size of the change896 with <span className="font-mono text-xs">{pull.agent}</span>
897 </>
898 )}
899 </span>
900 }
901 >
902 {pull.body ? (
903 <Markdown source={pull.body} repo={{ namespace: params.owner, name: params.repo }} />
904 ) : (
905 <p className="text-sm text-muted">
906 {working
907 ? "No description yet. It is written when the pull request is marked ready for review."
908 : "No description."}
909 </p>
910 )}
911 {canManage && pull.status === "open" && (
912 <details className="mt-3 border-t border-line pt-3 text-sm">
913 <summary className="cursor-pointer text-xs text-faint hover:text-fg">
914 Edit description
915 </summary>
916 <Form method="post" className="mt-3 space-y-2">
917 <Textarea
918 name="summary"
919 rows={6}
920 placeholder="What changed and why"
921 defaultValue={pull.body ?? ""}
922 />
923 <Button type="submit">Save</Button>
924 </Form>
925 </details>
926 )}
927 </TimelineItem>
Agents as a team: lifecycle, merge queue, billing and a new shell928
Pull requests: title and tabs across the full width, with counts and the size of the change929 <CommentList comments={comments} review={review} base={base} />
Agents as a team: lifecycle, merge queue, billing and a new shell930
Fast pages, required checks on the branch, self-hosted runners, honest incidents931 {(preview || build) && <DeploymentCard preview={preview} build={build} stacked={stacked} base={base} />}
Pull requests say where they are live; the project menu shows what is coming932
Pull requests: title and tabs across the full width, with counts and the size of the change933 {pull.status === "draft" && (
934 <StatusBox>
935 <StatusRow icon={<Loader size={16} className="text-faint" />} title="This is a draft">
936 It is still being worked on. It can be reviewed and merged once it is
937 marked ready.
938 </StatusRow>
939 {canManage && (
940 <Form method="post" className="space-y-2 px-4 py-3">
941 <Textarea name="summary" rows={3} placeholder="What changed and why" />
942 <Button type="submit">Mark ready for review</Button>
943 </Form>
944 )}
945 </StatusBox>
946 )}
Agents as a team: lifecycle, merge queue, billing and a new shell947
Pull requests: title and tabs across the full width, with counts and the size of the change948 {pull.status === "open" && (
949 <StatusBox>
Fast pages, required checks on the branch, self-hosted runners, honest incidents950 {/* Checks by status at once; job by job when the runs are read. */}
951 <Suspense fallback={checksSection({})}>
952 <Await resolve={workflowJobs}>{checksSection}</Await>
953 </Suspense>
954 {noChecks && <AddCiPrompt owner={params.owner} repo={params.repo} canAdd={canAddCi} compact />}
Agents as a team: lifecycle, merge queue, billing and a new shell955 <StatusRow
Pull requests: title and tabs across the full width, with counts and the size of the change956 icon={
957 reviews.some(({ verdict }) => verdict === "request_changes") ? (
958 <CircleSlash size={16} className="text-danger" />
959 ) : reviews.length > 0 ? (
960 <CircleCheck size={16} className="text-accent" />
961 ) : (
962 <MessageSquare size={16} className="text-faint" />
963 )
964 }
965 title={
966 reviews.length === 0
967 ? "No reviews yet"
968 : reviews
969 .map(
970 ({ reviewer, verdict }) =>
971 `${verdict === "approve" ? "Approved by" : "Changes requested by"} ${reviewer}`,
972 )
973 .join(" · ")
974 }
Agents as a team: lifecycle, merge queue, billing and a new shell975 >
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent976 {reviewPending && "g1t is reviewing it now. "}
Pull requests: title and tabs across the full width, with counts and the size of the change977 {requiredApprovals > 0 &&
978 `This repository requires ${requiredApprovals} approving ${
979 requiredApprovals === 1 ? "review" : "reviews"
980 } before merging.`}
Agents as a team: lifecycle, merge queue, billing and a new shell981 </StatusRow>
Pull requests: title and tabs across the full width, with counts and the size of the change982 {landing ? (
Agents as a team: lifecycle, merge queue, billing and a new shell983 <StatusRow
Pull requests: title and tabs across the full width, with counts and the size of the change984 icon={<Loader size={16} className="animate-spin text-accent" />}
985 title="Merging"
986 >
987 {defaultBranch} has moved, so g1t is bringing this up to date first. It
988 lands as soon as that is done. Watch it in the Session tab.
989 </StatusRow>
Catching up with main takes seconds when the two sides touched different files990 ) : agentCatchUp && behind ? (
991 <CatchUpProgress
992 owner={params.owner}
993 repo={params.repo}
994 number={pull.number}
995 defaultBranch={defaultBranch}
996 behind={behind}
997 update={agentCatchUp.update}
998 startedAt={agentCatchUp.startedAt}
999 retrying={catchUpPending}
1000 />
Pull requests: title and tabs across the full width, with counts and the size of the change1001 ) : conflicting ? (
1002 <ConflictsSection
1003 conflicts={conflicts}
1004 pull={pull}
1005 owner={params.owner}
1006 repo={params.repo}
1007 defaultBranch={defaultBranch}
1008 changesUrl={here + "?tab=changes"}
1009 canResolve={canUpdate && agentsEnabled}
Catching up with main takes seconds when the two sides touched different files1010 resolving={catchUpPending}
1011 error={catchUp && "error" in catchUp ? catchUp.error : null}
Agents as a team: lifecycle, merge queue, billing and a new shell1012 />
Pull requests: title and tabs across the full width, with counts and the size of the change1013 ) : probing ? (
1014 <MergeabilityRow mergeable={mergeable} defaultBranch={defaultBranch} />
1015 ) : behind ? (
1016 <StatusRow
1017 icon={<GitPullRequestArrow size={16} className="text-info" />}
1018 title={`${defaultBranch} has moved since this was made`}
1019 >
1020 {mergeable === "clean" && "It has no conflicts with it. "}
1021 {requireUpToDate
1022 ? "This repository requires pull requests to be up to date, so it has to catch up before it can merge."
1023 : "That does not stop it merging: it is brought up to date as part of the merge."}
Catching up with main takes seconds when the two sides touched different files1024 {canUpdate && (
1025 <Form method="post" className="mt-2">
Pull requests: title and tabs across the full width, with counts and the size of the change1026 <input type="hidden" name="action" value="update" />
Catching up with main takes seconds when the two sides touched different files1027 <Button variant="quiet" type="submit" disabled={catchUpPending}>
1028 {catchUpPending && <Loader size={14} className="animate-spin" />}
1029 {catchUpPending ? `Merging ${defaultBranch} in…` : `Catch up with ${defaultBranch} now`}
Pull requests: title and tabs across the full width, with counts and the size of the change1030 </Button>
1031 </Form>
1032 )}
Catching up with main takes seconds when the two sides touched different files1033 {catchUp && "error" in catchUp && <ErrorText>{catchUp.error}</ErrorText>}
Pull requests: title and tabs across the full width, with counts and the size of the change1034 </StatusRow>
1035 ) : (
1036 pull.headCommit && (
1037 <StatusRow
1038 icon={<CircleCheck size={16} className="text-accent" />}
Catching up with main takes seconds when the two sides touched different files1039 title={
1040 (caughtUp && !caughtUp.already) || agentCatchUp
1041 ? `Brought up to date with ${defaultBranch}`
1042 : `Up to date with ${defaultBranch}`
1043 }
1044 >
1045 {caughtUp && !caughtUp.already && (
1046 <>
1047 Merged <span className="font-mono">{defaultBranch}</span> in as{" "}
1048 <span className="font-mono text-fg">{caughtUp.commit.slice(0, 7)}</span>. Its checks run
1049 again on the new commit.
1050 </>
1051 )}
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent1052 {agentCatchUp && "g1t merged it in and pushed the result. Its checks run again on the new commit."}
Catching up with main takes seconds when the two sides touched different files1053 </StatusRow>
Pull requests: title and tabs across the full width, with counts and the size of the change1054 )
1055 )}
1056 {stalled && !lifecycle && (
1057 <StatusRow icon={<Hand size={16} className="text-warn" />} title="Needs you">
1058 {stalled}
1059 </StatusRow>
1060 )}
1061 {canMerge && lifecycle?.stage === "queued" && (
1062 <Form method="post" className="flex flex-wrap items-center gap-3 px-4 py-3">
1063 <ButtonLink to={`${base}/queue`}>
1064 <Layers size={15} />
1065 See the queue
1066 </ButtonLink>
1067 <Button variant="quiet" type="submit" name="action" value="unqueue">
1068 Remove from the queue
Agents as a team: lifecycle, merge queue, billing and a new shell1069 </Button>
Pull requests: title and tabs across the full width, with counts and the size of the change1070 </Form>
1071 )}
1072 {canMerge && !landing && lifecycle?.stage !== "queued" && (
1073 <Form method="post" className="space-y-3 px-4 py-3">
1074 {issue?.state === "open" && (
1075 <CheckboxOption
1076 name="keepIssueOpen"
1077 label={`Keep #${issue.number} open. This is only part of the work.`}
1078 labelClassName="text-xs text-muted"
1079 />
1080 )}
1081 {unchecked && canIgnoreChecks && (
1082 <CheckboxOption
1083 name="ignoreChecks"
Fast pages, required checks on the branch, self-hosted runners, honest incidents1084 label={`Bypass the required checks: merge although ${requiredFailed ? "one failed" : "they have not all passed"}.`}
Pull requests: title and tabs across the full width, with counts and the size of the change1085 labelClassName="text-xs text-muted"
1086 />
1087 )}
1088 <div className="flex flex-wrap items-center gap-3">
1089 <Button
1090 variant="accent"
1091 type="submit"
1092 name="action"
1093 value="merge"
1094 disabled={mergeBlocked != null}
1095 title={mergeBlocked ?? undefined}
1096 >
1097 {mergeQueue ? <Layers size={15} /> : <GitMerge size={15} />}
1098 {mergeQueue ? "Add to the merge queue" : `Merge into ${defaultBranch}`}
1099 </Button>
1100 <span className={`text-xs ${mergeBlocked ? "text-danger" : "text-muted"}`}>
1101 {mergeBlocked ?? (mergeQueue
1102 ? `Tested together with everything ahead of it, then lands on ${defaultBranch}.`
1103 : issue?.state === "open"
1104 ? `Closes issue #${issue.number}, and any other pull requests still open for it.`
1105 : `Lands its commits on ${defaultBranch}.`)}
1106 </span>
1107 </div>
1108 {actionData?.action === "merge" && (
1109 <ErrorText>{actionData.error}</ErrorText>
1110 )}
1111 </Form>
1112 )}
1113 </StatusBox>
1114 )}
Agents as a team: lifecycle, merge queue, billing and a new shell1115
Pull requests: title and tabs across the full width, with counts and the size of the change1116 <CommentForm
1117 author={viewer?.username ?? null}
1118 resetKey={comments.length}
1119 review={review}
1120 />
1121 {canManage && active && (
1122 <Form method="post" className="flex justify-end">
1123 <Button variant="quiet" type="submit" name="action" value="close">
1124 Close pull request
1125 </Button>
1126 </Form>
1127 )}
1128 {actionData &&
Fast pages, required checks on the branch, self-hosted runners, honest incidents1129 !["merge", "comment", "rerun-failed", "rerun-workflow", "update", "agent-review", "reviewers", "assign"].includes(
Pull requests: title and tabs across the full width, with counts and the size of the change1130 String(actionData.action),
1131 ) && <ErrorText>{actionData.error}</ErrorText>}
1132 </div>
1133 )}
1134 {actionData?.action === "comment" && (
1135 <div className="mt-2">
1136 <ErrorText>{actionData.error}</ErrorText>
1137 </div>
1138 )}
1139 </div>
1140 </div>
1141
1142 <aside className={tab === "changes" ? "hidden" : "space-y-6"}>
1143 {/* An open pull request shows its checks in full in the merge box. */}
Fast pages, required checks on the branch, self-hosted runners, honest incidents1144 {pull.status !== "open" && <WorkflowStatuses statuses={statuses} />}
Pull requests: title and tabs across the full width, with counts and the size of the change1145 {affects.length > 0 && (
1146 <section>
1147 <h3 className="flex items-center gap-1.5 text-sm font-medium">
1148 <Network size={14} className="text-faint" />
1149 Affects
1150 </h3>
1151 <p className="mt-1 text-xs text-muted">Projects that use this one, and so may feel this change:</p>
1152 <ul className="mt-2 space-y-1 text-sm">
1153 {affects.map((project) => (
1154 <li key={project.slug} className="flex items-center justify-between gap-2">
1155 <Link to={`/${params.owner}/${project.slug}`} className="hover:underline">
1156 {project.name}
1157 </Link>
1158 {project.as && <code className="font-mono text-xs text-faint">{project.as}</code>}
1159 </li>
1160 ))}
1161 </ul>
1162 {preview?.branch && canMerge && (
1163 <Form method="post" className="mt-3">
1164 <input type="hidden" name="action" value="stack" />
1165 <input type="hidden" name="branch" value={preview.branch} />
1166 <button
1167 type="submit"
1168 className="w-full rounded-md border border-line px-3 py-1.5 text-xs text-muted transition-colors hover:border-line-strong hover:text-fg"
1169 >
1170 Preview them against this change
1171 </button>
Agents as a team: lifecycle, merge queue, billing and a new shell1172 </Form>
1173 )}
Pull requests: title and tabs across the full width, with counts and the size of the change1174 {actionData?.action === "stack" &&
1175 ("notice" in actionData ? (
1176 <p className="mt-2 text-xs text-accent">{String(actionData.notice)}</p>
1177 ) : (
1178 <ErrorText>{actionData.error}</ErrorText>
1179 ))}
1180 </section>
Acceptance checks in sandboxes, line comments and review verdicts1181 )}
Issues and pull requests replace intents and attempts1182
Project dependencies: addresses, preview stacks, Affects, and agents who know1183 <section>
Pull requests: title and tabs across the full width, with counts and the size of the change1184 <h3 className="text-sm font-medium">Reviewers</h3>
1185 <ul className="mt-2 space-y-1.5 text-sm">
1186 {reviewerNames.map((name) => {
1187 const verdict = reviews.find(({ reviewer }) => reviewer === name)?.verdict;
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent1188 const pending = name === "g1t" && reviewPending;
Pull requests: title and tabs across the full width, with counts and the size of the change1189 return (
1190 <li key={name} className="flex items-center gap-2 px-1">
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent1191 {name === "g1t" ? (
Pull requests: title and tabs across the full width, with counts and the size of the change1192 <Sparkles size={16} className="shrink-0 text-accent" />
1193 ) : (
1194 <Avatar name={name} size={20} />
1195 )}
1196 <span className="grow truncate font-mono text-xs">{name}</span>
1197 {pending ? (
1198 <span className="flex items-center gap-1.5 text-xs text-muted">
1199 <span className="size-1.5 animate-pulse rounded-full bg-accent" />
1200 Reviewing
1201 </span>
1202 ) : verdict === "approve" ? (
1203 <span className="flex items-center gap-1 text-xs text-accent">
1204 <CircleCheck size={13} /> Approved
1205 </span>
1206 ) : verdict === "request_changes" ? (
1207 <span className="flex items-center gap-1 text-xs text-danger">
1208 <CircleSlash size={13} /> Changes requested
1209 </span>
1210 ) : (
1211 <span className="text-xs text-faint">Review requested</span>
1212 )}
1213 </li>
1214 );
1215 })}
1216 {reviewerNames.length === 0 && (
1217 <li className="px-1 text-xs text-faint">No reviews requested yet.</li>
1218 )}
Project dependencies: addresses, preview stacks, Affects, and agents who know1219 </ul>
Pull requests: title and tabs across the full width, with counts and the size of the change1220 {pull.status === "open" && canManage && (
1221 <div className="mt-3 space-y-2">
1222 {agentsEnabled && !reviewPending && (
1223 <Form method="post">
1224 <input type="hidden" name="action" value="agent-review" />
1225 {pull.reviewers.map((name) => (
1226 <input key={name} type="hidden" name="reviewer" value={name} />
1227 ))}
1228 <div className="*:w-full">
1229 <Button variant="quiet" type="submit">
1230 <Sparkles size={14} className="text-accent" />
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent1231 Request review from g1t
Pull requests: title and tabs across the full width, with counts and the size of the change1232 </Button>
1233 </div>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1234 <ComputeNote note={loaderData.computeNote} />
Pull requests: title and tabs across the full width, with counts and the size of the change1235 </Form>
1236 )}
1237 <details>
1238 <summary className="cursor-pointer text-xs text-faint hover:text-fg">
1239 Request review from people
1240 </summary>
1241 <Form method="post" className="mt-2 space-y-2" key={pull.reviewers.join()}>
1242 <input type="hidden" name="action" value="reviewers" />
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent1243 {pull.reviewers.includes("g1t") && (
1244 <input type="hidden" name="reviewer" value="g1t" />
Pull requests: title and tabs across the full width, with counts and the size of the change1245 )}
1246 <PeoplePicker
1247 name="reviewer"
1248 members={members.filter((name) => name !== pull.author.username)}
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent1249 chosen={pull.reviewers.filter((name) => name !== "g1t")}
Pull requests: title and tabs across the full width, with counts and the size of the change1250 />
1251 <Button variant="quiet" type="submit">
1252 Save reviewers
1253 </Button>
1254 </Form>
1255 </details>
1256 </div>
Project dependencies: addresses, preview stacks, Affects, and agents who know1257 )}
Pull requests: title and tabs across the full width, with counts and the size of the change1258 {["agent-review", "reviewers"].includes(String(actionData?.action)) && (
1259 <ErrorText>{actionData?.error}</ErrorText>
1260 )}
Project dependencies: addresses, preview stacks, Affects, and agents who know1261 </section>
Acceptance checks in sandboxes, line comments and review verdicts1262
Pull requests: title and tabs across the full width, with counts and the size of the change1263 <section>
1264 <h3 className="text-sm font-medium">Assignees</h3>
1265 <ul className="mt-2 space-y-1.5 text-sm">
1266 {pull.assignees.map((name) => (
Agents as a team: lifecycle, merge queue, billing and a new shell1267 <li key={name} className="flex items-center gap-2 px-1">
Pull requests: title and tabs across the full width, with counts and the size of the change1268 <Avatar name={name} size={20} />
Agents as a team: lifecycle, merge queue, billing and a new shell1269 <span className="grow truncate font-mono text-xs">{name}</span>
1270 </li>
Pull requests: title and tabs across the full width, with counts and the size of the change1271 ))}
1272 {pull.assignees.length === 0 && (
1273 <li className="px-1 text-xs text-faint">No one yet.</li>
1274 )}
1275 </ul>
1276 {viewer && canManage && active && (
1277 <div className="mt-3 space-y-2">
1278 {!pull.assignees.includes(viewer.username) && (
1279 <Form method="post">
1280 <input type="hidden" name="action" value="assign" />
1281 {pull.assignees.map((name) => (
1282 <input key={name} type="hidden" name="assignee" value={name} />
1283 ))}
1284 <input type="hidden" name="assignee" value={viewer.username} />
1285 <div className="*:w-full">
1286 <Button variant="quiet" type="submit">
1287 Assign yourself
1288 </Button>
1289 </div>
1290 </Form>
1291 )}
1292 <details>
1293 <summary className="cursor-pointer text-xs text-faint hover:text-fg">
1294 Assign people
1295 </summary>
1296 <Form method="post" className="mt-2 space-y-2" key={pull.assignees.join()}>
1297 <input type="hidden" name="action" value="assign" />
1298 <PeoplePicker name="assignee" members={members} chosen={pull.assignees} />
Agents as a team: lifecycle, merge queue, billing and a new shell1299 <Button variant="quiet" type="submit">
Pull requests: title and tabs across the full width, with counts and the size of the change1300 Save assignees
Agents as a team: lifecycle, merge queue, billing and a new shell1301 </Button>
Pull requests: title and tabs across the full width, with counts and the size of the change1302 </Form>
1303 </details>
1304 </div>
Issues and pull requests replace intents and attempts1305 )}
Pull requests: title and tabs across the full width, with counts and the size of the change1306 {actionData?.action === "assign" && <ErrorText>{actionData.error}</ErrorText>}
1307 </section>
Issues and pull requests replace intents and attempts1308
Pull requests: title and tabs across the full width, with counts and the size of the change1309 <section>
1310 <h3 className="text-sm font-medium">Working copy</h3>
1311 {pull.branch ? (
1312 <>
1313 <p className="mt-1 text-xs text-muted">
1314 <GitBranch size={13} className="mr-1 inline align-[-2px]" />
1315 Branch <span className="font-mono text-fg">{pull.branch}</span> of this
1316 repository. Pushes to it show up here.
1317 </p>
1318 <div className="mt-2">
1319 <CopyLine text={`git clone -b ${pull.branch} ${remote}`} />
1320 </div>
1321 </>
1322 ) : (
1323 <>
1324 <p className="mt-1 text-xs text-muted">
1325 This fork belongs to the pull request. Pushes to it show up here.
1326 </p>
1327 <div className="mt-2">
1328 <CopyLine text={`git clone ${remote}`} />
1329 </div>
1330 </>
1331 )}
1332 <p className="mt-3 flex items-center gap-1.5 font-mono text-xs text-faint">
1333 <GitCommitHorizontal size={13} />
1334 {pull.headCommit?.slice(0, 12) ?? "no commits pushed yet"}
1335 </p>
1336 </section>
1337 </aside>
1338 </div>
Issues and pull requests replace intents and attempts1339 </div>
1340 );
1341}
Pull requests say where they are live; the project menu shows what is coming1342
1343/** Where the pull request's change is live, as GitHub shows a deployment. */
1344function DeploymentCard({
1345 preview,
1346 build,
1347 stacked,
1348 base,
1349}: {
1350 preview: LiveApp | null;
1351 build: Deployment | null;
Fast pages, required checks on the branch, self-hosted runners, honest incidents1352 stacked: Promise<Stacked[]>;
Pull requests say where they are live; the project menu shows what is coming1353 base: string;
1354}) {
1355 const building = build?.status === "queued" || build?.status === "building";
1356 const failed = build?.status === "failed";
1357 return (
1358 <section className="ml-12 overflow-hidden rounded-xl border border-line bg-surface">
1359 <div className="flex flex-wrap items-center gap-3 px-4 py-3">
1360 <span
1361 className={`flex size-8 shrink-0 items-center justify-center rounded-full ring-1 ${
1362 failed ? "text-danger ring-danger/40" : building ? "text-warn ring-warn/40" : "text-accent ring-accent/40"
1363 }`}
1364 >
1365 {building ? <Loader size={15} className="animate-spin" /> : <Rocket size={15} />}
1366 </span>
1367 <div className="min-w-0 grow">
1368 <p className="text-sm font-medium">
1369 {building
1370 ? preview
1371 ? "Deploying the latest push. The last preview is still live."
1372 : "Deploying a preview"
1373 : failed
1374 ? "The preview failed to deploy"
1375 : "This branch is live"}
1376 </p>
1377 {preview ? (
1378 <a href={preview.url} className="mt-0.5 block truncate font-mono text-xs text-muted hover:text-accent">
1379 {preview.url.replace(/^https?:[/][/]/, "")}
1380 </a>
1381 ) : (
1382 build?.error && <p className="mt-0.5 truncate text-xs text-muted">{build.error}</p>
1383 )}
1384 </div>
1385 <div className="flex shrink-0 items-center gap-2">
1386 {build && (
1387 <Link to={`${base}/deployments/${build.id}`} className="text-xs text-muted hover:text-fg">
1388 {failed ? "See why" : "Build log"}
1389 </Link>
1390 )}
1391 {preview && (
1392 <a
1393 href={preview.url}
1394 className="inline-flex items-center gap-1.5 rounded-md bg-accent px-3 py-1.5 text-xs font-medium text-bg hover:opacity-90"
1395 >
1396 Visit preview
1397 <ArrowUpRight size={13} />
1398 </a>
1399 )}
1400 </div>
1401 </div>
1402 {preview && (
1403 <p className="border-t border-line px-4 py-2 text-xs text-faint">
1404 <span className="font-mono">{preview.commit.slice(0, 7)}</span> · deployed <TimeAgo at={preview.deployedAt} />
1405 {preview.branch && ` · from ${preview.branch}`}
1406 </p>
1407 )}
Fast pages, required checks on the branch, self-hosted runners, honest incidents1408 {/* Read after the page: the card's own part shows at once. */}
1409 <Suspense fallback={null}>
1410 <Await resolve={stacked}>
1411 {(stacked) =>
1412 stacked.length > 0 && (
1413 <div className="border-t border-line px-4 py-2.5">
1414 <p className="text-xs text-muted">Built against this change:</p>
1415 <ul className="mt-1.5 space-y-1">
1416 {stacked.map((entry) => (
1417 <li key={entry.slug} className="flex items-center gap-2 text-xs">
1418 <span className="font-medium">{entry.name}</span>
1419 <a href={entry.url} className="truncate font-mono text-muted hover:text-accent">
1420 {entry.url.replace(/^https?:[/][/]/, "")}
1421 </a>
1422 </li>
1423 ))}
1424 </ul>
1425 </div>
1426 )
1427 }
1428 </Await>
1429 </Suspense>
Pull requests say where they are live; the project menu shows what is coming1430 </section>
1431 );
1432}