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

144 lines6,591 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.

Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1import { Plus } from "lucide-react";
2import { Outlet, data, useLocation } 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";
Agents as a team: lifecycle, merge queue, billing and a new shell12import { identity } from "../../lib/services.server";
13import { getViewer, roleIn } from "../../lib/session.server";
14
Social cards for every page: og.g1t.sh15export function meta({ loaderData, params, ...args }: Route.MetaArgs) {
16 return page(args, { title: `${loaderData?.workspace.name ?? params.owner} · g1t` });
Agents as a team: lifecycle, merge queue, billing and a new shell17}
18
Agents and memory, checks and conflicts, profiles, slug renames, custom domains19export async function loader({ params, context, request }: Route.LoaderArgs) {
Agents as a team: lifecycle, merge queue, billing and a new shell20 const workspace = await identity.getWorkspace(params.owner);
Agents and memory, checks and conflicts, profiles, slug renames, custom domains21 if (!workspace) {
22 // A workspace's old name, after a rename: its pages are at the new one.
23 await redirectIfRenamed(request, params.owner);
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put24 throw notFound("workspace");
Agents and memory, checks and conflicts, profiles, slug renames, custom domains25 }
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put26 const role = roleIn(getViewer(context), workspace.slug);
27 // Opening one of your workspaces makes it the one you are in.
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas28 if (!role) return { workspace, role, welcome: false };
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put29 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 quotas30 const headers = new Headers({ "Set-Cookie": rememberWorkspace(workspace.slug, secure) });
31 // Just joined with an invite: welcomed once (routes/invite.tsx).
32 const welcome = welcomes(request.headers.get("cookie"), workspace.slug);
33 if (welcome) headers.append("Set-Cookie", clearWelcome(secure));
34 return data({ workspace, role, welcome }, { headers });
Agents as a team: lifecycle, merge queue, billing and a new shell35}
36
A catalogue of model providers, and settings that feel like settings37/** A workspace's own pages, each with its title and what it is for. */
38const PAGES: Record<string, { title: string; about: string }> = {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look39 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 settings40 people: {
41 title: "Members",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look42 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.",
43 },
44 repositories: {
45 title: "Repositories",
46 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 settings47 },
48 tokens: {
49 title: "Access tokens",
50 about: "Tokens that belong to the workspace, not a person: for CI, integrations and agents that work for the whole team.",
51 },
52 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 look53 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 domains54 agents: {
55 title: "Agent fleet",
56 about: "Every agent at work across the workspace's projects: what each holds, what it is doing now, and what it has cost.",
57 },
58 memory: {
59 title: "Workspace memory",
60 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.",
61 },
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API62 context: {
63 title: "Context",
64 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.",
65 },
66 audit: {
67 title: "Audit log",
68 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.",
69 },
70 guardrails: {
71 title: "Guardrails",
72 about: "What agents may reach, run, spend and take in every project's sandboxes. Each project can override these under its settings.",
73 },
Webhooks: every event, to your own addresses, signed and retried74 webhooks: {
75 title: "Webhooks",
76 about: "Every repository's events, sent to your own addresses as they happen. A repository can also have its own, under its settings.",
77 },
GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs78 secrets: {
79 title: "Secrets and variables",
Secrets and variables: one list, rows per environment, for workflows and deployments80 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 docs81 },
A catalogue of model providers, and settings that feel like settings82 integrations: {
83 title: "Integrations",
84 about: "Model providers, alerts and trackers. Secrets are sealed when saved, and agents never see them.",
85 },
86};
87
Agents as a team: lifecycle, merge queue, billing and a new shell88export default function WorkspaceLayout({ loaderData }: Route.ComponentProps) {
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas89 const { workspace, role, welcome } = loaderData;
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put90 // The sidebar finds the workspace's pages, for everyone, so its pages
91 // need a title, not the workspace's whole header again.
A catalogue of model providers, and settings that feel like settings92 const page = PAGES[useLocation().pathname.split("/-/")[1]?.split("/")[0] ?? ""];
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put93 if (page) {
A catalogue of model providers, and settings that feel like settings94 return (
95 <div className="mx-auto max-w-5xl px-4 py-10 sm:px-8">
96 <header className="mb-8 border-b border-line pb-6">
97 <h1 className="text-2xl font-semibold tracking-tight">{page.title}</h1>
98 <p className="mt-1.5 text-sm text-muted">{page.about}</p>
99 </header>
100 <Outlet />
101 </div>
102 );
103 }
Agents as a team: lifecycle, merge queue, billing and a new shell104 return (
105 <>
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put106 {/* The workspace's own header band. */}
Agents as a team: lifecycle, merge queue, billing and a new shell107 <div className="border-b border-line bg-surface/60">
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put108 <div className="mx-auto max-w-6xl px-4 pt-8 pb-8">
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas109 {welcome && (
110 <div className="mb-6">
111 <WelcomeBanner title={`You're in ${workspace.name}`}>
112 You joined as a {role}. Its projects, issues and agents are all here.
113 </WelcomeBanner>
114 </div>
115 )}
Agents as a team: lifecycle, merge queue, billing and a new shell116 <div className="flex flex-wrap items-center gap-4">
Workspace names and icons, and a component kit for every control117 <Avatar name={workspace.slug} image={workspace.avatar} size={52} square />
Agents as a team: lifecycle, merge queue, billing and a new shell118 <div className="min-w-0 grow">
119 <div className="flex flex-wrap items-center gap-2.5">
120 <h1 className="truncate text-2xl font-semibold tracking-tight">
121 {workspace.name}
122 </h1>
123 {role && <Pill>{role}</Pill>}
124 </div>
125 <p className="font-mono text-sm text-muted">g1t.sh/{workspace.slug}</p>
126 </div>
127 {role && (
128 <ButtonLink to={`/new?workspace=${workspace.slug}`}>
129 <Plus size={15} />
Projects live: fixes from walking them end to end130 New project
Agents as a team: lifecycle, merge queue, billing and a new shell131 </ButtonLink>
132 )}
133 </div>
134 {workspace.description && (
135 <p className="mt-4 max-w-2xl text-sm text-muted">{workspace.description}</p>
136 )}
137 </div>
138 </div>
139 <div className="mx-auto max-w-6xl px-4 py-8">
140 <Outlet />
141 </div>
142 </>
143 );
144}