Skip to content

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

1,687 lines76,820 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";
Issues and pull requests replace intents and attempts89import { assertSameOrigin, getViewer, requireUser } from "../../lib/session.server";
Inbox on the site: reasons, a reason filter, Subscribe, Watch and settings90import { accessTo, refusal, repoFor } from "../../lib/access.server";
91import { SubscriptionBox } from "../../components/notifications";
A click away from a live page arrives: pages stop refreshing themselves while a navigation is pending92import { REFRESH_MS, useRefreshWhile } from "../../lib/refresh";
Issues and pull requests replace intents and attempts93
94const EMPTY_COMPARISON: Comparison = { base: null, head: "", files: [], truncated: false };
95const TABS = ["conversation", "session", "changes"] as const;
96type Tab = (typeof TABS)[number];
97
Social cards for every page: og.g1t.sh98export function meta({ loaderData, params, ...args }: Route.MetaArgs) {
99 const pull = loaderData?.pull;
100 const title = pull ? `${pull.title} · Pull request #${pull.number} · ` : "";
101 const state = { draft: "Draft", open: "Open", merged: "Merged", closed: "Closed" }[pull?.status ?? "open"];
102 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-agent103 const opener = pull ? openedBy(pull) : null;
Social cards for every page: og.g1t.sh104 return page(args, {
105 title: `${title}${params.owner}/${params.repo} · g1t`,
106 description: pull
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent107 ? `${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.sh108 : null,
109 // The card shows the title and the state.
110 version: pull ? [pull.title, pull.status] : undefined,
111 type: "article",
112 });
Issues and pull requests replace intents and attempts113}
114
115export async function loader({ params, context, request }: Route.LoaderArgs) {
116 const viewer = getViewer(context);
117 const path = { namespace: params.owner, name: params.repo };
118 const number = Number(params.number);
119 const asked = new URL(request.url).searchParams.get("tab");
120 const tab: Tab = TABS.find((name) => name === asked) ?? "conversation";
121
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look122 // Members of the workspace pick reviewers and assignees from its people;
123 // what the viewer may do here goes by their role on the repository.
Agents as a team: lifecycle, merge queue, billing and a new shell124 const member = (viewer?.workspaces ?? []).some(
125 (membership) => membership.slug === params.owner,
126 );
Fast pages, required checks on the branch, self-hosted runners, honest incidents127 // Everything starts at once. Only what the viewer's role decides waits
128 // for the repository, which the layout is looking up in this request too
129 // (lib/access.server.ts), and only what the pull request decides waits
130 // for it.
Project dependencies: addresses, preview stacks, Affects, and agents who know131 const ref = { workspace: params.owner, slug: params.repo };
Fast pages, required checks on the branch, self-hosted runners, honest incidents132 const access = accessTo(context, params);
133 const pullFound = work.getPull(path, number, viewer);
134 const deps = projects.dependencies(params.owner, params.repo, viewer);
135 // Awaited below, unless the pull request is missing first.
136 deps.catch(() => null);
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar137 // Its labels and milestone, with the repository's to choose from.
138 const labelsFound = work.listLabels(path, viewer);
139 const milestonesFound = work.listMilestones(path, viewer);
140 labelsFound.catch(() => null);
141 milestonesFound.catch(() => null);
142 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 incidents143 access,
144 pullFound,
Issues and pull requests replace intents and attempts145 repos.get(path, viewer),
Agents as a team: lifecycle, merge queue, billing and a new shell146 work.getSettings(path, viewer),
g1t's agents only for listed workspaces, whatever the state of billing147 env.RUNNER.enabled(viewer, path),
Agents as a team: lifecycle, merge queue, billing and a new shell148 // A member picks reviewers and assignees from the workspace's people.
149 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-agent150 // Before someone asks g1t for something: whether the plan lets it start.
Fast pages, required checks on the branch, self-hosted runners, honest incidents151 access.then(({ can }) => (can.run ? computeNoteFor(params.owner, "agent") : null)),
152 tab === "session" ? work.readSession(path, number, viewer) : null,
153 // Its preview, for people with a role here: beside the rest, not after.
154 access.then(({ insider }) => (insider ? deployments.list(ref, viewer).catch(() => null) : null)),
Inbox on the site: reasons, a reason filter, Subscribe, Watch and settings155 // Whether the viewer hears of it, for the sidebar's Notifications.
156 viewer
157 ? repoFor(context, params).then((found) =>
158 found.ok ? inbox.subscription(viewer, { repoId: found.value.id, number }).catch(() => null) : null,
159 )
160 : null,
Merge branch 'worktree-agent-aca4790ea223aa235'161 // Its agent's latest runs, for the Agent panel: with the page, not after it.
162 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 bar163 // A member asks the workspace's teams to review, and mentions them.
164 member ? identity.listTeams(viewer, params.owner).catch(() => null) : null,
Issues and pull requests replace intents and attempts165 ]);
166 if (!found.ok) {
167 // Issues and pull requests share numbers; this one may be an issue.
168 const issue = await work.getIssue(path, number, viewer);
169 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 put170 throw notFound("pull");
Issues and pull requests replace intents and attempts171 }
172 const { pull } = found.value;
Pull requests from branches173 const range = pullComparison(pull);
Agents and memory, checks and conflicts, profiles, slug renames, custom domains174 // The jobs of each workflow run on its head, to list checks job by job.
175 // 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 incidents176 // Streamed: the checks show by status first, then job by job.
Agents and memory, checks and conflicts, profiles, slug renames, custom domains177 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 incidents178 const workflowJobs =
179 tab === "conversation" && pull.status === "open" && runIds.length > 0
180 ? Promise.all(runIds.map((id) => actions.run(path, viewer, id).catch(() => null))).then((runs) => {
181 const jobs: Record<string, Job[]> = {};
182 for (const run of runs) if (run?.ok) jobs[run.value.run.id] = run.value.jobs;
183 return jobs;
184 })
185 .catch(() => ({}) as Record<string, Job[]>)
186 : Promise.resolve({} as Record<string, Job[]>);
187 // No checks at all on an open pull request: whether that is because the
188 // repository has no workflows, to offer a starter one.
189 const unchecked =
190 tab === "conversation" &&
191 pull.status === "open" &&
192 (found.value.statuses ?? []).length === 0 &&
193 (found.value.requiredChecks ?? []).length === 0;
194 const [comparison, used, noChecks] = await Promise.all([
Usage, Billing settings and prepaid AI credit; fixes from the UX audit195 // The first screens of it highlighted here, so their colours do not pop in.
196 tab === "changes"
197 ? repos
198 .compare(range.repoId, viewer, range.base, range.head, range.baseBranch)
199 .then(async (found) => (found.ok ? { ok: true as const, value: await highlightFirstFiles(found.value) } : found))
200 : null,
Fast pages, required checks on the branch, self-hosted runners, honest incidents201 deps,
202 unchecked
203 ? actions
204 .workflows(path, viewer)
205 .then((found) => found.ok && found.value.length === 0)
206 .catch(() => false)
207 : false,
Issues and pull requests replace intents and attempts208 ]);
Fast pages, required checks on the branch, self-hosted runners, honest incidents209 const affects = used.ok ? used.value.usedBy : [];
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar210 const [labels, milestones] = await Promise.all([labelsFound, milestonesFound]);
211 const repoDefault = repo.ok ? repo.value.defaultBranch : "main";
212 // The default branch's protection covers pull requests into it only.
213 const protectedBase = (pull.base ?? repoDefault) === repoDefault;
Issues and pull requests replace intents and attempts214 return {
215 ...found.value,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar216 labels: labels.ok ? labels.value : [],
217 milestones: milestones.ok ? milestones.value : [],
218 // Labelling and milestones: Triage and up; its author, its labels.
219 canTriage: can.triage,
220 // The branch it merges into: Write and up.
221 canChangeBase: can.push,
Agents and memory, checks and conflicts, profiles, slug renames, custom domains222 workflowJobs,
Issues and pull requests replace intents and attempts223 tab,
224 session: session?.ok ? session.value : [],
225 // An empty comparison if it could not be made.
226 comparison: comparison && (comparison.ok ? comparison.value : EMPTY_COMPARISON),
227 viewer,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look228 // Write and up can merge.
229 canMerge: can.merge,
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights230 // Triage and up manage anyone's pull request; its author, their own,
231 // and whoever asked g1t for one, that one.
232 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 look233 // Telling its agent things, and re-running checks, spend compute: Write and up.
234 canRun: can.run,
Agents as a team: lifecycle, merge queue, billing and a new shell235 // 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 rights236 // whoever it is for (who asked g1t for it, or its author), a branch
237 // from anyone who can push.
238 canUpdate: pull.fork ? viewer?.id === workOwner(pull).id : can.push,
Agents as a team: lifecycle, merge queue, billing and a new shell239 agentsEnabled,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look240 computeNote,
Inbox on the site: reasons, a reason filter, Subscribe, Watch and settings241 subscription,
Merge branch 'worktree-agent-aca4790ea223aa235'242 // As the project's agents.json has them; left out, the panel fetches them.
243 agentRuns: runs?.ok ? { runs: runs.value, member: can.run } : undefined,
Agents as a team: lifecycle, merge queue, billing and a new shell244 members: members?.ok ? members.value.map((person) => person.username) : [],
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar245 // `workspace/slug` and name of each team the viewer can see.
246 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 merge247 // What the rules of the branch it merges into ask, as they stack; the
248 // default branch's settings where the rules are not known.
249 requireUpToDate: found.value.rules ? found.value.rules.strict : protectedBase && settings.ok && settings.value.requireUpToDate,
250 mergeQueue: found.value.rules ? found.value.rules.merge_queue : protectedBase && settings.ok && settings.value.mergeQueue,
251 requiredApprovals: found.value.rules
252 ? found.value.rules.required_approvals
253 : protectedBase && settings.ok
254 ? settings.value.requiredApprovals
255 : 0,
256 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 incidents257 noChecks,
258 // Who may open the pull request that adds CI: anyone who can push.
259 canAddCi: can.push,
260 // Who may choose the required checks.
261 canProtect: can.manage_protection,
Issues and pull requests replace intents and attempts262 defaultBranch: repo.ok ? repo.value.defaultBranch : "main",
Fast pages, required checks on the branch, self-hosted runners, honest incidents263 affects,
264 // Streamed: the conversation shows first, the preview card after.
265 ...deploymentOf(deployed?.ok ? deployed.value : null, number, params.owner, affects, viewer),
Issues and pull requests replace intents and attempts266 };
267}
268
Pull requests say where they are live; the project menu shows what is coming269/**
270 * Where this pull request is live: its preview (or its latest build, while
271 * one is going or after one failed), and the previews of the projects
272 * that use this one, built against it.
273 */
Fast pages, required checks on the branch, self-hosted runners, honest incidents274function deploymentOf(
Pull requests say where they are live; the project menu shows what is coming275 list: { deployments: Deployment[]; live: LiveApp[] } | null,
276 number: number,
277 owner: string,
278 affects: { slug: string; name: string }[],
279 viewer: Viewer,
Fast pages, required checks on the branch, self-hosted runners, honest incidents280): { preview: LiveApp | null; build: Deployment | null; stacked: Promise<Stacked[]> } {
281 const preview = list?.live.find((app) => app.kind === "preview" && app.number === number) ?? null;
282 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 coming283 const branch = preview?.branch ?? build?.branch ?? null;
Fast pages, required checks on the branch, self-hosted runners, honest incidents284 // Streamed: another lookup per project, shown in the card once known.
285 const stacked: Promise<Stacked[]> = branch
286 ? Promise.all(
287 affects.slice(0, 5).map(async (project) => {
288 const theirs = await deployments.list({ workspace: owner, slug: project.slug }, viewer).catch(() => null);
289 const app = theirs?.ok ? theirs.value.live.find((a) => a.kind === "preview" && a.branch === branch) : undefined;
290 return app ? { name: project.name, slug: project.slug, url: app.url } : null;
291 }),
292 ).then((found) => found.filter((entry) => entry != null))
293 : Promise.resolve([]);
Pull requests say where they are live; the project menu shows what is coming294 return { preview, build, stacked };
295}
296
Fast pages, required checks on the branch, self-hosted runners, honest incidents297/** A project that uses this one, with its preview built against this change. */
298type Stacked = { name: string; slug: string; url: string };
299
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look300const PULL_NEEDS: Record<string, Capability> = {
301 stack: "run",
302 "agent-review": "run",
303 "rerun-workflow": "run",
Fast pages, required checks on the branch, self-hosted runners, honest incidents304 "rerun-failed": "run",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look305 message: "run",
306 merge: "merge",
307 unqueue: "merge",
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar308 milestone: "triage",
309 base: "push",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look310};
311
Issues and pull requests replace intents and attempts312export async function action({ request, params, context }: Route.ActionArgs) {
313 assertSameOrigin(request);
314 const user = requireUser(context, request);
315 const form = await request.formData();
316 const path = { namespace: params.owner, name: params.repo };
317 const number = Number(params.number);
318 const action = form.get("action");
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look319 // What each form needs beyond reading; work checks the rest.
320 const needed = typeof action === "string" ? PULL_NEEDS[action] : undefined;
321 const refused = needed ? await refusal(context, params, needed) : null;
322 if (refused) return { error: refused, action: String(action) };
Acceptance checks in sandboxes, line comments and review verdicts323 const verdict = form.get("verdict");
324 const line = Number(form.get("line"));
Agents as a team: lifecycle, merge queue, billing and a new shell325 /** Names ticked in a people picker, plus those typed beside it. */
326 const picked = (field: string) => [
327 ...form.getAll(field).map(String),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar328 // `@acme/backend` is asked as `acme/backend`.
329 ...String(form.get("others") ?? "").split(/[\s,]+/).map((name) => name.replace(/^@/, "")),
Agents as a team: lifecycle, merge queue, billing and a new shell330 ];
Project dependencies: addresses, preview stacks, Affects, and agents who know331 // The projects that use this one, built against this pull request's preview.
332 if (action === "stack") {
333 const built = await deployments.stack(user, { workspace: params.owner, slug: params.repo }, String(form.get("branch") ?? ""));
334 return built.ok
335 ? { action, notice: `Building ${built.value.join(", ")} against this preview. They appear on their Deployments pages.` }
336 : { action, error: built.error.message };
337 }
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent338 // Asking g1t for its review records the request, then starts it.
Agents as a team: lifecycle, merge queue, billing and a new shell339 if (action === "agent-review") {
340 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-agent341 reviewers: [...form.getAll("reviewer").map(String), "g1t"],
Agents as a team: lifecycle, merge queue, billing and a new shell342 });
343 if (!asked.ok) return { error: asked.error.message, action };
344 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar345 // Its labels, milestone and base, from the sidebar's menus and the header.
346 if (action === "labels") {
347 const set = await work.setLabels(user, path, number, form.getAll("label").map(String));
348 return set.ok ? null : { error: set.error.message, action };
349 }
350 if (action === "milestone" || action === "base") {
351 const changed = await work.updatePull(
352 user,
353 path,
354 number,
355 action === "milestone"
356 ? { milestone: Number(form.get("milestone")) || 0 }
357 : { base: String(form.get("base") ?? "") },
358 );
359 return changed.ok ? null : { error: changed.error.message, action };
360 }
Catching up with main takes seconds when the two sides touched different files361 // Catching up: merged and pushed in seconds when the two sides changed
362 // different files; otherwise handed to a sandbox, which takes a minute.
363 if (action === "update") {
364 const caught = await work.catchUpPull(user, path, number);
365 if (!caught.ok) return { action, error: caught.error.message };
366 const update = caught.value;
367 if (update.outcome !== "needs_agent") {
368 return {
369 action,
370 updated: { commit: update.commit, already: update.outcome === "up_to_date", at: Date.now() },
371 };
372 }
373 const started = await env.RUNNER.update(user, path, number);
374 if (!started.ok) return { action, error: started.error.message };
375 return { action, agent: { update, startedAt: Date.now() } };
376 }
Agents and memory, checks and conflicts, profiles, slug renames, custom domains377 // A workflow run's failed jobs, run again. Who may is the actions service's call.
378 if (action === "rerun-workflow") {
379 const rerun = await actions.rerun(user, path, String(form.get("run") ?? ""), true);
380 return rerun.ok ? null : { error: rerun.error.message, action };
381 }
Fast pages, required checks on the branch, self-hosted runners, honest incidents382 // Every failed run on its head, run again: Mission control's quick action.
383 if (action === "rerun-failed") {
384 const found = await work.getPull(path, number, user);
385 if (!found.ok) return { error: found.error.message, action };
386 const failed = [
387 ...new Set(
388 (found.value.statuses ?? [])
389 .filter((status) => status.state === "failure" || status.state === "error")
390 .map(runIdOf)
391 .filter((id) => id != null),
392 ),
393 ];
394 if (failed.length === 0) return { error: "No workflow run failed on its latest commit.", action };
395 const reruns = await Promise.all(failed.map((id) => actions.rerun(user, path, id, true)));
396 const refused = reruns.find((rerun) => !rerun.ok);
397 return refused && !refused.ok ? { error: refused.error.message, action } : null;
398 }
Issues and pull requests replace intents and attempts399 const result =
400 action === "merge"
Acceptance checks in sandboxes, line comments and review verdicts401 ? await work.mergePull(user, path, number, {
402 keepIssueOpen: form.get("keepIssueOpen") === "on",
403 ignoreChecks: form.get("ignoreChecks") === "on",
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge404 bypassRules: form.get("bypassRules") === "on",
Acceptance checks in sandboxes, line comments and review verdicts405 })
Agents as a team: lifecycle, merge queue, billing and a new shell406 : action === "unqueue"
407 ? await work.removeFromQueue(user, path, number)
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request408 : action === "message"
409 ? await work.messageAgent(user, path, number, String(form.get("body") ?? ""))
Issues and pull requests replace intents and attempts410 : action === "close"
411 ? await work.closePull(user, path, number)
Fast pages, required checks on the branch, self-hosted runners, honest incidents412 : action === "agent-review"
Agents as a team: lifecycle, merge queue, billing and a new shell413 ? await env.RUNNER.review(user, path, number)
414 : action === "reviewers"
415 ? await work.updatePull(user, path, number, { reviewers: picked("reviewer") })
416 : action === "assign"
417 ? await work.updatePull(user, path, number, { assignees: picked("assignee") })
Acceptance checks in sandboxes, line comments and review verdicts418 : action === "comment"
419 ? await work.addComment(user, path, number, {
420 body: String(form.get("body") ?? ""),
421 path: String(form.get("path") ?? "") || undefined,
422 line: line > 0 ? line : undefined,
423 verdict:
424 verdict === "approve" || verdict === "request_changes" ? verdict : undefined,
425 })
426 : await work.readyPull(user, path, number, String(form.get("summary") ?? ""));
Issues and pull requests replace intents and attempts427 return result.ok ? null : { error: result.error.message, action };
428}
429
Pull requests: title and tabs across the full width, with counts and the size of the change430/** How many, beside a tab's name, when there are any. */
431function TabCount({ n }: { n: number }) {
432 if (n <= 0) return null;
433 return (
434 <span className="rounded-full bg-raised px-1.5 py-px text-xs tabular-nums text-muted ring-1 ring-line">
435 {n.toLocaleString("en-US")}
436 </span>
437 );
438}
439
Issues and pull requests replace intents and attempts440function TabLink({
441 to,
442 active,
443 children,
444}: {
445 to: string;
446 active: boolean;
447 children: React.ReactNode;
448}) {
449 return (
450 <Link
451 to={to}
452 preventScrollReset
Usage, Billing settings and prepaid AI credit; fixes from the UX audit453 aria-current={active ? "page" : undefined}
Issues and pull requests replace intents and attempts454 className={
Usage, Billing settings and prepaid AI credit; fixes from the UX audit455 "-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 attempts456 (active
457 ? "border-accent font-medium text-fg"
458 : "border-transparent text-muted hover:text-fg")
459 }
460 >
461 {children}
462 </Link>
463 );
464}
465
466/** One step of the session, on the timeline's rail. */
467function Entry({ entry, agent }: { entry: SessionEntry; agent: string }) {
468 const isTool = entry.kind === "tool_call" || entry.kind === "tool_result";
469 const Icon =
470 entry.kind === "prompt"
471 ? User
472 : entry.kind === "note"
473 ? StickyNote
474 : isTool
475 ? Wrench
476 : Bot;
477 return (
478 <li className="relative pl-10">
479 <span
480 className={`absolute top-0.5 left-0 flex size-7 items-center justify-center rounded-full border bg-bg ${
481 entry.kind === "prompt"
482 ? "border-accent/50 text-accent"
483 : "border-line text-faint"
484 }`}
485 >
486 <Icon size={14} />
487 </span>
488 {isTool ? (
489 <details className="group rounded-lg border border-line bg-surface">
490 <summary className="flex cursor-pointer list-none items-center gap-2 px-3 py-1.5 text-sm">
491 <ChevronRight
492 size={14}
493 className="text-faint transition-transform group-open:rotate-90"
494 />
495 <span className="font-mono text-xs text-accent">
496 {entry.tool ?? "tool"}
497 </span>
498 <span className="truncate font-mono text-xs text-muted">
499 {entry.kind === "tool_result" ? "→ " : ""}
500 {entry.text.split("\n")[0]}
501 </span>
502 </summary>
503 <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 line504 <code>{entry.text}</code>
Issues and pull requests replace intents and attempts505 </pre>
506 </details>
507 ) : (
508 <div>
509 <p className="text-xs font-medium text-faint">
510 {entry.kind === "prompt"
511 ? "Prompt"
512 : entry.kind === "note"
513 ? "Note"
514 : agent}
515 </p>
516 {entry.kind === "message" ? (
517 <div className="mt-1">
518 <Markdown source={entry.text} />
519 </div>
Agents as a team: lifecycle, merge queue, billing and a new shell520 ) : entry.kind === "prompt" ? (
521 <div className="mt-1.5 max-h-[32rem] overflow-y-auto rounded-xl border border-line bg-surface p-4">
522 <Markdown source={entry.text} />
523 </div>
Issues and pull requests replace intents and attempts524 ) : (
Agents as a team: lifecycle, merge queue, billing and a new shell525 <p className="mt-1 text-[0.9375rem] leading-relaxed wrap-break-word whitespace-pre-wrap">
Issues and pull requests replace intents and attempts526 {entry.text}
527 </p>
528 )}
529 </div>
530 )}
531 {entry.commit && (
532 <p className="mt-1.5 flex items-center gap-1 font-mono text-xs text-faint">
533 <GitCommitHorizontal size={12} />
534 {entry.commit.slice(0, 7)}
535 </p>
536 )}
537 </li>
538 );
539}
540
Agents as a team: lifecycle, merge queue, billing and a new shell541/** The box at the foot of the conversation saying what stands before a merge. */
542function StatusBox({ children }: { children: React.ReactNode }) {
543 return (
544 <div className="flex gap-3">
545 <span className="hidden w-8 shrink-0 sm:block" />
546 <section className="min-w-0 grow divide-y divide-line overflow-hidden rounded-xl border border-line-strong bg-surface">
547 {children}
548 </section>
549 </div>
550 );
551}
552
553function StatusRow({
554 icon,
555 title,
556 children,
557}: {
558 icon: React.ReactNode;
559 title: string;
560 children?: React.ReactNode;
561}) {
562 return (
563 <div className="flex gap-3 px-4 py-3 text-sm">
564 <span className="mt-0.5 shrink-0">{icon}</span>
565 <div className="min-w-0">
566 <p className="font-medium">{title}</p>
567 {children && <div className="mt-0.5 text-muted">{children}</div>}
568 </div>
569 </div>
570 );
571}
572
Issues and pull requests replace intents and attempts573export default function PullPage({ loaderData, actionData, params }: Route.ComponentProps) {
574 const {
575 pull,
576 issue,
577 comments,
Acceptance checks in sandboxes, line comments and review verdicts578 checks,
Agents as a team: lifecycle, merge queue, billing and a new shell579 overlaps,
580 behind,
581 reviewPending,
582 lifecycle,
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request583 messages,
GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs584 statuses = [],
Fast pages, required checks on the branch, self-hosted runners, honest incidents585 requiredChecks = [],
586 noChecks,
587 canAddCi,
588 canProtect,
Agents and memory, checks and conflicts, profiles, slug renames, custom domains589 mergeable = "unknown",
590 conflicts = [],
591 earlierChecks = [],
592 workflowJobs,
Project dependencies: addresses, preview stacks, Affects, and agents who know593 affects,
594 preview,
Pull requests say where they are live; the project menu shows what is coming595 build,
596 stacked,
Agents as a team: lifecycle, merge queue, billing and a new shell597 landing,
598 stalled,
599 requireUpToDate,
600 mergeQueue,
601 requiredApprovals,
602 canIgnoreChecks,
603 canUpdate,
604 agentsEnabled,
Merge branch 'worktree-agent-aca4790ea223aa235'605 agentRuns,
Agents as a team: lifecycle, merge queue, billing and a new shell606 members,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar607 teams,
608 codeOwners,
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge609 rules,
Issues and pull requests replace intents and attempts610 tab,
611 session,
612 comparison,
613 viewer,
614 canMerge,
615 canManage,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look616 canRun,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar617 defaultBranch: repoDefault,
Issues and pull requests replace intents and attempts618 } = loaderData;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar619 // The branch it merges into: what every line below names.
620 const defaultBranch = pull.base ?? repoDefault;
Issues and pull requests replace intents and attempts621 const base = `/${params.owner}/${params.repo}`;
622 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-agent623 // g1t, on a pull request it made: the person who asked for it is a line below.
624 const opener = openedBy(pull);
Merge branch 'worktree-agent-aca4790ea223aa235'625 const checksSection = (jobs: Record<string, Job[]>, loading = false) => (
Fast pages, required checks on the branch, self-hosted runners, honest incidents626 <ChecksSection
627 run={checks}
628 required={requiredChecks}
629 statuses={statuses}
630 jobs={jobs}
631 pull={pull}
632 base={base}
633 earlier={earlierChecks}
634 canRerunWorkflows={canMerge}
635 settingsUrl={canProtect ? `${base}/settings/branches` : null}
636 error={actionData?.action === "rerun-failed" || actionData?.action === "rerun-workflow" ? actionData.error : null}
Merge branch 'worktree-agent-aca4790ea223aa235'637 loading={loading}
Fast pages, required checks on the branch, self-hosted runners, honest incidents638 />
639 );
Merge branch 'worktree-agent-aaf03bdceac799c89'640 const addresses = useAddresses();
Pull requests from branches641 const remote = pull.fork
Merge branch 'worktree-agent-aaf03bdceac799c89'642 ? cloneUrl(addresses, `${pull.fork.namespace}/${pull.fork.name}`)
643 : cloneUrl(addresses, `${params.owner}/${params.repo}`);
Issues and pull requests replace intents and attempts644 const active = pull.status === "draft" || pull.status === "open";
645
Acceptance checks in sandboxes, line comments and review verdicts646 // Follow an agent at work, or checks in progress, without a manual reload.
Issues and pull requests replace intents and attempts647 const working = pull.status === "draft";
Fast pages, required checks on the branch, self-hosted runners, honest incidents648 const checking = statuses.some((status) => status.state === "pending");
Acceptance checks in sandboxes, line comments and review verdicts649 const reviews = verdicts(comments);
Fast pages, required checks on the branch, self-hosted runners, honest incidents650 // What stands between this pull request and a merge, if anything: a
651 // required check that has not passed, or the merge queue taking it out.
652 const unchecked = requiredChecks.some((check) => check.state !== "success") || checks?.status === "failed";
653 const requiredFailed = requiredChecks.some((check) => check.state === "failure") || checks?.status === "failed";
Agents as a team: lifecycle, merge queue, billing and a new shell654 // Pull requests for other issues changing the same files will conflict;
655 // ones for the same issue are alternatives, and expected to.
656 const collisions = overlaps.filter((other) => other.issue == null || other.issue !== pull.issue);
657 const review = {
658 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 rights659 // Nobody reviews their own pull request, nor one g1t made for them.
660 canJudge: active && viewer != null && viewer.id !== workOwner(pull).id,
Agents as a team: lifecycle, merge queue, billing and a new shell661 };
662 // Everyone whose review was asked for, then anyone who reviewed unasked.
663 const reviewerNames = [
664 ...new Set([...pull.reviewers, ...reviews.map(({ reviewer }) => reviewer)]),
665 ];
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar666 const teamReviewers = pull.teamReviewers ?? [];
667 // Teams to ask: the workspace's, and any asked already.
668 const teamChoices = [
669 ...teams,
670 ...teamReviewers.filter((ref) => !teams.some((team) => team.ref === ref)).map((ref) => ({ ref, name: ref })),
671 ];
672 // Branch protection wants code owners' approval it does not have yet.
673 const ownersMissing = codeOwners?.required && codeOwners.missing ? codeOwners.missing : null;
Catching up with main takes seconds when the two sides touched different files674 // A catch-up: the click shows at once; the answer says whether it is
675 // done already or a sandbox is on it.
Merge branch 'worktree-agent-a985c01f43538af01'676 const catchUpPending = usePending({ action: "update" });
Catching up with main takes seconds when the two sides touched different files677 const catchUp = actionData?.action === "update" ? actionData : null;
678 const caughtUp = catchUp && "updated" in catchUp ? catchUp.updated : null;
679 const agentCatchUp = catchUp && "agent" in catchUp ? catchUp.agent : null;
680 // Pushed already: followed until the pull request's head is the merge,
681 // so its checks show starting again, for a minute at most.
682 const settling =
683 caughtUp != null && !caughtUp.already && pull.headCommit !== caughtUp.commit && Date.now() - caughtUp.at < 60_000;
684 // Followed until it is pushed, or for so long, never longer.
685 const catchingUp =
686 agentCatchUp != null && behind && Date.now() - agentCatchUp.startedAt < CATCH_UP_TIMEOUT_MS + REFRESH_MS;
Agents as a team: lifecycle, merge queue, billing and a new shell687 // g1t is taking a step of its own accord, so the page will change.
688 const moving =
689 lifecycle != null && lifecycle.stage !== "ready" && lifecycle.stage !== "needs_you";
Agents and memory, checks and conflicts, profiles, slug renames, custom domains690 // Whether it merges cleanly is being worked out, so the box will change.
691 const probing = active && mergeable === "checking";
692 const conflicting = active && mergeable === "conflicting";
A click away from a live page arrives: pages stop refreshing themselves while a navigation is pending693 useRefreshWhile(working || checking || reviewPending || catchingUp || settling || moving || landing || probing);
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge694 // The rules of its base it does not meet: checks show in their own
695 // section, so the rest stop the button here. Someone a ruleset lets
696 // bypass it may tick a box and merge past them.
697 const [bypassing, setBypassing] = useState(false);
698 const rulesUnmet = (rules?.unmet ?? []).filter((violation) => violation.rule !== "required_status_checks");
699 const rulesBypassable = rules?.bypassable ?? [];
Agents and memory, checks and conflicts, profiles, slug renames, custom domains700 // Why the merge button cannot be pressed, if it cannot.
701 const mergeBlocked = conflicting
702 ? "Resolve the conflicts first."
703 : probing
704 ? "Waiting to find out whether it merges cleanly."
705 : behind && requireUpToDate
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge706 ? `The rules for ${defaultBranch} require it to be up to date first.`
Fast pages, required checks on the branch, self-hosted runners, honest incidents707 : unchecked && !canIgnoreChecks
708 ? `The checks ${defaultBranch} requires have to pass first.`
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge709 : rulesUnmet.length > 0
710 ? rulesUnmet[0]!.message
711 : rulesBypassable.length > 0 && !bypassing
712 ? `Rules for ${defaultBranch} are not met. You may bypass them.`
713 : ownersMissing
714 ? "Code owners have to approve first."
715 : null;
Issues and pull requests replace intents and attempts716
717 return (
Agents as a team: lifecycle, merge queue, billing and a new shell718 // 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 change719 <div>
720 {/* The title and the tabs run the full width; what is happening on
721 the pull request sits at the top of Conversation. */}
722 <header>
Issues and pull requests replace intents and attempts723 <h2 className="text-2xl font-semibold tracking-tight text-balance">
724 {pull.title} <span className="font-normal text-faint">#{pull.number}</span>
725 </h2>
726 <div className="mt-3 flex flex-wrap items-center gap-x-3 gap-y-2 text-sm text-muted">
727 <PullState status={pull.status} />
728 <span className="flex items-center gap-2">
Agents as a team: lifecycle, merge queue, billing and a new shell729 <Avatar
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent730 name={(pull.status === "merged" && pull.mergedBy) || opener.name}
Agents as a team: lifecycle, merge queue, billing and a new shell731 size={18}
732 />
Issues and pull requests replace intents and attempts733 <span>
Agents and memory, checks and conflicts, profiles, slug renames, custom domains734 <PersonLink
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent735 name={(pull.status === "merged" && pull.mergedBy) || opener.name}
Agents and memory, checks and conflicts, profiles, slug renames, custom domains736 className="font-medium text-fg hover:underline"
737 />{" "}
Pull requests from branches738 {pull.status === "merged" ? "merged" : "wants to merge"}
739 {pull.branch && (
740 <>
741 {" "}
742 <span className="font-mono text-fg">{pull.branch}</span>
743 </>
744 )}{" "}
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar745 into{" "}
746 <BaseBranch
747 base={defaultBranch}
748 head={pull.fork ? null : pull.branch}
749 canChange={loaderData.canChangeBase && active}
750 branchesUrl={`${base}/branches.json`}
751 />
Issues and pull requests replace intents and attempts752 </span>
753 </span>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar754 {(pull.labels ?? []).map((name) => (
755 <LabelChip key={name} name={name} color={loaderData.labels.find((label) => label.name === name)?.color} />
756 ))}
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent757 {opener.requestedBy && (
758 <span className="text-xs">
759 requested by{" "}
760 <PersonLink name={opener.requestedBy} className="font-medium text-fg-soft hover:underline" />
761 </span>
762 )}
763 {/* A pull request from a branch was made by its author, not an agent;
764 one g1t made already says so. */}
765 {!pull.branch && !opener.requestedBy && (
Pull requests from branches766 <span className="flex items-center gap-1.5 font-mono text-xs">
767 <Bot size={14} />
768 {pull.agent}
769 {pull.runtime === "hosted" && <span className="text-faint">on g1t</span>}
770 </span>
771 )}
Issues and pull requests replace intents and attempts772 </div>
773
Acceptance checks in sandboxes, line comments and review verdicts774 {reviews.length > 0 && (
775 <p className="mt-3 flex flex-wrap items-center gap-x-4 gap-y-1 text-sm">
776 {reviews.map(({ reviewer, verdict }) => (
777 <span
778 key={reviewer}
779 className={`flex items-center gap-1.5 ${
Merge branch 'worktree-agent-ad7c6d88d93adc817'780 verdict === "approve" ? "text-success" : "text-danger"
Acceptance checks in sandboxes, line comments and review verdicts781 }`}
782 >
783 {verdict === "approve" ? <CircleCheck size={15} /> : <CircleSlash size={15} />}
784 {verdict === "approve" ? "Approved by" : "Changes requested by"}{" "}
785 <span className="font-medium">{reviewer}</span>
786 </span>
787 ))}
788 </p>
789 )}
790
Usage, Billing settings and prepaid AI credit; fixes from the UX audit791 <TabStrip label="Pull request" className="mt-6 items-end gap-1 border-b border-line">
Issues and pull requests replace intents and attempts792 <TabLink to={here} active={tab === "conversation"}>
793 <MessageSquare size={15} />
794 Conversation
Pull requests: title and tabs across the full width, with counts and the size of the change795 <TabCount n={comments.length} />
Issues and pull requests replace intents and attempts796 </TabLink>
797 <TabLink to={here + "?tab=session"} active={tab === "session"}>
798 <MessagesSquare size={15} />
799 Session
800 </TabLink>
801 <TabLink to={here + "?tab=changes"} active={tab === "changes"}>
802 <FileDiff size={15} />
Pull requests: title and tabs across the full width, with counts and the size of the change803 Files changed
804 <TabCount n={pull.files.length} />
Issues and pull requests replace intents and attempts805 </TabLink>
Pull requests: title and tabs across the full width, with counts and the size of the change806 {pull.files.length > 0 && (
807 <span className="ml-auto hidden pb-2.5 font-mono text-xs sm:inline">
Merge branch 'worktree-agent-ad7c6d88d93adc817'808 <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 change809 <span className="text-danger">−{pull.files.reduce((sum, file) => sum + file.deletions, 0).toLocaleString("en-US")}</span>
810 </span>
811 )}
Usage, Billing settings and prepaid AI credit; fixes from the UX audit812 </TabStrip>
Pull requests: title and tabs across the full width, with counts and the size of the change813 </header>
814 <div className={`mt-6 grid gap-8 ${tab === "changes" ? "" : "lg:grid-cols-[1fr_19rem]"}`}>
815 <div className="min-w-0">
816 {/* What is happening on it now: its stage, its agent, what people told
817 the agent, and what else touches the same files. On the conversation,
818 so the tabs sit right under the title. */}
819 {tab === "conversation" && (
820 <div>
821 {issue && (
822 <Link
823 to={`${base}/issues/${issue.number}`}
824 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"
825 >
826 <IssueIcon issue={issue} />
827 <span className="text-muted">
828 {pull.status === "merged" && issue.resolvedBy === pull.number
829 ? "Resolved"
830 : "For issue"}
831 </span>
832 <span className="truncate font-medium">{issue.title}</span>
833 <span className="text-faint">#{issue.number}</span>
834 {issue.pullCount > 1 && (
835 <span className="ml-auto shrink-0 text-xs text-faint">
836 1 of {issue.pullCount} pull requests
Pull requests show their tabs right under the title; agent panels move into Conversation837 </span>
Pull requests: title and tabs across the full width, with counts and the size of the change838 )}
839 </Link>
840 )}
841 {pull.status === "merged" && (
842 <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">
843 <GitMerge size={16} className="shrink-0 text-merged" />
Agents as a team: lifecycle, merge queue, billing and a new shell844 <span>
Pull requests: title and tabs across the full width, with counts and the size of the change845 Merged into <span className="font-mono">{defaultBranch}</span> by{" "}
846 <span className="font-medium">{pull.mergedBy}</span>{" "}
847 {/* Not an account: the repository's settings said to merge it. */}
848 {pull.mergedBy === "g1t" && "automatically, once it was ready, "}
849 {pull.mergedAt && <TimeAgo at={pull.mergedAt} />}
850 {pull.headCommit && (
851 <span className="font-mono text-muted"> · {pull.headCommit.slice(0, 7)}</span>
Agents as a team: lifecycle, merge queue, billing and a new shell852 )}
853 </span>
Pull requests: title and tabs across the full width, with counts and the size of the change854 </p>
855 )}
856 {pull.supersededBy != null && (
857 <p className="mt-4 rounded-xl border border-line bg-surface px-4 py-2.5 text-sm text-muted">
858 Closed because{" "}
859 <Link
860 to={`${base}/pull/${pull.supersededBy}`}
861 className="font-medium text-fg hover:underline"
862 >
863 #{pull.supersededBy}
864 </Link>{" "}
865 was merged for this issue instead.
866 </p>
867 )}
868 {lifecycle && <LifecyclePanel lifecycle={lifecycle} />}
869 {/* 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 step870 <AgentPanel
871 owner={params.owner}
872 repo={params.repo}
873 number={pull.number}
874 stage={lifecycle?.stage ?? null}
875 confidence={pull.confidence ?? null}
Merge branch 'worktree-agent-aca4790ea223aa235'876 runs={agentRuns}
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step877 />
Pull requests: title and tabs across the full width, with counts and the size of the change878
879 {/* 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 look880 {canRun &&
Pull requests: title and tabs across the full width, with counts and the size of the change881 pull.runtime === "hosted" &&
882 (working || ["working", "revising", "catching_up", "answering"].includes(lifecycle?.stage ?? "")) && (
Merge branch 'worktree-agent-a985c01f43538af01'883 // Keyed by the messages so the box empties once one shows below.
Merge branch 'worktree-agent-ad7c6d88d93adc817'884 <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 change885 <p className="flex items-center gap-2 text-sm font-medium">
Merge branch 'worktree-agent-ad7c6d88d93adc817'886 <Sparkles size={15} className="text-accent" />
Pull requests: title and tabs across the full width, with counts and the size of the change887 Message the agent
888 </p>
889 <p className="mt-1 text-xs text-muted">
890 A correction, a hint, a change of plan. It reads it at its next step, without
891 starting over.
892 </p>
893 <div className="mt-3 flex gap-2">
894 <input type="hidden" name="action" value="message" />
895 <input
896 name="body"
897 required
898 autoComplete="off"
899 data-1p-ignore
900 placeholder="Keep the old flag working too…"
Merge branch 'worktree-agent-ad7c6d88d93adc817'901 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 shell902 />
Merge branch 'worktree-agent-a985c01f43538af01'903 <SubmitButton match={{ action: "message" }} pending="Sending…">
904 Send
905 </SubmitButton>
Pull requests: title and tabs across the full width, with counts and the size of the change906 </div>
907 </Form>
Agents as a team: lifecycle, merge queue, billing and a new shell908 )}
Pull requests: title and tabs across the full width, with counts and the size of the change909 {messages.length > 0 && (
910 <ul className="mt-3 space-y-1.5">
911 {messages.map((message) => (
912 <li key={message.id} className="flex items-start gap-2 text-sm">
913 <Avatar name={message.author} size={18} />
914 <span className="min-w-0 grow">
915 <span className="font-medium">
916 {message.fromNumber != null ? `The agent on #${message.fromNumber}` : message.author}
917 </span>{" "}
918 <span className="text-muted">
919 {message.kind === "question"
920 ? "asked:"
921 : message.kind === "handoff"
922 ? "handed over:"
923 : message.kind === "answer"
924 ? "answered:"
925 : "to the agent:"}
926 </span>{" "}
927 {message.body}
928 {message.answer && (
Merge branch 'worktree-agent-ad7c6d88d93adc817'929 <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 change930 {message.declined ? "Declined: " : "Answer: "}
931 {message.answer}
932 </span>
933 )}
934 </span>
Merge branch 'worktree-agent-ad7c6d88d93adc817'935 <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 change936 {message.deliveredAt ? "read by the agent" : "waiting for its next step"}
937 </span>
938 </li>
939 ))}
940 </ul>
941 )}
942 {collisions.length > 0 && active && (
943 <div className="mt-4 rounded-xl border border-line bg-surface px-4 py-3 text-sm">
944 <p className="flex items-center gap-2.5 font-medium">
945 <Radar size={16} className="shrink-0 text-info" />
946 Other work is changing the same files
947 </p>
948 <ul className="mt-2 space-y-1.5">
949 {collisions.map((other) => (
950 <li key={other.number} className="flex flex-wrap items-baseline gap-x-2 text-muted">
951 <Link
952 to={`${base}/pull/${other.number}`}
953 className="font-medium text-fg hover:underline"
954 >
955 {other.title} <span className="font-normal text-faint">#{other.number}</span>
956 </Link>
957 <span className="font-mono text-xs">{other.paths.join(", ")}</span>
958 </li>
959 ))}
960 </ul>
961 <p className="mt-2 text-xs text-faint">
962 Whichever merges second will have to catch up, and may conflict.
963 </p>
964 </div>
965 )}
966 </div>
967 )}
968 <div className="mt-5">
969 {comparison ? (
970 <DiffView
971 comparison={comparison}
972 review={{
973 comments: comments.filter((comment) => comment.path),
974 canComment: Boolean(viewer),
975 }}
976 />
977 ) : tab === "session" ? (
978 session.length === 0 ? (
979 <EmptyState title="Nothing recorded yet">
980 The agent's prompts, reasoning and tool calls appear here as it
981 works.
982 </EmptyState>
983 ) : (
984 <ol className="relative space-y-5 before:absolute before:top-2 before:bottom-2 before:left-3.25 before:w-px before:bg-line">
985 {session.map((entry) => (
986 <Entry key={entry.seq} entry={entry} agent={pull.agent} />
987 ))}
988 </ol>
989 )
990 ) : (
991 <div className="space-y-4">
992 <TimelineItem
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent993 author={opener.name}
Pull requests: title and tabs across the full width, with counts and the size of the change994 at={pull.createdAt}
995 action={
996 <span>
997 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-agent998 {opener.requestedBy ? (
Pull requests: title and tabs across the full width, with counts and the size of the change999 <>
1000 {" "}
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent1001 for{" "}
1002 <PersonLink name={opener.requestedBy} className="font-medium text-fg-soft hover:underline" />
1003 </>
1004 ) : !pull.branch && (
1005 <>
1006 {" "}
Pull requests: title and tabs across the full width, with counts and the size of the change1007 with <span className="font-mono text-xs">{pull.agent}</span>
1008 </>
1009 )}
1010 </span>
1011 }
1012 >
1013 {pull.body ? (
1014 <Markdown source={pull.body} repo={{ namespace: params.owner, name: params.repo }} />
1015 ) : (
1016 <p className="text-sm text-muted">
1017 {working
1018 ? "No description yet. It is written when the pull request is marked ready for review."
1019 : "No description."}
1020 </p>
1021 )}
1022 {canManage && pull.status === "open" && (
1023 <details className="mt-3 border-t border-line pt-3 text-sm">
1024 <summary className="cursor-pointer text-xs text-faint hover:text-fg">
1025 Edit description
1026 </summary>
1027 <Form method="post" className="mt-3 space-y-2">
Merge branch 'worktree-agent-a985c01f43538af01'1028 <input type="hidden" name="action" value="describe" />
Pull requests: title and tabs across the full width, with counts and the size of the change1029 <Textarea
1030 name="summary"
1031 rows={6}
1032 placeholder="What changed and why"
1033 defaultValue={pull.body ?? ""}
1034 />
Merge branch 'worktree-agent-a985c01f43538af01'1035 <SubmitButton match={{ action: "describe" }} pending="Saving…">
1036 Save
1037 </SubmitButton>
Pull requests: title and tabs across the full width, with counts and the size of the change1038 </Form>
1039 </details>
1040 )}
1041 </TimelineItem>
Agents as a team: lifecycle, merge queue, billing and a new shell1042
Pull requests: title and tabs across the full width, with counts and the size of the change1043 <CommentList comments={comments} review={review} base={base} />
Agents as a team: lifecycle, merge queue, billing and a new shell1044
Merge branch 'worktree-agent-aca4790ea223aa235'1045 {(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 coming1046
Pull requests: title and tabs across the full width, with counts and the size of the change1047 {pull.status === "draft" && (
1048 <StatusBox>
1049 <StatusRow icon={<Loader size={16} className="text-faint" />} title="This is a draft">
1050 It is still being worked on. It can be reviewed and merged once it is
1051 marked ready.
1052 </StatusRow>
1053 {canManage && (
1054 <Form method="post" className="space-y-2 px-4 py-3">
Merge branch 'worktree-agent-a985c01f43538af01'1055 <input type="hidden" name="action" value="ready" />
Pull requests: title and tabs across the full width, with counts and the size of the change1056 <Textarea name="summary" rows={3} placeholder="What changed and why" />
Merge branch 'worktree-agent-a985c01f43538af01'1057 <SubmitButton match={{ action: "ready" }} pending="Marking ready…">
1058 Mark ready for review
1059 </SubmitButton>
Pull requests: title and tabs across the full width, with counts and the size of the change1060 </Form>
1061 )}
1062 </StatusBox>
1063 )}
Agents as a team: lifecycle, merge queue, billing and a new shell1064
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1065 {codeOwners && active && (
1066 <div className="flex gap-3">
1067 <span className="hidden w-8 shrink-0 sm:block" />
1068 <div className="min-w-0 grow">
1069 <PullCodeOwnersPanel
1070 owners={codeOwners}
1071 base={base}
1072 branch={defaultBranch}
1073 errorsHref={canProtect ? `${base}/settings/branches#codeowners` : `${base}/blob/${encodeURIComponent(defaultBranch)}/${codeOwners.path}`}
1074 />
1075 </div>
1076 </div>
1077 )}
1078
Pull requests: title and tabs across the full width, with counts and the size of the change1079 {pull.status === "open" && (
1080 <StatusBox>
Fast pages, required checks on the branch, self-hosted runners, honest incidents1081 {/* Checks by status at once; job by job when the runs are read. */}
Merge branch 'worktree-agent-aca4790ea223aa235'1082 <Suspense fallback={checksSection({}, true)}>
1083 <Await resolve={workflowJobs}>{(jobs) => checksSection(jobs)}</Await>
Fast pages, required checks on the branch, self-hosted runners, honest incidents1084 </Suspense>
1085 {noChecks && <AddCiPrompt owner={params.owner} repo={params.repo} canAdd={canAddCi} compact />}
Agents as a team: lifecycle, merge queue, billing and a new shell1086 <StatusRow
Pull requests: title and tabs across the full width, with counts and the size of the change1087 icon={
1088 reviews.some(({ verdict }) => verdict === "request_changes") ? (
1089 <CircleSlash size={16} className="text-danger" />
1090 ) : reviews.length > 0 ? (
Merge branch 'worktree-agent-ad7c6d88d93adc817'1091 <CircleCheck size={16} className="text-success" />
Pull requests: title and tabs across the full width, with counts and the size of the change1092 ) : (
1093 <MessageSquare size={16} className="text-faint" />
1094 )
1095 }
1096 title={
1097 reviews.length === 0
1098 ? "No reviews yet"
1099 : reviews
1100 .map(
1101 ({ reviewer, verdict }) =>
1102 `${verdict === "approve" ? "Approved by" : "Changes requested by"} ${reviewer}`,
1103 )
1104 .join(" · ")
1105 }
Agents as a team: lifecycle, merge queue, billing and a new shell1106 >
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent1107 {reviewPending && "g1t is reviewing it now. "}
Pull requests: title and tabs across the full width, with counts and the size of the change1108 {requiredApprovals > 0 &&
1109 `This repository requires ${requiredApprovals} approving ${
1110 requiredApprovals === 1 ? "review" : "reviews"
1111 } before merging.`}
Agents as a team: lifecycle, merge queue, billing and a new shell1112 </StatusRow>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1113 {codeOwners?.required && (
1114 <StatusRow
1115 icon={
1116 ownersMissing ? (
1117 <CircleSlash size={16} className="text-warn" />
1118 ) : (
Merge branch 'worktree-agent-ad7c6d88d93adc817'1119 <CircleCheck size={16} className="text-success" />
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1120 )
1121 }
1122 title={ownersMissing ? "Waiting for code owners" : "Code owners approved"}
1123 >
1124 {ownersMissing ?? `Every file it changes has its owners' approval.`}{" "}
1125 {ownersMissing && "This repository requires their review before merging."}
1126 </StatusRow>
1127 )}
Pull requests: title and tabs across the full width, with counts and the size of the change1128 {landing ? (
Agents as a team: lifecycle, merge queue, billing and a new shell1129 <StatusRow
Pull requests: title and tabs across the full width, with counts and the size of the change1130 icon={<Loader size={16} className="animate-spin text-accent" />}
1131 title="Merging"
1132 >
1133 {defaultBranch} has moved, so g1t is bringing this up to date first. It
1134 lands as soon as that is done. Watch it in the Session tab.
1135 </StatusRow>
Catching up with main takes seconds when the two sides touched different files1136 ) : agentCatchUp && behind ? (
1137 <CatchUpProgress
1138 owner={params.owner}
1139 repo={params.repo}
1140 number={pull.number}
1141 defaultBranch={defaultBranch}
1142 behind={behind}
1143 update={agentCatchUp.update}
1144 startedAt={agentCatchUp.startedAt}
1145 retrying={catchUpPending}
Merge branch 'worktree-agent-aca4790ea223aa235'1146 runs={agentRuns}
Catching up with main takes seconds when the two sides touched different files1147 />
Pull requests: title and tabs across the full width, with counts and the size of the change1148 ) : conflicting ? (
1149 <ConflictsSection
1150 conflicts={conflicts}
1151 pull={pull}
1152 owner={params.owner}
1153 repo={params.repo}
1154 defaultBranch={defaultBranch}
1155 changesUrl={here + "?tab=changes"}
1156 canResolve={canUpdate && agentsEnabled}
Catching up with main takes seconds when the two sides touched different files1157 resolving={catchUpPending}
1158 error={catchUp && "error" in catchUp ? catchUp.error : null}
Agents as a team: lifecycle, merge queue, billing and a new shell1159 />
Pull requests: title and tabs across the full width, with counts and the size of the change1160 ) : probing ? (
1161 <MergeabilityRow mergeable={mergeable} defaultBranch={defaultBranch} />
1162 ) : behind ? (
1163 <StatusRow
1164 icon={<GitPullRequestArrow size={16} className="text-info" />}
1165 title={`${defaultBranch} has moved since this was made`}
1166 >
1167 {mergeable === "clean" && "It has no conflicts with it. "}
1168 {requireUpToDate
1169 ? "This repository requires pull requests to be up to date, so it has to catch up before it can merge."
1170 : "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 files1171 {canUpdate && (
1172 <Form method="post" className="mt-2">
Pull requests: title and tabs across the full width, with counts and the size of the change1173 <input type="hidden" name="action" value="update" />
Merge branch 'worktree-agent-a985c01f43538af01'1174 <SubmitButton variant="quiet" match={{ action: "update" }} pending={`Merging ${defaultBranch} in…`}>
1175 Catch up with {defaultBranch} now
1176 </SubmitButton>
Pull requests: title and tabs across the full width, with counts and the size of the change1177 </Form>
1178 )}
Catching up with main takes seconds when the two sides touched different files1179 {catchUp && "error" in catchUp && <ErrorText>{catchUp.error}</ErrorText>}
Pull requests: title and tabs across the full width, with counts and the size of the change1180 </StatusRow>
1181 ) : (
Merge branch 'worktree-agent-af9f0f386d52b6a73'1182 (pull.headCommit || (catchUp && "error" in catchUp)) && (
Pull requests: title and tabs across the full width, with counts and the size of the change1183 <StatusRow
Merge branch 'worktree-agent-ad7c6d88d93adc817'1184 icon={<CircleCheck size={16} className="text-success" />}
Catching up with main takes seconds when the two sides touched different files1185 title={
1186 (caughtUp && !caughtUp.already) || agentCatchUp
1187 ? `Brought up to date with ${defaultBranch}`
1188 : `Up to date with ${defaultBranch}`
1189 }
1190 >
1191 {caughtUp && !caughtUp.already && (
1192 <>
1193 Merged <span className="font-mono">{defaultBranch}</span> in as{" "}
1194 <span className="font-mono text-fg">{caughtUp.commit.slice(0, 7)}</span>. Its checks run
1195 again on the new commit.
1196 </>
1197 )}
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent1198 {agentCatchUp && "g1t merged it in and pushed the result. Its checks run again on the new commit."}
Merge branch 'worktree-agent-af9f0f386d52b6a73'1199 {/* A catch-up refused or failed after the pull request stopped being behind. */}
1200 {catchUp && "error" in catchUp && <ErrorText>{catchUp.error}</ErrorText>}
Catching up with main takes seconds when the two sides touched different files1201 </StatusRow>
Pull requests: title and tabs across the full width, with counts and the size of the change1202 )
1203 )}
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge1204 {active && rules && (rulesUnmet.length > 0 || rulesBypassable.length > 0 || rules.evaluate.length > 0) && (
1205 <StatusRow
1206 icon={
1207 <Scale
1208 size={16}
1209 className={rulesUnmet.length > 0 ? "text-danger" : rulesBypassable.length > 0 ? "text-warn" : "text-info"}
1210 />
1211 }
1212 title={
1213 rulesUnmet.length > 0
1214 ? `Rules for ${defaultBranch} are not met yet`
1215 : rulesBypassable.length > 0
1216 ? `You may bypass the rules for ${defaultBranch}`
1217 : "Rulesets being evaluated would refuse this merge"
1218 }
1219 >
1220 <div className="mt-2 space-y-3">
1221 {rulesUnmet.length > 0 && <ViolationList violations={rulesUnmet} />}
1222 {rulesBypassable.length > 0 && (
1223 <div>
1224 <p className="mb-1.5 text-xs text-muted">Not met, but a ruleset lets you bypass it:</p>
1225 <ViolationList violations={rulesBypassable} tone="warn" />
1226 </div>
1227 )}
1228 {rules.evaluate.length > 0 && (
1229 <div>
1230 <p className="mb-1.5 text-xs text-muted">Rulesets in evaluate would refuse it for this; nothing is held up:</p>
1231 <ViolationList violations={rules.evaluate} tone="info" />
1232 </div>
1233 )}
1234 {canProtect && (
1235 <Link
1236 to={`${base}/settings/rules?branch=${encodeURIComponent(defaultBranch)}`}
1237 className="inline-block text-xs text-muted underline underline-offset-2 hover:text-fg"
1238 >
1239 See every rule for {defaultBranch}
1240 </Link>
1241 )}
1242 </div>
1243 </StatusRow>
1244 )}
Pull requests: title and tabs across the full width, with counts and the size of the change1245 {stalled && !lifecycle && (
1246 <StatusRow icon={<Hand size={16} className="text-warn" />} title="Needs you">
1247 {stalled}
1248 </StatusRow>
1249 )}
1250 {canMerge && lifecycle?.stage === "queued" && (
1251 <Form method="post" className="flex flex-wrap items-center gap-3 px-4 py-3">
1252 <ButtonLink to={`${base}/queue`}>
1253 <Layers size={15} />
1254 See the queue
1255 </ButtonLink>
Merge branch 'worktree-agent-a985c01f43538af01'1256 <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 change1257 Remove from the queue
Merge branch 'worktree-agent-a985c01f43538af01'1258 </SubmitButton>
Pull requests: title and tabs across the full width, with counts and the size of the change1259 </Form>
1260 )}
1261 {canMerge && !landing && lifecycle?.stage !== "queued" && (
1262 <Form method="post" className="space-y-3 px-4 py-3">
1263 {issue?.state === "open" && (
1264 <CheckboxOption
1265 name="keepIssueOpen"
1266 label={`Keep #${issue.number} open. This is only part of the work.`}
1267 labelClassName="text-xs text-muted"
1268 />
1269 )}
1270 {unchecked && canIgnoreChecks && (
1271 <CheckboxOption
1272 name="ignoreChecks"
Fast pages, required checks on the branch, self-hosted runners, honest incidents1273 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 change1274 labelClassName="text-xs text-muted"
1275 />
1276 )}
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge1277 {rulesBypassable.length > 0 && rulesUnmet.length === 0 && (
1278 <CheckboxOption
1279 name="bypassRules"
1280 checked={bypassing}
1281 onCheckedChange={(checked) => setBypassing(checked === true)}
1282 label={`Bypass the rules for ${defaultBranch}: merge although they are not met. It is recorded.`}
1283 labelClassName="text-xs text-muted"
1284 />
1285 )}
Pull requests: title and tabs across the full width, with counts and the size of the change1286 <div className="flex flex-wrap items-center gap-3">
Merge branch 'worktree-agent-ae1299e92e4462012'1287 <Hint label={mergeBlocked} disabled={mergeBlocked != null}>
1288 <SubmitButton
1289 variant="accent"
1290 name="action"
1291 value="merge"
1292 disabled={mergeBlocked != null}
1293 pending={mergeQueue ? "Adding to the queue…" : "Merging…"}
1294 >
1295 {mergeQueue ? <Layers size={15} /> : <GitMerge size={15} />}
1296 {mergeQueue ? "Add to the merge queue" : `Merge into ${defaultBranch}`}
1297 </SubmitButton>
1298 </Hint>
Pull requests: title and tabs across the full width, with counts and the size of the change1299 <span className={`text-xs ${mergeBlocked ? "text-danger" : "text-muted"}`}>
1300 {mergeBlocked ?? (mergeQueue
1301 ? `Tested together with everything ahead of it, then lands on ${defaultBranch}.`
1302 : issue?.state === "open"
1303 ? `Closes issue #${issue.number}, and any other pull requests still open for it.`
1304 : `Lands its commits on ${defaultBranch}.`)}
1305 </span>
1306 </div>
1307 {actionData?.action === "merge" && (
1308 <ErrorText>{actionData.error}</ErrorText>
1309 )}
1310 </Form>
1311 )}
1312 </StatusBox>
1313 )}
Agents as a team: lifecycle, merge queue, billing and a new shell1314
Pull requests: title and tabs across the full width, with counts and the size of the change1315 <CommentForm
1316 author={viewer?.username ?? null}
1317 resetKey={comments.length}
1318 review={review}
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1319 handles={teams.map((team) => `@${team.ref}`)}
Pull requests: title and tabs across the full width, with counts and the size of the change1320 />
1321 {canManage && active && (
1322 <Form method="post" className="flex justify-end">
Merge branch 'worktree-agent-a985c01f43538af01'1323 <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 change1324 Close pull request
Merge branch 'worktree-agent-a985c01f43538af01'1325 </SubmitButton>
Pull requests: title and tabs across the full width, with counts and the size of the change1326 </Form>
1327 )}
1328 {actionData &&
Merge branch 'worktree-agent-a985c01f43538af01'1329 !["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 change1330 String(actionData.action),
1331 ) && <ErrorText>{actionData.error}</ErrorText>}
1332 </div>
1333 )}
1334 {actionData?.action === "comment" && (
1335 <div className="mt-2">
1336 <ErrorText>{actionData.error}</ErrorText>
1337 </div>
1338 )}
1339 </div>
1340 </div>
1341
1342 <aside className={tab === "changes" ? "hidden" : "space-y-6"}>
1343 {/* An open pull request shows its checks in full in the merge box. */}
Fast pages, required checks on the branch, self-hosted runners, honest incidents1344 {pull.status !== "open" && <WorkflowStatuses statuses={statuses} />}
Pull requests: title and tabs across the full width, with counts and the size of the change1345 {affects.length > 0 && (
1346 <section>
1347 <h3 className="flex items-center gap-1.5 text-sm font-medium">
1348 <Network size={14} className="text-faint" />
1349 Affects
1350 </h3>
1351 <p className="mt-1 text-xs text-muted">Projects that use this one, and so may feel this change:</p>
1352 <ul className="mt-2 space-y-1 text-sm">
1353 {affects.map((project) => (
1354 <li key={project.slug} className="flex items-center justify-between gap-2">
1355 <Link to={`/${params.owner}/${project.slug}`} className="hover:underline">
1356 {project.name}
1357 </Link>
1358 {project.as && <code className="font-mono text-xs text-faint">{project.as}</code>}
1359 </li>
1360 ))}
1361 </ul>
1362 {preview?.branch && canMerge && (
1363 <Form method="post" className="mt-3">
1364 <input type="hidden" name="action" value="stack" />
1365 <input type="hidden" name="branch" value={preview.branch} />
Merge branch 'worktree-agent-a985c01f43538af01'1366 <SubmitButton
1367 match={{ action: "stack" }}
1368 pending="Starting the builds…"
1369 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 change1370 >
1371 Preview them against this change
Merge branch 'worktree-agent-a985c01f43538af01'1372 </SubmitButton>
Agents as a team: lifecycle, merge queue, billing and a new shell1373 </Form>
1374 )}
Pull requests: title and tabs across the full width, with counts and the size of the change1375 {actionData?.action === "stack" &&
1376 ("notice" in actionData ? (
Merge branch 'worktree-agent-ad7c6d88d93adc817'1377 <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 change1378 ) : (
1379 <ErrorText>{actionData.error}</ErrorText>
1380 ))}
1381 </section>
Acceptance checks in sandboxes, line comments and review verdicts1382 )}
Issues and pull requests replace intents and attempts1383
Project dependencies: addresses, preview stacks, Affects, and agents who know1384 <section>
Pull requests: title and tabs across the full width, with counts and the size of the change1385 <h3 className="text-sm font-medium">Reviewers</h3>
1386 <ul className="mt-2 space-y-1.5 text-sm">
1387 {reviewerNames.map((name) => {
1388 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-agent1389 const pending = name === "g1t" && reviewPending;
Pull requests: title and tabs across the full width, with counts and the size of the change1390 return (
1391 <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-agent1392 {name === "g1t" ? (
Pull requests: title and tabs across the full width, with counts and the size of the change1393 <Sparkles size={16} className="shrink-0 text-accent" />
1394 ) : (
1395 <Avatar name={name} size={20} />
1396 )}
1397 <span className="grow truncate font-mono text-xs">{name}</span>
1398 {pending ? (
1399 <span className="flex items-center gap-1.5 text-xs text-muted">
1400 <span className="size-1.5 animate-pulse rounded-full bg-accent" />
1401 Reviewing
1402 </span>
1403 ) : verdict === "approve" ? (
Merge branch 'worktree-agent-ad7c6d88d93adc817'1404 <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 change1405 <CircleCheck size={13} /> Approved
1406 </span>
1407 ) : verdict === "request_changes" ? (
1408 <span className="flex items-center gap-1 text-xs text-danger">
1409 <CircleSlash size={13} /> Changes requested
1410 </span>
1411 ) : (
1412 <span className="text-xs text-faint">Review requested</span>
1413 )}
1414 </li>
1415 );
1416 })}
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1417 {teamReviewers.map((team) => (
1418 <TeamReviewer key={team} team={team} />
1419 ))}
1420 {reviewerNames.length === 0 && teamReviewers.length === 0 && (
Pull requests: title and tabs across the full width, with counts and the size of the change1421 <li className="px-1 text-xs text-faint">No reviews requested yet.</li>
1422 )}
Project dependencies: addresses, preview stacks, Affects, and agents who know1423 </ul>
Pull requests: title and tabs across the full width, with counts and the size of the change1424 {pull.status === "open" && canManage && (
1425 <div className="mt-3 space-y-2">
1426 {agentsEnabled && !reviewPending && (
1427 <Form method="post">
1428 <input type="hidden" name="action" value="agent-review" />
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1429 {[...pull.reviewers, ...teamReviewers].map((name) => (
Pull requests: title and tabs across the full width, with counts and the size of the change1430 <input key={name} type="hidden" name="reviewer" value={name} />
1431 ))}
1432 <div className="*:w-full">
Merge branch 'worktree-agent-a985c01f43538af01'1433 <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 change1434 <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-agent1435 Request review from g1t
Merge branch 'worktree-agent-a985c01f43538af01'1436 </SubmitButton>
Pull requests: title and tabs across the full width, with counts and the size of the change1437 </div>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1438 <ComputeNote note={loaderData.computeNote} />
Pull requests: title and tabs across the full width, with counts and the size of the change1439 </Form>
1440 )}
1441 <details>
1442 <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 bar1443 {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 change1444 </summary>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1445 <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 change1446 <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-agent1447 {pull.reviewers.includes("g1t") && (
1448 <input type="hidden" name="reviewer" value="g1t" />
Pull requests: title and tabs across the full width, with counts and the size of the change1449 )}
1450 <PeoplePicker
1451 name="reviewer"
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights1452 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-agent1453 chosen={pull.reviewers.filter((name) => name !== "g1t")}
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1454 placeholder="Other usernames or teams (acme/backend)"
Pull requests: title and tabs across the full width, with counts and the size of the change1455 />
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1456 {teamChoices.length > 0 && (
1457 <div className="space-y-1.5 border-t border-line pt-2">
1458 <p className="text-xs text-faint">Teams</p>
1459 {teamChoices.map((team) => (
1460 <CheckboxOption
1461 key={team.ref}
1462 name="reviewer"
1463 value={team.ref}
1464 defaultChecked={teamReviewers.includes(team.ref)}
1465 className="items-center"
1466 labelClassName="flex min-w-0 items-center gap-2"
1467 label={
1468 <>
1469 <Users size={14} className="shrink-0 text-faint" />
1470 <span className="truncate font-mono text-xs">@{team.ref}</span>
1471 </>
1472 }
1473 />
1474 ))}
1475 </div>
1476 )}
Merge branch 'worktree-agent-a985c01f43538af01'1477 <SubmitButton variant="quiet" match={{ action: "reviewers" }} pending="Saving…">
Pull requests: title and tabs across the full width, with counts and the size of the change1478 Save reviewers
Merge branch 'worktree-agent-a985c01f43538af01'1479 </SubmitButton>
Pull requests: title and tabs across the full width, with counts and the size of the change1480 </Form>
1481 </details>
1482 </div>
Project dependencies: addresses, preview stacks, Affects, and agents who know1483 )}
Pull requests: title and tabs across the full width, with counts and the size of the change1484 {["agent-review", "reviewers"].includes(String(actionData?.action)) && (
1485 <ErrorText>{actionData?.error}</ErrorText>
1486 )}
Project dependencies: addresses, preview stacks, Affects, and agents who know1487 </section>
Acceptance checks in sandboxes, line comments and review verdicts1488
Pull requests: title and tabs across the full width, with counts and the size of the change1489 <section>
1490 <h3 className="text-sm font-medium">Assignees</h3>
1491 <ul className="mt-2 space-y-1.5 text-sm">
1492 {pull.assignees.map((name) => (
Agents as a team: lifecycle, merge queue, billing and a new shell1493 <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 change1494 <Avatar name={name} size={20} />
Agents as a team: lifecycle, merge queue, billing and a new shell1495 <span className="grow truncate font-mono text-xs">{name}</span>
1496 </li>
Pull requests: title and tabs across the full width, with counts and the size of the change1497 ))}
1498 {pull.assignees.length === 0 && (
1499 <li className="px-1 text-xs text-faint">No one yet.</li>
1500 )}
1501 </ul>
1502 {viewer && canManage && active && (
1503 <div className="mt-3 space-y-2">
1504 {!pull.assignees.includes(viewer.username) && (
1505 <Form method="post">
1506 <input type="hidden" name="action" value="assign" />
1507 {pull.assignees.map((name) => (
1508 <input key={name} type="hidden" name="assignee" value={name} />
1509 ))}
1510 <input type="hidden" name="assignee" value={viewer.username} />
1511 <div className="*:w-full">
Merge branch 'worktree-agent-a985c01f43538af01'1512 <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 change1513 Assign yourself
Merge branch 'worktree-agent-a985c01f43538af01'1514 </SubmitButton>
Pull requests: title and tabs across the full width, with counts and the size of the change1515 </div>
1516 </Form>
1517 )}
1518 <details>
1519 <summary className="cursor-pointer text-xs text-faint hover:text-fg">
1520 Assign people
1521 </summary>
1522 <Form method="post" className="mt-2 space-y-2" key={pull.assignees.join()}>
1523 <input type="hidden" name="action" value="assign" />
1524 <PeoplePicker name="assignee" members={members} chosen={pull.assignees} />
Merge branch 'worktree-agent-a985c01f43538af01'1525 <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 change1526 Save assignees
Merge branch 'worktree-agent-a985c01f43538af01'1527 </SubmitButton>
Pull requests: title and tabs across the full width, with counts and the size of the change1528 </Form>
1529 </details>
1530 </div>
Issues and pull requests replace intents and attempts1531 )}
Pull requests: title and tabs across the full width, with counts and the size of the change1532 {actionData?.action === "assign" && <ErrorText>{actionData.error}</ErrorText>}
1533 </section>
Issues and pull requests replace intents and attempts1534
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1535 <LabelsBox
1536 labels={loaderData.labels}
1537 chosen={pull.labels ?? []}
1538 canEdit={canManage && active}
1539 canCreate={loaderData.canTriage}
1540 manageUrl={loaderData.canTriage ? `${base}/labels` : undefined}
1541 />
1542 <MilestoneBox
1543 milestones={loaderData.milestones}
1544 current={pull.milestone}
1545 canEdit={loaderData.canTriage}
1546 base={base}
1547 />
1548
Pull requests: title and tabs across the full width, with counts and the size of the change1549 <section>
1550 <h3 className="text-sm font-medium">Working copy</h3>
1551 {pull.branch ? (
1552 <>
1553 <p className="mt-1 text-xs text-muted">
1554 <GitBranch size={13} className="mr-1 inline align-[-2px]" />
1555 Branch <span className="font-mono text-fg">{pull.branch}</span> of this
1556 repository. Pushes to it show up here.
1557 </p>
1558 <div className="mt-2">
1559 <CopyLine text={`git clone -b ${pull.branch} ${remote}`} />
1560 </div>
1561 </>
1562 ) : (
1563 <>
1564 <p className="mt-1 text-xs text-muted">
1565 This fork belongs to the pull request. Pushes to it show up here.
1566 </p>
1567 <div className="mt-2">
1568 <CopyLine text={`git clone ${remote}`} />
1569 </div>
1570 </>
1571 )}
1572 <p className="mt-3 flex items-center gap-1.5 font-mono text-xs text-faint">
1573 <GitCommitHorizontal size={13} />
1574 {pull.headCommit?.slice(0, 12) ?? "no commits pushed yet"}
1575 </p>
1576 </section>
Inbox on the site: reasons, a reason filter, Subscribe, Watch and settings1577 {loaderData.viewer && (
1578 <SubscriptionBox action={`${base}/notifications`} number={pull.number} kind="pull" subscription={loaderData.subscription} />
1579 )}
Pull requests: title and tabs across the full width, with counts and the size of the change1580 </aside>
1581 </div>
Issues and pull requests replace intents and attempts1582 </div>
1583 );
1584}
Pull requests say where they are live; the project menu shows what is coming1585
1586/** Where the pull request's change is live, as GitHub shows a deployment. */
1587function DeploymentCard({
1588 preview,
1589 build,
1590 stacked,
Merge branch 'worktree-agent-aca4790ea223aa235'1591 stacking,
Pull requests say where they are live; the project menu shows what is coming1592 base,
1593}: {
1594 preview: LiveApp | null;
1595 build: Deployment | null;
Fast pages, required checks on the branch, self-hosted runners, honest incidents1596 stacked: Promise<Stacked[]>;
Merge branch 'worktree-agent-aca4790ea223aa235'1597 /** Projects use this one, so previews built against it may be on their way. */
1598 stacking: boolean;
Pull requests say where they are live; the project menu shows what is coming1599 base: string;
1600}) {
1601 const building = build?.status === "queued" || build?.status === "building";
1602 const failed = build?.status === "failed";
1603 return (
1604 <section className="ml-12 overflow-hidden rounded-xl border border-line bg-surface">
1605 <div className="flex flex-wrap items-center gap-3 px-4 py-3">
1606 <span
1607 className={`flex size-8 shrink-0 items-center justify-center rounded-full ring-1 ${
Merge branch 'worktree-agent-ad7c6d88d93adc817'1608 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 coming1609 }`}
1610 >
1611 {building ? <Loader size={15} className="animate-spin" /> : <Rocket size={15} />}
1612 </span>
1613 <div className="min-w-0 grow">
1614 <p className="text-sm font-medium">
1615 {building
1616 ? preview
1617 ? "Deploying the latest push. The last preview is still live."
1618 : "Deploying a preview"
1619 : failed
1620 ? "The preview failed to deploy"
1621 : "This branch is live"}
1622 </p>
1623 {preview ? (
1624 <a href={preview.url} className="mt-0.5 block truncate font-mono text-xs text-muted hover:text-accent">
1625 {preview.url.replace(/^https?:[/][/]/, "")}
1626 </a>
1627 ) : (
1628 build?.error && <p className="mt-0.5 truncate text-xs text-muted">{build.error}</p>
1629 )}
1630 </div>
1631 <div className="flex shrink-0 items-center gap-2">
1632 {build && (
1633 <Link to={`${base}/deployments/${build.id}`} className="text-xs text-muted hover:text-fg">
1634 {failed ? "See why" : "Build log"}
1635 </Link>
1636 )}
1637 {preview && (
1638 <a
1639 href={preview.url}
1640 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"
1641 >
1642 Visit preview
1643 <ArrowUpRight size={13} />
1644 </a>
1645 )}
1646 </div>
1647 </div>
1648 {preview && (
1649 <p className="border-t border-line px-4 py-2 text-xs text-faint">
1650 <span className="font-mono">{preview.commit.slice(0, 7)}</span> · deployed <TimeAgo at={preview.deployedAt} />
1651 {preview.branch && ` · from ${preview.branch}`}
1652 </p>
1653 )}
Fast pages, required checks on the branch, self-hosted runners, honest incidents1654 {/* Read after the page: the card's own part shows at once. */}
Merge branch 'worktree-agent-aca4790ea223aa235'1655 <Suspense
1656 fallback={
1657 stacking && (
1658 <Loading className="border-t border-line px-4 py-2.5 text-xs">
1659 <SkeletonLine className="w-40" />
1660 <SkeletonLine className="mt-1.5 w-64 max-w-full" />
1661 </Loading>
1662 )
1663 }
1664 >
Fast pages, required checks on the branch, self-hosted runners, honest incidents1665 <Await resolve={stacked}>
1666 {(stacked) =>
1667 stacked.length > 0 && (
Merge branch 'worktree-agent-aca4790ea223aa235'1668 <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 incidents1669 <p className="text-xs text-muted">Built against this change:</p>
1670 <ul className="mt-1.5 space-y-1">
1671 {stacked.map((entry) => (
1672 <li key={entry.slug} className="flex items-center gap-2 text-xs">
1673 <span className="font-medium">{entry.name}</span>
1674 <a href={entry.url} className="truncate font-mono text-muted hover:text-accent">
1675 {entry.url.replace(/^https?:[/][/]/, "")}
1676 </a>
1677 </li>
1678 ))}
1679 </ul>
1680 </div>
1681 )
1682 }
1683 </Await>
1684 </Suspense>
Pull requests say where they are live; the project menu shows what is coming1685 </section>
1686 );
1687}

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