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

1,358 lines59,214 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";
Catching up with main takes seconds when the two sides touched different files26import { useEffect } from "react";
27import { Form, Link, redirect, useNavigation, useRevalidator } from "react-router";
Issues and pull requests replace intents and attempts28
Pull requests say where they are live; the project menu shows what is coming29import {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look30 type Capability,
Pull requests say where they are live; the project menu shows what is coming31 type Comparison,
32 type Deployment,
Agents and memory, checks and conflicts, profiles, slug renames, custom domains33 type Job,
Pull requests say where they are live; the project menu shows what is coming34 type LiveApp,
35 type SessionEntry,
36 type Viewer,
37 pullComparison,
38} from "@g1t/contracts";
Issues and pull requests replace intents and attempts39
40import type { Route } from "./+types/pull";
Social cards for every page: og.g1t.sh41import { excerpt, page } from "../../lib/meta";
Issues and pull requests replace intents and attempts42import { DiffView } from "../../components/diff-view";
Agents as a team: lifecycle, merge queue, billing and a new shell43import { LifecyclePanel } from "../../components/lifecycle";
Agents and memory, checks and conflicts, profiles, slug renames, custom domains44import { AgentPanel } from "../../components/agents";
Issues and pull requests replace intents and attempts45import { Markdown } from "../../components/markdown";
46import {
47 Avatar,
48 Button,
Agents as a team: lifecycle, merge queue, billing and a new shell49 ButtonLink,
Issues and pull requests replace intents and attempts50 CopyLine,
51 EmptyState,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look52 ComputeNote,
Issues and pull requests replace intents and attempts53 ErrorText,
54 Textarea,
55 TimeAgo,
56} from "../../components/ui";
Workspace names and icons, and a component kit for every control57import { CheckboxOption } from "../../components/ui/checkbox";
GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs58import { WorkflowStatuses } from "../../components/actions";
Acceptance checks in sandboxes, line comments and review verdicts59import { ChecksPanel } from "../../components/checks";
Agents as a team: lifecycle, merge queue, billing and a new shell60import {
61 CommentForm,
62 CommentList,
63 IssueIcon,
64 PeoplePicker,
Agents and memory, checks and conflicts, profiles, slug renames, custom domains65 PersonLink,
Agents as a team: lifecycle, merge queue, billing and a new shell66 PullState,
67 TimelineItem,
68 verdicts,
69} from "../../components/work";
Catching up with main takes seconds when the two sides touched different files70import { CatchUpProgress, ChecksSection, ConflictsSection, MergeabilityRow, runIdOf } from "../../components/merge-box";
71import { CATCH_UP_TIMEOUT_MS } from "../../lib/catch-up";
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put72import { notFound } from "../../lib/not-found.server";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look73import { computeNoteFor } from "../../lib/compute.server";
Agents and memory, checks and conflicts, profiles, slug renames, custom domains74import { actions, deployments, identity, projects, repos, work } from "../../lib/services.server";
Issues and pull requests replace intents and attempts75import { assertSameOrigin, getViewer, requireUser } from "../../lib/session.server";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look76import { accessTo, refusal } from "../../lib/access.server";
Issues and pull requests replace intents and attempts77
78const REFRESH_MS = 4000;
79const EMPTY_COMPARISON: Comparison = { base: null, head: "", files: [], truncated: false };
80const TABS = ["conversation", "session", "changes"] as const;
81type Tab = (typeof TABS)[number];
82
Social cards for every page: og.g1t.sh83export function meta({ loaderData, params, ...args }: Route.MetaArgs) {
84 const pull = loaderData?.pull;
85 const title = pull ? `${pull.title} · Pull request #${pull.number} · ` : "";
86 const state = { draft: "Draft", open: "Open", merged: "Merged", closed: "Closed" }[pull?.status ?? "open"];
87 const body = excerpt(pull?.body);
88 return page(args, {
89 title: `${title}${params.owner}/${params.repo} · g1t`,
90 description: pull
91 ? `${state} pull request #${pull.number} on ${params.owner}/${params.repo} by ${pull.author.username}.${body ? ` ${body}` : ""}`
92 : null,
93 // The card shows the title and the state.
94 version: pull ? [pull.title, pull.status] : undefined,
95 type: "article",
96 });
Issues and pull requests replace intents and attempts97}
98
99export async function loader({ params, context, request }: Route.LoaderArgs) {
100 const viewer = getViewer(context);
101 const path = { namespace: params.owner, name: params.repo };
102 const number = Number(params.number);
103 const asked = new URL(request.url).searchParams.get("tab");
104 const tab: Tab = TABS.find((name) => name === asked) ?? "conversation";
105
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look106 // Members of the workspace pick reviewers and assignees from its people;
107 // what the viewer may do here goes by their role on the repository.
Agents as a team: lifecycle, merge queue, billing and a new shell108 const member = (viewer?.workspaces ?? []).some(
109 (membership) => membership.slug === params.owner,
110 );
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look111 const { can, insider } = await accessTo(context, params);
Agents as a team: lifecycle, merge queue, billing and a new shell112 // At once: none of these depends on another.
Project dependencies: addresses, preview stacks, Affects, and agents who know113 const ref = { workspace: params.owner, slug: params.repo };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look114 const [found, repo, settings, agentsEnabled, members, deps, deployed, computeNote] = await Promise.all([
Issues and pull requests replace intents and attempts115 work.getPull(path, number, viewer),
116 repos.get(path, viewer),
Agents as a team: lifecycle, merge queue, billing and a new shell117 work.getSettings(path, viewer),
g1t's agents only for listed workspaces, whatever the state of billing118 env.RUNNER.enabled(viewer, path),
Agents as a team: lifecycle, merge queue, billing and a new shell119 // A member picks reviewers and assignees from the workspace's people.
120 member ? identity.listMembers(params.owner, viewer) : null,
Project dependencies: addresses, preview stacks, Affects, and agents who know121 // The projects that use this one: what a change here can affect.
122 projects.dependencies(params.owner, params.repo, viewer),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look123 insider ? deployments.list(ref, viewer) : null,
124 // Before someone asks g1t-agent for something: whether the plan lets it start.
125 can.run ? computeNoteFor(params.owner, "agent") : null,
Issues and pull requests replace intents and attempts126 ]);
127 if (!found.ok) {
128 // Issues and pull requests share numbers; this one may be an issue.
129 const issue = await work.getIssue(path, number, viewer);
130 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 put131 throw notFound("pull");
Issues and pull requests replace intents and attempts132 }
133 const { pull } = found.value;
Pull requests from branches134 const range = pullComparison(pull);
Agents and memory, checks and conflicts, profiles, slug renames, custom domains135 // The jobs of each workflow run on its head, to list checks job by job.
136 // Read as the viewer: a run they cannot see is listed by its status alone.
137 const runIds = [...new Set((found.value.statuses ?? []).map(runIdOf).filter((id) => id != null))].slice(0, 10);
138 const [session, comparison, runs] = await Promise.all([
Issues and pull requests replace intents and attempts139 tab === "session" ? work.readSession(path, number, viewer) : null,
Pull requests from branches140 tab === "changes"
141 ? repos.compare(range.repoId, viewer, range.base, range.head)
142 : null,
Agents and memory, checks and conflicts, profiles, slug renames, custom domains143 tab === "conversation" && pull.status === "open"
144 ? Promise.all(runIds.map((id) => actions.run(path, viewer, id).catch(() => null)))
145 : [],
Issues and pull requests replace intents and attempts146 ]);
Agents and memory, checks and conflicts, profiles, slug renames, custom domains147 const workflowJobs: Record<string, Job[]> = {};
148 for (const run of runs) if (run?.ok) workflowJobs[run.value.run.id] = run.value.jobs;
Issues and pull requests replace intents and attempts149 return {
150 ...found.value,
Agents and memory, checks and conflicts, profiles, slug renames, custom domains151 workflowJobs,
Issues and pull requests replace intents and attempts152 tab,
153 session: session?.ok ? session.value : [],
154 // An empty comparison if it could not be made.
155 comparison: comparison && (comparison.ok ? comparison.value : EMPTY_COMPARISON),
156 viewer,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look157 // Write and up can merge.
158 canMerge: can.merge,
159 // Triage and up manage anyone's pull request; its author, their own.
160 canManage: can.triage || (viewer != null && viewer.id === pull.author.id),
161 // Telling its agent things, and re-running checks, spend compute: Write and up.
162 canRun: can.run,
Agents as a team: lifecycle, merge queue, billing and a new shell163 // A catch-up is pushed as the viewer: a fork takes pushes only from
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look164 // whoever opened it, a branch from anyone who can push.
165 canUpdate: pull.fork ? viewer?.id === pull.author.id : can.push,
Agents as a team: lifecycle, merge queue, billing and a new shell166 agentsEnabled,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look167 computeNote,
Agents as a team: lifecycle, merge queue, billing and a new shell168 members: members?.ok ? members.value.map((person) => person.username) : [],
169 requireUpToDate: settings.ok && settings.value.requireUpToDate,
170 mergeQueue: settings.ok && settings.value.mergeQueue,
171 requiredApprovals: settings.ok ? settings.value.requiredApprovals : 0,
172 canIgnoreChecks: !settings.ok || settings.value.allowIgnoringChecks,
Issues and pull requests replace intents and attempts173 defaultBranch: repo.ok ? repo.value.defaultBranch : "main",
Project dependencies: addresses, preview stacks, Affects, and agents who know174 affects: deps.ok ? deps.value.usedBy : [],
Pull requests say where they are live; the project menu shows what is coming175 ...(await deploymentOf(deployed?.ok ? deployed.value : null, number, params.owner, deps.ok ? deps.value.usedBy : [], viewer)),
Issues and pull requests replace intents and attempts176 };
177}
178
Pull requests say where they are live; the project menu shows what is coming179/**
180 * Where this pull request is live: its preview (or its latest build, while
181 * one is going or after one failed), and the previews of the projects
182 * that use this one, built against it.
183 */
184async function deploymentOf(
185 list: { deployments: Deployment[]; live: LiveApp[] } | null,
186 number: number,
187 owner: string,
188 affects: { slug: string; name: string }[],
189 viewer: Viewer,
190) {
191 if (!list) return { preview: null, build: null, stacked: [] };
192 const preview = list.live.find((app) => app.kind === "preview" && app.number === number) ?? null;
193 const build = list.deployments.find((d) => d.kind === "preview" && d.number === number) ?? null;
194 const branch = preview?.branch ?? build?.branch ?? null;
195 const stacked = branch
196 ? (
197 await Promise.all(
198 affects.slice(0, 5).map(async (project) => {
199 const theirs = await deployments.list({ workspace: owner, slug: project.slug }, viewer);
200 const app = theirs.ok ? theirs.value.live.find((a) => a.kind === "preview" && a.branch === branch) : undefined;
201 return app ? { name: project.name, slug: project.slug, url: app.url } : null;
202 }),
203 )
204 ).filter((entry) => entry != null)
205 : [];
206 return { preview, build, stacked };
207}
208
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look209const PULL_NEEDS: Record<string, Capability> = {
210 stack: "run",
211 "agent-review": "run",
212 "rerun-workflow": "run",
213 recheck: "run",
214 message: "run",
215 merge: "merge",
216 unqueue: "merge",
217};
218
Issues and pull requests replace intents and attempts219export async function action({ request, params, context }: Route.ActionArgs) {
220 assertSameOrigin(request);
221 const user = requireUser(context, request);
222 const form = await request.formData();
223 const path = { namespace: params.owner, name: params.repo };
224 const number = Number(params.number);
225 const action = form.get("action");
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look226 // What each form needs beyond reading; work checks the rest.
227 const needed = typeof action === "string" ? PULL_NEEDS[action] : undefined;
228 const refused = needed ? await refusal(context, params, needed) : null;
229 if (refused) return { error: refused, action: String(action) };
Acceptance checks in sandboxes, line comments and review verdicts230 const verdict = form.get("verdict");
231 const line = Number(form.get("line"));
Agents as a team: lifecycle, merge queue, billing and a new shell232 /** Names ticked in a people picker, plus those typed beside it. */
233 const picked = (field: string) => [
234 ...form.getAll(field).map(String),
235 ...String(form.get("others") ?? "").split(/[\s,]+/),
236 ];
Project dependencies: addresses, preview stacks, Affects, and agents who know237 // The projects that use this one, built against this pull request's preview.
238 if (action === "stack") {
239 const built = await deployments.stack(user, { workspace: params.owner, slug: params.repo }, String(form.get("branch") ?? ""));
240 return built.ok
241 ? { action, notice: `Building ${built.value.join(", ")} against this preview. They appear on their Deployments pages.` }
242 : { action, error: built.error.message };
243 }
Agents as a team: lifecycle, merge queue, billing and a new shell244 // Asking a g1t agent for its review records the request, then starts it.
245 if (action === "agent-review") {
246 const asked = await work.updatePull(user, path, number, {
247 reviewers: [...form.getAll("reviewer").map(String), "g1t-agent"],
248 });
249 if (!asked.ok) return { error: asked.error.message, action };
250 }
Catching up with main takes seconds when the two sides touched different files251 // Catching up: merged and pushed in seconds when the two sides changed
252 // different files; otherwise handed to a sandbox, which takes a minute.
253 if (action === "update") {
254 const caught = await work.catchUpPull(user, path, number);
255 if (!caught.ok) return { action, error: caught.error.message };
256 const update = caught.value;
257 if (update.outcome !== "needs_agent") {
258 return {
259 action,
260 updated: { commit: update.commit, already: update.outcome === "up_to_date", at: Date.now() },
261 };
262 }
263 const started = await env.RUNNER.update(user, path, number);
264 if (!started.ok) return { action, error: started.error.message };
265 return { action, agent: { update, startedAt: Date.now() } };
266 }
Agents and memory, checks and conflicts, profiles, slug renames, custom domains267 // A workflow run's failed jobs, run again. Who may is the actions service's call.
268 if (action === "rerun-workflow") {
269 const rerun = await actions.rerun(user, path, String(form.get("run") ?? ""), true);
270 return rerun.ok ? null : { error: rerun.error.message, action };
271 }
Issues and pull requests replace intents and attempts272 const result =
273 action === "merge"
Acceptance checks in sandboxes, line comments and review verdicts274 ? await work.mergePull(user, path, number, {
275 keepIssueOpen: form.get("keepIssueOpen") === "on",
276 ignoreChecks: form.get("ignoreChecks") === "on",
277 })
Agents as a team: lifecycle, merge queue, billing and a new shell278 : action === "unqueue"
279 ? await work.removeFromQueue(user, path, number)
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request280 : action === "message"
281 ? await work.messageAgent(user, path, number, String(form.get("body") ?? ""))
Issues and pull requests replace intents and attempts282 : action === "close"
283 ? await work.closePull(user, path, number)
Acceptance checks in sandboxes, line comments and review verdicts284 : action === "recheck"
285 ? await env.RUNNER.recheck(user, path, number)
Agents as a team: lifecycle, merge queue, billing and a new shell286 : action === "agent-review"
287 ? await env.RUNNER.review(user, path, number)
288 : action === "reviewers"
289 ? await work.updatePull(user, path, number, { reviewers: picked("reviewer") })
290 : action === "assign"
291 ? await work.updatePull(user, path, number, { assignees: picked("assignee") })
Acceptance checks in sandboxes, line comments and review verdicts292 : action === "comment"
293 ? await work.addComment(user, path, number, {
294 body: String(form.get("body") ?? ""),
295 path: String(form.get("path") ?? "") || undefined,
296 line: line > 0 ? line : undefined,
297 verdict:
298 verdict === "approve" || verdict === "request_changes" ? verdict : undefined,
299 })
300 : await work.readyPull(user, path, number, String(form.get("summary") ?? ""));
Issues and pull requests replace intents and attempts301 return result.ok ? null : { error: result.error.message, action };
302}
303
Pull requests: title and tabs across the full width, with counts and the size of the change304/** How many, beside a tab's name, when there are any. */
305function TabCount({ n }: { n: number }) {
306 if (n <= 0) return null;
307 return (
308 <span className="rounded-full bg-raised px-1.5 py-px text-xs tabular-nums text-muted ring-1 ring-line">
309 {n.toLocaleString("en-US")}
310 </span>
311 );
312}
313
Issues and pull requests replace intents and attempts314function TabLink({
315 to,
316 active,
317 children,
318}: {
319 to: string;
320 active: boolean;
321 children: React.ReactNode;
322}) {
323 return (
324 <Link
325 to={to}
326 preventScrollReset
327 className={
328 "-mb-px flex items-center gap-2 border-b-2 px-1 pb-2.5 text-sm transition-colors " +
329 (active
330 ? "border-accent font-medium text-fg"
331 : "border-transparent text-muted hover:text-fg")
332 }
333 >
334 {children}
335 </Link>
336 );
337}
338
339/** One step of the session, on the timeline's rail. */
340function Entry({ entry, agent }: { entry: SessionEntry; agent: string }) {
341 const isTool = entry.kind === "tool_call" || entry.kind === "tool_result";
342 const Icon =
343 entry.kind === "prompt"
344 ? User
345 : entry.kind === "note"
346 ? StickyNote
347 : isTool
348 ? Wrench
349 : Bot;
350 return (
351 <li className="relative pl-10">
352 <span
353 className={`absolute top-0.5 left-0 flex size-7 items-center justify-center rounded-full border bg-bg ${
354 entry.kind === "prompt"
355 ? "border-accent/50 text-accent"
356 : "border-line text-faint"
357 }`}
358 >
359 <Icon size={14} />
360 </span>
361 {isTool ? (
362 <details className="group rounded-lg border border-line bg-surface">
363 <summary className="flex cursor-pointer list-none items-center gap-2 px-3 py-1.5 text-sm">
364 <ChevronRight
365 size={14}
366 className="text-faint transition-transform group-open:rotate-90"
367 />
368 <span className="font-mono text-xs text-accent">
369 {entry.tool ?? "tool"}
370 </span>
371 <span className="truncate font-mono text-xs text-muted">
372 {entry.kind === "tool_result" ? "→ " : ""}
373 {entry.text.split("\n")[0]}
374 </span>
375 </summary>
376 <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 line377 <code>{entry.text}</code>
Issues and pull requests replace intents and attempts378 </pre>
379 </details>
380 ) : (
381 <div>
382 <p className="text-xs font-medium text-faint">
383 {entry.kind === "prompt"
384 ? "Prompt"
385 : entry.kind === "note"
386 ? "Note"
387 : agent}
388 </p>
389 {entry.kind === "message" ? (
390 <div className="mt-1">
391 <Markdown source={entry.text} />
392 </div>
Agents as a team: lifecycle, merge queue, billing and a new shell393 ) : entry.kind === "prompt" ? (
394 <div className="mt-1.5 max-h-[32rem] overflow-y-auto rounded-xl border border-line bg-surface p-4">
395 <Markdown source={entry.text} />
396 </div>
Issues and pull requests replace intents and attempts397 ) : (
Agents as a team: lifecycle, merge queue, billing and a new shell398 <p className="mt-1 text-[0.9375rem] leading-relaxed wrap-break-word whitespace-pre-wrap">
Issues and pull requests replace intents and attempts399 {entry.text}
400 </p>
401 )}
402 </div>
403 )}
404 {entry.commit && (
405 <p className="mt-1.5 flex items-center gap-1 font-mono text-xs text-faint">
406 <GitCommitHorizontal size={12} />
407 {entry.commit.slice(0, 7)}
408 </p>
409 )}
410 </li>
411 );
412}
413
Agents as a team: lifecycle, merge queue, billing and a new shell414/** The box at the foot of the conversation saying what stands before a merge. */
415function StatusBox({ children }: { children: React.ReactNode }) {
416 return (
417 <div className="flex gap-3">
418 <span className="hidden w-8 shrink-0 sm:block" />
419 <section className="min-w-0 grow divide-y divide-line overflow-hidden rounded-xl border border-line-strong bg-surface">
420 {children}
421 </section>
422 </div>
423 );
424}
425
426function StatusRow({
427 icon,
428 title,
429 children,
430}: {
431 icon: React.ReactNode;
432 title: string;
433 children?: React.ReactNode;
434}) {
435 return (
436 <div className="flex gap-3 px-4 py-3 text-sm">
437 <span className="mt-0.5 shrink-0">{icon}</span>
438 <div className="min-w-0">
439 <p className="font-medium">{title}</p>
440 {children && <div className="mt-0.5 text-muted">{children}</div>}
441 </div>
442 </div>
443 );
444}
445
Issues and pull requests replace intents and attempts446export default function PullPage({ loaderData, actionData, params }: Route.ComponentProps) {
447 const {
448 pull,
449 issue,
450 comments,
Acceptance checks in sandboxes, line comments and review verdicts451 checks,
Agents as a team: lifecycle, merge queue, billing and a new shell452 overlaps,
453 behind,
454 reviewPending,
455 lifecycle,
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request456 messages,
GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs457 statuses = [],
Agents and memory, checks and conflicts, profiles, slug renames, custom domains458 mergeable = "unknown",
459 conflicts = [],
460 earlierChecks = [],
461 workflowJobs,
Project dependencies: addresses, preview stacks, Affects, and agents who know462 affects,
463 preview,
Pull requests say where they are live; the project menu shows what is coming464 build,
465 stacked,
Agents as a team: lifecycle, merge queue, billing and a new shell466 landing,
467 stalled,
468 requireUpToDate,
469 mergeQueue,
470 requiredApprovals,
471 canIgnoreChecks,
472 canUpdate,
473 agentsEnabled,
474 members,
Issues and pull requests replace intents and attempts475 tab,
476 session,
477 comparison,
478 viewer,
479 canMerge,
480 canManage,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look481 canRun,
Issues and pull requests replace intents and attempts482 defaultBranch,
483 } = loaderData;
484 const base = `/${params.owner}/${params.repo}`;
485 const here = `${base}/pull/${pull.number}`;
Pull requests from branches486 const remote = pull.fork
487 ? `https://g1t.sh/${pull.fork.namespace}/${pull.fork.name}.git`
488 : `https://g1t.sh/${params.owner}/${params.repo}.git`;
Issues and pull requests replace intents and attempts489 const active = pull.status === "draft" || pull.status === "open";
490
Acceptance checks in sandboxes, line comments and review verdicts491 // Follow an agent at work, or checks in progress, without a manual reload.
Issues and pull requests replace intents and attempts492 const revalidator = useRevalidator();
493 const working = pull.status === "draft";
GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs494 const checking =
495 checks?.status === "queued" || checks?.status === "running" || statuses.some((status) => status.state === "pending");
Acceptance checks in sandboxes, line comments and review verdicts496 const reviews = verdicts(comments);
497 // What stands between this pull request and a merge, if anything.
498 const unchecked = checks && checks.status !== "passed";
Agents as a team: lifecycle, merge queue, billing and a new shell499 // Pull requests for other issues changing the same files will conflict;
500 // ones for the same issue are alternatives, and expected to.
501 const collisions = overlaps.filter((other) => other.issue == null || other.issue !== pull.issue);
502 const review = {
503 changesUrl: here + "?tab=changes",
504 // Nobody reviews their own pull request.
505 canJudge: active && viewer != null && viewer.id !== pull.author.id,
506 };
507 // Everyone whose review was asked for, then anyone who reviewed unasked.
508 const reviewerNames = [
509 ...new Set([...pull.reviewers, ...reviews.map(({ reviewer }) => reviewer)]),
510 ];
Catching up with main takes seconds when the two sides touched different files511 // A catch-up: the click shows at once; the answer says whether it is
512 // done already or a sandbox is on it.
513 const navigation = useNavigation();
514 const catchUpPending = navigation.state !== "idle" && navigation.formData?.get("action") === "update";
515 const catchUp = actionData?.action === "update" ? actionData : null;
516 const caughtUp = catchUp && "updated" in catchUp ? catchUp.updated : null;
517 const agentCatchUp = catchUp && "agent" in catchUp ? catchUp.agent : null;
518 // Pushed already: followed until the pull request's head is the merge,
519 // so its checks show starting again, for a minute at most.
520 const settling =
521 caughtUp != null && !caughtUp.already && pull.headCommit !== caughtUp.commit && Date.now() - caughtUp.at < 60_000;
522 // Followed until it is pushed, or for so long, never longer.
523 const catchingUp =
524 agentCatchUp != null && behind && Date.now() - agentCatchUp.startedAt < CATCH_UP_TIMEOUT_MS + REFRESH_MS;
Agents as a team: lifecycle, merge queue, billing and a new shell525 // g1t is taking a step of its own accord, so the page will change.
526 const moving =
527 lifecycle != null && lifecycle.stage !== "ready" && lifecycle.stage !== "needs_you";
Agents and memory, checks and conflicts, profiles, slug renames, custom domains528 // Whether it merges cleanly is being worked out, so the box will change.
529 const probing = active && mergeable === "checking";
530 const conflicting = active && mergeable === "conflicting";
Issues and pull requests replace intents and attempts531 useEffect(() => {
Catching up with main takes seconds when the two sides touched different files532 if (!working && !checking && !reviewPending && !catchingUp && !settling && !moving && !landing && !probing) return;
Issues and pull requests replace intents and attempts533 const timer = setInterval(() => {
534 if (document.visibilityState === "visible") revalidator.revalidate();
535 }, REFRESH_MS);
536 return () => clearInterval(timer);
Catching up with main takes seconds when the two sides touched different files537 }, [working, checking, reviewPending, catchingUp, settling, moving, landing, probing, revalidator]);
Agents and memory, checks and conflicts, profiles, slug renames, custom domains538 // Why the merge button cannot be pressed, if it cannot.
539 const mergeBlocked = conflicting
540 ? "Resolve the conflicts first."
541 : probing
542 ? "Waiting to find out whether it merges cleanly."
543 : behind && requireUpToDate
544 ? `This repository requires it to be up to date with ${defaultBranch} first.`
545 : null;
Issues and pull requests replace intents and attempts546
547 return (
Agents as a team: lifecycle, merge queue, billing and a new shell548 // 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 change549 <div>
550 {/* The title and the tabs run the full width; what is happening on
551 the pull request sits at the top of Conversation. */}
552 <header>
Issues and pull requests replace intents and attempts553 <h2 className="text-2xl font-semibold tracking-tight text-balance">
554 {pull.title} <span className="font-normal text-faint">#{pull.number}</span>
555 </h2>
556 <div className="mt-3 flex flex-wrap items-center gap-x-3 gap-y-2 text-sm text-muted">
557 <PullState status={pull.status} />
558 <span className="flex items-center gap-2">
Agents as a team: lifecycle, merge queue, billing and a new shell559 <Avatar
560 name={(pull.status === "merged" && pull.mergedBy) || pull.author.username}
561 size={18}
562 />
Issues and pull requests replace intents and attempts563 <span>
Agents and memory, checks and conflicts, profiles, slug renames, custom domains564 <PersonLink
565 name={(pull.status === "merged" && pull.mergedBy) || pull.author.username}
566 className="font-medium text-fg hover:underline"
567 />{" "}
Pull requests from branches568 {pull.status === "merged" ? "merged" : "wants to merge"}
569 {pull.branch && (
570 <>
571 {" "}
572 <span className="font-mono text-fg">{pull.branch}</span>
573 </>
574 )}{" "}
575 into <span className="font-mono text-fg">{defaultBranch}</span>
Issues and pull requests replace intents and attempts576 </span>
577 </span>
Pull requests from branches578 {/* A pull request from a branch was made by its author, not an agent. */}
579 {!pull.branch && (
580 <span className="flex items-center gap-1.5 font-mono text-xs">
581 <Bot size={14} />
582 {pull.agent}
583 {pull.runtime === "hosted" && <span className="text-faint">on g1t</span>}
584 </span>
585 )}
Issues and pull requests replace intents and attempts586 </div>
587
Acceptance checks in sandboxes, line comments and review verdicts588 {reviews.length > 0 && (
589 <p className="mt-3 flex flex-wrap items-center gap-x-4 gap-y-1 text-sm">
590 {reviews.map(({ reviewer, verdict }) => (
591 <span
592 key={reviewer}
593 className={`flex items-center gap-1.5 ${
594 verdict === "approve" ? "text-accent" : "text-danger"
595 }`}
596 >
597 {verdict === "approve" ? <CircleCheck size={15} /> : <CircleSlash size={15} />}
598 {verdict === "approve" ? "Approved by" : "Changes requested by"}{" "}
599 <span className="font-medium">{reviewer}</span>
600 </span>
601 ))}
602 </p>
603 )}
604
Pull requests: title and tabs across the full width, with counts and the size of the change605 <nav className="mt-6 flex items-end gap-6 border-b border-line">
Issues and pull requests replace intents and attempts606 <TabLink to={here} active={tab === "conversation"}>
607 <MessageSquare size={15} />
608 Conversation
Pull requests: title and tabs across the full width, with counts and the size of the change609 <TabCount n={comments.length} />
Issues and pull requests replace intents and attempts610 </TabLink>
611 <TabLink to={here + "?tab=session"} active={tab === "session"}>
612 <MessagesSquare size={15} />
613 Session
614 </TabLink>
615 <TabLink to={here + "?tab=changes"} active={tab === "changes"}>
616 <FileDiff size={15} />
Pull requests: title and tabs across the full width, with counts and the size of the change617 Files changed
618 <TabCount n={pull.files.length} />
Issues and pull requests replace intents and attempts619 </TabLink>
Pull requests: title and tabs across the full width, with counts and the size of the change620 {pull.files.length > 0 && (
621 <span className="ml-auto hidden pb-2.5 font-mono text-xs sm:inline">
622 <span className="text-accent">+{pull.files.reduce((sum, file) => sum + file.additions, 0).toLocaleString("en-US")}</span>{" "}
623 <span className="text-danger">−{pull.files.reduce((sum, file) => sum + file.deletions, 0).toLocaleString("en-US")}</span>
624 </span>
625 )}
Issues and pull requests replace intents and attempts626 </nav>
Pull requests: title and tabs across the full width, with counts and the size of the change627 </header>
628 <div className={`mt-6 grid gap-8 ${tab === "changes" ? "" : "lg:grid-cols-[1fr_19rem]"}`}>
629 <div className="min-w-0">
630 {/* What is happening on it now: its stage, its agent, what people told
631 the agent, and what else touches the same files. On the conversation,
632 so the tabs sit right under the title. */}
633 {tab === "conversation" && (
634 <div>
635 {issue && (
636 <Link
637 to={`${base}/issues/${issue.number}`}
638 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"
639 >
640 <IssueIcon issue={issue} />
641 <span className="text-muted">
642 {pull.status === "merged" && issue.resolvedBy === pull.number
643 ? "Resolved"
644 : "For issue"}
645 </span>
646 <span className="truncate font-medium">{issue.title}</span>
647 <span className="text-faint">#{issue.number}</span>
648 {issue.pullCount > 1 && (
649 <span className="ml-auto shrink-0 text-xs text-faint">
650 1 of {issue.pullCount} pull requests
Pull requests show their tabs right under the title; agent panels move into Conversation651 </span>
Pull requests: title and tabs across the full width, with counts and the size of the change652 )}
653 </Link>
654 )}
655 {pull.status === "merged" && (
656 <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">
657 <GitMerge size={16} className="shrink-0 text-merged" />
Agents as a team: lifecycle, merge queue, billing and a new shell658 <span>
Pull requests: title and tabs across the full width, with counts and the size of the change659 Merged into <span className="font-mono">{defaultBranch}</span> by{" "}
660 <span className="font-medium">{pull.mergedBy}</span>{" "}
661 {/* Not an account: the repository's settings said to merge it. */}
662 {pull.mergedBy === "g1t" && "automatically, once it was ready, "}
663 {pull.mergedAt && <TimeAgo at={pull.mergedAt} />}
664 {pull.headCommit && (
665 <span className="font-mono text-muted"> · {pull.headCommit.slice(0, 7)}</span>
Agents as a team: lifecycle, merge queue, billing and a new shell666 )}
667 </span>
Pull requests: title and tabs across the full width, with counts and the size of the change668 </p>
669 )}
670 {pull.supersededBy != null && (
671 <p className="mt-4 rounded-xl border border-line bg-surface px-4 py-2.5 text-sm text-muted">
672 Closed because{" "}
673 <Link
674 to={`${base}/pull/${pull.supersededBy}`}
675 className="font-medium text-fg hover:underline"
676 >
677 #{pull.supersededBy}
678 </Link>{" "}
679 was merged for this issue instead.
680 </p>
681 )}
682 {lifecycle && <LifecyclePanel lifecycle={lifecycle} />}
683 {/* 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 step684 <AgentPanel
685 owner={params.owner}
686 repo={params.repo}
687 number={pull.number}
688 stage={lifecycle?.stage ?? null}
689 confidence={pull.confidence ?? null}
690 />
Pull requests: title and tabs across the full width, with counts and the size of the change691
692 {/* 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 look693 {canRun &&
Pull requests: title and tabs across the full width, with counts and the size of the change694 pull.runtime === "hosted" &&
695 (working || ["working", "revising", "catching_up", "answering"].includes(lifecycle?.stage ?? "")) && (
696 <Form method="post" className="mt-4 rounded-2xl bg-surface p-4 ring-1 ring-merged/30">
697 <p className="flex items-center gap-2 text-sm font-medium">
698 <Sparkles size={15} className="text-merged" />
699 Message the agent
700 </p>
701 <p className="mt-1 text-xs text-muted">
702 A correction, a hint, a change of plan. It reads it at its next step, without
703 starting over.
704 </p>
705 <div className="mt-3 flex gap-2">
706 <input type="hidden" name="action" value="message" />
707 <input
708 name="body"
709 required
710 autoComplete="off"
711 data-1p-ignore
712 placeholder="Keep the old flag working too…"
713 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 shell714 />
Pull requests: title and tabs across the full width, with counts and the size of the change715 <Button type="submit">Send</Button>
716 </div>
717 </Form>
Agents as a team: lifecycle, merge queue, billing and a new shell718 )}
Pull requests: title and tabs across the full width, with counts and the size of the change719 {messages.length > 0 && (
720 <ul className="mt-3 space-y-1.5">
721 {messages.map((message) => (
722 <li key={message.id} className="flex items-start gap-2 text-sm">
723 <Avatar name={message.author} size={18} />
724 <span className="min-w-0 grow">
725 <span className="font-medium">
726 {message.fromNumber != null ? `The agent on #${message.fromNumber}` : message.author}
727 </span>{" "}
728 <span className="text-muted">
729 {message.kind === "question"
730 ? "asked:"
731 : message.kind === "handoff"
732 ? "handed over:"
733 : message.kind === "answer"
734 ? "answered:"
735 : "to the agent:"}
736 </span>{" "}
737 {message.body}
738 {message.answer && (
739 <span className="mt-1 block border-l-2 border-merged/40 pl-2 text-muted">
740 {message.declined ? "Declined: " : "Answer: "}
741 {message.answer}
742 </span>
743 )}
744 </span>
745 <span className={`shrink-0 text-xs ${message.deliveredAt ? "text-accent" : "text-faint"}`}>
746 {message.deliveredAt ? "read by the agent" : "waiting for its next step"}
747 </span>
748 </li>
749 ))}
750 </ul>
751 )}
752 {collisions.length > 0 && active && (
753 <div className="mt-4 rounded-xl border border-line bg-surface px-4 py-3 text-sm">
754 <p className="flex items-center gap-2.5 font-medium">
755 <Radar size={16} className="shrink-0 text-info" />
756 Other work is changing the same files
757 </p>
758 <ul className="mt-2 space-y-1.5">
759 {collisions.map((other) => (
760 <li key={other.number} className="flex flex-wrap items-baseline gap-x-2 text-muted">
761 <Link
762 to={`${base}/pull/${other.number}`}
763 className="font-medium text-fg hover:underline"
764 >
765 {other.title} <span className="font-normal text-faint">#{other.number}</span>
766 </Link>
767 <span className="font-mono text-xs">{other.paths.join(", ")}</span>
768 </li>
769 ))}
770 </ul>
771 <p className="mt-2 text-xs text-faint">
772 Whichever merges second will have to catch up, and may conflict.
773 </p>
774 </div>
775 )}
776 </div>
777 )}
778 <div className="mt-5">
779 {comparison ? (
780 <DiffView
781 comparison={comparison}
782 review={{
783 comments: comments.filter((comment) => comment.path),
784 canComment: Boolean(viewer),
785 }}
786 />
787 ) : tab === "session" ? (
788 session.length === 0 ? (
789 <EmptyState title="Nothing recorded yet">
790 The agent's prompts, reasoning and tool calls appear here as it
791 works.
792 </EmptyState>
793 ) : (
794 <ol className="relative space-y-5 before:absolute before:top-2 before:bottom-2 before:left-3.25 before:w-px before:bg-line">
795 {session.map((entry) => (
796 <Entry key={entry.seq} entry={entry} agent={pull.agent} />
797 ))}
798 </ol>
799 )
800 ) : (
801 <div className="space-y-4">
802 <TimelineItem
803 author={pull.author.username}
804 at={pull.createdAt}
805 action={
806 <span>
807 opened this pull request
808 {!pull.branch && (
809 <>
810 {" "}
811 with <span className="font-mono text-xs">{pull.agent}</span>
812 </>
813 )}
814 </span>
815 }
816 >
817 {pull.body ? (
818 <Markdown source={pull.body} repo={{ namespace: params.owner, name: params.repo }} />
819 ) : (
820 <p className="text-sm text-muted">
821 {working
822 ? "No description yet. It is written when the pull request is marked ready for review."
823 : "No description."}
824 </p>
825 )}
826 {canManage && pull.status === "open" && (
827 <details className="mt-3 border-t border-line pt-3 text-sm">
828 <summary className="cursor-pointer text-xs text-faint hover:text-fg">
829 Edit description
830 </summary>
831 <Form method="post" className="mt-3 space-y-2">
832 <Textarea
833 name="summary"
834 rows={6}
835 placeholder="What changed and why"
836 defaultValue={pull.body ?? ""}
837 />
838 <Button type="submit">Save</Button>
839 </Form>
840 </details>
841 )}
842 </TimelineItem>
Agents as a team: lifecycle, merge queue, billing and a new shell843
Pull requests: title and tabs across the full width, with counts and the size of the change844 <CommentList comments={comments} review={review} base={base} />
Agents as a team: lifecycle, merge queue, billing and a new shell845
Pull requests: title and tabs across the full width, with counts and the size of the change846 {(preview || build) && (
847 <DeploymentCard preview={preview} build={build} stacked={stacked} base={base} />
848 )}
Pull requests say where they are live; the project menu shows what is coming849
Pull requests: title and tabs across the full width, with counts and the size of the change850 {pull.status === "draft" && (
851 <StatusBox>
852 <StatusRow icon={<Loader size={16} className="text-faint" />} title="This is a draft">
853 It is still being worked on. It can be reviewed and merged once it is
854 marked ready.
855 </StatusRow>
856 {canManage && (
857 <Form method="post" className="space-y-2 px-4 py-3">
858 <Textarea name="summary" rows={3} placeholder="What changed and why" />
859 <Button type="submit">Mark ready for review</Button>
860 </Form>
861 )}
862 </StatusBox>
863 )}
Agents as a team: lifecycle, merge queue, billing and a new shell864
Pull requests: title and tabs across the full width, with counts and the size of the change865 {pull.status === "open" && (
866 <StatusBox>
867 <ChecksSection
868 run={checks}
869 commands={issue?.checks ?? []}
870 statuses={statuses}
871 jobs={workflowJobs}
Agents and memory, checks and conflicts, profiles, slug renames, custom domains872 pull={pull}
Pull requests: title and tabs across the full width, with counts and the size of the change873 base={base}
874 earlier={earlierChecks}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look875 canRerun={canRun}
Pull requests: title and tabs across the full width, with counts and the size of the change876 canRerunWorkflows={canMerge}
877 error={
878 actionData?.action === "recheck" || actionData?.action === "rerun-workflow"
879 ? actionData.error
880 : null
881 }
Agents and memory, checks and conflicts, profiles, slug renames, custom domains882 />
Agents as a team: lifecycle, merge queue, billing and a new shell883 <StatusRow
Pull requests: title and tabs across the full width, with counts and the size of the change884 icon={
885 reviews.some(({ verdict }) => verdict === "request_changes") ? (
886 <CircleSlash size={16} className="text-danger" />
887 ) : reviews.length > 0 ? (
888 <CircleCheck size={16} className="text-accent" />
889 ) : (
890 <MessageSquare size={16} className="text-faint" />
891 )
892 }
893 title={
894 reviews.length === 0
895 ? "No reviews yet"
896 : reviews
897 .map(
898 ({ reviewer, verdict }) =>
899 `${verdict === "approve" ? "Approved by" : "Changes requested by"} ${reviewer}`,
900 )
901 .join(" · ")
902 }
Agents as a team: lifecycle, merge queue, billing and a new shell903 >
Pull requests: title and tabs across the full width, with counts and the size of the change904 {reviewPending && "A g1t agent is reviewing it now. "}
905 {requiredApprovals > 0 &&
906 `This repository requires ${requiredApprovals} approving ${
907 requiredApprovals === 1 ? "review" : "reviews"
908 } before merging.`}
Agents as a team: lifecycle, merge queue, billing and a new shell909 </StatusRow>
Pull requests: title and tabs across the full width, with counts and the size of the change910 {landing ? (
Agents as a team: lifecycle, merge queue, billing and a new shell911 <StatusRow
Pull requests: title and tabs across the full width, with counts and the size of the change912 icon={<Loader size={16} className="animate-spin text-accent" />}
913 title="Merging"
914 >
915 {defaultBranch} has moved, so g1t is bringing this up to date first. It
916 lands as soon as that is done. Watch it in the Session tab.
917 </StatusRow>
Catching up with main takes seconds when the two sides touched different files918 ) : agentCatchUp && behind ? (
919 <CatchUpProgress
920 owner={params.owner}
921 repo={params.repo}
922 number={pull.number}
923 defaultBranch={defaultBranch}
924 behind={behind}
925 update={agentCatchUp.update}
926 startedAt={agentCatchUp.startedAt}
927 retrying={catchUpPending}
928 />
Pull requests: title and tabs across the full width, with counts and the size of the change929 ) : conflicting ? (
930 <ConflictsSection
931 conflicts={conflicts}
932 pull={pull}
933 owner={params.owner}
934 repo={params.repo}
935 defaultBranch={defaultBranch}
936 changesUrl={here + "?tab=changes"}
937 canResolve={canUpdate && agentsEnabled}
Catching up with main takes seconds when the two sides touched different files938 resolving={catchUpPending}
939 error={catchUp && "error" in catchUp ? catchUp.error : null}
Agents as a team: lifecycle, merge queue, billing and a new shell940 />
Pull requests: title and tabs across the full width, with counts and the size of the change941 ) : probing ? (
942 <MergeabilityRow mergeable={mergeable} defaultBranch={defaultBranch} />
943 ) : behind ? (
944 <StatusRow
945 icon={<GitPullRequestArrow size={16} className="text-info" />}
946 title={`${defaultBranch} has moved since this was made`}
947 >
948 {mergeable === "clean" && "It has no conflicts with it. "}
949 {requireUpToDate
950 ? "This repository requires pull requests to be up to date, so it has to catch up before it can merge."
951 : "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 files952 {canUpdate && (
953 <Form method="post" className="mt-2">
Pull requests: title and tabs across the full width, with counts and the size of the change954 <input type="hidden" name="action" value="update" />
Catching up with main takes seconds when the two sides touched different files955 <Button variant="quiet" type="submit" disabled={catchUpPending}>
956 {catchUpPending && <Loader size={14} className="animate-spin" />}
957 {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 change958 </Button>
959 </Form>
960 )}
Catching up with main takes seconds when the two sides touched different files961 {catchUp && "error" in catchUp && <ErrorText>{catchUp.error}</ErrorText>}
Pull requests: title and tabs across the full width, with counts and the size of the change962 </StatusRow>
963 ) : (
964 pull.headCommit && (
965 <StatusRow
966 icon={<CircleCheck size={16} className="text-accent" />}
Catching up with main takes seconds when the two sides touched different files967 title={
968 (caughtUp && !caughtUp.already) || agentCatchUp
969 ? `Brought up to date with ${defaultBranch}`
970 : `Up to date with ${defaultBranch}`
971 }
972 >
973 {caughtUp && !caughtUp.already && (
974 <>
975 Merged <span className="font-mono">{defaultBranch}</span> in as{" "}
976 <span className="font-mono text-fg">{caughtUp.commit.slice(0, 7)}</span>. Its checks run
977 again on the new commit.
978 </>
979 )}
980 {agentCatchUp && "g1t-agent merged it in and pushed the result. Its checks run again on the new commit."}
981 </StatusRow>
Pull requests: title and tabs across the full width, with counts and the size of the change982 )
983 )}
984 {stalled && !lifecycle && (
985 <StatusRow icon={<Hand size={16} className="text-warn" />} title="Needs you">
986 {stalled}
987 </StatusRow>
988 )}
989 {canMerge && lifecycle?.stage === "queued" && (
990 <Form method="post" className="flex flex-wrap items-center gap-3 px-4 py-3">
991 <ButtonLink to={`${base}/queue`}>
992 <Layers size={15} />
993 See the queue
994 </ButtonLink>
995 <Button variant="quiet" type="submit" name="action" value="unqueue">
996 Remove from the queue
Agents as a team: lifecycle, merge queue, billing and a new shell997 </Button>
Pull requests: title and tabs across the full width, with counts and the size of the change998 </Form>
999 )}
1000 {canMerge && !landing && lifecycle?.stage !== "queued" && (
1001 <Form method="post" className="space-y-3 px-4 py-3">
1002 {issue?.state === "open" && (
1003 <CheckboxOption
1004 name="keepIssueOpen"
1005 label={`Keep #${issue.number} open. This is only part of the work.`}
1006 labelClassName="text-xs text-muted"
1007 />
1008 )}
1009 {unchecked && canIgnoreChecks && (
1010 <CheckboxOption
1011 name="ignoreChecks"
1012 label={`Merge although the checks ${checking ? "have not finished" : "did not pass"}.`}
1013 labelClassName="text-xs text-muted"
1014 />
1015 )}
1016 <div className="flex flex-wrap items-center gap-3">
1017 <Button
1018 variant="accent"
1019 type="submit"
1020 name="action"
1021 value="merge"
1022 disabled={mergeBlocked != null}
1023 title={mergeBlocked ?? undefined}
1024 >
1025 {mergeQueue ? <Layers size={15} /> : <GitMerge size={15} />}
1026 {mergeQueue ? "Add to the merge queue" : `Merge into ${defaultBranch}`}
1027 </Button>
1028 <span className={`text-xs ${mergeBlocked ? "text-danger" : "text-muted"}`}>
1029 {mergeBlocked ?? (mergeQueue
1030 ? `Tested together with everything ahead of it, then lands on ${defaultBranch}.`
1031 : issue?.state === "open"
1032 ? `Closes issue #${issue.number}, and any other pull requests still open for it.`
1033 : `Lands its commits on ${defaultBranch}.`)}
1034 </span>
1035 </div>
1036 {actionData?.action === "merge" && (
1037 <ErrorText>{actionData.error}</ErrorText>
1038 )}
1039 </Form>
1040 )}
1041 </StatusBox>
1042 )}
Agents as a team: lifecycle, merge queue, billing and a new shell1043
Pull requests: title and tabs across the full width, with counts and the size of the change1044 <CommentForm
1045 author={viewer?.username ?? null}
1046 resetKey={comments.length}
1047 review={review}
1048 />
1049 {canManage && active && (
1050 <Form method="post" className="flex justify-end">
1051 <Button variant="quiet" type="submit" name="action" value="close">
1052 Close pull request
1053 </Button>
1054 </Form>
1055 )}
1056 {actionData &&
1057 !["merge", "comment", "recheck", "rerun-workflow", "update", "agent-review", "reviewers", "assign"].includes(
1058 String(actionData.action),
1059 ) && <ErrorText>{actionData.error}</ErrorText>}
1060 </div>
1061 )}
1062 {actionData?.action === "comment" && (
1063 <div className="mt-2">
1064 <ErrorText>{actionData.error}</ErrorText>
1065 </div>
1066 )}
1067 </div>
1068 </div>
1069
1070 <aside className={tab === "changes" ? "hidden" : "space-y-6"}>
1071 {/* An open pull request shows its checks in full in the merge box. */}
1072 {pull.status !== "open" && (
1073 <>
1074 <ChecksPanel run={checks} commands={issue?.checks ?? []} canRerun={false} />
1075 <WorkflowStatuses statuses={statuses} />
1076 </>
1077 )}
1078 {affects.length > 0 && (
1079 <section>
1080 <h3 className="flex items-center gap-1.5 text-sm font-medium">
1081 <Network size={14} className="text-faint" />
1082 Affects
1083 </h3>
1084 <p className="mt-1 text-xs text-muted">Projects that use this one, and so may feel this change:</p>
1085 <ul className="mt-2 space-y-1 text-sm">
1086 {affects.map((project) => (
1087 <li key={project.slug} className="flex items-center justify-between gap-2">
1088 <Link to={`/${params.owner}/${project.slug}`} className="hover:underline">
1089 {project.name}
1090 </Link>
1091 {project.as && <code className="font-mono text-xs text-faint">{project.as}</code>}
1092 </li>
1093 ))}
1094 </ul>
1095 {preview?.branch && canMerge && (
1096 <Form method="post" className="mt-3">
1097 <input type="hidden" name="action" value="stack" />
1098 <input type="hidden" name="branch" value={preview.branch} />
1099 <button
1100 type="submit"
1101 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"
1102 >
1103 Preview them against this change
1104 </button>
Agents as a team: lifecycle, merge queue, billing and a new shell1105 </Form>
1106 )}
Pull requests: title and tabs across the full width, with counts and the size of the change1107 {actionData?.action === "stack" &&
1108 ("notice" in actionData ? (
1109 <p className="mt-2 text-xs text-accent">{String(actionData.notice)}</p>
1110 ) : (
1111 <ErrorText>{actionData.error}</ErrorText>
1112 ))}
1113 </section>
Acceptance checks in sandboxes, line comments and review verdicts1114 )}
Issues and pull requests replace intents and attempts1115
Project dependencies: addresses, preview stacks, Affects, and agents who know1116 <section>
Pull requests: title and tabs across the full width, with counts and the size of the change1117 <h3 className="text-sm font-medium">Reviewers</h3>
1118 <ul className="mt-2 space-y-1.5 text-sm">
1119 {reviewerNames.map((name) => {
1120 const verdict = reviews.find(({ reviewer }) => reviewer === name)?.verdict;
1121 const pending = name === "g1t-agent" && reviewPending;
1122 return (
1123 <li key={name} className="flex items-center gap-2 px-1">
1124 {name === "g1t-agent" ? (
1125 <Sparkles size={16} className="shrink-0 text-accent" />
1126 ) : (
1127 <Avatar name={name} size={20} />
1128 )}
1129 <span className="grow truncate font-mono text-xs">{name}</span>
1130 {pending ? (
1131 <span className="flex items-center gap-1.5 text-xs text-muted">
1132 <span className="size-1.5 animate-pulse rounded-full bg-accent" />
1133 Reviewing
1134 </span>
1135 ) : verdict === "approve" ? (
1136 <span className="flex items-center gap-1 text-xs text-accent">
1137 <CircleCheck size={13} /> Approved
1138 </span>
1139 ) : verdict === "request_changes" ? (
1140 <span className="flex items-center gap-1 text-xs text-danger">
1141 <CircleSlash size={13} /> Changes requested
1142 </span>
1143 ) : (
1144 <span className="text-xs text-faint">Review requested</span>
1145 )}
1146 </li>
1147 );
1148 })}
1149 {reviewerNames.length === 0 && (
1150 <li className="px-1 text-xs text-faint">No reviews requested yet.</li>
1151 )}
Project dependencies: addresses, preview stacks, Affects, and agents who know1152 </ul>
Pull requests: title and tabs across the full width, with counts and the size of the change1153 {pull.status === "open" && canManage && (
1154 <div className="mt-3 space-y-2">
1155 {agentsEnabled && !reviewPending && (
1156 <Form method="post">
1157 <input type="hidden" name="action" value="agent-review" />
1158 {pull.reviewers.map((name) => (
1159 <input key={name} type="hidden" name="reviewer" value={name} />
1160 ))}
1161 <div className="*:w-full">
1162 <Button variant="quiet" type="submit">
1163 <Sparkles size={14} className="text-accent" />
1164 Request review from g1t agent
1165 </Button>
1166 </div>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1167 <ComputeNote note={loaderData.computeNote} />
Pull requests: title and tabs across the full width, with counts and the size of the change1168 </Form>
1169 )}
1170 <details>
1171 <summary className="cursor-pointer text-xs text-faint hover:text-fg">
1172 Request review from people
1173 </summary>
1174 <Form method="post" className="mt-2 space-y-2" key={pull.reviewers.join()}>
1175 <input type="hidden" name="action" value="reviewers" />
1176 {pull.reviewers.includes("g1t-agent") && (
1177 <input type="hidden" name="reviewer" value="g1t-agent" />
1178 )}
1179 <PeoplePicker
1180 name="reviewer"
1181 members={members.filter((name) => name !== pull.author.username)}
1182 chosen={pull.reviewers.filter((name) => name !== "g1t-agent")}
1183 />
1184 <Button variant="quiet" type="submit">
1185 Save reviewers
1186 </Button>
1187 </Form>
1188 </details>
1189 </div>
Project dependencies: addresses, preview stacks, Affects, and agents who know1190 )}
Pull requests: title and tabs across the full width, with counts and the size of the change1191 {["agent-review", "reviewers"].includes(String(actionData?.action)) && (
1192 <ErrorText>{actionData?.error}</ErrorText>
1193 )}
Project dependencies: addresses, preview stacks, Affects, and agents who know1194 </section>
Acceptance checks in sandboxes, line comments and review verdicts1195
Pull requests: title and tabs across the full width, with counts and the size of the change1196 <section>
1197 <h3 className="text-sm font-medium">Assignees</h3>
1198 <ul className="mt-2 space-y-1.5 text-sm">
1199 {pull.assignees.map((name) => (
Agents as a team: lifecycle, merge queue, billing and a new shell1200 <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 change1201 <Avatar name={name} size={20} />
Agents as a team: lifecycle, merge queue, billing and a new shell1202 <span className="grow truncate font-mono text-xs">{name}</span>
1203 </li>
Pull requests: title and tabs across the full width, with counts and the size of the change1204 ))}
1205 {pull.assignees.length === 0 && (
1206 <li className="px-1 text-xs text-faint">No one yet.</li>
1207 )}
1208 </ul>
1209 {viewer && canManage && active && (
1210 <div className="mt-3 space-y-2">
1211 {!pull.assignees.includes(viewer.username) && (
1212 <Form method="post">
1213 <input type="hidden" name="action" value="assign" />
1214 {pull.assignees.map((name) => (
1215 <input key={name} type="hidden" name="assignee" value={name} />
1216 ))}
1217 <input type="hidden" name="assignee" value={viewer.username} />
1218 <div className="*:w-full">
1219 <Button variant="quiet" type="submit">
1220 Assign yourself
1221 </Button>
1222 </div>
1223 </Form>
1224 )}
1225 <details>
1226 <summary className="cursor-pointer text-xs text-faint hover:text-fg">
1227 Assign people
1228 </summary>
1229 <Form method="post" className="mt-2 space-y-2" key={pull.assignees.join()}>
1230 <input type="hidden" name="action" value="assign" />
1231 <PeoplePicker name="assignee" members={members} chosen={pull.assignees} />
Agents as a team: lifecycle, merge queue, billing and a new shell1232 <Button variant="quiet" type="submit">
Pull requests: title and tabs across the full width, with counts and the size of the change1233 Save assignees
Agents as a team: lifecycle, merge queue, billing and a new shell1234 </Button>
Pull requests: title and tabs across the full width, with counts and the size of the change1235 </Form>
1236 </details>
1237 </div>
Issues and pull requests replace intents and attempts1238 )}
Pull requests: title and tabs across the full width, with counts and the size of the change1239 {actionData?.action === "assign" && <ErrorText>{actionData.error}</ErrorText>}
1240 </section>
Issues and pull requests replace intents and attempts1241
Pull requests: title and tabs across the full width, with counts and the size of the change1242 <section>
1243 <h3 className="text-sm font-medium">Working copy</h3>
1244 {pull.branch ? (
1245 <>
1246 <p className="mt-1 text-xs text-muted">
1247 <GitBranch size={13} className="mr-1 inline align-[-2px]" />
1248 Branch <span className="font-mono text-fg">{pull.branch}</span> of this
1249 repository. Pushes to it show up here.
1250 </p>
1251 <div className="mt-2">
1252 <CopyLine text={`git clone -b ${pull.branch} ${remote}`} />
1253 </div>
1254 </>
1255 ) : (
1256 <>
1257 <p className="mt-1 text-xs text-muted">
1258 This fork belongs to the pull request. Pushes to it show up here.
1259 </p>
1260 <div className="mt-2">
1261 <CopyLine text={`git clone ${remote}`} />
1262 </div>
1263 </>
1264 )}
1265 <p className="mt-3 flex items-center gap-1.5 font-mono text-xs text-faint">
1266 <GitCommitHorizontal size={13} />
1267 {pull.headCommit?.slice(0, 12) ?? "no commits pushed yet"}
1268 </p>
1269 </section>
1270 </aside>
1271 </div>
Issues and pull requests replace intents and attempts1272 </div>
1273 );
1274}
Pull requests say where they are live; the project menu shows what is coming1275
1276/** Where the pull request's change is live, as GitHub shows a deployment. */
1277function DeploymentCard({
1278 preview,
1279 build,
1280 stacked,
1281 base,
1282}: {
1283 preview: LiveApp | null;
1284 build: Deployment | null;
1285 stacked: { name: string; slug: string; url: string }[];
1286 base: string;
1287}) {
1288 const building = build?.status === "queued" || build?.status === "building";
1289 const failed = build?.status === "failed";
1290 return (
1291 <section className="ml-12 overflow-hidden rounded-xl border border-line bg-surface">
1292 <div className="flex flex-wrap items-center gap-3 px-4 py-3">
1293 <span
1294 className={`flex size-8 shrink-0 items-center justify-center rounded-full ring-1 ${
1295 failed ? "text-danger ring-danger/40" : building ? "text-warn ring-warn/40" : "text-accent ring-accent/40"
1296 }`}
1297 >
1298 {building ? <Loader size={15} className="animate-spin" /> : <Rocket size={15} />}
1299 </span>
1300 <div className="min-w-0 grow">
1301 <p className="text-sm font-medium">
1302 {building
1303 ? preview
1304 ? "Deploying the latest push. The last preview is still live."
1305 : "Deploying a preview"
1306 : failed
1307 ? "The preview failed to deploy"
1308 : "This branch is live"}
1309 </p>
1310 {preview ? (
1311 <a href={preview.url} className="mt-0.5 block truncate font-mono text-xs text-muted hover:text-accent">
1312 {preview.url.replace(/^https?:[/][/]/, "")}
1313 </a>
1314 ) : (
1315 build?.error && <p className="mt-0.5 truncate text-xs text-muted">{build.error}</p>
1316 )}
1317 </div>
1318 <div className="flex shrink-0 items-center gap-2">
1319 {build && (
1320 <Link to={`${base}/deployments/${build.id}`} className="text-xs text-muted hover:text-fg">
1321 {failed ? "See why" : "Build log"}
1322 </Link>
1323 )}
1324 {preview && (
1325 <a
1326 href={preview.url}
1327 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"
1328 >
1329 Visit preview
1330 <ArrowUpRight size={13} />
1331 </a>
1332 )}
1333 </div>
1334 </div>
1335 {preview && (
1336 <p className="border-t border-line px-4 py-2 text-xs text-faint">
1337 <span className="font-mono">{preview.commit.slice(0, 7)}</span> · deployed <TimeAgo at={preview.deployedAt} />
1338 {preview.branch && ` · from ${preview.branch}`}
1339 </p>
1340 )}
1341 {stacked.length > 0 && (
1342 <div className="border-t border-line px-4 py-2.5">
1343 <p className="text-xs text-muted">Built against this change:</p>
1344 <ul className="mt-1.5 space-y-1">
1345 {stacked.map((entry) => (
1346 <li key={entry.slug} className="flex items-center gap-2 text-xs">
1347 <span className="font-medium">{entry.name}</span>
1348 <a href={entry.url} className="truncate font-mono text-muted hover:text-accent">
1349 {entry.url.replace(/^https?:[/][/]/, "")}
1350 </a>
1351 </li>
1352 ))}
1353 </ul>
1354 </div>
1355 )}
1356 </section>
1357 );
1358}