Get started on mission control
A checklist above everything else until a new workspace has handed off its first outcome: create a workspace, add a repository, add agent credit, hand off an outcome. Each step ticks itself off and links to where it is done.
1 file+90−10/1 viewed
| 1 | 1 | import { ArrowRight, Hand, Plus } from "lucide-react"; | |
| 2 | 2 | import { type ReactNode, useEffect } from "react"; | |
| 3 | − | import { Link, data, useRevalidator } from "react-router"; | |
| 3 | + | import { Link, data, useRevalidator, useRouteLoaderData } from "react-router"; | |
| 4 | 4 | ||
| 5 | 5 | import type { Lifecycle, Pull, Repo } from "@g1t/contracts"; | |
| 6 | 6 | ||
| 7 | 7 | import type { Route } from "./+types/home"; | |
| 8 | 8 | import { Landing } from "../components/landing"; | |
| 9 | + | import type { ShellData } from "../components/shell"; | |
| 9 | 10 | import { STAGE_LABEL, StageDots } from "../components/lifecycle"; | |
| 10 | 11 | import { RepoList } from "../components/repo-list"; | |
| 11 | 12 | import { | |
| 137 | 138 | return `${count} ${count === 1 ? one : many}`; | |
| 138 | 139 | } | |
| 139 | 140 | ||
| 141 | + | type Step = { done: boolean; title: string; about: string; to: string | null; action: string }; | |
| 142 | + | ||
| 143 | + | /** | |
| 144 | + | * The first things to do, ticked off as they are done, until an outcome has | |
| 145 | + | * been handed to agents. Shown above everything else on mission control. | |
| 146 | + | */ | |
| 147 | + | function GetStarted({ steps }: { steps: Step[] }) { | |
| 148 | + | const left = steps.filter((step) => !step.done).length; | |
| 149 | + | return ( | |
| 150 | + | <section className="rounded-2xl bg-surface p-5 ring-1 ring-line"> | |
| 151 | + | <div className="flex items-baseline justify-between gap-3"> | |
| 152 | + | <h2 className="font-semibold tracking-tight">Get started</h2> | |
| 153 | + | <span className="text-xs text-muted"> | |
| 154 | + | {steps.length - left} of {steps.length} done | |
| 155 | + | </span> | |
| 156 | + | </div> | |
| 157 | + | <ol className="mt-4 space-y-2"> | |
| 158 | + | {steps.map((step, index) => ( | |
| 159 | + | <li | |
| 160 | + | key={step.title} | |
| 161 | + | className={`flex items-start gap-3 rounded-xl px-3 py-2.5 ${step.done ? "" : "bg-bg/50 ring-1 ring-line"}`} | |
| 162 | + | > | |
| 163 | + | <span | |
| 164 | + | className={`mt-0.5 flex size-5 shrink-0 items-center justify-center rounded-full text-[0.6875rem] font-medium ${ | |
| 165 | + | step.done ? "bg-accent text-bg" : "text-muted ring-1 ring-line-strong" | |
| 166 | + | }`} | |
| 167 | + | > | |
| 168 | + | {step.done ? "✓" : index + 1} | |
| 169 | + | </span> | |
| 170 | + | <span className="min-w-0 grow"> | |
| 171 | + | <span className={`block text-sm font-medium ${step.done ? "text-muted line-through decoration-faint" : ""}`}> | |
| 172 | + | {step.title} | |
| 173 | + | </span> | |
| 174 | + | {!step.done && <span className="mt-0.5 block text-xs leading-5 text-muted">{step.about}</span>} | |
| 175 | + | </span> | |
| 176 | + | {!step.done && step.to && ( | |
| 177 | + | <Link | |
| 178 | + | to={step.to} | |
| 179 | + | className="shrink-0 rounded-md bg-fg px-2.5 py-1 text-xs font-medium text-bg transition-colors hover:bg-white" | |
| 180 | + | > | |
| 181 | + | {step.action} | |
| 182 | + | </Link> | |
| 183 | + | )} | |
| 184 | + | </li> | |
| 185 | + | ))} | |
| 186 | + | </ol> | |
| 187 | + | </section> | |
| 188 | + | ); | |
| 189 | + | } | |
| 190 | + | ||
| 140 | 191 | export default function Home({ loaderData }: Route.ComponentProps) { | |
| 141 | 192 | const { viewer, repos, active, assigned } = loaderData; | |
| 142 | 193 | ||
| 156 | 207 | return () => clearInterval(timer); | |
| 157 | 208 | }, [changing, revalidator]); | |
| 158 | 209 | ||
| 210 | + | const shell = useRouteLoaderData("root")?.shell as ShellData | null | undefined; | |
| 159 | 211 | if (!viewer) return <Landing repos={repos} />; | |
| 160 | 212 | ||
| 213 | + | const workspace = shell?.workspace?.slug; | |
| 214 | + | const first = repos[0]; | |
| 215 | + | const handedOff = active.length > 0 || (shell?.monthSpentMicros ?? 0) > 0; | |
| 216 | + | const steps: Step[] = [ | |
| 217 | + | { | |
| 218 | + | done: Boolean(workspace), | |
| 219 | + | title: "Create a workspace", | |
| 220 | + | about: "Repositories, people and agent credit live in one.", | |
| 221 | + | to: "/workspaces/new", | |
| 222 | + | action: "Create", | |
| 223 | + | }, | |
| 224 | + | { | |
| 225 | + | done: repos.length > 0, | |
| 226 | + | title: "Add a repository", | |
| 227 | + | about: "Create one, or import one from GitHub by its address. Push to it with git as usual.", | |
| 228 | + | to: workspace ? `/new?workspace=${workspace}` : "/new", | |
| 229 | + | action: "Add", | |
| 230 | + | }, | |
| 231 | + | { | |
| 232 | + | done: shell?.creditMicros == null || shell.creditMicros > 0, | |
| 233 | + | title: "Add agent credit", | |
| 234 | + | about: "g1t's agents are paid for from the workspace's credit, at what the model costs plus 20%.", | |
| 235 | + | to: workspace ? `/${workspace}/-/billing` : null, | |
| 236 | + | action: "Add credit", | |
| 237 | + | }, | |
| 238 | + | { | |
| 239 | + | done: handedOff, | |
| 240 | + | title: "Hand off an outcome", | |
| 241 | + | about: "Write what you want on a repository's Plan page. A planner splits it into issues, and agents take them and land them on main.", | |
| 242 | + | to: first ? `/${first.namespace}/${first.name}/plans` : null, | |
| 243 | + | action: "Write one", | |
| 244 | + | }, | |
| 245 | + | ]; | |
| 246 | + | const starting = steps.some((step) => !step.done); | |
| 247 | + | ||
| 161 | 248 | const summary = [ | |
| 162 | 249 | moving.length > 0 && `${moving.length} in progress`, | |
| 163 | 250 | ready.length > 0 && `${ready.length} ready to merge`, | |
| 183 | 270 | </div> | |
| 184 | 271 | </section> | |
| 185 | 272 | ||
| 273 | + | {starting && <GetStarted steps={steps} />} | |
| 274 | + | ||
| 186 | 275 | {needsYou.length > 0 && ( | |
| 187 | 276 | <Section | |
| 188 | 277 | title={ |