Skip to content

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

1,088 lines49,986 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";
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily33import { Skeleton, 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";
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent58import { madeByG1t } from "../../lib/opened-by";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look59import { assertSameOrigin, getViewer, requireUser } from "../../lib/session.server";
Fast pages, required checks on the branch, self-hosted runners, honest incidents60import { accessTo, countsFor, refusal, repoFor } from "../../lib/access.server";
Projects: what a workspace builds and runs, first on every page61
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API62const MAX_LANDED = 6;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look63/** Branches read for the Active branches list, and shown. */
64const BRANCHES_READ = 10;
65const BRANCHES_SHOWN = 5;
66/** How far back each branch's history, and the default branch's, is read to count ahead and behind. */
67const BRANCH_DEPTH = 40;
68const MAIN_DEPTH = 120;
Projects: what a workspace builds and runs, first on every page69
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily70/**
71 * The overview streams: the layout's header and tabs (one repository
72 * lookup, one project) go out at once with a skeleton below them, and
73 * everything here, seventeen calls across six services, follows in the
74 * same response as it settles. Crawlers wait for all of it
75 * (entry.server.tsx). Before, the first byte waited on the slowest of them.
76 */
77export function loader({ params, context }: Route.LoaderArgs) {
78 return { overview: overviewData({ params, context }) };
79}
80
81async function overviewData({ params, context }: Pick<Route.LoaderArgs, "params" | "context">) {
Projects: what a workspace builds and runs, first on every page82 const viewer = getViewer(context);
83 const path = { namespace: params.owner, name: params.repo };
84 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 API85 const now = Date.now();
86 // A section whose service fails shows its own empty state; the page stays up.
87 const soft = <T,>(promise: Promise<T> | null): Promise<T | null> =>
88 promise ? promise.catch((error) => (console.warn("overview:", error), null)) : Promise.resolve(null);
Fast pages, required checks on the branch, self-hosted runners, honest incidents89 // People with a role of their own here see its running parts: deployments,
90 // memory, setup; everyone who can read it sees the rest. Not awaited: only
91 // those sections wait on the repository lookup, which the layout makes in
92 // this request too.
93 const accessP = accessTo(context, params);
94 const memberP = accessP.then((access) => access.insider);
95 const forMembers = <T,>(start: () => Promise<T>): Promise<T | null> =>
96 memberP.then((member) => (member ? soft(start()) : null));
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look97 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 API98 const eventsP = repoP.then((repo) => (repo?.ok ? soft(eventLog.list({ repoId: repo.value.id, limit: 150 })) : null));
99 const minePullsP = repoP.then((repo) =>
100 repo?.ok && viewer
101 ? soft(work.listActivePulls(viewer)).then((list) => (list ?? []).filter((item) => item.pull.repoId === repo.value.id))
102 : null,
103 );
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look104 const openP = soft(work.listPulls(path, viewer, "open"));
105 // Where its code came from on GitHub, for members.
Fast pages, required checks on the branch, self-hosted runners, honest incidents106 const githubP = repoP.then((repo) => (repo?.ok ? forMembers(() => githubApp.link(repo.value.id)) : null));
107 const listP = forMembers(() => deployments.list(ref, viewer));
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look108 // Active branches: the newest few besides the default, with how far each
109 // has moved. Branches with an open pull request are read first.
110 const branchesP = Promise.all([repoP, soft(repos.branches(path, viewer)), openP, listP]).then(
111 async ([repo, branchList, pulls, deploys]): Promise<{ main: string; total: number; shown: ActiveBranch[] } | null> => {
112 if (!repo?.ok || !branchList?.ok) return null;
113 const main = repo.value.defaultBranch;
114 const pullList = pulls?.ok ? pulls.value : [];
115 const pullOn = new Map(pullList.filter((pull) => pull.branch).map((pull) => [pull.branch as string, pull]));
116 const others = branchList.value.filter((branch) => branch.name !== main);
117 if (others.length === 0) return { main, total: 0, shown: [] };
118 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 incidents119 // By commit hash, not name: history from a commit never changes, so
120 // repos keeps it (services/repos/src/store.rs) and only new heads cost
121 // a walk.
122 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 look123 const [mainLog, ...logs] = await Promise.all([
Fast pages, required checks on the branch, self-hosted runners, honest incidents124 soft(repos.log(path, viewer, mainHead, MAIN_DEPTH)),
125 ...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 look126 ]);
127 const mainHashes = mainLog?.ok ? mainLog.value.map((c) => c.hash) : [];
128 const previews = deploys?.ok ? deploys.value.live.filter((app) => app.kind === "preview") : [];
129 const shown = read
130 .map((branch, index): ActiveBranch => {
131 const history = logs[index]?.ok ? logs[index].value : [];
132 const head = history[0];
133 const moved = drift(history.map((c) => c.hash), mainHashes, BRANCH_DEPTH);
134 const pull = pullOn.get(branch.name);
135 return {
136 name: branch.name,
137 commit: head ? { hash: head.hash, message: head.message.split("\n")[0], author: head.author.name, at: head.authoredAt } : null,
138 ...moved,
139 pull: pull ? { number: pull.number, title: pull.title, checkStatus: pull.checkStatus, draft: pull.status === "draft" } : null,
140 preview: previews.find((app) => app.branch === branch.name || (pull != null && app.number === pull.number))?.url ?? null,
141 };
142 })
143 .sort((a, b) => Date.parse(b.commit?.at ?? "0") - Date.parse(a.commit?.at ?? "0"))
144 .slice(0, BRANCHES_SHOWN);
145 return { main, total: others.length, shown };
146 },
147 );
Fast pages, required checks on the branch, self-hosted runners, honest incidents148 // Active branches read several logs each: streamed, so the rest shows first.
149 const branches = branchesP.catch(() => null);
150 const [{ insider: member, can }, project, settings, list, open, closed, log, counts, deps, runs, queue, issues, memories, recent, mine, domains, root] = await Promise.all([
151 accessP,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API152 soft(projects.get(params.owner, params.repo, viewer)),
Fast pages, required checks on the branch, self-hosted runners, honest incidents153 forMembers(() => deployments.settings(ref, viewer)),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look154 listP,
155 openP,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API156 soft(work.listPulls(path, viewer, "closed")),
157 soft(repos.log(path, viewer, null, 1)),
Fast pages, required checks on the branch, self-hosted runners, honest incidents158 soft(countsFor(context, params)),
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API159 soft(projects.dependencies(params.owner, params.repo, viewer)),
160 soft(agents.listRuns(viewer, { repo: path, limit: 60 })),
161 soft(work.queue(path, viewer)),
162 soft(work.listIssues(path, viewer, { state: "open" })),
Fast pages, required checks on the branch, self-hosted runners, honest incidents163 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 API164 eventsP,
165 minePullsP,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look166 // For the checklist, which only members see.
Fast pages, required checks on the branch, self-hosted runners, honest incidents167 forMembers(() => deployments.domains(ref, viewer)),
168 forMembers(() => repos.tree(path, viewer, null, "")),
Projects: what a workspace builds and runs, first on every page169 ]);
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API170 const ok = <T,>(result: { ok: true; value: T } | { ok: false } | null): T | null => (result?.ok ? result.value : null);
171
172 const openPulls = ok(open) ?? [];
173 const runList = ok(runs) ?? [];
174 const live = runList.filter((run) => isActiveRun(run.status));
175 const builds = ok(list)?.deployments ?? [];
176 const landed = (ok(closed) ?? [])
177 .filter((pull) => pull.status === "merged")
178 .sort((a, b) => Date.parse(b.mergedAt ?? b.updatedAt) - Date.parse(a.mergedAt ?? a.updatedAt));
179 const queued = queuedNumbers(ok(queue)?.active ?? []);
180 const runOn = new Map<number, AgentRun>();
181 for (const run of live) if (run.number != null && !runOn.has(run.number)) runOn.set(run.number, run);
182 const lifecycleOf = new Map((mine ?? []).map((item) => [item.pull.number, item.lifecycle]));
183
184 // --- The pipeline -----------------------------------------------------------
185 type Card = {
186 number: number;
187 title: string;
188 agent: string;
189 status: Pull["status"];
190 checkStatus: Pull["checkStatus"];
191 step: string | null;
192 runKind: string | null;
193 runStarted: string | null;
194 needsYou: boolean;
195 at: string;
196 };
197 const columns: Record<PipelineStage, Card[]> = { working: [], checking: [], reviewing: [], queue: [], landed: [] };
198 for (const pull of openPulls.slice(0, 40)) {
199 const run = runOn.get(pull.number);
Fast pages, required checks on the branch, self-hosted runners, honest incidents200 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 API201 number: pull.number,
202 title: pull.title,
203 agent: pull.agent,
204 status: pull.status,
205 checkStatus: pull.checkStatus,
206 step: run?.step ?? null,
207 runKind: run ? RUN_KIND_LABEL[run.kind] : null,
208 runStarted: run ? (run.startedAt ?? run.createdAt) : null,
209 needsYou: lifecycleOf.get(pull.number)?.stage === "needs_you",
210 at: pull.updatedAt,
211 });
212 }
213 for (const pull of landed.filter((p) => now - Date.parse(p.mergedAt ?? p.updatedAt) < 3 * TIME.DAY).slice(0, 5)) {
214 columns.landed.push({
215 number: pull.number,
216 title: pull.title,
217 agent: pull.agent,
218 status: pull.status,
219 checkStatus: pull.checkStatus,
220 step: null,
221 runKind: null,
222 runStarted: null,
223 needsYou: false,
224 at: pull.mergedAt ?? pull.updatedAt,
225 });
226 }
227
228 // --- Needs you ---------------------------------------------------------------
229 const base = `/${params.owner}/${params.repo}`;
230 const needs: Need[] = [];
231 const latestProduction = builds.find((build) => build.kind === "production") ?? null;
232 if (latestProduction?.status === "failed") {
233 needs.push({
234 key: "deploy",
235 kind: "deploy",
236 title: "The production build failed",
237 detail: latestProduction.error ?? "Production still serves the build before it.",
238 to: `${base}/deployments/${latestProduction.id}`,
239 action: "See the build",
240 at: Date.parse(latestProduction.finishedAt ?? latestProduction.createdAt),
241 where: latestProduction.commit.slice(0, 7),
242 });
243 }
244 const me = viewer?.username.toLowerCase();
245 for (const pull of openPulls) {
246 const to = `${base}/pull/${pull.number}`;
247 const where = `#${pull.number}`;
248 const lifecycle = lifecycleOf.get(pull.number);
249 if (lifecycle?.stage === "needs_you") {
250 const conflict = /conflict/i.test(lifecycle.detail);
251 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 });
252 } else if (lifecycle?.stage === "ready") {
253 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 });
254 }
255 if (me && pull.status === "open" && pull.author.username.toLowerCase() !== me && pull.reviewers.some((name) => name.toLowerCase() === me)) {
256 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 });
257 }
258 if (member && pull.status === "open" && pull.checkStatus === "failed" && !runOn.has(pull.number) && !lifecycle) {
Fast pages, required checks on the branch, self-hosted runners, honest incidents259 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 API260 }
261 }
262 for (const run of live) {
263 const minutes = stuckMinutes(run, now);
264 if (minutes == null) continue;
265 needs.push({
266 key: `run:${run.id}`,
267 kind: "stuck",
268 title: run.title ?? `${run.agent}'s run`,
269 detail: `${run.agent} has reported nothing for ${minutes} min${run.step ? `. Last: ${run.step}` : ""}.`,
270 to: `${base}/agents/runs/${run.id}`,
271 action: "Look",
272 at: Date.parse(run.updatedAt),
273 where: run.number != null ? `#${run.number}` : null,
274 });
275 }
276
277 // --- Activity and health -------------------------------------------------------
278 const eventList: G1tEvent[] = recent ?? [];
279 const items = eventList
280 .map((event) => eventItem(event, path))
281 .filter((item): item is ActivityItem => item != null);
282 const checkEvents = eventList.flatMap((event) =>
283 event.type === "checks.completed" ? [{ repo: "", number: event.data.number, at: Date.parse(event.time), passed: event.data.status === "passed" }] : [],
284 );
285 const memoryList = ok(memories)?.project ?? [];
286 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);
287 const openIssues = ok(issues);
288
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look289 // --- Getting to production ----------------------------------------------------
290 const wentLive = (status: string) => status === "ready" || status === "replaced" || status === "down";
291 const liveApps = ok(list)?.live ?? [];
292 const commitNow = ok(log)?.[0] ?? null;
293 const projectValue = ok(project);
294 const checklist = member
295 ? productionChecklist({
296 base,
297 hasCode: commitNow != null || projectValue?.source.kind === "mirror",
298 deploysEnabled: ok(settings)?.enabled ?? false,
299 productionDeployed:
300 liveApps.some((app) => app.kind === "production") ||
301 builds.some((build) => build.kind === "production" && wentLive(build.status)),
302 domains: ok(domains)?.domains.length ?? null,
303 previewOpened:
304 liveApps.some((app) => app.kind === "preview") || builds.some((build) => build.kind === "preview" && wentLive(build.status)),
305 instructions: commitNow == null ? false : root?.ok ? hasInstructions(root.value.entries.map((entry) => entry.name)) : null,
306 agentAssigned: agentWasAssigned({
307 runAgents: runList.map((run) => run.agent),
308 pullAgents: [...openPulls, ...(ok(closed) ?? [])].map((pull) => pull.agent),
309 issues: openIssues ?? [],
310 }),
311 })
312 : null;
313
314 const github = await githubP;
Projects: what a workspace builds and runs, first on every page315 return {
316 member,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look317 can,
318 github,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API319 project: ok(project),
320 settings: ok(settings),
321 builds: builds.slice(0, 30),
322 live: ok(list)?.live ?? [],
323 commit: ok(log)?.[0] ?? null,
324 open: ok(counts) ?? { issues: openIssues?.length ?? 0, pulls: openPulls.length },
325 dependencies: ok(deps),
326 agentsLive: live.slice(0, 6),
327 runsLoaded: runs?.ok ?? false,
328 pullsLoaded: open?.ok ?? false,
329 columns,
330 needs: rankNeeds(needs),
331 landed: landed.slice(0, MAX_LANDED),
332 groups: groupActivity(items).slice(0, 30),
333 eventsLoaded: recent != null,
334 health: {
335 passRate: passRate(checkEvents.map((c) => c.passed)),
336 firstPass: firstPassRate(checkEvents),
337 checkRuns: checkEvents.length,
338 ages: openIssues ? ageBuckets(openIssues.map((issue) => issue.createdAt), now) : null,
339 },
340 knows: knows as Memory[],
341 memoryCount: memoryList.length,
342 memoriesLoaded: memories?.ok ?? false,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look343 checklist,
344 branches,
Projects: what a workspace builds and runs, first on every page345 };
346}
347
348export async function action({ request, params, context }: Route.ActionArgs) {
349 assertSameOrigin(request);
350 const user = requireUser(context, request);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look351 const form = await request.formData().catch(() => null);
352 const intent = form?.get("intent");
353 // Syncing from GitHub is pushing; stopping it is an integration; deploying is compute.
354 const refused = await refusal(context, params, intent === "github-sync" ? "push" : intent === "github-stop" ? "manage_integrations" : "run");
355 if (refused) return { error: refused };
356 if (intent === "github-sync" || intent === "github-stop") {
357 const repo = await repos.get({ namespace: params.owner, name: params.repo }, user);
358 if (!repo.ok) return { error: repo.error.message };
359 const done =
360 intent === "github-sync" ? await githubApp.sync(user, repo.value.id) : await githubApp.unlinkRepo(user, repo.value.id);
361 return done.ok
362 ? { notice: intent === "github-sync" ? "Synced with GitHub." : "It is no longer kept in step with GitHub." }
363 : { error: done.error.message };
364 }
Projects: what a workspace builds and runs, first on every page365 const started = await deployments.redeploy(user, { workspace: params.owner, slug: params.repo }, null);
366 return started.ok ? { notice: "Production is building." } : { error: started.error.message };
367}
368
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily369type Loaded = Awaited<ReturnType<typeof overviewData>>;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API370type Card = Loaded["columns"]["working"][number];
371
372const STAGE_TONE: Record<PipelineStage, string> = {
373 working: "bg-merged",
374 checking: "bg-info",
375 reviewing: "bg-warn",
376 queue: "bg-accent-dim",
377 landed: "bg-accent",
378};
379
380function PipelineCard({ card, base, stage }: { card: Card; base: string; stage: PipelineStage }) {
381 return (
382 <li>
383 <Link
384 to={`${base}/pull/${card.number}`}
385 prefetch="intent"
386 className={`block rounded-lg border bg-bg/60 p-2.5 transition-colors hover:border-line-strong hover:bg-raised ${
387 card.needsYou ? "border-warn/50" : card.step ? "border-merged/30" : "border-line"
388 }`}
389 >
390 <span className="line-clamp-2 text-[0.8125rem] leading-snug font-medium">{card.title}</span>
391 <span className="mt-1.5 flex items-center gap-1.5 text-[0.6875rem] text-muted">
392 <Avatar name={card.agent} size={14} />
393 <span className="font-mono text-faint">#{card.number}</span>
394 <span className="grow" />
395 {card.needsYou ? (
396 <span className="inline-flex items-center gap-1 text-warn" title="Stopped: a person decides what happens next">
397 <Hand size={11} /> you
398 </span>
399 ) : stage === "landed" ? (
400 <TimeAgo at={card.at} />
401 ) : card.runStarted ? (
402 <span className="inline-flex items-center gap-1 text-merged">
403 <Loader2 size={11} className="animate-spin" />
404 <Elapsed from={card.runStarted} />
405 </span>
406 ) : (
407 <CheckBadge status={card.checkStatus} />
408 )}
409 </span>
410 {card.step && (
411 <span className="mt-1.5 block truncate font-mono text-[0.6875rem] text-fg/70" title={card.step}>
412 {card.runKind}: {card.step}
413 </span>
414 )}
415 </Link>
416 </li>
417 );
418}
419
420/** Open pull requests moving through Working → Checking → Reviewing → Queue → Landed. */
421function Pipeline({ columns, base }: { columns: Loaded["columns"]; base: string }) {
422 return (
423 <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 look424 {/* Each stage as tall as what is in it: an empty one stays short
425 instead of stretching to the busiest. */}
426 <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 API427 {PIPELINE.map(({ stage, label }, index) => {
428 const cards = columns[stage];
429 return (
430 <li key={stage} className="flex min-w-0 flex-col rounded-xl border border-line bg-surface p-2">
431 <p className="flex items-center gap-2 px-1 pt-0.5 pb-2 text-xs font-medium text-muted">
432 <span className={`size-1.5 rounded-full ${STAGE_TONE[stage]} ${cards.length && stage !== "landed" ? "animate-pulse" : ""}`} />
433 {label}
434 <span className="tabular-nums text-faint">{cards.length}</span>
435 {index < PIPELINE.length - 1 && <ArrowRight size={11} className="ml-auto text-line-strong" />}
436 </p>
437 {cards.length === 0 ? (
438 <p className="rounded-lg border border-dashed border-line/70 px-2 py-3 text-center text-[0.6875rem] text-faint">Empty</p>
439 ) : (
440 <ul className="space-y-1.5">
441 {cards.slice(0, 6).map((card) => (
442 <PipelineCard key={card.number} card={card} base={base} stage={stage} />
443 ))}
444 {cards.length > 6 && (
445 <li className="px-1 text-[0.6875rem] text-muted">
446 <Link to={`${base}/pulls`} className="hover:text-fg">
447 {cards.length - 6} more
448 </Link>
449 </li>
450 )}
451 </ul>
452 )}
453 </li>
454 );
455 })}
456 </ol>
457 </div>
458 );
459}
460
461function Stat({ label, value, to }: { label: string; value: ReactNode; to?: string }) {
462 const body = (
463 <>
464 <span className="block text-[0.6875rem] text-faint">{label}</span>
465 <span className="mt-0.5 block truncate text-[0.8125rem] font-medium">{value}</span>
466 </>
467 );
468 return to ? (
469 <Link to={to} className="min-w-0 px-5 py-3 transition-colors hover:bg-raised/50">
470 {body}
471 </Link>
472 ) : (
473 <div className="min-w-0 px-5 py-3">{body}</div>
474 );
475}
476
Projects: what a workspace builds and runs, first on every page477export default function ProjectOverview({ loaderData, actionData, params }: Route.ComponentProps) {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily478 return (
479 <Suspense fallback={<OverviewSkeleton />}>
480 <Await resolve={loaderData.overview} errorElement={<OverviewFailed />}>
481 {(overview) => <Overview loaderData={overview} actionData={actionData} params={params} />}
482 </Await>
483 </Suspense>
484 );
485}
486
487/** The overview's sections as they will sit, while they stream in. */
488function OverviewSkeleton() {
489 return (
490 <div className="space-y-8" aria-busy="true" aria-label="Loading the overview">
491 <section className="rounded-2xl border border-line bg-surface p-5 sm:p-6">
492 <Skeleton className="h-3 w-24" />
493 <Skeleton className="mt-3 h-5 w-64 max-w-full" />
494 <Skeleton className="mt-3 h-3 w-48 max-w-full" />
495 </section>
496 <section className="rounded-2xl border border-line bg-surface p-2">
497 <div className="grid grid-cols-2 gap-2 sm:grid-cols-5">
498 {Array.from({ length: 5 }, (_, index) => (
499 <Skeleton key={index} className="h-24 rounded-xl" />
500 ))}
501 </div>
502 </section>
503 <div className="grid gap-6 lg:grid-cols-2">
504 <section className="rounded-2xl border border-line bg-surface">
505 <SkeletonRows rows={4} />
506 </section>
507 <section className="rounded-2xl border border-line bg-surface">
508 <SkeletonRows rows={4} />
509 </section>
510 </div>
511 </div>
512 );
513}
514
515function OverviewFailed() {
516 return (
517 <p className="rounded-2xl border border-line bg-surface p-6 text-sm text-muted">
518 The overview could not be loaded just now. Reload the page to try again.
519 </p>
520 );
521}
522
523function Overview({
524 loaderData,
525 actionData,
526 params,
527}: {
528 loaderData: Loaded;
529 actionData: Route.ComponentProps["actionData"];
530 params: Route.ComponentProps["params"];
531}) {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look532 const { member, project, settings, builds, live, commit, open, dependencies, agentsLive, columns, needs, landed, groups, health, knows, checklist, branches } =
533 loaderData;
Projects: what a workspace builds and runs, first on every page534 const base = `/${params.owner}/${params.repo}`;
535 const production = live.find((app) => app.kind === "production") ?? null;
Agents and memory, checks and conflicts, profiles, slug renames, custom domains536 // The project's own domain, once it is active, is where production is visited.
537 const productionUrl = production ? (settings?.primaryDomain ? `https://${settings.primaryDomain}` : production.url) : null;
Projects: what a workspace builds and runs, first on every page538 const previews = live.filter((app) => app.kind === "preview");
539 const latestProduction = builds.find((build) => build.kind === "production") ?? null;
540 const busy = useNavigation().state === "submitting";
541 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 API542 const moving = agentsLive.length > 0 || columns.working.length + columns.checking.length > 0;
543 useLiveRefresh(moving);
544 const ages = health.ages;
545 const oldest = ages ? Math.max(1, ...ages.map((a) => a.count)) : 1;
Projects: what a workspace builds and runs, first on every page546
547 return (
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API548 <div className="space-y-8">
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look549 {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 API550 {/* What the project is, running, and where its code is. */}
551 <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 look552 <div className="flex flex-col gap-5 p-5 sm:flex-row sm:items-start sm:p-6">
553 {member && (production || settings?.enabled) && (
554 <ProductionShot
555 src={production ? `${base}/production.jpg?v=${production.commit}` : null}
556 href={production ? (productionUrl ?? production.url) : null}
557 label={host(production ? (productionUrl ?? production.url) : (settings?.productionUrl ?? ""))}
558 className="w-full shrink-0 sm:w-60 lg:w-72"
559 />
560 )}
561 <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 API562 <div className="min-w-0">
563 <p className="flex items-center gap-2 text-xs font-medium tracking-wide text-muted uppercase">
564 <Rocket size={13} className="text-accent" />
565 Production
566 </p>
567 {production ? (
568 <>
569 <a
570 href={productionUrl ?? production.url}
571 className="mt-2 flex items-center gap-1.5 truncate font-mono text-lg font-medium hover:text-accent"
572 >
573 {host(productionUrl ?? production.url)}
574 <ArrowUpRight size={16} className="shrink-0 text-faint" />
575 </a>
576 <p className="mt-1.5 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted">
577 {latestProduction && <StatusDot status={latestProduction.status} label={latestProduction.status === "ready" ? "Live" : undefined} />}
578 <span className="inline-flex items-center gap-1 font-mono">
579 <GitCommitHorizontal size={13} className="text-faint" />
580 {production.commit.slice(0, 7)}
581 </span>
582 <span>
583 deployed <TimeAgo at={production.deployedAt} />
584 </span>
585 {settings?.primaryDomain && <span className="text-faint">also at {host(production.url)}</span>}
586 </p>
587 </>
588 ) : settings?.enabled ? (
589 <>
590 <p className="mt-2 font-mono text-lg text-muted">{host(settings.productionUrl)}</p>
591 <p className="mt-1.5 text-xs text-muted">
592 {latestProduction ? (
593 <StatusDot status={latestProduction.status} label={latestProduction.status === "failed" ? "The last build failed" : undefined} />
594 ) : (
595 "Not deployed yet. Push to the default branch, or deploy it now."
596 )}
597 </p>
598 </>
599 ) : (
600 <>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look601 <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 API602 <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 look603 Nothing builds or runs until you turn them on. Then {project?.name ?? params.repo} goes up on g1t.page:
604 production from the default branch, and a live preview for every pull request. It runs only while
605 someone visits.
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API606 </p>
607 </>
608 )}
609 </div>
610 {member && (
611 <div className="flex shrink-0 items-center gap-2">
612 {production && (
613 <ButtonLink to={productionUrl ?? production.url} variant="accent" reloadDocument>
614 Visit
615 <ArrowUpRight size={14} />
616 </ButtonLink>
617 )}
618 {settings?.enabled ? (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look619 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 API620 <Button type="submit" variant="quiet" disabled={busy} title="Build production again from the default branch">
621 <RotateCw size={14} />
622 Redeploy
623 </Button>
624 </Form>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look625 ) : loaderData.can.manage_integrations && (
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API626 <ButtonLink to={`${base}/settings/deployments`} variant="accent">
627 <Rocket size={14} />
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look628 Turn on deployments
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API629 </ButtonLink>
Projects: what a workspace builds and runs, first on every page630 )}
631 </div>
632 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look633 </div>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API634 </div>
635 {actionData && (
636 <p className={`border-t border-line px-6 py-2.5 text-sm ${"error" in actionData ? "text-danger" : "text-accent"}`}>
637 {"error" in actionData ? actionData.error : actionData.notice}
638 </p>
639 )}
640 <div className="grid grid-cols-2 divide-line border-t border-line text-sm sm:grid-cols-4 sm:divide-x">
641 <Stat
642 label="Source"
643 to={`${base}/code`}
644 value={
645 source ? (
646 <span className="inline-flex items-center gap-1.5 font-mono">
647 {project?.private ? <Lock size={12} className="text-faint" /> : <Code2 size={12} className="text-faint" />}
648 {source.repo.name}
649 {source.rootDir ? `/${source.rootDir}` : ""}
650 </span>
651 ) : (
652 "—"
653 )
654 }
655 />
656 <Stat
657 label="Default branch"
658 to={`${base}/commits`}
659 value={
660 <span className="inline-flex items-center gap-1.5 font-mono">
661 <GitBranch size={12} className="text-faint" />
662 {source?.defaultBranch ?? "main"}
663 </span>
664 }
665 />
666 <Stat
667 label="Latest commit"
668 to={commit ? `${base}/commit/${commit.hash}` : undefined}
669 value={
670 commit ? (
671 <span title={commit.message.split("\n")[0]}>
672 <span className="font-mono">{commit.hash.slice(0, 7)}</span>{" "}
673 <span className="font-normal text-muted">
674 <TimeAgo at={commit.authoredAt} />
675 </span>
676 </span>
677 ) : (
678 "No commits yet"
679 )
680 }
681 />
682 <Stat
683 label={member && settings?.enabled ? "Last deploy" : "Open"}
684 to={member && settings?.enabled ? `${base}/deployments` : `${base}/pulls`}
685 value={
686 member && settings?.enabled ? (
687 latestProduction ? (
688 <span className="inline-flex items-center gap-2">
689 <StatusDot status={latestProduction.status} />
690 <span className="font-normal text-muted">
691 <TimeAgo at={latestProduction.createdAt} />
Projects: what a workspace builds and runs, first on every page692 </span>
693 </span>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API694 ) : (
695 "None yet"
696 )
697 ) : (
698 `${open.pulls} pull requests · ${open.issues} issues`
699 )
700 }
701 />
702 </div>
703 {commit && (
704 <p className="truncate border-t border-line px-5 py-2.5 text-xs text-muted sm:px-6">
705 <GitCommitHorizontal size={12} className="mr-1.5 inline text-faint" />
706 {commit.message.split("\n")[0]} <span className="text-faint">· {commit.author.name}</span>
707 </p>
708 )}
709 </section>
710
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look711 {checklist && <ProductionChecklist base={base} items={checklist} />}
712
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API713 <Panel
714 title="Right now"
715 icon={<span className={`block size-2 rounded-full ${agentsLive.length > 0 ? "animate-pulse bg-merged" : "bg-line-strong"}`} />}
716 count={agentsLive.length}
717 all={{ to: `${base}/agents`, label: "Agents" }}
718 >
719 {agentsLive.length > 0 && (
720 <ul className="mb-3 grid grid-cols-1 gap-2 sm:grid-cols-2 xl:grid-cols-3">
721 {agentsLive.map((run) => (
722 <li key={run.id} className="min-w-0 rounded-xl border border-merged/30 bg-surface px-3.5 py-3">
723 <div className="flex items-center gap-2 text-sm">
724 <Avatar name={run.agent} size={18} />
725 <span className="font-medium">{run.agent}</span>
726 <span className="text-xs text-muted">{RUN_KIND_LABEL[run.kind].toLowerCase()}</span>
727 <span className="grow" />
728 <span className="text-xs text-muted">
729 <Elapsed from={run.startedAt ?? run.createdAt} />
Projects: what a workspace builds and runs, first on every page730 </span>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API731 </div>
732 <Link
733 to={run.number != null ? `${base}/pull/${run.number}` : `${base}/agents/runs/${run.id}`}
734 className="mt-1 block truncate text-[0.8125rem] hover:text-accent"
735 >
736 {run.title ?? "A run"} {run.number != null && <span className="text-faint">#{run.number}</span>}
737 </Link>
738 {run.step && (
739 <p className="mt-1 truncate font-mono text-[0.6875rem] text-fg/70" title={run.step}>
740 <span className="mr-1.5 inline-block size-1.5 animate-pulse rounded-full bg-merged align-middle" />
741 {run.step}
742 </p>
743 )}
744 <p className="mt-1.5 flex items-center gap-3 text-[0.6875rem] text-faint">
745 {formatCost(run.costUsd) && <span>{formatCost(run.costUsd)} so far</span>}
746 <Link to={`${base}/agents/runs/${run.id}`} className="hover:text-fg">
747 {run.stepCount} steps
748 </Link>
749 {run.number != null && (
750 <Link to={`${base}/sessions/${run.number}`} className="hover:text-fg">
751 Session
752 </Link>
753 )}
754 </p>
755 </li>
756 ))}
757 </ul>
758 )}
759 {!loaderData.pullsLoaded ? (
760 <Unavailable what="Pull requests" />
761 ) : Object.values(columns).every((c) => c.length === 0) ? (
762 <Quiet
763 action={
764 member ? (
765 <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">
766 <Bot size={12} /> Open an issue for an agent
767 </Link>
768 ) : null
769 }
770 >
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent771 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 API772 </Quiet>
773 ) : (
774 <Pipeline columns={columns} base={base} />
Projects: what a workspace builds and runs, first on every page775 )}
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API776 </Panel>
Projects: what a workspace builds and runs, first on every page777
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API778 <div className="grid gap-x-8 gap-y-8 lg:grid-cols-[minmax(0,1fr)_20rem]">
779 <div className="min-w-0 space-y-8">
780 {needs.length > 0 && (
781 <Panel title="Needs you" icon={<Hand size={14} className="text-warn" />} count={needs.length}>
782 <NeedsList needs={needs} limit={5} />
783 </Panel>
784 )}
785
Fast pages, required checks on the branch, self-hosted runners, honest incidents786 <Suspense
787 fallback={
788 <Panel title="Active branches" icon={<GitBranch size={14} />}>
789 <div aria-busy="true">
790 <SkeletonRows rows={3} rowClassName="h-12" />
791 </div>
792 </Panel>
793 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look794 >
Fast pages, required checks on the branch, self-hosted runners, honest incidents795 <Await resolve={branches}>
796 {(branches) => (
797 <Panel title="Active branches" icon={<GitBranch size={14} />} count={branches?.total || null}>
798 {!branches ? (
799 <Unavailable what="Branches" />
800 ) : branches.shown.length === 0 ? (
801 <Quiet>
802 Only {branches.main} so far. Branches pushed here show with how far each has moved from {branches.main}, and
803 its pull request and preview.
804 </Quiet>
805 ) : (
806 <>
807 <ActiveBranches branches={branches.shown} base={base} main={branches.main} />
808 {branches.total > branches.shown.length && (
809 <p className="mt-2 px-1 text-xs text-faint">
810 The {branches.shown.length} most recently changed of {branches.total} branches.
811 </p>
812 )}
813 </>
814 )}
815 </Panel>
816 )}
817 </Await>
818 </Suspense>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look819
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API820 <Panel title="Recent changes" icon={<GitMerge size={14} />} all={{ to: `${base}/pulls?state=closed`, label: "All landed" }}>
821 {landed.length === 0 ? (
822 <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 page823 ) : (
824 <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 API825 {landed.map((pull) => {
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent826 const byAgent = pull.runtime === "hosted" || madeByG1t(pull);
Projects: what a workspace builds and runs, first on every page827 return (
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API828 <li key={pull.id} className="flex items-start gap-3 px-4 py-3">
829 <GitMerge size={15} className="mt-0.5 shrink-0 text-merged" />
830 <span className="min-w-0 grow">
831 <Link to={`${base}/pull/${pull.number}`} prefetch="intent" className="block truncate text-sm font-medium hover:text-accent">
832 {pull.title}
833 </Link>
834 <span className="mt-0.5 flex flex-wrap items-center gap-x-1.5 text-xs text-muted">
835 <span className="font-mono text-faint">#{pull.number}</span>
836 <span>·</span>
837 <span className="inline-flex items-center gap-1">
838 <Avatar name={pull.agent} size={13} />
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent839 {madeByG1t(pull)
840 ? `made by g1t for ${pull.author.username}`
841 : byAgent
842 ? `made by ${pull.agent}`
843 : `by ${pull.author.username}`}
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API844 </span>
845 {pull.issue != null && (
846 <>
847 <span>· for</span>
848 <Link to={`${base}/issues/${pull.issue}`} className="hover:text-fg">
849 #{pull.issue}
850 </Link>
851 </>
852 )}
853 {pull.mergedBy && <span>· landed by {pull.mergedBy}</span>}
854 <span>
855 · <TimeAgo at={pull.mergedAt ?? pull.updatedAt} />
Projects: what a workspace builds and runs, first on every page856 </span>
857 </span>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API858 </span>
859 <span className="hidden shrink-0 text-xs text-faint sm:block">
860 <ChangeSize files={pull.files} />
861 </span>
862 {byAgent && (
863 <Link to={`${base}/sessions/${pull.number}`} className="shrink-0 text-xs text-muted hover:text-fg">
864 Session
865 </Link>
866 )}
Projects: what a workspace builds and runs, first on every page867 </li>
868 );
869 })}
870 </ul>
871 )}
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API872 </Panel>
Projects: what a workspace builds and runs, first on every page873
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API874 <Panel title="Activity" icon={<ActivityIcon size={14} />}>
875 {loaderData.eventsLoaded ? (
876 <ActivityFeed groups={groups} showRepo={false} limit={8} empty="Nothing has happened here yet." />
877 ) : (
878 <Unavailable what="Activity" />
879 )}
880 </Panel>
881
882 {previews.length > 0 && (
883 <Panel title="Previews" icon={<GitBranch size={14} />} count={previews.length} all={{ to: `${base}/deployments`, label: "Deployments" }}>
884 <ul className="divide-y divide-line overflow-hidden rounded-xl border border-line bg-surface">
885 {previews.map((app) => (
886 <li key={app.url} className="flex items-center gap-3 px-4 py-2.5 text-sm">
887 <span className="min-w-0 grow">
888 <a href={app.url} className="block truncate font-mono text-[0.8125rem] hover:text-accent">
889 {host(app.url)}
890 </a>
891 <span className="text-xs text-muted">
892 {app.branch}
893 {app.number != null && (
894 <>
895 {" · "}
896 <Link to={`${base}/pull/${app.number}`} className="hover:underline">
897 #{app.number}
898 </Link>
899 </>
900 )}
901 </span>
Projects: what a workspace builds and runs, first on every page902 </span>
903 <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 API904 <TimeAgo at={app.deployedAt} />
Projects: what a workspace builds and runs, first on every page905 </span>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API906 </li>
907 ))}
908 </ul>
909 </Panel>
910 )}
911 </div>
Projects: what a workspace builds and runs, first on every page912
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API913 <aside className="space-y-4 text-sm">
914 {member && (
915 <section className="rounded-xl border border-line bg-surface p-5">
916 <div className="flex items-center justify-between">
917 <h2 className="flex items-center gap-1.5 text-sm font-semibold">
918 <Brain size={14} className="text-merged" />
919 What agents know here
920 </h2>
921 <Link to={`${base}/memory`} className="text-xs text-muted hover:text-fg">
922 Memory
923 </Link>
Projects: what a workspace builds and runs, first on every page924 </div>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API925 {!loaderData.memoriesLoaded ? (
926 <p className="mt-3 text-xs text-muted">Memory could not be loaded just now.</p>
927 ) : knows.length === 0 ? (
928 <p className="mt-3 text-xs leading-5 text-muted">
929 Nothing yet. Agents note conventions, decisions and traps as they work, and every agent starting here reads them.
930 </p>
931 ) : (
932 <ul className="mt-3 space-y-2.5">
933 {knows.map((memory) => (
934 <li key={memory.id} className="text-xs leading-5">
935 <p className="line-clamp-3 text-fg-soft">
936 {memory.pinned && <Pin size={11} className="mr-1 inline text-accent" />}
937 {memory.text}
938 </p>
939 <p className="text-faint">
940 {memory.kind} · {memory.createdBy} · <TimeAgo at={memory.createdAt} />
941 </p>
942 </li>
943 ))}
944 {loaderData.memoryCount > knows.length && (
945 <li>
946 <Link to={`${base}/memory`} className="text-xs text-muted hover:text-fg">
947 {loaderData.memoryCount - knows.length} more
948 </Link>
949 </li>
950 )}
951 </ul>
952 )}
953 </section>
Projects: what a workspace builds and runs, first on every page954 )}
955
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API956 <section className="rounded-xl border border-line bg-surface p-5">
957 <h2 className="flex items-center gap-1.5 text-sm font-semibold">
958 <HeartPulse size={14} className="text-faint" />
959 Health
Project dependencies: addresses, preview stacks, Affects, and agents who know960 </h2>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API961 <div className="mt-4 space-y-4 text-xs">
962 <div>
963 <div className="flex items-baseline justify-between">
964 <span className="text-muted">Checks passing</span>
965 <span className="font-medium tabular-nums">{percent(health.passRate)}</span>
966 </div>
967 <div className="mt-1.5">
968 <Meter value={health.passRate} tone={health.passRate != null && health.passRate < 0.7 ? "bg-warn" : "bg-accent"} />
969 </div>
970 <p className="mt-1 text-faint">
971 {health.checkRuns
972 ? `${health.checkRuns} recent runs · ${percent(health.firstPass.rate)} pass on the first try`
973 : "No checks have run recently."}
974 </p>
975 </div>
976 {member && (
Project dependencies: addresses, preview stacks, Affects, and agents who know977 <div>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API978 <div className="flex items-baseline justify-between">
979 <span className="text-muted">Deploys</span>
980 <Link to={`${base}/deployments`} className="text-faint hover:text-fg">
981 {builds.length ? `last ${Math.min(builds.length, 20)}` : "none yet"}
982 </Link>
983 </div>
984 <div className="mt-1.5">
985 <DeployStrip builds={builds} base={base} />
986 </div>
Project dependencies: addresses, preview stacks, Affects, and agents who know987 </div>
988 )}
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API989 <div>
990 <div className="flex items-baseline justify-between">
991 <span className="text-muted">Open issues by age</span>
992 <Link to={`${base}/issues`} className="font-medium tabular-nums hover:text-accent">
993 {open.issues}
994 </Link>
995 </div>
996 {ages ? (
997 <ul className="mt-2 space-y-1.5">
998 {ages.map((bucket) => (
999 <li key={bucket.label} className="grid grid-cols-[6.5rem_1fr_1.5rem] items-center gap-2">
1000 <span className="text-faint">{bucket.label}</span>
1001 <span className="block h-1.5 overflow-hidden rounded-full bg-line">
1002 <span
1003 className={`block h-full rounded-full ${bucket.label === "Older" ? "bg-warn" : "bg-fg-soft/60"}`}
1004 style={{ width: `${(bucket.count / oldest) * 100}%` }}
1005 />
1006 </span>
1007 <span className="text-right tabular-nums text-muted">{bucket.count}</span>
Project dependencies: addresses, preview stacks, Affects, and agents who know1008 </li>
1009 ))}
1010 </ul>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1011 ) : (
1012 <p className="mt-1 text-faint">Issues could not be loaded just now.</p>
1013 )}
1014 </div>
Project dependencies: addresses, preview stacks, Affects, and agents who know1015 </div>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1016 </section>
Project dependencies: addresses, preview stacks, Affects, and agents who know1017
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1018 <section className="rounded-xl border border-line bg-surface p-5">
1019 <div className="flex items-center justify-between">
1020 <h2 className="flex items-center gap-1.5 text-sm font-semibold">
1021 <Network size={14} className="text-faint" />
1022 Dependencies
1023 </h2>
1024 {member && (
1025 <Link to={`${base}/settings/dependencies`} className="text-xs text-muted hover:text-fg">
1026 Manage
1027 </Link>
1028 )}
1029 </div>
1030 {!dependencies ? (
1031 <p className="mt-3 text-xs text-muted">Dependencies could not be loaded just now.</p>
1032 ) : dependencies.dependsOn.length === 0 && dependencies.usedBy.length === 0 ? (
1033 <p className="mt-3 text-xs leading-5 text-muted">
1034 Uses no other project, and none uses it. Declare one, and builds get its address and agents know what depends on
1035 what.
1036 </p>
1037 ) : (
1038 <div className="mt-3 space-y-3 text-xs">
1039 {dependencies.dependsOn.length > 0 && (
1040 <div>
1041 <p className="flex items-center gap-1 text-muted">
1042 <ArrowUpRight size={12} /> Uses
1043 </p>
1044 <ul className="mt-1.5 space-y-1">
1045 {dependencies.dependsOn.map((d) => (
1046 <li key={d.slug} className="flex items-center justify-between gap-2">
1047 <Link to={`/${params.owner}/${d.slug}`} className="font-medium hover:underline">
1048 {d.name}
1049 </Link>
1050 {d.as && <code className="font-mono text-faint">{d.as}</code>}
1051 </li>
1052 ))}
1053 </ul>
1054 </div>
1055 )}
1056 {dependencies.usedBy.length > 0 && (
1057 <div>
1058 <p className="flex items-center gap-1 text-muted">
1059 <ArrowDownLeft size={12} /> Used by
1060 </p>
1061 <ul className="mt-1.5 space-y-1">
1062 {dependencies.usedBy.map((d) => (
1063 <li key={d.slug}>
1064 <Link to={`/${params.owner}/${d.slug}`} className="font-medium hover:underline">
1065 {d.name}
1066 </Link>
1067 </li>
1068 ))}
1069 </ul>
1070 </div>
1071 )}
1072 </div>
1073 )}
Projects: what a workspace builds and runs, first on every page1074 </section>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1075
1076 {source && (
1077 <section className="rounded-xl border border-line bg-surface p-5">
1078 <h2 className="text-sm font-semibold">Clone</h2>
1079 <div className="mt-3">
1080 <CopyLine text={`git clone https://g1t.sh/${source.repo.namespace}/${source.repo.name}.git`} />
1081 </div>
1082 </section>
1083 )}
1084 </aside>
1085 </div>
Projects: what a workspace builds and runs, first on every page1086 </div>
1087 );
1088}