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

291 lines11,056 bytesCodeBlame

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Agents as a team: lifecycle, merge queue, billing and a new shell1import { ArrowRight, Hand, Plus } from "lucide-react";
2import { type ReactNode, useEffect } from "react";
3import { Link, data, useRevalidator } from "react-router";
4
5import type { Lifecycle, Pull, Repo } from "@g1t/contracts";
Initial g1t: services, event bus, intents and attempts6
7import type { Route } from "./+types/home";
API and MCP server, Rust identity service, registration, site redesign8import { Landing } from "../components/landing";
Agents as a team: lifecycle, merge queue, billing and a new shell9import { STAGE_LABEL, StageDots } from "../components/lifecycle";
API and MCP server, Rust identity service, registration, site redesign10import { RepoList } from "../components/repo-list";
11import {
12 Avatar,
13 ButtonLink,
14 CopyLine,
15 EmptyState,
16 TimeAgo,
17} from "../components/ui";
Agents as a team: lifecycle, merge queue, billing and a new shell18import { Assignee, IssueIcon, PullIcon } from "../components/work";
Work service in Rust, with RFC 3339 timestamps19import { repos as reposApi, work } from "../lib/services.server";
Initial g1t: services, event bus, intents and attempts20import { getViewer } from "../lib/session.server";
21
Agents as a team: lifecycle, merge queue, billing and a new shell22const REFRESH_MS = 5000;
23
Initial g1t: services, event bus, intents and attempts24export function meta({}: Route.MetaArgs) {
25 return [
Email verification, password reset, and Git for AI scale positioning26 { title: "g1t — Git for AI scale" },
Initial g1t: services, event bus, intents and attempts27 {
28 name: "description",
29 content:
Issues and pull requests replace intents and attempts30 "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.",
Initial g1t: services, event bus, intents and attempts31 },
32 ];
33}
34
Agents as a team: lifecycle, merge queue, billing and a new shell35export function headers({ loaderHeaders }: Route.HeadersArgs) {
36 return { "Server-Timing": loaderHeaders.get("Server-Timing") ?? "" };
37}
38
Initial g1t: services, event bus, intents and attempts39export async function loader({ context }: Route.LoaderArgs) {
40 const viewer = getViewer(context);
Agents as a team: lifecycle, merge queue, billing and a new shell41 const started = Date.now();
42 const times: Record<string, number> = {};
43 const timed = <T,>(name: string, promise: Promise<T>) =>
44 promise.then((value) => ((times[name] = Date.now() - started), value));
45 const [repos, pulls, assigned] = await Promise.all([
46 timed("repos", reposApi.list(viewer, { memberOnly: Boolean(viewer) })),
47 timed("pulls", work.listActivePulls(viewer)),
48 timed("assigned", work.listAssignedIssues(viewer)),
Initial g1t: services, event bus, intents and attempts49 ]);
Agents as a team: lifecycle, merge queue, billing and a new shell50 // Each issue and pull request is shown under its repository. Most are in
51 // the viewer's own, already listed; the rest are looked up once each, all
52 // at once. One the viewer can no longer see drops out.
53 const known = new Map<string, Repo>(repos.map((repo) => [repo.id, repo]));
54 const missing = [
55 ...new Set([...assigned.map((issue) => issue.repoId), ...pulls.map(({ pull }) => pull.repoId)]),
56 ].filter((id) => !known.has(id));
57 const looked = await Promise.all(missing.map((id) => reposApi.getById(id, viewer)));
58 for (const found of looked) if (found.ok) known.set(found.value.id, found.value);
59 times.lookups = Date.now() - started;
60 const serverTiming = Object.entries(times)
61 .map(([name, ms]) => `${name};dur=${ms}`)
62 .join(", ");
63 return data({
Initial g1t: services, event bus, intents and attempts64 viewer,
65 repos,
Agents as a team: lifecycle, merge queue, billing and a new shell66 assigned: assigned.flatMap((issue) => {
67 const repo = known.get(issue.repoId);
68 return repo ? [{ issue, repo }] : [];
Initial g1t: services, event bus, intents and attempts69 }),
Agents as a team: lifecycle, merge queue, billing and a new shell70 active: pulls.flatMap((item) => {
71 const repo = known.get(item.pull.repoId);
72 return repo ? [{ ...item, repo }] : [];
73 }),
74 }, { headers: { "Server-Timing": serverTiming } });
75}
76
77type Active = { pull: Pull; lifecycle: Lifecycle | null; repo: Repo };
78
79/** What a pull request is waiting on, in a word or two. */
80function standing({ pull, lifecycle }: Active): string {
81 if (lifecycle) return STAGE_LABEL[lifecycle.stage];
82 return pull.status === "draft" ? "In progress" : "Ready for review";
83}
84
85function PullRows({ items, detail }: { items: Active[]; detail?: boolean }) {
86 return (
87 <ul className="divide-y divide-line overflow-hidden rounded-xl border border-line bg-surface">
88 {items.map((item) => {
89 const { pull, lifecycle, repo } = item;
90 return (
91 <li key={pull.id}>
92 <Link
93 prefetch="intent"
94 to={`/${repo.namespace}/${repo.name}/pull/${pull.number}`}
95 className="flex items-center gap-3 px-4 py-3 transition-colors hover:bg-raised"
96 >
97 <PullIcon status={pull.status} />
98 <span className="min-w-0 grow">
99 <span className="block truncate font-medium">{pull.title}</span>
100 <span className="block truncate font-mono text-xs text-muted">
101 {repo.namespace}/{repo.name}#{pull.number}
102 {pull.issue != null && ` · for #${pull.issue}`} · {pull.agent}
103 </span>
104 {detail && lifecycle && (
105 <span className="mt-1 block text-xs text-muted">{lifecycle.detail}</span>
106 )}
107 </span>
108 {lifecycle && (
109 <span className="hidden shrink-0 sm:block">
110 <StageDots stage={lifecycle.stage} />
111 </span>
112 )}
113 <span className="w-28 shrink-0 text-right text-xs text-muted">
114 {standing(item)}
115 </span>
116 <span className="hidden w-14 shrink-0 text-right text-xs text-faint sm:block">
117 <TimeAgo at={pull.updatedAt} />
118 </span>
119 </Link>
120 </li>
121 );
122 })}
123 </ul>
124 );
Initial g1t: services, event bus, intents and attempts125}
126
Agents as a team: lifecycle, merge queue, billing and a new shell127function Section({ title, children }: { title: ReactNode; children: ReactNode }) {
128 return (
129 <section>
130 <h2 className="flex items-center gap-2 text-sm font-medium text-muted">{title}</h2>
131 <div className="mt-3">{children}</div>
132 </section>
133 );
134}
135
136function plural(count: number, one: string, many: string): string {
137 return `${count} ${count === 1 ? one : many}`;
138}
139
Initial g1t: services, event bus, intents and attempts140export default function Home({ loaderData }: Route.ComponentProps) {
Agents as a team: lifecycle, merge queue, billing and a new shell141 const { viewer, repos, active, assigned } = loaderData;
142
143 const needsYou = active.filter((item) => item.lifecycle?.stage === "needs_you");
144 const ready = active.filter((item) => item.lifecycle?.stage === "ready");
145 const moving = active.filter(
146 (item) => item.lifecycle?.stage !== "needs_you" && item.lifecycle?.stage !== "ready",
147 );
148 // Agents are at work, so the page changes without anyone touching it.
149 const changing = moving.some((item) => item.lifecycle || item.pull.status === "draft");
150 const revalidator = useRevalidator();
151 useEffect(() => {
152 if (!changing) return;
153 const timer = setInterval(() => {
154 if (document.visibilityState === "visible") revalidator.revalidate();
155 }, REFRESH_MS);
156 return () => clearInterval(timer);
157 }, [changing, revalidator]);
158
API and MCP server, Rust identity service, registration, site redesign159 if (!viewer) return <Landing repos={repos} />;
Initial g1t: services, event bus, intents and attempts160
Agents as a team: lifecycle, merge queue, billing and a new shell161 const summary = [
162 moving.length > 0 && `${moving.length} in progress`,
163 ready.length > 0 && `${ready.length} ready to merge`,
164 needsYou.length > 0 && plural(needsYou.length, "needs you", "need you"),
165 assigned.length > 0 && `${assigned.length} assigned to you`,
166 ].filter(Boolean);
API and MCP server, Rust identity service, registration, site redesign167 return (
168 <main className="mx-auto grid max-w-6xl gap-10 px-4 py-10 lg:grid-cols-[1fr_20rem]">
169 <div className="min-w-0 space-y-10">
Initial g1t: services, event bus, intents and attempts170 <section>
API and MCP server, Rust identity service, registration, site redesign171 <div className="flex items-center gap-3">
172 <Avatar name={viewer.username} size={36} />
173 <div>
174 <h1 className="text-xl font-semibold tracking-tight">
175 Mission control
176 </h1>
177 <p className="text-sm text-muted">
Agents as a team: lifecycle, merge queue, billing and a new shell178 {summary.length === 0
Issues and pull requests replace intents and attempts179 ? "Nothing is being worked on right now."
Agents as a team: lifecycle, merge queue, billing and a new shell180 : `${summary.join(" · ")}.`}
API and MCP server, Rust identity service, registration, site redesign181 </p>
182 </div>
183 </div>
184 </section>
185
Agents as a team: lifecycle, merge queue, billing and a new shell186 {needsYou.length > 0 && (
187 <Section
188 title={
189 <>
190 <Hand size={14} className="text-warn" />
191 Needs you
192 </>
193 }
194 >
195 <PullRows items={needsYou} detail />
196 </Section>
197 )}
198
199 {ready.length > 0 && (
200 <Section title="Ready to merge">
201 <PullRows items={ready} />
202 </Section>
203 )}
204
205 {assigned.length > 0 && (
206 <Section title="Assigned to you">
207 <ul className="divide-y divide-line overflow-hidden rounded-xl border border-line bg-surface">
208 {assigned.map(({ issue, repo }) => (
209 <li key={issue.id}>
210 <Link
211 prefetch="intent"
212 to={`/${repo.namespace}/${repo.name}/issues/${issue.number}`}
213 className="flex items-center gap-3 px-4 py-3 transition-colors hover:bg-raised"
214 >
215 <IssueIcon issue={issue} />
216 <span className="min-w-0 grow">
217 <span className="block truncate font-medium">{issue.title}</span>
218 <span className="block truncate font-mono text-xs text-muted">
219 {repo.namespace}/{repo.name}#{issue.number}
Initial g1t: services, event bus, intents and attempts220 </span>
Agents as a team: lifecycle, merge queue, billing and a new shell221 </span>
222 {issue.agent && <Assignee agent={issue.agent} />}
223 <span className="hidden w-14 shrink-0 text-right text-xs text-faint sm:block">
224 <TimeAgo at={issue.updatedAt} />
225 </span>
226 </Link>
227 </li>
228 ))}
229 </ul>
230 </Section>
231 )}
232
233 <Section title="In progress">
234 {moving.length === 0 ? (
235 <EmptyState title="No pull requests in progress">
236 Assign issues to the g1t agent from a repository's Issues tab,
237 or point your own agent at one.
238 </EmptyState>
239 ) : (
240 <PullRows items={moving} />
241 )}
242 </Section>
API and MCP server, Rust identity service, registration, site redesign243
Initial g1t: services, event bus, intents and attempts244 <section>
API and MCP server, Rust identity service, registration, site redesign245 <div className="flex items-center justify-between">
246 <h2 className="text-sm font-medium text-muted">Your repositories</h2>
247 <ButtonLink to="/new" variant="quiet">
248 <Plus size={14} />
249 New
250 </ButtonLink>
251 </div>
Initial g1t: services, event bus, intents and attempts252 <RepoList repos={repos} />
253 </section>
API and MCP server, Rust identity service, registration, site redesign254 </div>
Initial g1t: services, event bus, intents and attempts255
API and MCP server, Rust identity service, registration, site redesign256 <aside className="space-y-4">
257 <div className="rounded-xl border border-line bg-surface p-5">
258 <h2 className="font-medium">Connect an agent</h2>
259 <p className="mt-1.5 text-sm text-muted">
OAuth 2.1 sign-in for MCP clients and other applications260 Add g1t to Claude Code, then run /mcp in it to sign in through
261 your browser.
API and MCP server, Rust identity service, registration, site redesign262 </p>
263 <div className="mt-4 space-y-2">
264 <CopyLine
265 prompt
OAuth 2.1 sign-in for MCP clients and other applications266 text="claude mcp add --transport http g1t https://mcp.g1t.sh"
API and MCP server, Rust identity service, registration, site redesign267 />
268 </div>
269 <Link
Docs as their own app; shared theme package270 to="https://docs.g1t.sh/guides/bring-your-own-agent/"
API and MCP server, Rust identity service, registration, site redesign271 className="mt-4 inline-flex items-center gap-1 text-sm text-accent hover:underline"
272 >
273 How it works <ArrowRight size={13} />
274 </Link>
275 </div>
276 <div className="rounded-xl border border-line bg-surface p-5">
277 <h2 className="font-medium">Explore</h2>
278 <p className="mt-1.5 text-sm text-muted">
Issues and pull requests replace intents and attempts279 Browse public repositories and the issues open on them.
API and MCP server, Rust identity service, registration, site redesign280 </p>
281 <Link
282 to="/explore"
283 className="mt-3 inline-flex items-center gap-1 text-sm text-accent hover:underline"
284 >
285 Public repositories <ArrowRight size={13} />
286 </Link>
287 </div>
288 </aside>
Initial g1t: services, event bus, intents and attempts289 </main>
290 );
291}