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

996 lines46,169 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";
21import type { ReactNode } from "react";
Projects: what a workspace builds and runs, first on every page22import { Form, Link, useNavigation } from "react-router";
23
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API24import { type AgentRun, type G1tEvent, type Memory, type Pull, RUN_KIND_LABEL, isActiveRun } from "@g1t/contracts";
25
Projects: what a workspace builds and runs, first on every page26import type { Route } from "./+types/overview";
27import { host, StatusDot } from "../../components/deploy";
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API28import { CheckBadge } from "../../components/checks";
29import { Elapsed, formatCost, useLiveRefresh } from "../../components/agents";
30import { ActivityFeed, DeployStrip, Meter, NeedsList, Panel, Quiet, Unavailable, percent } from "../../components/mission";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look31import { type ActiveBranch, ActiveBranches } from "../../components/branches";
32import { ProductionChecklist } from "../../components/checklist";
33import { ProductionShot } from "../../components/production-shot";
34import { GithubLinkStrip } from "../../components/github";
35import { githubApp } from "../../lib/github.server";
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API36import { Avatar, Button, ButtonLink, CopyLine, TimeAgo } from "../../components/ui";
37import { ChangeSize } from "../../components/work";
38import {
39 type ActivityItem,
40 type Need,
41 type PipelineStage,
42 PIPELINE,
43 TIME,
44 ageBuckets,
45 eventItem,
46 firstPassRate,
47 groupActivity,
48 passRate,
49 pipelineStage,
50 queuedNumbers,
51 rankNeeds,
52 stuckMinutes,
53} from "../../lib/mission";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look54import { drift } from "../../lib/branches";
55import { 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 API56import { agents, deployments, events as eventLog, projects, repos, work } from "../../lib/services.server";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look57import { assertSameOrigin, getViewer, requireUser } from "../../lib/session.server";
58import { accessTo, refusal, repoFor } from "../../lib/access.server";
Projects: what a workspace builds and runs, first on every page59
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API60const MAX_LANDED = 6;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look61/** Branches read for the Active branches list, and shown. */
62const BRANCHES_READ = 10;
63const BRANCHES_SHOWN = 5;
64/** How far back each branch's history, and the default branch's, is read to count ahead and behind. */
65const BRANCH_DEPTH = 40;
66const MAIN_DEPTH = 120;
Projects: what a workspace builds and runs, first on every page67
68export async function loader({ params, context }: Route.LoaderArgs) {
69 const viewer = getViewer(context);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look70 // People with a role of their own here see its running parts: deployments,
71 // memory, setup; everyone who can read it sees the rest.
72 const { insider: member, can } = await accessTo(context, params);
Projects: what a workspace builds and runs, first on every page73 const path = { namespace: params.owner, name: params.repo };
74 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 API75 const now = Date.now();
76 // A section whose service fails shows its own empty state; the page stays up.
77 const soft = <T,>(promise: Promise<T> | null): Promise<T | null> =>
78 promise ? promise.catch((error) => (console.warn("overview:", error), null)) : Promise.resolve(null);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look79 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 API80 const eventsP = repoP.then((repo) => (repo?.ok ? soft(eventLog.list({ repoId: repo.value.id, limit: 150 })) : null));
81 const minePullsP = repoP.then((repo) =>
82 repo?.ok && viewer
83 ? soft(work.listActivePulls(viewer)).then((list) => (list ?? []).filter((item) => item.pull.repoId === repo.value.id))
84 : null,
85 );
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look86 const openP = soft(work.listPulls(path, viewer, "open"));
87 // Where its code came from on GitHub, for members.
88 const githubP = repoP.then((repo) => (repo?.ok && member ? soft(githubApp.link(repo.value.id)) : null));
89 const listP = soft(member ? deployments.list(ref, viewer) : null);
90 // Active branches: the newest few besides the default, with how far each
91 // has moved. Branches with an open pull request are read first.
92 const branchesP = Promise.all([repoP, soft(repos.branches(path, viewer)), openP, listP]).then(
93 async ([repo, branchList, pulls, deploys]): Promise<{ main: string; total: number; shown: ActiveBranch[] } | null> => {
94 if (!repo?.ok || !branchList?.ok) return null;
95 const main = repo.value.defaultBranch;
96 const pullList = pulls?.ok ? pulls.value : [];
97 const pullOn = new Map(pullList.filter((pull) => pull.branch).map((pull) => [pull.branch as string, pull]));
98 const others = branchList.value.filter((branch) => branch.name !== main);
99 if (others.length === 0) return { main, total: 0, shown: [] };
100 const read = [...others.filter((b) => pullOn.has(b.name)), ...others.filter((b) => !pullOn.has(b.name))].slice(0, BRANCHES_READ);
101 const [mainLog, ...logs] = await Promise.all([
102 soft(repos.log(path, viewer, main, MAIN_DEPTH)),
103 ...read.map((branch) => soft(repos.log(path, viewer, branch.name, BRANCH_DEPTH))),
104 ]);
105 const mainHashes = mainLog?.ok ? mainLog.value.map((c) => c.hash) : [];
106 const previews = deploys?.ok ? deploys.value.live.filter((app) => app.kind === "preview") : [];
107 const shown = read
108 .map((branch, index): ActiveBranch => {
109 const history = logs[index]?.ok ? logs[index].value : [];
110 const head = history[0];
111 const moved = drift(history.map((c) => c.hash), mainHashes, BRANCH_DEPTH);
112 const pull = pullOn.get(branch.name);
113 return {
114 name: branch.name,
115 commit: head ? { hash: head.hash, message: head.message.split("\n")[0], author: head.author.name, at: head.authoredAt } : null,
116 ...moved,
117 pull: pull ? { number: pull.number, title: pull.title, checkStatus: pull.checkStatus, draft: pull.status === "draft" } : null,
118 preview: previews.find((app) => app.branch === branch.name || (pull != null && app.number === pull.number))?.url ?? null,
119 };
120 })
121 .sort((a, b) => Date.parse(b.commit?.at ?? "0") - Date.parse(a.commit?.at ?? "0"))
122 .slice(0, BRANCHES_SHOWN);
123 return { main, total: others.length, shown };
124 },
125 );
126 const [project, settings, list, open, closed, log, counts, deps, runs, queue, issues, memories, recent, mine, domains, root, branches] = await Promise.all([
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API127 soft(projects.get(params.owner, params.repo, viewer)),
128 soft(member ? deployments.settings(ref, viewer) : null),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look129 listP,
130 openP,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API131 soft(work.listPulls(path, viewer, "closed")),
132 soft(repos.log(path, viewer, null, 1)),
133 soft(work.counts(path, viewer)),
134 soft(projects.dependencies(params.owner, params.repo, viewer)),
135 soft(agents.listRuns(viewer, { repo: path, limit: 60 })),
136 soft(work.queue(path, viewer)),
137 soft(work.listIssues(path, viewer, { state: "open" })),
138 soft(member ? agents.listMemories(viewer, params.owner, path) : null),
139 eventsP,
140 minePullsP,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look141 // For the checklist, which only members see.
142 soft(member ? deployments.domains(ref, viewer) : null),
143 soft(member ? repos.tree(path, viewer, null, "") : null),
144 branchesP,
Projects: what a workspace builds and runs, first on every page145 ]);
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API146 const ok = <T,>(result: { ok: true; value: T } | { ok: false } | null): T | null => (result?.ok ? result.value : null);
147
148 const openPulls = ok(open) ?? [];
149 const runList = ok(runs) ?? [];
150 const live = runList.filter((run) => isActiveRun(run.status));
151 const builds = ok(list)?.deployments ?? [];
152 const landed = (ok(closed) ?? [])
153 .filter((pull) => pull.status === "merged")
154 .sort((a, b) => Date.parse(b.mergedAt ?? b.updatedAt) - Date.parse(a.mergedAt ?? a.updatedAt));
155 const queued = queuedNumbers(ok(queue)?.active ?? []);
156 const runOn = new Map<number, AgentRun>();
157 for (const run of live) if (run.number != null && !runOn.has(run.number)) runOn.set(run.number, run);
158 const lifecycleOf = new Map((mine ?? []).map((item) => [item.pull.number, item.lifecycle]));
159
160 // --- The pipeline -----------------------------------------------------------
161 type Card = {
162 number: number;
163 title: string;
164 agent: string;
165 status: Pull["status"];
166 checkStatus: Pull["checkStatus"];
167 step: string | null;
168 runKind: string | null;
169 runStarted: string | null;
170 needsYou: boolean;
171 at: string;
172 };
173 const columns: Record<PipelineStage, Card[]> = { working: [], checking: [], reviewing: [], queue: [], landed: [] };
174 for (const pull of openPulls.slice(0, 40)) {
175 const run = runOn.get(pull.number);
176 columns[pipelineStage(pull, run, queued)].push({
177 number: pull.number,
178 title: pull.title,
179 agent: pull.agent,
180 status: pull.status,
181 checkStatus: pull.checkStatus,
182 step: run?.step ?? null,
183 runKind: run ? RUN_KIND_LABEL[run.kind] : null,
184 runStarted: run ? (run.startedAt ?? run.createdAt) : null,
185 needsYou: lifecycleOf.get(pull.number)?.stage === "needs_you",
186 at: pull.updatedAt,
187 });
188 }
189 for (const pull of landed.filter((p) => now - Date.parse(p.mergedAt ?? p.updatedAt) < 3 * TIME.DAY).slice(0, 5)) {
190 columns.landed.push({
191 number: pull.number,
192 title: pull.title,
193 agent: pull.agent,
194 status: pull.status,
195 checkStatus: pull.checkStatus,
196 step: null,
197 runKind: null,
198 runStarted: null,
199 needsYou: false,
200 at: pull.mergedAt ?? pull.updatedAt,
201 });
202 }
203
204 // --- Needs you ---------------------------------------------------------------
205 const base = `/${params.owner}/${params.repo}`;
206 const needs: Need[] = [];
207 const latestProduction = builds.find((build) => build.kind === "production") ?? null;
208 if (latestProduction?.status === "failed") {
209 needs.push({
210 key: "deploy",
211 kind: "deploy",
212 title: "The production build failed",
213 detail: latestProduction.error ?? "Production still serves the build before it.",
214 to: `${base}/deployments/${latestProduction.id}`,
215 action: "See the build",
216 at: Date.parse(latestProduction.finishedAt ?? latestProduction.createdAt),
217 where: latestProduction.commit.slice(0, 7),
218 });
219 }
220 const me = viewer?.username.toLowerCase();
221 for (const pull of openPulls) {
222 const to = `${base}/pull/${pull.number}`;
223 const where = `#${pull.number}`;
224 const lifecycle = lifecycleOf.get(pull.number);
225 if (lifecycle?.stage === "needs_you") {
226 const conflict = /conflict/i.test(lifecycle.detail);
227 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 });
228 } else if (lifecycle?.stage === "ready") {
229 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 });
230 }
231 if (me && pull.status === "open" && pull.author.username.toLowerCase() !== me && pull.reviewers.some((name) => name.toLowerCase() === me)) {
232 needs.push({ key: `review:${pull.number}`, kind: "review", title: pull.title, detail: `${pull.author.username} asked for your review.`, to, action: "Review", at: Date.parse(pull.updatedAt), where });
233 }
234 if (member && pull.status === "open" && pull.checkStatus === "failed" && !runOn.has(pull.number) && !lifecycle) {
235 needs.push({ key: `checks:${pull.number}`, kind: "checks", title: pull.title, detail: "Its acceptance checks failed and no agent is fixing them.", to, action: "See checks", at: Date.parse(pull.updatedAt), where });
236 }
237 }
238 for (const run of live) {
239 const minutes = stuckMinutes(run, now);
240 if (minutes == null) continue;
241 needs.push({
242 key: `run:${run.id}`,
243 kind: "stuck",
244 title: run.title ?? `${run.agent}'s run`,
245 detail: `${run.agent} has reported nothing for ${minutes} min${run.step ? `. Last: ${run.step}` : ""}.`,
246 to: `${base}/agents/runs/${run.id}`,
247 action: "Look",
248 at: Date.parse(run.updatedAt),
249 where: run.number != null ? `#${run.number}` : null,
250 });
251 }
252
253 // --- Activity and health -------------------------------------------------------
254 const eventList: G1tEvent[] = recent ?? [];
255 const items = eventList
256 .map((event) => eventItem(event, path))
257 .filter((item): item is ActivityItem => item != null);
258 const checkEvents = eventList.flatMap((event) =>
259 event.type === "checks.completed" ? [{ repo: "", number: event.data.number, at: Date.parse(event.time), passed: event.data.status === "passed" }] : [],
260 );
261 const memoryList = ok(memories)?.project ?? [];
262 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);
263 const openIssues = ok(issues);
264
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look265 // --- Getting to production ----------------------------------------------------
266 const wentLive = (status: string) => status === "ready" || status === "replaced" || status === "down";
267 const liveApps = ok(list)?.live ?? [];
268 const commitNow = ok(log)?.[0] ?? null;
269 const projectValue = ok(project);
270 const checklist = member
271 ? productionChecklist({
272 base,
273 hasCode: commitNow != null || projectValue?.source.kind === "mirror",
274 deploysEnabled: ok(settings)?.enabled ?? false,
275 productionDeployed:
276 liveApps.some((app) => app.kind === "production") ||
277 builds.some((build) => build.kind === "production" && wentLive(build.status)),
278 domains: ok(domains)?.domains.length ?? null,
279 previewOpened:
280 liveApps.some((app) => app.kind === "preview") || builds.some((build) => build.kind === "preview" && wentLive(build.status)),
281 instructions: commitNow == null ? false : root?.ok ? hasInstructions(root.value.entries.map((entry) => entry.name)) : null,
282 agentAssigned: agentWasAssigned({
283 runAgents: runList.map((run) => run.agent),
284 pullAgents: [...openPulls, ...(ok(closed) ?? [])].map((pull) => pull.agent),
285 issues: openIssues ?? [],
286 }),
287 })
288 : null;
289
290 const github = await githubP;
Projects: what a workspace builds and runs, first on every page291 return {
292 member,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look293 can,
294 github,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API295 project: ok(project),
296 settings: ok(settings),
297 builds: builds.slice(0, 30),
298 live: ok(list)?.live ?? [],
299 commit: ok(log)?.[0] ?? null,
300 open: ok(counts) ?? { issues: openIssues?.length ?? 0, pulls: openPulls.length },
301 dependencies: ok(deps),
302 agentsLive: live.slice(0, 6),
303 runsLoaded: runs?.ok ?? false,
304 pullsLoaded: open?.ok ?? false,
305 columns,
306 needs: rankNeeds(needs),
307 landed: landed.slice(0, MAX_LANDED),
308 groups: groupActivity(items).slice(0, 30),
309 eventsLoaded: recent != null,
310 health: {
311 passRate: passRate(checkEvents.map((c) => c.passed)),
312 firstPass: firstPassRate(checkEvents),
313 checkRuns: checkEvents.length,
314 ages: openIssues ? ageBuckets(openIssues.map((issue) => issue.createdAt), now) : null,
315 },
316 knows: knows as Memory[],
317 memoryCount: memoryList.length,
318 memoriesLoaded: memories?.ok ?? false,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look319 checklist,
320 branches,
Projects: what a workspace builds and runs, first on every page321 };
322}
323
324export async function action({ request, params, context }: Route.ActionArgs) {
325 assertSameOrigin(request);
326 const user = requireUser(context, request);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look327 const form = await request.formData().catch(() => null);
328 const intent = form?.get("intent");
329 // Syncing from GitHub is pushing; stopping it is an integration; deploying is compute.
330 const refused = await refusal(context, params, intent === "github-sync" ? "push" : intent === "github-stop" ? "manage_integrations" : "run");
331 if (refused) return { error: refused };
332 if (intent === "github-sync" || intent === "github-stop") {
333 const repo = await repos.get({ namespace: params.owner, name: params.repo }, user);
334 if (!repo.ok) return { error: repo.error.message };
335 const done =
336 intent === "github-sync" ? await githubApp.sync(user, repo.value.id) : await githubApp.unlinkRepo(user, repo.value.id);
337 return done.ok
338 ? { notice: intent === "github-sync" ? "Synced with GitHub." : "It is no longer kept in step with GitHub." }
339 : { error: done.error.message };
340 }
Projects: what a workspace builds and runs, first on every page341 const started = await deployments.redeploy(user, { workspace: params.owner, slug: params.repo }, null);
342 return started.ok ? { notice: "Production is building." } : { error: started.error.message };
343}
344
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API345type Loaded = Route.ComponentProps["loaderData"];
346type Card = Loaded["columns"]["working"][number];
347
348const STAGE_TONE: Record<PipelineStage, string> = {
349 working: "bg-merged",
350 checking: "bg-info",
351 reviewing: "bg-warn",
352 queue: "bg-accent-dim",
353 landed: "bg-accent",
354};
355
356function PipelineCard({ card, base, stage }: { card: Card; base: string; stage: PipelineStage }) {
357 return (
358 <li>
359 <Link
360 to={`${base}/pull/${card.number}`}
361 prefetch="intent"
362 className={`block rounded-lg border bg-bg/60 p-2.5 transition-colors hover:border-line-strong hover:bg-raised ${
363 card.needsYou ? "border-warn/50" : card.step ? "border-merged/30" : "border-line"
364 }`}
365 >
366 <span className="line-clamp-2 text-[0.8125rem] leading-snug font-medium">{card.title}</span>
367 <span className="mt-1.5 flex items-center gap-1.5 text-[0.6875rem] text-muted">
368 <Avatar name={card.agent} size={14} />
369 <span className="font-mono text-faint">#{card.number}</span>
370 <span className="grow" />
371 {card.needsYou ? (
372 <span className="inline-flex items-center gap-1 text-warn" title="Stopped: a person decides what happens next">
373 <Hand size={11} /> you
374 </span>
375 ) : stage === "landed" ? (
376 <TimeAgo at={card.at} />
377 ) : card.runStarted ? (
378 <span className="inline-flex items-center gap-1 text-merged">
379 <Loader2 size={11} className="animate-spin" />
380 <Elapsed from={card.runStarted} />
381 </span>
382 ) : (
383 <CheckBadge status={card.checkStatus} />
384 )}
385 </span>
386 {card.step && (
387 <span className="mt-1.5 block truncate font-mono text-[0.6875rem] text-fg/70" title={card.step}>
388 {card.runKind}: {card.step}
389 </span>
390 )}
391 </Link>
392 </li>
393 );
394}
395
396/** Open pull requests moving through Working → Checking → Reviewing → Queue → Landed. */
397function Pipeline({ columns, base }: { columns: Loaded["columns"]; base: string }) {
398 return (
399 <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 look400 {/* Each stage as tall as what is in it: an empty one stays short
401 instead of stretching to the busiest. */}
402 <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 API403 {PIPELINE.map(({ stage, label }, index) => {
404 const cards = columns[stage];
405 return (
406 <li key={stage} className="flex min-w-0 flex-col rounded-xl border border-line bg-surface p-2">
407 <p className="flex items-center gap-2 px-1 pt-0.5 pb-2 text-xs font-medium text-muted">
408 <span className={`size-1.5 rounded-full ${STAGE_TONE[stage]} ${cards.length && stage !== "landed" ? "animate-pulse" : ""}`} />
409 {label}
410 <span className="tabular-nums text-faint">{cards.length}</span>
411 {index < PIPELINE.length - 1 && <ArrowRight size={11} className="ml-auto text-line-strong" />}
412 </p>
413 {cards.length === 0 ? (
414 <p className="rounded-lg border border-dashed border-line/70 px-2 py-3 text-center text-[0.6875rem] text-faint">Empty</p>
415 ) : (
416 <ul className="space-y-1.5">
417 {cards.slice(0, 6).map((card) => (
418 <PipelineCard key={card.number} card={card} base={base} stage={stage} />
419 ))}
420 {cards.length > 6 && (
421 <li className="px-1 text-[0.6875rem] text-muted">
422 <Link to={`${base}/pulls`} className="hover:text-fg">
423 {cards.length - 6} more
424 </Link>
425 </li>
426 )}
427 </ul>
428 )}
429 </li>
430 );
431 })}
432 </ol>
433 </div>
434 );
435}
436
437function Stat({ label, value, to }: { label: string; value: ReactNode; to?: string }) {
438 const body = (
439 <>
440 <span className="block text-[0.6875rem] text-faint">{label}</span>
441 <span className="mt-0.5 block truncate text-[0.8125rem] font-medium">{value}</span>
442 </>
443 );
444 return to ? (
445 <Link to={to} className="min-w-0 px-5 py-3 transition-colors hover:bg-raised/50">
446 {body}
447 </Link>
448 ) : (
449 <div className="min-w-0 px-5 py-3">{body}</div>
450 );
451}
452
Projects: what a workspace builds and runs, first on every page453export default function ProjectOverview({ loaderData, actionData, params }: Route.ComponentProps) {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look454 const { member, project, settings, builds, live, commit, open, dependencies, agentsLive, columns, needs, landed, groups, health, knows, checklist, branches } =
455 loaderData;
Projects: what a workspace builds and runs, first on every page456 const base = `/${params.owner}/${params.repo}`;
457 const production = live.find((app) => app.kind === "production") ?? null;
Agents and memory, checks and conflicts, profiles, slug renames, custom domains458 // The project's own domain, once it is active, is where production is visited.
459 const productionUrl = production ? (settings?.primaryDomain ? `https://${settings.primaryDomain}` : production.url) : null;
Projects: what a workspace builds and runs, first on every page460 const previews = live.filter((app) => app.kind === "preview");
461 const latestProduction = builds.find((build) => build.kind === "production") ?? null;
462 const busy = useNavigation().state === "submitting";
463 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 API464 const moving = agentsLive.length > 0 || columns.working.length + columns.checking.length > 0;
465 useLiveRefresh(moving);
466 const ages = health.ages;
467 const oldest = ages ? Math.max(1, ...ages.map((a) => a.count)) : 1;
Projects: what a workspace builds and runs, first on every page468
469 return (
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API470 <div className="space-y-8">
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look471 {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 API472 {/* What the project is, running, and where its code is. */}
473 <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 look474 <div className="flex flex-col gap-5 p-5 sm:flex-row sm:items-start sm:p-6">
475 {member && (production || settings?.enabled) && (
476 <ProductionShot
477 src={production ? `${base}/production.jpg?v=${production.commit}` : null}
478 href={production ? (productionUrl ?? production.url) : null}
479 label={host(production ? (productionUrl ?? production.url) : (settings?.productionUrl ?? ""))}
480 className="w-full shrink-0 sm:w-60 lg:w-72"
481 />
482 )}
483 <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 API484 <div className="min-w-0">
485 <p className="flex items-center gap-2 text-xs font-medium tracking-wide text-muted uppercase">
486 <Rocket size={13} className="text-accent" />
487 Production
488 </p>
489 {production ? (
490 <>
491 <a
492 href={productionUrl ?? production.url}
493 className="mt-2 flex items-center gap-1.5 truncate font-mono text-lg font-medium hover:text-accent"
494 >
495 {host(productionUrl ?? production.url)}
496 <ArrowUpRight size={16} className="shrink-0 text-faint" />
497 </a>
498 <p className="mt-1.5 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted">
499 {latestProduction && <StatusDot status={latestProduction.status} label={latestProduction.status === "ready" ? "Live" : undefined} />}
500 <span className="inline-flex items-center gap-1 font-mono">
501 <GitCommitHorizontal size={13} className="text-faint" />
502 {production.commit.slice(0, 7)}
503 </span>
504 <span>
505 deployed <TimeAgo at={production.deployedAt} />
506 </span>
507 {settings?.primaryDomain && <span className="text-faint">also at {host(production.url)}</span>}
508 </p>
509 </>
510 ) : settings?.enabled ? (
511 <>
512 <p className="mt-2 font-mono text-lg text-muted">{host(settings.productionUrl)}</p>
513 <p className="mt-1.5 text-xs text-muted">
514 {latestProduction ? (
515 <StatusDot status={latestProduction.status} label={latestProduction.status === "failed" ? "The last build failed" : undefined} />
516 ) : (
517 "Not deployed yet. Push to the default branch, or deploy it now."
518 )}
519 </p>
520 </>
521 ) : (
522 <>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look523 <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 API524 <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 look525 Nothing builds or runs until you turn them on. Then {project?.name ?? params.repo} goes up on g1t.page:
526 production from the default branch, and a live preview for every pull request. It runs only while
527 someone visits.
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API528 </p>
529 </>
530 )}
531 </div>
532 {member && (
533 <div className="flex shrink-0 items-center gap-2">
534 {production && (
535 <ButtonLink to={productionUrl ?? production.url} variant="accent" reloadDocument>
536 Visit
537 <ArrowUpRight size={14} />
538 </ButtonLink>
539 )}
540 {settings?.enabled ? (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look541 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 API542 <Button type="submit" variant="quiet" disabled={busy} title="Build production again from the default branch">
543 <RotateCw size={14} />
544 Redeploy
545 </Button>
546 </Form>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look547 ) : loaderData.can.manage_integrations && (
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API548 <ButtonLink to={`${base}/settings/deployments`} variant="accent">
549 <Rocket size={14} />
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look550 Turn on deployments
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API551 </ButtonLink>
Projects: what a workspace builds and runs, first on every page552 )}
553 </div>
554 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look555 </div>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API556 </div>
557 {actionData && (
558 <p className={`border-t border-line px-6 py-2.5 text-sm ${"error" in actionData ? "text-danger" : "text-accent"}`}>
559 {"error" in actionData ? actionData.error : actionData.notice}
560 </p>
561 )}
562 <div className="grid grid-cols-2 divide-line border-t border-line text-sm sm:grid-cols-4 sm:divide-x">
563 <Stat
564 label="Source"
565 to={`${base}/code`}
566 value={
567 source ? (
568 <span className="inline-flex items-center gap-1.5 font-mono">
569 {project?.private ? <Lock size={12} className="text-faint" /> : <Code2 size={12} className="text-faint" />}
570 {source.repo.name}
571 {source.rootDir ? `/${source.rootDir}` : ""}
572 </span>
573 ) : (
574 "—"
575 )
576 }
577 />
578 <Stat
579 label="Default branch"
580 to={`${base}/commits`}
581 value={
582 <span className="inline-flex items-center gap-1.5 font-mono">
583 <GitBranch size={12} className="text-faint" />
584 {source?.defaultBranch ?? "main"}
585 </span>
586 }
587 />
588 <Stat
589 label="Latest commit"
590 to={commit ? `${base}/commit/${commit.hash}` : undefined}
591 value={
592 commit ? (
593 <span title={commit.message.split("\n")[0]}>
594 <span className="font-mono">{commit.hash.slice(0, 7)}</span>{" "}
595 <span className="font-normal text-muted">
596 <TimeAgo at={commit.authoredAt} />
597 </span>
598 </span>
599 ) : (
600 "No commits yet"
601 )
602 }
603 />
604 <Stat
605 label={member && settings?.enabled ? "Last deploy" : "Open"}
606 to={member && settings?.enabled ? `${base}/deployments` : `${base}/pulls`}
607 value={
608 member && settings?.enabled ? (
609 latestProduction ? (
610 <span className="inline-flex items-center gap-2">
611 <StatusDot status={latestProduction.status} />
612 <span className="font-normal text-muted">
613 <TimeAgo at={latestProduction.createdAt} />
Projects: what a workspace builds and runs, first on every page614 </span>
615 </span>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API616 ) : (
617 "None yet"
618 )
619 ) : (
620 `${open.pulls} pull requests · ${open.issues} issues`
621 )
622 }
623 />
624 </div>
625 {commit && (
626 <p className="truncate border-t border-line px-5 py-2.5 text-xs text-muted sm:px-6">
627 <GitCommitHorizontal size={12} className="mr-1.5 inline text-faint" />
628 {commit.message.split("\n")[0]} <span className="text-faint">· {commit.author.name}</span>
629 </p>
630 )}
631 </section>
632
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look633 {checklist && <ProductionChecklist base={base} items={checklist} />}
634
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API635 <Panel
636 title="Right now"
637 icon={<span className={`block size-2 rounded-full ${agentsLive.length > 0 ? "animate-pulse bg-merged" : "bg-line-strong"}`} />}
638 count={agentsLive.length}
639 all={{ to: `${base}/agents`, label: "Agents" }}
640 >
641 {agentsLive.length > 0 && (
642 <ul className="mb-3 grid grid-cols-1 gap-2 sm:grid-cols-2 xl:grid-cols-3">
643 {agentsLive.map((run) => (
644 <li key={run.id} className="min-w-0 rounded-xl border border-merged/30 bg-surface px-3.5 py-3">
645 <div className="flex items-center gap-2 text-sm">
646 <Avatar name={run.agent} size={18} />
647 <span className="font-medium">{run.agent}</span>
648 <span className="text-xs text-muted">{RUN_KIND_LABEL[run.kind].toLowerCase()}</span>
649 <span className="grow" />
650 <span className="text-xs text-muted">
651 <Elapsed from={run.startedAt ?? run.createdAt} />
Projects: what a workspace builds and runs, first on every page652 </span>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API653 </div>
654 <Link
655 to={run.number != null ? `${base}/pull/${run.number}` : `${base}/agents/runs/${run.id}`}
656 className="mt-1 block truncate text-[0.8125rem] hover:text-accent"
657 >
658 {run.title ?? "A run"} {run.number != null && <span className="text-faint">#{run.number}</span>}
659 </Link>
660 {run.step && (
661 <p className="mt-1 truncate font-mono text-[0.6875rem] text-fg/70" title={run.step}>
662 <span className="mr-1.5 inline-block size-1.5 animate-pulse rounded-full bg-merged align-middle" />
663 {run.step}
664 </p>
665 )}
666 <p className="mt-1.5 flex items-center gap-3 text-[0.6875rem] text-faint">
667 {formatCost(run.costUsd) && <span>{formatCost(run.costUsd)} so far</span>}
668 <Link to={`${base}/agents/runs/${run.id}`} className="hover:text-fg">
669 {run.stepCount} steps
670 </Link>
671 {run.number != null && (
672 <Link to={`${base}/sessions/${run.number}`} className="hover:text-fg">
673 Session
674 </Link>
675 )}
676 </p>
677 </li>
678 ))}
679 </ul>
680 )}
681 {!loaderData.pullsLoaded ? (
682 <Unavailable what="Pull requests" />
683 ) : Object.values(columns).every((c) => c.length === 0) ? (
684 <Quiet
685 action={
686 member ? (
687 <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">
688 <Bot size={12} /> Open an issue for an agent
689 </Link>
690 ) : null
691 }
692 >
693 Nothing is moving. Open an issue and hand it to g1t-agent, or push a branch and open a pull request.
694 </Quiet>
695 ) : (
696 <Pipeline columns={columns} base={base} />
Projects: what a workspace builds and runs, first on every page697 )}
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API698 </Panel>
Projects: what a workspace builds and runs, first on every page699
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API700 <div className="grid gap-x-8 gap-y-8 lg:grid-cols-[minmax(0,1fr)_20rem]">
701 <div className="min-w-0 space-y-8">
702 {needs.length > 0 && (
703 <Panel title="Needs you" icon={<Hand size={14} className="text-warn" />} count={needs.length}>
704 <NeedsList needs={needs} limit={5} />
705 </Panel>
706 )}
707
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look708 <Panel
709 title="Active branches"
710 icon={<GitBranch size={14} />}
711 count={branches?.total || null}
712 >
713 {!branches ? (
714 <Unavailable what="Branches" />
715 ) : branches.shown.length === 0 ? (
716 <Quiet>
717 Only {branches.main} so far. Branches pushed here show with how far each has moved from {branches.main}, and
718 its pull request and preview.
719 </Quiet>
720 ) : (
721 <>
722 <ActiveBranches branches={branches.shown} base={base} main={branches.main} />
723 {branches.total > branches.shown.length && (
724 <p className="mt-2 px-1 text-xs text-faint">
725 The {branches.shown.length} most recently changed of {branches.total} branches.
726 </p>
727 )}
728 </>
729 )}
730 </Panel>
731
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API732 <Panel title="Recent changes" icon={<GitMerge size={14} />} all={{ to: `${base}/pulls?state=closed`, label: "All landed" }}>
733 {landed.length === 0 ? (
734 <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 page735 ) : (
736 <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 API737 {landed.map((pull) => {
738 const byAgent = pull.runtime === "hosted" || pull.agent === "g1t-agent";
Projects: what a workspace builds and runs, first on every page739 return (
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API740 <li key={pull.id} className="flex items-start gap-3 px-4 py-3">
741 <GitMerge size={15} className="mt-0.5 shrink-0 text-merged" />
742 <span className="min-w-0 grow">
743 <Link to={`${base}/pull/${pull.number}`} prefetch="intent" className="block truncate text-sm font-medium hover:text-accent">
744 {pull.title}
745 </Link>
746 <span className="mt-0.5 flex flex-wrap items-center gap-x-1.5 text-xs text-muted">
747 <span className="font-mono text-faint">#{pull.number}</span>
748 <span>·</span>
749 <span className="inline-flex items-center gap-1">
750 <Avatar name={pull.agent} size={13} />
751 {byAgent ? `made by ${pull.agent}` : `by ${pull.author.username}`}
752 </span>
753 {pull.issue != null && (
754 <>
755 <span>· for</span>
756 <Link to={`${base}/issues/${pull.issue}`} className="hover:text-fg">
757 #{pull.issue}
758 </Link>
759 </>
760 )}
761 {pull.mergedBy && <span>· landed by {pull.mergedBy}</span>}
762 <span>
763 · <TimeAgo at={pull.mergedAt ?? pull.updatedAt} />
Projects: what a workspace builds and runs, first on every page764 </span>
765 </span>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API766 </span>
767 <span className="hidden shrink-0 text-xs text-faint sm:block">
768 <ChangeSize files={pull.files} />
769 </span>
770 {byAgent && (
771 <Link to={`${base}/sessions/${pull.number}`} className="shrink-0 text-xs text-muted hover:text-fg">
772 Session
773 </Link>
774 )}
Projects: what a workspace builds and runs, first on every page775 </li>
776 );
777 })}
778 </ul>
779 )}
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API780 </Panel>
Projects: what a workspace builds and runs, first on every page781
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API782 <Panel title="Activity" icon={<ActivityIcon size={14} />}>
783 {loaderData.eventsLoaded ? (
784 <ActivityFeed groups={groups} showRepo={false} limit={8} empty="Nothing has happened here yet." />
785 ) : (
786 <Unavailable what="Activity" />
787 )}
788 </Panel>
789
790 {previews.length > 0 && (
791 <Panel title="Previews" icon={<GitBranch size={14} />} count={previews.length} all={{ to: `${base}/deployments`, label: "Deployments" }}>
792 <ul className="divide-y divide-line overflow-hidden rounded-xl border border-line bg-surface">
793 {previews.map((app) => (
794 <li key={app.url} className="flex items-center gap-3 px-4 py-2.5 text-sm">
795 <span className="min-w-0 grow">
796 <a href={app.url} className="block truncate font-mono text-[0.8125rem] hover:text-accent">
797 {host(app.url)}
798 </a>
799 <span className="text-xs text-muted">
800 {app.branch}
801 {app.number != null && (
802 <>
803 {" · "}
804 <Link to={`${base}/pull/${app.number}`} className="hover:underline">
805 #{app.number}
806 </Link>
807 </>
808 )}
809 </span>
Projects: what a workspace builds and runs, first on every page810 </span>
811 <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 API812 <TimeAgo at={app.deployedAt} />
Projects: what a workspace builds and runs, first on every page813 </span>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API814 </li>
815 ))}
816 </ul>
817 </Panel>
818 )}
819 </div>
Projects: what a workspace builds and runs, first on every page820
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API821 <aside className="space-y-4 text-sm">
822 {member && (
823 <section className="rounded-xl border border-line bg-surface p-5">
824 <div className="flex items-center justify-between">
825 <h2 className="flex items-center gap-1.5 text-sm font-semibold">
826 <Brain size={14} className="text-merged" />
827 What agents know here
828 </h2>
829 <Link to={`${base}/memory`} className="text-xs text-muted hover:text-fg">
830 Memory
831 </Link>
Projects: what a workspace builds and runs, first on every page832 </div>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API833 {!loaderData.memoriesLoaded ? (
834 <p className="mt-3 text-xs text-muted">Memory could not be loaded just now.</p>
835 ) : knows.length === 0 ? (
836 <p className="mt-3 text-xs leading-5 text-muted">
837 Nothing yet. Agents note conventions, decisions and traps as they work, and every agent starting here reads them.
838 </p>
839 ) : (
840 <ul className="mt-3 space-y-2.5">
841 {knows.map((memory) => (
842 <li key={memory.id} className="text-xs leading-5">
843 <p className="line-clamp-3 text-fg-soft">
844 {memory.pinned && <Pin size={11} className="mr-1 inline text-accent" />}
845 {memory.text}
846 </p>
847 <p className="text-faint">
848 {memory.kind} · {memory.createdBy} · <TimeAgo at={memory.createdAt} />
849 </p>
850 </li>
851 ))}
852 {loaderData.memoryCount > knows.length && (
853 <li>
854 <Link to={`${base}/memory`} className="text-xs text-muted hover:text-fg">
855 {loaderData.memoryCount - knows.length} more
856 </Link>
857 </li>
858 )}
859 </ul>
860 )}
861 </section>
Projects: what a workspace builds and runs, first on every page862 )}
863
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API864 <section className="rounded-xl border border-line bg-surface p-5">
865 <h2 className="flex items-center gap-1.5 text-sm font-semibold">
866 <HeartPulse size={14} className="text-faint" />
867 Health
Project dependencies: addresses, preview stacks, Affects, and agents who know868 </h2>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API869 <div className="mt-4 space-y-4 text-xs">
870 <div>
871 <div className="flex items-baseline justify-between">
872 <span className="text-muted">Checks passing</span>
873 <span className="font-medium tabular-nums">{percent(health.passRate)}</span>
874 </div>
875 <div className="mt-1.5">
876 <Meter value={health.passRate} tone={health.passRate != null && health.passRate < 0.7 ? "bg-warn" : "bg-accent"} />
877 </div>
878 <p className="mt-1 text-faint">
879 {health.checkRuns
880 ? `${health.checkRuns} recent runs · ${percent(health.firstPass.rate)} pass on the first try`
881 : "No checks have run recently."}
882 </p>
883 </div>
884 {member && (
Project dependencies: addresses, preview stacks, Affects, and agents who know885 <div>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API886 <div className="flex items-baseline justify-between">
887 <span className="text-muted">Deploys</span>
888 <Link to={`${base}/deployments`} className="text-faint hover:text-fg">
889 {builds.length ? `last ${Math.min(builds.length, 20)}` : "none yet"}
890 </Link>
891 </div>
892 <div className="mt-1.5">
893 <DeployStrip builds={builds} base={base} />
894 </div>
Project dependencies: addresses, preview stacks, Affects, and agents who know895 </div>
896 )}
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API897 <div>
898 <div className="flex items-baseline justify-between">
899 <span className="text-muted">Open issues by age</span>
900 <Link to={`${base}/issues`} className="font-medium tabular-nums hover:text-accent">
901 {open.issues}
902 </Link>
903 </div>
904 {ages ? (
905 <ul className="mt-2 space-y-1.5">
906 {ages.map((bucket) => (
907 <li key={bucket.label} className="grid grid-cols-[6.5rem_1fr_1.5rem] items-center gap-2">
908 <span className="text-faint">{bucket.label}</span>
909 <span className="block h-1.5 overflow-hidden rounded-full bg-line">
910 <span
911 className={`block h-full rounded-full ${bucket.label === "Older" ? "bg-warn" : "bg-fg-soft/60"}`}
912 style={{ width: `${(bucket.count / oldest) * 100}%` }}
913 />
914 </span>
915 <span className="text-right tabular-nums text-muted">{bucket.count}</span>
Project dependencies: addresses, preview stacks, Affects, and agents who know916 </li>
917 ))}
918 </ul>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API919 ) : (
920 <p className="mt-1 text-faint">Issues could not be loaded just now.</p>
921 )}
922 </div>
Project dependencies: addresses, preview stacks, Affects, and agents who know923 </div>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API924 </section>
Project dependencies: addresses, preview stacks, Affects, and agents who know925
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API926 <section className="rounded-xl border border-line bg-surface p-5">
927 <div className="flex items-center justify-between">
928 <h2 className="flex items-center gap-1.5 text-sm font-semibold">
929 <Network size={14} className="text-faint" />
930 Dependencies
931 </h2>
932 {member && (
933 <Link to={`${base}/settings/dependencies`} className="text-xs text-muted hover:text-fg">
934 Manage
935 </Link>
936 )}
937 </div>
938 {!dependencies ? (
939 <p className="mt-3 text-xs text-muted">Dependencies could not be loaded just now.</p>
940 ) : dependencies.dependsOn.length === 0 && dependencies.usedBy.length === 0 ? (
941 <p className="mt-3 text-xs leading-5 text-muted">
942 Uses no other project, and none uses it. Declare one, and builds get its address and agents know what depends on
943 what.
944 </p>
945 ) : (
946 <div className="mt-3 space-y-3 text-xs">
947 {dependencies.dependsOn.length > 0 && (
948 <div>
949 <p className="flex items-center gap-1 text-muted">
950 <ArrowUpRight size={12} /> Uses
951 </p>
952 <ul className="mt-1.5 space-y-1">
953 {dependencies.dependsOn.map((d) => (
954 <li key={d.slug} className="flex items-center justify-between gap-2">
955 <Link to={`/${params.owner}/${d.slug}`} className="font-medium hover:underline">
956 {d.name}
957 </Link>
958 {d.as && <code className="font-mono text-faint">{d.as}</code>}
959 </li>
960 ))}
961 </ul>
962 </div>
963 )}
964 {dependencies.usedBy.length > 0 && (
965 <div>
966 <p className="flex items-center gap-1 text-muted">
967 <ArrowDownLeft size={12} /> Used by
968 </p>
969 <ul className="mt-1.5 space-y-1">
970 {dependencies.usedBy.map((d) => (
971 <li key={d.slug}>
972 <Link to={`/${params.owner}/${d.slug}`} className="font-medium hover:underline">
973 {d.name}
974 </Link>
975 </li>
976 ))}
977 </ul>
978 </div>
979 )}
980 </div>
981 )}
Projects: what a workspace builds and runs, first on every page982 </section>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API983
984 {source && (
985 <section className="rounded-xl border border-line bg-surface p-5">
986 <h2 className="text-sm font-semibold">Clone</h2>
987 <div className="mt-3">
988 <CopyLine text={`git clone https://g1t.sh/${source.repo.namespace}/${source.repo.name}.git`} />
989 </div>
990 </section>
991 )}
992 </aside>
993 </div>
Projects: what a workspace builds and runs, first on every page994 </div>
995 );
996}