g1t/apps/web/app/routes/repo/overview.tsx

1,240 lines56,701 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.

Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1import {
2 Activity as ActivityIcon,
3 ArrowDownLeft,
4 ArrowRight,
5 ArrowUpRight,
6 Bot,
7 Brain,
8 Code2,
9 GitBranch,
10 GitCommitHorizontal,
11 GitMerge,
12 Hand,
13 HeartPulse,
14 Loader2,
15 Lock,
16 Network,
A project is an app or a library: libraries show their package and how to ship a release, not production17 Package as PackageGlyph,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API18 Pin,
19 Rocket,
20 RotateCw,
21} from "lucide-react";
Fast pages, required checks on the branch, self-hosted runners, honest incidents22import { type ReactNode, Suspense } from "react";
23import { Await, Form, Link, useNavigation } from "react-router";
Projects: what a workspace builds and runs, first on every page24
A project is an app or a library: libraries show their package and how to ship a release, not production25import {
26 type AgentRun,
27 type G1tEvent,
28 type Memory,
29 type PackageSummary,
30 type Project,
31 type Pull,
32 RUN_KIND_LABEL,
33 isActiveRun,
34 workOwner,
35} from "@g1t/contracts";
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API36
Projects: what a workspace builds and runs, first on every page37import type { Route } from "./+types/overview";
38import { host, StatusDot } from "../../components/deploy";
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API39import { CheckBadge } from "../../components/checks";
40import { Elapsed, formatCost, useLiveRefresh } from "../../components/agents";
41import { ActivityFeed, DeployStrip, Meter, NeedsList, Panel, Quiet, Unavailable, percent } from "../../components/mission";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look42import { type ActiveBranch, ActiveBranches } from "../../components/branches";
43import { ProductionChecklist } from "../../components/checklist";
A project is an app or a library: libraries show their package and how to ship a release, not production44import { PackageIcon } from "../../components/package-icon";
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily45import { Skeleton, SkeletonRows } from "../../components/ui/skeleton";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look46import { ProductionShot } from "../../components/production-shot";
47import { GithubLinkStrip } from "../../components/github";
48import { githubApp } from "../../lib/github.server";
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API49import { Avatar, Button, ButtonLink, CopyLine, TimeAgo } from "../../components/ui";
50import { ChangeSize } from "../../components/work";
51import {
52 type ActivityItem,
53 type Need,
54 type PipelineStage,
55 PIPELINE,
56 TIME,
57 ageBuckets,
58 eventItem,
59 firstPassRate,
60 groupActivity,
61 passRate,
62 pipelineStage,
63 queuedNumbers,
64 rankNeeds,
65 stuckMinutes,
66} from "../../lib/mission";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look67import { drift } from "../../lib/branches";
A project is an app or a library: libraries show their package and how to ship a release, not production68import { agentWasAssigned, hasInstructions, productionChecklist, releaseChecklist } from "../../lib/checklist";
69import { ECOSYSTEM_LABEL, installCommands } from "../../lib/packages";
70import { PUBLISH_GUIDES, hasRelease, libraryPackages, packageName, packagePath, publishGuide } from "../../lib/project-kind";
71import { actions, agents, deployments, events as eventLog, packages, projects, repos, work } from "../../lib/services.server";
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent72import { madeByG1t } from "../../lib/opened-by";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look73import { assertSameOrigin, getViewer, requireUser } from "../../lib/session.server";
Fast pages, required checks on the branch, self-hosted runners, honest incidents74import { accessTo, countsFor, refusal, repoFor } from "../../lib/access.server";
Production thumbnails follow each deploy, not each commit, and old ones are cleared daily75import { shotVersion } from "./production-screenshot";
Projects: what a workspace builds and runs, first on every page76
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API77const MAX_LANDED = 6;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look78/** Branches read for the Active branches list, and shown. */
79const BRANCHES_READ = 10;
80const BRANCHES_SHOWN = 5;
81/** How far back each branch's history, and the default branch's, is read to count ahead and behind. */
82const BRANCH_DEPTH = 40;
83const MAIN_DEPTH = 120;
A project is an app or a library: libraries show their package and how to ship a release, not production84/** A library's packages shown on its overview; the rest are a link away. */
85const PACKAGES_SHOWN = 3;
Projects: what a workspace builds and runs, first on every page86
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily87/**
88 * The overview streams: the layout's header and tabs (one repository
89 * lookup, one project) go out at once with a skeleton below them, and
90 * everything here, seventeen calls across six services, follows in the
91 * same response as it settles. Crawlers wait for all of it
92 * (entry.server.tsx). Before, the first byte waited on the slowest of them.
93 */
94export function loader({ params, context }: Route.LoaderArgs) {
95 return { overview: overviewData({ params, context }) };
96}
97
98async function overviewData({ params, context }: Pick<Route.LoaderArgs, "params" | "context">) {
Projects: what a workspace builds and runs, first on every page99 const viewer = getViewer(context);
100 const path = { namespace: params.owner, name: params.repo };
101 const ref = { workspace: params.owner, slug: params.repo };
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API102 const now = Date.now();
103 // A section whose service fails shows its own empty state; the page stays up.
104 const soft = <T,>(promise: Promise<T> | null): Promise<T | null> =>
105 promise ? promise.catch((error) => (console.warn("overview:", error), null)) : Promise.resolve(null);
Fast pages, required checks on the branch, self-hosted runners, honest incidents106 // People with a role of their own here see its running parts: deployments,
107 // memory, setup; everyone who can read it sees the rest. Not awaited: only
108 // those sections wait on the repository lookup, which the layout makes in
109 // this request too.
110 const accessP = accessTo(context, params);
111 const memberP = accessP.then((access) => access.insider);
112 const forMembers = <T,>(start: () => Promise<T>): Promise<T | null> =>
113 memberP.then((member) => (member ? soft(start()) : null));
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look114 const repoP = soft(repoFor(context, params));
A project is an app or a library: libraries show their package and how to ship a release, not production115 const projectP = soft(projects.get(params.owner, params.repo, viewer));
116 // A library shows its packages where an app shows production, and its
117 // checklist counts a workflow and a release instead of deploys.
118 const libraryRepoP = Promise.all([projectP, repoP]).then(([project, repo]) =>
119 project?.ok && project.value.kind === "library" && repo?.ok ? repo.value : null,
120 );
121 const packagesP = libraryRepoP.then((repo) => (repo ? soft(packages.list(params.owner, viewer, { repoId: repo.id })) : null));
122 const workflowsP = libraryRepoP.then((repo) => (repo ? forMembers(() => actions.workflows(path, viewer)) : null));
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API123 const eventsP = repoP.then((repo) => (repo?.ok ? soft(eventLog.list({ repoId: repo.value.id, limit: 150 })) : null));
124 const minePullsP = repoP.then((repo) =>
125 repo?.ok && viewer
126 ? soft(work.listActivePulls(viewer)).then((list) => (list ?? []).filter((item) => item.pull.repoId === repo.value.id))
127 : null,
128 );
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look129 const openP = soft(work.listPulls(path, viewer, "open"));
130 // Where its code came from on GitHub, for members.
Fast pages, required checks on the branch, self-hosted runners, honest incidents131 const githubP = repoP.then((repo) => (repo?.ok ? forMembers(() => githubApp.link(repo.value.id)) : null));
132 const listP = forMembers(() => deployments.list(ref, viewer));
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look133 // Active branches: the newest few besides the default, with how far each
134 // has moved. Branches with an open pull request are read first.
135 const branchesP = Promise.all([repoP, soft(repos.branches(path, viewer)), openP, listP]).then(
136 async ([repo, branchList, pulls, deploys]): Promise<{ main: string; total: number; shown: ActiveBranch[] } | null> => {
137 if (!repo?.ok || !branchList?.ok) return null;
138 const main = repo.value.defaultBranch;
139 const pullList = pulls?.ok ? pulls.value : [];
140 const pullOn = new Map(pullList.filter((pull) => pull.branch).map((pull) => [pull.branch as string, pull]));
141 const others = branchList.value.filter((branch) => branch.name !== main);
142 if (others.length === 0) return { main, total: 0, shown: [] };
143 const read = [...others.filter((b) => pullOn.has(b.name)), ...others.filter((b) => !pullOn.has(b.name))].slice(0, BRANCHES_READ);
Fast pages, required checks on the branch, self-hosted runners, honest incidents144 // By commit hash, not name: history from a commit never changes, so
145 // repos keeps it (services/repos/src/store.rs) and only new heads cost
146 // a walk.
147 const mainHead = branchList.value.find((branch) => branch.name === main)?.hash ?? main;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look148 const [mainLog, ...logs] = await Promise.all([
Fast pages, required checks on the branch, self-hosted runners, honest incidents149 soft(repos.log(path, viewer, mainHead, MAIN_DEPTH)),
150 ...read.map((branch) => soft(repos.log(path, viewer, branch.hash || branch.name, BRANCH_DEPTH))),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look151 ]);
152 const mainHashes = mainLog?.ok ? mainLog.value.map((c) => c.hash) : [];
153 const previews = deploys?.ok ? deploys.value.live.filter((app) => app.kind === "preview") : [];
154 const shown = read
155 .map((branch, index): ActiveBranch => {
156 const history = logs[index]?.ok ? logs[index].value : [];
157 const head = history[0];
158 const moved = drift(history.map((c) => c.hash), mainHashes, BRANCH_DEPTH);
159 const pull = pullOn.get(branch.name);
160 return {
161 name: branch.name,
162 commit: head ? { hash: head.hash, message: head.message.split("\n")[0], author: head.author.name, at: head.authoredAt } : null,
163 ...moved,
164 pull: pull ? { number: pull.number, title: pull.title, checkStatus: pull.checkStatus, draft: pull.status === "draft" } : null,
165 preview: previews.find((app) => app.branch === branch.name || (pull != null && app.number === pull.number))?.url ?? null,
166 };
167 })
168 .sort((a, b) => Date.parse(b.commit?.at ?? "0") - Date.parse(a.commit?.at ?? "0"))
169 .slice(0, BRANCHES_SHOWN);
170 return { main, total: others.length, shown };
171 },
172 );
Fast pages, required checks on the branch, self-hosted runners, honest incidents173 // Active branches read several logs each: streamed, so the rest shows first.
174 const branches = branchesP.catch(() => null);
A project is an app or a library: libraries show their package and how to ship a release, not production175 const [{ insider: member, can }, project, settings, list, open, closed, log, counts, deps, runs, queue, issues, memories, recent, mine, domains, root, packageList, workflows] = await Promise.all([
Fast pages, required checks on the branch, self-hosted runners, honest incidents176 accessP,
A project is an app or a library: libraries show their package and how to ship a release, not production177 projectP,
Fast pages, required checks on the branch, self-hosted runners, honest incidents178 forMembers(() => deployments.settings(ref, viewer)),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look179 listP,
180 openP,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API181 soft(work.listPulls(path, viewer, "closed")),
182 soft(repos.log(path, viewer, null, 1)),
Fast pages, required checks on the branch, self-hosted runners, honest incidents183 soft(countsFor(context, params)),
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API184 soft(projects.dependencies(params.owner, params.repo, viewer)),
185 soft(agents.listRuns(viewer, { repo: path, limit: 60 })),
186 soft(work.queue(path, viewer)),
187 soft(work.listIssues(path, viewer, { state: "open" })),
Fast pages, required checks on the branch, self-hosted runners, honest incidents188 forMembers(() => agents.listMemories(viewer, params.owner, path)),
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API189 eventsP,
190 minePullsP,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look191 // For the checklist, which only members see.
Fast pages, required checks on the branch, self-hosted runners, honest incidents192 forMembers(() => deployments.domains(ref, viewer)),
193 forMembers(() => repos.tree(path, viewer, null, "")),
A project is an app or a library: libraries show their package and how to ship a release, not production194 packagesP,
195 workflowsP,
Projects: what a workspace builds and runs, first on every page196 ]);
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API197 const ok = <T,>(result: { ok: true; value: T } | { ok: false } | null): T | null => (result?.ok ? result.value : null);
198
199 const openPulls = ok(open) ?? [];
200 const runList = ok(runs) ?? [];
201 const live = runList.filter((run) => isActiveRun(run.status));
202 const builds = ok(list)?.deployments ?? [];
203 const landed = (ok(closed) ?? [])
204 .filter((pull) => pull.status === "merged")
205 .sort((a, b) => Date.parse(b.mergedAt ?? b.updatedAt) - Date.parse(a.mergedAt ?? a.updatedAt));
206 const queued = queuedNumbers(ok(queue)?.active ?? []);
207 const runOn = new Map<number, AgentRun>();
208 for (const run of live) if (run.number != null && !runOn.has(run.number)) runOn.set(run.number, run);
209 const lifecycleOf = new Map((mine ?? []).map((item) => [item.pull.number, item.lifecycle]));
210
211 // --- The pipeline -----------------------------------------------------------
212 type Card = {
213 number: number;
214 title: string;
215 agent: string;
216 status: Pull["status"];
217 checkStatus: Pull["checkStatus"];
218 step: string | null;
219 runKind: string | null;
220 runStarted: string | null;
221 needsYou: boolean;
222 at: string;
223 };
224 const columns: Record<PipelineStage, Card[]> = { working: [], checking: [], reviewing: [], queue: [], landed: [] };
225 for (const pull of openPulls.slice(0, 40)) {
226 const run = runOn.get(pull.number);
Fast pages, required checks on the branch, self-hosted runners, honest incidents227 columns[pipelineStage(pull, run, queued, lifecycleOf.get(pull.number))].push({
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API228 number: pull.number,
229 title: pull.title,
230 agent: pull.agent,
231 status: pull.status,
232 checkStatus: pull.checkStatus,
233 step: run?.step ?? null,
234 runKind: run ? RUN_KIND_LABEL[run.kind] : null,
235 runStarted: run ? (run.startedAt ?? run.createdAt) : null,
236 needsYou: lifecycleOf.get(pull.number)?.stage === "needs_you",
237 at: pull.updatedAt,
238 });
239 }
240 for (const pull of landed.filter((p) => now - Date.parse(p.mergedAt ?? p.updatedAt) < 3 * TIME.DAY).slice(0, 5)) {
241 columns.landed.push({
242 number: pull.number,
243 title: pull.title,
244 agent: pull.agent,
245 status: pull.status,
246 checkStatus: pull.checkStatus,
247 step: null,
248 runKind: null,
249 runStarted: null,
250 needsYou: false,
251 at: pull.mergedAt ?? pull.updatedAt,
252 });
253 }
254
255 // --- Needs you ---------------------------------------------------------------
256 const base = `/${params.owner}/${params.repo}`;
257 const needs: Need[] = [];
258 const latestProduction = builds.find((build) => build.kind === "production") ?? null;
259 if (latestProduction?.status === "failed") {
260 needs.push({
261 key: "deploy",
262 kind: "deploy",
263 title: "The production build failed",
264 detail: latestProduction.error ?? "Production still serves the build before it.",
265 to: `${base}/deployments/${latestProduction.id}`,
266 action: "See the build",
267 at: Date.parse(latestProduction.finishedAt ?? latestProduction.createdAt),
268 where: latestProduction.commit.slice(0, 7),
269 });
270 }
271 const me = viewer?.username.toLowerCase();
272 for (const pull of openPulls) {
273 const to = `${base}/pull/${pull.number}`;
274 const where = `#${pull.number}`;
275 const lifecycle = lifecycleOf.get(pull.number);
276 if (lifecycle?.stage === "needs_you") {
277 const conflict = /conflict/i.test(lifecycle.detail);
278 needs.push({ key: `pull:${pull.number}`, kind: conflict ? "conflict" : "stalled", title: pull.title, detail: lifecycle.detail, to, action: conflict ? "Resolve" : "Decide", at: Date.parse(pull.updatedAt), where });
279 } else if (lifecycle?.stage === "ready") {
280 needs.push({ key: `pull:${pull.number}`, kind: "ready", title: pull.title, detail: "Ready to land when you merge it.", to, action: "Merge", at: Date.parse(pull.updatedAt), where });
281 }
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights282 // Never your own, nor one g1t made for you; whoever it is for asked.
283 const owner = workOwner(pull).username;
284 if (me && pull.status === "open" && owner.toLowerCase() !== me && pull.reviewers.some((name) => name.toLowerCase() === me)) {
285 needs.push({ key: `review:${pull.number}`, kind: "review", title: pull.title, detail: `${owner} asked for your review.`, to, action: "Review", at: Date.parse(pull.updatedAt), where });
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API286 }
287 if (member && pull.status === "open" && pull.checkStatus === "failed" && !runOn.has(pull.number) && !lifecycle) {
Fast pages, required checks on the branch, self-hosted runners, honest incidents288 needs.push({ key: `checks:${pull.number}`, kind: "checks", title: pull.title, detail: "It failed in the merge queue and no agent is fixing it.", to, action: "See checks", at: Date.parse(pull.updatedAt), where });
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API289 }
290 }
291 for (const run of live) {
292 const minutes = stuckMinutes(run, now);
293 if (minutes == null) continue;
294 needs.push({
295 key: `run:${run.id}`,
296 kind: "stuck",
297 title: run.title ?? `${run.agent}'s run`,
298 detail: `${run.agent} has reported nothing for ${minutes} min${run.step ? `. Last: ${run.step}` : ""}.`,
299 to: `${base}/agents/runs/${run.id}`,
300 action: "Look",
301 at: Date.parse(run.updatedAt),
302 where: run.number != null ? `#${run.number}` : null,
303 });
304 }
305
306 // --- Activity and health -------------------------------------------------------
307 const eventList: G1tEvent[] = recent ?? [];
308 const items = eventList
309 .map((event) => eventItem(event, path))
310 .filter((item): item is ActivityItem => item != null);
311 const checkEvents = eventList.flatMap((event) =>
312 event.type === "checks.completed" ? [{ repo: "", number: event.data.number, at: Date.parse(event.time), passed: event.data.status === "passed" }] : [],
313 );
314 const memoryList = ok(memories)?.project ?? [];
315 const knows = [...memoryList.filter((m) => m.pinned), ...memoryList.filter((m) => !m.pinned).sort((a, b) => Date.parse(b.createdAt) - Date.parse(a.createdAt))].slice(0, 5);
316 const openIssues = ok(issues);
317
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look318 // --- Getting to production ----------------------------------------------------
319 const wentLive = (status: string) => status === "ready" || status === "replaced" || status === "down";
320 const liveApps = ok(list)?.live ?? [];
321 const commitNow = ok(log)?.[0] ?? null;
322 const projectValue = ok(project);
A project is an app or a library: libraries show their package and how to ship a release, not production323 const isLibrary = projectValue?.kind === "library";
324 const published = libraryPackages(ok(packageList) ?? []);
325 const firstSteps = {
326 base,
327 hasCode: commitNow != null || projectValue?.source.kind === "mirror",
328 instructions: commitNow == null ? false : root?.ok ? hasInstructions(root.value.entries.map((entry) => entry.name)) : null,
329 agentAssigned: agentWasAssigned({
330 runAgents: runList.map((run) => run.agent),
331 pullAgents: [...openPulls, ...(ok(closed) ?? [])].map((pull) => pull.agent),
332 issues: openIssues ?? [],
333 }),
334 };
335 const checklist = !member
336 ? null
337 : isLibrary
338 ? releaseChecklist({
339 ...firstSteps,
340 hasWorkflow: workflows?.ok ? workflows.value.length > 0 : null,
341 released: hasRelease(published),
342 releaseTo: published[0]
343 ? packagePath(published[0])
344 : (publishGuide(projectValue?.ecosystem ?? null)?.guide ?? "https://docs.g1t.sh/guides/packages/"),
345 })
346 : productionChecklist({
347 ...firstSteps,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look348 deploysEnabled: ok(settings)?.enabled ?? false,
349 productionDeployed:
350 liveApps.some((app) => app.kind === "production") ||
351 builds.some((build) => build.kind === "production" && wentLive(build.status)),
352 domains: ok(domains)?.domains.length ?? null,
353 previewOpened:
354 liveApps.some((app) => app.kind === "preview") || builds.some((build) => build.kind === "preview" && wentLive(build.status)),
A project is an app or a library: libraries show their package and how to ship a release, not production355 });
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look356
357 const github = await githubP;
Projects: what a workspace builds and runs, first on every page358 return {
359 member,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look360 can,
361 github,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API362 project: ok(project),
363 settings: ok(settings),
364 builds: builds.slice(0, 30),
365 live: ok(list)?.live ?? [],
366 commit: ok(log)?.[0] ?? null,
367 open: ok(counts) ?? { issues: openIssues?.length ?? 0, pulls: openPulls.length },
368 dependencies: ok(deps),
369 agentsLive: live.slice(0, 6),
370 runsLoaded: runs?.ok ?? false,
371 pullsLoaded: open?.ok ?? false,
372 columns,
373 needs: rankNeeds(needs),
374 landed: landed.slice(0, MAX_LANDED),
375 groups: groupActivity(items).slice(0, 30),
376 eventsLoaded: recent != null,
377 health: {
378 passRate: passRate(checkEvents.map((c) => c.passed)),
379 firstPass: firstPassRate(checkEvents),
380 checkRuns: checkEvents.length,
381 ages: openIssues ? ageBuckets(openIssues.map((issue) => issue.createdAt), now) : null,
382 },
383 knows: knows as Memory[],
384 memoryCount: memoryList.length,
385 memoriesLoaded: memories?.ok ?? false,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look386 checklist,
A project is an app or a library: libraries show their package and how to ship a release, not production387 library: isLibrary
388 ? { packages: published.slice(0, PACKAGES_SHOWN), total: published.length, loaded: packageList?.ok ?? false }
389 : null,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look390 branches,
Projects: what a workspace builds and runs, first on every page391 };
392}
393
394export async function action({ request, params, context }: Route.ActionArgs) {
395 assertSameOrigin(request);
396 const user = requireUser(context, request);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look397 const form = await request.formData().catch(() => null);
398 const intent = form?.get("intent");
399 // Syncing from GitHub is pushing; stopping it is an integration; deploying is compute.
400 const refused = await refusal(context, params, intent === "github-sync" ? "push" : intent === "github-stop" ? "manage_integrations" : "run");
401 if (refused) return { error: refused };
402 if (intent === "github-sync" || intent === "github-stop") {
403 const repo = await repos.get({ namespace: params.owner, name: params.repo }, user);
404 if (!repo.ok) return { error: repo.error.message };
405 const done =
406 intent === "github-sync" ? await githubApp.sync(user, repo.value.id) : await githubApp.unlinkRepo(user, repo.value.id);
407 return done.ok
408 ? { notice: intent === "github-sync" ? "Synced with GitHub." : "It is no longer kept in step with GitHub." }
409 : { error: done.error.message };
410 }
Projects: what a workspace builds and runs, first on every page411 const started = await deployments.redeploy(user, { workspace: params.owner, slug: params.repo }, null);
412 return started.ok ? { notice: "Production is building." } : { error: started.error.message };
413}
414
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily415type Loaded = Awaited<ReturnType<typeof overviewData>>;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API416type Card = Loaded["columns"]["working"][number];
417
418const STAGE_TONE: Record<PipelineStage, string> = {
419 working: "bg-merged",
420 checking: "bg-info",
421 reviewing: "bg-warn",
422 queue: "bg-accent-dim",
423 landed: "bg-accent",
424};
425
426function PipelineCard({ card, base, stage }: { card: Card; base: string; stage: PipelineStage }) {
427 return (
428 <li>
429 <Link
430 to={`${base}/pull/${card.number}`}
431 prefetch="intent"
432 className={`block rounded-lg border bg-bg/60 p-2.5 transition-colors hover:border-line-strong hover:bg-raised ${
433 card.needsYou ? "border-warn/50" : card.step ? "border-merged/30" : "border-line"
434 }`}
435 >
436 <span className="line-clamp-2 text-[0.8125rem] leading-snug font-medium">{card.title}</span>
437 <span className="mt-1.5 flex items-center gap-1.5 text-[0.6875rem] text-muted">
438 <Avatar name={card.agent} size={14} />
439 <span className="font-mono text-faint">#{card.number}</span>
440 <span className="grow" />
441 {card.needsYou ? (
442 <span className="inline-flex items-center gap-1 text-warn" title="Stopped: a person decides what happens next">
443 <Hand size={11} /> you
444 </span>
445 ) : stage === "landed" ? (
446 <TimeAgo at={card.at} />
447 ) : card.runStarted ? (
448 <span className="inline-flex items-center gap-1 text-merged">
449 <Loader2 size={11} className="animate-spin" />
450 <Elapsed from={card.runStarted} />
451 </span>
452 ) : (
453 <CheckBadge status={card.checkStatus} />
454 )}
455 </span>
456 {card.step && (
457 <span className="mt-1.5 block truncate font-mono text-[0.6875rem] text-fg/70" title={card.step}>
458 {card.runKind}: {card.step}
459 </span>
460 )}
461 </Link>
462 </li>
463 );
464}
465
466/** Open pull requests moving through Working → Checking → Reviewing → Queue → Landed. */
467function Pipeline({ columns, base }: { columns: Loaded["columns"]; base: string }) {
468 return (
469 <div className="relative -mx-4 overflow-x-auto px-4 pb-1 sm:mx-0 sm:px-0">
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look470 {/* Each stage as tall as what is in it: an empty one stays short
471 instead of stretching to the busiest. */}
472 <ol className="grid min-w-176 grid-cols-5 items-start gap-2">
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API473 {PIPELINE.map(({ stage, label }, index) => {
474 const cards = columns[stage];
475 return (
476 <li key={stage} className="flex min-w-0 flex-col rounded-xl border border-line bg-surface p-2">
477 <p className="flex items-center gap-2 px-1 pt-0.5 pb-2 text-xs font-medium text-muted">
478 <span className={`size-1.5 rounded-full ${STAGE_TONE[stage]} ${cards.length && stage !== "landed" ? "animate-pulse" : ""}`} />
479 {label}
480 <span className="tabular-nums text-faint">{cards.length}</span>
481 {index < PIPELINE.length - 1 && <ArrowRight size={11} className="ml-auto text-line-strong" />}
482 </p>
483 {cards.length === 0 ? (
484 <p className="rounded-lg border border-dashed border-line/70 px-2 py-3 text-center text-[0.6875rem] text-faint">Empty</p>
485 ) : (
486 <ul className="space-y-1.5">
487 {cards.slice(0, 6).map((card) => (
488 <PipelineCard key={card.number} card={card} base={base} stage={stage} />
489 ))}
490 {cards.length > 6 && (
491 <li className="px-1 text-[0.6875rem] text-muted">
492 <Link to={`${base}/pulls`} className="hover:text-fg">
493 {cards.length - 6} more
494 </Link>
495 </li>
496 )}
497 </ul>
498 )}
499 </li>
500 );
501 })}
502 </ol>
503 </div>
504 );
505}
506
A project is an app or a library: libraries show their package and how to ship a release, not production507/**
508 * Where an app's overview shows production, a library's shows what it
509 * publishes: each package with its latest version and how to install it,
510 * or, before there is one, how to publish a first version.
511 */
512function LibraryHead({
513 library,
514 project,
515 base,
516 canChange,
517 workspace,
518}: {
519 library: { packages: PackageSummary[]; total: number; loaded: boolean };
520 project: Project | null;
521 base: string;
522 canChange: boolean;
523 workspace: string;
524}) {
525 const guide = publishGuide(project?.ecosystem ?? null);
526 const name = project?.name ?? base.split("/").pop();
527 return (
528 <div className="p-5 sm:p-6">
529 <p className="flex items-center gap-2 text-xs font-medium tracking-wide text-muted uppercase">
530 <PackageGlyph size={13} className="text-accent" />
531 Packages
532 </p>
533 {library.packages.length > 0 ? (
534 <>
535 <ul className="mt-3 space-y-4">
536 {library.packages.map((pkg) => {
537 const commands = installCommands(pkg, pkg.latest, "you");
538 return (
539 <li key={pkg.id} className="flex flex-col gap-2.5 lg:flex-row lg:items-start lg:gap-5">
540 <Link to={packagePath(pkg)} className="group flex min-w-0 items-center gap-3 lg:w-72 lg:shrink-0">
541 <PackageIcon ecosystem={pkg.ecosystem} size={32} />
542 <span className="min-w-0">
543 <span className="block truncate font-mono text-[0.9375rem] font-medium group-hover:text-accent">{packageName(pkg)}</span>
544 <span className="block text-xs text-muted">
545 {ECOSYSTEM_LABEL[pkg.ecosystem]} · {pkg.latest ?? "no versions yet"}
546 {pkg.versions > 1 && ` · ${pkg.versions} versions`}
547 </span>
548 </span>
549 </Link>
550 <div className="min-w-0 grow space-y-1.5">
551 {/* The workspace's registry first, or the tool installs a package of the same name from elsewhere. */}
552 {commands.registry && <CopyLine prompt text={commands.registry} />}
553 <CopyLine prompt text={commands.install} />
554 </div>
555 </li>
556 );
557 })}
558 </ul>
559 {library.total > library.packages.length && (
560 <Link to={`/${workspace}/-/packages`} className="mt-3 inline-block text-xs text-muted hover:text-fg">
561 All {library.total} packages
562 </Link>
563 )}
564 </>
565 ) : (
566 <>
567 <p className="mt-2 text-lg font-medium">{library.loaded ? "Not published yet" : "Packages could not be loaded just now"}</p>
568 {guide ? (
569 <>
570 <p className="mt-1 max-w-lg text-sm text-muted">
571 Publish a first version of {name} to the workspace's {guide.label} registry, for others to install
572 {guide.start ? ":" : "."}
573 </p>
574 {guide.start && (
575 <div className="mt-3 max-w-lg">
576 <CopyLine prompt text={guide.start} />
577 </div>
578 )}
579 <a href={guide.guide} className="mt-3 inline-flex items-center gap-1 text-sm text-accent hover:underline">
580 Publishing to {guide.label} <ArrowUpRight size={13} />
581 </a>
582 </>
583 ) : (
584 <>
585 <p className="mt-1 max-w-lg text-sm text-muted">Publish a first version of {name} to one of the workspace's registries:</p>
586 <p className="mt-2 flex flex-wrap gap-x-4 gap-y-1 text-sm">
587 {PUBLISH_GUIDES.map((one) => (
588 <a key={one.label} href={one.guide} className="inline-flex items-center gap-1 text-accent hover:underline">
589 {one.label} <ArrowUpRight size={13} />
590 </a>
591 ))}
592 </p>
593 </>
594 )}
595 </>
596 )}
597 {canChange && (
598 <p className="mt-4 text-xs text-faint">
599 Deploys?{" "}
600 <Link to={`${base}/settings#deploys`} className="underline-offset-4 hover:text-fg hover:underline">
601 Change it in Settings.
602 </Link>
603 </p>
604 )}
605 </div>
606 );
607}
608
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API609function Stat({ label, value, to }: { label: string; value: ReactNode; to?: string }) {
610 const body = (
611 <>
612 <span className="block text-[0.6875rem] text-faint">{label}</span>
613 <span className="mt-0.5 block truncate text-[0.8125rem] font-medium">{value}</span>
614 </>
615 );
616 return to ? (
617 <Link to={to} className="min-w-0 px-5 py-3 transition-colors hover:bg-raised/50">
618 {body}
619 </Link>
620 ) : (
621 <div className="min-w-0 px-5 py-3">{body}</div>
622 );
623}
624
Projects: what a workspace builds and runs, first on every page625export default function ProjectOverview({ loaderData, actionData, params }: Route.ComponentProps) {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily626 return (
627 <Suspense fallback={<OverviewSkeleton />}>
628 <Await resolve={loaderData.overview} errorElement={<OverviewFailed />}>
629 {(overview) => <Overview loaderData={overview} actionData={actionData} params={params} />}
630 </Await>
631 </Suspense>
632 );
633}
634
635/** The overview's sections as they will sit, while they stream in. */
636function OverviewSkeleton() {
637 return (
638 <div className="space-y-8" aria-busy="true" aria-label="Loading the overview">
639 <section className="rounded-2xl border border-line bg-surface p-5 sm:p-6">
640 <Skeleton className="h-3 w-24" />
641 <Skeleton className="mt-3 h-5 w-64 max-w-full" />
642 <Skeleton className="mt-3 h-3 w-48 max-w-full" />
643 </section>
644 <section className="rounded-2xl border border-line bg-surface p-2">
645 <div className="grid grid-cols-2 gap-2 sm:grid-cols-5">
646 {Array.from({ length: 5 }, (_, index) => (
647 <Skeleton key={index} className="h-24 rounded-xl" />
648 ))}
649 </div>
650 </section>
651 <div className="grid gap-6 lg:grid-cols-2">
652 <section className="rounded-2xl border border-line bg-surface">
653 <SkeletonRows rows={4} />
654 </section>
655 <section className="rounded-2xl border border-line bg-surface">
656 <SkeletonRows rows={4} />
657 </section>
658 </div>
659 </div>
660 );
661}
662
663function OverviewFailed() {
664 return (
665 <p className="rounded-2xl border border-line bg-surface p-6 text-sm text-muted">
666 The overview could not be loaded just now. Reload the page to try again.
667 </p>
668 );
669}
670
671function Overview({
672 loaderData,
673 actionData,
674 params,
675}: {
676 loaderData: Loaded;
677 actionData: Route.ComponentProps["actionData"];
678 params: Route.ComponentProps["params"];
679}) {
A project is an app or a library: libraries show their package and how to ship a release, not production680 const { member, project, settings, builds, live, commit, open, dependencies, agentsLive, columns, needs, landed, groups, health, knows, checklist, branches, library } =
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look681 loaderData;
Projects: what a workspace builds and runs, first on every page682 const base = `/${params.owner}/${params.repo}`;
683 const production = live.find((app) => app.kind === "production") ?? null;
Agents and memory, checks and conflicts, profiles, slug renames, custom domains684 // The project's own domain, once it is active, is where production is visited.
685 const productionUrl = production ? (settings?.primaryDomain ? `https://${settings.primaryDomain}` : production.url) : null;
Projects: what a workspace builds and runs, first on every page686 const previews = live.filter((app) => app.kind === "preview");
687 const latestProduction = builds.find((build) => build.kind === "production") ?? null;
688 const busy = useNavigation().state === "submitting";
689 const source = project?.source.kind === "hosted" ? project.source : null;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API690 const moving = agentsLive.length > 0 || columns.working.length + columns.checking.length > 0;
691 useLiveRefresh(moving);
692 const ages = health.ages;
693 const oldest = ages ? Math.max(1, ...ages.map((a) => a.count)) : 1;
Projects: what a workspace builds and runs, first on every page694
695 return (
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API696 <div className="space-y-8">
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look697 {loaderData.github && <GithubLinkStrip link={loaderData.github} />}
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API698 {/* What the project is, running, and where its code is. */}
699 <section className="overflow-hidden rounded-2xl border border-line bg-surface">
A project is an app or a library: libraries show their package and how to ship a release, not production700 {library ? (
701 <LibraryHead library={library} project={project} base={base} canChange={loaderData.can.manage_settings} workspace={params.owner} />
702 ) : (
703 <div className="flex flex-col gap-5 p-5 sm:flex-row sm:items-start sm:p-6">
704 {member && (production || settings?.enabled) && (
705 <ProductionShot
706 src={production ? `${base}/production.jpg?v=${shotVersion(production)}` : null}
707 href={production ? (productionUrl ?? production.url) : null}
708 label={host(production ? (productionUrl ?? production.url) : (settings?.productionUrl ?? ""))}
709 className="w-full shrink-0 sm:w-60 lg:w-72"
710 />
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API711 )}
A project is an app or a library: libraries show their package and how to ship a release, not production712 <div className="flex min-w-0 grow flex-wrap items-start justify-between gap-4">
713 <div className="min-w-0">
714 <p className="flex items-center gap-2 text-xs font-medium tracking-wide text-muted uppercase">
715 <Rocket size={13} className="text-accent" />
716 Production
717 </p>
718 {production ? (
719 <>
720 <a
721 href={productionUrl ?? production.url}
722 className="mt-2 flex items-center gap-1.5 truncate font-mono text-lg font-medium hover:text-accent"
723 >
724 {host(productionUrl ?? production.url)}
725 <ArrowUpRight size={16} className="shrink-0 text-faint" />
726 </a>
727 <p className="mt-1.5 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted">
728 {latestProduction && <StatusDot status={latestProduction.status} label={latestProduction.status === "ready" ? "Live" : undefined} />}
729 <span className="inline-flex items-center gap-1 font-mono">
730 <GitCommitHorizontal size={13} className="text-faint" />
731 {production.commit.slice(0, 7)}
732 </span>
733 <span>
734 deployed <TimeAgo at={production.deployedAt} />
735 </span>
736 {settings?.primaryDomain && <span className="text-faint">also at {host(production.url)}</span>}
737 </p>
738 </>
739 ) : settings?.enabled ? (
740 <>
741 <p className="mt-2 font-mono text-lg text-muted">{host(settings.productionUrl)}</p>
742 <p className="mt-1.5 text-xs text-muted">
743 {latestProduction ? (
744 <StatusDot status={latestProduction.status} label={latestProduction.status === "failed" ? "The last build failed" : undefined} />
745 ) : (
746 "Not deployed yet. Push to the default branch, or deploy it now."
747 )}
748 </p>
749 </>
750 ) : (
751 <>
752 <p className="mt-2 text-lg font-medium">Deployments are off</p>
753 <p className="mt-1 max-w-lg text-sm text-muted">
754 Nothing builds or runs until you turn them on. Then {project?.name ?? params.repo} goes up on g1t.page:
755 production from the default branch, and a live preview for every pull request. It runs only while
756 someone visits.
757 </p>
758 </>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API759 )}
Projects: what a workspace builds and runs, first on every page760 </div>
A project is an app or a library: libraries show their package and how to ship a release, not production761 {member && (
762 <div className="flex shrink-0 items-center gap-2">
763 {production && (
764 <ButtonLink to={productionUrl ?? production.url} variant="accent" reloadDocument>
765 Visit
766 <ArrowUpRight size={14} />
767 </ButtonLink>
768 )}
769 {settings?.enabled ? (
770 loaderData.can.run && <Form method="post">
771 <Button type="submit" variant="quiet" disabled={busy} title="Build production again from the default branch">
772 <RotateCw size={14} />
773 Redeploy
774 </Button>
775 </Form>
776 ) : loaderData.can.manage_integrations && (
777 <ButtonLink to={`${base}/settings/deployments`} variant="accent">
778 <Rocket size={14} />
779 Turn on deployments
780 </ButtonLink>
781 )}
782 </div>
783 )}
784 </div>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look785 </div>
A project is an app or a library: libraries show their package and how to ship a release, not production786 )}
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API787 {actionData && (
788 <p className={`border-t border-line px-6 py-2.5 text-sm ${"error" in actionData ? "text-danger" : "text-accent"}`}>
789 {"error" in actionData ? actionData.error : actionData.notice}
790 </p>
791 )}
792 <div className="grid grid-cols-2 divide-line border-t border-line text-sm sm:grid-cols-4 sm:divide-x">
793 <Stat
794 label="Source"
795 to={`${base}/code`}
796 value={
797 source ? (
798 <span className="inline-flex items-center gap-1.5 font-mono">
799 {project?.private ? <Lock size={12} className="text-faint" /> : <Code2 size={12} className="text-faint" />}
800 {source.repo.name}
801 {source.rootDir ? `/${source.rootDir}` : ""}
802 </span>
803 ) : (
804 "—"
805 )
806 }
807 />
808 <Stat
809 label="Default branch"
810 to={`${base}/commits`}
811 value={
812 <span className="inline-flex items-center gap-1.5 font-mono">
813 <GitBranch size={12} className="text-faint" />
814 {source?.defaultBranch ?? "main"}
815 </span>
816 }
817 />
818 <Stat
819 label="Latest commit"
820 to={commit ? `${base}/commit/${commit.hash}` : undefined}
821 value={
822 commit ? (
823 <span title={commit.message.split("\n")[0]}>
824 <span className="font-mono">{commit.hash.slice(0, 7)}</span>{" "}
825 <span className="font-normal text-muted">
826 <TimeAgo at={commit.authoredAt} />
827 </span>
828 </span>
829 ) : (
830 "No commits yet"
831 )
832 }
833 />
834 <Stat
A project is an app or a library: libraries show their package and how to ship a release, not production835 label={member && settings?.enabled && !library ? "Last deploy" : "Open"}
836 to={member && settings?.enabled && !library ? `${base}/deployments` : `${base}/pulls`}
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API837 value={
A project is an app or a library: libraries show their package and how to ship a release, not production838 member && settings?.enabled && !library ? (
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API839 latestProduction ? (
840 <span className="inline-flex items-center gap-2">
841 <StatusDot status={latestProduction.status} />
842 <span className="font-normal text-muted">
843 <TimeAgo at={latestProduction.createdAt} />
Projects: what a workspace builds and runs, first on every page844 </span>
845 </span>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API846 ) : (
847 "None yet"
848 )
849 ) : (
850 `${open.pulls} pull requests · ${open.issues} issues`
851 )
852 }
853 />
854 </div>
855 {commit && (
856 <p className="truncate border-t border-line px-5 py-2.5 text-xs text-muted sm:px-6">
857 <GitCommitHorizontal size={12} className="mr-1.5 inline text-faint" />
858 {commit.message.split("\n")[0]} <span className="text-faint">· {commit.author.name}</span>
859 </p>
860 )}
861 </section>
862
A project is an app or a library: libraries show their package and how to ship a release, not production863 {checklist && <ProductionChecklist base={base} items={checklist} title={library ? "Ship a release" : undefined} />}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look864
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API865 <Panel
866 title="Right now"
867 icon={<span className={`block size-2 rounded-full ${agentsLive.length > 0 ? "animate-pulse bg-merged" : "bg-line-strong"}`} />}
868 count={agentsLive.length}
869 all={{ to: `${base}/agents`, label: "Agents" }}
870 >
871 {agentsLive.length > 0 && (
872 <ul className="mb-3 grid grid-cols-1 gap-2 sm:grid-cols-2 xl:grid-cols-3">
873 {agentsLive.map((run) => (
874 <li key={run.id} className="min-w-0 rounded-xl border border-merged/30 bg-surface px-3.5 py-3">
875 <div className="flex items-center gap-2 text-sm">
876 <Avatar name={run.agent} size={18} />
877 <span className="font-medium">{run.agent}</span>
878 <span className="text-xs text-muted">{RUN_KIND_LABEL[run.kind].toLowerCase()}</span>
879 <span className="grow" />
880 <span className="text-xs text-muted">
881 <Elapsed from={run.startedAt ?? run.createdAt} />
Projects: what a workspace builds and runs, first on every page882 </span>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API883 </div>
884 <Link
885 to={run.number != null ? `${base}/pull/${run.number}` : `${base}/agents/runs/${run.id}`}
886 className="mt-1 block truncate text-[0.8125rem] hover:text-accent"
887 >
888 {run.title ?? "A run"} {run.number != null && <span className="text-faint">#{run.number}</span>}
889 </Link>
890 {run.step && (
891 <p className="mt-1 truncate font-mono text-[0.6875rem] text-fg/70" title={run.step}>
892 <span className="mr-1.5 inline-block size-1.5 animate-pulse rounded-full bg-merged align-middle" />
893 {run.step}
894 </p>
895 )}
896 <p className="mt-1.5 flex items-center gap-3 text-[0.6875rem] text-faint">
897 {formatCost(run.costUsd) && <span>{formatCost(run.costUsd)} so far</span>}
898 <Link to={`${base}/agents/runs/${run.id}`} className="hover:text-fg">
899 {run.stepCount} steps
900 </Link>
901 {run.number != null && (
902 <Link to={`${base}/sessions/${run.number}`} className="hover:text-fg">
903 Session
904 </Link>
905 )}
906 </p>
907 </li>
908 ))}
909 </ul>
910 )}
911 {!loaderData.pullsLoaded ? (
912 <Unavailable what="Pull requests" />
913 ) : Object.values(columns).every((c) => c.length === 0) ? (
914 <Quiet
915 action={
916 member ? (
917 <Link to={`${base}/issues/new`} className="inline-flex items-center gap-1.5 rounded-md bg-fg px-3 py-1.5 text-xs font-medium text-bg hover:bg-white">
918 <Bot size={12} /> Open an issue for an agent
919 </Link>
920 ) : null
921 }
922 >
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent923 Nothing is moving. Open an issue and hand it to g1t, or push a branch and open a pull request.
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API924 </Quiet>
925 ) : (
926 <Pipeline columns={columns} base={base} />
Projects: what a workspace builds and runs, first on every page927 )}
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API928 </Panel>
Projects: what a workspace builds and runs, first on every page929
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API930 <div className="grid gap-x-8 gap-y-8 lg:grid-cols-[minmax(0,1fr)_20rem]">
931 <div className="min-w-0 space-y-8">
932 {needs.length > 0 && (
933 <Panel title="Needs you" icon={<Hand size={14} className="text-warn" />} count={needs.length}>
934 <NeedsList needs={needs} limit={5} />
935 </Panel>
936 )}
937
Fast pages, required checks on the branch, self-hosted runners, honest incidents938 <Suspense
939 fallback={
940 <Panel title="Active branches" icon={<GitBranch size={14} />}>
941 <div aria-busy="true">
942 <SkeletonRows rows={3} rowClassName="h-12" />
943 </div>
944 </Panel>
945 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look946 >
Fast pages, required checks on the branch, self-hosted runners, honest incidents947 <Await resolve={branches}>
948 {(branches) => (
949 <Panel title="Active branches" icon={<GitBranch size={14} />} count={branches?.total || null}>
950 {!branches ? (
951 <Unavailable what="Branches" />
952 ) : branches.shown.length === 0 ? (
953 <Quiet>
954 Only {branches.main} so far. Branches pushed here show with how far each has moved from {branches.main}, and
955 its pull request and preview.
956 </Quiet>
957 ) : (
958 <>
959 <ActiveBranches branches={branches.shown} base={base} main={branches.main} />
960 {branches.total > branches.shown.length && (
961 <p className="mt-2 px-1 text-xs text-faint">
962 The {branches.shown.length} most recently changed of {branches.total} branches.
963 </p>
964 )}
965 </>
966 )}
967 </Panel>
968 )}
969 </Await>
970 </Suspense>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look971
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API972 <Panel title="Recent changes" icon={<GitMerge size={14} />} all={{ to: `${base}/pulls?state=closed`, label: "All landed" }}>
973 {landed.length === 0 ? (
974 <Quiet>Nothing has landed yet. Merged pull requests show here with who made them and why.</Quiet>
Projects: what a workspace builds and runs, first on every page975 ) : (
976 <ul className="divide-y divide-line overflow-hidden rounded-xl border border-line bg-surface">
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API977 {landed.map((pull) => {
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent978 const byAgent = pull.runtime === "hosted" || madeByG1t(pull);
Projects: what a workspace builds and runs, first on every page979 return (
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API980 <li key={pull.id} className="flex items-start gap-3 px-4 py-3">
981 <GitMerge size={15} className="mt-0.5 shrink-0 text-merged" />
982 <span className="min-w-0 grow">
983 <Link to={`${base}/pull/${pull.number}`} prefetch="intent" className="block truncate text-sm font-medium hover:text-accent">
984 {pull.title}
985 </Link>
986 <span className="mt-0.5 flex flex-wrap items-center gap-x-1.5 text-xs text-muted">
987 <span className="font-mono text-faint">#{pull.number}</span>
988 <span>·</span>
989 <span className="inline-flex items-center gap-1">
990 <Avatar name={pull.agent} size={13} />
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights991 {pull.requestedBy
992 ? `made by ${pull.author.username} for ${pull.requestedBy.username}`
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent993 : byAgent
994 ? `made by ${pull.agent}`
995 : `by ${pull.author.username}`}
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API996 </span>
997 {pull.issue != null && (
998 <>
999 <span>· for</span>
1000 <Link to={`${base}/issues/${pull.issue}`} className="hover:text-fg">
1001 #{pull.issue}
1002 </Link>
1003 </>
1004 )}
1005 {pull.mergedBy && <span>· landed by {pull.mergedBy}</span>}
1006 <span>
1007 · <TimeAgo at={pull.mergedAt ?? pull.updatedAt} />
Projects: what a workspace builds and runs, first on every page1008 </span>
1009 </span>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1010 </span>
1011 <span className="hidden shrink-0 text-xs text-faint sm:block">
1012 <ChangeSize files={pull.files} />
1013 </span>
1014 {byAgent && (
1015 <Link to={`${base}/sessions/${pull.number}`} className="shrink-0 text-xs text-muted hover:text-fg">
1016 Session
1017 </Link>
1018 )}
Projects: what a workspace builds and runs, first on every page1019 </li>
1020 );
1021 })}
1022 </ul>
1023 )}
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1024 </Panel>
Projects: what a workspace builds and runs, first on every page1025
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1026 <Panel title="Activity" icon={<ActivityIcon size={14} />}>
1027 {loaderData.eventsLoaded ? (
1028 <ActivityFeed groups={groups} showRepo={false} limit={8} empty="Nothing has happened here yet." />
1029 ) : (
1030 <Unavailable what="Activity" />
1031 )}
1032 </Panel>
1033
1034 {previews.length > 0 && (
1035 <Panel title="Previews" icon={<GitBranch size={14} />} count={previews.length} all={{ to: `${base}/deployments`, label: "Deployments" }}>
1036 <ul className="divide-y divide-line overflow-hidden rounded-xl border border-line bg-surface">
1037 {previews.map((app) => (
1038 <li key={app.url} className="flex items-center gap-3 px-4 py-2.5 text-sm">
1039 <span className="min-w-0 grow">
1040 <a href={app.url} className="block truncate font-mono text-[0.8125rem] hover:text-accent">
1041 {host(app.url)}
1042 </a>
1043 <span className="text-xs text-muted">
1044 {app.branch}
1045 {app.number != null && (
1046 <>
1047 {" · "}
1048 <Link to={`${base}/pull/${app.number}`} className="hover:underline">
1049 #{app.number}
1050 </Link>
1051 </>
1052 )}
1053 </span>
Projects: what a workspace builds and runs, first on every page1054 </span>
1055 <span className="shrink-0 text-xs text-faint">
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1056 <TimeAgo at={app.deployedAt} />
Projects: what a workspace builds and runs, first on every page1057 </span>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1058 </li>
1059 ))}
1060 </ul>
1061 </Panel>
1062 )}
1063 </div>
Projects: what a workspace builds and runs, first on every page1064
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1065 <aside className="space-y-4 text-sm">
1066 {member && (
1067 <section className="rounded-xl border border-line bg-surface p-5">
1068 <div className="flex items-center justify-between">
1069 <h2 className="flex items-center gap-1.5 text-sm font-semibold">
1070 <Brain size={14} className="text-merged" />
1071 What agents know here
1072 </h2>
1073 <Link to={`${base}/memory`} className="text-xs text-muted hover:text-fg">
1074 Memory
1075 </Link>
Projects: what a workspace builds and runs, first on every page1076 </div>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1077 {!loaderData.memoriesLoaded ? (
1078 <p className="mt-3 text-xs text-muted">Memory could not be loaded just now.</p>
1079 ) : knows.length === 0 ? (
1080 <p className="mt-3 text-xs leading-5 text-muted">
1081 Nothing yet. Agents note conventions, decisions and traps as they work, and every agent starting here reads them.
1082 </p>
1083 ) : (
1084 <ul className="mt-3 space-y-2.5">
1085 {knows.map((memory) => (
1086 <li key={memory.id} className="text-xs leading-5">
1087 <p className="line-clamp-3 text-fg-soft">
1088 {memory.pinned && <Pin size={11} className="mr-1 inline text-accent" />}
1089 {memory.text}
1090 </p>
1091 <p className="text-faint">
1092 {memory.kind} · {memory.createdBy} · <TimeAgo at={memory.createdAt} />
1093 </p>
1094 </li>
1095 ))}
1096 {loaderData.memoryCount > knows.length && (
1097 <li>
1098 <Link to={`${base}/memory`} className="text-xs text-muted hover:text-fg">
1099 {loaderData.memoryCount - knows.length} more
1100 </Link>
1101 </li>
1102 )}
1103 </ul>
1104 )}
1105 </section>
Projects: what a workspace builds and runs, first on every page1106 )}
1107
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1108 <section className="rounded-xl border border-line bg-surface p-5">
1109 <h2 className="flex items-center gap-1.5 text-sm font-semibold">
1110 <HeartPulse size={14} className="text-faint" />
1111 Health
Project dependencies: addresses, preview stacks, Affects, and agents who know1112 </h2>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1113 <div className="mt-4 space-y-4 text-xs">
1114 <div>
1115 <div className="flex items-baseline justify-between">
1116 <span className="text-muted">Checks passing</span>
1117 <span className="font-medium tabular-nums">{percent(health.passRate)}</span>
1118 </div>
1119 <div className="mt-1.5">
1120 <Meter value={health.passRate} tone={health.passRate != null && health.passRate < 0.7 ? "bg-warn" : "bg-accent"} />
1121 </div>
1122 <p className="mt-1 text-faint">
1123 {health.checkRuns
1124 ? `${health.checkRuns} recent runs · ${percent(health.firstPass.rate)} pass on the first try`
1125 : "No checks have run recently."}
1126 </p>
1127 </div>
A project is an app or a library: libraries show their package and how to ship a release, not production1128 {member && (!library || builds.length > 0) && (
Project dependencies: addresses, preview stacks, Affects, and agents who know1129 <div>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1130 <div className="flex items-baseline justify-between">
1131 <span className="text-muted">Deploys</span>
1132 <Link to={`${base}/deployments`} className="text-faint hover:text-fg">
1133 {builds.length ? `last ${Math.min(builds.length, 20)}` : "none yet"}
1134 </Link>
1135 </div>
1136 <div className="mt-1.5">
1137 <DeployStrip builds={builds} base={base} />
1138 </div>
Project dependencies: addresses, preview stacks, Affects, and agents who know1139 </div>
1140 )}
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1141 <div>
1142 <div className="flex items-baseline justify-between">
1143 <span className="text-muted">Open issues by age</span>
1144 <Link to={`${base}/issues`} className="font-medium tabular-nums hover:text-accent">
1145 {open.issues}
1146 </Link>
1147 </div>
1148 {ages ? (
1149 <ul className="mt-2 space-y-1.5">
1150 {ages.map((bucket) => (
1151 <li key={bucket.label} className="grid grid-cols-[6.5rem_1fr_1.5rem] items-center gap-2">
1152 <span className="text-faint">{bucket.label}</span>
1153 <span className="block h-1.5 overflow-hidden rounded-full bg-line">
1154 <span
1155 className={`block h-full rounded-full ${bucket.label === "Older" ? "bg-warn" : "bg-fg-soft/60"}`}
1156 style={{ width: `${(bucket.count / oldest) * 100}%` }}
1157 />
1158 </span>
1159 <span className="text-right tabular-nums text-muted">{bucket.count}</span>
Project dependencies: addresses, preview stacks, Affects, and agents who know1160 </li>
1161 ))}
1162 </ul>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1163 ) : (
1164 <p className="mt-1 text-faint">Issues could not be loaded just now.</p>
1165 )}
1166 </div>
Project dependencies: addresses, preview stacks, Affects, and agents who know1167 </div>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1168 </section>
Project dependencies: addresses, preview stacks, Affects, and agents who know1169
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1170 <section className="rounded-xl border border-line bg-surface p-5">
1171 <div className="flex items-center justify-between">
1172 <h2 className="flex items-center gap-1.5 text-sm font-semibold">
1173 <Network size={14} className="text-faint" />
1174 Dependencies
1175 </h2>
1176 {member && (
1177 <Link to={`${base}/settings/dependencies`} className="text-xs text-muted hover:text-fg">
1178 Manage
1179 </Link>
1180 )}
1181 </div>
1182 {!dependencies ? (
1183 <p className="mt-3 text-xs text-muted">Dependencies could not be loaded just now.</p>
1184 ) : dependencies.dependsOn.length === 0 && dependencies.usedBy.length === 0 ? (
1185 <p className="mt-3 text-xs leading-5 text-muted">
1186 Uses no other project, and none uses it. Declare one, and builds get its address and agents know what depends on
1187 what.
1188 </p>
1189 ) : (
1190 <div className="mt-3 space-y-3 text-xs">
1191 {dependencies.dependsOn.length > 0 && (
1192 <div>
1193 <p className="flex items-center gap-1 text-muted">
1194 <ArrowUpRight size={12} /> Uses
1195 </p>
1196 <ul className="mt-1.5 space-y-1">
1197 {dependencies.dependsOn.map((d) => (
1198 <li key={d.slug} className="flex items-center justify-between gap-2">
1199 <Link to={`/${params.owner}/${d.slug}`} className="font-medium hover:underline">
1200 {d.name}
1201 </Link>
1202 {d.as && <code className="font-mono text-faint">{d.as}</code>}
1203 </li>
1204 ))}
1205 </ul>
1206 </div>
1207 )}
1208 {dependencies.usedBy.length > 0 && (
1209 <div>
1210 <p className="flex items-center gap-1 text-muted">
1211 <ArrowDownLeft size={12} /> Used by
1212 </p>
1213 <ul className="mt-1.5 space-y-1">
1214 {dependencies.usedBy.map((d) => (
1215 <li key={d.slug}>
1216 <Link to={`/${params.owner}/${d.slug}`} className="font-medium hover:underline">
1217 {d.name}
1218 </Link>
1219 </li>
1220 ))}
1221 </ul>
1222 </div>
1223 )}
1224 </div>
1225 )}
Projects: what a workspace builds and runs, first on every page1226 </section>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1227
1228 {source && (
1229 <section className="rounded-xl border border-line bg-surface p-5">
1230 <h2 className="text-sm font-semibold">Clone</h2>
1231 <div className="mt-3">
1232 <CopyLine text={`git clone https://g1t.sh/${source.repo.namespace}/${source.repo.name}.git`} />
1233 </div>
1234 </section>
1235 )}
1236 </aside>
1237 </div>
Projects: what a workspace builds and runs, first on every page1238 </div>
1239 );
1240}