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

523 lines20,350 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";
3import { data } 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 {
6 type AgentRun,
7 type AuthoredItem,
8 type CheckStatus,
9 type Deployment,
10 type G1tEvent,
11 type Issue,
12 type Lifecycle,
13 type LiveApp,
14 type Pull,
15 type PullStatus,
16 type Repo,
17 type RepoPath,
18 type Stage,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look19 REPO_ROLE_LABELS,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API20 isActiveRun,
21} from "@g1t/contracts";
Initial g1t: services, event bus, intents and attempts22
23import type { Route } from "./+types/home";
Social cards for every page: og.g1t.sh24import { page } from "../lib/meta";
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put25import { 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 API26import {
27 type ActivityItem,
28 type Need,
29 SEEN_COOKIE,
30 TIME,
31 agentHours,
32 dailyBuckets,
33 digestParts,
34 eventItem,
35 firstPassRate,
36 greetingFor,
37 groupActivity,
38 hourIn,
39 issueToMerge,
40 median,
41 nextSeen,
42 rankNeeds,
43 readCookie,
44 stuckMinutes,
45} from "../lib/mission";
API and MCP server, Rust identity service, registration, site redesign46import { Landing } from "../components/landing";
47import {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API48 agents,
49 billing,
50 deployments,
51 events as eventLog,
52 identity,
53 projects as projectsApi,
54 repos as reposApi,
55 work,
56} from "../lib/services.server";
Mission control drops the composer for now57import { getViewer } from "../lib/session.server";
Initial g1t: services, event bus, intents and attempts58
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look59/** 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 API60const TZ_COOKIE = "g1t_tz";
61/** The most projects whose pull requests and events are read for the page. */
62const MAX_PROJECTS = 10;
63const EVENTS_PER_PROJECT = 80;
A free allowance on g1t's models, so anyone can try its agents64
Social cards for every page: og.g1t.sh65export function meta(args: Route.MetaArgs) {
66 return page(args, {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look67 title: "g1t — where people and agents ship software together",
Social cards for every page: og.g1t.sh68 description:
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look69 "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.sh70 });
Initial g1t: services, event bus, intents and attempts71}
72
Agents as a team: lifecycle, merge queue, billing and a new shell73export function headers({ loaderHeaders }: Route.HeadersArgs) {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API74 const out = new Headers();
75 const timing = loaderHeaders.get("Server-Timing");
76 if (timing) out.set("Server-Timing", timing);
77 const cookie = loaderHeaders.get("Set-Cookie");
78 if (cookie) out.set("Set-Cookie", cookie);
79 return out;
Agents as a team: lifecycle, merge queue, billing and a new shell80}
81
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API82/** A row of the viewer's pull requests, whichever list it comes from. */
83type PullRow = {
84 key: string;
85 repo: RepoPath;
86 number: number;
87 title: string;
88 status: PullStatus;
89 agent: string | null;
90 stage: Stage | null;
91 detail: string | null;
92 checkStatus: CheckStatus | null;
93 updatedAt: string;
94};
95
96/** A row of the viewer's issues. */
97type IssueRow = {
98 key: string;
99 repo: RepoPath;
100 number: number;
101 title: string;
102 agent: string | null;
103 queued: boolean;
104 updatedAt: string;
105};
106
107type ProjectHealth = {
108 slug: string;
109 name: string;
110 private: boolean;
111 repo: RepoPath | null;
112 deploys: boolean;
113 production: LiveApp | null;
114 latest: Deployment | null;
115 openPulls: number | null;
116 agents: number;
117 passRate: number | null;
118 checks: number;
119};
120
121const pullRow = (pull: Pull, repo: RepoPath, lifecycle: Lifecycle | null): PullRow => ({
122 key: pull.id,
123 repo,
124 number: pull.number,
125 title: pull.title,
126 status: pull.status,
127 agent: pull.agent,
128 stage: lifecycle?.stage ?? null,
129 detail: lifecycle?.detail ?? null,
130 checkStatus: pull.checkStatus,
131 updatedAt: pull.updatedAt,
132});
133
134const authoredPull = (item: AuthoredItem): PullRow => ({
135 key: `${item.repo.namespace}/${item.repo.name}#${item.number}`,
136 repo: item.repo,
137 number: item.number,
138 title: item.title,
139 status: item.status ?? (item.draft ? "draft" : "open"),
140 agent: null,
141 stage: null,
142 detail: null,
143 checkStatus: null,
144 updatedAt: item.updatedAt,
145});
146
147export async function loader({ context, request }: Route.LoaderArgs) {
Initial g1t: services, event bus, intents and attempts148 const viewer = getViewer(context);
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API149 if (!viewer) {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look150 // The signed-out home page is about signing up; it lists no repositories.
151 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 API152 }
153
154 const now = Date.now();
155 const weekAgo = now - 7 * TIME.DAY;
Agents as a team: lifecycle, merge queue, billing and a new shell156 const started = Date.now();
157 const times: Record<string, number> = {};
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API158 // Each call is timed, and one that fails leaves its section empty rather
159 // than taking the page down.
160 const soft = <T,>(name: string, promise: Promise<T>): Promise<T | null> =>
161 promise.then(
162 (value) => ((times[name] = Date.now() - started), value),
163 (error) => {
164 console.warn(`home: ${name} failed`, error);
165 times[name] = Date.now() - started;
166 return null;
167 },
168 );
169 const okOr = <T,>(result: { ok: true; value: T } | { ok: false } | null): T | null => (result?.ok ? result.value : null);
170
171 const cookies = request.headers.get("cookie");
172 const seen = nextSeen(readCookie(cookies, SEEN_COOKIE), now);
173 const tz = readCookie(cookies, TZ_COOKIE);
174 const memberships = viewer.workspaces ?? [];
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put175 // The workspace you chose, as the sidebar shows it (lib/workspace-choice.ts).
176 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 API177 const mine = new Set(memberships.map((m) => m.slug.toLowerCase()));
178 const username = viewer.username;
179
180 const reposP = soft("repos", reposApi.list(viewer, { memberOnly: true }));
181 // The workspace's projects' open pull requests and recent events, read
182 // once each, all at once, as soon as the projects are known.
183 const perRepoP = reposP.then((repos) =>
184 Promise.all(
185 (repos ?? [])
186 .filter((repo) => mine.has(repo.namespace.toLowerCase()))
187 .slice(0, MAX_PROJECTS)
188 .map(async (repo) => {
189 const path = { namespace: repo.namespace, name: repo.name };
190 const [pulls, log] = await Promise.all([
191 work.listPulls(path, viewer, "open").catch(() => null),
192 eventLog.list({ repoId: repo.id, limit: EVENTS_PER_PROJECT }).catch(() => null),
193 ]);
194 return { repo, pulls: pulls?.ok ? pulls.value.slice(0, 60) : null, events: log };
195 }),
196 ),
197 );
198
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look199 const [repos, perRepo, active, assigned, models, profile, runs, overview, usage, authoredPulls, authoredIssues, projectList, memories, invitations] =
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API200 await Promise.all([
201 reposP,
202 soft("projects", perRepoP),
203 soft("pulls", work.listActivePulls(viewer)),
204 soft("assigned", work.listAssignedIssues(viewer)),
205 slug ? soft("models", env.RUNNER.modelAccess(slug)) : null,
206 soft("profile", identity.profile(username)),
207 slug ? soft("runs", agents.listRuns(viewer, { workspace: slug, limit: 150 })) : null,
208 slug ? soft("deploys", deployments.overview(slug, viewer)) : null,
209 slug ? soft("usage", billing.usage(slug, viewer, new Date(weekAgo - TIME.DAY).toISOString())) : null,
210 soft("authoredPulls", work.byAuthor(username, viewer, { kind: "pull", state: "open", sort: "updated", limit: 10 })),
211 soft("authoredIssues", work.byAuthor(username, viewer, { kind: "issue", state: "open", sort: "updated", limit: 10 })),
212 slug ? soft("projectList", projectsApi.list(slug, viewer)) : null,
213 slug ? soft("memories", agents.listMemories(viewer, slug, null)) : null,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look214 // Repositories someone has invited the viewer to.
215 soft("invitations", identity.myRepoInvitations(viewer)),
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API216 ]);
217
218 const repoList = repos ?? [];
Agents as a team: lifecycle, merge queue, billing and a new shell219 // Each issue and pull request is shown under its repository. Most are in
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API220 // the viewer's own, already listed; the rest are looked up once each.
221 const known = new Map<string, Repo>(repoList.map((repo) => [repo.id, repo]));
Agents as a team: lifecycle, merge queue, billing and a new shell222 const missing = [
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API223 ...new Set([...(assigned ?? []).map((issue) => issue.repoId), ...(active ?? []).map(({ pull }) => pull.repoId)]),
Agents as a team: lifecycle, merge queue, billing and a new shell224 ].filter((id) => !known.has(id));
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API225 const looked = await Promise.all(missing.map((id) => reposApi.getById(id, viewer).catch(() => null)));
226 for (const found of looked) if (found?.ok) known.set(found.value.id, found.value);
227 const pathOf = (repo: Repo): RepoPath => ({ namespace: repo.namespace, name: repo.name });
228
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);
237
238 // --- What the viewer's pull requests and issues are ----------------------
239 const lower = username.toLowerCase();
240 const openPulls = (perRepo ?? []).flatMap(({ repo, pulls }) => (pulls ?? []).map((pull) => ({ pull, repo })));
241 const reviewRequested = openPulls.filter(
242 ({ pull }) =>
243 pull.status === "open" &&
244 pull.author.username.toLowerCase() !== lower &&
245 pull.reviewers.some((name) => name.toLowerCase() === lower),
246 );
247 const assignedPulls = openPulls.filter(({ pull }) => pull.assignees.some((name) => name.toLowerCase() === lower));
248 const authoredRows =
249 activeList.length > 0
250 ? activeList.map(({ pull, lifecycle, repo }) => pullRow(pull, pathOf(repo), lifecycle))
251 : (okOr(authoredPulls)?.items ?? []).map(authoredPull);
252 const pullsTabs = {
253 authored: authoredRows.slice(0, 8),
254 review: reviewRequested.slice(0, 8).map(({ pull, repo }) => pullRow(pull, pathOf(repo), null)),
255 assigned: assignedPulls.slice(0, 8).map(({ pull, repo }) => pullRow(pull, pathOf(repo), null)),
256 };
257 const issueRow = (issue: Issue, repo: RepoPath): IssueRow => ({
258 key: issue.id,
259 repo,
260 number: issue.number,
261 title: issue.title,
262 agent: issue.agent,
263 queued: issue.queued,
264 updatedAt: issue.updatedAt,
265 });
266 const issuesTabs = {
267 assigned: (assigned ?? []).flatMap((issue) => {
268 const repo = known.get(issue.repoId);
269 return repo ? [issueRow(issue, pathOf(repo))] : [];
270 }).slice(0, 8),
271 authored: (okOr(authoredIssues)?.items ?? []).slice(0, 8).map((item) => ({
272 key: `${item.repo.namespace}/${item.repo.name}#${item.number}`,
273 repo: item.repo,
274 number: item.number,
275 title: item.title,
276 agent: null,
277 queued: false,
278 updatedAt: item.updatedAt,
279 })),
280 };
281
282 // --- Needs you ------------------------------------------------------------
283 const needs: Need[] = [];
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look284 for (const invitation of invitations ?? []) {
285 if (invitation.status !== "pending") continue;
286 needs.push({
287 key: `invitation:${invitation.id}`,
288 kind: "invitation",
289 title: `${invitation.invited_by ?? "Someone"} invited you to ${invitation.repo}`,
290 detail: `With the ${REPO_ROLE_LABELS[invitation.role]} role. The invitation expires ${new Date(invitation.expires_at).toISOString().slice(0, 10)}.`,
291 to: `/${invitation.repo}/invitations`,
292 action: "Respond",
293 at: Date.parse(invitation.created_at),
294 where: invitation.repo,
295 });
296 }
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API297 for (const entry of overviewList ?? []) {
298 const latest = entry.latest;
299 if (latest?.kind === "production" && latest.status === "failed" && slug) {
300 needs.push({
301 key: `deploy:${entry.slug}`,
302 kind: "deploy",
303 title: `Production build of ${entry.slug} failed`,
304 detail: latest.error ?? "The last build of the default branch failed. Production still serves the build before it.",
305 to: `/${slug}/${entry.slug}/deployments/${latest.id}`,
306 action: "See the build",
307 at: Date.parse(latest.finishedAt ?? latest.createdAt),
308 where: `${slug}/${entry.slug}`,
309 });
310 }
311 }
312 for (const { pull, lifecycle, repo } of activeList) {
313 const where = `${repo.namespace}/${repo.name}#${pull.number}`;
314 const to = `/${repo.namespace}/${repo.name}/pull/${pull.number}`;
315 if (lifecycle?.stage === "needs_you") {
316 const conflict = /conflict/i.test(lifecycle.detail);
317 needs.push({
318 key: `pull:${pull.id}`,
319 kind: conflict ? "conflict" : "stalled",
320 title: pull.title,
321 detail: lifecycle.detail,
322 to,
323 action: conflict ? "Resolve" : "Decide",
324 at: Date.parse(pull.updatedAt),
325 where,
326 });
327 } else if (lifecycle?.stage === "ready") {
328 needs.push({ key: `pull:${pull.id}`, kind: "ready", title: pull.title, detail: "Checks passed and it was approved. It lands when you merge it.", to, action: "Merge", at: Date.parse(pull.updatedAt), where });
329 } else if (!lifecycle && pull.status === "open" && pull.checkStatus === "failed") {
330 needs.push({ key: `pull:${pull.id}`, kind: "checks", title: pull.title, detail: "Its acceptance checks failed on the latest push.", to, action: "See checks", at: Date.parse(pull.updatedAt), where });
331 }
332 }
333 for (const { pull, repo } of reviewRequested) {
334 needs.push({
335 key: `review:${pull.id}`,
336 kind: "review",
337 title: pull.title,
338 detail: `${pull.author.username} asked for your review.`,
339 to: `/${repo.namespace}/${repo.name}/pull/${pull.number}`,
340 action: "Review",
341 at: Date.parse(pull.updatedAt),
342 where: `${repo.namespace}/${repo.name}#${pull.number}`,
343 });
344 }
345 let quietest: number | null = null;
346 for (const run of liveRuns) {
347 const minutes = stuckMinutes(run, now);
348 if (minutes == null) continue;
349 quietest = Math.max(quietest ?? 0, minutes);
350 needs.push({
351 key: `run:${run.id}`,
352 kind: "stuck",
353 title: run.title ?? `${run.agent}'s run`,
354 detail: `${run.agent} has reported nothing for ${minutes} min${run.step ? `. Last: ${run.step}` : ""}.`,
355 to: `/${run.repo.namespace}/${run.repo.name}/agents/runs/${run.id}`,
356 action: "Look",
357 at: Date.parse(run.updatedAt),
358 where: `${run.repo.namespace}/${run.repo.name}${run.number != null ? `#${run.number}` : ""}`,
359 });
360 }
361
362 // --- Activity -------------------------------------------------------------
363 const items: ActivityItem[] = [];
364 const allEvents: { repo: string; event: G1tEvent }[] = [];
365 for (const { repo, events } of perRepo ?? []) {
366 for (const event of events ?? []) {
367 allEvents.push({ repo: repo.id, event });
368 const item = eventItem(event, pathOf(repo));
369 if (item) items.push(item);
370 }
371 }
372 for (const entry of overviewList ?? []) {
373 const latest = entry.latest;
374 const served = latest?.status === "ready" || latest?.status === "replaced";
375 if (!slug || latest?.kind !== "production" || (!served && latest.status !== "failed")) continue;
376 items.push({
377 id: `deploy:${latest.id}`,
378 at: Date.parse(latest.finishedAt ?? latest.createdAt),
379 repo: { namespace: slug, name: entry.slug },
380 actor: latest.createdBy,
381 verb: served ? "deployed" : "deploy_failed",
382 number: null,
383 to: `/${slug}/${entry.slug}/deployments/${latest.id}`,
384 });
385 }
386 for (const memory of okOr(memories ?? null)?.workspace ?? []) {
387 const repo = memory.repo ?? memory.source.repo;
388 if (!repo || !slug) continue;
389 items.push({
390 id: `memory:${memory.id}`,
391 at: Date.parse(memory.createdAt),
392 repo,
393 actor: memory.createdBy,
394 verb: "learned",
395 number: null,
396 text: memory.text.length > 90 ? `${memory.text.slice(0, 88)}…` : memory.text,
397 to: `/${slug}/-/memory`,
398 });
399 }
400 const groups = groupActivity(items).slice(0, 60);
401
402 // --- Since you were last here --------------------------------------------
403 const since = seen.since ?? now - TIME.DAY;
404 const after = (verb: ActivityItem["verb"]) => items.filter((item) => item.verb === verb && item.at > since).length;
405 const digest = digestParts({
406 landed: after("landed"),
407 reviews: reviewRequested.length,
408 opened: after("opened_issue"),
409 deploys: after("deployed"),
410 failedDeploys: after("deploy_failed"),
411 stuck: quietest,
412 });
413
414 // --- Pulse ----------------------------------------------------------------
415 const week = allEvents.filter(({ event }) => Date.parse(event.time) >= weekAgo);
416 const mergedWeek = week.filter(({ event }) => event.type === "pull.merged");
417 const spans = issueToMerge(
418 allEvents.flatMap(({ repo, event }) => (event.type === "issue.opened" ? [{ repo, number: event.data.number, at: Date.parse(event.time) }] : [])),
419 mergedWeek.flatMap(({ repo, event }) => (event.type === "pull.merged" ? [{ repo, issue: event.data.issue ?? null, at: Date.parse(event.time) }] : [])),
420 );
421 const checkEvents = allEvents.flatMap(({ repo, event }) =>
422 event.type === "checks.completed" ? [{ repo, number: event.data.number, at: Date.parse(event.time), passed: event.data.status === "passed" }] : [],
423 );
424 const firstPass = firstPassRate(checkEvents.filter((c) => c.at >= weekAgo));
425 const usageOk = okOr(usage ?? null);
426 const costPoints = usageOk
427 ? usageOk.byDay.map((slice) => ({ at: Date.parse(`${slice.key.split("/")[0]}T12:00:00Z`), value: slice.micros / 1_000_000 }))
428 : runList.filter((run) => run.costUsd != null).map((run) => ({ at: Date.parse(run.createdAt), value: run.costUsd ?? 0 }));
429 const costDays = dailyBuckets(costPoints, 7, now);
430 const pulse = {
431 merged: mergedWeek.length,
432 mergedDays: dailyBuckets(mergedWeek.map(({ event }) => ({ at: Date.parse(event.time) })), 7, now),
433 hours: agentHours(runList, weekAgo, now),
434 hoursDays: dailyBuckets(
435 runList
436 .filter((run) => run.startedAt)
437 .map((run) => ({ at: Date.parse(run.startedAt!), value: agentHours([run], weekAgo, now) })),
438 7,
439 now,
440 ),
441 cost: costDays.reduce((sum, v) => sum + v, 0),
442 costDays,
443 issueToMerge: median(spans),
444 mergedWithIssue: spans.length,
445 firstPass: firstPass.rate,
446 firstPassOf: firstPass.of,
447 };
448
449 // --- Projects -------------------------------------------------------------
450 const byRepoName = new Map((perRepo ?? []).map((entry) => [`${entry.repo.namespace}/${entry.repo.name}`.toLowerCase(), entry]));
451 const projectsHealth: ProjectHealth[] = (projectsOk ?? []).slice(0, 12).map((project) => {
452 const repo = project.source.kind === "hosted" ? project.source.repo : null;
453 const key = repo ? `${repo.namespace}/${repo.name}`.toLowerCase() : "";
454 const entry = byRepoName.get(key);
455 const deploy = overviewList?.find((d) => d.slug === project.slug) ?? null;
456 const checks = (entry?.events ?? []).flatMap((event) => (event.type === "checks.completed" ? [event.data.status === "passed"] : []));
457 return {
458 slug: project.slug,
459 name: project.name,
460 private: project.private,
461 repo,
462 deploys: deploy?.enabled ?? false,
463 production: deploy?.production ?? null,
464 latest: deploy?.latest ?? null,
465 openPulls: entry?.pulls ? entry.pulls.length : null,
466 agents: liveRuns.filter((run) => repo && `${run.repo.namespace}/${run.repo.name}`.toLowerCase() === key).length,
467 passRate: checks.length ? checks.filter(Boolean).length / checks.length : null,
468 checks: checks.length,
469 };
470 });
471
472 const models_ = models ?? null;
473 times.total = Date.now() - started;
Agents as a team: lifecycle, merge queue, billing and a new shell474 const serverTiming = Object.entries(times)
475 .map(([name, ms]) => `${name};dur=${ms}`)
476 .join(", ");
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API477 const secure = new URL(request.url).protocol === "https:" ? "; Secure" : "";
478 return data(
479 {
480 signedIn: true as const,
481 viewer,
482 name: profile?.name?.trim() || username,
483 greeting: greetingFor(hourIn(now, tz)),
484 seenBefore: seen.since,
485 repos: repoList,
486 canRunAgents: models_ == null || models_.hosted || models_.own != null,
487 trial: models_?.own == null ? (models_?.trial ?? null) : null,
488 active: activeList.map(({ pull, lifecycle, repo }) => ({ pull, lifecycle, repo })),
489 needs: rankNeeds(needs),
490 live: liveRuns.slice(0, 12) as AgentRun[],
491 liveTotal: liveRuns.length,
492 runsLoaded: runs != null && runs.ok,
493 pullsTabs,
494 issuesTabs,
495 perRepoLoaded: perRepo != null,
496 groups,
497 digest,
498 pulse,
499 projects: projectsHealth,
500 projectsLoaded: projectsOk != null,
501 },
502 {
503 headers: {
504 "Server-Timing": serverTiming,
505 "Set-Cookie": `${SEEN_COOKIE}=${seen.value}; Path=/; Max-Age=31536000; HttpOnly; SameSite=Lax${secure}`,
506 },
507 },
508 );
Agents as a team: lifecycle, merge queue, billing and a new shell509}
Get started on mission control510
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look511export type Loaded = Extract<Route.ComponentProps["loaderData"], { signedIn: true }>;
Get started on mission control512
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look513/** Mission control, loaded only for someone signed in (components/mission-control.tsx). */
514const MissionControl = lazy(() => import("../components/mission-control"));
Agents as a team: lifecycle, merge queue, billing and a new shell515
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API516export default function Home({ loaderData }: Route.ComponentProps) {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look517 if (!loaderData.signedIn) return <Landing />;
API and MCP server, Rust identity service, registration, site redesign518 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look519 <Suspense fallback={null}>
520 <MissionControl loaderData={loaderData} />
521 </Suspense>
Initial g1t: services, event bus, intents and attempts522 );
523}