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

1,435 lines63,242 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,
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";
Issues and pull requests replace intents and attempts79
80const REFRESH_MS = 4000;
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 revalidator = useRevalidator();
565 const working = pull.status === "draft";
Fast pages, required checks on the branch, self-hosted runners, honest incidents566 const checking = statuses.some((status) => status.state === "pending");
Acceptance checks in sandboxes, line comments and review verdicts567 const reviews = verdicts(comments);
Fast pages, required checks on the branch, self-hosted runners, honest incidents568 // What stands between this pull request and a merge, if anything: a
569 // required check that has not passed, or the merge queue taking it out.
570 const unchecked = requiredChecks.some((check) => check.state !== "success") || checks?.status === "failed";
571 const requiredFailed = requiredChecks.some((check) => check.state === "failure") || checks?.status === "failed";
Agents as a team: lifecycle, merge queue, billing and a new shell572 // Pull requests for other issues changing the same files will conflict;
573 // ones for the same issue are alternatives, and expected to.
574 const collisions = overlaps.filter((other) => other.issue == null || other.issue !== pull.issue);
575 const review = {
576 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 rights577 // Nobody reviews their own pull request, nor one g1t made for them.
578 canJudge: active && viewer != null && viewer.id !== workOwner(pull).id,
Agents as a team: lifecycle, merge queue, billing and a new shell579 };
580 // Everyone whose review was asked for, then anyone who reviewed unasked.
581 const reviewerNames = [
582 ...new Set([...pull.reviewers, ...reviews.map(({ reviewer }) => reviewer)]),
583 ];
Catching up with main takes seconds when the two sides touched different files584 // A catch-up: the click shows at once; the answer says whether it is
585 // done already or a sandbox is on it.
586 const navigation = useNavigation();
587 const catchUpPending = navigation.state !== "idle" && navigation.formData?.get("action") === "update";
588 const catchUp = actionData?.action === "update" ? actionData : null;
589 const caughtUp = catchUp && "updated" in catchUp ? catchUp.updated : null;
590 const agentCatchUp = catchUp && "agent" in catchUp ? catchUp.agent : null;
591 // Pushed already: followed until the pull request's head is the merge,
592 // so its checks show starting again, for a minute at most.
593 const settling =
594 caughtUp != null && !caughtUp.already && pull.headCommit !== caughtUp.commit && Date.now() - caughtUp.at < 60_000;
595 // Followed until it is pushed, or for so long, never longer.
596 const catchingUp =
597 agentCatchUp != null && behind && Date.now() - agentCatchUp.startedAt < CATCH_UP_TIMEOUT_MS + REFRESH_MS;
Agents as a team: lifecycle, merge queue, billing and a new shell598 // g1t is taking a step of its own accord, so the page will change.
599 const moving =
600 lifecycle != null && lifecycle.stage !== "ready" && lifecycle.stage !== "needs_you";
Agents and memory, checks and conflicts, profiles, slug renames, custom domains601 // Whether it merges cleanly is being worked out, so the box will change.
602 const probing = active && mergeable === "checking";
603 const conflicting = active && mergeable === "conflicting";
Issues and pull requests replace intents and attempts604 useEffect(() => {
Catching up with main takes seconds when the two sides touched different files605 if (!working && !checking && !reviewPending && !catchingUp && !settling && !moving && !landing && !probing) return;
Issues and pull requests replace intents and attempts606 const timer = setInterval(() => {
607 if (document.visibilityState === "visible") revalidator.revalidate();
608 }, REFRESH_MS);
609 return () => clearInterval(timer);
Catching up with main takes seconds when the two sides touched different files610 }, [working, checking, reviewPending, catchingUp, settling, moving, landing, probing, revalidator]);
Agents and memory, checks and conflicts, profiles, slug renames, custom domains611 // Why the merge button cannot be pressed, if it cannot.
612 const mergeBlocked = conflicting
613 ? "Resolve the conflicts first."
614 : probing
615 ? "Waiting to find out whether it merges cleanly."
616 : behind && requireUpToDate
617 ? `This repository requires it to be up to date with ${defaultBranch} first.`
Fast pages, required checks on the branch, self-hosted runners, honest incidents618 : unchecked && !canIgnoreChecks
619 ? `The checks ${defaultBranch} requires have to pass first.`
620 : null;
Issues and pull requests replace intents and attempts621
622 return (
Agents as a team: lifecycle, merge queue, billing and a new shell623 // 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 change624 <div>
625 {/* The title and the tabs run the full width; what is happening on
626 the pull request sits at the top of Conversation. */}
627 <header>
Issues and pull requests replace intents and attempts628 <h2 className="text-2xl font-semibold tracking-tight text-balance">
629 {pull.title} <span className="font-normal text-faint">#{pull.number}</span>
630 </h2>
631 <div className="mt-3 flex flex-wrap items-center gap-x-3 gap-y-2 text-sm text-muted">
632 <PullState status={pull.status} />
633 <span className="flex items-center gap-2">
Agents as a team: lifecycle, merge queue, billing and a new shell634 <Avatar
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent635 name={(pull.status === "merged" && pull.mergedBy) || opener.name}
Agents as a team: lifecycle, merge queue, billing and a new shell636 size={18}
637 />
Issues and pull requests replace intents and attempts638 <span>
Agents and memory, checks and conflicts, profiles, slug renames, custom domains639 <PersonLink
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent640 name={(pull.status === "merged" && pull.mergedBy) || opener.name}
Agents and memory, checks and conflicts, profiles, slug renames, custom domains641 className="font-medium text-fg hover:underline"
642 />{" "}
Pull requests from branches643 {pull.status === "merged" ? "merged" : "wants to merge"}
644 {pull.branch && (
645 <>
646 {" "}
647 <span className="font-mono text-fg">{pull.branch}</span>
648 </>
649 )}{" "}
650 into <span className="font-mono text-fg">{defaultBranch}</span>
Issues and pull requests replace intents and attempts651 </span>
652 </span>
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent653 {opener.requestedBy && (
654 <span className="text-xs">
655 requested by{" "}
656 <PersonLink name={opener.requestedBy} className="font-medium text-fg-soft hover:underline" />
657 </span>
658 )}
659 {/* A pull request from a branch was made by its author, not an agent;
660 one g1t made already says so. */}
661 {!pull.branch && !opener.requestedBy && (
Pull requests from branches662 <span className="flex items-center gap-1.5 font-mono text-xs">
663 <Bot size={14} />
664 {pull.agent}
665 {pull.runtime === "hosted" && <span className="text-faint">on g1t</span>}
666 </span>
667 )}
Issues and pull requests replace intents and attempts668 </div>
669
Acceptance checks in sandboxes, line comments and review verdicts670 {reviews.length > 0 && (
671 <p className="mt-3 flex flex-wrap items-center gap-x-4 gap-y-1 text-sm">
672 {reviews.map(({ reviewer, verdict }) => (
673 <span
674 key={reviewer}
675 className={`flex items-center gap-1.5 ${
676 verdict === "approve" ? "text-accent" : "text-danger"
677 }`}
678 >
679 {verdict === "approve" ? <CircleCheck size={15} /> : <CircleSlash size={15} />}
680 {verdict === "approve" ? "Approved by" : "Changes requested by"}{" "}
681 <span className="font-medium">{reviewer}</span>
682 </span>
683 ))}
684 </p>
685 )}
686
Fast pages, required checks on the branch, self-hosted runners, honest incidents687 <nav className="mt-6 flex items-end gap-1 overflow-x-auto border-b border-line">
Issues and pull requests replace intents and attempts688 <TabLink to={here} active={tab === "conversation"}>
689 <MessageSquare size={15} />
690 Conversation
Pull requests: title and tabs across the full width, with counts and the size of the change691 <TabCount n={comments.length} />
Issues and pull requests replace intents and attempts692 </TabLink>
693 <TabLink to={here + "?tab=session"} active={tab === "session"}>
694 <MessagesSquare size={15} />
695 Session
696 </TabLink>
697 <TabLink to={here + "?tab=changes"} active={tab === "changes"}>
698 <FileDiff size={15} />
Pull requests: title and tabs across the full width, with counts and the size of the change699 Files changed
700 <TabCount n={pull.files.length} />
Issues and pull requests replace intents and attempts701 </TabLink>
Pull requests: title and tabs across the full width, with counts and the size of the change702 {pull.files.length > 0 && (
703 <span className="ml-auto hidden pb-2.5 font-mono text-xs sm:inline">
704 <span className="text-accent">+{pull.files.reduce((sum, file) => sum + file.additions, 0).toLocaleString("en-US")}</span>{" "}
705 <span className="text-danger">−{pull.files.reduce((sum, file) => sum + file.deletions, 0).toLocaleString("en-US")}</span>
706 </span>
707 )}
Issues and pull requests replace intents and attempts708 </nav>
Pull requests: title and tabs across the full width, with counts and the size of the change709 </header>
710 <div className={`mt-6 grid gap-8 ${tab === "changes" ? "" : "lg:grid-cols-[1fr_19rem]"}`}>
711 <div className="min-w-0">
712 {/* What is happening on it now: its stage, its agent, what people told
713 the agent, and what else touches the same files. On the conversation,
714 so the tabs sit right under the title. */}
715 {tab === "conversation" && (
716 <div>
717 {issue && (
718 <Link
719 to={`${base}/issues/${issue.number}`}
720 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"
721 >
722 <IssueIcon issue={issue} />
723 <span className="text-muted">
724 {pull.status === "merged" && issue.resolvedBy === pull.number
725 ? "Resolved"
726 : "For issue"}
727 </span>
728 <span className="truncate font-medium">{issue.title}</span>
729 <span className="text-faint">#{issue.number}</span>
730 {issue.pullCount > 1 && (
731 <span className="ml-auto shrink-0 text-xs text-faint">
732 1 of {issue.pullCount} pull requests
Pull requests show their tabs right under the title; agent panels move into Conversation733 </span>
Pull requests: title and tabs across the full width, with counts and the size of the change734 )}
735 </Link>
736 )}
737 {pull.status === "merged" && (
738 <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">
739 <GitMerge size={16} className="shrink-0 text-merged" />
Agents as a team: lifecycle, merge queue, billing and a new shell740 <span>
Pull requests: title and tabs across the full width, with counts and the size of the change741 Merged into <span className="font-mono">{defaultBranch}</span> by{" "}
742 <span className="font-medium">{pull.mergedBy}</span>{" "}
743 {/* Not an account: the repository's settings said to merge it. */}
744 {pull.mergedBy === "g1t" && "automatically, once it was ready, "}
745 {pull.mergedAt && <TimeAgo at={pull.mergedAt} />}
746 {pull.headCommit && (
747 <span className="font-mono text-muted"> · {pull.headCommit.slice(0, 7)}</span>
Agents as a team: lifecycle, merge queue, billing and a new shell748 )}
749 </span>
Pull requests: title and tabs across the full width, with counts and the size of the change750 </p>
751 )}
752 {pull.supersededBy != null && (
753 <p className="mt-4 rounded-xl border border-line bg-surface px-4 py-2.5 text-sm text-muted">
754 Closed because{" "}
755 <Link
756 to={`${base}/pull/${pull.supersededBy}`}
757 className="font-medium text-fg hover:underline"
758 >
759 #{pull.supersededBy}
760 </Link>{" "}
761 was merged for this issue instead.
762 </p>
763 )}
764 {lifecycle && <LifecyclePanel lifecycle={lifecycle} />}
765 {/* 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 step766 <AgentPanel
767 owner={params.owner}
768 repo={params.repo}
769 number={pull.number}
770 stage={lifecycle?.stage ?? null}
771 confidence={pull.confidence ?? null}
772 />
Pull requests: title and tabs across the full width, with counts and the size of the change773
774 {/* 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 look775 {canRun &&
Pull requests: title and tabs across the full width, with counts and the size of the change776 pull.runtime === "hosted" &&
777 (working || ["working", "revising", "catching_up", "answering"].includes(lifecycle?.stage ?? "")) && (
778 <Form method="post" className="mt-4 rounded-2xl bg-surface p-4 ring-1 ring-merged/30">
779 <p className="flex items-center gap-2 text-sm font-medium">
780 <Sparkles size={15} className="text-merged" />
781 Message the agent
782 </p>
783 <p className="mt-1 text-xs text-muted">
784 A correction, a hint, a change of plan. It reads it at its next step, without
785 starting over.
786 </p>
787 <div className="mt-3 flex gap-2">
788 <input type="hidden" name="action" value="message" />
789 <input
790 name="body"
791 required
792 autoComplete="off"
793 data-1p-ignore
794 placeholder="Keep the old flag working too…"
795 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 shell796 />
Pull requests: title and tabs across the full width, with counts and the size of the change797 <Button type="submit">Send</Button>
798 </div>
799 </Form>
Agents as a team: lifecycle, merge queue, billing and a new shell800 )}
Pull requests: title and tabs across the full width, with counts and the size of the change801 {messages.length > 0 && (
802 <ul className="mt-3 space-y-1.5">
803 {messages.map((message) => (
804 <li key={message.id} className="flex items-start gap-2 text-sm">
805 <Avatar name={message.author} size={18} />
806 <span className="min-w-0 grow">
807 <span className="font-medium">
808 {message.fromNumber != null ? `The agent on #${message.fromNumber}` : message.author}
809 </span>{" "}
810 <span className="text-muted">
811 {message.kind === "question"
812 ? "asked:"
813 : message.kind === "handoff"
814 ? "handed over:"
815 : message.kind === "answer"
816 ? "answered:"
817 : "to the agent:"}
818 </span>{" "}
819 {message.body}
820 {message.answer && (
821 <span className="mt-1 block border-l-2 border-merged/40 pl-2 text-muted">
822 {message.declined ? "Declined: " : "Answer: "}
823 {message.answer}
824 </span>
825 )}
826 </span>
827 <span className={`shrink-0 text-xs ${message.deliveredAt ? "text-accent" : "text-faint"}`}>
828 {message.deliveredAt ? "read by the agent" : "waiting for its next step"}
829 </span>
830 </li>
831 ))}
832 </ul>
833 )}
834 {collisions.length > 0 && active && (
835 <div className="mt-4 rounded-xl border border-line bg-surface px-4 py-3 text-sm">
836 <p className="flex items-center gap-2.5 font-medium">
837 <Radar size={16} className="shrink-0 text-info" />
838 Other work is changing the same files
839 </p>
840 <ul className="mt-2 space-y-1.5">
841 {collisions.map((other) => (
842 <li key={other.number} className="flex flex-wrap items-baseline gap-x-2 text-muted">
843 <Link
844 to={`${base}/pull/${other.number}`}
845 className="font-medium text-fg hover:underline"
846 >
847 {other.title} <span className="font-normal text-faint">#{other.number}</span>
848 </Link>
849 <span className="font-mono text-xs">{other.paths.join(", ")}</span>
850 </li>
851 ))}
852 </ul>
853 <p className="mt-2 text-xs text-faint">
854 Whichever merges second will have to catch up, and may conflict.
855 </p>
856 </div>
857 )}
858 </div>
859 )}
860 <div className="mt-5">
861 {comparison ? (
862 <DiffView
863 comparison={comparison}
864 review={{
865 comments: comments.filter((comment) => comment.path),
866 canComment: Boolean(viewer),
867 }}
868 />
869 ) : tab === "session" ? (
870 session.length === 0 ? (
871 <EmptyState title="Nothing recorded yet">
872 The agent's prompts, reasoning and tool calls appear here as it
873 works.
874 </EmptyState>
875 ) : (
876 <ol className="relative space-y-5 before:absolute before:top-2 before:bottom-2 before:left-3.25 before:w-px before:bg-line">
877 {session.map((entry) => (
878 <Entry key={entry.seq} entry={entry} agent={pull.agent} />
879 ))}
880 </ol>
881 )
882 ) : (
883 <div className="space-y-4">
884 <TimelineItem
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent885 author={opener.name}
Pull requests: title and tabs across the full width, with counts and the size of the change886 at={pull.createdAt}
887 action={
888 <span>
889 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-agent890 {opener.requestedBy ? (
Pull requests: title and tabs across the full width, with counts and the size of the change891 <>
892 {" "}
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent893 for{" "}
894 <PersonLink name={opener.requestedBy} className="font-medium text-fg-soft hover:underline" />
895 </>
896 ) : !pull.branch && (
897 <>
898 {" "}
Pull requests: title and tabs across the full width, with counts and the size of the change899 with <span className="font-mono text-xs">{pull.agent}</span>
900 </>
901 )}
902 </span>
903 }
904 >
905 {pull.body ? (
906 <Markdown source={pull.body} repo={{ namespace: params.owner, name: params.repo }} />
907 ) : (
908 <p className="text-sm text-muted">
909 {working
910 ? "No description yet. It is written when the pull request is marked ready for review."
911 : "No description."}
912 </p>
913 )}
914 {canManage && pull.status === "open" && (
915 <details className="mt-3 border-t border-line pt-3 text-sm">
916 <summary className="cursor-pointer text-xs text-faint hover:text-fg">
917 Edit description
918 </summary>
919 <Form method="post" className="mt-3 space-y-2">
920 <Textarea
921 name="summary"
922 rows={6}
923 placeholder="What changed and why"
924 defaultValue={pull.body ?? ""}
925 />
926 <Button type="submit">Save</Button>
927 </Form>
928 </details>
929 )}
930 </TimelineItem>
Agents as a team: lifecycle, merge queue, billing and a new shell931
Pull requests: title and tabs across the full width, with counts and the size of the change932 <CommentList comments={comments} review={review} base={base} />
Agents as a team: lifecycle, merge queue, billing and a new shell933
Fast pages, required checks on the branch, self-hosted runners, honest incidents934 {(preview || build) && <DeploymentCard preview={preview} build={build} stacked={stacked} base={base} />}
Pull requests say where they are live; the project menu shows what is coming935
Pull requests: title and tabs across the full width, with counts and the size of the change936 {pull.status === "draft" && (
937 <StatusBox>
938 <StatusRow icon={<Loader size={16} className="text-faint" />} title="This is a draft">
939 It is still being worked on. It can be reviewed and merged once it is
940 marked ready.
941 </StatusRow>
942 {canManage && (
943 <Form method="post" className="space-y-2 px-4 py-3">
944 <Textarea name="summary" rows={3} placeholder="What changed and why" />
945 <Button type="submit">Mark ready for review</Button>
946 </Form>
947 )}
948 </StatusBox>
949 )}
Agents as a team: lifecycle, merge queue, billing and a new shell950
Pull requests: title and tabs across the full width, with counts and the size of the change951 {pull.status === "open" && (
952 <StatusBox>
Fast pages, required checks on the branch, self-hosted runners, honest incidents953 {/* Checks by status at once; job by job when the runs are read. */}
954 <Suspense fallback={checksSection({})}>
955 <Await resolve={workflowJobs}>{checksSection}</Await>
956 </Suspense>
957 {noChecks && <AddCiPrompt owner={params.owner} repo={params.repo} canAdd={canAddCi} compact />}
Agents as a team: lifecycle, merge queue, billing and a new shell958 <StatusRow
Pull requests: title and tabs across the full width, with counts and the size of the change959 icon={
960 reviews.some(({ verdict }) => verdict === "request_changes") ? (
961 <CircleSlash size={16} className="text-danger" />
962 ) : reviews.length > 0 ? (
963 <CircleCheck size={16} className="text-accent" />
964 ) : (
965 <MessageSquare size={16} className="text-faint" />
966 )
967 }
968 title={
969 reviews.length === 0
970 ? "No reviews yet"
971 : reviews
972 .map(
973 ({ reviewer, verdict }) =>
974 `${verdict === "approve" ? "Approved by" : "Changes requested by"} ${reviewer}`,
975 )
976 .join(" · ")
977 }
Agents as a team: lifecycle, merge queue, billing and a new shell978 >
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent979 {reviewPending && "g1t is reviewing it now. "}
Pull requests: title and tabs across the full width, with counts and the size of the change980 {requiredApprovals > 0 &&
981 `This repository requires ${requiredApprovals} approving ${
982 requiredApprovals === 1 ? "review" : "reviews"
983 } before merging.`}
Agents as a team: lifecycle, merge queue, billing and a new shell984 </StatusRow>
Pull requests: title and tabs across the full width, with counts and the size of the change985 {landing ? (
Agents as a team: lifecycle, merge queue, billing and a new shell986 <StatusRow
Pull requests: title and tabs across the full width, with counts and the size of the change987 icon={<Loader size={16} className="animate-spin text-accent" />}
988 title="Merging"
989 >
990 {defaultBranch} has moved, so g1t is bringing this up to date first. It
991 lands as soon as that is done. Watch it in the Session tab.
992 </StatusRow>
Catching up with main takes seconds when the two sides touched different files993 ) : agentCatchUp && behind ? (
994 <CatchUpProgress
995 owner={params.owner}
996 repo={params.repo}
997 number={pull.number}
998 defaultBranch={defaultBranch}
999 behind={behind}
1000 update={agentCatchUp.update}
1001 startedAt={agentCatchUp.startedAt}
1002 retrying={catchUpPending}
1003 />
Pull requests: title and tabs across the full width, with counts and the size of the change1004 ) : conflicting ? (
1005 <ConflictsSection
1006 conflicts={conflicts}
1007 pull={pull}
1008 owner={params.owner}
1009 repo={params.repo}
1010 defaultBranch={defaultBranch}
1011 changesUrl={here + "?tab=changes"}
1012 canResolve={canUpdate && agentsEnabled}
Catching up with main takes seconds when the two sides touched different files1013 resolving={catchUpPending}
1014 error={catchUp && "error" in catchUp ? catchUp.error : null}
Agents as a team: lifecycle, merge queue, billing and a new shell1015 />
Pull requests: title and tabs across the full width, with counts and the size of the change1016 ) : probing ? (
1017 <MergeabilityRow mergeable={mergeable} defaultBranch={defaultBranch} />
1018 ) : behind ? (
1019 <StatusRow
1020 icon={<GitPullRequestArrow size={16} className="text-info" />}
1021 title={`${defaultBranch} has moved since this was made`}
1022 >
1023 {mergeable === "clean" && "It has no conflicts with it. "}
1024 {requireUpToDate
1025 ? "This repository requires pull requests to be up to date, so it has to catch up before it can merge."
1026 : "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 files1027 {canUpdate && (
1028 <Form method="post" className="mt-2">
Pull requests: title and tabs across the full width, with counts and the size of the change1029 <input type="hidden" name="action" value="update" />
Catching up with main takes seconds when the two sides touched different files1030 <Button variant="quiet" type="submit" disabled={catchUpPending}>
1031 {catchUpPending && <Loader size={14} className="animate-spin" />}
1032 {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 change1033 </Button>
1034 </Form>
1035 )}
Catching up with main takes seconds when the two sides touched different files1036 {catchUp && "error" in catchUp && <ErrorText>{catchUp.error}</ErrorText>}
Pull requests: title and tabs across the full width, with counts and the size of the change1037 </StatusRow>
1038 ) : (
1039 pull.headCommit && (
1040 <StatusRow
1041 icon={<CircleCheck size={16} className="text-accent" />}
Catching up with main takes seconds when the two sides touched different files1042 title={
1043 (caughtUp && !caughtUp.already) || agentCatchUp
1044 ? `Brought up to date with ${defaultBranch}`
1045 : `Up to date with ${defaultBranch}`
1046 }
1047 >
1048 {caughtUp && !caughtUp.already && (
1049 <>
1050 Merged <span className="font-mono">{defaultBranch}</span> in as{" "}
1051 <span className="font-mono text-fg">{caughtUp.commit.slice(0, 7)}</span>. Its checks run
1052 again on the new commit.
1053 </>
1054 )}
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent1055 {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 files1056 </StatusRow>
Pull requests: title and tabs across the full width, with counts and the size of the change1057 )
1058 )}
1059 {stalled && !lifecycle && (
1060 <StatusRow icon={<Hand size={16} className="text-warn" />} title="Needs you">
1061 {stalled}
1062 </StatusRow>
1063 )}
1064 {canMerge && lifecycle?.stage === "queued" && (
1065 <Form method="post" className="flex flex-wrap items-center gap-3 px-4 py-3">
1066 <ButtonLink to={`${base}/queue`}>
1067 <Layers size={15} />
1068 See the queue
1069 </ButtonLink>
1070 <Button variant="quiet" type="submit" name="action" value="unqueue">
1071 Remove from the queue
Agents as a team: lifecycle, merge queue, billing and a new shell1072 </Button>
Pull requests: title and tabs across the full width, with counts and the size of the change1073 </Form>
1074 )}
1075 {canMerge && !landing && lifecycle?.stage !== "queued" && (
1076 <Form method="post" className="space-y-3 px-4 py-3">
1077 {issue?.state === "open" && (
1078 <CheckboxOption
1079 name="keepIssueOpen"
1080 label={`Keep #${issue.number} open. This is only part of the work.`}
1081 labelClassName="text-xs text-muted"
1082 />
1083 )}
1084 {unchecked && canIgnoreChecks && (
1085 <CheckboxOption
1086 name="ignoreChecks"
Fast pages, required checks on the branch, self-hosted runners, honest incidents1087 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 change1088 labelClassName="text-xs text-muted"
1089 />
1090 )}
1091 <div className="flex flex-wrap items-center gap-3">
1092 <Button
1093 variant="accent"
1094 type="submit"
1095 name="action"
1096 value="merge"
1097 disabled={mergeBlocked != null}
1098 title={mergeBlocked ?? undefined}
1099 >
1100 {mergeQueue ? <Layers size={15} /> : <GitMerge size={15} />}
1101 {mergeQueue ? "Add to the merge queue" : `Merge into ${defaultBranch}`}
1102 </Button>
1103 <span className={`text-xs ${mergeBlocked ? "text-danger" : "text-muted"}`}>
1104 {mergeBlocked ?? (mergeQueue
1105 ? `Tested together with everything ahead of it, then lands on ${defaultBranch}.`
1106 : issue?.state === "open"
1107 ? `Closes issue #${issue.number}, and any other pull requests still open for it.`
1108 : `Lands its commits on ${defaultBranch}.`)}
1109 </span>
1110 </div>
1111 {actionData?.action === "merge" && (
1112 <ErrorText>{actionData.error}</ErrorText>
1113 )}
1114 </Form>
1115 )}
1116 </StatusBox>
1117 )}
Agents as a team: lifecycle, merge queue, billing and a new shell1118
Pull requests: title and tabs across the full width, with counts and the size of the change1119 <CommentForm
1120 author={viewer?.username ?? null}
1121 resetKey={comments.length}
1122 review={review}
1123 />
1124 {canManage && active && (
1125 <Form method="post" className="flex justify-end">
1126 <Button variant="quiet" type="submit" name="action" value="close">
1127 Close pull request
1128 </Button>
1129 </Form>
1130 )}
1131 {actionData &&
Fast pages, required checks on the branch, self-hosted runners, honest incidents1132 !["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 change1133 String(actionData.action),
1134 ) && <ErrorText>{actionData.error}</ErrorText>}
1135 </div>
1136 )}
1137 {actionData?.action === "comment" && (
1138 <div className="mt-2">
1139 <ErrorText>{actionData.error}</ErrorText>
1140 </div>
1141 )}
1142 </div>
1143 </div>
1144
1145 <aside className={tab === "changes" ? "hidden" : "space-y-6"}>
1146 {/* An open pull request shows its checks in full in the merge box. */}
Fast pages, required checks on the branch, self-hosted runners, honest incidents1147 {pull.status !== "open" && <WorkflowStatuses statuses={statuses} />}
Pull requests: title and tabs across the full width, with counts and the size of the change1148 {affects.length > 0 && (
1149 <section>
1150 <h3 className="flex items-center gap-1.5 text-sm font-medium">
1151 <Network size={14} className="text-faint" />
1152 Affects
1153 </h3>
1154 <p className="mt-1 text-xs text-muted">Projects that use this one, and so may feel this change:</p>
1155 <ul className="mt-2 space-y-1 text-sm">
1156 {affects.map((project) => (
1157 <li key={project.slug} className="flex items-center justify-between gap-2">
1158 <Link to={`/${params.owner}/${project.slug}`} className="hover:underline">
1159 {project.name}
1160 </Link>
1161 {project.as && <code className="font-mono text-xs text-faint">{project.as}</code>}
1162 </li>
1163 ))}
1164 </ul>
1165 {preview?.branch && canMerge && (
1166 <Form method="post" className="mt-3">
1167 <input type="hidden" name="action" value="stack" />
1168 <input type="hidden" name="branch" value={preview.branch} />
1169 <button
1170 type="submit"
1171 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"
1172 >
1173 Preview them against this change
1174 </button>
Agents as a team: lifecycle, merge queue, billing and a new shell1175 </Form>
1176 )}
Pull requests: title and tabs across the full width, with counts and the size of the change1177 {actionData?.action === "stack" &&
1178 ("notice" in actionData ? (
1179 <p className="mt-2 text-xs text-accent">{String(actionData.notice)}</p>
1180 ) : (
1181 <ErrorText>{actionData.error}</ErrorText>
1182 ))}
1183 </section>
Acceptance checks in sandboxes, line comments and review verdicts1184 )}
Issues and pull requests replace intents and attempts1185
Project dependencies: addresses, preview stacks, Affects, and agents who know1186 <section>
Pull requests: title and tabs across the full width, with counts and the size of the change1187 <h3 className="text-sm font-medium">Reviewers</h3>
1188 <ul className="mt-2 space-y-1.5 text-sm">
1189 {reviewerNames.map((name) => {
1190 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-agent1191 const pending = name === "g1t" && reviewPending;
Pull requests: title and tabs across the full width, with counts and the size of the change1192 return (
1193 <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-agent1194 {name === "g1t" ? (
Pull requests: title and tabs across the full width, with counts and the size of the change1195 <Sparkles size={16} className="shrink-0 text-accent" />
1196 ) : (
1197 <Avatar name={name} size={20} />
1198 )}
1199 <span className="grow truncate font-mono text-xs">{name}</span>
1200 {pending ? (
1201 <span className="flex items-center gap-1.5 text-xs text-muted">
1202 <span className="size-1.5 animate-pulse rounded-full bg-accent" />
1203 Reviewing
1204 </span>
1205 ) : verdict === "approve" ? (
1206 <span className="flex items-center gap-1 text-xs text-accent">
1207 <CircleCheck size={13} /> Approved
1208 </span>
1209 ) : verdict === "request_changes" ? (
1210 <span className="flex items-center gap-1 text-xs text-danger">
1211 <CircleSlash size={13} /> Changes requested
1212 </span>
1213 ) : (
1214 <span className="text-xs text-faint">Review requested</span>
1215 )}
1216 </li>
1217 );
1218 })}
1219 {reviewerNames.length === 0 && (
1220 <li className="px-1 text-xs text-faint">No reviews requested yet.</li>
1221 )}
Project dependencies: addresses, preview stacks, Affects, and agents who know1222 </ul>
Pull requests: title and tabs across the full width, with counts and the size of the change1223 {pull.status === "open" && canManage && (
1224 <div className="mt-3 space-y-2">
1225 {agentsEnabled && !reviewPending && (
1226 <Form method="post">
1227 <input type="hidden" name="action" value="agent-review" />
1228 {pull.reviewers.map((name) => (
1229 <input key={name} type="hidden" name="reviewer" value={name} />
1230 ))}
1231 <div className="*:w-full">
1232 <Button variant="quiet" type="submit">
1233 <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-agent1234 Request review from g1t
Pull requests: title and tabs across the full width, with counts and the size of the change1235 </Button>
1236 </div>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1237 <ComputeNote note={loaderData.computeNote} />
Pull requests: title and tabs across the full width, with counts and the size of the change1238 </Form>
1239 )}
1240 <details>
1241 <summary className="cursor-pointer text-xs text-faint hover:text-fg">
1242 Request review from people
1243 </summary>
1244 <Form method="post" className="mt-2 space-y-2" key={pull.reviewers.join()}>
1245 <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-agent1246 {pull.reviewers.includes("g1t") && (
1247 <input type="hidden" name="reviewer" value="g1t" />
Pull requests: title and tabs across the full width, with counts and the size of the change1248 )}
1249 <PeoplePicker
1250 name="reviewer"
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights1251 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-agent1252 chosen={pull.reviewers.filter((name) => name !== "g1t")}
Pull requests: title and tabs across the full width, with counts and the size of the change1253 />
1254 <Button variant="quiet" type="submit">
1255 Save reviewers
1256 </Button>
1257 </Form>
1258 </details>
1259 </div>
Project dependencies: addresses, preview stacks, Affects, and agents who know1260 )}
Pull requests: title and tabs across the full width, with counts and the size of the change1261 {["agent-review", "reviewers"].includes(String(actionData?.action)) && (
1262 <ErrorText>{actionData?.error}</ErrorText>
1263 )}
Project dependencies: addresses, preview stacks, Affects, and agents who know1264 </section>
Acceptance checks in sandboxes, line comments and review verdicts1265
Pull requests: title and tabs across the full width, with counts and the size of the change1266 <section>
1267 <h3 className="text-sm font-medium">Assignees</h3>
1268 <ul className="mt-2 space-y-1.5 text-sm">
1269 {pull.assignees.map((name) => (
Agents as a team: lifecycle, merge queue, billing and a new shell1270 <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 change1271 <Avatar name={name} size={20} />
Agents as a team: lifecycle, merge queue, billing and a new shell1272 <span className="grow truncate font-mono text-xs">{name}</span>
1273 </li>
Pull requests: title and tabs across the full width, with counts and the size of the change1274 ))}
1275 {pull.assignees.length === 0 && (
1276 <li className="px-1 text-xs text-faint">No one yet.</li>
1277 )}
1278 </ul>
1279 {viewer && canManage && active && (
1280 <div className="mt-3 space-y-2">
1281 {!pull.assignees.includes(viewer.username) && (
1282 <Form method="post">
1283 <input type="hidden" name="action" value="assign" />
1284 {pull.assignees.map((name) => (
1285 <input key={name} type="hidden" name="assignee" value={name} />
1286 ))}
1287 <input type="hidden" name="assignee" value={viewer.username} />
1288 <div className="*:w-full">
1289 <Button variant="quiet" type="submit">
1290 Assign yourself
1291 </Button>
1292 </div>
1293 </Form>
1294 )}
1295 <details>
1296 <summary className="cursor-pointer text-xs text-faint hover:text-fg">
1297 Assign people
1298 </summary>
1299 <Form method="post" className="mt-2 space-y-2" key={pull.assignees.join()}>
1300 <input type="hidden" name="action" value="assign" />
1301 <PeoplePicker name="assignee" members={members} chosen={pull.assignees} />
Agents as a team: lifecycle, merge queue, billing and a new shell1302 <Button variant="quiet" type="submit">
Pull requests: title and tabs across the full width, with counts and the size of the change1303 Save assignees
Agents as a team: lifecycle, merge queue, billing and a new shell1304 </Button>
Pull requests: title and tabs across the full width, with counts and the size of the change1305 </Form>
1306 </details>
1307 </div>
Issues and pull requests replace intents and attempts1308 )}
Pull requests: title and tabs across the full width, with counts and the size of the change1309 {actionData?.action === "assign" && <ErrorText>{actionData.error}</ErrorText>}
1310 </section>
Issues and pull requests replace intents and attempts1311
Pull requests: title and tabs across the full width, with counts and the size of the change1312 <section>
1313 <h3 className="text-sm font-medium">Working copy</h3>
1314 {pull.branch ? (
1315 <>
1316 <p className="mt-1 text-xs text-muted">
1317 <GitBranch size={13} className="mr-1 inline align-[-2px]" />
1318 Branch <span className="font-mono text-fg">{pull.branch}</span> of this
1319 repository. Pushes to it show up here.
1320 </p>
1321 <div className="mt-2">
1322 <CopyLine text={`git clone -b ${pull.branch} ${remote}`} />
1323 </div>
1324 </>
1325 ) : (
1326 <>
1327 <p className="mt-1 text-xs text-muted">
1328 This fork belongs to the pull request. Pushes to it show up here.
1329 </p>
1330 <div className="mt-2">
1331 <CopyLine text={`git clone ${remote}`} />
1332 </div>
1333 </>
1334 )}
1335 <p className="mt-3 flex items-center gap-1.5 font-mono text-xs text-faint">
1336 <GitCommitHorizontal size={13} />
1337 {pull.headCommit?.slice(0, 12) ?? "no commits pushed yet"}
1338 </p>
1339 </section>
1340 </aside>
1341 </div>
Issues and pull requests replace intents and attempts1342 </div>
1343 );
1344}
Pull requests say where they are live; the project menu shows what is coming1345
1346/** Where the pull request's change is live, as GitHub shows a deployment. */
1347function DeploymentCard({
1348 preview,
1349 build,
1350 stacked,
1351 base,
1352}: {
1353 preview: LiveApp | null;
1354 build: Deployment | null;
Fast pages, required checks on the branch, self-hosted runners, honest incidents1355 stacked: Promise<Stacked[]>;
Pull requests say where they are live; the project menu shows what is coming1356 base: string;
1357}) {
1358 const building = build?.status === "queued" || build?.status === "building";
1359 const failed = build?.status === "failed";
1360 return (
1361 <section className="ml-12 overflow-hidden rounded-xl border border-line bg-surface">
1362 <div className="flex flex-wrap items-center gap-3 px-4 py-3">
1363 <span
1364 className={`flex size-8 shrink-0 items-center justify-center rounded-full ring-1 ${
1365 failed ? "text-danger ring-danger/40" : building ? "text-warn ring-warn/40" : "text-accent ring-accent/40"
1366 }`}
1367 >
1368 {building ? <Loader size={15} className="animate-spin" /> : <Rocket size={15} />}
1369 </span>
1370 <div className="min-w-0 grow">
1371 <p className="text-sm font-medium">
1372 {building
1373 ? preview
1374 ? "Deploying the latest push. The last preview is still live."
1375 : "Deploying a preview"
1376 : failed
1377 ? "The preview failed to deploy"
1378 : "This branch is live"}
1379 </p>
1380 {preview ? (
1381 <a href={preview.url} className="mt-0.5 block truncate font-mono text-xs text-muted hover:text-accent">
1382 {preview.url.replace(/^https?:[/][/]/, "")}
1383 </a>
1384 ) : (
1385 build?.error && <p className="mt-0.5 truncate text-xs text-muted">{build.error}</p>
1386 )}
1387 </div>
1388 <div className="flex shrink-0 items-center gap-2">
1389 {build && (
1390 <Link to={`${base}/deployments/${build.id}`} className="text-xs text-muted hover:text-fg">
1391 {failed ? "See why" : "Build log"}
1392 </Link>
1393 )}
1394 {preview && (
1395 <a
1396 href={preview.url}
1397 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"
1398 >
1399 Visit preview
1400 <ArrowUpRight size={13} />
1401 </a>
1402 )}
1403 </div>
1404 </div>
1405 {preview && (
1406 <p className="border-t border-line px-4 py-2 text-xs text-faint">
1407 <span className="font-mono">{preview.commit.slice(0, 7)}</span> · deployed <TimeAgo at={preview.deployedAt} />
1408 {preview.branch && ` · from ${preview.branch}`}
1409 </p>
1410 )}
Fast pages, required checks on the branch, self-hosted runners, honest incidents1411 {/* Read after the page: the card's own part shows at once. */}
1412 <Suspense fallback={null}>
1413 <Await resolve={stacked}>
1414 {(stacked) =>
1415 stacked.length > 0 && (
1416 <div className="border-t border-line px-4 py-2.5">
1417 <p className="text-xs text-muted">Built against this change:</p>
1418 <ul className="mt-1.5 space-y-1">
1419 {stacked.map((entry) => (
1420 <li key={entry.slug} className="flex items-center gap-2 text-xs">
1421 <span className="font-medium">{entry.name}</span>
1422 <a href={entry.url} className="truncate font-mono text-muted hover:text-accent">
1423 {entry.url.replace(/^https?:[/][/]/, "")}
1424 </a>
1425 </li>
1426 ))}
1427 </ul>
1428 </div>
1429 )
1430 }
1431 </Await>
1432 </Suspense>
Pull requests say where they are live; the project menu shows what is coming1433 </section>
1434 );
1435}