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

1,379 lines55,540 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 Eye,
10 FolderPlus,
11 GitPullRequest,
12 Hand,
13 Lock,
14 Map as MapIcon,
15 Plus,
16 Radio,
17 Send,
18 Settings,
19 Sparkles,
20 Users,
21} from "lucide-react";
New workspaces are told to connect a model, before anything fails22import { env } from "cloudflare:workers";
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API23import { type ReactNode, useEffect, useRef, useState } from "react";
24import { Form, Link, data, redirect, useNavigation, useRouteLoaderData } from "react-router";
Agents as a team: lifecycle, merge queue, billing and a new shell25
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API26import {
27 type AgentRun,
28 type AuthoredItem,
29 type CheckStatus,
30 type Deployment,
31 type G1tEvent,
32 type Issue,
33 type Lifecycle,
34 type LiveApp,
35 type Pull,
36 type PullStatus,
37 type Repo,
38 type RepoPath,
39 type Stage,
40 isActiveRun,
41} from "@g1t/contracts";
Initial g1t: services, event bus, intents and attempts42
43import type { Route } from "./+types/home";
Social cards for every page: og.g1t.sh44import { page } from "../lib/meta";
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API45import { CHANGELOG, changelogHref } from "../lib/changelog";
46import {
47 type ActivityItem,
48 type Need,
49 SEEN_COOKIE,
50 TIME,
51 agentHours,
52 dailyBuckets,
53 digestParts,
54 eventItem,
55 firstPassRate,
56 formatSpan,
57 greetingFor,
58 groupActivity,
59 hourIn,
60 issueToMerge,
61 median,
62 nextSeen,
63 rankNeeds,
64 readCookie,
65 splitRequest,
66 stuckMinutes,
67} from "../lib/mission";
API and MCP server, Rust identity service, registration, site redesign68import { Landing } from "../components/landing";
Get started on mission control69import type { ShellData } from "../components/shell";
Agents as a team: lifecycle, merge queue, billing and a new shell70import { 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 API71import { RunCard, formatCost, useLiveRefresh } from "../components/agents";
72import { CheckBadge } from "../components/checks";
73import { DEPLOY_STATUS, StatusDot, host } from "../components/deploy";
74import { ActivityFeed, Meter, NeedsList, Panel, PulseTile, Quiet, Unavailable, percent } from "../components/mission";
75import { Avatar, CopyLine, TimeAgo } from "../components/ui";
76import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../components/ui/select";
77import { Switch } from "../components/ui/switch";
78import { Tabs, TabsContent, TabsList, TabsTrigger } from "../components/ui/tabs";
79import { IssueIcon, PullIcon } from "../components/work";
API and MCP server, Rust identity service, registration, site redesign80import {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API81 agents,
82 billing,
83 deployments,
84 events as eventLog,
85 identity,
86 projects as projectsApi,
87 repos as reposApi,
88 work,
89} from "../lib/services.server";
90import { assertSameOrigin, getViewer, requireUser, roleIn } from "../lib/session.server";
Initial g1t: services, event bus, intents and attempts91
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API92/** The viewer's time zone, set by the page itself, so the greeting fits their day. */
93const TZ_COOKIE = "g1t_tz";
94/** The most projects whose pull requests and events are read for the page. */
95const MAX_PROJECTS = 10;
96const EVENTS_PER_PROJECT = 80;
Agents as a team: lifecycle, merge queue, billing and a new shell97
A free allowance on g1t's models, so anyone can try its agents98const dollars = (micros: number) => `$${(Math.max(0, micros) / 1_000_000).toFixed(2)}`;
99
Social cards for every page: og.g1t.sh100export function meta(args: Route.MetaArgs) {
101 return page(args, {
102 title: "g1t — Git for AI scale",
103 description:
104 "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.",
105 });
Initial g1t: services, event bus, intents and attempts106}
107
Agents as a team: lifecycle, merge queue, billing and a new shell108export function headers({ loaderHeaders }: Route.HeadersArgs) {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API109 const out = new Headers();
110 const timing = loaderHeaders.get("Server-Timing");
111 if (timing) out.set("Server-Timing", timing);
112 const cookie = loaderHeaders.get("Set-Cookie");
113 if (cookie) out.set("Set-Cookie", cookie);
114 return out;
Agents as a team: lifecycle, merge queue, billing and a new shell115}
116
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API117/** A row of the viewer's pull requests, whichever list it comes from. */
118type PullRow = {
119 key: string;
120 repo: RepoPath;
121 number: number;
122 title: string;
123 status: PullStatus;
124 agent: string | null;
125 stage: Stage | null;
126 detail: string | null;
127 checkStatus: CheckStatus | null;
128 updatedAt: string;
129};
130
131/** A row of the viewer's issues. */
132type IssueRow = {
133 key: string;
134 repo: RepoPath;
135 number: number;
136 title: string;
137 agent: string | null;
138 queued: boolean;
139 updatedAt: string;
140};
141
142type ProjectHealth = {
143 slug: string;
144 name: string;
145 private: boolean;
146 repo: RepoPath | null;
147 deploys: boolean;
148 production: LiveApp | null;
149 latest: Deployment | null;
150 openPulls: number | null;
151 agents: number;
152 passRate: number | null;
153 checks: number;
154};
155
156const pullRow = (pull: Pull, repo: RepoPath, lifecycle: Lifecycle | null): PullRow => ({
157 key: pull.id,
158 repo,
159 number: pull.number,
160 title: pull.title,
161 status: pull.status,
162 agent: pull.agent,
163 stage: lifecycle?.stage ?? null,
164 detail: lifecycle?.detail ?? null,
165 checkStatus: pull.checkStatus,
166 updatedAt: pull.updatedAt,
167});
168
169const authoredPull = (item: AuthoredItem): PullRow => ({
170 key: `${item.repo.namespace}/${item.repo.name}#${item.number}`,
171 repo: item.repo,
172 number: item.number,
173 title: item.title,
174 status: item.status ?? (item.draft ? "draft" : "open"),
175 agent: null,
176 stage: null,
177 detail: null,
178 checkStatus: null,
179 updatedAt: item.updatedAt,
180});
181
182export async function loader({ context, request }: Route.LoaderArgs) {
Initial g1t: services, event bus, intents and attempts183 const viewer = getViewer(context);
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API184 if (!viewer) {
185 const repos = await reposApi.list(viewer, { memberOnly: false });
186 return data({ signedIn: false as const, repos });
187 }
188
189 const now = Date.now();
190 const weekAgo = now - 7 * TIME.DAY;
Agents as a team: lifecycle, merge queue, billing and a new shell191 const started = Date.now();
192 const times: Record<string, number> = {};
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API193 // Each call is timed, and one that fails leaves its section empty rather
194 // than taking the page down.
195 const soft = <T,>(name: string, promise: Promise<T>): Promise<T | null> =>
196 promise.then(
197 (value) => ((times[name] = Date.now() - started), value),
198 (error) => {
199 console.warn(`home: ${name} failed`, error);
200 times[name] = Date.now() - started;
201 return null;
202 },
203 );
204 const okOr = <T,>(result: { ok: true; value: T } | { ok: false } | null): T | null => (result?.ok ? result.value : null);
205
206 const cookies = request.headers.get("cookie");
207 const seen = nextSeen(readCookie(cookies, SEEN_COOKIE), now);
208 const tz = readCookie(cookies, TZ_COOKIE);
209 const memberships = viewer.workspaces ?? [];
210 const slug = memberships[0]?.slug ?? null;
211 const mine = new Set(memberships.map((m) => m.slug.toLowerCase()));
212 const username = viewer.username;
213
214 const reposP = soft("repos", reposApi.list(viewer, { memberOnly: true }));
215 // The workspace's projects' open pull requests and recent events, read
216 // once each, all at once, as soon as the projects are known.
217 const perRepoP = reposP.then((repos) =>
218 Promise.all(
219 (repos ?? [])
220 .filter((repo) => mine.has(repo.namespace.toLowerCase()))
221 .slice(0, MAX_PROJECTS)
222 .map(async (repo) => {
223 const path = { namespace: repo.namespace, name: repo.name };
224 const [pulls, log] = await Promise.all([
225 work.listPulls(path, viewer, "open").catch(() => null),
226 eventLog.list({ repoId: repo.id, limit: EVENTS_PER_PROJECT }).catch(() => null),
227 ]);
228 return { repo, pulls: pulls?.ok ? pulls.value.slice(0, 60) : null, events: log };
229 }),
230 ),
231 );
232
233 const [repos, perRepo, active, assigned, models, profile, runs, overview, usage, authoredPulls, authoredIssues, projectList, memories] =
234 await Promise.all([
235 reposP,
236 soft("projects", perRepoP),
237 soft("pulls", work.listActivePulls(viewer)),
238 soft("assigned", work.listAssignedIssues(viewer)),
239 slug ? soft("models", env.RUNNER.modelAccess(slug)) : null,
240 soft("profile", identity.profile(username)),
241 slug ? soft("runs", agents.listRuns(viewer, { workspace: slug, limit: 150 })) : null,
242 slug ? soft("deploys", deployments.overview(slug, viewer)) : null,
243 slug ? soft("usage", billing.usage(slug, viewer, new Date(weekAgo - TIME.DAY).toISOString())) : null,
244 soft("authoredPulls", work.byAuthor(username, viewer, { kind: "pull", state: "open", sort: "updated", limit: 10 })),
245 soft("authoredIssues", work.byAuthor(username, viewer, { kind: "issue", state: "open", sort: "updated", limit: 10 })),
246 slug ? soft("projectList", projectsApi.list(slug, viewer)) : null,
247 slug ? soft("memories", agents.listMemories(viewer, slug, null)) : null,
248 ]);
249
250 const repoList = repos ?? [];
Agents as a team: lifecycle, merge queue, billing and a new shell251 // 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 API252 // the viewer's own, already listed; the rest are looked up once each.
253 const known = new Map<string, Repo>(repoList.map((repo) => [repo.id, repo]));
Agents as a team: lifecycle, merge queue, billing and a new shell254 const missing = [
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API255 ...new Set([...(assigned ?? []).map((issue) => issue.repoId), ...(active ?? []).map(({ pull }) => pull.repoId)]),
Agents as a team: lifecycle, merge queue, billing and a new shell256 ].filter((id) => !known.has(id));
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API257 const looked = await Promise.all(missing.map((id) => reposApi.getById(id, viewer).catch(() => null)));
258 for (const found of looked) if (found?.ok) known.set(found.value.id, found.value);
259 const pathOf = (repo: Repo): RepoPath => ({ namespace: repo.namespace, name: repo.name });
260
261 const activeList = (active ?? []).flatMap((item) => {
262 const repo = known.get(item.pull.repoId);
263 return repo ? [{ ...item, repo }] : [];
264 });
265 const runList = okOr(runs ?? null) ?? [];
266 const liveRuns = runList.filter((run) => isActiveRun(run.status));
267 const overviewList = okOr(overview ?? null);
268 const projectsOk = okOr(projectList ?? null);
269
270 // --- What the viewer's pull requests and issues are ----------------------
271 const lower = username.toLowerCase();
272 const openPulls = (perRepo ?? []).flatMap(({ repo, pulls }) => (pulls ?? []).map((pull) => ({ pull, repo })));
273 const reviewRequested = openPulls.filter(
274 ({ pull }) =>
275 pull.status === "open" &&
276 pull.author.username.toLowerCase() !== lower &&
277 pull.reviewers.some((name) => name.toLowerCase() === lower),
278 );
279 const assignedPulls = openPulls.filter(({ pull }) => pull.assignees.some((name) => name.toLowerCase() === lower));
280 const authoredRows =
281 activeList.length > 0
282 ? activeList.map(({ pull, lifecycle, repo }) => pullRow(pull, pathOf(repo), lifecycle))
283 : (okOr(authoredPulls)?.items ?? []).map(authoredPull);
284 const pullsTabs = {
285 authored: authoredRows.slice(0, 8),
286 review: reviewRequested.slice(0, 8).map(({ pull, repo }) => pullRow(pull, pathOf(repo), null)),
287 assigned: assignedPulls.slice(0, 8).map(({ pull, repo }) => pullRow(pull, pathOf(repo), null)),
288 };
289 const issueRow = (issue: Issue, repo: RepoPath): IssueRow => ({
290 key: issue.id,
291 repo,
292 number: issue.number,
293 title: issue.title,
294 agent: issue.agent,
295 queued: issue.queued,
296 updatedAt: issue.updatedAt,
297 });
298 const issuesTabs = {
299 assigned: (assigned ?? []).flatMap((issue) => {
300 const repo = known.get(issue.repoId);
301 return repo ? [issueRow(issue, pathOf(repo))] : [];
302 }).slice(0, 8),
303 authored: (okOr(authoredIssues)?.items ?? []).slice(0, 8).map((item) => ({
304 key: `${item.repo.namespace}/${item.repo.name}#${item.number}`,
305 repo: item.repo,
306 number: item.number,
307 title: item.title,
308 agent: null,
309 queued: false,
310 updatedAt: item.updatedAt,
311 })),
312 };
313
314 // --- Needs you ------------------------------------------------------------
315 const needs: Need[] = [];
316 for (const entry of overviewList ?? []) {
317 const latest = entry.latest;
318 if (latest?.kind === "production" && latest.status === "failed" && slug) {
319 needs.push({
320 key: `deploy:${entry.slug}`,
321 kind: "deploy",
322 title: `Production build of ${entry.slug} failed`,
323 detail: latest.error ?? "The last build of the default branch failed. Production still serves the build before it.",
324 to: `/${slug}/${entry.slug}/deployments/${latest.id}`,
325 action: "See the build",
326 at: Date.parse(latest.finishedAt ?? latest.createdAt),
327 where: `${slug}/${entry.slug}`,
328 });
329 }
330 }
331 for (const { pull, lifecycle, repo } of activeList) {
332 const where = `${repo.namespace}/${repo.name}#${pull.number}`;
333 const to = `/${repo.namespace}/${repo.name}/pull/${pull.number}`;
334 if (lifecycle?.stage === "needs_you") {
335 const conflict = /conflict/i.test(lifecycle.detail);
336 needs.push({
337 key: `pull:${pull.id}`,
338 kind: conflict ? "conflict" : "stalled",
339 title: pull.title,
340 detail: lifecycle.detail,
341 to,
342 action: conflict ? "Resolve" : "Decide",
343 at: Date.parse(pull.updatedAt),
344 where,
345 });
346 } else if (lifecycle?.stage === "ready") {
347 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 });
348 } else if (!lifecycle && pull.status === "open" && pull.checkStatus === "failed") {
349 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 });
350 }
351 }
352 for (const { pull, repo } of reviewRequested) {
353 needs.push({
354 key: `review:${pull.id}`,
355 kind: "review",
356 title: pull.title,
357 detail: `${pull.author.username} asked for your review.`,
358 to: `/${repo.namespace}/${repo.name}/pull/${pull.number}`,
359 action: "Review",
360 at: Date.parse(pull.updatedAt),
361 where: `${repo.namespace}/${repo.name}#${pull.number}`,
362 });
363 }
364 let quietest: number | null = null;
365 for (const run of liveRuns) {
366 const minutes = stuckMinutes(run, now);
367 if (minutes == null) continue;
368 quietest = Math.max(quietest ?? 0, minutes);
369 needs.push({
370 key: `run:${run.id}`,
371 kind: "stuck",
372 title: run.title ?? `${run.agent}'s run`,
373 detail: `${run.agent} has reported nothing for ${minutes} min${run.step ? `. Last: ${run.step}` : ""}.`,
374 to: `/${run.repo.namespace}/${run.repo.name}/agents/runs/${run.id}`,
375 action: "Look",
376 at: Date.parse(run.updatedAt),
377 where: `${run.repo.namespace}/${run.repo.name}${run.number != null ? `#${run.number}` : ""}`,
378 });
379 }
380
381 // --- Activity -------------------------------------------------------------
382 const items: ActivityItem[] = [];
383 const allEvents: { repo: string; event: G1tEvent }[] = [];
384 for (const { repo, events } of perRepo ?? []) {
385 for (const event of events ?? []) {
386 allEvents.push({ repo: repo.id, event });
387 const item = eventItem(event, pathOf(repo));
388 if (item) items.push(item);
389 }
390 }
391 for (const entry of overviewList ?? []) {
392 const latest = entry.latest;
393 const served = latest?.status === "ready" || latest?.status === "replaced";
394 if (!slug || latest?.kind !== "production" || (!served && latest.status !== "failed")) continue;
395 items.push({
396 id: `deploy:${latest.id}`,
397 at: Date.parse(latest.finishedAt ?? latest.createdAt),
398 repo: { namespace: slug, name: entry.slug },
399 actor: latest.createdBy,
400 verb: served ? "deployed" : "deploy_failed",
401 number: null,
402 to: `/${slug}/${entry.slug}/deployments/${latest.id}`,
403 });
404 }
405 for (const memory of okOr(memories ?? null)?.workspace ?? []) {
406 const repo = memory.repo ?? memory.source.repo;
407 if (!repo || !slug) continue;
408 items.push({
409 id: `memory:${memory.id}`,
410 at: Date.parse(memory.createdAt),
411 repo,
412 actor: memory.createdBy,
413 verb: "learned",
414 number: null,
415 text: memory.text.length > 90 ? `${memory.text.slice(0, 88)}…` : memory.text,
416 to: `/${slug}/-/memory`,
417 });
418 }
419 const groups = groupActivity(items).slice(0, 60);
420
421 // --- Since you were last here --------------------------------------------
422 const since = seen.since ?? now - TIME.DAY;
423 const after = (verb: ActivityItem["verb"]) => items.filter((item) => item.verb === verb && item.at > since).length;
424 const digest = digestParts({
425 landed: after("landed"),
426 reviews: reviewRequested.length,
427 opened: after("opened_issue"),
428 deploys: after("deployed"),
429 failedDeploys: after("deploy_failed"),
430 stuck: quietest,
431 });
432
433 // --- Pulse ----------------------------------------------------------------
434 const week = allEvents.filter(({ event }) => Date.parse(event.time) >= weekAgo);
435 const mergedWeek = week.filter(({ event }) => event.type === "pull.merged");
436 const spans = issueToMerge(
437 allEvents.flatMap(({ repo, event }) => (event.type === "issue.opened" ? [{ repo, number: event.data.number, at: Date.parse(event.time) }] : [])),
438 mergedWeek.flatMap(({ repo, event }) => (event.type === "pull.merged" ? [{ repo, issue: event.data.issue ?? null, at: Date.parse(event.time) }] : [])),
439 );
440 const checkEvents = allEvents.flatMap(({ repo, event }) =>
441 event.type === "checks.completed" ? [{ repo, number: event.data.number, at: Date.parse(event.time), passed: event.data.status === "passed" }] : [],
442 );
443 const firstPass = firstPassRate(checkEvents.filter((c) => c.at >= weekAgo));
444 const usageOk = okOr(usage ?? null);
445 const costPoints = usageOk
446 ? usageOk.byDay.map((slice) => ({ at: Date.parse(`${slice.key.split("/")[0]}T12:00:00Z`), value: slice.micros / 1_000_000 }))
447 : runList.filter((run) => run.costUsd != null).map((run) => ({ at: Date.parse(run.createdAt), value: run.costUsd ?? 0 }));
448 const costDays = dailyBuckets(costPoints, 7, now);
449 const pulse = {
450 merged: mergedWeek.length,
451 mergedDays: dailyBuckets(mergedWeek.map(({ event }) => ({ at: Date.parse(event.time) })), 7, now),
452 hours: agentHours(runList, weekAgo, now),
453 hoursDays: dailyBuckets(
454 runList
455 .filter((run) => run.startedAt)
456 .map((run) => ({ at: Date.parse(run.startedAt!), value: agentHours([run], weekAgo, now) })),
457 7,
458 now,
459 ),
460 cost: costDays.reduce((sum, v) => sum + v, 0),
461 costDays,
462 issueToMerge: median(spans),
463 mergedWithIssue: spans.length,
464 firstPass: firstPass.rate,
465 firstPassOf: firstPass.of,
466 };
467
468 // --- Projects -------------------------------------------------------------
469 const byRepoName = new Map((perRepo ?? []).map((entry) => [`${entry.repo.namespace}/${entry.repo.name}`.toLowerCase(), entry]));
470 const projectsHealth: ProjectHealth[] = (projectsOk ?? []).slice(0, 12).map((project) => {
471 const repo = project.source.kind === "hosted" ? project.source.repo : null;
472 const key = repo ? `${repo.namespace}/${repo.name}`.toLowerCase() : "";
473 const entry = byRepoName.get(key);
474 const deploy = overviewList?.find((d) => d.slug === project.slug) ?? null;
475 const checks = (entry?.events ?? []).flatMap((event) => (event.type === "checks.completed" ? [event.data.status === "passed"] : []));
476 return {
477 slug: project.slug,
478 name: project.name,
479 private: project.private,
480 repo,
481 deploys: deploy?.enabled ?? false,
482 production: deploy?.production ?? null,
483 latest: deploy?.latest ?? null,
484 openPulls: entry?.pulls ? entry.pulls.length : null,
485 agents: liveRuns.filter((run) => repo && `${run.repo.namespace}/${run.repo.name}`.toLowerCase() === key).length,
486 passRate: checks.length ? checks.filter(Boolean).length / checks.length : null,
487 checks: checks.length,
488 };
489 });
490
491 const models_ = models ?? null;
492 times.total = Date.now() - started;
Agents as a team: lifecycle, merge queue, billing and a new shell493 const serverTiming = Object.entries(times)
494 .map(([name, ms]) => `${name};dur=${ms}`)
495 .join(", ");
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API496 const secure = new URL(request.url).protocol === "https:" ? "; Secure" : "";
497 return data(
498 {
499 signedIn: true as const,
500 viewer,
501 name: profile?.name?.trim() || username,
502 greeting: greetingFor(hourIn(now, tz)),
503 seenBefore: seen.since,
504 repos: repoList,
505 // Projects the composer can open an issue in: those of the viewer's workspaces.
506 targets: repoList
507 .filter((repo) => roleIn(viewer, repo.namespace) != null && !repo.forkOf)
508 .map((repo) => ({ namespace: repo.namespace, name: repo.name, isPrivate: repo.isPrivate })),
509 canRunAgents: models_ == null || models_.hosted || models_.own != null,
510 trial: models_?.own == null ? (models_?.trial ?? null) : null,
511 active: activeList.map(({ pull, lifecycle, repo }) => ({ pull, lifecycle, repo })),
512 needs: rankNeeds(needs),
513 live: liveRuns.slice(0, 12) as AgentRun[],
514 liveTotal: liveRuns.length,
515 runsLoaded: runs != null && runs.ok,
516 pullsTabs,
517 issuesTabs,
518 perRepoLoaded: perRepo != null,
519 groups,
520 digest,
521 pulse,
522 projects: projectsHealth,
523 projectsLoaded: projectsOk != null,
524 },
525 {
526 headers: {
527 "Server-Timing": serverTiming,
528 "Set-Cookie": `${SEEN_COOKIE}=${seen.value}; Path=/; Max-Age=31536000; HttpOnly; SameSite=Lax${secure}`,
529 },
530 },
531 );
532}
533
534export async function action({ request, context }: Route.ActionArgs) {
535 assertSameOrigin(request);
536 const user = requireUser(context, request);
537 const form = await request.formData();
538 const [namespace, name] = String(form.get("project") ?? "").split("/");
539 if (!namespace || !name) return { error: "Choose the project this is for." };
540 const { title, body } = splitRequest(String(form.get("request") ?? ""));
541 if (!title) return { error: "Say what you want done first." };
542 const path = { namespace, name };
543 const opened = await work.openIssue(user, path, { title, body });
544 if (!opened.ok) return { error: opened.error.message };
545 const to = `/${namespace}/${name}/issues/${opened.value.number}`;
546 if (form.get("assign") === "on") {
547 // The issue exists either way; its page shows the agent's pull request,
548 // or, if none could start, how to put one on it.
549 await env.RUNNER.run(user, path, opened.value.number).catch(() => null);
550 }
551 throw redirect(to);
552}
553
554type Loaded = Extract<Route.ComponentProps["loaderData"], { signedIn: true }>;
555
556// --- Pieces ---------------------------------------------------------------------
557
558function Composer({
559 targets,
560 canRunAgents,
561 workspace,
562 onReview,
563}: {
564 targets: Loaded["targets"];
565 canRunAgents: boolean;
566 workspace: string | null;
567 onReview: () => void;
568}) {
569 const navigation = useNavigation();
570 const busy = navigation.state === "submitting" && navigation.formAction === "/?index";
571 const [project, setProject] = useState(() => (targets[0] ? `${targets[0].namespace}/${targets[0].name}` : ""));
572 const [assign, setAssign] = useState(canRunAgents);
573 const text = useRef<HTMLTextAreaElement>(null);
574 const base = project ? `/${project}` : null;
575 const pill =
576 "inline-flex items-center gap-1.5 rounded-full border border-line bg-bg px-3 py-1.5 text-xs font-medium text-fg/80 transition-colors hover:border-line-strong hover:bg-raised hover:text-fg";
577 if (targets.length === 0) {
578 return (
579 <div className="rounded-2xl border border-line bg-surface p-5">
580 <p className="font-medium">Add a project to hand work to agents</p>
581 <p className="mt-1 text-sm text-muted">
582 Create a repository or import one by its address. Then describe what you want done here, and an agent starts on it.
583 </p>
584 <Link to={workspace ? `/new?workspace=${workspace}` : "/new"} className={`${pill} mt-4`}>
585 <FolderPlus size={13} /> New project
586 </Link>
587 </div>
588 );
589 }
590 return (
591 <div>
592 <Form
593 method="post"
594 action="/?index"
595 className="rounded-2xl border border-line bg-surface shadow-[0_1px_0_0_rgb(255_255_255/0.03)_inset] transition-colors focus-within:border-accent-dim/70"
596 >
597 <label htmlFor="composer" className="sr-only">
598 Describe what you want done
599 </label>
600 <textarea
601 id="composer"
602 ref={text}
603 name="request"
604 required
605 rows={3}
606 maxLength={20000}
607 autoComplete="off"
608 data-1p-ignore
609 placeholder="Describe what you want done… The first line becomes the issue's title."
610 onKeyDown={(event) => {
611 if (event.key === "Enter" && (event.metaKey || event.ctrlKey)) event.currentTarget.form?.requestSubmit();
612 }}
613 className="block w-full resize-y rounded-t-2xl bg-transparent px-4 pt-4 pb-2 text-[0.9375rem] leading-6 outline-none placeholder:text-faint"
614 />
615 <div className="flex flex-wrap items-center gap-2 border-t border-line/70 px-3 py-2.5">
616 <Select name="project" value={project} onValueChange={setProject}>
617 <SelectTrigger size="sm" className="w-auto max-w-60" aria-label="Project">
618 <SelectValue placeholder="Project" />
619 </SelectTrigger>
620 <SelectContent>
621 {targets.map((target) => (
622 <SelectItem
623 key={`${target.namespace}/${target.name}`}
624 value={`${target.namespace}/${target.name}`}
625 icon={<Avatar name={target.name} size={16} square />}
626 description={target.namespace}
627 >
628 {target.name}
629 </SelectItem>
630 ))}
631 </SelectContent>
632 </Select>
633 <label
634 className={`inline-flex h-8 items-center gap-2 rounded-md px-2 text-xs ${canRunAgents ? "text-muted" : "text-faint"}`}
635 title={canRunAgents ? "Open the issue and start g1t-agent on it" : "Connect a model under Integrations to run agents"}
636 >
637 <Switch checked={assign} onCheckedChange={setAssign} disabled={!canRunAgents} aria-label="Hand to an agent" />
638 <Bot size={13} className={assign ? "text-merged" : ""} />
639 Hand to g1t-agent
640 </label>
641 {assign && <input type="hidden" name="assign" value="on" />}
642 <span className="grow" />
643 <span className="hidden text-[0.6875rem] text-faint sm:inline">Ctrl+Enter</span>
644 <button
645 type="submit"
646 disabled={busy}
647 className="inline-flex h-8 items-center gap-1.5 rounded-md bg-fg px-3 text-sm font-medium text-bg transition-colors hover:bg-white disabled:opacity-50"
648 >
649 <Send size={13} />
650 {busy ? (assign ? "Starting…" : "Opening…") : assign ? "Start" : "Open issue"}
651 </button>
652 </div>
653 </Form>
654 <div className="mt-3 flex flex-wrap gap-2">
655 {base && (
656 <Link to={`${base}/issues/new`} className={pill}>
657 <CircleDot size={13} /> New issue
658 </Link>
659 )}
660 <button
661 type="button"
662 className={pill}
663 disabled={!canRunAgents}
664 onClick={() => {
665 setAssign(true);
666 text.current?.focus();
667 }}
668 >
669 <Sparkles size={13} className="text-merged" /> Hand to an agent
670 </button>
671 {base && (
672 <Link to={`${base}/plans`} className={pill}>
673 <MapIcon size={13} /> Plan an outcome
674 </Link>
675 )}
676 <button type="button" className={pill} onClick={onReview}>
677 <Eye size={13} /> Review queue
678 </button>
679 <Link to={workspace ? `/new?workspace=${workspace}` : "/new"} className={pill}>
680 <FolderPlus size={13} /> New project
681 </Link>
682 </div>
683 </div>
684 );
Agents as a team: lifecycle, merge queue, billing and a new shell685}
686
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API687const TONE = { fg: "text-fg", warn: "text-warn", danger: "text-danger", accent: "text-accent" };
Agents as a team: lifecycle, merge queue, billing and a new shell688
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API689function Digest({ parts, seenBefore }: { parts: Loaded["digest"]; seenBefore: number | null }) {
690 const when = seenBefore ? (
691 <>
692 Since you were last here, <TimeAgo at={seenBefore} />
693 </>
694 ) : (
695 "In the last day"
696 );
697 if (parts.length === 0) {
698 return <p className="mt-1 text-sm text-muted">{when}: nothing new landed, and nothing is waiting on you.</p>;
699 }
700 return (
701 <p className="mt-1 text-sm leading-6 text-muted">
702 {when}:{" "}
703 {parts.map((part, index) => (
704 <span key={part.text}>
705 {index > 0 && (index === parts.length - 1 ? " and " : ", ")}
706 <a href={`#${part.anchor}`} className={`font-medium underline decoration-line-strong underline-offset-4 hover:decoration-current ${TONE[part.tone]}`}>
707 {part.text}
708 </a>
709 </span>
710 ))}
711 .
712 </p>
713 );
Agents as a team: lifecycle, merge queue, billing and a new shell714}
715
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API716function PullList({ rows, empty }: { rows: Loaded["pullsTabs"]["authored"]; empty: ReactNode }) {
717 if (rows.length === 0) return <Quiet>{empty}</Quiet>;
Agents as a team: lifecycle, merge queue, billing and a new shell718 return (
719 <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 API720 {rows.map((row) => (
721 <li key={row.key}>
722 <Link
723 prefetch="intent"
724 to={`/${row.repo.namespace}/${row.repo.name}/pull/${row.number}`}
725 className="flex items-center gap-3 px-4 py-2.5 transition-colors hover:bg-raised"
726 >
727 <PullIcon status={row.status} />
728 <span className="min-w-0 grow">
729 <span className="block truncate text-sm font-medium">{row.title}</span>
730 <span className="block truncate font-mono text-xs text-muted">
731 {row.repo.name}#{row.number}
732 {row.agent && ` · ${row.agent}`}
Agents as a team: lifecycle, merge queue, billing and a new shell733 </span>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API734 </span>
735 {row.stage ? (
736 <span className="hidden shrink-0 items-center gap-2 text-xs text-muted sm:flex">
737 <StageDots stage={row.stage} />
738 <span className={row.stage === "needs_you" ? "text-warn" : ""}>{STAGE_LABEL[row.stage]}</span>
Agents as a team: lifecycle, merge queue, billing and a new shell739 </span>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API740 ) : (
741 <span className="hidden shrink-0 sm:block">
742 <CheckBadge status={row.checkStatus} />
Agents as a team: lifecycle, merge queue, billing and a new shell743 </span>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API744 )}
745 <span className="w-14 shrink-0 text-right text-xs text-faint">
746 <TimeAgo at={row.updatedAt} />
747 </span>
748 </Link>
749 </li>
750 ))}
Agents as a team: lifecycle, merge queue, billing and a new shell751 </ul>
752 );
Initial g1t: services, event bus, intents and attempts753}
754
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API755function IssueList({ rows, empty }: { rows: Loaded["issuesTabs"]["assigned"]; empty: ReactNode }) {
756 if (rows.length === 0) return <Quiet>{empty}</Quiet>;
Agents as a team: lifecycle, merge queue, billing and a new shell757 return (
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API758 <ul className="divide-y divide-line overflow-hidden rounded-xl border border-line bg-surface">
759 {rows.map((row) => (
760 <li key={row.key}>
761 <Link
762 prefetch="intent"
763 to={`/${row.repo.namespace}/${row.repo.name}/issues/${row.number}`}
764 className="flex items-center gap-3 px-4 py-2.5 transition-colors hover:bg-raised"
765 >
766 <IssueIcon issue={{ state: "open", reason: null }} />
767 <span className="min-w-0 grow">
768 <span className="block truncate text-sm font-medium">{row.title}</span>
769 <span className="block truncate font-mono text-xs text-muted">
770 {row.repo.name}#{row.number}
771 </span>
772 </span>
773 {row.agent ? (
774 <span className="hidden shrink-0 items-center gap-1 text-xs text-merged sm:flex">
775 <Bot size={12} /> {row.agent} at work
776 </span>
777 ) : row.queued ? (
778 <span className="hidden shrink-0 text-xs text-muted sm:block">Queued for an agent</span>
779 ) : null}
780 <span className="w-14 shrink-0 text-right text-xs text-faint">
781 <TimeAgo at={row.updatedAt} />
782 </span>
783 </Link>
784 </li>
785 ))}
786 </ul>
Agents as a team: lifecycle, merge queue, billing and a new shell787 );
788}
789
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API790function ProjectCard({ project, workspace }: { project: Loaded["projects"][number]; workspace: string }) {
791 const base = `/${workspace}/${project.slug}`;
792 const latest = project.latest;
793 return (
794 <li className="flex flex-col rounded-xl border border-line bg-surface p-4 transition-colors hover:border-line-strong">
795 <div className="flex items-center gap-2.5">
796 <span className="flex size-7 shrink-0 items-center justify-center rounded-md bg-raised text-muted ring-1 ring-line">
797 {project.private ? <Lock size={13} /> : <Box size={13} />}
798 </span>
799 <Link to={base} prefetch="intent" className="min-w-0 grow truncate font-medium hover:text-accent">
800 {project.name}
801 </Link>
802 {project.agents > 0 && (
803 <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">
804 <span className="size-1.5 animate-pulse rounded-full bg-merged" />
805 {project.agents} at work
806 </span>
807 )}
808 </div>
809 <div className="mt-3 min-h-10 text-xs">
810 {project.production ? (
811 <>
812 <a href={project.production.url} className="flex items-center gap-1 truncate font-mono text-fg-soft hover:text-accent">
813 {host(project.production.url)}
814 <ArrowUpRight size={12} className="shrink-0 text-faint" />
815 </a>
816 <p className="mt-1 flex items-center gap-2 text-muted">
817 {latest && <StatusDot status={latest.status} />}
818 <span>
819 deployed <TimeAgo at={project.production.deployedAt} />
820 </span>
821 </p>
822 </>
823 ) : project.deploys ? (
824 <p className="text-muted">
825 {latest ? <StatusDot status={latest.status} label={`${DEPLOY_STATUS[latest.status].label}, not live yet`} /> : "Deploys on the next push to the default branch."}
826 </p>
827 ) : (
828 <p className="text-faint">
829 Not deployed.{" "}
830 <Link to={`${base}/settings/deployments`} className="text-muted hover:text-fg">
831 Put it on g1t.page
832 </Link>
833 </p>
834 )}
835 </div>
836 <dl className="mt-3 grid grid-cols-3 gap-2 border-t border-line pt-3 text-xs">
837 <div>
838 <dt className="text-faint">Open PRs</dt>
839 <dd className="mt-0.5 font-medium tabular-nums">
840 <Link to={`${base}/pulls`} className="hover:text-accent">
841 {project.openPulls ?? "—"}
842 </Link>
843 </dd>
844 </div>
845 <div>
846 <dt className="text-faint">Agents</dt>
847 <dd className="mt-0.5 font-medium tabular-nums">
848 <Link to={`${base}/agents`} className="hover:text-accent">
849 {project.agents}
850 </Link>
851 </dd>
852 </div>
853 <div title={project.checks ? `${project.checks} recent check runs` : "No recent check runs"}>
854 <dt className="text-faint">Checks</dt>
855 <dd className="mt-0.5 font-medium tabular-nums">{percent(project.passRate)}</dd>
856 <dd className="mt-1">
857 <Meter value={project.passRate} tone={project.passRate != null && project.passRate < 0.7 ? "bg-warn" : "bg-accent"} />
858 </dd>
859 </div>
860 </dl>
861 </li>
862 );
Agents as a team: lifecycle, merge queue, billing and a new shell863}
864
Get started on mission control865type Step = { done: boolean; title: string; about: string; to: string | null; action: string };
866
867/**
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API868 * The first things to do, ticked off as they are done, until work has been
869 * handed to agents.
Workspace things at the bottom of the sidebar, and on mission control870 */
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API871function GetStarted({ steps }: { steps: Step[] }) {
872 const left = steps.filter((step) => !step.done).length;
873 return (
874 <section className="rounded-2xl bg-surface p-5 ring-1 ring-line">
875 <div className="flex items-baseline justify-between gap-3">
876 <h2 className="font-semibold tracking-tight">Get started</h2>
877 <span className="text-xs text-muted">
878 {steps.length - left} of {steps.length} done
879 </span>
880 </div>
881 <ol className="mt-4 space-y-2">
882 {steps.map((step, index) => (
883 <li
884 key={step.title}
885 className={`flex items-start gap-3 rounded-xl px-3 py-2.5 ${step.done ? "" : "bg-bg/50 ring-1 ring-line"}`}
886 >
887 <span
888 className={`mt-0.5 flex size-5 shrink-0 items-center justify-center rounded-full text-[0.6875rem] font-medium ${
889 step.done ? "bg-accent text-bg" : "text-muted ring-1 ring-line-strong"
890 }`}
891 >
892 {step.done ? "✓" : index + 1}
893 </span>
894 <span className="min-w-0 grow">
895 <span className={`block text-sm font-medium ${step.done ? "text-muted line-through decoration-faint" : ""}`}>
896 {step.title}
897 </span>
898 {!step.done && <span className="mt-0.5 block text-xs leading-5 text-muted">{step.about}</span>}
899 </span>
900 {!step.done && step.to && (
901 <Link
902 to={step.to}
903 className="shrink-0 rounded-md bg-fg px-2.5 py-1 text-xs font-medium text-bg transition-colors hover:bg-white"
904 >
905 {step.action}
906 </Link>
907 )}
908 </li>
909 ))}
910 </ol>
911 </section>
912 );
913}
914
915/** The workspace being looked at: what it holds and what is moving in it. */
Workspace names and icons, and a component kit for every control916function WorkspaceCard({
917 slug,
918 name,
919 avatar,
920 role,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API921 projects,
922 needs,
923 live,
Workspace names and icons, and a component kit for every control924}: {
925 slug: string;
926 name: string;
927 avatar?: string | null;
928 role: string;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API929 projects: number;
930 needs: number;
931 live: number;
Workspace names and icons, and a component kit for every control932}) {
Workspace things at the bottom of the sidebar, and on mission control933 const stats = [
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API934 { value: projects, label: "projects" },
935 { value: live, label: "agents at work" },
936 { value: needs, label: "need you" },
Workspace things at the bottom of the sidebar, and on mission control937 ];
938 return (
939 <div className="rounded-xl border border-line bg-surface p-5">
940 <div className="flex items-center gap-3">
Workspace names and icons, and a component kit for every control941 <Avatar name={slug} image={avatar} size={32} square />
Workspace things at the bottom of the sidebar, and on mission control942 <div className="min-w-0">
Workspace names and icons, and a component kit for every control943 <Link to={`/${slug}`} title={name} className="block truncate font-medium hover:underline">
944 {name}
Workspace things at the bottom of the sidebar, and on mission control945 </Link>
Workspace names and icons, and a component kit for every control946 <p className="truncate text-xs text-muted">
947 <span className="font-mono">g1t.sh/{slug}</span> · <span className="capitalize">{role}</span>
948 </p>
Workspace things at the bottom of the sidebar, and on mission control949 </div>
950 </div>
951 <dl className="mt-4 grid grid-cols-3 gap-2">
952 {stats.map((stat) => (
953 <div key={stat.label} className="rounded-lg bg-bg/60 px-2.5 py-2 ring-1 ring-line">
954 <dt className="sr-only">{stat.label}</dt>
955 <dd className="text-lg font-semibold tabular-nums tracking-tight">{stat.value}</dd>
956 <dd className="text-[0.6875rem] leading-tight text-muted">{stat.label}</dd>
957 </div>
958 ))}
959 </dl>
960 <div className="mt-4 flex flex-wrap gap-x-4 gap-y-1 text-sm">
961 <Link to={`/${slug}/-/people`} className="inline-flex items-center gap-1.5 text-muted hover:text-fg">
962 <Users size={13} />
963 Members
964 </Link>
965 <Link
966 to={role === "owner" ? `/${slug}/-/settings` : `/${slug}/-/people`}
967 className="inline-flex items-center gap-1.5 text-muted hover:text-fg"
968 >
969 <Settings size={13} />
970 Settings
971 </Link>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API972 <Link to={`/${slug}/-/agents`} className="inline-flex items-center gap-1.5 text-muted hover:text-fg">
973 <Radio size={13} />
974 Fleet
Workspace things at the bottom of the sidebar, and on mission control975 </Link>
976 </div>
977 </div>
978 );
979}
980
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API981function UsageCard({ shell, weekCost }: { shell: ShellData; weekCost: number }) {
982 const slug = shell.workspace?.slug;
983 const used = shell.monthUsageMicros ?? 0;
984 const limit = shell.limit;
985 const ceiling = limit && !limit.comped ? limit.ceilingMicros : null;
986 const share = ceiling ? Math.min(1, limit!.exposureMicros / ceiling) : null;
987 const month = new Date().toLocaleDateString("en-US", { month: "long" });
Get started on mission control988 return (
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API989 <div className="rounded-xl border border-line bg-surface p-5">
990 <div className="flex items-center justify-between">
991 <h2 className="flex items-center gap-1.5 text-sm font-semibold">
992 <Coins size={14} className="text-faint" />
993 Usage in {month}
994 </h2>
995 {slug && (
996 <Link to={`/${slug}/-/usage`} className="text-xs text-muted hover:text-fg">
997 Details
998 </Link>
999 )}
Get started on mission control1000 </div>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1001 <p className="mt-3 text-2xl font-semibold tracking-tight tabular-nums">{dollars(used)}</p>
1002 <p className="text-xs text-muted">
1003 {shell.free ? "At cost. g1t charges nothing while it is being built out." : "Charged so far this month."}
1004 {weekCost > 0 && ` ${formatCost(weekCost)} of it in the last 7 days.`}
1005 </p>
1006 {ceiling != null && (
1007 <div className="mt-3">
1008 <Meter value={share} tone={limit!.state === "ok" ? "bg-accent" : limit!.state === "warning" ? "bg-warn" : "bg-danger"} />
1009 <p className="mt-1.5 text-[0.6875rem] text-faint">
1010 {dollars(limit!.exposureMicros)} of the {dollars(ceiling)} limit
1011 </p>
1012 </div>
1013 )}
1014 </div>
1015 );
1016}
1017
1018function WhatsNew({ workspace }: { workspace: string | null }) {
1019 return (
1020 <div className="rounded-xl border border-line bg-surface p-5">
1021 <h2 className="flex items-center gap-1.5 text-sm font-semibold">
1022 <Sparkles size={14} className="text-accent" />
1023 What's new on g1t
1024 </h2>
1025 <ul className="mt-3 space-y-3">
1026 {CHANGELOG.slice(0, 5).map((entry) => {
1027 const href = changelogHref(entry, workspace);
1028 const title = <span className="text-sm font-medium">{entry.title}</span>;
1029 return (
1030 <li key={entry.title}>
1031 {href ? (
1032 <Link to={href} className="hover:text-accent">
1033 {title}
1034 </Link>
1035 ) : (
1036 title
1037 )}
1038 <p className="mt-0.5 text-xs leading-5 text-muted">{entry.about}</p>
1039 </li>
1040 );
1041 })}
1042 </ul>
1043 <a href="https://docs.g1t.sh/" className="mt-4 inline-flex items-center gap-1 text-xs text-muted hover:text-fg">
1044 Read the docs <ArrowRight size={12} />
1045 </a>
1046 </div>
Get started on mission control1047 );
1048}
1049
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1050// --- The page -------------------------------------------------------------------
Agents as a team: lifecycle, merge queue, billing and a new shell1051
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1052export default function Home({ loaderData }: Route.ComponentProps) {
1053 const shell = useRouteLoaderData("root")?.shell as ShellData | null | undefined;
1054 const loaded = loaderData.signedIn ? loaderData : null;
Agents as a team: lifecycle, merge queue, billing and a new shell1055 // 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 API1056 const changing =
1057 (loaded?.liveTotal ?? 0) > 0 ||
1058 (loaded?.active ?? []).some((item) => item.lifecycle && item.lifecycle.stage !== "needs_you" && item.lifecycle.stage !== "ready");
1059 useLiveRefresh(changing);
1060 const [greeting, setGreeting] = useState(loaded?.greeting ?? "");
1061 const [pullsTab, setPullsTab] = useState(() => (loaded && loaded.pullsTabs.review.length > 0 ? "review" : "authored"));
Agents as a team: lifecycle, merge queue, billing and a new shell1062 useEffect(() => {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1063 // The greeting follows the viewer's own clock, and the server learns
1064 // their zone for next time.
1065 setGreeting(greetingFor(new Date().getHours()));
1066 try {
1067 const zone = Intl.DateTimeFormat().resolvedOptions().timeZone;
1068 if (zone) document.cookie = `${TZ_COOKIE}=${encodeURIComponent(zone)}; Path=/; Max-Age=31536000; SameSite=Lax`;
1069 } catch {
1070 // Nothing to remember.
1071 }
1072 }, []);
Agents as a team: lifecycle, merge queue, billing and a new shell1073
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1074 if (!loaded) return <Landing repos={loaderData.repos} />;
1075 const { viewer, repos, active, needs: serverNeeds, live, liveTotal, pullsTabs, issuesTabs, groups, pulse, projects, canRunAgents, trial } = loaded;
Initial g1t: services, event bus, intents and attempts1076
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1077 const workspace = shell?.workspace?.slug ?? viewer.workspaces?.[0]?.slug ?? null;
1078 // The usage limit, as the sidebar knows it, heads the list when it bites.
1079 const limitNeed: Need[] =
1080 shell?.limit && !shell.limit.comped && shell.limit.state !== "ok" && workspace
1081 ? [
1082 {
1083 key: "limit",
1084 kind: "limit",
1085 title: shell.limit.state === "stopped" ? "Agents are stopped: the usage limit was reached" : "Usage is nearing its limit",
1086 detail:
1087 shell.limit.state === "stopped"
1088 ? "No new agent starts until the limit is raised or a card is added."
1089 : `${dollars(shell.limit.exposureMicros)} of ${dollars(shell.limit.ceilingMicros ?? 0)} used. Add a card so work keeps going.`,
1090 to: `/${workspace}/-/billing`,
1091 action: "Billing",
1092 at: Date.now(),
1093 where: null,
1094 },
1095 ]
1096 : [];
1097 const needs = rankNeeds([...limitNeed, ...serverNeeds]);
1098
Get started on mission control1099 const first = repos[0];
Usage while free is shown at cost; agents get rustfmt and clippy1100 const handedOff = active.length > 0 || (shell?.monthUsageMicros ?? 0) > 0;
Get started on mission control1101 const steps: Step[] = [
1102 {
1103 done: Boolean(workspace),
1104 title: "Create a workspace",
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1105 about: "Projects, people and agent credit live in one.",
Get started on mission control1106 to: "/workspaces/new",
1107 action: "Create",
1108 },
1109 {
1110 done: repos.length > 0,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1111 title: "Add a project",
1112 about: "Create a repository, or import one by its address. Push to it with git as usual.",
Get started on mission control1113 to: workspace ? `/new?workspace=${workspace}` : "/new",
1114 action: "Add",
1115 },
A free allowance on g1t's models, so anyone can try its agents1116 trial?.open
New workspaces are told to connect a model, before anything fails1117 ? {
A free allowance on g1t's models, so anyone can try its agents1118 done: true,
1119 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 API1120 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 agents1121 to: workspace ? `/${workspace}/-/integrations` : null,
1122 action: "Connect",
1123 }
1124 : canRunAgents
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1125 ? {
1126 done: Boolean(shell?.free) || shell?.limit == null || shell.limit.comped || shell.limit.state === "ok",
1127 title: "Keep work running",
1128 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.",
1129 to: workspace ? `/${workspace}/-/billing` : null,
1130 action: "Billing",
1131 }
1132 : {
1133 done: false,
1134 title: "Connect a model",
1135 about: `${
1136 trial?.reason === "used"
1137 ? "This workspace has used its free allowance on g1t's models. "
1138 : trial?.reason === "pool" || trial?.reason === "ended"
1139 ? "The free allowance on g1t's models has ended. "
1140 : ""
1141 }Agents need a model to think with. Connect your Anthropic or OpenAI key, or any compatible endpoint.`,
1142 to: workspace ? `/${workspace}/-/integrations` : null,
1143 action: "Connect",
1144 },
Get started on mission control1145 {
1146 done: handedOff,
1147 title: "Hand off an outcome",
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1148 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 control1149 to: first ? `/${first.namespace}/${first.name}/plans` : null,
1150 action: "Write one",
1151 },
1152 ];
1153 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 API1154 const showReview = () => {
1155 setPullsTab("review");
1156 document.getElementById("your-pulls")?.scrollIntoView({ behavior: "smooth", block: "start" });
1157 };
1158 const counts = (n: number) => (n > 0 ? <span className="ml-1 tabular-nums text-faint">{n}</span> : null);
Get started on mission control1159
API and MCP server, Rust identity service, registration, site redesign1160 return (
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1161 <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 redesign1162 <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 API1163 <header>
1164 <h1 className="text-2xl font-semibold tracking-tight sm:text-[1.75rem]" suppressHydrationWarning>
1165 {greeting}, {loaded.name}
1166 </h1>
1167 <Digest parts={loaded.digest} seenBefore={loaded.seenBefore} />
1168 <div className="mt-5">
1169 <Composer targets={loaded.targets} canRunAgents={canRunAgents} workspace={workspace} onReview={showReview} />
API and MCP server, Rust identity service, registration, site redesign1170 </div>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1171 </header>
API and MCP server, Rust identity service, registration, site redesign1172
Get started on mission control1173 {starting && <GetStarted steps={steps} />}
1174
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1175 <Panel id="needs-you" title="Needs you" icon={<Hand size={14} className="text-warn" />} count={needs.length}>
1176 {needs.length === 0 ? (
1177 <Quiet>Nothing is waiting on you. Reviews, stopped work and failed deploys show up here first.</Quiet>
1178 ) : (
1179 <NeedsList needs={needs} />
1180 )}
1181 </Panel>
Agents as a team: lifecycle, merge queue, billing and a new shell1182
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1183 <Panel
1184 id="live"
1185 title="Live now"
1186 icon={<span className={`block size-2 rounded-full ${liveTotal > 0 ? "animate-pulse bg-merged" : "bg-line-strong"}`} />}
1187 count={liveTotal}
1188 all={workspace ? { to: `/${workspace}/-/agents`, label: "Fleet" } : null}
1189 >
1190 {!loaded.runsLoaded && workspace ? (
1191 <Unavailable what="Agent runs" />
1192 ) : live.length === 0 ? (
1193 <Quiet
1194 action={
1195 canRunAgents && loaded.targets.length > 0 ? (
1196 <button
1197 type="button"
1198 onClick={() => document.getElementById("composer")?.focus()}
1199 className="inline-flex items-center gap-1.5 rounded-md bg-fg px-3 py-1.5 text-xs font-medium text-bg hover:bg-white"
Agents as a team: lifecycle, merge queue, billing and a new shell1200 >
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1201 <Sparkles size={12} /> Hand something to an agent
1202 </button>
1203 ) : null
1204 }
1205 >
1206 No agent is at work right now. Describe a change above and one starts on it in seconds.
1207 </Quiet>
1208 ) : (
1209 <ul className="space-y-3">
1210 {live.map((run) => (
1211 <RunCard key={run.id} run={run} member showRepo />
1212 ))}
1213 {liveTotal > live.length && workspace && (
1214 <li className="text-xs text-muted">
1215 <Link to={`/${workspace}/-/agents`} className="hover:text-fg">
1216 {liveTotal - live.length} more at work in the fleet
Agents as a team: lifecycle, merge queue, billing and a new shell1217 </Link>
1218 </li>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1219 )}
Agents as a team: lifecycle, merge queue, billing and a new shell1220 </ul>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1221 )}
1222 </Panel>
1223
1224 <Panel
1225 title="This week"
1226 icon={<ActivityIcon size={14} />}
1227 all={workspace ? { to: `/${workspace}/-/usage`, label: "Usage" } : null}
1228 >
1229 <div className="grid grid-cols-2 gap-3 sm:grid-cols-3 xl:grid-cols-5">
1230 <PulseTile
1231 label="Changes landed"
1232 value={String(pulse.merged)}
1233 hint="last 7 days"
1234 values={pulse.mergedDays}
1235 format={(v) => `${v} landed`}
1236 />
1237 <PulseTile
1238 label="Agent hours"
1239 value={pulse.hours < 10 ? pulse.hours.toFixed(1) : String(Math.round(pulse.hours))}
1240 hint="time agents spent working"
1241 values={pulse.hoursDays}
1242 format={(v) => `${v.toFixed(1)} h`}
1243 to={workspace ? `/${workspace}/-/agents` : undefined}
1244 />
1245 <PulseTile
1246 label="Agent cost"
1247 value={formatCost(pulse.cost) ?? "$0.00"}
1248 hint="model and sandbox"
1249 values={pulse.costDays}
1250 format={(v) => formatCost(v) ?? "$0.00"}
1251 to={workspace ? `/${workspace}/-/usage` : undefined}
1252 />
1253 <PulseTile
1254 label="Issue to landed"
1255 value={formatSpan(pulse.issueToMerge)}
1256 hint={pulse.mergedWithIssue ? `median of ${pulse.mergedWithIssue}` : "no issue landed yet"}
1257 />
1258 <PulseTile
1259 label="Checks pass first time"
1260 value={percent(pulse.firstPass)}
1261 hint={pulse.firstPassOf ? `of ${pulse.firstPassOf} pull requests` : "no checks ran yet"}
1262 />
1263 </div>
1264 </Panel>
1265
1266 <div className="grid gap-10 xl:grid-cols-2">
1267 <Tabs value={pullsTab} onValueChange={setPullsTab}>
1268 <Panel
1269 id="your-pulls"
1270 title="Your pull requests"
1271 icon={<GitPullRequest size={14} />}
1272 all={{ to: `/u/${viewer.username}?tab=pulls` }}
1273 >
1274 <TabsList>
1275 <TabsTrigger value="authored">Opened{counts(pullsTabs.authored.length)}</TabsTrigger>
1276 <TabsTrigger value="review">Review requested{counts(pullsTabs.review.length)}</TabsTrigger>
1277 <TabsTrigger value="assigned">Assigned{counts(pullsTabs.assigned.length)}</TabsTrigger>
1278 </TabsList>
1279 <TabsContent value="authored">
1280 <PullList rows={pullsTabs.authored} empty="None open. Pull requests you or your agents start show here with where each stands." />
1281 </TabsContent>
1282 <TabsContent value="review">
1283 {loaded.perRepoLoaded ? (
1284 <PullList rows={pullsTabs.review} empty="No one is waiting on your review." />
1285 ) : (
1286 <Unavailable what="Review requests" />
1287 )}
1288 </TabsContent>
1289 <TabsContent value="assigned">
1290 <PullList rows={pullsTabs.assigned} empty="No open pull request is assigned to you." />
1291 </TabsContent>
1292 </Panel>
1293 </Tabs>
1294
1295 <Tabs defaultValue="assigned">
1296 <Panel title="Your issues" icon={<CircleDot size={14} />} all={{ to: `/u/${viewer.username}?tab=issues` }}>
1297 <TabsList>
1298 <TabsTrigger value="assigned">Assigned{counts(issuesTabs.assigned.length)}</TabsTrigger>
1299 <TabsTrigger value="authored">Opened{counts(issuesTabs.authored.length)}</TabsTrigger>
1300 </TabsList>
1301 <TabsContent value="assigned">
1302 <IssueList rows={issuesTabs.assigned} empty="Nothing is assigned to you." />
1303 </TabsContent>
1304 <TabsContent value="authored">
1305 <IssueList rows={issuesTabs.authored} empty="You have no open issues." />
1306 </TabsContent>
1307 </Panel>
1308 </Tabs>
1309 </div>
Agents as a team: lifecycle, merge queue, billing and a new shell1310
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1311 <Panel id="activity" title="Activity" icon={<ActivityIcon size={14} />}>
1312 {!loaded.perRepoLoaded ? (
1313 <Unavailable what="Activity" />
Agents as a team: lifecycle, merge queue, billing and a new shell1314 ) : (
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1315 <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 shell1316 )}
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1317 </Panel>
API and MCP server, Rust identity service, registration, site redesign1318
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1319 {workspace && (
1320 <Panel
1321 id="projects"
1322 title="Projects"
1323 icon={<Box size={14} />}
1324 count={projects.length}
1325 all={{ to: `/${workspace}`, label: "Workspace" }}
1326 extra={
1327 <Link to={`/new?workspace=${workspace}`} className="inline-flex items-center gap-1 text-xs text-muted hover:text-fg">
1328 <Plus size={12} /> New
1329 </Link>
1330 }
1331 >
1332 {!loaded.projectsLoaded ? (
1333 <Unavailable what="Projects" />
1334 ) : projects.length === 0 ? (
1335 <Quiet>No projects yet. Create a repository or import one, and it gets a project of its own.</Quiet>
1336 ) : (
1337 <ul className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
1338 {projects.map((project) => (
1339 <ProjectCard key={project.slug} project={project} workspace={workspace} />
1340 ))}
1341 </ul>
1342 )}
1343 </Panel>
1344 )}
API and MCP server, Rust identity service, registration, site redesign1345 </div>
Initial g1t: services, event bus, intents and attempts1346
API and MCP server, Rust identity service, registration, site redesign1347 <aside className="space-y-4">
Workspace things at the bottom of the sidebar, and on mission control1348 {shell?.workspace && (
1349 <WorkspaceCard
1350 slug={shell.workspace.slug.toLowerCase()}
Workspace names and icons, and a component kit for every control1351 name={shell.workspace.name || shell.workspace.slug}
1352 avatar={shell.workspace.avatar}
Workspace things at the bottom of the sidebar, and on mission control1353 role={shell.workspace.role}
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1354 projects={shell.repos.length}
1355 needs={needs.length}
1356 live={liveTotal}
Workspace things at the bottom of the sidebar, and on mission control1357 />
1358 )}
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1359 {shell?.workspace && <UsageCard shell={shell} weekCost={pulse.cost} />}
1360 <WhatsNew workspace={workspace} />
API and MCP server, Rust identity service, registration, site redesign1361 <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 API1362 <h2 className="text-sm font-semibold">Connect your own agent</h2>
1363 <p className="mt-1.5 text-xs leading-5 text-muted">
1364 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 redesign1365 </p>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1366 <div className="mt-3">
1367 <CopyLine prompt text="claude mcp add --transport http g1t https://mcp.g1t.sh" />
API and MCP server, Rust identity service, registration, site redesign1368 </div>
1369 <Link
Docs as their own app; shared theme package1370 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 API1371 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 redesign1372 >
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1373 How it works <ArrowRight size={12} />
API and MCP server, Rust identity service, registration, site redesign1374 </Link>
1375 </div>
1376 </aside>
Initial g1t: services, event bus, intents and attempts1377 </main>
1378 );
1379}