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
162 linesCodeBlameRaw
1/**
2 * The landing page's product tour: one story told as a timeline. Every
3 * frame is a pure function of the time into the loop, so the tour can be
4 * paused, resumed, jumped to a step, or shown still, and the server and
5 * the first client render agree (t = 0).
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 the people: #web, where Dana
11 * reported the bug and hears it shipped. Last, the docs page Dana updates.
12 */
13
14export type Scene = "code" | "session" | "chat" | "docs";
15
16/** Where the soft cursor points: an element marked `data-tour="<target>"`. */
17export type CursorTarget = "pr-session" | "rail-code" | "rail-chat" | "rail-docs" | "idle";
18
19export type Frame = {
20 scene: Scene;
21 /** Checks passed on the pull request, 0 to CHECKS.length. */
22 checks: number;
23 /** The diff is shown on the pull request. */
24 diff: boolean;
25 /** Dana approved. */
26 approved: boolean;
27 /** The merge queue merged it. */
28 merged: boolean;
29 /** Entries of the recorded session shown, 0 to SESSION.length. */
30 entries: number;
31 /** Priya posted in #web that it shipped. */
32 shipped: boolean;
33 /** Dana says support has been told, in #web. */
34 told: boolean;
35 /** The docs page shows the changed line. */
36 docUpdated: boolean;
37 cursor: CursorTarget;
38 /** The cursor is pressing. */
39 click: boolean;
40 /** The cursor is shown at all. */
41 cursorShown: boolean;
42};
43
44/** One entry of the session the coding agent recorded onto the pull request. */
45export type SessionEntry = { kind: "prompt" | "message" | "tool"; text: string; tool?: string; commit?: string };
46
47/** The session Priya's coding agent recorded onto #431 as it worked. */
48export const SESSION: SessionEntry[] = [
49 { kind: "prompt", text: "Big accounts time out on CSV export (Dana's report in #web). Find out why and fix it." },
50 { kind: "tool", tool: "read", text: "services/export/csv.py" },
51 { kind: "message", text: "Every row loads before the first byte is sent, so accounts over 100k rows hit the 60 s limit." },
52 { kind: "tool", tool: "bash", text: "pytest tests/test_export.py -k big_account → 3.1 s, was 61 s" },
53 { kind: "message", text: "Rows now stream in batches of 1,000. Opened #431.", commit: "3f9c2a1" },
54];
55
56export const CHECKS = ["build", "test", "lint", "preview"];
57
58/** The beats of the story, in ms from the start of the loop. */
59export const BEAT = {
60 code: 0,
61 check0: 900,
62 checkGap: 650,
63 diff: 1300,
64 approved: 3900,
65 merged: 5000,
66 sessionClick: 6300,
67 session: 6450,
68 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,
78} as const;
79
80/** The whole loop, in ms. */
81export const LOOP_MS = BEAT.end;
82
83/** The step pills under the frame: where each starts, for jumping to it. */
84export const PILLS: { scene: Scene; label: string; start: number }[] = [
85 { scene: "code", label: "Pull request", start: 0 },
86 { scene: "session", label: "Session", start: BEAT.session },
87 { scene: "chat", label: "Chat", start: BEAT.chat },
88 { scene: "docs", label: "Artifacts", start: BEAT.docs },
89];
90
91/** Which pill a time falls under. */
92export function pillAt(t: number): number {
93 let index = 0;
94 PILLS.forEach((pill, i) => {
95 if (t >= pill.start) index = i;
96 });
97 return index;
98}
99
100/** How far through its pill a time is, 0 to 1. */
101export function pillProgress(t: number): number {
102 const index = pillAt(t);
103 const start = PILLS[index].start;
104 const end = PILLS[index + 1]?.start ?? LOOP_MS;
105 return Math.min(1, Math.max(0, (t - start) / (end - start)));
106}
107
108function count(t: number, first: number, gap: number, max: number): number {
109 if (t < first) return 0;
110 return Math.min(max, Math.floor((t - first) / gap) + 1);
111}
112
113/** Where the cursor is headed, and whether it is pressing, at time t. */
114function cursorAt(t: number): { cursor: CursorTarget; click: boolean } {
115 const press = (at: number) => t >= at && t < at + 180;
116 const moves: [number, CursorTarget][] = [
117 [BEAT.merged + 300, "pr-session"],
118 [BEAT.session + 2600, "rail-chat"],
119 [BEAT.told + 500, "rail-docs"],
120 [BEAT.docs + 500, "idle"],
121 ];
122 let cursor: CursorTarget = "idle";
123 for (const [at, target] of moves) if (t >= at) cursor = target;
124 const click = press(BEAT.sessionClick) || press(BEAT.railChatClick) || press(BEAT.railDocsClick);
125 return { cursor, click };
126}
127
128/** The frame at time t (ms into the loop). */
129export function frameAt(time: number): Frame {
130 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";
132 const { cursor, click } = cursorAt(t);
133 return {
134 scene,
135 checks: count(t, BEAT.check0, BEAT.checkGap, CHECKS.length),
136 diff: t >= BEAT.diff,
137 approved: t >= BEAT.approved,
138 merged: t >= BEAT.merged,
139 // The session was recorded before the story opens; the tour plays it back once its tab is opened.
140 entries: t < BEAT.session ? 0 : count(t, BEAT.entry0, BEAT.entryGap, SESSION.length),
141 shipped: t >= BEAT.shipped,
142 told: t >= BEAT.told,
143 docUpdated: t >= BEAT.docUpdated,
144 cursor,
145 click,
146 cursorShown: t >= BEAT.merged && t < BEAT.end - 600,
147 };
148}
149
150/** A still frame for each pill, for reduced motion: the moment the step makes its point. */
151export const STILLS: Record<Scene, number> = {
152 code: BEAT.approved + 200,
153 session: BEAT.railChatClick - 400,
154 chat: BEAT.told + 400,
155 docs: BEAT.docUpdated + 400,
156};
157
158/** Whether two frames would draw the same. */
159export function sameFrame(a: Frame, b: Frame): boolean {
160 for (const key of Object.keys(a) as (keyof Frame)[]) if (a[key] !== b[key]) return false;
161 return true;
162}