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

678 lines28,046 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
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar502 ? [
503 {
504 repo: pathOf(repo),
505 number: pull.number,
506 title: pull.title,
507 agent: pull.agent,
508 // Who wrote it, not who merged it: a person's change that g1t
509 // auto-merged is still theirs.
510 authoredByAgent: pull.author.kind === "agent" || isAgent(pull.author.username),
511 mergedBy: pull.mergedBy,
512 mergedAt: pull.mergedAt,
513 files: pull.files,
514 },
515 ]
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms516 : [],
517 ),
518 );
519 const twoWeeksAgo = now - 14 * TIME.DAY;
520 const complete = perRepo != null && perRepo.every(({ closed }) => closed != null && reachesBack(closed, twoWeeksAgo, PULL_PAGE));
521 const week = weekOf(merged, now, tz, complete);
522 const landed = landedToday(merged, now, tz);
523
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API524 // --- Activity -------------------------------------------------------------
525 const items: ActivityItem[] = [];
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms526 const titles: Record<string, string> = {};
527 const titleKey = (repo: RepoPath, number: number) => `${repo.namespace}/${repo.name}#${number}`.toLowerCase();
528 for (const { repo, pulls, closed, events } of perRepo ?? []) {
529 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 API530 for (const event of events ?? []) {
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms531 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 API532 const item = eventItem(event, pathOf(repo));
533 if (item) items.push(item);
534 }
535 }
536 for (const entry of overviewList ?? []) {
537 const latest = entry.latest;
538 const served = latest?.status === "ready" || latest?.status === "replaced";
539 if (!slug || latest?.kind !== "production" || (!served && latest.status !== "failed")) continue;
540 items.push({
541 id: `deploy:${latest.id}`,
542 at: Date.parse(latest.finishedAt ?? latest.createdAt),
543 repo: { namespace: slug, name: entry.slug },
544 actor: latest.createdBy,
545 verb: served ? "deployed" : "deploy_failed",
546 number: null,
547 to: `/${slug}/${entry.slug}/deployments/${latest.id}`,
548 });
549 }
550 for (const memory of okOr(memories ?? null)?.workspace ?? []) {
551 const repo = memory.repo ?? memory.source.repo;
552 if (!repo || !slug) continue;
553 items.push({
554 id: `memory:${memory.id}`,
555 at: Date.parse(memory.createdAt),
556 repo,
557 actor: memory.createdBy,
558 verb: "learned",
559 number: null,
560 text: memory.text.length > 90 ? `${memory.text.slice(0, 88)}…` : memory.text,
561 to: `/${slug}/-/memory`,
562 });
563 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms564 const groups = groupActivity(items).slice(0, 40);
565 // Only the titles the feed names travel to the page.
566 const shownTitles: Record<string, string> = {};
567 for (const group of groups) {
568 for (const part of group.parts) {
569 for (const number of part.numbers) {
570 const key = titleKey(group.repo, number);
571 if (titles[key]) shownTitles[key] = titles[key];
572 }
573 }
574 }
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API575
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms576 // --- The strip ------------------------------------------------------------
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API577 const usageOk = okOr(usage ?? null);
578 const costPoints = usageOk
579 ? usageOk.byDay.map((slice) => ({ at: Date.parse(`${slice.key.split("/")[0]}T12:00:00Z`), value: slice.micros / 1_000_000 }))
580 : 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 200ms581 const weekCost = dailyBuckets(costPoints, 7, now).reduce((sum, v) => sum + v, 0);
582 const month = dayKey(now, tz).slice(0, 7);
583 const projectCount = projectsOk?.length ?? (perRepo != null ? perRepo.length : null);
584 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 API585
586 const models_ = models ?? null;
587 times.total = Date.now() - started;
Agents as a team: lifecycle, merge queue, billing and a new shell588 const serverTiming = Object.entries(times)
589 .map(([name, ms]) => `${name};dur=${ms}`)
590 .join(", ");
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API591 return data(
592 {
593 signedIn: true as const,
594 viewer,
595 name: profile?.name?.trim() || username,
596 greeting: greetingFor(hourIn(now, tz)),
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms597 date: dateLine(now, tz),
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar598 summary: summaryLine({
599 total: week.total,
600 byAgents: week.byAgents,
601 agentChanges: week.agentChanges,
602 people: week.people,
603 live: liveRuns.length,
604 needs: needRows.length,
605 }),
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms606 workspace: slug,
607 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 API608 canRunAgents: models_ == null || models_.hosted || models_.own != null,
609 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 200ms610 handedOff: activeList.length > 0 || runList.length > 0 || merged.length > 0,
611 needs: needRows,
612 waiting,
613 landed,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API614 liveTotal: liveRuns.length,
615 runsLoaded: runs != null && runs.ok,
616 perRepoLoaded: perRepo != null,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms617 stats: {
618 projects: projectCount,
619 projectsThisMonth,
620 agentHours: agentHours(runList, weekAgo, now),
621 weekCost,
622 },
623 week,
Mission control shows model usage, yours and the workspace's: tokens, cost, active days, cache share, each day, and the mix624 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 API625 groups,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms626 titles: shownTitles,
627 // A run that is going makes the page worth refreshing on its own.
628 changing:
629 liveRuns.length > 0 ||
630 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 API631 },
632 {
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms633 headers: { "Server-Timing": serverTiming },
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API634 },
635 );
Agents as a team: lifecycle, merge queue, billing and a new shell636}
Get started on mission control637
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look638export type Loaded = Extract<Route.ComponentProps["loaderData"], { signedIn: true }>;
Get started on mission control639
Fast pages, required checks on the branch, self-hosted runners, honest incidents640/**
641 * Mission control, loaded only for someone signed in (components/mission-control.tsx).
642 * The app shell starts loading it once the browser is idle, so the
643 * skeleton below is rarely seen.
644 */
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look645const MissionControl = lazy(() => import("../components/mission-control"));
Agents as a team: lifecycle, merge queue, billing and a new shell646
Fast pages, required checks on the branch, self-hosted runners, honest incidents647/** Mission control's outline, the same size, while its code arrives. */
648function MissionControlSkeleton() {
649 return (
650 <main aria-busy="true" className="mx-auto max-w-7xl space-y-6 px-4 py-8 sm:px-6 sm:py-10">
651 <header>
652 <Skeleton className="h-8 w-72 sm:h-9" />
653 <Skeleton className="mt-2.5 h-3.5 w-96 max-w-full" />
654 </header>
655 <div className="grid grid-cols-2 gap-px overflow-hidden rounded-xl border border-line bg-line lg:grid-cols-5">
656 {Array.from({ length: 5 }, (_, index) => (
657 <div key={index} className="bg-surface p-4">
658 <Skeleton className="h-3 w-16" />
659 <Skeleton className="mt-3 h-6 w-10" />
660 <Skeleton className="mt-2 h-3 w-24" />
661 </div>
662 ))}
663 </div>
664 <div className="rounded-xl border border-line bg-surface">
665 <SkeletonRows rows={5} rowClassName="h-14" />
666 </div>
667 </main>
668 );
669}
670
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step671export default function Home({ loaderData, actionData }: Route.ComponentProps) {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look672 if (!loaderData.signedIn) return <Landing />;
API and MCP server, Rust identity service, registration, site redesign673 return (
Fast pages, required checks on the branch, self-hosted runners, honest incidents674 <Suspense fallback={<MissionControlSkeleton />}>
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step675 <MissionControl loaderData={loaderData} delegated={actionData ?? null} />
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look676 </Suspense>
Initial g1t: services, event bus, intents and attempts677 );
678}