The landing page starts with Home, where the day starts: the hero tour opens on Priya's Home (notifications that settled overnight leave Where you're needed, Start here picks her pull request in review, What changed says what landed and is live), then she opens #431 from the summary and watches it pass its checks, get approved and merge, then its session, then #web hearing it shipped. Home's two pillars (Where you're needed, What changed) move out of Agents into a Home section right after the facts, and One workspace names Home first. The tour no longer stamps "Works today" under each step or says "Everything here works today"; the product is live. Checked: node --test app/lib/tour.test.ts (10); apps/web tsc -b; the landing page rendered in the dev server, tour stepped through Home, #431, its session and #web.
- G1t-Session:
- ses_01m4z8d9age2yr4rdbcd4d8pwq
4 files+165−2060/4 viewed
| 201 | 201 | ); | |
| 202 | 202 | } | |
| 203 | 203 | ||
| 204 | − | /** Facts under the opening, each one true today. */ | |
| 204 | + | /** Facts under the opening. */ | |
| 205 | 205 | const FACTS: [string, string][] = [ | |
| 206 | 206 | [ | |
| 207 | 207 | "$0", | |
| ⋯ | |||
| 454 | 454 | /> | |
| 455 | 455 | </section> | |
| 456 | 456 | ||
| 457 | − | {/* Facts, each one true today. */} | |
| 457 | + | {/* Facts. */} | |
| 458 | 458 | <section className="relative z-10 mx-auto max-w-6xl px-4"> | |
| 459 | 459 | <dl className="grid gap-px overflow-hidden rounded-2xl border border-line bg-line sm:grid-cols-2 lg:grid-cols-4"> | |
| 460 | 460 | {FACTS.map(([figure, about]) => ( | |
| ⋯ | |||
| 468 | 468 | </dl> | |
| 469 | 469 | </section> | |
| 470 | 470 | ||
| 471 | + | {/* Home: where the day starts. */} | |
| 472 | + | <section id="home" className="mx-auto max-w-6xl scroll-mt-20 px-4 pt-24"> | |
| 473 | + | <div className="max-w-3xl"> | |
| 474 | + | <Eyebrow>Home</Eyebrow> | |
| 475 | + | <h2 className="mt-3 text-3xl font-semibold tracking-tight text-balance sm:text-5xl"> | |
| 476 | + | Start every day knowing where things stand. | |
| 477 | + | </h2> | |
| 478 | + | <p className="mt-5 max-w-2xl text-lg leading-8 text-muted"> | |
| 479 | + | Home is your daily update. g1t keeps track of everything your team | |
| 480 | + | and its coding agents did, clears what resolved itself, tells you | |
| 481 | + | in plain English what changed and what is live, and asks you only | |
| 482 | + | for what is yours to decide: a merge, a review, a fix. | |
| 483 | + | </p> | |
| 484 | + | </div> | |
| 485 | + | ||
| 486 | + | <div className="mt-6 divide-y divide-line"> | |
| 487 | + | <Pillar | |
| 488 | + | eyebrow="Where you're needed" | |
| 489 | + | title="Only what still needs you" | |
| 490 | + | art={<NeedsYouArt />} | |
| 491 | + | points={[ | |
| 492 | + | "Notifications that settle themselves", | |
| 493 | + | "Why each one settled, and who did it", | |
| 494 | + | "Pull requests ready for you to merge", | |
| 495 | + | "Reviews asked of you, by name", | |
| 496 | + | "Start here: the one to do first, and why", | |
| 497 | + | ]} | |
| 498 | + | more={["Home", `${DOCS}/guides/home/`]} | |
| 499 | + | > | |
| 500 | + | A notification is done once a later event resolves it: the next run | |
| 501 | + | passes, the pull request merges, the fix deploys. g1t settles it | |
| 502 | + | for you, and Home shows only what still waits on you. Start here | |
| 503 | + | picks the one to do first and opens it. | |
| 504 | + | </Pillar> | |
| 505 | + | ||
| 506 | + | <Pillar | |
| 507 | + | flip | |
| 508 | + | eyebrow="What changed" | |
| 509 | + | title="Come back to what changed, in plain English" | |
| 510 | + | art={<WhatChangedArt />} | |
| 511 | + | points={[ | |
| 512 | + | "A summary of what landed since you were last here", | |
| 513 | + | "Whether each change is live, deploying or failed", | |
| 514 | + | "Explain on any change that landed", | |
| 515 | + | "Ask a follow-up, in your direct message with g1t", | |
| 516 | + | ]} | |
| 517 | + | more={["Ask g1t", `${DOCS}/guides/ask/`]} | |
| 518 | + | > | |
| 519 | + | Home asks “What are we building today?” Press | |
| 520 | + | Summarize and What changed reads the commits, pull requests, deploys | |
| 521 | + | and conversations since you were last here, then tells you what | |
| 522 | + | changed, why, and what is live. Explain any one change, or ask a | |
| 523 | + | follow-up. It runs on your workspace's own AI provider. | |
| 524 | + | </Pillar> | |
| 525 | + | </div> | |
| 526 | + | </section> | |
| 527 | + | ||
| 471 | 528 | {/* What a workspace holds. */} | |
| 472 | 529 | <section className="mx-auto max-w-6xl px-4 pt-24"> | |
| 473 | 530 | <div className="max-w-3xl"> | |
| ⋯ | |||
| 476 | 533 | Talk, write it down, ship. | |
| 477 | 534 | </h2> | |
| 478 | 535 | <p className="mt-5 max-w-2xl text-lg leading-8 text-muted"> | |
| 479 | − | Code, Chat and Artifacts share one set of members, one sign-in and | |
| 480 | − | one bill. The coding agents you connect work as you, through MCP, | |
| 536 | + | Home, Code, Chat and Artifacts share one set of members, one | |
| 537 | + | sign-in and one bill. The coding agents you connect work as you, through MCP, | |
| 481 | 538 | the g1t CLI and the API. | |
| 482 | 539 | </p> | |
| 483 | 540 | </div> | |
| ⋯ | |||
| 649 | 706 | Agents do the work. You make the calls. | |
| 650 | 707 | </h2> | |
| 651 | 708 | <p className="mt-5 max-w-2xl text-lg leading-8 text-muted"> | |
| 652 | − | Your coding agents push the branches and open the pull requests. | |
| 653 | − | g1t keeps track of all of it, clears what resolved itself, and asks | |
| 654 | − | you only for what is yours to decide: a merge, a review, a fix. | |
| 709 | + | Your coding agents push the branches and open the pull requests, | |
| 710 | + | and their work lands through the same checks as everyone's. | |
| 711 | + | You decide what they take on and what ships. | |
| 655 | 712 | </p> | |
| 656 | − | </div> | |
| 657 | − | ||
| 658 | − | <div className="mt-6 divide-y divide-line"> | |
| 659 | − | <Pillar | |
| 660 | − | eyebrow="Where you're needed" | |
| 661 | − | title="Only what still needs you" | |
| 662 | − | art={<NeedsYouArt />} | |
| 663 | − | points={[ | |
| 664 | − | "Notifications that settle themselves", | |
| 665 | − | "Why each one settled, and who did it", | |
| 666 | − | "Pull requests ready for you to merge", | |
| 667 | − | "Reviews asked of you, by name", | |
| 668 | − | "Start here: the one to do first, and why", | |
| 669 | − | ]} | |
| 670 | − | more={["Home", `${DOCS}/guides/home/`]} | |
| 671 | − | > | |
| 672 | − | A notification is done once a later event resolves it: the next run | |
| 673 | − | passes, the pull request merges, the fix deploys. g1t settles it | |
| 674 | − | for you, and Home shows only what still waits on you. Start here | |
| 675 | − | picks the one to do first and opens it. | |
| 676 | − | </Pillar> | |
| 677 | − | ||
| 678 | − | <Pillar | |
| 679 | − | flip | |
| 680 | − | eyebrow="What changed" | |
| 681 | − | title="Come back to what changed, in plain English" | |
| 682 | − | art={<WhatChangedArt />} | |
| 683 | − | points={[ | |
| 684 | − | "A summary of what landed since you were last here", | |
| 685 | − | "Whether each change is live, deploying or failed", | |
| 686 | − | "Explain on any change that landed", | |
| 687 | − | "Ask a follow-up, in your direct message with g1t", | |
| 688 | − | ]} | |
| 689 | − | more={["Ask g1t", `${DOCS}/guides/ask/`]} | |
| 690 | − | > | |
| 691 | − | Home asks “What are we building today?” Press | |
| 692 | − | Summarize and What changed reads the commits, pull requests, deploys | |
| 693 | − | and conversations since you were last here, then tells you what | |
| 694 | − | changed, why, and what is live. Explain any one change, or ask a | |
| 695 | − | follow-up. It runs on your workspace's own AI provider. | |
| 696 | − | </Pillar> | |
| 697 | 713 | </div> | |
| 698 | 714 | ||
| 699 | 715 | <div className="mt-10 grid gap-4 lg:grid-cols-2"> | |
| 1 | 1 | /** | |
| 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 | − | * 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, 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. | |
| 4 | + | * a soft cursor that moves and clicks: Home as its author starts her day | |
| 5 | + | * (what settled by itself, what still needs her, and What changed in plain | |
| 6 | + | * English), her pull request going green and merging, the session her | |
| 7 | + | * coding agent recorded onto it, and the people in #web hearing it shipped. | |
| 9 | 8 | * | |
| 10 | 9 | * The frame is a pure function of time (lib/tour.ts). One rAF loop drives | |
| 11 | 10 | * the clock and re-renders only when the frame changes; the step pills' | |
| ⋯ | |||
| 21 | 20 | * the conversation's own header as the app does, and the app's own cards. | |
| 22 | 21 | * The coding agent is one people bring: its commits carry its mark, as | |
| 23 | 22 | * components/coding-agent.tsx draws it, and its session is the pull | |
| 24 | − | * request's Session tab. Everything the tour shows works today. | |
| 23 | + | * request's Session tab. | |
| 25 | 24 | */ | |
| 26 | 25 | import { | |
| 27 | 26 | Activity, | |
| ⋯ | |||
| 131 | 130 | const motion = window.matchMedia("(prefers-reduced-motion: reduce)"); | |
| 132 | 131 | if (motion.matches) { | |
| 133 | 132 | setReduced(true); | |
| 134 | − | const next = frameAt(STILLS.code); | |
| 133 | + | const next = frameAt(STILLS.home); | |
| 135 | 134 | shown.current = next; | |
| 136 | 135 | setFrame(next); | |
| 137 | 136 | return; | |
| ⋯ | |||
| 195 | 194 | /** The pill a frame belongs to, from what it shows (the clock's time is not React state). */ | |
| 196 | 195 | function pillOf(frame: Frame): number { | |
| 197 | 196 | return Math.max(0, PILLS.findIndex((pill) => pill.scene === frame.scene)); | |
| 198 | − | } | |
| 199 | − | ||
| 200 | − | /** What the line under the frame says about the moment. Everything in the tour works today. */ | |
| 201 | − | const HONESTY: Record<Scene, string> = { | |
| 202 | − | code: "Works today", | |
| 203 | − | session: "Works today: recorded by your coding agent through MCP", | |
| 204 | − | home: "Works today: summaries run on your workspace's own AI provider", | |
| 205 | − | chat: "Works today", | |
| 206 | − | }; | |
| 207 | − | ||
| 208 | − | /** The line under the frame. Not part of the app, so it sits outside it. */ | |
| 209 | − | function HonestyNote({ frame, className }: { frame: Frame; className?: string }) { | |
| 210 | − | const label = HONESTY[frame.scene]; | |
| 211 | − | return ( | |
| 212 | − | <span | |
| 213 | − | key={label} | |
| 214 | − | className={cn( | |
| 215 | − | "tour-fade inline-flex items-center gap-1.5 rounded-full bg-success/10 px-2.5 py-1 text-[11px] font-medium text-success ring-1 ring-success/25", | |
| 216 | − | className, | |
| 217 | − | )} | |
| 218 | − | > | |
| 219 | − | <span className="size-1.5 rounded-full bg-success" /> | |
| 220 | − | {label} | |
| 221 | − | </span> | |
| 222 | − | ); | |
| 223 | 197 | } | |
| 224 | 198 | ||
| 225 | 199 | /* ------------------------------------------------------------------ */ | |
| ⋯ | |||
| 258 | 232 | ); | |
| 259 | 233 | } | |
| 260 | 234 | ||
| 261 | − | /** Text with @mentions, #channels and #numbers picked out, as chat shows them. */ | |
| 262 | − | function Rich({ text }: { text: string }) { | |
| 235 | + | /** Text with @mentions, #channels and #numbers picked out, as chat shows them; `targets` marks some for the cursor. */ | |
| 236 | + | function Rich({ text, targets }: { text: string; targets?: Record<string, CursorTarget> }) { | |
| 263 | 237 | const parts = text.split(/(@[a-z0-9-]+|#[a-z0-9]+)/gi); | |
| 264 | 238 | return ( | |
| 265 | 239 | <> | |
| 266 | 240 | {parts.map((part, index) => | |
| 267 | 241 | /^[@#]/.test(part) ? ( | |
| 268 | − | <span key={index} className={part.startsWith("@") ? "rounded bg-accent/15 px-0.5 text-accent" : "text-accent"}> | |
| 242 | + | <span key={index} data-tour={targets?.[part]} className={part.startsWith("@") ? "rounded bg-accent/15 px-0.5 text-accent" : "text-accent"}> | |
| 269 | 243 | {part} | |
| 270 | 244 | </span> | |
| 271 | 245 | ) : ( | |
| ⋯ | |||
| 297 | 271 | ||
| 298 | 272 | /** The rail's modes, in the app's order (components/rail.tsx), with its icons. */ | |
| 299 | 273 | const RAIL: { key: string; icon: ReactNode; label: string; target?: CursorTarget; scenes?: Scene[]; badge?: number }[] = [ | |
| 300 | − | { key: "home", icon: <House size={15} />, label: "Home", target: "rail-home", scenes: ["home"] }, | |
| 274 | + | { key: "home", icon: <House size={15} />, label: "Home", scenes: ["home"] }, | |
| 301 | 275 | { key: "chat", icon: <MessagesSquare size={15} />, label: "Chat", target: "rail-chat", scenes: ["chat"], badge: 3 }, | |
| 302 | 276 | { key: "code", icon: <Code2 size={15} />, label: "Code", target: "rail-code", scenes: ["code", "session"] }, | |
| 303 | 277 | { key: "artifacts", icon: <Shapes size={15} />, label: "Artifacts" }, | |
| ⋯ | |||
| 842 | 816 | ||
| 843 | 817 | const HOME_ROW = "flex items-center gap-2.5 border-t border-line px-4 py-2"; | |
| 844 | 818 | ||
| 845 | − | /** What still waits on Priya once the notifications above settle: her pull request ready to merge, and a review asked of her. */ | |
| 819 | + | /** What still waits on Priya once the notifications above settle: her pull request in review, and a review asked of her. */ | |
| 846 | 820 | 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" }, | |
| 821 | + | { icon: <GitPullRequest size={14} className="text-success" />, title: "Stream CSV exports in batches", detail: "acme/web #431 · Checks running · Dana is reviewing", stake: "Yours", action: "Open" }, | |
| 848 | 822 | { icon: <GitPullRequest size={14} className="text-info" />, title: "Add audit log export", detail: "acme/web #435 · Your review", stake: "+212 −40", action: "Review" }, | |
| 849 | 823 | ]; | |
| 850 | 824 | ||
| ⋯ | |||
| 899 | 873 | ); | |
| 900 | 874 | } | |
| 901 | 875 | ||
| 902 | − | /** Start here: the deploy failure while it is open (it comes before anything else), then the pull request ready to merge. */ | |
| 876 | + | /** Start here: the deploy failure while it is open (it comes before anything else), then Priya's pull request. */ | |
| 903 | 877 | function StartCard({ frame }: { frame: Frame }) { | |
| 904 | 878 | const deploy = frame.settled === 0; | |
| 905 | 879 | const pick = deploy | |
| 906 | 880 | ? { 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" }; | |
| 881 | + | : { icon: <GitPullRequest size={14} className="text-success" />, title: STAYING[0].title, next: "Lands when Dana approves", why: "Dana's report in #web is waiting on it. Once its checks pass and she approves, the merge queue lands it.", button: "Open pull request" }; | |
| 908 | 882 | return ( | |
| 909 | 883 | <HomeCard | |
| 910 | 884 | title={ | |
| ⋯ | |||
| 922 | 896 | <dt className="text-muted">Next</dt> | |
| 923 | 897 | <dd className="text-fg-soft">{pick.next}</dd> | |
| 924 | 898 | <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> | |
| 899 | + | <dd className={deploy ? "text-danger" : "text-fg-soft"}>{deploy ? "Production on an older build" : "Big accounts' exports time out"}</dd> | |
| 926 | 900 | </dl> | |
| 927 | 901 | <p className="mt-2.5 rounded-lg bg-raised/60 px-2.5 py-2 text-[10.5px] leading-relaxed text-fg-soft"> | |
| 928 | 902 | <span className="font-medium text-fg">Why this one: </span> | |
| ⋯ | |||
| 962 | 936 | ) : ( | |
| 963 | 937 | <> | |
| 964 | 938 | <p className="text-[12.5px] leading-[1.6] text-fg-soft"> | |
| 965 | − | <Rich text={text} /> | |
| 939 | + | <Rich text={text} targets={{ "#431": "home-pr" }} /> | |
| 966 | 940 | {!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 | 941 | </p> | |
| 968 | 942 | <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")}> | |
| ⋯ | |||
| 982 | 956 | ); | |
| 983 | 957 | } | |
| 984 | 958 | ||
| 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 }) { | |
| 959 | + | /** Landed: each change that came in overnight with whether it is live, the agent's mark on the one it helped make, and Explain. */ | |
| 960 | + | function LandedCardMock() { | |
| 987 | 961 | 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 | − | }, | |
| 962 | + | { title: "Pin the build image's cache key", detail: "web · 1 push by jordan", agent: false, live: "Live 4 min after push", at: "8:15 AM" }, | |
| 963 | + | { title: "Retry webhook deliveries with backoff", detail: "web#433 · by alex", agent: true, live: "Live 6 min after merge", at: "7:52 AM" }, | |
| 1019 | 964 | ]; | |
| 1020 | 965 | return ( | |
| 1021 | 966 | <HomeCard title="Landed" hint="1 pull request merged · 1 push"> | |
| ⋯ | |||
| 1029 | 974 | </p> | |
| 1030 | 975 | <div className="mt-0.5 flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1"> | |
| 1031 | 976 | <span className="truncate text-[10.5px] text-muted">{row.detail}</span> | |
| 1032 | − | {row.badge} | |
| 977 | + | <Badge tone="success"> | |
| 978 | + | <Rocket size={9} /> | |
| 979 | + | {row.live} | |
| 980 | + | </Badge> | |
| 1033 | 981 | </div> | |
| 1034 | 982 | </div> | |
| 1035 | − | <span data-tour={row.explain} className="flex size-6 shrink-0 items-center justify-center rounded-md text-muted"> | |
| 983 | + | <span className="flex size-6 shrink-0 items-center justify-center rounded-md text-muted"> | |
| 1036 | 984 | <Sparkles size={12} /> | |
| 1037 | 985 | </span> | |
| 1038 | 986 | <span className="shrink-0 text-[10.5px] text-faint tabular-nums">{row.at}</span> | |
| ⋯ | |||
| 1043 | 991 | } | |
| 1044 | 992 | ||
| 1045 | 993 | /** | |
| 1046 | − | * Home, as Priya comes back to it (routes/workspace/home.tsx): "What are | |
| 994 | + | * Home, as Priya starts her day on it (routes/workspace/home.tsx): "What are | |
| 1047 | 995 | * we building today?" over the box that asks g1t, Where you're needed | |
| 1048 | 996 | * beside Start here, then Since you were last here with What changed and | |
| 1049 | 997 | * Landed. Pressing the first starter brings What changed into view, as the | |
| ⋯ | |||
| 1112 | 1060 | <p className="truncate text-[11px] text-muted">Yesterday at 6:12 PM · 15 hours</p> | |
| 1113 | 1061 | </div> | |
| 1114 | 1062 | <WhatChangedCardMock frame={frame} /> | |
| 1115 | − | <LandedCardMock frame={frame} /> | |
| 1063 | + | <LandedCardMock /> | |
| 1116 | 1064 | </div> | |
| 1117 | 1065 | </div> | |
| 1118 | 1066 | ); | |
| ⋯ | |||
| 1193 | 1141 | observer.observe(element); | |
| 1194 | 1142 | return () => observer.disconnect(); | |
| 1195 | 1143 | }, []); | |
| 1196 | − | const scenes: Scene[] = ["code", "session", "home", "chat"]; | |
| 1144 | + | const scenes: Scene[] = ["home", "code", "session", "chat"]; | |
| 1197 | 1145 | const mains: Record<Scene, ReactNode> = { | |
| 1198 | 1146 | chat: <ChatMain frame={frame} />, | |
| 1199 | 1147 | session: <SessionMain frame={frame} />, | |
| ⋯ | |||
| 1259 | 1207 | ]; | |
| 1260 | 1208 | ||
| 1261 | 1209 | function Phone({ frame }: { frame: Frame }) { | |
| 1262 | − | const scenes: Scene[] = ["code", "session", "home", "chat"]; | |
| 1210 | + | const scenes: Scene[] = ["home", "code", "session", "chat"]; | |
| 1263 | 1211 | return ( | |
| 1264 | 1212 | <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"> | |
| 1265 | 1213 | <div className="flex h-12 shrink-0 items-center gap-2 border-b border-line px-3"> | |
| ⋯ | |||
| 1431 | 1379 | ||
| 1432 | 1380 | /** | |
| 1433 | 1381 | * The hero's tour: the frame (a full app on wider screens, one pane on a | |
| 1434 | − | * phone), the step pills under it, and a line saying what is a preview. | |
| 1382 | + | * phone), the step pills under it, and a line about the story. | |
| 1435 | 1383 | */ | |
| 1436 | 1384 | export function ProductTour({ className }: { className?: string }) { | |
| 1437 | 1385 | const clock = useTourClock(); | |
| ⋯ | |||
| 1441 | 1389 | <style dangerouslySetInnerHTML={{ __html: STYLES }} /> | |
| 1442 | 1390 | <div | |
| 1443 | 1391 | role="img" | |
| 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." | |
| 1392 | + | aria-label="A tour of g1t: on Home, notifications that settled themselves overnight leave Where you're needed, Start here picks the pull request in review, and What changed sums up in plain English what landed and that it is live. The pull request then passes its checks, is approved and is merged by the merge queue, and the session from the coding agent that helped write it is replayed. Then the team hears it shipped in a channel." | |
| 1445 | 1393 | onMouseEnter={() => clock.hover(true)} | |
| 1446 | 1394 | onMouseLeave={() => clock.hover(false)} | |
| 1447 | 1395 | > | |
| ⋯ | |||
| 1451 | 1399 | <div className="md:hidden"> | |
| 1452 | 1400 | <Phone frame={frame} /> | |
| 1453 | 1401 | </div> | |
| 1454 | − | </div> | |
| 1455 | − | <div className="mt-4 flex justify-center" aria-live="polite"> | |
| 1456 | − | <HonestyNote frame={frame} /> | |
| 1457 | 1402 | </div> | |
| 1458 | − | <div className="mx-auto mt-3 grid max-w-xl grid-cols-4 gap-2" role="tablist" aria-label="Steps of the tour"> | |
| 1403 | + | <div className="mx-auto mt-4 grid max-w-xl grid-cols-4 gap-2" role="tablist" aria-label="Steps of the tour"> | |
| 1459 | 1404 | {PILLS.map((pill, index) => { | |
| 1460 | 1405 | const on = clock.pill === index; | |
| 1461 | 1406 | return ( | |
| ⋯ | |||
| 1486 | 1431 | })} | |
| 1487 | 1432 | </div> | |
| 1488 | 1433 | <p className="mx-auto mt-3 max-w-xl text-center text-xs leading-5 text-faint text-balance"> | |
| 1489 | − | Everything here works today. Priya wrote the fix with Claude Code, connected to g1t through its MCP server: the | |
| 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. | |
| 1434 | + | Priya's day starts on Home. She wrote the fix with Claude Code, connected to g1t through its MCP server: the | |
| 1435 | + | commits carry its mark, and the session it recorded stays on the pull request. | |
| 1492 | 1436 | </p> | |
| 1493 | 1437 | </div> | |
| 1494 | 1438 | ); | |
| 3 | 3 | ||
| 4 | 4 | import { BEAT, CHECKS, LOOP_MS, PILLS, SESSION, SETTLED, STILLS, SUMMARIZED, SUMMARY_WORDS, frameAt, pillAt, pillProgress, sameFrame } from "./tour.ts"; | |
| 5 | 5 | ||
| 6 | − | test("the first frame is the pull request before its checks, the same on the server and the client", () => { | |
| 6 | + | test("the first frame is Home before anything settles, the same on the server and the client", () => { | |
| 7 | 7 | const frame = frameAt(0); | |
| 8 | − | assert.equal(frame.scene, "code"); | |
| 8 | + | assert.equal(frame.scene, "home"); | |
| 9 | + | assert.equal(frame.settled, 0); | |
| 10 | + | assert.equal(frame.summarizing, false); | |
| 9 | 11 | assert.equal(frame.checks, 0); | |
| 10 | − | assert.equal(frame.merged, false); | |
| 11 | 12 | assert.equal(frame.cursorShown, false); | |
| 12 | 13 | assert.ok(sameFrame(frame, frameAt(0))); | |
| 13 | 14 | }); | |
| 14 | 15 | ||
| 15 | − | test("the scenes come in the story's order: the pull request first", () => { | |
| 16 | + | test("the scenes come in the story's order: Home first", () => { | |
| 16 | 17 | const order: string[] = []; | |
| 17 | 18 | for (let t = 0; t < LOOP_MS; t += 100) { | |
| 18 | 19 | const scene = frameAt(t).scene; | |
| 19 | 20 | if (order[order.length - 1] !== scene) order.push(scene); | |
| 20 | 21 | } | |
| 21 | − | assert.deepEqual(order, ["code", "session", "home", "chat"]); | |
| 22 | + | assert.deepEqual(order, ["home", "code", "session", "chat"]); | |
| 22 | 23 | }); | |
| 23 | 24 | ||
| 24 | 25 | test("checks pass before review, and the queue merges only after approval", () => { | |
| ⋯ | |||
| 27 | 28 | assert.equal(frameAt(BEAT.merged).approved, true); | |
| 28 | 29 | }); | |
| 29 | 30 | ||
| 30 | − | test("the cursor leaves the code only once it has merged, for the Session tab", () => { | |
| 31 | + | test("Home's #431 opens the pull request, and the cursor stays out of its way until it has merged", () => { | |
| 32 | + | assert.equal(frameAt(BEAT.prClick).cursor, "home-pr"); | |
| 33 | + | assert.equal(frameAt(BEAT.prClick).click, true); | |
| 34 | + | assert.equal(frameAt(BEAT.prClick).scene, "home"); | |
| 35 | + | assert.equal(frameAt(BEAT.code).scene, "code"); | |
| 36 | + | assert.equal(frameAt(BEAT.code).checks, 0); | |
| 31 | 37 | assert.equal(frameAt(BEAT.merged - 1).cursorShown, false); | |
| 32 | 38 | assert.equal(frameAt(BEAT.sessionClick).cursor, "pr-session"); | |
| 33 | 39 | assert.equal(frameAt(BEAT.sessionClick).click, true); | |
| ⋯ | |||
| 35 | 41 | assert.equal(frameAt(BEAT.railChatClick).click, true); | |
| 36 | 42 | }); | |
| 37 | 43 | ||
| 38 | − | test("the session plays back in full before the tour moves on to Home", () => { | |
| 44 | + | test("the session plays back in full before the tour moves on to Chat", () => { | |
| 39 | 45 | assert.equal(frameAt(BEAT.session - 1).entries, 0); | |
| 40 | − | assert.equal(frameAt(BEAT.railHomeClick - 1).entries, SESSION.length); | |
| 46 | + | assert.equal(frameAt(BEAT.railChatClick - 1).entries, SESSION.length); | |
| 41 | 47 | assert.equal(SESSION[0].kind, "prompt"); | |
| 42 | 48 | }); | |
| 43 | 49 | ||
| ⋯ | |||
| 47 | 53 | }); | |
| 48 | 54 | ||
| 49 | 55 | test("pills start where their scene does, and progress runs 0 to 1", () => { | |
| 50 | − | assert.equal(PILLS[0].scene, "code"); | |
| 56 | + | assert.equal(PILLS[0].scene, "home"); | |
| 51 | 57 | PILLS.forEach((pill, index) => { | |
| 52 | 58 | assert.equal(frameAt(pill.start + 1).scene, pill.scene); | |
| 53 | 59 | assert.equal(pillAt(pill.start + 1), index); | |
| ⋯ | |||
| 69 | 75 | assert.equal(frameAt(BEAT.told).shipped, true); | |
| 70 | 76 | }); | |
| 71 | 77 | ||
| 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); | |
| 78 | + | test("on Home, notifications settle and leave before Summarize, and the summary finishes before #431 is opened", () => { | |
| 79 | + | assert.equal(frameAt(BEAT.settle0 - 1).settled, 0); | |
| 75 | 80 | const before = frameAt(BEAT.summarizeClick); | |
| 76 | 81 | assert.equal(before.settled, SETTLED.length); | |
| 77 | 82 | assert.equal(before.cleared, SETTLED.length); | |
| 78 | 83 | assert.equal(before.cursor, "home-summarize"); | |
| 79 | 84 | assert.equal(before.click, true); | |
| 80 | 85 | 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); | |
| 86 | + | assert.ok(SUMMARIZED < BEAT.prClick - 700); | |
| 87 | + | assert.equal(frameAt(BEAT.prClick - 1).words, SUMMARY_WORDS.length); | |
| 88 | + | assert.ok(SUMMARY_WORDS.some((word) => word.includes("#431"))); | |
| 85 | 89 | }); | |
| 4 | 4 | * paused, resumed, jumped to a step, or shown still, and the server and | |
| 5 | 5 | * the first client render agree (t = 0). | |
| 6 | 6 | * | |
| 7 | − | * The story is about the code. Priya's pull request #431 runs its checks, | |
| 8 | − | * is approved by Dana and is merged by the queue, and production deploys. | |
| 9 | − | * Then how it was made: the session Priya's coding agent recorded onto the | |
| 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. | |
| 7 | + | * The story is Priya's morning. She opens Home: the notifications that | |
| 8 | + | * settled overnight leave Where you're needed, Start here picks what is | |
| 9 | + | * left, and What changed tells her in plain English what landed, that it | |
| 10 | + | * is live, and that her pull request #431 is in review. She opens #431: it | |
| 11 | + | * runs its checks, is approved by Dana and is merged by the queue, and | |
| 12 | + | * production deploys. Then how it was made: the session Priya's coding | |
| 13 | + | * agent recorded onto the pull request through g1t's MCP server. Last, the | |
| 14 | + | * people: #web, where Dana reported the bug and hears it shipped. | |
| 15 | 15 | */ | |
| 16 | 16 | ||
| 17 | − | export type Scene = "code" | "session" | "home" | "chat"; | |
| 17 | + | export type Scene = "home" | "code" | "session" | "chat"; | |
| 18 | 18 | ||
| 19 | 19 | /** Where the soft cursor points: an element marked `data-tour="<target>"`. */ | |
| 20 | − | export type CursorTarget = "pr-session" | "rail-code" | "rail-home" | "home-summarize" | "home-explain" | "rail-chat" | "idle"; | |
| 20 | + | export type CursorTarget = "home-summarize" | "home-pr" | "pr-session" | "rail-code" | "rail-chat" | "idle"; | |
| 21 | 21 | ||
| 22 | 22 | export type Frame = { | |
| 23 | 23 | scene: Scene; | |
| ⋯ | |||
| 41 | 41 | words: number; | |
| 42 | 42 | /** The summary is finished: Ask a follow-up and Summarize again show. */ | |
| 43 | 43 | summarized: boolean; | |
| 44 | − | /** #431 is live in production (Landed says so). */ | |
| 45 | − | live: boolean; | |
| 46 | 44 | /** Priya posted in #web that it shipped. */ | |
| 47 | 45 | shipped: boolean; | |
| 48 | 46 | /** Dana says support has been told, in #web. */ | |
| ⋯ | |||
| 75 | 73 | */ | |
| 76 | 74 | export const SETTLED: { title: string; detail: string; why: string }[] = [ | |
| 77 | 75 | { 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" }, | |
| 76 | + | { title: "Preview failed on webhook-retries", detail: "acme/web #433 · Run 410", why: "Alex merged #433 after run 411 passed" }, | |
| 79 | 77 | ]; | |
| 80 | 78 | ||
| 81 | 79 | /** What Changed's answer, as g1t streams it onto Home. */ | |
| 82 | 80 | 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."; | |
| 81 | + | "Two changes landed in acme/web since you were last here, and both are live. Jordan pinned the build image's cache key, which fixed last night's failed deploy, and Alex merged webhook retries (#433). Your CSV export fix (#431) is up: rows now stream in batches of 1,000, so the largest accounts no longer time out. Dana is reviewing it."; | |
| 84 | 82 | ||
| 85 | 83 | export const SUMMARY_WORDS = SUMMARY.split(" "); | |
| 86 | 84 | ||
| 87 | 85 | /** The beats of the story, in ms from the start of the loop. */ | |
| 88 | 86 | export const BEAT = { | |
| 89 | − | code: 0, | |
| 90 | − | check0: 900, | |
| 91 | − | checkGap: 650, | |
| 92 | − | diff: 1300, | |
| 93 | − | approved: 3900, | |
| 94 | − | merged: 5000, | |
| 95 | − | sessionClick: 6300, | |
| 96 | − | session: 6450, | |
| 97 | − | entry0: 6700, | |
| 98 | − | entryGap: 450, | |
| 99 | − | railHomeClick: 9300, | |
| 100 | − | home: 9450, | |
| 101 | − | settle0: 10300, | |
| 87 | + | home: 0, | |
| 88 | + | settle0: 900, | |
| 102 | 89 | settleGap: 650, | |
| 103 | 90 | /** How long a settled row stays, saying why, before it leaves the list. */ | |
| 104 | 91 | clearAfter: 1100, | |
| 105 | − | summarizeClick: 12700, | |
| 106 | − | summarizing: 12850, | |
| 107 | − | word0: 13500, | |
| 92 | + | summarizeClick: 3600, | |
| 93 | + | summarizing: 3750, | |
| 94 | + | word0: 4400, | |
| 108 | 95 | wordGap: 48, | |
| 109 | − | live: 14600, | |
| 110 | − | railChatClick: 18200, | |
| 111 | − | chat: 18350, | |
| 112 | − | shipped: 19200, | |
| 113 | − | told: 20400, | |
| 114 | − | end: 23500, | |
| 96 | + | /** #431 in What changed's answer, pressed once the answer is in. */ | |
| 97 | + | prClick: 8500, | |
| 98 | + | code: 8650, | |
| 99 | + | check0: 9550, | |
| 100 | + | checkGap: 650, | |
| 101 | + | diff: 9950, | |
| 102 | + | approved: 12550, | |
| 103 | + | merged: 13650, | |
| 104 | + | sessionClick: 14950, | |
| 105 | + | session: 15100, | |
| 106 | + | entry0: 15350, | |
| 107 | + | entryGap: 450, | |
| 108 | + | railChatClick: 18000, | |
| 109 | + | chat: 18150, | |
| 110 | + | shipped: 19000, | |
| 111 | + | told: 20200, | |
| 112 | + | end: 23300, | |
| 115 | 113 | } as const; | |
| 116 | 114 | ||
| 117 | 115 | /** When the summary has finished streaming. */ | |
| ⋯ | |||
| 122 | 120 | ||
| 123 | 121 | /** The step pills under the frame: where each starts, for jumping to it. */ | |
| 124 | 122 | export const PILLS: { scene: Scene; label: string; start: number }[] = [ | |
| 125 | − | { scene: "code", label: "Pull request", start: 0 }, | |
| 123 | + | { scene: "home", label: "Home", start: BEAT.home }, | |
| 124 | + | { scene: "code", label: "Pull request", start: BEAT.code }, | |
| 126 | 125 | { scene: "session", label: "Session", start: BEAT.session }, | |
| 127 | − | { scene: "home", label: "Home", start: BEAT.home }, | |
| 128 | 126 | { scene: "chat", label: "Chat", start: BEAT.chat }, | |
| 129 | 127 | ]; | |
| 130 | 128 | ||
| ⋯ | |||
| 154 | 152 | function cursorAt(t: number): { cursor: CursorTarget; click: boolean } { | |
| 155 | 153 | const press = (at: number) => t >= at && t < at + 180; | |
| 156 | 154 | const moves: [number, CursorTarget][] = [ | |
| 157 | − | [BEAT.merged + 300, "pr-session"], | |
| 158 | − | [BEAT.session + 1900, "rail-home"], | |
| 159 | − | [BEAT.home + 400, "idle"], | |
| 160 | 155 | [BEAT.summarizeClick - 900, "home-summarize"], | |
| 161 | 156 | [BEAT.summarizing + 300, "idle"], | |
| 162 | − | [SUMMARIZED + 300, "home-explain"], | |
| 157 | + | [SUMMARIZED + 300, "home-pr"], | |
| 158 | + | [BEAT.code + 400, "idle"], | |
| 159 | + | [BEAT.merged + 300, "pr-session"], | |
| 163 | 160 | [BEAT.railChatClick - 900, "rail-chat"], | |
| 164 | 161 | [BEAT.chat + 500, "idle"], | |
| 165 | 162 | ]; | |
| 166 | 163 | let cursor: CursorTarget = "idle"; | |
| 167 | 164 | for (const [at, target] of moves) if (t >= at) cursor = target; | |
| 168 | − | const click = press(BEAT.sessionClick) || press(BEAT.railHomeClick) || press(BEAT.summarizeClick) || press(BEAT.railChatClick); | |
| 165 | + | const click = press(BEAT.summarizeClick) || press(BEAT.prClick) || press(BEAT.sessionClick) || press(BEAT.railChatClick); | |
| 169 | 166 | return { cursor, click }; | |
| 170 | 167 | } | |
| 171 | 168 | ||
| 172 | 169 | /** The frame at time t (ms into the loop). */ | |
| 173 | 170 | export function frameAt(time: number): Frame { | |
| 174 | 171 | const t = ((time % LOOP_MS) + LOOP_MS) % LOOP_MS; | |
| 175 | − | const scene: Scene = t >= BEAT.chat ? "chat" : t >= BEAT.home ? "home" : t >= BEAT.session ? "session" : "code"; | |
| 172 | + | const scene: Scene = t >= BEAT.chat ? "chat" : t >= BEAT.session ? "session" : t >= BEAT.code ? "code" : "home"; | |
| 176 | 173 | 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; | |
| 179 | 174 | return { | |
| 180 | 175 | scene, | |
| 181 | 176 | checks: count(t, BEAT.check0, BEAT.checkGap, CHECKS.length), | |
| ⋯ | |||
| 184 | 179 | merged: t >= BEAT.merged, | |
| 185 | 180 | // The session was recorded before the story opens; the tour plays it back once its tab is opened. | |
| 186 | 181 | 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, | |
| 182 | + | settled: count(t, BEAT.settle0, BEAT.settleGap, SETTLED.length), | |
| 183 | + | cleared: count(t, BEAT.settle0 + BEAT.clearAfter, BEAT.settleGap, SETTLED.length), | |
| 189 | 184 | summarizing: t >= BEAT.summarizing, | |
| 190 | 185 | words: count(t, BEAT.word0, BEAT.wordGap, SUMMARY_WORDS.length), | |
| 191 | 186 | summarized: t >= SUMMARIZED, | |
| 192 | − | live: t >= BEAT.live, | |
| 193 | 187 | shipped: t >= BEAT.shipped, | |
| 194 | 188 | told: t >= BEAT.told, | |
| 195 | 189 | cursor, | |
| 196 | 190 | click, | |
| 197 | − | cursorShown: t >= BEAT.merged && t < BEAT.end - 600, | |
| 191 | + | // Out of the way while the pull request's checks run and it merges. | |
| 192 | + | cursorShown: (t >= BEAT.summarizeClick - 1500 && t < BEAT.code + 600) || (t >= BEAT.merged && t < BEAT.end - 600), | |
| 198 | 193 | }; | |
| 199 | 194 | } | |
| 200 | 195 | ||
| 201 | 196 | /** A still frame for each pill, for reduced motion: the moment the step makes its point. */ | |
| 202 | 197 | export const STILLS: Record<Scene, number> = { | |
| 198 | + | home: SUMMARIZED + 400, | |
| 203 | 199 | code: BEAT.approved + 200, | |
| 204 | − | session: BEAT.railHomeClick - 400, | |
| 205 | − | home: SUMMARIZED + 400, | |
| 200 | + | session: BEAT.railChatClick - 400, | |
| 206 | 201 | chat: BEAT.told + 400, | |
| 207 | 202 | }; | |
| 208 | 203 | ||
Comments
Sign in to comment.