Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh

Commit

Merge branch 'worktree-agent-a2ed55708e8dc0828'

syntaqxcommitted Parents89ee2f7c427b38Browse files
5 files+869−3160/5 viewed
+172−0
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&apos;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+}
+267−189
2121 import { Link } from "react-router";
2222
2323 import { DeployArt, Live, QueueArt, WhyArt } from "./art";
24+import { NeedsYouArt, WhatChangedArt } from "./landing-home";
2425 import { ProductTour } from "./product-tour";
2526 import { ButtonLink, CopyLine } from "./ui";
2627
241242 about:
242243 "Channels, direct messages and threads, live, for everyone in the company.",
243244 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/`,
251245 },
252246 {
253247 icon: <Bot size={18} />,
256250 "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.",
257251 to: `${DOCS}/guides/bring-your-own-agent/`,
258252 },
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+ },
259260 ];
260261
261262 const RAILS: { icon: ReactNode; title: string; about: string; to: string }[] = [
403404 <div className="relative mx-auto max-w-6xl px-4 pt-20 text-center sm:pt-24">
404405 <Link
405406 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"
407408 >
408409 <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>
410412 <ArrowRight size={12} className="shrink-0" />
411413 </Link>
412414 <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">
607609 a thread. Messages arrive the moment they are sent, and everyone in
608610 the company can join for free.
609611 </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>
610616 <Points
617+ columns={false}
611618 points={[
612− "Public and private channels",
619+ "Private, workspace and public channels",
613620 "Direct messages",
614621 "Threads on any message",
615622 "@mentions",
616623 "Live delivery, typing and read state",
617624 "A desktop app for Windows, macOS and Linux",
618625 {
626+ text: "An Android app with notifications: scan the QR code on",
627+ link: { to: "/download", label: "Download" },
628+ },
629+ {
619630 text: "Pull requests, checks and deploys as cards in the channel",
620631 soon: true,
621632 },
622633 { 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− },
627634 { text: "The phone app on iPhone", soon: true },
628635 ]}
629636 />
630− <More to={`${DOCS}/guides/chat/`}>How Chat works</More>
631637 </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&apos;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.
642651 </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>
660652 </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 &ldquo;What are we building today?&rdquo; 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&apos;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&apos;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&apos;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>
661835 </section>
662836
663837 {/* The rails. */}
685859 <p className="mt-2 text-sm leading-6 text-muted">{item.about}</p>
686860 </Link>
687861 ))}
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&apos;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>
781862 </div>
782863 </section>
783864
848929 </div>
849930 </section>
850931
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">
854938 <div>
855− <Eyebrow>Bring your own agent</Eyebrow>
939+ <Eyebrow>Switching</Eyebrow>
856940 <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.
858942 </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.
864947 </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&apos;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.
871952 </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+ />
886971 </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&apos;t do yet
986+ </Link>
987+ </p>
988+ </div>
911989 </div>
912990 </div>
913991 </section>
+334−98
22 * The landing page's product tour: g1t browsing itself. A faithful app
33 * frame (rail, sidebars, the real colours) plays one story on a loop, with
44 * 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.
79 *
810 * The frame is a pure function of time (lib/tour.ts). One rAF loop drives
911 * the clock and re-renders only when the frame changes; the step pills'
2325 */
2426 import {
2527 Activity,
28+ ArrowRight,
29+ ArrowUp,
2630 AtSign,
2731 BarChart3,
2832 Bell,
29− BookOpen,
3033 Bot,
3134 Box,
3235 Building2,
3336 Check,
37+ CheckCheck,
3438 ChevronDown,
3539 ChevronLeft,
3640 CircleDot,
4448 GitPullRequest,
4549 Hash,
4650 House,
51+ Loader,
4752 MessageSquare,
4853 MessagesSquare,
4954 Paperclip,
5055 PlayCircle,
5156 Plus,
5257 Rocket,
58+ RotateCcw,
5359 Search,
5460 SendHorizontal,
5561 Settings,
5662 ShieldCheck,
5763 Shapes,
64+ Sparkles,
5865 SquarePen,
5966 User,
6067 Wrench,
6471 import { codingAgentById } from "@g1t/contracts/coding-agents";
6572
6673 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";
6875 import { AgentBadge, AgentMark } from "./coding-agent";
76+import { MarkGlyph } from "./logo";
6977
7078 import { Avatar } from "./ui/avatar";
79+import { Badge } from "./ui/badge";
7180 import { Card } from "./ui/card";
7281
7382 /** The frame's design size; it scales to fit, keeping its aspect. */
192201 const HONESTY: Record<Scene, string> = {
193202 code: "Works today",
194203 session: "Works today: recorded by your coding agent through MCP",
204+ home: "Works today: summaries run on your workspace's own AI provider",
195205 chat: "Works today",
196− docs: "Works today",
197206 };
198207
199208 /** The line under the frame. Not part of the app, so it sits outside it. */
288297
289298 /** The rail's modes, in the app's order (components/rail.tsx), with its icons. */
290299 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"] },
292301 { key: "chat", icon: <MessagesSquare size={15} />, label: "Chat", target: "rail-chat", scenes: ["chat"], badge: 3 },
293302 { 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" },
296305 ];
297306
298307 /** One rail item: the square behind its icon, filled for the mode you are in, and its name under it. */
303312 data-tour={target}
304313 className={cn(
305314 "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",
307316 )}
308317 >
309318 {icon}
313322 </span>
314323 ) : null}
315324 </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>
317326 </span>
318327 );
319328 }
323332 * switcher), the modes with their names, then the workspace's own pages,
324333 * help, and you, with the dot that says how others see you.
325334 */
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;
327339 return (
328340 <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)]">
329341 <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)]">
331343 </div>
332344 <div className="mt-2.5 flex w-full flex-col items-center gap-1.5 px-1">
333345 {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} />
335347 ))}
336348 </div>
337349 <div className="mt-auto flex w-full flex-col items-center gap-1.5 px-1">
805817 );
806818 }
807819
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";
809844
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 }) {
812853 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>
833875 </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>
835899 );
836900 }
837901
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" };
839908 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} />
847934 </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&apos;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+ ))}
8681102 </div>
8691103 </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>
8801113 </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} />
8831116 </div>
8841117 </div>
8851118 );
9281161
9291162 /* Desktop ------------------------------------------------------------- */
9301163
931−const SIDES: Record<Scene, (props: { frame: Frame }) => ReactNode> = {
1164+const SIDES: Record<Scene, ((props: { frame: Frame }) => ReactNode) | null> = {
9321165 code: CodeSide,
9331166 session: CodeSide,
1167+ home: null,
9341168 chat: ChatSide,
935− docs: DocsSide,
9361169 };
9371170
9381171 /** Where each page is, as the top bar's trail says it; Chat has no top bar on a computer. */
9401173 chat: null,
9411174 code: [{ label: "acme", mono: true }, { label: "web", mono: true }, { label: "Pull request #431" }],
9421175 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" }],
9441177 };
9451178
9461179 /** The sidebar's colour, a shade lighter than the page (shell.tsx, SIDEBAR_BOX). */
9601193 observer.observe(element);
9611194 return () => observer.disconnect();
9621195 }, []);
963− const scenes: Scene[] = ["code", "session", "chat", "docs"];
1196+ const scenes: Scene[] = ["code", "session", "home", "chat"];
9641197 const mains: Record<Scene, ReactNode> = {
9651198 chat: <ChatMain frame={frame} />,
9661199 session: <SessionMain frame={frame} />,
9671200 code: <CodeMain frame={frame} />,
968− docs: <DocsMain frame={frame} />,
1201+ home: <HomeMain frame={frame} />,
9691202 };
9701203 return (
9711204 <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}` }}>
9741207 className="absolute top-0 left-0 flex origin-top-left bg-bg text-left"
9751208 style={{ width: W, height: H, transform: scale === 1 ? undefined : `scale(${scale})`, ["--tour-side" as string]: SIDE_BG } as CSSProperties}
9761209 >
977− <Rail scene={frame.scene} />
1210+ <Rail frame={frame} />
9781211 <div className="relative min-w-0 flex-1">
9791212 {scenes.map((scene) => {
9801213 const Side = SIDES[scene];
9891222 on ? "translate-y-0 opacity-100" : "pointer-events-none translate-y-1 opacity-0",
9901223 )}
9911224 >
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+ )}
9951230 <div className="flex min-w-0 flex-1 flex-col">
9961231 {crumbs && <TopBar crumbs={crumbs} />}
9971232 <div className="min-h-0 flex-1">{mains[scene]}</div>
10121247 code: { icon: <GitPullRequest size={14} />, label: "Pull request #431" },
10131248 session: { icon: <MessagesSquare size={14} />, label: "#431 · Session" },
10141249 chat: { icon: <Hash size={14} />, label: "web" },
1015− docs: { icon: <BookOpen size={14} />, label: "Exporting data" },
1250+ home: { icon: <House size={14} />, label: "Home" },
10161251 };
10171252
10181253 /** The phone's tab bar, as the app has it (components/mobile.tsx). */
10191254 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" },
10211256 { scenes: ["chat"], icon: <MessagesSquare size={16} />, label: "Chat" },
10221257 { scenes: [], icon: <Bell size={16} />, label: "Notifications" },
10231258 { scenes: ["code", "session"], icon: <Code2 size={16} />, label: "Code" },
10241259 ];
10251260
10261261 function Phone({ frame }: { frame: Frame }) {
1027− const scenes: Scene[] = ["code", "session", "chat", "docs"];
1262+ const scenes: Scene[] = ["code", "session", "home", "chat"];
10281263 return (
10291264 <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">
10301265 <div className="flex h-12 shrink-0 items-center gap-2 border-b border-line px-3">
10461281 {scene === "code" && <PhoneCode frame={frame} />}
10471282 {scene === "session" && <PhoneSession frame={frame} />}
10481283 {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 />}
10541285 </div>
10551286 );
10561287 })}
10621293 const on = tab.scenes.includes(frame.scene);
10631294 return (
10641295 <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>
10671298 </span>
10681299 );
10691300 })}
11891420 @keyframes tour-fade { from { opacity: 0; } to { opacity: 1; } }
11901421 .tour-spin { animation: tour-spin .9s linear infinite; }
11911422 @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; } }
11921427 @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; }
11941429 }
11951430 `;
11961431
12061441 <style dangerouslySetInnerHTML={{ __html: STYLES }} />
12071442 <div
12081443 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."
12101445 onMouseEnter={() => clock.hover(true)}
12111446 onMouseLeave={() => clock.hover(false)}
12121447 >
12521487 </div>
12531488 <p className="mx-auto mt-3 max-w-xl text-center text-xs leading-5 text-faint text-balance">
12541489 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&apos;s summary runs on the
1491+ workspace&apos;s own AI provider.
12561492 </p>
12571493 </div>
12581494 );
+21−5
11 import assert from "node:assert/strict";
22 import { test } from "node:test";
33
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";
55
66 test("the first frame is the pull request before its checks, the same on the server and the client", () => {
77 const frame = frameAt(0);
1818 const scene = frameAt(t).scene;
1919 if (order[order.length - 1] !== scene) order.push(scene);
2020 }
21− assert.deepEqual(order, ["code", "session", "chat", "docs"]);
21+ assert.deepEqual(order, ["code", "session", "home", "chat"]);
2222 });
2323
2424 test("checks pass before review, and the queue merges only after approval", () => {
3535 assert.equal(frameAt(BEAT.railChatClick).click, true);
3636 });
3737
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", () => {
3939 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);
4141 assert.equal(SESSION[0].kind, "prompt");
4242 });
4343
6060 assert.equal(frameAt(STILLS.code).approved, true);
6161 assert.equal(frameAt(STILLS.session).entries, SESSION.length);
6262 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);
6465 });
6566
6667 test("Dana tells support only after Priya says it shipped", () => {
6768 assert.equal(frameAt(BEAT.told - 1).told, false);
6869 assert.equal(frameAt(BEAT.told).shipped, true);
6970 });
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+});
+75−24
77 * The story is about the code. Priya's pull request #431 runs its checks,
88 * is approved by Dana and is merged by the queue, and production deploys.
99 * 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.
1215 */
1316
14−export type Scene = "code" | "session" | "chat" | "docs";
17+export type Scene = "code" | "session" | "home" | "chat";
1518
1619 /** 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";
1821
1922 export type Frame = {
2023 scene: Scene;
2831 merged: boolean;
2932 /** Entries of the recorded session shown, 0 to SESSION.length. */
3033 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;
3146 /** Priya posted in #web that it shipped. */
3247 shipped: boolean;
3348 /** Dana says support has been told, in #web. */
3449 told: boolean;
35− /** The docs page shows the changed line. */
36− docUpdated: boolean;
3750 cursor: CursorTarget;
3851 /** The cursor is pressing. */
3952 click: boolean;
5568
5669 export const CHECKS = ["build", "test", "lint", "preview"];
5770
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+
5887 /** The beats of the story, in ms from the start of the loop. */
5988 export const BEAT = {
6089 code: 0,
6695 sessionClick: 6300,
6796 session: 6450,
6897 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,
78115 } as const;
79116
117+/** When the summary has finished streaming. */
118+export const SUMMARIZED = BEAT.word0 + SUMMARY_WORDS.length * BEAT.wordGap + 200;
119+
80120 /** The whole loop, in ms. */
81121 export const LOOP_MS = BEAT.end;
82122
84124 export const PILLS: { scene: Scene; label: string; start: number }[] = [
85125 { scene: "code", label: "Pull request", start: 0 },
86126 { scene: "session", label: "Session", start: BEAT.session },
127+ { scene: "home", label: "Home", start: BEAT.home },
87128 { scene: "chat", label: "Chat", start: BEAT.chat },
88− { scene: "docs", label: "Artifacts", start: BEAT.docs },
89129 ];
90130
91131 /** Which pill a time falls under. */
115155 const press = (at: number) => t >= at && t < at + 180;
116156 const moves: [number, CursorTarget][] = [
117157 [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"],
121165 ];
122166 let cursor: CursorTarget = "idle";
123167 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);
125169 return { cursor, click };
126170 }
127171
128172 /** The frame at time t (ms into the loop). */
129173 export function frameAt(time: number): Frame {
130174 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";
132176 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;
133179 return {
134180 scene,
135181 checks: count(t, BEAT.check0, BEAT.checkGap, CHECKS.length),
138184 merged: t >= BEAT.merged,
139185 // The session was recorded before the story opens; the tour plays it back once its tab is opened.
140186 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,
141193 shipped: t >= BEAT.shipped,
142194 told: t >= BEAT.told,
143− docUpdated: t >= BEAT.docUpdated,
144195 cursor,
145196 click,
146197 cursorShown: t >= BEAT.merged && t < BEAT.end - 600,
150201 /** A still frame for each pill, for reduced motion: the moment the step makes its point. */
151202 export const STILLS: Record<Scene, number> = {
152203 code: BEAT.approved + 200,
153− session: BEAT.railChatClick - 400,
204+ session: BEAT.railHomeClick - 400,
205+ home: SUMMARIZED + 400,
154206 chat: BEAT.told + 400,
155− docs: BEAT.docUpdated + 400,
156207 };
157208
158209 /** Whether two frames would draw the same. */

Comments

Sign in to comment.