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

1,428 lines62,914 bytesCodeBlame

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

Acceptance checks in sandboxes, line comments and review verdicts1import { env } from "cloudflare:workers";
Issues and pull requests replace intents and attempts2import {
3 Bot,
4 ChevronRight,
5 FileDiff,
Pull requests from branches6 GitBranch,
Issues and pull requests replace intents and attempts7 GitCommitHorizontal,
Acceptance checks in sandboxes, line comments and review verdicts8 CircleCheck,
9 CircleSlash,
Issues and pull requests replace intents and attempts10 GitMerge,
Agents as a team: lifecycle, merge queue, billing and a new shell11 Layers,
12 GitPullRequestArrow,
13 Hand,
14 Loader,
15 Radar,
16 Sparkles,
Issues and pull requests replace intents and attempts17 MessageSquare,
Pull requests say where they are live; the project menu shows what is coming18 ArrowUpRight,
Issues and pull requests replace intents and attempts19 MessagesSquare,
Pull requests say where they are live; the project menu shows what is coming20 Rocket,
Project dependencies: addresses, preview stacks, Affects, and agents who know21 Network,
Issues and pull requests replace intents and attempts22 StickyNote,
23 User,
24 Wrench,
25} from "lucide-react";
A click away from a live page arrives: pages stop refreshing themselves while a navigation is pending26import { Suspense } from "react";
27import { Await, Form, Link, redirect, useNavigation } from "react-router";
Issues and pull requests replace intents and attempts28
Pull requests say where they are live; the project menu shows what is coming29import {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look30 type Capability,
Pull requests say where they are live; the project menu shows what is coming31 type Comparison,
32 type Deployment,
Agents and memory, checks and conflicts, profiles, slug renames, custom domains33 type Job,
Pull requests say where they are live; the project menu shows what is coming34 type LiveApp,
35 type SessionEntry,
36 type Viewer,
37 pullComparison,
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights38 workOwner,
Pull requests say where they are live; the project menu shows what is coming39} from "@g1t/contracts";
Issues and pull requests replace intents and attempts40
41import type { Route } from "./+types/pull";
Social cards for every page: og.g1t.sh42import { excerpt, page } from "../../lib/meta";
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent43import { openedBy } from "../../lib/opened-by";
Issues and pull requests replace intents and attempts44import { DiffView } from "../../components/diff-view";
Agents as a team: lifecycle, merge queue, billing and a new shell45import { LifecyclePanel } from "../../components/lifecycle";
Agents and memory, checks and conflicts, profiles, slug renames, custom domains46import { AgentPanel } from "../../components/agents";
Issues and pull requests replace intents and attempts47import { Markdown } from "../../components/markdown";
48import {
49 Avatar,
50 Button,
Agents as a team: lifecycle, merge queue, billing and a new shell51 ButtonLink,
Issues and pull requests replace intents and attempts52 CopyLine,
53 EmptyState,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look54 ComputeNote,
Issues and pull requests replace intents and attempts55 ErrorText,
56 Textarea,
57 TimeAgo,
58} from "../../components/ui";
Workspace names and icons, and a component kit for every control59import { CheckboxOption } from "../../components/ui/checkbox";
GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs60import { WorkflowStatuses } from "../../components/actions";
Fast pages, required checks on the branch, self-hosted runners, honest incidents61import { AddCiPrompt } from "../../components/add-ci";
Agents as a team: lifecycle, merge queue, billing and a new shell62import {
63 CommentForm,
64 CommentList,
65 IssueIcon,
66 PeoplePicker,
Agents and memory, checks and conflicts, profiles, slug renames, custom domains67 PersonLink,
Agents as a team: lifecycle, merge queue, billing and a new shell68 PullState,
69 TimelineItem,
70 verdicts,
71} from "../../components/work";
Catching up with main takes seconds when the two sides touched different files72import { CatchUpProgress, ChecksSection, ConflictsSection, MergeabilityRow, runIdOf } from "../../components/merge-box";
73import { 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 put74import { notFound } from "../../lib/not-found.server";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look75import { computeNoteFor } from "../../lib/compute.server";
Agents and memory, checks and conflicts, profiles, slug renames, custom domains76import { actions, deployments, identity, projects, repos, work } from "../../lib/services.server";
Issues and pull requests replace intents and attempts77import { assertSameOrigin, getViewer, requireUser } from "../../lib/session.server";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look78import { accessTo, refusal } from "../../lib/access.server";
A click away from a live page arrives: pages stop refreshing themselves while a navigation is pending79import { REFRESH_MS, useRefreshWhile } from "../../lib/refresh";
Issues and pull requests replace intents and attempts80
81const EMPTY_COMPARISON: Comparison = { base: null, head: "", files: [], truncated: false };
82const TABS = ["conversation", "session", "changes"] as const;
83type Tab = (typeof TABS)[number];
84
Social cards for every page: og.g1t.sh85export function meta({ loaderData, params, ...args }: Route.MetaArgs) {
86 const pull = loaderData?.pull;
87 const title = pull ? `${pull.title} · Pull request #${pull.number} · ` : "";
88 const state = { draft: "Draft", open: "Open", merged: "Merged", closed: "Closed" }[pull?.status ?? "open"];
89 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-agent90 const opener = pull ? openedBy(pull) : null;
Social cards for every page: og.g1t.sh91 return page(args, {
92 title: `${title}${params.owner}/${params.repo} · g1t`,
93 description: pull
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent94 ? `${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.sh95 : null,
96 // The card shows the title and the state.
97 version: pull ? [pull.title, pull.status] : undefined,
98 type: "article",
99 });
Issues and pull requests replace intents and attempts100}
101
102export async function loader({ params, context, request }: Route.LoaderArgs) {
103 const viewer = getViewer(context);
104 const path = { namespace: params.owner, name: params.repo };
105 const number = Number(params.number);
106 const asked = new URL(request.url).searchParams.get("tab");
107 const tab: Tab = TABS.find((name) => name === asked) ?? "conversation";
108
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look109 // Members of the workspace pick reviewers and assignees from its people;
110 // what the viewer may do here goes by their role on the repository.
Agents as a team: lifecycle, merge queue, billing and a new shell111 const member = (viewer?.workspaces ?? []).some(
112 (membership) => membership.slug === params.owner,
113 );
Fast pages, required checks on the branch, self-hosted runners, honest incidents114 // Everything starts at once. Only what the viewer's role decides waits
115 // for the repository, which the layout is looking up in this request too
116 // (lib/access.server.ts), and only what the pull request decides waits
117 // for it.
Project dependencies: addresses, preview stacks, Affects, and agents who know118 const ref = { workspace: params.owner, slug: params.repo };
Fast pages, required checks on the branch, self-hosted runners, honest incidents119 const access = accessTo(context, params);
120 const pullFound = work.getPull(path, number, viewer);
121 const deps = projects.dependencies(params.owner, params.repo, viewer);
122 // Awaited below, unless the pull request is missing first.
123 deps.catch(() => null);
124 const [{ can }, found, repo, settings, agentsEnabled, members, computeNote, session, deployed] = await Promise.all([
125 access,
126 pullFound,
Issues and pull requests replace intents and attempts127 repos.get(path, viewer),
Agents as a team: lifecycle, merge queue, billing and a new shell128 work.getSettings(path, viewer),
g1t's agents only for listed workspaces, whatever the state of billing129 env.RUNNER.enabled(viewer, path),
Agents as a team: lifecycle, merge queue, billing and a new shell130 // A member picks reviewers and assignees from the workspace's people.
131 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-agent132 // Before someone asks g1t for something: whether the plan lets it start.
Fast pages, required checks on the branch, self-hosted runners, honest incidents133 access.then(({ can }) => (can.run ? computeNoteFor(params.owner, "agent") : null)),
134 tab === "session" ? work.readSession(path, number, viewer) : null,
135 // Its preview, for people with a role here: beside the rest, not after.
136 access.then(({ insider }) => (insider ? deployments.list(ref, viewer).catch(() => null) : null)),
Issues and pull requests replace intents and attempts137 ]);
138 if (!found.ok) {
139 // Issues and pull requests share numbers; this one may be an issue.
140 const issue = await work.getIssue(path, number, viewer);
141 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 put142 throw notFound("pull");
Issues and pull requests replace intents and attempts143 }
144 const { pull } = found.value;
Pull requests from branches145 const range = pullComparison(pull);
Agents and memory, checks and conflicts, profiles, slug renames, custom domains146 // The jobs of each workflow run on its head, to list checks job by job.
147 // 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 incidents148 // Streamed: the checks show by status first, then job by job.
Agents and memory, checks and conflicts, profiles, slug renames, custom domains149 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 incidents150 const workflowJobs =
151 tab === "conversation" && pull.status === "open" && runIds.length > 0
152 ? Promise.all(runIds.map((id) => actions.run(path, viewer, id).catch(() => null))).then((runs) => {
153 const jobs: Record<string, Job[]> = {};
154 for (const run of runs) if (run?.ok) jobs[run.value.run.id] = run.value.jobs;
155 return jobs;
156 })
157 .catch(() => ({}) as Record<string, Job[]>)
158 : Promise.resolve({} as Record<string, Job[]>);
159 // No checks at all on an open pull request: whether that is because the
160 // repository has no workflows, to offer a starter one.
161 const unchecked =
162 tab === "conversation" &&
163 pull.status === "open" &&
164 (found.value.statuses ?? []).length === 0 &&
165 (found.value.requiredChecks ?? []).length === 0;
166 const [comparison, used, noChecks] = await Promise.all([
167 tab === "changes" ? repos.compare(range.repoId, viewer, range.base, range.head) : null,
168 deps,
169 unchecked
170 ? actions
171 .workflows(path, viewer)
172 .then((found) => found.ok && found.value.length === 0)
173 .catch(() => false)
174 : false,
Issues and pull requests replace intents and attempts175 ]);
Fast pages, required checks on the branch, self-hosted runners, honest incidents176 const affects = used.ok ? used.value.usedBy : [];
Issues and pull requests replace intents and attempts177 return {
178 ...found.value,
Agents and memory, checks and conflicts, profiles, slug renames, custom domains179 workflowJobs,
Issues and pull requests replace intents and attempts180 tab,
181 session: session?.ok ? session.value : [],
182 // An empty comparison if it could not be made.
183 comparison: comparison && (comparison.ok ? comparison.value : EMPTY_COMPARISON),
184 viewer,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look185 // Write and up can merge.
186 canMerge: can.merge,
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights187 // Triage and up manage anyone's pull request; its author, their own,
188 // and whoever asked g1t for one, that one.
189 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 look190 // Telling its agent things, and re-running checks, spend compute: Write and up.
191 canRun: can.run,
Agents as a team: lifecycle, merge queue, billing and a new shell192 // 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 rights193 // whoever it is for (who asked g1t for it, or its author), a branch
194 // from anyone who can push.
195 canUpdate: pull.fork ? viewer?.id === workOwner(pull).id : can.push,
Agents as a team: lifecycle, merge queue, billing and a new shell196 agentsEnabled,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look197 computeNote,
Agents as a team: lifecycle, merge queue, billing and a new shell198 members: members?.ok ? members.value.map((person) => person.username) : [],
199 requireUpToDate: settings.ok && settings.value.requireUpToDate,
200 mergeQueue: settings.ok && settings.value.mergeQueue,
201 requiredApprovals: settings.ok ? settings.value.requiredApprovals : 0,
202 canIgnoreChecks: !settings.ok || settings.value.allowIgnoringChecks,
Fast pages, required checks on the branch, self-hosted runners, honest incidents203 noChecks,
204 // Who may open the pull request that adds CI: anyone who can push.
205 canAddCi: can.push,
206 // Who may choose the required checks.
207 canProtect: can.manage_protection,
Issues and pull requests replace intents and attempts208 defaultBranch: repo.ok ? repo.value.defaultBranch : "main",
Fast pages, required checks on the branch, self-hosted runners, honest incidents209 affects,
210 // Streamed: the conversation shows first, the preview card after.
211 ...deploymentOf(deployed?.ok ? deployed.value : null, number, params.owner, affects, viewer),
Issues and pull requests replace intents and attempts212 };
213}
214
Pull requests say where they are live; the project menu shows what is coming215/**
216 * Where this pull request is live: its preview (or its latest build, while
217 * one is going or after one failed), and the previews of the projects
218 * that use this one, built against it.
219 */
Fast pages, required checks on the branch, self-hosted runners, honest incidents220function deploymentOf(
Pull requests say where they are live; the project menu shows what is coming221 list: { deployments: Deployment[]; live: LiveApp[] } | null,
222 number: number,
223 owner: string,
224 affects: { slug: string; name: string }[],
225 viewer: Viewer,
Fast pages, required checks on the branch, self-hosted runners, honest incidents226): { preview: LiveApp | null; build: Deployment | null; stacked: Promise<Stacked[]> } {
227 const preview = list?.live.find((app) => app.kind === "preview" && app.number === number) ?? null;
228 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 coming229 const branch = preview?.branch ?? build?.branch ?? null;
Fast pages, required checks on the branch, self-hosted runners, honest incidents230 // Streamed: another lookup per project, shown in the card once known.
231 const stacked: Promise<Stacked[]> = branch
232 ? Promise.all(
233 affects.slice(0, 5).map(async (project) => {
234 const theirs = await deployments.list({ workspace: owner, slug: project.slug }, viewer).catch(() => null);
235 const app = theirs?.ok ? theirs.value.live.find((a) => a.kind === "preview" && a.branch === branch) : undefined;
236 return app ? { name: project.name, slug: project.slug, url: app.url } : null;
237 }),
238 ).then((found) => found.filter((entry) => entry != null))
239 : Promise.resolve([]);
Pull requests say where they are live; the project menu shows what is coming240 return { preview, build, stacked };
241}
242
Fast pages, required checks on the branch, self-hosted runners, honest incidents243/** A project that uses this one, with its preview built against this change. */
244type Stacked = { name: string; slug: string; url: string };
245
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look246const PULL_NEEDS: Record<string, Capability> = {
247 stack: "run",
248 "agent-review": "run",
249 "rerun-workflow": "run",
Fast pages, required checks on the branch, self-hosted runners, honest incidents250 "rerun-failed": "run",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look251 message: "run",
252 merge: "merge",
253 unqueue: "merge",
254};
255
Issues and pull requests replace intents and attempts256export async function action({ request, params, context }: Route.ActionArgs) {
257 assertSameOrigin(request);
258 const user = requireUser(context, request);
259 const form = await request.formData();
260 const path = { namespace: params.owner, name: params.repo };
261 const number = Number(params.number);
262 const action = form.get("action");
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look263 // What each form needs beyond reading; work checks the rest.
264 const needed = typeof action === "string" ? PULL_NEEDS[action] : undefined;
265 const refused = needed ? await refusal(context, params, needed) : null;
266 if (refused) return { error: refused, action: String(action) };
Acceptance checks in sandboxes, line comments and review verdicts267 const verdict = form.get("verdict");
268 const line = Number(form.get("line"));
Agents as a team: lifecycle, merge queue, billing and a new shell269 /** Names ticked in a people picker, plus those typed beside it. */
270 const picked = (field: string) => [
271 ...form.getAll(field).map(String),
272 ...String(form.get("others") ?? "").split(/[\s,]+/),
273 ];
Project dependencies: addresses, preview stacks, Affects, and agents who know274 // The projects that use this one, built against this pull request's preview.
275 if (action === "stack") {
276 const built = await deployments.stack(user, { workspace: params.owner, slug: params.repo }, String(form.get("branch") ?? ""));
277 return built.ok
278 ? { action, notice: `Building ${built.value.join(", ")} against this preview. They appear on their Deployments pages.` }
279 : { action, error: built.error.message };
280 }
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent281 // Asking g1t for its review records the request, then starts it.
Agents as a team: lifecycle, merge queue, billing and a new shell282 if (action === "agent-review") {
283 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-agent284 reviewers: [...form.getAll("reviewer").map(String), "g1t"],
Agents as a team: lifecycle, merge queue, billing and a new shell285 });
286 if (!asked.ok) return { error: asked.error.message, action };
287 }
Catching up with main takes seconds when the two sides touched different files288 // Catching up: merged and pushed in seconds when the two sides changed
289 // different files; otherwise handed to a sandbox, which takes a minute.
290 if (action === "update") {
291 const caught = await work.catchUpPull(user, path, number);
292 if (!caught.ok) return { action, error: caught.error.message };
293 const update = caught.value;
294 if (update.outcome !== "needs_agent") {
295 return {
296 action,
297 updated: { commit: update.commit, already: update.outcome === "up_to_date", at: Date.now() },
298 };
299 }
300 const started = await env.RUNNER.update(user, path, number);
301 if (!started.ok) return { action, error: started.error.message };
302 return { action, agent: { update, startedAt: Date.now() } };
303 }
Agents and memory, checks and conflicts, profiles, slug renames, custom domains304 // A workflow run's failed jobs, run again. Who may is the actions service's call.
305 if (action === "rerun-workflow") {
306 const rerun = await actions.rerun(user, path, String(form.get("run") ?? ""), true);
307 return rerun.ok ? null : { error: rerun.error.message, action };
308 }
Fast pages, required checks on the branch, self-hosted runners, honest incidents309 // Every failed run on its head, run again: Mission control's quick action.
310 if (action === "rerun-failed") {
311 const found = await work.getPull(path, number, user);
312 if (!found.ok) return { error: found.error.message, action };
313 const failed = [
314 ...new Set(
315 (found.value.statuses ?? [])
316 .filter((status) => status.state === "failure" || status.state === "error")
317 .map(runIdOf)
318 .filter((id) => id != null),
319 ),
320 ];
321 if (failed.length === 0) return { error: "No workflow run failed on its latest commit.", action };
322 const reruns = await Promise.all(failed.map((id) => actions.rerun(user, path, id, true)));
323 const refused = reruns.find((rerun) => !rerun.ok);
324 return refused && !refused.ok ? { error: refused.error.message, action } : null;
325 }
Issues and pull requests replace intents and attempts326 const result =
327 action === "merge"
Acceptance checks in sandboxes, line comments and review verdicts328 ? await work.mergePull(user, path, number, {
329 keepIssueOpen: form.get("keepIssueOpen") === "on",
330 ignoreChecks: form.get("ignoreChecks") === "on",
331 })
Agents as a team: lifecycle, merge queue, billing and a new shell332 : action === "unqueue"
333 ? await work.removeFromQueue(user, path, number)
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request334 : action === "message"
335 ? await work.messageAgent(user, path, number, String(form.get("body") ?? ""))
Issues and pull requests replace intents and attempts336 : action === "close"
337 ? await work.closePull(user, path, number)
Fast pages, required checks on the branch, self-hosted runners, honest incidents338 : action === "agent-review"
Agents as a team: lifecycle, merge queue, billing and a new shell339 ? await env.RUNNER.review(user, path, number)
340 : action === "reviewers"
341 ? await work.updatePull(user, path, number, { reviewers: picked("reviewer") })
342 : action === "assign"
343 ? await work.updatePull(user, path, number, { assignees: picked("assignee") })
Acceptance checks in sandboxes, line comments and review verdicts344 : action === "comment"
345 ? await work.addComment(user, path, number, {
346 body: String(form.get("body") ?? ""),
347 path: String(form.get("path") ?? "") || undefined,
348 line: line > 0 ? line : undefined,
349 verdict:
350 verdict === "approve" || verdict === "request_changes" ? verdict : undefined,
351 })
352 : await work.readyPull(user, path, number, String(form.get("summary") ?? ""));
Issues and pull requests replace intents and attempts353 return result.ok ? null : { error: result.error.message, action };
354}
355
Pull requests: title and tabs across the full width, with counts and the size of the change356/** How many, beside a tab's name, when there are any. */
357function TabCount({ n }: { n: number }) {
358 if (n <= 0) return null;
359 return (
360 <span className="rounded-full bg-raised px-1.5 py-px text-xs tabular-nums text-muted ring-1 ring-line">
361 {n.toLocaleString("en-US")}
362 </span>
363 );
364}
365
Issues and pull requests replace intents and attempts366function TabLink({
367 to,
368 active,
369 children,
370}: {
371 to: string;
372 active: boolean;
373 children: React.ReactNode;
374}) {
375 return (
376 <Link
377 to={to}
378 preventScrollReset
379 className={
Fast pages, required checks on the branch, self-hosted runners, honest incidents380 "-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 attempts381 (active
382 ? "border-accent font-medium text-fg"
383 : "border-transparent text-muted hover:text-fg")
384 }
385 >
386 {children}
387 </Link>
388 );
389}
390
391/** One step of the session, on the timeline's rail. */
392function Entry({ entry, agent }: { entry: SessionEntry; agent: string }) {
393 const isTool = entry.kind === "tool_call" || entry.kind === "tool_result";
394 const Icon =
395 entry.kind === "prompt"
396 ? User
397 : entry.kind === "note"
398 ? StickyNote
399 : isTool
400 ? Wrench
401 : Bot;
402 return (
403 <li className="relative pl-10">
404 <span
405 className={`absolute top-0.5 left-0 flex size-7 items-center justify-center rounded-full border bg-bg ${
406 entry.kind === "prompt"
407 ? "border-accent/50 text-accent"
408 : "border-line text-faint"
409 }`}
410 >
411 <Icon size={14} />
412 </span>
413 {isTool ? (
414 <details className="group rounded-lg border border-line bg-surface">
415 <summary className="flex cursor-pointer list-none items-center gap-2 px-3 py-1.5 text-sm">
416 <ChevronRight
417 size={14}
418 className="text-faint transition-transform group-open:rotate-90"
419 />
420 <span className="font-mono text-xs text-accent">
421 {entry.tool ?? "tool"}
422 </span>
423 <span className="truncate font-mono text-xs text-muted">
424 {entry.kind === "tool_result" ? "→ " : ""}
425 {entry.text.split("\n")[0]}
426 </span>
427 </summary>
428 <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 line429 <code>{entry.text}</code>
Issues and pull requests replace intents and attempts430 </pre>
431 </details>
432 ) : (
433 <div>
434 <p className="text-xs font-medium text-faint">
435 {entry.kind === "prompt"
436 ? "Prompt"
437 : entry.kind === "note"
438 ? "Note"
439 : agent}
440 </p>
441 {entry.kind === "message" ? (
442 <div className="mt-1">
443 <Markdown source={entry.text} />
444 </div>
Agents as a team: lifecycle, merge queue, billing and a new shell445 ) : entry.kind === "prompt" ? (
446 <div className="mt-1.5 max-h-[32rem] overflow-y-auto rounded-xl border border-line bg-surface p-4">
447 <Markdown source={entry.text} />
448 </div>
Issues and pull requests replace intents and attempts449 ) : (
Agents as a team: lifecycle, merge queue, billing and a new shell450 <p className="mt-1 text-[0.9375rem] leading-relaxed wrap-break-word whitespace-pre-wrap">
Issues and pull requests replace intents and attempts451 {entry.text}
452 </p>
453 )}
454 </div>
455 )}
456 {entry.commit && (
457 <p className="mt-1.5 flex items-center gap-1 font-mono text-xs text-faint">
458 <GitCommitHorizontal size={12} />
459 {entry.commit.slice(0, 7)}
460 </p>
461 )}
462 </li>
463 );
464}
465
Agents as a team: lifecycle, merge queue, billing and a new shell466/** The box at the foot of the conversation saying what stands before a merge. */
467function StatusBox({ children }: { children: React.ReactNode }) {
468 return (
469 <div className="flex gap-3">
470 <span className="hidden w-8 shrink-0 sm:block" />
471 <section className="min-w-0 grow divide-y divide-line overflow-hidden rounded-xl border border-line-strong bg-surface">
472 {children}
473 </section>
474 </div>
475 );
476}
477
478function StatusRow({
479 icon,
480 title,
481 children,
482}: {
483 icon: React.ReactNode;
484 title: string;
485 children?: React.ReactNode;
486}) {
487 return (
488 <div className="flex gap-3 px-4 py-3 text-sm">
489 <span className="mt-0.5 shrink-0">{icon}</span>
490 <div className="min-w-0">
491 <p className="font-medium">{title}</p>
492 {children && <div className="mt-0.5 text-muted">{children}</div>}
493 </div>
494 </div>
495 );
496}
497
Issues and pull requests replace intents and attempts498export default function PullPage({ loaderData, actionData, params }: Route.ComponentProps) {
499 const {
500 pull,
501 issue,
502 comments,
Acceptance checks in sandboxes, line comments and review verdicts503 checks,
Agents as a team: lifecycle, merge queue, billing and a new shell504 overlaps,
505 behind,
506 reviewPending,
507 lifecycle,
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request508 messages,
GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs509 statuses = [],
Fast pages, required checks on the branch, self-hosted runners, honest incidents510 requiredChecks = [],
511 noChecks,
512 canAddCi,
513 canProtect,
Agents and memory, checks and conflicts, profiles, slug renames, custom domains514 mergeable = "unknown",
515 conflicts = [],
516 earlierChecks = [],
517 workflowJobs,
Project dependencies: addresses, preview stacks, Affects, and agents who know518 affects,
519 preview,
Pull requests say where they are live; the project menu shows what is coming520 build,
521 stacked,
Agents as a team: lifecycle, merge queue, billing and a new shell522 landing,
523 stalled,
524 requireUpToDate,
525 mergeQueue,
526 requiredApprovals,
527 canIgnoreChecks,
528 canUpdate,
529 agentsEnabled,
530 members,
Issues and pull requests replace intents and attempts531 tab,
532 session,
533 comparison,
534 viewer,
535 canMerge,
536 canManage,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look537 canRun,
Issues and pull requests replace intents and attempts538 defaultBranch,
539 } = loaderData;
540 const base = `/${params.owner}/${params.repo}`;
541 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-agent542 // g1t, on a pull request it made: the person who asked for it is a line below.
543 const opener = openedBy(pull);
Fast pages, required checks on the branch, self-hosted runners, honest incidents544 const checksSection = (jobs: Record<string, Job[]>) => (
545 <ChecksSection
546 run={checks}
547 required={requiredChecks}
548 statuses={statuses}
549 jobs={jobs}
550 pull={pull}
551 base={base}
552 earlier={earlierChecks}
553 canRerunWorkflows={canMerge}
554 settingsUrl={canProtect ? `${base}/settings/branches` : null}
555 error={actionData?.action === "rerun-failed" || actionData?.action === "rerun-workflow" ? actionData.error : null}
556 />
557 );
Pull requests from branches558 const remote = pull.fork
559 ? `https://g1t.sh/${pull.fork.namespace}/${pull.fork.name}.git`
560 : `https://g1t.sh/${params.owner}/${params.repo}.git`;
Issues and pull requests replace intents and attempts561 const active = pull.status === "draft" || pull.status === "open";
562
Acceptance checks in sandboxes, line comments and review verdicts563 // Follow an agent at work, or checks in progress, without a manual reload.
Issues and pull requests replace intents and attempts564 const working = pull.status === "draft";
Fast pages, required checks on the branch, self-hosted runners, honest incidents565 const checking = statuses.some((status) => status.state === "pending");
Acceptance checks in sandboxes, line comments and review verdicts566 const reviews = verdicts(comments);
Fast pages, required checks on the branch, self-hosted runners, honest incidents567 // What stands between this pull request and a merge, if anything: a
568 // required check that has not passed, or the merge queue taking it out.
569 const unchecked = requiredChecks.some((check) => check.state !== "success") || checks?.status === "failed";
570 const requiredFailed = requiredChecks.some((check) => check.state === "failure") || checks?.status === "failed";
Agents as a team: lifecycle, merge queue, billing and a new shell571 // Pull requests for other issues changing the same files will conflict;
572 // ones for the same issue are alternatives, and expected to.
573 const collisions = overlaps.filter((other) => other.issue == null || other.issue !== pull.issue);
574 const review = {
575 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 rights576 // Nobody reviews their own pull request, nor one g1t made for them.
577 canJudge: active && viewer != null && viewer.id !== workOwner(pull).id,
Agents as a team: lifecycle, merge queue, billing and a new shell578 };
579 // Everyone whose review was asked for, then anyone who reviewed unasked.
580 const reviewerNames = [
581 ...new Set([...pull.reviewers, ...reviews.map(({ reviewer }) => reviewer)]),
582 ];
Catching up with main takes seconds when the two sides touched different files583 // A catch-up: the click shows at once; the answer says whether it is
584 // done already or a sandbox is on it.
585 const navigation = useNavigation();
586 const catchUpPending = navigation.state !== "idle" && navigation.formData?.get("action") === "update";
587 const catchUp = actionData?.action === "update" ? actionData : null;
588 const caughtUp = catchUp && "updated" in catchUp ? catchUp.updated : null;
589 const agentCatchUp = catchUp && "agent" in catchUp ? catchUp.agent : null;
590 // Pushed already: followed until the pull request's head is the merge,
591 // so its checks show starting again, for a minute at most.
592 const settling =
593 caughtUp != null && !caughtUp.already && pull.headCommit !== caughtUp.commit && Date.now() - caughtUp.at < 60_000;
594 // Followed until it is pushed, or for so long, never longer.
595 const catchingUp =
596 agentCatchUp != null && behind && Date.now() - agentCatchUp.startedAt < CATCH_UP_TIMEOUT_MS + REFRESH_MS;
Agents as a team: lifecycle, merge queue, billing and a new shell597 // g1t is taking a step of its own accord, so the page will change.
598 const moving =
599 lifecycle != null && lifecycle.stage !== "ready" && lifecycle.stage !== "needs_you";
Agents and memory, checks and conflicts, profiles, slug renames, custom domains600 // Whether it merges cleanly is being worked out, so the box will change.
601 const probing = active && mergeable === "checking";
602 const conflicting = active && mergeable === "conflicting";
A click away from a live page arrives: pages stop refreshing themselves while a navigation is pending603 useRefreshWhile(working || checking || reviewPending || catchingUp || settling || moving || landing || probing);
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
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent628 name={(pull.status === "merged" && pull.mergedBy) || opener.name}
Agents as a team: lifecycle, merge queue, billing and a new shell629 size={18}
630 />
Issues and pull requests replace intents and attempts631 <span>
Agents and memory, checks and conflicts, profiles, slug renames, custom domains632 <PersonLink
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent633 name={(pull.status === "merged" && pull.mergedBy) || opener.name}
Agents and memory, checks and conflicts, profiles, slug renames, custom domains634 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>
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent646 {opener.requestedBy && (
647 <span className="text-xs">
648 requested by{" "}
649 <PersonLink name={opener.requestedBy} className="font-medium text-fg-soft hover:underline" />
650 </span>
651 )}
652 {/* A pull request from a branch was made by its author, not an agent;
653 one g1t made already says so. */}
654 {!pull.branch && !opener.requestedBy && (
Pull requests from branches655 <span className="flex items-center gap-1.5 font-mono text-xs">
656 <Bot size={14} />
657 {pull.agent}
658 {pull.runtime === "hosted" && <span className="text-faint">on g1t</span>}
659 </span>
660 )}
Issues and pull requests replace intents and attempts661 </div>
662
Acceptance checks in sandboxes, line comments and review verdicts663 {reviews.length > 0 && (
664 <p className="mt-3 flex flex-wrap items-center gap-x-4 gap-y-1 text-sm">
665 {reviews.map(({ reviewer, verdict }) => (
666 <span
667 key={reviewer}
668 className={`flex items-center gap-1.5 ${
669 verdict === "approve" ? "text-accent" : "text-danger"
670 }`}
671 >
672 {verdict === "approve" ? <CircleCheck size={15} /> : <CircleSlash size={15} />}
673 {verdict === "approve" ? "Approved by" : "Changes requested by"}{" "}
674 <span className="font-medium">{reviewer}</span>
675 </span>
676 ))}
677 </p>
678 )}
679
Fast pages, required checks on the branch, self-hosted runners, honest incidents680 <nav className="mt-6 flex items-end gap-1 overflow-x-auto border-b border-line">
Issues and pull requests replace intents and attempts681 <TabLink to={here} active={tab === "conversation"}>
682 <MessageSquare size={15} />
683 Conversation
Pull requests: title and tabs across the full width, with counts and the size of the change684 <TabCount n={comments.length} />
Issues and pull requests replace intents and attempts685 </TabLink>
686 <TabLink to={here + "?tab=session"} active={tab === "session"}>
687 <MessagesSquare size={15} />
688 Session
689 </TabLink>
690 <TabLink to={here + "?tab=changes"} active={tab === "changes"}>
691 <FileDiff size={15} />
Pull requests: title and tabs across the full width, with counts and the size of the change692 Files changed
693 <TabCount n={pull.files.length} />
Issues and pull requests replace intents and attempts694 </TabLink>
Pull requests: title and tabs across the full width, with counts and the size of the change695 {pull.files.length > 0 && (
696 <span className="ml-auto hidden pb-2.5 font-mono text-xs sm:inline">
697 <span className="text-accent">+{pull.files.reduce((sum, file) => sum + file.additions, 0).toLocaleString("en-US")}</span>{" "}
698 <span className="text-danger">−{pull.files.reduce((sum, file) => sum + file.deletions, 0).toLocaleString("en-US")}</span>
699 </span>
700 )}
Issues and pull requests replace intents and attempts701 </nav>
Pull requests: title and tabs across the full width, with counts and the size of the change702 </header>
703 <div className={`mt-6 grid gap-8 ${tab === "changes" ? "" : "lg:grid-cols-[1fr_19rem]"}`}>
704 <div className="min-w-0">
705 {/* What is happening on it now: its stage, its agent, what people told
706 the agent, and what else touches the same files. On the conversation,
707 so the tabs sit right under the title. */}
708 {tab === "conversation" && (
709 <div>
710 {issue && (
711 <Link
712 to={`${base}/issues/${issue.number}`}
713 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"
714 >
715 <IssueIcon issue={issue} />
716 <span className="text-muted">
717 {pull.status === "merged" && issue.resolvedBy === pull.number
718 ? "Resolved"
719 : "For issue"}
720 </span>
721 <span className="truncate font-medium">{issue.title}</span>
722 <span className="text-faint">#{issue.number}</span>
723 {issue.pullCount > 1 && (
724 <span className="ml-auto shrink-0 text-xs text-faint">
725 1 of {issue.pullCount} pull requests
Pull requests show their tabs right under the title; agent panels move into Conversation726 </span>
Pull requests: title and tabs across the full width, with counts and the size of the change727 )}
728 </Link>
729 )}
730 {pull.status === "merged" && (
731 <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">
732 <GitMerge size={16} className="shrink-0 text-merged" />
Agents as a team: lifecycle, merge queue, billing and a new shell733 <span>
Pull requests: title and tabs across the full width, with counts and the size of the change734 Merged into <span className="font-mono">{defaultBranch}</span> by{" "}
735 <span className="font-medium">{pull.mergedBy}</span>{" "}
736 {/* Not an account: the repository's settings said to merge it. */}
737 {pull.mergedBy === "g1t" && "automatically, once it was ready, "}
738 {pull.mergedAt && <TimeAgo at={pull.mergedAt} />}
739 {pull.headCommit && (
740 <span className="font-mono text-muted"> · {pull.headCommit.slice(0, 7)}</span>
Agents as a team: lifecycle, merge queue, billing and a new shell741 )}
742 </span>
Pull requests: title and tabs across the full width, with counts and the size of the change743 </p>
744 )}
745 {pull.supersededBy != null && (
746 <p className="mt-4 rounded-xl border border-line bg-surface px-4 py-2.5 text-sm text-muted">
747 Closed because{" "}
748 <Link
749 to={`${base}/pull/${pull.supersededBy}`}
750 className="font-medium text-fg hover:underline"
751 >
752 #{pull.supersededBy}
753 </Link>{" "}
754 was merged for this issue instead.
755 </p>
756 )}
757 {lifecycle && <LifecyclePanel lifecycle={lifecycle} />}
758 {/* 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 step759 <AgentPanel
760 owner={params.owner}
761 repo={params.repo}
762 number={pull.number}
763 stage={lifecycle?.stage ?? null}
764 confidence={pull.confidence ?? null}
765 />
Pull requests: title and tabs across the full width, with counts and the size of the change766
767 {/* 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 look768 {canRun &&
Pull requests: title and tabs across the full width, with counts and the size of the change769 pull.runtime === "hosted" &&
770 (working || ["working", "revising", "catching_up", "answering"].includes(lifecycle?.stage ?? "")) && (
771 <Form method="post" className="mt-4 rounded-2xl bg-surface p-4 ring-1 ring-merged/30">
772 <p className="flex items-center gap-2 text-sm font-medium">
773 <Sparkles size={15} className="text-merged" />
774 Message the agent
775 </p>
776 <p className="mt-1 text-xs text-muted">
777 A correction, a hint, a change of plan. It reads it at its next step, without
778 starting over.
779 </p>
780 <div className="mt-3 flex gap-2">
781 <input type="hidden" name="action" value="message" />
782 <input
783 name="body"
784 required
785 autoComplete="off"
786 data-1p-ignore
787 placeholder="Keep the old flag working too…"
788 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 shell789 />
Pull requests: title and tabs across the full width, with counts and the size of the change790 <Button type="submit">Send</Button>
791 </div>
792 </Form>
Agents as a team: lifecycle, merge queue, billing and a new shell793 )}
Pull requests: title and tabs across the full width, with counts and the size of the change794 {messages.length > 0 && (
795 <ul className="mt-3 space-y-1.5">
796 {messages.map((message) => (
797 <li key={message.id} className="flex items-start gap-2 text-sm">
798 <Avatar name={message.author} size={18} />
799 <span className="min-w-0 grow">
800 <span className="font-medium">
801 {message.fromNumber != null ? `The agent on #${message.fromNumber}` : message.author}
802 </span>{" "}
803 <span className="text-muted">
804 {message.kind === "question"
805 ? "asked:"
806 : message.kind === "handoff"
807 ? "handed over:"
808 : message.kind === "answer"
809 ? "answered:"
810 : "to the agent:"}
811 </span>{" "}
812 {message.body}
813 {message.answer && (
814 <span className="mt-1 block border-l-2 border-merged/40 pl-2 text-muted">
815 {message.declined ? "Declined: " : "Answer: "}
816 {message.answer}
817 </span>
818 )}
819 </span>
820 <span className={`shrink-0 text-xs ${message.deliveredAt ? "text-accent" : "text-faint"}`}>
821 {message.deliveredAt ? "read by the agent" : "waiting for its next step"}
822 </span>
823 </li>
824 ))}
825 </ul>
826 )}
827 {collisions.length > 0 && active && (
828 <div className="mt-4 rounded-xl border border-line bg-surface px-4 py-3 text-sm">
829 <p className="flex items-center gap-2.5 font-medium">
830 <Radar size={16} className="shrink-0 text-info" />
831 Other work is changing the same files
832 </p>
833 <ul className="mt-2 space-y-1.5">
834 {collisions.map((other) => (
835 <li key={other.number} className="flex flex-wrap items-baseline gap-x-2 text-muted">
836 <Link
837 to={`${base}/pull/${other.number}`}
838 className="font-medium text-fg hover:underline"
839 >
840 {other.title} <span className="font-normal text-faint">#{other.number}</span>
841 </Link>
842 <span className="font-mono text-xs">{other.paths.join(", ")}</span>
843 </li>
844 ))}
845 </ul>
846 <p className="mt-2 text-xs text-faint">
847 Whichever merges second will have to catch up, and may conflict.
848 </p>
849 </div>
850 )}
851 </div>
852 )}
853 <div className="mt-5">
854 {comparison ? (
855 <DiffView
856 comparison={comparison}
857 review={{
858 comments: comments.filter((comment) => comment.path),
859 canComment: Boolean(viewer),
860 }}
861 />
862 ) : tab === "session" ? (
863 session.length === 0 ? (
864 <EmptyState title="Nothing recorded yet">
865 The agent's prompts, reasoning and tool calls appear here as it
866 works.
867 </EmptyState>
868 ) : (
869 <ol className="relative space-y-5 before:absolute before:top-2 before:bottom-2 before:left-3.25 before:w-px before:bg-line">
870 {session.map((entry) => (
871 <Entry key={entry.seq} entry={entry} agent={pull.agent} />
872 ))}
873 </ol>
874 )
875 ) : (
876 <div className="space-y-4">
877 <TimelineItem
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent878 author={opener.name}
Pull requests: title and tabs across the full width, with counts and the size of the change879 at={pull.createdAt}
880 action={
881 <span>
882 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-agent883 {opener.requestedBy ? (
Pull requests: title and tabs across the full width, with counts and the size of the change884 <>
885 {" "}
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent886 for{" "}
887 <PersonLink name={opener.requestedBy} className="font-medium text-fg-soft hover:underline" />
888 </>
889 ) : !pull.branch && (
890 <>
891 {" "}
Pull requests: title and tabs across the full width, with counts and the size of the change892 with <span className="font-mono text-xs">{pull.agent}</span>
893 </>
894 )}
895 </span>
896 }
897 >
898 {pull.body ? (
899 <Markdown source={pull.body} repo={{ namespace: params.owner, name: params.repo }} />
900 ) : (
901 <p className="text-sm text-muted">
902 {working
903 ? "No description yet. It is written when the pull request is marked ready for review."
904 : "No description."}
905 </p>
906 )}
907 {canManage && pull.status === "open" && (
908 <details className="mt-3 border-t border-line pt-3 text-sm">
909 <summary className="cursor-pointer text-xs text-faint hover:text-fg">
910 Edit description
911 </summary>
912 <Form method="post" className="mt-3 space-y-2">
913 <Textarea
914 name="summary"
915 rows={6}
916 placeholder="What changed and why"
917 defaultValue={pull.body ?? ""}
918 />
919 <Button type="submit">Save</Button>
920 </Form>
921 </details>
922 )}
923 </TimelineItem>
Agents as a team: lifecycle, merge queue, billing and a new shell924
Pull requests: title and tabs across the full width, with counts and the size of the change925 <CommentList comments={comments} review={review} base={base} />
Agents as a team: lifecycle, merge queue, billing and a new shell926
Fast pages, required checks on the branch, self-hosted runners, honest incidents927 {(preview || build) && <DeploymentCard preview={preview} build={build} stacked={stacked} base={base} />}
Pull requests say where they are live; the project menu shows what is coming928
Pull requests: title and tabs across the full width, with counts and the size of the change929 {pull.status === "draft" && (
930 <StatusBox>
931 <StatusRow icon={<Loader size={16} className="text-faint" />} title="This is a draft">
932 It is still being worked on. It can be reviewed and merged once it is
933 marked ready.
934 </StatusRow>
935 {canManage && (
936 <Form method="post" className="space-y-2 px-4 py-3">
937 <Textarea name="summary" rows={3} placeholder="What changed and why" />
938 <Button type="submit">Mark ready for review</Button>
939 </Form>
940 )}
941 </StatusBox>
942 )}
Agents as a team: lifecycle, merge queue, billing and a new shell943
Pull requests: title and tabs across the full width, with counts and the size of the change944 {pull.status === "open" && (
945 <StatusBox>
Fast pages, required checks on the branch, self-hosted runners, honest incidents946 {/* Checks by status at once; job by job when the runs are read. */}
947 <Suspense fallback={checksSection({})}>
948 <Await resolve={workflowJobs}>{checksSection}</Await>
949 </Suspense>
950 {noChecks && <AddCiPrompt owner={params.owner} repo={params.repo} canAdd={canAddCi} compact />}
Agents as a team: lifecycle, merge queue, billing and a new shell951 <StatusRow
Pull requests: title and tabs across the full width, with counts and the size of the change952 icon={
953 reviews.some(({ verdict }) => verdict === "request_changes") ? (
954 <CircleSlash size={16} className="text-danger" />
955 ) : reviews.length > 0 ? (
956 <CircleCheck size={16} className="text-accent" />
957 ) : (
958 <MessageSquare size={16} className="text-faint" />
959 )
960 }
961 title={
962 reviews.length === 0
963 ? "No reviews yet"
964 : reviews
965 .map(
966 ({ reviewer, verdict }) =>
967 `${verdict === "approve" ? "Approved by" : "Changes requested by"} ${reviewer}`,
968 )
969 .join(" · ")
970 }
Agents as a team: lifecycle, merge queue, billing and a new shell971 >
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent972 {reviewPending && "g1t is reviewing it now. "}
Pull requests: title and tabs across the full width, with counts and the size of the change973 {requiredApprovals > 0 &&
974 `This repository requires ${requiredApprovals} approving ${
975 requiredApprovals === 1 ? "review" : "reviews"
976 } before merging.`}
Agents as a team: lifecycle, merge queue, billing and a new shell977 </StatusRow>
Pull requests: title and tabs across the full width, with counts and the size of the change978 {landing ? (
Agents as a team: lifecycle, merge queue, billing and a new shell979 <StatusRow
Pull requests: title and tabs across the full width, with counts and the size of the change980 icon={<Loader size={16} className="animate-spin text-accent" />}
981 title="Merging"
982 >
983 {defaultBranch} has moved, so g1t is bringing this up to date first. It
984 lands as soon as that is done. Watch it in the Session tab.
985 </StatusRow>
Catching up with main takes seconds when the two sides touched different files986 ) : agentCatchUp && behind ? (
987 <CatchUpProgress
988 owner={params.owner}
989 repo={params.repo}
990 number={pull.number}
991 defaultBranch={defaultBranch}
992 behind={behind}
993 update={agentCatchUp.update}
994 startedAt={agentCatchUp.startedAt}
995 retrying={catchUpPending}
996 />
Pull requests: title and tabs across the full width, with counts and the size of the change997 ) : conflicting ? (
998 <ConflictsSection
999 conflicts={conflicts}
1000 pull={pull}
1001 owner={params.owner}
1002 repo={params.repo}
1003 defaultBranch={defaultBranch}
1004 changesUrl={here + "?tab=changes"}
1005 canResolve={canUpdate && agentsEnabled}
Catching up with main takes seconds when the two sides touched different files1006 resolving={catchUpPending}
1007 error={catchUp && "error" in catchUp ? catchUp.error : null}
Agents as a team: lifecycle, merge queue, billing and a new shell1008 />
Pull requests: title and tabs across the full width, with counts and the size of the change1009 ) : probing ? (
1010 <MergeabilityRow mergeable={mergeable} defaultBranch={defaultBranch} />
1011 ) : behind ? (
1012 <StatusRow
1013 icon={<GitPullRequestArrow size={16} className="text-info" />}
1014 title={`${defaultBranch} has moved since this was made`}
1015 >
1016 {mergeable === "clean" && "It has no conflicts with it. "}
1017 {requireUpToDate
1018 ? "This repository requires pull requests to be up to date, so it has to catch up before it can merge."
1019 : "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 files1020 {canUpdate && (
1021 <Form method="post" className="mt-2">
Pull requests: title and tabs across the full width, with counts and the size of the change1022 <input type="hidden" name="action" value="update" />
Catching up with main takes seconds when the two sides touched different files1023 <Button variant="quiet" type="submit" disabled={catchUpPending}>
1024 {catchUpPending && <Loader size={14} className="animate-spin" />}
1025 {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 change1026 </Button>
1027 </Form>
1028 )}
Catching up with main takes seconds when the two sides touched different files1029 {catchUp && "error" in catchUp && <ErrorText>{catchUp.error}</ErrorText>}
Pull requests: title and tabs across the full width, with counts and the size of the change1030 </StatusRow>
1031 ) : (
1032 pull.headCommit && (
1033 <StatusRow
1034 icon={<CircleCheck size={16} className="text-accent" />}
Catching up with main takes seconds when the two sides touched different files1035 title={
1036 (caughtUp && !caughtUp.already) || agentCatchUp
1037 ? `Brought up to date with ${defaultBranch}`
1038 : `Up to date with ${defaultBranch}`
1039 }
1040 >
1041 {caughtUp && !caughtUp.already && (
1042 <>
1043 Merged <span className="font-mono">{defaultBranch}</span> in as{" "}
1044 <span className="font-mono text-fg">{caughtUp.commit.slice(0, 7)}</span>. Its checks run
1045 again on the new commit.
1046 </>
1047 )}
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent1048 {agentCatchUp && "g1t merged it in and pushed the result. Its checks run again on the new commit."}
Catching up with main takes seconds when the two sides touched different files1049 </StatusRow>
Pull requests: title and tabs across the full width, with counts and the size of the change1050 )
1051 )}
1052 {stalled && !lifecycle && (
1053 <StatusRow icon={<Hand size={16} className="text-warn" />} title="Needs you">
1054 {stalled}
1055 </StatusRow>
1056 )}
1057 {canMerge && lifecycle?.stage === "queued" && (
1058 <Form method="post" className="flex flex-wrap items-center gap-3 px-4 py-3">
1059 <ButtonLink to={`${base}/queue`}>
1060 <Layers size={15} />
1061 See the queue
1062 </ButtonLink>
1063 <Button variant="quiet" type="submit" name="action" value="unqueue">
1064 Remove from the queue
Agents as a team: lifecycle, merge queue, billing and a new shell1065 </Button>
Pull requests: title and tabs across the full width, with counts and the size of the change1066 </Form>
1067 )}
1068 {canMerge && !landing && lifecycle?.stage !== "queued" && (
1069 <Form method="post" className="space-y-3 px-4 py-3">
1070 {issue?.state === "open" && (
1071 <CheckboxOption
1072 name="keepIssueOpen"
1073 label={`Keep #${issue.number} open. This is only part of the work.`}
1074 labelClassName="text-xs text-muted"
1075 />
1076 )}
1077 {unchecked && canIgnoreChecks && (
1078 <CheckboxOption
1079 name="ignoreChecks"
Fast pages, required checks on the branch, self-hosted runners, honest incidents1080 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 change1081 labelClassName="text-xs text-muted"
1082 />
1083 )}
1084 <div className="flex flex-wrap items-center gap-3">
1085 <Button
1086 variant="accent"
1087 type="submit"
1088 name="action"
1089 value="merge"
1090 disabled={mergeBlocked != null}
1091 title={mergeBlocked ?? undefined}
1092 >
1093 {mergeQueue ? <Layers size={15} /> : <GitMerge size={15} />}
1094 {mergeQueue ? "Add to the merge queue" : `Merge into ${defaultBranch}`}
1095 </Button>
1096 <span className={`text-xs ${mergeBlocked ? "text-danger" : "text-muted"}`}>
1097 {mergeBlocked ?? (mergeQueue
1098 ? `Tested together with everything ahead of it, then lands on ${defaultBranch}.`
1099 : issue?.state === "open"
1100 ? `Closes issue #${issue.number}, and any other pull requests still open for it.`
1101 : `Lands its commits on ${defaultBranch}.`)}
1102 </span>
1103 </div>
1104 {actionData?.action === "merge" && (
1105 <ErrorText>{actionData.error}</ErrorText>
1106 )}
1107 </Form>
1108 )}
1109 </StatusBox>
1110 )}
Agents as a team: lifecycle, merge queue, billing and a new shell1111
Pull requests: title and tabs across the full width, with counts and the size of the change1112 <CommentForm
1113 author={viewer?.username ?? null}
1114 resetKey={comments.length}
1115 review={review}
1116 />
1117 {canManage && active && (
1118 <Form method="post" className="flex justify-end">
1119 <Button variant="quiet" type="submit" name="action" value="close">
1120 Close pull request
1121 </Button>
1122 </Form>
1123 )}
1124 {actionData &&
Fast pages, required checks on the branch, self-hosted runners, honest incidents1125 !["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 change1126 String(actionData.action),
1127 ) && <ErrorText>{actionData.error}</ErrorText>}
1128 </div>
1129 )}
1130 {actionData?.action === "comment" && (
1131 <div className="mt-2">
1132 <ErrorText>{actionData.error}</ErrorText>
1133 </div>
1134 )}
1135 </div>
1136 </div>
1137
1138 <aside className={tab === "changes" ? "hidden" : "space-y-6"}>
1139 {/* An open pull request shows its checks in full in the merge box. */}
Fast pages, required checks on the branch, self-hosted runners, honest incidents1140 {pull.status !== "open" && <WorkflowStatuses statuses={statuses} />}
Pull requests: title and tabs across the full width, with counts and the size of the change1141 {affects.length > 0 && (
1142 <section>
1143 <h3 className="flex items-center gap-1.5 text-sm font-medium">
1144 <Network size={14} className="text-faint" />
1145 Affects
1146 </h3>
1147 <p className="mt-1 text-xs text-muted">Projects that use this one, and so may feel this change:</p>
1148 <ul className="mt-2 space-y-1 text-sm">
1149 {affects.map((project) => (
1150 <li key={project.slug} className="flex items-center justify-between gap-2">
1151 <Link to={`/${params.owner}/${project.slug}`} className="hover:underline">
1152 {project.name}
1153 </Link>
1154 {project.as && <code className="font-mono text-xs text-faint">{project.as}</code>}
1155 </li>
1156 ))}
1157 </ul>
1158 {preview?.branch && canMerge && (
1159 <Form method="post" className="mt-3">
1160 <input type="hidden" name="action" value="stack" />
1161 <input type="hidden" name="branch" value={preview.branch} />
1162 <button
1163 type="submit"
1164 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"
1165 >
1166 Preview them against this change
1167 </button>
Agents as a team: lifecycle, merge queue, billing and a new shell1168 </Form>
1169 )}
Pull requests: title and tabs across the full width, with counts and the size of the change1170 {actionData?.action === "stack" &&
1171 ("notice" in actionData ? (
1172 <p className="mt-2 text-xs text-accent">{String(actionData.notice)}</p>
1173 ) : (
1174 <ErrorText>{actionData.error}</ErrorText>
1175 ))}
1176 </section>
Acceptance checks in sandboxes, line comments and review verdicts1177 )}
Issues and pull requests replace intents and attempts1178
Project dependencies: addresses, preview stacks, Affects, and agents who know1179 <section>
Pull requests: title and tabs across the full width, with counts and the size of the change1180 <h3 className="text-sm font-medium">Reviewers</h3>
1181 <ul className="mt-2 space-y-1.5 text-sm">
1182 {reviewerNames.map((name) => {
1183 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-agent1184 const pending = name === "g1t" && reviewPending;
Pull requests: title and tabs across the full width, with counts and the size of the change1185 return (
1186 <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-agent1187 {name === "g1t" ? (
Pull requests: title and tabs across the full width, with counts and the size of the change1188 <Sparkles size={16} className="shrink-0 text-accent" />
1189 ) : (
1190 <Avatar name={name} size={20} />
1191 )}
1192 <span className="grow truncate font-mono text-xs">{name}</span>
1193 {pending ? (
1194 <span className="flex items-center gap-1.5 text-xs text-muted">
1195 <span className="size-1.5 animate-pulse rounded-full bg-accent" />
1196 Reviewing
1197 </span>
1198 ) : verdict === "approve" ? (
1199 <span className="flex items-center gap-1 text-xs text-accent">
1200 <CircleCheck size={13} /> Approved
1201 </span>
1202 ) : verdict === "request_changes" ? (
1203 <span className="flex items-center gap-1 text-xs text-danger">
1204 <CircleSlash size={13} /> Changes requested
1205 </span>
1206 ) : (
1207 <span className="text-xs text-faint">Review requested</span>
1208 )}
1209 </li>
1210 );
1211 })}
1212 {reviewerNames.length === 0 && (
1213 <li className="px-1 text-xs text-faint">No reviews requested yet.</li>
1214 )}
Project dependencies: addresses, preview stacks, Affects, and agents who know1215 </ul>
Pull requests: title and tabs across the full width, with counts and the size of the change1216 {pull.status === "open" && canManage && (
1217 <div className="mt-3 space-y-2">
1218 {agentsEnabled && !reviewPending && (
1219 <Form method="post">
1220 <input type="hidden" name="action" value="agent-review" />
1221 {pull.reviewers.map((name) => (
1222 <input key={name} type="hidden" name="reviewer" value={name} />
1223 ))}
1224 <div className="*:w-full">
1225 <Button variant="quiet" type="submit">
1226 <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-agent1227 Request review from g1t
Pull requests: title and tabs across the full width, with counts and the size of the change1228 </Button>
1229 </div>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1230 <ComputeNote note={loaderData.computeNote} />
Pull requests: title and tabs across the full width, with counts and the size of the change1231 </Form>
1232 )}
1233 <details>
1234 <summary className="cursor-pointer text-xs text-faint hover:text-fg">
1235 Request review from people
1236 </summary>
1237 <Form method="post" className="mt-2 space-y-2" key={pull.reviewers.join()}>
1238 <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-agent1239 {pull.reviewers.includes("g1t") && (
1240 <input type="hidden" name="reviewer" value="g1t" />
Pull requests: title and tabs across the full width, with counts and the size of the change1241 )}
1242 <PeoplePicker
1243 name="reviewer"
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights1244 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-agent1245 chosen={pull.reviewers.filter((name) => name !== "g1t")}
Pull requests: title and tabs across the full width, with counts and the size of the change1246 />
1247 <Button variant="quiet" type="submit">
1248 Save reviewers
1249 </Button>
1250 </Form>
1251 </details>
1252 </div>
Project dependencies: addresses, preview stacks, Affects, and agents who know1253 )}
Pull requests: title and tabs across the full width, with counts and the size of the change1254 {["agent-review", "reviewers"].includes(String(actionData?.action)) && (
1255 <ErrorText>{actionData?.error}</ErrorText>
1256 )}
Project dependencies: addresses, preview stacks, Affects, and agents who know1257 </section>
Acceptance checks in sandboxes, line comments and review verdicts1258
Pull requests: title and tabs across the full width, with counts and the size of the change1259 <section>
1260 <h3 className="text-sm font-medium">Assignees</h3>
1261 <ul className="mt-2 space-y-1.5 text-sm">
1262 {pull.assignees.map((name) => (
Agents as a team: lifecycle, merge queue, billing and a new shell1263 <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 change1264 <Avatar name={name} size={20} />
Agents as a team: lifecycle, merge queue, billing and a new shell1265 <span className="grow truncate font-mono text-xs">{name}</span>
1266 </li>
Pull requests: title and tabs across the full width, with counts and the size of the change1267 ))}
1268 {pull.assignees.length === 0 && (
1269 <li className="px-1 text-xs text-faint">No one yet.</li>
1270 )}
1271 </ul>
1272 {viewer && canManage && active && (
1273 <div className="mt-3 space-y-2">
1274 {!pull.assignees.includes(viewer.username) && (
1275 <Form method="post">
1276 <input type="hidden" name="action" value="assign" />
1277 {pull.assignees.map((name) => (
1278 <input key={name} type="hidden" name="assignee" value={name} />
1279 ))}
1280 <input type="hidden" name="assignee" value={viewer.username} />
1281 <div className="*:w-full">
1282 <Button variant="quiet" type="submit">
1283 Assign yourself
1284 </Button>
1285 </div>
1286 </Form>
1287 )}
1288 <details>
1289 <summary className="cursor-pointer text-xs text-faint hover:text-fg">
1290 Assign people
1291 </summary>
1292 <Form method="post" className="mt-2 space-y-2" key={pull.assignees.join()}>
1293 <input type="hidden" name="action" value="assign" />
1294 <PeoplePicker name="assignee" members={members} chosen={pull.assignees} />
Agents as a team: lifecycle, merge queue, billing and a new shell1295 <Button variant="quiet" type="submit">
Pull requests: title and tabs across the full width, with counts and the size of the change1296 Save assignees
Agents as a team: lifecycle, merge queue, billing and a new shell1297 </Button>
Pull requests: title and tabs across the full width, with counts and the size of the change1298 </Form>
1299 </details>
1300 </div>
Issues and pull requests replace intents and attempts1301 )}
Pull requests: title and tabs across the full width, with counts and the size of the change1302 {actionData?.action === "assign" && <ErrorText>{actionData.error}</ErrorText>}
1303 </section>
Issues and pull requests replace intents and attempts1304
Pull requests: title and tabs across the full width, with counts and the size of the change1305 <section>
1306 <h3 className="text-sm font-medium">Working copy</h3>
1307 {pull.branch ? (
1308 <>
1309 <p className="mt-1 text-xs text-muted">
1310 <GitBranch size={13} className="mr-1 inline align-[-2px]" />
1311 Branch <span className="font-mono text-fg">{pull.branch}</span> of this
1312 repository. Pushes to it show up here.
1313 </p>
1314 <div className="mt-2">
1315 <CopyLine text={`git clone -b ${pull.branch} ${remote}`} />
1316 </div>
1317 </>
1318 ) : (
1319 <>
1320 <p className="mt-1 text-xs text-muted">
1321 This fork belongs to the pull request. Pushes to it show up here.
1322 </p>
1323 <div className="mt-2">
1324 <CopyLine text={`git clone ${remote}`} />
1325 </div>
1326 </>
1327 )}
1328 <p className="mt-3 flex items-center gap-1.5 font-mono text-xs text-faint">
1329 <GitCommitHorizontal size={13} />
1330 {pull.headCommit?.slice(0, 12) ?? "no commits pushed yet"}
1331 </p>
1332 </section>
1333 </aside>
1334 </div>
Issues and pull requests replace intents and attempts1335 </div>
1336 );
1337}
Pull requests say where they are live; the project menu shows what is coming1338
1339/** Where the pull request's change is live, as GitHub shows a deployment. */
1340function DeploymentCard({
1341 preview,
1342 build,
1343 stacked,
1344 base,
1345}: {
1346 preview: LiveApp | null;
1347 build: Deployment | null;
Fast pages, required checks on the branch, self-hosted runners, honest incidents1348 stacked: Promise<Stacked[]>;
Pull requests say where they are live; the project menu shows what is coming1349 base: string;
1350}) {
1351 const building = build?.status === "queued" || build?.status === "building";
1352 const failed = build?.status === "failed";
1353 return (
1354 <section className="ml-12 overflow-hidden rounded-xl border border-line bg-surface">
1355 <div className="flex flex-wrap items-center gap-3 px-4 py-3">
1356 <span
1357 className={`flex size-8 shrink-0 items-center justify-center rounded-full ring-1 ${
1358 failed ? "text-danger ring-danger/40" : building ? "text-warn ring-warn/40" : "text-accent ring-accent/40"
1359 }`}
1360 >
1361 {building ? <Loader size={15} className="animate-spin" /> : <Rocket size={15} />}
1362 </span>
1363 <div className="min-w-0 grow">
1364 <p className="text-sm font-medium">
1365 {building
1366 ? preview
1367 ? "Deploying the latest push. The last preview is still live."
1368 : "Deploying a preview"
1369 : failed
1370 ? "The preview failed to deploy"
1371 : "This branch is live"}
1372 </p>
1373 {preview ? (
1374 <a href={preview.url} className="mt-0.5 block truncate font-mono text-xs text-muted hover:text-accent">
1375 {preview.url.replace(/^https?:[/][/]/, "")}
1376 </a>
1377 ) : (
1378 build?.error && <p className="mt-0.5 truncate text-xs text-muted">{build.error}</p>
1379 )}
1380 </div>
1381 <div className="flex shrink-0 items-center gap-2">
1382 {build && (
1383 <Link to={`${base}/deployments/${build.id}`} className="text-xs text-muted hover:text-fg">
1384 {failed ? "See why" : "Build log"}
1385 </Link>
1386 )}
1387 {preview && (
1388 <a
1389 href={preview.url}
1390 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"
1391 >
1392 Visit preview
1393 <ArrowUpRight size={13} />
1394 </a>
1395 )}
1396 </div>
1397 </div>
1398 {preview && (
1399 <p className="border-t border-line px-4 py-2 text-xs text-faint">
1400 <span className="font-mono">{preview.commit.slice(0, 7)}</span> · deployed <TimeAgo at={preview.deployedAt} />
1401 {preview.branch && ` · from ${preview.branch}`}
1402 </p>
1403 )}
Fast pages, required checks on the branch, self-hosted runners, honest incidents1404 {/* Read after the page: the card's own part shows at once. */}
1405 <Suspense fallback={null}>
1406 <Await resolve={stacked}>
1407 {(stacked) =>
1408 stacked.length > 0 && (
1409 <div className="border-t border-line px-4 py-2.5">
1410 <p className="text-xs text-muted">Built against this change:</p>
1411 <ul className="mt-1.5 space-y-1">
1412 {stacked.map((entry) => (
1413 <li key={entry.slug} className="flex items-center gap-2 text-xs">
1414 <span className="font-medium">{entry.name}</span>
1415 <a href={entry.url} className="truncate font-mono text-muted hover:text-accent">
1416 {entry.url.replace(/^https?:[/][/]/, "")}
1417 </a>
1418 </li>
1419 ))}
1420 </ul>
1421 </div>
1422 )
1423 }
1424 </Await>
1425 </Suspense>
Pull requests say where they are live; the project menu shows what is coming1426 </section>
1427 );
1428}