Skip to content

Commit

Projects and workspaces no longer open with get-started checklists. A project's overview drops its Get to production, Ship a release and Get started card, and with it the dismissal it kept in the browser; Code's Overview drops its Get started steps and its Connect your own agent card; a workspace with no projects shows a plain No projects yet with New project; and an empty repository shows only its push commands, without the walkthrough for having a coding agent start it. Which head a project's overview leads with (production, packages, docs or where it runs) is unchanged, now worked out by projectPlan in lib/project-kind.ts. Observability's empty state, which carries the OTLP endpoint and how data arrives, stays. The projects and deployments guides no longer describe the checklist, and the privacy policy no longer lists dismissed checklist items among what the browser keeps.

syntaqxcommitted Parent478db0bBrowse files
14 files+54−8360/14 viewed
+0−3
334334 too, and any project can show an uploaded image instead. See
335335 [production's picture](/guides/projects/#productions-picture).
336336
337−The [checklist on the overview](/guides/projects/#get-to-production) also
338−tracks the first production deploy, a custom domain and a first preview.
339−
340337 ## Deploying from a workflow
341338
342339 A [workflow](/guides/actions/) job can build and deploy the project
+3−49
3535
3636 | Page | Address | |
3737 | --- | --- | --- |
38−| **Overview** | `g1t.sh/<workspace>/<project>` | What it is and its links; production wherever it is deployed, or for a library its packages; the steps that apply to it; what is in progress, active branches, the latest commits, and its About. See [the overview](#the-overview). |
38+| **Overview** | `g1t.sh/<workspace>/<project>` | What it is and its links; production wherever it is deployed, or for a library its packages; what is in progress, active branches, the latest commits, and its About. See [the overview](#the-overview). |
3939 | **Code** | `…/code` | The repository's files, commits and branches. |
4040 | **Issues**, **Pull requests**, **Merge queue**, **Plan** | `…/issues` and so on | As they always were. |
4141 | **Actions** | `…/actions` | [GitHub Actions workflows](/guides/actions/). |
5757 | **Production** | [Production's picture](#productions-picture), which opens the live site, beside the rest. For an app [deployed on g1t](/guides/deployments/): its address, the commit it runs and when it went up; **Visit** and **Redeploy**. For an app deployed elsewhere: production's address and **Visit**, with its latest reported production deployment or the default branch's latest commit. For an app nobody has placed yet: one question, where it runs. |
5858 | **Packages** | For a library or a tool, in place of Production: the packages its repository publishes, each with its latest version and how to install it, or how to publish a first one. |
5959 | **Documentation**, **Homepage** | For docs, where they are read; for anything else, its homepage. |
60−| **Get to production**, **Ship a release** or **Get started** | The [checklist](#the-checklist) for what the project is, until every step is done or you dismiss it. |
6160 | **Right now** | Agents at work, and open pull requests moving from working to landed. |
6261 | **Needs you** | What is waiting on a person: a failed production build, a pull request to merge or review, a stuck run. |
6362 | **Active branches** | Branches other than the default, newest first. See [active branches](#active-branches). |
123122 Anyone who can change the project's settings (the **Maintain** role on
124123 its repository and up, as for production's address) can upload, replace
125124 or remove it.
126−
127−### The checklist
128−
129−People with a role on its repository see a card that counts what the
130−project has done, such as **3/5**. Its steps follow
131−[what the project is](#what-a-project-is): only an app deployed on g1t is
132−asked to deploy, add a domain or open a preview. Each step is worked out
133−from the project itself, and each links to where you do it. The card goes
134−away when every step is done. To hide it sooner, choose **×** on it. That
135−hides it for this project in this browser only.
136−
137−| What it is | Card | Steps |
138−| --- | --- | --- |
139−| App or site, deployed on g1t | **Get to production** | [Below](#get-to-production). |
140−| Library, tool | **Ship a release** | [Below](#ship-a-release). |
141−| App, deployed elsewhere | **Get started** | Push code; add production's address; add checks on pull requests; repository instructions; a first issue for g1t. |
142−| App nobody has placed yet | **Get started** | Push code; say where it runs; add checks on pull requests; repository instructions; a first issue for g1t. |
143−| Documentation | **Get started** | Push code; add where its docs are read (a docs or homepage address); repository instructions; a first issue for g1t. |
144−| Something else | **Get started** | Push code; add its links; repository instructions; a first issue for g1t. |
145−
146−### Get to production
147−
148−For an app or site deployed on g1t:
149125
150−| Step | Done when | Links to |
151−| --- | --- | --- |
152−| Connect a source or push code | The default branch has a commit, or the source is a mirror. | **Code**, which shows how to push, or how to have your coding agent start the project. |
153−| Deploy to production | A production build has gone live. | Deployments settings, or **Deployments** once they are on. |
154−| Add a custom domain | The project has a [custom domain](/guides/deployments/#custom-domains). | Domain settings. |
155−| Open a preview | A branch or pull request has had a [preview](/guides/deployments/#previews-of-branches). | A new pull request. |
156−| Set up repository instructions | `AGENTS.md` or `CLAUDE.md` is at the root of the default branch. | The instructions on the **Agents** page. See [repository instructions](/guides/working-with-g1t/#repository-instructions). |
157−| Assign a first issue to g1t | g1t has had a run, a pull request or an issue here. | A new issue. |
158−
159−### Ship a release
160−
161−A library or a tool does not deploy, so its card counts the steps to a
162−first release instead:
163−
164−| Step | Done when | Links to |
165−| --- | --- | --- |
166−| Connect a source or push code | As above. | **Code**. |
167−| Add checks on pull requests | The repository has a workflow in `.g1t/workflows`. Its runs are every pull request's checks. | **Actions**, which offers to add a starter workflow. |
168−| Tag a release or publish a package | A package its repository publishes has a version. For [Composer](/guides/composer/) and [Go](/guides/go/), pushing a tag such as `v1.0.0` is the release. | The package's page, or the guide for its registry. |
169−| Set up repository instructions | As above. | The **Agents** page. |
170−| Assign a first issue to g1t | As above. | A new issue. |
171−
172126 ## What a project is
173127
174128 A project is one of these, and its overview follows:
175129
176130 | What it is | Its overview shows |
177131 | --- | --- |
178−| **App or site, deployed on g1t** | Production on g1t.page, with previews and domains, and **Get to production**. |
132+| **App or site, deployed on g1t** | Production on g1t.page, with previews and domains. |
179133 | **App or site, deployed elsewhere** | Production at the address you give, deployed by your own pipeline, with **Visit**. It is never asked to turn on Deployments; Deployment settings stay one link away. |
180−| **Library or package** | The packages its repository publishes and how to install them, and **Ship a release**. |
134+| **Library or package** | The packages its repository publishes and how to install them. |
181135 | **Tool or CLI** | The same as a library: its packages and releases. |
182136 | **Documentation** | Where its docs are read. Docs can be published on g1t.page too, by turning on Deployments. |
183137 | **Something else** | Its homepage and links. |
+0−99
1−import { Check, ChevronRight, X } from "lucide-react";
2−import { useCallback, useSyncExternalStore } from "react";
3−import { Link } from "react-router";
4−
5−import { type ChecklistItem, dismiss, isDismissed, progress } from "../lib/checklist";
6−import { cn } from "../lib/cn";
7−import { Button } from "./ui/button";
8−import { Hint } from "./ui/hint";
9−
10−const storage = () => (typeof window === "undefined" ? null : window.localStorage);
11−const listeners = new Set<() => void>();
12−const subscribe = (listener: () => void) => {
13− listeners.add(listener);
14− return () => listeners.delete(listener);
15−};
16−
17−/**
18− * The steps to production, or to a library's first release, for one
19− * project, as `3/6`, until they are all done or someone dismisses it.
20− * Dismissing is per project and remembered by this browser.
21− */
22−export function ProductionChecklist({
23− base,
24− items,
25− title = "Get to production",
26−}: {
27− base: string;
28− items: ChecklistItem[];
29− /** "Ship a release" for a library. */
30− title?: string;
31−}) {
32− const hidden = useSyncExternalStore(
33− subscribe,
34− useCallback(() => isDismissed(storage, base), [base]),
35− () => false,
36− );
37− const { done, total, complete } = progress(items);
38− if (hidden || complete) return null;
39− const next = items.find((item) => !item.done)?.key;
40−
41− return (
42− <section aria-labelledby="checklist-title" className="rounded-2xl border border-line bg-surface">
43− <div className="flex items-center gap-3 px-5 pt-4 pb-3 sm:px-6">
44− <h2 id="checklist-title" className="text-sm font-semibold">
45− {title}
46− </h2>
47− <span className="rounded-full bg-raised px-2 py-px text-xs font-medium tabular-nums text-muted">
48− {done}/{total}
49− </span>
50− <span className="hidden h-1.5 w-24 overflow-hidden rounded-full bg-line sm:block" aria-hidden>
51− <span className="block h-full rounded-full bg-success" style={{ width: `${(done / total) * 100}%` }} />
52− </span>
53− <Hint label="Dismiss for this project">
54− <Button
55− type="button"
56− onClick={() => {
57− dismiss(storage, base);
58− for (const listener of listeners) listener();
59− }} variant="ghost" size="inline" className="ml-auto p-1 text-faint" aria-label="Dismiss the checklist for this project">
60− <X size={14} />
61− </Button>
62− </Hint>
63− </div>
64− <ol className="grid border-t border-line sm:grid-cols-2 lg:grid-cols-3">
65− {items.map((item) => (
66− <li key={item.key} className="border-b border-line last:border-b-0 sm:[&:nth-last-child(-n+2)]:border-b-0 lg:[&:nth-last-child(-n+3)]:border-b-0">
67− <Link
68− to={item.to}
69− className={cn(
70− "group flex h-full items-start gap-3 px-5 py-3 transition-colors hover:bg-raised/50 sm:px-6",
71− item.done && "opacity-70",
72− )}
73− >
74− <span
75− className={cn(
76− "mt-px flex size-4.5 shrink-0 items-center justify-center rounded-full border",
77− item.done ? "border-success bg-success text-bg" : item.key === next ? "border-success" : "border-line-strong",
78− )}
79− aria-hidden
80− >
81− {item.done && <Check size={11} strokeWidth={3} />}
82− </span>
83− <span className="min-w-0 grow">
84− <span className={cn("block text-[0.8125rem] font-medium", item.done && "text-muted line-through decoration-line-strong")}>
85− {item.title}
86− <span className="sr-only">{item.done ? " (done)" : ""}</span>
87− </span>
88− {!item.done && <span className="mt-0.5 block text-xs leading-5 text-muted">{item.detail}</span>}
89− </span>
90− {!item.done && (
91− <ChevronRight size={14} className="mt-0.5 shrink-0 text-faint transition-colors group-hover:text-fg" />
92− )}
93− </Link>
94− </li>
95− ))}
96− </ol>
97− </section>
98− );
99−}
+1−124
22 import { type ReactNode, useEffect, useState } from "react";
33 import { Link, useFetcher, useLocation, useRouteLoaderData, useSearchParams } from "react-router";
44
5−import { trialClosed } from "../lib/trial";
65 import { type ActivityGroup, type Verb, greetingFor, isAgent } from "../lib/mission";
76 import {
87 BLOCKING,
2827 import { cn } from "../lib/cn";
2928 import { microsOf, money } from "../lib/money";
3029 import { AgentComposer, type ComposerResult } from "./agent-composer";
31−import { AgentSetup } from "./agent-setup";
3230 import { InboxNeedsCard } from "./inbox";
3331 import type { ShellData } from "./shell";
3432 import { useLiveRefresh } from "./agents";
621619 </li>
622620 );
623621 }
624−
625−// --- Get started --------------------------------------------------------------------
626622
627−type Step = { done: boolean; title: string; about: string; to: string | null; action: string };
628−
629−/** The first things to do, ticked off as they are done, until work has been handed to agents. */
630−function GetStarted({ steps }: { steps: Step[] }) {
631− const left = steps.filter((step) => !step.done).length;
632− return (
633− <Card asChild className="p-5">
634− <section>
635− <div className="flex items-baseline justify-between gap-3">
636− <h2 className="text-base font-semibold tracking-tight">Get started</h2>
637− <span className="text-xs text-muted">
638− {steps.length - left} of {steps.length} done
639− </span>
640− </div>
641− <ol className="mt-4 space-y-2">
642− {steps.map((step, index) => (
643− <li
644− key={step.title}
645− className={cn("flex items-start gap-3 rounded-lg px-3 py-2.5", step.done ? "" : "bg-bg/50 ring-1 ring-line")}
646− >
647− <span
648− className={cn(
649− "mt-0.5 flex size-5 shrink-0 items-center justify-center rounded-full text-[0.6875rem] font-medium",
650− step.done ? "bg-success text-bg" : "text-muted ring-1 ring-line-strong",
651− )}
652− >
653− {step.done ? <Check size={12} /> : index + 1}
654− </span>
655− <span className="min-w-0 grow">
656− <span className={cn("block text-sm font-medium", step.done && "text-muted line-through decoration-faint")}>{step.title}</span>
657− {!step.done && <span className="mt-0.5 block text-xs leading-5 text-muted">{step.about}</span>}
658− </span>
659− {!step.done && step.to && (
660− <Link
661− to={step.to}
662− className="shrink-0 rounded-md bg-fg px-2.5 py-1 text-xs font-medium text-bg transition-colors hover:bg-fg-hover"
663− >
664− {step.action}
665− </Link>
666− )}
667− </li>
668− ))}
669− </ol>
670− </section>
671− </Card>
672− );
673−}
674−
675623 // --- The page -----------------------------------------------------------------------
676624
677625 const TAB_LABEL: Record<Tab, string> = { needs: "Needs you", waiting: "Waiting on agents", landed: "Landed today" };
709657 }
710658 }, []);
711659
712− const { viewer, repos, waiting, landed, liveTotal, week, groups, titles, stats, canRunAgents, trial } = loaded;
660+ const { viewer, repos, waiting, landed, liveTotal, week, groups, titles, stats, canRunAgents } = loaded;
713661 const workspace = shell?.workspace?.slug ?? loaded.workspace ?? viewer.workspaces?.[0]?.slug ?? null;
714662
715663 // The usage limit, as the sidebar knows it, heads the list when it bites.
761709 return text ? `?${text}` : pathname;
762710 };
763711
764− const steps: Step[] = [
765− {
766− done: Boolean(workspace),
767− title: "Create a workspace",
768− about: "Projects, people and agent credit live in one.",
769− to: "/workspaces/new",
770− action: "Create",
771− },
772− {
773− done: repos.length > 0,
774− title: "Add a project",
775− about: "Create a repository, or import one by its address. Push to it with git as usual.",
776− to: "/new",
777− action: "Add",
778− },
779− trial?.open
780− ? {
781− done: true,
782− title: "Try g1t free",
783− about: trial.granted
784− ? `This workspace has ${dollars(trial.limitMicros - trial.usedMicros)} of its ${dollars(trial.limitMicros)} trial credit left, for g1t's models and sandboxes.`
785− : `This workspace gets ${dollars(trial.limitMicros)} of trial credit, for g1t's models and sandboxes, the first time g1t works here.`,
786− to: workspace ? `/${workspace}/-/integrations/models` : null,
787− action: "Connect",
788− }
789− : canRunAgents
790− ? {
791− done: Boolean(shell?.free) || shell?.limit == null || shell.limit.comped || shell.limit.state === "ok",
792− title: "Keep work running",
793− about:
794− "Usage is charged after it runs. Add a card under Billing, so g1t charges it as you near your limit instead of stopping work.",
795− to: workspace ? `/${workspace}/-/billing` : null,
796− action: "Billing",
797− }
798− : {
799− done: false,
800− title: "Connect a model",
801− about: `${
802− trialClosed(trial, workspace ?? "This workspace") ? `${trialClosed(trial, workspace ?? "This workspace")} ` : ""
803− }Agents need a model to think with. Connect your Anthropic or OpenAI key, or any compatible endpoint.`,
804− to: workspace ? `/${workspace}/-/integrations/models` : null,
805− action: "Connect",
806− },
807− {
808− done: loaded.handedOff,
809− title: "Hand off an outcome",
810− about:
811− "Open an issue and assign it to g1t, or write an outcome on a project's Plans page and let a planner split it into issues.",
812− to: repos[0] ? `/${repos[0].namespace}/${repos[0].name}/plans` : null,
813− action: "Write one",
814− },
815− ];
816− const starting = steps.some((step) => !step.done);
817712 const noProjects = repos.length === 0;
818713
819714 const rows = tab === "needs" ? sortRows(needs, sort) : tab === "waiting" ? sortRows(waiting, sort) : sortRows(landed, sort);
921816 </Link>
922817 )}
923818 </section>
924−
925− {starting && <GetStarted steps={steps} />}
926819
927820 <Card asChild tone="plain" className="grid grid-cols-2 gap-px overflow-hidden bg-line lg:grid-cols-5">
928821 <section
12031096 ))}
12041097 </ol>
12051098 )}
1206− </section>
1207− </Card>
1208−
1209− <Card asChild className="p-5">
1210− <section>
1211− <h2 className="text-sm font-semibold">Connect your own agent</h2>
1212− <p className="mt-1.5 text-xs leading-5 text-muted">
1213− Add g1t to your coding agent. It signs in through your browser; there is no token to copy.
1214− </p>
1215− <AgentSetup className="mt-3" />
1216− <Link
1217− to="https://docs.g1t.sh/guides/bring-your-own-agent/"
1218− className="mt-3 inline-flex items-center gap-1 text-xs text-muted hover:text-fg"
1219− >
1220− How it works <ArrowRight size={12} />
1221− </Link>
12221099 </section>
12231100 </Card>
12241101 </aside>
+0−7
1313 import { DeploymentsPanel } from "./deployments-panel";
1414 import { CodeLines } from "./code-lines";
1515
16−import { AgentSetup } from "./agent-setup";
1716 import { useAddresses } from "../lib/addresses";
1817 import { isImagePath } from "../lib/usercontent";
1918 import { CloneBox } from "./clone-box";
356355 <CopyLine prompt text={`git remote add g1t ${cloneUrl}`} />
357356 <CopyLine prompt text={`git push -u g1t ${ref}`} />
358357 </div>
359− <h3 className="mt-7 text-sm font-medium">Or have your coding agent start it</h3>
360− <p className="mt-1 text-sm text-muted">
361− Connect it to g1t, then ask it to build the first version of{" "}
362− <span className="font-mono text-fg">{repo.name}</span> and push it here.
363− </p>
364− <AgentSetup className="mt-3" />
365358 </Card>
366359 );
367360 }
+1−1
7777
7878 Cloudflare may set its own security cookies (such as `__cf_bm`) to tell people from bots when g1t is under attack.
7979
80−The site also keeps a few preferences in your browser's local storage, which never leave your device: which coding agent you set up with, how you like diffs shown, and checklist items you've dismissed. HeyCatch keeps a copy of its analytics id there too, which is sent with each analytics event.
80+The site also keeps a few preferences in your browser's local storage, which never leave your device: which coding agent you set up with and how you like diffs shown. HeyCatch keeps a copy of its analytics id there too, which is sent with each analytics event.
8181
8282 **Fonts.** g1t.sh and docs.g1t.sh serve their typefaces themselves, so loading a page asks no one else for them.
8383
+0−174
1−import assert from "node:assert/strict";
2−import { test } from "node:test";
3−
4−import {
5− type ChecklistFacts,
6− agentWasAssigned,
7− checklistPlan,
8− dismiss,
9− dismissKey,
10− hasInstructions,
11− isDismissed,
12− productionChecklist,
13− progress,
14− releaseChecklist,
15− startChecklist,
16−} from "./checklist.ts";
17−
18−const fresh: ChecklistFacts = {
19− base: "/acme/web",
20− hasCode: false,
21− deploysEnabled: false,
22− productionDeployed: false,
23− domains: 0,
24− previewOpened: false,
25− instructions: false,
26− agentAssigned: false,
27−};
28−
29−test("a new project has everything to do, in order", () => {
30− const items = productionChecklist(fresh);
31− assert.deepEqual(
32− items.map((item) => item.key),
33− ["code", "deploy", "domain", "preview", "instructions", "agent"],
34− );
35− assert.deepEqual(progress(items), { done: 0, total: 6, complete: false });
36−});
37−
38−test("each step is done from its own fact", () => {
39− const items = productionChecklist({ ...fresh, hasCode: true, productionDeployed: true, domains: 2 });
40− assert.deepEqual(progress(items), { done: 3, total: 6, complete: false });
41− const all = productionChecklist({
42− ...fresh,
43− hasCode: true,
44− productionDeployed: true,
45− domains: 1,
46− previewOpened: true,
47− instructions: true,
48− agentAssigned: true,
49− });
50− assert.equal(progress(all).complete, true);
51−});
52−
53−test("unknown facts count as not done", () => {
54− const items = productionChecklist({ ...fresh, domains: null, instructions: null });
55− assert.equal(items.find((item) => item.key === "domain")?.done, false);
56− assert.equal(items.find((item) => item.key === "instructions")?.done, false);
57−});
58−
59−test("each step links to where it is done", () => {
60− const off = productionChecklist(fresh);
61− assert.equal(off.find((item) => item.key === "deploy")?.to, "/acme/web/settings/deployments");
62− const on = productionChecklist({ ...fresh, deploysEnabled: true });
63− assert.equal(on.find((item) => item.key === "deploy")?.to, "/acme/web/deployments");
64− assert.equal(on.find((item) => item.key === "domain")?.to, "/acme/web/settings/domains");
65− assert.equal(on.find((item) => item.key === "agent")?.to, "/acme/web/issues/new");
66−});
67−
68−test("instructions are AGENTS.md or CLAUDE.md at the root", () => {
69− assert.equal(hasInstructions(["README.md", "AGENTS.md"]), true);
70− assert.equal(hasInstructions(["claude.md"]), true);
71− assert.equal(hasInstructions(["README.md", "AGENTS.mdx", "docs"]), false);
72−});
73−
74−test("g1t counts as assigned from a run, a pull request or an issue", () => {
75− const none = { runAgents: ["claude-code"], pullAgents: ["claude-code"], issues: [{ assignees: ["ana"], agent: null }] };
76− assert.equal(agentWasAssigned(none), false);
77− assert.equal(agentWasAssigned({ ...none, runAgents: ["g1t"] }), true);
78− assert.equal(agentWasAssigned({ ...none, pullAgents: ["g1t"] }), true);
79− assert.equal(agentWasAssigned({ ...none, issues: [{ assignees: ["G1T"], agent: null }] }), true);
80− assert.equal(agentWasAssigned({ ...none, issues: [{ assignees: [], agent: "g1t" }] }), true);
81−});
82−
83−test("dismissing is remembered per project, and storage that throws is ignored", () => {
84− const map = new Map<string, string>();
85− const store = { getItem: (key: string) => map.get(key) ?? null, setItem: (key: string, value: string) => void map.set(key, value) };
86− assert.equal(isDismissed(() => store, "/acme/web"), false);
87− assert.equal(dismiss(() => store, "/Acme/Web"), true);
88− assert.equal(isDismissed(() => store, "/acme/web"), true);
89− assert.equal(isDismissed(() => store, "/acme/api"), false);
90− assert.equal(dismissKey("/Acme/Web"), "g1t:checklist-dismissed:/acme/web");
91− const blocked = {
92− getItem: () => {
93− throw new Error("blocked");
94− },
95− setItem: () => {
96− throw new Error("blocked");
97− },
98− };
99− assert.equal(isDismissed(() => blocked, "/acme/web"), false);
100− assert.equal(dismiss(() => blocked, "/acme/web"), false);
101−});
102−
103−test("a library's checklist ships a release instead of deploying", () => {
104− const facts = {
105− base: "/flagon-io/php-log",
106− hasCode: true,
107− instructions: false,
108− agentAssigned: false,
109− hasWorkflow: false,
110− released: true,
111− releaseTo: "/flagon-io/-/packages/composer/psr/log",
112− };
113− const items = releaseChecklist(facts);
114− assert.deepEqual(
115− items.map((item) => item.key),
116− ["code", "checks", "release", "instructions", "agent"],
117− );
118− assert.deepEqual(progress(items), { done: 2, total: 5, complete: false });
119− assert.equal(items.find((item) => item.key === "checks")?.to, "/flagon-io/php-log/actions");
120− assert.equal(items.find((item) => item.key === "release")?.to, "/flagon-io/-/packages/composer/psr/log");
121− assert.ok(items.every((item) => !/deploy|production|domain|preview/i.test(item.title)));
122− // An unknown workflow list counts as not done.
123− assert.equal(releaseChecklist({ ...facts, hasWorkflow: null }).find((item) => item.key === "checks")?.done, false);
124−});
125−
126−const start = {
127− base: "/flagon-io/g1t",
128− hasCode: true,
129− instructions: true,
130− agentAssigned: false,
131− hasWorkflow: true,
132− productionUrl: null,
133− hasLinks: false,
134− hasDocsLink: false,
135−};
136−
137−test("only what g1t deploys gets production's steps", () => {
138− assert.deepEqual(checklistPlan({ kind: "app", runs: "g1t" }), { plan: "production", title: "Get to production" });
139− assert.deepEqual(checklistPlan({ kind: "docs", runs: "g1t" }).plan, "production");
140− assert.deepEqual(checklistPlan({ kind: "app", runs: "elsewhere" }), { plan: "elsewhere", title: "Get started" });
141− assert.equal(checklistPlan({ kind: "app", runs: null }).plan, "unknown");
142− assert.deepEqual(checklistPlan({ kind: "tool", runs: null }), { plan: "release", title: "Ship a release" });
143− assert.equal(checklistPlan({ kind: "library", runs: null }).plan, "release");
144− assert.equal(checklistPlan({ kind: "docs", runs: "elsewhere" }).plan, "docs");
145− assert.equal(checklistPlan({ kind: "other", runs: null }).plan, "other");
146−});
147−
148−test("an app deployed elsewhere is never asked to deploy on g1t", () => {
149− const items = startChecklist("elsewhere", start);
150− assert.deepEqual(
151− items.map((item) => item.key),
152− ["code", "production", "checks", "instructions", "agent"],
153− );
154− assert.ok(items.every((item) => !/deploy to|turn on|domain|preview/i.test(`${item.title} ${item.detail}`)));
155− assert.equal(items.find((item) => item.key === "production")?.done, false);
156− assert.equal(startChecklist("elsewhere", { ...start, productionUrl: "https://g1t.sh" }).find((item) => item.key === "production")?.done, true);
157− // Every step can be done.
158− const all = startChecklist("elsewhere", { ...start, productionUrl: "https://g1t.sh", agentAssigned: true });
159− assert.equal(progress(all).complete, true);
160−});
161−
162−test("an app nobody has placed asks where it runs; docs and other ask for links", () => {
163− assert.deepEqual(
164− startChecklist("unknown", start).map((item) => item.key),
165− ["code", "where", "checks", "instructions", "agent"],
166− );
167− assert.equal(startChecklist("unknown", start).find((item) => item.key === "where")?.to, "/flagon-io/g1t/settings#kind");
168− const docs = startChecklist("docs", { ...start, hasDocsLink: true });
169− assert.deepEqual(docs.map((item) => item.key), ["code", "links", "instructions", "agent"]);
170− assert.equal(docs.find((item) => item.key === "links")?.done, true);
171− const other = startChecklist("other", start);
172− assert.equal(other.find((item) => item.key === "links")?.done, false);
173− assert.equal(startChecklist("other", { ...start, hasLinks: true, agentAssigned: true }).every((item) => item.done), true);
174−});
+0−282
1−/**
2− * The steps that take a project to production, or for a library to its
3− * first release, each worked out from what the project has done, with
4− * where to do the ones it has not. Shown on the project's overview to its
5− * members until they are all done or it is dismissed.
6− */
7−
8−export type ChecklistFacts = {
9− /** The project's address, `/<workspace>/<project>`. */
10− base: string;
11− /** Its default branch has a commit, or its source is a mirror. */
12− hasCode: boolean;
13− /** Deployments are switched on for it. */
14− deploysEnabled: boolean;
15− /** A production build has gone live, now or before. */
16− productionDeployed: boolean;
17− /** Custom domains it has; null when they could not be read. */
18− domains: number | null;
19− /** A preview has been built for a branch or pull request. */
20− previewOpened: boolean;
21− /** An AGENTS.md or CLAUDE.md at the root of its default branch; null when unknown. */
22− instructions: boolean | null;
23− /** g1t has been given an issue here. */
24− agentAssigned: boolean;
25−};
26−
27−export type ChecklistItem = {
28− key: "code" | "deploy" | "domain" | "preview" | "checks" | "release" | "production" | "where" | "links" | "instructions" | "agent";
29− title: string;
30− detail: string;
31− done: boolean;
32− to: string;
33− action: string;
34−};
35−
36−export function productionChecklist(facts: ChecklistFacts): ChecklistItem[] {
37− const { base } = facts;
38− return [
39− {
40− key: "code",
41− title: "Connect a source or push code",
42− detail: "Push an existing project, or have your coding agent start one.",
43− done: facts.hasCode,
44− to: `${base}/code`,
45− action: "Push code",
46− },
47− {
48− key: "deploy",
49− title: "Deploy to production",
50− detail: facts.deploysEnabled
51− ? "Production builds from the default branch on every push."
52− : "Deployments are off until you turn them on. Then production builds from the default branch on every push.",
53− done: facts.productionDeployed,
54− to: facts.deploysEnabled ? `${base}/deployments` : `${base}/settings/deployments`,
55− action: facts.deploysEnabled ? "Deployments" : "Turn on",
56− },
57− {
58− key: "domain",
59− title: "Add a custom domain",
60− detail: "Serve production from a domain of your own.",
61− done: (facts.domains ?? 0) > 0,
62− to: `${base}/settings/domains`,
63− action: "Add",
64− },
65− {
66− key: "preview",
67− title: "Open a preview",
68− detail: "Every pull request gets a live preview of its branch.",
69− done: facts.previewOpened,
70− to: `${base}/pulls/new`,
71− action: "New pull request",
72− },
73− {
74− key: "instructions",
75− title: "Set up repository instructions",
76− detail: "Commit an AGENTS.md with how to build and test; every agent run reads it.",
77− done: facts.instructions === true,
78− to: `${base}/agents#instructions`,
79− action: "How",
80− },
81− {
82− key: "agent",
83− title: "Assign a first issue to g1t",
84− detail: "It opens a pull request, makes the change and sees it through checks and review.",
85− done: facts.agentAssigned,
86− to: `${base}/issues/new`,
87− action: "New issue",
88− },
89− ];
90−}
91−
92−export type ReleaseFacts = Pick<ChecklistFacts, "base" | "hasCode" | "instructions" | "agentAssigned"> & {
93− /** It has a workflow, whose runs are its pull requests' checks; null when unknown. */
94− hasWorkflow: boolean | null;
95− /** A package its repository publishes has a version. */
96− released: boolean;
97− /** Where publishing a first version is explained: its package's page or its registry's guide. */
98− releaseTo: string;
99−};
100−
101−/**
102− * The steps for a library or a tool, which ships as releases rather than
103− * deploying: the same first and last steps as production, with checks and
104− * a first version in between.
105− */
106−export function releaseChecklist(facts: ReleaseFacts): ChecklistItem[] {
107− const shared = productionChecklist({ ...facts, deploysEnabled: false, productionDeployed: false, domains: null, previewOpened: false });
108− const step = (key: ChecklistItem["key"]) => shared.find((item) => item.key === key)!;
109− return [
110− step("code"),
111− {
112− key: "checks",
113− title: "Add checks on pull requests",
114− detail: "A workflow that builds and tests it. Its runs are every pull request's checks.",
115− done: facts.hasWorkflow === true,
116− to: `${facts.base}/actions`,
117− action: "Add CI",
118− },
119− {
120− key: "release",
121− title: "Tag a release or publish a package",
122− detail: "Publish a first version to the workspace's registry for others to install.",
123− done: facts.released,
124− to: facts.releaseTo,
125− action: "How",
126− },
127− step("instructions"),
128− step("agent"),
129− ];
130−}
131−
132−export type StartFacts = Pick<ChecklistFacts, "base" | "hasCode" | "instructions" | "agentAssigned"> & {
133− /** It has a workflow, whose runs are its pull requests' checks; null when unknown. */
134− hasWorkflow: boolean | null;
135− /** Production's address, for an app deployed elsewhere. */
136− productionUrl: string | null;
137− /** Its homepage, docs or any other link is set. */
138− hasLinks: boolean;
139− /** Its docs address is set, or its homepage. */
140− hasDocsLink: boolean;
141−};
142−
143−/** Where a project's own settings are, for the steps that are done there. */
144−const settingsAt = (base: string, anchor: string) => `${base}/settings#${anchor}`;
145−
146−/**
147− * The steps for a project g1t does not deploy, by what it is. Every step
148− * applies to it and each is done from its own fact: nothing about turning
149− * on Deployments, domains or previews.
150− *
151− * - An app deployed elsewhere: its production address, then checks.
152− * - An app nobody has said where it runs: saying so, then checks.
153− * - Docs: where they are read.
154− * - Anything else: its links.
155− */
156−export function startChecklist(kind: "elsewhere" | "unknown" | "docs" | "other", facts: StartFacts): ChecklistItem[] {
157− const shared = productionChecklist({ ...facts, deploysEnabled: false, productionDeployed: false, domains: null, previewOpened: false });
158− const step = (key: ChecklistItem["key"]) => shared.find((item) => item.key === key)!;
159− const checks: ChecklistItem = {
160− key: "checks",
161− title: "Add checks on pull requests",
162− detail: "A workflow that builds and tests it. Its runs are every pull request's checks.",
163− done: facts.hasWorkflow === true,
164− to: `${facts.base}/actions`,
165− action: "Add CI",
166− };
167− const middle: ChecklistItem[] =
168− kind === "elsewhere"
169− ? [
170− {
171− key: "production",
172− title: "Add production's address",
173− detail: "Where your own pipeline deploys it, so its overview links to production.",
174− done: facts.productionUrl != null,
175− to: settingsAt(facts.base, "kind"),
176− action: "Add",
177− },
178− checks,
179− ]
180− : kind === "unknown"
181− ? [
182− {
183− key: "where",
184− title: "Say where it runs",
185− detail: "Deployed on g1t, deployed elsewhere, or not deployed at all, such as a library.",
186− done: false,
187− to: settingsAt(facts.base, "kind"),
188− action: "Choose",
189− },
190− checks,
191− ]
192− : kind === "docs"
193− ? [
194− {
195− key: "links",
196− title: "Add where its docs are read",
197− detail: "A docs or homepage address, shown on its overview and wherever the project is listed.",
198− done: facts.hasDocsLink,
199− to: settingsAt(facts.base, "links"),
200− action: "Add",
201− },
202− ]
203− : [
204− {
205− key: "links",
206− title: "Add its links",
207− detail: "A homepage, docs, or any other address people go to for it.",
208− done: facts.hasLinks,
209− to: settingsAt(facts.base, "links"),
210− action: "Add",
211− },
212− ];
213− return [step("code"), ...middle, step("instructions"), step("agent")];
214−}
215−
216−export type ChecklistPlan = "production" | "release" | "elsewhere" | "unknown" | "docs" | "other";
217−
218−/**
219− * Which steps a project gets, and their heading, from what it is and where
220− * it runs. Only what g1t deploys gets production's steps.
221− */
222−export function checklistPlan(project: {
223− kind: "app" | "library" | "tool" | "docs" | "other";
224− runs: "g1t" | "elsewhere" | null;
225−}): { plan: ChecklistPlan; title: string } {
226− if (project.runs === "g1t") return { plan: "production", title: "Get to production" };
227− if (project.kind === "library" || project.kind === "tool") return { plan: "release", title: "Ship a release" };
228− if (project.kind === "app") return { plan: project.runs === "elsewhere" ? "elsewhere" : "unknown", title: "Get started" };
229− return { plan: project.kind, title: "Get started" };
230−}
231−
232−/** `3/6`, for the card's heading. */
233−export function progress(items: ChecklistItem[]): { done: number; total: number; complete: boolean } {
234− const done = items.filter((item) => item.done).length;
235− return { done, total: items.length, complete: done === items.length };
236−}
237−
238−/** A repository's root lists instructions for agents. */
239−export function hasInstructions(names: string[]): boolean {
240− return names.some((name) => /^(agents|claude)\.md$/i.test(name));
241−}
242−
243−/** Whether g1t has worked here, from its runs, pull requests or issues. */
244−export function agentWasAssigned(input: {
245− runAgents: string[];
246− pullAgents: string[];
247− issues: { assignees: string[]; agent: string | null }[];
248−}): boolean {
249− const isAgent = (name: string | null) => name?.toLowerCase() === "g1t";
250− return (
251− input.runAgents.some(isAgent) ||
252− input.pullAgents.some(isAgent) ||
253− input.issues.some((issue) => isAgent(issue.agent) || issue.assignees.some(isAgent))
254− );
255−}
256−
257−// --- Dismissing it, per project, in this browser -----------------------------
258−
259−type Store = Pick<Storage, "getItem" | "setItem">;
260−
261−export function dismissKey(base: string): string {
262− return `g1t:checklist-dismissed:${base.toLowerCase()}`;
263−}
264−
265−export function isDismissed(storage: () => Store | null | undefined, base: string): boolean {
266− try {
267− return storage()?.getItem(dismissKey(base)) === "1";
268− } catch {
269− return false;
270− }
271−}
272−
273−export function dismiss(storage: () => Store | null | undefined, base: string): boolean {
274− try {
275− const store = storage();
276− if (!store) return false;
277− store.setItem(dismissKey(base), "1");
278− return true;
279− } catch {
280− return false;
281− }
282−}
+0−2
623623 workspace: slug,
624624 repos: repoList.filter((repo) => inWorkspace(repo)).map(pathOf),
625625 canRunAgents: models_ == null || models_.hosted || models_.own != null,
626− trial: models_?.own == null ? (models_?.trial ?? null) : null,
627− handedOff: activeList.length > 0 || runList.length > 0 || merged.length > 0,
628626 needs: needRows,
629627 waiting,
630628 landed,
+12−0
1212 libraryPackages,
1313 neverDeploys,
1414 primaryLink,
15+ projectPlan,
1516 projectChanges,
1617 linksFromForm,
1718 linksToShow,
147148 { label: "", url: "npmjs.com/x" },
148149 ]);
149150 });
151+
152+test("only what g1t deploys leads with production", () => {
153+ assert.equal(projectPlan({ kind: "app", runs: "g1t" }), "production");
154+ assert.equal(projectPlan({ kind: "docs", runs: "g1t" }), "production");
155+ assert.equal(projectPlan({ kind: "app", runs: "elsewhere" }), "elsewhere");
156+ assert.equal(projectPlan({ kind: "app", runs: null }), "unknown");
157+ assert.equal(projectPlan({ kind: "tool", runs: null }), "release");
158+ assert.equal(projectPlan({ kind: "library", runs: null }), "release");
159+ assert.equal(projectPlan({ kind: "docs", runs: "elsewhere" }), "docs");
160+ assert.equal(projectPlan({ kind: "other", runs: null }), "other");
161+});
+14−0
8989 }
9090 }
9191
92+/** What a project's overview leads with: production, a release, or what it says of itself. */
93+export type ProjectPlan = "production" | "release" | "elsewhere" | "unknown" | "docs" | "other";
94+
95+/**
96+ * Which head a project's overview shows, from what it is and where it
97+ * runs. Only what g1t deploys gets production's.
98+ */
99+export function projectPlan(project: Pick<Project, "kind" | "runs">): ProjectPlan {
100+ if (project.runs === "g1t") return "production";
101+ if (project.kind === "library" || project.kind === "tool") return "release";
102+ if (project.kind === "app") return project.runs === "elsewhere" ? "elsewhere" : "unknown";
103+ return project.kind;
104+}
105+
92106 /** A link as pages list it: what it is, its label and address. */
93107 export type ShownLink = { key: string; type: "homepage" | "docs" | "production" | "custom"; label: string; url: string };
94108
+2−2
4747 {!overview ? (
4848 <Card className="p-6 text-sm text-muted">Observability could not be loaded just now. Reload the page to try again.</Card>
4949 ) : nothing ? (
50− <GetStarted base={base} canManage={canManage} />
50+ <NothingYet base={base} canManage={canManage} />
5151 ) : (
5252 <Loaded overview={overview} errors={errors} base={base} path={path} view={view} />
5353 )}
314314 }
315315
316316 /** Nothing has arrived yet: the two ways it does. */
317−function GetStarted({ base, canManage }: { base: string; canManage: boolean }) {
317+function NothingYet({ base, canManage }: { base: string; canManage: boolean }) {
318318 return (
319319 <Card asChild>
320320 <section>
+12−68
4545 import { Elapsed, formatCost, useLiveRefresh } from "../../components/agents";
4646 import { ActivityFeed, DeployStrip, Meter, NeedsList, Panel, Quiet, Unavailable, percent } from "../../components/mission";
4747 import { type ActiveBranch, ActiveBranches } from "../../components/branches";
48−import { ProductionChecklist } from "../../components/checklist";
4948 import { PackageIcon } from "../../components/package-icon";
5049 import { Hint } from "../../components/ui/hint";
5150 import { Loading, Skeleton, SkeletonRows } from "../../components/ui/skeleton";
7978 runHealth,
8079 stuckMinutes,
8180 } from "../../lib/mission";
82−import { agentWasAssigned, checklistPlan, hasInstructions, productionChecklist, releaseChecklist, startChecklist } from "../../lib/checklist";
8381 import { ECOSYSTEM_LABEL, installCommands } from "../../lib/packages";
8482 import {
8583 PUBLISH_GUIDES,
8684 bare,
87− hasRelease,
8885 kindLabel,
8986 linksToShow,
9087 latestTag,
9289 packageName,
9390 packagePath,
9491 projectChanges,
92+ projectPlan,
9593 publishGuide,
9694 } from "../../lib/project-kind";
9795 import { AboutEditor, KindMenu, LinkList } from "../../components/project-about";
150148 const soft = <T,>(promise: Promise<T> | null): Promise<T | null> =>
151149 promise ? promise.catch((error) => (console.warn("overview:", error), null)) : Promise.resolve(null);
152150 // People with a role of their own here see its running parts: deployments,
153− // memory, setup; everyone who can read it sees the rest. Not awaited: only
151+ // memory; everyone who can read it sees the rest. Not awaited: only
154152 // those sections wait on the repository lookup, which the layout makes in
155153 // this request too.
156154 const accessP = accessTo(context, params);
159157 memberP.then((member) => (member ? soft(start()) : null));
160158 const repoP = soft(repoFor(context, params));
161159 const projectP = soft(projectFor(context, params));
162− // A library or a tool shows its packages where an app shows production,
163− // and every project g1t does not deploy counts a workflow in its checklist.
160+ // A library or a tool shows its packages where an app shows production.
164161 // Deployments from anywhere (g1t.page, g1t Actions, the API), by environment, for anyone who can read it.
165162 const environmentsP = projectP.then((found) =>
166163 soft(deployments.environments(found?.ok && found.value.source.kind === "hosted" ? found.value.source.repo : path, viewer)),
167164 );
168− const planP = projectP.then((project) => (project?.ok ? checklistPlan(project.value).plan : null));
165+ const planP = projectP.then((project) => (project?.ok ? projectPlan(project.value) : null));
169166 const libraryRepoP = Promise.all([planP, repoP]).then(([plan, repo]) => (plan === "release" && repo?.ok ? repo.value : null));
170167 const packagesP = libraryRepoP.then((repo) => (repo ? soft(packages.list(params.owner, viewer, { repoId: repo.id })) : null));
171− const workflowsP = planP.then((plan) => (plan && plan !== "production" ? forMembers(() => actions.workflows(path, viewer)) : null));
172168 // Recent workflow runs, for Health when no pull request's checks have finished lately.
173169 const workflowRunsP = soft(actions.runs(path, viewer, { limit: 30 }));
174170 // Only the kinds the feed shows: the newest events are mostly session
218214 branchesP.catch(() => null),
219215 new Promise<"slow">((resolve) => setTimeout(() => resolve("slow"), crawler ? BRANCHES_WAIT_CRAWLER_MS : BRANCHES_WAIT_MS)),
220216 ]);
221− const [{ insider: member, can }, project, settings, list, open, closed, log, counts, runs, queue, issues, memories, recent, mine, domains, root, packageList, workflows, tagList] = await Promise.all([
217+ const [{ insider: member, can }, project, settings, list, open, closed, log, counts, runs, queue, issues, memories, recent, mine, packageList, tagList] = await Promise.all([
222218 accessP,
223219 projectP,
224220 forMembers(() => deployments.settings(ref, viewer)),
234230 forMembers(() => agents.listMemories(viewer, params.owner, path)),
235231 eventsP,
236232 minePullsP,
237− // For the checklist, which only members see.
238− forMembers(() => deployments.domains(ref, viewer)),
239− forMembers(() => repos.tree(path, viewer, null, "")),
240233 packagesP,
241− workflowsP,
242234 // The latest release, for About.
243235 soft(repos.tags(path, viewer)),
244236 ]);
367359 const knows = distinctFacts([...memoryList.filter((m) => m.pinned), ...memoryList.filter((m) => !m.pinned).sort((a, b) => Date.parse(b.createdAt) - Date.parse(a.createdAt))]).slice(0, 5);
368360 const openIssues = ok(issues);
369361
370− // --- Getting to production ----------------------------------------------------
371− const wentLive = (status: string) => status === "ready" || status === "replaced" || status === "down";
372− const liveApps = ok(list)?.live ?? [];
362+ // --- Where it runs ------------------------------------------------------------
373363 const commitNow = ok(log)?.[0] ?? null;
374364 // Production deployed somewhere other than g1t.page (by g1t Actions, or
375365 // reported through the API): the slot an app deployed elsewhere shows.
380370 // elsewhere, runs elsewhere.
381371 const found = ok(project);
382372 const projectValue = found && deployment && found.kind === "app" && found.runs == null ? { ...found, runs: "elsewhere" as const } : found;
383− const plan = projectValue ? checklistPlan(projectValue) : { plan: "production" as const, title: "Get to production" };
384− const isLibrary = plan.plan === "release";
373+ const isLibrary = projectValue != null && projectPlan(projectValue) === "release";
385374 const published = libraryPackages(ok(packageList) ?? []);
386− const hasWorkflow = workflows?.ok ? workflows.value.length > 0 : null;
387− const firstSteps = {
388− base,
389− hasCode: commitNow != null || projectValue?.source.kind === "mirror",
390− instructions: commitNow == null ? false : root?.ok ? hasInstructions(root.value.entries.map((entry) => entry.name)) : null,
391− agentAssigned: agentWasAssigned({
392− runAgents: runList.map((run) => run.agent),
393− pullAgents: [...openPulls, ...(ok(closed) ?? [])].map((pull) => pull.agent),
394− issues: openIssues ?? [],
395− }),
396− };
397− const links = projectValue?.links;
398− const checklist = !member
399− ? null
400− : plan.plan !== "production" && plan.plan !== "release"
401− ? startChecklist(plan.plan, {
402− ...firstSteps,
403− hasWorkflow,
404− productionUrl: projectValue?.productionUrl ?? null,
405− hasLinks: !!links && (links.homepage != null || links.docs != null || links.custom.length > 0),
406− hasDocsLink: !!links && (links.docs != null || links.homepage != null),
407− })
408− : isLibrary
409− ? releaseChecklist({
410− ...firstSteps,
411− hasWorkflow,
412− released: hasRelease(published),
413− releaseTo: published[0]
414− ? packagePath(published[0])
415− : (publishGuide(projectValue?.ecosystem ?? null)?.guide ?? "https://docs.g1t.sh/guides/packages/"),
416− })
417− : productionChecklist({
418− ...firstSteps,
419− deploysEnabled: ok(settings)?.enabled ?? false,
420− productionDeployed:
421− liveApps.some((app) => app.kind === "production") ||
422− builds.some((build) => build.kind === "production" && wentLive(build.status)),
423− domains: ok(domains)?.domains.length ?? null,
424− previewOpened:
425− liveApps.some((app) => app.kind === "preview") || builds.some((build) => build.kind === "preview" && wentLive(build.status)),
426− });
427375
428376 // The checks on the latest commit and each active branch's head, in
429377 // one call once the branches are read: streamed in beside them.
442390 commit: ok(log)?.[0] ?? null,
443391 commits: (ok(log) ?? []).map((one) => ({ hash: one.hash, message: one.message.split("\n")[0] ?? "", author: one.author, coAuthors: one.coAuthors, at: one.authoredAt })),
444392 release: latestTag(ok(tagList) ?? []),
445− checklistTitle: plan.title,
446393 // Production as reported from outside g1t, for an app deployed elsewhere.
447394 deployment,
448395 // Its screenshot's address on the site (production-elsewhere.ts), when it has one to show.
472419 knows: knows as Memory[],
473420 memoryCount: memoryList.length,
474421 memoriesLoaded: memories?.ok ?? false,
475− checklist,
476422 library: isLibrary
477423 ? { packages: published.slice(0, PACKAGES_SHOWN), total: published.length, loaded: packageList?.ok ?? false }
478424 : null,
794740 actionData: Route.ComponentProps["actionData"];
795741 params: Route.ComponentProps["params"];
796742 }) {
797− const { member, project, settings, builds, live, commit, open, agentsLive, columns, needs, landed, groups, health, knows, checklist, branches, library, checks } =
743+ const { member, project, settings, builds, live, commit, open, agentsLive, columns, needs, landed, groups, health, knows, branches, library, checks } =
798744 loaderData;
799745 const base = `/${params.owner}/${params.repo}`;
800746 // What it is to its remotes, from the repository layout.
804750 const productionUrl = production ? (settings?.primaryDomain ? `https://${settings.primaryDomain}` : production.url) : null;
805751 const previews = live.filter((app) => app.kind === "preview");
806752 const latestProduction = builds.find((build) => build.kind === "production") ?? null;
807− const plan = project ? checklistPlan(project).plan : "production";
753+ const plan = project ? projectPlan(project) : "production";
808754 const canChange = member && loaderData.can.manage_settings;
809755 // Its homepage and docs beside what it is, unless the card below already links there.
810756 const headUrl =
836782
837783 // A mirror that is not taken over is a read-only copy (lib/mirror.ts
838784 // `offers`): what it is, where it comes from and what it holds, and
839− // nothing to start. Where it runs, the checklist, Right now and the
840− // agents' panels come back with a takeover.
785+ // nothing to start. Where it runs, Right now and the agents' panels
786+ // come back with a takeover.
841787 const readOnlyMirror = mirrorRepo?.mirror && !writable(mirrorRepo.mirror) ? mirrorRepo.mirror : null;
842788 if (readOnlyMirror) {
843789 return (
10781024 </p>
10791025 )}
10801026 </section>
1081−
1082− {checklist && <ProductionChecklist base={base} items={checklist} title={loaderData.checklistTitle} />}
10831027
10841028 <Panel
10851029 title="Right now"
15731517 * The overview of a mirror g1t does not lead (standing by, in CI failover
15741518 * or handing back): the remote it copies and when it last did, the facts
15751519 * of the copy, its latest commits, and About. Nothing here starts
1576− * anything: there is no place it runs, no checklist and no Right now, since
1520+ * anything: there is no place it runs and no Right now, since
15771521 * work happens on the remote until someone takes over.
15781522 */
15791523 function MirrorOverview({
+9−25
55
66 import type { Route } from "./+types/overview";
77 import { DeployLink, host, StatusDot } from "../../components/deploy";
8−import { ButtonLink, CopyLine, TimeAgo } from "../../components/ui";
8+import { ButtonLink, EmptyState, TimeAgo } from "../../components/ui";
99 import { Avatar } from "../../components/ui/avatar";
1010 import { Badge } from "../../components/ui/badge";
1111 import { Card } from "../../components/ui/card";
1212 import { UsageCard } from "../../components/usage-card";
1313 import { PullIcon } from "../../components/work-icons";
1414 import { openedBy } from "../../lib/opened-by";
15−import { cloneUrl, useAddresses } from "../../lib/addresses";
1615 import { kindLabel, libraryPackages, packageLine, packagePath, primaryLink } from "../../lib/project-kind";
1716 import { PinButton } from "../../components/pin-button";
1817 import { sortProjects } from "../../lib/project-list";
118117 }
119118
120119 /** What a member sees in a workspace with no projects yet. */
121−function GetStarted({ slug }: { slug: string }) {
122− const addresses = useAddresses();
120+function NoProjects({ slug }: { slug: string }) {
123121 return (
124− <div className="rounded-2xl border border-line bg-surface p-8">
125− <span className="flex size-10 items-center justify-center rounded-xl bg-accent/10 text-accent ring-1 ring-accent/30">
126− <Box size={18} />
127− </span>
128− <h2 className="mt-4 text-lg font-semibold tracking-tight">Create your first project</h2>
129− <p className="mt-1.5 max-w-xl text-sm text-muted">
130− A project is what you build and run: its code, the issues that say what should change, the agents and
131− people who change it, and where it is deployed.
132− </p>
133− <div className="mt-6 flex flex-wrap gap-3">
134− <ButtonLink to={`/new?workspace=${slug}`} variant="accent">
135− <Plus size={14} />
136− New project
137− </ButtonLink>
138− </div>
139− <p className="mt-6 text-sm text-muted">Or push code you already have, and it becomes a project:</p>
140− <div className="mt-2 max-w-xl">
141− <CopyLine prompt text={`git push ${cloneUrl(addresses, `${slug}/my-project`)} main`} />
142− </div>
143− </div>
122+ <EmptyState title="No projects yet">
123+ <ButtonLink to={`/new?workspace=${slug}`} variant="accent" className="mt-3">
124+ <Plus size={14} />
125+ New project
126+ </ButtonLink>
127+ </EmptyState>
144128 );
145129 }
146130
284268 <div className="grid gap-10 lg:grid-cols-[1fr_18rem]">
285269 <div className="min-w-0 space-y-10">
286270 {total === 0 && role ? (
287− <GetStarted slug={slug} />
271+ <NoProjects slug={slug} />
288272 ) : (
289273 <>
290274 <div className="grid grid-cols-2 gap-3 sm:grid-cols-4">