flagon-io/g1t

public

Git for AI scale: a forge for thousands of agents working on the same code at once.

Commit

A flat project menu with Code first, views as tabs, and Stripe for every owner

The project menu is flat, in the order people use it: Code, Issues, Pull requests, Agents, Workflows, Deployments, Security, Insights, Settings, with the project's own row as its overview. A page with more than one view shows them as tabs across its top (Files, Commits and what is coming under Code; Issues and Outcomes; Pull requests and the merge queue; Deployments, environments, logs and the rest), Soon views included, the way GitHub and Vercel do. What spans projects belongs to the workspace, not to one project: Boards (like GitHub Projects), Roadmap, Packages and the Agent fleet, under Across projects in the workspace's menu, each with its page. Billing opens with Stripe for every owner, comped workspaces included: card, invoices, receipts and billing details.

syntaqxcommitted Parentb1efcf2Browse files
8 files+346−2530/8 viewed
+79−210
11 import {
22 ArrowLeft,
3− ChevronRight,
3+ GanttChart,
4+ KanbanSquare,
5+ Package,
6+ Sparkles,
47 BarChart3,
58 BookMarked,
69 BookOpen,
3033 Users,
3134 Webhook,
3235 PlayCircle,
33− Activity,
3436 Bot,
35− Kanban,
3637 ShieldCheck,
3738 Rocket,
3839 X,
149150 }
150151
151152 /**
152− * A section of the project menu that opens to show its pages, as GitLab's
153− * does: one row with an icon, and its pages indented under it. It opens
154− * by itself when one of its pages is the current one, and otherwise
155− * remembers whether it was left open.
153+ * A page that is coming, as a link to its roadmap page: faint, with Soon,
154+ * and marked current on any of its section's Soon pages.
156155 */
157−function SidebarSection({
158− title,
159− icon,
160− paths = [],
161− soon = false,
162− children,
163−}: {
164− title: string;
165− icon: ReactNode;
166− /** Path prefixes of its pages: one being current opens it. */
167− paths?: string[];
168− /** Nothing in it is built yet. */
169− soon?: boolean;
170− children: ReactNode;
171−}) {
172− const { pathname } = useLocation();
173− const current = paths.some((prefix) => pathname === prefix || pathname.startsWith(prefix + "/"));
174− const key = `g1t.sidebar.${title}`;
175− const [open, setOpen] = useState<boolean>(current);
176− // What the person left it as, once the page is in the browser.
177− useEffect(() => {
178− if (current) {
179− setOpen(true);
180− return;
181− }
182− try {
183− const saved = localStorage.getItem(key);
184− if (saved != null) setOpen(saved === "1");
185− } catch {
186− // No storage: it stays as it is.
187− }
188− }, [current, key]);
189− const toggle = () => {
190− const next = !open;
191− setOpen(next);
192− try {
193− localStorage.setItem(key, next ? "1" : "0");
194− } catch {
195− // Remembered for this page only.
196− }
197− };
198− return (
199− <section>
200− <button
201− type="button"
202− onClick={toggle}
203− aria-expanded={open}
204− className={`group flex h-8 w-full items-center gap-2.5 rounded-md px-2 text-left text-[0.8125rem] transition-colors hover:bg-raised/60 ${
205− current ? "text-fg" : soon ? "text-faint hover:text-muted" : "text-muted hover:text-fg"
206− }`}
207− >
208− <span className={`shrink-0 ${current ? "text-muted" : "text-faint group-hover:text-muted"}`}>{icon}</span>
209− <span className={`min-w-0 grow truncate ${current ? "font-medium" : ""}`}>{title}</span>
210− {soon && !open && (
211− <span className="rounded-full px-1.5 py-px text-[0.625rem] font-medium tracking-wide text-muted uppercase ring-1 ring-line">
212− Soon
213− </span>
214− )}
215− <ChevronRight
216− size={14}
217− className={`shrink-0 text-faint transition-transform duration-150 ${open ? "rotate-90" : ""}`}
218− aria-hidden="true"
219− />
220− </button>
221− {open && (
222− <div className="relative mt-px mb-1 space-y-px before:absolute before:top-1 before:bottom-1 before:left-[1.1875rem] before:w-px before:bg-line">
223− {children}
224− </div>
225− )}
226− </section>
227− );
228−}
229−
230−/** A page inside a section: indented, without an icon of its own. */
231−function SidebarSubLink({
156+function SidebarSoonLink({
232157 to,
233− end,
234− count,
235158 also,
159+ icon,
160+ about,
236161 children,
237162 }: {
238163 to: string;
239− end?: boolean;
240− count?: number;
241− also?: string | string[];
164+ also?: string[];
165+ icon: ReactNode;
166+ about: string;
242167 children: ReactNode;
243168 }) {
244169 const { pathname } = useLocation();
245170 return (
246171 <NavLink
247172 to={to}
248− end={end}
173+ title={about}
249174 prefetch="intent"
250− className={({ isActive, isPending }) => {
251− const current =
252− isActive || [also ?? []].flat().some((prefix) => pathname === prefix || pathname.startsWith(prefix + "/"));
253− return `relative flex h-8 items-center gap-2 rounded-md pr-2 pl-[2.375rem] text-[0.8125rem] transition-colors ${
254− current
255− ? "bg-raised font-medium text-fg before:absolute before:top-1.5 before:bottom-1.5 before:left-[1.1875rem] before:z-10 before:w-px before:bg-accent"
256− : isPending
257− ? "bg-raised/60 text-fg"
258− : "text-muted hover:bg-raised/60 hover:text-fg"
175+ className={({ isActive }) => {
176+ const current = isActive || (also ?? []).some((prefix) => pathname === prefix || pathname.startsWith(prefix + "/"));
177+ return `group flex h-8 items-center gap-2.5 rounded-md px-2 text-[0.8125rem] transition-colors ${
178+ current ? "bg-raised font-medium text-fg" : "text-faint hover:bg-raised/60 hover:text-muted"
259179 }`;
260180 }}
261181 >
182+ <span className="shrink-0 opacity-80">{icon}</span>
262183 <span className="min-w-0 grow truncate">{children}</span>
263− {count != null && count > 0 && (
264− <span className="rounded bg-line px-1.5 text-[0.6875rem] tabular-nums text-muted">{count}</span>
265− )}
266− </NavLink>
267− );
268−}
269−
270−/**
271− * A page a section will have: a link to a page saying what it will be,
272− * so people can see where g1t is going, and read about it.
273− */
274−function SidebarSubSoon({ to, about, children }: { to: string; about: string; children: ReactNode }) {
275− return (
276− <NavLink
277− to={to}
278− title={about}
279− prefetch="intent"
280− className={({ isActive }) =>
281− `relative flex h-8 items-center gap-2 rounded-md pr-2 pl-[2.375rem] text-[0.8125rem] transition-colors ${
282− isActive
283− ? "bg-raised text-fg before:absolute before:top-1.5 before:bottom-1.5 before:left-[1.1875rem] before:z-10 before:w-px before:bg-accent"
284− : "text-faint hover:bg-raised/60 hover:text-muted"
285− }`
286− }
287− >
288− <span className="grow truncate">{children}</span>
289184 <span className="rounded-full px-1.5 py-px text-[0.625rem] font-medium tracking-wide text-muted uppercase ring-1 ring-line">
290185 Soon
291186 </span>
293188 );
294189 }
295190
296−/** A section's Soon pages, from the roadmap. */
297−function SoonItems({ base, section }: { base: string; section: RoadmapItem["section"] }) {
298− return (
299− <>
300− {roadmapIn(section).map((item) => (
301− <SidebarSubSoon key={item.key} to={`${base}/soon/${item.key}`} about={item.summary}>
302− {item.title}
303− </SidebarSubSoon>
304− ))}
305− </>
306− );
307−}
191+/** Icons for what spans projects. */
192+const WORKSPACE_ICONS: Record<string, ReactNode> = {
193+ board: <KanbanSquare size={15} />,
194+ roadmap: <GanttChart size={15} />,
195+ packages: <Package size={15} />,
196+ fleet: <Bot size={15} />,
197+};
308198
309−/** A section's own pages and its Soon pages, for opening it when current. */
310−function sectionPaths(base: string, section: RoadmapItem["section"], own: string[]): string[] {
311− return [...own, ...roadmapIn(section).map((item) => `${base}/soon/${item.key}`)];
199+/** The Soon pages of a section, as paths, so its link is current on them. */
200+function soonPaths(base: string, section: RoadmapItem["section"]): string[] {
201+ return roadmapIn(section).map((item) => `${base}/soon/${item.key}`);
312202 }
313203
314204 function SidebarGroup({ title, action, children }: { title: string; action?: ReactNode; children: ReactNode }) {
555445 <ArrowLeft size={15} className="text-faint transition-transform group-hover:-translate-x-0.5 group-hover:text-muted" />
556446 Mission control
557447 </Link>
558− <Link
448+ <NavLink
559449 to={base}
560− className="mt-3 flex items-center gap-2 rounded-md px-2 py-1.5 transition-colors hover:bg-raised/60"
450+ end
451+ title="Overview"
452+ className={({ isActive }) =>
453+ `mt-3 flex items-center gap-2 rounded-md px-2 py-1.5 transition-colors ${isActive ? "bg-raised" : "hover:bg-raised/60"}`
454+ }
561455 >
562456 <span className="flex size-6 shrink-0 items-center justify-center rounded-md bg-raised text-muted ring-1 ring-line">
563457 {isPrivate ? <Lock size={13} /> : <Box size={13} />}
566460 <span className="text-faint">{repo.namespace}/</span>
567461 <span className="font-semibold text-fg">{repo.name}</span>
568462 </span>
569− </Link>
463+ </NavLink>
464+ {/* A project's pages, flat and in the order people use them. A page
465+ with more than one view shows them as tabs across its top. */}
570466 <div className="mt-3 space-y-px">
571− <SidebarLink to={base} end icon={<LayoutGrid size={15} />}>
572− Overview
467+ <SidebarLink to={`${base}/code`} also={[`${base}/tree`, `${base}/blob`, `${base}/commits`, `${base}/commit`, ...soonPaths(base, "Code")]} icon={<Code2 size={15} />}>
468+ Code
469+ </SidebarLink>
470+ <SidebarLink to={`${base}/issues`} also={[`${base}/plans`, ...soonPaths(base, "Issues")]} icon={<CircleDot size={15} />} count={repo.issues}>
471+ Issues
472+ </SidebarLink>
473+ <SidebarLink to={`${base}/pulls`} also={[`${base}/pull`, `${base}/queue`]} icon={<GitPullRequest size={15} />} count={repo.pulls}>
474+ Pull requests
475+ </SidebarLink>
476+ <SidebarSoonLink to={`${base}/soon/agents`} also={soonPaths(base, "Agents")} icon={<Bot size={15} />} about="Every agent at work on this project, its sessions, playbooks and memory.">
477+ Agents
478+ </SidebarSoonLink>
479+ <SidebarLink to={`${base}/actions`} icon={<PlayCircle size={15} />}>
480+ Workflows
573481 </SidebarLink>
574−
575− <SidebarSection
576− title="Work"
577− icon={<Kanban size={15} />}
578− paths={sectionPaths(base, "Work", [`${base}/issues`, `${base}/pulls`, `${base}/pull`, `${base}/queue`, `${base}/plans`])}
579− >
580− <SidebarSubLink to={`${base}/issues`} count={repo.issues}>
581− Issues
582− </SidebarSubLink>
583− <SidebarSubLink to={`${base}/pulls`} also={`${base}/pull`} count={repo.pulls}>
584− Pull requests
585− </SidebarSubLink>
586− <SidebarSubLink to={`${base}/queue`}>Merge queue</SidebarSubLink>
587− {repo.member && <SidebarSubLink to={`${base}/plans`}>Outcomes</SidebarSubLink>}
588− <SoonItems base={base} section="Work" />
589− </SidebarSection>
590−
591− <SidebarSection
592− title="Code"
593− icon={<Code2 size={15} />}
594− paths={sectionPaths(base, "Code", [`${base}/code`, `${base}/tree`, `${base}/blob`, `${base}/commits`, `${base}/commit`])}
595− >
596− <SidebarSubLink to={`${base}/code`} also={[`${base}/tree`, `${base}/blob`]}>
597− Files
598− </SidebarSubLink>
599− <SidebarSubLink to={`${base}/commits`} also={`${base}/commit`}>
600− Commits
601− </SidebarSubLink>
602− <SoonItems base={base} section="Code" />
603− </SidebarSection>
604−
605− <SidebarSection title="Agents" icon={<Bot size={15} />} paths={sectionPaths(base, "Agents", [])} soon>
606− <SoonItems base={base} section="Agents" />
607− </SidebarSection>
608−
609− <SidebarSection
610− title="Ship"
611− icon={<Rocket size={15} />}
612− paths={sectionPaths(base, "Ship", [`${base}/actions`, ...(repo.member ? [`${base}/deployments`] : [])])}
613− >
614− <SidebarSubLink to={`${base}/actions`}>Workflows</SidebarSubLink>
615− {repo.member && <SidebarSubLink to={`${base}/deployments`}>Deployments</SidebarSubLink>}
616− <SoonItems base={base} section="Ship" />
617− </SidebarSection>
618−
619− <SidebarSection title="Security" icon={<ShieldCheck size={15} />} paths={sectionPaths(base, "Security", [])} soon>
620− <SoonItems base={base} section="Security" />
621− </SidebarSection>
622−
623− <SidebarSection title="Observe" icon={<Activity size={15} />} paths={sectionPaths(base, "Observe", [])} soon>
624− <SoonItems base={base} section="Observe" />
625− </SidebarSection>
626−
627− <SidebarSection title="Insights" icon={<BarChart3 size={15} />} paths={sectionPaths(base, "Insights", [])} soon>
628− <SoonItems base={base} section="Insights" />
629− </SidebarSection>
630−
482+ {repo.member ? (
483+ <SidebarLink to={`${base}/deployments`} also={soonPaths(base, "Deployments")} icon={<Rocket size={15} />}>
484+ Deployments
485+ </SidebarLink>
486+ ) : null}
487+ <SidebarSoonLink to={`${base}/soon/security`} also={soonPaths(base, "Security")} icon={<ShieldCheck size={15} />} about="Findings, secret scanning, dependency updates and code scanning, each fixed by an agent.">
488+ Security
489+ </SidebarSoonLink>
490+ <SidebarSoonLink to={`${base}/soon/delivery`} also={soonPaths(base, "Insights")} icon={<BarChart3 size={15} />} about="Delivery metrics, costs and the work agents do.">
491+ Insights
492+ </SidebarSoonLink>
631493 {repo.member && (
632− <SidebarSection title="Settings" icon={<Settings size={15} />} paths={[`${base}/settings`]}>
633− <SidebarSubLink to={`${base}/settings`} end>
634− General
635− </SidebarSubLink>
636− <SidebarSubLink to={`${base}/settings/repository`}>Repository</SidebarSubLink>
637− <SidebarSubLink to={`${base}/settings/deployments`}>Deployments</SidebarSubLink>
638− <SidebarSubLink to={`${base}/settings/dependencies`}>Dependencies</SidebarSubLink>
639− <SidebarSubLink to={`${base}/settings/secrets`}>Secrets and variables</SidebarSubLink>
640− <SidebarSubLink to={`${base}/settings/webhooks`}>Webhooks</SidebarSubLink>
641− </SidebarSection>
494+ <SidebarLink to={`${base}/settings`} icon={<Settings size={15} />}>
495+ Settings
496+ </SidebarLink>
642497 )}
643498 </div>
644499 </nav>
802657 );
803658 })}
804659 </SidebarGroup>
660+ {ws && (
661+ <SidebarGroup title="Across projects">
662+ {roadmapIn("Workspace").map((item) => (
663+ <SidebarSoonLink
664+ key={item.key}
665+ to={`/${ws.slug}/-/soon/${item.key}`}
666+ icon={WORKSPACE_ICONS[item.key] ?? <Sparkles size={15} />}
667+ about={item.summary}
668+ >
669+ {item.title === "Board" ? "Boards" : item.title}
670+ </SidebarSoonLink>
671+ ))}
672+ </SidebarGroup>
673+ )}
805674 </nav>
806675 </div>
807676 <div className={`${LAYER} ${away ? "translate-x-0 opacity-100" : "pointer-events-none translate-x-full opacity-0"}`}>
+87−0
1+/**
2+ * A project's pages and the tabs within them, in one place: the sidebar
3+ * lists the pages, and a page with more than one view shows them as tabs
4+ * across its top, the way GitHub and Vercel do. Soon tabs come from the
5+ * roadmap and open its page for them, under the same tabs.
6+ */
7+import { type RoadmapItem, roadmapIn } from "./roadmap";
8+
9+export type Tab = {
10+ label: string;
11+ /** Under the project, without a leading slash; "" is its overview. */
12+ path: string;
13+ /** Other paths under which this tab is the current one. */
14+ also?: string[];
15+ soon?: boolean;
16+ about?: string;
17+ /** Members only. */
18+ members?: boolean;
19+};
20+
21+export type Section = {
22+ key: RoadmapItem["section"] | "Workflows" | "Settings";
23+ tabs: Tab[];
24+};
25+
26+function soon(section: RoadmapItem["section"]): Tab[] {
27+ return roadmapIn(section).map((item) => ({ label: item.title, path: `soon/${item.key}`, soon: true, about: item.summary }));
28+}
29+
30+export const SECTIONS: Section[] = [
31+ {
32+ key: "Code",
33+ tabs: [
34+ { label: "Files", path: "code", also: ["tree", "blob"] },
35+ { label: "Commits", path: "commits", also: ["commit"] },
36+ ...soon("Code"),
37+ ],
38+ },
39+ {
40+ key: "Issues",
41+ tabs: [
42+ { label: "Issues", path: "issues" },
43+ { label: "Outcomes", path: "plans", members: true },
44+ ...soon("Issues"),
45+ ],
46+ },
47+ {
48+ key: "Agents",
49+ tabs: soon("Agents"),
50+ },
51+ {
52+ key: "Deployments",
53+ tabs: [{ label: "Deployments", path: "deployments", members: true }, ...soon("Deployments")],
54+ },
55+ { key: "Security", tabs: soon("Security") },
56+ { key: "Insights", tabs: soon("Insights") },
57+];
58+
59+/** Pull requests and the merge queue share a page's tabs too. */
60+export const PULLS: Section = {
61+ key: "Code",
62+ tabs: [
63+ { label: "Pull requests", path: "pulls", also: ["pull"] },
64+ { label: "Merge queue", path: "queue" },
65+ ],
66+};
67+
68+function matches(rest: string, tab: Tab): boolean {
69+ return [tab.path, ...(tab.also ?? [])].some((path) => path !== "" && (rest === path || rest.startsWith(`${path}/`)));
70+}
71+
72+/**
73+ * The tabs for the page at `rest` (the path under the project), if it
74+ * belongs to a page with more than one view.
75+ */
76+export function tabsFor(rest: string, member: boolean): Tab[] | null {
77+ for (const section of [...SECTIONS, PULLS]) {
78+ const visible = section.tabs.filter((tab) => member || !tab.members);
79+ if (visible.length > 1 && visible.some((tab) => matches(rest, tab))) return visible;
80+ }
81+ return null;
82+}
83+
84+/** The first page of a section, for its sidebar link: a built one if any. */
85+export function entryOf(key: Section["key"]): Tab | undefined {
86+ return SECTIONS.find((section) => section.key === key)?.tabs[0];
87+}
+35−19
1111 /** In the address: `/<owner>/<project>/soon/<key>`. */
1212 key: string;
1313 title: string;
14− /** The menu section it sits in. */
15− section: "Work" | "Code" | "Agents" | "Ship" | "Security" | "Observe" | "Insights";
14+ /**
15+ * Where it lives: a project page whose tabs it joins, or the workspace,
16+ * for what spans projects (boards, the roadmap, packages, the fleet).
17+ */
18+ section: "Code" | "Issues" | "Agents" | "Deployments" | "Security" | "Insights" | "Workspace";
1619 /** One line, for the menu's tooltip and the page's lead. */
1720 summary: string;
1821 /** Why it matters, in two or three sentences. */
2831 {
2932 key: "board",
3033 title: "Board",
31− section: "Work",
34+ section: "Workspace",
3235 summary: "Issues and pull requests as a board, a table or a roadmap, with fields of your own.",
3336 why: "Like GitHub Projects: one view of everything in flight, arranged the way your team thinks about it. Agents move the cards as they work, so the board is never out of date.",
3437 plans: [
3639 "Custom fields: status, priority, size, iteration, dates, anything",
3740 "Group and filter by owner, outcome, label, agent or state",
3841 "Cards move by themselves as agents open, revise and land changes",
39− "Boards across projects, for a workspace's whole portfolio",
42+ "Across projects: one board can hold work from any of the workspace's projects",
43+ "Attach a board to a project, so it shows on that project too",
4044 ],
41− today: { label: "Issues", path: "issues" },
4245 },
4346 {
4447 key: "roadmap",
4548 title: "Roadmap",
46− section: "Work",
49+ section: "Workspace",
4750 summary: "Outcomes on a timeline, with their dependencies across projects.",
4851 why: "An outcome is what should be true when the work is done. On a roadmap you see when each is expected, what it waits on in other projects, and how much has landed.",
4952 plans: [
5760 {
5861 key: "milestones",
5962 title: "Milestones",
60− section: "Work",
63+ section: "Issues",
6164 summary: "Dates to land outcomes by, with what is left and what is at risk.",
6265 why: "A milestone groups the issues that must land together, by a date. g1t shows what remains and which agent or person holds each part.",
6366 plans: [
168171 plans: ["Facts agents learned, with where they learned them", "Edit or remove anything", "Shared across every agent on the project"],
169172 },
170173
171− // --- Ship ---------------------------------------------------------------
172174 {
175+ key: "fleet",
176+ title: "Agent fleet",
177+ section: "Workspace",
178+ summary: "Every agent at work across the workspace, and what each is costing.",
179+ why: "Agents work on many projects at once. The fleet shows them all: what each holds, how far along it is, and where people are needed.",
180+ plans: [
181+ "Every running agent across projects, live",
182+ "Queue of work waiting for an agent, by priority",
183+ "Spend by agent, project and kind of work",
184+ "Pause or redirect agents across the workspace",
185+ ],
186+ },
187+
188+ // --- Deployments ----------------------------------------------------------
189+ {
173190 key: "environments",
174191 title: "Environments",
175− section: "Ship",
192+ section: "Deployments",
176193 summary: "Staging, production and others, with approvers and branch rules.",
177194 why: "Production and previews exist today. Environments add the rest: staging, QA, per-customer, each with its own variables, approvers and rules for what may deploy there.",
178195 plans: [
186203 {
187204 key: "releases",
188205 title: "Releases",
189− section: "Ship",
206+ section: "Deployments",
190207 summary: "Tagged releases with notes written from what landed.",
191208 why: "Release notes from the pull requests and sessions that made the release: what changed, why, and who or what changed it.",
192209 plans: ["Notes drafted from merged work", "Assets and checksums attached", "Published to the project's page and a feed"],
195212 {
196213 key: "packages",
197214 title: "Packages",
198− section: "Ship",
199− summary: "npm, container and other packages published from the project.",
215+ section: "Workspace",
216+ summary: "The workspace's package registry: npm, containers and more.",
200217 why: "Publish packages from workflows to g1t's registry, with the same access as the code.",
201− plans: ["npm, OCI containers, Cargo, PyPI and Go modules", "Published from workflows", "Private packages for the workspace"],
202− today: { label: "Workflows", path: "actions" },
218+ plans: ["npm, OCI containers, Cargo, PyPI and Go modules", "Published from any project's workflows", "Private packages for the workspace"],
203219 },
204220 {
205221 key: "flags",
206222 title: "Feature flags",
207− section: "Ship",
223+ section: "Deployments",
208224 summary: "Turn features on per environment or per user, without a deploy.",
209225 why: "Ship code dark and turn it on when ready, for some users first, at the edge, with no deploy.",
210226 plans: ["Flags read at the edge by deployed apps", "Rollouts by percentage, user or environment", "Flags cleaned up by agents when fully on"],
258274 {
259275 key: "logs",
260276 title: "Logs and metrics",
261− section: "Observe",
277+ section: "Deployments",
262278 summary: "Requests, errors and CPU time of each deployment, and its logs.",
263279 why: "Every deployed app's logs and numbers, by deployment, so a regression points at the change that caused it.",
264280 plans: ["Live and searchable logs", "Requests, errors, latency and CPU by deployment", "Compare a preview with production"],
267283 {
268284 key: "errors",
269285 title: "Errors and incidents",
270− section: "Observe",
286+ section: "Deployments",
271287 summary: "Errors from the running app, each becoming an issue an agent can take.",
272288 why: "An error in production should become a fix, not a dashboard. Each new error is grouped, explained, and turned into an issue with the context an agent needs.",
273289 plans: ["Errors grouped with stack and request", "One click to an issue for an agent", "Incidents with a timeline and who was told"],
276292 {
277293 key: "uptime",
278294 title: "Uptime",
279− section: "Observe",
295+ section: "Deployments",
280296 summary: "Checks that the app answers, from around the world.",
281297 why: "Know the app is down before your users tell you, and who was told.",
282298 plans: ["Checks from many places", "Alerts by email and webhook", "A public status page"],
284300 {
285301 key: "analytics",
286302 title: "Web analytics",
287− section: "Observe",
303+ section: "Deployments",
288304 summary: "Who visits the deployed apps, without cookies.",
289305 why: "Visits, pages and referrers for each app, private by design, from Cloudflare's own analytics.",
290306 plans: ["Visits, pages, referrers and countries", "No cookies, no personal data", "Per deployment and per preview"],
+2−0
2727 route("-/webhooks", "routes/workspace/webhooks.tsx"),
2828 route("-/secrets", "routes/workspace/secrets.tsx"),
2929 route("-/settings", "routes/workspace/settings.tsx"),
30+ // What the workspace will have across its projects.
31+ route("-/soon/:feature", "routes/workspace/soon.tsx"),
3032 ]),
3133 // Why a line is the way it is, fetched by the blame view.
3234 route(":owner/:repo/why/:hash", "routes/repo/why.ts"),
+48−2
11 import { Box, CircleDot, Code2, GitPullRequest, History, LayoutGrid, ListTree, Lock, Rocket, Settings } from "lucide-react";
2−import { Link, Outlet, data, useRouteLoaderData } from "react-router";
2+import { Link, NavLink, Outlet, data, useLocation, useRouteLoaderData } from "react-router";
33
44 import type { Project } from "@g1t/contracts";
55
66 import type { Route } from "./+types/layout";
77 import { page } from "../../lib/meta";
8+import { type Tab as PageTab, tabsFor } from "../../lib/project-nav";
89 import { Pill, TabLink as Tab } from "../../components/ui";
910 import { projects, repos, work } from "../../lib/services.server";
1011 import { getViewer, roleIn, unwrap } from "../../lib/session.server";
7273 );
7374 }
7475
76+/**
77+ * The views of the page the project is on, as tabs across its top: Files,
78+ * Commits and what is coming under Code, and so on. Soon tabs open their
79+ * roadmap page, under the same tabs.
80+ */
81+function PageTabs({ base, tabs }: { base: string; tabs: PageTab[] }) {
82+ const { pathname } = useLocation();
83+ const rest = pathname.slice(base.length + 1);
84+ const current = (tab: PageTab) =>
85+ [tab.path, ...(tab.also ?? [])].some((path) => rest === path || rest.startsWith(`${path}/`));
86+ return (
87+ <nav aria-label="Views" className="-mb-px flex gap-1 overflow-x-auto">
88+ {tabs.map((tab) => (
89+ <NavLink
90+ key={tab.path}
91+ to={`${base}/${tab.path}`}
92+ title={tab.about}
93+ prefetch="intent"
94+ className={`flex shrink-0 items-center gap-1.5 border-b-2 px-3 pt-1 pb-2.5 text-sm transition-colors ${
95+ current(tab)
96+ ? "border-accent font-medium text-fg"
97+ : tab.soon
98+ ? "border-transparent text-faint hover:text-muted"
99+ : "border-transparent text-muted hover:text-fg"
100+ }`}
101+ >
102+ {tab.label}
103+ {tab.soon && (
104+ <span className="rounded-full px-1.5 py-px text-[0.625rem] font-medium tracking-wide text-muted uppercase ring-1 ring-line">
105+ Soon
106+ </span>
107+ )}
108+ </NavLink>
109+ ))}
110+ </nav>
111+ );
112+}
113+
75114 export default function ProjectLayout({ loaderData }: Route.ComponentProps) {
76115 const { repo, project, open, member } = loaderData;
77116 const base = `/${repo.namespace}/${repo.name}`;
78117 const signedIn = useRouteLoaderData("root")?.user != null;
79118 const description = project?.description ?? repo.description;
119+ const { pathname } = useLocation();
120+ const tabs = tabsFor(pathname.slice(base.length + 1), member);
80121 if (signedIn) {
81122 return (
82123 <>
83124 <div className="border-b border-line">
84− <div className="mx-auto max-w-6xl px-4 py-3 sm:px-6">
125+ <div className={`mx-auto max-w-6xl px-4 sm:px-6 ${tabs ? "pt-3" : "py-3"}`}>
85126 <Header
86127 project={project}
87128 isPrivate={repo.isPrivate}
89130 name={repo.name}
90131 description={description}
91132 />
133+ {tabs && (
134+ <div className="mt-3">
135+ <PageTabs base={base} tabs={tabs} />
136+ </div>
137+ )}
92138 </div>
93139 </div>
94140 <div className="mx-auto max-w-6xl px-4 py-6 sm:px-6">
+14−5
33
44 import type { Route } from "./+types/soon";
55 import { page } from "../../lib/meta";
6−import { ROADMAP, roadmapIn, roadmapItem } from "../../lib/roadmap";
6+import { ROADMAP, type RoadmapItem, roadmapIn, roadmapItem } from "../../lib/roadmap";
77
88 export function meta({ params, ...args }: Route.MetaArgs) {
99 const item = roadmapItem(params.feature);
2525 * to use until then. Reached from the project menu's Soon items.
2626 */
2727 export default function Soon({ loaderData, params }: Route.ComponentProps) {
28− const { item } = loaderData;
29− const base = `/${params.owner}/${params.repo}`;
28+ return <SoonView item={loaderData.item} base={`/${params.owner}/${params.repo}`} />;
29+}
30+
31+/**
32+ * A Soon page, for a project's (under `base`, `/<owner>/<project>`) or the
33+ * workspace's (`/<owner>/-`).
34+ */
35+export function SoonView({ item, base }: { item: RoadmapItem; base: string }) {
3036 const siblings = roadmapIn(item.section).filter((other) => other.key !== item.key);
37+ const inWorkspace = item.section === "Workspace";
3138 return (
3239 <div className="mx-auto max-w-3xl">
3340 <p className="flex items-center gap-2 text-sm text-muted">
34− <span>{item.section}</span>
41+ <span>{inWorkspace ? "Across projects" : item.section}</span>
3542 <span className="text-line-strong">/</span>
3643 <span className="text-fg">{item.title}</span>
3744 </p>
7279
7380 {siblings.length > 0 && (
7481 <section className="mt-10">
75− <h2 className="text-sm font-medium text-muted">Also coming to {item.section}</h2>
82+ <h2 className="text-sm font-medium text-muted">
83+ Also coming {inWorkspace ? "across projects" : `to ${item.section}`}
84+ </h2>
7685 <ul className="mt-3 grid gap-3 sm:grid-cols-2">
7786 {siblings.map((other) => (
7887 <li key={other.key}>
+50−17
1−import { CreditCard, Rocket } from "lucide-react";
1+import { ArrowUpRight, CreditCard, FileText, Receipt, Rocket } from "lucide-react";
22 import { Form, Link, data, redirect, useNavigation } from "react-router";
33
44 import {
145145 </p>
146146 </div>
147147 )}
148+ {account.status.enabled && (
149+ <section className="mb-8 flex flex-wrap items-center gap-4 rounded-xl border border-line bg-surface p-5">
150+ <span className="flex size-10 shrink-0 items-center justify-center rounded-lg bg-accent/10 text-accent ring-1 ring-accent/30">
151+ <Receipt size={18} />
152+ </span>
153+ <div className="min-w-0 grow">
154+ <h2 className="font-medium">Billing on Stripe</h2>
155+ <p className="mt-0.5 text-sm text-muted">
156+ {account.card
157+ ? `${account.card.brand[0].toUpperCase()}${account.card.brand.slice(1)} ending ${account.card.last4}. `
158+ : "No card yet. "}
159+ Your card, invoices and receipts, and the billing email, address and tax ID.
160+ </p>
161+ </div>
162+ {role === "owner" ? (
163+ <Form method="post">
164+ <Button variant="accent" type="submit" name="intent" value="portal" disabled={paying}>
165+ Open Stripe billing
166+ <ArrowUpRight size={14} />
167+ </Button>
168+ </Form>
169+ ) : (
170+ <p className="text-xs text-faint">An owner manages billing.</p>
171+ )}
172+ </section>
173+ )}
174+
148175 {limit && account.status.enabled && (
149176 <LimitCard limit={limit} owner={role === "owner"} busy={paying} error={actionData?.error} />
150177 )}
151178
152− {account.status.enabled && limit?.trust !== "internal" && (
179+ {account.status.enabled && (
153180 <section className="mb-10 rounded-xl border border-line bg-surface p-5">
154181 <div className="flex flex-wrap items-center justify-between gap-3">
155182 <div>
333360 <h3 className="font-medium">How it is charged</h3>
334361 <ul className="mt-2 list-disc space-y-1.5 pl-4 text-muted">
335362 <li>
336− Each run is charged when it finishes, to the workspace that owns the
337− repository, whoever assigned the issue.
338− </li>
339− <li>
340− A change, a review, a revision and a catch-up are each a run. The
341− statement links each to its pull request.
363+ Usage is charged after it runs, to the workspace that owns the repository: agents at what the model cost
364+ plus {account.marginPercent}%, and sandbox time, builds and apps at what they cost g1t plus a set markup.
342365 </li>
343366 <li>
344− Every pull request's session ends with what that run cost before
345− the margin.
367+ Each month closes with an itemised invoice, charged to the card on file. Near your limit, g1t sends one
368+ sooner, so work keeps going.
346369 </li>
347370 <li>
348371 With your own model provider, connected under{" "}
349372 <Link to={`/${slug}/-/integrations`} className="text-fg hover:underline">
350373 Integrations
351374 </Link>
352− , the provider bills you for the model and each run here is a flat{" "}
353− {dollars(account.orchestrationFeeMicros)}
354− {status.free && " once pricing starts; nothing while g1t is being built out"}.
355− </li>
356− <li>
357− Only members of <span className="font-mono text-fg">{slug}</span> can
358− put agents to work on its repositories.
375+ , the provider bills you for the model and each run here is {dollars(account.orchestrationFeeMicros)}, plus
376+ its sandbox time.
359377 </li>
378+ <li>No seats: add as many people and agents as you like.</li>
360379 </ul>
380+ <div className="mt-4 space-y-1.5 border-t border-line pt-4 text-sm">
381+ <Link to={`/${slug}/-/usage`} className="flex items-center gap-2 text-muted hover:text-fg">
382+ <FileText size={14} /> Usage, run by run
383+ </Link>
384+ <a href="https://g1t.sh/pricing" className="flex items-center gap-2 text-muted hover:text-fg">
385+ <CreditCard size={14} /> Today's prices
386+ </a>
387+ <a
388+ href="https://docs.g1t.sh/guides/usage-and-billing/"
389+ className="flex items-center gap-2 text-muted hover:text-fg"
390+ >
391+ <ArrowUpRight size={14} /> How usage and billing work
392+ </a>
393+ </div>
361394 </section>
362395 </aside>
363396 </div>
+31−0
1+import { data } from "react-router";
2+
3+import type { Route } from "./+types/soon";
4+import { page } from "../../lib/meta";
5+import { roadmapItem } from "../../lib/roadmap";
6+import { SoonView } from "../repo/soon";
7+
8+export function meta({ params, ...args }: Route.MetaArgs) {
9+ const item = roadmapItem(params.feature);
10+ return page(args, {
11+ title: `${item?.title ?? "Soon"} · ${params.owner} · g1t`,
12+ description: item ? `Soon on g1t: ${item.summary}` : null,
13+ version: item ? [item.title, item.summary] : undefined,
14+ });
15+}
16+
17+export function loader({ params }: Route.LoaderArgs) {
18+ const item = roadmapItem(params.feature);
19+ // Only what spans projects lives under the workspace.
20+ if (!item || item.section !== "Workspace") throw data(null, { status: 404 });
21+ return { item };
22+}
23+
24+/** What a workspace will have across its projects: boards, the roadmap, packages, the fleet. */
25+export default function WorkspaceSoon({ loaderData, params }: Route.ComponentProps) {
26+ return (
27+ <div className="mx-auto max-w-5xl px-4 py-10 sm:px-8">
28+ <SoonView item={loaderData.item} base={`/${params.owner}/-`} />
29+ </div>
30+ );
31+}