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

1,091 lines50,172 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";
Production thumbnails follow each deploy, not each commit, and old ones are cleared daily61import { shotVersion } from "./production-screenshot";
Projects: what a workspace builds and runs, first on every page62
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API63const MAX_LANDED = 6;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look64/** Branches read for the Active branches list, and shown. */
65const BRANCHES_READ = 10;
66const BRANCHES_SHOWN = 5;
67/** How far back each branch's history, and the default branch's, is read to count ahead and behind. */
68const BRANCH_DEPTH = 40;
69const MAIN_DEPTH = 120;
Projects: what a workspace builds and runs, first on every page70
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily71/**
72 * The overview streams: the layout's header and tabs (one repository
73 * lookup, one project) go out at once with a skeleton below them, and
74 * everything here, seventeen calls across six services, follows in the
75 * same response as it settles. Crawlers wait for all of it
76 * (entry.server.tsx). Before, the first byte waited on the slowest of them.
77 */
78export function loader({ params, context }: Route.LoaderArgs) {
79 return { overview: overviewData({ params, context }) };
80}
81
82async function overviewData({ params, context }: Pick<Route.LoaderArgs, "params" | "context">) {
Projects: what a workspace builds and runs, first on every page83 const viewer = getViewer(context);
84 const path = { namespace: params.owner, name: params.repo };
85 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 API86 const now = Date.now();
87 // A section whose service fails shows its own empty state; the page stays up.
88 const soft = <T,>(promise: Promise<T> | null): Promise<T | null> =>
89 promise ? promise.catch((error) => (console.warn("overview:", error), null)) : Promise.resolve(null);
Fast pages, required checks on the branch, self-hosted runners, honest incidents90 // People with a role of their own here see its running parts: deployments,
91 // memory, setup; everyone who can read it sees the rest. Not awaited: only
92 // those sections wait on the repository lookup, which the layout makes in
93 // this request too.
94 const accessP = accessTo(context, params);
95 const memberP = accessP.then((access) => access.insider);
96 const forMembers = <T,>(start: () => Promise<T>): Promise<T | null> =>
97 memberP.then((member) => (member ? soft(start()) : null));
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look98 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 API99 const eventsP = repoP.then((repo) => (repo?.ok ? soft(eventLog.list({ repoId: repo.value.id, limit: 150 })) : null));
100 const minePullsP = repoP.then((repo) =>
101 repo?.ok && viewer
102 ? soft(work.listActivePulls(viewer)).then((list) => (list ?? []).filter((item) => item.pull.repoId === repo.value.id))
103 : null,
104 );
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look105 const openP = soft(work.listPulls(path, viewer, "open"));
106 // Where its code came from on GitHub, for members.
Fast pages, required checks on the branch, self-hosted runners, honest incidents107 const githubP = repoP.then((repo) => (repo?.ok ? forMembers(() => githubApp.link(repo.value.id)) : null));
108 const listP = forMembers(() => deployments.list(ref, viewer));
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look109 // Active branches: the newest few besides the default, with how far each
110 // has moved. Branches with an open pull request are read first.
111 const branchesP = Promise.all([repoP, soft(repos.branches(path, viewer)), openP, listP]).then(
112 async ([repo, branchList, pulls, deploys]): Promise<{ main: string; total: number; shown: ActiveBranch[] } | null> => {
113 if (!repo?.ok || !branchList?.ok) return null;
114 const main = repo.value.defaultBranch;
115 const pullList = pulls?.ok ? pulls.value : [];
116 const pullOn = new Map(pullList.filter((pull) => pull.branch).map((pull) => [pull.branch as string, pull]));
117 const others = branchList.value.filter((branch) => branch.name !== main);
118 if (others.length === 0) return { main, total: 0, shown: [] };
119 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 incidents120 // By commit hash, not name: history from a commit never changes, so
121 // repos keeps it (services/repos/src/store.rs) and only new heads cost
122 // a walk.
123 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 look124 const [mainLog, ...logs] = await Promise.all([
Fast pages, required checks on the branch, self-hosted runners, honest incidents125 soft(repos.log(path, viewer, mainHead, MAIN_DEPTH)),
126 ...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 look127 ]);
128 const mainHashes = mainLog?.ok ? mainLog.value.map((c) => c.hash) : [];
129 const previews = deploys?.ok ? deploys.value.live.filter((app) => app.kind === "preview") : [];
130 const shown = read
131 .map((branch, index): ActiveBranch => {
132 const history = logs[index]?.ok ? logs[index].value : [];
133 const head = history[0];
134 const moved = drift(history.map((c) => c.hash), mainHashes, BRANCH_DEPTH);
135 const pull = pullOn.get(branch.name);
136 return {
137 name: branch.name,
138 commit: head ? { hash: head.hash, message: head.message.split("\n")[0], author: head.author.name, at: head.authoredAt } : null,
139 ...moved,
140 pull: pull ? { number: pull.number, title: pull.title, checkStatus: pull.checkStatus, draft: pull.status === "draft" } : null,
141 preview: previews.find((app) => app.branch === branch.name || (pull != null && app.number === pull.number))?.url ?? null,
142 };
143 })
144 .sort((a, b) => Date.parse(b.commit?.at ?? "0") - Date.parse(a.commit?.at ?? "0"))
145 .slice(0, BRANCHES_SHOWN);
146 return { main, total: others.length, shown };
147 },
148 );
Fast pages, required checks on the branch, self-hosted runners, honest incidents149 // Active branches read several logs each: streamed, so the rest shows first.
150 const branches = branchesP.catch(() => null);
151 const [{ insider: member, can }, project, settings, list, open, closed, log, counts, deps, runs, queue, issues, memories, recent, mine, domains, root] = await Promise.all([
152 accessP,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API153 soft(projects.get(params.owner, params.repo, viewer)),
Fast pages, required checks on the branch, self-hosted runners, honest incidents154 forMembers(() => deployments.settings(ref, viewer)),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look155 listP,
156 openP,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API157 soft(work.listPulls(path, viewer, "closed")),
158 soft(repos.log(path, viewer, null, 1)),
Fast pages, required checks on the branch, self-hosted runners, honest incidents159 soft(countsFor(context, params)),
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API160 soft(projects.dependencies(params.owner, params.repo, viewer)),
161 soft(agents.listRuns(viewer, { repo: path, limit: 60 })),
162 soft(work.queue(path, viewer)),
163 soft(work.listIssues(path, viewer, { state: "open" })),
Fast pages, required checks on the branch, self-hosted runners, honest incidents164 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 API165 eventsP,
166 minePullsP,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look167 // For the checklist, which only members see.
Fast pages, required checks on the branch, self-hosted runners, honest incidents168 forMembers(() => deployments.domains(ref, viewer)),
169 forMembers(() => repos.tree(path, viewer, null, "")),
Projects: what a workspace builds and runs, first on every page170 ]);
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API171 const ok = <T,>(result: { ok: true; value: T } | { ok: false } | null): T | null => (result?.ok ? result.value : null);
172
173 const openPulls = ok(open) ?? [];
174 const runList = ok(runs) ?? [];
175 const live = runList.filter((run) => isActiveRun(run.status));
176 const builds = ok(list)?.deployments ?? [];
177 const landed = (ok(closed) ?? [])
178 .filter((pull) => pull.status === "merged")
179 .sort((a, b) => Date.parse(b.mergedAt ?? b.updatedAt) - Date.parse(a.mergedAt ?? a.updatedAt));
180 const queued = queuedNumbers(ok(queue)?.active ?? []);
181 const runOn = new Map<number, AgentRun>();
182 for (const run of live) if (run.number != null && !runOn.has(run.number)) runOn.set(run.number, run);
183 const lifecycleOf = new Map((mine ?? []).map((item) => [item.pull.number, item.lifecycle]));
184
185 // --- The pipeline -----------------------------------------------------------
186 type Card = {
187 number: number;
188 title: string;
189 agent: string;
190 status: Pull["status"];
191 checkStatus: Pull["checkStatus"];
192 step: string | null;
193 runKind: string | null;
194 runStarted: string | null;
195 needsYou: boolean;
196 at: string;
197 };
198 const columns: Record<PipelineStage, Card[]> = { working: [], checking: [], reviewing: [], queue: [], landed: [] };
199 for (const pull of openPulls.slice(0, 40)) {
200 const run = runOn.get(pull.number);
Fast pages, required checks on the branch, self-hosted runners, honest incidents201 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 API202 number: pull.number,
203 title: pull.title,
204 agent: pull.agent,
205 status: pull.status,
206 checkStatus: pull.checkStatus,
207 step: run?.step ?? null,
208 runKind: run ? RUN_KIND_LABEL[run.kind] : null,
209 runStarted: run ? (run.startedAt ?? run.createdAt) : null,
210 needsYou: lifecycleOf.get(pull.number)?.stage === "needs_you",
211 at: pull.updatedAt,
212 });
213 }
214 for (const pull of landed.filter((p) => now - Date.parse(p.mergedAt ?? p.updatedAt) < 3 * TIME.DAY).slice(0, 5)) {
215 columns.landed.push({
216 number: pull.number,
217 title: pull.title,
218 agent: pull.agent,
219 status: pull.status,
220 checkStatus: pull.checkStatus,
221 step: null,
222 runKind: null,
223 runStarted: null,
224 needsYou: false,
225 at: pull.mergedAt ?? pull.updatedAt,
226 });
227 }
228
229 // --- Needs you ---------------------------------------------------------------
230 const base = `/${params.owner}/${params.repo}`;
231 const needs: Need[] = [];
232 const latestProduction = builds.find((build) => build.kind === "production") ?? null;
233 if (latestProduction?.status === "failed") {
234 needs.push({
235 key: "deploy",
236 kind: "deploy",
237 title: "The production build failed",
238 detail: latestProduction.error ?? "Production still serves the build before it.",
239 to: `${base}/deployments/${latestProduction.id}`,
240 action: "See the build",
241 at: Date.parse(latestProduction.finishedAt ?? latestProduction.createdAt),
242 where: latestProduction.commit.slice(0, 7),
243 });
244 }
245 const me = viewer?.username.toLowerCase();
246 for (const pull of openPulls) {
247 const to = `${base}/pull/${pull.number}`;
248 const where = `#${pull.number}`;
249 const lifecycle = lifecycleOf.get(pull.number);
250 if (lifecycle?.stage === "needs_you") {
251 const conflict = /conflict/i.test(lifecycle.detail);
252 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 });
253 } else if (lifecycle?.stage === "ready") {
254 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 });
255 }
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights256 // Never your own, nor one g1t made for you; whoever it is for asked.
257 const owner = workOwner(pull).username;
258 if (me && pull.status === "open" && owner.toLowerCase() !== me && pull.reviewers.some((name) => name.toLowerCase() === me)) {
259 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 API260 }
261 if (member && pull.status === "open" && pull.checkStatus === "failed" && !runOn.has(pull.number) && !lifecycle) {
Fast pages, required checks on the branch, self-hosted runners, honest incidents262 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 API263 }
264 }
265 for (const run of live) {
266 const minutes = stuckMinutes(run, now);
267 if (minutes == null) continue;
268 needs.push({
269 key: `run:${run.id}`,
270 kind: "stuck",
271 title: run.title ?? `${run.agent}'s run`,
272 detail: `${run.agent} has reported nothing for ${minutes} min${run.step ? `. Last: ${run.step}` : ""}.`,
273 to: `${base}/agents/runs/${run.id}`,
274 action: "Look",
275 at: Date.parse(run.updatedAt),
276 where: run.number != null ? `#${run.number}` : null,
277 });
278 }
279
280 // --- Activity and health -------------------------------------------------------
281 const eventList: G1tEvent[] = recent ?? [];
282 const items = eventList
283 .map((event) => eventItem(event, path))
284 .filter((item): item is ActivityItem => item != null);
285 const checkEvents = eventList.flatMap((event) =>
286 event.type === "checks.completed" ? [{ repo: "", number: event.data.number, at: Date.parse(event.time), passed: event.data.status === "passed" }] : [],
287 );
288 const memoryList = ok(memories)?.project ?? [];
289 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);
290 const openIssues = ok(issues);
291
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look292 // --- Getting to production ----------------------------------------------------
293 const wentLive = (status: string) => status === "ready" || status === "replaced" || status === "down";
294 const liveApps = ok(list)?.live ?? [];
295 const commitNow = ok(log)?.[0] ?? null;
296 const projectValue = ok(project);
297 const checklist = member
298 ? productionChecklist({
299 base,
300 hasCode: commitNow != null || projectValue?.source.kind === "mirror",
301 deploysEnabled: ok(settings)?.enabled ?? false,
302 productionDeployed:
303 liveApps.some((app) => app.kind === "production") ||
304 builds.some((build) => build.kind === "production" && wentLive(build.status)),
305 domains: ok(domains)?.domains.length ?? null,
306 previewOpened:
307 liveApps.some((app) => app.kind === "preview") || builds.some((build) => build.kind === "preview" && wentLive(build.status)),
308 instructions: commitNow == null ? false : root?.ok ? hasInstructions(root.value.entries.map((entry) => entry.name)) : null,
309 agentAssigned: agentWasAssigned({
310 runAgents: runList.map((run) => run.agent),
311 pullAgents: [...openPulls, ...(ok(closed) ?? [])].map((pull) => pull.agent),
312 issues: openIssues ?? [],
313 }),
314 })
315 : null;
316
317 const github = await githubP;
Projects: what a workspace builds and runs, first on every page318 return {
319 member,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look320 can,
321 github,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API322 project: ok(project),
323 settings: ok(settings),
324 builds: builds.slice(0, 30),
325 live: ok(list)?.live ?? [],
326 commit: ok(log)?.[0] ?? null,
327 open: ok(counts) ?? { issues: openIssues?.length ?? 0, pulls: openPulls.length },
328 dependencies: ok(deps),
329 agentsLive: live.slice(0, 6),
330 runsLoaded: runs?.ok ?? false,
331 pullsLoaded: open?.ok ?? false,
332 columns,
333 needs: rankNeeds(needs),
334 landed: landed.slice(0, MAX_LANDED),
335 groups: groupActivity(items).slice(0, 30),
336 eventsLoaded: recent != null,
337 health: {
338 passRate: passRate(checkEvents.map((c) => c.passed)),
339 firstPass: firstPassRate(checkEvents),
340 checkRuns: checkEvents.length,
341 ages: openIssues ? ageBuckets(openIssues.map((issue) => issue.createdAt), now) : null,
342 },
343 knows: knows as Memory[],
344 memoryCount: memoryList.length,
345 memoriesLoaded: memories?.ok ?? false,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look346 checklist,
347 branches,
Projects: what a workspace builds and runs, first on every page348 };
349}
350
351export async function action({ request, params, context }: Route.ActionArgs) {
352 assertSameOrigin(request);
353 const user = requireUser(context, request);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look354 const form = await request.formData().catch(() => null);
355 const intent = form?.get("intent");
356 // Syncing from GitHub is pushing; stopping it is an integration; deploying is compute.
357 const refused = await refusal(context, params, intent === "github-sync" ? "push" : intent === "github-stop" ? "manage_integrations" : "run");
358 if (refused) return { error: refused };
359 if (intent === "github-sync" || intent === "github-stop") {
360 const repo = await repos.get({ namespace: params.owner, name: params.repo }, user);
361 if (!repo.ok) return { error: repo.error.message };
362 const done =
363 intent === "github-sync" ? await githubApp.sync(user, repo.value.id) : await githubApp.unlinkRepo(user, repo.value.id);
364 return done.ok
365 ? { notice: intent === "github-sync" ? "Synced with GitHub." : "It is no longer kept in step with GitHub." }
366 : { error: done.error.message };
367 }
Projects: what a workspace builds and runs, first on every page368 const started = await deployments.redeploy(user, { workspace: params.owner, slug: params.repo }, null);
369 return started.ok ? { notice: "Production is building." } : { error: started.error.message };
370}
371
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily372type Loaded = Awaited<ReturnType<typeof overviewData>>;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API373type Card = Loaded["columns"]["working"][number];
374
375const STAGE_TONE: Record<PipelineStage, string> = {
376 working: "bg-merged",
377 checking: "bg-info",
378 reviewing: "bg-warn",
379 queue: "bg-accent-dim",
380 landed: "bg-accent",
381};
382
383function PipelineCard({ card, base, stage }: { card: Card; base: string; stage: PipelineStage }) {
384 return (
385 <li>
386 <Link
387 to={`${base}/pull/${card.number}`}
388 prefetch="intent"
389 className={`block rounded-lg border bg-bg/60 p-2.5 transition-colors hover:border-line-strong hover:bg-raised ${
390 card.needsYou ? "border-warn/50" : card.step ? "border-merged/30" : "border-line"
391 }`}
392 >
393 <span className="line-clamp-2 text-[0.8125rem] leading-snug font-medium">{card.title}</span>
394 <span className="mt-1.5 flex items-center gap-1.5 text-[0.6875rem] text-muted">
395 <Avatar name={card.agent} size={14} />
396 <span className="font-mono text-faint">#{card.number}</span>
397 <span className="grow" />
398 {card.needsYou ? (
399 <span className="inline-flex items-center gap-1 text-warn" title="Stopped: a person decides what happens next">
400 <Hand size={11} /> you
401 </span>
402 ) : stage === "landed" ? (
403 <TimeAgo at={card.at} />
404 ) : card.runStarted ? (
405 <span className="inline-flex items-center gap-1 text-merged">
406 <Loader2 size={11} className="animate-spin" />
407 <Elapsed from={card.runStarted} />
408 </span>
409 ) : (
410 <CheckBadge status={card.checkStatus} />
411 )}
412 </span>
413 {card.step && (
414 <span className="mt-1.5 block truncate font-mono text-[0.6875rem] text-fg/70" title={card.step}>
415 {card.runKind}: {card.step}
416 </span>
417 )}
418 </Link>
419 </li>
420 );
421}
422
423/** Open pull requests moving through Working → Checking → Reviewing → Queue → Landed. */
424function Pipeline({ columns, base }: { columns: Loaded["columns"]; base: string }) {
425 return (
426 <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 look427 {/* Each stage as tall as what is in it: an empty one stays short
428 instead of stretching to the busiest. */}
429 <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 API430 {PIPELINE.map(({ stage, label }, index) => {
431 const cards = columns[stage];
432 return (
433 <li key={stage} className="flex min-w-0 flex-col rounded-xl border border-line bg-surface p-2">
434 <p className="flex items-center gap-2 px-1 pt-0.5 pb-2 text-xs font-medium text-muted">
435 <span className={`size-1.5 rounded-full ${STAGE_TONE[stage]} ${cards.length && stage !== "landed" ? "animate-pulse" : ""}`} />
436 {label}
437 <span className="tabular-nums text-faint">{cards.length}</span>
438 {index < PIPELINE.length - 1 && <ArrowRight size={11} className="ml-auto text-line-strong" />}
439 </p>
440 {cards.length === 0 ? (
441 <p className="rounded-lg border border-dashed border-line/70 px-2 py-3 text-center text-[0.6875rem] text-faint">Empty</p>
442 ) : (
443 <ul className="space-y-1.5">
444 {cards.slice(0, 6).map((card) => (
445 <PipelineCard key={card.number} card={card} base={base} stage={stage} />
446 ))}
447 {cards.length > 6 && (
448 <li className="px-1 text-[0.6875rem] text-muted">
449 <Link to={`${base}/pulls`} className="hover:text-fg">
450 {cards.length - 6} more
451 </Link>
452 </li>
453 )}
454 </ul>
455 )}
456 </li>
457 );
458 })}
459 </ol>
460 </div>
461 );
462}
463
464function Stat({ label, value, to }: { label: string; value: ReactNode; to?: string }) {
465 const body = (
466 <>
467 <span className="block text-[0.6875rem] text-faint">{label}</span>
468 <span className="mt-0.5 block truncate text-[0.8125rem] font-medium">{value}</span>
469 </>
470 );
471 return to ? (
472 <Link to={to} className="min-w-0 px-5 py-3 transition-colors hover:bg-raised/50">
473 {body}
474 </Link>
475 ) : (
476 <div className="min-w-0 px-5 py-3">{body}</div>
477 );
478}
479
Projects: what a workspace builds and runs, first on every page480export default function ProjectOverview({ loaderData, actionData, params }: Route.ComponentProps) {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily481 return (
482 <Suspense fallback={<OverviewSkeleton />}>
483 <Await resolve={loaderData.overview} errorElement={<OverviewFailed />}>
484 {(overview) => <Overview loaderData={overview} actionData={actionData} params={params} />}
485 </Await>
486 </Suspense>
487 );
488}
489
490/** The overview's sections as they will sit, while they stream in. */
491function OverviewSkeleton() {
492 return (
493 <div className="space-y-8" aria-busy="true" aria-label="Loading the overview">
494 <section className="rounded-2xl border border-line bg-surface p-5 sm:p-6">
495 <Skeleton className="h-3 w-24" />
496 <Skeleton className="mt-3 h-5 w-64 max-w-full" />
497 <Skeleton className="mt-3 h-3 w-48 max-w-full" />
498 </section>
499 <section className="rounded-2xl border border-line bg-surface p-2">
500 <div className="grid grid-cols-2 gap-2 sm:grid-cols-5">
501 {Array.from({ length: 5 }, (_, index) => (
502 <Skeleton key={index} className="h-24 rounded-xl" />
503 ))}
504 </div>
505 </section>
506 <div className="grid gap-6 lg:grid-cols-2">
507 <section className="rounded-2xl border border-line bg-surface">
508 <SkeletonRows rows={4} />
509 </section>
510 <section className="rounded-2xl border border-line bg-surface">
511 <SkeletonRows rows={4} />
512 </section>
513 </div>
514 </div>
515 );
516}
517
518function OverviewFailed() {
519 return (
520 <p className="rounded-2xl border border-line bg-surface p-6 text-sm text-muted">
521 The overview could not be loaded just now. Reload the page to try again.
522 </p>
523 );
524}
525
526function Overview({
527 loaderData,
528 actionData,
529 params,
530}: {
531 loaderData: Loaded;
532 actionData: Route.ComponentProps["actionData"];
533 params: Route.ComponentProps["params"];
534}) {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look535 const { member, project, settings, builds, live, commit, open, dependencies, agentsLive, columns, needs, landed, groups, health, knows, checklist, branches } =
536 loaderData;
Projects: what a workspace builds and runs, first on every page537 const base = `/${params.owner}/${params.repo}`;
538 const production = live.find((app) => app.kind === "production") ?? null;
Agents and memory, checks and conflicts, profiles, slug renames, custom domains539 // The project's own domain, once it is active, is where production is visited.
540 const productionUrl = production ? (settings?.primaryDomain ? `https://${settings.primaryDomain}` : production.url) : null;
Projects: what a workspace builds and runs, first on every page541 const previews = live.filter((app) => app.kind === "preview");
542 const latestProduction = builds.find((build) => build.kind === "production") ?? null;
543 const busy = useNavigation().state === "submitting";
544 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 API545 const moving = agentsLive.length > 0 || columns.working.length + columns.checking.length > 0;
546 useLiveRefresh(moving);
547 const ages = health.ages;
548 const oldest = ages ? Math.max(1, ...ages.map((a) => a.count)) : 1;
Projects: what a workspace builds and runs, first on every page549
550 return (
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API551 <div className="space-y-8">
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look552 {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 API553 {/* What the project is, running, and where its code is. */}
554 <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 look555 <div className="flex flex-col gap-5 p-5 sm:flex-row sm:items-start sm:p-6">
556 {member && (production || settings?.enabled) && (
557 <ProductionShot
Production thumbnails follow each deploy, not each commit, and old ones are cleared daily558 src={production ? `${base}/production.jpg?v=${shotVersion(production)}` : null}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look559 href={production ? (productionUrl ?? production.url) : null}
560 label={host(production ? (productionUrl ?? production.url) : (settings?.productionUrl ?? ""))}
561 className="w-full shrink-0 sm:w-60 lg:w-72"
562 />
563 )}
564 <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 API565 <div className="min-w-0">
566 <p className="flex items-center gap-2 text-xs font-medium tracking-wide text-muted uppercase">
567 <Rocket size={13} className="text-accent" />
568 Production
569 </p>
570 {production ? (
571 <>
572 <a
573 href={productionUrl ?? production.url}
574 className="mt-2 flex items-center gap-1.5 truncate font-mono text-lg font-medium hover:text-accent"
575 >
576 {host(productionUrl ?? production.url)}
577 <ArrowUpRight size={16} className="shrink-0 text-faint" />
578 </a>
579 <p className="mt-1.5 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted">
580 {latestProduction && <StatusDot status={latestProduction.status} label={latestProduction.status === "ready" ? "Live" : undefined} />}
581 <span className="inline-flex items-center gap-1 font-mono">
582 <GitCommitHorizontal size={13} className="text-faint" />
583 {production.commit.slice(0, 7)}
584 </span>
585 <span>
586 deployed <TimeAgo at={production.deployedAt} />
587 </span>
588 {settings?.primaryDomain && <span className="text-faint">also at {host(production.url)}</span>}
589 </p>
590 </>
591 ) : settings?.enabled ? (
592 <>
593 <p className="mt-2 font-mono text-lg text-muted">{host(settings.productionUrl)}</p>
594 <p className="mt-1.5 text-xs text-muted">
595 {latestProduction ? (
596 <StatusDot status={latestProduction.status} label={latestProduction.status === "failed" ? "The last build failed" : undefined} />
597 ) : (
598 "Not deployed yet. Push to the default branch, or deploy it now."
599 )}
600 </p>
601 </>
602 ) : (
603 <>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look604 <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 API605 <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 look606 Nothing builds or runs until you turn them on. Then {project?.name ?? params.repo} goes up on g1t.page:
607 production from the default branch, and a live preview for every pull request. It runs only while
608 someone visits.
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API609 </p>
610 </>
611 )}
612 </div>
613 {member && (
614 <div className="flex shrink-0 items-center gap-2">
615 {production && (
616 <ButtonLink to={productionUrl ?? production.url} variant="accent" reloadDocument>
617 Visit
618 <ArrowUpRight size={14} />
619 </ButtonLink>
620 )}
621 {settings?.enabled ? (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look622 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 API623 <Button type="submit" variant="quiet" disabled={busy} title="Build production again from the default branch">
624 <RotateCw size={14} />
625 Redeploy
626 </Button>
627 </Form>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look628 ) : loaderData.can.manage_integrations && (
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API629 <ButtonLink to={`${base}/settings/deployments`} variant="accent">
630 <Rocket size={14} />
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look631 Turn on deployments
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API632 </ButtonLink>
Projects: what a workspace builds and runs, first on every page633 )}
634 </div>
635 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look636 </div>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API637 </div>
638 {actionData && (
639 <p className={`border-t border-line px-6 py-2.5 text-sm ${"error" in actionData ? "text-danger" : "text-accent"}`}>
640 {"error" in actionData ? actionData.error : actionData.notice}
641 </p>
642 )}
643 <div className="grid grid-cols-2 divide-line border-t border-line text-sm sm:grid-cols-4 sm:divide-x">
644 <Stat
645 label="Source"
646 to={`${base}/code`}
647 value={
648 source ? (
649 <span className="inline-flex items-center gap-1.5 font-mono">
650 {project?.private ? <Lock size={12} className="text-faint" /> : <Code2 size={12} className="text-faint" />}
651 {source.repo.name}
652 {source.rootDir ? `/${source.rootDir}` : ""}
653 </span>
654 ) : (
655 "—"
656 )
657 }
658 />
659 <Stat
660 label="Default branch"
661 to={`${base}/commits`}
662 value={
663 <span className="inline-flex items-center gap-1.5 font-mono">
664 <GitBranch size={12} className="text-faint" />
665 {source?.defaultBranch ?? "main"}
666 </span>
667 }
668 />
669 <Stat
670 label="Latest commit"
671 to={commit ? `${base}/commit/${commit.hash}` : undefined}
672 value={
673 commit ? (
674 <span title={commit.message.split("\n")[0]}>
675 <span className="font-mono">{commit.hash.slice(0, 7)}</span>{" "}
676 <span className="font-normal text-muted">
677 <TimeAgo at={commit.authoredAt} />
678 </span>
679 </span>
680 ) : (
681 "No commits yet"
682 )
683 }
684 />
685 <Stat
686 label={member && settings?.enabled ? "Last deploy" : "Open"}
687 to={member && settings?.enabled ? `${base}/deployments` : `${base}/pulls`}
688 value={
689 member && settings?.enabled ? (
690 latestProduction ? (
691 <span className="inline-flex items-center gap-2">
692 <StatusDot status={latestProduction.status} />
693 <span className="font-normal text-muted">
694 <TimeAgo at={latestProduction.createdAt} />
Projects: what a workspace builds and runs, first on every page695 </span>
696 </span>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API697 ) : (
698 "None yet"
699 )
700 ) : (
701 `${open.pulls} pull requests · ${open.issues} issues`
702 )
703 }
704 />
705 </div>
706 {commit && (
707 <p className="truncate border-t border-line px-5 py-2.5 text-xs text-muted sm:px-6">
708 <GitCommitHorizontal size={12} className="mr-1.5 inline text-faint" />
709 {commit.message.split("\n")[0]} <span className="text-faint">· {commit.author.name}</span>
710 </p>
711 )}
712 </section>
713
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look714 {checklist && <ProductionChecklist base={base} items={checklist} />}
715
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API716 <Panel
717 title="Right now"
718 icon={<span className={`block size-2 rounded-full ${agentsLive.length > 0 ? "animate-pulse bg-merged" : "bg-line-strong"}`} />}
719 count={agentsLive.length}
720 all={{ to: `${base}/agents`, label: "Agents" }}
721 >
722 {agentsLive.length > 0 && (
723 <ul className="mb-3 grid grid-cols-1 gap-2 sm:grid-cols-2 xl:grid-cols-3">
724 {agentsLive.map((run) => (
725 <li key={run.id} className="min-w-0 rounded-xl border border-merged/30 bg-surface px-3.5 py-3">
726 <div className="flex items-center gap-2 text-sm">
727 <Avatar name={run.agent} size={18} />
728 <span className="font-medium">{run.agent}</span>
729 <span className="text-xs text-muted">{RUN_KIND_LABEL[run.kind].toLowerCase()}</span>
730 <span className="grow" />
731 <span className="text-xs text-muted">
732 <Elapsed from={run.startedAt ?? run.createdAt} />
Projects: what a workspace builds and runs, first on every page733 </span>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API734 </div>
735 <Link
736 to={run.number != null ? `${base}/pull/${run.number}` : `${base}/agents/runs/${run.id}`}
737 className="mt-1 block truncate text-[0.8125rem] hover:text-accent"
738 >
739 {run.title ?? "A run"} {run.number != null && <span className="text-faint">#{run.number}</span>}
740 </Link>
741 {run.step && (
742 <p className="mt-1 truncate font-mono text-[0.6875rem] text-fg/70" title={run.step}>
743 <span className="mr-1.5 inline-block size-1.5 animate-pulse rounded-full bg-merged align-middle" />
744 {run.step}
745 </p>
746 )}
747 <p className="mt-1.5 flex items-center gap-3 text-[0.6875rem] text-faint">
748 {formatCost(run.costUsd) && <span>{formatCost(run.costUsd)} so far</span>}
749 <Link to={`${base}/agents/runs/${run.id}`} className="hover:text-fg">
750 {run.stepCount} steps
751 </Link>
752 {run.number != null && (
753 <Link to={`${base}/sessions/${run.number}`} className="hover:text-fg">
754 Session
755 </Link>
756 )}
757 </p>
758 </li>
759 ))}
760 </ul>
761 )}
762 {!loaderData.pullsLoaded ? (
763 <Unavailable what="Pull requests" />
764 ) : Object.values(columns).every((c) => c.length === 0) ? (
765 <Quiet
766 action={
767 member ? (
768 <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">
769 <Bot size={12} /> Open an issue for an agent
770 </Link>
771 ) : null
772 }
773 >
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent774 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 API775 </Quiet>
776 ) : (
777 <Pipeline columns={columns} base={base} />
Projects: what a workspace builds and runs, first on every page778 )}
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API779 </Panel>
Projects: what a workspace builds and runs, first on every page780
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API781 <div className="grid gap-x-8 gap-y-8 lg:grid-cols-[minmax(0,1fr)_20rem]">
782 <div className="min-w-0 space-y-8">
783 {needs.length > 0 && (
784 <Panel title="Needs you" icon={<Hand size={14} className="text-warn" />} count={needs.length}>
785 <NeedsList needs={needs} limit={5} />
786 </Panel>
787 )}
788
Fast pages, required checks on the branch, self-hosted runners, honest incidents789 <Suspense
790 fallback={
791 <Panel title="Active branches" icon={<GitBranch size={14} />}>
792 <div aria-busy="true">
793 <SkeletonRows rows={3} rowClassName="h-12" />
794 </div>
795 </Panel>
796 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look797 >
Fast pages, required checks on the branch, self-hosted runners, honest incidents798 <Await resolve={branches}>
799 {(branches) => (
800 <Panel title="Active branches" icon={<GitBranch size={14} />} count={branches?.total || null}>
801 {!branches ? (
802 <Unavailable what="Branches" />
803 ) : branches.shown.length === 0 ? (
804 <Quiet>
805 Only {branches.main} so far. Branches pushed here show with how far each has moved from {branches.main}, and
806 its pull request and preview.
807 </Quiet>
808 ) : (
809 <>
810 <ActiveBranches branches={branches.shown} base={base} main={branches.main} />
811 {branches.total > branches.shown.length && (
812 <p className="mt-2 px-1 text-xs text-faint">
813 The {branches.shown.length} most recently changed of {branches.total} branches.
814 </p>
815 )}
816 </>
817 )}
818 </Panel>
819 )}
820 </Await>
821 </Suspense>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look822
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API823 <Panel title="Recent changes" icon={<GitMerge size={14} />} all={{ to: `${base}/pulls?state=closed`, label: "All landed" }}>
824 {landed.length === 0 ? (
825 <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 page826 ) : (
827 <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 API828 {landed.map((pull) => {
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent829 const byAgent = pull.runtime === "hosted" || madeByG1t(pull);
Projects: what a workspace builds and runs, first on every page830 return (
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API831 <li key={pull.id} className="flex items-start gap-3 px-4 py-3">
832 <GitMerge size={15} className="mt-0.5 shrink-0 text-merged" />
833 <span className="min-w-0 grow">
834 <Link to={`${base}/pull/${pull.number}`} prefetch="intent" className="block truncate text-sm font-medium hover:text-accent">
835 {pull.title}
836 </Link>
837 <span className="mt-0.5 flex flex-wrap items-center gap-x-1.5 text-xs text-muted">
838 <span className="font-mono text-faint">#{pull.number}</span>
839 <span>·</span>
840 <span className="inline-flex items-center gap-1">
841 <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 rights842 {pull.requestedBy
843 ? `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-agent844 : byAgent
845 ? `made by ${pull.agent}`
846 : `by ${pull.author.username}`}
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API847 </span>
848 {pull.issue != null && (
849 <>
850 <span>· for</span>
851 <Link to={`${base}/issues/${pull.issue}`} className="hover:text-fg">
852 #{pull.issue}
853 </Link>
854 </>
855 )}
856 {pull.mergedBy && <span>· landed by {pull.mergedBy}</span>}
857 <span>
858 · <TimeAgo at={pull.mergedAt ?? pull.updatedAt} />
Projects: what a workspace builds and runs, first on every page859 </span>
860 </span>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API861 </span>
862 <span className="hidden shrink-0 text-xs text-faint sm:block">
863 <ChangeSize files={pull.files} />
864 </span>
865 {byAgent && (
866 <Link to={`${base}/sessions/${pull.number}`} className="shrink-0 text-xs text-muted hover:text-fg">
867 Session
868 </Link>
869 )}
Projects: what a workspace builds and runs, first on every page870 </li>
871 );
872 })}
873 </ul>
874 )}
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API875 </Panel>
Projects: what a workspace builds and runs, first on every page876
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API877 <Panel title="Activity" icon={<ActivityIcon size={14} />}>
878 {loaderData.eventsLoaded ? (
879 <ActivityFeed groups={groups} showRepo={false} limit={8} empty="Nothing has happened here yet." />
880 ) : (
881 <Unavailable what="Activity" />
882 )}
883 </Panel>
884
885 {previews.length > 0 && (
886 <Panel title="Previews" icon={<GitBranch size={14} />} count={previews.length} all={{ to: `${base}/deployments`, label: "Deployments" }}>
887 <ul className="divide-y divide-line overflow-hidden rounded-xl border border-line bg-surface">
888 {previews.map((app) => (
889 <li key={app.url} className="flex items-center gap-3 px-4 py-2.5 text-sm">
890 <span className="min-w-0 grow">
891 <a href={app.url} className="block truncate font-mono text-[0.8125rem] hover:text-accent">
892 {host(app.url)}
893 </a>
894 <span className="text-xs text-muted">
895 {app.branch}
896 {app.number != null && (
897 <>
898 {" · "}
899 <Link to={`${base}/pull/${app.number}`} className="hover:underline">
900 #{app.number}
901 </Link>
902 </>
903 )}
904 </span>
Projects: what a workspace builds and runs, first on every page905 </span>
906 <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 API907 <TimeAgo at={app.deployedAt} />
Projects: what a workspace builds and runs, first on every page908 </span>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API909 </li>
910 ))}
911 </ul>
912 </Panel>
913 )}
914 </div>
Projects: what a workspace builds and runs, first on every page915
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API916 <aside className="space-y-4 text-sm">
917 {member && (
918 <section className="rounded-xl border border-line bg-surface p-5">
919 <div className="flex items-center justify-between">
920 <h2 className="flex items-center gap-1.5 text-sm font-semibold">
921 <Brain size={14} className="text-merged" />
922 What agents know here
923 </h2>
924 <Link to={`${base}/memory`} className="text-xs text-muted hover:text-fg">
925 Memory
926 </Link>
Projects: what a workspace builds and runs, first on every page927 </div>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API928 {!loaderData.memoriesLoaded ? (
929 <p className="mt-3 text-xs text-muted">Memory could not be loaded just now.</p>
930 ) : knows.length === 0 ? (
931 <p className="mt-3 text-xs leading-5 text-muted">
932 Nothing yet. Agents note conventions, decisions and traps as they work, and every agent starting here reads them.
933 </p>
934 ) : (
935 <ul className="mt-3 space-y-2.5">
936 {knows.map((memory) => (
937 <li key={memory.id} className="text-xs leading-5">
938 <p className="line-clamp-3 text-fg-soft">
939 {memory.pinned && <Pin size={11} className="mr-1 inline text-accent" />}
940 {memory.text}
941 </p>
942 <p className="text-faint">
943 {memory.kind} · {memory.createdBy} · <TimeAgo at={memory.createdAt} />
944 </p>
945 </li>
946 ))}
947 {loaderData.memoryCount > knows.length && (
948 <li>
949 <Link to={`${base}/memory`} className="text-xs text-muted hover:text-fg">
950 {loaderData.memoryCount - knows.length} more
951 </Link>
952 </li>
953 )}
954 </ul>
955 )}
956 </section>
Projects: what a workspace builds and runs, first on every page957 )}
958
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API959 <section className="rounded-xl border border-line bg-surface p-5">
960 <h2 className="flex items-center gap-1.5 text-sm font-semibold">
961 <HeartPulse size={14} className="text-faint" />
962 Health
Project dependencies: addresses, preview stacks, Affects, and agents who know963 </h2>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API964 <div className="mt-4 space-y-4 text-xs">
965 <div>
966 <div className="flex items-baseline justify-between">
967 <span className="text-muted">Checks passing</span>
968 <span className="font-medium tabular-nums">{percent(health.passRate)}</span>
969 </div>
970 <div className="mt-1.5">
971 <Meter value={health.passRate} tone={health.passRate != null && health.passRate < 0.7 ? "bg-warn" : "bg-accent"} />
972 </div>
973 <p className="mt-1 text-faint">
974 {health.checkRuns
975 ? `${health.checkRuns} recent runs · ${percent(health.firstPass.rate)} pass on the first try`
976 : "No checks have run recently."}
977 </p>
978 </div>
979 {member && (
Project dependencies: addresses, preview stacks, Affects, and agents who know980 <div>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API981 <div className="flex items-baseline justify-between">
982 <span className="text-muted">Deploys</span>
983 <Link to={`${base}/deployments`} className="text-faint hover:text-fg">
984 {builds.length ? `last ${Math.min(builds.length, 20)}` : "none yet"}
985 </Link>
986 </div>
987 <div className="mt-1.5">
988 <DeployStrip builds={builds} base={base} />
989 </div>
Project dependencies: addresses, preview stacks, Affects, and agents who know990 </div>
991 )}
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API992 <div>
993 <div className="flex items-baseline justify-between">
994 <span className="text-muted">Open issues by age</span>
995 <Link to={`${base}/issues`} className="font-medium tabular-nums hover:text-accent">
996 {open.issues}
997 </Link>
998 </div>
999 {ages ? (
1000 <ul className="mt-2 space-y-1.5">
1001 {ages.map((bucket) => (
1002 <li key={bucket.label} className="grid grid-cols-[6.5rem_1fr_1.5rem] items-center gap-2">
1003 <span className="text-faint">{bucket.label}</span>
1004 <span className="block h-1.5 overflow-hidden rounded-full bg-line">
1005 <span
1006 className={`block h-full rounded-full ${bucket.label === "Older" ? "bg-warn" : "bg-fg-soft/60"}`}
1007 style={{ width: `${(bucket.count / oldest) * 100}%` }}
1008 />
1009 </span>
1010 <span className="text-right tabular-nums text-muted">{bucket.count}</span>
Project dependencies: addresses, preview stacks, Affects, and agents who know1011 </li>
1012 ))}
1013 </ul>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1014 ) : (
1015 <p className="mt-1 text-faint">Issues could not be loaded just now.</p>
1016 )}
1017 </div>
Project dependencies: addresses, preview stacks, Affects, and agents who know1018 </div>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1019 </section>
Project dependencies: addresses, preview stacks, Affects, and agents who know1020
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1021 <section className="rounded-xl border border-line bg-surface p-5">
1022 <div className="flex items-center justify-between">
1023 <h2 className="flex items-center gap-1.5 text-sm font-semibold">
1024 <Network size={14} className="text-faint" />
1025 Dependencies
1026 </h2>
1027 {member && (
1028 <Link to={`${base}/settings/dependencies`} className="text-xs text-muted hover:text-fg">
1029 Manage
1030 </Link>
1031 )}
1032 </div>
1033 {!dependencies ? (
1034 <p className="mt-3 text-xs text-muted">Dependencies could not be loaded just now.</p>
1035 ) : dependencies.dependsOn.length === 0 && dependencies.usedBy.length === 0 ? (
1036 <p className="mt-3 text-xs leading-5 text-muted">
1037 Uses no other project, and none uses it. Declare one, and builds get its address and agents know what depends on
1038 what.
1039 </p>
1040 ) : (
1041 <div className="mt-3 space-y-3 text-xs">
1042 {dependencies.dependsOn.length > 0 && (
1043 <div>
1044 <p className="flex items-center gap-1 text-muted">
1045 <ArrowUpRight size={12} /> Uses
1046 </p>
1047 <ul className="mt-1.5 space-y-1">
1048 {dependencies.dependsOn.map((d) => (
1049 <li key={d.slug} className="flex items-center justify-between gap-2">
1050 <Link to={`/${params.owner}/${d.slug}`} className="font-medium hover:underline">
1051 {d.name}
1052 </Link>
1053 {d.as && <code className="font-mono text-faint">{d.as}</code>}
1054 </li>
1055 ))}
1056 </ul>
1057 </div>
1058 )}
1059 {dependencies.usedBy.length > 0 && (
1060 <div>
1061 <p className="flex items-center gap-1 text-muted">
1062 <ArrowDownLeft size={12} /> Used by
1063 </p>
1064 <ul className="mt-1.5 space-y-1">
1065 {dependencies.usedBy.map((d) => (
1066 <li key={d.slug}>
1067 <Link to={`/${params.owner}/${d.slug}`} className="font-medium hover:underline">
1068 {d.name}
1069 </Link>
1070 </li>
1071 ))}
1072 </ul>
1073 </div>
1074 )}
1075 </div>
1076 )}
Projects: what a workspace builds and runs, first on every page1077 </section>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1078
1079 {source && (
1080 <section className="rounded-xl border border-line bg-surface p-5">
1081 <h2 className="text-sm font-semibold">Clone</h2>
1082 <div className="mt-3">
1083 <CopyLine text={`git clone https://g1t.sh/${source.repo.namespace}/${source.repo.name}.git`} />
1084 </div>
1085 </section>
1086 )}
1087 </aside>
1088 </div>
Projects: what a workspace builds and runs, first on every page1089 </div>
1090 );
1091}