Skip to content

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

729 lines30,865 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.

New workspaces are told to connect a model, before anything fails1import { env } from "cloudflare:workers";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2import { Suspense, lazy } from "react";
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step3import { type ShouldRevalidateFunctionArgs, data, redirect } from "react-router";
Agents as a team: lifecycle, merge queue, billing and a new shell4
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API5import {
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step6 type Confidence,
Mission control counts people's direct pushes to the default branch7 type G1tEvent,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API8 type Lifecycle,
9 type Pull,
10 type Repo,
11 type RepoPath,
Mission control counts people's direct pushes to the default branch12 type Viewer,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look13 REPO_ROLE_LABELS,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API14 isActiveRun,
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights15 workOwner,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API16} from "@g1t/contracts";
Initial g1t: services, event bus, intents and attempts17
18import type { Route } from "./+types/home";
Social cards for every page: og.g1t.sh19import { page } from "../lib/meta";
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put20import { WORKSPACE_COOKIE, chosenWorkspace } from "../lib/workspace-choice";
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API21import {
22 type ActivityItem,
23 type Need,
24 TIME,
25 agentHours,
26 dailyBuckets,
27 eventItem,
28 greetingFor,
29 groupActivity,
30 hourIn,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms31 isAgent,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API32 rankNeeds,
33 readCookie,
34 stuckMinutes,
35} from "../lib/mission";
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms36import {
37 type Fact,
38 type Merged,
Mission control counts people's direct pushes to the default branch39 pushedCommits,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms40 type NeedRow,
41 type QuickAction,
42 RUN_LABEL,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step43 confidenceAsk,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms44 dateLine,
45 dayKey,
46 landedToday,
47 needPathKey,
48 pullFacts,
49 reachesBack,
50 reasonFor,
51 summaryLine,
52 usd,
53 waitingRows,
54 weekOf,
55 who,
56 whyFor,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step57 withConfidence,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms58} from "../lib/mission-control";
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step59import { type NotStarted, chosenRepo, delegateForm, issuePath, notStarted } from "../lib/delegate";
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all60import { needsYou } from "../lib/inbox";
API and MCP server, Rust identity service, registration, site redesign61import { Landing } from "../components/landing";
Fast pages, required checks on the branch, self-hosted runners, honest incidents62import { Skeleton, SkeletonRows } from "../components/ui/skeleton";
API and MCP server, Rust identity service, registration, site redesign63import {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API64 agents,
65 billing,
66 deployments,
67 events as eventLog,
68 identity,
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all69 inbox,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API70 projects as projectsApi,
71 repos as reposApi,
72 work,
73} from "../lib/services.server";
Runners are owner-only: the page, its actions, the Settings entry and the links to it74import { assertSameOrigin, getViewer, requireUser, roleIn } from "../lib/session.server";
Fast pages, required checks on the branch, self-hosted runners, honest incidents75import { runners } from "../lib/runners.server";
76import { readableRepos } from "../lib/access.server";
Initial g1t: services, event bus, intents and attempts77
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look78/** The viewer's time zone, which mission control sets, so the greeting fits their day. */
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API79const TZ_COOKIE = "g1t_tz";
80/** The most projects whose pull requests and events are read for the page. */
81const MAX_PROJECTS = 10;
82const EVENTS_PER_PROJECT = 80;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms83/** How many pull requests work lists at once (`LIST_PAGE` in services/work). */
84const PULL_PAGE = 100;
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all85/** Unread inbox items read for the Needs you card, and how many it shows. */
86const INBOX_READ = 50;
87const INBOX_SHOWN = 4;
A free allowance on g1t's models, so anyone can try its agents88
Social cards for every page: og.g1t.sh89export function meta(args: Route.MetaArgs) {
90 return page(args, {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look91 title: "g1t — where people and agents ship software together",
Social cards for every page: og.g1t.sh92 description:
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look93 "The open-source git platform for people and agents: issues, pull requests and review, agents you assign like teammates, a merge queue that keeps main green, and deployments to the edge. Priced at cost plus 20%, never per seat.",
Social cards for every page: og.g1t.sh94 });
Initial g1t: services, event bus, intents and attempts95}
96
Agents as a team: lifecycle, merge queue, billing and a new shell97export function headers({ loaderHeaders }: Route.HeadersArgs) {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API98 const out = new Headers();
99 const timing = loaderHeaders.get("Server-Timing");
100 if (timing) out.set("Server-Timing", timing);
101 const cookie = loaderHeaders.get("Set-Cookie");
102 if (cookie) out.set("Set-Cookie", cookie);
103 return out;
Agents as a team: lifecycle, merge queue, billing and a new shell104}
105
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms106/**
107 * Switching tabs, the sort or the activity list changes only the address:
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all108 * everything is already loaded. Marking an inbox item read or done changes
109 * nothing else here: the card steps out by itself, and the bell's count
110 * comes with the shell (root.tsx). A refresh, a form, or anything else
111 * loads again as usual.
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms112 */
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all113export function shouldRevalidate({ currentUrl, nextUrl, formMethod, formAction, defaultShouldRevalidate }: ShouldRevalidateFunctionArgs) {
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms114 if (!formMethod && currentUrl.pathname === nextUrl.pathname && currentUrl.search !== nextUrl.search) return false;
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all115 if (formAction === "/inbox") return false;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms116 return defaultShouldRevalidate;
117}
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API118
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms119/** Where a need came from, so its row can say what is known about it. */
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step120type Extra = Partial<Pick<NeedRow, "repo" | "ref" | "by" | "for" | "facts" | "quick" | "link" | "open">> & {
121 /** How sure g1t is of the agent's change, for a pull request. */
122 confidence?: Confidence | null;
123};
124
125/** What the composer said back, when the issue opened but the agent did not start, or nothing opened. */
126export type DelegateResult = { error: string; notStarted: null } | { error: null; notStarted: NotStarted };
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API127
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step128/**
129 * "Put an agent on it": opens an issue in one of the viewer's projects and
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent130 * puts g1t on it, then lands on the issue. When the agent could not
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step131 * start, the issue is still open, and the composer says why and where to
132 * fix it.
133 */
134export async function action({ request, context }: Route.ActionArgs) {
135 assertSameOrigin(request);
136 const user = requireUser(context, request);
137 const form = await request.formData();
138 if (form.get("intent") !== "delegate") return data<DelegateResult>({ error: "Nothing to do.", notStarted: null }, { status: 400 });
139 const projects = await reposApi.list(user, { memberOnly: true }).catch(() => []);
140 const repo = chosenRepo(form.get("repo"), projects.map((repo) => ({ namespace: repo.namespace, name: repo.name })));
141 if (!repo) return data<DelegateResult>({ error: "Choose one of your projects.", notStarted: null }, { status: 400 });
142 const result = await env.RUNNER.delegate(user, repo, delegateForm(form));
143 if (!result.ok) return data<DelegateResult>({ error: result.error.message, notStarted: null }, { status: 400 });
144 const { issue, agent } = result.value;
145 const refused = notStarted(agent, repo, issue.number);
146 if (!refused) throw redirect(issuePath(repo, issue.number));
147 return data<DelegateResult>({ error: null, notStarted: refused });
148}
149
Mission control counts people's direct pushes to the default branch150/** Pushes to the default branch read for the week, per project, and commits read back from each. */
151const PUSHES_READ = 40;
152const PUSH_DEPTH = 60;
153
154/**
155 * The commits people pushed straight to a project's default branch in the
156 * last two weeks, each with when its push landed. Merges and agents'
157 * commits are left out: pull requests count those.
158 */
159async function directCommits(repo: Repo, viewer: Viewer): Promise<{ hash: string; at: string }[]> {
160 const since = Date.now() - 14 * TIME.DAY;
161 const pushes = (await eventLog.list({ repoId: repo.id, types: ["git.push"], limit: PUSHES_READ })).filter(
162 (event): event is G1tEvent<"git.push"> => event.type === "git.push" && event.data.defaultBranch && Date.parse(event.time) >= since,
163 );
164 const path = { namespace: repo.namespace, name: repo.name };
165 const read = await Promise.all(
166 pushes.map(async (push) => {
167 const history = await reposApi.log(path, viewer, push.data.after, PUSH_DEPTH).catch(() => null);
168 return history?.ok ? pushedCommits(history.value, push.data.before).map((commit) => ({ hash: commit.hash, at: push.time })) : [];
169 }),
170 );
171 // A commit pushed twice (after a force push, say) counts once, at its first landing.
172 const seen = new Map<string, string>();
173 for (const commit of read.flat().reverse()) if (!seen.has(commit.hash)) seen.set(commit.hash, commit.at);
174 return [...seen].map(([hash, at]) => ({ hash, at }));
175}
176
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API177export async function loader({ context, request }: Route.LoaderArgs) {
Initial g1t: services, event bus, intents and attempts178 const viewer = getViewer(context);
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API179 if (!viewer) {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look180 // The signed-out home page is about signing up; it lists no repositories.
181 return data({ signedIn: false as const });
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API182 }
183
184 const now = Date.now();
185 const weekAgo = now - 7 * TIME.DAY;
Agents as a team: lifecycle, merge queue, billing and a new shell186 const started = Date.now();
187 const times: Record<string, number> = {};
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API188 // Each call is timed, and one that fails leaves its section empty rather
189 // than taking the page down.
190 const soft = <T,>(name: string, promise: Promise<T>): Promise<T | null> =>
191 promise.then(
192 (value) => ((times[name] = Date.now() - started), value),
193 (error) => {
194 console.warn(`home: ${name} failed`, error);
195 times[name] = Date.now() - started;
196 return null;
197 },
198 );
199 const okOr = <T,>(result: { ok: true; value: T } | { ok: false } | null): T | null => (result?.ok ? result.value : null);
200
201 const cookies = request.headers.get("cookie");
202 const tz = readCookie(cookies, TZ_COOKIE);
203 const memberships = viewer.workspaces ?? [];
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put204 // The workspace you chose, as the sidebar shows it (lib/workspace-choice.ts).
205 const slug = chosenWorkspace(memberships, readCookie(cookies, WORKSPACE_COOKIE))?.slug ?? null;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API206 const username = viewer.username;
207
208 const reposP = soft("repos", reposApi.list(viewer, { memberOnly: true }));
Fast pages, required checks on the branch, self-hosted runners, honest incidents209 // Workflow jobs stuck waiting for a self-hosted runner that is not there.
210 const stuckJobsP = soft("stuck_jobs", runners.stuck(viewer));
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms211 // The chosen workspace's projects: their open and recently merged pull
Fast pages, required checks on the branch, self-hosted runners, honest incidents212 // requests, all in one call to work, and their recent events, read as
213 // soon as the projects are known.
214 const perRepoP = reposP.then(async (repos) => {
215 const chosen = (repos ?? [])
216 .filter((repo) => slug != null && repo.namespace.toLowerCase() === slug.toLowerCase())
217 .slice(0, MAX_PROJECTS);
Mission control counts people's direct pushes to the default branch218 const [batch, logs, pushes] = await Promise.all([
Fast pages, required checks on the branch, self-hosted runners, honest incidents219 work.pullsForRepos(chosen.map((repo) => repo.id), viewer, PULL_PAGE).catch(() => []),
220 Promise.all(chosen.map((repo) => eventLog.list({ repoId: repo.id, limit: EVENTS_PER_PROJECT }).catch(() => null))),
Mission control counts people's direct pushes to the default branch221 // People's pushes straight to the default branch, which no pull
222 // request counts: the commits each brought, for the week.
223 Promise.all(chosen.map((repo) => directCommits(repo, viewer).catch(() => []))),
Fast pages, required checks on the branch, self-hosted runners, honest incidents224 ]);
225 const byId = new Map(batch.map((entry) => [entry.repoId, entry]));
226 return Promise.all(
227 chosen.map(async (repo, index) => {
228 const found = byId.get(repo.id);
Mission control counts people's direct pushes to the default branch229 if (found) return { repo, pulls: found.open.slice(0, 60), closed: found.closed, events: logs[index], direct: pushes[index] ?? [] };
Fast pages, required checks on the branch, self-hosted runners, honest incidents230 // A fork, which the batch leaves out: asked on its own.
231 const path = { namespace: repo.namespace, name: repo.name };
232 const [pulls, closed] = await Promise.all([
233 work.listPulls(path, viewer, "open").catch(() => null),
234 work.listPulls(path, viewer, "closed").catch(() => null),
235 ]);
236 return {
237 repo,
238 pulls: pulls?.ok ? pulls.value.slice(0, 60) : null,
239 closed: closed?.ok ? closed.value : null,
240 events: logs[index],
Mission control counts people's direct pushes to the default branch241 direct: pushes[index] ?? [],
Fast pages, required checks on the branch, self-hosted runners, honest incidents242 };
243 }),
244 );
245 });
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API246
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all247 const [repos, perRepo, active, models, profile, runs, overview, usage, projectList, memories, invitations, tokens, myTokens, unread] = await Promise.all([
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms248 reposP,
249 soft("projects", perRepoP),
250 soft("pulls", work.listActivePulls(viewer)),
251 slug ? soft("models", env.RUNNER.modelAccess(slug)) : null,
252 soft("profile", identity.profile(username)),
253 slug ? soft("runs", agents.listRuns(viewer, { workspace: slug, limit: 150 })) : null,
254 slug ? soft("deploys", deployments.overview(slug, viewer)) : null,
255 slug ? soft("usage", billing.usage(slug, viewer, new Date(weekAgo - TIME.DAY).toISOString())) : null,
256 slug ? soft("projectList", projectsApi.list(slug, viewer)) : null,
257 slug ? soft("memories", agents.listMemories(viewer, slug, null)) : null,
258 // Repositories someone has invited the viewer to.
259 soft("invitations", identity.myRepoInvitations(viewer)),
Mission control shows model usage, yours and the workspace's: tokens, cost, active days, cache share, each day, and the mix260 // Model tokens over the last six weeks: the workspace's, and yours.
261 slug ? soft("tokens", billing.tokenUsage(slug, viewer)) : null,
262 slug ? soft("my_tokens", billing.tokenUsage(slug, viewer, { person: username })) : null,
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all263 // What in their inbox is unread: an agent waiting on them, or a failure, goes on the Needs you card.
264 soft("inbox", inbox.list(viewer, { unread: true, limit: INBOX_READ })),
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms265 ]);
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API266
267 const repoList = repos ?? [];
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms268 // Each pull request is shown under its repository. Most are in the
269 // viewer's own, already listed; the rest are looked up once each.
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API270 const known = new Map<string, Repo>(repoList.map((repo) => [repo.id, repo]));
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms271 const missing = [...new Set((active ?? []).map(({ pull }) => pull.repoId))].filter((id) => !known.has(id));
Fast pages, required checks on the branch, self-hosted runners, honest incidents272 for (const repo of await readableRepos(missing, viewer)) known.set(repo.id, repo);
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API273 const pathOf = (repo: Repo): RepoPath => ({ namespace: repo.namespace, name: repo.name });
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms274 const inWorkspace = (repo: RepoPath) => slug != null && repo.namespace.toLowerCase() === slug.toLowerCase();
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API275
276 const activeList = (active ?? []).flatMap((item) => {
277 const repo = known.get(item.pull.repoId);
278 return repo ? [{ ...item, repo }] : [];
279 });
280 const runList = okOr(runs ?? null) ?? [];
281 const liveRuns = runList.filter((run) => isActiveRun(run.status));
282 const overviewList = okOr(overview ?? null);
283 const projectsOk = okOr(projectList ?? null);
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms284 const runsOn = (repo: RepoPath, number: number) =>
285 runList.filter(
286 (run) => run.number === number && `${run.repo.namespace}/${run.repo.name}`.toLowerCase() === `${repo.namespace}/${repo.name}`.toLowerCase(),
287 );
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API288
289 const lower = username.toLowerCase();
290 const openPulls = (perRepo ?? []).flatMap(({ repo, pulls }) => (pulls ?? []).map((pull) => ({ pull, repo })));
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights291 // Never your own, nor one g1t made for you.
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API292 const reviewRequested = openPulls.filter(
293 ({ pull }) =>
294 pull.status === "open" &&
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights295 workOwner(pull).username.toLowerCase() !== lower &&
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API296 pull.reviewers.some((name) => name.toLowerCase() === lower),
297 );
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms298
299 // --- Needs you ------------------------------------------------------------
300 const needs: Need[] = [];
301 const extras = new Map<string, Extra>();
302 /** What every pull request's row shares: where it is, who is on it, what is known. */
303 const pullExtra = (pull: Pull, repo: Repo, lifecycle: Lifecycle | null): Extra => {
304 const base = `/${repo.namespace}/${repo.name}`;
305 const agentWork = isAgent(pull.agent);
306 return {
307 repo: pathOf(repo),
308 ref: `#${pull.number}`,
309 by: agentWork ? who(pull.agent) : who(pull.author.username),
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights310 for: agentWork ? workOwner(pull).username : null,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step311 facts: pullFacts({
312 checkStatus: pull.checkStatus,
313 files: pull.files,
314 lifecycle,
315 runs: runsOn(repo, pull.number),
316 confidence: pull.confidence,
317 }),
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms318 open: pull.issue != null ? `${base}/issues/${pull.issue}` : `${base}/pull/${pull.number}?tab=changes`,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step319 confidence: pull.confidence ?? null,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms320 };
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API321 };
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms322 const pullAction = (repo: Repo, pull: Pull, quick: Omit<QuickAction, "to">): QuickAction => ({
323 ...quick,
324 to: `/${repo.namespace}/${repo.name}/pull/${pull.number}`,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API325 });
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms326 const approve = (repo: Repo, pull: Pull) =>
327 pullAction(repo, pull, {
328 label: isAgent(pull.agent) ? "Approve the agent's change" : "Approve the change",
329 fields: { action: "comment", verdict: "approve", body: "" },
330 done: "Approved",
331 });
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API332
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look333 for (const invitation of invitations ?? []) {
334 if (invitation.status !== "pending") continue;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms335 const key = `invitation:${invitation.id}`;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look336 needs.push({
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms337 key,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look338 kind: "invitation",
339 title: `${invitation.invited_by ?? "Someone"} invited you to ${invitation.repo}`,
340 detail: `With the ${REPO_ROLE_LABELS[invitation.role]} role. The invitation expires ${new Date(invitation.expires_at).toISOString().slice(0, 10)}.`,
341 to: `/${invitation.repo}/invitations`,
342 action: "Respond",
343 at: Date.parse(invitation.created_at),
344 where: invitation.repo,
345 });
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms346 const [namespace, name] = invitation.repo.split("/");
347 extras.set(key, {
348 repo: namespace && name ? { namespace, name } : null,
349 by: who(invitation.invited_by),
350 facts: [
351 { label: "Role", value: REPO_ROLE_LABELS[invitation.role], tone: null },
352 { label: "Expires", value: new Date(invitation.expires_at).toISOString().slice(0, 10), tone: null },
353 ],
354 });
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look355 }
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API356 for (const entry of overviewList ?? []) {
357 const latest = entry.latest;
358 if (latest?.kind === "production" && latest.status === "failed" && slug) {
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms359 const key = `deploy:${entry.slug}`;
360 const to = `/${slug}/${entry.slug}/deployments/${latest.id}`;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API361 needs.push({
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms362 key,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API363 kind: "deploy",
364 title: `Production build of ${entry.slug} failed`,
365 detail: latest.error ?? "The last build of the default branch failed. Production still serves the build before it.",
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms366 to,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API367 action: "See the build",
368 at: Date.parse(latest.finishedAt ?? latest.createdAt),
369 where: `${slug}/${entry.slug}`,
370 });
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms371 const facts: Fact[] = [
372 { label: "Commit", value: latest.commit.slice(0, 7), tone: null },
373 {
374 label: "Production",
375 value: entry.production ? "Serving the build before" : "Not live yet",
376 tone: entry.production ? "good" : "warn",
377 },
378 ];
379 extras.set(key, {
380 repo: { namespace: slug, name: entry.slug },
381 by: who(latest.createdBy),
382 facts,
383 link: { label: "Deployment settings", to: `/${slug}/${entry.slug}/settings/deployments` },
384 });
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API385 }
386 }
387 for (const { pull, lifecycle, repo } of activeList) {
388 const where = `${repo.namespace}/${repo.name}#${pull.number}`;
389 const to = `/${repo.namespace}/${repo.name}/pull/${pull.number}`;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms390 const key = `pull:${pull.id}`;
391 const extra = pullExtra(pull, repo, lifecycle);
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step392 // Held for its low confidence: the ask says so in a sentence of its own.
393 const lowConfidence = pull.confidence?.level === "low" ? pull.confidence : null;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API394 if (lifecycle?.stage === "needs_you") {
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step395 const held = lowConfidence != null && /confidence in this change is low/i.test(lifecycle.detail);
396 const conflict = !held && /conflict/i.test(lifecycle.detail);
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms397 const need: Need = {
398 key,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API399 kind: conflict ? "conflict" : "stalled",
400 title: pull.title,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step401 detail: held && lowConfidence ? confidenceAsk(lowConfidence) : lifecycle.detail,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API402 to,
403 action: conflict ? "Resolve" : "Decide",
404 at: Date.parse(pull.updatedAt),
405 where,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms406 };
407 needs.push(need);
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step408 const reason = held ? "low_confidence" : reasonFor(need);
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms409 extras.set(key, {
410 ...extra,
411 quick:
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step412 reason === "needs_review" || reason === "low_confidence"
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms413 ? approve(repo, pull)
Fast pages, required checks on the branch, self-hosted runners, honest incidents414 : /required checks? .*still fails?\b/i.test(lifecycle.detail)
415 ? pullAction(repo, pull, { label: "Re-run failed jobs", fields: { action: "rerun-failed" }, done: "Re-running" })
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms416 : null,
417 link: reason === "outside_guardrails" ? { label: "Raise the cap", to: `/${repo.namespace}/${repo.name}/settings/guardrails` } : null,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API418 });
419 } else if (lifecycle?.stage === "ready") {
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step420 needs.push({
421 key,
422 kind: "ready",
423 title: pull.title,
Fast pages, required checks on the branch, self-hosted runners, honest incidents424 detail: lowConfidence ? confidenceAsk(lowConfidence) : "Its required checks passed and it was approved. It lands when you merge it.",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step425 to,
426 action: "Merge",
427 at: Date.parse(pull.updatedAt),
428 where,
429 });
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms430 extras.set(key, {
431 ...extra,
432 quick: pullAction(repo, pull, {
433 label: isAgent(pull.agent) ? "Merge the agent's change" : "Merge it",
434 fields: { action: "merge" },
435 done: "Merging",
436 }),
437 });
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API438 } else if (!lifecycle && pull.status === "open" && pull.checkStatus === "failed") {
Fast pages, required checks on the branch, self-hosted runners, honest incidents439 // Taken out of the merge queue: its change failed combined with what was ahead.
440 needs.push({ key, kind: "checks", title: pull.title, detail: "It failed in the merge queue. Push a fix, then merge it again.", to, action: "See checks", at: Date.parse(pull.updatedAt), where });
441 extras.set(key, { ...extra, quick: null });
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API442 }
443 }
444 for (const { pull, repo } of reviewRequested) {
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms445 const key = `review:${pull.id}`;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API446 needs.push({
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms447 key,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API448 kind: "review",
449 title: pull.title,
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights450 detail: `${workOwner(pull).username} asked for your review.`,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API451 to: `/${repo.namespace}/${repo.name}/pull/${pull.number}`,
452 action: "Review",
453 at: Date.parse(pull.updatedAt),
454 where: `${repo.namespace}/${repo.name}#${pull.number}`,
455 });
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms456 extras.set(key, { ...pullExtra(pull, repo, null), quick: approve(repo, pull) });
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API457 }
Fast pages, required checks on the branch, self-hosted runners, honest incidents458 for (const job of (await stuckJobsP) ?? []) {
459 const key = `runner:${job.id}`;
460 const minutes = Math.max(10, Math.round((now - Date.parse(job.queuedAt)) / 60_000));
461 const [namespace, name] = job.repo.split("/");
462 needs.push({
463 key,
464 kind: "stuck",
465 title: `${job.name} is waiting for a self-hosted runner`,
466 detail: `No runner with labels ${job.labels} is online. Start one, or change the job's runs-on.`,
467 to: `/${job.repo}/actions/runs/${job.runId}`,
468 action: "Look",
469 at: Date.parse(job.queuedAt),
470 where: job.repo,
471 });
472 extras.set(key, {
473 repo: namespace && name ? { namespace, name } : undefined,
474 facts: [
475 { label: "Labels", value: job.labels, tone: null },
476 { label: "Waiting", value: `${minutes} min`, tone: "warn" },
477 ],
Runners are owner-only: the page, its actions, the Settings entry and the links to it478 // Runners are the owners' to see to.
479 ...(namespace && roleIn(viewer, namespace) === "owner" ? { link: { label: "Runners", to: `/${namespace}/-/runners` } } : {}),
Fast pages, required checks on the branch, self-hosted runners, honest incidents480 });
481 }
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API482 for (const run of liveRuns) {
483 const minutes = stuckMinutes(run, now);
484 if (minutes == null) continue;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms485 const key = `run:${run.id}`;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API486 needs.push({
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms487 key,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API488 kind: "stuck",
489 title: run.title ?? `${run.agent}'s run`,
490 detail: `${run.agent} has reported nothing for ${minutes} min${run.step ? `. Last: ${run.step}` : ""}.`,
491 to: `/${run.repo.namespace}/${run.repo.name}/agents/runs/${run.id}`,
492 action: "Look",
493 at: Date.parse(run.updatedAt),
494 where: `${run.repo.namespace}/${run.repo.name}${run.number != null ? `#${run.number}` : ""}`,
495 });
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms496 extras.set(key, {
497 repo: run.repo,
498 ref: run.number != null ? `#${run.number}` : null,
499 by: who(run.agent),
500 facts: [
501 { label: "Run", value: RUN_LABEL[run.kind], tone: null },
502 { label: "Quiet for", value: `${minutes} min`, tone: "warn" },
503 { label: "Steps so far", value: String(run.stepCount), tone: null },
504 ...(run.costUsd != null ? [{ label: "Cost so far", value: usd(run.costUsd), tone: null }] : []),
505 ],
506 open: run.number != null ? `/${run.repo.namespace}/${run.repo.name}/pull/${run.number}` : undefined,
507 });
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API508 }
509
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms510 const needRows: NeedRow[] = rankNeeds(needs).map((need) => {
511 const extra = extras.get(need.key) ?? {};
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step512 const reason = withConfidence(reasonFor(need), extra.confidence);
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms513 return {
514 key: need.key,
515 reason,
516 repo: extra.repo ?? null,
517 ref: extra.ref ?? null,
518 title: need.title,
519 ask: need.detail,
520 by: extra.by ?? null,
521 for: extra.for ?? null,
522 at: need.at,
523 to: need.to,
524 open: extra.open ?? need.to,
525 facts: extra.facts ?? [],
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step526 why: whyFor(reason, need, extra.confidence),
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms527 quick: extra.quick ?? null,
528 link: extra.link ?? null,
529 };
530 });
531
532 // --- Waiting on agents ----------------------------------------------------
533 const needKeys = new Set(
534 needRows.flatMap((row) => (row.repo && row.ref ? [needPathKey(row.repo, Number(row.ref.slice(1)))] : [])),
535 );
536 const waiting = waitingRows({
537 active: activeList.filter(({ repo }) => inWorkspace(repo)).map(({ pull, lifecycle, repo }) => ({ pull, lifecycle, repo: pathOf(repo) })),
538 live: liveRuns,
539 drafts: openPulls.filter(({ pull }) => pull.status === "draft").map(({ pull, repo }) => ({ ...pull, repo: pathOf(repo) })),
540 needKeys,
541 });
542
543 // --- Landed ---------------------------------------------------------------
544 const merged: Merged[] = (perRepo ?? []).flatMap(({ repo, closed }) =>
545 (closed ?? []).flatMap((pull) =>
546 pull.status === "merged" && pull.mergedAt
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar547 ? [
548 {
549 repo: pathOf(repo),
550 number: pull.number,
551 title: pull.title,
552 agent: pull.agent,
553 // Who wrote it, not who merged it: a person's change that g1t
554 // auto-merged is still theirs.
555 authoredByAgent: pull.author.kind === "agent" || isAgent(pull.author.username),
556 mergedBy: pull.mergedBy,
557 mergedAt: pull.mergedAt,
558 files: pull.files,
559 },
560 ]
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms561 : [],
562 ),
563 );
564 const twoWeeksAgo = now - 14 * TIME.DAY;
565 const complete = perRepo != null && perRepo.every(({ closed }) => closed != null && reachesBack(closed, twoWeeksAgo, PULL_PAGE));
Mission control counts people's direct pushes to the default branch566 // A person's commits pushed straight to the default branch are their own
567 // changes too, on the day they landed.
568 const direct = (perRepo ?? []).flatMap(({ direct }) =>
569 direct.map((commit) => ({ mergedAt: commit.at, mergedBy: null, authoredByAgent: false })),
570 );
571 const week = weekOf([...merged, ...direct], now, tz, complete);
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms572 const landed = landedToday(merged, now, tz);
573
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API574 // --- Activity -------------------------------------------------------------
575 const items: ActivityItem[] = [];
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms576 const titles: Record<string, string> = {};
577 const titleKey = (repo: RepoPath, number: number) => `${repo.namespace}/${repo.name}#${number}`.toLowerCase();
578 for (const { repo, pulls, closed, events } of perRepo ?? []) {
579 for (const pull of [...(pulls ?? []), ...(closed ?? [])]) titles[titleKey(repo, pull.number)] = pull.title;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API580 for (const event of events ?? []) {
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms581 if (event.type === "issue.opened") titles[titleKey(repo, event.data.number)] ??= event.data.title;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API582 const item = eventItem(event, pathOf(repo));
583 if (item) items.push(item);
584 }
585 }
586 for (const entry of overviewList ?? []) {
587 const latest = entry.latest;
588 const served = latest?.status === "ready" || latest?.status === "replaced";
589 if (!slug || latest?.kind !== "production" || (!served && latest.status !== "failed")) continue;
590 items.push({
591 id: `deploy:${latest.id}`,
592 at: Date.parse(latest.finishedAt ?? latest.createdAt),
593 repo: { namespace: slug, name: entry.slug },
594 actor: latest.createdBy,
595 verb: served ? "deployed" : "deploy_failed",
596 number: null,
597 to: `/${slug}/${entry.slug}/deployments/${latest.id}`,
598 });
599 }
600 for (const memory of okOr(memories ?? null)?.workspace ?? []) {
601 const repo = memory.repo ?? memory.source.repo;
602 if (!repo || !slug) continue;
603 items.push({
604 id: `memory:${memory.id}`,
605 at: Date.parse(memory.createdAt),
606 repo,
607 actor: memory.createdBy,
608 verb: "learned",
609 number: null,
610 text: memory.text.length > 90 ? `${memory.text.slice(0, 88)}…` : memory.text,
611 to: `/${slug}/-/memory`,
612 });
613 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms614 const groups = groupActivity(items).slice(0, 40);
615 // Only the titles the feed names travel to the page.
616 const shownTitles: Record<string, string> = {};
617 for (const group of groups) {
618 for (const part of group.parts) {
619 for (const number of part.numbers) {
620 const key = titleKey(group.repo, number);
621 if (titles[key]) shownTitles[key] = titles[key];
622 }
623 }
624 }
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API625
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms626 // --- The strip ------------------------------------------------------------
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API627 const usageOk = okOr(usage ?? null);
628 const costPoints = usageOk
629 ? usageOk.byDay.map((slice) => ({ at: Date.parse(`${slice.key.split("/")[0]}T12:00:00Z`), value: slice.micros / 1_000_000 }))
630 : runList.filter((run) => run.costUsd != null).map((run) => ({ at: Date.parse(run.createdAt), value: run.costUsd ?? 0 }));
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms631 const weekCost = dailyBuckets(costPoints, 7, now).reduce((sum, v) => sum + v, 0);
632 const month = dayKey(now, tz).slice(0, 7);
633 const projectCount = projectsOk?.length ?? (perRepo != null ? perRepo.length : null);
634 const projectsThisMonth = projectsOk ? projectsOk.filter((project) => dayKey(Date.parse(project.createdAt), tz).slice(0, 7) === month).length : null;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API635
636 const models_ = models ?? null;
637 times.total = Date.now() - started;
Agents as a team: lifecycle, merge queue, billing and a new shell638 const serverTiming = Object.entries(times)
639 .map(([name, ms]) => `${name};dur=${ms}`)
640 .join(", ");
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API641 return data(
642 {
643 signedIn: true as const,
644 viewer,
645 name: profile?.name?.trim() || username,
646 greeting: greetingFor(hourIn(now, tz)),
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms647 date: dateLine(now, tz),
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar648 summary: summaryLine({
649 total: week.total,
650 byAgents: week.byAgents,
651 agentChanges: week.agentChanges,
652 people: week.people,
653 live: liveRuns.length,
654 needs: needRows.length,
655 }),
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms656 workspace: slug,
657 repos: repoList.filter((repo) => inWorkspace(repo)).map(pathOf),
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API658 canRunAgents: models_ == null || models_.hosted || models_.own != null,
659 trial: models_?.own == null ? (models_?.trial ?? null) : null,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms660 handedOff: activeList.length > 0 || runList.length > 0 || merged.length > 0,
661 needs: needRows,
662 waiting,
663 landed,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API664 liveTotal: liveRuns.length,
665 runsLoaded: runs != null && runs.ok,
666 perRepoLoaded: perRepo != null,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms667 stats: {
668 projects: projectCount,
669 projectsThisMonth,
670 agentHours: agentHours(runList, weekAgo, now),
671 weekCost,
672 },
673 week,
Mission control shows model usage, yours and the workspace's: tokens, cost, active days, cache share, each day, and the mix674 tokens: { workspace: okOr(tokens), mine: okOr(myTokens) },
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API675 groups,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms676 titles: shownTitles,
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all677 inboxNeeds: needsYou(unread?.items ?? [], INBOX_SHOWN),
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms678 // A run that is going makes the page worth refreshing on its own.
679 changing:
680 liveRuns.length > 0 ||
681 activeList.some((item) => item.lifecycle && item.lifecycle.stage !== "needs_you" && item.lifecycle.stage !== "ready"),
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API682 },
683 {
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms684 headers: { "Server-Timing": serverTiming },
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API685 },
686 );
Agents as a team: lifecycle, merge queue, billing and a new shell687}
Get started on mission control688
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look689export type Loaded = Extract<Route.ComponentProps["loaderData"], { signedIn: true }>;
Get started on mission control690
Fast pages, required checks on the branch, self-hosted runners, honest incidents691/**
692 * Mission control, loaded only for someone signed in (components/mission-control.tsx).
693 * The app shell starts loading it once the browser is idle, so the
694 * skeleton below is rarely seen.
695 */
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look696const MissionControl = lazy(() => import("../components/mission-control"));
Agents as a team: lifecycle, merge queue, billing and a new shell697
Fast pages, required checks on the branch, self-hosted runners, honest incidents698/** Mission control's outline, the same size, while its code arrives. */
699function MissionControlSkeleton() {
700 return (
701 <main aria-busy="true" className="mx-auto max-w-7xl space-y-6 px-4 py-8 sm:px-6 sm:py-10">
702 <header>
703 <Skeleton className="h-8 w-72 sm:h-9" />
704 <Skeleton className="mt-2.5 h-3.5 w-96 max-w-full" />
705 </header>
706 <div className="grid grid-cols-2 gap-px overflow-hidden rounded-xl border border-line bg-line lg:grid-cols-5">
707 {Array.from({ length: 5 }, (_, index) => (
708 <div key={index} className="bg-surface p-4">
709 <Skeleton className="h-3 w-16" />
710 <Skeleton className="mt-3 h-6 w-10" />
711 <Skeleton className="mt-2 h-3 w-24" />
712 </div>
713 ))}
714 </div>
715 <div className="rounded-xl border border-line bg-surface">
716 <SkeletonRows rows={5} rowClassName="h-14" />
717 </div>
718 </main>
719 );
720}
721
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step722export default function Home({ loaderData, actionData }: Route.ComponentProps) {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look723 if (!loaderData.signedIn) return <Landing />;
API and MCP server, Rust identity service, registration, site redesign724 return (
Fast pages, required checks on the branch, self-hosted runners, honest incidents725 <Suspense fallback={<MissionControlSkeleton />}>
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step726 <MissionControl loaderData={loaderData} delegated={actionData ?? null} />
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look727 </Suspense>
Initial g1t: services, event bus, intents and attempts728 );
729}