Skip to content

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

1,700 lines77,533 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,
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge12 Scale,
Agents as a team: lifecycle, merge queue, billing and a new shell13 GitPullRequestArrow,
14 Hand,
15 Loader,
16 Radar,
17 Sparkles,
Issues and pull requests replace intents and attempts18 MessageSquare,
Pull requests say where they are live; the project menu shows what is coming19 ArrowUpRight,
Issues and pull requests replace intents and attempts20 MessagesSquare,
Pull requests say where they are live; the project menu shows what is coming21 Rocket,
Project dependencies: addresses, preview stacks, Affects, and agents who know22 Network,
Issues and pull requests replace intents and attempts23 StickyNote,
24 User,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar25 Users,
Issues and pull requests replace intents and attempts26 Wrench,
27} from "lucide-react";
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge28import { Suspense, useState } from "react";
Merge branch 'worktree-agent-a985c01f43538af01'29import { Await, Form, Link, redirect } from "react-router";
Issues and pull requests replace intents and attempts30
Pull requests say where they are live; the project menu shows what is coming31import {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look32 type Capability,
Pull requests say where they are live; the project menu shows what is coming33 type Comparison,
34 type Deployment,
Agents and memory, checks and conflicts, profiles, slug renames, custom domains35 type Job,
Pull requests say where they are live; the project menu shows what is coming36 type LiveApp,
37 type SessionEntry,
38 type Viewer,
39 pullComparison,
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights40 workOwner,
Pull requests say where they are live; the project menu shows what is coming41} from "@g1t/contracts";
Issues and pull requests replace intents and attempts42
43import type { Route } from "./+types/pull";
Usage, Billing settings and prepaid AI credit; fixes from the UX audit44import { highlightFirstFiles } from "../../lib/highlight.server";
Social cards for every page: og.g1t.sh45import { 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-agent46import { openedBy } from "../../lib/opened-by";
Merge branch 'worktree-agent-aaf03bdceac799c89'47import { cloneUrl, useAddresses } from "../../lib/addresses";
Issues and pull requests replace intents and attempts48import { DiffView } from "../../components/diff-view";
Agents as a team: lifecycle, merge queue, billing and a new shell49import { LifecyclePanel } from "../../components/lifecycle";
Agents and memory, checks and conflicts, profiles, slug renames, custom domains50import { AgentPanel } from "../../components/agents";
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar51import { BaseBranch } from "../../components/base-branch";
52import { LabelChip, LabelsBox, MilestoneBox } from "../../components/labels";
Issues and pull requests replace intents and attempts53import { Markdown } from "../../components/markdown";
54import {
55 Avatar,
Agents as a team: lifecycle, merge queue, billing and a new shell56 ButtonLink,
Issues and pull requests replace intents and attempts57 CopyLine,
58 EmptyState,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look59 ComputeNote,
Issues and pull requests replace intents and attempts60 ErrorText,
Merge branch 'worktree-agent-a985c01f43538af01'61 SubmitButton,
Issues and pull requests replace intents and attempts62 Textarea,
63 TimeAgo,
Merge branch 'worktree-agent-a985c01f43538af01'64 usePending,
Issues and pull requests replace intents and attempts65} from "../../components/ui";
Workspace names and icons, and a component kit for every control66import { CheckboxOption } from "../../components/ui/checkbox";
Merge branch 'worktree-agent-ae1299e92e4462012'67import { Hint } from "../../components/ui/hint";
Merge branch 'worktree-agent-aca4790ea223aa235'68import { Loading, SkeletonLine } from "../../components/ui/skeleton";
Usage, Billing settings and prepaid AI credit; fixes from the UX audit69import { TabStrip } from "../../components/ui/tab-strip";
GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs70import { WorkflowStatuses } from "../../components/actions";
Fast pages, required checks on the branch, self-hosted runners, honest incidents71import { AddCiPrompt } from "../../components/add-ci";
Agents as a team: lifecycle, merge queue, billing and a new shell72import {
73 CommentForm,
74 CommentList,
75 IssueIcon,
76 PeoplePicker,
Agents and memory, checks and conflicts, profiles, slug renames, custom domains77 PersonLink,
Agents as a team: lifecycle, merge queue, billing and a new shell78 PullState,
79 TimelineItem,
80 verdicts,
81} from "../../components/work";
Catching up with main takes seconds when the two sides touched different files82import { CatchUpProgress, ChecksSection, ConflictsSection, MergeabilityRow, runIdOf } from "../../components/merge-box";
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge83import { ViolationList } from "../../components/rules";
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar84import { PullCodeOwnersPanel, TeamReviewer } from "../../components/codeowners";
Catching up with main takes seconds when the two sides touched different files85import { 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 put86import { notFound } from "../../lib/not-found.server";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look87import { computeNoteFor } from "../../lib/compute.server";
Merge branch 'worktree-agent-aca4790ea223aa235'88import { actions, agents, deployments, identity, inbox, projects, repos, work } from "../../lib/services.server";
Merge checks: statuses and check runs on every commit89import { commitChecksFor } from "../../lib/commit-checks.server";
90import { CommitChecksBadge } from "../../components/commit-checks";
Issues and pull requests replace intents and attempts91import { assertSameOrigin, getViewer, requireUser } from "../../lib/session.server";
Inbox on the site: reasons, a reason filter, Subscribe, Watch and settings92import { accessTo, refusal, repoFor } from "../../lib/access.server";
93import { SubscriptionBox } from "../../components/notifications";
A click away from a live page arrives: pages stop refreshing themselves while a navigation is pending94import { REFRESH_MS, useRefreshWhile } from "../../lib/refresh";
Issues and pull requests replace intents and attempts95
96const EMPTY_COMPARISON: Comparison = { base: null, head: "", files: [], truncated: false };
97const TABS = ["conversation", "session", "changes"] as const;
98type Tab = (typeof TABS)[number];
99
Social cards for every page: og.g1t.sh100export function meta({ loaderData, params, ...args }: Route.MetaArgs) {
101 const pull = loaderData?.pull;
102 const title = pull ? `${pull.title} · Pull request #${pull.number} · ` : "";
103 const state = { draft: "Draft", open: "Open", merged: "Merged", closed: "Closed" }[pull?.status ?? "open"];
104 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-agent105 const opener = pull ? openedBy(pull) : null;
Social cards for every page: og.g1t.sh106 return page(args, {
107 title: `${title}${params.owner}/${params.repo} · g1t`,
108 description: pull
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent109 ? `${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.sh110 : null,
111 // The card shows the title and the state.
112 version: pull ? [pull.title, pull.status] : undefined,
113 type: "article",
114 });
Issues and pull requests replace intents and attempts115}
116
117export async function loader({ params, context, request }: Route.LoaderArgs) {
118 const viewer = getViewer(context);
119 const path = { namespace: params.owner, name: params.repo };
120 const number = Number(params.number);
121 const asked = new URL(request.url).searchParams.get("tab");
122 const tab: Tab = TABS.find((name) => name === asked) ?? "conversation";
123
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look124 // Members of the workspace pick reviewers and assignees from its people;
125 // what the viewer may do here goes by their role on the repository.
Agents as a team: lifecycle, merge queue, billing and a new shell126 const member = (viewer?.workspaces ?? []).some(
127 (membership) => membership.slug === params.owner,
128 );
Fast pages, required checks on the branch, self-hosted runners, honest incidents129 // Everything starts at once. Only what the viewer's role decides waits
130 // for the repository, which the layout is looking up in this request too
131 // (lib/access.server.ts), and only what the pull request decides waits
132 // for it.
Project dependencies: addresses, preview stacks, Affects, and agents who know133 const ref = { workspace: params.owner, slug: params.repo };
Fast pages, required checks on the branch, self-hosted runners, honest incidents134 const access = accessTo(context, params);
135 const pullFound = work.getPull(path, number, viewer);
136 const deps = projects.dependencies(params.owner, params.repo, viewer);
137 // Awaited below, unless the pull request is missing first.
138 deps.catch(() => null);
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar139 // Its labels and milestone, with the repository's to choose from.
140 const labelsFound = work.listLabels(path, viewer);
141 const milestonesFound = work.listMilestones(path, viewer);
142 labelsFound.catch(() => null);
143 milestonesFound.catch(() => null);
144 const [{ can }, found, repo, settings, agentsEnabled, members, computeNote, session, deployed, subscription, runs, teamList] = await Promise.all([
Fast pages, required checks on the branch, self-hosted runners, honest incidents145 access,
146 pullFound,
Issues and pull requests replace intents and attempts147 repos.get(path, viewer),
Agents as a team: lifecycle, merge queue, billing and a new shell148 work.getSettings(path, viewer),
g1t's agents only for listed workspaces, whatever the state of billing149 env.RUNNER.enabled(viewer, path),
Agents as a team: lifecycle, merge queue, billing and a new shell150 // A member picks reviewers and assignees from the workspace's people.
151 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-agent152 // Before someone asks g1t for something: whether the plan lets it start.
Fast pages, required checks on the branch, self-hosted runners, honest incidents153 access.then(({ can }) => (can.run ? computeNoteFor(params.owner, "agent") : null)),
154 tab === "session" ? work.readSession(path, number, viewer) : null,
155 // Its preview, for people with a role here: beside the rest, not after.
156 access.then(({ insider }) => (insider ? deployments.list(ref, viewer).catch(() => null) : null)),
Inbox on the site: reasons, a reason filter, Subscribe, Watch and settings157 // Whether the viewer hears of it, for the sidebar's Notifications.
158 viewer
159 ? repoFor(context, params).then((found) =>
160 found.ok ? inbox.subscription(viewer, { repoId: found.value.id, number }).catch(() => null) : null,
161 )
162 : null,
Merge branch 'worktree-agent-aca4790ea223aa235'163 // Its agent's latest runs, for the Agent panel: with the page, not after it.
164 agents.listRuns(viewer, { repo: path, number, limit: 5 }).catch(() => null),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar165 // A member asks the workspace's teams to review, and mentions them.
166 member ? identity.listTeams(viewer, params.owner).catch(() => null) : null,
Issues and pull requests replace intents and attempts167 ]);
168 if (!found.ok) {
169 // Issues and pull requests share numbers; this one may be an issue.
170 const issue = await work.getIssue(path, number, viewer);
171 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 put172 throw notFound("pull");
Issues and pull requests replace intents and attempts173 }
174 const { pull } = found.value;
Pull requests from branches175 const range = pullComparison(pull);
Agents and memory, checks and conflicts, profiles, slug renames, custom domains176 // The jobs of each workflow run on its head, to list checks job by job.
177 // 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 incidents178 // Streamed: the checks show by status first, then job by job.
Agents and memory, checks and conflicts, profiles, slug renames, custom domains179 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 incidents180 const workflowJobs =
181 tab === "conversation" && pull.status === "open" && runIds.length > 0
182 ? Promise.all(runIds.map((id) => actions.run(path, viewer, id).catch(() => null))).then((runs) => {
183 const jobs: Record<string, Job[]> = {};
184 for (const run of runs) if (run?.ok) jobs[run.value.run.id] = run.value.jobs;
185 return jobs;
186 })
187 .catch(() => ({}) as Record<string, Job[]>)
188 : Promise.resolve({} as Record<string, Job[]>);
189 // No checks at all on an open pull request: whether that is because the
190 // repository has no workflows, to offer a starter one.
191 const unchecked =
192 tab === "conversation" &&
193 pull.status === "open" &&
194 (found.value.statuses ?? []).length === 0 &&
195 (found.value.requiredChecks ?? []).length === 0;
196 const [comparison, used, noChecks] = await Promise.all([
Usage, Billing settings and prepaid AI credit; fixes from the UX audit197 // The first screens of it highlighted here, so their colours do not pop in.
198 tab === "changes"
199 ? repos
200 .compare(range.repoId, viewer, range.base, range.head, range.baseBranch)
201 .then(async (found) => (found.ok ? { ok: true as const, value: await highlightFirstFiles(found.value) } : found))
202 : null,
Fast pages, required checks on the branch, self-hosted runners, honest incidents203 deps,
204 unchecked
205 ? actions
206 .workflows(path, viewer)
207 .then((found) => found.ok && found.value.length === 0)
208 .catch(() => false)
209 : false,
Issues and pull requests replace intents and attempts210 ]);
Fast pages, required checks on the branch, self-hosted runners, honest incidents211 const affects = used.ok ? used.value.usedBy : [];
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar212 const [labels, milestones] = await Promise.all([labelsFound, milestonesFound]);
213 const repoDefault = repo.ok ? repo.value.defaultBranch : "main";
214 // The default branch's protection covers pull requests into it only.
215 const protectedBase = (pull.base ?? repoDefault) === repoDefault;
Issues and pull requests replace intents and attempts216 return {
217 ...found.value,
Merge checks: statuses and check runs on every commit218 // Every check on its head, as the commit's own list shows them: streamed.
219 headChecks: commitChecksFor(path, viewer, [pull.headCommit]),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar220 labels: labels.ok ? labels.value : [],
221 milestones: milestones.ok ? milestones.value : [],
222 // Labelling and milestones: Triage and up; its author, its labels.
223 canTriage: can.triage,
Merge membership: owners, org roles, GitHub's repo roles, privileges, 2FA224 // Making labels needs Write; applying them, Triage.
225 canMakeLabels: can.manage_labels,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar226 // The branch it merges into: Write and up.
227 canChangeBase: can.push,
Agents and memory, checks and conflicts, profiles, slug renames, custom domains228 workflowJobs,
Issues and pull requests replace intents and attempts229 tab,
230 session: session?.ok ? session.value : [],
231 // An empty comparison if it could not be made.
232 comparison: comparison && (comparison.ok ? comparison.value : EMPTY_COMPARISON),
233 viewer,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look234 // Write and up can merge.
235 canMerge: can.merge,
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights236 // Triage and up manage anyone's pull request; its author, their own,
237 // and whoever asked g1t for one, that one.
238 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 look239 // Telling its agent things, and re-running checks, spend compute: Write and up.
240 canRun: can.run,
Agents as a team: lifecycle, merge queue, billing and a new shell241 // 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 rights242 // whoever it is for (who asked g1t for it, or its author), a branch
243 // from anyone who can push.
244 canUpdate: pull.fork ? viewer?.id === workOwner(pull).id : can.push,
Agents as a team: lifecycle, merge queue, billing and a new shell245 agentsEnabled,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look246 computeNote,
Inbox on the site: reasons, a reason filter, Subscribe, Watch and settings247 subscription,
Merge branch 'worktree-agent-aca4790ea223aa235'248 // As the project's agents.json has them; left out, the panel fetches them.
249 agentRuns: runs?.ok ? { runs: runs.value, member: can.run } : undefined,
Agents as a team: lifecycle, merge queue, billing and a new shell250 members: members?.ok ? members.value.map((person) => person.username) : [],
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar251 // `workspace/slug` and name of each team the viewer can see.
252 teams: teamList?.ok ? teamList.value.map((team) => ({ ref: `${team.workspace}/${team.slug}`, name: team.name })) : [],
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge253 // What the rules of the branch it merges into ask, as they stack; the
254 // default branch's settings where the rules are not known.
255 requireUpToDate: found.value.rules ? found.value.rules.strict : protectedBase && settings.ok && settings.value.requireUpToDate,
256 mergeQueue: found.value.rules ? found.value.rules.merge_queue : protectedBase && settings.ok && settings.value.mergeQueue,
257 requiredApprovals: found.value.rules
258 ? found.value.rules.required_approvals
259 : protectedBase && settings.ok
260 ? settings.value.requiredApprovals
261 : 0,
262 canIgnoreChecks: found.value.rules ? found.value.rules.allow_bypass_on_merge : !settings.ok || settings.value.allowIgnoringChecks,
Fast pages, required checks on the branch, self-hosted runners, honest incidents263 noChecks,
264 // Who may open the pull request that adds CI: anyone who can push.
265 canAddCi: can.push,
266 // Who may choose the required checks.
267 canProtect: can.manage_protection,
Issues and pull requests replace intents and attempts268 defaultBranch: repo.ok ? repo.value.defaultBranch : "main",
Fast pages, required checks on the branch, self-hosted runners, honest incidents269 affects,
270 // Streamed: the conversation shows first, the preview card after.
271 ...deploymentOf(deployed?.ok ? deployed.value : null, number, params.owner, affects, viewer),
Issues and pull requests replace intents and attempts272 };
273}
274
Pull requests say where they are live; the project menu shows what is coming275/**
276 * Where this pull request is live: its preview (or its latest build, while
277 * one is going or after one failed), and the previews of the projects
278 * that use this one, built against it.
279 */
Fast pages, required checks on the branch, self-hosted runners, honest incidents280function deploymentOf(
Pull requests say where they are live; the project menu shows what is coming281 list: { deployments: Deployment[]; live: LiveApp[] } | null,
282 number: number,
283 owner: string,
284 affects: { slug: string; name: string }[],
285 viewer: Viewer,
Fast pages, required checks on the branch, self-hosted runners, honest incidents286): { preview: LiveApp | null; build: Deployment | null; stacked: Promise<Stacked[]> } {
287 const preview = list?.live.find((app) => app.kind === "preview" && app.number === number) ?? null;
288 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 coming289 const branch = preview?.branch ?? build?.branch ?? null;
Fast pages, required checks on the branch, self-hosted runners, honest incidents290 // Streamed: another lookup per project, shown in the card once known.
291 const stacked: Promise<Stacked[]> = branch
292 ? Promise.all(
293 affects.slice(0, 5).map(async (project) => {
294 const theirs = await deployments.list({ workspace: owner, slug: project.slug }, viewer).catch(() => null);
295 const app = theirs?.ok ? theirs.value.live.find((a) => a.kind === "preview" && a.branch === branch) : undefined;
296 return app ? { name: project.name, slug: project.slug, url: app.url } : null;
297 }),
298 ).then((found) => found.filter((entry) => entry != null))
299 : Promise.resolve([]);
Pull requests say where they are live; the project menu shows what is coming300 return { preview, build, stacked };
301}
302
Fast pages, required checks on the branch, self-hosted runners, honest incidents303/** A project that uses this one, with its preview built against this change. */
304type Stacked = { name: string; slug: string; url: string };
305
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look306const PULL_NEEDS: Record<string, Capability> = {
307 stack: "run",
308 "agent-review": "run",
309 "rerun-workflow": "run",
Fast pages, required checks on the branch, self-hosted runners, honest incidents310 "rerun-failed": "run",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look311 message: "run",
312 merge: "merge",
313 unqueue: "merge",
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar314 milestone: "triage",
315 base: "push",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look316};
317
Issues and pull requests replace intents and attempts318export async function action({ request, params, context }: Route.ActionArgs) {
319 assertSameOrigin(request);
320 const user = requireUser(context, request);
321 const form = await request.formData();
322 const path = { namespace: params.owner, name: params.repo };
323 const number = Number(params.number);
324 const action = form.get("action");
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look325 // What each form needs beyond reading; work checks the rest.
326 const needed = typeof action === "string" ? PULL_NEEDS[action] : undefined;
327 const refused = needed ? await refusal(context, params, needed) : null;
328 if (refused) return { error: refused, action: String(action) };
Acceptance checks in sandboxes, line comments and review verdicts329 const verdict = form.get("verdict");
330 const line = Number(form.get("line"));
Agents as a team: lifecycle, merge queue, billing and a new shell331 /** Names ticked in a people picker, plus those typed beside it. */
332 const picked = (field: string) => [
333 ...form.getAll(field).map(String),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar334 // `@acme/backend` is asked as `acme/backend`.
335 ...String(form.get("others") ?? "").split(/[\s,]+/).map((name) => name.replace(/^@/, "")),
Agents as a team: lifecycle, merge queue, billing and a new shell336 ];
Project dependencies: addresses, preview stacks, Affects, and agents who know337 // The projects that use this one, built against this pull request's preview.
338 if (action === "stack") {
339 const built = await deployments.stack(user, { workspace: params.owner, slug: params.repo }, String(form.get("branch") ?? ""));
340 return built.ok
341 ? { action, notice: `Building ${built.value.join(", ")} against this preview. They appear on their Deployments pages.` }
342 : { action, error: built.error.message };
343 }
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent344 // Asking g1t for its review records the request, then starts it.
Agents as a team: lifecycle, merge queue, billing and a new shell345 if (action === "agent-review") {
346 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-agent347 reviewers: [...form.getAll("reviewer").map(String), "g1t"],
Agents as a team: lifecycle, merge queue, billing and a new shell348 });
349 if (!asked.ok) return { error: asked.error.message, action };
350 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar351 // Its labels, milestone and base, from the sidebar's menus and the header.
352 if (action === "labels") {
353 const set = await work.setLabels(user, path, number, form.getAll("label").map(String));
354 return set.ok ? null : { error: set.error.message, action };
355 }
356 if (action === "milestone" || action === "base") {
357 const changed = await work.updatePull(
358 user,
359 path,
360 number,
361 action === "milestone"
362 ? { milestone: Number(form.get("milestone")) || 0 }
363 : { base: String(form.get("base") ?? "") },
364 );
365 return changed.ok ? null : { error: changed.error.message, action };
366 }
Catching up with main takes seconds when the two sides touched different files367 // Catching up: merged and pushed in seconds when the two sides changed
368 // different files; otherwise handed to a sandbox, which takes a minute.
369 if (action === "update") {
370 const caught = await work.catchUpPull(user, path, number);
371 if (!caught.ok) return { action, error: caught.error.message };
372 const update = caught.value;
373 if (update.outcome !== "needs_agent") {
374 return {
375 action,
376 updated: { commit: update.commit, already: update.outcome === "up_to_date", at: Date.now() },
377 };
378 }
379 const started = await env.RUNNER.update(user, path, number);
380 if (!started.ok) return { action, error: started.error.message };
381 return { action, agent: { update, startedAt: Date.now() } };
382 }
Agents and memory, checks and conflicts, profiles, slug renames, custom domains383 // A workflow run's failed jobs, run again. Who may is the actions service's call.
384 if (action === "rerun-workflow") {
385 const rerun = await actions.rerun(user, path, String(form.get("run") ?? ""), true);
386 return rerun.ok ? null : { error: rerun.error.message, action };
387 }
Fast pages, required checks on the branch, self-hosted runners, honest incidents388 // Every failed run on its head, run again: Mission control's quick action.
389 if (action === "rerun-failed") {
390 const found = await work.getPull(path, number, user);
391 if (!found.ok) return { error: found.error.message, action };
392 const failed = [
393 ...new Set(
394 (found.value.statuses ?? [])
395 .filter((status) => status.state === "failure" || status.state === "error")
396 .map(runIdOf)
397 .filter((id) => id != null),
398 ),
399 ];
400 if (failed.length === 0) return { error: "No workflow run failed on its latest commit.", action };
401 const reruns = await Promise.all(failed.map((id) => actions.rerun(user, path, id, true)));
402 const refused = reruns.find((rerun) => !rerun.ok);
403 return refused && !refused.ok ? { error: refused.error.message, action } : null;
404 }
Issues and pull requests replace intents and attempts405 const result =
406 action === "merge"
Acceptance checks in sandboxes, line comments and review verdicts407 ? await work.mergePull(user, path, number, {
408 keepIssueOpen: form.get("keepIssueOpen") === "on",
409 ignoreChecks: form.get("ignoreChecks") === "on",
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge410 bypassRules: form.get("bypassRules") === "on",
Acceptance checks in sandboxes, line comments and review verdicts411 })
Agents as a team: lifecycle, merge queue, billing and a new shell412 : action === "unqueue"
413 ? await work.removeFromQueue(user, path, number)
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request414 : action === "message"
415 ? await work.messageAgent(user, path, number, String(form.get("body") ?? ""))
Issues and pull requests replace intents and attempts416 : action === "close"
417 ? await work.closePull(user, path, number)
Fast pages, required checks on the branch, self-hosted runners, honest incidents418 : action === "agent-review"
Agents as a team: lifecycle, merge queue, billing and a new shell419 ? await env.RUNNER.review(user, path, number)
420 : action === "reviewers"
421 ? await work.updatePull(user, path, number, { reviewers: picked("reviewer") })
422 : action === "assign"
423 ? await work.updatePull(user, path, number, { assignees: picked("assignee") })
Acceptance checks in sandboxes, line comments and review verdicts424 : action === "comment"
425 ? await work.addComment(user, path, number, {
426 body: String(form.get("body") ?? ""),
427 path: String(form.get("path") ?? "") || undefined,
428 line: line > 0 ? line : undefined,
429 verdict:
430 verdict === "approve" || verdict === "request_changes" ? verdict : undefined,
431 })
432 : await work.readyPull(user, path, number, String(form.get("summary") ?? ""));
Issues and pull requests replace intents and attempts433 return result.ok ? null : { error: result.error.message, action };
434}
435
Pull requests: title and tabs across the full width, with counts and the size of the change436/** How many, beside a tab's name, when there are any. */
437function TabCount({ n }: { n: number }) {
438 if (n <= 0) return null;
439 return (
440 <span className="rounded-full bg-raised px-1.5 py-px text-xs tabular-nums text-muted ring-1 ring-line">
441 {n.toLocaleString("en-US")}
442 </span>
443 );
444}
445
Issues and pull requests replace intents and attempts446function TabLink({
447 to,
448 active,
449 children,
450}: {
451 to: string;
452 active: boolean;
453 children: React.ReactNode;
454}) {
455 return (
456 <Link
457 to={to}
458 preventScrollReset
Usage, Billing settings and prepaid AI credit; fixes from the UX audit459 aria-current={active ? "page" : undefined}
Issues and pull requests replace intents and attempts460 className={
Usage, Billing settings and prepaid AI credit; fixes from the UX audit461 "-mb-px flex items-center gap-2 border-b-2 px-3 pb-2.5 text-sm whitespace-nowrap transition-colors " +
Issues and pull requests replace intents and attempts462 (active
463 ? "border-accent font-medium text-fg"
464 : "border-transparent text-muted hover:text-fg")
465 }
466 >
467 {children}
468 </Link>
469 );
470}
471
472/** One step of the session, on the timeline's rail. */
473function Entry({ entry, agent }: { entry: SessionEntry; agent: string }) {
474 const isTool = entry.kind === "tool_call" || entry.kind === "tool_result";
475 const Icon =
476 entry.kind === "prompt"
477 ? User
478 : entry.kind === "note"
479 ? StickyNote
480 : isTool
481 ? Wrench
482 : Bot;
483 return (
484 <li className="relative pl-10">
485 <span
486 className={`absolute top-0.5 left-0 flex size-7 items-center justify-center rounded-full border bg-bg ${
487 entry.kind === "prompt"
488 ? "border-accent/50 text-accent"
489 : "border-line text-faint"
490 }`}
491 >
492 <Icon size={14} />
493 </span>
494 {isTool ? (
495 <details className="group rounded-lg border border-line bg-surface">
496 <summary className="flex cursor-pointer list-none items-center gap-2 px-3 py-1.5 text-sm">
497 <ChevronRight
498 size={14}
499 className="text-faint transition-transform group-open:rotate-90"
500 />
501 <span className="font-mono text-xs text-accent">
502 {entry.tool ?? "tool"}
503 </span>
504 <span className="truncate font-mono text-xs text-muted">
505 {entry.kind === "tool_result" ? "→ " : ""}
506 {entry.text.split("\n")[0]}
507 </span>
508 </summary>
509 <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 line510 <code>{entry.text}</code>
Issues and pull requests replace intents and attempts511 </pre>
512 </details>
513 ) : (
514 <div>
515 <p className="text-xs font-medium text-faint">
516 {entry.kind === "prompt"
517 ? "Prompt"
518 : entry.kind === "note"
519 ? "Note"
520 : agent}
521 </p>
522 {entry.kind === "message" ? (
523 <div className="mt-1">
524 <Markdown source={entry.text} />
525 </div>
Agents as a team: lifecycle, merge queue, billing and a new shell526 ) : entry.kind === "prompt" ? (
527 <div className="mt-1.5 max-h-[32rem] overflow-y-auto rounded-xl border border-line bg-surface p-4">
528 <Markdown source={entry.text} />
529 </div>
Issues and pull requests replace intents and attempts530 ) : (
Agents as a team: lifecycle, merge queue, billing and a new shell531 <p className="mt-1 text-[0.9375rem] leading-relaxed wrap-break-word whitespace-pre-wrap">
Issues and pull requests replace intents and attempts532 {entry.text}
533 </p>
534 )}
535 </div>
536 )}
537 {entry.commit && (
538 <p className="mt-1.5 flex items-center gap-1 font-mono text-xs text-faint">
539 <GitCommitHorizontal size={12} />
540 {entry.commit.slice(0, 7)}
541 </p>
542 )}
543 </li>
544 );
545}
546
Agents as a team: lifecycle, merge queue, billing and a new shell547/** The box at the foot of the conversation saying what stands before a merge. */
548function StatusBox({ children }: { children: React.ReactNode }) {
549 return (
550 <div className="flex gap-3">
551 <span className="hidden w-8 shrink-0 sm:block" />
552 <section className="min-w-0 grow divide-y divide-line overflow-hidden rounded-xl border border-line-strong bg-surface">
553 {children}
554 </section>
555 </div>
556 );
557}
558
559function StatusRow({
560 icon,
561 title,
562 children,
563}: {
564 icon: React.ReactNode;
565 title: string;
566 children?: React.ReactNode;
567}) {
568 return (
569 <div className="flex gap-3 px-4 py-3 text-sm">
570 <span className="mt-0.5 shrink-0">{icon}</span>
571 <div className="min-w-0">
572 <p className="font-medium">{title}</p>
573 {children && <div className="mt-0.5 text-muted">{children}</div>}
574 </div>
575 </div>
576 );
577}
578
Issues and pull requests replace intents and attempts579export default function PullPage({ loaderData, actionData, params }: Route.ComponentProps) {
580 const {
581 pull,
582 issue,
583 comments,
Acceptance checks in sandboxes, line comments and review verdicts584 checks,
Agents as a team: lifecycle, merge queue, billing and a new shell585 overlaps,
586 behind,
587 reviewPending,
588 lifecycle,
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request589 messages,
GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs590 statuses = [],
Fast pages, required checks on the branch, self-hosted runners, honest incidents591 requiredChecks = [],
592 noChecks,
593 canAddCi,
594 canProtect,
Agents and memory, checks and conflicts, profiles, slug renames, custom domains595 mergeable = "unknown",
596 conflicts = [],
597 earlierChecks = [],
598 workflowJobs,
Project dependencies: addresses, preview stacks, Affects, and agents who know599 affects,
600 preview,
Pull requests say where they are live; the project menu shows what is coming601 build,
602 stacked,
Agents as a team: lifecycle, merge queue, billing and a new shell603 landing,
604 stalled,
605 requireUpToDate,
606 mergeQueue,
607 requiredApprovals,
608 canIgnoreChecks,
609 canUpdate,
610 agentsEnabled,
Merge branch 'worktree-agent-aca4790ea223aa235'611 agentRuns,
Agents as a team: lifecycle, merge queue, billing and a new shell612 members,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar613 teams,
614 codeOwners,
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge615 rules,
Issues and pull requests replace intents and attempts616 tab,
617 session,
618 comparison,
619 viewer,
620 canMerge,
621 canManage,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look622 canRun,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar623 defaultBranch: repoDefault,
Issues and pull requests replace intents and attempts624 } = loaderData;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar625 // The branch it merges into: what every line below names.
626 const defaultBranch = pull.base ?? repoDefault;
Issues and pull requests replace intents and attempts627 const base = `/${params.owner}/${params.repo}`;
628 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-agent629 // g1t, on a pull request it made: the person who asked for it is a line below.
630 const opener = openedBy(pull);
Merge branch 'worktree-agent-aca4790ea223aa235'631 const checksSection = (jobs: Record<string, Job[]>, loading = false) => (
Fast pages, required checks on the branch, self-hosted runners, honest incidents632 <ChecksSection
633 run={checks}
634 required={requiredChecks}
635 statuses={statuses}
636 jobs={jobs}
637 pull={pull}
638 base={base}
639 earlier={earlierChecks}
640 canRerunWorkflows={canMerge}
641 settingsUrl={canProtect ? `${base}/settings/branches` : null}
642 error={actionData?.action === "rerun-failed" || actionData?.action === "rerun-workflow" ? actionData.error : null}
Merge branch 'worktree-agent-aca4790ea223aa235'643 loading={loading}
Fast pages, required checks on the branch, self-hosted runners, honest incidents644 />
645 );
Merge branch 'worktree-agent-aaf03bdceac799c89'646 const addresses = useAddresses();
Pull requests from branches647 const remote = pull.fork
Merge branch 'worktree-agent-aaf03bdceac799c89'648 ? cloneUrl(addresses, `${pull.fork.namespace}/${pull.fork.name}`)
649 : cloneUrl(addresses, `${params.owner}/${params.repo}`);
Issues and pull requests replace intents and attempts650 const active = pull.status === "draft" || pull.status === "open";
651
Acceptance checks in sandboxes, line comments and review verdicts652 // Follow an agent at work, or checks in progress, without a manual reload.
Issues and pull requests replace intents and attempts653 const working = pull.status === "draft";
Fast pages, required checks on the branch, self-hosted runners, honest incidents654 const checking = statuses.some((status) => status.state === "pending");
Acceptance checks in sandboxes, line comments and review verdicts655 const reviews = verdicts(comments);
Fast pages, required checks on the branch, self-hosted runners, honest incidents656 // What stands between this pull request and a merge, if anything: a
657 // required check that has not passed, or the merge queue taking it out.
658 const unchecked = requiredChecks.some((check) => check.state !== "success") || checks?.status === "failed";
659 const requiredFailed = requiredChecks.some((check) => check.state === "failure") || checks?.status === "failed";
Agents as a team: lifecycle, merge queue, billing and a new shell660 // Pull requests for other issues changing the same files will conflict;
661 // ones for the same issue are alternatives, and expected to.
662 const collisions = overlaps.filter((other) => other.issue == null || other.issue !== pull.issue);
663 const review = {
664 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 rights665 // Nobody reviews their own pull request, nor one g1t made for them.
666 canJudge: active && viewer != null && viewer.id !== workOwner(pull).id,
Agents as a team: lifecycle, merge queue, billing and a new shell667 };
668 // Everyone whose review was asked for, then anyone who reviewed unasked.
669 const reviewerNames = [
670 ...new Set([...pull.reviewers, ...reviews.map(({ reviewer }) => reviewer)]),
671 ];
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar672 const teamReviewers = pull.teamReviewers ?? [];
673 // Teams to ask: the workspace's, and any asked already.
674 const teamChoices = [
675 ...teams,
676 ...teamReviewers.filter((ref) => !teams.some((team) => team.ref === ref)).map((ref) => ({ ref, name: ref })),
677 ];
678 // Branch protection wants code owners' approval it does not have yet.
679 const ownersMissing = codeOwners?.required && codeOwners.missing ? codeOwners.missing : null;
Catching up with main takes seconds when the two sides touched different files680 // A catch-up: the click shows at once; the answer says whether it is
681 // done already or a sandbox is on it.
Merge branch 'worktree-agent-a985c01f43538af01'682 const catchUpPending = usePending({ action: "update" });
Catching up with main takes seconds when the two sides touched different files683 const catchUp = actionData?.action === "update" ? actionData : null;
684 const caughtUp = catchUp && "updated" in catchUp ? catchUp.updated : null;
685 const agentCatchUp = catchUp && "agent" in catchUp ? catchUp.agent : null;
686 // Pushed already: followed until the pull request's head is the merge,
687 // so its checks show starting again, for a minute at most.
688 const settling =
689 caughtUp != null && !caughtUp.already && pull.headCommit !== caughtUp.commit && Date.now() - caughtUp.at < 60_000;
690 // Followed until it is pushed, or for so long, never longer.
691 const catchingUp =
692 agentCatchUp != null && behind && Date.now() - agentCatchUp.startedAt < CATCH_UP_TIMEOUT_MS + REFRESH_MS;
Agents as a team: lifecycle, merge queue, billing and a new shell693 // g1t is taking a step of its own accord, so the page will change.
694 const moving =
695 lifecycle != null && lifecycle.stage !== "ready" && lifecycle.stage !== "needs_you";
Agents and memory, checks and conflicts, profiles, slug renames, custom domains696 // Whether it merges cleanly is being worked out, so the box will change.
697 const probing = active && mergeable === "checking";
698 const conflicting = active && mergeable === "conflicting";
A click away from a live page arrives: pages stop refreshing themselves while a navigation is pending699 useRefreshWhile(working || checking || reviewPending || catchingUp || settling || moving || landing || probing);
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge700 // The rules of its base it does not meet: checks show in their own
701 // section, so the rest stop the button here. Someone a ruleset lets
702 // bypass it may tick a box and merge past them.
703 const [bypassing, setBypassing] = useState(false);
704 const rulesUnmet = (rules?.unmet ?? []).filter((violation) => violation.rule !== "required_status_checks");
705 const rulesBypassable = rules?.bypassable ?? [];
Agents and memory, checks and conflicts, profiles, slug renames, custom domains706 // Why the merge button cannot be pressed, if it cannot.
707 const mergeBlocked = conflicting
708 ? "Resolve the conflicts first."
709 : probing
710 ? "Waiting to find out whether it merges cleanly."
711 : behind && requireUpToDate
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge712 ? `The rules for ${defaultBranch} require it to be up to date first.`
Fast pages, required checks on the branch, self-hosted runners, honest incidents713 : unchecked && !canIgnoreChecks
714 ? `The checks ${defaultBranch} requires have to pass first.`
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge715 : rulesUnmet.length > 0
716 ? rulesUnmet[0]!.message
717 : rulesBypassable.length > 0 && !bypassing
718 ? `Rules for ${defaultBranch} are not met. You may bypass them.`
719 : ownersMissing
720 ? "Code owners have to approve first."
721 : null;
Issues and pull requests replace intents and attempts722
723 return (
Agents as a team: lifecycle, merge queue, billing and a new shell724 // 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 change725 <div>
726 {/* The title and the tabs run the full width; what is happening on
727 the pull request sits at the top of Conversation. */}
728 <header>
Issues and pull requests replace intents and attempts729 <h2 className="text-2xl font-semibold tracking-tight text-balance">
730 {pull.title} <span className="font-normal text-faint">#{pull.number}</span>
731 </h2>
732 <div className="mt-3 flex flex-wrap items-center gap-x-3 gap-y-2 text-sm text-muted">
733 <PullState status={pull.status} />
734 <span className="flex items-center gap-2">
Agents as a team: lifecycle, merge queue, billing and a new shell735 <Avatar
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent736 name={(pull.status === "merged" && pull.mergedBy) || opener.name}
Agents as a team: lifecycle, merge queue, billing and a new shell737 size={18}
738 />
Issues and pull requests replace intents and attempts739 <span>
Agents and memory, checks and conflicts, profiles, slug renames, custom domains740 <PersonLink
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent741 name={(pull.status === "merged" && pull.mergedBy) || opener.name}
Agents and memory, checks and conflicts, profiles, slug renames, custom domains742 className="font-medium text-fg hover:underline"
743 />{" "}
Pull requests from branches744 {pull.status === "merged" ? "merged" : "wants to merge"}
745 {pull.branch && (
746 <>
747 {" "}
748 <span className="font-mono text-fg">{pull.branch}</span>
749 </>
750 )}{" "}
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar751 into{" "}
752 <BaseBranch
753 base={defaultBranch}
754 head={pull.fork ? null : pull.branch}
755 canChange={loaderData.canChangeBase && active}
756 branchesUrl={`${base}/branches.json`}
757 />
Issues and pull requests replace intents and attempts758 </span>
759 </span>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar760 {(pull.labels ?? []).map((name) => (
761 <LabelChip key={name} name={name} color={loaderData.labels.find((label) => label.name === name)?.color} />
762 ))}
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent763 {opener.requestedBy && (
764 <span className="text-xs">
765 requested by{" "}
766 <PersonLink name={opener.requestedBy} className="font-medium text-fg-soft hover:underline" />
767 </span>
768 )}
769 {/* A pull request from a branch was made by its author, not an agent;
770 one g1t made already says so. */}
771 {!pull.branch && !opener.requestedBy && (
Pull requests from branches772 <span className="flex items-center gap-1.5 font-mono text-xs">
773 <Bot size={14} />
774 {pull.agent}
775 {pull.runtime === "hosted" && <span className="text-faint">on g1t</span>}
776 </span>
777 )}
Issues and pull requests replace intents and attempts778 </div>
779
Acceptance checks in sandboxes, line comments and review verdicts780 {reviews.length > 0 && (
781 <p className="mt-3 flex flex-wrap items-center gap-x-4 gap-y-1 text-sm">
782 {reviews.map(({ reviewer, verdict }) => (
783 <span
784 key={reviewer}
785 className={`flex items-center gap-1.5 ${
Merge branch 'worktree-agent-ad7c6d88d93adc817'786 verdict === "approve" ? "text-success" : "text-danger"
Acceptance checks in sandboxes, line comments and review verdicts787 }`}
788 >
789 {verdict === "approve" ? <CircleCheck size={15} /> : <CircleSlash size={15} />}
790 {verdict === "approve" ? "Approved by" : "Changes requested by"}{" "}
791 <span className="font-medium">{reviewer}</span>
792 </span>
793 ))}
794 </p>
795 )}
796
Usage, Billing settings and prepaid AI credit; fixes from the UX audit797 <TabStrip label="Pull request" className="mt-6 items-end gap-1 border-b border-line">
Issues and pull requests replace intents and attempts798 <TabLink to={here} active={tab === "conversation"}>
799 <MessageSquare size={15} />
800 Conversation
Pull requests: title and tabs across the full width, with counts and the size of the change801 <TabCount n={comments.length} />
Issues and pull requests replace intents and attempts802 </TabLink>
803 <TabLink to={here + "?tab=session"} active={tab === "session"}>
804 <MessagesSquare size={15} />
805 Session
806 </TabLink>
807 <TabLink to={here + "?tab=changes"} active={tab === "changes"}>
808 <FileDiff size={15} />
Pull requests: title and tabs across the full width, with counts and the size of the change809 Files changed
810 <TabCount n={pull.files.length} />
Issues and pull requests replace intents and attempts811 </TabLink>
Pull requests: title and tabs across the full width, with counts and the size of the change812 {pull.files.length > 0 && (
813 <span className="ml-auto hidden pb-2.5 font-mono text-xs sm:inline">
Merge branch 'worktree-agent-ad7c6d88d93adc817'814 <span className="text-success">+{pull.files.reduce((sum, file) => sum + file.additions, 0).toLocaleString("en-US")}</span>{" "}
Pull requests: title and tabs across the full width, with counts and the size of the change815 <span className="text-danger">−{pull.files.reduce((sum, file) => sum + file.deletions, 0).toLocaleString("en-US")}</span>
816 </span>
817 )}
Usage, Billing settings and prepaid AI credit; fixes from the UX audit818 </TabStrip>
Pull requests: title and tabs across the full width, with counts and the size of the change819 </header>
820 <div className={`mt-6 grid gap-8 ${tab === "changes" ? "" : "lg:grid-cols-[1fr_19rem]"}`}>
821 <div className="min-w-0">
822 {/* What is happening on it now: its stage, its agent, what people told
823 the agent, and what else touches the same files. On the conversation,
824 so the tabs sit right under the title. */}
825 {tab === "conversation" && (
826 <div>
827 {issue && (
828 <Link
829 to={`${base}/issues/${issue.number}`}
830 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"
831 >
832 <IssueIcon issue={issue} />
833 <span className="text-muted">
834 {pull.status === "merged" && issue.resolvedBy === pull.number
835 ? "Resolved"
836 : "For issue"}
837 </span>
838 <span className="truncate font-medium">{issue.title}</span>
839 <span className="text-faint">#{issue.number}</span>
840 {issue.pullCount > 1 && (
841 <span className="ml-auto shrink-0 text-xs text-faint">
842 1 of {issue.pullCount} pull requests
Pull requests show their tabs right under the title; agent panels move into Conversation843 </span>
Pull requests: title and tabs across the full width, with counts and the size of the change844 )}
845 </Link>
846 )}
847 {pull.status === "merged" && (
848 <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">
849 <GitMerge size={16} className="shrink-0 text-merged" />
Agents as a team: lifecycle, merge queue, billing and a new shell850 <span>
Pull requests: title and tabs across the full width, with counts and the size of the change851 Merged into <span className="font-mono">{defaultBranch}</span> by{" "}
852 <span className="font-medium">{pull.mergedBy}</span>{" "}
853 {/* Not an account: the repository's settings said to merge it. */}
854 {pull.mergedBy === "g1t" && "automatically, once it was ready, "}
855 {pull.mergedAt && <TimeAgo at={pull.mergedAt} />}
856 {pull.headCommit && (
857 <span className="font-mono text-muted"> · {pull.headCommit.slice(0, 7)}</span>
Agents as a team: lifecycle, merge queue, billing and a new shell858 )}
859 </span>
Pull requests: title and tabs across the full width, with counts and the size of the change860 </p>
861 )}
862 {pull.supersededBy != null && (
863 <p className="mt-4 rounded-xl border border-line bg-surface px-4 py-2.5 text-sm text-muted">
864 Closed because{" "}
865 <Link
866 to={`${base}/pull/${pull.supersededBy}`}
867 className="font-medium text-fg hover:underline"
868 >
869 #{pull.supersededBy}
870 </Link>{" "}
871 was merged for this issue instead.
872 </p>
873 )}
874 {lifecycle && <LifecyclePanel lifecycle={lifecycle} />}
875 {/* 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 step876 <AgentPanel
877 owner={params.owner}
878 repo={params.repo}
879 number={pull.number}
880 stage={lifecycle?.stage ?? null}
881 confidence={pull.confidence ?? null}
Merge branch 'worktree-agent-aca4790ea223aa235'882 runs={agentRuns}
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step883 />
Pull requests: title and tabs across the full width, with counts and the size of the change884
885 {/* 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 look886 {canRun &&
Pull requests: title and tabs across the full width, with counts and the size of the change887 pull.runtime === "hosted" &&
888 (working || ["working", "revising", "catching_up", "answering"].includes(lifecycle?.stage ?? "")) && (
Merge branch 'worktree-agent-a985c01f43538af01'889 // Keyed by the messages so the box empties once one shows below.
Merge branch 'worktree-agent-ad7c6d88d93adc817'890 <Form method="post" className="mt-4 rounded-2xl bg-surface p-4 ring-1 ring-accent/30" key={messages.length}>
Pull requests: title and tabs across the full width, with counts and the size of the change891 <p className="flex items-center gap-2 text-sm font-medium">
Merge branch 'worktree-agent-ad7c6d88d93adc817'892 <Sparkles size={15} className="text-accent" />
Pull requests: title and tabs across the full width, with counts and the size of the change893 Message the agent
894 </p>
895 <p className="mt-1 text-xs text-muted">
896 A correction, a hint, a change of plan. It reads it at its next step, without
897 starting over.
898 </p>
899 <div className="mt-3 flex gap-2">
900 <input type="hidden" name="action" value="message" />
901 <input
902 name="body"
903 required
904 autoComplete="off"
905 data-1p-ignore
906 placeholder="Keep the old flag working too…"
Merge branch 'worktree-agent-ad7c6d88d93adc817'907 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-accent/60"
Agents as a team: lifecycle, merge queue, billing and a new shell908 />
Merge branch 'worktree-agent-a985c01f43538af01'909 <SubmitButton match={{ action: "message" }} pending="Sending…">
910 Send
911 </SubmitButton>
Pull requests: title and tabs across the full width, with counts and the size of the change912 </div>
913 </Form>
Agents as a team: lifecycle, merge queue, billing and a new shell914 )}
Pull requests: title and tabs across the full width, with counts and the size of the change915 {messages.length > 0 && (
916 <ul className="mt-3 space-y-1.5">
917 {messages.map((message) => (
918 <li key={message.id} className="flex items-start gap-2 text-sm">
919 <Avatar name={message.author} size={18} />
920 <span className="min-w-0 grow">
921 <span className="font-medium">
922 {message.fromNumber != null ? `The agent on #${message.fromNumber}` : message.author}
923 </span>{" "}
924 <span className="text-muted">
925 {message.kind === "question"
926 ? "asked:"
927 : message.kind === "handoff"
928 ? "handed over:"
929 : message.kind === "answer"
930 ? "answered:"
931 : "to the agent:"}
932 </span>{" "}
933 {message.body}
934 {message.answer && (
Merge branch 'worktree-agent-ad7c6d88d93adc817'935 <span className="mt-1 block border-l-2 border-accent/40 pl-2 text-muted">
Pull requests: title and tabs across the full width, with counts and the size of the change936 {message.declined ? "Declined: " : "Answer: "}
937 {message.answer}
938 </span>
939 )}
940 </span>
Merge branch 'worktree-agent-ad7c6d88d93adc817'941 <span className={`shrink-0 text-xs ${message.deliveredAt ? "text-success" : "text-faint"}`}>
Pull requests: title and tabs across the full width, with counts and the size of the change942 {message.deliveredAt ? "read by the agent" : "waiting for its next step"}
943 </span>
944 </li>
945 ))}
946 </ul>
947 )}
948 {collisions.length > 0 && active && (
949 <div className="mt-4 rounded-xl border border-line bg-surface px-4 py-3 text-sm">
950 <p className="flex items-center gap-2.5 font-medium">
951 <Radar size={16} className="shrink-0 text-info" />
952 Other work is changing the same files
953 </p>
954 <ul className="mt-2 space-y-1.5">
955 {collisions.map((other) => (
956 <li key={other.number} className="flex flex-wrap items-baseline gap-x-2 text-muted">
957 <Link
958 to={`${base}/pull/${other.number}`}
959 className="font-medium text-fg hover:underline"
960 >
961 {other.title} <span className="font-normal text-faint">#{other.number}</span>
962 </Link>
963 <span className="font-mono text-xs">{other.paths.join(", ")}</span>
964 </li>
965 ))}
966 </ul>
967 <p className="mt-2 text-xs text-faint">
968 Whichever merges second will have to catch up, and may conflict.
969 </p>
970 </div>
971 )}
972 </div>
973 )}
974 <div className="mt-5">
975 {comparison ? (
976 <DiffView
977 comparison={comparison}
978 review={{
979 comments: comments.filter((comment) => comment.path),
980 canComment: Boolean(viewer),
981 }}
982 />
983 ) : tab === "session" ? (
984 session.length === 0 ? (
985 <EmptyState title="Nothing recorded yet">
986 The agent's prompts, reasoning and tool calls appear here as it
987 works.
988 </EmptyState>
989 ) : (
990 <ol className="relative space-y-5 before:absolute before:top-2 before:bottom-2 before:left-3.25 before:w-px before:bg-line">
991 {session.map((entry) => (
992 <Entry key={entry.seq} entry={entry} agent={pull.agent} />
993 ))}
994 </ol>
995 )
996 ) : (
997 <div className="space-y-4">
998 <TimelineItem
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent999 author={opener.name}
Pull requests: title and tabs across the full width, with counts and the size of the change1000 at={pull.createdAt}
1001 action={
1002 <span>
1003 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-agent1004 {opener.requestedBy ? (
Pull requests: title and tabs across the full width, with counts and the size of the change1005 <>
1006 {" "}
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent1007 for{" "}
1008 <PersonLink name={opener.requestedBy} className="font-medium text-fg-soft hover:underline" />
1009 </>
1010 ) : !pull.branch && (
1011 <>
1012 {" "}
Pull requests: title and tabs across the full width, with counts and the size of the change1013 with <span className="font-mono text-xs">{pull.agent}</span>
1014 </>
1015 )}
1016 </span>
1017 }
1018 >
1019 {pull.body ? (
1020 <Markdown source={pull.body} repo={{ namespace: params.owner, name: params.repo }} />
1021 ) : (
1022 <p className="text-sm text-muted">
1023 {working
1024 ? "No description yet. It is written when the pull request is marked ready for review."
1025 : "No description."}
1026 </p>
1027 )}
1028 {canManage && pull.status === "open" && (
1029 <details className="mt-3 border-t border-line pt-3 text-sm">
1030 <summary className="cursor-pointer text-xs text-faint hover:text-fg">
1031 Edit description
1032 </summary>
1033 <Form method="post" className="mt-3 space-y-2">
Merge branch 'worktree-agent-a985c01f43538af01'1034 <input type="hidden" name="action" value="describe" />
Pull requests: title and tabs across the full width, with counts and the size of the change1035 <Textarea
1036 name="summary"
1037 rows={6}
1038 placeholder="What changed and why"
1039 defaultValue={pull.body ?? ""}
1040 />
Merge branch 'worktree-agent-a985c01f43538af01'1041 <SubmitButton match={{ action: "describe" }} pending="Saving…">
1042 Save
1043 </SubmitButton>
Pull requests: title and tabs across the full width, with counts and the size of the change1044 </Form>
1045 </details>
1046 )}
1047 </TimelineItem>
Agents as a team: lifecycle, merge queue, billing and a new shell1048
Pull requests: title and tabs across the full width, with counts and the size of the change1049 <CommentList comments={comments} review={review} base={base} />
Agents as a team: lifecycle, merge queue, billing and a new shell1050
Merge branch 'worktree-agent-aca4790ea223aa235'1051 {(preview || build) && <DeploymentCard preview={preview} build={build} stacked={stacked} stacking={affects.length > 0} base={base} />}
Pull requests say where they are live; the project menu shows what is coming1052
Pull requests: title and tabs across the full width, with counts and the size of the change1053 {pull.status === "draft" && (
1054 <StatusBox>
1055 <StatusRow icon={<Loader size={16} className="text-faint" />} title="This is a draft">
1056 It is still being worked on. It can be reviewed and merged once it is
1057 marked ready.
1058 </StatusRow>
1059 {canManage && (
1060 <Form method="post" className="space-y-2 px-4 py-3">
Merge branch 'worktree-agent-a985c01f43538af01'1061 <input type="hidden" name="action" value="ready" />
Pull requests: title and tabs across the full width, with counts and the size of the change1062 <Textarea name="summary" rows={3} placeholder="What changed and why" />
Merge branch 'worktree-agent-a985c01f43538af01'1063 <SubmitButton match={{ action: "ready" }} pending="Marking ready…">
1064 Mark ready for review
1065 </SubmitButton>
Pull requests: title and tabs across the full width, with counts and the size of the change1066 </Form>
1067 )}
1068 </StatusBox>
1069 )}
Agents as a team: lifecycle, merge queue, billing and a new shell1070
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1071 {codeOwners && active && (
1072 <div className="flex gap-3">
1073 <span className="hidden w-8 shrink-0 sm:block" />
1074 <div className="min-w-0 grow">
1075 <PullCodeOwnersPanel
1076 owners={codeOwners}
1077 base={base}
1078 branch={defaultBranch}
1079 errorsHref={canProtect ? `${base}/settings/branches#codeowners` : `${base}/blob/${encodeURIComponent(defaultBranch)}/${codeOwners.path}`}
1080 />
1081 </div>
1082 </div>
1083 )}
1084
Pull requests: title and tabs across the full width, with counts and the size of the change1085 {pull.status === "open" && (
1086 <StatusBox>
Fast pages, required checks on the branch, self-hosted runners, honest incidents1087 {/* Checks by status at once; job by job when the runs are read. */}
Merge branch 'worktree-agent-aca4790ea223aa235'1088 <Suspense fallback={checksSection({}, true)}>
1089 <Await resolve={workflowJobs}>{(jobs) => checksSection(jobs)}</Await>
Fast pages, required checks on the branch, self-hosted runners, honest incidents1090 </Suspense>
1091 {noChecks && <AddCiPrompt owner={params.owner} repo={params.repo} canAdd={canAddCi} compact />}
Agents as a team: lifecycle, merge queue, billing and a new shell1092 <StatusRow
Pull requests: title and tabs across the full width, with counts and the size of the change1093 icon={
1094 reviews.some(({ verdict }) => verdict === "request_changes") ? (
1095 <CircleSlash size={16} className="text-danger" />
1096 ) : reviews.length > 0 ? (
Merge branch 'worktree-agent-ad7c6d88d93adc817'1097 <CircleCheck size={16} className="text-success" />
Pull requests: title and tabs across the full width, with counts and the size of the change1098 ) : (
1099 <MessageSquare size={16} className="text-faint" />
1100 )
1101 }
1102 title={
1103 reviews.length === 0
1104 ? "No reviews yet"
1105 : reviews
1106 .map(
1107 ({ reviewer, verdict }) =>
1108 `${verdict === "approve" ? "Approved by" : "Changes requested by"} ${reviewer}`,
1109 )
1110 .join(" · ")
1111 }
Agents as a team: lifecycle, merge queue, billing and a new shell1112 >
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent1113 {reviewPending && "g1t is reviewing it now. "}
Pull requests: title and tabs across the full width, with counts and the size of the change1114 {requiredApprovals > 0 &&
1115 `This repository requires ${requiredApprovals} approving ${
1116 requiredApprovals === 1 ? "review" : "reviews"
1117 } before merging.`}
Agents as a team: lifecycle, merge queue, billing and a new shell1118 </StatusRow>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1119 {codeOwners?.required && (
1120 <StatusRow
1121 icon={
1122 ownersMissing ? (
1123 <CircleSlash size={16} className="text-warn" />
1124 ) : (
Merge branch 'worktree-agent-ad7c6d88d93adc817'1125 <CircleCheck size={16} className="text-success" />
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1126 )
1127 }
1128 title={ownersMissing ? "Waiting for code owners" : "Code owners approved"}
1129 >
1130 {ownersMissing ?? `Every file it changes has its owners' approval.`}{" "}
1131 {ownersMissing && "This repository requires their review before merging."}
1132 </StatusRow>
1133 )}
Pull requests: title and tabs across the full width, with counts and the size of the change1134 {landing ? (
Agents as a team: lifecycle, merge queue, billing and a new shell1135 <StatusRow
Pull requests: title and tabs across the full width, with counts and the size of the change1136 icon={<Loader size={16} className="animate-spin text-accent" />}
1137 title="Merging"
1138 >
1139 {defaultBranch} has moved, so g1t is bringing this up to date first. It
1140 lands as soon as that is done. Watch it in the Session tab.
1141 </StatusRow>
Catching up with main takes seconds when the two sides touched different files1142 ) : agentCatchUp && behind ? (
1143 <CatchUpProgress
1144 owner={params.owner}
1145 repo={params.repo}
1146 number={pull.number}
1147 defaultBranch={defaultBranch}
1148 behind={behind}
1149 update={agentCatchUp.update}
1150 startedAt={agentCatchUp.startedAt}
1151 retrying={catchUpPending}
Merge branch 'worktree-agent-aca4790ea223aa235'1152 runs={agentRuns}
Catching up with main takes seconds when the two sides touched different files1153 />
Pull requests: title and tabs across the full width, with counts and the size of the change1154 ) : conflicting ? (
1155 <ConflictsSection
1156 conflicts={conflicts}
1157 pull={pull}
1158 owner={params.owner}
1159 repo={params.repo}
1160 defaultBranch={defaultBranch}
1161 changesUrl={here + "?tab=changes"}
1162 canResolve={canUpdate && agentsEnabled}
Catching up with main takes seconds when the two sides touched different files1163 resolving={catchUpPending}
1164 error={catchUp && "error" in catchUp ? catchUp.error : null}
Agents as a team: lifecycle, merge queue, billing and a new shell1165 />
Pull requests: title and tabs across the full width, with counts and the size of the change1166 ) : probing ? (
1167 <MergeabilityRow mergeable={mergeable} defaultBranch={defaultBranch} />
1168 ) : behind ? (
1169 <StatusRow
1170 icon={<GitPullRequestArrow size={16} className="text-info" />}
1171 title={`${defaultBranch} has moved since this was made`}
1172 >
1173 {mergeable === "clean" && "It has no conflicts with it. "}
1174 {requireUpToDate
1175 ? "This repository requires pull requests to be up to date, so it has to catch up before it can merge."
1176 : "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 files1177 {canUpdate && (
1178 <Form method="post" className="mt-2">
Pull requests: title and tabs across the full width, with counts and the size of the change1179 <input type="hidden" name="action" value="update" />
Merge branch 'worktree-agent-a985c01f43538af01'1180 <SubmitButton variant="quiet" match={{ action: "update" }} pending={`Merging ${defaultBranch} in…`}>
1181 Catch up with {defaultBranch} now
1182 </SubmitButton>
Pull requests: title and tabs across the full width, with counts and the size of the change1183 </Form>
1184 )}
Catching up with main takes seconds when the two sides touched different files1185 {catchUp && "error" in catchUp && <ErrorText>{catchUp.error}</ErrorText>}
Pull requests: title and tabs across the full width, with counts and the size of the change1186 </StatusRow>
1187 ) : (
Merge branch 'worktree-agent-af9f0f386d52b6a73'1188 (pull.headCommit || (catchUp && "error" in catchUp)) && (
Pull requests: title and tabs across the full width, with counts and the size of the change1189 <StatusRow
Merge branch 'worktree-agent-ad7c6d88d93adc817'1190 icon={<CircleCheck size={16} className="text-success" />}
Catching up with main takes seconds when the two sides touched different files1191 title={
1192 (caughtUp && !caughtUp.already) || agentCatchUp
1193 ? `Brought up to date with ${defaultBranch}`
1194 : `Up to date with ${defaultBranch}`
1195 }
1196 >
1197 {caughtUp && !caughtUp.already && (
1198 <>
1199 Merged <span className="font-mono">{defaultBranch}</span> in as{" "}
1200 <span className="font-mono text-fg">{caughtUp.commit.slice(0, 7)}</span>. Its checks run
1201 again on the new commit.
1202 </>
1203 )}
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent1204 {agentCatchUp && "g1t merged it in and pushed the result. Its checks run again on the new commit."}
Merge branch 'worktree-agent-af9f0f386d52b6a73'1205 {/* A catch-up refused or failed after the pull request stopped being behind. */}
1206 {catchUp && "error" in catchUp && <ErrorText>{catchUp.error}</ErrorText>}
Catching up with main takes seconds when the two sides touched different files1207 </StatusRow>
Pull requests: title and tabs across the full width, with counts and the size of the change1208 )
1209 )}
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge1210 {active && rules && (rulesUnmet.length > 0 || rulesBypassable.length > 0 || rules.evaluate.length > 0) && (
1211 <StatusRow
1212 icon={
1213 <Scale
1214 size={16}
1215 className={rulesUnmet.length > 0 ? "text-danger" : rulesBypassable.length > 0 ? "text-warn" : "text-info"}
1216 />
1217 }
1218 title={
1219 rulesUnmet.length > 0
1220 ? `Rules for ${defaultBranch} are not met yet`
1221 : rulesBypassable.length > 0
1222 ? `You may bypass the rules for ${defaultBranch}`
1223 : "Rulesets being evaluated would refuse this merge"
1224 }
1225 >
1226 <div className="mt-2 space-y-3">
1227 {rulesUnmet.length > 0 && <ViolationList violations={rulesUnmet} />}
1228 {rulesBypassable.length > 0 && (
1229 <div>
1230 <p className="mb-1.5 text-xs text-muted">Not met, but a ruleset lets you bypass it:</p>
1231 <ViolationList violations={rulesBypassable} tone="warn" />
1232 </div>
1233 )}
1234 {rules.evaluate.length > 0 && (
1235 <div>
1236 <p className="mb-1.5 text-xs text-muted">Rulesets in evaluate would refuse it for this; nothing is held up:</p>
1237 <ViolationList violations={rules.evaluate} tone="info" />
1238 </div>
1239 )}
1240 {canProtect && (
1241 <Link
1242 to={`${base}/settings/rules?branch=${encodeURIComponent(defaultBranch)}`}
1243 className="inline-block text-xs text-muted underline underline-offset-2 hover:text-fg"
1244 >
1245 See every rule for {defaultBranch}
1246 </Link>
1247 )}
1248 </div>
1249 </StatusRow>
1250 )}
Pull requests: title and tabs across the full width, with counts and the size of the change1251 {stalled && !lifecycle && (
1252 <StatusRow icon={<Hand size={16} className="text-warn" />} title="Needs you">
1253 {stalled}
1254 </StatusRow>
1255 )}
1256 {canMerge && lifecycle?.stage === "queued" && (
1257 <Form method="post" className="flex flex-wrap items-center gap-3 px-4 py-3">
1258 <ButtonLink to={`${base}/queue`}>
1259 <Layers size={15} />
1260 See the queue
1261 </ButtonLink>
Merge branch 'worktree-agent-a985c01f43538af01'1262 <SubmitButton variant="quiet" name="action" value="unqueue" pending="Removing…">
Pull requests: title and tabs across the full width, with counts and the size of the change1263 Remove from the queue
Merge branch 'worktree-agent-a985c01f43538af01'1264 </SubmitButton>
Pull requests: title and tabs across the full width, with counts and the size of the change1265 </Form>
1266 )}
1267 {canMerge && !landing && lifecycle?.stage !== "queued" && (
1268 <Form method="post" className="space-y-3 px-4 py-3">
1269 {issue?.state === "open" && (
1270 <CheckboxOption
1271 name="keepIssueOpen"
1272 label={`Keep #${issue.number} open. This is only part of the work.`}
1273 labelClassName="text-xs text-muted"
1274 />
1275 )}
1276 {unchecked && canIgnoreChecks && (
1277 <CheckboxOption
1278 name="ignoreChecks"
Fast pages, required checks on the branch, self-hosted runners, honest incidents1279 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 change1280 labelClassName="text-xs text-muted"
1281 />
1282 )}
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge1283 {rulesBypassable.length > 0 && rulesUnmet.length === 0 && (
1284 <CheckboxOption
1285 name="bypassRules"
1286 checked={bypassing}
1287 onCheckedChange={(checked) => setBypassing(checked === true)}
1288 label={`Bypass the rules for ${defaultBranch}: merge although they are not met. It is recorded.`}
1289 labelClassName="text-xs text-muted"
1290 />
1291 )}
Pull requests: title and tabs across the full width, with counts and the size of the change1292 <div className="flex flex-wrap items-center gap-3">
Merge branch 'worktree-agent-ae1299e92e4462012'1293 <Hint label={mergeBlocked} disabled={mergeBlocked != null}>
1294 <SubmitButton
1295 variant="accent"
1296 name="action"
1297 value="merge"
1298 disabled={mergeBlocked != null}
1299 pending={mergeQueue ? "Adding to the queue…" : "Merging…"}
1300 >
1301 {mergeQueue ? <Layers size={15} /> : <GitMerge size={15} />}
1302 {mergeQueue ? "Add to the merge queue" : `Merge into ${defaultBranch}`}
1303 </SubmitButton>
1304 </Hint>
Pull requests: title and tabs across the full width, with counts and the size of the change1305 <span className={`text-xs ${mergeBlocked ? "text-danger" : "text-muted"}`}>
1306 {mergeBlocked ?? (mergeQueue
1307 ? `Tested together with everything ahead of it, then lands on ${defaultBranch}.`
1308 : issue?.state === "open"
1309 ? `Closes issue #${issue.number}, and any other pull requests still open for it.`
1310 : `Lands its commits on ${defaultBranch}.`)}
1311 </span>
1312 </div>
1313 {actionData?.action === "merge" && (
1314 <ErrorText>{actionData.error}</ErrorText>
1315 )}
1316 </Form>
1317 )}
1318 </StatusBox>
1319 )}
Agents as a team: lifecycle, merge queue, billing and a new shell1320
Pull requests: title and tabs across the full width, with counts and the size of the change1321 <CommentForm
1322 author={viewer?.username ?? null}
1323 resetKey={comments.length}
1324 review={review}
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1325 handles={teams.map((team) => `@${team.ref}`)}
Pull requests: title and tabs across the full width, with counts and the size of the change1326 />
1327 {canManage && active && (
1328 <Form method="post" className="flex justify-end">
Merge branch 'worktree-agent-a985c01f43538af01'1329 <SubmitButton variant="quiet" name="action" value="close" pending="Closing…">
Pull requests: title and tabs across the full width, with counts and the size of the change1330 Close pull request
Merge branch 'worktree-agent-a985c01f43538af01'1331 </SubmitButton>
Pull requests: title and tabs across the full width, with counts and the size of the change1332 </Form>
1333 )}
1334 {actionData &&
Merge branch 'worktree-agent-a985c01f43538af01'1335 !["merge", "comment", "stack", "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 change1336 String(actionData.action),
1337 ) && <ErrorText>{actionData.error}</ErrorText>}
1338 </div>
1339 )}
1340 {actionData?.action === "comment" && (
1341 <div className="mt-2">
1342 <ErrorText>{actionData.error}</ErrorText>
1343 </div>
1344 )}
1345 </div>
1346 </div>
1347
1348 <aside className={tab === "changes" ? "hidden" : "space-y-6"}>
1349 {/* An open pull request shows its checks in full in the merge box. */}
Fast pages, required checks on the branch, self-hosted runners, honest incidents1350 {pull.status !== "open" && <WorkflowStatuses statuses={statuses} />}
Pull requests: title and tabs across the full width, with counts and the size of the change1351 {affects.length > 0 && (
1352 <section>
1353 <h3 className="flex items-center gap-1.5 text-sm font-medium">
1354 <Network size={14} className="text-faint" />
1355 Affects
1356 </h3>
1357 <p className="mt-1 text-xs text-muted">Projects that use this one, and so may feel this change:</p>
1358 <ul className="mt-2 space-y-1 text-sm">
1359 {affects.map((project) => (
1360 <li key={project.slug} className="flex items-center justify-between gap-2">
1361 <Link to={`/${params.owner}/${project.slug}`} className="hover:underline">
1362 {project.name}
1363 </Link>
1364 {project.as && <code className="font-mono text-xs text-faint">{project.as}</code>}
1365 </li>
1366 ))}
1367 </ul>
1368 {preview?.branch && canMerge && (
1369 <Form method="post" className="mt-3">
1370 <input type="hidden" name="action" value="stack" />
1371 <input type="hidden" name="branch" value={preview.branch} />
Merge branch 'worktree-agent-a985c01f43538af01'1372 <SubmitButton
1373 match={{ action: "stack" }}
1374 pending="Starting the builds…"
1375 className="flex w-full items-center justify-center gap-1.5 rounded-md border border-line px-3 py-1.5 text-xs text-muted transition-colors hover:border-line-strong hover:text-fg disabled:opacity-50"
Pull requests: title and tabs across the full width, with counts and the size of the change1376 >
1377 Preview them against this change
Merge branch 'worktree-agent-a985c01f43538af01'1378 </SubmitButton>
Agents as a team: lifecycle, merge queue, billing and a new shell1379 </Form>
1380 )}
Pull requests: title and tabs across the full width, with counts and the size of the change1381 {actionData?.action === "stack" &&
1382 ("notice" in actionData ? (
Merge branch 'worktree-agent-ad7c6d88d93adc817'1383 <p className="mt-2 text-xs text-success">{String(actionData.notice)}</p>
Pull requests: title and tabs across the full width, with counts and the size of the change1384 ) : (
1385 <ErrorText>{actionData.error}</ErrorText>
1386 ))}
1387 </section>
Acceptance checks in sandboxes, line comments and review verdicts1388 )}
Issues and pull requests replace intents and attempts1389
Project dependencies: addresses, preview stacks, Affects, and agents who know1390 <section>
Pull requests: title and tabs across the full width, with counts and the size of the change1391 <h3 className="text-sm font-medium">Reviewers</h3>
1392 <ul className="mt-2 space-y-1.5 text-sm">
1393 {reviewerNames.map((name) => {
1394 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-agent1395 const pending = name === "g1t" && reviewPending;
Pull requests: title and tabs across the full width, with counts and the size of the change1396 return (
1397 <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-agent1398 {name === "g1t" ? (
Pull requests: title and tabs across the full width, with counts and the size of the change1399 <Sparkles size={16} className="shrink-0 text-accent" />
1400 ) : (
1401 <Avatar name={name} size={20} />
1402 )}
1403 <span className="grow truncate font-mono text-xs">{name}</span>
1404 {pending ? (
1405 <span className="flex items-center gap-1.5 text-xs text-muted">
1406 <span className="size-1.5 animate-pulse rounded-full bg-accent" />
1407 Reviewing
1408 </span>
1409 ) : verdict === "approve" ? (
Merge branch 'worktree-agent-ad7c6d88d93adc817'1410 <span className="flex items-center gap-1 text-xs text-success">
Pull requests: title and tabs across the full width, with counts and the size of the change1411 <CircleCheck size={13} /> Approved
1412 </span>
1413 ) : verdict === "request_changes" ? (
1414 <span className="flex items-center gap-1 text-xs text-danger">
1415 <CircleSlash size={13} /> Changes requested
1416 </span>
1417 ) : (
1418 <span className="text-xs text-faint">Review requested</span>
1419 )}
1420 </li>
1421 );
1422 })}
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1423 {teamReviewers.map((team) => (
1424 <TeamReviewer key={team} team={team} />
1425 ))}
1426 {reviewerNames.length === 0 && teamReviewers.length === 0 && (
Pull requests: title and tabs across the full width, with counts and the size of the change1427 <li className="px-1 text-xs text-faint">No reviews requested yet.</li>
1428 )}
Project dependencies: addresses, preview stacks, Affects, and agents who know1429 </ul>
Pull requests: title and tabs across the full width, with counts and the size of the change1430 {pull.status === "open" && canManage && (
1431 <div className="mt-3 space-y-2">
1432 {agentsEnabled && !reviewPending && (
1433 <Form method="post">
1434 <input type="hidden" name="action" value="agent-review" />
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1435 {[...pull.reviewers, ...teamReviewers].map((name) => (
Pull requests: title and tabs across the full width, with counts and the size of the change1436 <input key={name} type="hidden" name="reviewer" value={name} />
1437 ))}
1438 <div className="*:w-full">
Merge branch 'worktree-agent-a985c01f43538af01'1439 <SubmitButton variant="quiet" match={{ action: "agent-review" }} pending="Asking g1t…">
Pull requests: title and tabs across the full width, with counts and the size of the change1440 <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-agent1441 Request review from g1t
Merge branch 'worktree-agent-a985c01f43538af01'1442 </SubmitButton>
Pull requests: title and tabs across the full width, with counts and the size of the change1443 </div>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1444 <ComputeNote note={loaderData.computeNote} />
Pull requests: title and tabs across the full width, with counts and the size of the change1445 </Form>
1446 )}
1447 <details>
1448 <summary className="cursor-pointer text-xs text-faint hover:text-fg">
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1449 {teamChoices.length > 0 ? "Request review from people or teams" : "Request review from people"}
Pull requests: title and tabs across the full width, with counts and the size of the change1450 </summary>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1451 <Form method="post" className="mt-2 space-y-2" key={[...pull.reviewers, ...teamReviewers].join()}>
Pull requests: title and tabs across the full width, with counts and the size of the change1452 <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-agent1453 {pull.reviewers.includes("g1t") && (
1454 <input type="hidden" name="reviewer" value="g1t" />
Pull requests: title and tabs across the full width, with counts and the size of the change1455 )}
1456 <PeoplePicker
1457 name="reviewer"
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights1458 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-agent1459 chosen={pull.reviewers.filter((name) => name !== "g1t")}
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1460 placeholder="Other usernames or teams (acme/backend)"
Pull requests: title and tabs across the full width, with counts and the size of the change1461 />
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1462 {teamChoices.length > 0 && (
1463 <div className="space-y-1.5 border-t border-line pt-2">
1464 <p className="text-xs text-faint">Teams</p>
1465 {teamChoices.map((team) => (
1466 <CheckboxOption
1467 key={team.ref}
1468 name="reviewer"
1469 value={team.ref}
1470 defaultChecked={teamReviewers.includes(team.ref)}
1471 className="items-center"
1472 labelClassName="flex min-w-0 items-center gap-2"
1473 label={
1474 <>
1475 <Users size={14} className="shrink-0 text-faint" />
1476 <span className="truncate font-mono text-xs">@{team.ref}</span>
1477 </>
1478 }
1479 />
1480 ))}
1481 </div>
1482 )}
Merge branch 'worktree-agent-a985c01f43538af01'1483 <SubmitButton variant="quiet" match={{ action: "reviewers" }} pending="Saving…">
Pull requests: title and tabs across the full width, with counts and the size of the change1484 Save reviewers
Merge branch 'worktree-agent-a985c01f43538af01'1485 </SubmitButton>
Pull requests: title and tabs across the full width, with counts and the size of the change1486 </Form>
1487 </details>
1488 </div>
Project dependencies: addresses, preview stacks, Affects, and agents who know1489 )}
Pull requests: title and tabs across the full width, with counts and the size of the change1490 {["agent-review", "reviewers"].includes(String(actionData?.action)) && (
1491 <ErrorText>{actionData?.error}</ErrorText>
1492 )}
Project dependencies: addresses, preview stacks, Affects, and agents who know1493 </section>
Acceptance checks in sandboxes, line comments and review verdicts1494
Pull requests: title and tabs across the full width, with counts and the size of the change1495 <section>
1496 <h3 className="text-sm font-medium">Assignees</h3>
1497 <ul className="mt-2 space-y-1.5 text-sm">
1498 {pull.assignees.map((name) => (
Agents as a team: lifecycle, merge queue, billing and a new shell1499 <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 change1500 <Avatar name={name} size={20} />
Agents as a team: lifecycle, merge queue, billing and a new shell1501 <span className="grow truncate font-mono text-xs">{name}</span>
1502 </li>
Pull requests: title and tabs across the full width, with counts and the size of the change1503 ))}
1504 {pull.assignees.length === 0 && (
1505 <li className="px-1 text-xs text-faint">No one yet.</li>
1506 )}
1507 </ul>
1508 {viewer && canManage && active && (
1509 <div className="mt-3 space-y-2">
1510 {!pull.assignees.includes(viewer.username) && (
1511 <Form method="post">
1512 <input type="hidden" name="action" value="assign" />
1513 {pull.assignees.map((name) => (
1514 <input key={name} type="hidden" name="assignee" value={name} />
1515 ))}
1516 <input type="hidden" name="assignee" value={viewer.username} />
1517 <div className="*:w-full">
Merge branch 'worktree-agent-a985c01f43538af01'1518 <SubmitButton variant="quiet" name="who" value="self" pending="Assigning…">
Pull requests: title and tabs across the full width, with counts and the size of the change1519 Assign yourself
Merge branch 'worktree-agent-a985c01f43538af01'1520 </SubmitButton>
Pull requests: title and tabs across the full width, with counts and the size of the change1521 </div>
1522 </Form>
1523 )}
1524 <details>
1525 <summary className="cursor-pointer text-xs text-faint hover:text-fg">
1526 Assign people
1527 </summary>
1528 <Form method="post" className="mt-2 space-y-2" key={pull.assignees.join()}>
1529 <input type="hidden" name="action" value="assign" />
1530 <PeoplePicker name="assignee" members={members} chosen={pull.assignees} />
Merge branch 'worktree-agent-a985c01f43538af01'1531 <SubmitButton variant="quiet" name="who" value="picked" pending="Saving…">
Pull requests: title and tabs across the full width, with counts and the size of the change1532 Save assignees
Merge branch 'worktree-agent-a985c01f43538af01'1533 </SubmitButton>
Pull requests: title and tabs across the full width, with counts and the size of the change1534 </Form>
1535 </details>
1536 </div>
Issues and pull requests replace intents and attempts1537 )}
Pull requests: title and tabs across the full width, with counts and the size of the change1538 {actionData?.action === "assign" && <ErrorText>{actionData.error}</ErrorText>}
1539 </section>
Issues and pull requests replace intents and attempts1540
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1541 <LabelsBox
1542 labels={loaderData.labels}
1543 chosen={pull.labels ?? []}
1544 canEdit={canManage && active}
Merge membership: owners, org roles, GitHub's repo roles, privileges, 2FA1545 canCreate={loaderData.canMakeLabels}
1546 manageUrl={loaderData.canMakeLabels ? `${base}/labels` : undefined}
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1547 />
1548 <MilestoneBox
1549 milestones={loaderData.milestones}
1550 current={pull.milestone}
1551 canEdit={loaderData.canTriage}
1552 base={base}
1553 />
1554
Pull requests: title and tabs across the full width, with counts and the size of the change1555 <section>
1556 <h3 className="text-sm font-medium">Working copy</h3>
1557 {pull.branch ? (
1558 <>
1559 <p className="mt-1 text-xs text-muted">
1560 <GitBranch size={13} className="mr-1 inline align-[-2px]" />
1561 Branch <span className="font-mono text-fg">{pull.branch}</span> of this
1562 repository. Pushes to it show up here.
1563 </p>
1564 <div className="mt-2">
1565 <CopyLine text={`git clone -b ${pull.branch} ${remote}`} />
1566 </div>
1567 </>
1568 ) : (
1569 <>
1570 <p className="mt-1 text-xs text-muted">
1571 This fork belongs to the pull request. Pushes to it show up here.
1572 </p>
1573 <div className="mt-2">
1574 <CopyLine text={`git clone ${remote}`} />
1575 </div>
1576 </>
1577 )}
1578 <p className="mt-3 flex items-center gap-1.5 font-mono text-xs text-faint">
1579 <GitCommitHorizontal size={13} />
Merge checks: statuses and check runs on every commit1580 {pull.headCommit ? (
1581 <Link to={`${base}/commit/${pull.headCommit}`} className="hover:text-fg">
1582 {pull.headCommit.slice(0, 12)}
1583 </Link>
1584 ) : (
1585 "no commits pushed yet"
1586 )}
1587 {pull.headCommit && <CommitChecksBadge checks={loaderData.headChecks} sha={pull.headCommit} className="size-5" />}
Pull requests: title and tabs across the full width, with counts and the size of the change1588 </p>
1589 </section>
Inbox on the site: reasons, a reason filter, Subscribe, Watch and settings1590 {loaderData.viewer && (
1591 <SubscriptionBox action={`${base}/notifications`} number={pull.number} kind="pull" subscription={loaderData.subscription} />
1592 )}
Pull requests: title and tabs across the full width, with counts and the size of the change1593 </aside>
1594 </div>
Issues and pull requests replace intents and attempts1595 </div>
1596 );
1597}
Pull requests say where they are live; the project menu shows what is coming1598
1599/** Where the pull request's change is live, as GitHub shows a deployment. */
1600function DeploymentCard({
1601 preview,
1602 build,
1603 stacked,
Merge branch 'worktree-agent-aca4790ea223aa235'1604 stacking,
Pull requests say where they are live; the project menu shows what is coming1605 base,
1606}: {
1607 preview: LiveApp | null;
1608 build: Deployment | null;
Fast pages, required checks on the branch, self-hosted runners, honest incidents1609 stacked: Promise<Stacked[]>;
Merge branch 'worktree-agent-aca4790ea223aa235'1610 /** Projects use this one, so previews built against it may be on their way. */
1611 stacking: boolean;
Pull requests say where they are live; the project menu shows what is coming1612 base: string;
1613}) {
1614 const building = build?.status === "queued" || build?.status === "building";
1615 const failed = build?.status === "failed";
1616 return (
1617 <section className="ml-12 overflow-hidden rounded-xl border border-line bg-surface">
1618 <div className="flex flex-wrap items-center gap-3 px-4 py-3">
1619 <span
1620 className={`flex size-8 shrink-0 items-center justify-center rounded-full ring-1 ${
Merge branch 'worktree-agent-ad7c6d88d93adc817'1621 failed ? "text-danger ring-danger/40" : building ? "text-warn ring-warn/40" : "text-success ring-success/40"
Pull requests say where they are live; the project menu shows what is coming1622 }`}
1623 >
1624 {building ? <Loader size={15} className="animate-spin" /> : <Rocket size={15} />}
1625 </span>
1626 <div className="min-w-0 grow">
1627 <p className="text-sm font-medium">
1628 {building
1629 ? preview
1630 ? "Deploying the latest push. The last preview is still live."
1631 : "Deploying a preview"
1632 : failed
1633 ? "The preview failed to deploy"
1634 : "This branch is live"}
1635 </p>
1636 {preview ? (
1637 <a href={preview.url} className="mt-0.5 block truncate font-mono text-xs text-muted hover:text-accent">
1638 {preview.url.replace(/^https?:[/][/]/, "")}
1639 </a>
1640 ) : (
1641 build?.error && <p className="mt-0.5 truncate text-xs text-muted">{build.error}</p>
1642 )}
1643 </div>
1644 <div className="flex shrink-0 items-center gap-2">
1645 {build && (
1646 <Link to={`${base}/deployments/${build.id}`} className="text-xs text-muted hover:text-fg">
1647 {failed ? "See why" : "Build log"}
1648 </Link>
1649 )}
1650 {preview && (
1651 <a
1652 href={preview.url}
1653 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"
1654 >
1655 Visit preview
1656 <ArrowUpRight size={13} />
1657 </a>
1658 )}
1659 </div>
1660 </div>
1661 {preview && (
1662 <p className="border-t border-line px-4 py-2 text-xs text-faint">
1663 <span className="font-mono">{preview.commit.slice(0, 7)}</span> · deployed <TimeAgo at={preview.deployedAt} />
1664 {preview.branch && ` · from ${preview.branch}`}
1665 </p>
1666 )}
Fast pages, required checks on the branch, self-hosted runners, honest incidents1667 {/* Read after the page: the card's own part shows at once. */}
Merge branch 'worktree-agent-aca4790ea223aa235'1668 <Suspense
1669 fallback={
1670 stacking && (
1671 <Loading className="border-t border-line px-4 py-2.5 text-xs">
1672 <SkeletonLine className="w-40" />
1673 <SkeletonLine className="mt-1.5 w-64 max-w-full" />
1674 </Loading>
1675 )
1676 }
1677 >
Fast pages, required checks on the branch, self-hosted runners, honest incidents1678 <Await resolve={stacked}>
1679 {(stacked) =>
1680 stacked.length > 0 && (
Merge branch 'worktree-agent-aca4790ea223aa235'1681 <div className="animate-fade-in border-t border-line px-4 py-2.5">
Fast pages, required checks on the branch, self-hosted runners, honest incidents1682 <p className="text-xs text-muted">Built against this change:</p>
1683 <ul className="mt-1.5 space-y-1">
1684 {stacked.map((entry) => (
1685 <li key={entry.slug} className="flex items-center gap-2 text-xs">
1686 <span className="font-medium">{entry.name}</span>
1687 <a href={entry.url} className="truncate font-mono text-muted hover:text-accent">
1688 {entry.url.replace(/^https?:[/][/]/, "")}
1689 </a>
1690 </li>
1691 ))}
1692 </ul>
1693 </div>
1694 )
1695 }
1696 </Await>
1697 </Suspense>
Pull requests say where they are live; the project menu shows what is coming1698 </section>
1699 );
1700}

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