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

658 lines27,584 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,
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights13 workOwner,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API14} from "@g1t/contracts";
Initial g1t: services, event bus, intents and attempts15
16import type { Route } from "./+types/home";
Social cards for every page: og.g1t.sh17import { page } from "../lib/meta";
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put18import { 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 API19import {
20 type ActivityItem,
21 type Need,
22 TIME,
23 agentHours,
24 dailyBuckets,
25 eventItem,
26 greetingFor,
27 groupActivity,
28 hourIn,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms29 isAgent,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API30 rankNeeds,
31 readCookie,
32 stuckMinutes,
33} from "../lib/mission";
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms34import {
35 type Fact,
36 type Merged,
37 type NeedRow,
38 type QuickAction,
39 RUN_LABEL,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step40 confidenceAsk,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms41 dateLine,
42 dayKey,
43 landedToday,
44 needPathKey,
45 pullFacts,
46 reachesBack,
47 reasonFor,
48 summaryLine,
49 usd,
50 waitingRows,
51 weekOf,
52 who,
53 whyFor,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step54 withConfidence,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms55} from "../lib/mission-control";
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step56import { type NotStarted, chosenRepo, delegateForm, issuePath, notStarted } from "../lib/delegate";
API and MCP server, Rust identity service, registration, site redesign57import { Landing } from "../components/landing";
Fast pages, required checks on the branch, self-hosted runners, honest incidents58import { Skeleton, SkeletonRows } from "../components/ui/skeleton";
API and MCP server, Rust identity service, registration, site redesign59import {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API60 agents,
61 billing,
62 deployments,
63 events as eventLog,
64 identity,
65 projects as projectsApi,
66 repos as reposApi,
67 work,
68} from "../lib/services.server";
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step69import { assertSameOrigin, getViewer, requireUser } from "../lib/session.server";
Fast pages, required checks on the branch, self-hosted runners, honest incidents70import { runners } from "../lib/runners.server";
71import { readableRepos } from "../lib/access.server";
Initial g1t: services, event bus, intents and attempts72
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look73/** 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 API74const TZ_COOKIE = "g1t_tz";
75/** The most projects whose pull requests and events are read for the page. */
76const MAX_PROJECTS = 10;
77const EVENTS_PER_PROJECT = 80;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms78/** How many pull requests work lists at once (`LIST_PAGE` in services/work). */
79const PULL_PAGE = 100;
A free allowance on g1t's models, so anyone can try its agents80
Social cards for every page: og.g1t.sh81export function meta(args: Route.MetaArgs) {
82 return page(args, {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look83 title: "g1t — where people and agents ship software together",
Social cards for every page: og.g1t.sh84 description:
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look85 "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.sh86 });
Initial g1t: services, event bus, intents and attempts87}
88
Agents as a team: lifecycle, merge queue, billing and a new shell89export function headers({ loaderHeaders }: Route.HeadersArgs) {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API90 const out = new Headers();
91 const timing = loaderHeaders.get("Server-Timing");
92 if (timing) out.set("Server-Timing", timing);
93 const cookie = loaderHeaders.get("Set-Cookie");
94 if (cookie) out.set("Set-Cookie", cookie);
95 return out;
Agents as a team: lifecycle, merge queue, billing and a new shell96}
97
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms98/**
99 * Switching tabs, the sort or the activity list changes only the address:
100 * everything is already loaded. A refresh, a form, or anything else loads
101 * again as usual.
102 */
103export function shouldRevalidate({ currentUrl, nextUrl, formMethod, defaultShouldRevalidate }: ShouldRevalidateFunctionArgs) {
104 if (!formMethod && currentUrl.pathname === nextUrl.pathname && currentUrl.search !== nextUrl.search) return false;
105 return defaultShouldRevalidate;
106}
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API107
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms108/** 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 step109type Extra = Partial<Pick<NeedRow, "repo" | "ref" | "by" | "for" | "facts" | "quick" | "link" | "open">> & {
110 /** How sure g1t is of the agent's change, for a pull request. */
111 confidence?: Confidence | null;
112};
113
114/** What the composer said back, when the issue opened but the agent did not start, or nothing opened. */
115export 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 API116
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step117/**
118 * "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-agent119 * 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 step120 * start, the issue is still open, and the composer says why and where to
121 * fix it.
122 */
123export async function action({ request, context }: Route.ActionArgs) {
124 assertSameOrigin(request);
125 const user = requireUser(context, request);
126 const form = await request.formData();
127 if (form.get("intent") !== "delegate") return data<DelegateResult>({ error: "Nothing to do.", notStarted: null }, { status: 400 });
128 const projects = await reposApi.list(user, { memberOnly: true }).catch(() => []);
129 const repo = chosenRepo(form.get("repo"), projects.map((repo) => ({ namespace: repo.namespace, name: repo.name })));
130 if (!repo) return data<DelegateResult>({ error: "Choose one of your projects.", notStarted: null }, { status: 400 });
131 const result = await env.RUNNER.delegate(user, repo, delegateForm(form));
132 if (!result.ok) return data<DelegateResult>({ error: result.error.message, notStarted: null }, { status: 400 });
133 const { issue, agent } = result.value;
134 const refused = notStarted(agent, repo, issue.number);
135 if (!refused) throw redirect(issuePath(repo, issue.number));
136 return data<DelegateResult>({ error: null, notStarted: refused });
137}
138
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API139export async function loader({ context, request }: Route.LoaderArgs) {
Initial g1t: services, event bus, intents and attempts140 const viewer = getViewer(context);
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API141 if (!viewer) {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look142 // The signed-out home page is about signing up; it lists no repositories.
143 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 API144 }
145
146 const now = Date.now();
147 const weekAgo = now - 7 * TIME.DAY;
Agents as a team: lifecycle, merge queue, billing and a new shell148 const started = Date.now();
149 const times: Record<string, number> = {};
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API150 // Each call is timed, and one that fails leaves its section empty rather
151 // than taking the page down.
152 const soft = <T,>(name: string, promise: Promise<T>): Promise<T | null> =>
153 promise.then(
154 (value) => ((times[name] = Date.now() - started), value),
155 (error) => {
156 console.warn(`home: ${name} failed`, error);
157 times[name] = Date.now() - started;
158 return null;
159 },
160 );
161 const okOr = <T,>(result: { ok: true; value: T } | { ok: false } | null): T | null => (result?.ok ? result.value : null);
162
163 const cookies = request.headers.get("cookie");
164 const tz = readCookie(cookies, TZ_COOKIE);
165 const memberships = viewer.workspaces ?? [];
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put166 // The workspace you chose, as the sidebar shows it (lib/workspace-choice.ts).
167 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 API168 const username = viewer.username;
169
170 const reposP = soft("repos", reposApi.list(viewer, { memberOnly: true }));
Fast pages, required checks on the branch, self-hosted runners, honest incidents171 // Workflow jobs stuck waiting for a self-hosted runner that is not there.
172 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 200ms173 // The chosen workspace's projects: their open and recently merged pull
Fast pages, required checks on the branch, self-hosted runners, honest incidents174 // requests, all in one call to work, and their recent events, read as
175 // soon as the projects are known.
176 const perRepoP = reposP.then(async (repos) => {
177 const chosen = (repos ?? [])
178 .filter((repo) => slug != null && repo.namespace.toLowerCase() === slug.toLowerCase())
179 .slice(0, MAX_PROJECTS);
180 const [batch, logs] = await Promise.all([
181 work.pullsForRepos(chosen.map((repo) => repo.id), viewer, PULL_PAGE).catch(() => []),
182 Promise.all(chosen.map((repo) => eventLog.list({ repoId: repo.id, limit: EVENTS_PER_PROJECT }).catch(() => null))),
183 ]);
184 const byId = new Map(batch.map((entry) => [entry.repoId, entry]));
185 return Promise.all(
186 chosen.map(async (repo, index) => {
187 const found = byId.get(repo.id);
188 if (found) return { repo, pulls: found.open.slice(0, 60), closed: found.closed, events: logs[index] };
189 // A fork, which the batch leaves out: asked on its own.
190 const path = { namespace: repo.namespace, name: repo.name };
191 const [pulls, closed] = await Promise.all([
192 work.listPulls(path, viewer, "open").catch(() => null),
193 work.listPulls(path, viewer, "closed").catch(() => null),
194 ]);
195 return {
196 repo,
197 pulls: pulls?.ok ? pulls.value.slice(0, 60) : null,
198 closed: closed?.ok ? closed.value : null,
199 events: logs[index],
200 };
201 }),
202 );
203 });
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API204
Mission control shows model usage, yours and the workspace's: tokens, cost, active days, cache share, each day, and the mix205 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 200ms206 reposP,
207 soft("projects", perRepoP),
208 soft("pulls", work.listActivePulls(viewer)),
209 slug ? soft("models", env.RUNNER.modelAccess(slug)) : null,
210 soft("profile", identity.profile(username)),
211 slug ? soft("runs", agents.listRuns(viewer, { workspace: slug, limit: 150 })) : null,
212 slug ? soft("deploys", deployments.overview(slug, viewer)) : null,
213 slug ? soft("usage", billing.usage(slug, viewer, new Date(weekAgo - TIME.DAY).toISOString())) : null,
214 slug ? soft("projectList", projectsApi.list(slug, viewer)) : null,
215 slug ? soft("memories", agents.listMemories(viewer, slug, null)) : null,
216 // Repositories someone has invited the viewer to.
217 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 mix218 // Model tokens over the last six weeks: the workspace's, and yours.
219 slug ? soft("tokens", billing.tokenUsage(slug, viewer)) : null,
220 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 200ms221 ]);
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API222
223 const repoList = repos ?? [];
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms224 // Each pull request is shown under its repository. Most are in the
225 // 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 API226 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 200ms227 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 incidents228 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 API229 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 200ms230 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 API231
232 const activeList = (active ?? []).flatMap((item) => {
233 const repo = known.get(item.pull.repoId);
234 return repo ? [{ ...item, repo }] : [];
235 });
236 const runList = okOr(runs ?? null) ?? [];
237 const liveRuns = runList.filter((run) => isActiveRun(run.status));
238 const overviewList = okOr(overview ?? null);
239 const projectsOk = okOr(projectList ?? null);
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms240 const runsOn = (repo: RepoPath, number: number) =>
241 runList.filter(
242 (run) => run.number === number && `${run.repo.namespace}/${run.repo.name}`.toLowerCase() === `${repo.namespace}/${repo.name}`.toLowerCase(),
243 );
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API244
245 const lower = username.toLowerCase();
246 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 rights247 // 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 API248 const reviewRequested = openPulls.filter(
249 ({ pull }) =>
250 pull.status === "open" &&
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights251 workOwner(pull).username.toLowerCase() !== lower &&
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API252 pull.reviewers.some((name) => name.toLowerCase() === lower),
253 );
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms254
255 // --- Needs you ------------------------------------------------------------
256 const needs: Need[] = [];
257 const extras = new Map<string, Extra>();
258 /** What every pull request's row shares: where it is, who is on it, what is known. */
259 const pullExtra = (pull: Pull, repo: Repo, lifecycle: Lifecycle | null): Extra => {
260 const base = `/${repo.namespace}/${repo.name}`;
261 const agentWork = isAgent(pull.agent);
262 return {
263 repo: pathOf(repo),
264 ref: `#${pull.number}`,
265 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 rights266 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 step267 facts: pullFacts({
268 checkStatus: pull.checkStatus,
269 files: pull.files,
270 lifecycle,
271 runs: runsOn(repo, pull.number),
272 confidence: pull.confidence,
273 }),
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms274 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 step275 confidence: pull.confidence ?? null,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms276 };
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API277 };
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms278 const pullAction = (repo: Repo, pull: Pull, quick: Omit<QuickAction, "to">): QuickAction => ({
279 ...quick,
280 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 API281 });
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms282 const approve = (repo: Repo, pull: Pull) =>
283 pullAction(repo, pull, {
284 label: isAgent(pull.agent) ? "Approve the agent's change" : "Approve the change",
285 fields: { action: "comment", verdict: "approve", body: "" },
286 done: "Approved",
287 });
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API288
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look289 for (const invitation of invitations ?? []) {
290 if (invitation.status !== "pending") continue;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms291 const key = `invitation:${invitation.id}`;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look292 needs.push({
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms293 key,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look294 kind: "invitation",
295 title: `${invitation.invited_by ?? "Someone"} invited you to ${invitation.repo}`,
296 detail: `With the ${REPO_ROLE_LABELS[invitation.role]} role. The invitation expires ${new Date(invitation.expires_at).toISOString().slice(0, 10)}.`,
297 to: `/${invitation.repo}/invitations`,
298 action: "Respond",
299 at: Date.parse(invitation.created_at),
300 where: invitation.repo,
301 });
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms302 const [namespace, name] = invitation.repo.split("/");
303 extras.set(key, {
304 repo: namespace && name ? { namespace, name } : null,
305 by: who(invitation.invited_by),
306 facts: [
307 { label: "Role", value: REPO_ROLE_LABELS[invitation.role], tone: null },
308 { label: "Expires", value: new Date(invitation.expires_at).toISOString().slice(0, 10), tone: null },
309 ],
310 });
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look311 }
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API312 for (const entry of overviewList ?? []) {
313 const latest = entry.latest;
314 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 200ms315 const key = `deploy:${entry.slug}`;
316 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 API317 needs.push({
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms318 key,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API319 kind: "deploy",
320 title: `Production build of ${entry.slug} failed`,
321 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 200ms322 to,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API323 action: "See the build",
324 at: Date.parse(latest.finishedAt ?? latest.createdAt),
325 where: `${slug}/${entry.slug}`,
326 });
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms327 const facts: Fact[] = [
328 { label: "Commit", value: latest.commit.slice(0, 7), tone: null },
329 {
330 label: "Production",
331 value: entry.production ? "Serving the build before" : "Not live yet",
332 tone: entry.production ? "good" : "warn",
333 },
334 ];
335 extras.set(key, {
336 repo: { namespace: slug, name: entry.slug },
337 by: who(latest.createdBy),
338 facts,
339 link: { label: "Deployment settings", to: `/${slug}/${entry.slug}/settings/deployments` },
340 });
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API341 }
342 }
343 for (const { pull, lifecycle, repo } of activeList) {
344 const where = `${repo.namespace}/${repo.name}#${pull.number}`;
345 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 200ms346 const key = `pull:${pull.id}`;
347 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 step348 // Held for its low confidence: the ask says so in a sentence of its own.
349 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 API350 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 step351 const held = lowConfidence != null && /confidence in this change is low/i.test(lifecycle.detail);
352 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 200ms353 const need: Need = {
354 key,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API355 kind: conflict ? "conflict" : "stalled",
356 title: pull.title,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step357 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 API358 to,
359 action: conflict ? "Resolve" : "Decide",
360 at: Date.parse(pull.updatedAt),
361 where,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms362 };
363 needs.push(need);
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step364 const reason = held ? "low_confidence" : reasonFor(need);
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms365 extras.set(key, {
366 ...extra,
367 quick:
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step368 reason === "needs_review" || reason === "low_confidence"
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms369 ? approve(repo, pull)
Fast pages, required checks on the branch, self-hosted runners, honest incidents370 : /required checks? .*still fails?\b/i.test(lifecycle.detail)
371 ? 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 200ms372 : null,
373 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 API374 });
375 } 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 step376 needs.push({
377 key,
378 kind: "ready",
379 title: pull.title,
Fast pages, required checks on the branch, self-hosted runners, honest incidents380 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 step381 to,
382 action: "Merge",
383 at: Date.parse(pull.updatedAt),
384 where,
385 });
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms386 extras.set(key, {
387 ...extra,
388 quick: pullAction(repo, pull, {
389 label: isAgent(pull.agent) ? "Merge the agent's change" : "Merge it",
390 fields: { action: "merge" },
391 done: "Merging",
392 }),
393 });
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API394 } else if (!lifecycle && pull.status === "open" && pull.checkStatus === "failed") {
Fast pages, required checks on the branch, self-hosted runners, honest incidents395 // Taken out of the merge queue: its change failed combined with what was ahead.
396 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 });
397 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 API398 }
399 }
400 for (const { pull, repo } of reviewRequested) {
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms401 const key = `review:${pull.id}`;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API402 needs.push({
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms403 key,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API404 kind: "review",
405 title: pull.title,
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights406 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 API407 to: `/${repo.namespace}/${repo.name}/pull/${pull.number}`,
408 action: "Review",
409 at: Date.parse(pull.updatedAt),
410 where: `${repo.namespace}/${repo.name}#${pull.number}`,
411 });
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms412 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 API413 }
Fast pages, required checks on the branch, self-hosted runners, honest incidents414 for (const job of (await stuckJobsP) ?? []) {
415 const key = `runner:${job.id}`;
416 const minutes = Math.max(10, Math.round((now - Date.parse(job.queuedAt)) / 60_000));
417 const [namespace, name] = job.repo.split("/");
418 needs.push({
419 key,
420 kind: "stuck",
421 title: `${job.name} is waiting for a self-hosted runner`,
422 detail: `No runner with labels ${job.labels} is online. Start one, or change the job's runs-on.`,
423 to: `/${job.repo}/actions/runs/${job.runId}`,
424 action: "Look",
425 at: Date.parse(job.queuedAt),
426 where: job.repo,
427 });
428 extras.set(key, {
429 repo: namespace && name ? { namespace, name } : undefined,
430 facts: [
431 { label: "Labels", value: job.labels, tone: null },
432 { label: "Waiting", value: `${minutes} min`, tone: "warn" },
433 ],
434 link: { label: "Runners", to: `/${namespace}/-/runners` },
435 });
436 }
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API437 for (const run of liveRuns) {
438 const minutes = stuckMinutes(run, now);
439 if (minutes == null) continue;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms440 const key = `run:${run.id}`;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API441 needs.push({
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms442 key,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API443 kind: "stuck",
444 title: run.title ?? `${run.agent}'s run`,
445 detail: `${run.agent} has reported nothing for ${minutes} min${run.step ? `. Last: ${run.step}` : ""}.`,
446 to: `/${run.repo.namespace}/${run.repo.name}/agents/runs/${run.id}`,
447 action: "Look",
448 at: Date.parse(run.updatedAt),
449 where: `${run.repo.namespace}/${run.repo.name}${run.number != null ? `#${run.number}` : ""}`,
450 });
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms451 extras.set(key, {
452 repo: run.repo,
453 ref: run.number != null ? `#${run.number}` : null,
454 by: who(run.agent),
455 facts: [
456 { label: "Run", value: RUN_LABEL[run.kind], tone: null },
457 { label: "Quiet for", value: `${minutes} min`, tone: "warn" },
458 { label: "Steps so far", value: String(run.stepCount), tone: null },
459 ...(run.costUsd != null ? [{ label: "Cost so far", value: usd(run.costUsd), tone: null }] : []),
460 ],
461 open: run.number != null ? `/${run.repo.namespace}/${run.repo.name}/pull/${run.number}` : undefined,
462 });
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API463 }
464
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms465 const needRows: NeedRow[] = rankNeeds(needs).map((need) => {
466 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 step467 const reason = withConfidence(reasonFor(need), extra.confidence);
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms468 return {
469 key: need.key,
470 reason,
471 repo: extra.repo ?? null,
472 ref: extra.ref ?? null,
473 title: need.title,
474 ask: need.detail,
475 by: extra.by ?? null,
476 for: extra.for ?? null,
477 at: need.at,
478 to: need.to,
479 open: extra.open ?? need.to,
480 facts: extra.facts ?? [],
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step481 why: whyFor(reason, need, extra.confidence),
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms482 quick: extra.quick ?? null,
483 link: extra.link ?? null,
484 };
485 });
486
487 // --- Waiting on agents ----------------------------------------------------
488 const needKeys = new Set(
489 needRows.flatMap((row) => (row.repo && row.ref ? [needPathKey(row.repo, Number(row.ref.slice(1)))] : [])),
490 );
491 const waiting = waitingRows({
492 active: activeList.filter(({ repo }) => inWorkspace(repo)).map(({ pull, lifecycle, repo }) => ({ pull, lifecycle, repo: pathOf(repo) })),
493 live: liveRuns,
494 drafts: openPulls.filter(({ pull }) => pull.status === "draft").map(({ pull, repo }) => ({ ...pull, repo: pathOf(repo) })),
495 needKeys,
496 });
497
498 // --- Landed ---------------------------------------------------------------
499 const merged: Merged[] = (perRepo ?? []).flatMap(({ repo, closed }) =>
500 (closed ?? []).flatMap((pull) =>
501 pull.status === "merged" && pull.mergedAt
502 ? [{ repo: pathOf(repo), number: pull.number, title: pull.title, agent: pull.agent, mergedBy: pull.mergedBy, mergedAt: pull.mergedAt, files: pull.files }]
503 : [],
504 ),
505 );
506 const twoWeeksAgo = now - 14 * TIME.DAY;
507 const complete = perRepo != null && perRepo.every(({ closed }) => closed != null && reachesBack(closed, twoWeeksAgo, PULL_PAGE));
508 const week = weekOf(merged, now, tz, complete);
509 const landed = landedToday(merged, now, tz);
510
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API511 // --- Activity -------------------------------------------------------------
512 const items: ActivityItem[] = [];
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms513 const titles: Record<string, string> = {};
514 const titleKey = (repo: RepoPath, number: number) => `${repo.namespace}/${repo.name}#${number}`.toLowerCase();
515 for (const { repo, pulls, closed, events } of perRepo ?? []) {
516 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 API517 for (const event of events ?? []) {
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms518 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 API519 const item = eventItem(event, pathOf(repo));
520 if (item) items.push(item);
521 }
522 }
523 for (const entry of overviewList ?? []) {
524 const latest = entry.latest;
525 const served = latest?.status === "ready" || latest?.status === "replaced";
526 if (!slug || latest?.kind !== "production" || (!served && latest.status !== "failed")) continue;
527 items.push({
528 id: `deploy:${latest.id}`,
529 at: Date.parse(latest.finishedAt ?? latest.createdAt),
530 repo: { namespace: slug, name: entry.slug },
531 actor: latest.createdBy,
532 verb: served ? "deployed" : "deploy_failed",
533 number: null,
534 to: `/${slug}/${entry.slug}/deployments/${latest.id}`,
535 });
536 }
537 for (const memory of okOr(memories ?? null)?.workspace ?? []) {
538 const repo = memory.repo ?? memory.source.repo;
539 if (!repo || !slug) continue;
540 items.push({
541 id: `memory:${memory.id}`,
542 at: Date.parse(memory.createdAt),
543 repo,
544 actor: memory.createdBy,
545 verb: "learned",
546 number: null,
547 text: memory.text.length > 90 ? `${memory.text.slice(0, 88)}…` : memory.text,
548 to: `/${slug}/-/memory`,
549 });
550 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms551 const groups = groupActivity(items).slice(0, 40);
552 // Only the titles the feed names travel to the page.
553 const shownTitles: Record<string, string> = {};
554 for (const group of groups) {
555 for (const part of group.parts) {
556 for (const number of part.numbers) {
557 const key = titleKey(group.repo, number);
558 if (titles[key]) shownTitles[key] = titles[key];
559 }
560 }
561 }
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API562
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms563 // --- The strip ------------------------------------------------------------
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API564 const usageOk = okOr(usage ?? null);
565 const costPoints = usageOk
566 ? usageOk.byDay.map((slice) => ({ at: Date.parse(`${slice.key.split("/")[0]}T12:00:00Z`), value: slice.micros / 1_000_000 }))
567 : 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 200ms568 const weekCost = dailyBuckets(costPoints, 7, now).reduce((sum, v) => sum + v, 0);
569 const month = dayKey(now, tz).slice(0, 7);
570 const projectCount = projectsOk?.length ?? (perRepo != null ? perRepo.length : null);
571 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 API572
573 const models_ = models ?? null;
574 times.total = Date.now() - started;
Agents as a team: lifecycle, merge queue, billing and a new shell575 const serverTiming = Object.entries(times)
576 .map(([name, ms]) => `${name};dur=${ms}`)
577 .join(", ");
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API578 return data(
579 {
580 signedIn: true as const,
581 viewer,
582 name: profile?.name?.trim() || username,
583 greeting: greetingFor(hourIn(now, tz)),
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms584 date: dateLine(now, tz),
585 summary: summaryLine({ total: week.total, byAgents: week.byAgents, live: liveRuns.length, needs: needRows.length }),
586 workspace: slug,
587 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 API588 canRunAgents: models_ == null || models_.hosted || models_.own != null,
589 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 200ms590 handedOff: activeList.length > 0 || runList.length > 0 || merged.length > 0,
591 needs: needRows,
592 waiting,
593 landed,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API594 liveTotal: liveRuns.length,
595 runsLoaded: runs != null && runs.ok,
596 perRepoLoaded: perRepo != null,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms597 stats: {
598 projects: projectCount,
599 projectsThisMonth,
600 agentHours: agentHours(runList, weekAgo, now),
601 weekCost,
602 },
603 week,
Mission control shows model usage, yours and the workspace's: tokens, cost, active days, cache share, each day, and the mix604 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 API605 groups,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms606 titles: shownTitles,
607 // A run that is going makes the page worth refreshing on its own.
608 changing:
609 liveRuns.length > 0 ||
610 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 API611 },
612 {
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms613 headers: { "Server-Timing": serverTiming },
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API614 },
615 );
Agents as a team: lifecycle, merge queue, billing and a new shell616}
Get started on mission control617
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look618export type Loaded = Extract<Route.ComponentProps["loaderData"], { signedIn: true }>;
Get started on mission control619
Fast pages, required checks on the branch, self-hosted runners, honest incidents620/**
621 * Mission control, loaded only for someone signed in (components/mission-control.tsx).
622 * The app shell starts loading it once the browser is idle, so the
623 * skeleton below is rarely seen.
624 */
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look625const MissionControl = lazy(() => import("../components/mission-control"));
Agents as a team: lifecycle, merge queue, billing and a new shell626
Fast pages, required checks on the branch, self-hosted runners, honest incidents627/** Mission control's outline, the same size, while its code arrives. */
628function MissionControlSkeleton() {
629 return (
630 <main aria-busy="true" className="mx-auto max-w-7xl space-y-6 px-4 py-8 sm:px-6 sm:py-10">
631 <header>
632 <Skeleton className="h-8 w-72 sm:h-9" />
633 <Skeleton className="mt-2.5 h-3.5 w-96 max-w-full" />
634 </header>
635 <div className="grid grid-cols-2 gap-px overflow-hidden rounded-xl border border-line bg-line lg:grid-cols-5">
636 {Array.from({ length: 5 }, (_, index) => (
637 <div key={index} className="bg-surface p-4">
638 <Skeleton className="h-3 w-16" />
639 <Skeleton className="mt-3 h-6 w-10" />
640 <Skeleton className="mt-2 h-3 w-24" />
641 </div>
642 ))}
643 </div>
644 <div className="rounded-xl border border-line bg-surface">
645 <SkeletonRows rows={5} rowClassName="h-14" />
646 </div>
647 </main>
648 );
649}
650
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step651export default function Home({ loaderData, actionData }: Route.ComponentProps) {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look652 if (!loaderData.signedIn) return <Landing />;
API and MCP server, Rust identity service, registration, site redesign653 return (
Fast pages, required checks on the branch, self-hosted runners, honest incidents654 <Suspense fallback={<MissionControlSkeleton />}>
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step655 <MissionControl loaderData={loaderData} delegated={actionData ?? null} />
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look656 </Suspense>
Initial g1t: services, event bus, intents and attempts657 );
658}