Skip to content

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

475 lines21,356 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.

Navigation: the workspace's own page with tabs, pins and recent in the sidebar1import { ArrowRight, ArrowUpRight, Box, CircleDot, GitBranch, GitPullRequest, KeyRound, Lock, Pin, Plus, Rocket } from "lucide-react";
Agents as a team: lifecycle, merge queue, billing and a new shell2import { Link } from "react-router";
3
A project is an app or a library: libraries show their package and how to ship a release, not production4import type { PackageSummary, Project, ProjectDeploys, User } from "@g1t/contracts";
Projects: what a workspace builds and runs, first on every page5
Agents as a team: lifecycle, merge queue, billing and a new shell6import type { Route } from "./+types/overview";
Projects: what a workspace builds and runs, first on every page7import { host, StatusDot } from "../../components/deploy";
Agents as a team: lifecycle, merge queue, billing and a new shell8import { Avatar, ButtonLink, CopyLine, Pill, TimeAgo } from "../../components/ui";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look9import { UsageCard } from "../../components/usage-card";
10import { PullIcon } from "../../components/work-icons";
11import { planStatus, type UsageGlance, usageGlance } from "../../lib/billing";
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent12import { openedBy } from "../../lib/opened-by";
Merge branch 'worktree-agent-aaf03bdceac799c89'13import { cloneUrl, useAddresses } from "../../lib/addresses";
Merge branch 'projects-kind-and-links'14import { kindLabel, libraryPackages, packageLine, packagePath, primaryLink } from "../../lib/project-kind";
Navigation: the workspace's own page with tabs, pins and recent in the sidebar15import { PinButton } from "../../components/pin-button";
16import { sortProjects } from "../../lib/project-list";
A project is an app or a library: libraries show their package and how to ship a release, not production17import { billing, deployments, identity, packages, projects as projectsApi, work } from "../../lib/services.server";
Agents as a team: lifecycle, merge queue, billing and a new shell18import { getViewer, roleIn } from "../../lib/session.server";
Navigation: the workspace's own page with tabs, pins and recent in the sidebar19import { workspaceProjects } from "../../lib/workspace-projects.server";
Agents as a team: lifecycle, merge queue, billing and a new shell20
Navigation: the workspace's own page with tabs, pins and recent in the sidebar21/** Projects whose open issues and pull requests are counted: the most active. */
Agents as a team: lifecycle, merge queue, billing and a new shell22const MAX_COUNTED = 30;
Navigation: the workspace's own page with tabs, pins and recent in the sidebar23/** Projects shown under Recently active, after the pins. */
24const MAX_ACTIVE = 6;
Projects: what a workspace builds and runs, first on every page25/** Projects whose pull requests are listed under "In progress". */
Agents as a team: lifecycle, merge queue, billing and a new shell26const MAX_LISTED = 8;
27const MAX_PULLS = 8;
28const MAX_FACES = 8;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look29/** The trial as published, when the price book cannot be read. */
30const DEFAULT_TRIAL_MICROS = 5_000_000;
Agents as a team: lifecycle, merge queue, billing and a new shell31
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look32/**
33 * The month for the Usage card, for members only: what was spent, what
34 * pays first and what it went on. Fetched here, not with the sidebar, so
35 * only this page pays for it; a billing service that cannot answer leaves
36 * the card out rather than the page.
37 */
38async function usageFor(slug: string, viewer: User | null): Promise<UsageGlance | null> {
39 const now = new Date();
40 const monthStart = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1)).toISOString();
41 const [account, usage, features, entitlements, limit, book] = await Promise.all([
42 billing.account(slug, viewer).catch(() => null),
43 billing.usage(slug, viewer, monthStart).catch(() => null),
44 billing.features(slug, viewer).catch(() => null),
45 billing.entitlements(slug).catch(() => null),
46 billing.limit(slug, viewer).catch(() => null),
47 billing.prices().catch(() => null),
48 ]);
49 if (!account?.ok || !usage?.ok) return null;
50 // Without payments set up (a g1t run without billing), there is no plan to show.
51 if (!account.value.status.enabled && !usage.value.free) return null;
52 const plan = features?.ok ? (features.value.find((state) => state.plan.feature === "plan") ?? null) : null;
53 return usageGlance({
54 usage: usage.value,
55 status: planStatus(plan, entitlements),
56 entitlements,
57 limit: limit?.ok ? limit.value : null,
58 trialMicros: book?.free?.trialWorkspaceMicros ?? DEFAULT_TRIAL_MICROS,
59 });
60}
61
Agents as a team: lifecycle, merge queue, billing and a new shell62export async function loader({ params, context }: Route.LoaderArgs) {
63 const viewer = getViewer(context);
64 const slug = params.owner.toLowerCase();
65 const role = roleIn(viewer, slug);
Navigation: the workspace's own page with tabs, pins and recent in the sidebar66 const [listed, members, deploys, usage, published, shortcuts] = await Promise.all([
67 // The listing the sidebar and the Projects tab share.
68 workspaceProjects(slug, viewer),
Agents as a team: lifecycle, merge queue, billing and a new shell69 role ? identity.listMembers(slug, viewer) : null,
Projects: what a workspace builds and runs, first on every page70 role ? deployments.overview(slug, viewer) : null,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look71 role ? usageFor(slug, viewer) : null,
A project is an app or a library: libraries show their package and how to ship a release, not production72 // One listing for every card: a library's card shows its package where an app's shows production.
73 packages.list(slug, viewer).catch(() => null),
Navigation: the workspace's own page with tabs, pins and recent in the sidebar74 // The viewer's pins lead the page, as they lead the sidebar.
75 role ? projectsApi.shortcuts(slug, viewer).catch(() => null) : null,
Agents as a team: lifecycle, merge queue, billing and a new shell76 ]);
Projects: what a workspace builds and runs, first on every page77 const projects = listed.ok ? listed.value : [];
Navigation: the workspace's own page with tabs, pins and recent in the sidebar78 const pinnedIds = new Set((shortcuts?.pinned ?? []).map((project) => project.id));
79 const byId = new Map(projects.map((project) => [project.id, project]));
80 const pinned = (shortcuts?.pinned ?? []).map((pin) => byId.get(pin.id)).filter((project): project is Project => project != null);
81 // However many there are: the most active first, archived ones last.
82 const byActivity = sortProjects(
83 projects.map((project) => ({ ...project, pushedAt: project.pushedAt ?? null, activity: project.activity ?? 0, deploying: false })),
84 "active",
85 )
86 .sort((a, b) => Number(a.archived) - Number(b.archived))
87 .map((listedProject) => byId.get(listedProject.id)!);
88 const active = byActivity.filter((project) => !pinnedIds.has(project.id)).slice(0, MAX_ACTIVE);
Agents as a team: lifecycle, merge queue, billing and a new shell89
Projects: what a workspace builds and runs, first on every page90 const pathOf = (project: Project) =>
91 project.source.kind === "hosted" ? project.source.repo : { namespace: project.workspace, name: project.slug };
Navigation: the workspace's own page with tabs, pins and recent in the sidebar92 const counted = [...new Map([...pinned, ...active, ...byActivity].map((project) => [project.id, project])).values()].slice(0, Math.max(MAX_COUNTED, pinned.length + active.length));
Projects: what a workspace builds and runs, first on every page93 const counts = await Promise.all(counted.map((project) => work.counts(pathOf(project), viewer)));
Agents as a team: lifecycle, merge queue, billing and a new shell94 const open: Record<string, { issues: number; pulls: number }> = {};
Projects: what a workspace builds and runs, first on every page95 counted.forEach((project, i) => {
Agents as a team: lifecycle, merge queue, billing and a new shell96 const result = counts[i];
Projects: what a workspace builds and runs, first on every page97 if (result.ok) open[project.id] = result.value;
Agents as a team: lifecycle, merge queue, billing and a new shell98 });
99
Projects: what a workspace builds and runs, first on every page100 const busy = counted.filter((project) => (open[project.id]?.pulls ?? 0) > 0).slice(0, MAX_LISTED);
101 const lists = await Promise.all(busy.map((project) => work.listPulls(pathOf(project), viewer, "open")));
Agents as a team: lifecycle, merge queue, billing and a new shell102 const pulls = lists
Projects: what a workspace builds and runs, first on every page103 .flatMap((result, i) => (result.ok ? result.value.map((pull) => ({ pull, project: busy[i] })) : []))
Agents as a team: lifecycle, merge queue, billing and a new shell104 .sort((a, b) => b.pull.updatedAt.localeCompare(a.pull.updatedAt))
105 .slice(0, MAX_PULLS);
106
Projects: what a workspace builds and runs, first on every page107 const bySlug: Record<string, ProjectDeploys> = {};
108 for (const entry of deploys?.ok ? deploys.value : []) bySlug[entry.slug] = entry;
109
A project is an app or a library: libraries show their package and how to ship a release, not production110 const byRepo = new Map<string, PackageSummary[]>();
111 for (const pkg of published?.ok ? published.value : []) {
112 if (pkg.repo) byRepo.set(pkg.repo.id, [...(byRepo.get(pkg.repo.id) ?? []), pkg]);
113 }
114 const packageOf: Record<string, PackageSummary> = {};
Navigation: the workspace's own page with tabs, pins and recent in the sidebar115 for (const project of [...pinned, ...active]) {
Merge branch 'projects-kind-and-links'116 if ((project.kind !== "library" && project.kind !== "tool") || project.source.kind !== "hosted") continue;
A project is an app or a library: libraries show their package and how to ship a release, not production117 const first = libraryPackages(byRepo.get(project.source.repoId) ?? [])[0];
118 if (first) packageOf[project.id] = first;
119 }
120
Projects: what a workspace builds and runs, first on every page121 return {
122 slug,
123 role,
Navigation: the workspace's own page with tabs, pins and recent in the sidebar124 total: projects.length,
Merge branch 'projects-kind-and-links'125 elsewhere: projects.filter((project) => project.runs === "elsewhere" && project.productionUrl).length,
Navigation: the workspace's own page with tabs, pins and recent in the sidebar126 pinned,
127 active,
Projects: what a workspace builds and runs, first on every page128 open,
129 deploys: bySlug,
A project is an app or a library: libraries show their package and how to ship a release, not production130 packageOf,
Projects: what a workspace builds and runs, first on every page131 pulls: pulls.map(({ pull, project }) => ({ pull, slug: project.slug, name: project.name })),
132 members: members?.ok ? members.value : null,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look133 usage,
Projects: what a workspace builds and runs, first on every page134 };
Agents as a team: lifecycle, merge queue, billing and a new shell135}
136
137function Stat({ value, label }: { value: number; label: string }) {
138 return (
139 <div className="rounded-xl border border-line bg-surface px-4 py-3">
140 <p className="text-xl font-semibold tabular-nums tracking-tight">{value}</p>
141 <p className="text-xs text-muted">{label}</p>
142 </div>
143 );
144}
145
Projects: what a workspace builds and runs, first on every page146/** What a member sees in a workspace with no projects yet. */
Agents as a team: lifecycle, merge queue, billing and a new shell147function GetStarted({ slug }: { slug: string }) {
Merge branch 'worktree-agent-aaf03bdceac799c89'148 const addresses = useAddresses();
Agents as a team: lifecycle, merge queue, billing and a new shell149 return (
Projects: what a workspace builds and runs, first on every page150 <div className="rounded-2xl border border-line bg-surface p-8">
151 <span className="flex size-10 items-center justify-center rounded-xl bg-accent/10 text-accent ring-1 ring-accent/30">
152 <Box size={18} />
153 </span>
154 <h2 className="mt-4 text-lg font-semibold tracking-tight">Create your first project</h2>
Agents as a team: lifecycle, merge queue, billing and a new shell155 <p className="mt-1.5 max-w-xl text-sm text-muted">
Projects: what a workspace builds and runs, first on every page156 A project is what you build and run: its code, the issues that say what should change, the agents and
157 people who change it, and where it is deployed.
Agents as a team: lifecycle, merge queue, billing and a new shell158 </p>
Projects: what a workspace builds and runs, first on every page159 <div className="mt-6 flex flex-wrap gap-3">
160 <ButtonLink to={`/new?workspace=${slug}`} variant="accent">
161 <Plus size={14} />
162 New project
163 </ButtonLink>
Agents as a team: lifecycle, merge queue, billing and a new shell164 </div>
Projects: what a workspace builds and runs, first on every page165 <p className="mt-6 text-sm text-muted">Or push code you already have, and it becomes a project:</p>
Agents as a team: lifecycle, merge queue, billing and a new shell166 <div className="mt-2 max-w-xl">
Merge branch 'worktree-agent-aaf03bdceac799c89'167 <CopyLine prompt text={`git push ${cloneUrl(addresses, `${slug}/my-project`)} main`} />
Agents as a team: lifecycle, merge queue, billing and a new shell168 </div>
169 </div>
170 );
171}
172
Projects: what a workspace builds and runs, first on every page173function ProjectCard({
174 project,
175 open,
176 deploys,
A project is an app or a library: libraries show their package and how to ship a release, not production177 pkg,
Projects: what a workspace builds and runs, first on every page178 member,
Navigation: the workspace's own page with tabs, pins and recent in the sidebar179 pinned,
Projects: what a workspace builds and runs, first on every page180}: {
181 project: Project;
182 open: { issues: number; pulls: number } | undefined;
183 deploys: ProjectDeploys | undefined;
A project is an app or a library: libraries show their package and how to ship a release, not production184 /** For a library, the package it publishes. */
185 pkg: PackageSummary | undefined;
Projects: what a workspace builds and runs, first on every page186 member: boolean;
Navigation: the workspace's own page with tabs, pins and recent in the sidebar187 /** Whether the viewer pinned it; null when they cannot pin. */
188 pinned: boolean | null;
Projects: what a workspace builds and runs, first on every page189}) {
Merge branch 'projects-kind-and-links'190 // A library or a tool shows the package it publishes.
191 const library = project.kind === "library" || project.kind === "tool";
Projects: what a workspace builds and runs, first on every page192 const base = `/${project.workspace}/${project.slug}`;
193 const production = deploys?.production ?? null;
Merge branch 'projects-kind-and-links'194 // Production where g1t serves it or elsewhere, else its homepage or docs.
195 const link = primaryLink(project, production?.url);
Projects: what a workspace builds and runs, first on every page196 const latest = deploys?.latest ?? null;
197 const source = project.source.kind === "hosted" ? project.source : null;
198 return (
199 <li className="group relative flex flex-col rounded-2xl border border-line bg-surface p-5 transition-colors hover:border-line-strong">
200 <div className="flex items-start gap-3">
201 <span className="flex size-9 shrink-0 items-center justify-center rounded-lg bg-raised text-muted ring-1 ring-line">
202 {project.private ? <Lock size={15} /> : <Box size={15} />}
203 </span>
204 <div className="min-w-0 grow">
205 <Link to={base} prefetch="intent" className="font-medium after:absolute after:inset-0 hover:underline">
206 {project.name}
207 </Link>
Merge branch 'projects-kind-and-links'208 {link && !(library && pkg && !production) ? (
Projects: what a workspace builds and runs, first on every page209 <a
Merge branch 'projects-kind-and-links'210 href={link}
Projects: what a workspace builds and runs, first on every page211 className="relative z-10 mt-0.5 flex items-center gap-1 truncate font-mono text-xs text-muted hover:text-accent"
212 >
Merge branch 'projects-kind-and-links'213 {host(link)}
Projects: what a workspace builds and runs, first on every page214 <ArrowUpRight size={11} className="shrink-0" />
215 </a>
A project is an app or a library: libraries show their package and how to ship a release, not production216 ) : library && pkg ? (
217 <Link
218 to={packagePath(pkg)}
219 className="relative z-10 mt-0.5 block truncate font-mono text-xs text-muted hover:text-accent"
220 >
221 {packageLine(pkg)}
222 </Link>
Projects: what a workspace builds and runs, first on every page223 ) : (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look224 <p className="mt-0.5 truncate text-xs text-faint">
Merge branch 'projects-kind-and-links'225 {project.description ??
226 (library
227 ? "Not published yet"
228 : project.runs === "g1t"
229 ? deploys?.enabled
230 ? "Not deployed yet"
231 : "Deployments are off"
232 : kindLabel(project))}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look233 </p>
Projects: what a workspace builds and runs, first on every page234 )}
235 </div>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look236 {(project.private || project.archived) && (
237 <span className="flex shrink-0 gap-1.5">
238 {project.private && <Pill>private</Pill>}
239 {project.archived && <Pill>archived</Pill>}
240 </span>
241 )}
Navigation: the workspace's own page with tabs, pins and recent in the sidebar242 {pinned != null && (
243 <PinButton workspace={project.workspace} slug={project.slug} name={project.name} pinned={pinned} compact className="-mt-1.5 -mr-2" />
244 )}
Projects: what a workspace builds and runs, first on every page245 </div>
246
Merge branch 'projects-kind-and-links'247 {project.description && (link || (library && pkg)) && <p className="mt-3 line-clamp-2 text-sm text-muted">{project.description}</p>}
Projects: what a workspace builds and runs, first on every page248
249 <div className="mt-auto pt-5">
250 {member && latest && (
251 <p className="mb-3 flex items-center gap-2 text-xs text-muted">
252 <StatusDot status={latest.status} />
253 <span className="truncate">
254 {latest.kind === "production" ? "Production" : `Preview of ${latest.branch}`}
255 </span>
256 <span className="ml-auto shrink-0 text-faint">
257 <TimeAgo at={latest.createdAt} />
258 </span>
259 </p>
260 )}
261 <div className="flex items-center gap-4 border-t border-line pt-3 text-xs text-muted">
262 {source && (
263 <span className="flex min-w-0 items-center gap-1.5 font-mono">
264 <GitBranch size={12} className="shrink-0 text-faint" />
265 <span className="truncate">{source.defaultBranch}</span>
266 </span>
267 )}
268 <span className="flex items-center gap-1">
269 <CircleDot size={12} className="text-faint" />
270 {open?.issues ?? 0}
271 </span>
272 <span className="flex items-center gap-1">
273 <GitPullRequest size={12} className="text-faint" />
274 {open?.pulls ?? 0}
275 </span>
276 {member && deploys?.enabled && (deploys.previews ?? 0) > 0 && (
Merge branch 'worktree-agent-ad7c6d88d93adc817'277 <span className="ml-auto flex items-center gap-1 text-success">
Projects: what a workspace builds and runs, first on every page278 <Rocket size={12} />
279 {deploys.previews} {deploys.previews === 1 ? "preview" : "previews"}
280 </span>
281 )}
282 </div>
283 </div>
284 </li>
285 );
286}
287
Agents as a team: lifecycle, merge queue, billing and a new shell288export default function WorkspaceOverview({ loaderData }: Route.ComponentProps) {
Navigation: the workspace's own page with tabs, pins and recent in the sidebar289 const { slug, role, total, pinned, active, open, deploys, pulls, members, usage } = loaderData;
290 const card = (project: Project, isPinned: boolean) => (
291 <ProjectCard
292 key={project.id}
293 project={project}
294 open={open[project.id]}
295 deploys={deploys[project.slug]}
296 pkg={loaderData.packageOf[project.id]}
297 member={role != null}
298 pinned={role ? isPinned : null}
299 />
300 );
Agents as a team: lifecycle, merge queue, billing and a new shell301 const totals = Object.values(open).reduce(
Projects: what a workspace builds and runs, first on every page302 (sum, counts) => ({ issues: sum.issues + counts.issues, pulls: sum.pulls + counts.pulls }),
Agents as a team: lifecycle, merge queue, billing and a new shell303 { issues: 0, pulls: 0 },
304 );
Merge branch 'projects-kind-and-links'305 // Live on g1t, or deployed elsewhere with an address.
306 const liveApps =
307 Object.values(deploys).filter((entry) => entry.production).length +
308 loaderData.elsewhere;
Agents as a team: lifecycle, merge queue, billing and a new shell309 return (
310 <div className="grid gap-10 lg:grid-cols-[1fr_18rem]">
311 <div className="min-w-0 space-y-10">
Navigation: the workspace's own page with tabs, pins and recent in the sidebar312 {total === 0 && role ? (
Agents as a team: lifecycle, merge queue, billing and a new shell313 <GetStarted slug={slug} />
314 ) : (
315 <>
Projects: what a workspace builds and runs, first on every page316 <div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
Navigation: the workspace's own page with tabs, pins and recent in the sidebar317 <Stat value={total} label={total === 1 ? "Project" : "Projects"} />
Projects: what a workspace builds and runs, first on every page318 <Stat value={liveApps} label="In production" />
Agents as a team: lifecycle, merge queue, billing and a new shell319 <Stat value={totals.issues} label="Open issues" />
320 <Stat value={totals.pulls} label="Pull requests in progress" />
321 </div>
322
Navigation: the workspace's own page with tabs, pins and recent in the sidebar323 {role && (
324 <section aria-labelledby="pinned">
325 <h2 id="pinned" className="text-sm font-medium text-muted">
326 Pinned
327 </h2>
328 {pinned.length > 0 ? (
329 <ul className="mt-3 grid gap-4 sm:grid-cols-2">{pinned.map((project) => card(project, true))}</ul>
330 ) : (
331 <p className="mt-3 flex items-center gap-2 rounded-xl border border-dashed border-line px-4 py-3 text-sm text-muted">
332 <Pin size={14} className="shrink-0 text-faint" />
333 Pin the projects you use most, from their page or from Projects, to keep them here and in your sidebar.
334 </p>
Projects: what a workspace builds and runs, first on every page335 )}
Navigation: the workspace's own page with tabs, pins and recent in the sidebar336 </section>
337 )}
338
339 {active.length > 0 && (
340 <section aria-labelledby="active">
341 <div className="flex items-center justify-between">
342 <h2 id="active" className="text-sm font-medium text-muted">
343 {role ? "Recently active" : "Projects"}
344 </h2>
345 {role && (
346 <Link to={`/new?workspace=${slug}`} className="inline-flex items-center gap-1 text-xs text-muted hover:text-fg">
347 <Plus size={13} />
348 New project
349 </Link>
350 )}
351 </div>
352 <ul className="mt-3 grid gap-4 sm:grid-cols-2">{active.map((project) => card(project, false))}</ul>
353 </section>
354 )}
355
356 <Link
357 to={`/${slug}/-/projects`}
358 prefetch="intent"
359 className="flex items-center justify-between rounded-xl border border-line bg-surface px-4 py-3 text-sm transition-colors hover:border-line-strong"
360 >
361 <span>
362 All projects <span className="tabular-nums text-muted">({total})</span>
363 </span>
364 <span className="flex items-center gap-1 text-xs text-muted">
365 Search, filter and sort
366 <ArrowRight size={13} />
367 </span>
368 </Link>
Projects: what a workspace builds and runs, first on every page369
Agents as a team: lifecycle, merge queue, billing and a new shell370 {pulls.length > 0 && (
371 <section>
372 <h2 className="text-sm font-medium text-muted">In progress</h2>
373 <ul className="mt-3 divide-y divide-line overflow-hidden rounded-xl border border-line bg-surface">
Projects: what a workspace builds and runs, first on every page374 {pulls.map(({ pull, slug: project, name }) => (
Agents as a team: lifecycle, merge queue, billing and a new shell375 <li key={pull.id}>
376 <Link
377 prefetch="intent"
Projects: what a workspace builds and runs, first on every page378 to={`/${slug}/${project}/pull/${pull.number}`}
Agents as a team: lifecycle, merge queue, billing and a new shell379 className="flex items-center gap-3 px-4 py-3 transition-colors hover:bg-raised"
380 >
381 <PullIcon status={pull.status} />
382 <span className="min-w-0 grow">
383 <span className="block truncate font-medium">{pull.title}</span>
Projects: what a workspace builds and runs, first on every page384 <span className="text-xs text-muted">
385 {name} <span className="font-mono">#{pull.number}</span>
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent386 {pull.issue != null && ` · for #${pull.issue}`} · {openedBy(pull).name}
Agents as a team: lifecycle, merge queue, billing and a new shell387 </span>
388 </span>
389 <span className="hidden shrink-0 text-xs text-muted sm:block">
390 {pull.status === "draft" ? "Being worked on" : "Ready for review"}
391 </span>
392 <span className="w-14 shrink-0 text-right text-xs text-faint">
393 <TimeAgo at={pull.updatedAt} />
394 </span>
395 </Link>
396 </li>
397 ))}
398 </ul>
399 </section>
400 )}
401 </>
402 )}
403 </div>
404
405 <aside className="space-y-6">
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look406 {role && usage && <UsageCard slug={slug} glance={usage} owner={role === "owner"} />}
407
Agents as a team: lifecycle, merge queue, billing and a new shell408 {members ? (
409 <section>
410 <div className="flex items-center justify-between">
Projects: what a workspace builds and runs, first on every page411 <h2 className="text-sm font-medium">Members</h2>
Agents as a team: lifecycle, merge queue, billing and a new shell412 <Link to={`/${slug}/-/people`} className="text-xs text-muted hover:text-fg">
413 {role === "owner" ? "Manage" : "See all"}
414 </Link>
415 </div>
416 <ul className="mt-3 space-y-1.5">
417 {members.slice(0, MAX_FACES).map((member) => (
418 <li key={member.username} className="flex items-center gap-2 text-sm">
419 <Avatar name={member.username} />
420 <span className="grow truncate font-mono">{member.username}</span>
421 {member.role === "owner" && <Pill>owner</Pill>}
422 </li>
423 ))}
424 </ul>
Projects: what a workspace builds and runs, first on every page425 {members.length > MAX_FACES && <p className="mt-2 text-xs text-faint">and {members.length - MAX_FACES} more</p>}
Agents as a team: lifecycle, merge queue, billing and a new shell426 </section>
427 ) : (
428 <p className="text-sm text-muted">
Projects: what a workspace builds and runs, first on every page429 Members of this workspace build its projects. Anyone can open an issue or a pull request on a public one.
Agents as a team: lifecycle, merge queue, billing and a new shell430 </p>
431 )}
432
433 {role && (
434 <section className="rounded-xl border border-line bg-surface p-5">
435 <h2 className="flex items-center gap-2 font-medium">
Projects: what a workspace builds and runs, first on every page436 <Rocket size={15} className="text-faint" />
437 Deploy on g1t.page
438 </h2>
439 <p className="mt-1.5 text-sm text-muted">
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look440 Off until you turn them on for a project, under its Settings. Then production builds from its default
441 branch, with a live preview for every pull request. Apps cost nothing while no one visits.
Projects: what a workspace builds and runs, first on every page442 </p>
443 <Link
444 prefetch="intent"
445 to={`/${slug}/-/billing`}
446 className="mt-3 inline-flex items-center gap-1 text-sm text-accent hover:underline"
447 >
448 Billing and plans <ArrowRight size={13} />
449 </Link>
450 </section>
451 )}
452
453 {role && (
454 <section className="rounded-xl border border-line bg-surface p-5">
455 <h2 className="flex items-center gap-2 font-medium">
Agents as a team: lifecycle, merge queue, billing and a new shell456 <KeyRound size={15} className="text-faint" />
457 Automate without a service account
458 </h2>
459 <p className="mt-1.5 text-sm text-muted">
Projects: what a workspace builds and runs, first on every page460 A workspace has access tokens of its own for CI, integrations and agents. They act as the workspace,
461 not as a person.
Agents as a team: lifecycle, merge queue, billing and a new shell462 </p>
463 <Link
464 prefetch="intent"
465 to={`/${slug}/-/tokens`}
466 className="mt-3 inline-flex items-center gap-1 text-sm text-accent hover:underline"
467 >
468 Access tokens <ArrowRight size={13} />
469 </Link>
470 </section>
471 )}
472 </aside>
473 </div>
474 );
475}

This file's history is long; its oldest lines are credited to the oldest commit read.