Skip to content
106 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 { SkeletonLine } from "./ui/skeleton";
16
17function ReviewIcon({ state }: { state: Attempt["review"]["state"] }) {
18 if (state === "approved") return <CircleCheck size={14} className="shrink-0 text-success" />;
19 if (state === "changes_requested") return <CircleSlash size={14} className="shrink-0 text-danger" />;
20 if (state === "requested") return <MessageSquare size={14} className="shrink-0 text-warn" />;
21 return <CircleDashed size={14} className="shrink-0 text-faint" />;
22}
23
24function AttemptRow({ attempt, base }: { attempt: Attempt; base: string }) {
25 const merged = attempt.status === "merged";
26 return (
27 <li className={`px-4 py-3 ${attempt.current ? "bg-raised/40" : ""}`}>
28 <div className="flex min-w-0 items-center gap-2">
29 <PullIcon status={attempt.status} size={15} />
30 {attempt.current ? (
31 <span className="min-w-0 truncate font-medium">
32 {attempt.title} <span className="font-normal text-faint">#{attempt.number}</span>
33 </span>
34 ) : (
35 <Link to={`${base}/pull/${attempt.number}`} className="min-w-0 truncate font-medium hover:underline">
36 {attempt.title} <span className="font-normal text-faint">#{attempt.number}</span>
37 </Link>
38 )}
39 {attempt.current && (
40 <span className="shrink-0 rounded-full border border-line px-1.5 py-px text-[0.625rem] text-faint">This one</span>
41 )}
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 issue `issue`, 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, issue, base }: { attempts: Promise<Attempt[]>; issue: number; base: string }) {
76 return (
77 <Suspense
78 fallback={
79 <div className="mt-4 rounded-xl border border-line bg-surface px-4 py-3" aria-busy>
80 <SkeletonLine className="text-sm" barClassName="w-48" />
81 </div>
82 }
83 >
84 <Await resolve={attempts} errorElement={null}>
85 {(found) =>
86 found.filter((attempt) => !attempt.current).length > 0 && (
87 <section aria-label={`Other attempts at #${issue}`} className="mt-4 overflow-hidden rounded-xl border border-line bg-surface text-sm">
88 <h3 className="flex items-center gap-2.5 px-4 pt-3 font-medium">
89 <GitPullRequestArrow size={16} className="shrink-0 text-info" />
90 Other attempts at #{issue}
91 </h3>
92 <p className="px-4 pt-0.5 pb-2 text-xs text-faint">
93 Pull requests for the same issue. Merging one closes the others.
94 </p>
95 <ul className="divide-y divide-line border-t border-line">
96 {found.map((attempt) => (
97 <AttemptRow key={attempt.number} attempt={attempt} base={base} />
98 ))}
99 </ul>
100 </section>
101 )
102 }
103 </Await>
104 </Suspense>
105 );
106}