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

102 lines4,289 bytesCodeBlame
1import { Bot, GitBranch, Plus } from "lucide-react";
2import { Link } from "react-router";
3
4import type { Route } from "./+types/pulls";
5import { page } from "../../lib/meta";
6import { openedBy } from "../../lib/opened-by";
7import { ButtonLink, EmptyState, TimeAgo } from "../../components/ui";
8import { CheckBadge } from "../../components/checks";
9import { ChangeSize, PullIcon, StateTabs } from "../../components/work";
10import { AgentBadge, useActiveRuns } from "../../components/agents";
11import { work } from "../../lib/services.server";
12import { getViewer, unwrap } from "../../lib/session.server";
13
14export function meta({ params, ...args }: Route.MetaArgs) {
15 return page(args, { title: `Pull requests · ${params.owner}/${params.repo} · g1t` });
16}
17
18export async function loader({ params, context, request }: Route.LoaderArgs) {
19 const path = { namespace: params.owner, name: params.repo };
20 const state =
21 new URL(request.url).searchParams.get("state") === "closed" ? "closed" : "open";
22 return {
23 pulls: unwrap(await work.listPulls(path, getViewer(context), state)),
24 state,
25 } as const;
26}
27
28export default function Pulls({ loaderData, params }: Route.ComponentProps) {
29 const { pulls, state } = loaderData;
30 const base = `/${params.owner}/${params.repo}`;
31 // Which pull requests an agent is working on this minute, and at what.
32 const working = useActiveRuns(params.owner, params.repo);
33 return (
34 <div>
35 <StateTabs
36 to={`${base}/pulls`}
37 state={state}
38 action={
39 <ButtonLink to={`${base}/pulls/new`}>
40 <Plus size={15} />
41 New pull request
42 </ButtonLink>
43 }
44 />
45 <div className="mt-4">
46 {pulls.length === 0 ? (
47 <EmptyState
48 title={state === "open" ? "No open pull requests" : "No closed pull requests"}
49 >
50 A pull request proposes a change. Assign agents to an issue and
51 each opens one in its own fork, or push a branch and open one
52 yourself.
53 </EmptyState>
54 ) : (
55 <ul className="divide-y divide-line overflow-hidden rounded-xl border border-line">
56 {pulls.map((pull) => (
57 <li key={pull.id}>
58 <Link
59 prefetch="intent"
60 to={`${base}/pull/${pull.number}`}
61 className="flex items-start gap-3 px-4 py-3 transition-colors hover:bg-surface"
62 >
63 <span className="mt-0.5">
64 <PullIcon status={pull.status} />
65 </span>
66 <span className="min-w-0 grow">
67 <span className="flex items-center gap-2">
68 <span className="truncate font-medium">{pull.title}</span>
69 {pull.status === "draft" && (
70 <span className="shrink-0 rounded-full border border-line px-1.5 py-px text-[0.6875rem] text-faint">
71 draft
72 </span>
73 )}
74 <AgentBadge run={working.get(pull.number)} />
75 </span>
76 <span className="mt-0.5 block text-xs text-faint">
77 #{pull.number} opened <TimeAgo at={pull.createdAt} /> by{" "}
78 {openedBy(pull).name}
79 {pull.requestedBy && <> for {pull.requestedBy.username}</>}
80 {pull.issue != null && <> · for #{pull.issue}</>}
81 {pull.supersededBy != null && <> · superseded by #{pull.supersededBy}</>}
82 </span>
83 </span>
84 <span className="mt-0.5 hidden sm:block">
85 <ChangeSize files={pull.files} />
86 </span>
87 <span className="mt-0.5">
88 <CheckBadge status={pull.checkStatus} />
89 </span>
90 <span className="mt-0.5 flex shrink-0 items-center gap-1 font-mono text-xs text-muted">
91 {pull.branch ? <GitBranch size={13} /> : <Bot size={13} />}
92 {pull.branch ?? pull.agent}
93 </span>
94 </Link>
95 </li>
96 ))}
97 </ul>
98 )}
99 </div>
100 </div>
101 );
102}