Skip to content

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

106 linesCodeBlameRaw
1/**
2 * The pull requests for one work item, side by side, on each of them: how each
3 * stands, how its checks went, where its review is and how big it is, so
4 * two agents' attempts at the same work item can be compared and one chosen.
5 */
6import { CircleCheck, CircleDashed, CircleSlash, GitPullRequestArrow, MessageSquare } from "lucide-react";
7import { Suspense } from "react";
8import { Await, Link } from "react-router";
9
10import { type Attempt, attemptOutcome } from "../lib/attempts";
11import { checksTally } from "../lib/commit-checks";
12import { CheckStateIcon } from "./commit-checks";
13import { ChangeSize } from "./work";
14import { PullIcon } from "./work-icons";
15import { Badge } from "./ui/badge";
16import { Card } from "./ui/card";
17import { SkeletonLine } from "./ui/skeleton";
18
19function ReviewIcon({ state }: { state: Attempt["review"]["state"] }) {
20 if (state === "approved") return <CircleCheck size={14} className="shrink-0 text-success" />;
21 if (state === "changes_requested") return <CircleSlash size={14} className="shrink-0 text-danger" />;
22 if (state === "requested") return <MessageSquare size={14} className="shrink-0 text-warn" />;
23 return <CircleDashed size={14} className="shrink-0 text-faint" />;
24}
25
26function AttemptRow({ attempt, base }: { attempt: Attempt; base: string }) {
27 const merged = attempt.status === "merged";
28 return (
29 <li className={`px-4 py-3 ${attempt.current ? "bg-raised/40" : ""}`}>
30 <div className="flex min-w-0 items-center gap-2">
31 <PullIcon status={attempt.status} size={15} />
32 {attempt.current ? (
33 <span className="min-w-0 truncate font-medium">
34 {attempt.title} <span className="font-normal text-faint">#{attempt.number}</span>
35 </span>
36 ) : (
37 <Link to={`${base}/pull/${attempt.number}`} className="min-w-0 truncate font-medium hover:underline">
38 {attempt.title} <span className="font-normal text-faint">#{attempt.number}</span>
39 </Link>
40 )}
41 {attempt.current && <Badge className="px-1.5 text-[0.625rem] font-normal text-faint">This one</Badge>}
42 <span className="ml-auto shrink-0">
43 <ChangeSize files={attempt.files} />
44 </span>
45 </div>
46 <dl className="mt-2 grid gap-x-4 gap-y-1.5 text-xs sm:grid-cols-3">
47 <div className="flex min-w-0 items-center gap-1.5">
48 <dt className="sr-only">State</dt>
49 <dd className={`truncate ${merged ? "font-medium text-merged" : "text-muted"}`}>
50 {attemptOutcome(attempt)} · by {attempt.author}
51 </dd>
52 </div>
53 <div className="flex min-w-0 items-center gap-1.5">
54 <dt className="sr-only">Checks</dt>
55 <CheckStateIcon state={attempt.checks?.state ?? "none"} size={14} />
56 <dd className="truncate text-muted">{attempt.checks && attempt.checks.total > 0 ? checksTally(attempt.checks) : "No checks"}</dd>
57 </div>
58 <div className="flex min-w-0 items-center gap-1.5">
59 <dt className="sr-only">Review</dt>
60 <ReviewIcon state={attempt.review.state} />
61 <dd className="truncate text-muted">{attempt.review.text}</dd>
62 </div>
63 </dl>
64 {!attempt.current && attempt.shared.length > 0 && (
65 <p className="mt-1.5 truncate font-mono text-xs text-faint">Also changes {attempt.shared.join(", ")}</p>
66 )}
67 </li>
68 );
69}
70
71/**
72 * Every pull request for work item `item`, this one first: streamed, so the
73 * page shows before the others are read. Nothing while there are no others.
74 */
75export function AttemptsBox({ attempts, item, base }: { attempts: Promise<Attempt[]>; item: number; base: string }) {
76 return (
77 <Suspense
78 fallback={
79 <Card className="mt-4 px-4 py-3" aria-busy>
80 <SkeletonLine className="text-sm" barClassName="w-48" />
81 </Card>
82 }
83 >
84 <Await resolve={attempts} errorElement={null}>
85 {(found) =>
86 found.filter((attempt) => !attempt.current).length > 0 && (
87 <Card asChild className="mt-4 overflow-hidden text-sm">
88 <section aria-label={`Other attempts at #${item}`}>
89 <h3 className="flex items-center gap-2.5 px-4 pt-3 font-medium">
90 <GitPullRequestArrow size={16} className="shrink-0 text-info" />
91 Other attempts at #{item}
92 </h3>
93 <p className="px-4 pt-0.5 pb-2 text-xs text-faint">Pull requests for the same work item in this repository. Merging one closes the others.</p>
94 <ul className="divide-y divide-line border-t border-line">
95 {found.map((attempt) => (
96 <AttemptRow key={attempt.number} attempt={attempt} base={base} />
97 ))}
98 </ul>
99 </section>
100 </Card>
101 )
102 }
103 </Await>
104 </Suspense>
105 );
106}