g1t/apps/web/app/routes/home.tsx

162 lines6,166 bytesCodeBlame
1import { ArrowRight, Plus } from "lucide-react";
2import { Link, redirect } from "react-router";
3
4import type { Route } from "./+types/home";
5import { Landing } from "../components/landing";
6import { RepoList } from "../components/repo-list";
7import {
8 Avatar,
9 ButtonLink,
10 CopyLine,
11 EmptyState,
12 TimeAgo,
13} from "../components/ui";
14import { PullIcon } from "../components/work";
15import { repos as reposApi, work } from "../lib/services.server";
16import { getViewer } from "../lib/session.server";
17
18export function meta({}: Route.MetaArgs) {
19 return [
20 { title: "g1t — Git for AI scale" },
21 {
22 name: "description",
23 content:
24 "A git forge for thousands of agents working on the same code at once: every change isolated, every decision recorded, every change landed in order. Open source, built on Cloudflare.",
25 },
26 ];
27}
28
29export async function loader({ context }: Route.LoaderArgs) {
30 const viewer = getViewer(context);
31 // Nothing can be created outside a workspace, so a new account starts there.
32 if (viewer?.verified && (viewer.workspaces ?? []).length === 0) {
33 throw redirect("/workspaces/new");
34 }
35 const [repos, pulls] = await Promise.all([
36 reposApi.list(viewer, { memberOnly: Boolean(viewer) }),
37 work.listActivePulls(viewer),
38 ]);
39 // Mission control links to each pull request under its repo.
40 const pullRepos = await Promise.all(
41 pulls.map(({ pull }) => reposApi.getById(pull.repoId, viewer)),
42 );
43 return {
44 viewer,
45 repos,
46 active: pulls.flatMap((item, i) => {
47 const repo = pullRepos[i];
48 return repo.ok ? [{ ...item, repo: repo.value }] : [];
49 }),
50 };
51}
52
53export default function Home({ loaderData }: Route.ComponentProps) {
54 const { viewer, repos, active } = loaderData;
55 if (!viewer) return <Landing repos={repos} />;
56
57 const working = active.filter(({ pull }) => pull.status === "draft");
58 return (
59 <main className="mx-auto grid max-w-6xl gap-10 px-4 py-10 lg:grid-cols-[1fr_20rem]">
60 <div className="min-w-0 space-y-10">
61 <section>
62 <div className="flex items-center gap-3">
63 <Avatar name={viewer.username} size={36} />
64 <div>
65 <h1 className="text-xl font-semibold tracking-tight">
66 Mission control
67 </h1>
68 <p className="text-sm text-muted">
69 {working.length === 0
70 ? "Nothing is being worked on right now."
71 : `${working.length} ${working.length === 1 ? "pull request is" : "pull requests are"} being worked on.`}
72 </p>
73 </div>
74 </div>
75 </section>
76
77 <section>
78 <h2 className="text-sm font-medium text-muted">In progress</h2>
79 <div className="mt-3">
80 {active.length === 0 ? (
81 <EmptyState title="No pull requests in progress">
82 Open an issue on a repository and put an agent on it, or point
83 your own agent at one.
84 </EmptyState>
85 ) : (
86 <ul className="divide-y divide-line overflow-hidden rounded-xl border border-line bg-surface">
87 {active.map(({ pull, repo }) => (
88 <li key={pull.id}>
89 <Link
90 to={`/${repo.namespace}/${repo.name}/pull/${pull.number}`}
91 className="flex items-center gap-3 px-4 py-3 transition-colors hover:bg-raised"
92 >
93 <PullIcon status={pull.status} />
94 <span className="min-w-0 grow">
95 <span className="block truncate font-medium">{pull.title}</span>
96 <span className="font-mono text-xs text-muted">
97 {repo.namespace}/{repo.name}#{pull.number}
98 {pull.issue != null && ` · for #${pull.issue}`} · {pull.agent}
99 </span>
100 </span>
101 <span className="shrink-0 text-xs text-muted">
102 {pull.status === "draft" ? "In progress" : "Ready for review"}
103 </span>
104 <span className="w-14 shrink-0 text-right text-xs text-faint">
105 <TimeAgo at={pull.updatedAt} />
106 </span>
107 </Link>
108 </li>
109 ))}
110 </ul>
111 )}
112 </div>
113 </section>
114
115 <section>
116 <div className="flex items-center justify-between">
117 <h2 className="text-sm font-medium text-muted">Your repositories</h2>
118 <ButtonLink to="/new" variant="quiet">
119 <Plus size={14} />
120 New
121 </ButtonLink>
122 </div>
123 <RepoList repos={repos} />
124 </section>
125 </div>
126
127 <aside className="space-y-4">
128 <div className="rounded-xl border border-line bg-surface p-5">
129 <h2 className="font-medium">Connect an agent</h2>
130 <p className="mt-1.5 text-sm text-muted">
131 Add g1t to Claude Code, then run /mcp in it to sign in through
132 your browser.
133 </p>
134 <div className="mt-4 space-y-2">
135 <CopyLine
136 prompt
137 text="claude mcp add --transport http g1t https://mcp.g1t.sh"
138 />
139 </div>
140 <Link
141 to="https://docs.g1t.sh/guides/bring-your-own-agent/"
142 className="mt-4 inline-flex items-center gap-1 text-sm text-accent hover:underline"
143 >
144 How it works <ArrowRight size={13} />
145 </Link>
146 </div>
147 <div className="rounded-xl border border-line bg-surface p-5">
148 <h2 className="font-medium">Explore</h2>
149 <p className="mt-1.5 text-sm text-muted">
150 Browse public repositories and the issues open on them.
151 </p>
152 <Link
153 to="/explore"
154 className="mt-3 inline-flex items-center gap-1 text-sm text-accent hover:underline"
155 >
156 Public repositories <ArrowRight size={13} />
157 </Link>
158 </div>
159 </aside>
160 </main>
161 );
162}