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

156 lines5,816 bytesCodeBlame
1import { ArrowRight, Plus } from "lucide-react";
2import { Link } 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 Status,
13 TimeAgo,
14} from "../components/ui";
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 attempt 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 const [repos, attempts] = await Promise.all([
32 reposApi.list(viewer, viewer ? { namespace: viewer.username } : {}),
33 work.listActiveAttempts(viewer),
34 ]);
35 // Mission control links to each attempt under its repo.
36 const attemptRepos = await Promise.all(
37 attempts.map(({ attempt }) => reposApi.getById(attempt.repoId, viewer)),
38 );
39 return {
40 viewer,
41 repos,
42 active: attempts.flatMap((item, i) => {
43 const repo = attemptRepos[i];
44 return repo.ok ? [{ ...item, repo: repo.value }] : [];
45 }),
46 };
47}
48
49export default function Home({ loaderData }: Route.ComponentProps) {
50 const { viewer, repos, active } = loaderData;
51 if (!viewer) return <Landing repos={repos} />;
52
53 const working = active.filter(({ attempt }) => attempt.status === "working");
54 return (
55 <main className="mx-auto grid max-w-6xl gap-10 px-4 py-10 lg:grid-cols-[1fr_20rem]">
56 <div className="min-w-0 space-y-10">
57 <section>
58 <div className="flex items-center gap-3">
59 <Avatar name={viewer.username} size={36} />
60 <div>
61 <h1 className="text-xl font-semibold tracking-tight">
62 Mission control
63 </h1>
64 <p className="text-sm text-muted">
65 {working.length === 0
66 ? "Nothing is running right now."
67 : `${working.length} ${working.length === 1 ? "attempt is" : "attempts are"} running.`}
68 </p>
69 </div>
70 </div>
71 </section>
72
73 <section>
74 <h2 className="text-sm font-medium text-muted">In progress</h2>
75 <div className="mt-3">
76 {active.length === 0 ? (
77 <EmptyState title="No attempts in progress">
78 Open an intent on a repository and start an attempt, or point
79 your agent at one.
80 </EmptyState>
81 ) : (
82 <ul className="divide-y divide-line overflow-hidden rounded-xl border border-line bg-surface">
83 {active.map(({ attempt, intent, repo }) => (
84 <li key={attempt.id}>
85 <Link
86 to={`/${repo.namespace}/${repo.name}/attempts/${attempt.id}`}
87 className="flex items-center gap-4 px-4 py-3 transition-colors hover:bg-raised"
88 >
89 <span className="min-w-0 grow">
90 <span className="block truncate font-medium">
91 {intent.title}
92 </span>
93 <span className="font-mono text-xs text-muted">
94 {repo.namespace}/{repo.name} #{intent.number} · attempt{" "}
95 {attempt.number} · {attempt.agent}
96 </span>
97 </span>
98 <span className="shrink-0 text-xs text-faint">
99 <TimeAgo at={attempt.updatedAt} />
100 </span>
101 <Status value={attempt.status} />
102 </Link>
103 </li>
104 ))}
105 </ul>
106 )}
107 </div>
108 </section>
109
110 <section>
111 <div className="flex items-center justify-between">
112 <h2 className="text-sm font-medium text-muted">Your repositories</h2>
113 <ButtonLink to="/new" variant="quiet">
114 <Plus size={14} />
115 New
116 </ButtonLink>
117 </div>
118 <RepoList repos={repos} />
119 </section>
120 </div>
121
122 <aside className="space-y-4">
123 <div className="rounded-xl border border-line bg-surface p-5">
124 <h2 className="font-medium">Connect an agent</h2>
125 <p className="mt-1.5 text-sm text-muted">
126 Create an access token in settings, then add g1t to Claude Code.
127 </p>
128 <div className="mt-4 space-y-2">
129 <CopyLine
130 prompt
131 text='claude mcp add --transport http g1t https://mcp.g1t.sh --header "Authorization: Bearer $G1T_TOKEN"'
132 />
133 </div>
134 <Link
135 to="https://docs.g1t.sh/guides/bring-your-own-agent/"
136 className="mt-4 inline-flex items-center gap-1 text-sm text-accent hover:underline"
137 >
138 How it works <ArrowRight size={13} />
139 </Link>
140 </div>
141 <div className="rounded-xl border border-line bg-surface p-5">
142 <h2 className="font-medium">Explore</h2>
143 <p className="mt-1.5 text-sm text-muted">
144 Browse public repositories and the intents open on them.
145 </p>
146 <Link
147 to="/explore"
148 className="mt-3 inline-flex items-center gap-1 text-sm text-accent hover:underline"
149 >
150 Public repositories <ArrowRight size={13} />
151 </Link>
152 </div>
153 </aside>
154 </main>
155 );
156}