flagon-io/g1t

public

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

Polish: phones, copy boxes, the plan page, the landing page, a real glide #5

Mergedsyntaqx merged immaculate into main
10 files+155−420/10 viewed
+2−3
341341 // Outside systems reporting to a connection. They sign what they send
342342 // with the connection's own secret, which is not a g1t token, so this
343343 // comes before anything that would read one.
344− if method == "POST" && !on_mcp {
345− if let Some(id) = path.strip_prefix("/hooks/").filter(|id| !id.is_empty() && !id.contains('/')) {
344+ if method == "POST" && !on_mcp
345+ && let Some(id) = path.strip_prefix("/hooks/").filter(|id| !id.is_empty() && !id.contains('/')) {
346346 return receive_hook(&mut request, &services, id).await;
347347 }
348− }
349348
350349 let viewer = match authenticate(&request, &services).await? {
351350 Ok(viewer) => viewer,
+14−0
6262 outline: 2px solid var(--color-accent);
6363 outline-offset: 2px;
6464 }
65+
66+ /*
67+ * A grid's items may shrink below their content. Without this, one long
68+ * line that does not wrap (a command, a commit hash, an address) widens
69+ * the whole column, and on a phone the page scrolls sideways.
70+ */
71+ .grid > * {
72+ min-width: 0;
73+ }
74+
75+ /* Checkboxes, radios and ranges in g1t's own colour, not the browser's. */
76+ :root {
77+ accent-color: var(--color-accent);
78+ }
6579 }
6680
6781 @media (prefers-reduced-motion: reduce) {
+79−1
22 import type { ReactNode } from "react";
33 import { Link } from "react-router";
44
5−import type { Repo } from "@g1t/contracts";
5+import { type Provider, PROVIDERS, type Repo } from "@g1t/contracts";
66
77 import { ConvergeArt, PlanArt, QueueArt, TeamArt, WhyArt } from "./art";
8+import { ProviderMark } from "./model-providers";
89 import { RepoList } from "./repo-list";
910 import { ButtonLink, CopyLine } from "./ui";
1011
7980 "REST API",
8081 "MCP server",
8182 "OAuth sign-in",
83+ "Any model provider",
84+ "Sentry, Jira, Linear",
8285 ];
8386
8487 const COMPARISON: [string, string, string][] = [
9194 ["You in the loop", "Reading every diff", "Asking for changes, approving, steering; the agents do the rest"],
9295 ];
9396
97+/** The systems g1t connects to, shown on the landing page. */
98+const STACK: Provider[] = [
99+ "anthropic",
100+ "openai",
101+ "gemini",
102+ "xai",
103+ "mistral",
104+ "deepseek",
105+ "azure_openai",
106+ "openrouter",
107+ "groq",
108+ "sentry",
109+ "jira",
110+ "linear",
111+];
112+
113+/** What happens to a production error once Sentry is connected. */
114+const LOOP = [
115+ "Sentry: TypeError in checkout",
116+ "g1t opens an issue, with the stack trace",
117+ "An agent fixes it; another reviews it",
118+ "The merge queue lands it on main",
119+ "Sentry marks the error resolved",
120+];
121+
94122 const PLATFORM = ["Workers", "Artifacts", "Containers", "D1", "Queues", "AI Gateway", "Rust"];
95123
96124 export function Landing({ repos }: { repos: Repo[] }) {
231259 </div>
232260 </section>
233261
262+ {/* Your stack */}
263+ <section className="mx-auto grid max-w-6xl items-center gap-12 px-4 pb-24 lg:grid-cols-2">
264+ <div className="lg:order-2">
265+ <Eyebrow>Your stack</Eyebrow>
266+ <h2 className="mt-3 text-3xl font-semibold tracking-tight text-balance">
267+ Your models. Your alerts. Your tickets.
268+ </h2>
269+ <p className="mt-4 max-w-md leading-7 text-muted">
270+ Run g1t's agents on g1t's models, or on your own accounts with any of the labs and
271+ platforms, and choose which model does which work. Keys stay with g1t: an agent's
272+ sandbox only ever holds a token for its own run.
273+ </p>
274+ <p className="mt-4 max-w-md leading-7 text-muted">
275+ Connect Sentry and a new error becomes an issue, an agent fixes it, and Sentry hears
276+ it was resolved, before anyone was paged. Mention TECH-1234 and the agent reads the
277+ Jira ticket.
278+ </p>
279+ <div className="mt-6">
280+ <ButtonLink to="https://docs.g1t.sh/guides/integrations/" variant="quiet">
281+ See the integrations
282+ <ArrowRight size={14} />
283+ </ButtonLink>
284+ </div>
285+ </div>
286+ <div className="space-y-6 lg:order-1">
287+ <div className="grid grid-cols-3 gap-2 sm:grid-cols-4">
288+ {STACK.map((provider) => (
289+ <div
290+ key={provider}
291+ className="flex flex-col items-center gap-2 rounded-xl bg-surface px-2 py-3 ring-1 ring-line"
292+ title={PROVIDERS[provider].label}
293+ >
294+ <ProviderMark provider={provider} size={30} />
295+ <span className="w-full text-center text-xs leading-tight text-muted">{PROVIDERS[provider].label}</span>
296+ </div>
297+ ))}
298+ </div>
299+ <ol className="space-y-2 font-mono text-[0.8125rem]">
300+ {LOOP.map((step, index) => (
301+ <li key={step} className="flex items-center gap-3 text-fg-soft">
302+ <span className="flex size-6 shrink-0 items-center justify-center rounded-full bg-surface text-xs text-faint ring-1 ring-line">
303+ {index + 1}
304+ </span>
305+ {step}
306+ </li>
307+ ))}
308+ </ol>
309+ </div>
310+ </section>
311+
234312 {/* Comparison */}
235313 <section className="mx-auto max-w-6xl px-4 pb-24">
236314 <Eyebrow>What changes</Eyebrow>
+26−14
244244 /** A workspace's settings pages, which the sidebar slides over to. */
245245 const SETTINGS_PAGE = /^\/([^/]+)\/-\/(settings|people|tokens|billing|integrations)(\/|$)/;
246246
247−/** How the sidebar's two menus slide past each other. */
248−const SLIDE = "absolute inset-0 overflow-y-auto px-2 pb-4 transition-[transform,opacity] duration-300 ease-[cubic-bezier(0.32,0.72,0,1)] motion-reduce:transition-none";
247+/**
248+ * The sidebar's menus sit side by side on one track, and the track slides:
249+ * the main menu leaves to the left as the settings arrive from the right,
250+ * together, with a long ease-out, the way a phone pushes a screen.
251+ */
252+const TRACK = "flex h-full w-[200%] transition-transform duration-[550ms] ease-[cubic-bezier(0.45,0,0.15,1)] will-change-transform motion-reduce:transition-none";
253+/** One menu on the track. */
254+const PANEL = "h-full w-1/2 shrink-0 overflow-y-auto px-2 pb-4";
249255
250256 /** A workspace's settings, as the sidebar shows them in place of everything else. */
251257 function SettingsMenu({ slug, owner, open }: { slug: string; owner: boolean; open: boolean }) {
253259 <nav
254260 aria-label="Workspace settings"
255261 inert={!open}
256− className={`${SLIDE} ${open ? "translate-x-0 opacity-100" : "pointer-events-none translate-x-full opacity-0"}`}
262+ className={PANEL}
257263 >
258264 <Link
259265 to={`/${slug}`}
303309 <nav
304310 aria-label="Your settings"
305311 inert={!open}
306− className={`${SLIDE} ${open ? "translate-x-0 opacity-100" : "pointer-events-none translate-x-full opacity-0"}`}
312+ className={PANEL}
307313 >
308314 <Link
309315 to="/"
330336 const inSettings = ws != null && SETTINGS_PAGE.exec(going ?? pathname)?.[1]?.toLowerCase() === ws.slug;
331337 const inAccount = (going ?? pathname) === "/settings";
332338 const away = inSettings || inAccount;
339+ // Which settings sit on the far side of the track. Kept while sliding back,
340+ // so they do not vanish on the way out.
341+ const side = useRef<"workspace" | "account">("workspace");
342+ if (inAccount) side.current = "account";
343+ else if (inSettings) side.current = "workspace";
333344 const active = shell.repo;
334345 const repoBase = active ? `/${active.namespace}/${active.name}` : null;
335346 // The repository being looked at is listed even when it is someone else's.
360371 <kbd className="rounded bg-raised px-1.5 font-mono text-[0.625rem] text-muted ring-1 ring-line">⌘K</kbd>
361372 </button>
362373 </div>
363− <div className="relative min-h-0 grow overflow-hidden">
364− <nav
365− aria-label="g1t"
366− inert={away}
367− className={`${SLIDE} ${away ? "pointer-events-none -translate-x-1/3 opacity-0" : "translate-x-0 opacity-100"}`}
368− >
374+ <div className="min-h-0 grow overflow-hidden">
375+ <div className={`${TRACK} ${away ? "-translate-x-1/2" : "translate-x-0"}`}>
376+ <nav aria-label="g1t" inert={away} className={PANEL}>
369377 <div className="mt-3 space-y-px">
370378 <SidebarLink to="/" end icon={<LayoutDashboard size={15} />}>
371379 Mission control
475483 })}
476484 </SidebarGroup>
477485 </nav>
478− {ws && <SettingsMenu slug={ws.slug} owner={ws.role === "owner"} open={inSettings} />}
479− <AccountSettingsMenu open={inAccount} />
486+ {side.current === "account" || !ws ? (
487+ <AccountSettingsMenu open={inAccount} />
488+ ) : (
489+ <SettingsMenu slug={ws.slug} owner={ws.role === "owner"} open={inSettings} />
490+ )}
491+ </div>
480492 </div>
481493
482494 <div className="space-y-2 p-2">
805817 </div>
806818 )}
807819
808− <div className="flex min-h-screen flex-col lg:pl-64">
820+ <div className="flex min-h-screen min-w-0 flex-col lg:pl-64">
809821 <header className="sticky top-0 z-30 flex h-12 items-center gap-2 border-b border-line bg-bg/85 px-3 backdrop-blur sm:px-4">
810822 <button
811823 type="button"
865877 </div>
866878 </header>
867879 {banner}
868− <main className="grow">{children}</main>
880+ <main className="min-w-0 grow">{children}</main>
869881 </div>
870882 <CommandPalette open={palette} onClose={() => setPalette(false)} commands={commands} />
871883 </div>
+13−2
210210 const [copied, setCopied] = useState(false);
211211 return (
212212 <div className="group flex items-center gap-3 rounded-lg border border-line bg-surface py-2 pr-2 pl-3.5 font-mono text-[0.8125rem]">
213− <code className="min-w-0 grow overflow-x-auto whitespace-nowrap">
213+ {/* Wraps rather than hides: a command or an address is no use half seen. */}
214+ <code className="min-w-0 grow whitespace-pre-wrap [overflow-wrap:anywhere]">
214215 {prompt && <span className="mr-2 text-faint select-none">$</span>}
215− {text}
216+ {/* Short words with hyphens, such as `--transport`, stay whole when
217+ the line wraps; long ones, such as addresses, may still break. */}
218+ {text.split(/(\s+)/).map((part, index) =>
219+ part.includes("-") && part.length <= 24 ? (
220+ <span key={index} className="whitespace-nowrap">
221+ {part}
222+ </span>
223+ ) : (
224+ part
225+ ),
226+ )}
216227 </code>
217228 <button
218229 type="button"
+3−1
252252 ["Explore repositories", "/explore"],
253253 ["g1t agents", "https://docs.g1t.sh/guides/g1t-agents/"],
254254 ["Bring your own agent", "https://docs.g1t.sh/guides/bring-your-own-agent/"],
255+ ["Integrations", "https://docs.g1t.sh/guides/integrations/"],
256+ ["Model providers", "https://docs.g1t.sh/guides/models/"],
255257 ["Sign up", "/register"],
256258 ],
257259 },
259261 title: "Developers",
260262 links: [
261263 ["Quickstart", "https://docs.g1t.sh/quickstart/"],
262− ["Concepts", "https://docs.g1t.sh/concepts/overview/"],
264+ ["How g1t works", "https://docs.g1t.sh/concepts/overview/"],
263265 ["API reference", "https://docs.g1t.sh/api/reference/"],
264266 ["OpenAPI", "https://api.g1t.sh/openapi.json"],
265267 ["llms.txt", "/llms.txt"],
+7−4
112112 </Link>
113113 </p>
114114 <h2 className="mt-2 text-xl font-semibold tracking-tight">The outcome</h2>
115− <div className="mt-3 rounded-xl border border-line bg-surface p-5 text-sm whitespace-pre-wrap">
116− {plan.brief}
115+ <div className="mt-3 rounded-xl border border-line bg-surface p-5 text-sm">
116+ <Markdown source={plan.brief} repo={{ namespace: params.owner, name: params.repo }} />
117117 </div>
118118 <p className="mt-2 text-xs text-faint">
119119 Asked for by {plan.author.username} <TimeAgo at={plan.createdAt} />
278278 </span>
279279 </div>
280280 )}
281− {plan.status === "applied" && (
281+ {plan.status === "applied" && !moving && (
282+ <p className="mt-6 text-sm text-muted">Everything in this plan has landed.</p>
283+ )}
284+ {plan.status === "applied" && moving && (
282285 <p className="mt-6 text-sm text-muted">
283− These are open. Follow them under{" "}
286+ Some are still open. Follow them under{" "}
284287 <Link to={`${base}/issues`} className="text-fg underline underline-offset-4">
285288 Issues
286289 </Link>{" "}
+2−3
9595 let (mut from_ssh, mut to_ssh) = tokio::io::split(theirs);
9696 tokio::spawn(async move {
9797 while let Some(Ok(message)) = source.next().await {
98− if let Message::Binary(bytes) = message {
99− if to_ssh.write_all(&bytes).await.is_err() {
98+ if let Message::Binary(bytes) = message
99+ && to_ssh.write_all(&bytes).await.is_err() {
100100 break;
101101 }
102− }
103102 }
104103 let _ = to_ssh.shutdown().await;
105104 });
+5−8
7676 let diff = TextDiff::from_lines(&before, &after);
7777 let mut map = vec![None; new.len()];
7878 for change in diff.iter_all_changes() {
79− if change.tag() == ChangeTag::Equal {
80− if let (Some(o), Some(n)) = (change.old_index(), change.new_index()) {
81− if let Some(slot) = map.get_mut(n) {
79+ if change.tag() == ChangeTag::Equal
80+ && let (Some(o), Some(n)) = (change.old_index(), change.new_index())
81+ && let Some(slot) = map.get_mut(n) {
8282 *slot = Some(o);
8383 }
84− }
85− }
8684 }
8785 map
8886 }
189187 }
190188 let mut commits: Vec<Commit> = Vec::new();
191189 for range in &ranges {
192− if !commits.iter().any(|commit| commit.hash == range.commit) {
193− if let Some(commit) = by_hash.get(range.commit.as_str()) {
190+ if !commits.iter().any(|commit| commit.hash == range.commit)
191+ && let Some(commit) = by_hash.get(range.commit.as_str()) {
194192 commits.push((*commit).clone());
195193 }
196− }
197194 }
198195 Ok(Some(Blame {
199196 head: first.hash.clone(),
+4−6
235235 }
236236
237237 async fn read_tree(&self, tree_hash: &str) -> Result<Option<Vec<TreeEntry>>> {
238− if let Some(bytes) = self.cached("tree", tree_hash).await {
239− if let Ok(entries) = serde_json::from_slice::<Vec<TreeEntry>>(&bytes) {
238+ if let Some(bytes) = self.cached("tree", tree_hash).await
239+ && let Ok(entries) = serde_json::from_slice::<Vec<TreeEntry>>(&bytes) {
240240 return Ok(Some(entries));
241241 }
242− }
243242 let entries: Option<Vec<RawEntry>> =
244243 js::from_js(&js::call(&self.handle, "readTree", &[tree_hash.into()]).await?)?;
245244 let entries: Option<Vec<TreeEntry>> = entries.map(|entries| {
252251 })
253252 .collect()
254253 });
255− if let Some(entries) = &entries {
256− if let Ok(bytes) = serde_json::to_vec(entries) {
254+ if let Some(entries) = &entries
255+ && let Ok(bytes) = serde_json::to_vec(entries) {
257256 self.keep("tree", tree_hash, bytes).await;
258257 }
259− }
260258 Ok(entries)
261259 }
262260