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