Skip to content
110 linesCodeBlameRaw
1/**
2 * The pull requests for one issue, 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 issue 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 && (
42 <Badge className="px-1.5 text-[0.625rem] font-normal text-faint">This one</Badge>
43 )}
44 <span className="ml-auto shrink-0">
45 <ChangeSize files={attempt.files} />
46 </span>
47 </div>
48 <dl className="mt-2 grid gap-x-4 gap-y-1.5 text-xs sm:grid-cols-3">
49 <div className="flex min-w-0 items-center gap-1.5">
50 <dt className="sr-only">State</dt>
51 <dd className={`truncate ${merged ? "font-medium text-merged" : "text-muted"}`}>
52 {attemptOutcome(attempt)} · by {attempt.author}
53 </dd>
54 </div>
55 <div className="flex min-w-0 items-center gap-1.5">
56 <dt className="sr-only">Checks</dt>
57 <CheckStateIcon state={attempt.checks?.state ?? "none"} size={14} />
58 <dd className="truncate text-muted">{attempt.checks && attempt.checks.total > 0 ? checksTally(attempt.checks) : "No checks"}</dd>
59 </div>
60 <div className="flex min-w-0 items-center gap-1.5">
61 <dt className="sr-only">Review</dt>
62 <ReviewIcon state={attempt.review.state} />
63 <dd className="truncate text-muted">{attempt.review.text}</dd>
64 </div>
65 </dl>
66 {!attempt.current && attempt.shared.length > 0 && (
67 <p className="mt-1.5 truncate font-mono text-xs text-faint">Also changes {attempt.shared.join(", ")}</p>
68 )}
69 </li>
70 );
71}
72
73/**
74 * Every pull request for issue `issue`, this one first: streamed, so the
75 * page shows before the others are read. Nothing while there are no others.
76 */
77export function AttemptsBox({ attempts, issue, base }: { attempts: Promise<Attempt[]>; issue: number; base: string }) {
78 return (
79 <Suspense
80 fallback={
81 <Card className="mt-4 px-4 py-3" aria-busy>
82 <SkeletonLine className="text-sm" barClassName="w-48" />
83 </Card>
84 }
85 >
86 <Await resolve={attempts} errorElement={null}>
87 {(found) =>
88 found.filter((attempt) => !attempt.current).length > 0 && (
89 <Card asChild className="mt-4 overflow-hidden text-sm">
90 <section aria-label={`Other attempts at #${issue}`}>
91 <h3 className="flex items-center gap-2.5 px-4 pt-3 font-medium">
92 <GitPullRequestArrow size={16} className="shrink-0 text-info" />
93 Other attempts at #{issue}
94 </h3>
95 <p className="px-4 pt-0.5 pb-2 text-xs text-faint">
96 Pull requests for the same issue. Merging one closes the others.
97 </p>
98 <ul className="divide-y divide-line border-t border-line">
99 {found.map((attempt) => (
100 <AttemptRow key={attempt.number} attempt={attempt} base={base} />
101 ))}
102 </ul>
103 </section>
104 </Card>
105 )
106 }
107 </Await>
108 </Suspense>
109 );
110}