Skip to content

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

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