Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh
95 linesCodeBlameRaw
1import { env } from "cloudflare:workers";
2import { useEffect } from "react";
3import { data, redirect, useNavigate } from "react-router";
4
5import type { Route } from "./+types/home";
6import { DESKTOP_COOKIE, DESKTOP_COOKIE_SECONDS, DESKTOP_QUERY, DESKTOP_VALUE, fromDesktopApp, launchedFromDesktopApp, rememberDesktopApp } from "../lib/desktop";
7import { cookie } from "../lib/github";
8import { desktopBridge } from "../lib/notify-client";
9import { page } from "../lib/meta";
10import { application, organization } from "../lib/structured-data";
11import { WORKSPACE_COOKIE, chosenWorkspace } from "../lib/workspace-choice";
12import { readCookie } from "../lib/mission";
13import { type NotStarted, chosenRepo, delegateForm, issuePath, notStarted } from "../lib/delegate";
14import { Landing } from "../components/landing";
15import { repos as reposApi } from "../lib/services.server";
16import { assertSameOrigin, getViewer, requireUser } from "../lib/session.server";
17import { homePath } from "../lib/workspace-nav";
18
19export function meta(args: Route.MetaArgs) {
20 return [
21 ...page(args, {
22 title: "g1t · Your team and its agents, working in one place",
23 description:
24 "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.",
25 }),
26 organization(),
27 application(20),
28 ];
29}
30
31/**
32 * g1t.sh: the landing page for a visitor. Someone signed in has a Home in
33 * their workspace (routes/workspace/home.tsx); Mission control's code
34 * panels are Code's Overview. `/?agent=new` and the rest of the query go
35 * with them.
36 *
37 * The desktop app is for using g1t, so a visitor there goes to the sign-in
38 * page instead (lib/desktop.ts). Its launcher opens `/?app=desktop`, which
39 * sets the cookie that says so.
40 */
41export async function loader({ context, request }: Route.LoaderArgs) {
42 const viewer = getViewer(context);
43 const url = new URL(request.url);
44 const launched = launchedFromDesktopApp(url);
45 const headers = new Headers();
46 if (launched) {
47 headers.set("set-cookie", cookie(DESKTOP_COOKIE, DESKTOP_VALUE, DESKTOP_COOKIE_SECONDS));
48 url.searchParams.delete(DESKTOP_QUERY);
49 }
50 if (!viewer) {
51 if (launched || fromDesktopApp(request)) throw redirect("/login", { headers });
52 return data({ signedIn: false as const });
53 }
54 const slug = chosenWorkspace(viewer.workspaces ?? [], readCookie(request.headers.get("cookie"), WORKSPACE_COOKIE))?.slug ?? null;
55 // A workspace-less account is sent to make one by the session (lib/session.server.ts).
56 if (!slug) throw redirect("/workspaces/new", { headers });
57 throw redirect(homePath(slug, url.search), { headers });
58}
59
60/** What the composer said back, when the issue opened but the agent did not start, or nothing opened. */
61export type DelegateResult = { error: string; notStarted: null } | { error: null; notStarted: NotStarted };
62
63/**
64 * "Put an agent on it": opens an issue in one of the viewer's projects and
65 * puts g1t on it, then lands on the issue. When the agent could not
66 * start, the issue is still open, and the composer says why and where to
67 * fix it.
68 */
69export async function action({ request, context }: Route.ActionArgs) {
70 assertSameOrigin(request);
71 const user = requireUser(context, request);
72 const form = await request.formData();
73 if (form.get("intent") !== "delegate") return data<DelegateResult>({ error: "Nothing to do.", notStarted: null }, { status: 400 });
74 const projects = await reposApi.list(user, { memberOnly: true }).catch(() => []);
75 const repo = chosenRepo(form.get("repo"), projects.map((repo) => ({ namespace: repo.namespace, name: repo.name })));
76 if (!repo) return data<DelegateResult>({ error: "Choose one of your projects.", notStarted: null }, { status: 400 });
77 const result = await env.RUNNER.delegate(user, repo, delegateForm(form));
78 if (!result.ok) return data<DelegateResult>({ error: result.error.message, notStarted: null }, { status: 400 });
79 const { issue, agent } = result.value;
80 const refused = notStarted(agent, repo, issue.number);
81 if (!refused) throw redirect(issuePath(repo, issue.number));
82 return data<DelegateResult>({ error: null, notStarted: refused });
83}
84
85export default function Home() {
86 const navigate = useNavigate();
87 // A build of the desktop app older than `?app=desktop` opens `/` as it
88 // is: the page sets the cookie itself and moves on to signing in.
89 useEffect(() => {
90 if (!desktopBridge()) return;
91 rememberDesktopApp();
92 navigate("/login", { replace: true });
93 }, [navigate]);
94 return <Landing />;
95}