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

1,090 lines50,111 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
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights24import { type AgentRun, type G1tEvent, type Memory, type Pull, RUN_KIND_LABEL, isActiveRun, workOwner } from "@g1t/contracts";
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API25
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 }
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights255 // Never your own, nor one g1t made for you; whoever it is for asked.
256 const owner = workOwner(pull).username;
257 if (me && pull.status === "open" && owner.toLowerCase() !== me && pull.reviewers.some((name) => name.toLowerCase() === me)) {
258 needs.push({ key: `review:${pull.number}`, kind: "review", title: pull.title, detail: `${owner} asked for your review.`, to, action: "Review", at: Date.parse(pull.updatedAt), where });
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API259 }
260 if (member && pull.status === "open" && pull.checkStatus === "failed" && !runOn.has(pull.number) && !lifecycle) {
Fast pages, required checks on the branch, self-hosted runners, honest incidents261 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 API262 }
263 }
264 for (const run of live) {
265 const minutes = stuckMinutes(run, now);
266 if (minutes == null) continue;
267 needs.push({
268 key: `run:${run.id}`,
269 kind: "stuck",
270 title: run.title ?? `${run.agent}'s run`,
271 detail: `${run.agent} has reported nothing for ${minutes} min${run.step ? `. Last: ${run.step}` : ""}.`,
272 to: `${base}/agents/runs/${run.id}`,
273 action: "Look",
274 at: Date.parse(run.updatedAt),
275 where: run.number != null ? `#${run.number}` : null,
276 });
277 }
278
279 // --- Activity and health -------------------------------------------------------
280 const eventList: G1tEvent[] = recent ?? [];
281 const items = eventList
282 .map((event) => eventItem(event, path))
283 .filter((item): item is ActivityItem => item != null);
284 const checkEvents = eventList.flatMap((event) =>
285 event.type === "checks.completed" ? [{ repo: "", number: event.data.number, at: Date.parse(event.time), passed: event.data.status === "passed" }] : [],
286 );
287 const memoryList = ok(memories)?.project ?? [];
288 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);
289 const openIssues = ok(issues);
290
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look291 // --- Getting to production ----------------------------------------------------
292 const wentLive = (status: string) => status === "ready" || status === "replaced" || status === "down";
293 const liveApps = ok(list)?.live ?? [];
294 const commitNow = ok(log)?.[0] ?? null;
295 const projectValue = ok(project);
296 const checklist = member
297 ? productionChecklist({
298 base,
299 hasCode: commitNow != null || projectValue?.source.kind === "mirror",
300 deploysEnabled: ok(settings)?.enabled ?? false,
301 productionDeployed:
302 liveApps.some((app) => app.kind === "production") ||
303 builds.some((build) => build.kind === "production" && wentLive(build.status)),
304 domains: ok(domains)?.domains.length ?? null,
305 previewOpened:
306 liveApps.some((app) => app.kind === "preview") || builds.some((build) => build.kind === "preview" && wentLive(build.status)),
307 instructions: commitNow == null ? false : root?.ok ? hasInstructions(root.value.entries.map((entry) => entry.name)) : null,
308 agentAssigned: agentWasAssigned({
309 runAgents: runList.map((run) => run.agent),
310 pullAgents: [...openPulls, ...(ok(closed) ?? [])].map((pull) => pull.agent),
311 issues: openIssues ?? [],
312 }),
313 })
314 : null;
315
316 const github = await githubP;
Projects: what a workspace builds and runs, first on every page317 return {
318 member,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look319 can,
320 github,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API321 project: ok(project),
322 settings: ok(settings),
323 builds: builds.slice(0, 30),
324 live: ok(list)?.live ?? [],
325 commit: ok(log)?.[0] ?? null,
326 open: ok(counts) ?? { issues: openIssues?.length ?? 0, pulls: openPulls.length },
327 dependencies: ok(deps),
328 agentsLive: live.slice(0, 6),
329 runsLoaded: runs?.ok ?? false,
330 pullsLoaded: open?.ok ?? false,
331 columns,
332 needs: rankNeeds(needs),
333 landed: landed.slice(0, MAX_LANDED),
334 groups: groupActivity(items).slice(0, 30),
335 eventsLoaded: recent != null,
336 health: {
337 passRate: passRate(checkEvents.map((c) => c.passed)),
338 firstPass: firstPassRate(checkEvents),
339 checkRuns: checkEvents.length,
340 ages: openIssues ? ageBuckets(openIssues.map((issue) => issue.createdAt), now) : null,
341 },
342 knows: knows as Memory[],
343 memoryCount: memoryList.length,
344 memoriesLoaded: memories?.ok ?? false,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look345 checklist,
346 branches,
Projects: what a workspace builds and runs, first on every page347 };
348}
349
350export async function action({ request, params, context }: Route.ActionArgs) {
351 assertSameOrigin(request);
352 const user = requireUser(context, request);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look353 const form = await request.formData().catch(() => null);
354 const intent = form?.get("intent");
355 // Syncing from GitHub is pushing; stopping it is an integration; deploying is compute.
356 const refused = await refusal(context, params, intent === "github-sync" ? "push" : intent === "github-stop" ? "manage_integrations" : "run");
357 if (refused) return { error: refused };
358 if (intent === "github-sync" || intent === "github-stop") {
359 const repo = await repos.get({ namespace: params.owner, name: params.repo }, user);
360 if (!repo.ok) return { error: repo.error.message };
361 const done =
362 intent === "github-sync" ? await githubApp.sync(user, repo.value.id) : await githubApp.unlinkRepo(user, repo.value.id);
363 return done.ok
364 ? { notice: intent === "github-sync" ? "Synced with GitHub." : "It is no longer kept in step with GitHub." }
365 : { error: done.error.message };
366 }
Projects: what a workspace builds and runs, first on every page367 const started = await deployments.redeploy(user, { workspace: params.owner, slug: params.repo }, null);
368 return started.ok ? { notice: "Production is building." } : { error: started.error.message };
369}
370
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily371type Loaded = Awaited<ReturnType<typeof overviewData>>;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API372type Card = Loaded["columns"]["working"][number];
373
374const STAGE_TONE: Record<PipelineStage, string> = {
375 working: "bg-merged",
376 checking: "bg-info",
377 reviewing: "bg-warn",
378 queue: "bg-accent-dim",
379 landed: "bg-accent",
380};
381
382function PipelineCard({ card, base, stage }: { card: Card; base: string; stage: PipelineStage }) {
383 return (
384 <li>
385 <Link
386 to={`${base}/pull/${card.number}`}
387 prefetch="intent"
388 className={`block rounded-lg border bg-bg/60 p-2.5 transition-colors hover:border-line-strong hover:bg-raised ${
389 card.needsYou ? "border-warn/50" : card.step ? "border-merged/30" : "border-line"
390 }`}
391 >
392 <span className="line-clamp-2 text-[0.8125rem] leading-snug font-medium">{card.title}</span>
393 <span className="mt-1.5 flex items-center gap-1.5 text-[0.6875rem] text-muted">
394 <Avatar name={card.agent} size={14} />
395 <span className="font-mono text-faint">#{card.number}</span>
396 <span className="grow" />
397 {card.needsYou ? (
398 <span className="inline-flex items-center gap-1 text-warn" title="Stopped: a person decides what happens next">
399 <Hand size={11} /> you
400 </span>
401 ) : stage === "landed" ? (
402 <TimeAgo at={card.at} />
403 ) : card.runStarted ? (
404 <span className="inline-flex items-center gap-1 text-merged">
405 <Loader2 size={11} className="animate-spin" />
406 <Elapsed from={card.runStarted} />
407 </span>
408 ) : (
409 <CheckBadge status={card.checkStatus} />
410 )}
411 </span>
412 {card.step && (
413 <span className="mt-1.5 block truncate font-mono text-[0.6875rem] text-fg/70" title={card.step}>
414 {card.runKind}: {card.step}
415 </span>
416 )}
417 </Link>
418 </li>
419 );
420}
421
422/** Open pull requests moving through Working → Checking → Reviewing → Queue → Landed. */
423function Pipeline({ columns, base }: { columns: Loaded["columns"]; base: string }) {
424 return (
425 <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 look426 {/* Each stage as tall as what is in it: an empty one stays short
427 instead of stretching to the busiest. */}
428 <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 API429 {PIPELINE.map(({ stage, label }, index) => {
430 const cards = columns[stage];
431 return (
432 <li key={stage} className="flex min-w-0 flex-col rounded-xl border border-line bg-surface p-2">
433 <p className="flex items-center gap-2 px-1 pt-0.5 pb-2 text-xs font-medium text-muted">
434 <span className={`size-1.5 rounded-full ${STAGE_TONE[stage]} ${cards.length && stage !== "landed" ? "animate-pulse" : ""}`} />
435 {label}
436 <span className="tabular-nums text-faint">{cards.length}</span>
437 {index < PIPELINE.length - 1 && <ArrowRight size={11} className="ml-auto text-line-strong" />}
438 </p>
439 {cards.length === 0 ? (
440 <p className="rounded-lg border border-dashed border-line/70 px-2 py-3 text-center text-[0.6875rem] text-faint">Empty</p>
441 ) : (
442 <ul className="space-y-1.5">
443 {cards.slice(0, 6).map((card) => (
444 <PipelineCard key={card.number} card={card} base={base} stage={stage} />
445 ))}
446 {cards.length > 6 && (
447 <li className="px-1 text-[0.6875rem] text-muted">
448 <Link to={`${base}/pulls`} className="hover:text-fg">
449 {cards.length - 6} more
450 </Link>
451 </li>
452 )}
453 </ul>
454 )}
455 </li>
456 );
457 })}
458 </ol>
459 </div>
460 );
461}
462
463function Stat({ label, value, to }: { label: string; value: ReactNode; to?: string }) {
464 const body = (
465 <>
466 <span className="block text-[0.6875rem] text-faint">{label}</span>
467 <span className="mt-0.5 block truncate text-[0.8125rem] font-medium">{value}</span>
468 </>
469 );
470 return to ? (
471 <Link to={to} className="min-w-0 px-5 py-3 transition-colors hover:bg-raised/50">
472 {body}
473 </Link>
474 ) : (
475 <div className="min-w-0 px-5 py-3">{body}</div>
476 );
477}
478
Projects: what a workspace builds and runs, first on every page479export default function ProjectOverview({ loaderData, actionData, params }: Route.ComponentProps) {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily480 return (
481 <Suspense fallback={<OverviewSkeleton />}>
482 <Await resolve={loaderData.overview} errorElement={<OverviewFailed />}>
483 {(overview) => <Overview loaderData={overview} actionData={actionData} params={params} />}
484 </Await>
485 </Suspense>
486 );
487}
488
489/** The overview's sections as they will sit, while they stream in. */
490function OverviewSkeleton() {
491 return (
492 <div className="space-y-8" aria-busy="true" aria-label="Loading the overview">
493 <section className="rounded-2xl border border-line bg-surface p-5 sm:p-6">
494 <Skeleton className="h-3 w-24" />
495 <Skeleton className="mt-3 h-5 w-64 max-w-full" />
496 <Skeleton className="mt-3 h-3 w-48 max-w-full" />
497 </section>
498 <section className="rounded-2xl border border-line bg-surface p-2">
499 <div className="grid grid-cols-2 gap-2 sm:grid-cols-5">
500 {Array.from({ length: 5 }, (_, index) => (
501 <Skeleton key={index} className="h-24 rounded-xl" />
502 ))}
503 </div>
504 </section>
505 <div className="grid gap-6 lg:grid-cols-2">
506 <section className="rounded-2xl border border-line bg-surface">
507 <SkeletonRows rows={4} />
508 </section>
509 <section className="rounded-2xl border border-line bg-surface">
510 <SkeletonRows rows={4} />
511 </section>
512 </div>
513 </div>
514 );
515}
516
517function OverviewFailed() {
518 return (
519 <p className="rounded-2xl border border-line bg-surface p-6 text-sm text-muted">
520 The overview could not be loaded just now. Reload the page to try again.
521 </p>
522 );
523}
524
525function Overview({
526 loaderData,
527 actionData,
528 params,
529}: {
530 loaderData: Loaded;
531 actionData: Route.ComponentProps["actionData"];
532 params: Route.ComponentProps["params"];
533}) {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look534 const { member, project, settings, builds, live, commit, open, dependencies, agentsLive, columns, needs, landed, groups, health, knows, checklist, branches } =
535 loaderData;
Projects: what a workspace builds and runs, first on every page536 const base = `/${params.owner}/${params.repo}`;
537 const production = live.find((app) => app.kind === "production") ?? null;
Agents and memory, checks and conflicts, profiles, slug renames, custom domains538 // The project's own domain, once it is active, is where production is visited.
539 const productionUrl = production ? (settings?.primaryDomain ? `https://${settings.primaryDomain}` : production.url) : null;
Projects: what a workspace builds and runs, first on every page540 const previews = live.filter((app) => app.kind === "preview");
541 const latestProduction = builds.find((build) => build.kind === "production") ?? null;
542 const busy = useNavigation().state === "submitting";
543 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 API544 const moving = agentsLive.length > 0 || columns.working.length + columns.checking.length > 0;
545 useLiveRefresh(moving);
546 const ages = health.ages;
547 const oldest = ages ? Math.max(1, ...ages.map((a) => a.count)) : 1;
Projects: what a workspace builds and runs, first on every page548
549 return (
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API550 <div className="space-y-8">
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look551 {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 API552 {/* What the project is, running, and where its code is. */}
553 <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 look554 <div className="flex flex-col gap-5 p-5 sm:flex-row sm:items-start sm:p-6">
555 {member && (production || settings?.enabled) && (
556 <ProductionShot
557 src={production ? `${base}/production.jpg?v=${production.commit}` : null}
558 href={production ? (productionUrl ?? production.url) : null}
559 label={host(production ? (productionUrl ?? production.url) : (settings?.productionUrl ?? ""))}
560 className="w-full shrink-0 sm:w-60 lg:w-72"
561 />
562 )}
563 <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 API564 <div className="min-w-0">
565 <p className="flex items-center gap-2 text-xs font-medium tracking-wide text-muted uppercase">
566 <Rocket size={13} className="text-accent" />
567 Production
568 </p>
569 {production ? (
570 <>
571 <a
572 href={productionUrl ?? production.url}
573 className="mt-2 flex items-center gap-1.5 truncate font-mono text-lg font-medium hover:text-accent"
574 >
575 {host(productionUrl ?? production.url)}
576 <ArrowUpRight size={16} className="shrink-0 text-faint" />
577 </a>
578 <p className="mt-1.5 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted">
579 {latestProduction && <StatusDot status={latestProduction.status} label={latestProduction.status === "ready" ? "Live" : undefined} />}
580 <span className="inline-flex items-center gap-1 font-mono">
581 <GitCommitHorizontal size={13} className="text-faint" />
582 {production.commit.slice(0, 7)}
583 </span>
584 <span>
585 deployed <TimeAgo at={production.deployedAt} />
586 </span>
587 {settings?.primaryDomain && <span className="text-faint">also at {host(production.url)}</span>}
588 </p>
589 </>
590 ) : settings?.enabled ? (
591 <>
592 <p className="mt-2 font-mono text-lg text-muted">{host(settings.productionUrl)}</p>
593 <p className="mt-1.5 text-xs text-muted">
594 {latestProduction ? (
595 <StatusDot status={latestProduction.status} label={latestProduction.status === "failed" ? "The last build failed" : undefined} />
596 ) : (
597 "Not deployed yet. Push to the default branch, or deploy it now."
598 )}
599 </p>
600 </>
601 ) : (
602 <>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look603 <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 API604 <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 look605 Nothing builds or runs until you turn them on. Then {project?.name ?? params.repo} goes up on g1t.page:
606 production from the default branch, and a live preview for every pull request. It runs only while
607 someone visits.
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API608 </p>
609 </>
610 )}
611 </div>
612 {member && (
613 <div className="flex shrink-0 items-center gap-2">
614 {production && (
615 <ButtonLink to={productionUrl ?? production.url} variant="accent" reloadDocument>
616 Visit
617 <ArrowUpRight size={14} />
618 </ButtonLink>
619 )}
620 {settings?.enabled ? (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look621 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 API622 <Button type="submit" variant="quiet" disabled={busy} title="Build production again from the default branch">
623 <RotateCw size={14} />
624 Redeploy
625 </Button>
626 </Form>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look627 ) : loaderData.can.manage_integrations && (
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API628 <ButtonLink to={`${base}/settings/deployments`} variant="accent">
629 <Rocket size={14} />
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look630 Turn on deployments
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API631 </ButtonLink>
Projects: what a workspace builds and runs, first on every page632 )}
633 </div>
634 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look635 </div>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API636 </div>
637 {actionData && (
638 <p className={`border-t border-line px-6 py-2.5 text-sm ${"error" in actionData ? "text-danger" : "text-accent"}`}>
639 {"error" in actionData ? actionData.error : actionData.notice}
640 </p>
641 )}
642 <div className="grid grid-cols-2 divide-line border-t border-line text-sm sm:grid-cols-4 sm:divide-x">
643 <Stat
644 label="Source"
645 to={`${base}/code`}
646 value={
647 source ? (
648 <span className="inline-flex items-center gap-1.5 font-mono">
649 {project?.private ? <Lock size={12} className="text-faint" /> : <Code2 size={12} className="text-faint" />}
650 {source.repo.name}
651 {source.rootDir ? `/${source.rootDir}` : ""}
652 </span>
653 ) : (
654 "—"
655 )
656 }
657 />
658 <Stat
659 label="Default branch"
660 to={`${base}/commits`}
661 value={
662 <span className="inline-flex items-center gap-1.5 font-mono">
663 <GitBranch size={12} className="text-faint" />
664 {source?.defaultBranch ?? "main"}
665 </span>
666 }
667 />
668 <Stat
669 label="Latest commit"
670 to={commit ? `${base}/commit/${commit.hash}` : undefined}
671 value={
672 commit ? (
673 <span title={commit.message.split("\n")[0]}>
674 <span className="font-mono">{commit.hash.slice(0, 7)}</span>{" "}
675 <span className="font-normal text-muted">
676 <TimeAgo at={commit.authoredAt} />
677 </span>
678 </span>
679 ) : (
680 "No commits yet"
681 )
682 }
683 />
684 <Stat
685 label={member && settings?.enabled ? "Last deploy" : "Open"}
686 to={member && settings?.enabled ? `${base}/deployments` : `${base}/pulls`}
687 value={
688 member && settings?.enabled ? (
689 latestProduction ? (
690 <span className="inline-flex items-center gap-2">
691 <StatusDot status={latestProduction.status} />
692 <span className="font-normal text-muted">
693 <TimeAgo at={latestProduction.createdAt} />
Projects: what a workspace builds and runs, first on every page694 </span>
695 </span>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API696 ) : (
697 "None yet"
698 )
699 ) : (
700 `${open.pulls} pull requests · ${open.issues} issues`
701 )
702 }
703 />
704 </div>
705 {commit && (
706 <p className="truncate border-t border-line px-5 py-2.5 text-xs text-muted sm:px-6">
707 <GitCommitHorizontal size={12} className="mr-1.5 inline text-faint" />
708 {commit.message.split("\n")[0]} <span className="text-faint">· {commit.author.name}</span>
709 </p>
710 )}
711 </section>
712
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look713 {checklist && <ProductionChecklist base={base} items={checklist} />}
714
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API715 <Panel
716 title="Right now"
717 icon={<span className={`block size-2 rounded-full ${agentsLive.length > 0 ? "animate-pulse bg-merged" : "bg-line-strong"}`} />}
718 count={agentsLive.length}
719 all={{ to: `${base}/agents`, label: "Agents" }}
720 >
721 {agentsLive.length > 0 && (
722 <ul className="mb-3 grid grid-cols-1 gap-2 sm:grid-cols-2 xl:grid-cols-3">
723 {agentsLive.map((run) => (
724 <li key={run.id} className="min-w-0 rounded-xl border border-merged/30 bg-surface px-3.5 py-3">
725 <div className="flex items-center gap-2 text-sm">
726 <Avatar name={run.agent} size={18} />
727 <span className="font-medium">{run.agent}</span>
728 <span className="text-xs text-muted">{RUN_KIND_LABEL[run.kind].toLowerCase()}</span>
729 <span className="grow" />
730 <span className="text-xs text-muted">
731 <Elapsed from={run.startedAt ?? run.createdAt} />
Projects: what a workspace builds and runs, first on every page732 </span>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API733 </div>
734 <Link
735 to={run.number != null ? `${base}/pull/${run.number}` : `${base}/agents/runs/${run.id}`}
736 className="mt-1 block truncate text-[0.8125rem] hover:text-accent"
737 >
738 {run.title ?? "A run"} {run.number != null && <span className="text-faint">#{run.number}</span>}
739 </Link>
740 {run.step && (
741 <p className="mt-1 truncate font-mono text-[0.6875rem] text-fg/70" title={run.step}>
742 <span className="mr-1.5 inline-block size-1.5 animate-pulse rounded-full bg-merged align-middle" />
743 {run.step}
744 </p>
745 )}
746 <p className="mt-1.5 flex items-center gap-3 text-[0.6875rem] text-faint">
747 {formatCost(run.costUsd) && <span>{formatCost(run.costUsd)} so far</span>}
748 <Link to={`${base}/agents/runs/${run.id}`} className="hover:text-fg">
749 {run.stepCount} steps
750 </Link>
751 {run.number != null && (
752 <Link to={`${base}/sessions/${run.number}`} className="hover:text-fg">
753 Session
754 </Link>
755 )}
756 </p>
757 </li>
758 ))}
759 </ul>
760 )}
761 {!loaderData.pullsLoaded ? (
762 <Unavailable what="Pull requests" />
763 ) : Object.values(columns).every((c) => c.length === 0) ? (
764 <Quiet
765 action={
766 member ? (
767 <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">
768 <Bot size={12} /> Open an issue for an agent
769 </Link>
770 ) : null
771 }
772 >
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent773 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 API774 </Quiet>
775 ) : (
776 <Pipeline columns={columns} base={base} />
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 </Panel>
Projects: what a workspace builds and runs, first on every page779
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API780 <div className="grid gap-x-8 gap-y-8 lg:grid-cols-[minmax(0,1fr)_20rem]">
781 <div className="min-w-0 space-y-8">
782 {needs.length > 0 && (
783 <Panel title="Needs you" icon={<Hand size={14} className="text-warn" />} count={needs.length}>
784 <NeedsList needs={needs} limit={5} />
785 </Panel>
786 )}
787
Fast pages, required checks on the branch, self-hosted runners, honest incidents788 <Suspense
789 fallback={
790 <Panel title="Active branches" icon={<GitBranch size={14} />}>
791 <div aria-busy="true">
792 <SkeletonRows rows={3} rowClassName="h-12" />
793 </div>
794 </Panel>
795 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look796 >
Fast pages, required checks on the branch, self-hosted runners, honest incidents797 <Await resolve={branches}>
798 {(branches) => (
799 <Panel title="Active branches" icon={<GitBranch size={14} />} count={branches?.total || null}>
800 {!branches ? (
801 <Unavailable what="Branches" />
802 ) : branches.shown.length === 0 ? (
803 <Quiet>
804 Only {branches.main} so far. Branches pushed here show with how far each has moved from {branches.main}, and
805 its pull request and preview.
806 </Quiet>
807 ) : (
808 <>
809 <ActiveBranches branches={branches.shown} base={base} main={branches.main} />
810 {branches.total > branches.shown.length && (
811 <p className="mt-2 px-1 text-xs text-faint">
812 The {branches.shown.length} most recently changed of {branches.total} branches.
813 </p>
814 )}
815 </>
816 )}
817 </Panel>
818 )}
819 </Await>
820 </Suspense>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look821
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API822 <Panel title="Recent changes" icon={<GitMerge size={14} />} all={{ to: `${base}/pulls?state=closed`, label: "All landed" }}>
823 {landed.length === 0 ? (
824 <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 page825 ) : (
826 <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 API827 {landed.map((pull) => {
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent828 const byAgent = pull.runtime === "hosted" || madeByG1t(pull);
Projects: what a workspace builds and runs, first on every page829 return (
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API830 <li key={pull.id} className="flex items-start gap-3 px-4 py-3">
831 <GitMerge size={15} className="mt-0.5 shrink-0 text-merged" />
832 <span className="min-w-0 grow">
833 <Link to={`${base}/pull/${pull.number}`} prefetch="intent" className="block truncate text-sm font-medium hover:text-accent">
834 {pull.title}
835 </Link>
836 <span className="mt-0.5 flex flex-wrap items-center gap-x-1.5 text-xs text-muted">
837 <span className="font-mono text-faint">#{pull.number}</span>
838 <span>·</span>
839 <span className="inline-flex items-center gap-1">
840 <Avatar name={pull.agent} size={13} />
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights841 {pull.requestedBy
842 ? `made by ${pull.author.username} for ${pull.requestedBy.username}`
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent843 : byAgent
844 ? `made by ${pull.agent}`
845 : `by ${pull.author.username}`}
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API846 </span>
847 {pull.issue != null && (
848 <>
849 <span>· for</span>
850 <Link to={`${base}/issues/${pull.issue}`} className="hover:text-fg">
851 #{pull.issue}
852 </Link>
853 </>
854 )}
855 {pull.mergedBy && <span>· landed by {pull.mergedBy}</span>}
856 <span>
857 · <TimeAgo at={pull.mergedAt ?? pull.updatedAt} />
Projects: what a workspace builds and runs, first on every page858 </span>
859 </span>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API860 </span>
861 <span className="hidden shrink-0 text-xs text-faint sm:block">
862 <ChangeSize files={pull.files} />
863 </span>
864 {byAgent && (
865 <Link to={`${base}/sessions/${pull.number}`} className="shrink-0 text-xs text-muted hover:text-fg">
866 Session
867 </Link>
868 )}
Projects: what a workspace builds and runs, first on every page869 </li>
870 );
871 })}
872 </ul>
873 )}
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API874 </Panel>
Projects: what a workspace builds and runs, first on every page875
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API876 <Panel title="Activity" icon={<ActivityIcon size={14} />}>
877 {loaderData.eventsLoaded ? (
878 <ActivityFeed groups={groups} showRepo={false} limit={8} empty="Nothing has happened here yet." />
879 ) : (
880 <Unavailable what="Activity" />
881 )}
882 </Panel>
883
884 {previews.length > 0 && (
885 <Panel title="Previews" icon={<GitBranch size={14} />} count={previews.length} all={{ to: `${base}/deployments`, label: "Deployments" }}>
886 <ul className="divide-y divide-line overflow-hidden rounded-xl border border-line bg-surface">
887 {previews.map((app) => (
888 <li key={app.url} className="flex items-center gap-3 px-4 py-2.5 text-sm">
889 <span className="min-w-0 grow">
890 <a href={app.url} className="block truncate font-mono text-[0.8125rem] hover:text-accent">
891 {host(app.url)}
892 </a>
893 <span className="text-xs text-muted">
894 {app.branch}
895 {app.number != null && (
896 <>
897 {" · "}
898 <Link to={`${base}/pull/${app.number}`} className="hover:underline">
899 #{app.number}
900 </Link>
901 </>
902 )}
903 </span>
Projects: what a workspace builds and runs, first on every page904 </span>
905 <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 API906 <TimeAgo at={app.deployedAt} />
Projects: what a workspace builds and runs, first on every page907 </span>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API908 </li>
909 ))}
910 </ul>
911 </Panel>
912 )}
913 </div>
Projects: what a workspace builds and runs, first on every page914
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API915 <aside className="space-y-4 text-sm">
916 {member && (
917 <section className="rounded-xl border border-line bg-surface p-5">
918 <div className="flex items-center justify-between">
919 <h2 className="flex items-center gap-1.5 text-sm font-semibold">
920 <Brain size={14} className="text-merged" />
921 What agents know here
922 </h2>
923 <Link to={`${base}/memory`} className="text-xs text-muted hover:text-fg">
924 Memory
925 </Link>
Projects: what a workspace builds and runs, first on every page926 </div>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API927 {!loaderData.memoriesLoaded ? (
928 <p className="mt-3 text-xs text-muted">Memory could not be loaded just now.</p>
929 ) : knows.length === 0 ? (
930 <p className="mt-3 text-xs leading-5 text-muted">
931 Nothing yet. Agents note conventions, decisions and traps as they work, and every agent starting here reads them.
932 </p>
933 ) : (
934 <ul className="mt-3 space-y-2.5">
935 {knows.map((memory) => (
936 <li key={memory.id} className="text-xs leading-5">
937 <p className="line-clamp-3 text-fg-soft">
938 {memory.pinned && <Pin size={11} className="mr-1 inline text-accent" />}
939 {memory.text}
940 </p>
941 <p className="text-faint">
942 {memory.kind} · {memory.createdBy} · <TimeAgo at={memory.createdAt} />
943 </p>
944 </li>
945 ))}
946 {loaderData.memoryCount > knows.length && (
947 <li>
948 <Link to={`${base}/memory`} className="text-xs text-muted hover:text-fg">
949 {loaderData.memoryCount - knows.length} more
950 </Link>
951 </li>
952 )}
953 </ul>
954 )}
955 </section>
Projects: what a workspace builds and runs, first on every page956 )}
957
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API958 <section className="rounded-xl border border-line bg-surface p-5">
959 <h2 className="flex items-center gap-1.5 text-sm font-semibold">
960 <HeartPulse size={14} className="text-faint" />
961 Health
Project dependencies: addresses, preview stacks, Affects, and agents who know962 </h2>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API963 <div className="mt-4 space-y-4 text-xs">
964 <div>
965 <div className="flex items-baseline justify-between">
966 <span className="text-muted">Checks passing</span>
967 <span className="font-medium tabular-nums">{percent(health.passRate)}</span>
968 </div>
969 <div className="mt-1.5">
970 <Meter value={health.passRate} tone={health.passRate != null && health.passRate < 0.7 ? "bg-warn" : "bg-accent"} />
971 </div>
972 <p className="mt-1 text-faint">
973 {health.checkRuns
974 ? `${health.checkRuns} recent runs · ${percent(health.firstPass.rate)} pass on the first try`
975 : "No checks have run recently."}
976 </p>
977 </div>
978 {member && (
Project dependencies: addresses, preview stacks, Affects, and agents who know979 <div>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API980 <div className="flex items-baseline justify-between">
981 <span className="text-muted">Deploys</span>
982 <Link to={`${base}/deployments`} className="text-faint hover:text-fg">
983 {builds.length ? `last ${Math.min(builds.length, 20)}` : "none yet"}
984 </Link>
985 </div>
986 <div className="mt-1.5">
987 <DeployStrip builds={builds} base={base} />
988 </div>
Project dependencies: addresses, preview stacks, Affects, and agents who know989 </div>
990 )}
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API991 <div>
992 <div className="flex items-baseline justify-between">
993 <span className="text-muted">Open issues by age</span>
994 <Link to={`${base}/issues`} className="font-medium tabular-nums hover:text-accent">
995 {open.issues}
996 </Link>
997 </div>
998 {ages ? (
999 <ul className="mt-2 space-y-1.5">
1000 {ages.map((bucket) => (
1001 <li key={bucket.label} className="grid grid-cols-[6.5rem_1fr_1.5rem] items-center gap-2">
1002 <span className="text-faint">{bucket.label}</span>
1003 <span className="block h-1.5 overflow-hidden rounded-full bg-line">
1004 <span
1005 className={`block h-full rounded-full ${bucket.label === "Older" ? "bg-warn" : "bg-fg-soft/60"}`}
1006 style={{ width: `${(bucket.count / oldest) * 100}%` }}
1007 />
1008 </span>
1009 <span className="text-right tabular-nums text-muted">{bucket.count}</span>
Project dependencies: addresses, preview stacks, Affects, and agents who know1010 </li>
1011 ))}
1012 </ul>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1013 ) : (
1014 <p className="mt-1 text-faint">Issues could not be loaded just now.</p>
1015 )}
1016 </div>
Project dependencies: addresses, preview stacks, Affects, and agents who know1017 </div>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1018 </section>
Project dependencies: addresses, preview stacks, Affects, and agents who know1019
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1020 <section className="rounded-xl border border-line bg-surface p-5">
1021 <div className="flex items-center justify-between">
1022 <h2 className="flex items-center gap-1.5 text-sm font-semibold">
1023 <Network size={14} className="text-faint" />
1024 Dependencies
1025 </h2>
1026 {member && (
1027 <Link to={`${base}/settings/dependencies`} className="text-xs text-muted hover:text-fg">
1028 Manage
1029 </Link>
1030 )}
1031 </div>
1032 {!dependencies ? (
1033 <p className="mt-3 text-xs text-muted">Dependencies could not be loaded just now.</p>
1034 ) : dependencies.dependsOn.length === 0 && dependencies.usedBy.length === 0 ? (
1035 <p className="mt-3 text-xs leading-5 text-muted">
1036 Uses no other project, and none uses it. Declare one, and builds get its address and agents know what depends on
1037 what.
1038 </p>
1039 ) : (
1040 <div className="mt-3 space-y-3 text-xs">
1041 {dependencies.dependsOn.length > 0 && (
1042 <div>
1043 <p className="flex items-center gap-1 text-muted">
1044 <ArrowUpRight size={12} /> Uses
1045 </p>
1046 <ul className="mt-1.5 space-y-1">
1047 {dependencies.dependsOn.map((d) => (
1048 <li key={d.slug} className="flex items-center justify-between gap-2">
1049 <Link to={`/${params.owner}/${d.slug}`} className="font-medium hover:underline">
1050 {d.name}
1051 </Link>
1052 {d.as && <code className="font-mono text-faint">{d.as}</code>}
1053 </li>
1054 ))}
1055 </ul>
1056 </div>
1057 )}
1058 {dependencies.usedBy.length > 0 && (
1059 <div>
1060 <p className="flex items-center gap-1 text-muted">
1061 <ArrowDownLeft size={12} /> Used by
1062 </p>
1063 <ul className="mt-1.5 space-y-1">
1064 {dependencies.usedBy.map((d) => (
1065 <li key={d.slug}>
1066 <Link to={`/${params.owner}/${d.slug}`} className="font-medium hover:underline">
1067 {d.name}
1068 </Link>
1069 </li>
1070 ))}
1071 </ul>
1072 </div>
1073 )}
1074 </div>
1075 )}
Projects: what a workspace builds and runs, first on every page1076 </section>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1077
1078 {source && (
1079 <section className="rounded-xl border border-line bg-surface p-5">
1080 <h2 className="text-sm font-semibold">Clone</h2>
1081 <div className="mt-3">
1082 <CopyLine text={`git clone https://g1t.sh/${source.repo.namespace}/${source.repo.name}.git`} />
1083 </div>
1084 </section>
1085 )}
1086 </aside>
1087 </div>
Projects: what a workspace builds and runs, first on every page1088 </div>
1089 );
1090}