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

652 lines27,201 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";
Fast pages, required checks on the branch, self-hosted runners, honest incidents57import { Skeleton, SkeletonRows } from "../components/ui/skeleton";
API and MCP server, Rust identity service, registration, site redesign58import {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API59 agents,
60 billing,
61 deployments,
62 events as eventLog,
63 identity,
64 projects as projectsApi,
65 repos as reposApi,
66 work,
67} from "../lib/services.server";
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step68import { assertSameOrigin, getViewer, requireUser } from "../lib/session.server";
Fast pages, required checks on the branch, self-hosted runners, honest incidents69import { runners } from "../lib/runners.server";
70import { readableRepos } from "../lib/access.server";
Initial g1t: services, event bus, intents and attempts71
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look72/** 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 API73const TZ_COOKIE = "g1t_tz";
74/** The most projects whose pull requests and events are read for the page. */
75const MAX_PROJECTS = 10;
76const EVENTS_PER_PROJECT = 80;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms77/** How many pull requests work lists at once (`LIST_PAGE` in services/work). */
78const PULL_PAGE = 100;
A free allowance on g1t's models, so anyone can try its agents79
Social cards for every page: og.g1t.sh80export function meta(args: Route.MetaArgs) {
81 return page(args, {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look82 title: "g1t — where people and agents ship software together",
Social cards for every page: og.g1t.sh83 description:
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look84 "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.sh85 });
Initial g1t: services, event bus, intents and attempts86}
87
Agents as a team: lifecycle, merge queue, billing and a new shell88export function headers({ loaderHeaders }: Route.HeadersArgs) {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API89 const out = new Headers();
90 const timing = loaderHeaders.get("Server-Timing");
91 if (timing) out.set("Server-Timing", timing);
92 const cookie = loaderHeaders.get("Set-Cookie");
93 if (cookie) out.set("Set-Cookie", cookie);
94 return out;
Agents as a team: lifecycle, merge queue, billing and a new shell95}
96
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms97/**
98 * Switching tabs, the sort or the activity list changes only the address:
99 * everything is already loaded. A refresh, a form, or anything else loads
100 * again as usual.
101 */
102export function shouldRevalidate({ currentUrl, nextUrl, formMethod, defaultShouldRevalidate }: ShouldRevalidateFunctionArgs) {
103 if (!formMethod && currentUrl.pathname === nextUrl.pathname && currentUrl.search !== nextUrl.search) return false;
104 return defaultShouldRevalidate;
105}
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API106
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms107/** 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 step108type Extra = Partial<Pick<NeedRow, "repo" | "ref" | "by" | "for" | "facts" | "quick" | "link" | "open">> & {
109 /** How sure g1t is of the agent's change, for a pull request. */
110 confidence?: Confidence | null;
111};
112
113/** What the composer said back, when the issue opened but the agent did not start, or nothing opened. */
114export 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 API115
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step116/**
117 * "Put an agent on it": opens an issue in one of the viewer's projects and
118 * puts g1t-agent on it, then lands on the issue. When the agent could not
119 * start, the issue is still open, and the composer says why and where to
120 * fix it.
121 */
122export async function action({ request, context }: Route.ActionArgs) {
123 assertSameOrigin(request);
124 const user = requireUser(context, request);
125 const form = await request.formData();
126 if (form.get("intent") !== "delegate") return data<DelegateResult>({ error: "Nothing to do.", notStarted: null }, { status: 400 });
127 const projects = await reposApi.list(user, { memberOnly: true }).catch(() => []);
128 const repo = chosenRepo(form.get("repo"), projects.map((repo) => ({ namespace: repo.namespace, name: repo.name })));
129 if (!repo) return data<DelegateResult>({ error: "Choose one of your projects.", notStarted: null }, { status: 400 });
130 const result = await env.RUNNER.delegate(user, repo, delegateForm(form));
131 if (!result.ok) return data<DelegateResult>({ error: result.error.message, notStarted: null }, { status: 400 });
132 const { issue, agent } = result.value;
133 const refused = notStarted(agent, repo, issue.number);
134 if (!refused) throw redirect(issuePath(repo, issue.number));
135 return data<DelegateResult>({ error: null, notStarted: refused });
136}
137
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API138export async function loader({ context, request }: Route.LoaderArgs) {
Initial g1t: services, event bus, intents and attempts139 const viewer = getViewer(context);
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API140 if (!viewer) {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look141 // The signed-out home page is about signing up; it lists no repositories.
142 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 API143 }
144
145 const now = Date.now();
146 const weekAgo = now - 7 * TIME.DAY;
Agents as a team: lifecycle, merge queue, billing and a new shell147 const started = Date.now();
148 const times: Record<string, number> = {};
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API149 // Each call is timed, and one that fails leaves its section empty rather
150 // than taking the page down.
151 const soft = <T,>(name: string, promise: Promise<T>): Promise<T | null> =>
152 promise.then(
153 (value) => ((times[name] = Date.now() - started), value),
154 (error) => {
155 console.warn(`home: ${name} failed`, error);
156 times[name] = Date.now() - started;
157 return null;
158 },
159 );
160 const okOr = <T,>(result: { ok: true; value: T } | { ok: false } | null): T | null => (result?.ok ? result.value : null);
161
162 const cookies = request.headers.get("cookie");
163 const tz = readCookie(cookies, TZ_COOKIE);
164 const memberships = viewer.workspaces ?? [];
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put165 // The workspace you chose, as the sidebar shows it (lib/workspace-choice.ts).
166 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 API167 const username = viewer.username;
168
169 const reposP = soft("repos", reposApi.list(viewer, { memberOnly: true }));
Fast pages, required checks on the branch, self-hosted runners, honest incidents170 // Workflow jobs stuck waiting for a self-hosted runner that is not there.
171 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 200ms172 // The chosen workspace's projects: their open and recently merged pull
Fast pages, required checks on the branch, self-hosted runners, honest incidents173 // requests, all in one call to work, and their recent events, read as
174 // soon as the projects are known.
175 const perRepoP = reposP.then(async (repos) => {
176 const chosen = (repos ?? [])
177 .filter((repo) => slug != null && repo.namespace.toLowerCase() === slug.toLowerCase())
178 .slice(0, MAX_PROJECTS);
179 const [batch, logs] = await Promise.all([
180 work.pullsForRepos(chosen.map((repo) => repo.id), viewer, PULL_PAGE).catch(() => []),
181 Promise.all(chosen.map((repo) => eventLog.list({ repoId: repo.id, limit: EVENTS_PER_PROJECT }).catch(() => null))),
182 ]);
183 const byId = new Map(batch.map((entry) => [entry.repoId, entry]));
184 return Promise.all(
185 chosen.map(async (repo, index) => {
186 const found = byId.get(repo.id);
187 if (found) return { repo, pulls: found.open.slice(0, 60), closed: found.closed, events: logs[index] };
188 // A fork, which the batch leaves out: asked on its own.
189 const path = { namespace: repo.namespace, name: repo.name };
190 const [pulls, closed] = await Promise.all([
191 work.listPulls(path, viewer, "open").catch(() => null),
192 work.listPulls(path, viewer, "closed").catch(() => null),
193 ]);
194 return {
195 repo,
196 pulls: pulls?.ok ? pulls.value.slice(0, 60) : null,
197 closed: closed?.ok ? closed.value : null,
198 events: logs[index],
199 };
200 }),
201 );
202 });
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API203
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms204 const [repos, perRepo, active, models, profile, runs, overview, usage, projectList, memories, invitations] = await Promise.all([
205 reposP,
206 soft("projects", perRepoP),
207 soft("pulls", work.listActivePulls(viewer)),
208 slug ? soft("models", env.RUNNER.modelAccess(slug)) : null,
209 soft("profile", identity.profile(username)),
210 slug ? soft("runs", agents.listRuns(viewer, { workspace: slug, limit: 150 })) : null,
211 slug ? soft("deploys", deployments.overview(slug, viewer)) : null,
212 slug ? soft("usage", billing.usage(slug, viewer, new Date(weekAgo - TIME.DAY).toISOString())) : null,
213 slug ? soft("projectList", projectsApi.list(slug, viewer)) : null,
214 slug ? soft("memories", agents.listMemories(viewer, slug, null)) : null,
215 // Repositories someone has invited the viewer to.
216 soft("invitations", identity.myRepoInvitations(viewer)),
217 ]);
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API218
219 const repoList = repos ?? [];
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms220 // Each pull request is shown under its repository. Most are in the
221 // 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 API222 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 200ms223 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 incidents224 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 API225 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 200ms226 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 API227
228 const activeList = (active ?? []).flatMap((item) => {
229 const repo = known.get(item.pull.repoId);
230 return repo ? [{ ...item, repo }] : [];
231 });
232 const runList = okOr(runs ?? null) ?? [];
233 const liveRuns = runList.filter((run) => isActiveRun(run.status));
234 const overviewList = okOr(overview ?? null);
235 const projectsOk = okOr(projectList ?? null);
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms236 const runsOn = (repo: RepoPath, number: number) =>
237 runList.filter(
238 (run) => run.number === number && `${run.repo.namespace}/${run.repo.name}`.toLowerCase() === `${repo.namespace}/${repo.name}`.toLowerCase(),
239 );
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API240
241 const lower = username.toLowerCase();
242 const openPulls = (perRepo ?? []).flatMap(({ repo, pulls }) => (pulls ?? []).map((pull) => ({ pull, repo })));
243 const reviewRequested = openPulls.filter(
244 ({ pull }) =>
245 pull.status === "open" &&
246 pull.author.username.toLowerCase() !== lower &&
247 pull.reviewers.some((name) => name.toLowerCase() === lower),
248 );
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms249
250 // --- Needs you ------------------------------------------------------------
251 const needs: Need[] = [];
252 const extras = new Map<string, Extra>();
253 /** What every pull request's row shares: where it is, who is on it, what is known. */
254 const pullExtra = (pull: Pull, repo: Repo, lifecycle: Lifecycle | null): Extra => {
255 const base = `/${repo.namespace}/${repo.name}`;
256 const agentWork = isAgent(pull.agent);
257 return {
258 repo: pathOf(repo),
259 ref: `#${pull.number}`,
260 by: agentWork ? who(pull.agent) : who(pull.author.username),
261 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 step262 facts: pullFacts({
263 checkStatus: pull.checkStatus,
264 files: pull.files,
265 lifecycle,
266 runs: runsOn(repo, pull.number),
267 confidence: pull.confidence,
268 }),
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms269 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 step270 confidence: pull.confidence ?? null,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms271 };
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API272 };
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms273 const pullAction = (repo: Repo, pull: Pull, quick: Omit<QuickAction, "to">): QuickAction => ({
274 ...quick,
275 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 API276 });
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms277 const approve = (repo: Repo, pull: Pull) =>
278 pullAction(repo, pull, {
279 label: isAgent(pull.agent) ? "Approve the agent's change" : "Approve the change",
280 fields: { action: "comment", verdict: "approve", body: "" },
281 done: "Approved",
282 });
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API283
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look284 for (const invitation of invitations ?? []) {
285 if (invitation.status !== "pending") continue;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms286 const key = `invitation:${invitation.id}`;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look287 needs.push({
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms288 key,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look289 kind: "invitation",
290 title: `${invitation.invited_by ?? "Someone"} invited you to ${invitation.repo}`,
291 detail: `With the ${REPO_ROLE_LABELS[invitation.role]} role. The invitation expires ${new Date(invitation.expires_at).toISOString().slice(0, 10)}.`,
292 to: `/${invitation.repo}/invitations`,
293 action: "Respond",
294 at: Date.parse(invitation.created_at),
295 where: invitation.repo,
296 });
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms297 const [namespace, name] = invitation.repo.split("/");
298 extras.set(key, {
299 repo: namespace && name ? { namespace, name } : null,
300 by: who(invitation.invited_by),
301 facts: [
302 { label: "Role", value: REPO_ROLE_LABELS[invitation.role], tone: null },
303 { label: "Expires", value: new Date(invitation.expires_at).toISOString().slice(0, 10), tone: null },
304 ],
305 });
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look306 }
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API307 for (const entry of overviewList ?? []) {
308 const latest = entry.latest;
309 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 200ms310 const key = `deploy:${entry.slug}`;
311 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 API312 needs.push({
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms313 key,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API314 kind: "deploy",
315 title: `Production build of ${entry.slug} failed`,
316 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 200ms317 to,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API318 action: "See the build",
319 at: Date.parse(latest.finishedAt ?? latest.createdAt),
320 where: `${slug}/${entry.slug}`,
321 });
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms322 const facts: Fact[] = [
323 { label: "Commit", value: latest.commit.slice(0, 7), tone: null },
324 {
325 label: "Production",
326 value: entry.production ? "Serving the build before" : "Not live yet",
327 tone: entry.production ? "good" : "warn",
328 },
329 ];
330 extras.set(key, {
331 repo: { namespace: slug, name: entry.slug },
332 by: who(latest.createdBy),
333 facts,
334 link: { label: "Deployment settings", to: `/${slug}/${entry.slug}/settings/deployments` },
335 });
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API336 }
337 }
338 for (const { pull, lifecycle, repo } of activeList) {
339 const where = `${repo.namespace}/${repo.name}#${pull.number}`;
340 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 200ms341 const key = `pull:${pull.id}`;
342 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 step343 // Held for its low confidence: the ask says so in a sentence of its own.
344 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 API345 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 step346 const held = lowConfidence != null && /confidence in this change is low/i.test(lifecycle.detail);
347 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 200ms348 const need: Need = {
349 key,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API350 kind: conflict ? "conflict" : "stalled",
351 title: pull.title,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step352 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 API353 to,
354 action: conflict ? "Resolve" : "Decide",
355 at: Date.parse(pull.updatedAt),
356 where,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms357 };
358 needs.push(need);
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step359 const reason = held ? "low_confidence" : reasonFor(need);
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms360 extras.set(key, {
361 ...extra,
362 quick:
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step363 reason === "needs_review" || reason === "low_confidence"
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms364 ? approve(repo, pull)
Fast pages, required checks on the branch, self-hosted runners, honest incidents365 : /required checks? .*still fails?\b/i.test(lifecycle.detail)
366 ? 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 200ms367 : null,
368 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 API369 });
370 } 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 step371 needs.push({
372 key,
373 kind: "ready",
374 title: pull.title,
Fast pages, required checks on the branch, self-hosted runners, honest incidents375 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 step376 to,
377 action: "Merge",
378 at: Date.parse(pull.updatedAt),
379 where,
380 });
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms381 extras.set(key, {
382 ...extra,
383 quick: pullAction(repo, pull, {
384 label: isAgent(pull.agent) ? "Merge the agent's change" : "Merge it",
385 fields: { action: "merge" },
386 done: "Merging",
387 }),
388 });
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API389 } else if (!lifecycle && pull.status === "open" && pull.checkStatus === "failed") {
Fast pages, required checks on the branch, self-hosted runners, honest incidents390 // Taken out of the merge queue: its change failed combined with what was ahead.
391 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 });
392 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 API393 }
394 }
395 for (const { pull, repo } of reviewRequested) {
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms396 const key = `review:${pull.id}`;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API397 needs.push({
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms398 key,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API399 kind: "review",
400 title: pull.title,
401 detail: `${pull.author.username} asked for your review.`,
402 to: `/${repo.namespace}/${repo.name}/pull/${pull.number}`,
403 action: "Review",
404 at: Date.parse(pull.updatedAt),
405 where: `${repo.namespace}/${repo.name}#${pull.number}`,
406 });
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms407 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 API408 }
Fast pages, required checks on the branch, self-hosted runners, honest incidents409 for (const job of (await stuckJobsP) ?? []) {
410 const key = `runner:${job.id}`;
411 const minutes = Math.max(10, Math.round((now - Date.parse(job.queuedAt)) / 60_000));
412 const [namespace, name] = job.repo.split("/");
413 needs.push({
414 key,
415 kind: "stuck",
416 title: `${job.name} is waiting for a self-hosted runner`,
417 detail: `No runner with labels ${job.labels} is online. Start one, or change the job's runs-on.`,
418 to: `/${job.repo}/actions/runs/${job.runId}`,
419 action: "Look",
420 at: Date.parse(job.queuedAt),
421 where: job.repo,
422 });
423 extras.set(key, {
424 repo: namespace && name ? { namespace, name } : undefined,
425 facts: [
426 { label: "Labels", value: job.labels, tone: null },
427 { label: "Waiting", value: `${minutes} min`, tone: "warn" },
428 ],
429 link: { label: "Runners", to: `/${namespace}/-/runners` },
430 });
431 }
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API432 for (const run of liveRuns) {
433 const minutes = stuckMinutes(run, now);
434 if (minutes == null) continue;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms435 const key = `run:${run.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: "stuck",
439 title: run.title ?? `${run.agent}'s run`,
440 detail: `${run.agent} has reported nothing for ${minutes} min${run.step ? `. Last: ${run.step}` : ""}.`,
441 to: `/${run.repo.namespace}/${run.repo.name}/agents/runs/${run.id}`,
442 action: "Look",
443 at: Date.parse(run.updatedAt),
444 where: `${run.repo.namespace}/${run.repo.name}${run.number != null ? `#${run.number}` : ""}`,
445 });
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms446 extras.set(key, {
447 repo: run.repo,
448 ref: run.number != null ? `#${run.number}` : null,
449 by: who(run.agent),
450 facts: [
451 { label: "Run", value: RUN_LABEL[run.kind], tone: null },
452 { label: "Quiet for", value: `${minutes} min`, tone: "warn" },
453 { label: "Steps so far", value: String(run.stepCount), tone: null },
454 ...(run.costUsd != null ? [{ label: "Cost so far", value: usd(run.costUsd), tone: null }] : []),
455 ],
456 open: run.number != null ? `/${run.repo.namespace}/${run.repo.name}/pull/${run.number}` : undefined,
457 });
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API458 }
459
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms460 const needRows: NeedRow[] = rankNeeds(needs).map((need) => {
461 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 step462 const reason = withConfidence(reasonFor(need), extra.confidence);
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms463 return {
464 key: need.key,
465 reason,
466 repo: extra.repo ?? null,
467 ref: extra.ref ?? null,
468 title: need.title,
469 ask: need.detail,
470 by: extra.by ?? null,
471 for: extra.for ?? null,
472 at: need.at,
473 to: need.to,
474 open: extra.open ?? need.to,
475 facts: extra.facts ?? [],
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step476 why: whyFor(reason, need, extra.confidence),
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms477 quick: extra.quick ?? null,
478 link: extra.link ?? null,
479 };
480 });
481
482 // --- Waiting on agents ----------------------------------------------------
483 const needKeys = new Set(
484 needRows.flatMap((row) => (row.repo && row.ref ? [needPathKey(row.repo, Number(row.ref.slice(1)))] : [])),
485 );
486 const waiting = waitingRows({
487 active: activeList.filter(({ repo }) => inWorkspace(repo)).map(({ pull, lifecycle, repo }) => ({ pull, lifecycle, repo: pathOf(repo) })),
488 live: liveRuns,
489 drafts: openPulls.filter(({ pull }) => pull.status === "draft").map(({ pull, repo }) => ({ ...pull, repo: pathOf(repo) })),
490 needKeys,
491 });
492
493 // --- Landed ---------------------------------------------------------------
494 const merged: Merged[] = (perRepo ?? []).flatMap(({ repo, closed }) =>
495 (closed ?? []).flatMap((pull) =>
496 pull.status === "merged" && pull.mergedAt
497 ? [{ repo: pathOf(repo), number: pull.number, title: pull.title, agent: pull.agent, mergedBy: pull.mergedBy, mergedAt: pull.mergedAt, files: pull.files }]
498 : [],
499 ),
500 );
501 const twoWeeksAgo = now - 14 * TIME.DAY;
502 const complete = perRepo != null && perRepo.every(({ closed }) => closed != null && reachesBack(closed, twoWeeksAgo, PULL_PAGE));
503 const week = weekOf(merged, now, tz, complete);
504 const landed = landedToday(merged, now, tz);
505
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API506 // --- Activity -------------------------------------------------------------
507 const items: ActivityItem[] = [];
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms508 const titles: Record<string, string> = {};
509 const titleKey = (repo: RepoPath, number: number) => `${repo.namespace}/${repo.name}#${number}`.toLowerCase();
510 for (const { repo, pulls, closed, events } of perRepo ?? []) {
511 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 API512 for (const event of events ?? []) {
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms513 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 API514 const item = eventItem(event, pathOf(repo));
515 if (item) items.push(item);
516 }
517 }
518 for (const entry of overviewList ?? []) {
519 const latest = entry.latest;
520 const served = latest?.status === "ready" || latest?.status === "replaced";
521 if (!slug || latest?.kind !== "production" || (!served && latest.status !== "failed")) continue;
522 items.push({
523 id: `deploy:${latest.id}`,
524 at: Date.parse(latest.finishedAt ?? latest.createdAt),
525 repo: { namespace: slug, name: entry.slug },
526 actor: latest.createdBy,
527 verb: served ? "deployed" : "deploy_failed",
528 number: null,
529 to: `/${slug}/${entry.slug}/deployments/${latest.id}`,
530 });
531 }
532 for (const memory of okOr(memories ?? null)?.workspace ?? []) {
533 const repo = memory.repo ?? memory.source.repo;
534 if (!repo || !slug) continue;
535 items.push({
536 id: `memory:${memory.id}`,
537 at: Date.parse(memory.createdAt),
538 repo,
539 actor: memory.createdBy,
540 verb: "learned",
541 number: null,
542 text: memory.text.length > 90 ? `${memory.text.slice(0, 88)}…` : memory.text,
543 to: `/${slug}/-/memory`,
544 });
545 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms546 const groups = groupActivity(items).slice(0, 40);
547 // Only the titles the feed names travel to the page.
548 const shownTitles: Record<string, string> = {};
549 for (const group of groups) {
550 for (const part of group.parts) {
551 for (const number of part.numbers) {
552 const key = titleKey(group.repo, number);
553 if (titles[key]) shownTitles[key] = titles[key];
554 }
555 }
556 }
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API557
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms558 // --- The strip ------------------------------------------------------------
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API559 const usageOk = okOr(usage ?? null);
560 const costPoints = usageOk
561 ? usageOk.byDay.map((slice) => ({ at: Date.parse(`${slice.key.split("/")[0]}T12:00:00Z`), value: slice.micros / 1_000_000 }))
562 : 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 200ms563 const weekCost = dailyBuckets(costPoints, 7, now).reduce((sum, v) => sum + v, 0);
564 const month = dayKey(now, tz).slice(0, 7);
565 const projectCount = projectsOk?.length ?? (perRepo != null ? perRepo.length : null);
566 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 API567
568 const models_ = models ?? null;
569 times.total = Date.now() - started;
Agents as a team: lifecycle, merge queue, billing and a new shell570 const serverTiming = Object.entries(times)
571 .map(([name, ms]) => `${name};dur=${ms}`)
572 .join(", ");
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API573 return data(
574 {
575 signedIn: true as const,
576 viewer,
577 name: profile?.name?.trim() || username,
578 greeting: greetingFor(hourIn(now, tz)),
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms579 date: dateLine(now, tz),
580 summary: summaryLine({ total: week.total, byAgents: week.byAgents, live: liveRuns.length, needs: needRows.length }),
581 workspace: slug,
582 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 API583 canRunAgents: models_ == null || models_.hosted || models_.own != null,
584 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 200ms585 handedOff: activeList.length > 0 || runList.length > 0 || merged.length > 0,
586 needs: needRows,
587 waiting,
588 landed,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API589 liveTotal: liveRuns.length,
590 runsLoaded: runs != null && runs.ok,
591 perRepoLoaded: perRepo != null,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms592 stats: {
593 projects: projectCount,
594 projectsThisMonth,
595 agentHours: agentHours(runList, weekAgo, now),
596 weekCost,
597 },
598 week,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API599 groups,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms600 titles: shownTitles,
601 // A run that is going makes the page worth refreshing on its own.
602 changing:
603 liveRuns.length > 0 ||
604 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 API605 },
606 {
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms607 headers: { "Server-Timing": serverTiming },
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API608 },
609 );
Agents as a team: lifecycle, merge queue, billing and a new shell610}
Get started on mission control611
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look612export type Loaded = Extract<Route.ComponentProps["loaderData"], { signedIn: true }>;
Get started on mission control613
Fast pages, required checks on the branch, self-hosted runners, honest incidents614/**
615 * Mission control, loaded only for someone signed in (components/mission-control.tsx).
616 * The app shell starts loading it once the browser is idle, so the
617 * skeleton below is rarely seen.
618 */
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look619const MissionControl = lazy(() => import("../components/mission-control"));
Agents as a team: lifecycle, merge queue, billing and a new shell620
Fast pages, required checks on the branch, self-hosted runners, honest incidents621/** Mission control's outline, the same size, while its code arrives. */
622function MissionControlSkeleton() {
623 return (
624 <main aria-busy="true" className="mx-auto max-w-7xl space-y-6 px-4 py-8 sm:px-6 sm:py-10">
625 <header>
626 <Skeleton className="h-8 w-72 sm:h-9" />
627 <Skeleton className="mt-2.5 h-3.5 w-96 max-w-full" />
628 </header>
629 <div className="grid grid-cols-2 gap-px overflow-hidden rounded-xl border border-line bg-line lg:grid-cols-5">
630 {Array.from({ length: 5 }, (_, index) => (
631 <div key={index} className="bg-surface p-4">
632 <Skeleton className="h-3 w-16" />
633 <Skeleton className="mt-3 h-6 w-10" />
634 <Skeleton className="mt-2 h-3 w-24" />
635 </div>
636 ))}
637 </div>
638 <div className="rounded-xl border border-line bg-surface">
639 <SkeletonRows rows={5} rowClassName="h-14" />
640 </div>
641 </main>
642 );
643}
644
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step645export default function Home({ loaderData, actionData }: Route.ComponentProps) {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look646 if (!loaderData.signedIn) return <Landing />;
API and MCP server, Rust identity service, registration, site redesign647 return (
Fast pages, required checks on the branch, self-hosted runners, honest incidents648 <Suspense fallback={<MissionControlSkeleton />}>
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step649 <MissionControl loaderData={loaderData} delegated={actionData ?? null} />
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look650 </Suspense>
Initial g1t: services, event bus, intents and attempts651 );
652}