Skip to content
243 linesCodeBlameRaw

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.

Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1import { Plus } from "lucide-react";
Navigation: the workspace's own page with tabs, pins and recent in the sidebar2import { NavLink, Outlet, data, redirect, useLocation, useNavigation } from "react-router";
Agents as a team: lifecycle, merge queue, billing and a new shell3
4import type { Route } from "./+types/layout";
Social cards for every page: og.g1t.sh5import { page } from "../../lib/meta";
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put6import { Avatar, ButtonLink, Pill } from "../../components/ui";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas7import { WelcomeBanner } from "../../components/welcome";
8import { clearWelcome, welcomes } from "../../lib/invites";
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put9import { notFound } from "../../lib/not-found.server";
Agents and memory, checks and conflicts, profiles, slug renames, custom domains10import { redirectIfRenamed } from "../../lib/renamed.server";
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put11import { rememberWorkspace } from "../../lib/workspace-choice";
Navigation: the workspace's own page with tabs, pins and recent in the sidebar12import { type WorkspaceTab, type WorkspaceTabKey, workspaceRedirect, workspaceTab, workspaceTabs } from "../../lib/workspace-nav";
13import { workspaceProjects } from "../../lib/workspace-projects.server";
14import { WorkspaceTabSkeleton } from "../../components/workspace-skeletons";
Agents as a team: lifecycle, merge queue, billing and a new shell15import { identity } from "../../lib/services.server";
16import { getViewer, roleIn } from "../../lib/session.server";
17
Social cards for every page: og.g1t.sh18export function meta({ loaderData, params, ...args }: Route.MetaArgs) {
19 return page(args, { title: `${loaderData?.workspace.name ?? params.owner} · g1t` });
Agents as a team: lifecycle, merge queue, billing and a new shell20}
21
Agents and memory, checks and conflicts, profiles, slug renames, custom domains22export async function loader({ params, context, request }: Route.LoaderArgs) {
Navigation: the workspace's own page with tabs, pins and recent in the sidebar23 const url = new URL(request.url);
24 // `?tab=projects` and the like, from habit: that tab's own address.
25 const moved = url.search ? workspaceRedirect(url.pathname, url.search) : null;
26 if (moved) throw redirect(moved);
27 const viewer = getViewer(context);
28 // Under the tabs, the count of projects beside Projects: the same
29 // listing the sidebar and the Projects page read, asked at once.
30 const tabbed = workspaceTab(url.pathname, params.owner) != null;
31 const [workspace, listed] = await Promise.all([
32 identity.getWorkspace(params.owner),
33 tabbed ? workspaceProjects(params.owner, viewer).catch(() => null) : null,
34 ]);
Agents and memory, checks and conflicts, profiles, slug renames, custom domains35 if (!workspace) {
36 // A workspace's old name, after a rename: its pages are at the new one.
37 await redirectIfRenamed(request, params.owner);
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put38 throw notFound("workspace");
Agents and memory, checks and conflicts, profiles, slug renames, custom domains39 }
Navigation: the workspace's own page with tabs, pins and recent in the sidebar40 const role = roleIn(viewer, workspace.slug);
41 const projectCount = listed?.ok ? listed.value.length : null;
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put42 // Opening one of your workspaces makes it the one you are in.
Navigation: the workspace's own page with tabs, pins and recent in the sidebar43 if (!role) return { workspace, role, welcome: false, projectCount };
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put44 const secure = new URL(request.url).protocol === "https:";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas45 const headers = new Headers({ "Set-Cookie": rememberWorkspace(workspace.slug, secure) });
46 // Just joined with an invite: welcomed once (routes/invite.tsx).
47 const welcome = welcomes(request.headers.get("cookie"), workspace.slug);
48 if (welcome) headers.append("Set-Cookie", clearWelcome(secure));
Navigation: the workspace's own page with tabs, pins and recent in the sidebar49 return data({ workspace, role, welcome, projectCount }, { headers });
Agents as a team: lifecycle, merge queue, billing and a new shell50}
51
A catalogue of model providers, and settings that feel like settings52/** A workspace's own pages, each with its title and what it is for. */
53const PAGES: Record<string, { title: string; about: string }> = {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look54 settings: { title: "General", about: "The workspace's name, icon, address and description, and deleting it." },
A catalogue of model providers, and settings that feel like settings55 people: {
Navigation: the workspace's own page with tabs, pins and recent in the sidebar56 title: "People",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look57 about: "Members create repositories and have the base permission on each one. Owners are Admins on every repository, and also manage members, tokens, billing and integrations.",
58 },
Navigation: the workspace's own page with tabs, pins and recent in the sidebar59 projects: {
60 title: "Projects",
61 about: "Everything the workspace builds and runs. Search, filter and sort them, and pin the ones you use most to keep them in your sidebar.",
62 },
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar63 teams: {
64 title: "Teams",
65 about: "Groups of members, given roles on repositories together, mentioned as @workspace/team and asked to review together. Child teams inherit their parent's access.",
66 },
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look67 repositories: {
68 title: "Repositories",
69 about: "Every repository in the workspace: who can see it, whether it is archived, and the ones deleted recently.",
A catalogue of model providers, and settings that feel like settings70 },
71 tokens: {
72 title: "Access tokens",
73 about: "Tokens that belong to the workspace, not a person: for CI, integrations and agents that work for the whole team.",
74 },
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member75 packages: {
76 title: "Packages",
The Packages page names every registry it has77 about: "What the workspace publishes and installs, beside its code: container images, npm, Composer, Go, Cargo, Maven, NuGet and RubyGems, with the same members, roles and tokens.",
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member78 },
A catalogue of model providers, and settings that feel like settings79 usage: { title: "Usage", about: "What the workspace's agents cost, run by run, by repository, pull request and model." },
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look80 billing: { title: "Billing and plans", about: "The g1t plan, the trial, your spend limit and caps, prepaying, and every charge." },
Agents and memory, checks and conflicts, profiles, slug renames, custom domains81 agents: {
82 title: "Agent fleet",
83 about: "Every agent at work across the workspace's projects: what each holds, what it is doing now, and what it has cost.",
84 },
85 memory: {
86 title: "Workspace memory",
87 about: "What holds across all of the workspace's projects, given to every agent in every one of them, beside each project's own memory.",
88 },
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API89 context: {
90 title: "Context",
91 about: "Everything the workspace builds and runs, and what it knows: a catalog built from its projects, memory with a review queue, one search over all of it, and a scorecard for each project.",
92 },
93 audit: {
94 title: "Audit log",
95 about: "Every action agents took with their run credentials, and every change people and tokens made: by whom, on whose behalf, to what, and whether it was allowed.",
96 },
97 guardrails: {
98 title: "Guardrails",
99 about: "What agents may reach, run, spend and take in every project's sandboxes. Each project can override these under its settings.",
100 },
Webhooks: every event, to your own addresses, signed and retried101 webhooks: {
102 title: "Webhooks",
103 about: "Every repository's events, sent to your own addresses as they happen. A repository can also have its own, under its settings.",
104 },
GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs105 secrets: {
106 title: "Secrets and variables",
Secrets and variables: one list, rows per environment, for workflows and deployments107 about: "Shared with every repository, or the ones you link: read by workflows, deployments, or both. A repository's own row of the same key wins.",
GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs108 },
Fast pages, required checks on the branch, self-hosted runners, honest incidents109 runners: {
110 title: "Self-hosted runners",
111 about: "Your own machines, which run workflow jobs, and if you choose agents' work, for free. They connect out to g1t; nothing reaches in.",
112 },
Security matches the workspace's other pages and leaves out deleted repositories; every section has a name in the trail113 security: {
114 title: "Security",
115 about: "Open alerts in every project: secrets found in pushes and history, and vulnerable dependencies. Each project's Security page has the details and the security update g1t opened for each.",
116 },
A catalogue of model providers, and settings that feel like settings117 integrations: {
118 title: "Integrations",
119 about: "Model providers, alerts and trackers. Secrets are sealed when saved, and agents never see them.",
120 },
121};
122
Navigation: the workspace's own page with tabs, pins and recent in the sidebar123/**
124 * The workspace's tabs, as a project's are: a row under its header that
125 * scrolls sideways on a phone, without a scrollbar. Soon tabs say so with
126 * a dot.
127 */
128function WorkspaceTabs({ tabs, current }: { tabs: WorkspaceTab[]; current: WorkspaceTabKey | null }) {
129 return (
130 <nav aria-label="Workspace" className="relative -mb-px flex gap-1 overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
131 {tabs.map((tab) => (
132 <NavLink
133 key={tab.key}
134 to={tab.to}
135 end={tab.key === "overview"}
136 prefetch="intent"
137 aria-current={current === tab.key ? "page" : undefined}
138 className={`flex shrink-0 items-center gap-1.5 border-b-2 px-3 pt-1 pb-2.5 text-sm transition-colors ${
139 current === tab.key
140 ? "border-accent font-medium text-fg"
141 : tab.soon
142 ? "border-transparent text-faint hover:text-muted"
143 : "border-transparent text-muted hover:text-fg"
144 }`}
145 >
146 {tab.label}
147 {tab.count != null && (
148 <span className="rounded-full bg-raised px-1.5 py-px text-xs tabular-nums text-muted">{tab.count}</span>
149 )}
150 {tab.soon && (
151 <>
152 <span aria-hidden="true" className="size-1.5 rounded-full bg-accent/70" />
153 <span className="sr-only">(soon)</span>
154 </>
155 )}
156 </NavLink>
157 ))}
158 </nav>
159 );
160}
161
Agents as a team: lifecycle, merge queue, billing and a new shell162export default function WorkspaceLayout({ loaderData }: Route.ComponentProps) {
Navigation: the workspace's own page with tabs, pins and recent in the sidebar163 const { workspace, role, welcome, projectCount } = loaderData;
164 const { pathname } = useLocation();
165 const going = useNavigation().location;
166 const tab = workspaceTab(pathname, workspace.slug);
167 // The sidebar finds the workspace's other pages, for everyone, so they
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put168 // need a title, not the workspace's whole header again.
Navigation: the workspace's own page with tabs, pins and recent in the sidebar169 const parts = (pathname.split("/-/")[1] ?? "").split("/").filter(Boolean);
170 const page = tab ? undefined : PAGES[parts[0] ?? ""];
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member171 // A page within one (a single package) has its own heading.
172 if (page && parts.length > 1) {
173 return (
174 <div className="mx-auto max-w-5xl px-4 py-10 sm:px-8">
175 <Outlet />
176 </div>
177 );
178 }
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put179 if (page) {
A catalogue of model providers, and settings that feel like settings180 return (
181 <div className="mx-auto max-w-5xl px-4 py-10 sm:px-8">
182 <header className="mb-8 border-b border-line pb-6">
183 <h1 className="text-2xl font-semibold tracking-tight">{page.title}</h1>
184 <p className="mt-1.5 text-sm text-muted">{page.about}</p>
185 </header>
186 <Outlet />
187 </div>
188 );
189 }
Navigation: the workspace's own page with tabs, pins and recent in the sidebar190 const tabs = workspaceTabs(workspace.slug, {
191 member: role != null,
192 owner: role === "owner",
193 projects: projectCount,
194 people: role ? workspace.memberCount : null,
195 });
196 // On the way to another of its tabs: that tab's shape until it arrives.
197 const next = going && going.pathname !== pathname ? workspaceTab(going.pathname, workspace.slug) : null;
Agents as a team: lifecycle, merge queue, billing and a new shell198 return (
199 <>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar200 {/* The workspace's own header band, with its tabs along the bottom. */}
Agents as a team: lifecycle, merge queue, billing and a new shell201 <div className="border-b border-line bg-surface/60">
Navigation: the workspace's own page with tabs, pins and recent in the sidebar202 <div className="mx-auto max-w-6xl px-4 pt-8">
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas203 {welcome && (
204 <div className="mb-6">
205 <WelcomeBanner title={`You're in ${workspace.name}`}>
206 You joined as a {role}. Its projects, issues and agents are all here.
207 </WelcomeBanner>
208 </div>
209 )}
Agents as a team: lifecycle, merge queue, billing and a new shell210 <div className="flex flex-wrap items-center gap-4">
Workspace names and icons, and a component kit for every control211 <Avatar name={workspace.slug} image={workspace.avatar} size={52} square />
Agents as a team: lifecycle, merge queue, billing and a new shell212 <div className="min-w-0 grow">
213 <div className="flex flex-wrap items-center gap-2.5">
214 <h1 className="truncate text-2xl font-semibold tracking-tight">
215 {workspace.name}
216 </h1>
217 {role && <Pill>{role}</Pill>}
218 </div>
219 <p className="font-mono text-sm text-muted">g1t.sh/{workspace.slug}</p>
220 </div>
221 {role && (
222 <ButtonLink to={`/new?workspace=${workspace.slug}`}>
223 <Plus size={15} />
Projects live: fixes from walking them end to end224 New project
Agents as a team: lifecycle, merge queue, billing and a new shell225 </ButtonLink>
226 )}
227 </div>
228 {workspace.description && (
229 <p className="mt-4 max-w-2xl text-sm text-muted">{workspace.description}</p>
230 )}
Navigation: the workspace's own page with tabs, pins and recent in the sidebar231 <div className="mt-6">
232 <WorkspaceTabs tabs={tabs} current={next ?? tab} />
233 </div>
Agents as a team: lifecycle, merge queue, billing and a new shell234 </div>
235 </div>
236 <div className="mx-auto max-w-6xl px-4 py-8">
Navigation: the workspace's own page with tabs, pins and recent in the sidebar237 {/* What a tab is for, said once above it. */}
238 {tab && tab !== "overview" && !next && PAGES[tab] && <p className="mb-6 max-w-3xl text-sm text-muted">{PAGES[tab].about}</p>}
239 {next ? <WorkspaceTabSkeleton tab={next} /> : <Outlet />}
Agents as a team: lifecycle, merge queue, billing and a new shell240 </div>
241 </>
242 );
243}