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,199 lines47,880 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";
42import {
43 type ActivityItem,
44 type Need,
45 SEEN_COOKIE,
46 TIME,
47 agentHours,
48 dailyBuckets,
49 digestParts,
50 eventItem,
51 firstPassRate,
52 formatSpan,
53 greetingFor,
54 groupActivity,
55 hourIn,
56 issueToMerge,
57 median,
58 nextSeen,
59 rankNeeds,
60 readCookie,
61 stuckMinutes,
62} from "../lib/mission";
API and MCP server, Rust identity service, registration, site redesign63import { Landing } from "../components/landing";
Get started on mission control64import type { ShellData } from "../components/shell";
Agents as a team: lifecycle, merge queue, billing and a new shell65import { 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 API66import { RunCard, formatCost, useLiveRefresh } from "../components/agents";
67import { CheckBadge } from "../components/checks";
68import { DEPLOY_STATUS, StatusDot, host } from "../components/deploy";
69import { ActivityFeed, Meter, NeedsList, Panel, PulseTile, Quiet, Unavailable, percent } from "../components/mission";
70import { Avatar, CopyLine, TimeAgo } from "../components/ui";
71import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../components/ui/select";
72import { Tabs, TabsContent, TabsList, TabsTrigger } from "../components/ui/tabs";
73import { IssueIcon, PullIcon } from "../components/work";
API and MCP server, Rust identity service, registration, site redesign74import {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API75 agents,
76 billing,
77 deployments,
78 events as eventLog,
79 identity,
80 projects as projectsApi,
81 repos as reposApi,
82 work,
83} from "../lib/services.server";
Mission control drops the composer for now84import { getViewer } from "../lib/session.server";
Initial g1t: services, event bus, intents and attempts85
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API86/** The viewer's time zone, set by the page itself, so the greeting fits their day. */
87const TZ_COOKIE = "g1t_tz";
88/** The most projects whose pull requests and events are read for the page. */
89const MAX_PROJECTS = 10;
90const EVENTS_PER_PROJECT = 80;
Agents as a team: lifecycle, merge queue, billing and a new shell91
A free allowance on g1t's models, so anyone can try its agents92const dollars = (micros: number) => `$${(Math.max(0, micros) / 1_000_000).toFixed(2)}`;
93
Social cards for every page: og.g1t.sh94export function meta(args: Route.MetaArgs) {
95 return page(args, {
96 title: "g1t — Git for AI scale",
97 description:
98 "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.",
99 });
Initial g1t: services, event bus, intents and attempts100}
101
Agents as a team: lifecycle, merge queue, billing and a new shell102export function headers({ loaderHeaders }: Route.HeadersArgs) {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API103 const out = new Headers();
104 const timing = loaderHeaders.get("Server-Timing");
105 if (timing) out.set("Server-Timing", timing);
106 const cookie = loaderHeaders.get("Set-Cookie");
107 if (cookie) out.set("Set-Cookie", cookie);
108 return out;
Agents as a team: lifecycle, merge queue, billing and a new shell109}
110
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API111/** A row of the viewer's pull requests, whichever list it comes from. */
112type PullRow = {
113 key: string;
114 repo: RepoPath;
115 number: number;
116 title: string;
117 status: PullStatus;
118 agent: string | null;
119 stage: Stage | null;
120 detail: string | null;
121 checkStatus: CheckStatus | null;
122 updatedAt: string;
123};
124
125/** A row of the viewer's issues. */
126type IssueRow = {
127 key: string;
128 repo: RepoPath;
129 number: number;
130 title: string;
131 agent: string | null;
132 queued: boolean;
133 updatedAt: string;
134};
135
136type ProjectHealth = {
137 slug: string;
138 name: string;
139 private: boolean;
140 repo: RepoPath | null;
141 deploys: boolean;
142 production: LiveApp | null;
143 latest: Deployment | null;
144 openPulls: number | null;
145 agents: number;
146 passRate: number | null;
147 checks: number;
148};
149
150const pullRow = (pull: Pull, repo: RepoPath, lifecycle: Lifecycle | null): PullRow => ({
151 key: pull.id,
152 repo,
153 number: pull.number,
154 title: pull.title,
155 status: pull.status,
156 agent: pull.agent,
157 stage: lifecycle?.stage ?? null,
158 detail: lifecycle?.detail ?? null,
159 checkStatus: pull.checkStatus,
160 updatedAt: pull.updatedAt,
161});
162
163const authoredPull = (item: AuthoredItem): PullRow => ({
164 key: `${item.repo.namespace}/${item.repo.name}#${item.number}`,
165 repo: item.repo,
166 number: item.number,
167 title: item.title,
168 status: item.status ?? (item.draft ? "draft" : "open"),
169 agent: null,
170 stage: null,
171 detail: null,
172 checkStatus: null,
173 updatedAt: item.updatedAt,
174});
175
176export async function loader({ context, request }: Route.LoaderArgs) {
Initial g1t: services, event bus, intents and attempts177 const viewer = getViewer(context);
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API178 if (!viewer) {
179 const repos = await reposApi.list(viewer, { memberOnly: false });
180 return data({ signedIn: false as const, repos });
181 }
182
183 const now = Date.now();
184 const weekAgo = now - 7 * TIME.DAY;
Agents as a team: lifecycle, merge queue, billing and a new shell185 const started = Date.now();
186 const times: Record<string, number> = {};
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API187 // Each call is timed, and one that fails leaves its section empty rather
188 // than taking the page down.
189 const soft = <T,>(name: string, promise: Promise<T>): Promise<T | null> =>
190 promise.then(
191 (value) => ((times[name] = Date.now() - started), value),
192 (error) => {
193 console.warn(`home: ${name} failed`, error);
194 times[name] = Date.now() - started;
195 return null;
196 },
197 );
198 const okOr = <T,>(result: { ok: true; value: T } | { ok: false } | null): T | null => (result?.ok ? result.value : null);
199
200 const cookies = request.headers.get("cookie");
201 const seen = nextSeen(readCookie(cookies, SEEN_COOKIE), now);
202 const tz = readCookie(cookies, TZ_COOKIE);
203 const memberships = viewer.workspaces ?? [];
204 const slug = memberships[0]?.slug ?? null;
205 const mine = new Set(memberships.map((m) => m.slug.toLowerCase()));
206 const username = viewer.username;
207
208 const reposP = soft("repos", reposApi.list(viewer, { memberOnly: true }));
209 // The workspace's projects' open pull requests and recent events, read
210 // once each, all at once, as soon as the projects are known.
211 const perRepoP = reposP.then((repos) =>
212 Promise.all(
213 (repos ?? [])
214 .filter((repo) => mine.has(repo.namespace.toLowerCase()))
215 .slice(0, MAX_PROJECTS)
216 .map(async (repo) => {
217 const path = { namespace: repo.namespace, name: repo.name };
218 const [pulls, log] = await Promise.all([
219 work.listPulls(path, viewer, "open").catch(() => null),
220 eventLog.list({ repoId: repo.id, limit: EVENTS_PER_PROJECT }).catch(() => null),
221 ]);
222 return { repo, pulls: pulls?.ok ? pulls.value.slice(0, 60) : null, events: log };
223 }),
224 ),
225 );
226
227 const [repos, perRepo, active, assigned, models, profile, runs, overview, usage, authoredPulls, authoredIssues, projectList, memories] =
228 await Promise.all([
229 reposP,
230 soft("projects", perRepoP),
231 soft("pulls", work.listActivePulls(viewer)),
232 soft("assigned", work.listAssignedIssues(viewer)),
233 slug ? soft("models", env.RUNNER.modelAccess(slug)) : null,
234 soft("profile", identity.profile(username)),
235 slug ? soft("runs", agents.listRuns(viewer, { workspace: slug, limit: 150 })) : null,
236 slug ? soft("deploys", deployments.overview(slug, viewer)) : null,
237 slug ? soft("usage", billing.usage(slug, viewer, new Date(weekAgo - TIME.DAY).toISOString())) : null,
238 soft("authoredPulls", work.byAuthor(username, viewer, { kind: "pull", state: "open", sort: "updated", limit: 10 })),
239 soft("authoredIssues", work.byAuthor(username, viewer, { kind: "issue", state: "open", sort: "updated", limit: 10 })),
240 slug ? soft("projectList", projectsApi.list(slug, viewer)) : null,
241 slug ? soft("memories", agents.listMemories(viewer, slug, null)) : null,
242 ]);
243
244 const repoList = repos ?? [];
Agents as a team: lifecycle, merge queue, billing and a new shell245 // 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 API246 // the viewer's own, already listed; the rest are looked up once each.
247 const known = new Map<string, Repo>(repoList.map((repo) => [repo.id, repo]));
Agents as a team: lifecycle, merge queue, billing and a new shell248 const missing = [
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API249 ...new Set([...(assigned ?? []).map((issue) => issue.repoId), ...(active ?? []).map(({ pull }) => pull.repoId)]),
Agents as a team: lifecycle, merge queue, billing and a new shell250 ].filter((id) => !known.has(id));
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API251 const looked = await Promise.all(missing.map((id) => reposApi.getById(id, viewer).catch(() => null)));
252 for (const found of looked) if (found?.ok) known.set(found.value.id, found.value);
253 const pathOf = (repo: Repo): RepoPath => ({ namespace: repo.namespace, name: repo.name });
254
255 const activeList = (active ?? []).flatMap((item) => {
256 const repo = known.get(item.pull.repoId);
257 return repo ? [{ ...item, repo }] : [];
258 });
259 const runList = okOr(runs ?? null) ?? [];
260 const liveRuns = runList.filter((run) => isActiveRun(run.status));
261 const overviewList = okOr(overview ?? null);
262 const projectsOk = okOr(projectList ?? null);
263
264 // --- What the viewer's pull requests and issues are ----------------------
265 const lower = username.toLowerCase();
266 const openPulls = (perRepo ?? []).flatMap(({ repo, pulls }) => (pulls ?? []).map((pull) => ({ pull, repo })));
267 const reviewRequested = openPulls.filter(
268 ({ pull }) =>
269 pull.status === "open" &&
270 pull.author.username.toLowerCase() !== lower &&
271 pull.reviewers.some((name) => name.toLowerCase() === lower),
272 );
273 const assignedPulls = openPulls.filter(({ pull }) => pull.assignees.some((name) => name.toLowerCase() === lower));
274 const authoredRows =
275 activeList.length > 0
276 ? activeList.map(({ pull, lifecycle, repo }) => pullRow(pull, pathOf(repo), lifecycle))
277 : (okOr(authoredPulls)?.items ?? []).map(authoredPull);
278 const pullsTabs = {
279 authored: authoredRows.slice(0, 8),
280 review: reviewRequested.slice(0, 8).map(({ pull, repo }) => pullRow(pull, pathOf(repo), null)),
281 assigned: assignedPulls.slice(0, 8).map(({ pull, repo }) => pullRow(pull, pathOf(repo), null)),
282 };
283 const issueRow = (issue: Issue, repo: RepoPath): IssueRow => ({
284 key: issue.id,
285 repo,
286 number: issue.number,
287 title: issue.title,
288 agent: issue.agent,
289 queued: issue.queued,
290 updatedAt: issue.updatedAt,
291 });
292 const issuesTabs = {
293 assigned: (assigned ?? []).flatMap((issue) => {
294 const repo = known.get(issue.repoId);
295 return repo ? [issueRow(issue, pathOf(repo))] : [];
296 }).slice(0, 8),
297 authored: (okOr(authoredIssues)?.items ?? []).slice(0, 8).map((item) => ({
298 key: `${item.repo.namespace}/${item.repo.name}#${item.number}`,
299 repo: item.repo,
300 number: item.number,
301 title: item.title,
302 agent: null,
303 queued: false,
304 updatedAt: item.updatedAt,
305 })),
306 };
307
308 // --- Needs you ------------------------------------------------------------
309 const needs: Need[] = [];
310 for (const entry of overviewList ?? []) {
311 const latest = entry.latest;
312 if (latest?.kind === "production" && latest.status === "failed" && slug) {
313 needs.push({
314 key: `deploy:${entry.slug}`,
315 kind: "deploy",
316 title: `Production build of ${entry.slug} failed`,
317 detail: latest.error ?? "The last build of the default branch failed. Production still serves the build before it.",
318 to: `/${slug}/${entry.slug}/deployments/${latest.id}`,
319 action: "See the build",
320 at: Date.parse(latest.finishedAt ?? latest.createdAt),
321 where: `${slug}/${entry.slug}`,
322 });
323 }
324 }
325 for (const { pull, lifecycle, repo } of activeList) {
326 const where = `${repo.namespace}/${repo.name}#${pull.number}`;
327 const to = `/${repo.namespace}/${repo.name}/pull/${pull.number}`;
328 if (lifecycle?.stage === "needs_you") {
329 const conflict = /conflict/i.test(lifecycle.detail);
330 needs.push({
331 key: `pull:${pull.id}`,
332 kind: conflict ? "conflict" : "stalled",
333 title: pull.title,
334 detail: lifecycle.detail,
335 to,
336 action: conflict ? "Resolve" : "Decide",
337 at: Date.parse(pull.updatedAt),
338 where,
339 });
340 } else if (lifecycle?.stage === "ready") {
341 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 });
342 } else if (!lifecycle && pull.status === "open" && pull.checkStatus === "failed") {
343 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 });
344 }
345 }
346 for (const { pull, repo } of reviewRequested) {
347 needs.push({
348 key: `review:${pull.id}`,
349 kind: "review",
350 title: pull.title,
351 detail: `${pull.author.username} asked for your review.`,
352 to: `/${repo.namespace}/${repo.name}/pull/${pull.number}`,
353 action: "Review",
354 at: Date.parse(pull.updatedAt),
355 where: `${repo.namespace}/${repo.name}#${pull.number}`,
356 });
357 }
358 let quietest: number | null = null;
359 for (const run of liveRuns) {
360 const minutes = stuckMinutes(run, now);
361 if (minutes == null) continue;
362 quietest = Math.max(quietest ?? 0, minutes);
363 needs.push({
364 key: `run:${run.id}`,
365 kind: "stuck",
366 title: run.title ?? `${run.agent}'s run`,
367 detail: `${run.agent} has reported nothing for ${minutes} min${run.step ? `. Last: ${run.step}` : ""}.`,
368 to: `/${run.repo.namespace}/${run.repo.name}/agents/runs/${run.id}`,
369 action: "Look",
370 at: Date.parse(run.updatedAt),
371 where: `${run.repo.namespace}/${run.repo.name}${run.number != null ? `#${run.number}` : ""}`,
372 });
373 }
374
375 // --- Activity -------------------------------------------------------------
376 const items: ActivityItem[] = [];
377 const allEvents: { repo: string; event: G1tEvent }[] = [];
378 for (const { repo, events } of perRepo ?? []) {
379 for (const event of events ?? []) {
380 allEvents.push({ repo: repo.id, event });
381 const item = eventItem(event, pathOf(repo));
382 if (item) items.push(item);
383 }
384 }
385 for (const entry of overviewList ?? []) {
386 const latest = entry.latest;
387 const served = latest?.status === "ready" || latest?.status === "replaced";
388 if (!slug || latest?.kind !== "production" || (!served && latest.status !== "failed")) continue;
389 items.push({
390 id: `deploy:${latest.id}`,
391 at: Date.parse(latest.finishedAt ?? latest.createdAt),
392 repo: { namespace: slug, name: entry.slug },
393 actor: latest.createdBy,
394 verb: served ? "deployed" : "deploy_failed",
395 number: null,
396 to: `/${slug}/${entry.slug}/deployments/${latest.id}`,
397 });
398 }
399 for (const memory of okOr(memories ?? null)?.workspace ?? []) {
400 const repo = memory.repo ?? memory.source.repo;
401 if (!repo || !slug) continue;
402 items.push({
403 id: `memory:${memory.id}`,
404 at: Date.parse(memory.createdAt),
405 repo,
406 actor: memory.createdBy,
407 verb: "learned",
408 number: null,
409 text: memory.text.length > 90 ? `${memory.text.slice(0, 88)}…` : memory.text,
410 to: `/${slug}/-/memory`,
411 });
412 }
413 const groups = groupActivity(items).slice(0, 60);
414
415 // --- Since you were last here --------------------------------------------
416 const since = seen.since ?? now - TIME.DAY;
417 const after = (verb: ActivityItem["verb"]) => items.filter((item) => item.verb === verb && item.at > since).length;
418 const digest = digestParts({
419 landed: after("landed"),
420 reviews: reviewRequested.length,
421 opened: after("opened_issue"),
422 deploys: after("deployed"),
423 failedDeploys: after("deploy_failed"),
424 stuck: quietest,
425 });
426
427 // --- Pulse ----------------------------------------------------------------
428 const week = allEvents.filter(({ event }) => Date.parse(event.time) >= weekAgo);
429 const mergedWeek = week.filter(({ event }) => event.type === "pull.merged");
430 const spans = issueToMerge(
431 allEvents.flatMap(({ repo, event }) => (event.type === "issue.opened" ? [{ repo, number: event.data.number, at: Date.parse(event.time) }] : [])),
432 mergedWeek.flatMap(({ repo, event }) => (event.type === "pull.merged" ? [{ repo, issue: event.data.issue ?? null, at: Date.parse(event.time) }] : [])),
433 );
434 const checkEvents = allEvents.flatMap(({ repo, event }) =>
435 event.type === "checks.completed" ? [{ repo, number: event.data.number, at: Date.parse(event.time), passed: event.data.status === "passed" }] : [],
436 );
437 const firstPass = firstPassRate(checkEvents.filter((c) => c.at >= weekAgo));
438 const usageOk = okOr(usage ?? null);
439 const costPoints = usageOk
440 ? usageOk.byDay.map((slice) => ({ at: Date.parse(`${slice.key.split("/")[0]}T12:00:00Z`), value: slice.micros / 1_000_000 }))
441 : runList.filter((run) => run.costUsd != null).map((run) => ({ at: Date.parse(run.createdAt), value: run.costUsd ?? 0 }));
442 const costDays = dailyBuckets(costPoints, 7, now);
443 const pulse = {
444 merged: mergedWeek.length,
445 mergedDays: dailyBuckets(mergedWeek.map(({ event }) => ({ at: Date.parse(event.time) })), 7, now),
446 hours: agentHours(runList, weekAgo, now),
447 hoursDays: dailyBuckets(
448 runList
449 .filter((run) => run.startedAt)
450 .map((run) => ({ at: Date.parse(run.startedAt!), value: agentHours([run], weekAgo, now) })),
451 7,
452 now,
453 ),
454 cost: costDays.reduce((sum, v) => sum + v, 0),
455 costDays,
456 issueToMerge: median(spans),
457 mergedWithIssue: spans.length,
458 firstPass: firstPass.rate,
459 firstPassOf: firstPass.of,
460 };
461
462 // --- Projects -------------------------------------------------------------
463 const byRepoName = new Map((perRepo ?? []).map((entry) => [`${entry.repo.namespace}/${entry.repo.name}`.toLowerCase(), entry]));
464 const projectsHealth: ProjectHealth[] = (projectsOk ?? []).slice(0, 12).map((project) => {
465 const repo = project.source.kind === "hosted" ? project.source.repo : null;
466 const key = repo ? `${repo.namespace}/${repo.name}`.toLowerCase() : "";
467 const entry = byRepoName.get(key);
468 const deploy = overviewList?.find((d) => d.slug === project.slug) ?? null;
469 const checks = (entry?.events ?? []).flatMap((event) => (event.type === "checks.completed" ? [event.data.status === "passed"] : []));
470 return {
471 slug: project.slug,
472 name: project.name,
473 private: project.private,
474 repo,
475 deploys: deploy?.enabled ?? false,
476 production: deploy?.production ?? null,
477 latest: deploy?.latest ?? null,
478 openPulls: entry?.pulls ? entry.pulls.length : null,
479 agents: liveRuns.filter((run) => repo && `${run.repo.namespace}/${run.repo.name}`.toLowerCase() === key).length,
480 passRate: checks.length ? checks.filter(Boolean).length / checks.length : null,
481 checks: checks.length,
482 };
483 });
484
485 const models_ = models ?? null;
486 times.total = Date.now() - started;
Agents as a team: lifecycle, merge queue, billing and a new shell487 const serverTiming = Object.entries(times)
488 .map(([name, ms]) => `${name};dur=${ms}`)
489 .join(", ");
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API490 const secure = new URL(request.url).protocol === "https:" ? "; Secure" : "";
491 return data(
492 {
493 signedIn: true as const,
494 viewer,
495 name: profile?.name?.trim() || username,
496 greeting: greetingFor(hourIn(now, tz)),
497 seenBefore: seen.since,
498 repos: repoList,
499 canRunAgents: models_ == null || models_.hosted || models_.own != null,
500 trial: models_?.own == null ? (models_?.trial ?? null) : null,
501 active: activeList.map(({ pull, lifecycle, repo }) => ({ pull, lifecycle, repo })),
502 needs: rankNeeds(needs),
503 live: liveRuns.slice(0, 12) as AgentRun[],
504 liveTotal: liveRuns.length,
505 runsLoaded: runs != null && runs.ok,
506 pullsTabs,
507 issuesTabs,
508 perRepoLoaded: perRepo != null,
509 groups,
510 digest,
511 pulse,
512 projects: projectsHealth,
513 projectsLoaded: projectsOk != null,
514 },
515 {
516 headers: {
517 "Server-Timing": serverTiming,
518 "Set-Cookie": `${SEEN_COOKIE}=${seen.value}; Path=/; Max-Age=31536000; HttpOnly; SameSite=Lax${secure}`,
519 },
520 },
521 );
522}
523
524type Loaded = Extract<Route.ComponentProps["loaderData"], { signedIn: true }>;
525
526// --- Pieces ---------------------------------------------------------------------
Agents as a team: lifecycle, merge queue, billing and a new shell527
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API528const TONE = { fg: "text-fg", warn: "text-warn", danger: "text-danger", accent: "text-accent" };
Agents as a team: lifecycle, merge queue, billing and a new shell529
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API530function Digest({ parts, seenBefore }: { parts: Loaded["digest"]; seenBefore: number | null }) {
531 const when = seenBefore ? (
532 <>
533 Since you were last here, <TimeAgo at={seenBefore} />
534 </>
535 ) : (
536 "In the last day"
537 );
538 if (parts.length === 0) {
539 return <p className="mt-1 text-sm text-muted">{when}: nothing new landed, and nothing is waiting on you.</p>;
540 }
541 return (
542 <p className="mt-1 text-sm leading-6 text-muted">
543 {when}:{" "}
544 {parts.map((part, index) => (
545 <span key={part.text}>
546 {index > 0 && (index === parts.length - 1 ? " and " : ", ")}
547 <a href={`#${part.anchor}`} className={`font-medium underline decoration-line-strong underline-offset-4 hover:decoration-current ${TONE[part.tone]}`}>
548 {part.text}
549 </a>
550 </span>
551 ))}
552 .
553 </p>
554 );
Agents as a team: lifecycle, merge queue, billing and a new shell555}
556
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API557function PullList({ rows, empty }: { rows: Loaded["pullsTabs"]["authored"]; empty: ReactNode }) {
558 if (rows.length === 0) return <Quiet>{empty}</Quiet>;
Agents as a team: lifecycle, merge queue, billing and a new shell559 return (
560 <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 API561 {rows.map((row) => (
562 <li key={row.key}>
563 <Link
564 prefetch="intent"
565 to={`/${row.repo.namespace}/${row.repo.name}/pull/${row.number}`}
566 className="flex items-center gap-3 px-4 py-2.5 transition-colors hover:bg-raised"
567 >
568 <PullIcon status={row.status} />
569 <span className="min-w-0 grow">
570 <span className="block truncate text-sm font-medium">{row.title}</span>
571 <span className="block truncate font-mono text-xs text-muted">
572 {row.repo.name}#{row.number}
573 {row.agent && ` · ${row.agent}`}
Agents as a team: lifecycle, merge queue, billing and a new shell574 </span>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API575 </span>
576 {row.stage ? (
577 <span className="hidden shrink-0 items-center gap-2 text-xs text-muted sm:flex">
578 <StageDots stage={row.stage} />
579 <span className={row.stage === "needs_you" ? "text-warn" : ""}>{STAGE_LABEL[row.stage]}</span>
Agents as a team: lifecycle, merge queue, billing and a new shell580 </span>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API581 ) : (
582 <span className="hidden shrink-0 sm:block">
583 <CheckBadge status={row.checkStatus} />
Agents as a team: lifecycle, merge queue, billing and a new shell584 </span>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API585 )}
586 <span className="w-14 shrink-0 text-right text-xs text-faint">
587 <TimeAgo at={row.updatedAt} />
588 </span>
589 </Link>
590 </li>
591 ))}
Agents as a team: lifecycle, merge queue, billing and a new shell592 </ul>
593 );
Initial g1t: services, event bus, intents and attempts594}
595
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API596function IssueList({ rows, empty }: { rows: Loaded["issuesTabs"]["assigned"]; empty: ReactNode }) {
597 if (rows.length === 0) return <Quiet>{empty}</Quiet>;
Agents as a team: lifecycle, merge queue, billing and a new shell598 return (
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API599 <ul className="divide-y divide-line overflow-hidden rounded-xl border border-line bg-surface">
600 {rows.map((row) => (
601 <li key={row.key}>
602 <Link
603 prefetch="intent"
604 to={`/${row.repo.namespace}/${row.repo.name}/issues/${row.number}`}
605 className="flex items-center gap-3 px-4 py-2.5 transition-colors hover:bg-raised"
606 >
607 <IssueIcon issue={{ state: "open", reason: null }} />
608 <span className="min-w-0 grow">
609 <span className="block truncate text-sm font-medium">{row.title}</span>
610 <span className="block truncate font-mono text-xs text-muted">
611 {row.repo.name}#{row.number}
612 </span>
613 </span>
614 {row.agent ? (
615 <span className="hidden shrink-0 items-center gap-1 text-xs text-merged sm:flex">
616 <Bot size={12} /> {row.agent} at work
617 </span>
618 ) : row.queued ? (
619 <span className="hidden shrink-0 text-xs text-muted sm:block">Queued for an agent</span>
620 ) : null}
621 <span className="w-14 shrink-0 text-right text-xs text-faint">
622 <TimeAgo at={row.updatedAt} />
623 </span>
624 </Link>
625 </li>
626 ))}
627 </ul>
Agents as a team: lifecycle, merge queue, billing and a new shell628 );
629}
630
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API631function ProjectCard({ project, workspace }: { project: Loaded["projects"][number]; workspace: string }) {
632 const base = `/${workspace}/${project.slug}`;
633 const latest = project.latest;
634 return (
635 <li className="flex flex-col rounded-xl border border-line bg-surface p-4 transition-colors hover:border-line-strong">
636 <div className="flex items-center gap-2.5">
637 <span className="flex size-7 shrink-0 items-center justify-center rounded-md bg-raised text-muted ring-1 ring-line">
638 {project.private ? <Lock size={13} /> : <Box size={13} />}
639 </span>
640 <Link to={base} prefetch="intent" className="min-w-0 grow truncate font-medium hover:text-accent">
641 {project.name}
642 </Link>
643 {project.agents > 0 && (
644 <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">
645 <span className="size-1.5 animate-pulse rounded-full bg-merged" />
646 {project.agents} at work
647 </span>
648 )}
649 </div>
650 <div className="mt-3 min-h-10 text-xs">
651 {project.production ? (
652 <>
653 <a href={project.production.url} className="flex items-center gap-1 truncate font-mono text-fg-soft hover:text-accent">
654 {host(project.production.url)}
655 <ArrowUpRight size={12} className="shrink-0 text-faint" />
656 </a>
657 <p className="mt-1 flex items-center gap-2 text-muted">
658 {latest && <StatusDot status={latest.status} />}
659 <span>
660 deployed <TimeAgo at={project.production.deployedAt} />
661 </span>
662 </p>
663 </>
664 ) : project.deploys ? (
665 <p className="text-muted">
666 {latest ? <StatusDot status={latest.status} label={`${DEPLOY_STATUS[latest.status].label}, not live yet`} /> : "Deploys on the next push to the default branch."}
667 </p>
668 ) : (
669 <p className="text-faint">
670 Not deployed.{" "}
671 <Link to={`${base}/settings/deployments`} className="text-muted hover:text-fg">
672 Put it on g1t.page
673 </Link>
674 </p>
675 )}
676 </div>
677 <dl className="mt-3 grid grid-cols-3 gap-2 border-t border-line pt-3 text-xs">
678 <div>
679 <dt className="text-faint">Open PRs</dt>
680 <dd className="mt-0.5 font-medium tabular-nums">
681 <Link to={`${base}/pulls`} className="hover:text-accent">
682 {project.openPulls ?? "—"}
683 </Link>
684 </dd>
685 </div>
686 <div>
687 <dt className="text-faint">Agents</dt>
688 <dd className="mt-0.5 font-medium tabular-nums">
689 <Link to={`${base}/agents`} className="hover:text-accent">
690 {project.agents}
691 </Link>
692 </dd>
693 </div>
694 <div title={project.checks ? `${project.checks} recent check runs` : "No recent check runs"}>
695 <dt className="text-faint">Checks</dt>
696 <dd className="mt-0.5 font-medium tabular-nums">{percent(project.passRate)}</dd>
697 <dd className="mt-1">
698 <Meter value={project.passRate} tone={project.passRate != null && project.passRate < 0.7 ? "bg-warn" : "bg-accent"} />
699 </dd>
700 </div>
701 </dl>
702 </li>
703 );
Agents as a team: lifecycle, merge queue, billing and a new shell704}
705
Get started on mission control706type Step = { done: boolean; title: string; about: string; to: string | null; action: string };
707
708/**
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API709 * The first things to do, ticked off as they are done, until work has been
710 * handed to agents.
Workspace things at the bottom of the sidebar, and on mission control711 */
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API712function GetStarted({ steps }: { steps: Step[] }) {
713 const left = steps.filter((step) => !step.done).length;
714 return (
715 <section className="rounded-2xl bg-surface p-5 ring-1 ring-line">
716 <div className="flex items-baseline justify-between gap-3">
717 <h2 className="font-semibold tracking-tight">Get started</h2>
718 <span className="text-xs text-muted">
719 {steps.length - left} of {steps.length} done
720 </span>
721 </div>
722 <ol className="mt-4 space-y-2">
723 {steps.map((step, index) => (
724 <li
725 key={step.title}
726 className={`flex items-start gap-3 rounded-xl px-3 py-2.5 ${step.done ? "" : "bg-bg/50 ring-1 ring-line"}`}
727 >
728 <span
729 className={`mt-0.5 flex size-5 shrink-0 items-center justify-center rounded-full text-[0.6875rem] font-medium ${
730 step.done ? "bg-accent text-bg" : "text-muted ring-1 ring-line-strong"
731 }`}
732 >
733 {step.done ? "✓" : index + 1}
734 </span>
735 <span className="min-w-0 grow">
736 <span className={`block text-sm font-medium ${step.done ? "text-muted line-through decoration-faint" : ""}`}>
737 {step.title}
738 </span>
739 {!step.done && <span className="mt-0.5 block text-xs leading-5 text-muted">{step.about}</span>}
740 </span>
741 {!step.done && step.to && (
742 <Link
743 to={step.to}
744 className="shrink-0 rounded-md bg-fg px-2.5 py-1 text-xs font-medium text-bg transition-colors hover:bg-white"
745 >
746 {step.action}
747 </Link>
748 )}
749 </li>
750 ))}
751 </ol>
752 </section>
753 );
754}
755
756/** The workspace being looked at: what it holds and what is moving in it. */
Workspace names and icons, and a component kit for every control757function WorkspaceCard({
758 slug,
759 name,
760 avatar,
761 role,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API762 projects,
763 needs,
764 live,
Workspace names and icons, and a component kit for every control765}: {
766 slug: string;
767 name: string;
768 avatar?: string | null;
769 role: string;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API770 projects: number;
771 needs: number;
772 live: number;
Workspace names and icons, and a component kit for every control773}) {
Workspace things at the bottom of the sidebar, and on mission control774 const stats = [
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API775 { value: projects, label: "projects" },
776 { value: live, label: "agents at work" },
777 { value: needs, label: "need you" },
Workspace things at the bottom of the sidebar, and on mission control778 ];
779 return (
780 <div className="rounded-xl border border-line bg-surface p-5">
781 <div className="flex items-center gap-3">
Workspace names and icons, and a component kit for every control782 <Avatar name={slug} image={avatar} size={32} square />
Workspace things at the bottom of the sidebar, and on mission control783 <div className="min-w-0">
Workspace names and icons, and a component kit for every control784 <Link to={`/${slug}`} title={name} className="block truncate font-medium hover:underline">
785 {name}
Workspace things at the bottom of the sidebar, and on mission control786 </Link>
Workspace names and icons, and a component kit for every control787 <p className="truncate text-xs text-muted">
788 <span className="font-mono">g1t.sh/{slug}</span> · <span className="capitalize">{role}</span>
789 </p>
Workspace things at the bottom of the sidebar, and on mission control790 </div>
791 </div>
792 <dl className="mt-4 grid grid-cols-3 gap-2">
793 {stats.map((stat) => (
794 <div key={stat.label} className="rounded-lg bg-bg/60 px-2.5 py-2 ring-1 ring-line">
795 <dt className="sr-only">{stat.label}</dt>
796 <dd className="text-lg font-semibold tabular-nums tracking-tight">{stat.value}</dd>
797 <dd className="text-[0.6875rem] leading-tight text-muted">{stat.label}</dd>
798 </div>
799 ))}
800 </dl>
801 <div className="mt-4 flex flex-wrap gap-x-4 gap-y-1 text-sm">
802 <Link to={`/${slug}/-/people`} className="inline-flex items-center gap-1.5 text-muted hover:text-fg">
803 <Users size={13} />
804 Members
805 </Link>
806 <Link
807 to={role === "owner" ? `/${slug}/-/settings` : `/${slug}/-/people`}
808 className="inline-flex items-center gap-1.5 text-muted hover:text-fg"
809 >
810 <Settings size={13} />
811 Settings
812 </Link>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API813 <Link to={`/${slug}/-/agents`} className="inline-flex items-center gap-1.5 text-muted hover:text-fg">
814 <Radio size={13} />
815 Fleet
Workspace things at the bottom of the sidebar, and on mission control816 </Link>
817 </div>
818 </div>
819 );
820}
821
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API822function UsageCard({ shell, weekCost }: { shell: ShellData; weekCost: number }) {
823 const slug = shell.workspace?.slug;
824 const used = shell.monthUsageMicros ?? 0;
825 const limit = shell.limit;
826 const ceiling = limit && !limit.comped ? limit.ceilingMicros : null;
827 const share = ceiling ? Math.min(1, limit!.exposureMicros / ceiling) : null;
828 const month = new Date().toLocaleDateString("en-US", { month: "long" });
Get started on mission control829 return (
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API830 <div className="rounded-xl border border-line bg-surface p-5">
831 <div className="flex items-center justify-between">
832 <h2 className="flex items-center gap-1.5 text-sm font-semibold">
833 <Coins size={14} className="text-faint" />
834 Usage in {month}
835 </h2>
836 {slug && (
837 <Link to={`/${slug}/-/usage`} className="text-xs text-muted hover:text-fg">
838 Details
839 </Link>
840 )}
Get started on mission control841 </div>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API842 <p className="mt-3 text-2xl font-semibold tracking-tight tabular-nums">{dollars(used)}</p>
843 <p className="text-xs text-muted">
844 {shell.free ? "At cost. g1t charges nothing while it is being built out." : "Charged so far this month."}
845 {weekCost > 0 && ` ${formatCost(weekCost)} of it in the last 7 days.`}
846 </p>
847 {ceiling != null && (
848 <div className="mt-3">
849 <Meter value={share} tone={limit!.state === "ok" ? "bg-accent" : limit!.state === "warning" ? "bg-warn" : "bg-danger"} />
850 <p className="mt-1.5 text-[0.6875rem] text-faint">
851 {dollars(limit!.exposureMicros)} of the {dollars(ceiling)} limit
852 </p>
853 </div>
854 )}
855 </div>
856 );
857}
858
859function WhatsNew({ workspace }: { workspace: string | null }) {
860 return (
861 <div className="rounded-xl border border-line bg-surface p-5">
862 <h2 className="flex items-center gap-1.5 text-sm font-semibold">
863 <Sparkles size={14} className="text-accent" />
864 What's new on g1t
865 </h2>
866 <ul className="mt-3 space-y-3">
867 {CHANGELOG.slice(0, 5).map((entry) => {
868 const href = changelogHref(entry, workspace);
869 const title = <span className="text-sm font-medium">{entry.title}</span>;
870 return (
871 <li key={entry.title}>
872 {href ? (
873 <Link to={href} className="hover:text-accent">
874 {title}
875 </Link>
876 ) : (
877 title
878 )}
879 <p className="mt-0.5 text-xs leading-5 text-muted">{entry.about}</p>
880 </li>
881 );
882 })}
883 </ul>
884 <a href="https://docs.g1t.sh/" className="mt-4 inline-flex items-center gap-1 text-xs text-muted hover:text-fg">
885 Read the docs <ArrowRight size={12} />
886 </a>
887 </div>
Get started on mission control888 );
889}
890
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API891// --- The page -------------------------------------------------------------------
Agents as a team: lifecycle, merge queue, billing and a new shell892
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API893export default function Home({ loaderData }: Route.ComponentProps) {
894 const shell = useRouteLoaderData("root")?.shell as ShellData | null | undefined;
895 const loaded = loaderData.signedIn ? loaderData : null;
Agents as a team: lifecycle, merge queue, billing and a new shell896 // 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 API897 const changing =
898 (loaded?.liveTotal ?? 0) > 0 ||
899 (loaded?.active ?? []).some((item) => item.lifecycle && item.lifecycle.stage !== "needs_you" && item.lifecycle.stage !== "ready");
900 useLiveRefresh(changing);
901 const [greeting, setGreeting] = useState(loaded?.greeting ?? "");
902 const [pullsTab, setPullsTab] = useState(() => (loaded && loaded.pullsTabs.review.length > 0 ? "review" : "authored"));
Agents as a team: lifecycle, merge queue, billing and a new shell903 useEffect(() => {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API904 // The greeting follows the viewer's own clock, and the server learns
905 // their zone for next time.
906 setGreeting(greetingFor(new Date().getHours()));
907 try {
908 const zone = Intl.DateTimeFormat().resolvedOptions().timeZone;
909 if (zone) document.cookie = `${TZ_COOKIE}=${encodeURIComponent(zone)}; Path=/; Max-Age=31536000; SameSite=Lax`;
910 } catch {
911 // Nothing to remember.
912 }
913 }, []);
Agents as a team: lifecycle, merge queue, billing and a new shell914
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API915 if (!loaded) return <Landing repos={loaderData.repos} />;
916 const { viewer, repos, active, needs: serverNeeds, live, liveTotal, pullsTabs, issuesTabs, groups, pulse, projects, canRunAgents, trial } = loaded;
Initial g1t: services, event bus, intents and attempts917
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API918 const workspace = shell?.workspace?.slug ?? viewer.workspaces?.[0]?.slug ?? null;
919 // The usage limit, as the sidebar knows it, heads the list when it bites.
920 const limitNeed: Need[] =
921 shell?.limit && !shell.limit.comped && shell.limit.state !== "ok" && workspace
922 ? [
923 {
924 key: "limit",
925 kind: "limit",
926 title: shell.limit.state === "stopped" ? "Agents are stopped: the usage limit was reached" : "Usage is nearing its limit",
927 detail:
928 shell.limit.state === "stopped"
929 ? "No new agent starts until the limit is raised or a card is added."
930 : `${dollars(shell.limit.exposureMicros)} of ${dollars(shell.limit.ceilingMicros ?? 0)} used. Add a card so work keeps going.`,
931 to: `/${workspace}/-/billing`,
932 action: "Billing",
933 at: Date.now(),
934 where: null,
935 },
936 ]
937 : [];
938 const needs = rankNeeds([...limitNeed, ...serverNeeds]);
939
Get started on mission control940 const first = repos[0];
Usage while free is shown at cost; agents get rustfmt and clippy941 const handedOff = active.length > 0 || (shell?.monthUsageMicros ?? 0) > 0;
Get started on mission control942 const steps: Step[] = [
943 {
944 done: Boolean(workspace),
945 title: "Create a workspace",
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API946 about: "Projects, people and agent credit live in one.",
Get started on mission control947 to: "/workspaces/new",
948 action: "Create",
949 },
950 {
951 done: repos.length > 0,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API952 title: "Add a project",
953 about: "Create a repository, or import one by its address. Push to it with git as usual.",
Get started on mission control954 to: workspace ? `/new?workspace=${workspace}` : "/new",
955 action: "Add",
956 },
A free allowance on g1t's models, so anyone can try its agents957 trial?.open
New workspaces are told to connect a model, before anything fails958 ? {
A free allowance on g1t's models, so anyone can try its agents959 done: true,
960 title: "Try g1t's agents free",
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API961 about: `This workspace has ${dollars(trial.limitMicros - trial.usedMicros)} of its free ${dollars(trial.limitMicros)} left on g1t's models.`,
A free allowance on g1t's models, so anyone can try its agents962 to: workspace ? `/${workspace}/-/integrations` : null,
963 action: "Connect",
964 }
965 : canRunAgents
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API966 ? {
967 done: Boolean(shell?.free) || shell?.limit == null || shell.limit.comped || shell.limit.state === "ok",
968 title: "Keep work running",
969 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.",
970 to: workspace ? `/${workspace}/-/billing` : null,
971 action: "Billing",
972 }
973 : {
974 done: false,
975 title: "Connect a model",
976 about: `${
977 trial?.reason === "used"
978 ? "This workspace has used its free allowance on g1t's models. "
979 : trial?.reason === "pool" || trial?.reason === "ended"
980 ? "The free allowance on g1t's models has ended. "
981 : ""
982 }Agents need a model to think with. Connect your Anthropic or OpenAI key, or any compatible endpoint.`,
983 to: workspace ? `/${workspace}/-/integrations` : null,
984 action: "Connect",
985 },
Get started on mission control986 {
987 done: handedOff,
988 title: "Hand off an outcome",
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API989 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 control990 to: first ? `/${first.namespace}/${first.name}/plans` : null,
991 action: "Write one",
992 },
993 ];
994 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 API995 const counts = (n: number) => (n > 0 ? <span className="ml-1 tabular-nums text-faint">{n}</span> : null);
Get started on mission control996
API and MCP server, Rust identity service, registration, site redesign997 return (
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API998 <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 redesign999 <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 API1000 <header>
1001 <h1 className="text-2xl font-semibold tracking-tight sm:text-[1.75rem]" suppressHydrationWarning>
1002 {greeting}, {loaded.name}
1003 </h1>
1004 <Digest parts={loaded.digest} seenBefore={loaded.seenBefore} />
1005 </header>
API and MCP server, Rust identity service, registration, site redesign1006
Get started on mission control1007 {starting && <GetStarted steps={steps} />}
1008
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1009 <Panel id="needs-you" title="Needs you" icon={<Hand size={14} className="text-warn" />} count={needs.length}>
1010 {needs.length === 0 ? (
1011 <Quiet>Nothing is waiting on you. Reviews, stopped work and failed deploys show up here first.</Quiet>
1012 ) : (
1013 <NeedsList needs={needs} />
1014 )}
1015 </Panel>
Agents as a team: lifecycle, merge queue, billing and a new shell1016
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1017 <Panel
1018 id="live"
1019 title="Live now"
1020 icon={<span className={`block size-2 rounded-full ${liveTotal > 0 ? "animate-pulse bg-merged" : "bg-line-strong"}`} />}
1021 count={liveTotal}
1022 all={workspace ? { to: `/${workspace}/-/agents`, label: "Fleet" } : null}
1023 >
1024 {!loaded.runsLoaded && workspace ? (
1025 <Unavailable what="Agent runs" />
1026 ) : live.length === 0 ? (
Mission control drops the composer for now1027 <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 API1028 ) : (
1029 <ul className="space-y-3">
1030 {live.map((run) => (
1031 <RunCard key={run.id} run={run} member showRepo />
1032 ))}
1033 {liveTotal > live.length && workspace && (
1034 <li className="text-xs text-muted">
1035 <Link to={`/${workspace}/-/agents`} className="hover:text-fg">
1036 {liveTotal - live.length} more at work in the fleet
Agents as a team: lifecycle, merge queue, billing and a new shell1037 </Link>
1038 </li>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1039 )}
Agents as a team: lifecycle, merge queue, billing and a new shell1040 </ul>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1041 )}
1042 </Panel>
1043
1044 <Panel
1045 title="This week"
1046 icon={<ActivityIcon size={14} />}
1047 all={workspace ? { to: `/${workspace}/-/usage`, label: "Usage" } : null}
1048 >
1049 <div className="grid grid-cols-2 gap-3 sm:grid-cols-3 xl:grid-cols-5">
1050 <PulseTile
1051 label="Changes landed"
1052 value={String(pulse.merged)}
1053 hint="last 7 days"
1054 values={pulse.mergedDays}
1055 format={(v) => `${v} landed`}
1056 />
1057 <PulseTile
1058 label="Agent hours"
1059 value={pulse.hours < 10 ? pulse.hours.toFixed(1) : String(Math.round(pulse.hours))}
1060 hint="time agents spent working"
1061 values={pulse.hoursDays}
1062 format={(v) => `${v.toFixed(1)} h`}
1063 to={workspace ? `/${workspace}/-/agents` : undefined}
1064 />
1065 <PulseTile
1066 label="Agent cost"
1067 value={formatCost(pulse.cost) ?? "$0.00"}
1068 hint="model and sandbox"
1069 values={pulse.costDays}
1070 format={(v) => formatCost(v) ?? "$0.00"}
1071 to={workspace ? `/${workspace}/-/usage` : undefined}
1072 />
1073 <PulseTile
1074 label="Issue to landed"
1075 value={formatSpan(pulse.issueToMerge)}
1076 hint={pulse.mergedWithIssue ? `median of ${pulse.mergedWithIssue}` : "no issue landed yet"}
1077 />
1078 <PulseTile
1079 label="Checks pass first time"
1080 value={percent(pulse.firstPass)}
1081 hint={pulse.firstPassOf ? `of ${pulse.firstPassOf} pull requests` : "no checks ran yet"}
1082 />
1083 </div>
1084 </Panel>
1085
1086 <div className="grid gap-10 xl:grid-cols-2">
1087 <Tabs value={pullsTab} onValueChange={setPullsTab}>
1088 <Panel
1089 id="your-pulls"
1090 title="Your pull requests"
1091 icon={<GitPullRequest size={14} />}
1092 all={{ to: `/u/${viewer.username}?tab=pulls` }}
1093 >
1094 <TabsList>
1095 <TabsTrigger value="authored">Opened{counts(pullsTabs.authored.length)}</TabsTrigger>
1096 <TabsTrigger value="review">Review requested{counts(pullsTabs.review.length)}</TabsTrigger>
1097 <TabsTrigger value="assigned">Assigned{counts(pullsTabs.assigned.length)}</TabsTrigger>
1098 </TabsList>
1099 <TabsContent value="authored">
1100 <PullList rows={pullsTabs.authored} empty="None open. Pull requests you or your agents start show here with where each stands." />
1101 </TabsContent>
1102 <TabsContent value="review">
1103 {loaded.perRepoLoaded ? (
1104 <PullList rows={pullsTabs.review} empty="No one is waiting on your review." />
1105 ) : (
1106 <Unavailable what="Review requests" />
1107 )}
1108 </TabsContent>
1109 <TabsContent value="assigned">
1110 <PullList rows={pullsTabs.assigned} empty="No open pull request is assigned to you." />
1111 </TabsContent>
1112 </Panel>
1113 </Tabs>
1114
1115 <Tabs defaultValue="assigned">
1116 <Panel title="Your issues" icon={<CircleDot size={14} />} all={{ to: `/u/${viewer.username}?tab=issues` }}>
1117 <TabsList>
1118 <TabsTrigger value="assigned">Assigned{counts(issuesTabs.assigned.length)}</TabsTrigger>
1119 <TabsTrigger value="authored">Opened{counts(issuesTabs.authored.length)}</TabsTrigger>
1120 </TabsList>
1121 <TabsContent value="assigned">
1122 <IssueList rows={issuesTabs.assigned} empty="Nothing is assigned to you." />
1123 </TabsContent>
1124 <TabsContent value="authored">
1125 <IssueList rows={issuesTabs.authored} empty="You have no open issues." />
1126 </TabsContent>
1127 </Panel>
1128 </Tabs>
1129 </div>
Agents as a team: lifecycle, merge queue, billing and a new shell1130
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1131 <Panel id="activity" title="Activity" icon={<ActivityIcon size={14} />}>
1132 {!loaded.perRepoLoaded ? (
1133 <Unavailable what="Activity" />
Agents as a team: lifecycle, merge queue, billing and a new shell1134 ) : (
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1135 <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 shell1136 )}
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1137 </Panel>
API and MCP server, Rust identity service, registration, site redesign1138
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1139 {workspace && (
1140 <Panel
1141 id="projects"
1142 title="Projects"
1143 icon={<Box size={14} />}
1144 count={projects.length}
1145 all={{ to: `/${workspace}`, label: "Workspace" }}
1146 extra={
1147 <Link to={`/new?workspace=${workspace}`} className="inline-flex items-center gap-1 text-xs text-muted hover:text-fg">
1148 <Plus size={12} /> New
1149 </Link>
1150 }
1151 >
1152 {!loaded.projectsLoaded ? (
1153 <Unavailable what="Projects" />
1154 ) : projects.length === 0 ? (
1155 <Quiet>No projects yet. Create a repository or import one, and it gets a project of its own.</Quiet>
1156 ) : (
1157 <ul className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
1158 {projects.map((project) => (
1159 <ProjectCard key={project.slug} project={project} workspace={workspace} />
1160 ))}
1161 </ul>
1162 )}
1163 </Panel>
1164 )}
API and MCP server, Rust identity service, registration, site redesign1165 </div>
Initial g1t: services, event bus, intents and attempts1166
API and MCP server, Rust identity service, registration, site redesign1167 <aside className="space-y-4">
Workspace things at the bottom of the sidebar, and on mission control1168 {shell?.workspace && (
1169 <WorkspaceCard
1170 slug={shell.workspace.slug.toLowerCase()}
Workspace names and icons, and a component kit for every control1171 name={shell.workspace.name || shell.workspace.slug}
1172 avatar={shell.workspace.avatar}
Workspace things at the bottom of the sidebar, and on mission control1173 role={shell.workspace.role}
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1174 projects={shell.repos.length}
1175 needs={needs.length}
1176 live={liveTotal}
Workspace things at the bottom of the sidebar, and on mission control1177 />
1178 )}
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1179 {shell?.workspace && <UsageCard shell={shell} weekCost={pulse.cost} />}
1180 <WhatsNew workspace={workspace} />
API and MCP server, Rust identity service, registration, site redesign1181 <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 API1182 <h2 className="text-sm font-semibold">Connect your own agent</h2>
1183 <p className="mt-1.5 text-xs leading-5 text-muted">
1184 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 redesign1185 </p>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1186 <div className="mt-3">
1187 <CopyLine prompt text="claude mcp add --transport http g1t https://mcp.g1t.sh" />
API and MCP server, Rust identity service, registration, site redesign1188 </div>
1189 <Link
Docs as their own app; shared theme package1190 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 API1191 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 redesign1192 >
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1193 How it works <ArrowRight size={12} />
API and MCP server, Rust identity service, registration, site redesign1194 </Link>
1195 </div>
1196 </aside>
Initial g1t: services, event bus, intents and attempts1197 </main>
1198 );
1199}