flagon-io/g1t

public

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/apps/web/app/routes/home.tsx

1,200 lines48,177 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.

Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1import {
2 Activity as ActivityIcon,
3 ArrowRight,
4 ArrowUpRight,
5 Bot,
6 Box,
7 CircleDot,
8 Coins,
9 GitPullRequest,
10 Hand,
11 Lock,
12 Plus,
13 Radio,
14 Settings,
15 Sparkles,
16 Users,
17} from "lucide-react";
New workspaces are told to connect a model, before anything fails18import { env } from "cloudflare:workers";
Mission control drops the composer for now19import { type ReactNode, useEffect, useState } from "react";
20import { Link, data, useRouteLoaderData } from "react-router";
Agents as a team: lifecycle, merge queue, billing and a new shell21
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API22import {
23 type AgentRun,
24 type AuthoredItem,
25 type CheckStatus,
26 type Deployment,
27 type G1tEvent,
28 type Issue,
29 type Lifecycle,
30 type LiveApp,
31 type Pull,
32 type PullStatus,
33 type Repo,
34 type RepoPath,
35 type Stage,
36 isActiveRun,
37} from "@g1t/contracts";
Initial g1t: services, event bus, intents and attempts38
39import type { Route } from "./+types/home";
Social cards for every page: og.g1t.sh40import { page } from "../lib/meta";
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API41import { CHANGELOG, changelogHref } from "../lib/changelog";
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put42import { WORKSPACE_COOKIE, chosenWorkspace } from "../lib/workspace-choice";
43import { trialClosed } from "../lib/trial";
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API44import {
45 type ActivityItem,
46 type Need,
47 SEEN_COOKIE,
48 TIME,
49 agentHours,
50 dailyBuckets,
51 digestParts,
52 eventItem,
53 firstPassRate,
54 formatSpan,
55 greetingFor,
56 groupActivity,
57 hourIn,
58 issueToMerge,
59 median,
60 nextSeen,
61 rankNeeds,
62 readCookie,
63 stuckMinutes,
64} from "../lib/mission";
API and MCP server, Rust identity service, registration, site redesign65import { Landing } from "../components/landing";
Get started on mission control66import type { ShellData } from "../components/shell";
Agents as a team: lifecycle, merge queue, billing and a new shell67import { STAGE_LABEL, StageDots } from "../components/lifecycle";
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API68import { RunCard, formatCost, useLiveRefresh } from "../components/agents";
69import { CheckBadge } from "../components/checks";
70import { DEPLOY_STATUS, StatusDot, host } from "../components/deploy";
71import { ActivityFeed, Meter, NeedsList, Panel, PulseTile, Quiet, Unavailable, percent } from "../components/mission";
72import { Avatar, CopyLine, TimeAgo } from "../components/ui";
73import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../components/ui/select";
74import { Tabs, TabsContent, TabsList, TabsTrigger } from "../components/ui/tabs";
75import { IssueIcon, PullIcon } from "../components/work";
API and MCP server, Rust identity service, registration, site redesign76import {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API77 agents,
78 billing,
79 deployments,
80 events as eventLog,
81 identity,
82 projects as projectsApi,
83 repos as reposApi,
84 work,
85} from "../lib/services.server";
Mission control drops the composer for now86import { getViewer } from "../lib/session.server";
Initial g1t: services, event bus, intents and attempts87
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API88/** The viewer's time zone, set by the page itself, so the greeting fits their day. */
89const TZ_COOKIE = "g1t_tz";
90/** The most projects whose pull requests and events are read for the page. */
91const MAX_PROJECTS = 10;
92const EVENTS_PER_PROJECT = 80;
Agents as a team: lifecycle, merge queue, billing and a new shell93
A free allowance on g1t's models, so anyone can try its agents94const dollars = (micros: number) => `$${(Math.max(0, micros) / 1_000_000).toFixed(2)}`;
95
Social cards for every page: og.g1t.sh96export function meta(args: Route.MetaArgs) {
97 return page(args, {
98 title: "g1t — Git for AI scale",
99 description:
100 "A git forge for thousands of agents working on the same code at once: every change isolated, every decision recorded, every change landed in order. Open source, built on Cloudflare.",
101 });
Initial g1t: services, event bus, intents and attempts102}
103
Agents as a team: lifecycle, merge queue, billing and a new shell104export function headers({ loaderHeaders }: Route.HeadersArgs) {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API105 const out = new Headers();
106 const timing = loaderHeaders.get("Server-Timing");
107 if (timing) out.set("Server-Timing", timing);
108 const cookie = loaderHeaders.get("Set-Cookie");
109 if (cookie) out.set("Set-Cookie", cookie);
110 return out;
Agents as a team: lifecycle, merge queue, billing and a new shell111}
112
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API113/** A row of the viewer's pull requests, whichever list it comes from. */
114type PullRow = {
115 key: string;
116 repo: RepoPath;
117 number: number;
118 title: string;
119 status: PullStatus;
120 agent: string | null;
121 stage: Stage | null;
122 detail: string | null;
123 checkStatus: CheckStatus | null;
124 updatedAt: string;
125};
126
127/** A row of the viewer's issues. */
128type IssueRow = {
129 key: string;
130 repo: RepoPath;
131 number: number;
132 title: string;
133 agent: string | null;
134 queued: boolean;
135 updatedAt: string;
136};
137
138type ProjectHealth = {
139 slug: string;
140 name: string;
141 private: boolean;
142 repo: RepoPath | null;
143 deploys: boolean;
144 production: LiveApp | null;
145 latest: Deployment | null;
146 openPulls: number | null;
147 agents: number;
148 passRate: number | null;
149 checks: number;
150};
151
152const pullRow = (pull: Pull, repo: RepoPath, lifecycle: Lifecycle | null): PullRow => ({
153 key: pull.id,
154 repo,
155 number: pull.number,
156 title: pull.title,
157 status: pull.status,
158 agent: pull.agent,
159 stage: lifecycle?.stage ?? null,
160 detail: lifecycle?.detail ?? null,
161 checkStatus: pull.checkStatus,
162 updatedAt: pull.updatedAt,
163});
164
165const authoredPull = (item: AuthoredItem): PullRow => ({
166 key: `${item.repo.namespace}/${item.repo.name}#${item.number}`,
167 repo: item.repo,
168 number: item.number,
169 title: item.title,
170 status: item.status ?? (item.draft ? "draft" : "open"),
171 agent: null,
172 stage: null,
173 detail: null,
174 checkStatus: null,
175 updatedAt: item.updatedAt,
176});
177
178export async function loader({ context, request }: Route.LoaderArgs) {
Initial g1t: services, event bus, intents and attempts179 const viewer = getViewer(context);
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API180 if (!viewer) {
181 const repos = await reposApi.list(viewer, { memberOnly: false });
182 return data({ signedIn: false as const, repos });
183 }
184
185 const now = Date.now();
186 const weekAgo = now - 7 * TIME.DAY;
Agents as a team: lifecycle, merge queue, billing and a new shell187 const started = Date.now();
188 const times: Record<string, number> = {};
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API189 // Each call is timed, and one that fails leaves its section empty rather
190 // than taking the page down.
191 const soft = <T,>(name: string, promise: Promise<T>): Promise<T | null> =>
192 promise.then(
193 (value) => ((times[name] = Date.now() - started), value),
194 (error) => {
195 console.warn(`home: ${name} failed`, error);
196 times[name] = Date.now() - started;
197 return null;
198 },
199 );
200 const okOr = <T,>(result: { ok: true; value: T } | { ok: false } | null): T | null => (result?.ok ? result.value : null);
201
202 const cookies = request.headers.get("cookie");
203 const seen = nextSeen(readCookie(cookies, SEEN_COOKIE), now);
204 const tz = readCookie(cookies, TZ_COOKIE);
205 const memberships = viewer.workspaces ?? [];
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put206 // The workspace you chose, as the sidebar shows it (lib/workspace-choice.ts).
207 const slug = chosenWorkspace(memberships, readCookie(cookies, WORKSPACE_COOKIE))?.slug ?? null;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API208 const mine = new Set(memberships.map((m) => m.slug.toLowerCase()));
209 const username = viewer.username;
210
211 const reposP = soft("repos", reposApi.list(viewer, { memberOnly: true }));
212 // The workspace's projects' open pull requests and recent events, read
213 // once each, all at once, as soon as the projects are known.
214 const perRepoP = reposP.then((repos) =>
215 Promise.all(
216 (repos ?? [])
217 .filter((repo) => mine.has(repo.namespace.toLowerCase()))
218 .slice(0, MAX_PROJECTS)
219 .map(async (repo) => {
220 const path = { namespace: repo.namespace, name: repo.name };
221 const [pulls, log] = await Promise.all([
222 work.listPulls(path, viewer, "open").catch(() => null),
223 eventLog.list({ repoId: repo.id, limit: EVENTS_PER_PROJECT }).catch(() => null),
224 ]);
225 return { repo, pulls: pulls?.ok ? pulls.value.slice(0, 60) : null, events: log };
226 }),
227 ),
228 );
229
230 const [repos, perRepo, active, assigned, models, profile, runs, overview, usage, authoredPulls, authoredIssues, projectList, memories] =
231 await Promise.all([
232 reposP,
233 soft("projects", perRepoP),
234 soft("pulls", work.listActivePulls(viewer)),
235 soft("assigned", work.listAssignedIssues(viewer)),
236 slug ? soft("models", env.RUNNER.modelAccess(slug)) : null,
237 soft("profile", identity.profile(username)),
238 slug ? soft("runs", agents.listRuns(viewer, { workspace: slug, limit: 150 })) : null,
239 slug ? soft("deploys", deployments.overview(slug, viewer)) : null,
240 slug ? soft("usage", billing.usage(slug, viewer, new Date(weekAgo - TIME.DAY).toISOString())) : null,
241 soft("authoredPulls", work.byAuthor(username, viewer, { kind: "pull", state: "open", sort: "updated", limit: 10 })),
242 soft("authoredIssues", work.byAuthor(username, viewer, { kind: "issue", state: "open", sort: "updated", limit: 10 })),
243 slug ? soft("projectList", projectsApi.list(slug, viewer)) : null,
244 slug ? soft("memories", agents.listMemories(viewer, slug, null)) : null,
245 ]);
246
247 const repoList = repos ?? [];
Agents as a team: lifecycle, merge queue, billing and a new shell248 // Each issue and pull request is shown under its repository. Most are in
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API249 // the viewer's own, already listed; the rest are looked up once each.
250 const known = new Map<string, Repo>(repoList.map((repo) => [repo.id, repo]));
Agents as a team: lifecycle, merge queue, billing and a new shell251 const missing = [
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API252 ...new Set([...(assigned ?? []).map((issue) => issue.repoId), ...(active ?? []).map(({ pull }) => pull.repoId)]),
Agents as a team: lifecycle, merge queue, billing and a new shell253 ].filter((id) => !known.has(id));
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API254 const looked = await Promise.all(missing.map((id) => reposApi.getById(id, viewer).catch(() => null)));
255 for (const found of looked) if (found?.ok) known.set(found.value.id, found.value);
256 const pathOf = (repo: Repo): RepoPath => ({ namespace: repo.namespace, name: repo.name });
257
258 const activeList = (active ?? []).flatMap((item) => {
259 const repo = known.get(item.pull.repoId);
260 return repo ? [{ ...item, repo }] : [];
261 });
262 const runList = okOr(runs ?? null) ?? [];
263 const liveRuns = runList.filter((run) => isActiveRun(run.status));
264 const overviewList = okOr(overview ?? null);
265 const projectsOk = okOr(projectList ?? null);
266
267 // --- What the viewer's pull requests and issues are ----------------------
268 const lower = username.toLowerCase();
269 const openPulls = (perRepo ?? []).flatMap(({ repo, pulls }) => (pulls ?? []).map((pull) => ({ pull, repo })));
270 const reviewRequested = openPulls.filter(
271 ({ pull }) =>
272 pull.status === "open" &&
273 pull.author.username.toLowerCase() !== lower &&
274 pull.reviewers.some((name) => name.toLowerCase() === lower),
275 );
276 const assignedPulls = openPulls.filter(({ pull }) => pull.assignees.some((name) => name.toLowerCase() === lower));
277 const authoredRows =
278 activeList.length > 0
279 ? activeList.map(({ pull, lifecycle, repo }) => pullRow(pull, pathOf(repo), lifecycle))
280 : (okOr(authoredPulls)?.items ?? []).map(authoredPull);
281 const pullsTabs = {
282 authored: authoredRows.slice(0, 8),
283 review: reviewRequested.slice(0, 8).map(({ pull, repo }) => pullRow(pull, pathOf(repo), null)),
284 assigned: assignedPulls.slice(0, 8).map(({ pull, repo }) => pullRow(pull, pathOf(repo), null)),
285 };
286 const issueRow = (issue: Issue, repo: RepoPath): IssueRow => ({
287 key: issue.id,
288 repo,
289 number: issue.number,
290 title: issue.title,
291 agent: issue.agent,
292 queued: issue.queued,
293 updatedAt: issue.updatedAt,
294 });
295 const issuesTabs = {
296 assigned: (assigned ?? []).flatMap((issue) => {
297 const repo = known.get(issue.repoId);
298 return repo ? [issueRow(issue, pathOf(repo))] : [];
299 }).slice(0, 8),
300 authored: (okOr(authoredIssues)?.items ?? []).slice(0, 8).map((item) => ({
301 key: `${item.repo.namespace}/${item.repo.name}#${item.number}`,
302 repo: item.repo,
303 number: item.number,
304 title: item.title,
305 agent: null,
306 queued: false,
307 updatedAt: item.updatedAt,
308 })),
309 };
310
311 // --- Needs you ------------------------------------------------------------
312 const needs: Need[] = [];
313 for (const entry of overviewList ?? []) {
314 const latest = entry.latest;
315 if (latest?.kind === "production" && latest.status === "failed" && slug) {
316 needs.push({
317 key: `deploy:${entry.slug}`,
318 kind: "deploy",
319 title: `Production build of ${entry.slug} failed`,
320 detail: latest.error ?? "The last build of the default branch failed. Production still serves the build before it.",
321 to: `/${slug}/${entry.slug}/deployments/${latest.id}`,
322 action: "See the build",
323 at: Date.parse(latest.finishedAt ?? latest.createdAt),
324 where: `${slug}/${entry.slug}`,
325 });
326 }
327 }
328 for (const { pull, lifecycle, repo } of activeList) {
329 const where = `${repo.namespace}/${repo.name}#${pull.number}`;
330 const to = `/${repo.namespace}/${repo.name}/pull/${pull.number}`;
331 if (lifecycle?.stage === "needs_you") {
332 const conflict = /conflict/i.test(lifecycle.detail);
333 needs.push({
334 key: `pull:${pull.id}`,
335 kind: conflict ? "conflict" : "stalled",
336 title: pull.title,
337 detail: lifecycle.detail,
338 to,
339 action: conflict ? "Resolve" : "Decide",
340 at: Date.parse(pull.updatedAt),
341 where,
342 });
343 } else if (lifecycle?.stage === "ready") {
344 needs.push({ key: `pull:${pull.id}`, kind: "ready", title: pull.title, detail: "Checks passed and it was approved. It lands when you merge it.", to, action: "Merge", at: Date.parse(pull.updatedAt), where });
345 } else if (!lifecycle && pull.status === "open" && pull.checkStatus === "failed") {
346 needs.push({ key: `pull:${pull.id}`, kind: "checks", title: pull.title, detail: "Its acceptance checks failed on the latest push.", to, action: "See checks", at: Date.parse(pull.updatedAt), where });
347 }
348 }
349 for (const { pull, repo } of reviewRequested) {
350 needs.push({
351 key: `review:${pull.id}`,
352 kind: "review",
353 title: pull.title,
354 detail: `${pull.author.username} asked for your review.`,
355 to: `/${repo.namespace}/${repo.name}/pull/${pull.number}`,
356 action: "Review",
357 at: Date.parse(pull.updatedAt),
358 where: `${repo.namespace}/${repo.name}#${pull.number}`,
359 });
360 }
361 let quietest: number | null = null;
362 for (const run of liveRuns) {
363 const minutes = stuckMinutes(run, now);
364 if (minutes == null) continue;
365 quietest = Math.max(quietest ?? 0, minutes);
366 needs.push({
367 key: `run:${run.id}`,
368 kind: "stuck",
369 title: run.title ?? `${run.agent}'s run`,
370 detail: `${run.agent} has reported nothing for ${minutes} min${run.step ? `. Last: ${run.step}` : ""}.`,
371 to: `/${run.repo.namespace}/${run.repo.name}/agents/runs/${run.id}`,
372 action: "Look",
373 at: Date.parse(run.updatedAt),
374 where: `${run.repo.namespace}/${run.repo.name}${run.number != null ? `#${run.number}` : ""}`,
375 });
376 }
377
378 // --- Activity -------------------------------------------------------------
379 const items: ActivityItem[] = [];
380 const allEvents: { repo: string; event: G1tEvent }[] = [];
381 for (const { repo, events } of perRepo ?? []) {
382 for (const event of events ?? []) {
383 allEvents.push({ repo: repo.id, event });
384 const item = eventItem(event, pathOf(repo));
385 if (item) items.push(item);
386 }
387 }
388 for (const entry of overviewList ?? []) {
389 const latest = entry.latest;
390 const served = latest?.status === "ready" || latest?.status === "replaced";
391 if (!slug || latest?.kind !== "production" || (!served && latest.status !== "failed")) continue;
392 items.push({
393 id: `deploy:${latest.id}`,
394 at: Date.parse(latest.finishedAt ?? latest.createdAt),
395 repo: { namespace: slug, name: entry.slug },
396 actor: latest.createdBy,
397 verb: served ? "deployed" : "deploy_failed",
398 number: null,
399 to: `/${slug}/${entry.slug}/deployments/${latest.id}`,
400 });
401 }
402 for (const memory of okOr(memories ?? null)?.workspace ?? []) {
403 const repo = memory.repo ?? memory.source.repo;
404 if (!repo || !slug) continue;
405 items.push({
406 id: `memory:${memory.id}`,
407 at: Date.parse(memory.createdAt),
408 repo,
409 actor: memory.createdBy,
410 verb: "learned",
411 number: null,
412 text: memory.text.length > 90 ? `${memory.text.slice(0, 88)}…` : memory.text,
413 to: `/${slug}/-/memory`,
414 });
415 }
416 const groups = groupActivity(items).slice(0, 60);
417
418 // --- Since you were last here --------------------------------------------
419 const since = seen.since ?? now - TIME.DAY;
420 const after = (verb: ActivityItem["verb"]) => items.filter((item) => item.verb === verb && item.at > since).length;
421 const digest = digestParts({
422 landed: after("landed"),
423 reviews: reviewRequested.length,
424 opened: after("opened_issue"),
425 deploys: after("deployed"),
426 failedDeploys: after("deploy_failed"),
427 stuck: quietest,
428 });
429
430 // --- Pulse ----------------------------------------------------------------
431 const week = allEvents.filter(({ event }) => Date.parse(event.time) >= weekAgo);
432 const mergedWeek = week.filter(({ event }) => event.type === "pull.merged");
433 const spans = issueToMerge(
434 allEvents.flatMap(({ repo, event }) => (event.type === "issue.opened" ? [{ repo, number: event.data.number, at: Date.parse(event.time) }] : [])),
435 mergedWeek.flatMap(({ repo, event }) => (event.type === "pull.merged" ? [{ repo, issue: event.data.issue ?? null, at: Date.parse(event.time) }] : [])),
436 );
437 const checkEvents = allEvents.flatMap(({ repo, event }) =>
438 event.type === "checks.completed" ? [{ repo, number: event.data.number, at: Date.parse(event.time), passed: event.data.status === "passed" }] : [],
439 );
440 const firstPass = firstPassRate(checkEvents.filter((c) => c.at >= weekAgo));
441 const usageOk = okOr(usage ?? null);
442 const costPoints = usageOk
443 ? usageOk.byDay.map((slice) => ({ at: Date.parse(`${slice.key.split("/")[0]}T12:00:00Z`), value: slice.micros / 1_000_000 }))
444 : runList.filter((run) => run.costUsd != null).map((run) => ({ at: Date.parse(run.createdAt), value: run.costUsd ?? 0 }));
445 const costDays = dailyBuckets(costPoints, 7, now);
446 const pulse = {
447 merged: mergedWeek.length,
448 mergedDays: dailyBuckets(mergedWeek.map(({ event }) => ({ at: Date.parse(event.time) })), 7, now),
449 hours: agentHours(runList, weekAgo, now),
450 hoursDays: dailyBuckets(
451 runList
452 .filter((run) => run.startedAt)
453 .map((run) => ({ at: Date.parse(run.startedAt!), value: agentHours([run], weekAgo, now) })),
454 7,
455 now,
456 ),
457 cost: costDays.reduce((sum, v) => sum + v, 0),
458 costDays,
459 issueToMerge: median(spans),
460 mergedWithIssue: spans.length,
461 firstPass: firstPass.rate,
462 firstPassOf: firstPass.of,
463 };
464
465 // --- Projects -------------------------------------------------------------
466 const byRepoName = new Map((perRepo ?? []).map((entry) => [`${entry.repo.namespace}/${entry.repo.name}`.toLowerCase(), entry]));
467 const projectsHealth: ProjectHealth[] = (projectsOk ?? []).slice(0, 12).map((project) => {
468 const repo = project.source.kind === "hosted" ? project.source.repo : null;
469 const key = repo ? `${repo.namespace}/${repo.name}`.toLowerCase() : "";
470 const entry = byRepoName.get(key);
471 const deploy = overviewList?.find((d) => d.slug === project.slug) ?? null;
472 const checks = (entry?.events ?? []).flatMap((event) => (event.type === "checks.completed" ? [event.data.status === "passed"] : []));
473 return {
474 slug: project.slug,
475 name: project.name,
476 private: project.private,
477 repo,
478 deploys: deploy?.enabled ?? false,
479 production: deploy?.production ?? null,
480 latest: deploy?.latest ?? null,
481 openPulls: entry?.pulls ? entry.pulls.length : null,
482 agents: liveRuns.filter((run) => repo && `${run.repo.namespace}/${run.repo.name}`.toLowerCase() === key).length,
483 passRate: checks.length ? checks.filter(Boolean).length / checks.length : null,
484 checks: checks.length,
485 };
486 });
487
488 const models_ = models ?? null;
489 times.total = Date.now() - started;
Agents as a team: lifecycle, merge queue, billing and a new shell490 const serverTiming = Object.entries(times)
491 .map(([name, ms]) => `${name};dur=${ms}`)
492 .join(", ");
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API493 const secure = new URL(request.url).protocol === "https:" ? "; Secure" : "";
494 return data(
495 {
496 signedIn: true as const,
497 viewer,
498 name: profile?.name?.trim() || username,
499 greeting: greetingFor(hourIn(now, tz)),
500 seenBefore: seen.since,
501 repos: repoList,
502 canRunAgents: models_ == null || models_.hosted || models_.own != null,
503 trial: models_?.own == null ? (models_?.trial ?? null) : null,
504 active: activeList.map(({ pull, lifecycle, repo }) => ({ pull, lifecycle, repo })),
505 needs: rankNeeds(needs),
506 live: liveRuns.slice(0, 12) as AgentRun[],
507 liveTotal: liveRuns.length,
508 runsLoaded: runs != null && runs.ok,
509 pullsTabs,
510 issuesTabs,
511 perRepoLoaded: perRepo != null,
512 groups,
513 digest,
514 pulse,
515 projects: projectsHealth,
516 projectsLoaded: projectsOk != null,
517 },
518 {
519 headers: {
520 "Server-Timing": serverTiming,
521 "Set-Cookie": `${SEEN_COOKIE}=${seen.value}; Path=/; Max-Age=31536000; HttpOnly; SameSite=Lax${secure}`,
522 },
523 },
524 );
525}
526
527type Loaded = Extract<Route.ComponentProps["loaderData"], { signedIn: true }>;
528
529// --- Pieces ---------------------------------------------------------------------
Agents as a team: lifecycle, merge queue, billing and a new shell530
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API531const TONE = { fg: "text-fg", warn: "text-warn", danger: "text-danger", accent: "text-accent" };
Agents as a team: lifecycle, merge queue, billing and a new shell532
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API533function Digest({ parts, seenBefore }: { parts: Loaded["digest"]; seenBefore: number | null }) {
534 const when = seenBefore ? (
535 <>
536 Since you were last here, <TimeAgo at={seenBefore} />
537 </>
538 ) : (
539 "In the last day"
540 );
541 if (parts.length === 0) {
542 return <p className="mt-1 text-sm text-muted">{when}: nothing new landed, and nothing is waiting on you.</p>;
543 }
544 return (
545 <p className="mt-1 text-sm leading-6 text-muted">
546 {when}:{" "}
547 {parts.map((part, index) => (
548 <span key={part.text}>
549 {index > 0 && (index === parts.length - 1 ? " and " : ", ")}
550 <a href={`#${part.anchor}`} className={`font-medium underline decoration-line-strong underline-offset-4 hover:decoration-current ${TONE[part.tone]}`}>
551 {part.text}
552 </a>
553 </span>
554 ))}
555 .
556 </p>
557 );
Agents as a team: lifecycle, merge queue, billing and a new shell558}
559
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API560function PullList({ rows, empty }: { rows: Loaded["pullsTabs"]["authored"]; empty: ReactNode }) {
561 if (rows.length === 0) return <Quiet>{empty}</Quiet>;
Agents as a team: lifecycle, merge queue, billing and a new shell562 return (
563 <ul className="divide-y divide-line overflow-hidden rounded-xl border border-line bg-surface">
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API564 {rows.map((row) => (
565 <li key={row.key}>
566 <Link
567 prefetch="intent"
568 to={`/${row.repo.namespace}/${row.repo.name}/pull/${row.number}`}
569 className="flex items-center gap-3 px-4 py-2.5 transition-colors hover:bg-raised"
570 >
571 <PullIcon status={row.status} />
572 <span className="min-w-0 grow">
573 <span className="block truncate text-sm font-medium">{row.title}</span>
574 <span className="block truncate font-mono text-xs text-muted">
575 {row.repo.name}#{row.number}
576 {row.agent && ` · ${row.agent}`}
Agents as a team: lifecycle, merge queue, billing and a new shell577 </span>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API578 </span>
579 {row.stage ? (
580 <span className="hidden shrink-0 items-center gap-2 text-xs text-muted sm:flex">
581 <StageDots stage={row.stage} />
582 <span className={row.stage === "needs_you" ? "text-warn" : ""}>{STAGE_LABEL[row.stage]}</span>
Agents as a team: lifecycle, merge queue, billing and a new shell583 </span>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API584 ) : (
585 <span className="hidden shrink-0 sm:block">
586 <CheckBadge status={row.checkStatus} />
Agents as a team: lifecycle, merge queue, billing and a new shell587 </span>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API588 )}
589 <span className="w-14 shrink-0 text-right text-xs text-faint">
590 <TimeAgo at={row.updatedAt} />
591 </span>
592 </Link>
593 </li>
594 ))}
Agents as a team: lifecycle, merge queue, billing and a new shell595 </ul>
596 );
Initial g1t: services, event bus, intents and attempts597}
598
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API599function IssueList({ rows, empty }: { rows: Loaded["issuesTabs"]["assigned"]; empty: ReactNode }) {
600 if (rows.length === 0) return <Quiet>{empty}</Quiet>;
Agents as a team: lifecycle, merge queue, billing and a new shell601 return (
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API602 <ul className="divide-y divide-line overflow-hidden rounded-xl border border-line bg-surface">
603 {rows.map((row) => (
604 <li key={row.key}>
605 <Link
606 prefetch="intent"
607 to={`/${row.repo.namespace}/${row.repo.name}/issues/${row.number}`}
608 className="flex items-center gap-3 px-4 py-2.5 transition-colors hover:bg-raised"
609 >
610 <IssueIcon issue={{ state: "open", reason: null }} />
611 <span className="min-w-0 grow">
612 <span className="block truncate text-sm font-medium">{row.title}</span>
613 <span className="block truncate font-mono text-xs text-muted">
614 {row.repo.name}#{row.number}
615 </span>
616 </span>
617 {row.agent ? (
618 <span className="hidden shrink-0 items-center gap-1 text-xs text-merged sm:flex">
619 <Bot size={12} /> {row.agent} at work
620 </span>
621 ) : row.queued ? (
622 <span className="hidden shrink-0 text-xs text-muted sm:block">Queued for an agent</span>
623 ) : null}
624 <span className="w-14 shrink-0 text-right text-xs text-faint">
625 <TimeAgo at={row.updatedAt} />
626 </span>
627 </Link>
628 </li>
629 ))}
630 </ul>
Agents as a team: lifecycle, merge queue, billing and a new shell631 );
632}
633
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API634function ProjectCard({ project, workspace }: { project: Loaded["projects"][number]; workspace: string }) {
635 const base = `/${workspace}/${project.slug}`;
636 const latest = project.latest;
637 return (
638 <li className="flex flex-col rounded-xl border border-line bg-surface p-4 transition-colors hover:border-line-strong">
639 <div className="flex items-center gap-2.5">
640 <span className="flex size-7 shrink-0 items-center justify-center rounded-md bg-raised text-muted ring-1 ring-line">
641 {project.private ? <Lock size={13} /> : <Box size={13} />}
642 </span>
643 <Link to={base} prefetch="intent" className="min-w-0 grow truncate font-medium hover:text-accent">
644 {project.name}
645 </Link>
646 {project.agents > 0 && (
647 <span className="inline-flex items-center gap-1 rounded-full border border-merged/40 bg-merged/10 px-2 py-0.5 text-[0.6875rem] text-merged">
648 <span className="size-1.5 animate-pulse rounded-full bg-merged" />
649 {project.agents} at work
650 </span>
651 )}
652 </div>
653 <div className="mt-3 min-h-10 text-xs">
654 {project.production ? (
655 <>
656 <a href={project.production.url} className="flex items-center gap-1 truncate font-mono text-fg-soft hover:text-accent">
657 {host(project.production.url)}
658 <ArrowUpRight size={12} className="shrink-0 text-faint" />
659 </a>
660 <p className="mt-1 flex items-center gap-2 text-muted">
661 {latest && <StatusDot status={latest.status} />}
662 <span>
663 deployed <TimeAgo at={project.production.deployedAt} />
664 </span>
665 </p>
666 </>
667 ) : project.deploys ? (
668 <p className="text-muted">
669 {latest ? <StatusDot status={latest.status} label={`${DEPLOY_STATUS[latest.status].label}, not live yet`} /> : "Deploys on the next push to the default branch."}
670 </p>
671 ) : (
672 <p className="text-faint">
673 Not deployed.{" "}
674 <Link to={`${base}/settings/deployments`} className="text-muted hover:text-fg">
675 Put it on g1t.page
676 </Link>
677 </p>
678 )}
679 </div>
680 <dl className="mt-3 grid grid-cols-3 gap-2 border-t border-line pt-3 text-xs">
681 <div>
682 <dt className="text-faint">Open PRs</dt>
683 <dd className="mt-0.5 font-medium tabular-nums">
684 <Link to={`${base}/pulls`} className="hover:text-accent">
685 {project.openPulls ?? "—"}
686 </Link>
687 </dd>
688 </div>
689 <div>
690 <dt className="text-faint">Agents</dt>
691 <dd className="mt-0.5 font-medium tabular-nums">
692 <Link to={`${base}/agents`} className="hover:text-accent">
693 {project.agents}
694 </Link>
695 </dd>
696 </div>
697 <div title={project.checks ? `${project.checks} recent check runs` : "No recent check runs"}>
698 <dt className="text-faint">Checks</dt>
699 <dd className="mt-0.5 font-medium tabular-nums">{percent(project.passRate)}</dd>
700 <dd className="mt-1">
701 <Meter value={project.passRate} tone={project.passRate != null && project.passRate < 0.7 ? "bg-warn" : "bg-accent"} />
702 </dd>
703 </div>
704 </dl>
705 </li>
706 );
Agents as a team: lifecycle, merge queue, billing and a new shell707}
708
Get started on mission control709type Step = { done: boolean; title: string; about: string; to: string | null; action: string };
710
711/**
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API712 * The first things to do, ticked off as they are done, until work has been
713 * handed to agents.
Workspace things at the bottom of the sidebar, and on mission control714 */
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API715function GetStarted({ steps }: { steps: Step[] }) {
716 const left = steps.filter((step) => !step.done).length;
717 return (
718 <section className="rounded-2xl bg-surface p-5 ring-1 ring-line">
719 <div className="flex items-baseline justify-between gap-3">
720 <h2 className="font-semibold tracking-tight">Get started</h2>
721 <span className="text-xs text-muted">
722 {steps.length - left} of {steps.length} done
723 </span>
724 </div>
725 <ol className="mt-4 space-y-2">
726 {steps.map((step, index) => (
727 <li
728 key={step.title}
729 className={`flex items-start gap-3 rounded-xl px-3 py-2.5 ${step.done ? "" : "bg-bg/50 ring-1 ring-line"}`}
730 >
731 <span
732 className={`mt-0.5 flex size-5 shrink-0 items-center justify-center rounded-full text-[0.6875rem] font-medium ${
733 step.done ? "bg-accent text-bg" : "text-muted ring-1 ring-line-strong"
734 }`}
735 >
736 {step.done ? "✓" : index + 1}
737 </span>
738 <span className="min-w-0 grow">
739 <span className={`block text-sm font-medium ${step.done ? "text-muted line-through decoration-faint" : ""}`}>
740 {step.title}
741 </span>
742 {!step.done && <span className="mt-0.5 block text-xs leading-5 text-muted">{step.about}</span>}
743 </span>
744 {!step.done && step.to && (
745 <Link
746 to={step.to}
747 className="shrink-0 rounded-md bg-fg px-2.5 py-1 text-xs font-medium text-bg transition-colors hover:bg-white"
748 >
749 {step.action}
750 </Link>
751 )}
752 </li>
753 ))}
754 </ol>
755 </section>
756 );
757}
758
759/** The workspace being looked at: what it holds and what is moving in it. */
Workspace names and icons, and a component kit for every control760function WorkspaceCard({
761 slug,
762 name,
763 avatar,
764 role,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API765 projects,
766 needs,
767 live,
Workspace names and icons, and a component kit for every control768}: {
769 slug: string;
770 name: string;
771 avatar?: string | null;
772 role: string;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API773 projects: number;
774 needs: number;
775 live: number;
Workspace names and icons, and a component kit for every control776}) {
Workspace things at the bottom of the sidebar, and on mission control777 const stats = [
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API778 { value: projects, label: "projects" },
779 { value: live, label: "agents at work" },
780 { value: needs, label: "need you" },
Workspace things at the bottom of the sidebar, and on mission control781 ];
782 return (
783 <div className="rounded-xl border border-line bg-surface p-5">
784 <div className="flex items-center gap-3">
Workspace names and icons, and a component kit for every control785 <Avatar name={slug} image={avatar} size={32} square />
Workspace things at the bottom of the sidebar, and on mission control786 <div className="min-w-0">
Workspace names and icons, and a component kit for every control787 <Link to={`/${slug}`} title={name} className="block truncate font-medium hover:underline">
788 {name}
Workspace things at the bottom of the sidebar, and on mission control789 </Link>
Workspace names and icons, and a component kit for every control790 <p className="truncate text-xs text-muted">
791 <span className="font-mono">g1t.sh/{slug}</span> · <span className="capitalize">{role}</span>
792 </p>
Workspace things at the bottom of the sidebar, and on mission control793 </div>
794 </div>
795 <dl className="mt-4 grid grid-cols-3 gap-2">
796 {stats.map((stat) => (
797 <div key={stat.label} className="rounded-lg bg-bg/60 px-2.5 py-2 ring-1 ring-line">
798 <dt className="sr-only">{stat.label}</dt>
799 <dd className="text-lg font-semibold tabular-nums tracking-tight">{stat.value}</dd>
800 <dd className="text-[0.6875rem] leading-tight text-muted">{stat.label}</dd>
801 </div>
802 ))}
803 </dl>
804 <div className="mt-4 flex flex-wrap gap-x-4 gap-y-1 text-sm">
805 <Link to={`/${slug}/-/people`} className="inline-flex items-center gap-1.5 text-muted hover:text-fg">
806 <Users size={13} />
807 Members
808 </Link>
809 <Link
810 to={role === "owner" ? `/${slug}/-/settings` : `/${slug}/-/people`}
811 className="inline-flex items-center gap-1.5 text-muted hover:text-fg"
812 >
813 <Settings size={13} />
814 Settings
815 </Link>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API816 <Link to={`/${slug}/-/agents`} className="inline-flex items-center gap-1.5 text-muted hover:text-fg">
817 <Radio size={13} />
818 Fleet
Workspace things at the bottom of the sidebar, and on mission control819 </Link>
820 </div>
821 </div>
822 );
823}
824
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API825function UsageCard({ shell, weekCost }: { shell: ShellData; weekCost: number }) {
826 const slug = shell.workspace?.slug;
827 const used = shell.monthUsageMicros ?? 0;
828 const limit = shell.limit;
829 const ceiling = limit && !limit.comped ? limit.ceilingMicros : null;
830 const share = ceiling ? Math.min(1, limit!.exposureMicros / ceiling) : null;
831 const month = new Date().toLocaleDateString("en-US", { month: "long" });
Get started on mission control832 return (
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API833 <div className="rounded-xl border border-line bg-surface p-5">
834 <div className="flex items-center justify-between">
835 <h2 className="flex items-center gap-1.5 text-sm font-semibold">
836 <Coins size={14} className="text-faint" />
837 Usage in {month}
838 </h2>
839 {slug && (
840 <Link to={`/${slug}/-/usage`} className="text-xs text-muted hover:text-fg">
841 Details
842 </Link>
843 )}
Get started on mission control844 </div>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API845 <p className="mt-3 text-2xl font-semibold tracking-tight tabular-nums">{dollars(used)}</p>
846 <p className="text-xs text-muted">
847 {shell.free ? "At cost. g1t charges nothing while it is being built out." : "Charged so far this month."}
848 {weekCost > 0 && ` ${formatCost(weekCost)} of it in the last 7 days.`}
849 </p>
850 {ceiling != null && (
851 <div className="mt-3">
852 <Meter value={share} tone={limit!.state === "ok" ? "bg-accent" : limit!.state === "warning" ? "bg-warn" : "bg-danger"} />
853 <p className="mt-1.5 text-[0.6875rem] text-faint">
854 {dollars(limit!.exposureMicros)} of the {dollars(ceiling)} limit
855 </p>
856 </div>
857 )}
858 </div>
859 );
860}
861
862function WhatsNew({ workspace }: { workspace: string | null }) {
863 return (
864 <div className="rounded-xl border border-line bg-surface p-5">
865 <h2 className="flex items-center gap-1.5 text-sm font-semibold">
866 <Sparkles size={14} className="text-accent" />
867 What's new on g1t
868 </h2>
869 <ul className="mt-3 space-y-3">
870 {CHANGELOG.slice(0, 5).map((entry) => {
871 const href = changelogHref(entry, workspace);
872 const title = <span className="text-sm font-medium">{entry.title}</span>;
873 return (
874 <li key={entry.title}>
875 {href ? (
876 <Link to={href} className="hover:text-accent">
877 {title}
878 </Link>
879 ) : (
880 title
881 )}
882 <p className="mt-0.5 text-xs leading-5 text-muted">{entry.about}</p>
883 </li>
884 );
885 })}
886 </ul>
887 <a href="https://docs.g1t.sh/" className="mt-4 inline-flex items-center gap-1 text-xs text-muted hover:text-fg">
888 Read the docs <ArrowRight size={12} />
889 </a>
890 </div>
Get started on mission control891 );
892}
893
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API894// --- The page -------------------------------------------------------------------
Agents as a team: lifecycle, merge queue, billing and a new shell895
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API896export default function Home({ loaderData }: Route.ComponentProps) {
897 const shell = useRouteLoaderData("root")?.shell as ShellData | null | undefined;
898 const loaded = loaderData.signedIn ? loaderData : null;
Agents as a team: lifecycle, merge queue, billing and a new shell899 // Agents are at work, so the page changes without anyone touching it.
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API900 const changing =
901 (loaded?.liveTotal ?? 0) > 0 ||
902 (loaded?.active ?? []).some((item) => item.lifecycle && item.lifecycle.stage !== "needs_you" && item.lifecycle.stage !== "ready");
903 useLiveRefresh(changing);
904 const [greeting, setGreeting] = useState(loaded?.greeting ?? "");
905 const [pullsTab, setPullsTab] = useState(() => (loaded && loaded.pullsTabs.review.length > 0 ? "review" : "authored"));
Agents as a team: lifecycle, merge queue, billing and a new shell906 useEffect(() => {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API907 // The greeting follows the viewer's own clock, and the server learns
908 // their zone for next time.
909 setGreeting(greetingFor(new Date().getHours()));
910 try {
911 const zone = Intl.DateTimeFormat().resolvedOptions().timeZone;
912 if (zone) document.cookie = `${TZ_COOKIE}=${encodeURIComponent(zone)}; Path=/; Max-Age=31536000; SameSite=Lax`;
913 } catch {
914 // Nothing to remember.
915 }
916 }, []);
Agents as a team: lifecycle, merge queue, billing and a new shell917
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API918 if (!loaded) return <Landing repos={loaderData.repos} />;
919 const { viewer, repos, active, needs: serverNeeds, live, liveTotal, pullsTabs, issuesTabs, groups, pulse, projects, canRunAgents, trial } = loaded;
Initial g1t: services, event bus, intents and attempts920
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API921 const workspace = shell?.workspace?.slug ?? viewer.workspaces?.[0]?.slug ?? null;
922 // The usage limit, as the sidebar knows it, heads the list when it bites.
923 const limitNeed: Need[] =
924 shell?.limit && !shell.limit.comped && shell.limit.state !== "ok" && workspace
925 ? [
926 {
927 key: "limit",
928 kind: "limit",
929 title: shell.limit.state === "stopped" ? "Agents are stopped: the usage limit was reached" : "Usage is nearing its limit",
930 detail:
931 shell.limit.state === "stopped"
932 ? "No new agent starts until the limit is raised or a card is added."
933 : `${dollars(shell.limit.exposureMicros)} of ${dollars(shell.limit.ceilingMicros ?? 0)} used. Add a card so work keeps going.`,
934 to: `/${workspace}/-/billing`,
935 action: "Billing",
936 at: Date.now(),
937 where: null,
938 },
939 ]
940 : [];
941 const needs = rankNeeds([...limitNeed, ...serverNeeds]);
942
Get started on mission control943 const first = repos[0];
Usage while free is shown at cost; agents get rustfmt and clippy944 const handedOff = active.length > 0 || (shell?.monthUsageMicros ?? 0) > 0;
Get started on mission control945 const steps: Step[] = [
946 {
947 done: Boolean(workspace),
948 title: "Create a workspace",
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API949 about: "Projects, people and agent credit live in one.",
Get started on mission control950 to: "/workspaces/new",
951 action: "Create",
952 },
953 {
954 done: repos.length > 0,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API955 title: "Add a project",
956 about: "Create a repository, or import one by its address. Push to it with git as usual.",
Get started on mission control957 to: workspace ? `/new?workspace=${workspace}` : "/new",
958 action: "Add",
959 },
A free allowance on g1t's models, so anyone can try its agents960 trial?.open
New workspaces are told to connect a model, before anything fails961 ? {
A free allowance on g1t's models, so anyone can try its agents962 done: true,
963 title: "Try g1t's agents free",
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put964 about: trial.granted
965 ? `This workspace has ${dollars(trial.limitMicros - trial.usedMicros)} of its ${dollars(trial.limitMicros)} trial credit left, for g1t's models and sandboxes.`
966 : `This workspace gets ${dollars(trial.limitMicros)} of trial credit, for g1t's models and sandboxes, the first time its agents work.`,
A free allowance on g1t's models, so anyone can try its agents967 to: workspace ? `/${workspace}/-/integrations` : null,
968 action: "Connect",
969 }
970 : canRunAgents
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API971 ? {
972 done: Boolean(shell?.free) || shell?.limit == null || shell.limit.comped || shell.limit.state === "ok",
973 title: "Keep work running",
974 about: "Usage is charged after it runs. Add a card under Billing, so g1t charges it as you near your limit instead of stopping work.",
975 to: workspace ? `/${workspace}/-/billing` : null,
976 action: "Billing",
977 }
978 : {
979 done: false,
980 title: "Connect a model",
981 about: `${
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put982 trialClosed(trial, workspace ?? "This workspace") ? `${trialClosed(trial, workspace ?? "This workspace")} ` : ""
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API983 }Agents need a model to think with. Connect your Anthropic or OpenAI key, or any compatible endpoint.`,
984 to: workspace ? `/${workspace}/-/integrations` : null,
985 action: "Connect",
986 },
Get started on mission control987 {
988 done: handedOff,
989 title: "Hand off an outcome",
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API990 about: "Describe what you want done above, or write an outcome on a project's Plans page and let a planner split it into issues.",
Get started on mission control991 to: first ? `/${first.namespace}/${first.name}/plans` : null,
992 action: "Write one",
993 },
994 ];
995 const starting = steps.some((step) => !step.done);
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API996 const counts = (n: number) => (n > 0 ? <span className="ml-1 tabular-nums text-faint">{n}</span> : null);
Get started on mission control997
API and MCP server, Rust identity service, registration, site redesign998 return (
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API999 <main className="mx-auto grid max-w-7xl gap-x-10 gap-y-10 px-4 py-8 sm:py-10 lg:grid-cols-[minmax(0,1fr)_19rem]">
API and MCP server, Rust identity service, registration, site redesign1000 <div className="min-w-0 space-y-10">
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1001 <header>
1002 <h1 className="text-2xl font-semibold tracking-tight sm:text-[1.75rem]" suppressHydrationWarning>
1003 {greeting}, {loaded.name}
1004 </h1>
1005 <Digest parts={loaded.digest} seenBefore={loaded.seenBefore} />
1006 </header>
API and MCP server, Rust identity service, registration, site redesign1007
Get started on mission control1008 {starting && <GetStarted steps={steps} />}
1009
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1010 <Panel id="needs-you" title="Needs you" icon={<Hand size={14} className="text-warn" />} count={needs.length}>
1011 {needs.length === 0 ? (
1012 <Quiet>Nothing is waiting on you. Reviews, stopped work and failed deploys show up here first.</Quiet>
1013 ) : (
1014 <NeedsList needs={needs} />
1015 )}
1016 </Panel>
Agents as a team: lifecycle, merge queue, billing and a new shell1017
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1018 <Panel
1019 id="live"
1020 title="Live now"
1021 icon={<span className={`block size-2 rounded-full ${liveTotal > 0 ? "animate-pulse bg-merged" : "bg-line-strong"}`} />}
1022 count={liveTotal}
1023 all={workspace ? { to: `/${workspace}/-/agents`, label: "Fleet" } : null}
1024 >
1025 {!loaded.runsLoaded && workspace ? (
1026 <Unavailable what="Agent runs" />
1027 ) : live.length === 0 ? (
Mission control drops the composer for now1028 <Quiet>No agent is at work right now. Assign an issue to g1t-agent and one starts on it in seconds.</Quiet>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1029 ) : (
1030 <ul className="space-y-3">
1031 {live.map((run) => (
1032 <RunCard key={run.id} run={run} member showRepo />
1033 ))}
1034 {liveTotal > live.length && workspace && (
1035 <li className="text-xs text-muted">
1036 <Link to={`/${workspace}/-/agents`} className="hover:text-fg">
1037 {liveTotal - live.length} more at work in the fleet
Agents as a team: lifecycle, merge queue, billing and a new shell1038 </Link>
1039 </li>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1040 )}
Agents as a team: lifecycle, merge queue, billing and a new shell1041 </ul>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1042 )}
1043 </Panel>
1044
1045 <Panel
1046 title="This week"
1047 icon={<ActivityIcon size={14} />}
1048 all={workspace ? { to: `/${workspace}/-/usage`, label: "Usage" } : null}
1049 >
1050 <div className="grid grid-cols-2 gap-3 sm:grid-cols-3 xl:grid-cols-5">
1051 <PulseTile
1052 label="Changes landed"
1053 value={String(pulse.merged)}
1054 hint="last 7 days"
1055 values={pulse.mergedDays}
1056 format={(v) => `${v} landed`}
1057 />
1058 <PulseTile
1059 label="Agent hours"
1060 value={pulse.hours < 10 ? pulse.hours.toFixed(1) : String(Math.round(pulse.hours))}
1061 hint="time agents spent working"
1062 values={pulse.hoursDays}
1063 format={(v) => `${v.toFixed(1)} h`}
1064 to={workspace ? `/${workspace}/-/agents` : undefined}
1065 />
1066 <PulseTile
1067 label="Agent cost"
1068 value={formatCost(pulse.cost) ?? "$0.00"}
1069 hint="model and sandbox"
1070 values={pulse.costDays}
1071 format={(v) => formatCost(v) ?? "$0.00"}
1072 to={workspace ? `/${workspace}/-/usage` : undefined}
1073 />
1074 <PulseTile
1075 label="Issue to landed"
1076 value={formatSpan(pulse.issueToMerge)}
1077 hint={pulse.mergedWithIssue ? `median of ${pulse.mergedWithIssue}` : "no issue landed yet"}
1078 />
1079 <PulseTile
1080 label="Checks pass first time"
1081 value={percent(pulse.firstPass)}
1082 hint={pulse.firstPassOf ? `of ${pulse.firstPassOf} pull requests` : "no checks ran yet"}
1083 />
1084 </div>
1085 </Panel>
1086
1087 <div className="grid gap-10 xl:grid-cols-2">
1088 <Tabs value={pullsTab} onValueChange={setPullsTab}>
1089 <Panel
1090 id="your-pulls"
1091 title="Your pull requests"
1092 icon={<GitPullRequest size={14} />}
1093 all={{ to: `/u/${viewer.username}?tab=pulls` }}
1094 >
1095 <TabsList>
1096 <TabsTrigger value="authored">Opened{counts(pullsTabs.authored.length)}</TabsTrigger>
1097 <TabsTrigger value="review">Review requested{counts(pullsTabs.review.length)}</TabsTrigger>
1098 <TabsTrigger value="assigned">Assigned{counts(pullsTabs.assigned.length)}</TabsTrigger>
1099 </TabsList>
1100 <TabsContent value="authored">
1101 <PullList rows={pullsTabs.authored} empty="None open. Pull requests you or your agents start show here with where each stands." />
1102 </TabsContent>
1103 <TabsContent value="review">
1104 {loaded.perRepoLoaded ? (
1105 <PullList rows={pullsTabs.review} empty="No one is waiting on your review." />
1106 ) : (
1107 <Unavailable what="Review requests" />
1108 )}
1109 </TabsContent>
1110 <TabsContent value="assigned">
1111 <PullList rows={pullsTabs.assigned} empty="No open pull request is assigned to you." />
1112 </TabsContent>
1113 </Panel>
1114 </Tabs>
1115
1116 <Tabs defaultValue="assigned">
1117 <Panel title="Your issues" icon={<CircleDot size={14} />} all={{ to: `/u/${viewer.username}?tab=issues` }}>
1118 <TabsList>
1119 <TabsTrigger value="assigned">Assigned{counts(issuesTabs.assigned.length)}</TabsTrigger>
1120 <TabsTrigger value="authored">Opened{counts(issuesTabs.authored.length)}</TabsTrigger>
1121 </TabsList>
1122 <TabsContent value="assigned">
1123 <IssueList rows={issuesTabs.assigned} empty="Nothing is assigned to you." />
1124 </TabsContent>
1125 <TabsContent value="authored">
1126 <IssueList rows={issuesTabs.authored} empty="You have no open issues." />
1127 </TabsContent>
1128 </Panel>
1129 </Tabs>
1130 </div>
Agents as a team: lifecycle, merge queue, billing and a new shell1131
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1132 <Panel id="activity" title="Activity" icon={<ActivityIcon size={14} />}>
1133 {!loaded.perRepoLoaded ? (
1134 <Unavailable what="Activity" />
Agents as a team: lifecycle, merge queue, billing and a new shell1135 ) : (
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1136 <ActivityFeed groups={groups} empty="Nothing has moved yet. Merges, deploys, checks, reviews and what agents learn show up here." />
Agents as a team: lifecycle, merge queue, billing and a new shell1137 )}
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1138 </Panel>
API and MCP server, Rust identity service, registration, site redesign1139
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1140 {workspace && (
1141 <Panel
1142 id="projects"
1143 title="Projects"
1144 icon={<Box size={14} />}
1145 count={projects.length}
1146 all={{ to: `/${workspace}`, label: "Workspace" }}
1147 extra={
1148 <Link to={`/new?workspace=${workspace}`} className="inline-flex items-center gap-1 text-xs text-muted hover:text-fg">
1149 <Plus size={12} /> New
1150 </Link>
1151 }
1152 >
1153 {!loaded.projectsLoaded ? (
1154 <Unavailable what="Projects" />
1155 ) : projects.length === 0 ? (
1156 <Quiet>No projects yet. Create a repository or import one, and it gets a project of its own.</Quiet>
1157 ) : (
1158 <ul className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
1159 {projects.map((project) => (
1160 <ProjectCard key={project.slug} project={project} workspace={workspace} />
1161 ))}
1162 </ul>
1163 )}
1164 </Panel>
1165 )}
API and MCP server, Rust identity service, registration, site redesign1166 </div>
Initial g1t: services, event bus, intents and attempts1167
API and MCP server, Rust identity service, registration, site redesign1168 <aside className="space-y-4">
Workspace things at the bottom of the sidebar, and on mission control1169 {shell?.workspace && (
1170 <WorkspaceCard
1171 slug={shell.workspace.slug.toLowerCase()}
Workspace names and icons, and a component kit for every control1172 name={shell.workspace.name || shell.workspace.slug}
1173 avatar={shell.workspace.avatar}
Workspace things at the bottom of the sidebar, and on mission control1174 role={shell.workspace.role}
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1175 projects={shell.repos.length}
1176 needs={needs.length}
1177 live={liveTotal}
Workspace things at the bottom of the sidebar, and on mission control1178 />
1179 )}
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1180 {shell?.workspace && <UsageCard shell={shell} weekCost={pulse.cost} />}
1181 <WhatsNew workspace={workspace} />
API and MCP server, Rust identity service, registration, site redesign1182 <div className="rounded-xl border border-line bg-surface p-5">
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1183 <h2 className="text-sm font-semibold">Connect your own agent</h2>
1184 <p className="mt-1.5 text-xs leading-5 text-muted">
1185 Add g1t to Claude Code, then run /mcp in it to sign in through your browser.
API and MCP server, Rust identity service, registration, site redesign1186 </p>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1187 <div className="mt-3">
1188 <CopyLine prompt text="claude mcp add --transport http g1t https://mcp.g1t.sh" />
API and MCP server, Rust identity service, registration, site redesign1189 </div>
1190 <Link
Docs as their own app; shared theme package1191 to="https://docs.g1t.sh/guides/bring-your-own-agent/"
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1192 className="mt-3 inline-flex items-center gap-1 text-xs text-muted hover:text-fg"
API and MCP server, Rust identity service, registration, site redesign1193 >
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1194 How it works <ArrowRight size={12} />
API and MCP server, Rust identity service, registration, site redesign1195 </Link>
1196 </div>
1197 </aside>
Initial g1t: services, event bus, intents and attempts1198 </main>
1199 );
1200}