Merge branch 'worktree-agent-a2ed55708e8dc0828'
5 files+869−3160/5 viewed
| 1 | + | /** | |
| 2 | + | * The landing page's pictures of Home (routes/workspace/home.tsx), drawn | |
| 3 | + | * from the same parts and words as the real cards in components/home.tsx | |
| 4 | + | * and components/home-ask.tsx: What changed and Landed, for coming back to | |
| 5 | + | * what happened; While you were away, Where you're needed and Start here, | |
| 6 | + | * for what still needs you. They are pictures, so nothing in them takes | |
| 7 | + | * focus or a click (`inert`), and the page reads them as one image. | |
| 8 | + | */ | |
| 9 | + | import { ArrowRight, ArrowUp, CheckCheck, GitCommitHorizontal, GitMerge, GitPullRequest, RotateCcw, Rocket, Sparkles } from "lucide-react"; | |
| 10 | + | import type { ReactNode } from "react"; | |
| 11 | + | ||
| 12 | + | import { MarkGlyph } from "./logo"; | |
| 13 | + | import { Badge } from "./ui/badge"; | |
| 14 | + | import { Button } from "./ui/button"; | |
| 15 | + | import { Card } from "./ui/card"; | |
| 16 | + | ||
| 17 | + | /** One of Home's cards, as components/home.tsx draws it: a title, a few words under it, then its rows. */ | |
| 18 | + | function HomeCard({ title, hint, children }: { title: ReactNode; hint?: string; children: ReactNode }) { | |
| 19 | + | return ( | |
| 20 | + | <Card tone="bg" className="overflow-hidden rounded-2xl text-left shadow-sm"> | |
| 21 | + | <header className="px-4 pt-3.5 pb-2.5"> | |
| 22 | + | <p className="text-sm font-semibold text-fg">{title}</p> | |
| 23 | + | {hint && <p className="mt-0.5 text-xs text-muted">{hint}</p>} | |
| 24 | + | </header> | |
| 25 | + | {children} | |
| 26 | + | </Card> | |
| 27 | + | ); | |
| 28 | + | } | |
| 29 | + | ||
| 30 | + | const ROW = "flex items-center gap-3 border-t border-line px-4 py-2.5"; | |
| 31 | + | ||
| 32 | + | /** A picture, not a control: kept out of the tab order and read as one image. */ | |
| 33 | + | function Picture({ label, children }: { label: string; children: ReactNode }) { | |
| 34 | + | return ( | |
| 35 | + | <div role="img" aria-label={label} className="space-y-3"> | |
| 36 | + | <div inert className="space-y-3"> | |
| 37 | + | {children} | |
| 38 | + | </div> | |
| 39 | + | </div> | |
| 40 | + | ); | |
| 41 | + | } | |
| 42 | + | ||
| 43 | + | /** Coming back: the Ask box, What changed in plain English, and Landed with whether each change is live. */ | |
| 44 | + | export function WhatChangedArt() { | |
| 45 | + | return ( | |
| 46 | + | <Picture label="Home: What are we building today? over a box that asks g1t, What changed summarizing in plain English what landed, and Landed with whether each change is live"> | |
| 47 | + | <div className="text-center"> | |
| 48 | + | <p className="text-lg font-semibold tracking-tight text-fg">What are we building today?</p> | |
| 49 | + | <div className="mt-3 flex items-center gap-2 rounded-2xl border border-line bg-bg p-1.5 pl-3.5 text-left shadow-sm"> | |
| 50 | + | <MarkGlyph className="h-3.5 shrink-0" /> | |
| 51 | + | <span className="min-w-0 grow truncate text-[0.8125rem] text-faint">Ask g1t what changed, what's blocked, or what to do next</span> | |
| 52 | + | <Button type="button" variant="accent" size="icon-xs" tabIndex={-1} aria-hidden="true"> | |
| 53 | + | <ArrowUp size={13} /> | |
| 54 | + | </Button> | |
| 55 | + | </div> | |
| 56 | + | <div className="mt-2.5 flex flex-wrap justify-center gap-1.5"> | |
| 57 | + | <Badge tone="accent" size="md" className="whitespace-normal"> | |
| 58 | + | Summarize what changed since I was last here | |
| 59 | + | </Badge> | |
| 60 | + | <Badge size="md" className="hidden sm:inline-flex"> | |
| 61 | + | What needs me first? | |
| 62 | + | </Badge> | |
| 63 | + | </div> | |
| 64 | + | </div> | |
| 65 | + | ||
| 66 | + | <HomeCard title="What changed" hint="In plain English, since I was last here"> | |
| 67 | + | <div className="space-y-2 px-4 pb-3.5 text-[0.8125rem] leading-relaxed text-fg-soft"> | |
| 68 | + | <p> | |
| 69 | + | Two changes landed in <span className="font-medium text-fg">acme/web</span>. CSV exports now stream in batches of 1,000 rows, so the | |
| 70 | + | largest accounts no longer time out; it went live 9 minutes after the merge queue merged it. Jordan pinned the build image's cache key, which fixed last night's failed deploy. | |
| 71 | + | </p> | |
| 72 | + | <div className="flex flex-wrap items-center gap-1.5 border-t border-line pt-2.5"> | |
| 73 | + | <Button type="button" variant="soft" size="xs" tabIndex={-1}> | |
| 74 | + | <MarkGlyph className="h-2.5" /> | |
| 75 | + | Ask a follow-up | |
| 76 | + | </Button> | |
| 77 | + | <Button type="button" variant="ghost" size="xs" tabIndex={-1}> | |
| 78 | + | <RotateCcw size={12} /> | |
| 79 | + | Summarize again | |
| 80 | + | </Button> | |
| 81 | + | </div> | |
| 82 | + | </div> | |
| 83 | + | </HomeCard> | |
| 84 | + | ||
| 85 | + | <HomeCard title="Landed" hint="1 pull request merged · 1 push"> | |
| 86 | + | {[ | |
| 87 | + | { title: "Stream CSV exports in batches", detail: "web#431 · by priya", push: false, ship: <Badge tone="success"><Rocket size={10} aria-hidden="true" />Live 9 min after merge</Badge> }, | |
| 88 | + | { title: "Pin the build image's cache key", detail: "web · 1 push by jordan", push: true, ship: <Badge tone="success"><Rocket size={10} aria-hidden="true" />Live 4 min after push</Badge> }, | |
| 89 | + | ].map((row) => ( | |
| 90 | + | <div key={row.title} className={ROW}> | |
| 91 | + | {row.push ? <GitCommitHorizontal size={15} className="shrink-0 text-muted" aria-hidden="true" /> : <GitMerge size={15} className="shrink-0 text-merged" aria-hidden="true" />} | |
| 92 | + | <div className="min-w-0 grow"> | |
| 93 | + | <p className="truncate text-sm text-fg">{row.title}</p> | |
| 94 | + | <div className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1"> | |
| 95 | + | <span className="truncate text-xs text-muted">{row.detail}</span> | |
| 96 | + | {row.ship} | |
| 97 | + | </div> | |
| 98 | + | </div> | |
| 99 | + | <span className="flex size-7 shrink-0 items-center justify-center rounded-md text-accent"> | |
| 100 | + | <Sparkles size={13} aria-hidden="true" /> | |
| 101 | + | </span> | |
| 102 | + | </div> | |
| 103 | + | ))} | |
| 104 | + | </HomeCard> | |
| 105 | + | </Picture> | |
| 106 | + | ); | |
| 107 | + | } | |
| 108 | + | ||
| 109 | + | /** What still needs you: what settled by itself, Where you're needed, and Start here. */ | |
| 110 | + | export function NeedsYouArt() { | |
| 111 | + | return ( | |
| 112 | + | <Picture label="Home: While you were away lists notifications that settled themselves and why, Where you're needed lists a pull request ready to merge and a review asked of you, and Start here opens the one to do first"> | |
| 113 | + | <HomeCard title="While you were away" hint="What finished by itself, and what is live"> | |
| 114 | + | <div className="flex flex-wrap gap-1.5 px-4 pb-3"> | |
| 115 | + | <Badge tone="success" size="md">3 notifications settled</Badge> | |
| 116 | + | <Badge tone="warn" size="md">2 things still need you</Badge> | |
| 117 | + | </div> | |
| 118 | + | {[ | |
| 119 | + | { title: "CI failed on main", why: "CI passed in run 330" }, | |
| 120 | + | { title: "Review requested: Cache the dependency graph", why: "Merged by @ana" }, | |
| 121 | + | ].map((row) => ( | |
| 122 | + | <div key={row.title} className={ROW}> | |
| 123 | + | <CheckCheck size={15} className="shrink-0 text-success" aria-hidden="true" /> | |
| 124 | + | <div className="min-w-0 grow"> | |
| 125 | + | <p className="truncate text-sm text-fg">{row.title}</p> | |
| 126 | + | <p className="truncate text-xs text-fg-soft">{row.why}</p> | |
| 127 | + | </div> | |
| 128 | + | </div> | |
| 129 | + | ))} | |
| 130 | + | </HomeCard> | |
| 131 | + | ||
| 132 | + | <HomeCard title="Where you're needed"> | |
| 133 | + | {[ | |
| 134 | + | { icon: <GitMerge size={15} className="shrink-0 text-success" aria-hidden="true" />, title: "Retry webhook deliveries with backoff", detail: "acme/web #433 · Ready to land when you merge it.", stake: "Ready to merge", action: "Open" }, | |
| 135 | + | { icon: <GitPullRequest size={15} className="shrink-0 text-info" aria-hidden="true" />, title: "Add audit log export", detail: "acme/web #435 · Your review", stake: "+212 −40", action: "Review" }, | |
| 136 | + | ].map((row) => ( | |
| 137 | + | <div key={row.title} className={ROW}> | |
| 138 | + | {row.icon} | |
| 139 | + | <div className="min-w-0 grow"> | |
| 140 | + | <p className="truncate text-sm font-semibold text-fg">{row.title}</p> | |
| 141 | + | <p className="truncate text-xs text-muted">{row.detail}</p> | |
| 142 | + | </div> | |
| 143 | + | <span className="hidden shrink-0 text-xs font-medium text-fg-soft sm:inline">{row.stake}</span> | |
| 144 | + | <Button type="button" variant="outline" size="xs" tabIndex={-1} className="border-line-strong px-2.5 text-fg"> | |
| 145 | + | {row.action} | |
| 146 | + | </Button> | |
| 147 | + | </div> | |
| 148 | + | ))} | |
| 149 | + | </HomeCard> | |
| 150 | + | ||
| 151 | + | <HomeCard | |
| 152 | + | title={ | |
| 153 | + | <> | |
| 154 | + | Start here <span className="font-normal text-faint">· picked by g1t</span> | |
| 155 | + | </> | |
| 156 | + | } | |
| 157 | + | > | |
| 158 | + | <div className="px-4 pb-4"> | |
| 159 | + | <p className="text-sm font-semibold text-fg">Retry webhook deliveries with backoff</p> | |
| 160 | + | <p className="mt-2.5 rounded-lg bg-raised/60 px-3 py-2 text-xs leading-relaxed text-fg-soft"> | |
| 161 | + | <span className="font-medium text-fg">Why this one: </span> | |
| 162 | + | Everything it needs is met; it lands when you merge it. | |
| 163 | + | </p> | |
| 164 | + | <Button type="button" variant="accent" size="sm" tabIndex={-1} className="mt-3"> | |
| 165 | + | Open pull request | |
| 166 | + | <ArrowRight size={13} /> | |
| 167 | + | </Button> | |
| 168 | + | </div> | |
| 169 | + | </HomeCard> | |
| 170 | + | </Picture> | |
| 171 | + | ); | |
| 172 | + | } |
| 21 | 21 | import { Link } from "react-router"; | |
| 22 | 22 | ||
| 23 | 23 | import { DeployArt, Live, QueueArt, WhyArt } from "./art"; | |
| 24 | + | import { NeedsYouArt, WhatChangedArt } from "./landing-home"; | |
| 24 | 25 | import { ProductTour } from "./product-tour"; | |
| 25 | 26 | import { ButtonLink, CopyLine } from "./ui"; | |
| 26 | 27 | ||
| ⋯ | |||
| 241 | 242 | about: | |
| 242 | 243 | "Channels, direct messages and threads, live, for everyone in the company.", | |
| 243 | 244 | to: `${DOCS}/guides/chat/`, | |
| 244 | − | }, | |
| 245 | − | { | |
| 246 | − | icon: <BookOpen size={18} />, | |
| 247 | − | name: "Artifacts", | |
| 248 | − | about: | |
| 249 | − | "Docs now, and slides, designs and dashboards next, made together live.", | |
| 250 | − | to: `${DOCS}/guides/artifacts/`, | |
| 251 | 245 | }, | |
| 252 | 246 | { | |
| 253 | 247 | icon: <Bot size={18} />, | |
| ⋯ | |||
| 256 | 250 | "Claude Code, Codex, Cursor or any MCP client, working as you through MCP, the g1t CLI or the API, with only the access you give them.", | |
| 257 | 251 | to: `${DOCS}/guides/bring-your-own-agent/`, | |
| 258 | 252 | }, | |
| 253 | + | { | |
| 254 | + | icon: <BookOpen size={18} />, | |
| 255 | + | name: "Artifacts", | |
| 256 | + | about: | |
| 257 | + | "Docs now, and slides, designs and dashboards next, made together live.", | |
| 258 | + | to: `${DOCS}/guides/artifacts/`, | |
| 259 | + | }, | |
| 259 | 260 | ]; | |
| 260 | 261 | ||
| 261 | 262 | const RAILS: { icon: ReactNode; title: string; about: string; to: string }[] = [ | |
| ⋯ | |||
| 403 | 404 | <div className="relative mx-auto max-w-6xl px-4 pt-20 text-center sm:pt-24"> | |
| 404 | 405 | <Link | |
| 405 | 406 | to={`${DOCS}/guides/bring-your-own-agent/`} | |
| 406 | − | className="inline-flex animate-fade-up items-center gap-2 rounded-full bg-bg/50 px-3 py-1 text-xs text-fg-soft ring-1 ring-line backdrop-blur transition-colors hover:bg-bg/70" | |
| 407 | + | className="inline-flex animate-fade-up items-center gap-2 rounded-full bg-bg/50 px-3 py-1 text-xs whitespace-nowrap text-fg-soft ring-1 ring-line backdrop-blur transition-colors hover:bg-bg/70" | |
| 407 | 408 | > | |
| 408 | 409 | <span className="size-1.5 shrink-0 rounded-full bg-accent" /> | |
| 409 | − | Works with Claude Code, Codex, Cursor and any MCP client | |
| 410 | + | <span className="sm:hidden">Works with Claude Code, Codex and Cursor</span> | |
| 411 | + | <span className="hidden sm:inline">Works with Claude Code, Codex, Cursor and any MCP client</span> | |
| 410 | 412 | <ArrowRight size={12} className="shrink-0" /> | |
| 411 | 413 | </Link> | |
| 412 | 414 | <h1 className="mx-auto mt-7 max-w-4xl animate-fade-up text-[2.75rem] leading-[1.04] font-semibold tracking-tight text-balance sm:text-7xl"> | |
| ⋯ | |||
| 607 | 609 | a thread. Messages arrive the moment they are sent, and everyone in | |
| 608 | 610 | the company can join for free. | |
| 609 | 611 | </p> | |
| 612 | + | <More to={`${DOCS}/guides/chat/`}>How Chat works</More> | |
| 613 | + | </div> | |
| 614 | + | <div className="rounded-3xl bg-surface p-7 ring-1 ring-line"> | |
| 615 | + | <p className="text-sm font-medium">Free on every plan</p> | |
| 610 | 616 | <Points | |
| 617 | + | columns={false} | |
| 611 | 618 | points={[ | |
| 612 | − | "Public and private channels", | |
| 619 | + | "Private, workspace and public channels", | |
| 613 | 620 | "Direct messages", | |
| 614 | 621 | "Threads on any message", | |
| 615 | 622 | "@mentions", | |
| 616 | 623 | "Live delivery, typing and read state", | |
| 617 | 624 | "A desktop app for Windows, macOS and Linux", | |
| 618 | 625 | { | |
| 626 | + | text: "An Android app with notifications: scan the QR code on", | |
| 627 | + | link: { to: "/download", label: "Download" }, | |
| 628 | + | }, | |
| 629 | + | { | |
| 619 | 630 | text: "Pull requests, checks and deploys as cards in the channel", | |
| 620 | 631 | soon: true, | |
| 621 | 632 | }, | |
| 622 | 633 | { text: "Chat mentions in Notifications", soon: true }, | |
| 623 | − | { | |
| 624 | − | text: "An Android app with notifications: scan the QR code on", | |
| 625 | − | link: { to: "/download", label: "Download" }, | |
| 626 | − | }, | |
| 627 | 634 | { text: "The phone app on iPhone", soon: true }, | |
| 628 | 635 | ]} | |
| 629 | 636 | /> | |
| 630 | − | <More to={`${DOCS}/guides/chat/`}>How Chat works</More> | |
| 631 | 637 | </div> | |
| 632 | − | <div className="rounded-3xl bg-surface p-7 ring-1 ring-line"> | |
| 633 | − | <Eyebrow>Issues</Eyebrow> | |
| 634 | − | <h3 className="mt-3 text-xl font-semibold tracking-tight"> | |
| 635 | − | Point your coding agent at an issue | |
| 636 | − | </h3> | |
| 637 | − | <p className="mt-3 text-sm leading-6 text-muted"> | |
| 638 | − | Your coding agent connects to g1t through MCP and works as you. It | |
| 639 | − | reads the issue, pushes a branch, opens the pull request and | |
| 640 | − | records its session onto it, and the pull request goes through the | |
| 641 | − | same checks and reviews as anyone's. | |
| 638 | + | </section> | |
| 639 | + | ||
| 640 | + | {/* Agents: they do the work, you make the calls. */} | |
| 641 | + | <section id="agents" className="mx-auto max-w-6xl scroll-mt-20 px-4 pt-24"> | |
| 642 | + | <div className="max-w-3xl"> | |
| 643 | + | <Eyebrow>Agents</Eyebrow> | |
| 644 | + | <h2 className="mt-3 text-3xl font-semibold tracking-tight text-balance sm:text-5xl"> | |
| 645 | + | Agents do the work. You make the calls. | |
| 646 | + | </h2> | |
| 647 | + | <p className="mt-5 max-w-2xl text-lg leading-8 text-muted"> | |
| 648 | + | Your coding agents push the branches and open the pull requests. | |
| 649 | + | g1t keeps track of all of it, clears what resolved itself, and asks | |
| 650 | + | you only for what is yours to decide: a merge, a review, a fix. | |
| 642 | 651 | </p> | |
| 643 | − | <ol className="mt-6 space-y-3 text-sm"> | |
| 644 | − | {[ | |
| 645 | − | "Open an issue: what should change, and what done looks like", | |
| 646 | − | "Work it yourself, or hand it to your coding agent", | |
| 647 | − | "The pull request runs your checks and lands through the queue", | |
| 648 | − | ].map((step, index) => ( | |
| 649 | − | <li key={step} className="flex items-start gap-3 text-fg-soft"> | |
| 650 | − | <span className="flex size-6 shrink-0 items-center justify-center rounded-full bg-bg font-mono text-xs text-faint ring-1 ring-line"> | |
| 651 | − | {index + 1} | |
| 652 | − | </span> | |
| 653 | − | {step} | |
| 654 | − | </li> | |
| 655 | − | ))} | |
| 656 | − | </ol> | |
| 657 | − | <More to={`${DOCS}/guides/bring-your-own-agent/`}> | |
| 658 | − | Connect a coding agent | |
| 659 | − | </More> | |
| 660 | 652 | </div> | |
| 653 | + | ||
| 654 | + | <div className="mt-6 divide-y divide-line"> | |
| 655 | + | <Pillar | |
| 656 | + | eyebrow="Where you're needed" | |
| 657 | + | title="Only what still needs you" | |
| 658 | + | art={<NeedsYouArt />} | |
| 659 | + | points={[ | |
| 660 | + | "Notifications that settle themselves", | |
| 661 | + | "Why each one settled, and who did it", | |
| 662 | + | "Pull requests ready for you to merge", | |
| 663 | + | "Reviews asked of you, by name", | |
| 664 | + | "Start here: the one to do first, and why", | |
| 665 | + | ]} | |
| 666 | + | more={["Home", `${DOCS}/guides/home/`]} | |
| 667 | + | > | |
| 668 | + | A notification is done once a later event resolves it: the next run | |
| 669 | + | passes, the pull request merges, the fix deploys. g1t settles it | |
| 670 | + | for you, and Home shows only what still waits on you. Start here | |
| 671 | + | picks the one to do first and opens it. | |
| 672 | + | </Pillar> | |
| 673 | + | ||
| 674 | + | <Pillar | |
| 675 | + | flip | |
| 676 | + | eyebrow="What changed" | |
| 677 | + | title="Come back to what changed, in plain English" | |
| 678 | + | art={<WhatChangedArt />} | |
| 679 | + | points={[ | |
| 680 | + | "A summary of what landed since you were last here", | |
| 681 | + | "Whether each change is live, deploying or failed", | |
| 682 | + | "Explain on any change that landed", | |
| 683 | + | "Ask a follow-up, in your direct message with g1t", | |
| 684 | + | ]} | |
| 685 | + | more={["Ask g1t", `${DOCS}/guides/ask/`]} | |
| 686 | + | > | |
| 687 | + | Home asks “What are we building today?” Press | |
| 688 | + | Summarize and What changed reads the commits, pull requests, deploys | |
| 689 | + | and conversations since you were last here, then tells you what | |
| 690 | + | changed, why, and what is live. Explain any one change, or ask a | |
| 691 | + | follow-up. It runs on your workspace's own AI provider. | |
| 692 | + | </Pillar> | |
| 693 | + | </div> | |
| 694 | + | ||
| 695 | + | <div className="mt-10 grid gap-4 lg:grid-cols-2"> | |
| 696 | + | <div className="rounded-3xl bg-surface p-7 ring-1 ring-line sm:p-8"> | |
| 697 | + | <Eyebrow>Issues</Eyebrow> | |
| 698 | + | <h3 className="mt-3 text-2xl font-semibold tracking-tight text-balance"> | |
| 699 | + | Point your coding agent at an issue | |
| 700 | + | </h3> | |
| 701 | + | <p className="mt-3 text-sm leading-6 text-muted"> | |
| 702 | + | Your coding agent connects to g1t through MCP and works as you. It | |
| 703 | + | reads the issue, pushes a branch, opens the pull request and | |
| 704 | + | records its session onto it, and the pull request goes through | |
| 705 | + | the same checks and reviews as anyone's. | |
| 706 | + | </p> | |
| 707 | + | <ol className="mt-6 space-y-3 text-sm"> | |
| 708 | + | {[ | |
| 709 | + | "Open an issue: what should change, and what done looks like", | |
| 710 | + | "Work it yourself, or hand it to your coding agent", | |
| 711 | + | "The pull request runs your checks and lands through the queue", | |
| 712 | + | ].map((step, index) => ( | |
| 713 | + | <li key={step} className="flex items-start gap-3 text-fg-soft"> | |
| 714 | + | <span className="flex size-6 shrink-0 items-center justify-center rounded-full bg-bg font-mono text-xs text-faint ring-1 ring-line"> | |
| 715 | + | {index + 1} | |
| 716 | + | </span> | |
| 717 | + | {step} | |
| 718 | + | </li> | |
| 719 | + | ))} | |
| 720 | + | </ol> | |
| 721 | + | <More to={`${DOCS}/guides/bring-your-own-agent/`}> | |
| 722 | + | Connect a coding agent | |
| 723 | + | </More> | |
| 724 | + | </div> | |
| 725 | + | <div className="rounded-3xl bg-surface p-7 ring-1 ring-line sm:p-8"> | |
| 726 | + | <Eyebrow>Coordination</Eyebrow> | |
| 727 | + | <h3 className="mt-3 text-2xl font-semibold tracking-tight text-balance"> | |
| 728 | + | Many agents, no collisions | |
| 729 | + | </h3> | |
| 730 | + | <p className="mt-3 text-sm leading-6 text-muted"> | |
| 731 | + | Run as many coding agents as you like. Each change is a pull | |
| 732 | + | request, pull requests that touch the same files are flagged to | |
| 733 | + | each other, conflicts are found on every push, and the merge queue | |
| 734 | + | tests changes together with what lands ahead of them. Five agents | |
| 735 | + | can open pull requests the same afternoon and main still only | |
| 736 | + | moves to what passed. | |
| 737 | + | </p> | |
| 738 | + | <More to={`${DOCS}/guides/merge-queue/`}>The merge queue</More> | |
| 739 | + | </div> | |
| 740 | + | </div> | |
| 741 | + | </section> | |
| 742 | + | ||
| 743 | + | {/* Connect */} | |
| 744 | + | <section className="mt-24 border-y border-line bg-surface/40"> | |
| 745 | + | <div className="mx-auto grid max-w-6xl items-center gap-12 px-4 py-24 lg:grid-cols-2"> | |
| 746 | + | <div> | |
| 747 | + | <Eyebrow>Bring your own agent</Eyebrow> | |
| 748 | + | <h2 className="mt-3 text-3xl font-semibold tracking-tight text-balance"> | |
| 749 | + | Any agent that speaks MCP works on g1t | |
| 750 | + | </h2> | |
| 751 | + | <p className="mt-4 max-w-md leading-7 text-muted"> | |
| 752 | + | Connect Claude Code, Codex, Cursor or any other MCP client and | |
| 753 | + | sign in once. It works as you: it reads issues, pushes branches, | |
| 754 | + | opens pull requests, and records its session and its decisions | |
| 755 | + | onto them. | |
| 756 | + | </p> | |
| 757 | + | <p className="mt-4 max-w-md leading-7 text-muted"> | |
| 758 | + | Every action is a REST route and an MCP tool, with an OpenAPI | |
| 759 | + | document, OAuth apps, access tokens and signed webhooks for every | |
| 760 | + | event. The g1t CLI does the same from a terminal or a script: | |
| 761 | + | open a pull request, wait on its checks, read a failed run's | |
| 762 | + | logs. | |
| 763 | + | </p> | |
| 764 | + | <div className="mt-6 flex flex-wrap gap-3"> | |
| 765 | + | <ButtonLink | |
| 766 | + | to={`${DOCS}/guides/bring-your-own-agent/`} | |
| 767 | + | variant="outline" | |
| 768 | + | > | |
| 769 | + | Connect an agent | |
| 770 | + | <ArrowRight size={14} /> | |
| 771 | + | </ButtonLink> | |
| 772 | + | <ButtonLink to={`${DOCS}/guides/cli/`} variant="outline"> | |
| 773 | + | The g1t CLI | |
| 774 | + | </ButtonLink> | |
| 775 | + | <ButtonLink to={`${DOCS}/reference/api/`} variant="outline"> | |
| 776 | + | API reference | |
| 777 | + | </ButtonLink> | |
| 778 | + | </div> | |
| 779 | + | </div> | |
| 780 | + | <div className="min-w-0 space-y-3"> | |
| 781 | + | <p className="text-sm text-muted"> | |
| 782 | + | Add g1t as a remote MCP server, then sign in through your browser: | |
| 783 | + | </p> | |
| 784 | + | <CopyLine text="https://mcp.g1t.sh" /> | |
| 785 | + | <p className="pt-3 text-sm text-muted"> | |
| 786 | + | Install the CLI, then sign in with{" "} | |
| 787 | + | <code className="font-mono text-fg-soft">g1t auth login</code>: | |
| 788 | + | </p> | |
| 789 | + | <CopyLine prompt text="curl -fsSL https://g1t.sh/install | sh" /> | |
| 790 | + | <p className="pt-3 text-sm text-muted"> | |
| 791 | + | Underneath, it is plain git and HTTP: | |
| 792 | + | </p> | |
| 793 | + | <CopyLine | |
| 794 | + | prompt | |
| 795 | + | breakAtSlashes | |
| 796 | + | text="git clone https://g1t.sh/flagon-io/g1t.git" | |
| 797 | + | /> | |
| 798 | + | <CopyLine | |
| 799 | + | prompt | |
| 800 | + | breakAtSlashes | |
| 801 | + | text="curl https://api.g1t.sh/repos/flagon-io/g1t/queue" | |
| 802 | + | /> | |
| 803 | + | </div> | |
| 804 | + | </div> | |
| 805 | + | </section> | |
| 806 | + | ||
| 807 | + | {/* Artifacts: the docs the work cites. */} | |
| 808 | + | <section className="mx-auto max-w-6xl px-4 pt-24"> | |
| 809 | + | <div className="grid gap-10 rounded-3xl bg-surface p-8 ring-1 ring-line sm:p-10 lg:grid-cols-[1fr_1.1fr] lg:gap-16"> | |
| 810 | + | <div> | |
| 811 | + | <Eyebrow>Artifacts</Eyebrow> | |
| 812 | + | <h2 className="mt-3 text-3xl font-semibold tracking-tight text-balance"> | |
| 813 | + | A knowledge base that keeps itself true | |
| 814 | + | </h2> | |
| 815 | + | <p className="mt-4 max-w-xl leading-7 text-muted"> | |
| 816 | + | Docs, private until you share them or kept in spaces, edited | |
| 817 | + | together live, with history, comments and backlinks. When a merged | |
| 818 | + | change touches something a doc cites, the doc is flagged for its | |
| 819 | + | owner to update. | |
| 820 | + | </p> | |
| 821 | + | <More to={`${DOCS}/guides/artifacts/`}>How Artifacts work</More> | |
| 822 | + | </div> | |
| 823 | + | <div> | |
| 824 | + | <Points | |
| 825 | + | columns={false} | |
| 826 | + | points={[ | |
| 827 | + | "Docs edited together live", | |
| 828 | + | "History, comments and backlinks", | |
| 829 | + | "Flagged when a merged change touches what they cite", | |
| 830 | + | { text: "Slides, designs and dashboards", soon: true }, | |
| 831 | + | ]} | |
| 832 | + | /> | |
| 833 | + | </div> | |
| 834 | + | </div> | |
| 661 | 835 | </section> | |
| 662 | 836 | ||
| 663 | 837 | {/* The rails. */} | |
| ⋯ | |||
| 685 | 859 | <p className="mt-2 text-sm leading-6 text-muted">{item.about}</p> | |
| 686 | 860 | </Link> | |
| 687 | 861 | ))} | |
| 688 | − | </div> | |
| 689 | − | </section> | |
| 690 | − | ||
| 691 | − | {/* Switching: what moving in costs, and what holds if g1t goes away. */} | |
| 692 | − | <section | |
| 693 | − | id="switching" | |
| 694 | − | className="mx-auto max-w-6xl scroll-mt-20 px-4 pb-24" | |
| 695 | − | > | |
| 696 | − | <div className="grid gap-10 rounded-3xl bg-surface p-8 ring-1 ring-line sm:p-10 lg:grid-cols-[1fr_1.1fr] lg:gap-16"> | |
| 697 | − | <div> | |
| 698 | − | <Eyebrow>Switching</Eyebrow> | |
| 699 | − | <h2 className="mt-3 text-3xl font-semibold tracking-tight text-balance"> | |
| 700 | − | Moving in is an import. Leaving is a git clone. | |
| 701 | − | </h2> | |
| 702 | − | <p className="mt-4 max-w-xl leading-7 text-muted"> | |
| 703 | − | Bring repositories across from GitHub in a few clicks: every | |
| 704 | − | branch and tag with full history, and their issues if you want | |
| 705 | − | them. The copy on g1t is then your own. | |
| 706 | − | </p> | |
| 707 | − | <p className="mt-4 max-w-xl leading-7 text-muted"> | |
| 708 | − | If g1t went away tomorrow, your code is plain git, the source is | |
| 709 | − | MIT licensed, and the core forge runs on your own machine with | |
| 710 | − | Docker Compose. | |
| 711 | − | </p> | |
| 712 | − | <More | |
| 713 | − | to={`${DOCS}/guides/github/#import-a-repository`} | |
| 714 | − | > | |
| 715 | − | Import from GitHub | |
| 716 | − | </More> | |
| 717 | − | </div> | |
| 718 | − | <div className="space-y-6"> | |
| 719 | − | <div> | |
| 720 | − | <p className="text-sm font-medium">What keeps working</p> | |
| 721 | − | <Points | |
| 722 | − | columns={false} | |
| 723 | − | points={[ | |
| 724 | − | "git over HTTPS, with your history as it is", | |
| 725 | − | "GitHub Actions workflows: rename .github to .g1t and push", | |
| 726 | − | "The coding agents you use today, through MCP", | |
| 727 | − | "Leaving: git clone, and the REST API for the rest", | |
| 728 | − | ]} | |
| 729 | − | /> | |
| 730 | − | </div> | |
| 731 | − | <div className="rounded-xl border border-dashed border-line-strong px-4 py-3.5 text-sm leading-6 text-muted"> | |
| 732 | − | <p className="font-medium text-fg-soft"> | |
| 733 | − | Not a fit yet if you need | |
| 734 | − | </p> | |
| 735 | − | <p className="mt-1.5"> | |
| 736 | − | Single sign-on, git over SSH, or chat and deployments on your | |
| 737 | − | own machines: those run only on g1t.sh today. Open pull | |
| 738 | − | requests, issue comments, releases and wikis stay behind on | |
| 739 | − | import.{" "} | |
| 740 | − | <Link | |
| 741 | − | to={`${DOCS}/about/limitations/`} | |
| 742 | − | className="text-fg-soft underline-offset-4 hover:text-accent hover:underline" | |
| 743 | − | > | |
| 744 | − | What g1t can't do yet | |
| 745 | − | </Link> | |
| 746 | − | </p> | |
| 747 | − | </div> | |
| 748 | − | </div> | |
| 749 | − | </div> | |
| 750 | − | </section> | |
| 751 | − | ||
| 752 | − | {/* Coordination, as it works today; and Docs, coming next. */} | |
| 753 | − | <section className="mx-auto grid max-w-6xl gap-4 px-4 pb-24 lg:grid-cols-2"> | |
| 754 | − | <div className="rounded-3xl bg-surface p-8 ring-1 ring-line"> | |
| 755 | − | <Eyebrow>Coordination</Eyebrow> | |
| 756 | − | <h3 className="mt-3 text-2xl font-semibold tracking-tight text-balance"> | |
| 757 | − | Many agents, no collisions | |
| 758 | − | </h3> | |
| 759 | − | <p className="mt-3 text-sm leading-6 text-muted"> | |
| 760 | − | Run as many coding agents as you like. Each change is a pull | |
| 761 | − | request, pull requests that touch the same files are flagged to | |
| 762 | − | each other, conflicts are found on every push, and the merge queue | |
| 763 | − | tests changes together with what lands ahead of them. Five agents | |
| 764 | − | can open pull requests the same afternoon and main still only | |
| 765 | − | moves to what passed. | |
| 766 | − | </p> | |
| 767 | − | <More to={`${DOCS}/guides/merge-queue/`}>The merge queue</More> | |
| 768 | − | </div> | |
| 769 | − | <div className="rounded-3xl bg-surface p-8 ring-1 ring-line"> | |
| 770 | − | <Eyebrow>Artifacts</Eyebrow> | |
| 771 | − | <h3 className="mt-3 text-2xl font-semibold tracking-tight text-balance"> | |
| 772 | − | A knowledge base that keeps itself true | |
| 773 | − | </h3> | |
| 774 | − | <p className="mt-3 text-sm leading-6 text-muted"> | |
| 775 | − | Docs, private until you share them or kept in spaces, edited | |
| 776 | − | together live, with history, comments and backlinks. When a merged | |
| 777 | − | change touches something a doc cites, the doc is flagged for its | |
| 778 | − | owner to update. | |
| 779 | − | </p> | |
| 780 | − | <More to={`${DOCS}/guides/artifacts/`}>How Artifacts work</More> | |
| 781 | 862 | </div> | |
| 782 | 863 | </section> | |
| 783 | 864 | ||
| ⋯ | |||
| 848 | 929 | </div> | |
| 849 | 930 | </section> | |
| 850 | 931 | ||
| 851 | − | {/* Connect */} | |
| 852 | − | <section className="border-y border-line bg-surface/40"> | |
| 853 | − | <div className="mx-auto grid max-w-6xl items-center gap-12 px-4 py-24 lg:grid-cols-2"> | |
| 932 | + | {/* Switching: what moving in costs, and what holds if g1t goes away. */} | |
| 933 | + | <section | |
| 934 | + | id="switching" | |
| 935 | + | className="mx-auto max-w-6xl scroll-mt-20 px-4 pb-24" | |
| 936 | + | > | |
| 937 | + | <div className="grid gap-10 rounded-3xl bg-surface p-8 ring-1 ring-line sm:p-10 lg:grid-cols-[1fr_1.1fr] lg:gap-16"> | |
| 854 | 938 | <div> | |
| 855 | − | <Eyebrow>Bring your own agent</Eyebrow> | |
| 939 | + | <Eyebrow>Switching</Eyebrow> | |
| 856 | 940 | <h2 className="mt-3 text-3xl font-semibold tracking-tight text-balance"> | |
| 857 | − | Any agent that speaks MCP works on g1t | |
| 941 | + | Moving in is an import. Leaving is a git clone. | |
| 858 | 942 | </h2> | |
| 859 | − | <p className="mt-4 max-w-md leading-7 text-muted"> | |
| 860 | − | Connect Claude Code, Codex, Cursor or any other MCP client and | |
| 861 | − | sign in once. It works as you: it reads issues, pushes branches, | |
| 862 | − | opens pull requests, and records its session and its decisions | |
| 863 | − | onto them. | |
| 943 | + | <p className="mt-4 max-w-xl leading-7 text-muted"> | |
| 944 | + | Bring repositories across from GitHub in a few clicks: every | |
| 945 | + | branch and tag with full history, and their issues if you want | |
| 946 | + | them. The copy on g1t is then your own. | |
| 864 | 947 | </p> | |
| 865 | − | <p className="mt-4 max-w-md leading-7 text-muted"> | |
| 866 | − | Every action is a REST route and an MCP tool, with an OpenAPI | |
| 867 | − | document, OAuth apps, access tokens and signed webhooks for every | |
| 868 | − | event. The g1t CLI does the same from a terminal or a script: | |
| 869 | − | open a pull request, wait on its checks, read a failed run's | |
| 870 | − | logs. | |
| 948 | + | <p className="mt-4 max-w-xl leading-7 text-muted"> | |
| 949 | + | If g1t went away tomorrow, your code is plain git, the source is | |
| 950 | + | MIT licensed, and the core forge runs on your own machine with | |
| 951 | + | Docker Compose. | |
| 871 | 952 | </p> | |
| 872 | − | <div className="mt-6 flex flex-wrap gap-3"> | |
| 873 | − | <ButtonLink | |
| 874 | − | to={`${DOCS}/guides/bring-your-own-agent/`} | |
| 875 | − | variant="outline" | |
| 876 | − | > | |
| 877 | − | Connect an agent | |
| 878 | − | <ArrowRight size={14} /> | |
| 879 | − | </ButtonLink> | |
| 880 | − | <ButtonLink to={`${DOCS}/guides/cli/`} variant="outline"> | |
| 881 | − | The g1t CLI | |
| 882 | − | </ButtonLink> | |
| 883 | − | <ButtonLink to={`${DOCS}/reference/api/`} variant="outline"> | |
| 884 | − | API reference | |
| 885 | − | </ButtonLink> | |
| 953 | + | <More | |
| 954 | + | to={`${DOCS}/guides/github/#import-a-repository`} | |
| 955 | + | > | |
| 956 | + | Import from GitHub | |
| 957 | + | </More> | |
| 958 | + | </div> | |
| 959 | + | <div className="space-y-6"> | |
| 960 | + | <div> | |
| 961 | + | <p className="text-sm font-medium">What keeps working</p> | |
| 962 | + | <Points | |
| 963 | + | columns={false} | |
| 964 | + | points={[ | |
| 965 | + | "git over HTTPS, with your history as it is", | |
| 966 | + | "GitHub Actions workflows: rename .github to .g1t and push", | |
| 967 | + | "The coding agents you use today, through MCP", | |
| 968 | + | "Leaving: git clone, and the REST API for the rest", | |
| 969 | + | ]} | |
| 970 | + | /> | |
| 886 | 971 | </div> | |
| 887 | − | </div> | |
| 888 | − | <div className="min-w-0 space-y-3"> | |
| 889 | − | <p className="text-sm text-muted"> | |
| 890 | − | Add g1t as a remote MCP server, then sign in through your browser: | |
| 891 | − | </p> | |
| 892 | − | <CopyLine text="https://mcp.g1t.sh" /> | |
| 893 | − | <p className="pt-3 text-sm text-muted"> | |
| 894 | − | Install the CLI, then sign in with{" "} | |
| 895 | − | <code className="font-mono text-fg-soft">g1t auth login</code>: | |
| 896 | − | </p> | |
| 897 | − | <CopyLine prompt text="curl -fsSL https://g1t.sh/install | sh" /> | |
| 898 | − | <p className="pt-3 text-sm text-muted"> | |
| 899 | − | Underneath, it is plain git and HTTP: | |
| 900 | − | </p> | |
| 901 | − | <CopyLine | |
| 902 | − | prompt | |
| 903 | − | breakAtSlashes | |
| 904 | − | text="git clone https://g1t.sh/flagon-io/g1t.git" | |
| 905 | − | /> | |
| 906 | − | <CopyLine | |
| 907 | − | prompt | |
| 908 | − | breakAtSlashes | |
| 909 | − | text="curl https://api.g1t.sh/repos/flagon-io/g1t/queue" | |
| 910 | − | /> | |
| 972 | + | <div className="rounded-xl border border-dashed border-line-strong px-4 py-3.5 text-sm leading-6 text-muted"> | |
| 973 | + | <p className="font-medium text-fg-soft"> | |
| 974 | + | Not a fit yet if you need | |
| 975 | + | </p> | |
| 976 | + | <p className="mt-1.5"> | |
| 977 | + | Single sign-on, git over SSH, or chat and deployments on your | |
| 978 | + | own machines: those run only on g1t.sh today. Open pull | |
| 979 | + | requests, issue comments, releases and wikis stay behind on | |
| 980 | + | import.{" "} | |
| 981 | + | <Link | |
| 982 | + | to={`${DOCS}/about/limitations/`} | |
| 983 | + | className="text-fg-soft underline-offset-4 hover:text-accent hover:underline" | |
| 984 | + | > | |
| 985 | + | What g1t can't do yet | |
| 986 | + | </Link> | |
| 987 | + | </p> | |
| 988 | + | </div> | |
| 911 | 989 | </div> | |
| 912 | 990 | </div> | |
| 913 | 991 | </section> | |
| 2 | 2 | * The landing page's product tour: g1t browsing itself. A faithful app | |
| 3 | 3 | * frame (rail, sidebars, the real colours) plays one story on a loop, with | |
| 4 | 4 | * a soft cursor that moves and clicks: the pull request going green and | |
| 5 | − | * merging, the session its author's coding agent recorded onto it, the | |
| 6 | − | * people in #web hearing it shipped, and the docs page. | |
| 5 | + | * merging, the session its author's coding agent recorded onto it, Home | |
| 6 | + | * when its author comes back (what settled by itself, what still needs her, | |
| 7 | + | * and What changed in plain English), and the people in #web hearing it | |
| 8 | + | * shipped. | |
| 7 | 9 | * | |
| 8 | 10 | * The frame is a pure function of time (lib/tour.ts). One rAF loop drives | |
| 9 | 11 | * the clock and re-renders only when the frame changes; the step pills' | |
| ⋯ | |||
| 23 | 25 | */ | |
| 24 | 26 | import { | |
| 25 | 27 | Activity, | |
| 28 | + | ArrowRight, | |
| 29 | + | ArrowUp, | |
| 26 | 30 | AtSign, | |
| 27 | 31 | BarChart3, | |
| 28 | 32 | Bell, | |
| 29 | − | BookOpen, | |
| 30 | 33 | Bot, | |
| 31 | 34 | Box, | |
| 32 | 35 | Building2, | |
| 33 | 36 | Check, | |
| 37 | + | CheckCheck, | |
| 34 | 38 | ChevronDown, | |
| 35 | 39 | ChevronLeft, | |
| 36 | 40 | CircleDot, | |
| ⋯ | |||
| 44 | 48 | GitPullRequest, | |
| 45 | 49 | Hash, | |
| 46 | 50 | House, | |
| 51 | + | Loader, | |
| 47 | 52 | MessageSquare, | |
| 48 | 53 | MessagesSquare, | |
| 49 | 54 | Paperclip, | |
| 50 | 55 | PlayCircle, | |
| 51 | 56 | Plus, | |
| 52 | 57 | Rocket, | |
| 58 | + | RotateCcw, | |
| 53 | 59 | Search, | |
| 54 | 60 | SendHorizontal, | |
| 55 | 61 | Settings, | |
| 56 | 62 | ShieldCheck, | |
| 57 | 63 | Shapes, | |
| 64 | + | Sparkles, | |
| 58 | 65 | SquarePen, | |
| 59 | 66 | User, | |
| 60 | 67 | Wrench, | |
| ⋯ | |||
| 64 | 71 | import { codingAgentById } from "@g1t/contracts/coding-agents"; | |
| 65 | 72 | ||
| 66 | 73 | import { cn } from "../lib/cn"; | |
| 67 | − | import { CHECKS, type CursorTarget, type Frame, LOOP_MS, PILLS, SESSION, type SessionEntry, STILLS, type Scene, frameAt, pillAt, pillProgress, sameFrame } from "../lib/tour"; | |
| 74 | + | import { CHECKS, type CursorTarget, type Frame, LOOP_MS, PILLS, SESSION, SETTLED, SUMMARY_WORDS, type SessionEntry, STILLS, type Scene, frameAt, pillAt, pillProgress, sameFrame } from "../lib/tour"; | |
| 68 | 75 | import { AgentBadge, AgentMark } from "./coding-agent"; | |
| 76 | + | import { MarkGlyph } from "./logo"; | |
| 69 | 77 | ||
| 70 | 78 | import { Avatar } from "./ui/avatar"; | |
| 79 | + | import { Badge } from "./ui/badge"; | |
| 71 | 80 | import { Card } from "./ui/card"; | |
| 72 | 81 | ||
| 73 | 82 | /** The frame's design size; it scales to fit, keeping its aspect. */ | |
| ⋯ | |||
| 192 | 201 | const HONESTY: Record<Scene, string> = { | |
| 193 | 202 | code: "Works today", | |
| 194 | 203 | session: "Works today: recorded by your coding agent through MCP", | |
| 204 | + | home: "Works today: summaries run on your workspace's own AI provider", | |
| 195 | 205 | chat: "Works today", | |
| 196 | − | docs: "Works today", | |
| 197 | 206 | }; | |
| 198 | 207 | ||
| 199 | 208 | /** The line under the frame. Not part of the app, so it sits outside it. */ | |
| ⋯ | |||
| 288 | 297 | ||
| 289 | 298 | /** The rail's modes, in the app's order (components/rail.tsx), with its icons. */ | |
| 290 | 299 | const RAIL: { key: string; icon: ReactNode; label: string; target?: CursorTarget; scenes?: Scene[]; badge?: number }[] = [ | |
| 291 | − | { key: "home", icon: <House size={15} />, label: "Home" }, | |
| 300 | + | { key: "home", icon: <House size={15} />, label: "Home", target: "rail-home", scenes: ["home"] }, | |
| 292 | 301 | { key: "chat", icon: <MessagesSquare size={15} />, label: "Chat", target: "rail-chat", scenes: ["chat"], badge: 3 }, | |
| 293 | 302 | { key: "code", icon: <Code2 size={15} />, label: "Code", target: "rail-code", scenes: ["code", "session"] }, | |
| 294 | − | { key: "artifacts", icon: <Shapes size={15} />, label: "Artifacts", target: "rail-docs", scenes: ["docs"] }, | |
| 295 | − | { key: "inbox", icon: <Bell size={15} />, label: "Notifications", badge: 2 }, | |
| 303 | + | { key: "artifacts", icon: <Shapes size={15} />, label: "Artifacts" }, | |
| 304 | + | { key: "inbox", icon: <Bell size={15} />, label: "Notifications" }, | |
| 296 | 305 | ]; | |
| 297 | 306 | ||
| 298 | 307 | /** One rail item: the square behind its icon, filled for the mode you are in, and its name under it. */ | |
| ⋯ | |||
| 303 | 312 | data-tour={target} | |
| 304 | 313 | className={cn( | |
| 305 | 314 | "relative flex size-7 items-center justify-center rounded-lg transition-colors duration-500", | |
| 306 | − | on ? "bg-[#2c2c33] text-fg shadow-[inset_0_0_0_1px_rgba(255,255,255,0.06)]" : "text-muted", | |
| 315 | + | on ? "bg-[#2c2c33] text-[#ededef] shadow-[inset_0_0_0_1px_rgba(255,255,255,0.06)]" : "text-muted", | |
| 307 | 316 | )} | |
| 308 | 317 | > | |
| 309 | 318 | {icon} | |
| ⋯ | |||
| 313 | 322 | </span> | |
| 314 | 323 | ) : null} | |
| 315 | 324 | </span> | |
| 316 | − | <span className={cn("text-[9px] leading-none font-medium transition-colors duration-500", on ? "text-fg" : "text-faint")}>{label}</span> | |
| 325 | + | <span className={cn("text-[9px] leading-none font-medium transition-colors duration-500", on ? "text-[#ededef]" : "text-faint")}>{label}</span> | |
| 317 | 326 | </span> | |
| 318 | 327 | ); | |
| 319 | 328 | } | |
| ⋯ | |||
| 323 | 332 | * switcher), the modes with their names, then the workspace's own pages, | |
| 324 | 333 | * help, and you, with the dot that says how others see you. | |
| 325 | 334 | */ | |
| 326 | − | function Rail({ scene }: { scene: Scene }) { | |
| 335 | + | function Rail({ frame }: { frame: Frame }) { | |
| 336 | + | const scene = frame.scene; | |
| 337 | + | // Notifications still unread: the two that settle on Home leave the count as they do. | |
| 338 | + | const unread = SETTLED.length - frame.settled; | |
| 327 | 339 | return ( | |
| 328 | 340 | <nav aria-hidden="true" className="flex w-16 shrink-0 flex-col items-center bg-[#0b0b0d] pb-2.5 shadow-[inset_-1px_0_0_var(--color-line)]"> | |
| 329 | 341 | <div className="flex h-11 w-full shrink-0 items-center justify-center border-b border-line bg-bg shadow-[inset_-1px_0_0_var(--color-line)]"> | |
| ⋯ | |||
| 331 | 343 | </div> | |
| 332 | 344 | <div className="mt-2.5 flex w-full flex-col items-center gap-1.5 px-1"> | |
| 333 | 345 | {RAIL.map((item) => ( | |
| 334 | − | <RailItem key={item.key} icon={item.icon} label={item.label} on={!!item.scenes?.includes(scene)} target={item.target} badge={item.badge} /> | |
| 346 | + | <RailItem key={item.key} icon={item.icon} label={item.label} on={!!item.scenes?.includes(scene)} target={item.target} badge={item.key === "inbox" ? unread : item.badge} /> | |
| 335 | 347 | ))} | |
| 336 | 348 | </div> | |
| 337 | 349 | <div className="mt-auto flex w-full flex-col items-center gap-1.5 px-1"> | |
| ⋯ | |||
| 805 | 817 | ); | |
| 806 | 818 | } | |
| 807 | 819 | ||
| 808 | − | /* Docs ---------------------------------------------------------------- */ | |
| 820 | + | /* Home ---------------------------------------------------------------- */ | |
| 821 | + | ||
| 822 | + | /** One of Home's cards (components/home.tsx Card): its title, a few words under it, then its rows. */ | |
| 823 | + | function HomeCard({ title, hint, link, children, className }: { title: ReactNode; hint?: string; link?: string; children: ReactNode; className?: string }) { | |
| 824 | + | return ( | |
| 825 | + | <section className={cn("overflow-hidden rounded-2xl border border-line bg-surface", className)}> | |
| 826 | + | <header className="flex items-start gap-2 px-4 pt-3 pb-2"> | |
| 827 | + | <div className="min-w-0"> | |
| 828 | + | <p className="text-[12.5px] font-semibold text-fg">{title}</p> | |
| 829 | + | {hint && <p className="mt-0.5 truncate text-[10.5px] text-muted">{hint}</p>} | |
| 830 | + | </div> | |
| 831 | + | {link && ( | |
| 832 | + | <span className="ml-auto flex shrink-0 items-center gap-1 pt-0.5 text-[10.5px] text-muted"> | |
| 833 | + | {link} | |
| 834 | + | <ArrowRight size={10} /> | |
| 835 | + | </span> | |
| 836 | + | )} | |
| 837 | + | </header> | |
| 838 | + | {children} | |
| 839 | + | </section> | |
| 840 | + | ); | |
| 841 | + | } | |
| 842 | + | ||
| 843 | + | const HOME_ROW = "flex items-center gap-2.5 border-t border-line px-4 py-2"; | |
| 809 | 844 | ||
| 810 | − | /** Docs' sidebar: search, then each space's pages. */ | |
| 811 | − | function DocsSide() { | |
| 845 | + | /** What still waits on Priya once the notifications above settle: her pull request ready to merge, and a review asked of her. */ | |
| 846 | + | const STAYING = [ | |
| 847 | + | { icon: <GitMerge size={14} className="text-success" />, title: "Retry webhook deliveries with backoff", detail: "acme/web #433 · Ready to land when you merge it.", stake: "Ready to merge", action: "Open" }, | |
| 848 | + | { icon: <GitPullRequest size={14} className="text-info" />, title: "Add audit log export", detail: "acme/web #435 · Your review", stake: "+212 −40", action: "Review" }, | |
| 849 | + | ]; | |
| 850 | + | ||
| 851 | + | /** A row of Where you're needed that settles while you look: it says why, then leaves the list. */ | |
| 852 | + | function SettlingRow({ row, settled, cleared, index }: { row: (typeof SETTLED)[number]; settled: boolean; cleared: boolean; index: number }) { | |
| 812 | 853 | return ( | |
| 813 | − | <> | |
| 814 | − | <SideHead action={<Plus size={14} />}> | |
| 815 | − | Docs | |
| 816 | − | </SideHead> | |
| 817 | − | <div className="px-2 pt-2.5"> | |
| 818 | − | <SideSearch>Search docs</SideSearch> | |
| 819 | − | <SideLabel>Product</SideLabel> | |
| 820 | − | {["Getting started", "Billing", "Exporting data", "Integrations"].map((page) => ( | |
| 821 | − | <SideRow key={page} on={page === "Exporting data"}> | |
| 822 | − | <FileText size={12} className="text-faint" /> | |
| 823 | − | {page} | |
| 824 | − | </SideRow> | |
| 825 | − | ))} | |
| 826 | − | <SideLabel>Engineering</SideLabel> | |
| 827 | − | {["Runbooks", "Decisions"].map((page) => ( | |
| 828 | − | <SideRow key={page}> | |
| 829 | − | <FileText size={12} className="text-faint" /> | |
| 830 | − | {page} | |
| 831 | − | </SideRow> | |
| 832 | − | ))} | |
| 854 | + | <div | |
| 855 | + | className={cn( | |
| 856 | + | "grid transition-[grid-template-rows,opacity] duration-500 ease-out motion-reduce:transition-none", | |
| 857 | + | cleared ? "grid-rows-[0fr] opacity-0" : "grid-rows-[1fr] opacity-100", | |
| 858 | + | )} | |
| 859 | + | > | |
| 860 | + | <div className="min-h-0 overflow-hidden"> | |
| 861 | + | <div className={cn(HOME_ROW, index === 0 && "border-t-0", "transition-colors duration-500", settled && "bg-success/5")}> | |
| 862 | + | <span key={settled ? "done" : "open"} className="tour-pop shrink-0"> | |
| 863 | + | {settled ? <CheckCheck size={14} className="text-success" /> : index === 0 ? <Rocket size={14} className="text-danger" /> : <PlayCircle size={14} className="text-warn" />} | |
| 864 | + | </span> | |
| 865 | + | <div className="min-w-0 grow"> | |
| 866 | + | <p className={cn("truncate text-[12px] font-semibold transition-colors duration-500", settled ? "text-muted line-through decoration-faint" : "text-fg")}>{row.title}</p> | |
| 867 | + | <p key={settled ? "why" : "detail"} className={cn("tour-fade truncate text-[10.5px]", settled ? "text-success" : "text-muted")}> | |
| 868 | + | {settled ? `Settled · ${row.why}` : row.detail} | |
| 869 | + | </p> | |
| 870 | + | </div> | |
| 871 | + | <span className={cn("shrink-0 rounded-md border border-line-strong px-2 py-1 text-[10.5px] text-fg transition-opacity duration-300", settled && "opacity-0")}> | |
| 872 | + | {index === 0 ? "See the build" : "Look"} | |
| 873 | + | </span> | |
| 874 | + | </div> | |
| 833 | 875 | </div> | |
| 834 | − | </> | |
| 876 | + | </div> | |
| 877 | + | ); | |
| 878 | + | } | |
| 879 | + | ||
| 880 | + | /** Where you're needed: the two notifications that settle themselves, then what is left for Priya. */ | |
| 881 | + | function NeededCard({ frame, phone }: { frame: Frame; phone?: boolean }) { | |
| 882 | + | return ( | |
| 883 | + | <HomeCard title="Where you're needed" hint={phone ? undefined : "Everything waiting on you, however long ago it started"} link="Notifications"> | |
| 884 | + | {SETTLED.map((row, index) => ( | |
| 885 | + | <SettlingRow key={row.title} row={row} index={index} settled={frame.settled > index} cleared={frame.cleared > index} /> | |
| 886 | + | ))} | |
| 887 | + | {STAYING.map((row, index) => ( | |
| 888 | + | <div key={row.title} className={cn(HOME_ROW, index === 0 && frame.cleared >= SETTLED.length && "border-t-0")}> | |
| 889 | + | <span className="shrink-0">{row.icon}</span> | |
| 890 | + | <div className="min-w-0 grow"> | |
| 891 | + | <p className="truncate text-[12px] font-semibold text-fg">{row.title}</p> | |
| 892 | + | <p className="truncate text-[10.5px] text-muted">{row.detail}</p> | |
| 893 | + | </div> | |
| 894 | + | {!phone && <span className="shrink-0 text-[10.5px] font-medium text-fg-soft">{row.stake}</span>} | |
| 895 | + | <span className="shrink-0 rounded-md border border-line-strong px-2 py-1 text-[10.5px] text-fg">{row.action}</span> | |
| 896 | + | </div> | |
| 897 | + | ))} | |
| 898 | + | </HomeCard> | |
| 835 | 899 | ); | |
| 836 | 900 | } | |
| 837 | 901 | ||
| 838 | − | function DocsMain({ frame }: { frame: Frame }) { | |
| 902 | + | /** Start here: the deploy failure while it is open (it comes before anything else), then the pull request ready to merge. */ | |
| 903 | + | function StartCard({ frame }: { frame: Frame }) { | |
| 904 | + | const deploy = frame.settled === 0; | |
| 905 | + | const pick = deploy | |
| 906 | + | ? { icon: <Rocket size={14} className="text-danger" />, title: SETTLED[0].title, next: "See the build", why: "Production is serving an older build until this is fixed, so it comes before anything else.", button: "See the build" } | |
| 907 | + | : { icon: <GitMerge size={14} className="text-success" />, title: STAYING[0].title, next: "Merge", why: "Everything it needs is met; it lands when you merge it.", button: "Open pull request" }; | |
| 839 | 908 | return ( | |
| 840 | − | <div className="relative h-full px-10 py-7"> | |
| 841 | − | <p className="text-[11px] text-faint">Product / Exporting data</p> | |
| 842 | − | <h3 className="mt-2 text-[22px] font-semibold tracking-tight text-fg">Exporting data</h3> | |
| 843 | − | <p className="mt-1.5 flex items-center gap-2 text-[11px] text-faint"> | |
| 844 | − | Owned by <span className="text-fg-soft">{PEOPLE.Dana}</span> ·{" "} | |
| 845 | − | <span key={frame.docUpdated ? "now" : "then"} className="tour-fade"> | |
| 846 | − | {frame.docUpdated ? "updated just now from #431" : "updated 3 weeks ago"} | |
| 909 | + | <HomeCard | |
| 910 | + | title={ | |
| 911 | + | <> | |
| 912 | + | Start here <span className="font-normal text-faint">· picked by g1t</span> | |
| 913 | + | </> | |
| 914 | + | } | |
| 915 | + | > | |
| 916 | + | <div key={deploy ? "deploy" : "ready"} className="tour-in px-4 pb-4"> | |
| 917 | + | <div className="flex items-start gap-2.5"> | |
| 918 | + | <span className="mt-0.5 shrink-0">{pick.icon}</span> | |
| 919 | + | <p className="text-[12.5px] leading-snug font-semibold text-fg">{pick.title}</p> | |
| 920 | + | </div> | |
| 921 | + | <dl className="mt-2.5 grid grid-cols-[auto_1fr] gap-x-3 gap-y-1 text-[10.5px]"> | |
| 922 | + | <dt className="text-muted">Next</dt> | |
| 923 | + | <dd className="text-fg-soft">{pick.next}</dd> | |
| 924 | + | <dt className="text-muted">At stake</dt> | |
| 925 | + | <dd className={deploy ? "text-danger" : "text-fg-soft"}>{deploy ? "Production on an older build" : "Ready to merge"}</dd> | |
| 926 | + | </dl> | |
| 927 | + | <p className="mt-2.5 rounded-lg bg-raised/60 px-2.5 py-2 text-[10.5px] leading-relaxed text-fg-soft"> | |
| 928 | + | <span className="font-medium text-fg">Why this one: </span> | |
| 929 | + | {pick.why} | |
| 930 | + | </p> | |
| 931 | + | <span className="mt-3 inline-flex h-7 items-center gap-1.5 rounded-lg bg-accent px-2.5 text-[11.5px] font-medium text-bg"> | |
| 932 | + | {pick.button} | |
| 933 | + | <ArrowRight size={12} /> | |
| 847 | 934 | </span> | |
| 848 | − | </p> | |
| 849 | − | <div className="mt-5 max-w-xl space-y-3 text-[13px] leading-[1.7] text-fg-soft"> | |
| 850 | − | <p>Export any table as CSV from Settings, then Data. An export includes every row you can see, in the order shown.</p> | |
| 851 | − | <p className="pt-2 text-[14px] font-semibold text-fg">Large accounts</p> | |
| 852 | − | <div className="relative"> | |
| 853 | − | <p | |
| 854 | − | className={cn( | |
| 855 | − | "rounded-md px-3 py-2 transition-all duration-500", | |
| 856 | − | frame.docUpdated ? "bg-danger/5 text-faint line-through decoration-danger/50" : "text-fg-soft", | |
| 857 | − | )} | |
| 858 | − | > | |
| 859 | − | Exports over 50,000 rows can time out. Split them by date range. | |
| 860 | − | </p> | |
| 861 | − | <div | |
| 862 | − | className={cn( | |
| 863 | − | "mt-1.5 rounded-md border-l-2 border-success bg-success/10 px-3 py-2 text-fg transition-all duration-700", | |
| 864 | − | frame.docUpdated ? "translate-y-0 opacity-100" : "translate-y-1 opacity-0", | |
| 865 | − | )} | |
| 866 | − | > | |
| 867 | − | Exports of any size stream in batches of 1,000 rows. A 200,000-row account finishes in about 3 seconds. | |
| 935 | + | </div> | |
| 936 | + | </HomeCard> | |
| 937 | + | ); | |
| 938 | + | } | |
| 939 | + | ||
| 940 | + | /** What changed: Summarize, then g1t's answer streaming in, word by word, then Ask a follow-up. */ | |
| 941 | + | function WhatChangedCardMock({ frame }: { frame: Frame }) { | |
| 942 | + | const text = SUMMARY_WORDS.slice(0, frame.words).join(" "); | |
| 943 | + | return ( | |
| 944 | + | <HomeCard title="What changed" hint="In plain English, since I was last here"> | |
| 945 | + | <div className="px-4 pb-3.5"> | |
| 946 | + | {!frame.summarizing ? ( | |
| 947 | + | <div className="flex items-center gap-3"> | |
| 948 | + | <p className="grow text-[11.5px] leading-relaxed text-muted"> | |
| 949 | + | g1t reads the commits, pull requests, deploys and conversations since I was last here and tells you what changed, why, and what is live. | |
| 950 | + | </p> | |
| 951 | + | <span className="inline-flex h-7 shrink-0 items-center gap-1.5 rounded-md bg-accent px-2.5 text-[11.5px] font-medium text-bg"> | |
| 952 | + | <Sparkles size={12} /> | |
| 953 | + | Summarize | |
| 954 | + | </span> | |
| 955 | + | </div> | |
| 956 | + | ) : frame.words === 0 ? ( | |
| 957 | + | <div className="tour-fade space-y-2"> | |
| 958 | + | <p className="text-[11.5px] text-muted">Reading what happened…</p> | |
| 959 | + | <span className="tour-shimmer block h-2.5 w-11/12 rounded bg-raised" /> | |
| 960 | + | <span className="tour-shimmer block h-2.5 w-3/4 rounded bg-raised" /> | |
| 961 | + | </div> | |
| 962 | + | ) : ( | |
| 963 | + | <> | |
| 964 | + | <p className="text-[12.5px] leading-[1.6] text-fg-soft"> | |
| 965 | + | <Rich text={text} /> | |
| 966 | + | {!frame.summarized && <span className="tour-caret ml-0.5 inline-block h-3.5 w-1.5 translate-y-0.5 rounded-[1px] bg-accent" />} | |
| 967 | + | </p> | |
| 968 | + | <div className={cn("mt-3 flex items-center gap-1.5 border-t border-line pt-2.5 transition-opacity duration-500", frame.summarized ? "opacity-100" : "opacity-0")}> | |
| 969 | + | <span className="inline-flex h-6 items-center gap-1.5 rounded-md bg-accent/10 px-2 text-[10.5px] font-medium text-fg ring-1 ring-accent/20 ring-inset"> | |
| 970 | + | <MarkGlyph className="h-2.5" /> | |
| 971 | + | Ask a follow-up | |
| 972 | + | </span> | |
| 973 | + | <span className="inline-flex h-6 items-center gap-1.5 px-2 text-[10.5px] text-muted"> | |
| 974 | + | <RotateCcw size={11} /> | |
| 975 | + | Summarize again | |
| 976 | + | </span> | |
| 977 | + | </div> | |
| 978 | + | </> | |
| 979 | + | )} | |
| 980 | + | </div> | |
| 981 | + | </HomeCard> | |
| 982 | + | ); | |
| 983 | + | } | |
| 984 | + | ||
| 985 | + | /** Landed: each change with whether it is live, the agent's mark on the one it helped make, and Explain. */ | |
| 986 | + | function LandedCardMock({ frame }: { frame: Frame }) { | |
| 987 | + | const rows = [ | |
| 988 | + | { | |
| 989 | + | title: "Stream CSV exports in batches", | |
| 990 | + | detail: "web#431 · by priya", | |
| 991 | + | agent: true, | |
| 992 | + | explain: "home-explain" as const, | |
| 993 | + | badge: frame.live ? ( | |
| 994 | + | <Badge key="live" tone="success" className="tour-pop"> | |
| 995 | + | <Rocket size={9} /> | |
| 996 | + | Live 9 min after merge | |
| 997 | + | </Badge> | |
| 998 | + | ) : ( | |
| 999 | + | <Badge key="deploying" tone="info"> | |
| 1000 | + | <Loader size={9} className="tour-spin" /> | |
| 1001 | + | Deploying | |
| 1002 | + | </Badge> | |
| 1003 | + | ), | |
| 1004 | + | at: "9:41 AM", | |
| 1005 | + | }, | |
| 1006 | + | { | |
| 1007 | + | title: "Pin the build image's cache key", | |
| 1008 | + | detail: "web · 1 push by jordan", | |
| 1009 | + | agent: false, | |
| 1010 | + | explain: undefined, | |
| 1011 | + | badge: ( | |
| 1012 | + | <Badge tone="success"> | |
| 1013 | + | <Rocket size={9} /> | |
| 1014 | + | Live 4 min after push | |
| 1015 | + | </Badge> | |
| 1016 | + | ), | |
| 1017 | + | at: "8:15 AM", | |
| 1018 | + | }, | |
| 1019 | + | ]; | |
| 1020 | + | return ( | |
| 1021 | + | <HomeCard title="Landed" hint="1 pull request merged · 1 push"> | |
| 1022 | + | {rows.map((row) => ( | |
| 1023 | + | <div key={row.title} className={HOME_ROW}> | |
| 1024 | + | <span className="shrink-0">{row.agent ? <GitMerge size={14} className="text-merged" /> : <GitCommitHorizontal size={14} className="text-muted" />}</span> | |
| 1025 | + | <div className="min-w-0 grow"> | |
| 1026 | + | <p className="flex min-w-0 items-center gap-1.5 text-[12px] text-fg"> | |
| 1027 | + | <span className="truncate">{row.title}</span> | |
| 1028 | + | {row.agent && <AgentMark agent={CLAUDE} size={12} />} | |
| 1029 | + | </p> | |
| 1030 | + | <div className="mt-0.5 flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1"> | |
| 1031 | + | <span className="truncate text-[10.5px] text-muted">{row.detail}</span> | |
| 1032 | + | {row.badge} | |
| 1033 | + | </div> | |
| 1034 | + | </div> | |
| 1035 | + | <span data-tour={row.explain} className="flex size-6 shrink-0 items-center justify-center rounded-md text-muted"> | |
| 1036 | + | <Sparkles size={12} /> | |
| 1037 | + | </span> | |
| 1038 | + | <span className="shrink-0 text-[10.5px] text-faint tabular-nums">{row.at}</span> | |
| 1039 | + | </div> | |
| 1040 | + | ))} | |
| 1041 | + | </HomeCard> | |
| 1042 | + | ); | |
| 1043 | + | } | |
| 1044 | + | ||
| 1045 | + | /** | |
| 1046 | + | * Home, as Priya comes back to it (routes/workspace/home.tsx): "What are | |
| 1047 | + | * we building today?" over the box that asks g1t, Where you're needed | |
| 1048 | + | * beside Start here, then Since you were last here with What changed and | |
| 1049 | + | * Landed. Pressing the first starter brings What changed into view, as the | |
| 1050 | + | * real one does, and its answer streams in. | |
| 1051 | + | */ | |
| 1052 | + | function HomeMain({ frame, phone = false }: { frame: Frame; phone?: boolean }) { | |
| 1053 | + | const viewport = useRef<HTMLDivElement | null>(null); | |
| 1054 | + | const content = useRef<HTMLDivElement | null>(null); | |
| 1055 | + | const since = useRef<HTMLDivElement | null>(null); | |
| 1056 | + | const [offset, setOffset] = useState(0); | |
| 1057 | + | // Measured again as the answer grows, so the page follows it down as it streams in. | |
| 1058 | + | const grown = Math.floor(frame.words / 12); | |
| 1059 | + | useIsoLayoutEffect(() => { | |
| 1060 | + | if (!frame.summarizing) { | |
| 1061 | + | setOffset(0); | |
| 1062 | + | return; | |
| 1063 | + | } | |
| 1064 | + | const view = viewport.current; | |
| 1065 | + | const body = content.current; | |
| 1066 | + | const mark = since.current; | |
| 1067 | + | if (!view || !body || !mark) return; | |
| 1068 | + | setOffset(Math.max(0, Math.min(mark.offsetTop - 14, body.scrollHeight - view.clientHeight))); | |
| 1069 | + | }, [frame.summarizing, grown, frame.summarized]); | |
| 1070 | + | const starters = phone ? ["What needs me first?"] : ["What needs me first?", "What shipped today?", "Who's working on what?"]; | |
| 1071 | + | return ( | |
| 1072 | + | <div ref={viewport} className="h-full overflow-hidden"> | |
| 1073 | + | <div | |
| 1074 | + | ref={content} | |
| 1075 | + | className={cn("relative transition-transform duration-1000 ease-[cubic-bezier(.45,.05,.2,1)] motion-reduce:transition-none", phone ? "space-y-3 px-3 pt-4 pb-4" : "space-y-4 px-6 pt-5 pb-6")} | |
| 1076 | + | style={{ transform: offset ? `translateY(${-offset}px)` : undefined }} | |
| 1077 | + | > | |
| 1078 | + | <div className={cn("mx-auto flex flex-col items-center", phone ? "gap-2.5" : "max-w-xl gap-3")}> | |
| 1079 | + | <p className={cn("text-center font-semibold tracking-tight text-fg", phone ? "text-[17px]" : "text-[21px]")}>What are we building today?</p> | |
| 1080 | + | <div className="flex w-full items-center gap-2 rounded-2xl border border-line bg-surface p-1.5 pl-3 shadow-sm"> | |
| 1081 | + | <MarkGlyph className="h-3.5 shrink-0" /> | |
| 1082 | + | <span className="min-w-0 grow truncate text-[12px] text-faint">Ask g1t what changed, what's blocked, or what to do next</span> | |
| 1083 | + | <span className="flex size-7 shrink-0 items-center justify-center rounded-md bg-accent text-bg opacity-50"> | |
| 1084 | + | <ArrowUp size={13} /> | |
| 1085 | + | </span> | |
| 1086 | + | </div> | |
| 1087 | + | <div className="flex flex-wrap justify-center gap-1.5"> | |
| 1088 | + | <span | |
| 1089 | + | data-tour="home-summarize" | |
| 1090 | + | className={cn( | |
| 1091 | + | "rounded-full border px-2.5 py-1 text-[10.5px] text-fg transition-colors duration-300", | |
| 1092 | + | frame.summarizing ? "border-accent bg-accent/15" : "border-accent-dim", | |
| 1093 | + | )} | |
| 1094 | + | > | |
| 1095 | + | Summarize what changed since I was last here | |
| 1096 | + | </span> | |
| 1097 | + | {starters.map((label) => ( | |
| 1098 | + | <span key={label} className="rounded-full border border-line px-2.5 py-1 text-[10.5px] text-fg-soft"> | |
| 1099 | + | {label} | |
| 1100 | + | </span> | |
| 1101 | + | ))} | |
| 868 | 1102 | </div> | |
| 869 | 1103 | </div> | |
| 870 | − | <div | |
| 871 | − | className={cn( | |
| 872 | − | "flex items-center gap-2 pt-1 text-[11px] text-muted transition-opacity delay-300 duration-500", | |
| 873 | − | frame.docUpdated ? "opacity-100" : "opacity-0", | |
| 874 | − | )} | |
| 875 | − | > | |
| 876 | − | <Face who="Dana" size={18} /> | |
| 877 | − | <span> | |
| 878 | − | Updated by <span className="text-fg">{PEOPLE.Dana}</span> after <span className="text-accent">#431</span> merged | |
| 879 | − | </span> | |
| 1104 | + | ||
| 1105 | + | <div className={cn("grid items-start gap-3", !phone && "grid-cols-[minmax(0,1.6fr)_minmax(0,1fr)] gap-4")}> | |
| 1106 | + | <NeededCard frame={frame} phone={phone} /> | |
| 1107 | + | <StartCard frame={frame} /> | |
| 1108 | + | </div> | |
| 1109 | + | ||
| 1110 | + | <div ref={since} className="flex items-baseline gap-2.5 pt-1"> | |
| 1111 | + | <p className={cn("font-semibold tracking-tight text-fg", phone ? "text-[14px]" : "text-[15px]")}>Since you were last here</p> | |
| 1112 | + | <p className="truncate text-[11px] text-muted">Yesterday at 6:12 PM · 15 hours</p> | |
| 880 | 1113 | </div> | |
| 881 | − | <p className="pt-3 text-[14px] font-semibold text-fg">Scheduled exports</p> | |
| 882 | − | <p>Owners can send an export to a storage bucket every day or week. Each run uses the same columns as the last.</p> | |
| 1114 | + | <WhatChangedCardMock frame={frame} /> | |
| 1115 | + | <LandedCardMock frame={frame} /> | |
| 883 | 1116 | </div> | |
| 884 | 1117 | </div> | |
| 885 | 1118 | ); | |
| ⋯ | |||
| 928 | 1161 | ||
| 929 | 1162 | /* Desktop ------------------------------------------------------------- */ | |
| 930 | 1163 | ||
| 931 | − | const SIDES: Record<Scene, (props: { frame: Frame }) => ReactNode> = { | |
| 1164 | + | const SIDES: Record<Scene, ((props: { frame: Frame }) => ReactNode) | null> = { | |
| 932 | 1165 | code: CodeSide, | |
| 933 | 1166 | session: CodeSide, | |
| 1167 | + | home: null, | |
| 934 | 1168 | chat: ChatSide, | |
| 935 | − | docs: DocsSide, | |
| 936 | 1169 | }; | |
| 937 | 1170 | ||
| 938 | 1171 | /** Where each page is, as the top bar's trail says it; Chat has no top bar on a computer. */ | |
| ⋯ | |||
| 940 | 1173 | chat: null, | |
| 941 | 1174 | code: [{ label: "acme", mono: true }, { label: "web", mono: true }, { label: "Pull request #431" }], | |
| 942 | 1175 | session: [{ label: "acme", mono: true }, { label: "web", mono: true }, { label: "Pull request #431" }], | |
| 943 | − | docs: [{ label: "acme", mono: true }, { label: "Docs" }], | |
| 1176 | + | home: [{ label: "acme", mono: true }, { label: "Home" }], | |
| 944 | 1177 | }; | |
| 945 | 1178 | ||
| 946 | 1179 | /** The sidebar's colour, a shade lighter than the page (shell.tsx, SIDEBAR_BOX). */ | |
| ⋯ | |||
| 960 | 1193 | observer.observe(element); | |
| 961 | 1194 | return () => observer.disconnect(); | |
| 962 | 1195 | }, []); | |
| 963 | − | const scenes: Scene[] = ["code", "session", "chat", "docs"]; | |
| 1196 | + | const scenes: Scene[] = ["code", "session", "home", "chat"]; | |
| 964 | 1197 | const mains: Record<Scene, ReactNode> = { | |
| 965 | 1198 | chat: <ChatMain frame={frame} />, | |
| 966 | 1199 | session: <SessionMain frame={frame} />, | |
| 967 | 1200 | code: <CodeMain frame={frame} />, | |
| 968 | − | docs: <DocsMain frame={frame} />, | |
| 1201 | + | home: <HomeMain frame={frame} />, | |
| 969 | 1202 | }; | |
| 970 | 1203 | return ( | |
| 971 | 1204 | <div ref={outer} className="relative w-full overflow-hidden rounded-2xl text-fg shadow-2xl shadow-black/50 ring-1 ring-line scheme-dark" style={{ aspectRatio: `${W} / ${H}` }}> | |
| ⋯ | |||
| 974 | 1207 | className="absolute top-0 left-0 flex origin-top-left bg-bg text-left" | |
| 975 | 1208 | style={{ width: W, height: H, transform: scale === 1 ? undefined : `scale(${scale})`, ["--tour-side" as string]: SIDE_BG } as CSSProperties} | |
| 976 | 1209 | > | |
| 977 | − | <Rail scene={frame.scene} /> | |
| 1210 | + | <Rail frame={frame} /> | |
| 978 | 1211 | <div className="relative min-w-0 flex-1"> | |
| 979 | 1212 | {scenes.map((scene) => { | |
| 980 | 1213 | const Side = SIDES[scene]; | |
| ⋯ | |||
| 989 | 1222 | on ? "translate-y-0 opacity-100" : "pointer-events-none translate-y-1 opacity-0", | |
| 990 | 1223 | )} | |
| 991 | 1224 | > | |
| 992 | − | <div className="flex w-52 shrink-0 flex-col overflow-hidden border-r border-line" style={{ background: SIDE_BG }}> | |
| 993 | − | <Side frame={frame} /> | |
| 994 | − | </div> | |
| 1225 | + | {Side && ( | |
| 1226 | + | <div className="flex w-52 shrink-0 flex-col overflow-hidden border-r border-line" style={{ background: SIDE_BG }}> | |
| 1227 | + | <Side frame={frame} /> | |
| 1228 | + | </div> | |
| 1229 | + | )} | |
| 995 | 1230 | <div className="flex min-w-0 flex-1 flex-col"> | |
| 996 | 1231 | {crumbs && <TopBar crumbs={crumbs} />} | |
| 997 | 1232 | <div className="min-h-0 flex-1">{mains[scene]}</div> | |
| ⋯ | |||
| 1012 | 1247 | code: { icon: <GitPullRequest size={14} />, label: "Pull request #431" }, | |
| 1013 | 1248 | session: { icon: <MessagesSquare size={14} />, label: "#431 · Session" }, | |
| 1014 | 1249 | chat: { icon: <Hash size={14} />, label: "web" }, | |
| 1015 | − | docs: { icon: <BookOpen size={14} />, label: "Exporting data" }, | |
| 1250 | + | home: { icon: <House size={14} />, label: "Home" }, | |
| 1016 | 1251 | }; | |
| 1017 | 1252 | ||
| 1018 | 1253 | /** The phone's tab bar, as the app has it (components/mobile.tsx). */ | |
| 1019 | 1254 | const TABS: { scenes: Scene[]; icon: ReactNode; label: string }[] = [ | |
| 1020 | − | { scenes: [], icon: <House size={16} />, label: "Home" }, | |
| 1255 | + | { scenes: ["home"], icon: <House size={16} />, label: "Home" }, | |
| 1021 | 1256 | { scenes: ["chat"], icon: <MessagesSquare size={16} />, label: "Chat" }, | |
| 1022 | 1257 | { scenes: [], icon: <Bell size={16} />, label: "Notifications" }, | |
| 1023 | 1258 | { scenes: ["code", "session"], icon: <Code2 size={16} />, label: "Code" }, | |
| 1024 | 1259 | ]; | |
| 1025 | 1260 | ||
| 1026 | 1261 | function Phone({ frame }: { frame: Frame }) { | |
| 1027 | − | const scenes: Scene[] = ["code", "session", "chat", "docs"]; | |
| 1262 | + | const scenes: Scene[] = ["code", "session", "home", "chat"]; | |
| 1028 | 1263 | return ( | |
| 1029 | 1264 | <div className="relative flex h-136 w-full flex-col overflow-hidden rounded-2xl bg-bg text-left text-fg shadow-2xl shadow-black/50 ring-1 ring-line scheme-dark"> | |
| 1030 | 1265 | <div className="flex h-12 shrink-0 items-center gap-2 border-b border-line px-3"> | |
| ⋯ | |||
| 1046 | 1281 | {scene === "code" && <PhoneCode frame={frame} />} | |
| 1047 | 1282 | {scene === "session" && <PhoneSession frame={frame} />} | |
| 1048 | 1283 | {scene === "chat" && <PhoneChat frame={frame} />} | |
| 1049 | − | {scene === "docs" && ( | |
| 1050 | − | <div className="origin-top-left scale-[0.86]" style={{ width: "116%" }}> | |
| 1051 | − | <DocsMain frame={frame} /> | |
| 1052 | − | </div> | |
| 1053 | − | )} | |
| 1284 | + | {scene === "home" && <HomeMain frame={frame} phone />} | |
| 1054 | 1285 | </div> | |
| 1055 | 1286 | ); | |
| 1056 | 1287 | })} | |
| ⋯ | |||
| 1062 | 1293 | const on = tab.scenes.includes(frame.scene); | |
| 1063 | 1294 | return ( | |
| 1064 | 1295 | <span key={tab.label} className="flex flex-col items-center justify-center gap-0.5"> | |
| 1065 | − | <span className={cn("flex h-6 w-10 items-center justify-center rounded-full", on ? "bg-[#2c2c33] text-fg" : "text-muted")}>{tab.icon}</span> | |
| 1066 | − | <span className={cn("text-[9.5px] font-medium", on ? "text-fg" : "text-faint")}>{tab.label}</span> | |
| 1296 | + | <span className={cn("flex h-6 w-10 items-center justify-center rounded-full", on ? "bg-[#2c2c33] text-[#ededef]" : "text-muted")}>{tab.icon}</span> | |
| 1297 | + | <span className={cn("text-[9.5px] font-medium", on ? "text-[#ededef]" : "text-faint")}>{tab.label}</span> | |
| 1067 | 1298 | </span> | |
| 1068 | 1299 | ); | |
| 1069 | 1300 | })} | |
| ⋯ | |||
| 1189 | 1420 | @keyframes tour-fade { from { opacity: 0; } to { opacity: 1; } } | |
| 1190 | 1421 | .tour-spin { animation: tour-spin .9s linear infinite; } | |
| 1191 | 1422 | @keyframes tour-spin { to { transform: rotate(360deg); } } | |
| 1423 | + | .tour-caret { animation: tour-caret 1s steps(1) infinite; } | |
| 1424 | + | @keyframes tour-caret { 50% { opacity: 0; } } | |
| 1425 | + | .tour-shimmer { animation: tour-shimmer 1.2s ease-in-out infinite; } | |
| 1426 | + | @keyframes tour-shimmer { 50% { opacity: .45; } } | |
| 1192 | 1427 | @media (prefers-reduced-motion: reduce) { | |
| 1193 | − | .tour-in, .tour-pop, .tour-fade, .tour-spin { animation: none; } | |
| 1428 | + | .tour-in, .tour-pop, .tour-fade, .tour-spin, .tour-caret, .tour-shimmer { animation: none; } | |
| 1194 | 1429 | } | |
| 1195 | 1430 | `; | |
| 1196 | 1431 | ||
| ⋯ | |||
| 1206 | 1441 | <style dangerouslySetInnerHTML={{ __html: STYLES }} /> | |
| 1207 | 1442 | <div | |
| 1208 | 1443 | role="img" | |
| 1209 | − | aria-label="A tour of g1t: a pull request passes its checks, is approved and is merged by the merge queue, its session from the coding agent that helped write it is replayed, the team hears it shipped in a channel, and the docs page is updated." | |
| 1444 | + | aria-label="A tour of g1t: a pull request passes its checks, is approved and is merged by the merge queue, and its session from the coding agent that helped write it is replayed. Back on Home, notifications that settled themselves leave Where you're needed, Start here picks the pull request ready to merge, and What changed sums up in plain English what landed and that it is live. Then the team hears it shipped in a channel." | |
| 1210 | 1445 | onMouseEnter={() => clock.hover(true)} | |
| 1211 | 1446 | onMouseLeave={() => clock.hover(false)} | |
| 1212 | 1447 | > | |
| ⋯ | |||
| 1252 | 1487 | </div> | |
| 1253 | 1488 | <p className="mx-auto mt-3 max-w-xl text-center text-xs leading-5 text-faint text-balance"> | |
| 1254 | 1489 | Everything here works today. Priya wrote the fix with Claude Code, connected to g1t through its MCP server: the | |
| 1255 | − | commits carry its mark, and the session it recorded stays on the pull request. | |
| 1490 | + | commits carry its mark, and the session it recorded stays on the pull request. Home's summary runs on the | |
| 1491 | + | workspace's own AI provider. | |
| 1256 | 1492 | </p> | |
| 1257 | 1493 | </div> | |
| 1258 | 1494 | ); | |
| 1 | 1 | import assert from "node:assert/strict"; | |
| 2 | 2 | import { test } from "node:test"; | |
| 3 | 3 | ||
| 4 | − | import { BEAT, CHECKS, LOOP_MS, PILLS, SESSION, STILLS, frameAt, pillAt, pillProgress, sameFrame } from "./tour.ts"; | |
| 4 | + | import { BEAT, CHECKS, LOOP_MS, PILLS, SESSION, SETTLED, STILLS, SUMMARIZED, SUMMARY_WORDS, frameAt, pillAt, pillProgress, sameFrame } from "./tour.ts"; | |
| 5 | 5 | ||
| 6 | 6 | test("the first frame is the pull request before its checks, the same on the server and the client", () => { | |
| 7 | 7 | const frame = frameAt(0); | |
| ⋯ | |||
| 18 | 18 | const scene = frameAt(t).scene; | |
| 19 | 19 | if (order[order.length - 1] !== scene) order.push(scene); | |
| 20 | 20 | } | |
| 21 | − | assert.deepEqual(order, ["code", "session", "chat", "docs"]); | |
| 21 | + | assert.deepEqual(order, ["code", "session", "home", "chat"]); | |
| 22 | 22 | }); | |
| 23 | 23 | ||
| 24 | 24 | test("checks pass before review, and the queue merges only after approval", () => { | |
| ⋯ | |||
| 35 | 35 | assert.equal(frameAt(BEAT.railChatClick).click, true); | |
| 36 | 36 | }); | |
| 37 | 37 | ||
| 38 | − | test("the session plays back in full before the tour moves on to chat", () => { | |
| 38 | + | test("the session plays back in full before the tour moves on to Home", () => { | |
| 39 | 39 | assert.equal(frameAt(BEAT.session - 1).entries, 0); | |
| 40 | − | assert.equal(frameAt(BEAT.railChatClick - 1).entries, SESSION.length); | |
| 40 | + | assert.equal(frameAt(BEAT.railHomeClick - 1).entries, SESSION.length); | |
| 41 | 41 | assert.equal(SESSION[0].kind, "prompt"); | |
| 42 | 42 | }); | |
| 43 | 43 | ||
| ⋯ | |||
| 60 | 60 | assert.equal(frameAt(STILLS.code).approved, true); | |
| 61 | 61 | assert.equal(frameAt(STILLS.session).entries, SESSION.length); | |
| 62 | 62 | assert.equal(frameAt(STILLS.chat).told, true); | |
| 63 | − | assert.equal(frameAt(STILLS.docs).docUpdated, true); | |
| 63 | + | assert.equal(frameAt(STILLS.home).words, SUMMARY_WORDS.length); | |
| 64 | + | assert.equal(frameAt(STILLS.home).cleared, SETTLED.length); | |
| 64 | 65 | }); | |
| 65 | 66 | ||
| 66 | 67 | test("Dana tells support only after Priya says it shipped", () => { | |
| 67 | 68 | assert.equal(frameAt(BEAT.told - 1).told, false); | |
| 68 | 69 | assert.equal(frameAt(BEAT.told).shipped, true); | |
| 69 | 70 | }); | |
| 71 | + | ||
| 72 | + | test("on Home, notifications settle and leave before Summarize, and the summary finishes before chat", () => { | |
| 73 | + | assert.equal(frameAt(BEAT.home - 1).settled, 0); | |
| 74 | + | assert.equal(frameAt(BEAT.home + 1).settled, 0); | |
| 75 | + | const before = frameAt(BEAT.summarizeClick); | |
| 76 | + | assert.equal(before.settled, SETTLED.length); | |
| 77 | + | assert.equal(before.cleared, SETTLED.length); | |
| 78 | + | assert.equal(before.cursor, "home-summarize"); | |
| 79 | + | assert.equal(before.click, true); | |
| 80 | + | assert.equal(frameAt(BEAT.word0 - 1).words, 0); | |
| 81 | + | assert.ok(SUMMARIZED < BEAT.railChatClick - 1500); | |
| 82 | + | assert.equal(frameAt(BEAT.railChatClick - 1).words, SUMMARY_WORDS.length); | |
| 83 | + | assert.equal(frameAt(BEAT.live - 1).live, false); | |
| 84 | + | assert.equal(frameAt(BEAT.live).live, true); | |
| 85 | + | }); | |
| 7 | 7 | * The story is about the code. Priya's pull request #431 runs its checks, | |
| 8 | 8 | * is approved by Dana and is merged by the queue, and production deploys. | |
| 9 | 9 | * Then how it was made: the session Priya's coding agent recorded onto the | |
| 10 | − | * pull request through g1t's MCP server. Then the people: #web, where Dana | |
| 11 | − | * reported the bug and hears it shipped. Last, the docs page Dana updates. | |
| 10 | + | * pull request through g1t's MCP server. Then Priya comes back to Home: | |
| 11 | + | * the notifications that settled themselves leave Where you're needed, | |
| 12 | + | * Start here picks what is left, and What changed tells her in plain | |
| 13 | + | * English what landed and that #431 is live. Last, the people: #web, where | |
| 14 | + | * Dana reported the bug and hears it shipped. | |
| 12 | 15 | */ | |
| 13 | 16 | ||
| 14 | − | export type Scene = "code" | "session" | "chat" | "docs"; | |
| 17 | + | export type Scene = "code" | "session" | "home" | "chat"; | |
| 15 | 18 | ||
| 16 | 19 | /** Where the soft cursor points: an element marked `data-tour="<target>"`. */ | |
| 17 | − | export type CursorTarget = "pr-session" | "rail-code" | "rail-chat" | "rail-docs" | "idle"; | |
| 20 | + | export type CursorTarget = "pr-session" | "rail-code" | "rail-home" | "home-summarize" | "home-explain" | "rail-chat" | "idle"; | |
| 18 | 21 | ||
| 19 | 22 | export type Frame = { | |
| 20 | 23 | scene: Scene; | |
| ⋯ | |||
| 28 | 31 | merged: boolean; | |
| 29 | 32 | /** Entries of the recorded session shown, 0 to SESSION.length. */ | |
| 30 | 33 | entries: number; | |
| 34 | + | /** Notifications on Home that have settled themselves, 0 to SETTLED.length. */ | |
| 35 | + | settled: number; | |
| 36 | + | /** Settled rows that have left Where you're needed, 0 to SETTLED.length. */ | |
| 37 | + | cleared: number; | |
| 38 | + | /** Home has scrolled to What changed, as Summarize brings it into view. */ | |
| 39 | + | summarizing: boolean; | |
| 40 | + | /** Words of What changed's summary shown, 0 to SUMMARY_WORDS.length. */ | |
| 41 | + | words: number; | |
| 42 | + | /** The summary is finished: Ask a follow-up and Summarize again show. */ | |
| 43 | + | summarized: boolean; | |
| 44 | + | /** #431 is live in production (Landed says so). */ | |
| 45 | + | live: boolean; | |
| 31 | 46 | /** Priya posted in #web that it shipped. */ | |
| 32 | 47 | shipped: boolean; | |
| 33 | 48 | /** Dana says support has been told, in #web. */ | |
| 34 | 49 | told: boolean; | |
| 35 | − | /** The docs page shows the changed line. */ | |
| 36 | − | docUpdated: boolean; | |
| 37 | 50 | cursor: CursorTarget; | |
| 38 | 51 | /** The cursor is pressing. */ | |
| 39 | 52 | click: boolean; | |
| ⋯ | |||
| 55 | 68 | ||
| 56 | 69 | export const CHECKS = ["build", "test", "lint", "preview"]; | |
| 57 | 70 | ||
| 71 | + | /** | |
| 72 | + | * The notifications waiting on Priya that settle themselves while she | |
| 73 | + | * looks, each with the reason the events service writes when it settles | |
| 74 | + | * (services/events/src/inbox.rs). | |
| 75 | + | */ | |
| 76 | + | export const SETTLED: { title: string; detail: string; why: string }[] = [ | |
| 77 | + | { title: "Deploy failed on main", detail: "acme/web main · Production on an older build", why: "Production of web deployed at 3f9c2a1" }, | |
| 78 | + | { title: "CI failed on csv-export", detail: "acme/web csv-export · Run 412", why: "CI passed in run 413" }, | |
| 79 | + | ]; | |
| 80 | + | ||
| 81 | + | /** What Changed's answer, as g1t streams it onto Home. */ | |
| 82 | + | export const SUMMARY = | |
| 83 | + | "Two changes landed in acme/web since you were last here. CSV exports now stream in batches of 1,000 rows (#431), so the largest accounts no longer time out; it went live 9 minutes after the merge queue merged it. Jordan pinned the build image's cache key, which fixed last night's failed deploy. Your webhook retries (#433) are ready to merge."; | |
| 84 | + | ||
| 85 | + | export const SUMMARY_WORDS = SUMMARY.split(" "); | |
| 86 | + | ||
| 58 | 87 | /** The beats of the story, in ms from the start of the loop. */ | |
| 59 | 88 | export const BEAT = { | |
| 60 | 89 | code: 0, | |
| ⋯ | |||
| 66 | 95 | sessionClick: 6300, | |
| 67 | 96 | session: 6450, | |
| 68 | 97 | entry0: 6700, | |
| 69 | − | entryGap: 500, | |
| 70 | − | railChatClick: 10000, | |
| 71 | − | chat: 10150, | |
| 72 | − | shipped: 11000, | |
| 73 | − | told: 12200, | |
| 74 | − | railDocsClick: 14000, | |
| 75 | − | docs: 14150, | |
| 76 | − | docUpdated: 15300, | |
| 77 | − | end: 20000, | |
| 98 | + | entryGap: 450, | |
| 99 | + | railHomeClick: 9300, | |
| 100 | + | home: 9450, | |
| 101 | + | settle0: 10300, | |
| 102 | + | settleGap: 650, | |
| 103 | + | /** How long a settled row stays, saying why, before it leaves the list. */ | |
| 104 | + | clearAfter: 1100, | |
| 105 | + | summarizeClick: 12700, | |
| 106 | + | summarizing: 12850, | |
| 107 | + | word0: 13500, | |
| 108 | + | wordGap: 48, | |
| 109 | + | live: 14600, | |
| 110 | + | railChatClick: 18200, | |
| 111 | + | chat: 18350, | |
| 112 | + | shipped: 19200, | |
| 113 | + | told: 20400, | |
| 114 | + | end: 23500, | |
| 78 | 115 | } as const; | |
| 79 | 116 | ||
| 117 | + | /** When the summary has finished streaming. */ | |
| 118 | + | export const SUMMARIZED = BEAT.word0 + SUMMARY_WORDS.length * BEAT.wordGap + 200; | |
| 119 | + | ||
| 80 | 120 | /** The whole loop, in ms. */ | |
| 81 | 121 | export const LOOP_MS = BEAT.end; | |
| 82 | 122 | ||
| ⋯ | |||
| 84 | 124 | export const PILLS: { scene: Scene; label: string; start: number }[] = [ | |
| 85 | 125 | { scene: "code", label: "Pull request", start: 0 }, | |
| 86 | 126 | { scene: "session", label: "Session", start: BEAT.session }, | |
| 127 | + | { scene: "home", label: "Home", start: BEAT.home }, | |
| 87 | 128 | { scene: "chat", label: "Chat", start: BEAT.chat }, | |
| 88 | − | { scene: "docs", label: "Artifacts", start: BEAT.docs }, | |
| 89 | 129 | ]; | |
| 90 | 130 | ||
| 91 | 131 | /** Which pill a time falls under. */ | |
| ⋯ | |||
| 115 | 155 | const press = (at: number) => t >= at && t < at + 180; | |
| 116 | 156 | const moves: [number, CursorTarget][] = [ | |
| 117 | 157 | [BEAT.merged + 300, "pr-session"], | |
| 118 | − | [BEAT.session + 2600, "rail-chat"], | |
| 119 | − | [BEAT.told + 500, "rail-docs"], | |
| 120 | − | [BEAT.docs + 500, "idle"], | |
| 158 | + | [BEAT.session + 1900, "rail-home"], | |
| 159 | + | [BEAT.home + 400, "idle"], | |
| 160 | + | [BEAT.summarizeClick - 900, "home-summarize"], | |
| 161 | + | [BEAT.summarizing + 300, "idle"], | |
| 162 | + | [SUMMARIZED + 300, "home-explain"], | |
| 163 | + | [BEAT.railChatClick - 900, "rail-chat"], | |
| 164 | + | [BEAT.chat + 500, "idle"], | |
| 121 | 165 | ]; | |
| 122 | 166 | let cursor: CursorTarget = "idle"; | |
| 123 | 167 | for (const [at, target] of moves) if (t >= at) cursor = target; | |
| 124 | − | const click = press(BEAT.sessionClick) || press(BEAT.railChatClick) || press(BEAT.railDocsClick); | |
| 168 | + | const click = press(BEAT.sessionClick) || press(BEAT.railHomeClick) || press(BEAT.summarizeClick) || press(BEAT.railChatClick); | |
| 125 | 169 | return { cursor, click }; | |
| 126 | 170 | } | |
| 127 | 171 | ||
| 128 | 172 | /** The frame at time t (ms into the loop). */ | |
| 129 | 173 | export function frameAt(time: number): Frame { | |
| 130 | 174 | const t = ((time % LOOP_MS) + LOOP_MS) % LOOP_MS; | |
| 131 | − | const scene: Scene = t >= BEAT.docs ? "docs" : t >= BEAT.chat ? "chat" : t >= BEAT.session ? "session" : "code"; | |
| 175 | + | const scene: Scene = t >= BEAT.chat ? "chat" : t >= BEAT.home ? "home" : t >= BEAT.session ? "session" : "code"; | |
| 132 | 176 | const { cursor, click } = cursorAt(t); | |
| 177 | + | // Home is what Priya comes back to: nothing on it moves until she is there. | |
| 178 | + | const onHome = t >= BEAT.home; | |
| 133 | 179 | return { | |
| 134 | 180 | scene, | |
| 135 | 181 | checks: count(t, BEAT.check0, BEAT.checkGap, CHECKS.length), | |
| ⋯ | |||
| 138 | 184 | merged: t >= BEAT.merged, | |
| 139 | 185 | // The session was recorded before the story opens; the tour plays it back once its tab is opened. | |
| 140 | 186 | entries: t < BEAT.session ? 0 : count(t, BEAT.entry0, BEAT.entryGap, SESSION.length), | |
| 187 | + | settled: onHome ? count(t, BEAT.settle0, BEAT.settleGap, SETTLED.length) : 0, | |
| 188 | + | cleared: onHome ? count(t, BEAT.settle0 + BEAT.clearAfter, BEAT.settleGap, SETTLED.length) : 0, | |
| 189 | + | summarizing: t >= BEAT.summarizing, | |
| 190 | + | words: count(t, BEAT.word0, BEAT.wordGap, SUMMARY_WORDS.length), | |
| 191 | + | summarized: t >= SUMMARIZED, | |
| 192 | + | live: t >= BEAT.live, | |
| 141 | 193 | shipped: t >= BEAT.shipped, | |
| 142 | 194 | told: t >= BEAT.told, | |
| 143 | − | docUpdated: t >= BEAT.docUpdated, | |
| 144 | 195 | cursor, | |
| 145 | 196 | click, | |
| 146 | 197 | cursorShown: t >= BEAT.merged && t < BEAT.end - 600, | |
| ⋯ | |||
| 150 | 201 | /** A still frame for each pill, for reduced motion: the moment the step makes its point. */ | |
| 151 | 202 | export const STILLS: Record<Scene, number> = { | |
| 152 | 203 | code: BEAT.approved + 200, | |
| 153 | − | session: BEAT.railChatClick - 400, | |
| 204 | + | session: BEAT.railHomeClick - 400, | |
| 205 | + | home: SUMMARIZED + 400, | |
| 154 | 206 | chat: BEAT.told + 400, | |
| 155 | − | docs: BEAT.docUpdated + 400, | |
| 156 | 207 | }; | |
| 157 | 208 | ||
| 158 | 209 | /** Whether two frames would draw the same. */ | |
Comments
Sign in to comment.