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

718 lines30,230 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";
API and MCP server, Rust identity service, registration, site redesign60import { Landing } from "../components/landing";
Fast pages, required checks on the branch, self-hosted runners, honest incidents61import { Skeleton, SkeletonRows } from "../components/ui/skeleton";
API and MCP server, Rust identity service, registration, site redesign62import {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API63 agents,
64 billing,
65 deployments,
66 events as eventLog,
67 identity,
68 projects as projectsApi,
69 repos as reposApi,
70 work,
71} from "../lib/services.server";
Runners are owner-only: the page, its actions, the Settings entry and the links to it72import { assertSameOrigin, getViewer, requireUser, roleIn } from "../lib/session.server";
Fast pages, required checks on the branch, self-hosted runners, honest incidents73import { runners } from "../lib/runners.server";
74import { readableRepos } from "../lib/access.server";
Initial g1t: services, event bus, intents and attempts75
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look76/** 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 API77const TZ_COOKIE = "g1t_tz";
78/** The most projects whose pull requests and events are read for the page. */
79const MAX_PROJECTS = 10;
80const EVENTS_PER_PROJECT = 80;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms81/** How many pull requests work lists at once (`LIST_PAGE` in services/work). */
82const PULL_PAGE = 100;
A free allowance on g1t's models, so anyone can try its agents83
Social cards for every page: og.g1t.sh84export function meta(args: Route.MetaArgs) {
85 return page(args, {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look86 title: "g1t — where people and agents ship software together",
Social cards for every page: og.g1t.sh87 description:
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look88 "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.sh89 });
Initial g1t: services, event bus, intents and attempts90}
91
Agents as a team: lifecycle, merge queue, billing and a new shell92export function headers({ loaderHeaders }: Route.HeadersArgs) {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API93 const out = new Headers();
94 const timing = loaderHeaders.get("Server-Timing");
95 if (timing) out.set("Server-Timing", timing);
96 const cookie = loaderHeaders.get("Set-Cookie");
97 if (cookie) out.set("Set-Cookie", cookie);
98 return out;
Agents as a team: lifecycle, merge queue, billing and a new shell99}
100
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms101/**
102 * Switching tabs, the sort or the activity list changes only the address:
103 * everything is already loaded. A refresh, a form, or anything else loads
104 * again as usual.
105 */
106export function shouldRevalidate({ currentUrl, nextUrl, formMethod, defaultShouldRevalidate }: ShouldRevalidateFunctionArgs) {
107 if (!formMethod && currentUrl.pathname === nextUrl.pathname && currentUrl.search !== nextUrl.search) return false;
108 return defaultShouldRevalidate;
109}
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API110
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms111/** 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 step112type Extra = Partial<Pick<NeedRow, "repo" | "ref" | "by" | "for" | "facts" | "quick" | "link" | "open">> & {
113 /** How sure g1t is of the agent's change, for a pull request. */
114 confidence?: Confidence | null;
115};
116
117/** What the composer said back, when the issue opened but the agent did not start, or nothing opened. */
118export 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 API119
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step120/**
121 * "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-agent122 * 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 step123 * start, the issue is still open, and the composer says why and where to
124 * fix it.
125 */
126export async function action({ request, context }: Route.ActionArgs) {
127 assertSameOrigin(request);
128 const user = requireUser(context, request);
129 const form = await request.formData();
130 if (form.get("intent") !== "delegate") return data<DelegateResult>({ error: "Nothing to do.", notStarted: null }, { status: 400 });
131 const projects = await reposApi.list(user, { memberOnly: true }).catch(() => []);
132 const repo = chosenRepo(form.get("repo"), projects.map((repo) => ({ namespace: repo.namespace, name: repo.name })));
133 if (!repo) return data<DelegateResult>({ error: "Choose one of your projects.", notStarted: null }, { status: 400 });
134 const result = await env.RUNNER.delegate(user, repo, delegateForm(form));
135 if (!result.ok) return data<DelegateResult>({ error: result.error.message, notStarted: null }, { status: 400 });
136 const { issue, agent } = result.value;
137 const refused = notStarted(agent, repo, issue.number);
138 if (!refused) throw redirect(issuePath(repo, issue.number));
139 return data<DelegateResult>({ error: null, notStarted: refused });
140}
141
Mission control counts people's direct pushes to the default branch142/** Pushes to the default branch read for the week, per project, and commits read back from each. */
143const PUSHES_READ = 40;
144const PUSH_DEPTH = 60;
145
146/**
147 * The commits people pushed straight to a project's default branch in the
148 * last two weeks, each with when its push landed. Merges and agents'
149 * commits are left out: pull requests count those.
150 */
151async function directCommits(repo: Repo, viewer: Viewer): Promise<{ hash: string; at: string }[]> {
152 const since = Date.now() - 14 * TIME.DAY;
153 const pushes = (await eventLog.list({ repoId: repo.id, types: ["git.push"], limit: PUSHES_READ })).filter(
154 (event): event is G1tEvent<"git.push"> => event.type === "git.push" && event.data.defaultBranch && Date.parse(event.time) >= since,
155 );
156 const path = { namespace: repo.namespace, name: repo.name };
157 const read = await Promise.all(
158 pushes.map(async (push) => {
159 const history = await reposApi.log(path, viewer, push.data.after, PUSH_DEPTH).catch(() => null);
160 return history?.ok ? pushedCommits(history.value, push.data.before).map((commit) => ({ hash: commit.hash, at: push.time })) : [];
161 }),
162 );
163 // A commit pushed twice (after a force push, say) counts once, at its first landing.
164 const seen = new Map<string, string>();
165 for (const commit of read.flat().reverse()) if (!seen.has(commit.hash)) seen.set(commit.hash, commit.at);
166 return [...seen].map(([hash, at]) => ({ hash, at }));
167}
168
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API169export async function loader({ context, request }: Route.LoaderArgs) {
Initial g1t: services, event bus, intents and attempts170 const viewer = getViewer(context);
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API171 if (!viewer) {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look172 // The signed-out home page is about signing up; it lists no repositories.
173 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 API174 }
175
176 const now = Date.now();
177 const weekAgo = now - 7 * TIME.DAY;
Agents as a team: lifecycle, merge queue, billing and a new shell178 const started = Date.now();
179 const times: Record<string, number> = {};
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API180 // Each call is timed, and one that fails leaves its section empty rather
181 // than taking the page down.
182 const soft = <T,>(name: string, promise: Promise<T>): Promise<T | null> =>
183 promise.then(
184 (value) => ((times[name] = Date.now() - started), value),
185 (error) => {
186 console.warn(`home: ${name} failed`, error);
187 times[name] = Date.now() - started;
188 return null;
189 },
190 );
191 const okOr = <T,>(result: { ok: true; value: T } | { ok: false } | null): T | null => (result?.ok ? result.value : null);
192
193 const cookies = request.headers.get("cookie");
194 const tz = readCookie(cookies, TZ_COOKIE);
195 const memberships = viewer.workspaces ?? [];
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put196 // The workspace you chose, as the sidebar shows it (lib/workspace-choice.ts).
197 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 API198 const username = viewer.username;
199
200 const reposP = soft("repos", reposApi.list(viewer, { memberOnly: true }));
Fast pages, required checks on the branch, self-hosted runners, honest incidents201 // Workflow jobs stuck waiting for a self-hosted runner that is not there.
202 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 200ms203 // The chosen workspace's projects: their open and recently merged pull
Fast pages, required checks on the branch, self-hosted runners, honest incidents204 // requests, all in one call to work, and their recent events, read as
205 // soon as the projects are known.
206 const perRepoP = reposP.then(async (repos) => {
207 const chosen = (repos ?? [])
208 .filter((repo) => slug != null && repo.namespace.toLowerCase() === slug.toLowerCase())
209 .slice(0, MAX_PROJECTS);
Mission control counts people's direct pushes to the default branch210 const [batch, logs, pushes] = await Promise.all([
Fast pages, required checks on the branch, self-hosted runners, honest incidents211 work.pullsForRepos(chosen.map((repo) => repo.id), viewer, PULL_PAGE).catch(() => []),
212 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 branch213 // People's pushes straight to the default branch, which no pull
214 // request counts: the commits each brought, for the week.
215 Promise.all(chosen.map((repo) => directCommits(repo, viewer).catch(() => []))),
Fast pages, required checks on the branch, self-hosted runners, honest incidents216 ]);
217 const byId = new Map(batch.map((entry) => [entry.repoId, entry]));
218 return Promise.all(
219 chosen.map(async (repo, index) => {
220 const found = byId.get(repo.id);
Mission control counts people's direct pushes to the default branch221 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 incidents222 // A fork, which the batch leaves out: asked on its own.
223 const path = { namespace: repo.namespace, name: repo.name };
224 const [pulls, closed] = await Promise.all([
225 work.listPulls(path, viewer, "open").catch(() => null),
226 work.listPulls(path, viewer, "closed").catch(() => null),
227 ]);
228 return {
229 repo,
230 pulls: pulls?.ok ? pulls.value.slice(0, 60) : null,
231 closed: closed?.ok ? closed.value : null,
232 events: logs[index],
Mission control counts people's direct pushes to the default branch233 direct: pushes[index] ?? [],
Fast pages, required checks on the branch, self-hosted runners, honest incidents234 };
235 }),
236 );
237 });
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API238
Mission control shows model usage, yours and the workspace's: tokens, cost, active days, cache share, each day, and the mix239 const [repos, perRepo, active, models, profile, runs, overview, usage, projectList, memories, invitations, tokens, myTokens] = await Promise.all([
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms240 reposP,
241 soft("projects", perRepoP),
242 soft("pulls", work.listActivePulls(viewer)),
243 slug ? soft("models", env.RUNNER.modelAccess(slug)) : null,
244 soft("profile", identity.profile(username)),
245 slug ? soft("runs", agents.listRuns(viewer, { workspace: slug, limit: 150 })) : null,
246 slug ? soft("deploys", deployments.overview(slug, viewer)) : null,
247 slug ? soft("usage", billing.usage(slug, viewer, new Date(weekAgo - TIME.DAY).toISOString())) : null,
248 slug ? soft("projectList", projectsApi.list(slug, viewer)) : null,
249 slug ? soft("memories", agents.listMemories(viewer, slug, null)) : null,
250 // Repositories someone has invited the viewer to.
251 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 mix252 // Model tokens over the last six weeks: the workspace's, and yours.
253 slug ? soft("tokens", billing.tokenUsage(slug, viewer)) : null,
254 slug ? soft("my_tokens", billing.tokenUsage(slug, viewer, { person: username })) : null,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms255 ]);
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API256
257 const repoList = repos ?? [];
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms258 // Each pull request is shown under its repository. Most are in the
259 // 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 API260 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 200ms261 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 incidents262 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 API263 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 200ms264 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 API265
266 const activeList = (active ?? []).flatMap((item) => {
267 const repo = known.get(item.pull.repoId);
268 return repo ? [{ ...item, repo }] : [];
269 });
270 const runList = okOr(runs ?? null) ?? [];
271 const liveRuns = runList.filter((run) => isActiveRun(run.status));
272 const overviewList = okOr(overview ?? null);
273 const projectsOk = okOr(projectList ?? null);
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms274 const runsOn = (repo: RepoPath, number: number) =>
275 runList.filter(
276 (run) => run.number === number && `${run.repo.namespace}/${run.repo.name}`.toLowerCase() === `${repo.namespace}/${repo.name}`.toLowerCase(),
277 );
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API278
279 const lower = username.toLowerCase();
280 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 rights281 // 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 API282 const reviewRequested = openPulls.filter(
283 ({ pull }) =>
284 pull.status === "open" &&
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights285 workOwner(pull).username.toLowerCase() !== lower &&
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API286 pull.reviewers.some((name) => name.toLowerCase() === lower),
287 );
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms288
289 // --- Needs you ------------------------------------------------------------
290 const needs: Need[] = [];
291 const extras = new Map<string, Extra>();
292 /** What every pull request's row shares: where it is, who is on it, what is known. */
293 const pullExtra = (pull: Pull, repo: Repo, lifecycle: Lifecycle | null): Extra => {
294 const base = `/${repo.namespace}/${repo.name}`;
295 const agentWork = isAgent(pull.agent);
296 return {
297 repo: pathOf(repo),
298 ref: `#${pull.number}`,
299 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 rights300 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 step301 facts: pullFacts({
302 checkStatus: pull.checkStatus,
303 files: pull.files,
304 lifecycle,
305 runs: runsOn(repo, pull.number),
306 confidence: pull.confidence,
307 }),
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms308 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 step309 confidence: pull.confidence ?? null,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms310 };
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API311 };
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms312 const pullAction = (repo: Repo, pull: Pull, quick: Omit<QuickAction, "to">): QuickAction => ({
313 ...quick,
314 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 API315 });
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms316 const approve = (repo: Repo, pull: Pull) =>
317 pullAction(repo, pull, {
318 label: isAgent(pull.agent) ? "Approve the agent's change" : "Approve the change",
319 fields: { action: "comment", verdict: "approve", body: "" },
320 done: "Approved",
321 });
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API322
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look323 for (const invitation of invitations ?? []) {
324 if (invitation.status !== "pending") continue;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms325 const key = `invitation:${invitation.id}`;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look326 needs.push({
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms327 key,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look328 kind: "invitation",
329 title: `${invitation.invited_by ?? "Someone"} invited you to ${invitation.repo}`,
330 detail: `With the ${REPO_ROLE_LABELS[invitation.role]} role. The invitation expires ${new Date(invitation.expires_at).toISOString().slice(0, 10)}.`,
331 to: `/${invitation.repo}/invitations`,
332 action: "Respond",
333 at: Date.parse(invitation.created_at),
334 where: invitation.repo,
335 });
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms336 const [namespace, name] = invitation.repo.split("/");
337 extras.set(key, {
338 repo: namespace && name ? { namespace, name } : null,
339 by: who(invitation.invited_by),
340 facts: [
341 { label: "Role", value: REPO_ROLE_LABELS[invitation.role], tone: null },
342 { label: "Expires", value: new Date(invitation.expires_at).toISOString().slice(0, 10), tone: null },
343 ],
344 });
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look345 }
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API346 for (const entry of overviewList ?? []) {
347 const latest = entry.latest;
348 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 200ms349 const key = `deploy:${entry.slug}`;
350 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 API351 needs.push({
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms352 key,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API353 kind: "deploy",
354 title: `Production build of ${entry.slug} failed`,
355 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 200ms356 to,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API357 action: "See the build",
358 at: Date.parse(latest.finishedAt ?? latest.createdAt),
359 where: `${slug}/${entry.slug}`,
360 });
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms361 const facts: Fact[] = [
362 { label: "Commit", value: latest.commit.slice(0, 7), tone: null },
363 {
364 label: "Production",
365 value: entry.production ? "Serving the build before" : "Not live yet",
366 tone: entry.production ? "good" : "warn",
367 },
368 ];
369 extras.set(key, {
370 repo: { namespace: slug, name: entry.slug },
371 by: who(latest.createdBy),
372 facts,
373 link: { label: "Deployment settings", to: `/${slug}/${entry.slug}/settings/deployments` },
374 });
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API375 }
376 }
377 for (const { pull, lifecycle, repo } of activeList) {
378 const where = `${repo.namespace}/${repo.name}#${pull.number}`;
379 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 200ms380 const key = `pull:${pull.id}`;
381 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 step382 // Held for its low confidence: the ask says so in a sentence of its own.
383 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 API384 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 step385 const held = lowConfidence != null && /confidence in this change is low/i.test(lifecycle.detail);
386 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 200ms387 const need: Need = {
388 key,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API389 kind: conflict ? "conflict" : "stalled",
390 title: pull.title,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step391 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 API392 to,
393 action: conflict ? "Resolve" : "Decide",
394 at: Date.parse(pull.updatedAt),
395 where,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms396 };
397 needs.push(need);
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step398 const reason = held ? "low_confidence" : reasonFor(need);
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms399 extras.set(key, {
400 ...extra,
401 quick:
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step402 reason === "needs_review" || reason === "low_confidence"
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms403 ? approve(repo, pull)
Fast pages, required checks on the branch, self-hosted runners, honest incidents404 : /required checks? .*still fails?\b/i.test(lifecycle.detail)
405 ? 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 200ms406 : null,
407 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 API408 });
409 } 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 step410 needs.push({
411 key,
412 kind: "ready",
413 title: pull.title,
Fast pages, required checks on the branch, self-hosted runners, honest incidents414 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 step415 to,
416 action: "Merge",
417 at: Date.parse(pull.updatedAt),
418 where,
419 });
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms420 extras.set(key, {
421 ...extra,
422 quick: pullAction(repo, pull, {
423 label: isAgent(pull.agent) ? "Merge the agent's change" : "Merge it",
424 fields: { action: "merge" },
425 done: "Merging",
426 }),
427 });
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API428 } else if (!lifecycle && pull.status === "open" && pull.checkStatus === "failed") {
Fast pages, required checks on the branch, self-hosted runners, honest incidents429 // Taken out of the merge queue: its change failed combined with what was ahead.
430 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 });
431 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 API432 }
433 }
434 for (const { pull, repo } of reviewRequested) {
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms435 const key = `review:${pull.id}`;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API436 needs.push({
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms437 key,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API438 kind: "review",
439 title: pull.title,
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights440 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 API441 to: `/${repo.namespace}/${repo.name}/pull/${pull.number}`,
442 action: "Review",
443 at: Date.parse(pull.updatedAt),
444 where: `${repo.namespace}/${repo.name}#${pull.number}`,
445 });
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms446 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 API447 }
Fast pages, required checks on the branch, self-hosted runners, honest incidents448 for (const job of (await stuckJobsP) ?? []) {
449 const key = `runner:${job.id}`;
450 const minutes = Math.max(10, Math.round((now - Date.parse(job.queuedAt)) / 60_000));
451 const [namespace, name] = job.repo.split("/");
452 needs.push({
453 key,
454 kind: "stuck",
455 title: `${job.name} is waiting for a self-hosted runner`,
456 detail: `No runner with labels ${job.labels} is online. Start one, or change the job's runs-on.`,
457 to: `/${job.repo}/actions/runs/${job.runId}`,
458 action: "Look",
459 at: Date.parse(job.queuedAt),
460 where: job.repo,
461 });
462 extras.set(key, {
463 repo: namespace && name ? { namespace, name } : undefined,
464 facts: [
465 { label: "Labels", value: job.labels, tone: null },
466 { label: "Waiting", value: `${minutes} min`, tone: "warn" },
467 ],
Runners are owner-only: the page, its actions, the Settings entry and the links to it468 // Runners are the owners' to see to.
469 ...(namespace && roleIn(viewer, namespace) === "owner" ? { link: { label: "Runners", to: `/${namespace}/-/runners` } } : {}),
Fast pages, required checks on the branch, self-hosted runners, honest incidents470 });
471 }
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API472 for (const run of liveRuns) {
473 const minutes = stuckMinutes(run, now);
474 if (minutes == null) continue;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms475 const key = `run:${run.id}`;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API476 needs.push({
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms477 key,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API478 kind: "stuck",
479 title: run.title ?? `${run.agent}'s run`,
480 detail: `${run.agent} has reported nothing for ${minutes} min${run.step ? `. Last: ${run.step}` : ""}.`,
481 to: `/${run.repo.namespace}/${run.repo.name}/agents/runs/${run.id}`,
482 action: "Look",
483 at: Date.parse(run.updatedAt),
484 where: `${run.repo.namespace}/${run.repo.name}${run.number != null ? `#${run.number}` : ""}`,
485 });
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms486 extras.set(key, {
487 repo: run.repo,
488 ref: run.number != null ? `#${run.number}` : null,
489 by: who(run.agent),
490 facts: [
491 { label: "Run", value: RUN_LABEL[run.kind], tone: null },
492 { label: "Quiet for", value: `${minutes} min`, tone: "warn" },
493 { label: "Steps so far", value: String(run.stepCount), tone: null },
494 ...(run.costUsd != null ? [{ label: "Cost so far", value: usd(run.costUsd), tone: null }] : []),
495 ],
496 open: run.number != null ? `/${run.repo.namespace}/${run.repo.name}/pull/${run.number}` : undefined,
497 });
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API498 }
499
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms500 const needRows: NeedRow[] = rankNeeds(needs).map((need) => {
501 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 step502 const reason = withConfidence(reasonFor(need), extra.confidence);
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms503 return {
504 key: need.key,
505 reason,
506 repo: extra.repo ?? null,
507 ref: extra.ref ?? null,
508 title: need.title,
509 ask: need.detail,
510 by: extra.by ?? null,
511 for: extra.for ?? null,
512 at: need.at,
513 to: need.to,
514 open: extra.open ?? need.to,
515 facts: extra.facts ?? [],
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step516 why: whyFor(reason, need, extra.confidence),
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms517 quick: extra.quick ?? null,
518 link: extra.link ?? null,
519 };
520 });
521
522 // --- Waiting on agents ----------------------------------------------------
523 const needKeys = new Set(
524 needRows.flatMap((row) => (row.repo && row.ref ? [needPathKey(row.repo, Number(row.ref.slice(1)))] : [])),
525 );
526 const waiting = waitingRows({
527 active: activeList.filter(({ repo }) => inWorkspace(repo)).map(({ pull, lifecycle, repo }) => ({ pull, lifecycle, repo: pathOf(repo) })),
528 live: liveRuns,
529 drafts: openPulls.filter(({ pull }) => pull.status === "draft").map(({ pull, repo }) => ({ ...pull, repo: pathOf(repo) })),
530 needKeys,
531 });
532
533 // --- Landed ---------------------------------------------------------------
534 const merged: Merged[] = (perRepo ?? []).flatMap(({ repo, closed }) =>
535 (closed ?? []).flatMap((pull) =>
536 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 bar537 ? [
538 {
539 repo: pathOf(repo),
540 number: pull.number,
541 title: pull.title,
542 agent: pull.agent,
543 // Who wrote it, not who merged it: a person's change that g1t
544 // auto-merged is still theirs.
545 authoredByAgent: pull.author.kind === "agent" || isAgent(pull.author.username),
546 mergedBy: pull.mergedBy,
547 mergedAt: pull.mergedAt,
548 files: pull.files,
549 },
550 ]
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms551 : [],
552 ),
553 );
554 const twoWeeksAgo = now - 14 * TIME.DAY;
555 const complete = perRepo != null && perRepo.every(({ closed }) => closed != null && reachesBack(closed, twoWeeksAgo, PULL_PAGE));
Mission control counts people's direct pushes to the default branch556 // A person's commits pushed straight to the default branch are their own
557 // changes too, on the day they landed.
558 const direct = (perRepo ?? []).flatMap(({ direct }) =>
559 direct.map((commit) => ({ mergedAt: commit.at, mergedBy: null, authoredByAgent: false })),
560 );
561 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 200ms562 const landed = landedToday(merged, now, tz);
563
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API564 // --- Activity -------------------------------------------------------------
565 const items: ActivityItem[] = [];
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms566 const titles: Record<string, string> = {};
567 const titleKey = (repo: RepoPath, number: number) => `${repo.namespace}/${repo.name}#${number}`.toLowerCase();
568 for (const { repo, pulls, closed, events } of perRepo ?? []) {
569 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 API570 for (const event of events ?? []) {
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms571 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 API572 const item = eventItem(event, pathOf(repo));
573 if (item) items.push(item);
574 }
575 }
576 for (const entry of overviewList ?? []) {
577 const latest = entry.latest;
578 const served = latest?.status === "ready" || latest?.status === "replaced";
579 if (!slug || latest?.kind !== "production" || (!served && latest.status !== "failed")) continue;
580 items.push({
581 id: `deploy:${latest.id}`,
582 at: Date.parse(latest.finishedAt ?? latest.createdAt),
583 repo: { namespace: slug, name: entry.slug },
584 actor: latest.createdBy,
585 verb: served ? "deployed" : "deploy_failed",
586 number: null,
587 to: `/${slug}/${entry.slug}/deployments/${latest.id}`,
588 });
589 }
590 for (const memory of okOr(memories ?? null)?.workspace ?? []) {
591 const repo = memory.repo ?? memory.source.repo;
592 if (!repo || !slug) continue;
593 items.push({
594 id: `memory:${memory.id}`,
595 at: Date.parse(memory.createdAt),
596 repo,
597 actor: memory.createdBy,
598 verb: "learned",
599 number: null,
600 text: memory.text.length > 90 ? `${memory.text.slice(0, 88)}…` : memory.text,
601 to: `/${slug}/-/memory`,
602 });
603 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms604 const groups = groupActivity(items).slice(0, 40);
605 // Only the titles the feed names travel to the page.
606 const shownTitles: Record<string, string> = {};
607 for (const group of groups) {
608 for (const part of group.parts) {
609 for (const number of part.numbers) {
610 const key = titleKey(group.repo, number);
611 if (titles[key]) shownTitles[key] = titles[key];
612 }
613 }
614 }
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API615
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms616 // --- The strip ------------------------------------------------------------
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API617 const usageOk = okOr(usage ?? null);
618 const costPoints = usageOk
619 ? usageOk.byDay.map((slice) => ({ at: Date.parse(`${slice.key.split("/")[0]}T12:00:00Z`), value: slice.micros / 1_000_000 }))
620 : 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 200ms621 const weekCost = dailyBuckets(costPoints, 7, now).reduce((sum, v) => sum + v, 0);
622 const month = dayKey(now, tz).slice(0, 7);
623 const projectCount = projectsOk?.length ?? (perRepo != null ? perRepo.length : null);
624 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 API625
626 const models_ = models ?? null;
627 times.total = Date.now() - started;
Agents as a team: lifecycle, merge queue, billing and a new shell628 const serverTiming = Object.entries(times)
629 .map(([name, ms]) => `${name};dur=${ms}`)
630 .join(", ");
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API631 return data(
632 {
633 signedIn: true as const,
634 viewer,
635 name: profile?.name?.trim() || username,
636 greeting: greetingFor(hourIn(now, tz)),
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms637 date: dateLine(now, tz),
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar638 summary: summaryLine({
639 total: week.total,
640 byAgents: week.byAgents,
641 agentChanges: week.agentChanges,
642 people: week.people,
643 live: liveRuns.length,
644 needs: needRows.length,
645 }),
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms646 workspace: slug,
647 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 API648 canRunAgents: models_ == null || models_.hosted || models_.own != null,
649 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 200ms650 handedOff: activeList.length > 0 || runList.length > 0 || merged.length > 0,
651 needs: needRows,
652 waiting,
653 landed,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API654 liveTotal: liveRuns.length,
655 runsLoaded: runs != null && runs.ok,
656 perRepoLoaded: perRepo != null,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms657 stats: {
658 projects: projectCount,
659 projectsThisMonth,
660 agentHours: agentHours(runList, weekAgo, now),
661 weekCost,
662 },
663 week,
Mission control shows model usage, yours and the workspace's: tokens, cost, active days, cache share, each day, and the mix664 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 API665 groups,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms666 titles: shownTitles,
667 // A run that is going makes the page worth refreshing on its own.
668 changing:
669 liveRuns.length > 0 ||
670 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 API671 },
672 {
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms673 headers: { "Server-Timing": serverTiming },
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API674 },
675 );
Agents as a team: lifecycle, merge queue, billing and a new shell676}
Get started on mission control677
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look678export type Loaded = Extract<Route.ComponentProps["loaderData"], { signedIn: true }>;
Get started on mission control679
Fast pages, required checks on the branch, self-hosted runners, honest incidents680/**
681 * Mission control, loaded only for someone signed in (components/mission-control.tsx).
682 * The app shell starts loading it once the browser is idle, so the
683 * skeleton below is rarely seen.
684 */
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look685const MissionControl = lazy(() => import("../components/mission-control"));
Agents as a team: lifecycle, merge queue, billing and a new shell686
Fast pages, required checks on the branch, self-hosted runners, honest incidents687/** Mission control's outline, the same size, while its code arrives. */
688function MissionControlSkeleton() {
689 return (
690 <main aria-busy="true" className="mx-auto max-w-7xl space-y-6 px-4 py-8 sm:px-6 sm:py-10">
691 <header>
692 <Skeleton className="h-8 w-72 sm:h-9" />
693 <Skeleton className="mt-2.5 h-3.5 w-96 max-w-full" />
694 </header>
695 <div className="grid grid-cols-2 gap-px overflow-hidden rounded-xl border border-line bg-line lg:grid-cols-5">
696 {Array.from({ length: 5 }, (_, index) => (
697 <div key={index} className="bg-surface p-4">
698 <Skeleton className="h-3 w-16" />
699 <Skeleton className="mt-3 h-6 w-10" />
700 <Skeleton className="mt-2 h-3 w-24" />
701 </div>
702 ))}
703 </div>
704 <div className="rounded-xl border border-line bg-surface">
705 <SkeletonRows rows={5} rowClassName="h-14" />
706 </div>
707 </main>
708 );
709}
710
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step711export default function Home({ loaderData, actionData }: Route.ComponentProps) {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look712 if (!loaderData.signedIn) return <Landing />;
API and MCP server, Rust identity service, registration, site redesign713 return (
Fast pages, required checks on the branch, self-hosted runners, honest incidents714 <Suspense fallback={<MissionControlSkeleton />}>
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step715 <MissionControl loaderData={loaderData} delegated={actionData ?? null} />
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look716 </Suspense>
Initial g1t: services, event bus, intents and attempts717 );
718}