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

1,415 lines62,314 bytesCodeBlame

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Acceptance checks in sandboxes, line comments and review verdicts1import { env } from "cloudflare:workers";
Issues and pull requests replace intents and attempts2import {
3 Bot,
4 ChevronRight,
5 FileDiff,
Pull requests from branches6 GitBranch,
Issues and pull requests replace intents and attempts7 GitCommitHorizontal,
Acceptance checks in sandboxes, line comments and review verdicts8 CircleCheck,
9 CircleSlash,
Issues and pull requests replace intents and attempts10 GitMerge,
Agents as a team: lifecycle, merge queue, billing and a new shell11 Layers,
12 GitPullRequestArrow,
13 Hand,
14 Loader,
15 Radar,
16 Sparkles,
Issues and pull requests replace intents and attempts17 MessageSquare,
Pull requests say where they are live; the project menu shows what is coming18 ArrowUpRight,
Issues and pull requests replace intents and attempts19 MessagesSquare,
Pull requests say where they are live; the project menu shows what is coming20 Rocket,
Project dependencies: addresses, preview stacks, Affects, and agents who know21 Network,
Issues and pull requests replace intents and attempts22 StickyNote,
23 User,
24 Wrench,
25} from "lucide-react";
Fast pages, required checks on the branch, self-hosted runners, honest incidents26import { Suspense, useEffect } from "react";
27import { Await, Form, Link, redirect, useNavigation, useRevalidator } from "react-router";
Issues and pull requests replace intents and attempts28
Pull requests say where they are live; the project menu shows what is coming29import {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look30 type Capability,
Pull requests say where they are live; the project menu shows what is coming31 type Comparison,
32 type Deployment,
Agents and memory, checks and conflicts, profiles, slug renames, custom domains33 type Job,
Pull requests say where they are live; the project menu shows what is coming34 type LiveApp,
35 type SessionEntry,
36 type Viewer,
37 pullComparison,
38} from "@g1t/contracts";
Issues and pull requests replace intents and attempts39
40import type { Route } from "./+types/pull";
Social cards for every page: og.g1t.sh41import { excerpt, page } from "../../lib/meta";
Issues and pull requests replace intents and attempts42import { DiffView } from "../../components/diff-view";
Agents as a team: lifecycle, merge queue, billing and a new shell43import { LifecyclePanel } from "../../components/lifecycle";
Agents and memory, checks and conflicts, profiles, slug renames, custom domains44import { AgentPanel } from "../../components/agents";
Issues and pull requests replace intents and attempts45import { Markdown } from "../../components/markdown";
46import {
47 Avatar,
48 Button,
Agents as a team: lifecycle, merge queue, billing and a new shell49 ButtonLink,
Issues and pull requests replace intents and attempts50 CopyLine,
51 EmptyState,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look52 ComputeNote,
Issues and pull requests replace intents and attempts53 ErrorText,
54 Textarea,
55 TimeAgo,
56} from "../../components/ui";
Workspace names and icons, and a component kit for every control57import { CheckboxOption } from "../../components/ui/checkbox";
GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs58import { WorkflowStatuses } from "../../components/actions";
Fast pages, required checks on the branch, self-hosted runners, honest incidents59import { AddCiPrompt } from "../../components/add-ci";
Agents as a team: lifecycle, merge queue, billing and a new shell60import {
61 CommentForm,
62 CommentList,
63 IssueIcon,
64 PeoplePicker,
Agents and memory, checks and conflicts, profiles, slug renames, custom domains65 PersonLink,
Agents as a team: lifecycle, merge queue, billing and a new shell66 PullState,
67 TimelineItem,
68 verdicts,
69} from "../../components/work";
Catching up with main takes seconds when the two sides touched different files70import { CatchUpProgress, ChecksSection, ConflictsSection, MergeabilityRow, runIdOf } from "../../components/merge-box";
71import { 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 put72import { notFound } from "../../lib/not-found.server";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look73import { computeNoteFor } from "../../lib/compute.server";
Agents and memory, checks and conflicts, profiles, slug renames, custom domains74import { actions, deployments, identity, projects, repos, work } from "../../lib/services.server";
Issues and pull requests replace intents and attempts75import { assertSameOrigin, getViewer, requireUser } from "../../lib/session.server";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look76import { accessTo, refusal } from "../../lib/access.server";
Issues and pull requests replace intents and attempts77
78const REFRESH_MS = 4000;
79const EMPTY_COMPARISON: Comparison = { base: null, head: "", files: [], truncated: false };
80const TABS = ["conversation", "session", "changes"] as const;
81type Tab = (typeof TABS)[number];
82
Social cards for every page: og.g1t.sh83export function meta({ loaderData, params, ...args }: Route.MetaArgs) {
84 const pull = loaderData?.pull;
85 const title = pull ? `${pull.title} · Pull request #${pull.number} · ` : "";
86 const state = { draft: "Draft", open: "Open", merged: "Merged", closed: "Closed" }[pull?.status ?? "open"];
87 const body = excerpt(pull?.body);
88 return page(args, {
89 title: `${title}${params.owner}/${params.repo} · g1t`,
90 description: pull
91 ? `${state} pull request #${pull.number} on ${params.owner}/${params.repo} by ${pull.author.username}.${body ? ` ${body}` : ""}`
92 : null,
93 // The card shows the title and the state.
94 version: pull ? [pull.title, pull.status] : undefined,
95 type: "article",
96 });
Issues and pull requests replace intents and attempts97}
98
99export async function loader({ params, context, request }: Route.LoaderArgs) {
100 const viewer = getViewer(context);
101 const path = { namespace: params.owner, name: params.repo };
102 const number = Number(params.number);
103 const asked = new URL(request.url).searchParams.get("tab");
104 const tab: Tab = TABS.find((name) => name === asked) ?? "conversation";
105
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look106 // Members of the workspace pick reviewers and assignees from its people;
107 // what the viewer may do here goes by their role on the repository.
Agents as a team: lifecycle, merge queue, billing and a new shell108 const member = (viewer?.workspaces ?? []).some(
109 (membership) => membership.slug === params.owner,
110 );
Fast pages, required checks on the branch, self-hosted runners, honest incidents111 // Everything starts at once. Only what the viewer's role decides waits
112 // for the repository, which the layout is looking up in this request too
113 // (lib/access.server.ts), and only what the pull request decides waits
114 // for it.
Project dependencies: addresses, preview stacks, Affects, and agents who know115 const ref = { workspace: params.owner, slug: params.repo };
Fast pages, required checks on the branch, self-hosted runners, honest incidents116 const access = accessTo(context, params);
117 const pullFound = work.getPull(path, number, viewer);
118 const deps = projects.dependencies(params.owner, params.repo, viewer);
119 // Awaited below, unless the pull request is missing first.
120 deps.catch(() => null);
121 const [{ can }, found, repo, settings, agentsEnabled, members, computeNote, session, deployed] = await Promise.all([
122 access,
123 pullFound,
Issues and pull requests replace intents and attempts124 repos.get(path, viewer),
Agents as a team: lifecycle, merge queue, billing and a new shell125 work.getSettings(path, viewer),
g1t's agents only for listed workspaces, whatever the state of billing126 env.RUNNER.enabled(viewer, path),
Agents as a team: lifecycle, merge queue, billing and a new shell127 // A member picks reviewers and assignees from the workspace's people.
128 member ? identity.listMembers(params.owner, viewer) : null,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look129 // Before someone asks g1t-agent for something: whether the plan lets it start.
Fast pages, required checks on the branch, self-hosted runners, honest incidents130 access.then(({ can }) => (can.run ? computeNoteFor(params.owner, "agent") : null)),
131 tab === "session" ? work.readSession(path, number, viewer) : null,
132 // Its preview, for people with a role here: beside the rest, not after.
133 access.then(({ insider }) => (insider ? deployments.list(ref, viewer).catch(() => null) : null)),
Issues and pull requests replace intents and attempts134 ]);
135 if (!found.ok) {
136 // Issues and pull requests share numbers; this one may be an issue.
137 const issue = await work.getIssue(path, number, viewer);
138 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 put139 throw notFound("pull");
Issues and pull requests replace intents and attempts140 }
141 const { pull } = found.value;
Pull requests from branches142 const range = pullComparison(pull);
Agents and memory, checks and conflicts, profiles, slug renames, custom domains143 // The jobs of each workflow run on its head, to list checks job by job.
144 // 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 incidents145 // Streamed: the checks show by status first, then job by job.
Agents and memory, checks and conflicts, profiles, slug renames, custom domains146 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 incidents147 const workflowJobs =
148 tab === "conversation" && pull.status === "open" && runIds.length > 0
149 ? Promise.all(runIds.map((id) => actions.run(path, viewer, id).catch(() => null))).then((runs) => {
150 const jobs: Record<string, Job[]> = {};
151 for (const run of runs) if (run?.ok) jobs[run.value.run.id] = run.value.jobs;
152 return jobs;
153 })
154 .catch(() => ({}) as Record<string, Job[]>)
155 : Promise.resolve({} as Record<string, Job[]>);
156 // No checks at all on an open pull request: whether that is because the
157 // repository has no workflows, to offer a starter one.
158 const unchecked =
159 tab === "conversation" &&
160 pull.status === "open" &&
161 (found.value.statuses ?? []).length === 0 &&
162 (found.value.requiredChecks ?? []).length === 0;
163 const [comparison, used, noChecks] = await Promise.all([
164 tab === "changes" ? repos.compare(range.repoId, viewer, range.base, range.head) : null,
165 deps,
166 unchecked
167 ? actions
168 .workflows(path, viewer)
169 .then((found) => found.ok && found.value.length === 0)
170 .catch(() => false)
171 : false,
Issues and pull requests replace intents and attempts172 ]);
Fast pages, required checks on the branch, self-hosted runners, honest incidents173 const affects = used.ok ? used.value.usedBy : [];
Issues and pull requests replace intents and attempts174 return {
175 ...found.value,
Agents and memory, checks and conflicts, profiles, slug renames, custom domains176 workflowJobs,
Issues and pull requests replace intents and attempts177 tab,
178 session: session?.ok ? session.value : [],
179 // An empty comparison if it could not be made.
180 comparison: comparison && (comparison.ok ? comparison.value : EMPTY_COMPARISON),
181 viewer,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look182 // Write and up can merge.
183 canMerge: can.merge,
184 // Triage and up manage anyone's pull request; its author, their own.
185 canManage: can.triage || (viewer != null && viewer.id === pull.author.id),
186 // Telling its agent things, and re-running checks, spend compute: Write and up.
187 canRun: can.run,
Agents as a team: lifecycle, merge queue, billing and a new shell188 // A catch-up is pushed as the viewer: a fork takes pushes only from
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look189 // whoever opened it, a branch from anyone who can push.
190 canUpdate: pull.fork ? viewer?.id === pull.author.id : can.push,
Agents as a team: lifecycle, merge queue, billing and a new shell191 agentsEnabled,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look192 computeNote,
Agents as a team: lifecycle, merge queue, billing and a new shell193 members: members?.ok ? members.value.map((person) => person.username) : [],
194 requireUpToDate: settings.ok && settings.value.requireUpToDate,
195 mergeQueue: settings.ok && settings.value.mergeQueue,
196 requiredApprovals: settings.ok ? settings.value.requiredApprovals : 0,
197 canIgnoreChecks: !settings.ok || settings.value.allowIgnoringChecks,
Fast pages, required checks on the branch, self-hosted runners, honest incidents198 noChecks,
199 // Who may open the pull request that adds CI: anyone who can push.
200 canAddCi: can.push,
201 // Who may choose the required checks.
202 canProtect: can.manage_protection,
Issues and pull requests replace intents and attempts203 defaultBranch: repo.ok ? repo.value.defaultBranch : "main",
Fast pages, required checks on the branch, self-hosted runners, honest incidents204 affects,
205 // Streamed: the conversation shows first, the preview card after.
206 ...deploymentOf(deployed?.ok ? deployed.value : null, number, params.owner, affects, viewer),
Issues and pull requests replace intents and attempts207 };
208}
209
Pull requests say where they are live; the project menu shows what is coming210/**
211 * Where this pull request is live: its preview (or its latest build, while
212 * one is going or after one failed), and the previews of the projects
213 * that use this one, built against it.
214 */
Fast pages, required checks on the branch, self-hosted runners, honest incidents215function deploymentOf(
Pull requests say where they are live; the project menu shows what is coming216 list: { deployments: Deployment[]; live: LiveApp[] } | null,
217 number: number,
218 owner: string,
219 affects: { slug: string; name: string }[],
220 viewer: Viewer,
Fast pages, required checks on the branch, self-hosted runners, honest incidents221): { preview: LiveApp | null; build: Deployment | null; stacked: Promise<Stacked[]> } {
222 const preview = list?.live.find((app) => app.kind === "preview" && app.number === number) ?? null;
223 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 coming224 const branch = preview?.branch ?? build?.branch ?? null;
Fast pages, required checks on the branch, self-hosted runners, honest incidents225 // Streamed: another lookup per project, shown in the card once known.
226 const stacked: Promise<Stacked[]> = branch
227 ? Promise.all(
228 affects.slice(0, 5).map(async (project) => {
229 const theirs = await deployments.list({ workspace: owner, slug: project.slug }, viewer).catch(() => null);
230 const app = theirs?.ok ? theirs.value.live.find((a) => a.kind === "preview" && a.branch === branch) : undefined;
231 return app ? { name: project.name, slug: project.slug, url: app.url } : null;
232 }),
233 ).then((found) => found.filter((entry) => entry != null))
234 : Promise.resolve([]);
Pull requests say where they are live; the project menu shows what is coming235 return { preview, build, stacked };
236}
237
Fast pages, required checks on the branch, self-hosted runners, honest incidents238/** A project that uses this one, with its preview built against this change. */
239type Stacked = { name: string; slug: string; url: string };
240
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look241const PULL_NEEDS: Record<string, Capability> = {
242 stack: "run",
243 "agent-review": "run",
244 "rerun-workflow": "run",
Fast pages, required checks on the branch, self-hosted runners, honest incidents245 "rerun-failed": "run",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look246 message: "run",
247 merge: "merge",
248 unqueue: "merge",
249};
250
Issues and pull requests replace intents and attempts251export async function action({ request, params, context }: Route.ActionArgs) {
252 assertSameOrigin(request);
253 const user = requireUser(context, request);
254 const form = await request.formData();
255 const path = { namespace: params.owner, name: params.repo };
256 const number = Number(params.number);
257 const action = form.get("action");
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look258 // What each form needs beyond reading; work checks the rest.
259 const needed = typeof action === "string" ? PULL_NEEDS[action] : undefined;
260 const refused = needed ? await refusal(context, params, needed) : null;
261 if (refused) return { error: refused, action: String(action) };
Acceptance checks in sandboxes, line comments and review verdicts262 const verdict = form.get("verdict");
263 const line = Number(form.get("line"));
Agents as a team: lifecycle, merge queue, billing and a new shell264 /** Names ticked in a people picker, plus those typed beside it. */
265 const picked = (field: string) => [
266 ...form.getAll(field).map(String),
267 ...String(form.get("others") ?? "").split(/[\s,]+/),
268 ];
Project dependencies: addresses, preview stacks, Affects, and agents who know269 // The projects that use this one, built against this pull request's preview.
270 if (action === "stack") {
271 const built = await deployments.stack(user, { workspace: params.owner, slug: params.repo }, String(form.get("branch") ?? ""));
272 return built.ok
273 ? { action, notice: `Building ${built.value.join(", ")} against this preview. They appear on their Deployments pages.` }
274 : { action, error: built.error.message };
275 }
Agents as a team: lifecycle, merge queue, billing and a new shell276 // Asking a g1t agent for its review records the request, then starts it.
277 if (action === "agent-review") {
278 const asked = await work.updatePull(user, path, number, {
279 reviewers: [...form.getAll("reviewer").map(String), "g1t-agent"],
280 });
281 if (!asked.ok) return { error: asked.error.message, action };
282 }
Catching up with main takes seconds when the two sides touched different files283 // Catching up: merged and pushed in seconds when the two sides changed
284 // different files; otherwise handed to a sandbox, which takes a minute.
285 if (action === "update") {
286 const caught = await work.catchUpPull(user, path, number);
287 if (!caught.ok) return { action, error: caught.error.message };
288 const update = caught.value;
289 if (update.outcome !== "needs_agent") {
290 return {
291 action,
292 updated: { commit: update.commit, already: update.outcome === "up_to_date", at: Date.now() },
293 };
294 }
295 const started = await env.RUNNER.update(user, path, number);
296 if (!started.ok) return { action, error: started.error.message };
297 return { action, agent: { update, startedAt: Date.now() } };
298 }
Agents and memory, checks and conflicts, profiles, slug renames, custom domains299 // A workflow run's failed jobs, run again. Who may is the actions service's call.
300 if (action === "rerun-workflow") {
301 const rerun = await actions.rerun(user, path, String(form.get("run") ?? ""), true);
302 return rerun.ok ? null : { error: rerun.error.message, action };
303 }
Fast pages, required checks on the branch, self-hosted runners, honest incidents304 // Every failed run on its head, run again: Mission control's quick action.
305 if (action === "rerun-failed") {
306 const found = await work.getPull(path, number, user);
307 if (!found.ok) return { error: found.error.message, action };
308 const failed = [
309 ...new Set(
310 (found.value.statuses ?? [])
311 .filter((status) => status.state === "failure" || status.state === "error")
312 .map(runIdOf)
313 .filter((id) => id != null),
314 ),
315 ];
316 if (failed.length === 0) return { error: "No workflow run failed on its latest commit.", action };
317 const reruns = await Promise.all(failed.map((id) => actions.rerun(user, path, id, true)));
318 const refused = reruns.find((rerun) => !rerun.ok);
319 return refused && !refused.ok ? { error: refused.error.message, action } : null;
320 }
Issues and pull requests replace intents and attempts321 const result =
322 action === "merge"
Acceptance checks in sandboxes, line comments and review verdicts323 ? await work.mergePull(user, path, number, {
324 keepIssueOpen: form.get("keepIssueOpen") === "on",
325 ignoreChecks: form.get("ignoreChecks") === "on",
326 })
Agents as a team: lifecycle, merge queue, billing and a new shell327 : action === "unqueue"
328 ? await work.removeFromQueue(user, path, number)
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request329 : action === "message"
330 ? await work.messageAgent(user, path, number, String(form.get("body") ?? ""))
Issues and pull requests replace intents and attempts331 : action === "close"
332 ? await work.closePull(user, path, number)
Fast pages, required checks on the branch, self-hosted runners, honest incidents333 : action === "agent-review"
Agents as a team: lifecycle, merge queue, billing and a new shell334 ? await env.RUNNER.review(user, path, number)
335 : action === "reviewers"
336 ? await work.updatePull(user, path, number, { reviewers: picked("reviewer") })
337 : action === "assign"
338 ? await work.updatePull(user, path, number, { assignees: picked("assignee") })
Acceptance checks in sandboxes, line comments and review verdicts339 : action === "comment"
340 ? await work.addComment(user, path, number, {
341 body: String(form.get("body") ?? ""),
342 path: String(form.get("path") ?? "") || undefined,
343 line: line > 0 ? line : undefined,
344 verdict:
345 verdict === "approve" || verdict === "request_changes" ? verdict : undefined,
346 })
347 : await work.readyPull(user, path, number, String(form.get("summary") ?? ""));
Issues and pull requests replace intents and attempts348 return result.ok ? null : { error: result.error.message, action };
349}
350
Pull requests: title and tabs across the full width, with counts and the size of the change351/** How many, beside a tab's name, when there are any. */
352function TabCount({ n }: { n: number }) {
353 if (n <= 0) return null;
354 return (
355 <span className="rounded-full bg-raised px-1.5 py-px text-xs tabular-nums text-muted ring-1 ring-line">
356 {n.toLocaleString("en-US")}
357 </span>
358 );
359}
360
Issues and pull requests replace intents and attempts361function TabLink({
362 to,
363 active,
364 children,
365}: {
366 to: string;
367 active: boolean;
368 children: React.ReactNode;
369}) {
370 return (
371 <Link
372 to={to}
373 preventScrollReset
374 className={
Fast pages, required checks on the branch, self-hosted runners, honest incidents375 "-mb-px flex items-center gap-2 border-b-2 px-3 pb-2.5 text-sm transition-colors " +
Issues and pull requests replace intents and attempts376 (active
377 ? "border-accent font-medium text-fg"
378 : "border-transparent text-muted hover:text-fg")
379 }
380 >
381 {children}
382 </Link>
383 );
384}
385
386/** One step of the session, on the timeline's rail. */
387function Entry({ entry, agent }: { entry: SessionEntry; agent: string }) {
388 const isTool = entry.kind === "tool_call" || entry.kind === "tool_result";
389 const Icon =
390 entry.kind === "prompt"
391 ? User
392 : entry.kind === "note"
393 ? StickyNote
394 : isTool
395 ? Wrench
396 : Bot;
397 return (
398 <li className="relative pl-10">
399 <span
400 className={`absolute top-0.5 left-0 flex size-7 items-center justify-center rounded-full border bg-bg ${
401 entry.kind === "prompt"
402 ? "border-accent/50 text-accent"
403 : "border-line text-faint"
404 }`}
405 >
406 <Icon size={14} />
407 </span>
408 {isTool ? (
409 <details className="group rounded-lg border border-line bg-surface">
410 <summary className="flex cursor-pointer list-none items-center gap-2 px-3 py-1.5 text-sm">
411 <ChevronRight
412 size={14}
413 className="text-faint transition-transform group-open:rotate-90"
414 />
415 <span className="font-mono text-xs text-accent">
416 {entry.tool ?? "tool"}
417 </span>
418 <span className="truncate font-mono text-xs text-muted">
419 {entry.kind === "tool_result" ? "→ " : ""}
420 {entry.text.split("\n")[0]}
421 </span>
422 </summary>
423 <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 line424 <code>{entry.text}</code>
Issues and pull requests replace intents and attempts425 </pre>
426 </details>
427 ) : (
428 <div>
429 <p className="text-xs font-medium text-faint">
430 {entry.kind === "prompt"
431 ? "Prompt"
432 : entry.kind === "note"
433 ? "Note"
434 : agent}
435 </p>
436 {entry.kind === "message" ? (
437 <div className="mt-1">
438 <Markdown source={entry.text} />
439 </div>
Agents as a team: lifecycle, merge queue, billing and a new shell440 ) : entry.kind === "prompt" ? (
441 <div className="mt-1.5 max-h-[32rem] overflow-y-auto rounded-xl border border-line bg-surface p-4">
442 <Markdown source={entry.text} />
443 </div>
Issues and pull requests replace intents and attempts444 ) : (
Agents as a team: lifecycle, merge queue, billing and a new shell445 <p className="mt-1 text-[0.9375rem] leading-relaxed wrap-break-word whitespace-pre-wrap">
Issues and pull requests replace intents and attempts446 {entry.text}
447 </p>
448 )}
449 </div>
450 )}
451 {entry.commit && (
452 <p className="mt-1.5 flex items-center gap-1 font-mono text-xs text-faint">
453 <GitCommitHorizontal size={12} />
454 {entry.commit.slice(0, 7)}
455 </p>
456 )}
457 </li>
458 );
459}
460
Agents as a team: lifecycle, merge queue, billing and a new shell461/** The box at the foot of the conversation saying what stands before a merge. */
462function StatusBox({ children }: { children: React.ReactNode }) {
463 return (
464 <div className="flex gap-3">
465 <span className="hidden w-8 shrink-0 sm:block" />
466 <section className="min-w-0 grow divide-y divide-line overflow-hidden rounded-xl border border-line-strong bg-surface">
467 {children}
468 </section>
469 </div>
470 );
471}
472
473function StatusRow({
474 icon,
475 title,
476 children,
477}: {
478 icon: React.ReactNode;
479 title: string;
480 children?: React.ReactNode;
481}) {
482 return (
483 <div className="flex gap-3 px-4 py-3 text-sm">
484 <span className="mt-0.5 shrink-0">{icon}</span>
485 <div className="min-w-0">
486 <p className="font-medium">{title}</p>
487 {children && <div className="mt-0.5 text-muted">{children}</div>}
488 </div>
489 </div>
490 );
491}
492
Issues and pull requests replace intents and attempts493export default function PullPage({ loaderData, actionData, params }: Route.ComponentProps) {
494 const {
495 pull,
496 issue,
497 comments,
Acceptance checks in sandboxes, line comments and review verdicts498 checks,
Agents as a team: lifecycle, merge queue, billing and a new shell499 overlaps,
500 behind,
501 reviewPending,
502 lifecycle,
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request503 messages,
GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs504 statuses = [],
Fast pages, required checks on the branch, self-hosted runners, honest incidents505 requiredChecks = [],
506 noChecks,
507 canAddCi,
508 canProtect,
Agents and memory, checks and conflicts, profiles, slug renames, custom domains509 mergeable = "unknown",
510 conflicts = [],
511 earlierChecks = [],
512 workflowJobs,
Project dependencies: addresses, preview stacks, Affects, and agents who know513 affects,
514 preview,
Pull requests say where they are live; the project menu shows what is coming515 build,
516 stacked,
Agents as a team: lifecycle, merge queue, billing and a new shell517 landing,
518 stalled,
519 requireUpToDate,
520 mergeQueue,
521 requiredApprovals,
522 canIgnoreChecks,
523 canUpdate,
524 agentsEnabled,
525 members,
Issues and pull requests replace intents and attempts526 tab,
527 session,
528 comparison,
529 viewer,
530 canMerge,
531 canManage,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look532 canRun,
Issues and pull requests replace intents and attempts533 defaultBranch,
534 } = loaderData;
535 const base = `/${params.owner}/${params.repo}`;
536 const here = `${base}/pull/${pull.number}`;
Fast pages, required checks on the branch, self-hosted runners, honest incidents537 const checksSection = (jobs: Record<string, Job[]>) => (
538 <ChecksSection
539 run={checks}
540 required={requiredChecks}
541 statuses={statuses}
542 jobs={jobs}
543 pull={pull}
544 base={base}
545 earlier={earlierChecks}
546 canRerunWorkflows={canMerge}
547 settingsUrl={canProtect ? `${base}/settings/branches` : null}
548 error={actionData?.action === "rerun-failed" || actionData?.action === "rerun-workflow" ? actionData.error : null}
549 />
550 );
Pull requests from branches551 const remote = pull.fork
552 ? `https://g1t.sh/${pull.fork.namespace}/${pull.fork.name}.git`
553 : `https://g1t.sh/${params.owner}/${params.repo}.git`;
Issues and pull requests replace intents and attempts554 const active = pull.status === "draft" || pull.status === "open";
555
Acceptance checks in sandboxes, line comments and review verdicts556 // Follow an agent at work, or checks in progress, without a manual reload.
Issues and pull requests replace intents and attempts557 const revalidator = useRevalidator();
558 const working = pull.status === "draft";
Fast pages, required checks on the branch, self-hosted runners, honest incidents559 const checking = statuses.some((status) => status.state === "pending");
Acceptance checks in sandboxes, line comments and review verdicts560 const reviews = verdicts(comments);
Fast pages, required checks on the branch, self-hosted runners, honest incidents561 // What stands between this pull request and a merge, if anything: a
562 // required check that has not passed, or the merge queue taking it out.
563 const unchecked = requiredChecks.some((check) => check.state !== "success") || checks?.status === "failed";
564 const requiredFailed = requiredChecks.some((check) => check.state === "failure") || checks?.status === "failed";
Agents as a team: lifecycle, merge queue, billing and a new shell565 // Pull requests for other issues changing the same files will conflict;
566 // ones for the same issue are alternatives, and expected to.
567 const collisions = overlaps.filter((other) => other.issue == null || other.issue !== pull.issue);
568 const review = {
569 changesUrl: here + "?tab=changes",
570 // Nobody reviews their own pull request.
571 canJudge: active && viewer != null && viewer.id !== pull.author.id,
572 };
573 // Everyone whose review was asked for, then anyone who reviewed unasked.
574 const reviewerNames = [
575 ...new Set([...pull.reviewers, ...reviews.map(({ reviewer }) => reviewer)]),
576 ];
Catching up with main takes seconds when the two sides touched different files577 // A catch-up: the click shows at once; the answer says whether it is
578 // done already or a sandbox is on it.
579 const navigation = useNavigation();
580 const catchUpPending = navigation.state !== "idle" && navigation.formData?.get("action") === "update";
581 const catchUp = actionData?.action === "update" ? actionData : null;
582 const caughtUp = catchUp && "updated" in catchUp ? catchUp.updated : null;
583 const agentCatchUp = catchUp && "agent" in catchUp ? catchUp.agent : null;
584 // Pushed already: followed until the pull request's head is the merge,
585 // so its checks show starting again, for a minute at most.
586 const settling =
587 caughtUp != null && !caughtUp.already && pull.headCommit !== caughtUp.commit && Date.now() - caughtUp.at < 60_000;
588 // Followed until it is pushed, or for so long, never longer.
589 const catchingUp =
590 agentCatchUp != null && behind && Date.now() - agentCatchUp.startedAt < CATCH_UP_TIMEOUT_MS + REFRESH_MS;
Agents as a team: lifecycle, merge queue, billing and a new shell591 // g1t is taking a step of its own accord, so the page will change.
592 const moving =
593 lifecycle != null && lifecycle.stage !== "ready" && lifecycle.stage !== "needs_you";
Agents and memory, checks and conflicts, profiles, slug renames, custom domains594 // Whether it merges cleanly is being worked out, so the box will change.
595 const probing = active && mergeable === "checking";
596 const conflicting = active && mergeable === "conflicting";
Issues and pull requests replace intents and attempts597 useEffect(() => {
Catching up with main takes seconds when the two sides touched different files598 if (!working && !checking && !reviewPending && !catchingUp && !settling && !moving && !landing && !probing) return;
Issues and pull requests replace intents and attempts599 const timer = setInterval(() => {
600 if (document.visibilityState === "visible") revalidator.revalidate();
601 }, REFRESH_MS);
602 return () => clearInterval(timer);
Catching up with main takes seconds when the two sides touched different files603 }, [working, checking, reviewPending, catchingUp, settling, moving, landing, probing, revalidator]);
Agents and memory, checks and conflicts, profiles, slug renames, custom domains604 // Why the merge button cannot be pressed, if it cannot.
605 const mergeBlocked = conflicting
606 ? "Resolve the conflicts first."
607 : probing
608 ? "Waiting to find out whether it merges cleanly."
609 : behind && requireUpToDate
610 ? `This repository requires it to be up to date with ${defaultBranch} first.`
Fast pages, required checks on the branch, self-hosted runners, honest incidents611 : unchecked && !canIgnoreChecks
612 ? `The checks ${defaultBranch} requires have to pass first.`
613 : null;
Issues and pull requests replace intents and attempts614
615 return (
Agents as a team: lifecycle, merge queue, billing and a new shell616 // 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 change617 <div>
618 {/* The title and the tabs run the full width; what is happening on
619 the pull request sits at the top of Conversation. */}
620 <header>
Issues and pull requests replace intents and attempts621 <h2 className="text-2xl font-semibold tracking-tight text-balance">
622 {pull.title} <span className="font-normal text-faint">#{pull.number}</span>
623 </h2>
624 <div className="mt-3 flex flex-wrap items-center gap-x-3 gap-y-2 text-sm text-muted">
625 <PullState status={pull.status} />
626 <span className="flex items-center gap-2">
Agents as a team: lifecycle, merge queue, billing and a new shell627 <Avatar
628 name={(pull.status === "merged" && pull.mergedBy) || pull.author.username}
629 size={18}
630 />
Issues and pull requests replace intents and attempts631 <span>
Agents and memory, checks and conflicts, profiles, slug renames, custom domains632 <PersonLink
633 name={(pull.status === "merged" && pull.mergedBy) || pull.author.username}
634 className="font-medium text-fg hover:underline"
635 />{" "}
Pull requests from branches636 {pull.status === "merged" ? "merged" : "wants to merge"}
637 {pull.branch && (
638 <>
639 {" "}
640 <span className="font-mono text-fg">{pull.branch}</span>
641 </>
642 )}{" "}
643 into <span className="font-mono text-fg">{defaultBranch}</span>
Issues and pull requests replace intents and attempts644 </span>
645 </span>
Pull requests from branches646 {/* A pull request from a branch was made by its author, not an agent. */}
647 {!pull.branch && (
648 <span className="flex items-center gap-1.5 font-mono text-xs">
649 <Bot size={14} />
650 {pull.agent}
651 {pull.runtime === "hosted" && <span className="text-faint">on g1t</span>}
652 </span>
653 )}
Issues and pull requests replace intents and attempts654 </div>
655
Acceptance checks in sandboxes, line comments and review verdicts656 {reviews.length > 0 && (
657 <p className="mt-3 flex flex-wrap items-center gap-x-4 gap-y-1 text-sm">
658 {reviews.map(({ reviewer, verdict }) => (
659 <span
660 key={reviewer}
661 className={`flex items-center gap-1.5 ${
662 verdict === "approve" ? "text-accent" : "text-danger"
663 }`}
664 >
665 {verdict === "approve" ? <CircleCheck size={15} /> : <CircleSlash size={15} />}
666 {verdict === "approve" ? "Approved by" : "Changes requested by"}{" "}
667 <span className="font-medium">{reviewer}</span>
668 </span>
669 ))}
670 </p>
671 )}
672
Fast pages, required checks on the branch, self-hosted runners, honest incidents673 <nav className="mt-6 flex items-end gap-1 overflow-x-auto border-b border-line">
Issues and pull requests replace intents and attempts674 <TabLink to={here} active={tab === "conversation"}>
675 <MessageSquare size={15} />
676 Conversation
Pull requests: title and tabs across the full width, with counts and the size of the change677 <TabCount n={comments.length} />
Issues and pull requests replace intents and attempts678 </TabLink>
679 <TabLink to={here + "?tab=session"} active={tab === "session"}>
680 <MessagesSquare size={15} />
681 Session
682 </TabLink>
683 <TabLink to={here + "?tab=changes"} active={tab === "changes"}>
684 <FileDiff size={15} />
Pull requests: title and tabs across the full width, with counts and the size of the change685 Files changed
686 <TabCount n={pull.files.length} />
Issues and pull requests replace intents and attempts687 </TabLink>
Pull requests: title and tabs across the full width, with counts and the size of the change688 {pull.files.length > 0 && (
689 <span className="ml-auto hidden pb-2.5 font-mono text-xs sm:inline">
690 <span className="text-accent">+{pull.files.reduce((sum, file) => sum + file.additions, 0).toLocaleString("en-US")}</span>{" "}
691 <span className="text-danger">−{pull.files.reduce((sum, file) => sum + file.deletions, 0).toLocaleString("en-US")}</span>
692 </span>
693 )}
Issues and pull requests replace intents and attempts694 </nav>
Pull requests: title and tabs across the full width, with counts and the size of the change695 </header>
696 <div className={`mt-6 grid gap-8 ${tab === "changes" ? "" : "lg:grid-cols-[1fr_19rem]"}`}>
697 <div className="min-w-0">
698 {/* What is happening on it now: its stage, its agent, what people told
699 the agent, and what else touches the same files. On the conversation,
700 so the tabs sit right under the title. */}
701 {tab === "conversation" && (
702 <div>
703 {issue && (
704 <Link
705 to={`${base}/issues/${issue.number}`}
706 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"
707 >
708 <IssueIcon issue={issue} />
709 <span className="text-muted">
710 {pull.status === "merged" && issue.resolvedBy === pull.number
711 ? "Resolved"
712 : "For issue"}
713 </span>
714 <span className="truncate font-medium">{issue.title}</span>
715 <span className="text-faint">#{issue.number}</span>
716 {issue.pullCount > 1 && (
717 <span className="ml-auto shrink-0 text-xs text-faint">
718 1 of {issue.pullCount} pull requests
Pull requests show their tabs right under the title; agent panels move into Conversation719 </span>
Pull requests: title and tabs across the full width, with counts and the size of the change720 )}
721 </Link>
722 )}
723 {pull.status === "merged" && (
724 <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">
725 <GitMerge size={16} className="shrink-0 text-merged" />
Agents as a team: lifecycle, merge queue, billing and a new shell726 <span>
Pull requests: title and tabs across the full width, with counts and the size of the change727 Merged into <span className="font-mono">{defaultBranch}</span> by{" "}
728 <span className="font-medium">{pull.mergedBy}</span>{" "}
729 {/* Not an account: the repository's settings said to merge it. */}
730 {pull.mergedBy === "g1t" && "automatically, once it was ready, "}
731 {pull.mergedAt && <TimeAgo at={pull.mergedAt} />}
732 {pull.headCommit && (
733 <span className="font-mono text-muted"> · {pull.headCommit.slice(0, 7)}</span>
Agents as a team: lifecycle, merge queue, billing and a new shell734 )}
735 </span>
Pull requests: title and tabs across the full width, with counts and the size of the change736 </p>
737 )}
738 {pull.supersededBy != null && (
739 <p className="mt-4 rounded-xl border border-line bg-surface px-4 py-2.5 text-sm text-muted">
740 Closed because{" "}
741 <Link
742 to={`${base}/pull/${pull.supersededBy}`}
743 className="font-medium text-fg hover:underline"
744 >
745 #{pull.supersededBy}
746 </Link>{" "}
747 was merged for this issue instead.
748 </p>
749 )}
750 {lifecycle && <LifecyclePanel lifecycle={lifecycle} />}
751 {/* 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 step752 <AgentPanel
753 owner={params.owner}
754 repo={params.repo}
755 number={pull.number}
756 stage={lifecycle?.stage ?? null}
757 confidence={pull.confidence ?? null}
758 />
Pull requests: title and tabs across the full width, with counts and the size of the change759
760 {/* 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 look761 {canRun &&
Pull requests: title and tabs across the full width, with counts and the size of the change762 pull.runtime === "hosted" &&
763 (working || ["working", "revising", "catching_up", "answering"].includes(lifecycle?.stage ?? "")) && (
764 <Form method="post" className="mt-4 rounded-2xl bg-surface p-4 ring-1 ring-merged/30">
765 <p className="flex items-center gap-2 text-sm font-medium">
766 <Sparkles size={15} className="text-merged" />
767 Message the agent
768 </p>
769 <p className="mt-1 text-xs text-muted">
770 A correction, a hint, a change of plan. It reads it at its next step, without
771 starting over.
772 </p>
773 <div className="mt-3 flex gap-2">
774 <input type="hidden" name="action" value="message" />
775 <input
776 name="body"
777 required
778 autoComplete="off"
779 data-1p-ignore
780 placeholder="Keep the old flag working too…"
781 className="h-9 min-w-0 grow rounded-md bg-bg px-3 text-sm ring-1 ring-line outline-none placeholder:text-faint focus:ring-merged/60"
Agents as a team: lifecycle, merge queue, billing and a new shell782 />
Pull requests: title and tabs across the full width, with counts and the size of the change783 <Button type="submit">Send</Button>
784 </div>
785 </Form>
Agents as a team: lifecycle, merge queue, billing and a new shell786 )}
Pull requests: title and tabs across the full width, with counts and the size of the change787 {messages.length > 0 && (
788 <ul className="mt-3 space-y-1.5">
789 {messages.map((message) => (
790 <li key={message.id} className="flex items-start gap-2 text-sm">
791 <Avatar name={message.author} size={18} />
792 <span className="min-w-0 grow">
793 <span className="font-medium">
794 {message.fromNumber != null ? `The agent on #${message.fromNumber}` : message.author}
795 </span>{" "}
796 <span className="text-muted">
797 {message.kind === "question"
798 ? "asked:"
799 : message.kind === "handoff"
800 ? "handed over:"
801 : message.kind === "answer"
802 ? "answered:"
803 : "to the agent:"}
804 </span>{" "}
805 {message.body}
806 {message.answer && (
807 <span className="mt-1 block border-l-2 border-merged/40 pl-2 text-muted">
808 {message.declined ? "Declined: " : "Answer: "}
809 {message.answer}
810 </span>
811 )}
812 </span>
813 <span className={`shrink-0 text-xs ${message.deliveredAt ? "text-accent" : "text-faint"}`}>
814 {message.deliveredAt ? "read by the agent" : "waiting for its next step"}
815 </span>
816 </li>
817 ))}
818 </ul>
819 )}
820 {collisions.length > 0 && active && (
821 <div className="mt-4 rounded-xl border border-line bg-surface px-4 py-3 text-sm">
822 <p className="flex items-center gap-2.5 font-medium">
823 <Radar size={16} className="shrink-0 text-info" />
824 Other work is changing the same files
825 </p>
826 <ul className="mt-2 space-y-1.5">
827 {collisions.map((other) => (
828 <li key={other.number} className="flex flex-wrap items-baseline gap-x-2 text-muted">
829 <Link
830 to={`${base}/pull/${other.number}`}
831 className="font-medium text-fg hover:underline"
832 >
833 {other.title} <span className="font-normal text-faint">#{other.number}</span>
834 </Link>
835 <span className="font-mono text-xs">{other.paths.join(", ")}</span>
836 </li>
837 ))}
838 </ul>
839 <p className="mt-2 text-xs text-faint">
840 Whichever merges second will have to catch up, and may conflict.
841 </p>
842 </div>
843 )}
844 </div>
845 )}
846 <div className="mt-5">
847 {comparison ? (
848 <DiffView
849 comparison={comparison}
850 review={{
851 comments: comments.filter((comment) => comment.path),
852 canComment: Boolean(viewer),
853 }}
854 />
855 ) : tab === "session" ? (
856 session.length === 0 ? (
857 <EmptyState title="Nothing recorded yet">
858 The agent's prompts, reasoning and tool calls appear here as it
859 works.
860 </EmptyState>
861 ) : (
862 <ol className="relative space-y-5 before:absolute before:top-2 before:bottom-2 before:left-3.25 before:w-px before:bg-line">
863 {session.map((entry) => (
864 <Entry key={entry.seq} entry={entry} agent={pull.agent} />
865 ))}
866 </ol>
867 )
868 ) : (
869 <div className="space-y-4">
870 <TimelineItem
871 author={pull.author.username}
872 at={pull.createdAt}
873 action={
874 <span>
875 opened this pull request
876 {!pull.branch && (
877 <>
878 {" "}
879 with <span className="font-mono text-xs">{pull.agent}</span>
880 </>
881 )}
882 </span>
883 }
884 >
885 {pull.body ? (
886 <Markdown source={pull.body} repo={{ namespace: params.owner, name: params.repo }} />
887 ) : (
888 <p className="text-sm text-muted">
889 {working
890 ? "No description yet. It is written when the pull request is marked ready for review."
891 : "No description."}
892 </p>
893 )}
894 {canManage && pull.status === "open" && (
895 <details className="mt-3 border-t border-line pt-3 text-sm">
896 <summary className="cursor-pointer text-xs text-faint hover:text-fg">
897 Edit description
898 </summary>
899 <Form method="post" className="mt-3 space-y-2">
900 <Textarea
901 name="summary"
902 rows={6}
903 placeholder="What changed and why"
904 defaultValue={pull.body ?? ""}
905 />
906 <Button type="submit">Save</Button>
907 </Form>
908 </details>
909 )}
910 </TimelineItem>
Agents as a team: lifecycle, merge queue, billing and a new shell911
Pull requests: title and tabs across the full width, with counts and the size of the change912 <CommentList comments={comments} review={review} base={base} />
Agents as a team: lifecycle, merge queue, billing and a new shell913
Fast pages, required checks on the branch, self-hosted runners, honest incidents914 {(preview || build) && <DeploymentCard preview={preview} build={build} stacked={stacked} base={base} />}
Pull requests say where they are live; the project menu shows what is coming915
Pull requests: title and tabs across the full width, with counts and the size of the change916 {pull.status === "draft" && (
917 <StatusBox>
918 <StatusRow icon={<Loader size={16} className="text-faint" />} title="This is a draft">
919 It is still being worked on. It can be reviewed and merged once it is
920 marked ready.
921 </StatusRow>
922 {canManage && (
923 <Form method="post" className="space-y-2 px-4 py-3">
924 <Textarea name="summary" rows={3} placeholder="What changed and why" />
925 <Button type="submit">Mark ready for review</Button>
926 </Form>
927 )}
928 </StatusBox>
929 )}
Agents as a team: lifecycle, merge queue, billing and a new shell930
Pull requests: title and tabs across the full width, with counts and the size of the change931 {pull.status === "open" && (
932 <StatusBox>
Fast pages, required checks on the branch, self-hosted runners, honest incidents933 {/* Checks by status at once; job by job when the runs are read. */}
934 <Suspense fallback={checksSection({})}>
935 <Await resolve={workflowJobs}>{checksSection}</Await>
936 </Suspense>
937 {noChecks && <AddCiPrompt owner={params.owner} repo={params.repo} canAdd={canAddCi} compact />}
Agents as a team: lifecycle, merge queue, billing and a new shell938 <StatusRow
Pull requests: title and tabs across the full width, with counts and the size of the change939 icon={
940 reviews.some(({ verdict }) => verdict === "request_changes") ? (
941 <CircleSlash size={16} className="text-danger" />
942 ) : reviews.length > 0 ? (
943 <CircleCheck size={16} className="text-accent" />
944 ) : (
945 <MessageSquare size={16} className="text-faint" />
946 )
947 }
948 title={
949 reviews.length === 0
950 ? "No reviews yet"
951 : reviews
952 .map(
953 ({ reviewer, verdict }) =>
954 `${verdict === "approve" ? "Approved by" : "Changes requested by"} ${reviewer}`,
955 )
956 .join(" · ")
957 }
Agents as a team: lifecycle, merge queue, billing and a new shell958 >
Pull requests: title and tabs across the full width, with counts and the size of the change959 {reviewPending && "A g1t agent is reviewing it now. "}
960 {requiredApprovals > 0 &&
961 `This repository requires ${requiredApprovals} approving ${
962 requiredApprovals === 1 ? "review" : "reviews"
963 } before merging.`}
Agents as a team: lifecycle, merge queue, billing and a new shell964 </StatusRow>
Pull requests: title and tabs across the full width, with counts and the size of the change965 {landing ? (
Agents as a team: lifecycle, merge queue, billing and a new shell966 <StatusRow
Pull requests: title and tabs across the full width, with counts and the size of the change967 icon={<Loader size={16} className="animate-spin text-accent" />}
968 title="Merging"
969 >
970 {defaultBranch} has moved, so g1t is bringing this up to date first. It
971 lands as soon as that is done. Watch it in the Session tab.
972 </StatusRow>
Catching up with main takes seconds when the two sides touched different files973 ) : agentCatchUp && behind ? (
974 <CatchUpProgress
975 owner={params.owner}
976 repo={params.repo}
977 number={pull.number}
978 defaultBranch={defaultBranch}
979 behind={behind}
980 update={agentCatchUp.update}
981 startedAt={agentCatchUp.startedAt}
982 retrying={catchUpPending}
983 />
Pull requests: title and tabs across the full width, with counts and the size of the change984 ) : conflicting ? (
985 <ConflictsSection
986 conflicts={conflicts}
987 pull={pull}
988 owner={params.owner}
989 repo={params.repo}
990 defaultBranch={defaultBranch}
991 changesUrl={here + "?tab=changes"}
992 canResolve={canUpdate && agentsEnabled}
Catching up with main takes seconds when the two sides touched different files993 resolving={catchUpPending}
994 error={catchUp && "error" in catchUp ? catchUp.error : null}
Agents as a team: lifecycle, merge queue, billing and a new shell995 />
Pull requests: title and tabs across the full width, with counts and the size of the change996 ) : probing ? (
997 <MergeabilityRow mergeable={mergeable} defaultBranch={defaultBranch} />
998 ) : behind ? (
999 <StatusRow
1000 icon={<GitPullRequestArrow size={16} className="text-info" />}
1001 title={`${defaultBranch} has moved since this was made`}
1002 >
1003 {mergeable === "clean" && "It has no conflicts with it. "}
1004 {requireUpToDate
1005 ? "This repository requires pull requests to be up to date, so it has to catch up before it can merge."
1006 : "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 files1007 {canUpdate && (
1008 <Form method="post" className="mt-2">
Pull requests: title and tabs across the full width, with counts and the size of the change1009 <input type="hidden" name="action" value="update" />
Catching up with main takes seconds when the two sides touched different files1010 <Button variant="quiet" type="submit" disabled={catchUpPending}>
1011 {catchUpPending && <Loader size={14} className="animate-spin" />}
1012 {catchUpPending ? `Merging ${defaultBranch} in…` : `Catch up with ${defaultBranch} now`}
Pull requests: title and tabs across the full width, with counts and the size of the change1013 </Button>
1014 </Form>
1015 )}
Catching up with main takes seconds when the two sides touched different files1016 {catchUp && "error" in catchUp && <ErrorText>{catchUp.error}</ErrorText>}
Pull requests: title and tabs across the full width, with counts and the size of the change1017 </StatusRow>
1018 ) : (
1019 pull.headCommit && (
1020 <StatusRow
1021 icon={<CircleCheck size={16} className="text-accent" />}
Catching up with main takes seconds when the two sides touched different files1022 title={
1023 (caughtUp && !caughtUp.already) || agentCatchUp
1024 ? `Brought up to date with ${defaultBranch}`
1025 : `Up to date with ${defaultBranch}`
1026 }
1027 >
1028 {caughtUp && !caughtUp.already && (
1029 <>
1030 Merged <span className="font-mono">{defaultBranch}</span> in as{" "}
1031 <span className="font-mono text-fg">{caughtUp.commit.slice(0, 7)}</span>. Its checks run
1032 again on the new commit.
1033 </>
1034 )}
1035 {agentCatchUp && "g1t-agent merged it in and pushed the result. Its checks run again on the new commit."}
1036 </StatusRow>
Pull requests: title and tabs across the full width, with counts and the size of the change1037 )
1038 )}
1039 {stalled && !lifecycle && (
1040 <StatusRow icon={<Hand size={16} className="text-warn" />} title="Needs you">
1041 {stalled}
1042 </StatusRow>
1043 )}
1044 {canMerge && lifecycle?.stage === "queued" && (
1045 <Form method="post" className="flex flex-wrap items-center gap-3 px-4 py-3">
1046 <ButtonLink to={`${base}/queue`}>
1047 <Layers size={15} />
1048 See the queue
1049 </ButtonLink>
1050 <Button variant="quiet" type="submit" name="action" value="unqueue">
1051 Remove from the queue
Agents as a team: lifecycle, merge queue, billing and a new shell1052 </Button>
Pull requests: title and tabs across the full width, with counts and the size of the change1053 </Form>
1054 )}
1055 {canMerge && !landing && lifecycle?.stage !== "queued" && (
1056 <Form method="post" className="space-y-3 px-4 py-3">
1057 {issue?.state === "open" && (
1058 <CheckboxOption
1059 name="keepIssueOpen"
1060 label={`Keep #${issue.number} open. This is only part of the work.`}
1061 labelClassName="text-xs text-muted"
1062 />
1063 )}
1064 {unchecked && canIgnoreChecks && (
1065 <CheckboxOption
1066 name="ignoreChecks"
Fast pages, required checks on the branch, self-hosted runners, honest incidents1067 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 change1068 labelClassName="text-xs text-muted"
1069 />
1070 )}
1071 <div className="flex flex-wrap items-center gap-3">
1072 <Button
1073 variant="accent"
1074 type="submit"
1075 name="action"
1076 value="merge"
1077 disabled={mergeBlocked != null}
1078 title={mergeBlocked ?? undefined}
1079 >
1080 {mergeQueue ? <Layers size={15} /> : <GitMerge size={15} />}
1081 {mergeQueue ? "Add to the merge queue" : `Merge into ${defaultBranch}`}
1082 </Button>
1083 <span className={`text-xs ${mergeBlocked ? "text-danger" : "text-muted"}`}>
1084 {mergeBlocked ?? (mergeQueue
1085 ? `Tested together with everything ahead of it, then lands on ${defaultBranch}.`
1086 : issue?.state === "open"
1087 ? `Closes issue #${issue.number}, and any other pull requests still open for it.`
1088 : `Lands its commits on ${defaultBranch}.`)}
1089 </span>
1090 </div>
1091 {actionData?.action === "merge" && (
1092 <ErrorText>{actionData.error}</ErrorText>
1093 )}
1094 </Form>
1095 )}
1096 </StatusBox>
1097 )}
Agents as a team: lifecycle, merge queue, billing and a new shell1098
Pull requests: title and tabs across the full width, with counts and the size of the change1099 <CommentForm
1100 author={viewer?.username ?? null}
1101 resetKey={comments.length}
1102 review={review}
1103 />
1104 {canManage && active && (
1105 <Form method="post" className="flex justify-end">
1106 <Button variant="quiet" type="submit" name="action" value="close">
1107 Close pull request
1108 </Button>
1109 </Form>
1110 )}
1111 {actionData &&
Fast pages, required checks on the branch, self-hosted runners, honest incidents1112 !["merge", "comment", "rerun-failed", "rerun-workflow", "update", "agent-review", "reviewers", "assign"].includes(
Pull requests: title and tabs across the full width, with counts and the size of the change1113 String(actionData.action),
1114 ) && <ErrorText>{actionData.error}</ErrorText>}
1115 </div>
1116 )}
1117 {actionData?.action === "comment" && (
1118 <div className="mt-2">
1119 <ErrorText>{actionData.error}</ErrorText>
1120 </div>
1121 )}
1122 </div>
1123 </div>
1124
1125 <aside className={tab === "changes" ? "hidden" : "space-y-6"}>
1126 {/* An open pull request shows its checks in full in the merge box. */}
Fast pages, required checks on the branch, self-hosted runners, honest incidents1127 {pull.status !== "open" && <WorkflowStatuses statuses={statuses} />}
Pull requests: title and tabs across the full width, with counts and the size of the change1128 {affects.length > 0 && (
1129 <section>
1130 <h3 className="flex items-center gap-1.5 text-sm font-medium">
1131 <Network size={14} className="text-faint" />
1132 Affects
1133 </h3>
1134 <p className="mt-1 text-xs text-muted">Projects that use this one, and so may feel this change:</p>
1135 <ul className="mt-2 space-y-1 text-sm">
1136 {affects.map((project) => (
1137 <li key={project.slug} className="flex items-center justify-between gap-2">
1138 <Link to={`/${params.owner}/${project.slug}`} className="hover:underline">
1139 {project.name}
1140 </Link>
1141 {project.as && <code className="font-mono text-xs text-faint">{project.as}</code>}
1142 </li>
1143 ))}
1144 </ul>
1145 {preview?.branch && canMerge && (
1146 <Form method="post" className="mt-3">
1147 <input type="hidden" name="action" value="stack" />
1148 <input type="hidden" name="branch" value={preview.branch} />
1149 <button
1150 type="submit"
1151 className="w-full rounded-md border border-line px-3 py-1.5 text-xs text-muted transition-colors hover:border-line-strong hover:text-fg"
1152 >
1153 Preview them against this change
1154 </button>
Agents as a team: lifecycle, merge queue, billing and a new shell1155 </Form>
1156 )}
Pull requests: title and tabs across the full width, with counts and the size of the change1157 {actionData?.action === "stack" &&
1158 ("notice" in actionData ? (
1159 <p className="mt-2 text-xs text-accent">{String(actionData.notice)}</p>
1160 ) : (
1161 <ErrorText>{actionData.error}</ErrorText>
1162 ))}
1163 </section>
Acceptance checks in sandboxes, line comments and review verdicts1164 )}
Issues and pull requests replace intents and attempts1165
Project dependencies: addresses, preview stacks, Affects, and agents who know1166 <section>
Pull requests: title and tabs across the full width, with counts and the size of the change1167 <h3 className="text-sm font-medium">Reviewers</h3>
1168 <ul className="mt-2 space-y-1.5 text-sm">
1169 {reviewerNames.map((name) => {
1170 const verdict = reviews.find(({ reviewer }) => reviewer === name)?.verdict;
1171 const pending = name === "g1t-agent" && reviewPending;
1172 return (
1173 <li key={name} className="flex items-center gap-2 px-1">
1174 {name === "g1t-agent" ? (
1175 <Sparkles size={16} className="shrink-0 text-accent" />
1176 ) : (
1177 <Avatar name={name} size={20} />
1178 )}
1179 <span className="grow truncate font-mono text-xs">{name}</span>
1180 {pending ? (
1181 <span className="flex items-center gap-1.5 text-xs text-muted">
1182 <span className="size-1.5 animate-pulse rounded-full bg-accent" />
1183 Reviewing
1184 </span>
1185 ) : verdict === "approve" ? (
1186 <span className="flex items-center gap-1 text-xs text-accent">
1187 <CircleCheck size={13} /> Approved
1188 </span>
1189 ) : verdict === "request_changes" ? (
1190 <span className="flex items-center gap-1 text-xs text-danger">
1191 <CircleSlash size={13} /> Changes requested
1192 </span>
1193 ) : (
1194 <span className="text-xs text-faint">Review requested</span>
1195 )}
1196 </li>
1197 );
1198 })}
1199 {reviewerNames.length === 0 && (
1200 <li className="px-1 text-xs text-faint">No reviews requested yet.</li>
1201 )}
Project dependencies: addresses, preview stacks, Affects, and agents who know1202 </ul>
Pull requests: title and tabs across the full width, with counts and the size of the change1203 {pull.status === "open" && canManage && (
1204 <div className="mt-3 space-y-2">
1205 {agentsEnabled && !reviewPending && (
1206 <Form method="post">
1207 <input type="hidden" name="action" value="agent-review" />
1208 {pull.reviewers.map((name) => (
1209 <input key={name} type="hidden" name="reviewer" value={name} />
1210 ))}
1211 <div className="*:w-full">
1212 <Button variant="quiet" type="submit">
1213 <Sparkles size={14} className="text-accent" />
1214 Request review from g1t agent
1215 </Button>
1216 </div>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1217 <ComputeNote note={loaderData.computeNote} />
Pull requests: title and tabs across the full width, with counts and the size of the change1218 </Form>
1219 )}
1220 <details>
1221 <summary className="cursor-pointer text-xs text-faint hover:text-fg">
1222 Request review from people
1223 </summary>
1224 <Form method="post" className="mt-2 space-y-2" key={pull.reviewers.join()}>
1225 <input type="hidden" name="action" value="reviewers" />
1226 {pull.reviewers.includes("g1t-agent") && (
1227 <input type="hidden" name="reviewer" value="g1t-agent" />
1228 )}
1229 <PeoplePicker
1230 name="reviewer"
1231 members={members.filter((name) => name !== pull.author.username)}
1232 chosen={pull.reviewers.filter((name) => name !== "g1t-agent")}
1233 />
1234 <Button variant="quiet" type="submit">
1235 Save reviewers
1236 </Button>
1237 </Form>
1238 </details>
1239 </div>
Project dependencies: addresses, preview stacks, Affects, and agents who know1240 )}
Pull requests: title and tabs across the full width, with counts and the size of the change1241 {["agent-review", "reviewers"].includes(String(actionData?.action)) && (
1242 <ErrorText>{actionData?.error}</ErrorText>
1243 )}
Project dependencies: addresses, preview stacks, Affects, and agents who know1244 </section>
Acceptance checks in sandboxes, line comments and review verdicts1245
Pull requests: title and tabs across the full width, with counts and the size of the change1246 <section>
1247 <h3 className="text-sm font-medium">Assignees</h3>
1248 <ul className="mt-2 space-y-1.5 text-sm">
1249 {pull.assignees.map((name) => (
Agents as a team: lifecycle, merge queue, billing and a new shell1250 <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 change1251 <Avatar name={name} size={20} />
Agents as a team: lifecycle, merge queue, billing and a new shell1252 <span className="grow truncate font-mono text-xs">{name}</span>
1253 </li>
Pull requests: title and tabs across the full width, with counts and the size of the change1254 ))}
1255 {pull.assignees.length === 0 && (
1256 <li className="px-1 text-xs text-faint">No one yet.</li>
1257 )}
1258 </ul>
1259 {viewer && canManage && active && (
1260 <div className="mt-3 space-y-2">
1261 {!pull.assignees.includes(viewer.username) && (
1262 <Form method="post">
1263 <input type="hidden" name="action" value="assign" />
1264 {pull.assignees.map((name) => (
1265 <input key={name} type="hidden" name="assignee" value={name} />
1266 ))}
1267 <input type="hidden" name="assignee" value={viewer.username} />
1268 <div className="*:w-full">
1269 <Button variant="quiet" type="submit">
1270 Assign yourself
1271 </Button>
1272 </div>
1273 </Form>
1274 )}
1275 <details>
1276 <summary className="cursor-pointer text-xs text-faint hover:text-fg">
1277 Assign people
1278 </summary>
1279 <Form method="post" className="mt-2 space-y-2" key={pull.assignees.join()}>
1280 <input type="hidden" name="action" value="assign" />
1281 <PeoplePicker name="assignee" members={members} chosen={pull.assignees} />
Agents as a team: lifecycle, merge queue, billing and a new shell1282 <Button variant="quiet" type="submit">
Pull requests: title and tabs across the full width, with counts and the size of the change1283 Save assignees
Agents as a team: lifecycle, merge queue, billing and a new shell1284 </Button>
Pull requests: title and tabs across the full width, with counts and the size of the change1285 </Form>
1286 </details>
1287 </div>
Issues and pull requests replace intents and attempts1288 )}
Pull requests: title and tabs across the full width, with counts and the size of the change1289 {actionData?.action === "assign" && <ErrorText>{actionData.error}</ErrorText>}
1290 </section>
Issues and pull requests replace intents and attempts1291
Pull requests: title and tabs across the full width, with counts and the size of the change1292 <section>
1293 <h3 className="text-sm font-medium">Working copy</h3>
1294 {pull.branch ? (
1295 <>
1296 <p className="mt-1 text-xs text-muted">
1297 <GitBranch size={13} className="mr-1 inline align-[-2px]" />
1298 Branch <span className="font-mono text-fg">{pull.branch}</span> of this
1299 repository. Pushes to it show up here.
1300 </p>
1301 <div className="mt-2">
1302 <CopyLine text={`git clone -b ${pull.branch} ${remote}`} />
1303 </div>
1304 </>
1305 ) : (
1306 <>
1307 <p className="mt-1 text-xs text-muted">
1308 This fork belongs to the pull request. Pushes to it show up here.
1309 </p>
1310 <div className="mt-2">
1311 <CopyLine text={`git clone ${remote}`} />
1312 </div>
1313 </>
1314 )}
1315 <p className="mt-3 flex items-center gap-1.5 font-mono text-xs text-faint">
1316 <GitCommitHorizontal size={13} />
1317 {pull.headCommit?.slice(0, 12) ?? "no commits pushed yet"}
1318 </p>
1319 </section>
1320 </aside>
1321 </div>
Issues and pull requests replace intents and attempts1322 </div>
1323 );
1324}
Pull requests say where they are live; the project menu shows what is coming1325
1326/** Where the pull request's change is live, as GitHub shows a deployment. */
1327function DeploymentCard({
1328 preview,
1329 build,
1330 stacked,
1331 base,
1332}: {
1333 preview: LiveApp | null;
1334 build: Deployment | null;
Fast pages, required checks on the branch, self-hosted runners, honest incidents1335 stacked: Promise<Stacked[]>;
Pull requests say where they are live; the project menu shows what is coming1336 base: string;
1337}) {
1338 const building = build?.status === "queued" || build?.status === "building";
1339 const failed = build?.status === "failed";
1340 return (
1341 <section className="ml-12 overflow-hidden rounded-xl border border-line bg-surface">
1342 <div className="flex flex-wrap items-center gap-3 px-4 py-3">
1343 <span
1344 className={`flex size-8 shrink-0 items-center justify-center rounded-full ring-1 ${
1345 failed ? "text-danger ring-danger/40" : building ? "text-warn ring-warn/40" : "text-accent ring-accent/40"
1346 }`}
1347 >
1348 {building ? <Loader size={15} className="animate-spin" /> : <Rocket size={15} />}
1349 </span>
1350 <div className="min-w-0 grow">
1351 <p className="text-sm font-medium">
1352 {building
1353 ? preview
1354 ? "Deploying the latest push. The last preview is still live."
1355 : "Deploying a preview"
1356 : failed
1357 ? "The preview failed to deploy"
1358 : "This branch is live"}
1359 </p>
1360 {preview ? (
1361 <a href={preview.url} className="mt-0.5 block truncate font-mono text-xs text-muted hover:text-accent">
1362 {preview.url.replace(/^https?:[/][/]/, "")}
1363 </a>
1364 ) : (
1365 build?.error && <p className="mt-0.5 truncate text-xs text-muted">{build.error}</p>
1366 )}
1367 </div>
1368 <div className="flex shrink-0 items-center gap-2">
1369 {build && (
1370 <Link to={`${base}/deployments/${build.id}`} className="text-xs text-muted hover:text-fg">
1371 {failed ? "See why" : "Build log"}
1372 </Link>
1373 )}
1374 {preview && (
1375 <a
1376 href={preview.url}
1377 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"
1378 >
1379 Visit preview
1380 <ArrowUpRight size={13} />
1381 </a>
1382 )}
1383 </div>
1384 </div>
1385 {preview && (
1386 <p className="border-t border-line px-4 py-2 text-xs text-faint">
1387 <span className="font-mono">{preview.commit.slice(0, 7)}</span> · deployed <TimeAgo at={preview.deployedAt} />
1388 {preview.branch && ` · from ${preview.branch}`}
1389 </p>
1390 )}
Fast pages, required checks on the branch, self-hosted runners, honest incidents1391 {/* Read after the page: the card's own part shows at once. */}
1392 <Suspense fallback={null}>
1393 <Await resolve={stacked}>
1394 {(stacked) =>
1395 stacked.length > 0 && (
1396 <div className="border-t border-line px-4 py-2.5">
1397 <p className="text-xs text-muted">Built against this change:</p>
1398 <ul className="mt-1.5 space-y-1">
1399 {stacked.map((entry) => (
1400 <li key={entry.slug} className="flex items-center gap-2 text-xs">
1401 <span className="font-medium">{entry.name}</span>
1402 <a href={entry.url} className="truncate font-mono text-muted hover:text-accent">
1403 {entry.url.replace(/^https?:[/][/]/, "")}
1404 </a>
1405 </li>
1406 ))}
1407 </ul>
1408 </div>
1409 )
1410 }
1411 </Await>
1412 </Suspense>
Pull requests say where they are live; the project menu shows what is coming1413 </section>
1414 );
1415}