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

654 lines27,267 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 where you are needed and what agents landed without you; git answers in about 200ms205 const [repos, perRepo, active, models, profile, runs, overview, usage, projectList, memories, invitations] = await Promise.all([
206 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)),
218 ]);
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API219
220 const repoList = repos ?? [];
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms221 // Each pull request is shown under its repository. Most are in the
222 // 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 API223 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 200ms224 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 incidents225 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 API226 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 200ms227 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 API228
229 const activeList = (active ?? []).flatMap((item) => {
230 const repo = known.get(item.pull.repoId);
231 return repo ? [{ ...item, repo }] : [];
232 });
233 const runList = okOr(runs ?? null) ?? [];
234 const liveRuns = runList.filter((run) => isActiveRun(run.status));
235 const overviewList = okOr(overview ?? null);
236 const projectsOk = okOr(projectList ?? null);
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms237 const runsOn = (repo: RepoPath, number: number) =>
238 runList.filter(
239 (run) => run.number === number && `${run.repo.namespace}/${run.repo.name}`.toLowerCase() === `${repo.namespace}/${repo.name}`.toLowerCase(),
240 );
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API241
242 const lower = username.toLowerCase();
243 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 rights244 // 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 API245 const reviewRequested = openPulls.filter(
246 ({ pull }) =>
247 pull.status === "open" &&
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights248 workOwner(pull).username.toLowerCase() !== lower &&
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API249 pull.reviewers.some((name) => name.toLowerCase() === lower),
250 );
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms251
252 // --- Needs you ------------------------------------------------------------
253 const needs: Need[] = [];
254 const extras = new Map<string, Extra>();
255 /** What every pull request's row shares: where it is, who is on it, what is known. */
256 const pullExtra = (pull: Pull, repo: Repo, lifecycle: Lifecycle | null): Extra => {
257 const base = `/${repo.namespace}/${repo.name}`;
258 const agentWork = isAgent(pull.agent);
259 return {
260 repo: pathOf(repo),
261 ref: `#${pull.number}`,
262 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 rights263 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 step264 facts: pullFacts({
265 checkStatus: pull.checkStatus,
266 files: pull.files,
267 lifecycle,
268 runs: runsOn(repo, pull.number),
269 confidence: pull.confidence,
270 }),
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms271 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 step272 confidence: pull.confidence ?? null,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms273 };
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API274 };
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms275 const pullAction = (repo: Repo, pull: Pull, quick: Omit<QuickAction, "to">): QuickAction => ({
276 ...quick,
277 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 API278 });
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms279 const approve = (repo: Repo, pull: Pull) =>
280 pullAction(repo, pull, {
281 label: isAgent(pull.agent) ? "Approve the agent's change" : "Approve the change",
282 fields: { action: "comment", verdict: "approve", body: "" },
283 done: "Approved",
284 });
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API285
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look286 for (const invitation of invitations ?? []) {
287 if (invitation.status !== "pending") continue;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms288 const key = `invitation:${invitation.id}`;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look289 needs.push({
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms290 key,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look291 kind: "invitation",
292 title: `${invitation.invited_by ?? "Someone"} invited you to ${invitation.repo}`,
293 detail: `With the ${REPO_ROLE_LABELS[invitation.role]} role. The invitation expires ${new Date(invitation.expires_at).toISOString().slice(0, 10)}.`,
294 to: `/${invitation.repo}/invitations`,
295 action: "Respond",
296 at: Date.parse(invitation.created_at),
297 where: invitation.repo,
298 });
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms299 const [namespace, name] = invitation.repo.split("/");
300 extras.set(key, {
301 repo: namespace && name ? { namespace, name } : null,
302 by: who(invitation.invited_by),
303 facts: [
304 { label: "Role", value: REPO_ROLE_LABELS[invitation.role], tone: null },
305 { label: "Expires", value: new Date(invitation.expires_at).toISOString().slice(0, 10), tone: null },
306 ],
307 });
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look308 }
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API309 for (const entry of overviewList ?? []) {
310 const latest = entry.latest;
311 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 200ms312 const key = `deploy:${entry.slug}`;
313 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 API314 needs.push({
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms315 key,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API316 kind: "deploy",
317 title: `Production build of ${entry.slug} failed`,
318 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 200ms319 to,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API320 action: "See the build",
321 at: Date.parse(latest.finishedAt ?? latest.createdAt),
322 where: `${slug}/${entry.slug}`,
323 });
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms324 const facts: Fact[] = [
325 { label: "Commit", value: latest.commit.slice(0, 7), tone: null },
326 {
327 label: "Production",
328 value: entry.production ? "Serving the build before" : "Not live yet",
329 tone: entry.production ? "good" : "warn",
330 },
331 ];
332 extras.set(key, {
333 repo: { namespace: slug, name: entry.slug },
334 by: who(latest.createdBy),
335 facts,
336 link: { label: "Deployment settings", to: `/${slug}/${entry.slug}/settings/deployments` },
337 });
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API338 }
339 }
340 for (const { pull, lifecycle, repo } of activeList) {
341 const where = `${repo.namespace}/${repo.name}#${pull.number}`;
342 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 200ms343 const key = `pull:${pull.id}`;
344 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 step345 // Held for its low confidence: the ask says so in a sentence of its own.
346 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 API347 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 step348 const held = lowConfidence != null && /confidence in this change is low/i.test(lifecycle.detail);
349 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 200ms350 const need: Need = {
351 key,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API352 kind: conflict ? "conflict" : "stalled",
353 title: pull.title,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step354 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 API355 to,
356 action: conflict ? "Resolve" : "Decide",
357 at: Date.parse(pull.updatedAt),
358 where,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms359 };
360 needs.push(need);
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step361 const reason = held ? "low_confidence" : reasonFor(need);
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms362 extras.set(key, {
363 ...extra,
364 quick:
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step365 reason === "needs_review" || reason === "low_confidence"
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms366 ? approve(repo, pull)
Fast pages, required checks on the branch, self-hosted runners, honest incidents367 : /required checks? .*still fails?\b/i.test(lifecycle.detail)
368 ? 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 200ms369 : null,
370 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 API371 });
372 } 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 step373 needs.push({
374 key,
375 kind: "ready",
376 title: pull.title,
Fast pages, required checks on the branch, self-hosted runners, honest incidents377 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 step378 to,
379 action: "Merge",
380 at: Date.parse(pull.updatedAt),
381 where,
382 });
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms383 extras.set(key, {
384 ...extra,
385 quick: pullAction(repo, pull, {
386 label: isAgent(pull.agent) ? "Merge the agent's change" : "Merge it",
387 fields: { action: "merge" },
388 done: "Merging",
389 }),
390 });
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API391 } else if (!lifecycle && pull.status === "open" && pull.checkStatus === "failed") {
Fast pages, required checks on the branch, self-hosted runners, honest incidents392 // Taken out of the merge queue: its change failed combined with what was ahead.
393 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 });
394 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 API395 }
396 }
397 for (const { pull, repo } of reviewRequested) {
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms398 const key = `review:${pull.id}`;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API399 needs.push({
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms400 key,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API401 kind: "review",
402 title: pull.title,
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights403 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 API404 to: `/${repo.namespace}/${repo.name}/pull/${pull.number}`,
405 action: "Review",
406 at: Date.parse(pull.updatedAt),
407 where: `${repo.namespace}/${repo.name}#${pull.number}`,
408 });
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms409 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 API410 }
Fast pages, required checks on the branch, self-hosted runners, honest incidents411 for (const job of (await stuckJobsP) ?? []) {
412 const key = `runner:${job.id}`;
413 const minutes = Math.max(10, Math.round((now - Date.parse(job.queuedAt)) / 60_000));
414 const [namespace, name] = job.repo.split("/");
415 needs.push({
416 key,
417 kind: "stuck",
418 title: `${job.name} is waiting for a self-hosted runner`,
419 detail: `No runner with labels ${job.labels} is online. Start one, or change the job's runs-on.`,
420 to: `/${job.repo}/actions/runs/${job.runId}`,
421 action: "Look",
422 at: Date.parse(job.queuedAt),
423 where: job.repo,
424 });
425 extras.set(key, {
426 repo: namespace && name ? { namespace, name } : undefined,
427 facts: [
428 { label: "Labels", value: job.labels, tone: null },
429 { label: "Waiting", value: `${minutes} min`, tone: "warn" },
430 ],
431 link: { label: "Runners", to: `/${namespace}/-/runners` },
432 });
433 }
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API434 for (const run of liveRuns) {
435 const minutes = stuckMinutes(run, now);
436 if (minutes == null) continue;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms437 const key = `run:${run.id}`;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API438 needs.push({
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms439 key,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API440 kind: "stuck",
441 title: run.title ?? `${run.agent}'s run`,
442 detail: `${run.agent} has reported nothing for ${minutes} min${run.step ? `. Last: ${run.step}` : ""}.`,
443 to: `/${run.repo.namespace}/${run.repo.name}/agents/runs/${run.id}`,
444 action: "Look",
445 at: Date.parse(run.updatedAt),
446 where: `${run.repo.namespace}/${run.repo.name}${run.number != null ? `#${run.number}` : ""}`,
447 });
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms448 extras.set(key, {
449 repo: run.repo,
450 ref: run.number != null ? `#${run.number}` : null,
451 by: who(run.agent),
452 facts: [
453 { label: "Run", value: RUN_LABEL[run.kind], tone: null },
454 { label: "Quiet for", value: `${minutes} min`, tone: "warn" },
455 { label: "Steps so far", value: String(run.stepCount), tone: null },
456 ...(run.costUsd != null ? [{ label: "Cost so far", value: usd(run.costUsd), tone: null }] : []),
457 ],
458 open: run.number != null ? `/${run.repo.namespace}/${run.repo.name}/pull/${run.number}` : undefined,
459 });
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API460 }
461
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms462 const needRows: NeedRow[] = rankNeeds(needs).map((need) => {
463 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 step464 const reason = withConfidence(reasonFor(need), extra.confidence);
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms465 return {
466 key: need.key,
467 reason,
468 repo: extra.repo ?? null,
469 ref: extra.ref ?? null,
470 title: need.title,
471 ask: need.detail,
472 by: extra.by ?? null,
473 for: extra.for ?? null,
474 at: need.at,
475 to: need.to,
476 open: extra.open ?? need.to,
477 facts: extra.facts ?? [],
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step478 why: whyFor(reason, need, extra.confidence),
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms479 quick: extra.quick ?? null,
480 link: extra.link ?? null,
481 };
482 });
483
484 // --- Waiting on agents ----------------------------------------------------
485 const needKeys = new Set(
486 needRows.flatMap((row) => (row.repo && row.ref ? [needPathKey(row.repo, Number(row.ref.slice(1)))] : [])),
487 );
488 const waiting = waitingRows({
489 active: activeList.filter(({ repo }) => inWorkspace(repo)).map(({ pull, lifecycle, repo }) => ({ pull, lifecycle, repo: pathOf(repo) })),
490 live: liveRuns,
491 drafts: openPulls.filter(({ pull }) => pull.status === "draft").map(({ pull, repo }) => ({ ...pull, repo: pathOf(repo) })),
492 needKeys,
493 });
494
495 // --- Landed ---------------------------------------------------------------
496 const merged: Merged[] = (perRepo ?? []).flatMap(({ repo, closed }) =>
497 (closed ?? []).flatMap((pull) =>
498 pull.status === "merged" && pull.mergedAt
499 ? [{ repo: pathOf(repo), number: pull.number, title: pull.title, agent: pull.agent, mergedBy: pull.mergedBy, mergedAt: pull.mergedAt, files: pull.files }]
500 : [],
501 ),
502 );
503 const twoWeeksAgo = now - 14 * TIME.DAY;
504 const complete = perRepo != null && perRepo.every(({ closed }) => closed != null && reachesBack(closed, twoWeeksAgo, PULL_PAGE));
505 const week = weekOf(merged, now, tz, complete);
506 const landed = landedToday(merged, now, tz);
507
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API508 // --- Activity -------------------------------------------------------------
509 const items: ActivityItem[] = [];
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms510 const titles: Record<string, string> = {};
511 const titleKey = (repo: RepoPath, number: number) => `${repo.namespace}/${repo.name}#${number}`.toLowerCase();
512 for (const { repo, pulls, closed, events } of perRepo ?? []) {
513 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 API514 for (const event of events ?? []) {
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms515 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 API516 const item = eventItem(event, pathOf(repo));
517 if (item) items.push(item);
518 }
519 }
520 for (const entry of overviewList ?? []) {
521 const latest = entry.latest;
522 const served = latest?.status === "ready" || latest?.status === "replaced";
523 if (!slug || latest?.kind !== "production" || (!served && latest.status !== "failed")) continue;
524 items.push({
525 id: `deploy:${latest.id}`,
526 at: Date.parse(latest.finishedAt ?? latest.createdAt),
527 repo: { namespace: slug, name: entry.slug },
528 actor: latest.createdBy,
529 verb: served ? "deployed" : "deploy_failed",
530 number: null,
531 to: `/${slug}/${entry.slug}/deployments/${latest.id}`,
532 });
533 }
534 for (const memory of okOr(memories ?? null)?.workspace ?? []) {
535 const repo = memory.repo ?? memory.source.repo;
536 if (!repo || !slug) continue;
537 items.push({
538 id: `memory:${memory.id}`,
539 at: Date.parse(memory.createdAt),
540 repo,
541 actor: memory.createdBy,
542 verb: "learned",
543 number: null,
544 text: memory.text.length > 90 ? `${memory.text.slice(0, 88)}…` : memory.text,
545 to: `/${slug}/-/memory`,
546 });
547 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms548 const groups = groupActivity(items).slice(0, 40);
549 // Only the titles the feed names travel to the page.
550 const shownTitles: Record<string, string> = {};
551 for (const group of groups) {
552 for (const part of group.parts) {
553 for (const number of part.numbers) {
554 const key = titleKey(group.repo, number);
555 if (titles[key]) shownTitles[key] = titles[key];
556 }
557 }
558 }
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API559
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms560 // --- The strip ------------------------------------------------------------
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API561 const usageOk = okOr(usage ?? null);
562 const costPoints = usageOk
563 ? usageOk.byDay.map((slice) => ({ at: Date.parse(`${slice.key.split("/")[0]}T12:00:00Z`), value: slice.micros / 1_000_000 }))
564 : 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 200ms565 const weekCost = dailyBuckets(costPoints, 7, now).reduce((sum, v) => sum + v, 0);
566 const month = dayKey(now, tz).slice(0, 7);
567 const projectCount = projectsOk?.length ?? (perRepo != null ? perRepo.length : null);
568 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 API569
570 const models_ = models ?? null;
571 times.total = Date.now() - started;
Agents as a team: lifecycle, merge queue, billing and a new shell572 const serverTiming = Object.entries(times)
573 .map(([name, ms]) => `${name};dur=${ms}`)
574 .join(", ");
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API575 return data(
576 {
577 signedIn: true as const,
578 viewer,
579 name: profile?.name?.trim() || username,
580 greeting: greetingFor(hourIn(now, tz)),
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms581 date: dateLine(now, tz),
582 summary: summaryLine({ total: week.total, byAgents: week.byAgents, live: liveRuns.length, needs: needRows.length }),
583 workspace: slug,
584 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 API585 canRunAgents: models_ == null || models_.hosted || models_.own != null,
586 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 200ms587 handedOff: activeList.length > 0 || runList.length > 0 || merged.length > 0,
588 needs: needRows,
589 waiting,
590 landed,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API591 liveTotal: liveRuns.length,
592 runsLoaded: runs != null && runs.ok,
593 perRepoLoaded: perRepo != null,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms594 stats: {
595 projects: projectCount,
596 projectsThisMonth,
597 agentHours: agentHours(runList, weekAgo, now),
598 weekCost,
599 },
600 week,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API601 groups,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms602 titles: shownTitles,
603 // A run that is going makes the page worth refreshing on its own.
604 changing:
605 liveRuns.length > 0 ||
606 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 API607 },
608 {
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms609 headers: { "Server-Timing": serverTiming },
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API610 },
611 );
Agents as a team: lifecycle, merge queue, billing and a new shell612}
Get started on mission control613
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look614export type Loaded = Extract<Route.ComponentProps["loaderData"], { signedIn: true }>;
Get started on mission control615
Fast pages, required checks on the branch, self-hosted runners, honest incidents616/**
617 * Mission control, loaded only for someone signed in (components/mission-control.tsx).
618 * The app shell starts loading it once the browser is idle, so the
619 * skeleton below is rarely seen.
620 */
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look621const MissionControl = lazy(() => import("../components/mission-control"));
Agents as a team: lifecycle, merge queue, billing and a new shell622
Fast pages, required checks on the branch, self-hosted runners, honest incidents623/** Mission control's outline, the same size, while its code arrives. */
624function MissionControlSkeleton() {
625 return (
626 <main aria-busy="true" className="mx-auto max-w-7xl space-y-6 px-4 py-8 sm:px-6 sm:py-10">
627 <header>
628 <Skeleton className="h-8 w-72 sm:h-9" />
629 <Skeleton className="mt-2.5 h-3.5 w-96 max-w-full" />
630 </header>
631 <div className="grid grid-cols-2 gap-px overflow-hidden rounded-xl border border-line bg-line lg:grid-cols-5">
632 {Array.from({ length: 5 }, (_, index) => (
633 <div key={index} className="bg-surface p-4">
634 <Skeleton className="h-3 w-16" />
635 <Skeleton className="mt-3 h-6 w-10" />
636 <Skeleton className="mt-2 h-3 w-24" />
637 </div>
638 ))}
639 </div>
640 <div className="rounded-xl border border-line bg-surface">
641 <SkeletonRows rows={5} rowClassName="h-14" />
642 </div>
643 </main>
644 );
645}
646
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step647export default function Home({ loaderData, actionData }: Route.ComponentProps) {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look648 if (!loaderData.signedIn) return <Landing />;
API and MCP server, Rust identity service, registration, site redesign649 return (
Fast pages, required checks on the branch, self-hosted runners, honest incidents650 <Suspense fallback={<MissionControlSkeleton />}>
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step651 <MissionControl loaderData={loaderData} delegated={actionData ?? null} />
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look652 </Suspense>
Initial g1t: services, event bus, intents and attempts653 );
654}