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

1,018 lines47,477 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,
17 Pin,
18 Rocket,
19 RotateCw,
20} from "lucide-react";
Fast pages, required checks on the branch, self-hosted runners, honest incidents21import { type ReactNode, Suspense } from "react";
22import { Await, Form, Link, useNavigation } from "react-router";
Projects: what a workspace builds and runs, first on every page23
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API24import { type AgentRun, type G1tEvent, type Memory, type Pull, RUN_KIND_LABEL, isActiveRun } from "@g1t/contracts";
25
Projects: what a workspace builds and runs, first on every page26import type { Route } from "./+types/overview";
27import { host, StatusDot } from "../../components/deploy";
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API28import { CheckBadge } from "../../components/checks";
29import { Elapsed, formatCost, useLiveRefresh } from "../../components/agents";
30import { 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 look31import { type ActiveBranch, ActiveBranches } from "../../components/branches";
32import { ProductionChecklist } from "../../components/checklist";
Fast pages, required checks on the branch, self-hosted runners, honest incidents33import { SkeletonRows } from "../../components/ui/skeleton";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look34import { ProductionShot } from "../../components/production-shot";
35import { GithubLinkStrip } from "../../components/github";
36import { githubApp } from "../../lib/github.server";
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API37import { Avatar, Button, ButtonLink, CopyLine, TimeAgo } from "../../components/ui";
38import { ChangeSize } from "../../components/work";
39import {
40 type ActivityItem,
41 type Need,
42 type PipelineStage,
43 PIPELINE,
44 TIME,
45 ageBuckets,
46 eventItem,
47 firstPassRate,
48 groupActivity,
49 passRate,
50 pipelineStage,
51 queuedNumbers,
52 rankNeeds,
53 stuckMinutes,
54} from "../../lib/mission";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look55import { drift } from "../../lib/branches";
56import { agentWasAssigned, hasInstructions, productionChecklist } from "../../lib/checklist";
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API57import { agents, deployments, events as eventLog, projects, repos, work } from "../../lib/services.server";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look58import { assertSameOrigin, getViewer, requireUser } from "../../lib/session.server";
Fast pages, required checks on the branch, self-hosted runners, honest incidents59import { accessTo, countsFor, refusal, repoFor } from "../../lib/access.server";
Projects: what a workspace builds and runs, first on every page60
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API61const MAX_LANDED = 6;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look62/** Branches read for the Active branches list, and shown. */
63const BRANCHES_READ = 10;
64const BRANCHES_SHOWN = 5;
65/** How far back each branch's history, and the default branch's, is read to count ahead and behind. */
66const BRANCH_DEPTH = 40;
67const MAIN_DEPTH = 120;
Projects: what a workspace builds and runs, first on every page68
69export async function loader({ params, context }: Route.LoaderArgs) {
70 const viewer = getViewer(context);
71 const path = { namespace: params.owner, name: params.repo };
72 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 API73 const now = Date.now();
74 // A section whose service fails shows its own empty state; the page stays up.
75 const soft = <T,>(promise: Promise<T> | null): Promise<T | null> =>
76 promise ? promise.catch((error) => (console.warn("overview:", error), null)) : Promise.resolve(null);
Fast pages, required checks on the branch, self-hosted runners, honest incidents77 // People with a role of their own here see its running parts: deployments,
78 // memory, setup; everyone who can read it sees the rest. Not awaited: only
79 // those sections wait on the repository lookup, which the layout makes in
80 // this request too.
81 const accessP = accessTo(context, params);
82 const memberP = accessP.then((access) => access.insider);
83 const forMembers = <T,>(start: () => Promise<T>): Promise<T | null> =>
84 memberP.then((member) => (member ? soft(start()) : null));
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look85 const repoP = soft(repoFor(context, params));
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API86 const eventsP = repoP.then((repo) => (repo?.ok ? soft(eventLog.list({ repoId: repo.value.id, limit: 150 })) : null));
87 const minePullsP = repoP.then((repo) =>
88 repo?.ok && viewer
89 ? soft(work.listActivePulls(viewer)).then((list) => (list ?? []).filter((item) => item.pull.repoId === repo.value.id))
90 : null,
91 );
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look92 const openP = soft(work.listPulls(path, viewer, "open"));
93 // Where its code came from on GitHub, for members.
Fast pages, required checks on the branch, self-hosted runners, honest incidents94 const githubP = repoP.then((repo) => (repo?.ok ? forMembers(() => githubApp.link(repo.value.id)) : null));
95 const listP = forMembers(() => deployments.list(ref, viewer));
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look96 // Active branches: the newest few besides the default, with how far each
97 // has moved. Branches with an open pull request are read first.
98 const branchesP = Promise.all([repoP, soft(repos.branches(path, viewer)), openP, listP]).then(
99 async ([repo, branchList, pulls, deploys]): Promise<{ main: string; total: number; shown: ActiveBranch[] } | null> => {
100 if (!repo?.ok || !branchList?.ok) return null;
101 const main = repo.value.defaultBranch;
102 const pullList = pulls?.ok ? pulls.value : [];
103 const pullOn = new Map(pullList.filter((pull) => pull.branch).map((pull) => [pull.branch as string, pull]));
104 const others = branchList.value.filter((branch) => branch.name !== main);
105 if (others.length === 0) return { main, total: 0, shown: [] };
106 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 incidents107 // By commit hash, not name: history from a commit never changes, so
108 // repos keeps it (services/repos/src/store.rs) and only new heads cost
109 // a walk.
110 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 look111 const [mainLog, ...logs] = await Promise.all([
Fast pages, required checks on the branch, self-hosted runners, honest incidents112 soft(repos.log(path, viewer, mainHead, MAIN_DEPTH)),
113 ...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 look114 ]);
115 const mainHashes = mainLog?.ok ? mainLog.value.map((c) => c.hash) : [];
116 const previews = deploys?.ok ? deploys.value.live.filter((app) => app.kind === "preview") : [];
117 const shown = read
118 .map((branch, index): ActiveBranch => {
119 const history = logs[index]?.ok ? logs[index].value : [];
120 const head = history[0];
121 const moved = drift(history.map((c) => c.hash), mainHashes, BRANCH_DEPTH);
122 const pull = pullOn.get(branch.name);
123 return {
124 name: branch.name,
125 commit: head ? { hash: head.hash, message: head.message.split("\n")[0], author: head.author.name, at: head.authoredAt } : null,
126 ...moved,
127 pull: pull ? { number: pull.number, title: pull.title, checkStatus: pull.checkStatus, draft: pull.status === "draft" } : null,
128 preview: previews.find((app) => app.branch === branch.name || (pull != null && app.number === pull.number))?.url ?? null,
129 };
130 })
131 .sort((a, b) => Date.parse(b.commit?.at ?? "0") - Date.parse(a.commit?.at ?? "0"))
132 .slice(0, BRANCHES_SHOWN);
133 return { main, total: others.length, shown };
134 },
135 );
Fast pages, required checks on the branch, self-hosted runners, honest incidents136 // Active branches read several logs each: streamed, so the rest shows first.
137 const branches = branchesP.catch(() => null);
138 const [{ insider: member, can }, project, settings, list, open, closed, log, counts, deps, runs, queue, issues, memories, recent, mine, domains, root] = await Promise.all([
139 accessP,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API140 soft(projects.get(params.owner, params.repo, viewer)),
Fast pages, required checks on the branch, self-hosted runners, honest incidents141 forMembers(() => deployments.settings(ref, viewer)),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look142 listP,
143 openP,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API144 soft(work.listPulls(path, viewer, "closed")),
145 soft(repos.log(path, viewer, null, 1)),
Fast pages, required checks on the branch, self-hosted runners, honest incidents146 soft(countsFor(context, params)),
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API147 soft(projects.dependencies(params.owner, params.repo, viewer)),
148 soft(agents.listRuns(viewer, { repo: path, limit: 60 })),
149 soft(work.queue(path, viewer)),
150 soft(work.listIssues(path, viewer, { state: "open" })),
Fast pages, required checks on the branch, self-hosted runners, honest incidents151 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 API152 eventsP,
153 minePullsP,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look154 // For the checklist, which only members see.
Fast pages, required checks on the branch, self-hosted runners, honest incidents155 forMembers(() => deployments.domains(ref, viewer)),
156 forMembers(() => repos.tree(path, viewer, null, "")),
Projects: what a workspace builds and runs, first on every page157 ]);
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API158 const ok = <T,>(result: { ok: true; value: T } | { ok: false } | null): T | null => (result?.ok ? result.value : null);
159
160 const openPulls = ok(open) ?? [];
161 const runList = ok(runs) ?? [];
162 const live = runList.filter((run) => isActiveRun(run.status));
163 const builds = ok(list)?.deployments ?? [];
164 const landed = (ok(closed) ?? [])
165 .filter((pull) => pull.status === "merged")
166 .sort((a, b) => Date.parse(b.mergedAt ?? b.updatedAt) - Date.parse(a.mergedAt ?? a.updatedAt));
167 const queued = queuedNumbers(ok(queue)?.active ?? []);
168 const runOn = new Map<number, AgentRun>();
169 for (const run of live) if (run.number != null && !runOn.has(run.number)) runOn.set(run.number, run);
170 const lifecycleOf = new Map((mine ?? []).map((item) => [item.pull.number, item.lifecycle]));
171
172 // --- The pipeline -----------------------------------------------------------
173 type Card = {
174 number: number;
175 title: string;
176 agent: string;
177 status: Pull["status"];
178 checkStatus: Pull["checkStatus"];
179 step: string | null;
180 runKind: string | null;
181 runStarted: string | null;
182 needsYou: boolean;
183 at: string;
184 };
185 const columns: Record<PipelineStage, Card[]> = { working: [], checking: [], reviewing: [], queue: [], landed: [] };
186 for (const pull of openPulls.slice(0, 40)) {
187 const run = runOn.get(pull.number);
Fast pages, required checks on the branch, self-hosted runners, honest incidents188 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 API189 number: pull.number,
190 title: pull.title,
191 agent: pull.agent,
192 status: pull.status,
193 checkStatus: pull.checkStatus,
194 step: run?.step ?? null,
195 runKind: run ? RUN_KIND_LABEL[run.kind] : null,
196 runStarted: run ? (run.startedAt ?? run.createdAt) : null,
197 needsYou: lifecycleOf.get(pull.number)?.stage === "needs_you",
198 at: pull.updatedAt,
199 });
200 }
201 for (const pull of landed.filter((p) => now - Date.parse(p.mergedAt ?? p.updatedAt) < 3 * TIME.DAY).slice(0, 5)) {
202 columns.landed.push({
203 number: pull.number,
204 title: pull.title,
205 agent: pull.agent,
206 status: pull.status,
207 checkStatus: pull.checkStatus,
208 step: null,
209 runKind: null,
210 runStarted: null,
211 needsYou: false,
212 at: pull.mergedAt ?? pull.updatedAt,
213 });
214 }
215
216 // --- Needs you ---------------------------------------------------------------
217 const base = `/${params.owner}/${params.repo}`;
218 const needs: Need[] = [];
219 const latestProduction = builds.find((build) => build.kind === "production") ?? null;
220 if (latestProduction?.status === "failed") {
221 needs.push({
222 key: "deploy",
223 kind: "deploy",
224 title: "The production build failed",
225 detail: latestProduction.error ?? "Production still serves the build before it.",
226 to: `${base}/deployments/${latestProduction.id}`,
227 action: "See the build",
228 at: Date.parse(latestProduction.finishedAt ?? latestProduction.createdAt),
229 where: latestProduction.commit.slice(0, 7),
230 });
231 }
232 const me = viewer?.username.toLowerCase();
233 for (const pull of openPulls) {
234 const to = `${base}/pull/${pull.number}`;
235 const where = `#${pull.number}`;
236 const lifecycle = lifecycleOf.get(pull.number);
237 if (lifecycle?.stage === "needs_you") {
238 const conflict = /conflict/i.test(lifecycle.detail);
239 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 });
240 } else if (lifecycle?.stage === "ready") {
241 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 });
242 }
243 if (me && pull.status === "open" && pull.author.username.toLowerCase() !== me && pull.reviewers.some((name) => name.toLowerCase() === me)) {
244 needs.push({ key: `review:${pull.number}`, kind: "review", title: pull.title, detail: `${pull.author.username} asked for your review.`, to, action: "Review", at: Date.parse(pull.updatedAt), where });
245 }
246 if (member && pull.status === "open" && pull.checkStatus === "failed" && !runOn.has(pull.number) && !lifecycle) {
Fast pages, required checks on the branch, self-hosted runners, honest incidents247 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 API248 }
249 }
250 for (const run of live) {
251 const minutes = stuckMinutes(run, now);
252 if (minutes == null) continue;
253 needs.push({
254 key: `run:${run.id}`,
255 kind: "stuck",
256 title: run.title ?? `${run.agent}'s run`,
257 detail: `${run.agent} has reported nothing for ${minutes} min${run.step ? `. Last: ${run.step}` : ""}.`,
258 to: `${base}/agents/runs/${run.id}`,
259 action: "Look",
260 at: Date.parse(run.updatedAt),
261 where: run.number != null ? `#${run.number}` : null,
262 });
263 }
264
265 // --- Activity and health -------------------------------------------------------
266 const eventList: G1tEvent[] = recent ?? [];
267 const items = eventList
268 .map((event) => eventItem(event, path))
269 .filter((item): item is ActivityItem => item != null);
270 const checkEvents = eventList.flatMap((event) =>
271 event.type === "checks.completed" ? [{ repo: "", number: event.data.number, at: Date.parse(event.time), passed: event.data.status === "passed" }] : [],
272 );
273 const memoryList = ok(memories)?.project ?? [];
274 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);
275 const openIssues = ok(issues);
276
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look277 // --- Getting to production ----------------------------------------------------
278 const wentLive = (status: string) => status === "ready" || status === "replaced" || status === "down";
279 const liveApps = ok(list)?.live ?? [];
280 const commitNow = ok(log)?.[0] ?? null;
281 const projectValue = ok(project);
282 const checklist = member
283 ? productionChecklist({
284 base,
285 hasCode: commitNow != null || projectValue?.source.kind === "mirror",
286 deploysEnabled: ok(settings)?.enabled ?? false,
287 productionDeployed:
288 liveApps.some((app) => app.kind === "production") ||
289 builds.some((build) => build.kind === "production" && wentLive(build.status)),
290 domains: ok(domains)?.domains.length ?? null,
291 previewOpened:
292 liveApps.some((app) => app.kind === "preview") || builds.some((build) => build.kind === "preview" && wentLive(build.status)),
293 instructions: commitNow == null ? false : root?.ok ? hasInstructions(root.value.entries.map((entry) => entry.name)) : null,
294 agentAssigned: agentWasAssigned({
295 runAgents: runList.map((run) => run.agent),
296 pullAgents: [...openPulls, ...(ok(closed) ?? [])].map((pull) => pull.agent),
297 issues: openIssues ?? [],
298 }),
299 })
300 : null;
301
302 const github = await githubP;
Projects: what a workspace builds and runs, first on every page303 return {
304 member,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look305 can,
306 github,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API307 project: ok(project),
308 settings: ok(settings),
309 builds: builds.slice(0, 30),
310 live: ok(list)?.live ?? [],
311 commit: ok(log)?.[0] ?? null,
312 open: ok(counts) ?? { issues: openIssues?.length ?? 0, pulls: openPulls.length },
313 dependencies: ok(deps),
314 agentsLive: live.slice(0, 6),
315 runsLoaded: runs?.ok ?? false,
316 pullsLoaded: open?.ok ?? false,
317 columns,
318 needs: rankNeeds(needs),
319 landed: landed.slice(0, MAX_LANDED),
320 groups: groupActivity(items).slice(0, 30),
321 eventsLoaded: recent != null,
322 health: {
323 passRate: passRate(checkEvents.map((c) => c.passed)),
324 firstPass: firstPassRate(checkEvents),
325 checkRuns: checkEvents.length,
326 ages: openIssues ? ageBuckets(openIssues.map((issue) => issue.createdAt), now) : null,
327 },
328 knows: knows as Memory[],
329 memoryCount: memoryList.length,
330 memoriesLoaded: memories?.ok ?? false,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look331 checklist,
332 branches,
Projects: what a workspace builds and runs, first on every page333 };
334}
335
336export async function action({ request, params, context }: Route.ActionArgs) {
337 assertSameOrigin(request);
338 const user = requireUser(context, request);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look339 const form = await request.formData().catch(() => null);
340 const intent = form?.get("intent");
341 // Syncing from GitHub is pushing; stopping it is an integration; deploying is compute.
342 const refused = await refusal(context, params, intent === "github-sync" ? "push" : intent === "github-stop" ? "manage_integrations" : "run");
343 if (refused) return { error: refused };
344 if (intent === "github-sync" || intent === "github-stop") {
345 const repo = await repos.get({ namespace: params.owner, name: params.repo }, user);
346 if (!repo.ok) return { error: repo.error.message };
347 const done =
348 intent === "github-sync" ? await githubApp.sync(user, repo.value.id) : await githubApp.unlinkRepo(user, repo.value.id);
349 return done.ok
350 ? { notice: intent === "github-sync" ? "Synced with GitHub." : "It is no longer kept in step with GitHub." }
351 : { error: done.error.message };
352 }
Projects: what a workspace builds and runs, first on every page353 const started = await deployments.redeploy(user, { workspace: params.owner, slug: params.repo }, null);
354 return started.ok ? { notice: "Production is building." } : { error: started.error.message };
355}
356
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API357type Loaded = Route.ComponentProps["loaderData"];
358type Card = Loaded["columns"]["working"][number];
359
360const STAGE_TONE: Record<PipelineStage, string> = {
361 working: "bg-merged",
362 checking: "bg-info",
363 reviewing: "bg-warn",
364 queue: "bg-accent-dim",
365 landed: "bg-accent",
366};
367
368function PipelineCard({ card, base, stage }: { card: Card; base: string; stage: PipelineStage }) {
369 return (
370 <li>
371 <Link
372 to={`${base}/pull/${card.number}`}
373 prefetch="intent"
374 className={`block rounded-lg border bg-bg/60 p-2.5 transition-colors hover:border-line-strong hover:bg-raised ${
375 card.needsYou ? "border-warn/50" : card.step ? "border-merged/30" : "border-line"
376 }`}
377 >
378 <span className="line-clamp-2 text-[0.8125rem] leading-snug font-medium">{card.title}</span>
379 <span className="mt-1.5 flex items-center gap-1.5 text-[0.6875rem] text-muted">
380 <Avatar name={card.agent} size={14} />
381 <span className="font-mono text-faint">#{card.number}</span>
382 <span className="grow" />
383 {card.needsYou ? (
384 <span className="inline-flex items-center gap-1 text-warn" title="Stopped: a person decides what happens next">
385 <Hand size={11} /> you
386 </span>
387 ) : stage === "landed" ? (
388 <TimeAgo at={card.at} />
389 ) : card.runStarted ? (
390 <span className="inline-flex items-center gap-1 text-merged">
391 <Loader2 size={11} className="animate-spin" />
392 <Elapsed from={card.runStarted} />
393 </span>
394 ) : (
395 <CheckBadge status={card.checkStatus} />
396 )}
397 </span>
398 {card.step && (
399 <span className="mt-1.5 block truncate font-mono text-[0.6875rem] text-fg/70" title={card.step}>
400 {card.runKind}: {card.step}
401 </span>
402 )}
403 </Link>
404 </li>
405 );
406}
407
408/** Open pull requests moving through Working → Checking → Reviewing → Queue → Landed. */
409function Pipeline({ columns, base }: { columns: Loaded["columns"]; base: string }) {
410 return (
411 <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 look412 {/* Each stage as tall as what is in it: an empty one stays short
413 instead of stretching to the busiest. */}
414 <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 API415 {PIPELINE.map(({ stage, label }, index) => {
416 const cards = columns[stage];
417 return (
418 <li key={stage} className="flex min-w-0 flex-col rounded-xl border border-line bg-surface p-2">
419 <p className="flex items-center gap-2 px-1 pt-0.5 pb-2 text-xs font-medium text-muted">
420 <span className={`size-1.5 rounded-full ${STAGE_TONE[stage]} ${cards.length && stage !== "landed" ? "animate-pulse" : ""}`} />
421 {label}
422 <span className="tabular-nums text-faint">{cards.length}</span>
423 {index < PIPELINE.length - 1 && <ArrowRight size={11} className="ml-auto text-line-strong" />}
424 </p>
425 {cards.length === 0 ? (
426 <p className="rounded-lg border border-dashed border-line/70 px-2 py-3 text-center text-[0.6875rem] text-faint">Empty</p>
427 ) : (
428 <ul className="space-y-1.5">
429 {cards.slice(0, 6).map((card) => (
430 <PipelineCard key={card.number} card={card} base={base} stage={stage} />
431 ))}
432 {cards.length > 6 && (
433 <li className="px-1 text-[0.6875rem] text-muted">
434 <Link to={`${base}/pulls`} className="hover:text-fg">
435 {cards.length - 6} more
436 </Link>
437 </li>
438 )}
439 </ul>
440 )}
441 </li>
442 );
443 })}
444 </ol>
445 </div>
446 );
447}
448
449function Stat({ label, value, to }: { label: string; value: ReactNode; to?: string }) {
450 const body = (
451 <>
452 <span className="block text-[0.6875rem] text-faint">{label}</span>
453 <span className="mt-0.5 block truncate text-[0.8125rem] font-medium">{value}</span>
454 </>
455 );
456 return to ? (
457 <Link to={to} className="min-w-0 px-5 py-3 transition-colors hover:bg-raised/50">
458 {body}
459 </Link>
460 ) : (
461 <div className="min-w-0 px-5 py-3">{body}</div>
462 );
463}
464
Projects: what a workspace builds and runs, first on every page465export default function ProjectOverview({ loaderData, actionData, params }: Route.ComponentProps) {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look466 const { member, project, settings, builds, live, commit, open, dependencies, agentsLive, columns, needs, landed, groups, health, knows, checklist, branches } =
467 loaderData;
Projects: what a workspace builds and runs, first on every page468 const base = `/${params.owner}/${params.repo}`;
469 const production = live.find((app) => app.kind === "production") ?? null;
Agents and memory, checks and conflicts, profiles, slug renames, custom domains470 // The project's own domain, once it is active, is where production is visited.
471 const productionUrl = production ? (settings?.primaryDomain ? `https://${settings.primaryDomain}` : production.url) : null;
Projects: what a workspace builds and runs, first on every page472 const previews = live.filter((app) => app.kind === "preview");
473 const latestProduction = builds.find((build) => build.kind === "production") ?? null;
474 const busy = useNavigation().state === "submitting";
475 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 API476 const moving = agentsLive.length > 0 || columns.working.length + columns.checking.length > 0;
477 useLiveRefresh(moving);
478 const ages = health.ages;
479 const oldest = ages ? Math.max(1, ...ages.map((a) => a.count)) : 1;
Projects: what a workspace builds and runs, first on every page480
481 return (
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API482 <div className="space-y-8">
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look483 {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 API484 {/* What the project is, running, and where its code is. */}
485 <section className="overflow-hidden rounded-2xl border border-line bg-surface">
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look486 <div className="flex flex-col gap-5 p-5 sm:flex-row sm:items-start sm:p-6">
487 {member && (production || settings?.enabled) && (
488 <ProductionShot
489 src={production ? `${base}/production.jpg?v=${production.commit}` : null}
490 href={production ? (productionUrl ?? production.url) : null}
491 label={host(production ? (productionUrl ?? production.url) : (settings?.productionUrl ?? ""))}
492 className="w-full shrink-0 sm:w-60 lg:w-72"
493 />
494 )}
495 <div className="flex min-w-0 grow flex-wrap items-start justify-between gap-4">
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API496 <div className="min-w-0">
497 <p className="flex items-center gap-2 text-xs font-medium tracking-wide text-muted uppercase">
498 <Rocket size={13} className="text-accent" />
499 Production
500 </p>
501 {production ? (
502 <>
503 <a
504 href={productionUrl ?? production.url}
505 className="mt-2 flex items-center gap-1.5 truncate font-mono text-lg font-medium hover:text-accent"
506 >
507 {host(productionUrl ?? production.url)}
508 <ArrowUpRight size={16} className="shrink-0 text-faint" />
509 </a>
510 <p className="mt-1.5 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted">
511 {latestProduction && <StatusDot status={latestProduction.status} label={latestProduction.status === "ready" ? "Live" : undefined} />}
512 <span className="inline-flex items-center gap-1 font-mono">
513 <GitCommitHorizontal size={13} className="text-faint" />
514 {production.commit.slice(0, 7)}
515 </span>
516 <span>
517 deployed <TimeAgo at={production.deployedAt} />
518 </span>
519 {settings?.primaryDomain && <span className="text-faint">also at {host(production.url)}</span>}
520 </p>
521 </>
522 ) : settings?.enabled ? (
523 <>
524 <p className="mt-2 font-mono text-lg text-muted">{host(settings.productionUrl)}</p>
525 <p className="mt-1.5 text-xs text-muted">
526 {latestProduction ? (
527 <StatusDot status={latestProduction.status} label={latestProduction.status === "failed" ? "The last build failed" : undefined} />
528 ) : (
529 "Not deployed yet. Push to the default branch, or deploy it now."
530 )}
531 </p>
532 </>
533 ) : (
534 <>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look535 <p className="mt-2 text-lg font-medium">Deployments are off</p>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API536 <p className="mt-1 max-w-lg text-sm text-muted">
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look537 Nothing builds or runs until you turn them on. Then {project?.name ?? params.repo} goes up on g1t.page:
538 production from the default branch, and a live preview for every pull request. It runs only while
539 someone visits.
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API540 </p>
541 </>
542 )}
543 </div>
544 {member && (
545 <div className="flex shrink-0 items-center gap-2">
546 {production && (
547 <ButtonLink to={productionUrl ?? production.url} variant="accent" reloadDocument>
548 Visit
549 <ArrowUpRight size={14} />
550 </ButtonLink>
551 )}
552 {settings?.enabled ? (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look553 loaderData.can.run && <Form method="post">
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API554 <Button type="submit" variant="quiet" disabled={busy} title="Build production again from the default branch">
555 <RotateCw size={14} />
556 Redeploy
557 </Button>
558 </Form>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look559 ) : loaderData.can.manage_integrations && (
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API560 <ButtonLink to={`${base}/settings/deployments`} variant="accent">
561 <Rocket size={14} />
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look562 Turn on deployments
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API563 </ButtonLink>
Projects: what a workspace builds and runs, first on every page564 )}
565 </div>
566 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look567 </div>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API568 </div>
569 {actionData && (
570 <p className={`border-t border-line px-6 py-2.5 text-sm ${"error" in actionData ? "text-danger" : "text-accent"}`}>
571 {"error" in actionData ? actionData.error : actionData.notice}
572 </p>
573 )}
574 <div className="grid grid-cols-2 divide-line border-t border-line text-sm sm:grid-cols-4 sm:divide-x">
575 <Stat
576 label="Source"
577 to={`${base}/code`}
578 value={
579 source ? (
580 <span className="inline-flex items-center gap-1.5 font-mono">
581 {project?.private ? <Lock size={12} className="text-faint" /> : <Code2 size={12} className="text-faint" />}
582 {source.repo.name}
583 {source.rootDir ? `/${source.rootDir}` : ""}
584 </span>
585 ) : (
586 "—"
587 )
588 }
589 />
590 <Stat
591 label="Default branch"
592 to={`${base}/commits`}
593 value={
594 <span className="inline-flex items-center gap-1.5 font-mono">
595 <GitBranch size={12} className="text-faint" />
596 {source?.defaultBranch ?? "main"}
597 </span>
598 }
599 />
600 <Stat
601 label="Latest commit"
602 to={commit ? `${base}/commit/${commit.hash}` : undefined}
603 value={
604 commit ? (
605 <span title={commit.message.split("\n")[0]}>
606 <span className="font-mono">{commit.hash.slice(0, 7)}</span>{" "}
607 <span className="font-normal text-muted">
608 <TimeAgo at={commit.authoredAt} />
609 </span>
610 </span>
611 ) : (
612 "No commits yet"
613 )
614 }
615 />
616 <Stat
617 label={member && settings?.enabled ? "Last deploy" : "Open"}
618 to={member && settings?.enabled ? `${base}/deployments` : `${base}/pulls`}
619 value={
620 member && settings?.enabled ? (
621 latestProduction ? (
622 <span className="inline-flex items-center gap-2">
623 <StatusDot status={latestProduction.status} />
624 <span className="font-normal text-muted">
625 <TimeAgo at={latestProduction.createdAt} />
Projects: what a workspace builds and runs, first on every page626 </span>
627 </span>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API628 ) : (
629 "None yet"
630 )
631 ) : (
632 `${open.pulls} pull requests · ${open.issues} issues`
633 )
634 }
635 />
636 </div>
637 {commit && (
638 <p className="truncate border-t border-line px-5 py-2.5 text-xs text-muted sm:px-6">
639 <GitCommitHorizontal size={12} className="mr-1.5 inline text-faint" />
640 {commit.message.split("\n")[0]} <span className="text-faint">· {commit.author.name}</span>
641 </p>
642 )}
643 </section>
644
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look645 {checklist && <ProductionChecklist base={base} items={checklist} />}
646
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API647 <Panel
648 title="Right now"
649 icon={<span className={`block size-2 rounded-full ${agentsLive.length > 0 ? "animate-pulse bg-merged" : "bg-line-strong"}`} />}
650 count={agentsLive.length}
651 all={{ to: `${base}/agents`, label: "Agents" }}
652 >
653 {agentsLive.length > 0 && (
654 <ul className="mb-3 grid grid-cols-1 gap-2 sm:grid-cols-2 xl:grid-cols-3">
655 {agentsLive.map((run) => (
656 <li key={run.id} className="min-w-0 rounded-xl border border-merged/30 bg-surface px-3.5 py-3">
657 <div className="flex items-center gap-2 text-sm">
658 <Avatar name={run.agent} size={18} />
659 <span className="font-medium">{run.agent}</span>
660 <span className="text-xs text-muted">{RUN_KIND_LABEL[run.kind].toLowerCase()}</span>
661 <span className="grow" />
662 <span className="text-xs text-muted">
663 <Elapsed from={run.startedAt ?? run.createdAt} />
Projects: what a workspace builds and runs, first on every page664 </span>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API665 </div>
666 <Link
667 to={run.number != null ? `${base}/pull/${run.number}` : `${base}/agents/runs/${run.id}`}
668 className="mt-1 block truncate text-[0.8125rem] hover:text-accent"
669 >
670 {run.title ?? "A run"} {run.number != null && <span className="text-faint">#{run.number}</span>}
671 </Link>
672 {run.step && (
673 <p className="mt-1 truncate font-mono text-[0.6875rem] text-fg/70" title={run.step}>
674 <span className="mr-1.5 inline-block size-1.5 animate-pulse rounded-full bg-merged align-middle" />
675 {run.step}
676 </p>
677 )}
678 <p className="mt-1.5 flex items-center gap-3 text-[0.6875rem] text-faint">
679 {formatCost(run.costUsd) && <span>{formatCost(run.costUsd)} so far</span>}
680 <Link to={`${base}/agents/runs/${run.id}`} className="hover:text-fg">
681 {run.stepCount} steps
682 </Link>
683 {run.number != null && (
684 <Link to={`${base}/sessions/${run.number}`} className="hover:text-fg">
685 Session
686 </Link>
687 )}
688 </p>
689 </li>
690 ))}
691 </ul>
692 )}
693 {!loaderData.pullsLoaded ? (
694 <Unavailable what="Pull requests" />
695 ) : Object.values(columns).every((c) => c.length === 0) ? (
696 <Quiet
697 action={
698 member ? (
699 <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">
700 <Bot size={12} /> Open an issue for an agent
701 </Link>
702 ) : null
703 }
704 >
705 Nothing is moving. Open an issue and hand it to g1t-agent, or push a branch and open a pull request.
706 </Quiet>
707 ) : (
708 <Pipeline columns={columns} base={base} />
Projects: what a workspace builds and runs, first on every page709 )}
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API710 </Panel>
Projects: what a workspace builds and runs, first on every page711
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API712 <div className="grid gap-x-8 gap-y-8 lg:grid-cols-[minmax(0,1fr)_20rem]">
713 <div className="min-w-0 space-y-8">
714 {needs.length > 0 && (
715 <Panel title="Needs you" icon={<Hand size={14} className="text-warn" />} count={needs.length}>
716 <NeedsList needs={needs} limit={5} />
717 </Panel>
718 )}
719
Fast pages, required checks on the branch, self-hosted runners, honest incidents720 <Suspense
721 fallback={
722 <Panel title="Active branches" icon={<GitBranch size={14} />}>
723 <div aria-busy="true">
724 <SkeletonRows rows={3} rowClassName="h-12" />
725 </div>
726 </Panel>
727 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look728 >
Fast pages, required checks on the branch, self-hosted runners, honest incidents729 <Await resolve={branches}>
730 {(branches) => (
731 <Panel title="Active branches" icon={<GitBranch size={14} />} count={branches?.total || null}>
732 {!branches ? (
733 <Unavailable what="Branches" />
734 ) : branches.shown.length === 0 ? (
735 <Quiet>
736 Only {branches.main} so far. Branches pushed here show with how far each has moved from {branches.main}, and
737 its pull request and preview.
738 </Quiet>
739 ) : (
740 <>
741 <ActiveBranches branches={branches.shown} base={base} main={branches.main} />
742 {branches.total > branches.shown.length && (
743 <p className="mt-2 px-1 text-xs text-faint">
744 The {branches.shown.length} most recently changed of {branches.total} branches.
745 </p>
746 )}
747 </>
748 )}
749 </Panel>
750 )}
751 </Await>
752 </Suspense>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look753
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API754 <Panel title="Recent changes" icon={<GitMerge size={14} />} all={{ to: `${base}/pulls?state=closed`, label: "All landed" }}>
755 {landed.length === 0 ? (
756 <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 page757 ) : (
758 <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 API759 {landed.map((pull) => {
760 const byAgent = pull.runtime === "hosted" || pull.agent === "g1t-agent";
Projects: what a workspace builds and runs, first on every page761 return (
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API762 <li key={pull.id} className="flex items-start gap-3 px-4 py-3">
763 <GitMerge size={15} className="mt-0.5 shrink-0 text-merged" />
764 <span className="min-w-0 grow">
765 <Link to={`${base}/pull/${pull.number}`} prefetch="intent" className="block truncate text-sm font-medium hover:text-accent">
766 {pull.title}
767 </Link>
768 <span className="mt-0.5 flex flex-wrap items-center gap-x-1.5 text-xs text-muted">
769 <span className="font-mono text-faint">#{pull.number}</span>
770 <span>·</span>
771 <span className="inline-flex items-center gap-1">
772 <Avatar name={pull.agent} size={13} />
773 {byAgent ? `made by ${pull.agent}` : `by ${pull.author.username}`}
774 </span>
775 {pull.issue != null && (
776 <>
777 <span>· for</span>
778 <Link to={`${base}/issues/${pull.issue}`} className="hover:text-fg">
779 #{pull.issue}
780 </Link>
781 </>
782 )}
783 {pull.mergedBy && <span>· landed by {pull.mergedBy}</span>}
784 <span>
785 · <TimeAgo at={pull.mergedAt ?? pull.updatedAt} />
Projects: what a workspace builds and runs, first on every page786 </span>
787 </span>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API788 </span>
789 <span className="hidden shrink-0 text-xs text-faint sm:block">
790 <ChangeSize files={pull.files} />
791 </span>
792 {byAgent && (
793 <Link to={`${base}/sessions/${pull.number}`} className="shrink-0 text-xs text-muted hover:text-fg">
794 Session
795 </Link>
796 )}
Projects: what a workspace builds and runs, first on every page797 </li>
798 );
799 })}
800 </ul>
801 )}
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API802 </Panel>
Projects: what a workspace builds and runs, first on every page803
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API804 <Panel title="Activity" icon={<ActivityIcon size={14} />}>
805 {loaderData.eventsLoaded ? (
806 <ActivityFeed groups={groups} showRepo={false} limit={8} empty="Nothing has happened here yet." />
807 ) : (
808 <Unavailable what="Activity" />
809 )}
810 </Panel>
811
812 {previews.length > 0 && (
813 <Panel title="Previews" icon={<GitBranch size={14} />} count={previews.length} all={{ to: `${base}/deployments`, label: "Deployments" }}>
814 <ul className="divide-y divide-line overflow-hidden rounded-xl border border-line bg-surface">
815 {previews.map((app) => (
816 <li key={app.url} className="flex items-center gap-3 px-4 py-2.5 text-sm">
817 <span className="min-w-0 grow">
818 <a href={app.url} className="block truncate font-mono text-[0.8125rem] hover:text-accent">
819 {host(app.url)}
820 </a>
821 <span className="text-xs text-muted">
822 {app.branch}
823 {app.number != null && (
824 <>
825 {" · "}
826 <Link to={`${base}/pull/${app.number}`} className="hover:underline">
827 #{app.number}
828 </Link>
829 </>
830 )}
831 </span>
Projects: what a workspace builds and runs, first on every page832 </span>
833 <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 API834 <TimeAgo at={app.deployedAt} />
Projects: what a workspace builds and runs, first on every page835 </span>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API836 </li>
837 ))}
838 </ul>
839 </Panel>
840 )}
841 </div>
Projects: what a workspace builds and runs, first on every page842
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API843 <aside className="space-y-4 text-sm">
844 {member && (
845 <section className="rounded-xl border border-line bg-surface p-5">
846 <div className="flex items-center justify-between">
847 <h2 className="flex items-center gap-1.5 text-sm font-semibold">
848 <Brain size={14} className="text-merged" />
849 What agents know here
850 </h2>
851 <Link to={`${base}/memory`} className="text-xs text-muted hover:text-fg">
852 Memory
853 </Link>
Projects: what a workspace builds and runs, first on every page854 </div>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API855 {!loaderData.memoriesLoaded ? (
856 <p className="mt-3 text-xs text-muted">Memory could not be loaded just now.</p>
857 ) : knows.length === 0 ? (
858 <p className="mt-3 text-xs leading-5 text-muted">
859 Nothing yet. Agents note conventions, decisions and traps as they work, and every agent starting here reads them.
860 </p>
861 ) : (
862 <ul className="mt-3 space-y-2.5">
863 {knows.map((memory) => (
864 <li key={memory.id} className="text-xs leading-5">
865 <p className="line-clamp-3 text-fg-soft">
866 {memory.pinned && <Pin size={11} className="mr-1 inline text-accent" />}
867 {memory.text}
868 </p>
869 <p className="text-faint">
870 {memory.kind} · {memory.createdBy} · <TimeAgo at={memory.createdAt} />
871 </p>
872 </li>
873 ))}
874 {loaderData.memoryCount > knows.length && (
875 <li>
876 <Link to={`${base}/memory`} className="text-xs text-muted hover:text-fg">
877 {loaderData.memoryCount - knows.length} more
878 </Link>
879 </li>
880 )}
881 </ul>
882 )}
883 </section>
Projects: what a workspace builds and runs, first on every page884 )}
885
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API886 <section className="rounded-xl border border-line bg-surface p-5">
887 <h2 className="flex items-center gap-1.5 text-sm font-semibold">
888 <HeartPulse size={14} className="text-faint" />
889 Health
Project dependencies: addresses, preview stacks, Affects, and agents who know890 </h2>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API891 <div className="mt-4 space-y-4 text-xs">
892 <div>
893 <div className="flex items-baseline justify-between">
894 <span className="text-muted">Checks passing</span>
895 <span className="font-medium tabular-nums">{percent(health.passRate)}</span>
896 </div>
897 <div className="mt-1.5">
898 <Meter value={health.passRate} tone={health.passRate != null && health.passRate < 0.7 ? "bg-warn" : "bg-accent"} />
899 </div>
900 <p className="mt-1 text-faint">
901 {health.checkRuns
902 ? `${health.checkRuns} recent runs · ${percent(health.firstPass.rate)} pass on the first try`
903 : "No checks have run recently."}
904 </p>
905 </div>
906 {member && (
Project dependencies: addresses, preview stacks, Affects, and agents who know907 <div>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API908 <div className="flex items-baseline justify-between">
909 <span className="text-muted">Deploys</span>
910 <Link to={`${base}/deployments`} className="text-faint hover:text-fg">
911 {builds.length ? `last ${Math.min(builds.length, 20)}` : "none yet"}
912 </Link>
913 </div>
914 <div className="mt-1.5">
915 <DeployStrip builds={builds} base={base} />
916 </div>
Project dependencies: addresses, preview stacks, Affects, and agents who know917 </div>
918 )}
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API919 <div>
920 <div className="flex items-baseline justify-between">
921 <span className="text-muted">Open issues by age</span>
922 <Link to={`${base}/issues`} className="font-medium tabular-nums hover:text-accent">
923 {open.issues}
924 </Link>
925 </div>
926 {ages ? (
927 <ul className="mt-2 space-y-1.5">
928 {ages.map((bucket) => (
929 <li key={bucket.label} className="grid grid-cols-[6.5rem_1fr_1.5rem] items-center gap-2">
930 <span className="text-faint">{bucket.label}</span>
931 <span className="block h-1.5 overflow-hidden rounded-full bg-line">
932 <span
933 className={`block h-full rounded-full ${bucket.label === "Older" ? "bg-warn" : "bg-fg-soft/60"}`}
934 style={{ width: `${(bucket.count / oldest) * 100}%` }}
935 />
936 </span>
937 <span className="text-right tabular-nums text-muted">{bucket.count}</span>
Project dependencies: addresses, preview stacks, Affects, and agents who know938 </li>
939 ))}
940 </ul>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API941 ) : (
942 <p className="mt-1 text-faint">Issues could not be loaded just now.</p>
943 )}
944 </div>
Project dependencies: addresses, preview stacks, Affects, and agents who know945 </div>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API946 </section>
Project dependencies: addresses, preview stacks, Affects, and agents who know947
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API948 <section className="rounded-xl border border-line bg-surface p-5">
949 <div className="flex items-center justify-between">
950 <h2 className="flex items-center gap-1.5 text-sm font-semibold">
951 <Network size={14} className="text-faint" />
952 Dependencies
953 </h2>
954 {member && (
955 <Link to={`${base}/settings/dependencies`} className="text-xs text-muted hover:text-fg">
956 Manage
957 </Link>
958 )}
959 </div>
960 {!dependencies ? (
961 <p className="mt-3 text-xs text-muted">Dependencies could not be loaded just now.</p>
962 ) : dependencies.dependsOn.length === 0 && dependencies.usedBy.length === 0 ? (
963 <p className="mt-3 text-xs leading-5 text-muted">
964 Uses no other project, and none uses it. Declare one, and builds get its address and agents know what depends on
965 what.
966 </p>
967 ) : (
968 <div className="mt-3 space-y-3 text-xs">
969 {dependencies.dependsOn.length > 0 && (
970 <div>
971 <p className="flex items-center gap-1 text-muted">
972 <ArrowUpRight size={12} /> Uses
973 </p>
974 <ul className="mt-1.5 space-y-1">
975 {dependencies.dependsOn.map((d) => (
976 <li key={d.slug} className="flex items-center justify-between gap-2">
977 <Link to={`/${params.owner}/${d.slug}`} className="font-medium hover:underline">
978 {d.name}
979 </Link>
980 {d.as && <code className="font-mono text-faint">{d.as}</code>}
981 </li>
982 ))}
983 </ul>
984 </div>
985 )}
986 {dependencies.usedBy.length > 0 && (
987 <div>
988 <p className="flex items-center gap-1 text-muted">
989 <ArrowDownLeft size={12} /> Used by
990 </p>
991 <ul className="mt-1.5 space-y-1">
992 {dependencies.usedBy.map((d) => (
993 <li key={d.slug}>
994 <Link to={`/${params.owner}/${d.slug}`} className="font-medium hover:underline">
995 {d.name}
996 </Link>
997 </li>
998 ))}
999 </ul>
1000 </div>
1001 )}
1002 </div>
1003 )}
Projects: what a workspace builds and runs, first on every page1004 </section>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1005
1006 {source && (
1007 <section className="rounded-xl border border-line bg-surface p-5">
1008 <h2 className="text-sm font-semibold">Clone</h2>
1009 <div className="mt-3">
1010 <CopyLine text={`git clone https://g1t.sh/${source.repo.namespace}/${source.repo.name}.git`} />
1011 </div>
1012 </section>
1013 )}
1014 </aside>
1015 </div>
Projects: what a workspace builds and runs, first on every page1016 </div>
1017 );
1018}