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

1,430 lines63,087 bytesCodeBlame

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";
A click away from a live page arrives: pages stop refreshing themselves while a navigation is pending26import { Suspense } from "react";
27import { Await, Form, Link, redirect, useNavigation } 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,
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights38 workOwner,
Pull requests say where they are live; the project menu shows what is coming39} from "@g1t/contracts";
Issues and pull requests replace intents and attempts40
41import type { Route } from "./+types/pull";
Social cards for every page: og.g1t.sh42import { 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-agent43import { openedBy } from "../../lib/opened-by";
Merge branch 'worktree-agent-aaf03bdceac799c89'44import { cloneUrl, useAddresses } from "../../lib/addresses";
Issues and pull requests replace intents and attempts45import { DiffView } from "../../components/diff-view";
Agents as a team: lifecycle, merge queue, billing and a new shell46import { LifecyclePanel } from "../../components/lifecycle";
Agents and memory, checks and conflicts, profiles, slug renames, custom domains47import { AgentPanel } from "../../components/agents";
Issues and pull requests replace intents and attempts48import { Markdown } from "../../components/markdown";
49import {
50 Avatar,
51 Button,
Agents as a team: lifecycle, merge queue, billing and a new shell52 ButtonLink,
Issues and pull requests replace intents and attempts53 CopyLine,
54 EmptyState,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look55 ComputeNote,
Issues and pull requests replace intents and attempts56 ErrorText,
57 Textarea,
58 TimeAgo,
59} from "../../components/ui";
Workspace names and icons, and a component kit for every control60import { CheckboxOption } from "../../components/ui/checkbox";
GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs61import { WorkflowStatuses } from "../../components/actions";
Fast pages, required checks on the branch, self-hosted runners, honest incidents62import { AddCiPrompt } from "../../components/add-ci";
Agents as a team: lifecycle, merge queue, billing and a new shell63import {
64 CommentForm,
65 CommentList,
66 IssueIcon,
67 PeoplePicker,
Agents and memory, checks and conflicts, profiles, slug renames, custom domains68 PersonLink,
Agents as a team: lifecycle, merge queue, billing and a new shell69 PullState,
70 TimelineItem,
71 verdicts,
72} from "../../components/work";
Catching up with main takes seconds when the two sides touched different files73import { CatchUpProgress, ChecksSection, ConflictsSection, MergeabilityRow, runIdOf } from "../../components/merge-box";
74import { 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 put75import { notFound } from "../../lib/not-found.server";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look76import { computeNoteFor } from "../../lib/compute.server";
Agents and memory, checks and conflicts, profiles, slug renames, custom domains77import { actions, deployments, identity, projects, repos, work } from "../../lib/services.server";
Issues and pull requests replace intents and attempts78import { assertSameOrigin, getViewer, requireUser } from "../../lib/session.server";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look79import { accessTo, refusal } from "../../lib/access.server";
A click away from a live page arrives: pages stop refreshing themselves while a navigation is pending80import { REFRESH_MS, useRefreshWhile } from "../../lib/refresh";
Issues and pull requests replace intents and attempts81
82const EMPTY_COMPARISON: Comparison = { base: null, head: "", files: [], truncated: false };
83const TABS = ["conversation", "session", "changes"] as const;
84type Tab = (typeof TABS)[number];
85
Social cards for every page: og.g1t.sh86export function meta({ loaderData, params, ...args }: Route.MetaArgs) {
87 const pull = loaderData?.pull;
88 const title = pull ? `${pull.title} · Pull request #${pull.number} · ` : "";
89 const state = { draft: "Draft", open: "Open", merged: "Merged", closed: "Closed" }[pull?.status ?? "open"];
90 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-agent91 const opener = pull ? openedBy(pull) : null;
Social cards for every page: og.g1t.sh92 return page(args, {
93 title: `${title}${params.owner}/${params.repo} · g1t`,
94 description: pull
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent95 ? `${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.sh96 : null,
97 // The card shows the title and the state.
98 version: pull ? [pull.title, pull.status] : undefined,
99 type: "article",
100 });
Issues and pull requests replace intents and attempts101}
102
103export async function loader({ params, context, request }: Route.LoaderArgs) {
104 const viewer = getViewer(context);
105 const path = { namespace: params.owner, name: params.repo };
106 const number = Number(params.number);
107 const asked = new URL(request.url).searchParams.get("tab");
108 const tab: Tab = TABS.find((name) => name === asked) ?? "conversation";
109
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look110 // Members of the workspace pick reviewers and assignees from its people;
111 // what the viewer may do here goes by their role on the repository.
Agents as a team: lifecycle, merge queue, billing and a new shell112 const member = (viewer?.workspaces ?? []).some(
113 (membership) => membership.slug === params.owner,
114 );
Fast pages, required checks on the branch, self-hosted runners, honest incidents115 // Everything starts at once. Only what the viewer's role decides waits
116 // for the repository, which the layout is looking up in this request too
117 // (lib/access.server.ts), and only what the pull request decides waits
118 // for it.
Project dependencies: addresses, preview stacks, Affects, and agents who know119 const ref = { workspace: params.owner, slug: params.repo };
Fast pages, required checks on the branch, self-hosted runners, honest incidents120 const access = accessTo(context, params);
121 const pullFound = work.getPull(path, number, viewer);
122 const deps = projects.dependencies(params.owner, params.repo, viewer);
123 // Awaited below, unless the pull request is missing first.
124 deps.catch(() => null);
125 const [{ can }, found, repo, settings, agentsEnabled, members, computeNote, session, deployed] = await Promise.all([
126 access,
127 pullFound,
Issues and pull requests replace intents and attempts128 repos.get(path, viewer),
Agents as a team: lifecycle, merge queue, billing and a new shell129 work.getSettings(path, viewer),
g1t's agents only for listed workspaces, whatever the state of billing130 env.RUNNER.enabled(viewer, path),
Agents as a team: lifecycle, merge queue, billing and a new shell131 // A member picks reviewers and assignees from the workspace's people.
132 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-agent133 // Before someone asks g1t for something: whether the plan lets it start.
Fast pages, required checks on the branch, self-hosted runners, honest incidents134 access.then(({ can }) => (can.run ? computeNoteFor(params.owner, "agent") : null)),
135 tab === "session" ? work.readSession(path, number, viewer) : null,
136 // Its preview, for people with a role here: beside the rest, not after.
137 access.then(({ insider }) => (insider ? deployments.list(ref, viewer).catch(() => null) : null)),
Issues and pull requests replace intents and attempts138 ]);
139 if (!found.ok) {
140 // Issues and pull requests share numbers; this one may be an issue.
141 const issue = await work.getIssue(path, number, viewer);
142 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 put143 throw notFound("pull");
Issues and pull requests replace intents and attempts144 }
145 const { pull } = found.value;
Pull requests from branches146 const range = pullComparison(pull);
Agents and memory, checks and conflicts, profiles, slug renames, custom domains147 // The jobs of each workflow run on its head, to list checks job by job.
148 // 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 incidents149 // Streamed: the checks show by status first, then job by job.
Agents and memory, checks and conflicts, profiles, slug renames, custom domains150 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 incidents151 const workflowJobs =
152 tab === "conversation" && pull.status === "open" && runIds.length > 0
153 ? Promise.all(runIds.map((id) => actions.run(path, viewer, id).catch(() => null))).then((runs) => {
154 const jobs: Record<string, Job[]> = {};
155 for (const run of runs) if (run?.ok) jobs[run.value.run.id] = run.value.jobs;
156 return jobs;
157 })
158 .catch(() => ({}) as Record<string, Job[]>)
159 : Promise.resolve({} as Record<string, Job[]>);
160 // No checks at all on an open pull request: whether that is because the
161 // repository has no workflows, to offer a starter one.
162 const unchecked =
163 tab === "conversation" &&
164 pull.status === "open" &&
165 (found.value.statuses ?? []).length === 0 &&
166 (found.value.requiredChecks ?? []).length === 0;
167 const [comparison, used, noChecks] = await Promise.all([
168 tab === "changes" ? repos.compare(range.repoId, viewer, range.base, range.head) : null,
169 deps,
170 unchecked
171 ? actions
172 .workflows(path, viewer)
173 .then((found) => found.ok && found.value.length === 0)
174 .catch(() => false)
175 : false,
Issues and pull requests replace intents and attempts176 ]);
Fast pages, required checks on the branch, self-hosted runners, honest incidents177 const affects = used.ok ? used.value.usedBy : [];
Issues and pull requests replace intents and attempts178 return {
179 ...found.value,
Agents and memory, checks and conflicts, profiles, slug renames, custom domains180 workflowJobs,
Issues and pull requests replace intents and attempts181 tab,
182 session: session?.ok ? session.value : [],
183 // An empty comparison if it could not be made.
184 comparison: comparison && (comparison.ok ? comparison.value : EMPTY_COMPARISON),
185 viewer,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look186 // Write and up can merge.
187 canMerge: can.merge,
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights188 // Triage and up manage anyone's pull request; its author, their own,
189 // and whoever asked g1t for one, that one.
190 canManage: can.triage || (viewer != null && viewer.id === workOwner(pull).id),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look191 // Telling its agent things, and re-running checks, spend compute: Write and up.
192 canRun: can.run,
Agents as a team: lifecycle, merge queue, billing and a new shell193 // A catch-up is pushed as the viewer: a fork takes pushes only from
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights194 // whoever it is for (who asked g1t for it, or its author), a branch
195 // from anyone who can push.
196 canUpdate: pull.fork ? viewer?.id === workOwner(pull).id : can.push,
Agents as a team: lifecycle, merge queue, billing and a new shell197 agentsEnabled,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look198 computeNote,
Agents as a team: lifecycle, merge queue, billing and a new shell199 members: members?.ok ? members.value.map((person) => person.username) : [],
200 requireUpToDate: settings.ok && settings.value.requireUpToDate,
201 mergeQueue: settings.ok && settings.value.mergeQueue,
202 requiredApprovals: settings.ok ? settings.value.requiredApprovals : 0,
203 canIgnoreChecks: !settings.ok || settings.value.allowIgnoringChecks,
Fast pages, required checks on the branch, self-hosted runners, honest incidents204 noChecks,
205 // Who may open the pull request that adds CI: anyone who can push.
206 canAddCi: can.push,
207 // Who may choose the required checks.
208 canProtect: can.manage_protection,
Issues and pull requests replace intents and attempts209 defaultBranch: repo.ok ? repo.value.defaultBranch : "main",
Fast pages, required checks on the branch, self-hosted runners, honest incidents210 affects,
211 // Streamed: the conversation shows first, the preview card after.
212 ...deploymentOf(deployed?.ok ? deployed.value : null, number, params.owner, affects, viewer),
Issues and pull requests replace intents and attempts213 };
214}
215
Pull requests say where they are live; the project menu shows what is coming216/**
217 * Where this pull request is live: its preview (or its latest build, while
218 * one is going or after one failed), and the previews of the projects
219 * that use this one, built against it.
220 */
Fast pages, required checks on the branch, self-hosted runners, honest incidents221function deploymentOf(
Pull requests say where they are live; the project menu shows what is coming222 list: { deployments: Deployment[]; live: LiveApp[] } | null,
223 number: number,
224 owner: string,
225 affects: { slug: string; name: string }[],
226 viewer: Viewer,
Fast pages, required checks on the branch, self-hosted runners, honest incidents227): { preview: LiveApp | null; build: Deployment | null; stacked: Promise<Stacked[]> } {
228 const preview = list?.live.find((app) => app.kind === "preview" && app.number === number) ?? null;
229 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 coming230 const branch = preview?.branch ?? build?.branch ?? null;
Fast pages, required checks on the branch, self-hosted runners, honest incidents231 // Streamed: another lookup per project, shown in the card once known.
232 const stacked: Promise<Stacked[]> = branch
233 ? Promise.all(
234 affects.slice(0, 5).map(async (project) => {
235 const theirs = await deployments.list({ workspace: owner, slug: project.slug }, viewer).catch(() => null);
236 const app = theirs?.ok ? theirs.value.live.find((a) => a.kind === "preview" && a.branch === branch) : undefined;
237 return app ? { name: project.name, slug: project.slug, url: app.url } : null;
238 }),
239 ).then((found) => found.filter((entry) => entry != null))
240 : Promise.resolve([]);
Pull requests say where they are live; the project menu shows what is coming241 return { preview, build, stacked };
242}
243
Fast pages, required checks on the branch, self-hosted runners, honest incidents244/** A project that uses this one, with its preview built against this change. */
245type Stacked = { name: string; slug: string; url: string };
246
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look247const PULL_NEEDS: Record<string, Capability> = {
248 stack: "run",
249 "agent-review": "run",
250 "rerun-workflow": "run",
Fast pages, required checks on the branch, self-hosted runners, honest incidents251 "rerun-failed": "run",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look252 message: "run",
253 merge: "merge",
254 unqueue: "merge",
255};
256
Issues and pull requests replace intents and attempts257export async function action({ request, params, context }: Route.ActionArgs) {
258 assertSameOrigin(request);
259 const user = requireUser(context, request);
260 const form = await request.formData();
261 const path = { namespace: params.owner, name: params.repo };
262 const number = Number(params.number);
263 const action = form.get("action");
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look264 // What each form needs beyond reading; work checks the rest.
265 const needed = typeof action === "string" ? PULL_NEEDS[action] : undefined;
266 const refused = needed ? await refusal(context, params, needed) : null;
267 if (refused) return { error: refused, action: String(action) };
Acceptance checks in sandboxes, line comments and review verdicts268 const verdict = form.get("verdict");
269 const line = Number(form.get("line"));
Agents as a team: lifecycle, merge queue, billing and a new shell270 /** Names ticked in a people picker, plus those typed beside it. */
271 const picked = (field: string) => [
272 ...form.getAll(field).map(String),
273 ...String(form.get("others") ?? "").split(/[\s,]+/),
274 ];
Project dependencies: addresses, preview stacks, Affects, and agents who know275 // The projects that use this one, built against this pull request's preview.
276 if (action === "stack") {
277 const built = await deployments.stack(user, { workspace: params.owner, slug: params.repo }, String(form.get("branch") ?? ""));
278 return built.ok
279 ? { action, notice: `Building ${built.value.join(", ")} against this preview. They appear on their Deployments pages.` }
280 : { action, error: built.error.message };
281 }
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent282 // Asking g1t for its review records the request, then starts it.
Agents as a team: lifecycle, merge queue, billing and a new shell283 if (action === "agent-review") {
284 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-agent285 reviewers: [...form.getAll("reviewer").map(String), "g1t"],
Agents as a team: lifecycle, merge queue, billing and a new shell286 });
287 if (!asked.ok) return { error: asked.error.message, action };
288 }
Catching up with main takes seconds when the two sides touched different files289 // Catching up: merged and pushed in seconds when the two sides changed
290 // different files; otherwise handed to a sandbox, which takes a minute.
291 if (action === "update") {
292 const caught = await work.catchUpPull(user, path, number);
293 if (!caught.ok) return { action, error: caught.error.message };
294 const update = caught.value;
295 if (update.outcome !== "needs_agent") {
296 return {
297 action,
298 updated: { commit: update.commit, already: update.outcome === "up_to_date", at: Date.now() },
299 };
300 }
301 const started = await env.RUNNER.update(user, path, number);
302 if (!started.ok) return { action, error: started.error.message };
303 return { action, agent: { update, startedAt: Date.now() } };
304 }
Agents and memory, checks and conflicts, profiles, slug renames, custom domains305 // A workflow run's failed jobs, run again. Who may is the actions service's call.
306 if (action === "rerun-workflow") {
307 const rerun = await actions.rerun(user, path, String(form.get("run") ?? ""), true);
308 return rerun.ok ? null : { error: rerun.error.message, action };
309 }
Fast pages, required checks on the branch, self-hosted runners, honest incidents310 // Every failed run on its head, run again: Mission control's quick action.
311 if (action === "rerun-failed") {
312 const found = await work.getPull(path, number, user);
313 if (!found.ok) return { error: found.error.message, action };
314 const failed = [
315 ...new Set(
316 (found.value.statuses ?? [])
317 .filter((status) => status.state === "failure" || status.state === "error")
318 .map(runIdOf)
319 .filter((id) => id != null),
320 ),
321 ];
322 if (failed.length === 0) return { error: "No workflow run failed on its latest commit.", action };
323 const reruns = await Promise.all(failed.map((id) => actions.rerun(user, path, id, true)));
324 const refused = reruns.find((rerun) => !rerun.ok);
325 return refused && !refused.ok ? { error: refused.error.message, action } : null;
326 }
Issues and pull requests replace intents and attempts327 const result =
328 action === "merge"
Acceptance checks in sandboxes, line comments and review verdicts329 ? await work.mergePull(user, path, number, {
330 keepIssueOpen: form.get("keepIssueOpen") === "on",
331 ignoreChecks: form.get("ignoreChecks") === "on",
332 })
Agents as a team: lifecycle, merge queue, billing and a new shell333 : action === "unqueue"
334 ? await work.removeFromQueue(user, path, number)
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request335 : action === "message"
336 ? await work.messageAgent(user, path, number, String(form.get("body") ?? ""))
Issues and pull requests replace intents and attempts337 : action === "close"
338 ? await work.closePull(user, path, number)
Fast pages, required checks on the branch, self-hosted runners, honest incidents339 : action === "agent-review"
Agents as a team: lifecycle, merge queue, billing and a new shell340 ? await env.RUNNER.review(user, path, number)
341 : action === "reviewers"
342 ? await work.updatePull(user, path, number, { reviewers: picked("reviewer") })
343 : action === "assign"
344 ? await work.updatePull(user, path, number, { assignees: picked("assignee") })
Acceptance checks in sandboxes, line comments and review verdicts345 : action === "comment"
346 ? await work.addComment(user, path, number, {
347 body: String(form.get("body") ?? ""),
348 path: String(form.get("path") ?? "") || undefined,
349 line: line > 0 ? line : undefined,
350 verdict:
351 verdict === "approve" || verdict === "request_changes" ? verdict : undefined,
352 })
353 : await work.readyPull(user, path, number, String(form.get("summary") ?? ""));
Issues and pull requests replace intents and attempts354 return result.ok ? null : { error: result.error.message, action };
355}
356
Pull requests: title and tabs across the full width, with counts and the size of the change357/** How many, beside a tab's name, when there are any. */
358function TabCount({ n }: { n: number }) {
359 if (n <= 0) return null;
360 return (
361 <span className="rounded-full bg-raised px-1.5 py-px text-xs tabular-nums text-muted ring-1 ring-line">
362 {n.toLocaleString("en-US")}
363 </span>
364 );
365}
366
Issues and pull requests replace intents and attempts367function TabLink({
368 to,
369 active,
370 children,
371}: {
372 to: string;
373 active: boolean;
374 children: React.ReactNode;
375}) {
376 return (
377 <Link
378 to={to}
379 preventScrollReset
380 className={
Fast pages, required checks on the branch, self-hosted runners, honest incidents381 "-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 attempts382 (active
383 ? "border-accent font-medium text-fg"
384 : "border-transparent text-muted hover:text-fg")
385 }
386 >
387 {children}
388 </Link>
389 );
390}
391
392/** One step of the session, on the timeline's rail. */
393function Entry({ entry, agent }: { entry: SessionEntry; agent: string }) {
394 const isTool = entry.kind === "tool_call" || entry.kind === "tool_result";
395 const Icon =
396 entry.kind === "prompt"
397 ? User
398 : entry.kind === "note"
399 ? StickyNote
400 : isTool
401 ? Wrench
402 : Bot;
403 return (
404 <li className="relative pl-10">
405 <span
406 className={`absolute top-0.5 left-0 flex size-7 items-center justify-center rounded-full border bg-bg ${
407 entry.kind === "prompt"
408 ? "border-accent/50 text-accent"
409 : "border-line text-faint"
410 }`}
411 >
412 <Icon size={14} />
413 </span>
414 {isTool ? (
415 <details className="group rounded-lg border border-line bg-surface">
416 <summary className="flex cursor-pointer list-none items-center gap-2 px-3 py-1.5 text-sm">
417 <ChevronRight
418 size={14}
419 className="text-faint transition-transform group-open:rotate-90"
420 />
421 <span className="font-mono text-xs text-accent">
422 {entry.tool ?? "tool"}
423 </span>
424 <span className="truncate font-mono text-xs text-muted">
425 {entry.kind === "tool_result" ? "→ " : ""}
426 {entry.text.split("\n")[0]}
427 </span>
428 </summary>
429 <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 line430 <code>{entry.text}</code>
Issues and pull requests replace intents and attempts431 </pre>
432 </details>
433 ) : (
434 <div>
435 <p className="text-xs font-medium text-faint">
436 {entry.kind === "prompt"
437 ? "Prompt"
438 : entry.kind === "note"
439 ? "Note"
440 : agent}
441 </p>
442 {entry.kind === "message" ? (
443 <div className="mt-1">
444 <Markdown source={entry.text} />
445 </div>
Agents as a team: lifecycle, merge queue, billing and a new shell446 ) : entry.kind === "prompt" ? (
447 <div className="mt-1.5 max-h-[32rem] overflow-y-auto rounded-xl border border-line bg-surface p-4">
448 <Markdown source={entry.text} />
449 </div>
Issues and pull requests replace intents and attempts450 ) : (
Agents as a team: lifecycle, merge queue, billing and a new shell451 <p className="mt-1 text-[0.9375rem] leading-relaxed wrap-break-word whitespace-pre-wrap">
Issues and pull requests replace intents and attempts452 {entry.text}
453 </p>
454 )}
455 </div>
456 )}
457 {entry.commit && (
458 <p className="mt-1.5 flex items-center gap-1 font-mono text-xs text-faint">
459 <GitCommitHorizontal size={12} />
460 {entry.commit.slice(0, 7)}
461 </p>
462 )}
463 </li>
464 );
465}
466
Agents as a team: lifecycle, merge queue, billing and a new shell467/** The box at the foot of the conversation saying what stands before a merge. */
468function StatusBox({ children }: { children: React.ReactNode }) {
469 return (
470 <div className="flex gap-3">
471 <span className="hidden w-8 shrink-0 sm:block" />
472 <section className="min-w-0 grow divide-y divide-line overflow-hidden rounded-xl border border-line-strong bg-surface">
473 {children}
474 </section>
475 </div>
476 );
477}
478
479function StatusRow({
480 icon,
481 title,
482 children,
483}: {
484 icon: React.ReactNode;
485 title: string;
486 children?: React.ReactNode;
487}) {
488 return (
489 <div className="flex gap-3 px-4 py-3 text-sm">
490 <span className="mt-0.5 shrink-0">{icon}</span>
491 <div className="min-w-0">
492 <p className="font-medium">{title}</p>
493 {children && <div className="mt-0.5 text-muted">{children}</div>}
494 </div>
495 </div>
496 );
497}
498
Issues and pull requests replace intents and attempts499export default function PullPage({ loaderData, actionData, params }: Route.ComponentProps) {
500 const {
501 pull,
502 issue,
503 comments,
Acceptance checks in sandboxes, line comments and review verdicts504 checks,
Agents as a team: lifecycle, merge queue, billing and a new shell505 overlaps,
506 behind,
507 reviewPending,
508 lifecycle,
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request509 messages,
GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs510 statuses = [],
Fast pages, required checks on the branch, self-hosted runners, honest incidents511 requiredChecks = [],
512 noChecks,
513 canAddCi,
514 canProtect,
Agents and memory, checks and conflicts, profiles, slug renames, custom domains515 mergeable = "unknown",
516 conflicts = [],
517 earlierChecks = [],
518 workflowJobs,
Project dependencies: addresses, preview stacks, Affects, and agents who know519 affects,
520 preview,
Pull requests say where they are live; the project menu shows what is coming521 build,
522 stacked,
Agents as a team: lifecycle, merge queue, billing and a new shell523 landing,
524 stalled,
525 requireUpToDate,
526 mergeQueue,
527 requiredApprovals,
528 canIgnoreChecks,
529 canUpdate,
530 agentsEnabled,
531 members,
Issues and pull requests replace intents and attempts532 tab,
533 session,
534 comparison,
535 viewer,
536 canMerge,
537 canManage,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look538 canRun,
Issues and pull requests replace intents and attempts539 defaultBranch,
540 } = loaderData;
541 const base = `/${params.owner}/${params.repo}`;
542 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-agent543 // g1t, on a pull request it made: the person who asked for it is a line below.
544 const opener = openedBy(pull);
Fast pages, required checks on the branch, self-hosted runners, honest incidents545 const checksSection = (jobs: Record<string, Job[]>) => (
546 <ChecksSection
547 run={checks}
548 required={requiredChecks}
549 statuses={statuses}
550 jobs={jobs}
551 pull={pull}
552 base={base}
553 earlier={earlierChecks}
554 canRerunWorkflows={canMerge}
555 settingsUrl={canProtect ? `${base}/settings/branches` : null}
556 error={actionData?.action === "rerun-failed" || actionData?.action === "rerun-workflow" ? actionData.error : null}
557 />
558 );
Merge branch 'worktree-agent-aaf03bdceac799c89'559 const addresses = useAddresses();
Pull requests from branches560 const remote = pull.fork
Merge branch 'worktree-agent-aaf03bdceac799c89'561 ? cloneUrl(addresses, `${pull.fork.namespace}/${pull.fork.name}`)
562 : cloneUrl(addresses, `${params.owner}/${params.repo}`);
Issues and pull requests replace intents and attempts563 const active = pull.status === "draft" || pull.status === "open";
564
Acceptance checks in sandboxes, line comments and review verdicts565 // Follow an agent at work, or checks in progress, without a manual reload.
Issues and pull requests replace intents and attempts566 const working = pull.status === "draft";
Fast pages, required checks on the branch, self-hosted runners, honest incidents567 const checking = statuses.some((status) => status.state === "pending");
Acceptance checks in sandboxes, line comments and review verdicts568 const reviews = verdicts(comments);
Fast pages, required checks on the branch, self-hosted runners, honest incidents569 // What stands between this pull request and a merge, if anything: a
570 // required check that has not passed, or the merge queue taking it out.
571 const unchecked = requiredChecks.some((check) => check.state !== "success") || checks?.status === "failed";
572 const requiredFailed = requiredChecks.some((check) => check.state === "failure") || checks?.status === "failed";
Agents as a team: lifecycle, merge queue, billing and a new shell573 // Pull requests for other issues changing the same files will conflict;
574 // ones for the same issue are alternatives, and expected to.
575 const collisions = overlaps.filter((other) => other.issue == null || other.issue !== pull.issue);
576 const review = {
577 changesUrl: here + "?tab=changes",
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights578 // Nobody reviews their own pull request, nor one g1t made for them.
579 canJudge: active && viewer != null && viewer.id !== workOwner(pull).id,
Agents as a team: lifecycle, merge queue, billing and a new shell580 };
581 // Everyone whose review was asked for, then anyone who reviewed unasked.
582 const reviewerNames = [
583 ...new Set([...pull.reviewers, ...reviews.map(({ reviewer }) => reviewer)]),
584 ];
Catching up with main takes seconds when the two sides touched different files585 // A catch-up: the click shows at once; the answer says whether it is
586 // done already or a sandbox is on it.
587 const navigation = useNavigation();
588 const catchUpPending = navigation.state !== "idle" && navigation.formData?.get("action") === "update";
589 const catchUp = actionData?.action === "update" ? actionData : null;
590 const caughtUp = catchUp && "updated" in catchUp ? catchUp.updated : null;
591 const agentCatchUp = catchUp && "agent" in catchUp ? catchUp.agent : null;
592 // Pushed already: followed until the pull request's head is the merge,
593 // so its checks show starting again, for a minute at most.
594 const settling =
595 caughtUp != null && !caughtUp.already && pull.headCommit !== caughtUp.commit && Date.now() - caughtUp.at < 60_000;
596 // Followed until it is pushed, or for so long, never longer.
597 const catchingUp =
598 agentCatchUp != null && behind && Date.now() - agentCatchUp.startedAt < CATCH_UP_TIMEOUT_MS + REFRESH_MS;
Agents as a team: lifecycle, merge queue, billing and a new shell599 // g1t is taking a step of its own accord, so the page will change.
600 const moving =
601 lifecycle != null && lifecycle.stage !== "ready" && lifecycle.stage !== "needs_you";
Agents and memory, checks and conflicts, profiles, slug renames, custom domains602 // Whether it merges cleanly is being worked out, so the box will change.
603 const probing = active && mergeable === "checking";
604 const conflicting = active && mergeable === "conflicting";
A click away from a live page arrives: pages stop refreshing themselves while a navigation is pending605 useRefreshWhile(working || checking || reviewPending || catchingUp || settling || moving || landing || probing);
Agents and memory, checks and conflicts, profiles, slug renames, custom domains606 // Why the merge button cannot be pressed, if it cannot.
607 const mergeBlocked = conflicting
608 ? "Resolve the conflicts first."
609 : probing
610 ? "Waiting to find out whether it merges cleanly."
611 : behind && requireUpToDate
612 ? `This repository requires it to be up to date with ${defaultBranch} first.`
Fast pages, required checks on the branch, self-hosted runners, honest incidents613 : unchecked && !canIgnoreChecks
614 ? `The checks ${defaultBranch} requires have to pass first.`
615 : null;
Issues and pull requests replace intents and attempts616
617 return (
Agents as a team: lifecycle, merge queue, billing and a new shell618 // 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 change619 <div>
620 {/* The title and the tabs run the full width; what is happening on
621 the pull request sits at the top of Conversation. */}
622 <header>
Issues and pull requests replace intents and attempts623 <h2 className="text-2xl font-semibold tracking-tight text-balance">
624 {pull.title} <span className="font-normal text-faint">#{pull.number}</span>
625 </h2>
626 <div className="mt-3 flex flex-wrap items-center gap-x-3 gap-y-2 text-sm text-muted">
627 <PullState status={pull.status} />
628 <span className="flex items-center gap-2">
Agents as a team: lifecycle, merge queue, billing and a new shell629 <Avatar
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent630 name={(pull.status === "merged" && pull.mergedBy) || opener.name}
Agents as a team: lifecycle, merge queue, billing and a new shell631 size={18}
632 />
Issues and pull requests replace intents and attempts633 <span>
Agents and memory, checks and conflicts, profiles, slug renames, custom domains634 <PersonLink
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent635 name={(pull.status === "merged" && pull.mergedBy) || opener.name}
Agents and memory, checks and conflicts, profiles, slug renames, custom domains636 className="font-medium text-fg hover:underline"
637 />{" "}
Pull requests from branches638 {pull.status === "merged" ? "merged" : "wants to merge"}
639 {pull.branch && (
640 <>
641 {" "}
642 <span className="font-mono text-fg">{pull.branch}</span>
643 </>
644 )}{" "}
645 into <span className="font-mono text-fg">{defaultBranch}</span>
Issues and pull requests replace intents and attempts646 </span>
647 </span>
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent648 {opener.requestedBy && (
649 <span className="text-xs">
650 requested by{" "}
651 <PersonLink name={opener.requestedBy} className="font-medium text-fg-soft hover:underline" />
652 </span>
653 )}
654 {/* A pull request from a branch was made by its author, not an agent;
655 one g1t made already says so. */}
656 {!pull.branch && !opener.requestedBy && (
Pull requests from branches657 <span className="flex items-center gap-1.5 font-mono text-xs">
658 <Bot size={14} />
659 {pull.agent}
660 {pull.runtime === "hosted" && <span className="text-faint">on g1t</span>}
661 </span>
662 )}
Issues and pull requests replace intents and attempts663 </div>
664
Acceptance checks in sandboxes, line comments and review verdicts665 {reviews.length > 0 && (
666 <p className="mt-3 flex flex-wrap items-center gap-x-4 gap-y-1 text-sm">
667 {reviews.map(({ reviewer, verdict }) => (
668 <span
669 key={reviewer}
670 className={`flex items-center gap-1.5 ${
671 verdict === "approve" ? "text-accent" : "text-danger"
672 }`}
673 >
674 {verdict === "approve" ? <CircleCheck size={15} /> : <CircleSlash size={15} />}
675 {verdict === "approve" ? "Approved by" : "Changes requested by"}{" "}
676 <span className="font-medium">{reviewer}</span>
677 </span>
678 ))}
679 </p>
680 )}
681
Tab bars on pull requests, search and profiles scroll without showing a scrollbar682 <nav className="mt-6 flex items-end gap-1 overflow-x-auto overflow-y-hidden border-b border-line [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
Issues and pull requests replace intents and attempts683 <TabLink to={here} active={tab === "conversation"}>
684 <MessageSquare size={15} />
685 Conversation
Pull requests: title and tabs across the full width, with counts and the size of the change686 <TabCount n={comments.length} />
Issues and pull requests replace intents and attempts687 </TabLink>
688 <TabLink to={here + "?tab=session"} active={tab === "session"}>
689 <MessagesSquare size={15} />
690 Session
691 </TabLink>
692 <TabLink to={here + "?tab=changes"} active={tab === "changes"}>
693 <FileDiff size={15} />
Pull requests: title and tabs across the full width, with counts and the size of the change694 Files changed
695 <TabCount n={pull.files.length} />
Issues and pull requests replace intents and attempts696 </TabLink>
Pull requests: title and tabs across the full width, with counts and the size of the change697 {pull.files.length > 0 && (
698 <span className="ml-auto hidden pb-2.5 font-mono text-xs sm:inline">
699 <span className="text-accent">+{pull.files.reduce((sum, file) => sum + file.additions, 0).toLocaleString("en-US")}</span>{" "}
700 <span className="text-danger">−{pull.files.reduce((sum, file) => sum + file.deletions, 0).toLocaleString("en-US")}</span>
701 </span>
702 )}
Issues and pull requests replace intents and attempts703 </nav>
Pull requests: title and tabs across the full width, with counts and the size of the change704 </header>
705 <div className={`mt-6 grid gap-8 ${tab === "changes" ? "" : "lg:grid-cols-[1fr_19rem]"}`}>
706 <div className="min-w-0">
707 {/* What is happening on it now: its stage, its agent, what people told
708 the agent, and what else touches the same files. On the conversation,
709 so the tabs sit right under the title. */}
710 {tab === "conversation" && (
711 <div>
712 {issue && (
713 <Link
714 to={`${base}/issues/${issue.number}`}
715 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"
716 >
717 <IssueIcon issue={issue} />
718 <span className="text-muted">
719 {pull.status === "merged" && issue.resolvedBy === pull.number
720 ? "Resolved"
721 : "For issue"}
722 </span>
723 <span className="truncate font-medium">{issue.title}</span>
724 <span className="text-faint">#{issue.number}</span>
725 {issue.pullCount > 1 && (
726 <span className="ml-auto shrink-0 text-xs text-faint">
727 1 of {issue.pullCount} pull requests
Pull requests show their tabs right under the title; agent panels move into Conversation728 </span>
Pull requests: title and tabs across the full width, with counts and the size of the change729 )}
730 </Link>
731 )}
732 {pull.status === "merged" && (
733 <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">
734 <GitMerge size={16} className="shrink-0 text-merged" />
Agents as a team: lifecycle, merge queue, billing and a new shell735 <span>
Pull requests: title and tabs across the full width, with counts and the size of the change736 Merged into <span className="font-mono">{defaultBranch}</span> by{" "}
737 <span className="font-medium">{pull.mergedBy}</span>{" "}
738 {/* Not an account: the repository's settings said to merge it. */}
739 {pull.mergedBy === "g1t" && "automatically, once it was ready, "}
740 {pull.mergedAt && <TimeAgo at={pull.mergedAt} />}
741 {pull.headCommit && (
742 <span className="font-mono text-muted"> · {pull.headCommit.slice(0, 7)}</span>
Agents as a team: lifecycle, merge queue, billing and a new shell743 )}
744 </span>
Pull requests: title and tabs across the full width, with counts and the size of the change745 </p>
746 )}
747 {pull.supersededBy != null && (
748 <p className="mt-4 rounded-xl border border-line bg-surface px-4 py-2.5 text-sm text-muted">
749 Closed because{" "}
750 <Link
751 to={`${base}/pull/${pull.supersededBy}`}
752 className="font-medium text-fg hover:underline"
753 >
754 #{pull.supersededBy}
755 </Link>{" "}
756 was merged for this issue instead.
757 </p>
758 )}
759 {lifecycle && <LifecyclePanel lifecycle={lifecycle} />}
760 {/* 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 step761 <AgentPanel
762 owner={params.owner}
763 repo={params.repo}
764 number={pull.number}
765 stage={lifecycle?.stage ?? null}
766 confidence={pull.confidence ?? null}
767 />
Pull requests: title and tabs across the full width, with counts and the size of the change768
769 {/* 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 look770 {canRun &&
Pull requests: title and tabs across the full width, with counts and the size of the change771 pull.runtime === "hosted" &&
772 (working || ["working", "revising", "catching_up", "answering"].includes(lifecycle?.stage ?? "")) && (
773 <Form method="post" className="mt-4 rounded-2xl bg-surface p-4 ring-1 ring-merged/30">
774 <p className="flex items-center gap-2 text-sm font-medium">
775 <Sparkles size={15} className="text-merged" />
776 Message the agent
777 </p>
778 <p className="mt-1 text-xs text-muted">
779 A correction, a hint, a change of plan. It reads it at its next step, without
780 starting over.
781 </p>
782 <div className="mt-3 flex gap-2">
783 <input type="hidden" name="action" value="message" />
784 <input
785 name="body"
786 required
787 autoComplete="off"
788 data-1p-ignore
789 placeholder="Keep the old flag working too…"
790 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 shell791 />
Pull requests: title and tabs across the full width, with counts and the size of the change792 <Button type="submit">Send</Button>
793 </div>
794 </Form>
Agents as a team: lifecycle, merge queue, billing and a new shell795 )}
Pull requests: title and tabs across the full width, with counts and the size of the change796 {messages.length > 0 && (
797 <ul className="mt-3 space-y-1.5">
798 {messages.map((message) => (
799 <li key={message.id} className="flex items-start gap-2 text-sm">
800 <Avatar name={message.author} size={18} />
801 <span className="min-w-0 grow">
802 <span className="font-medium">
803 {message.fromNumber != null ? `The agent on #${message.fromNumber}` : message.author}
804 </span>{" "}
805 <span className="text-muted">
806 {message.kind === "question"
807 ? "asked:"
808 : message.kind === "handoff"
809 ? "handed over:"
810 : message.kind === "answer"
811 ? "answered:"
812 : "to the agent:"}
813 </span>{" "}
814 {message.body}
815 {message.answer && (
816 <span className="mt-1 block border-l-2 border-merged/40 pl-2 text-muted">
817 {message.declined ? "Declined: " : "Answer: "}
818 {message.answer}
819 </span>
820 )}
821 </span>
822 <span className={`shrink-0 text-xs ${message.deliveredAt ? "text-accent" : "text-faint"}`}>
823 {message.deliveredAt ? "read by the agent" : "waiting for its next step"}
824 </span>
825 </li>
826 ))}
827 </ul>
828 )}
829 {collisions.length > 0 && active && (
830 <div className="mt-4 rounded-xl border border-line bg-surface px-4 py-3 text-sm">
831 <p className="flex items-center gap-2.5 font-medium">
832 <Radar size={16} className="shrink-0 text-info" />
833 Other work is changing the same files
834 </p>
835 <ul className="mt-2 space-y-1.5">
836 {collisions.map((other) => (
837 <li key={other.number} className="flex flex-wrap items-baseline gap-x-2 text-muted">
838 <Link
839 to={`${base}/pull/${other.number}`}
840 className="font-medium text-fg hover:underline"
841 >
842 {other.title} <span className="font-normal text-faint">#{other.number}</span>
843 </Link>
844 <span className="font-mono text-xs">{other.paths.join(", ")}</span>
845 </li>
846 ))}
847 </ul>
848 <p className="mt-2 text-xs text-faint">
849 Whichever merges second will have to catch up, and may conflict.
850 </p>
851 </div>
852 )}
853 </div>
854 )}
855 <div className="mt-5">
856 {comparison ? (
857 <DiffView
858 comparison={comparison}
859 review={{
860 comments: comments.filter((comment) => comment.path),
861 canComment: Boolean(viewer),
862 }}
863 />
864 ) : tab === "session" ? (
865 session.length === 0 ? (
866 <EmptyState title="Nothing recorded yet">
867 The agent's prompts, reasoning and tool calls appear here as it
868 works.
869 </EmptyState>
870 ) : (
871 <ol className="relative space-y-5 before:absolute before:top-2 before:bottom-2 before:left-3.25 before:w-px before:bg-line">
872 {session.map((entry) => (
873 <Entry key={entry.seq} entry={entry} agent={pull.agent} />
874 ))}
875 </ol>
876 )
877 ) : (
878 <div className="space-y-4">
879 <TimelineItem
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent880 author={opener.name}
Pull requests: title and tabs across the full width, with counts and the size of the change881 at={pull.createdAt}
882 action={
883 <span>
884 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-agent885 {opener.requestedBy ? (
Pull requests: title and tabs across the full width, with counts and the size of the change886 <>
887 {" "}
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent888 for{" "}
889 <PersonLink name={opener.requestedBy} className="font-medium text-fg-soft hover:underline" />
890 </>
891 ) : !pull.branch && (
892 <>
893 {" "}
Pull requests: title and tabs across the full width, with counts and the size of the change894 with <span className="font-mono text-xs">{pull.agent}</span>
895 </>
896 )}
897 </span>
898 }
899 >
900 {pull.body ? (
901 <Markdown source={pull.body} repo={{ namespace: params.owner, name: params.repo }} />
902 ) : (
903 <p className="text-sm text-muted">
904 {working
905 ? "No description yet. It is written when the pull request is marked ready for review."
906 : "No description."}
907 </p>
908 )}
909 {canManage && pull.status === "open" && (
910 <details className="mt-3 border-t border-line pt-3 text-sm">
911 <summary className="cursor-pointer text-xs text-faint hover:text-fg">
912 Edit description
913 </summary>
914 <Form method="post" className="mt-3 space-y-2">
915 <Textarea
916 name="summary"
917 rows={6}
918 placeholder="What changed and why"
919 defaultValue={pull.body ?? ""}
920 />
921 <Button type="submit">Save</Button>
922 </Form>
923 </details>
924 )}
925 </TimelineItem>
Agents as a team: lifecycle, merge queue, billing and a new shell926
Pull requests: title and tabs across the full width, with counts and the size of the change927 <CommentList comments={comments} review={review} base={base} />
Agents as a team: lifecycle, merge queue, billing and a new shell928
Fast pages, required checks on the branch, self-hosted runners, honest incidents929 {(preview || build) && <DeploymentCard preview={preview} build={build} stacked={stacked} base={base} />}
Pull requests say where they are live; the project menu shows what is coming930
Pull requests: title and tabs across the full width, with counts and the size of the change931 {pull.status === "draft" && (
932 <StatusBox>
933 <StatusRow icon={<Loader size={16} className="text-faint" />} title="This is a draft">
934 It is still being worked on. It can be reviewed and merged once it is
935 marked ready.
936 </StatusRow>
937 {canManage && (
938 <Form method="post" className="space-y-2 px-4 py-3">
939 <Textarea name="summary" rows={3} placeholder="What changed and why" />
940 <Button type="submit">Mark ready for review</Button>
941 </Form>
942 )}
943 </StatusBox>
944 )}
Agents as a team: lifecycle, merge queue, billing and a new shell945
Pull requests: title and tabs across the full width, with counts and the size of the change946 {pull.status === "open" && (
947 <StatusBox>
Fast pages, required checks on the branch, self-hosted runners, honest incidents948 {/* Checks by status at once; job by job when the runs are read. */}
949 <Suspense fallback={checksSection({})}>
950 <Await resolve={workflowJobs}>{checksSection}</Await>
951 </Suspense>
952 {noChecks && <AddCiPrompt owner={params.owner} repo={params.repo} canAdd={canAddCi} compact />}
Agents as a team: lifecycle, merge queue, billing and a new shell953 <StatusRow
Pull requests: title and tabs across the full width, with counts and the size of the change954 icon={
955 reviews.some(({ verdict }) => verdict === "request_changes") ? (
956 <CircleSlash size={16} className="text-danger" />
957 ) : reviews.length > 0 ? (
958 <CircleCheck size={16} className="text-accent" />
959 ) : (
960 <MessageSquare size={16} className="text-faint" />
961 )
962 }
963 title={
964 reviews.length === 0
965 ? "No reviews yet"
966 : reviews
967 .map(
968 ({ reviewer, verdict }) =>
969 `${verdict === "approve" ? "Approved by" : "Changes requested by"} ${reviewer}`,
970 )
971 .join(" · ")
972 }
Agents as a team: lifecycle, merge queue, billing and a new shell973 >
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent974 {reviewPending && "g1t is reviewing it now. "}
Pull requests: title and tabs across the full width, with counts and the size of the change975 {requiredApprovals > 0 &&
976 `This repository requires ${requiredApprovals} approving ${
977 requiredApprovals === 1 ? "review" : "reviews"
978 } before merging.`}
Agents as a team: lifecycle, merge queue, billing and a new shell979 </StatusRow>
Pull requests: title and tabs across the full width, with counts and the size of the change980 {landing ? (
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 icon={<Loader size={16} className="animate-spin text-accent" />}
983 title="Merging"
984 >
985 {defaultBranch} has moved, so g1t is bringing this up to date first. It
986 lands as soon as that is done. Watch it in the Session tab.
987 </StatusRow>
Catching up with main takes seconds when the two sides touched different files988 ) : agentCatchUp && behind ? (
989 <CatchUpProgress
990 owner={params.owner}
991 repo={params.repo}
992 number={pull.number}
993 defaultBranch={defaultBranch}
994 behind={behind}
995 update={agentCatchUp.update}
996 startedAt={agentCatchUp.startedAt}
997 retrying={catchUpPending}
998 />
Pull requests: title and tabs across the full width, with counts and the size of the change999 ) : conflicting ? (
1000 <ConflictsSection
1001 conflicts={conflicts}
1002 pull={pull}
1003 owner={params.owner}
1004 repo={params.repo}
1005 defaultBranch={defaultBranch}
1006 changesUrl={here + "?tab=changes"}
1007 canResolve={canUpdate && agentsEnabled}
Catching up with main takes seconds when the two sides touched different files1008 resolving={catchUpPending}
1009 error={catchUp && "error" in catchUp ? catchUp.error : null}
Agents as a team: lifecycle, merge queue, billing and a new shell1010 />
Pull requests: title and tabs across the full width, with counts and the size of the change1011 ) : probing ? (
1012 <MergeabilityRow mergeable={mergeable} defaultBranch={defaultBranch} />
1013 ) : behind ? (
1014 <StatusRow
1015 icon={<GitPullRequestArrow size={16} className="text-info" />}
1016 title={`${defaultBranch} has moved since this was made`}
1017 >
1018 {mergeable === "clean" && "It has no conflicts with it. "}
1019 {requireUpToDate
1020 ? "This repository requires pull requests to be up to date, so it has to catch up before it can merge."
1021 : "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 files1022 {canUpdate && (
1023 <Form method="post" className="mt-2">
Pull requests: title and tabs across the full width, with counts and the size of the change1024 <input type="hidden" name="action" value="update" />
Catching up with main takes seconds when the two sides touched different files1025 <Button variant="quiet" type="submit" disabled={catchUpPending}>
1026 {catchUpPending && <Loader size={14} className="animate-spin" />}
1027 {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 change1028 </Button>
1029 </Form>
1030 )}
Catching up with main takes seconds when the two sides touched different files1031 {catchUp && "error" in catchUp && <ErrorText>{catchUp.error}</ErrorText>}
Pull requests: title and tabs across the full width, with counts and the size of the change1032 </StatusRow>
1033 ) : (
1034 pull.headCommit && (
1035 <StatusRow
1036 icon={<CircleCheck size={16} className="text-accent" />}
Catching up with main takes seconds when the two sides touched different files1037 title={
1038 (caughtUp && !caughtUp.already) || agentCatchUp
1039 ? `Brought up to date with ${defaultBranch}`
1040 : `Up to date with ${defaultBranch}`
1041 }
1042 >
1043 {caughtUp && !caughtUp.already && (
1044 <>
1045 Merged <span className="font-mono">{defaultBranch}</span> in as{" "}
1046 <span className="font-mono text-fg">{caughtUp.commit.slice(0, 7)}</span>. Its checks run
1047 again on the new commit.
1048 </>
1049 )}
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent1050 {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 files1051 </StatusRow>
Pull requests: title and tabs across the full width, with counts and the size of the change1052 )
1053 )}
1054 {stalled && !lifecycle && (
1055 <StatusRow icon={<Hand size={16} className="text-warn" />} title="Needs you">
1056 {stalled}
1057 </StatusRow>
1058 )}
1059 {canMerge && lifecycle?.stage === "queued" && (
1060 <Form method="post" className="flex flex-wrap items-center gap-3 px-4 py-3">
1061 <ButtonLink to={`${base}/queue`}>
1062 <Layers size={15} />
1063 See the queue
1064 </ButtonLink>
1065 <Button variant="quiet" type="submit" name="action" value="unqueue">
1066 Remove from the queue
Agents as a team: lifecycle, merge queue, billing and a new shell1067 </Button>
Pull requests: title and tabs across the full width, with counts and the size of the change1068 </Form>
1069 )}
1070 {canMerge && !landing && lifecycle?.stage !== "queued" && (
1071 <Form method="post" className="space-y-3 px-4 py-3">
1072 {issue?.state === "open" && (
1073 <CheckboxOption
1074 name="keepIssueOpen"
1075 label={`Keep #${issue.number} open. This is only part of the work.`}
1076 labelClassName="text-xs text-muted"
1077 />
1078 )}
1079 {unchecked && canIgnoreChecks && (
1080 <CheckboxOption
1081 name="ignoreChecks"
Fast pages, required checks on the branch, self-hosted runners, honest incidents1082 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 change1083 labelClassName="text-xs text-muted"
1084 />
1085 )}
1086 <div className="flex flex-wrap items-center gap-3">
1087 <Button
1088 variant="accent"
1089 type="submit"
1090 name="action"
1091 value="merge"
1092 disabled={mergeBlocked != null}
1093 title={mergeBlocked ?? undefined}
1094 >
1095 {mergeQueue ? <Layers size={15} /> : <GitMerge size={15} />}
1096 {mergeQueue ? "Add to the merge queue" : `Merge into ${defaultBranch}`}
1097 </Button>
1098 <span className={`text-xs ${mergeBlocked ? "text-danger" : "text-muted"}`}>
1099 {mergeBlocked ?? (mergeQueue
1100 ? `Tested together with everything ahead of it, then lands on ${defaultBranch}.`
1101 : issue?.state === "open"
1102 ? `Closes issue #${issue.number}, and any other pull requests still open for it.`
1103 : `Lands its commits on ${defaultBranch}.`)}
1104 </span>
1105 </div>
1106 {actionData?.action === "merge" && (
1107 <ErrorText>{actionData.error}</ErrorText>
1108 )}
1109 </Form>
1110 )}
1111 </StatusBox>
1112 )}
Agents as a team: lifecycle, merge queue, billing and a new shell1113
Pull requests: title and tabs across the full width, with counts and the size of the change1114 <CommentForm
1115 author={viewer?.username ?? null}
1116 resetKey={comments.length}
1117 review={review}
1118 />
1119 {canManage && active && (
1120 <Form method="post" className="flex justify-end">
1121 <Button variant="quiet" type="submit" name="action" value="close">
1122 Close pull request
1123 </Button>
1124 </Form>
1125 )}
1126 {actionData &&
Fast pages, required checks on the branch, self-hosted runners, honest incidents1127 !["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 change1128 String(actionData.action),
1129 ) && <ErrorText>{actionData.error}</ErrorText>}
1130 </div>
1131 )}
1132 {actionData?.action === "comment" && (
1133 <div className="mt-2">
1134 <ErrorText>{actionData.error}</ErrorText>
1135 </div>
1136 )}
1137 </div>
1138 </div>
1139
1140 <aside className={tab === "changes" ? "hidden" : "space-y-6"}>
1141 {/* An open pull request shows its checks in full in the merge box. */}
Fast pages, required checks on the branch, self-hosted runners, honest incidents1142 {pull.status !== "open" && <WorkflowStatuses statuses={statuses} />}
Pull requests: title and tabs across the full width, with counts and the size of the change1143 {affects.length > 0 && (
1144 <section>
1145 <h3 className="flex items-center gap-1.5 text-sm font-medium">
1146 <Network size={14} className="text-faint" />
1147 Affects
1148 </h3>
1149 <p className="mt-1 text-xs text-muted">Projects that use this one, and so may feel this change:</p>
1150 <ul className="mt-2 space-y-1 text-sm">
1151 {affects.map((project) => (
1152 <li key={project.slug} className="flex items-center justify-between gap-2">
1153 <Link to={`/${params.owner}/${project.slug}`} className="hover:underline">
1154 {project.name}
1155 </Link>
1156 {project.as && <code className="font-mono text-xs text-faint">{project.as}</code>}
1157 </li>
1158 ))}
1159 </ul>
1160 {preview?.branch && canMerge && (
1161 <Form method="post" className="mt-3">
1162 <input type="hidden" name="action" value="stack" />
1163 <input type="hidden" name="branch" value={preview.branch} />
1164 <button
1165 type="submit"
1166 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"
1167 >
1168 Preview them against this change
1169 </button>
Agents as a team: lifecycle, merge queue, billing and a new shell1170 </Form>
1171 )}
Pull requests: title and tabs across the full width, with counts and the size of the change1172 {actionData?.action === "stack" &&
1173 ("notice" in actionData ? (
1174 <p className="mt-2 text-xs text-accent">{String(actionData.notice)}</p>
1175 ) : (
1176 <ErrorText>{actionData.error}</ErrorText>
1177 ))}
1178 </section>
Acceptance checks in sandboxes, line comments and review verdicts1179 )}
Issues and pull requests replace intents and attempts1180
Project dependencies: addresses, preview stacks, Affects, and agents who know1181 <section>
Pull requests: title and tabs across the full width, with counts and the size of the change1182 <h3 className="text-sm font-medium">Reviewers</h3>
1183 <ul className="mt-2 space-y-1.5 text-sm">
1184 {reviewerNames.map((name) => {
1185 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-agent1186 const pending = name === "g1t" && reviewPending;
Pull requests: title and tabs across the full width, with counts and the size of the change1187 return (
1188 <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-agent1189 {name === "g1t" ? (
Pull requests: title and tabs across the full width, with counts and the size of the change1190 <Sparkles size={16} className="shrink-0 text-accent" />
1191 ) : (
1192 <Avatar name={name} size={20} />
1193 )}
1194 <span className="grow truncate font-mono text-xs">{name}</span>
1195 {pending ? (
1196 <span className="flex items-center gap-1.5 text-xs text-muted">
1197 <span className="size-1.5 animate-pulse rounded-full bg-accent" />
1198 Reviewing
1199 </span>
1200 ) : verdict === "approve" ? (
1201 <span className="flex items-center gap-1 text-xs text-accent">
1202 <CircleCheck size={13} /> Approved
1203 </span>
1204 ) : verdict === "request_changes" ? (
1205 <span className="flex items-center gap-1 text-xs text-danger">
1206 <CircleSlash size={13} /> Changes requested
1207 </span>
1208 ) : (
1209 <span className="text-xs text-faint">Review requested</span>
1210 )}
1211 </li>
1212 );
1213 })}
1214 {reviewerNames.length === 0 && (
1215 <li className="px-1 text-xs text-faint">No reviews requested yet.</li>
1216 )}
Project dependencies: addresses, preview stacks, Affects, and agents who know1217 </ul>
Pull requests: title and tabs across the full width, with counts and the size of the change1218 {pull.status === "open" && canManage && (
1219 <div className="mt-3 space-y-2">
1220 {agentsEnabled && !reviewPending && (
1221 <Form method="post">
1222 <input type="hidden" name="action" value="agent-review" />
1223 {pull.reviewers.map((name) => (
1224 <input key={name} type="hidden" name="reviewer" value={name} />
1225 ))}
1226 <div className="*:w-full">
1227 <Button variant="quiet" type="submit">
1228 <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-agent1229 Request review from g1t
Pull requests: title and tabs across the full width, with counts and the size of the change1230 </Button>
1231 </div>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1232 <ComputeNote note={loaderData.computeNote} />
Pull requests: title and tabs across the full width, with counts and the size of the change1233 </Form>
1234 )}
1235 <details>
1236 <summary className="cursor-pointer text-xs text-faint hover:text-fg">
1237 Request review from people
1238 </summary>
1239 <Form method="post" className="mt-2 space-y-2" key={pull.reviewers.join()}>
1240 <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-agent1241 {pull.reviewers.includes("g1t") && (
1242 <input type="hidden" name="reviewer" value="g1t" />
Pull requests: title and tabs across the full width, with counts and the size of the change1243 )}
1244 <PeoplePicker
1245 name="reviewer"
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights1246 members={members.filter((name) => name !== workOwner(pull).username)}
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent1247 chosen={pull.reviewers.filter((name) => name !== "g1t")}
Pull requests: title and tabs across the full width, with counts and the size of the change1248 />
1249 <Button variant="quiet" type="submit">
1250 Save reviewers
1251 </Button>
1252 </Form>
1253 </details>
1254 </div>
Project dependencies: addresses, preview stacks, Affects, and agents who know1255 )}
Pull requests: title and tabs across the full width, with counts and the size of the change1256 {["agent-review", "reviewers"].includes(String(actionData?.action)) && (
1257 <ErrorText>{actionData?.error}</ErrorText>
1258 )}
Project dependencies: addresses, preview stacks, Affects, and agents who know1259 </section>
Acceptance checks in sandboxes, line comments and review verdicts1260
Pull requests: title and tabs across the full width, with counts and the size of the change1261 <section>
1262 <h3 className="text-sm font-medium">Assignees</h3>
1263 <ul className="mt-2 space-y-1.5 text-sm">
1264 {pull.assignees.map((name) => (
Agents as a team: lifecycle, merge queue, billing and a new shell1265 <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 change1266 <Avatar name={name} size={20} />
Agents as a team: lifecycle, merge queue, billing and a new shell1267 <span className="grow truncate font-mono text-xs">{name}</span>
1268 </li>
Pull requests: title and tabs across the full width, with counts and the size of the change1269 ))}
1270 {pull.assignees.length === 0 && (
1271 <li className="px-1 text-xs text-faint">No one yet.</li>
1272 )}
1273 </ul>
1274 {viewer && canManage && active && (
1275 <div className="mt-3 space-y-2">
1276 {!pull.assignees.includes(viewer.username) && (
1277 <Form method="post">
1278 <input type="hidden" name="action" value="assign" />
1279 {pull.assignees.map((name) => (
1280 <input key={name} type="hidden" name="assignee" value={name} />
1281 ))}
1282 <input type="hidden" name="assignee" value={viewer.username} />
1283 <div className="*:w-full">
1284 <Button variant="quiet" type="submit">
1285 Assign yourself
1286 </Button>
1287 </div>
1288 </Form>
1289 )}
1290 <details>
1291 <summary className="cursor-pointer text-xs text-faint hover:text-fg">
1292 Assign people
1293 </summary>
1294 <Form method="post" className="mt-2 space-y-2" key={pull.assignees.join()}>
1295 <input type="hidden" name="action" value="assign" />
1296 <PeoplePicker name="assignee" members={members} chosen={pull.assignees} />
Agents as a team: lifecycle, merge queue, billing and a new shell1297 <Button variant="quiet" type="submit">
Pull requests: title and tabs across the full width, with counts and the size of the change1298 Save assignees
Agents as a team: lifecycle, merge queue, billing and a new shell1299 </Button>
Pull requests: title and tabs across the full width, with counts and the size of the change1300 </Form>
1301 </details>
1302 </div>
Issues and pull requests replace intents and attempts1303 )}
Pull requests: title and tabs across the full width, with counts and the size of the change1304 {actionData?.action === "assign" && <ErrorText>{actionData.error}</ErrorText>}
1305 </section>
Issues and pull requests replace intents and attempts1306
Pull requests: title and tabs across the full width, with counts and the size of the change1307 <section>
1308 <h3 className="text-sm font-medium">Working copy</h3>
1309 {pull.branch ? (
1310 <>
1311 <p className="mt-1 text-xs text-muted">
1312 <GitBranch size={13} className="mr-1 inline align-[-2px]" />
1313 Branch <span className="font-mono text-fg">{pull.branch}</span> of this
1314 repository. Pushes to it show up here.
1315 </p>
1316 <div className="mt-2">
1317 <CopyLine text={`git clone -b ${pull.branch} ${remote}`} />
1318 </div>
1319 </>
1320 ) : (
1321 <>
1322 <p className="mt-1 text-xs text-muted">
1323 This fork belongs to the pull request. Pushes to it show up here.
1324 </p>
1325 <div className="mt-2">
1326 <CopyLine text={`git clone ${remote}`} />
1327 </div>
1328 </>
1329 )}
1330 <p className="mt-3 flex items-center gap-1.5 font-mono text-xs text-faint">
1331 <GitCommitHorizontal size={13} />
1332 {pull.headCommit?.slice(0, 12) ?? "no commits pushed yet"}
1333 </p>
1334 </section>
1335 </aside>
1336 </div>
Issues and pull requests replace intents and attempts1337 </div>
1338 );
1339}
Pull requests say where they are live; the project menu shows what is coming1340
1341/** Where the pull request's change is live, as GitHub shows a deployment. */
1342function DeploymentCard({
1343 preview,
1344 build,
1345 stacked,
1346 base,
1347}: {
1348 preview: LiveApp | null;
1349 build: Deployment | null;
Fast pages, required checks on the branch, self-hosted runners, honest incidents1350 stacked: Promise<Stacked[]>;
Pull requests say where they are live; the project menu shows what is coming1351 base: string;
1352}) {
1353 const building = build?.status === "queued" || build?.status === "building";
1354 const failed = build?.status === "failed";
1355 return (
1356 <section className="ml-12 overflow-hidden rounded-xl border border-line bg-surface">
1357 <div className="flex flex-wrap items-center gap-3 px-4 py-3">
1358 <span
1359 className={`flex size-8 shrink-0 items-center justify-center rounded-full ring-1 ${
1360 failed ? "text-danger ring-danger/40" : building ? "text-warn ring-warn/40" : "text-accent ring-accent/40"
1361 }`}
1362 >
1363 {building ? <Loader size={15} className="animate-spin" /> : <Rocket size={15} />}
1364 </span>
1365 <div className="min-w-0 grow">
1366 <p className="text-sm font-medium">
1367 {building
1368 ? preview
1369 ? "Deploying the latest push. The last preview is still live."
1370 : "Deploying a preview"
1371 : failed
1372 ? "The preview failed to deploy"
1373 : "This branch is live"}
1374 </p>
1375 {preview ? (
1376 <a href={preview.url} className="mt-0.5 block truncate font-mono text-xs text-muted hover:text-accent">
1377 {preview.url.replace(/^https?:[/][/]/, "")}
1378 </a>
1379 ) : (
1380 build?.error && <p className="mt-0.5 truncate text-xs text-muted">{build.error}</p>
1381 )}
1382 </div>
1383 <div className="flex shrink-0 items-center gap-2">
1384 {build && (
1385 <Link to={`${base}/deployments/${build.id}`} className="text-xs text-muted hover:text-fg">
1386 {failed ? "See why" : "Build log"}
1387 </Link>
1388 )}
1389 {preview && (
1390 <a
1391 href={preview.url}
1392 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"
1393 >
1394 Visit preview
1395 <ArrowUpRight size={13} />
1396 </a>
1397 )}
1398 </div>
1399 </div>
1400 {preview && (
1401 <p className="border-t border-line px-4 py-2 text-xs text-faint">
1402 <span className="font-mono">{preview.commit.slice(0, 7)}</span> · deployed <TimeAgo at={preview.deployedAt} />
1403 {preview.branch && ` · from ${preview.branch}`}
1404 </p>
1405 )}
Fast pages, required checks on the branch, self-hosted runners, honest incidents1406 {/* Read after the page: the card's own part shows at once. */}
1407 <Suspense fallback={null}>
1408 <Await resolve={stacked}>
1409 {(stacked) =>
1410 stacked.length > 0 && (
1411 <div className="border-t border-line px-4 py-2.5">
1412 <p className="text-xs text-muted">Built against this change:</p>
1413 <ul className="mt-1.5 space-y-1">
1414 {stacked.map((entry) => (
1415 <li key={entry.slug} className="flex items-center gap-2 text-xs">
1416 <span className="font-medium">{entry.name}</span>
1417 <a href={entry.url} className="truncate font-mono text-muted hover:text-accent">
1418 {entry.url.replace(/^https?:[/][/]/, "")}
1419 </a>
1420 </li>
1421 ))}
1422 </ul>
1423 </div>
1424 )
1425 }
1426 </Await>
1427 </Suspense>
Pull requests say where they are live; the project menu shows what is coming1428 </section>
1429 );
1430}

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