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

220 lines9,268 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.

Workspace names and icons, and a component kit for every control1import { Box, Download, GitBranch, Globe, Lock, Sparkles } from "lucide-react";
Projects: what a workspace builds and runs, first on every page2import { useState } from "react";
Merge branch 'worktree-agent-a985c01f43538af01'3import { Form, redirect, useNavigate } from "react-router";
Initial g1t: services, event bus, intents and attempts4
5import type { Route } from "./+types/new";
Social cards for every page: og.g1t.sh6import { page } from "../lib/meta";
Merge branch 'worktree-agent-a985c01f43538af01'7import { Avatar, ErrorText, SubmitButton } from "../components/ui";
Workspace names and icons, and a component kit for every control8import { Field, FieldDescription, FieldLabel, FieldLegend, FieldSet } from "../components/ui/field";
9import { Input, InputAddon, InputGroup } from "../components/ui/input";
10import { RadioCard, RadioGroup } from "../components/ui/radio-group";
11import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue } from "../components/ui/select";
Rust repos service with shipping; pull requests kept in the model12import { repos } from "../lib/services.server";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look13import { GithubMark } from "../components/github";
14import { githubApp } from "../lib/github.server";
Initial g1t: services, event bus, intents and attempts15import { assertSameOrigin, requireUser } from "../lib/session.server";
16
Social cards for every page: og.g1t.sh17export function meta(args: Route.MetaArgs) {
18 return page(args, { title: "New project · g1t" });
Initial g1t: services, event bus, intents and attempts19}
20
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look21export async function loader({ request, context }: Route.LoaderArgs) {
Workspaces own repositories22 const user = requireUser(context, request);
Workspace names and icons, and a component kit for every control23 const workspaces = (user.workspaces ?? []).map((membership) => ({
24 slug: membership.slug,
25 role: membership.role,
26 name: membership.name ?? membership.slug,
27 avatar: membership.avatar ?? null,
28 }));
Projects: what a workspace builds and runs, first on every page29 // Projects live in a workspace, so there has to be one first.
Workspaces own repositories30 if (workspaces.length === 0) throw redirect("/workspaces/new");
31 const asked = new URL(request.url).searchParams.get("workspace");
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look32 const selected = asked && workspaces.some((workspace) => workspace.slug === asked) ? asked : workspaces[0].slug;
33 // With g1t's GitHub App configured, repositories can come from GitHub.
34 const github = await githubApp.status(user, selected).catch(() => null);
35 return { workspaces, selected, github: Boolean(github?.ok && github.value.configured) };
Initial g1t: services, event bus, intents and attempts36}
37
38export async function action({ request, context }: Route.ActionArgs) {
39 assertSameOrigin(request);
40 const user = requireUser(context, request);
41 const form = await request.formData();
Projects: what a workspace builds and runs, first on every page42 // A project hosted on g1t: its repository is made with it, and the
43 // project takes the repository's name.
Rust repos service with shipping; pull requests kept in the model44 const result = await repos.create(user, {
Workspaces own repositories45 namespace: String(form.get("workspace") ?? ""),
Initial g1t: services, event bus, intents and attempts46 name: String(form.get("name") ?? ""),
47 description: String(form.get("description") ?? ""),
48 isPrivate: form.get("visibility") === "private",
Projects: what a workspace builds and runs, first on every page49 importUrl: form.get("source") === "import" ? String(form.get("importUrl") ?? "").trim() || undefined : undefined,
Initial g1t: services, event bus, intents and attempts50 });
51 if (!result.ok) return { error: result.error.message };
52 throw redirect(`/${result.value.namespace}/${result.value.name}`);
53}
54
Projects: what a workspace builds and runs, first on every page55type Source = "empty" | "import";
56
57const SOURCES: { id: Source | "mirror"; title: string; text: string; icon: React.ReactNode; soon?: boolean }[] = [
58 {
59 id: "empty",
60 title: "Start empty",
61 text: "A new repository on g1t. Push to it, or hand an agent the first issue.",
Workspace names and icons, and a component kit for every control62 icon: <Sparkles />,
Projects: what a workspace builds and runs, first on every page63 },
64 {
65 id: "import",
66 title: "Import code",
67 text: "Copy a public repository from GitHub or any git host into a new one on g1t.",
Workspace names and icons, and a component kit for every control68 icon: <Download />,
Projects: what a workspace builds and runs, first on every page69 },
70 {
71 id: "mirror",
72 title: "Mirror GitHub, GitLab or Bitbucket",
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent73 text: "Keep the code where it is and work on it with g1t. Deployments are yours to turn on.",
Workspace names and icons, and a component kit for every control74 icon: <GitBranch />,
Projects: what a workspace builds and runs, first on every page75 soon: true,
76 },
77];
78
Workspace names and icons, and a component kit for every control79const ROLE_LABELS: Record<string, string> = { owner: "Owner", admin: "Admin", member: "Member" };
80
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look81/** Replaces the mirror card when g1t's GitHub App is configured. */
82const GITHUB_SOURCE = {
83 id: "github" as const,
84 title: "Import from GitHub",
85 text: "Import, mirror or move repositories you can reach on GitHub, private ones too.",
86 icon: <GithubMark />,
87 soon: false,
88};
89
Projects: what a workspace builds and runs, first on every page90export default function NewProject({ loaderData, actionData }: Route.ComponentProps) {
91 const [source, setSource] = useState<Source>("empty");
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look92 const navigate = useNavigate();
93 const sources = loaderData.github ? SOURCES.map((option) => (option.id === "mirror" ? GITHUB_SOURCE : option)) : SOURCES;
Workspace names and icons, and a component kit for every control94 const [workspace, setWorkspace] = useState(loaderData.selected);
95 const [name, setName] = useState("");
Initial g1t: services, event bus, intents and attempts96 return (
Projects: what a workspace builds and runs, first on every page97 <main className="mx-auto max-w-2xl px-4 py-12">
98 <span className="flex size-10 items-center justify-center rounded-xl bg-accent/10 text-accent ring-1 ring-accent/30">
99 <Box size={18} />
100 </span>
101 <h1 className="mt-4 text-2xl font-semibold tracking-tight">New project</h1>
102 <p className="mt-1.5 text-sm text-muted">
103 A project is what you build and run. Its code lives in its source; its deployments, environments, secrets and
104 variables belong to the project.
105 </p>
106
107 <Form method="post" className="mt-8 space-y-8">
Workspace names and icons, and a component kit for every control108 <FieldSet>
109 <FieldLegend>Where its code comes from</FieldLegend>
110 <RadioGroup
111 name="source"
112 value={source}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look113 onValueChange={(value) =>
114 value === "github" ? navigate(`/new/github?workspace=${workspace}`) : setSource(value as Source)
115 }
Workspace names and icons, and a component kit for every control116 aria-label="Where its code comes from"
117 className="gap-3 sm:grid-cols-3"
118 >
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look119 {sources.map((option) => (
Workspace names and icons, and a component kit for every control120 <RadioCard
121 key={option.id}
122 value={option.id}
123 title={option.title}
124 description={option.text}
125 icon={option.icon}
126 disabled={option.soon}
127 badge={option.soon ? "Soon" : undefined}
128 />
129 ))}
130 </RadioGroup>
131 </FieldSet>
Projects: what a workspace builds and runs, first on every page132
133 {source === "import" && (
Workspace names and icons, and a component kit for every control134 <Field className="animate-fade-in">
135 <FieldLabel htmlFor="importUrl">Repository to import</FieldLabel>
136 <Input id="importUrl" name="importUrl" type="url" required placeholder="https://github.com/owner/repo" />
137 <FieldDescription>The address of a public repository. Its default branch is copied, up to 40 MB.</FieldDescription>
Projects: what a workspace builds and runs, first on every page138 </Field>
139 )}
140
Workspace names and icons, and a component kit for every control141 <Field>
142 <FieldLabel htmlFor="name">Name</FieldLabel>
143 <InputGroup className="h-10">
144 <Select name="workspace" value={workspace} onValueChange={setWorkspace}>
145 <SelectTrigger
146 aria-label="Workspace"
147 className="h-full w-auto max-w-[55%] shrink-0 rounded-none border-0 bg-surface px-2.5 font-medium hover:bg-raised focus-visible:ring-0 data-[state=open]:bg-raised data-[state=open]:ring-0 sm:max-w-[45%]"
148 >
149 <SelectValue />
150 </SelectTrigger>
151 <SelectContent align="start" className="min-w-64">
152 <SelectGroup>
153 <SelectLabel>Workspaces</SelectLabel>
154 {loaderData.workspaces.map((option) => (
155 <SelectItem
156 key={option.slug}
157 value={option.slug}
158 icon={<Avatar name={option.name} image={option.avatar} size={18} square />}
159 description={`g1t.sh/${option.slug} · ${ROLE_LABELS[option.role] ?? option.role}`}
160 >
161 {option.name}
162 </SelectItem>
163 ))}
164 </SelectGroup>
165 </SelectContent>
166 </Select>
167 <InputAddon className="border-l border-line px-2 font-mono text-base text-faint">/</InputAddon>
168 <Input
169 id="name"
170 name="name"
171 required
172 autoFocus
173 maxLength={100}
174 placeholder="my-app"
175 value={name}
176 onChange={(event) => setName(event.target.value)}
177 className="pl-1 font-mono"
178 />
179 </InputGroup>
180 <FieldDescription>
181 The project and its repository share it:{" "}
182 <span className="font-mono text-muted">
183 g1t.sh/{workspace}/<span className={name ? "text-fg" : undefined}>{name || "name"}</span>
184 </span>
185 </FieldDescription>
Initial g1t: services, event bus, intents and attempts186 </Field>
Workspace names and icons, and a component kit for every control187
188 <Field>
189 <FieldLabel htmlFor="description">
190 Description <span className="font-normal text-faint">(optional)</span>
191 </FieldLabel>
192 <Input id="description" name="description" maxLength={200} placeholder="What it is, in a line" />
Agents as a team: lifecycle, merge queue, billing and a new shell193 </Field>
Workspace names and icons, and a component kit for every control194
195 <FieldSet>
196 <FieldLegend>Who can see it</FieldLegend>
197 <RadioGroup name="visibility" defaultValue="public" aria-label="Who can see it" className="gap-3 sm:grid-cols-2">
198 <RadioCard
199 value="public"
200 icon={<Globe />}
201 title="Public"
202 description="Anyone can see its code and open issues."
203 />
204 <RadioCard
205 value="private"
206 icon={<Lock />}
207 title="Private"
208 description="Only members of the workspace."
209 />
210 </RadioGroup>
211 </FieldSet>
212
Initial g1t: services, event bus, intents and attempts213 <ErrorText>{actionData?.error}</ErrorText>
Merge branch 'worktree-agent-a985c01f43538af01'214 <SubmitButton variant="accent" pending={source === "import" ? "Importing…" : "Creating…"}>
Projects: what a workspace builds and runs, first on every page215 Create project
Merge branch 'worktree-agent-a985c01f43538af01'216 </SubmitButton>
Initial g1t: services, event bus, intents and attempts217 </Form>
218 </main>
219 );
220}

This file's history is long; its oldest lines are credited to the oldest commit read.