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

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