flagon-io/g1t

public

Git for AI scale: a forge for thousands of agents working on the same code at once.

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

260 lines9,757 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.

API and MCP server, Rust identity service, registration, site redesign1import { identity } from "../lib/services.server";
Agents as a team: lifecycle, merge queue, billing and a new shell2import { Form, Link } from "react-router";
Initial g1t: services, event bus, intents and attempts3
4import type { Route } from "./+types/settings";
Social cards for every page: og.g1t.sh5import { page } from "../lib/meta";
Workspace names and icons, and a component kit for every control6import { AvatarField } from "../components/avatar-field";
OAuth 2.1 sign-in for MCP clients and other applications7import { Button, ErrorText, Field, Input, TimeAgo } from "../components/ui";
Workspace names and icons, and a component kit for every control8import { readAvatarUpload } from "../lib/avatar-upload";
Initial g1t: services, event bus, intents and attempts9import { assertSameOrigin, requireUser } from "../lib/session.server";
Agents and memory, checks and conflicts, profiles, slug renames, custom domains10import { ProfileSection } from "../components/profile-form";
Initial g1t: services, event bus, intents and attempts11
Social cards for every page: og.g1t.sh12export function meta(args: Route.MetaArgs) {
13 return page(args, { title: "Settings · g1t" });
Initial g1t: services, event bus, intents and attempts14}
15
16export async function loader({ request, context }: Route.LoaderArgs) {
17 const user = requireUser(context, request);
Agents and memory, checks and conflicts, profiles, slug renames, custom domains18 const [keys, tokens, applications, profile] = await Promise.all([
API and MCP server, Rust identity service, registration, site redesign19 identity.listSshKeys(user),
20 identity.listAccessTokens(user),
OAuth 2.1 sign-in for MCP clients and other applications21 identity.listOAuthGrants(user),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains22 identity.profile(user.username),
Initial g1t: services, event bus, intents and attempts23 ]);
Agents and memory, checks and conflicts, profiles, slug renames, custom domains24 return { user, keys, tokens, applications, profile };
Initial g1t: services, event bus, intents and attempts25}
26
27export async function action({ request, context }: Route.ActionArgs) {
28 assertSameOrigin(request);
29 const user = requireUser(context, request);
30 const form = await request.formData();
31 const id = String(form.get("id") ?? "");
32
33 switch (form.get("intent")) {
Agents and memory, checks and conflicts, profiles, slug renames, custom domains34 // Identity checks every field again, and the website most of all.
35 case "profile": {
36 const text = (name: string) => String(form.get(name) ?? "");
37 const result = await identity.updateProfile(user, {
38 name: text("name"),
39 bio: text("bio"),
40 location: text("location"),
41 website: text("website"),
42 pronouns: text("pronouns"),
43 });
44 return result.ok ? { profileSaved: true } : { profileError: result.error.message };
45 }
Workspace names and icons, and a component kit for every control46 // The picture: identity checks the image's bytes again.
47 case "avatar": {
48 const upload = await readAvatarUpload(form);
49 if ("error" in upload) return { avatarError: upload.error };
50 const result = await identity.setUserAvatar(user, upload.image);
51 return result.ok ? null : { avatarError: result.error.message };
52 }
53 case "remove-avatar": {
54 const result = await identity.setUserAvatar(user, null);
55 return result.ok ? null : { avatarError: result.error.message };
56 }
Initial g1t: services, event bus, intents and attempts57 case "add-key": {
API and MCP server, Rust identity service, registration, site redesign58 const result = await identity.addSshKey(
Initial g1t: services, event bus, intents and attempts59 user,
60 String(form.get("title") ?? ""),
61 String(form.get("key") ?? ""),
62 );
63 return result.ok ? null : { keyError: result.error.message };
64 }
65 case "delete-key":
API and MCP server, Rust identity service, registration, site redesign66 await identity.removeSshKey(user, id);
Initial g1t: services, event bus, intents and attempts67 return null;
68 case "add-token": {
API and MCP server, Rust identity service, registration, site redesign69 const created = await identity.createAccessToken(
Initial g1t: services, event bus, intents and attempts70 user,
Agents as a team: lifecycle, merge queue, billing and a new shell71 String(form.get("label") ?? ""),
Initial g1t: services, event bus, intents and attempts72 );
73 return { newToken: created.token };
74 }
75 case "delete-token":
API and MCP server, Rust identity service, registration, site redesign76 await identity.removeAccessToken(user, id);
Initial g1t: services, event bus, intents and attempts77 return null;
OAuth 2.1 sign-in for MCP clients and other applications78 case "sign-out-application":
79 await identity.revokeOAuthGrant(user, id);
80 return null;
Initial g1t: services, event bus, intents and attempts81 }
82 return null;
83}
84
OAuth 2.1 sign-in for MCP clients and other applications85function DeleteButton({
86 intent,
87 id,
88 label = "Delete",
89}: {
90 intent: string;
91 id: string;
92 label?: string;
93}) {
Initial g1t: services, event bus, intents and attempts94 return (
95 <Form method="post" className="ml-auto">
96 <input type="hidden" name="intent" value={intent} />
97 <input type="hidden" name="id" value={id} />
98 <Button variant="quiet" type="submit">
OAuth 2.1 sign-in for MCP clients and other applications99 {label}
Initial g1t: services, event bus, intents and attempts100 </Button>
101 </Form>
102 );
103}
104
105export default function Settings({
106 loaderData,
107 actionData,
108}: Route.ComponentProps) {
Agents and memory, checks and conflicts, profiles, slug renames, custom domains109 const { user, keys, tokens, applications, profile } = loaderData;
Initial g1t: services, event bus, intents and attempts110 return (
Account settings in the same shape as a workspace's111 <main className="mx-auto max-w-5xl px-4 py-10 sm:px-8">
112 <header className="mb-8 border-b border-line pb-6">
113 <h1 className="text-2xl font-semibold tracking-tight">Account</h1>
114 <p className="mt-1.5 text-sm text-muted">
115 How <span className="font-mono text-fg">{user.username}</span> signs in from git, tools and agents.
116 </p>
117 </header>
118 <div className="max-w-2xl space-y-12">
Workspace names and icons, and a component kit for every control119 <section id="picture" className="scroll-mt-20">
120 <h2 className="font-medium">Picture</h2>
121 <div className="mt-4">
122 <AvatarField
123 name={user.username}
124 image={user.avatar}
125 error={actionData && "avatarError" in actionData ? actionData.avatarError : undefined}
126 about="Shown beside your username in the sidebar and on mission control. Without one, g1t draws your first letter."
127 />
128 </div>
129 </section>
Agents and memory, checks and conflicts, profiles, slug renames, custom domains130 <ProfileSection
131 username={user.username}
132 profile={profile}
133 error={actionData && "profileError" in actionData ? actionData.profileError : undefined}
134 saved={Boolean(actionData && "profileSaved" in actionData)}
135 />
Account settings in the same shape as a workspace's136 <section id="ssh-keys" className="scroll-mt-20">
Initial g1t: services, event bus, intents and attempts137 <h2 className="font-medium">SSH keys</h2>
138 <ul className="mt-4 divide-y divide-line rounded-md border border-line empty:hidden">
139 {keys.map((key) => (
140 <li key={key.id} className="flex items-center gap-4 px-4 py-3">
141 <div className="min-w-0">
142 <p className="text-sm">{key.title}</p>
143 <p className="truncate font-mono text-xs text-muted">
144 {key.fingerprint}
145 </p>
146 </div>
147 <DeleteButton intent="delete-key" id={key.id} />
148 </li>
149 ))}
150 </ul>
151 <Form method="post" className="mt-4 space-y-3">
152 <input type="hidden" name="intent" value="add-key" />
153 <Field label="Title (optional)">
154 <Input name="title" maxLength={100} />
155 </Field>
156 <Field label="Public key">
157 <Input name="key" placeholder="ssh-ed25519 AAAA…" required />
158 </Field>
159 <ErrorText>{actionData?.keyError}</ErrorText>
160 <Button type="submit">Add SSH key</Button>
161 </Form>
162 </section>
163
Account settings in the same shape as a workspace's164 <section id="tokens" className="scroll-mt-20">
Initial g1t: services, event bus, intents and attempts165 <h2 className="font-medium">Access tokens</h2>
166 <p className="mt-1 text-sm text-muted">
167 Use a token as the password when git asks for one over HTTPS, and to
Agents as a team: lifecycle, merge queue, billing and a new shell168 authenticate agents and the API. A token here acts as you.
Initial g1t: services, event bus, intents and attempts169 </p>
Agents as a team: lifecycle, merge queue, billing and a new shell170 {(user.workspaces ?? []).length > 0 && (
171 <p className="mt-1 text-sm text-muted">
172 For CI and integrations that work for a team, use a workspace's
173 own tokens instead:{" "}
174 {(user.workspaces ?? []).map((membership, i) => (
175 <span key={membership.slug}>
176 {i > 0 && ", "}
177 <Link
178 to={`/${membership.slug}/-/tokens`}
179 className="font-mono text-fg underline underline-offset-4"
180 >
181 {membership.slug}
182 </Link>
183 </span>
184 ))}
185 .
186 </p>
187 )}
Initial g1t: services, event bus, intents and attempts188 {actionData?.newToken && (
189 <div className="mt-4 rounded-md border border-accent/40 bg-surface p-4">
190 <p className="text-sm">Copy it now. It will not be shown again.</p>
191 <pre className="mt-2 overflow-x-auto font-mono text-sm text-accent">
192 {actionData.newToken}
193 </pre>
194 </div>
195 )}
196 <ul className="mt-4 divide-y divide-line rounded-md border border-line empty:hidden">
197 {tokens.map((token) => (
198 <li key={token.id} className="flex items-center gap-4 px-4 py-3">
Agents as a team: lifecycle, merge queue, billing and a new shell199 <div className="min-w-0">
200 <p className="truncate text-sm">{token.name}</p>
201 <p className="text-xs text-faint">
202 Created <TimeAgo at={token.createdAt} /> ·{" "}
203 {token.lastUsedAt ? (
204 <>
205 last used <TimeAgo at={token.lastUsedAt} />
206 </>
207 ) : (
208 "never used"
209 )}
210 </p>
211 </div>
Initial g1t: services, event bus, intents and attempts212 <DeleteButton intent="delete-token" id={token.id} />
213 </li>
214 ))}
215 </ul>
216 <Form method="post" className="mt-4 flex items-end gap-3">
217 <input type="hidden" name="intent" value="add-token" />
218 <div className="grow">
219 <Field label="Name">
Agents as a team: lifecycle, merge queue, billing and a new shell220 <Input name="label" maxLength={100} placeholder="laptop" />
Initial g1t: services, event bus, intents and attempts221 </Field>
222 </div>
223 <Button type="submit">Create token</Button>
224 </Form>
225 </section>
OAuth 2.1 sign-in for MCP clients and other applications226
Account settings in the same shape as a workspace's227 <section id="applications" className="scroll-mt-20">
OAuth 2.1 sign-in for MCP clients and other applications228 <h2 className="font-medium">Connected applications</h2>
229 <p className="mt-1 text-sm text-muted">
230 Applications you signed in to through your browser, such as an agent
231 connected to the g1t MCP server. Signing one out ends its access at
232 once.
233 </p>
234 {applications.length === 0 ? (
235 <p className="mt-4 text-sm text-faint">None yet.</p>
236 ) : (
237 <ul className="mt-4 divide-y divide-line rounded-md border border-line">
238 {applications.map((application) => (
239 <li key={application.id} className="flex items-center gap-4 px-4 py-3">
240 <div>
241 <p className="text-sm">{application.clientName}</p>
242 <p className="text-xs text-faint">
243 Connected <TimeAgo at={application.createdAt} /> · last used{" "}
244 <TimeAgo at={application.lastUsedAt} />
245 </p>
246 </div>
247 <DeleteButton
248 intent="sign-out-application"
249 id={application.id}
250 label="Sign out"
251 />
252 </li>
253 ))}
254 </ul>
255 )}
256 </section>
Account settings in the same shape as a workspace's257 </div>
Initial g1t: services, event bus, intents and attempts258 </main>
259 );
260}