flagon-io/g1t

public

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

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

1,018 lines41,855 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,
18 MessagesSquare,
19 StickyNote,
20 User,
21 Wrench,
22} from "lucide-react";
Agents as a team: lifecycle, merge queue, billing and a new shell23import { useEffect, useState } from "react";
Issues and pull requests replace intents and attempts24import { Form, Link, redirect, useRevalidator } from "react-router";
25
Pull requests from branches26import { type Comparison, type SessionEntry, pullComparison } from "@g1t/contracts";
Issues and pull requests replace intents and attempts27
28import type { Route } from "./+types/pull";
29import { DiffView } from "../../components/diff-view";
Agents as a team: lifecycle, merge queue, billing and a new shell30import { LifecyclePanel } from "../../components/lifecycle";
Issues and pull requests replace intents and attempts31import { Markdown } from "../../components/markdown";
32import {
33 Avatar,
34 Button,
Agents as a team: lifecycle, merge queue, billing and a new shell35 ButtonLink,
Issues and pull requests replace intents and attempts36 CopyLine,
37 EmptyState,
38 ErrorText,
39 Textarea,
40 TimeAgo,
41} from "../../components/ui";
Acceptance checks in sandboxes, line comments and review verdicts42import { ChecksPanel } from "../../components/checks";
Agents as a team: lifecycle, merge queue, billing and a new shell43import {
44 CommentForm,
45 CommentList,
46 IssueIcon,
47 PeoplePicker,
48 PullState,
49 TimelineItem,
50 verdicts,
51} from "../../components/work";
52import { identity, repos, work } from "../../lib/services.server";
Issues and pull requests replace intents and attempts53import { assertSameOrigin, getViewer, requireUser } from "../../lib/session.server";
54
55const REFRESH_MS = 4000;
56const EMPTY_COMPARISON: Comparison = { base: null, head: "", files: [], truncated: false };
57const TABS = ["conversation", "session", "changes"] as const;
58type Tab = (typeof TABS)[number];
59
60export function meta({ loaderData, params }: Route.MetaArgs) {
61 const title = loaderData
62 ? `${loaderData.pull.title} · Pull request #${loaderData.pull.number} · `
63 : "";
64 return [{ title: `${title}${params.owner}/${params.repo} · g1t` }];
65}
66
67export async function loader({ params, context, request }: Route.LoaderArgs) {
68 const viewer = getViewer(context);
69 const path = { namespace: params.owner, name: params.repo };
70 const number = Number(params.number);
71 const asked = new URL(request.url).searchParams.get("tab");
72 const tab: Tab = TABS.find((name) => name === asked) ?? "conversation";
73
Agents as a team: lifecycle, merge queue, billing and a new shell74 const member = (viewer?.workspaces ?? []).some(
75 (membership) => membership.slug === params.owner,
76 );
77 // At once: none of these depends on another.
78 const [found, repo, settings, agentsEnabled, members] = await Promise.all([
Issues and pull requests replace intents and attempts79 work.getPull(path, number, viewer),
80 repos.get(path, viewer),
Agents as a team: lifecycle, merge queue, billing and a new shell81 work.getSettings(path, viewer),
g1t's agents only for listed workspaces, whatever the state of billing82 env.RUNNER.enabled(viewer, path),
Agents as a team: lifecycle, merge queue, billing and a new shell83 // A member picks reviewers and assignees from the workspace's people.
84 member ? identity.listMembers(params.owner, viewer) : null,
Issues and pull requests replace intents and attempts85 ]);
86 if (!found.ok) {
87 // Issues and pull requests share numbers; this one may be an issue.
88 const issue = await work.getIssue(path, number, viewer);
89 if (issue.ok) throw redirect(`/${params.owner}/${params.repo}/issues/${number}`);
90 throw new Response("Pull request not found.", { status: 404 });
91 }
92 const { pull } = found.value;
Pull requests from branches93 const range = pullComparison(pull);
Issues and pull requests replace intents and attempts94 const [session, comparison] = await Promise.all([
95 tab === "session" ? work.readSession(path, number, viewer) : null,
Pull requests from branches96 tab === "changes"
97 ? repos.compare(range.repoId, viewer, range.base, range.head)
98 : null,
Issues and pull requests replace intents and attempts99 ]);
100 return {
101 ...found.value,
102 tab,
103 session: session?.ok ? session.value : [],
104 // An empty comparison if it could not be made.
105 comparison: comparison && (comparison.ok ? comparison.value : EMPTY_COMPARISON),
106 viewer,
107 // Members of the repository's workspace can merge.
108 canMerge: member,
109 canManage: member || viewer?.id === pull.author.id,
Agents as a team: lifecycle, merge queue, billing and a new shell110 // A catch-up is pushed as the viewer: a fork takes pushes only from
111 // whoever opened it, a branch from any member.
112 canUpdate: pull.fork ? viewer?.id === pull.author.id : member,
113 agentsEnabled,
114 members: members?.ok ? members.value.map((person) => person.username) : [],
115 requireUpToDate: settings.ok && settings.value.requireUpToDate,
116 mergeQueue: settings.ok && settings.value.mergeQueue,
117 requiredApprovals: settings.ok ? settings.value.requiredApprovals : 0,
118 canIgnoreChecks: !settings.ok || settings.value.allowIgnoringChecks,
Issues and pull requests replace intents and attempts119 defaultBranch: repo.ok ? repo.value.defaultBranch : "main",
120 };
121}
122
123export async function action({ request, params, context }: Route.ActionArgs) {
124 assertSameOrigin(request);
125 const user = requireUser(context, request);
126 const form = await request.formData();
127 const path = { namespace: params.owner, name: params.repo };
128 const number = Number(params.number);
129 const action = form.get("action");
Acceptance checks in sandboxes, line comments and review verdicts130 const verdict = form.get("verdict");
131 const line = Number(form.get("line"));
Agents as a team: lifecycle, merge queue, billing and a new shell132 /** Names ticked in a people picker, plus those typed beside it. */
133 const picked = (field: string) => [
134 ...form.getAll(field).map(String),
135 ...String(form.get("others") ?? "").split(/[\s,]+/),
136 ];
137 // Asking a g1t agent for its review records the request, then starts it.
138 if (action === "agent-review") {
139 const asked = await work.updatePull(user, path, number, {
140 reviewers: [...form.getAll("reviewer").map(String), "g1t-agent"],
141 });
142 if (!asked.ok) return { error: asked.error.message, action };
143 }
Issues and pull requests replace intents and attempts144 const result =
145 action === "merge"
Acceptance checks in sandboxes, line comments and review verdicts146 ? await work.mergePull(user, path, number, {
147 keepIssueOpen: form.get("keepIssueOpen") === "on",
148 ignoreChecks: form.get("ignoreChecks") === "on",
149 })
Agents as a team: lifecycle, merge queue, billing and a new shell150 : action === "unqueue"
151 ? await work.removeFromQueue(user, path, number)
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request152 : action === "message"
153 ? await work.messageAgent(user, path, number, String(form.get("body") ?? ""))
Issues and pull requests replace intents and attempts154 : action === "close"
155 ? await work.closePull(user, path, number)
Acceptance checks in sandboxes, line comments and review verdicts156 : action === "recheck"
157 ? await env.RUNNER.recheck(user, path, number)
Agents as a team: lifecycle, merge queue, billing and a new shell158 : action === "update"
159 ? await env.RUNNER.update(user, path, number)
160 : action === "agent-review"
161 ? await env.RUNNER.review(user, path, number)
162 : action === "reviewers"
163 ? await work.updatePull(user, path, number, { reviewers: picked("reviewer") })
164 : action === "assign"
165 ? await work.updatePull(user, path, number, { assignees: picked("assignee") })
Acceptance checks in sandboxes, line comments and review verdicts166 : action === "comment"
167 ? await work.addComment(user, path, number, {
168 body: String(form.get("body") ?? ""),
169 path: String(form.get("path") ?? "") || undefined,
170 line: line > 0 ? line : undefined,
171 verdict:
172 verdict === "approve" || verdict === "request_changes" ? verdict : undefined,
173 })
174 : await work.readyPull(user, path, number, String(form.get("summary") ?? ""));
Issues and pull requests replace intents and attempts175 return result.ok ? null : { error: result.error.message, action };
176}
177
178function TabLink({
179 to,
180 active,
181 children,
182}: {
183 to: string;
184 active: boolean;
185 children: React.ReactNode;
186}) {
187 return (
188 <Link
189 to={to}
190 preventScrollReset
191 className={
192 "-mb-px flex items-center gap-2 border-b-2 px-1 pb-2.5 text-sm transition-colors " +
193 (active
194 ? "border-accent font-medium text-fg"
195 : "border-transparent text-muted hover:text-fg")
196 }
197 >
198 {children}
199 </Link>
200 );
201}
202
203/** One step of the session, on the timeline's rail. */
204function Entry({ entry, agent }: { entry: SessionEntry; agent: string }) {
205 const isTool = entry.kind === "tool_call" || entry.kind === "tool_result";
206 const Icon =
207 entry.kind === "prompt"
208 ? User
209 : entry.kind === "note"
210 ? StickyNote
211 : isTool
212 ? Wrench
213 : Bot;
214 return (
215 <li className="relative pl-10">
216 <span
217 className={`absolute top-0.5 left-0 flex size-7 items-center justify-center rounded-full border bg-bg ${
218 entry.kind === "prompt"
219 ? "border-accent/50 text-accent"
220 : "border-line text-faint"
221 }`}
222 >
223 <Icon size={14} />
224 </span>
225 {isTool ? (
226 <details className="group rounded-lg border border-line bg-surface">
227 <summary className="flex cursor-pointer list-none items-center gap-2 px-3 py-1.5 text-sm">
228 <ChevronRight
229 size={14}
230 className="text-faint transition-transform group-open:rotate-90"
231 />
232 <span className="font-mono text-xs text-accent">
233 {entry.tool ?? "tool"}
234 </span>
235 <span className="truncate font-mono text-xs text-muted">
236 {entry.kind === "tool_result" ? "→ " : ""}
237 {entry.text.split("\n")[0]}
238 </span>
239 </summary>
240 <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 line241 <code>{entry.text}</code>
Issues and pull requests replace intents and attempts242 </pre>
243 </details>
244 ) : (
245 <div>
246 <p className="text-xs font-medium text-faint">
247 {entry.kind === "prompt"
248 ? "Prompt"
249 : entry.kind === "note"
250 ? "Note"
251 : agent}
252 </p>
253 {entry.kind === "message" ? (
254 <div className="mt-1">
255 <Markdown source={entry.text} />
256 </div>
Agents as a team: lifecycle, merge queue, billing and a new shell257 ) : entry.kind === "prompt" ? (
258 <div className="mt-1.5 max-h-[32rem] overflow-y-auto rounded-xl border border-line bg-surface p-4">
259 <Markdown source={entry.text} />
260 </div>
Issues and pull requests replace intents and attempts261 ) : (
Agents as a team: lifecycle, merge queue, billing and a new shell262 <p className="mt-1 text-[0.9375rem] leading-relaxed wrap-break-word whitespace-pre-wrap">
Issues and pull requests replace intents and attempts263 {entry.text}
264 </p>
265 )}
266 </div>
267 )}
268 {entry.commit && (
269 <p className="mt-1.5 flex items-center gap-1 font-mono text-xs text-faint">
270 <GitCommitHorizontal size={12} />
271 {entry.commit.slice(0, 7)}
272 </p>
273 )}
274 </li>
275 );
276}
277
Agents as a team: lifecycle, merge queue, billing and a new shell278/** The box at the foot of the conversation saying what stands before a merge. */
279function StatusBox({ children }: { children: React.ReactNode }) {
280 return (
281 <div className="flex gap-3">
282 <span className="hidden w-8 shrink-0 sm:block" />
283 <section className="min-w-0 grow divide-y divide-line overflow-hidden rounded-xl border border-line-strong bg-surface">
284 {children}
285 </section>
286 </div>
287 );
288}
289
290function StatusRow({
291 icon,
292 title,
293 children,
294}: {
295 icon: React.ReactNode;
296 title: string;
297 children?: React.ReactNode;
298}) {
299 return (
300 <div className="flex gap-3 px-4 py-3 text-sm">
301 <span className="mt-0.5 shrink-0">{icon}</span>
302 <div className="min-w-0">
303 <p className="font-medium">{title}</p>
304 {children && <div className="mt-0.5 text-muted">{children}</div>}
305 </div>
306 </div>
307 );
308}
309
Issues and pull requests replace intents and attempts310export default function PullPage({ loaderData, actionData, params }: Route.ComponentProps) {
311 const {
312 pull,
313 issue,
314 comments,
Acceptance checks in sandboxes, line comments and review verdicts315 checks,
Agents as a team: lifecycle, merge queue, billing and a new shell316 overlaps,
317 behind,
318 reviewPending,
319 lifecycle,
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request320 messages,
Agents as a team: lifecycle, merge queue, billing and a new shell321 landing,
322 stalled,
323 requireUpToDate,
324 mergeQueue,
325 requiredApprovals,
326 canIgnoreChecks,
327 canUpdate,
328 agentsEnabled,
329 members,
Issues and pull requests replace intents and attempts330 tab,
331 session,
332 comparison,
333 viewer,
334 canMerge,
335 canManage,
336 defaultBranch,
337 } = loaderData;
338 const base = `/${params.owner}/${params.repo}`;
339 const here = `${base}/pull/${pull.number}`;
Pull requests from branches340 const remote = pull.fork
341 ? `https://g1t.sh/${pull.fork.namespace}/${pull.fork.name}.git`
342 : `https://g1t.sh/${params.owner}/${params.repo}.git`;
Issues and pull requests replace intents and attempts343 const active = pull.status === "draft" || pull.status === "open";
344
Acceptance checks in sandboxes, line comments and review verdicts345 // Follow an agent at work, or checks in progress, without a manual reload.
Issues and pull requests replace intents and attempts346 const revalidator = useRevalidator();
347 const working = pull.status === "draft";
Acceptance checks in sandboxes, line comments and review verdicts348 const checking = checks?.status === "queued" || checks?.status === "running";
349 const reviews = verdicts(comments);
350 // What stands between this pull request and a merge, if anything.
351 const unchecked = checks && checks.status !== "passed";
Agents as a team: lifecycle, merge queue, billing and a new shell352 // Pull requests for other issues changing the same files will conflict;
353 // ones for the same issue are alternatives, and expected to.
354 const collisions = overlaps.filter((other) => other.issue == null || other.issue !== pull.issue);
355 const review = {
356 changesUrl: here + "?tab=changes",
357 // Nobody reviews their own pull request.
358 canJudge: active && viewer != null && viewer.id !== pull.author.id,
359 };
360 // Everyone whose review was asked for, then anyone who reviewed unasked.
361 const reviewerNames = [
362 ...new Set([...pull.reviewers, ...reviews.map(({ reviewer }) => reviewer)]),
363 ];
364 const [submitted, setSubmitted] = useState<string | null>(null);
365 const catchingUp = submitted === "update" && behind;
366 // g1t is taking a step of its own accord, so the page will change.
367 const moving =
368 lifecycle != null && lifecycle.stage !== "ready" && lifecycle.stage !== "needs_you";
Issues and pull requests replace intents and attempts369 useEffect(() => {
Agents as a team: lifecycle, merge queue, billing and a new shell370 if (!working && !checking && !reviewPending && !catchingUp && !moving && !landing) return;
Issues and pull requests replace intents and attempts371 const timer = setInterval(() => {
372 if (document.visibilityState === "visible") revalidator.revalidate();
373 }, REFRESH_MS);
374 return () => clearInterval(timer);
Agents as a team: lifecycle, merge queue, billing and a new shell375 }, [working, checking, reviewPending, catchingUp, moving, landing, revalidator]);
Issues and pull requests replace intents and attempts376
377 return (
Agents as a team: lifecycle, merge queue, billing and a new shell378 // The changes get the whole width; people and settings are a tab away.
379 <div className={`grid gap-8 ${tab === "changes" ? "" : "lg:grid-cols-[1fr_19rem]"}`}>
Issues and pull requests replace intents and attempts380 <div className="min-w-0">
381 <h2 className="text-2xl font-semibold tracking-tight text-balance">
382 {pull.title} <span className="font-normal text-faint">#{pull.number}</span>
383 </h2>
384 <div className="mt-3 flex flex-wrap items-center gap-x-3 gap-y-2 text-sm text-muted">
385 <PullState status={pull.status} />
386 <span className="flex items-center gap-2">
Agents as a team: lifecycle, merge queue, billing and a new shell387 <Avatar
388 name={(pull.status === "merged" && pull.mergedBy) || pull.author.username}
389 size={18}
390 />
Issues and pull requests replace intents and attempts391 <span>
Agents as a team: lifecycle, merge queue, billing and a new shell392 <span className="font-medium text-fg">
393 {(pull.status === "merged" && pull.mergedBy) || pull.author.username}
394 </span>{" "}
Pull requests from branches395 {pull.status === "merged" ? "merged" : "wants to merge"}
396 {pull.branch && (
397 <>
398 {" "}
399 <span className="font-mono text-fg">{pull.branch}</span>
400 </>
401 )}{" "}
402 into <span className="font-mono text-fg">{defaultBranch}</span>
Issues and pull requests replace intents and attempts403 </span>
404 </span>
Pull requests from branches405 {/* A pull request from a branch was made by its author, not an agent. */}
406 {!pull.branch && (
407 <span className="flex items-center gap-1.5 font-mono text-xs">
408 <Bot size={14} />
409 {pull.agent}
410 {pull.runtime === "hosted" && <span className="text-faint">on g1t</span>}
411 </span>
412 )}
Issues and pull requests replace intents and attempts413 </div>
414
Acceptance checks in sandboxes, line comments and review verdicts415 {reviews.length > 0 && (
416 <p className="mt-3 flex flex-wrap items-center gap-x-4 gap-y-1 text-sm">
417 {reviews.map(({ reviewer, verdict }) => (
418 <span
419 key={reviewer}
420 className={`flex items-center gap-1.5 ${
421 verdict === "approve" ? "text-accent" : "text-danger"
422 }`}
423 >
424 {verdict === "approve" ? <CircleCheck size={15} /> : <CircleSlash size={15} />}
425 {verdict === "approve" ? "Approved by" : "Changes requested by"}{" "}
426 <span className="font-medium">{reviewer}</span>
427 </span>
428 ))}
429 </p>
430 )}
431
Agents as a team: lifecycle, merge queue, billing and a new shell432 {lifecycle && <LifecyclePanel lifecycle={lifecycle} />}
433
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request434 {/* Steering: while its agent works, people can tell it things. */}
435 {canManage &&
436 pull.runtime === "hosted" &&
437 (working || ["working", "revising", "catching_up"].includes(lifecycle?.stage ?? "")) && (
438 <Form method="post" className="mt-4 rounded-2xl bg-surface p-4 ring-1 ring-merged/30">
439 <p className="flex items-center gap-2 text-sm font-medium">
440 <Sparkles size={15} className="text-merged" />
441 Message the agent
442 </p>
443 <p className="mt-1 text-xs text-muted">
444 A correction, a hint, a change of plan. It reads it at its next step, without
445 starting over.
446 </p>
447 <div className="mt-3 flex gap-2">
448 <input type="hidden" name="action" value="message" />
449 <input
450 name="body"
451 required
452 autoComplete="off"
453 data-1p-ignore
454 placeholder="Keep the old flag working too…"
455 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"
456 />
457 <Button type="submit">Send</Button>
458 </div>
459 </Form>
460 )}
461 {messages.length > 0 && (
462 <ul className="mt-3 space-y-1.5">
463 {messages.map((message) => (
464 <li key={message.id} className="flex items-start gap-2 text-sm">
465 <Avatar name={message.author} size={18} />
466 <span className="min-w-0 grow">
Agents ask each other, hand each other work, and answer467 <span className="font-medium">
468 {message.fromNumber != null ? `The agent on #${message.fromNumber}` : message.author}
469 </span>{" "}
470 <span className="text-muted">
471 {message.kind === "question"
472 ? "asked:"
473 : message.kind === "handoff"
474 ? "handed over:"
475 : message.kind === "answer"
476 ? "answered:"
477 : "to the agent:"}
478 </span>{" "}
479 {message.body}
480 {message.answer && (
481 <span className="mt-1 block border-l-2 border-merged/40 pl-2 text-muted">
482 {message.declined ? "Declined: " : "Answer: "}
483 {message.answer}
484 </span>
485 )}
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request486 </span>
487 <span className={`shrink-0 text-xs ${message.deliveredAt ? "text-accent" : "text-faint"}`}>
488 {message.deliveredAt ? "read by the agent" : "waiting for its next step"}
489 </span>
490 </li>
491 ))}
492 </ul>
493 )}
494
Issues and pull requests replace intents and attempts495 {issue && (
496 <Link
497 to={`${base}/issues/${issue.number}`}
498 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"
499 >
500 <IssueIcon issue={issue} />
501 <span className="text-muted">
502 {pull.status === "merged" && issue.resolvedBy === pull.number
503 ? "Resolved"
504 : "For issue"}
505 </span>
506 <span className="truncate font-medium">{issue.title}</span>
507 <span className="text-faint">#{issue.number}</span>
508 {issue.pullCount > 1 && (
509 <span className="ml-auto shrink-0 text-xs text-faint">
510 1 of {issue.pullCount} pull requests
511 </span>
512 )}
513 </Link>
514 )}
515
516 {pull.status === "merged" && (
517 <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">
518 <GitMerge size={16} className="shrink-0 text-merged" />
519 <span>
520 Merged into <span className="font-mono">{defaultBranch}</span> by{" "}
521 <span className="font-medium">{pull.mergedBy}</span>{" "}
Agents as a team: lifecycle, merge queue, billing and a new shell522 {/* Not an account: the repository's settings said to merge it. */}
523 {pull.mergedBy === "g1t" && "automatically, once it was ready, "}
Issues and pull requests replace intents and attempts524 {pull.mergedAt && <TimeAgo at={pull.mergedAt} />}
525 {pull.headCommit && (
526 <span className="font-mono text-muted"> · {pull.headCommit.slice(0, 7)}</span>
527 )}
528 </span>
529 </p>
530 )}
531 {pull.supersededBy != null && (
532 <p className="mt-4 rounded-xl border border-line bg-surface px-4 py-2.5 text-sm text-muted">
533 Closed because{" "}
534 <Link
535 to={`${base}/pull/${pull.supersededBy}`}
536 className="font-medium text-fg hover:underline"
537 >
538 #{pull.supersededBy}
539 </Link>{" "}
540 was merged for this issue instead.
541 </p>
542 )}
543
Agents as a team: lifecycle, merge queue, billing and a new shell544 {collisions.length > 0 && active && (
545 <div className="mt-4 rounded-xl border border-line bg-surface px-4 py-3 text-sm">
546 <p className="flex items-center gap-2.5 font-medium">
547 <Radar size={16} className="shrink-0 text-info" />
548 Other work is changing the same files
549 </p>
550 <ul className="mt-2 space-y-1.5">
551 {collisions.map((other) => (
552 <li key={other.number} className="flex flex-wrap items-baseline gap-x-2 text-muted">
553 <Link
554 to={`${base}/pull/${other.number}`}
555 className="font-medium text-fg hover:underline"
556 >
557 {other.title} <span className="font-normal text-faint">#{other.number}</span>
558 </Link>
559 <span className="font-mono text-xs">{other.paths.join(", ")}</span>
560 </li>
561 ))}
562 </ul>
563 <p className="mt-2 text-xs text-faint">
564 Whichever merges second will have to catch up, and may conflict.
565 </p>
566 </div>
567 )}
568
Issues and pull requests replace intents and attempts569 <nav className="mt-8 flex gap-6 border-b border-line">
570 <TabLink to={here} active={tab === "conversation"}>
571 <MessageSquare size={15} />
572 Conversation
573 </TabLink>
574 <TabLink to={here + "?tab=session"} active={tab === "session"}>
575 <MessagesSquare size={15} />
576 Session
577 </TabLink>
578 <TabLink to={here + "?tab=changes"} active={tab === "changes"}>
579 <FileDiff size={15} />
580 Changes
581 </TabLink>
582 </nav>
583 <div className="mt-5">
584 {comparison ? (
Acceptance checks in sandboxes, line comments and review verdicts585 <DiffView
586 comparison={comparison}
587 review={{
588 comments: comments.filter((comment) => comment.path),
589 canComment: Boolean(viewer),
590 }}
591 />
Issues and pull requests replace intents and attempts592 ) : tab === "session" ? (
593 session.length === 0 ? (
594 <EmptyState title="Nothing recorded yet">
595 The agent's prompts, reasoning and tool calls appear here as it
596 works.
597 </EmptyState>
598 ) : (
599 <ol className="relative space-y-5 before:absolute before:top-2 before:bottom-2 before:left-3.25 before:w-px before:bg-line">
600 {session.map((entry) => (
601 <Entry key={entry.seq} entry={entry} agent={pull.agent} />
602 ))}
603 </ol>
604 )
605 ) : (
606 <div className="space-y-4">
Agents as a team: lifecycle, merge queue, billing and a new shell607 <TimelineItem
608 author={pull.author.username}
609 at={pull.createdAt}
610 action={
611 <span>
612 opened this pull request
613 {!pull.branch && (
614 <>
615 {" "}
616 with <span className="font-mono text-xs">{pull.agent}</span>
617 </>
618 )}
619 </span>
620 }
621 >
622 {pull.body ? (
623 <Markdown source={pull.body} repo={{ namespace: params.owner, name: params.repo }} />
624 ) : (
625 <p className="text-sm text-muted">
626 {working
627 ? "No description yet. It is written when the pull request is marked ready for review."
628 : "No description."}
629 </p>
630 )}
631 {canManage && pull.status === "open" && (
632 <details className="mt-3 border-t border-line pt-3 text-sm">
633 <summary className="cursor-pointer text-xs text-faint hover:text-fg">
634 Edit description
635 </summary>
636 <Form method="post" className="mt-3 space-y-2">
637 <Textarea
638 name="summary"
639 rows={6}
640 placeholder="What changed and why"
641 defaultValue={pull.body ?? ""}
642 />
643 <Button type="submit">Save</Button>
644 </Form>
645 </details>
646 )}
647 </TimelineItem>
648
649 <CommentList comments={comments} review={review} base={base} />
650
651 {pull.status === "draft" && (
652 <StatusBox>
653 <StatusRow icon={<Loader size={16} className="text-faint" />} title="This is a draft">
654 It is still being worked on. It can be reviewed and merged once it is
655 marked ready.
656 </StatusRow>
657 {canManage && (
658 <Form method="post" className="space-y-2 px-4 py-3">
659 <Textarea name="summary" rows={3} placeholder="What changed and why" />
660 <Button type="submit">Mark ready for review</Button>
661 </Form>
662 )}
663 </StatusBox>
664 )}
665
666 {pull.status === "open" && (
667 <StatusBox>
668 {checks && (
669 <StatusRow
670 icon={
671 checks.status === "passed" ? (
672 <CircleCheck size={16} className="text-accent" />
673 ) : checking ? (
674 <Loader size={16} className="animate-spin text-faint" />
675 ) : (
676 <CircleSlash size={16} className="text-danger" />
677 )
678 }
679 title={
680 checks.status === "passed"
681 ? "Acceptance checks passed"
682 : checking
683 ? "Acceptance checks are running"
684 : checks.status === "failed"
685 ? "Acceptance checks failed"
686 : "Acceptance checks could not be run"
687 }
688 >
689 {checks.results.length > 0 &&
690 `${checks.results.filter((result) => result.passed).length} of ${checks.results.length} passed, in a clean sandbox.`}
691 </StatusRow>
692 )}
693 <StatusRow
694 icon={
695 reviews.some(({ verdict }) => verdict === "request_changes") ? (
696 <CircleSlash size={16} className="text-danger" />
697 ) : reviews.length > 0 ? (
698 <CircleCheck size={16} className="text-accent" />
699 ) : (
700 <MessageSquare size={16} className="text-faint" />
701 )
702 }
703 title={
704 reviews.length === 0
705 ? "No reviews yet"
706 : reviews
707 .map(
708 ({ reviewer, verdict }) =>
709 `${verdict === "approve" ? "Approved by" : "Changes requested by"} ${reviewer}`,
710 )
711 .join(" · ")
712 }
713 >
714 {reviewPending && "A g1t agent is reviewing it now. "}
715 {requiredApprovals > 0 &&
716 `This repository requires ${requiredApprovals} approving ${
717 requiredApprovals === 1 ? "review" : "reviews"
718 } before merging.`}
719 </StatusRow>
720 {landing ? (
721 <StatusRow
722 icon={<Loader size={16} className="animate-spin text-accent" />}
723 title="Merging"
724 >
725 {defaultBranch} has moved, so g1t is bringing this up to date first. It
726 lands as soon as that is done. Watch it in the Session tab.
727 </StatusRow>
728 ) : behind ? (
729 <StatusRow
730 icon={<GitPullRequestArrow size={16} className="text-info" />}
731 title={`${defaultBranch} has moved since this was made`}
732 >
733 {requireUpToDate
734 ? "This repository requires pull requests to be up to date, so it has to catch up before it can merge."
735 : "That does not stop it merging: it is brought up to date as part of the merge."}
736 {canUpdate && agentsEnabled && (
737 <Form
738 method="post"
739 className="mt-2"
740 onSubmit={() => setSubmitted("update")}
741 >
742 <input type="hidden" name="action" value="update" />
743 <Button variant="quiet" type="submit" disabled={catchingUp}>
744 {catchingUp ? "Catching up…" : `Catch up with ${defaultBranch} now`}
745 </Button>
746 </Form>
747 )}
748 {actionData?.action === "update" && (
749 <ErrorText>{actionData.error}</ErrorText>
750 )}
751 </StatusRow>
752 ) : (
753 pull.headCommit && (
754 <StatusRow
755 icon={<CircleCheck size={16} className="text-accent" />}
756 title={`Up to date with ${defaultBranch}`}
757 />
758 )
759 )}
760 {stalled && !lifecycle && (
761 <StatusRow icon={<Hand size={16} className="text-warn" />} title="Needs you">
762 {stalled}
763 </StatusRow>
764 )}
765 {canMerge && lifecycle?.stage === "queued" && (
766 <Form method="post" className="flex flex-wrap items-center gap-3 px-4 py-3">
767 <ButtonLink to={`${base}/queue`}>
768 <Layers size={15} />
769 See the queue
770 </ButtonLink>
771 <Button variant="quiet" type="submit" name="action" value="unqueue">
772 Remove from the queue
773 </Button>
774 </Form>
775 )}
776 {canMerge && !landing && lifecycle?.stage !== "queued" && (
777 <Form method="post" className="space-y-3 px-4 py-3">
778 {issue?.state === "open" && (
779 <label className="flex items-start gap-2 text-xs text-muted">
780 <input
781 type="checkbox"
782 name="keepIssueOpen"
783 className="mt-0.5 accent-accent"
784 />
785 <span>Keep #{issue.number} open. This is only part of the work.</span>
786 </label>
787 )}
788 {unchecked && canIgnoreChecks && (
789 <label className="flex items-start gap-2 text-xs text-muted">
790 <input
791 type="checkbox"
792 name="ignoreChecks"
793 className="mt-0.5 accent-accent"
794 />
795 <span>
796 Merge although the checks{" "}
797 {checking ? "have not finished" : "did not pass"}.
798 </span>
799 </label>
800 )}
801 <div className="flex flex-wrap items-center gap-3">
802 <Button
803 variant="accent"
804 type="submit"
805 name="action"
806 value="merge"
807 disabled={behind && requireUpToDate}
808 >
809 {mergeQueue ? <Layers size={15} /> : <GitMerge size={15} />}
810 {mergeQueue ? "Add to the merge queue" : `Merge into ${defaultBranch}`}
811 </Button>
812 <span className="text-xs text-muted">
813 {mergeQueue
814 ? `Tested together with everything ahead of it, then lands on ${defaultBranch}.`
815 : issue?.state === "open"
816 ? `Closes issue #${issue.number}, and any other pull requests still open for it.`
817 : `Lands its commits on ${defaultBranch}.`}
818 </span>
819 </div>
820 {actionData?.action === "merge" && (
821 <ErrorText>{actionData.error}</ErrorText>
822 )}
823 </Form>
824 )}
825 </StatusBox>
Issues and pull requests replace intents and attempts826 )}
Agents as a team: lifecycle, merge queue, billing and a new shell827
828 <CommentForm
829 author={viewer?.username ?? null}
830 resetKey={comments.length}
831 review={review}
Acceptance checks in sandboxes, line comments and review verdicts832 />
Agents as a team: lifecycle, merge queue, billing and a new shell833 {canManage && active && (
834 <Form method="post" className="flex justify-end">
835 <Button variant="quiet" type="submit" name="action" value="close">
836 Close pull request
837 </Button>
838 </Form>
839 )}
840 {actionData &&
841 !["merge", "comment", "recheck", "update", "agent-review", "reviewers", "assign"].includes(
842 String(actionData.action),
843 ) && <ErrorText>{actionData.error}</ErrorText>}
Issues and pull requests replace intents and attempts844 </div>
845 )}
Acceptance checks in sandboxes, line comments and review verdicts846 {actionData?.action === "comment" && (
847 <div className="mt-2">
848 <ErrorText>{actionData.error}</ErrorText>
849 </div>
850 )}
Issues and pull requests replace intents and attempts851 </div>
852 </div>
853
Agents as a team: lifecycle, merge queue, billing and a new shell854 <aside className={tab === "changes" ? "hidden" : "space-y-6"}>
Acceptance checks in sandboxes, line comments and review verdicts855 <ChecksPanel
856 run={checks}
857 commands={issue?.checks ?? []}
858 canRerun={canManage && pull.status === "open"}
859 />
860 {actionData?.action === "recheck" && <ErrorText>{actionData.error}</ErrorText>}
861
Agents as a team: lifecycle, merge queue, billing and a new shell862 <section>
863 <h3 className="text-sm font-medium">Reviewers</h3>
864 <ul className="mt-2 space-y-1.5 text-sm">
865 {reviewerNames.map((name) => {
866 const verdict = reviews.find(({ reviewer }) => reviewer === name)?.verdict;
867 const pending = name === "g1t-agent" && reviewPending;
868 return (
869 <li key={name} className="flex items-center gap-2 px-1">
870 {name === "g1t-agent" ? (
871 <Sparkles size={16} className="shrink-0 text-accent" />
872 ) : (
873 <Avatar name={name} size={20} />
874 )}
875 <span className="grow truncate font-mono text-xs">{name}</span>
876 {pending ? (
877 <span className="flex items-center gap-1.5 text-xs text-muted">
878 <span className="size-1.5 animate-pulse rounded-full bg-accent" />
879 Reviewing
880 </span>
881 ) : verdict === "approve" ? (
882 <span className="flex items-center gap-1 text-xs text-accent">
883 <CircleCheck size={13} /> Approved
884 </span>
885 ) : verdict === "request_changes" ? (
886 <span className="flex items-center gap-1 text-xs text-danger">
887 <CircleSlash size={13} /> Changes requested
888 </span>
889 ) : (
890 <span className="text-xs text-faint">Review requested</span>
891 )}
892 </li>
893 );
894 })}
895 {reviewerNames.length === 0 && (
896 <li className="px-1 text-xs text-faint">No reviews requested yet.</li>
897 )}
898 </ul>
899 {pull.status === "open" && canManage && (
900 <div className="mt-3 space-y-2">
901 {agentsEnabled && !reviewPending && (
902 <Form method="post">
903 <input type="hidden" name="action" value="agent-review" />
904 {pull.reviewers.map((name) => (
905 <input key={name} type="hidden" name="reviewer" value={name} />
906 ))}
907 <div className="*:w-full">
908 <Button variant="quiet" type="submit">
909 <Sparkles size={14} className="text-accent" />
910 Request review from g1t agent
911 </Button>
912 </div>
913 </Form>
Issues and pull requests replace intents and attempts914 )}
Agents as a team: lifecycle, merge queue, billing and a new shell915 <details>
916 <summary className="cursor-pointer text-xs text-faint hover:text-fg">
917 Request review from people
918 </summary>
919 <Form method="post" className="mt-2 space-y-2" key={pull.reviewers.join()}>
920 <input type="hidden" name="action" value="reviewers" />
921 {pull.reviewers.includes("g1t-agent") && (
922 <input type="hidden" name="reviewer" value="g1t-agent" />
923 )}
924 <PeoplePicker
925 name="reviewer"
926 members={members.filter((name) => name !== pull.author.username)}
927 chosen={pull.reviewers.filter((name) => name !== "g1t-agent")}
928 />
929 <Button variant="quiet" type="submit">
930 Save reviewers
931 </Button>
932 </Form>
933 </details>
934 </div>
935 )}
936 {["agent-review", "reviewers"].includes(String(actionData?.action)) && (
937 <ErrorText>{actionData?.error}</ErrorText>
938 )}
939 </section>
Issues and pull requests replace intents and attempts940
Agents as a team: lifecycle, merge queue, billing and a new shell941 <section>
942 <h3 className="text-sm font-medium">Assignees</h3>
943 <ul className="mt-2 space-y-1.5 text-sm">
944 {pull.assignees.map((name) => (
945 <li key={name} className="flex items-center gap-2 px-1">
946 <Avatar name={name} size={20} />
947 <span className="grow truncate font-mono text-xs">{name}</span>
948 </li>
949 ))}
950 {pull.assignees.length === 0 && (
951 <li className="px-1 text-xs text-faint">No one yet.</li>
Issues and pull requests replace intents and attempts952 )}
Agents as a team: lifecycle, merge queue, billing and a new shell953 </ul>
954 {viewer && canManage && active && (
955 <div className="mt-3 space-y-2">
956 {!pull.assignees.includes(viewer.username) && (
957 <Form method="post">
958 <input type="hidden" name="action" value="assign" />
959 {pull.assignees.map((name) => (
960 <input key={name} type="hidden" name="assignee" value={name} />
961 ))}
962 <input type="hidden" name="assignee" value={viewer.username} />
963 <div className="*:w-full">
964 <Button variant="quiet" type="submit">
965 Assign yourself
966 </Button>
967 </div>
968 </Form>
969 )}
970 <details>
971 <summary className="cursor-pointer text-xs text-faint hover:text-fg">
972 Assign people
973 </summary>
974 <Form method="post" className="mt-2 space-y-2" key={pull.assignees.join()}>
975 <input type="hidden" name="action" value="assign" />
976 <PeoplePicker name="assignee" members={members} chosen={pull.assignees} />
977 <Button variant="quiet" type="submit">
978 Save assignees
979 </Button>
980 </Form>
981 </details>
982 </div>
983 )}
984 {actionData?.action === "assign" && <ErrorText>{actionData.error}</ErrorText>}
985 </section>
Issues and pull requests replace intents and attempts986
987 <section>
988 <h3 className="text-sm font-medium">Working copy</h3>
Pull requests from branches989 {pull.branch ? (
990 <>
Agents as a team: lifecycle, merge queue, billing and a new shell991 <p className="mt-1 text-xs text-muted">
992 <GitBranch size={13} className="mr-1 inline align-[-2px]" />
Pull requests from branches993 Branch <span className="font-mono text-fg">{pull.branch}</span> of this
994 repository. Pushes to it show up here.
995 </p>
996 <div className="mt-2">
997 <CopyLine text={`git clone -b ${pull.branch} ${remote}`} />
998 </div>
999 </>
1000 ) : (
1001 <>
1002 <p className="mt-1 text-xs text-muted">
1003 This fork belongs to the pull request. Pushes to it show up here.
1004 </p>
1005 <div className="mt-2">
1006 <CopyLine text={`git clone ${remote}`} />
1007 </div>
1008 </>
1009 )}
Issues and pull requests replace intents and attempts1010 <p className="mt-3 flex items-center gap-1.5 font-mono text-xs text-faint">
1011 <GitCommitHorizontal size={13} />
1012 {pull.headCommit?.slice(0, 12) ?? "no commits pushed yet"}
1013 </p>
1014 </section>
1015 </aside>
1016 </div>
1017 );
1018}