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

592 lines24,559 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,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API7 type Lifecycle,
8 type Pull,
9 type Repo,
10 type RepoPath,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look11 REPO_ROLE_LABELS,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API12 isActiveRun,
13} from "@g1t/contracts";
Initial g1t: services, event bus, intents and attempts14
15import type { Route } from "./+types/home";
Social cards for every page: og.g1t.sh16import { page } from "../lib/meta";
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put17import { 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 API18import {
19 type ActivityItem,
20 type Need,
21 TIME,
22 agentHours,
23 dailyBuckets,
24 eventItem,
25 greetingFor,
26 groupActivity,
27 hourIn,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms28 isAgent,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API29 rankNeeds,
30 readCookie,
31 stuckMinutes,
32} from "../lib/mission";
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms33import {
34 type Fact,
35 type Merged,
36 type NeedRow,
37 type QuickAction,
38 RUN_LABEL,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step39 confidenceAsk,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms40 dateLine,
41 dayKey,
42 landedToday,
43 needPathKey,
44 pullFacts,
45 reachesBack,
46 reasonFor,
47 summaryLine,
48 usd,
49 waitingRows,
50 weekOf,
51 who,
52 whyFor,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step53 withConfidence,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms54} from "../lib/mission-control";
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step55import { type NotStarted, chosenRepo, delegateForm, issuePath, notStarted } from "../lib/delegate";
API and MCP server, Rust identity service, registration, site redesign56import { Landing } from "../components/landing";
57import {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API58 agents,
59 billing,
60 deployments,
61 events as eventLog,
62 identity,
63 projects as projectsApi,
64 repos as reposApi,
65 work,
66} from "../lib/services.server";
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step67import { assertSameOrigin, getViewer, requireUser } from "../lib/session.server";
Initial g1t: services, event bus, intents and attempts68
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look69/** 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 API70const TZ_COOKIE = "g1t_tz";
71/** The most projects whose pull requests and events are read for the page. */
72const MAX_PROJECTS = 10;
73const EVENTS_PER_PROJECT = 80;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms74/** How many pull requests work lists at once (`LIST_PAGE` in services/work). */
75const PULL_PAGE = 100;
A free allowance on g1t's models, so anyone can try its agents76
Social cards for every page: og.g1t.sh77export function meta(args: Route.MetaArgs) {
78 return page(args, {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look79 title: "g1t — where people and agents ship software together",
Social cards for every page: og.g1t.sh80 description:
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look81 "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.sh82 });
Initial g1t: services, event bus, intents and attempts83}
84
Agents as a team: lifecycle, merge queue, billing and a new shell85export function headers({ loaderHeaders }: Route.HeadersArgs) {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API86 const out = new Headers();
87 const timing = loaderHeaders.get("Server-Timing");
88 if (timing) out.set("Server-Timing", timing);
89 const cookie = loaderHeaders.get("Set-Cookie");
90 if (cookie) out.set("Set-Cookie", cookie);
91 return out;
Agents as a team: lifecycle, merge queue, billing and a new shell92}
93
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms94/**
95 * Switching tabs, the sort or the activity list changes only the address:
96 * everything is already loaded. A refresh, a form, or anything else loads
97 * again as usual.
98 */
99export function shouldRevalidate({ currentUrl, nextUrl, formMethod, defaultShouldRevalidate }: ShouldRevalidateFunctionArgs) {
100 if (!formMethod && currentUrl.pathname === nextUrl.pathname && currentUrl.search !== nextUrl.search) return false;
101 return defaultShouldRevalidate;
102}
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API103
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms104/** 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 step105type Extra = Partial<Pick<NeedRow, "repo" | "ref" | "by" | "for" | "facts" | "quick" | "link" | "open">> & {
106 /** How sure g1t is of the agent's change, for a pull request. */
107 confidence?: Confidence | null;
108};
109
110/** What the composer said back, when the issue opened but the agent did not start, or nothing opened. */
111export 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 API112
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step113/**
114 * "Put an agent on it": opens an issue in one of the viewer's projects and
115 * puts g1t-agent on it, then lands on the issue. When the agent could not
116 * start, the issue is still open, and the composer says why and where to
117 * fix it.
118 */
119export async function action({ request, context }: Route.ActionArgs) {
120 assertSameOrigin(request);
121 const user = requireUser(context, request);
122 const form = await request.formData();
123 if (form.get("intent") !== "delegate") return data<DelegateResult>({ error: "Nothing to do.", notStarted: null }, { status: 400 });
124 const projects = await reposApi.list(user, { memberOnly: true }).catch(() => []);
125 const repo = chosenRepo(form.get("repo"), projects.map((repo) => ({ namespace: repo.namespace, name: repo.name })));
126 if (!repo) return data<DelegateResult>({ error: "Choose one of your projects.", notStarted: null }, { status: 400 });
127 const result = await env.RUNNER.delegate(user, repo, delegateForm(form));
128 if (!result.ok) return data<DelegateResult>({ error: result.error.message, notStarted: null }, { status: 400 });
129 const { issue, agent } = result.value;
130 const refused = notStarted(agent, repo, issue.number);
131 if (!refused) throw redirect(issuePath(repo, issue.number));
132 return data<DelegateResult>({ error: null, notStarted: refused });
133}
134
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API135export async function loader({ context, request }: Route.LoaderArgs) {
Initial g1t: services, event bus, intents and attempts136 const viewer = getViewer(context);
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API137 if (!viewer) {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look138 // The signed-out home page is about signing up; it lists no repositories.
139 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 API140 }
141
142 const now = Date.now();
143 const weekAgo = now - 7 * TIME.DAY;
Agents as a team: lifecycle, merge queue, billing and a new shell144 const started = Date.now();
145 const times: Record<string, number> = {};
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API146 // Each call is timed, and one that fails leaves its section empty rather
147 // than taking the page down.
148 const soft = <T,>(name: string, promise: Promise<T>): Promise<T | null> =>
149 promise.then(
150 (value) => ((times[name] = Date.now() - started), value),
151 (error) => {
152 console.warn(`home: ${name} failed`, error);
153 times[name] = Date.now() - started;
154 return null;
155 },
156 );
157 const okOr = <T,>(result: { ok: true; value: T } | { ok: false } | null): T | null => (result?.ok ? result.value : null);
158
159 const cookies = request.headers.get("cookie");
160 const tz = readCookie(cookies, TZ_COOKIE);
161 const memberships = viewer.workspaces ?? [];
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put162 // The workspace you chose, as the sidebar shows it (lib/workspace-choice.ts).
163 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 API164 const username = viewer.username;
165
166 const reposP = soft("repos", reposApi.list(viewer, { memberOnly: true }));
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms167 // The chosen workspace's projects: their open and recently merged pull
168 // requests and recent events, read once each, all at once, as soon as
169 // the projects are known.
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API170 const perRepoP = reposP.then((repos) =>
171 Promise.all(
172 (repos ?? [])
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms173 .filter((repo) => 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 API174 .slice(0, MAX_PROJECTS)
175 .map(async (repo) => {
176 const path = { namespace: repo.namespace, name: repo.name };
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms177 const [pulls, closed, log] = await Promise.all([
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API178 work.listPulls(path, viewer, "open").catch(() => null),
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms179 work.listPulls(path, viewer, "closed").catch(() => null),
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API180 eventLog.list({ repoId: repo.id, limit: EVENTS_PER_PROJECT }).catch(() => null),
181 ]);
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms182 return {
183 repo,
184 pulls: pulls?.ok ? pulls.value.slice(0, 60) : null,
185 closed: closed?.ok ? closed.value : null,
186 events: log,
187 };
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API188 }),
189 ),
190 );
191
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms192 const [repos, perRepo, active, models, profile, runs, overview, usage, projectList, memories, invitations] = await Promise.all([
193 reposP,
194 soft("projects", perRepoP),
195 soft("pulls", work.listActivePulls(viewer)),
196 slug ? soft("models", env.RUNNER.modelAccess(slug)) : null,
197 soft("profile", identity.profile(username)),
198 slug ? soft("runs", agents.listRuns(viewer, { workspace: slug, limit: 150 })) : null,
199 slug ? soft("deploys", deployments.overview(slug, viewer)) : null,
200 slug ? soft("usage", billing.usage(slug, viewer, new Date(weekAgo - TIME.DAY).toISOString())) : null,
201 slug ? soft("projectList", projectsApi.list(slug, viewer)) : null,
202 slug ? soft("memories", agents.listMemories(viewer, slug, null)) : null,
203 // Repositories someone has invited the viewer to.
204 soft("invitations", identity.myRepoInvitations(viewer)),
205 ]);
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API206
207 const repoList = repos ?? [];
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms208 // Each pull request is shown under its repository. Most are in the
209 // 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 API210 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 200ms211 const missing = [...new Set((active ?? []).map(({ pull }) => pull.repoId))].filter((id) => !known.has(id));
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API212 const looked = await Promise.all(missing.map((id) => reposApi.getById(id, viewer).catch(() => null)));
213 for (const found of looked) if (found?.ok) known.set(found.value.id, found.value);
214 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 200ms215 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 API216
217 const activeList = (active ?? []).flatMap((item) => {
218 const repo = known.get(item.pull.repoId);
219 return repo ? [{ ...item, repo }] : [];
220 });
221 const runList = okOr(runs ?? null) ?? [];
222 const liveRuns = runList.filter((run) => isActiveRun(run.status));
223 const overviewList = okOr(overview ?? null);
224 const projectsOk = okOr(projectList ?? null);
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms225 const runsOn = (repo: RepoPath, number: number) =>
226 runList.filter(
227 (run) => run.number === number && `${run.repo.namespace}/${run.repo.name}`.toLowerCase() === `${repo.namespace}/${repo.name}`.toLowerCase(),
228 );
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API229
230 const lower = username.toLowerCase();
231 const openPulls = (perRepo ?? []).flatMap(({ repo, pulls }) => (pulls ?? []).map((pull) => ({ pull, repo })));
232 const reviewRequested = openPulls.filter(
233 ({ pull }) =>
234 pull.status === "open" &&
235 pull.author.username.toLowerCase() !== lower &&
236 pull.reviewers.some((name) => name.toLowerCase() === lower),
237 );
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms238
239 // --- Needs you ------------------------------------------------------------
240 const needs: Need[] = [];
241 const extras = new Map<string, Extra>();
242 /** What every pull request's row shares: where it is, who is on it, what is known. */
243 const pullExtra = (pull: Pull, repo: Repo, lifecycle: Lifecycle | null): Extra => {
244 const base = `/${repo.namespace}/${repo.name}`;
245 const agentWork = isAgent(pull.agent);
246 return {
247 repo: pathOf(repo),
248 ref: `#${pull.number}`,
249 by: agentWork ? who(pull.agent) : who(pull.author.username),
250 for: agentWork ? pull.author.username : null,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step251 facts: pullFacts({
252 checkStatus: pull.checkStatus,
253 files: pull.files,
254 lifecycle,
255 runs: runsOn(repo, pull.number),
256 confidence: pull.confidence,
257 }),
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms258 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 step259 confidence: pull.confidence ?? null,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms260 };
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API261 };
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms262 const pullAction = (repo: Repo, pull: Pull, quick: Omit<QuickAction, "to">): QuickAction => ({
263 ...quick,
264 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 API265 });
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms266 const approve = (repo: Repo, pull: Pull) =>
267 pullAction(repo, pull, {
268 label: isAgent(pull.agent) ? "Approve the agent's change" : "Approve the change",
269 fields: { action: "comment", verdict: "approve", body: "" },
270 done: "Approved",
271 });
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API272
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look273 for (const invitation of invitations ?? []) {
274 if (invitation.status !== "pending") continue;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms275 const key = `invitation:${invitation.id}`;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look276 needs.push({
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms277 key,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look278 kind: "invitation",
279 title: `${invitation.invited_by ?? "Someone"} invited you to ${invitation.repo}`,
280 detail: `With the ${REPO_ROLE_LABELS[invitation.role]} role. The invitation expires ${new Date(invitation.expires_at).toISOString().slice(0, 10)}.`,
281 to: `/${invitation.repo}/invitations`,
282 action: "Respond",
283 at: Date.parse(invitation.created_at),
284 where: invitation.repo,
285 });
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms286 const [namespace, name] = invitation.repo.split("/");
287 extras.set(key, {
288 repo: namespace && name ? { namespace, name } : null,
289 by: who(invitation.invited_by),
290 facts: [
291 { label: "Role", value: REPO_ROLE_LABELS[invitation.role], tone: null },
292 { label: "Expires", value: new Date(invitation.expires_at).toISOString().slice(0, 10), tone: null },
293 ],
294 });
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look295 }
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API296 for (const entry of overviewList ?? []) {
297 const latest = entry.latest;
298 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 200ms299 const key = `deploy:${entry.slug}`;
300 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 API301 needs.push({
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms302 key,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API303 kind: "deploy",
304 title: `Production build of ${entry.slug} failed`,
305 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 200ms306 to,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API307 action: "See the build",
308 at: Date.parse(latest.finishedAt ?? latest.createdAt),
309 where: `${slug}/${entry.slug}`,
310 });
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms311 const facts: Fact[] = [
312 { label: "Commit", value: latest.commit.slice(0, 7), tone: null },
313 {
314 label: "Production",
315 value: entry.production ? "Serving the build before" : "Not live yet",
316 tone: entry.production ? "good" : "warn",
317 },
318 ];
319 extras.set(key, {
320 repo: { namespace: slug, name: entry.slug },
321 by: who(latest.createdBy),
322 facts,
323 link: { label: "Deployment settings", to: `/${slug}/${entry.slug}/settings/deployments` },
324 });
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API325 }
326 }
327 for (const { pull, lifecycle, repo } of activeList) {
328 const where = `${repo.namespace}/${repo.name}#${pull.number}`;
329 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 200ms330 const key = `pull:${pull.id}`;
331 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 step332 // Held for its low confidence: the ask says so in a sentence of its own.
333 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 API334 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 step335 const held = lowConfidence != null && /confidence in this change is low/i.test(lifecycle.detail);
336 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 200ms337 const need: Need = {
338 key,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API339 kind: conflict ? "conflict" : "stalled",
340 title: pull.title,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step341 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 API342 to,
343 action: conflict ? "Resolve" : "Decide",
344 at: Date.parse(pull.updatedAt),
345 where,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms346 };
347 needs.push(need);
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step348 const reason = held ? "low_confidence" : reasonFor(need);
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms349 extras.set(key, {
350 ...extra,
351 quick:
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step352 reason === "needs_review" || reason === "low_confidence"
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms353 ? approve(repo, pull)
354 : /could not be run/i.test(lifecycle.detail)
355 ? pullAction(repo, pull, { label: "Run the checks again", fields: { action: "recheck" }, done: "Checks started" })
356 : null,
357 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 API358 });
359 } 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 step360 needs.push({
361 key,
362 kind: "ready",
363 title: pull.title,
364 detail: lowConfidence ? confidenceAsk(lowConfidence) : "Checks passed and it was approved. It lands when you merge it.",
365 to,
366 action: "Merge",
367 at: Date.parse(pull.updatedAt),
368 where,
369 });
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms370 extras.set(key, {
371 ...extra,
372 quick: pullAction(repo, pull, {
373 label: isAgent(pull.agent) ? "Merge the agent's change" : "Merge it",
374 fields: { action: "merge" },
375 done: "Merging",
376 }),
377 });
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API378 } else if (!lifecycle && pull.status === "open" && pull.checkStatus === "failed") {
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms379 needs.push({ key, kind: "checks", title: pull.title, detail: "Its acceptance checks failed on the latest push.", to, action: "See checks", at: Date.parse(pull.updatedAt), where });
380 extras.set(key, {
381 ...extra,
382 quick: pullAction(repo, pull, { label: "Run the checks again", fields: { action: "recheck" }, done: "Checks started" }),
383 });
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API384 }
385 }
386 for (const { pull, repo } of reviewRequested) {
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms387 const key = `review:${pull.id}`;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API388 needs.push({
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms389 key,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API390 kind: "review",
391 title: pull.title,
392 detail: `${pull.author.username} asked for your review.`,
393 to: `/${repo.namespace}/${repo.name}/pull/${pull.number}`,
394 action: "Review",
395 at: Date.parse(pull.updatedAt),
396 where: `${repo.namespace}/${repo.name}#${pull.number}`,
397 });
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms398 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 API399 }
400 for (const run of liveRuns) {
401 const minutes = stuckMinutes(run, now);
402 if (minutes == null) continue;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms403 const key = `run:${run.id}`;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API404 needs.push({
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms405 key,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API406 kind: "stuck",
407 title: run.title ?? `${run.agent}'s run`,
408 detail: `${run.agent} has reported nothing for ${minutes} min${run.step ? `. Last: ${run.step}` : ""}.`,
409 to: `/${run.repo.namespace}/${run.repo.name}/agents/runs/${run.id}`,
410 action: "Look",
411 at: Date.parse(run.updatedAt),
412 where: `${run.repo.namespace}/${run.repo.name}${run.number != null ? `#${run.number}` : ""}`,
413 });
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms414 extras.set(key, {
415 repo: run.repo,
416 ref: run.number != null ? `#${run.number}` : null,
417 by: who(run.agent),
418 facts: [
419 { label: "Run", value: RUN_LABEL[run.kind], tone: null },
420 { label: "Quiet for", value: `${minutes} min`, tone: "warn" },
421 { label: "Steps so far", value: String(run.stepCount), tone: null },
422 ...(run.costUsd != null ? [{ label: "Cost so far", value: usd(run.costUsd), tone: null }] : []),
423 ],
424 open: run.number != null ? `/${run.repo.namespace}/${run.repo.name}/pull/${run.number}` : undefined,
425 });
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API426 }
427
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms428 const needRows: NeedRow[] = rankNeeds(needs).map((need) => {
429 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 step430 const reason = withConfidence(reasonFor(need), extra.confidence);
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms431 return {
432 key: need.key,
433 reason,
434 repo: extra.repo ?? null,
435 ref: extra.ref ?? null,
436 title: need.title,
437 ask: need.detail,
438 by: extra.by ?? null,
439 for: extra.for ?? null,
440 at: need.at,
441 to: need.to,
442 open: extra.open ?? need.to,
443 facts: extra.facts ?? [],
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step444 why: whyFor(reason, need, extra.confidence),
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms445 quick: extra.quick ?? null,
446 link: extra.link ?? null,
447 };
448 });
449
450 // --- Waiting on agents ----------------------------------------------------
451 const needKeys = new Set(
452 needRows.flatMap((row) => (row.repo && row.ref ? [needPathKey(row.repo, Number(row.ref.slice(1)))] : [])),
453 );
454 const waiting = waitingRows({
455 active: activeList.filter(({ repo }) => inWorkspace(repo)).map(({ pull, lifecycle, repo }) => ({ pull, lifecycle, repo: pathOf(repo) })),
456 live: liveRuns,
457 drafts: openPulls.filter(({ pull }) => pull.status === "draft").map(({ pull, repo }) => ({ ...pull, repo: pathOf(repo) })),
458 needKeys,
459 });
460
461 // --- Landed ---------------------------------------------------------------
462 const merged: Merged[] = (perRepo ?? []).flatMap(({ repo, closed }) =>
463 (closed ?? []).flatMap((pull) =>
464 pull.status === "merged" && pull.mergedAt
465 ? [{ repo: pathOf(repo), number: pull.number, title: pull.title, agent: pull.agent, mergedBy: pull.mergedBy, mergedAt: pull.mergedAt, files: pull.files }]
466 : [],
467 ),
468 );
469 const twoWeeksAgo = now - 14 * TIME.DAY;
470 const complete = perRepo != null && perRepo.every(({ closed }) => closed != null && reachesBack(closed, twoWeeksAgo, PULL_PAGE));
471 const week = weekOf(merged, now, tz, complete);
472 const landed = landedToday(merged, now, tz);
473
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API474 // --- Activity -------------------------------------------------------------
475 const items: ActivityItem[] = [];
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms476 const titles: Record<string, string> = {};
477 const titleKey = (repo: RepoPath, number: number) => `${repo.namespace}/${repo.name}#${number}`.toLowerCase();
478 for (const { repo, pulls, closed, events } of perRepo ?? []) {
479 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 API480 for (const event of events ?? []) {
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms481 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 API482 const item = eventItem(event, pathOf(repo));
483 if (item) items.push(item);
484 }
485 }
486 for (const entry of overviewList ?? []) {
487 const latest = entry.latest;
488 const served = latest?.status === "ready" || latest?.status === "replaced";
489 if (!slug || latest?.kind !== "production" || (!served && latest.status !== "failed")) continue;
490 items.push({
491 id: `deploy:${latest.id}`,
492 at: Date.parse(latest.finishedAt ?? latest.createdAt),
493 repo: { namespace: slug, name: entry.slug },
494 actor: latest.createdBy,
495 verb: served ? "deployed" : "deploy_failed",
496 number: null,
497 to: `/${slug}/${entry.slug}/deployments/${latest.id}`,
498 });
499 }
500 for (const memory of okOr(memories ?? null)?.workspace ?? []) {
501 const repo = memory.repo ?? memory.source.repo;
502 if (!repo || !slug) continue;
503 items.push({
504 id: `memory:${memory.id}`,
505 at: Date.parse(memory.createdAt),
506 repo,
507 actor: memory.createdBy,
508 verb: "learned",
509 number: null,
510 text: memory.text.length > 90 ? `${memory.text.slice(0, 88)}…` : memory.text,
511 to: `/${slug}/-/memory`,
512 });
513 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms514 const groups = groupActivity(items).slice(0, 40);
515 // Only the titles the feed names travel to the page.
516 const shownTitles: Record<string, string> = {};
517 for (const group of groups) {
518 for (const part of group.parts) {
519 for (const number of part.numbers) {
520 const key = titleKey(group.repo, number);
521 if (titles[key]) shownTitles[key] = titles[key];
522 }
523 }
524 }
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API525
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms526 // --- The strip ------------------------------------------------------------
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API527 const usageOk = okOr(usage ?? null);
528 const costPoints = usageOk
529 ? usageOk.byDay.map((slice) => ({ at: Date.parse(`${slice.key.split("/")[0]}T12:00:00Z`), value: slice.micros / 1_000_000 }))
530 : 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 200ms531 const weekCost = dailyBuckets(costPoints, 7, now).reduce((sum, v) => sum + v, 0);
532 const month = dayKey(now, tz).slice(0, 7);
533 const projectCount = projectsOk?.length ?? (perRepo != null ? perRepo.length : null);
534 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 API535
536 const models_ = models ?? null;
537 times.total = Date.now() - started;
Agents as a team: lifecycle, merge queue, billing and a new shell538 const serverTiming = Object.entries(times)
539 .map(([name, ms]) => `${name};dur=${ms}`)
540 .join(", ");
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API541 return data(
542 {
543 signedIn: true as const,
544 viewer,
545 name: profile?.name?.trim() || username,
546 greeting: greetingFor(hourIn(now, tz)),
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms547 date: dateLine(now, tz),
548 summary: summaryLine({ total: week.total, byAgents: week.byAgents, live: liveRuns.length, needs: needRows.length }),
549 workspace: slug,
550 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 API551 canRunAgents: models_ == null || models_.hosted || models_.own != null,
552 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 200ms553 handedOff: activeList.length > 0 || runList.length > 0 || merged.length > 0,
554 needs: needRows,
555 waiting,
556 landed,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API557 liveTotal: liveRuns.length,
558 runsLoaded: runs != null && runs.ok,
559 perRepoLoaded: perRepo != null,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms560 stats: {
561 projects: projectCount,
562 projectsThisMonth,
563 agentHours: agentHours(runList, weekAgo, now),
564 weekCost,
565 },
566 week,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API567 groups,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms568 titles: shownTitles,
569 // A run that is going makes the page worth refreshing on its own.
570 changing:
571 liveRuns.length > 0 ||
572 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 API573 },
574 {
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms575 headers: { "Server-Timing": serverTiming },
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API576 },
577 );
Agents as a team: lifecycle, merge queue, billing and a new shell578}
Get started on mission control579
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look580export type Loaded = Extract<Route.ComponentProps["loaderData"], { signedIn: true }>;
Get started on mission control581
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look582/** Mission control, loaded only for someone signed in (components/mission-control.tsx). */
583const MissionControl = lazy(() => import("../components/mission-control"));
Agents as a team: lifecycle, merge queue, billing and a new shell584
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step585export default function Home({ loaderData, actionData }: Route.ComponentProps) {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look586 if (!loaderData.signedIn) return <Landing />;
API and MCP server, Rust identity service, registration, site redesign587 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look588 <Suspense fallback={null}>
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step589 <MissionControl loaderData={loaderData} delegated={actionData ?? null} />
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look590 </Suspense>
Initial g1t: services, event bus, intents and attempts591 );
592}