Skip to content
66 linesCodeBlameRaw
1import { env } from "cloudflare:workers";
2import { data, redirect } from "react-router";
3
4import type { Route } from "./+types/home";
5import { page } from "../lib/meta";
6import { WORKSPACE_COOKIE, chosenWorkspace } from "../lib/workspace-choice";
7import { readCookie } from "../lib/mission";
8import { type NotStarted, chosenRepo, delegateForm, issuePath, notStarted } from "../lib/delegate";
9import { Landing } from "../components/landing";
10import { repos as reposApi } from "../lib/services.server";
11import { assertSameOrigin, getViewer, requireUser } from "../lib/session.server";
12import { homePath } from "../lib/workspace-nav";
13
14export function meta(args: Route.MetaArgs) {
15 return page(args, {
16 title: "g1t · Your team and its agents, working in one place",
17 description:
18 "Chat with your team and your agents in channels and DMs. Agents are teammates with a role, a personality and a budget, and their code lands through pull requests, checks and a merge queue. Chat is free on every plan, never per seat.",
19 });
20}
21
22/**
23 * g1t.sh: the landing page for a visitor. Someone signed in has a Home in
24 * their workspace (routes/workspace/home.tsx); Mission control's code
25 * panels are Code's Overview. `/?agent=new` and the rest of the query go
26 * with them.
27 */
28export async function loader({ context, request }: Route.LoaderArgs) {
29 const viewer = getViewer(context);
30 if (!viewer) return data({ signedIn: false as const });
31 const url = new URL(request.url);
32 const slug = chosenWorkspace(viewer.workspaces ?? [], readCookie(request.headers.get("cookie"), WORKSPACE_COOKIE))?.slug ?? null;
33 // A workspace-less account is sent to make one by the session (lib/session.server.ts).
34 if (!slug) throw redirect("/workspaces/new");
35 throw redirect(homePath(slug, url.search));
36}
37
38/** What the composer said back, when the issue opened but the agent did not start, or nothing opened. */
39export type DelegateResult = { error: string; notStarted: null } | { error: null; notStarted: NotStarted };
40
41/**
42 * "Put an agent on it": opens an issue in one of the viewer's projects and
43 * puts g1t on it, then lands on the issue. When the agent could not
44 * start, the issue is still open, and the composer says why and where to
45 * fix it.
46 */
47export async function action({ request, context }: Route.ActionArgs) {
48 assertSameOrigin(request);
49 const user = requireUser(context, request);
50 const form = await request.formData();
51 if (form.get("intent") !== "delegate") return data<DelegateResult>({ error: "Nothing to do.", notStarted: null }, { status: 400 });
52 const projects = await reposApi.list(user, { memberOnly: true }).catch(() => []);
53 const repo = chosenRepo(form.get("repo"), projects.map((repo) => ({ namespace: repo.namespace, name: repo.name })));
54 if (!repo) return data<DelegateResult>({ error: "Choose one of your projects.", notStarted: null }, { status: 400 });
55 const result = await env.RUNNER.delegate(user, repo, delegateForm(form));
56 if (!result.ok) return data<DelegateResult>({ error: result.error.message, notStarted: null }, { status: 400 });
57 const { issue, agent } = result.value;
58 const refused = notStarted(agent, repo, issue.number);
59 if (!refused) throw redirect(issuePath(repo, issue.number));
60 return data<DelegateResult>({ error: null, notStarted: refused });
61}
62
63
64export default function Home() {
65 return <Landing />;
66}