Skip to content
1,618 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)1/**
2 * The landing page's product tour: g1t browsing itself. A faithful app
3 * frame (rail, sidebars, the real colours) plays one story on a loop, with
Merge site-audit: SEO routes, analytics with consent first in Europe, security page, founder and X account, docs header; the tour opens on the pull request and leads with Code, on the real shell, with Agents and Docs working today4 * a soft cursor that moves and clicks: the pull request going green and
5 * merging, the request in #web it came from and the ship note, the agent's
6 * sessions, the docs page.
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)7 *
8 * The frame is a pure function of time (lib/tour.ts). One rAF loop drives
9 * the clock and re-renders only when the frame changes; the step pills'
10 * progress bars are written straight to the DOM. The server renders t = 0
11 * and the clock starts after mount, so there is no hydration mismatch.
12 * Motion is transforms and opacity only. Reduced motion gets a still frame
13 * per step, chosen with the pills.
14 *
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers15 * It is an honest miniature of the app: the same rail (the workspace on
16 * the top bar's line, then Home, Code, Chat, Docs, Agents and the Inbox,
17 * with names), each mode's sidebar with its real sections and words, the
18 * same top bar (where you are, Explore, Docs, Ask g1t, the inbox and +),
19 * which Chat gives up to the conversation's own header as the app does,
20 * and the app's own cards. Parts of the story are not built yet (g1t
21 * splitting a request between colleagues, and Docs); the line under the
22 * frame says so as they play, and the caption under the steps.
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)23 */
24import {
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers25 Activity,
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)26 ArrowRight,
27 AtSign,
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers28 BarChart3,
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)29 Bell,
30 BookOpen,
31 Bot,
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers32 Box,
33 Brain,
34 Building2,
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)35 Check,
36 ChevronDown,
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers37 ChevronLeft,
38 CircleDot,
39 CircleHelp,
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)40 Code2,
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers41 Compass,
42 CornerDownRight,
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)43 FileText,
44 GitMerge,
45 GitPullRequest,
46 Hash,
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers47 House,
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)48 Inbox,
49 MessagesSquare,
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers50 Network,
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)51 Paperclip,
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers52 PlayCircle,
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)53 Plus,
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers54 Rocket,
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)55 Search,
56 SendHorizontal,
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers57 Settings,
58 ShieldCheck,
59 Sparkles,
60 SquarePen,
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)61} from "lucide-react";
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers62import { type CSSProperties, Fragment, type ReactNode, useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)63
64import { cn } from "../lib/cn";
65import { ASK, CHECKS, type CursorTarget, type Frame, LOOP_MS, PILLS, STEPS, STILLS, type Scene, frameAt, pillAt, pillProgress, sameFrame } from "../lib/tour";
66import { AgentAvatar } from "./agent-avatar";
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers67import { G1tMark } from "./orchestrator";
68import { Avatar } from "./ui";
69import { Badge } from "./ui/badge";
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)70
71/** The frame's design size; it scales to fit, keeping its aspect. */
72const W = 992;
73const H = 600;
74
75const useIsoLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
76
77/* ------------------------------------------------------------------ */
78/* The clock */
79/* ------------------------------------------------------------------ */
80
81type Clock = {
82 frame: Frame;
83 reduced: boolean;
84 /** The pill shown as current. */
85 pill: number;
86 jump: (index: number) => void;
87 hover: (on: boolean) => void;
88 /** Observed for being on screen. */
89 rootRef: React.RefObject<HTMLDivElement | null>;
90 /** The pills' progress bars, written each tick. */
91 barRefs: React.RefObject<(HTMLSpanElement | null)[]>;
92};
93
94function useTourClock(): Clock {
95 const [frame, setFrame] = useState<Frame>(() => frameAt(0));
96 const [reduced, setReduced] = useState(false);
97 const [still, setStill] = useState(0);
98 const rootRef = useRef<HTMLDivElement | null>(null);
99 const barRefs = useRef<(HTMLSpanElement | null)[]>([]);
100 const time = useRef(0);
101 const shown = useRef(frame);
102 const paused = useRef({ hover: false, offscreen: true, hidden: false });
103
104 const paint = useCallback((t: number) => {
105 const next = frameAt(t);
106 if (!sameFrame(next, shown.current)) {
107 shown.current = next;
108 setFrame(next);
109 }
110 const current = pillAt(t);
111 const progress = pillProgress(t);
112 barRefs.current.forEach((bar, index) => {
113 if (!bar) return;
114 const amount = index === current ? progress : 0;
115 bar.style.transform = `scaleX(${amount})`;
116 });
117 }, []);
118
119 useEffect(() => {
120 const motion = window.matchMedia("(prefers-reduced-motion: reduce)");
121 if (motion.matches) {
122 setReduced(true);
123 const next = frameAt(STILLS.chat);
124 shown.current = next;
125 setFrame(next);
126 return;
127 }
128 const root = rootRef.current;
129 const observer =
130 typeof IntersectionObserver === "undefined"
131 ? null
132 : new IntersectionObserver(([entry]) => {
133 paused.current.offscreen = !entry?.isIntersecting;
134 });
135 if (root && observer) observer.observe(root);
136 else paused.current.offscreen = false;
137 const visibility = () => {
138 paused.current.hidden = document.hidden;
139 };
140 visibility();
141 document.addEventListener("visibilitychange", visibility);
142
143 let last: number | null = null;
144 let raf = 0;
145 const tick = (now: number) => {
146 const { hover, offscreen, hidden } = paused.current;
147 // A long gap (a background tab, a debugger) moves time on by one frame, not by the gap.
148 if (last !== null && !hover && !offscreen && !hidden) time.current = (time.current + Math.min(now - last, 64)) % LOOP_MS;
149 last = now;
150 paint(time.current);
151 raf = requestAnimationFrame(tick);
152 };
153 raf = requestAnimationFrame(tick);
154 return () => {
155 cancelAnimationFrame(raf);
156 observer?.disconnect();
157 document.removeEventListener("visibilitychange", visibility);
158 };
159 }, [paint]);
160
161 const jump = useCallback(
162 (index: number) => {
163 const pill = PILLS[index];
164 if (reduced) {
165 setStill(index);
166 const next = frameAt(STILLS[pill.scene]);
167 shown.current = next;
168 setFrame(next);
169 return;
170 }
171 time.current = pill.start + 1;
172 paint(time.current);
173 },
174 [paint, reduced],
175 );
176
177 const hover = useCallback((on: boolean) => {
178 paused.current.hover = on;
179 }, []);
180
181 return { frame, reduced, pill: reduced ? still : pillOf(frame), jump, hover, rootRef, barRefs };
182}
183
184/** The pill a frame belongs to, from what it shows (the clock's time is not React state). */
185function pillOf(frame: Frame): number {
Merge site-audit: SEO routes, analytics with consent first in Europe, security page, founder and X account, docs header; the tour opens on the pull request and leads with Code, on the real shell, with Agents and Docs working today186 return Math.max(0, PILLS.findIndex((pill) => pill.scene === frame.scene));
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)187}
188
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers189/** What the line under the frame says about the moment: working today, or a preview. */
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)190function honesty(frame: Frame): { today: boolean; label: string } {
Docs: a workspace knowledge base people and agents write together191 if (frame.scene === "docs") return { today: false, label: "Docs works today; an agent updating a page after a merge on its own is coming soon" };
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers192 if (frame.scene === "agents") return { today: true, label: "Sessions work today" };
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)193 if (frame.scene === "code") return { today: true, label: "Works today" };
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers194 if (frame.handoff && !frame.shipped) return { today: false, label: "g1t splitting work between colleagues: coming soon" };
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)195 return { today: true, label: "Works today" };
196}
197
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers198/** The line under the frame: today, or soon. Not part of the app, so it sits outside it. */
199function HonestyNote({ frame, className }: { frame: Frame; className?: string }) {
200 const note = honesty(frame);
201 return (
202 <span
203 key={note.label}
204 className={cn(
205 "tour-fade inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-[11px] font-medium ring-1",
206 note.today ? "bg-success/10 text-success ring-success/25" : "bg-raised text-muted ring-line-strong",
207 className,
208 )}
209 >
210 <span className={cn("size-1.5 rounded-full", note.today ? "bg-success" : "bg-faint")} />
211 {note.label}
212 </span>
213 );
214}
215
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)216/* ------------------------------------------------------------------ */
217/* Small parts */
218/* ------------------------------------------------------------------ */
219
220/** The people in the story; everyone else is an agent. */
Merge branch 'chat-sidebar' into fast-push221const PEOPLE: Record<string, string> = { Priya: "Priya Shah", Dana: "Dana Ruiz", Alex: "Alex Lee" };
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)222
223/** The agents in the story: a human name, a title, and a pixel creature of their own. */
224const AGENTS: Record<string, { title: string; team: string }> = {
225 g1t: { title: "Orchestrator", team: "" },
226 Otto: { title: "Software Engineer", team: "Engineering" },
227 Margo: { title: "QA Engineer", team: "QA" },
228 Inky: { title: "Technical Writer", team: "Docs" },
Merge branch 'chat-sidebar' into fast-push229 Sam: { title: "Support Specialist", team: "Customer Support" },
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)230 Dot: { title: "Product Manager", team: "Product" },
231 David: { title: "Sales Operations", team: "Sales" },
232 Bruno: { title: "Operations Engineer", team: "Operations" },
233};
234
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers235/** A face: an agent's own creature (g1t keeps its pixel 1), or a person's avatar, as the app draws them. */
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)236function Face({ who, size = 28 }: { who: string; size?: number }) {
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers237 if (!PEOPLE[who]) return <AgentAvatar agent={{ handle: who.toLowerCase(), name: who, builtin: who === "g1t" }} size={size} />;
238 return <Avatar name={who.toLowerCase()} size={size} />;
239}
240
241/** A person's presence, as the app shows it on their avatar: active, or away. */
242function PresenceDot({ away, ring = "var(--tour-side)" }: { away?: boolean; ring?: string }) {
243 return (
244 <span
245 className={cn("absolute -right-0.5 -bottom-0.5 size-[7px] rounded-full", away ? "bg-[var(--tour-side)] shadow-[inset_0_0_0_1.5px_var(--color-faint)]" : "bg-success")}
246 style={{ boxShadow: `0 0 0 1.5px ${ring}${away ? ", inset 0 0 0 1.5px var(--color-faint)" : ""}` }}
247 />
248 );
249}
250
251/** An agent's status dot, as chat/marks.tsx draws it: working glows green, idle is quiet. */
252function AgentDot({ working }: { working: boolean }) {
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)253 return (
254 <span
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers255 className={cn(
256 "size-1.5 shrink-0 rounded-full transition-colors duration-500",
257 working ? "bg-success shadow-[0_0_0_3px_color-mix(in_srgb,var(--color-success)_18%,transparent)]" : "bg-faint/70",
258 )}
259 />
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)260 );
261}
262
263function AgentTag() {
264 return <span className="rounded bg-accent/15 px-1 py-px font-mono text-[9px] tracking-wide text-accent uppercase">Agent</span>;
265}
266
267/** Text with @mentions, #channels and #numbers picked out, as chat shows them. */
268function Rich({ text }: { text: string }) {
269 const parts = text.split(/(@[a-z0-9-]+|#[a-z0-9]+)/gi);
270 return (
271 <>
272 {parts.map((part, index) =>
273 /^[@#]/.test(part) ? (
274 <span key={index} className={part.startsWith("@") ? "rounded bg-accent/15 px-0.5 text-accent" : "text-accent"}>
275 {part}
276 </span>
277 ) : (
278 part
279 ),
280 )}
281 </>
282 );
283}
284
285function Message({ who, role, time, children, enter }: { who: string; role?: string; time: string; children: ReactNode; enter?: boolean }) {
286 const agent = !PEOPLE[who];
287 return (
288 <div className={cn("flex gap-3 px-5 py-2", enter && "tour-in")}>
289 <Face who={who} />
290 <div className="min-w-0 flex-1">
291 <p className="flex flex-wrap items-baseline gap-x-1.5 text-[13px]">
292 <span className="font-semibold text-fg">{PEOPLE[who] ?? who}</span>
293 {agent && <AgentTag />}
294 {(role ?? AGENTS[who]?.title) && <span className="text-[11px] text-faint">{role ?? AGENTS[who]?.title}</span>}
295 <span className="text-[11px] text-faint">{time}</span>
296 </p>
297 <div className="mt-0.5 space-y-2 text-[13px] leading-[1.55] text-fg-soft">{children}</div>
298 </div>
299 </div>
300 );
301}
302
303function Dots() {
304 return (
305 <span className="inline-flex items-center gap-1" aria-hidden="true">
306 {[0, 1, 2].map((i) => (
307 <span key={i} className="tour-dot size-1.5 rounded-full bg-faint" style={{ animationDelay: `${i * 160}ms` }} />
308 ))}
309 </span>
310 );
311}
312
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers313/**
314 * The session's card in #web, drawn as chat draws an agent session's card
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar315 * (components/chat/card.tsx, CardBox): it changes in place as the
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers316 * session moves, wherever the story is.
317 */
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)318function TaskCard({ frame }: { frame: Frame }) {
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers319 const done = frame.cardMerged;
320 const cost = STEPS[frame.steps - 1]?.cost ?? 0;
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)321 return (
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers322 <div className="mt-1.5 flex max-w-[26rem] items-start gap-3 rounded-xl border border-line bg-surface px-3.5 py-3">
323 <span className={cn("mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-lg transition-colors", done ? "bg-raised text-muted" : "bg-accent/10 text-accent")}>
324 <Bot size={16} />
325 </span>
326 <span className="min-w-0 grow">
327 <span className="line-clamp-2 text-[13px] font-medium text-fg">CSV export times out for big accounts</span>
328 <span className="mt-0.5 block truncate font-mono text-[11px] text-muted tabular-nums">
329 @otto · {frame.steps} of {STEPS.length} steps · ${cost.toFixed(2)}
330 </span>
331 </span>
332 <Badge key={done ? "done" : "working"} tone={done ? "success" : "accent"} className="tour-pop mt-0.5">
333 {!done && (
334 <span aria-hidden="true" className="relative flex size-1.5">
335 <span className="absolute inline-flex size-full animate-ping rounded-full bg-current opacity-60 motion-reduce:animate-none" />
336 <span className="relative inline-flex size-1.5 rounded-full bg-current" />
337 </span>
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)338 )}
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers339 {done ? "Done" : "Working"}
340 </Badge>
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)341 </div>
342 );
343}
344
345/* ------------------------------------------------------------------ */
346/* The app frame */
347/* ------------------------------------------------------------------ */
348
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers349/** The rail's modes, in the app's order (components/rail.tsx), with its icons. */
350const RAIL: { key: string; icon: ReactNode; label: string; target?: CursorTarget; scene?: Scene; badge?: number }[] = [
351 { key: "home", icon: <House size={15} />, label: "Home" },
352 { key: "code", icon: <Code2 size={15} />, label: "Code", target: "rail-code", scene: "code" },
353 { key: "chat", icon: <MessagesSquare size={15} />, label: "Chat", target: "rail-chat", scene: "chat", badge: 3 },
354 { key: "docs", icon: <BookOpen size={15} />, label: "Docs", target: "rail-docs", scene: "docs" },
355 { key: "agents", icon: <Sparkles size={15} />, label: "Agents", target: "rail-agents", scene: "agents" },
356 { key: "inbox", icon: <Inbox size={15} />, label: "Inbox", badge: 2 },
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)357];
358
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers359/** One rail item: the square behind its icon, filled for the mode you are in, and its name under it. */
360function RailItem({ icon, label, on, target, badge }: { icon: ReactNode; label: string; on: boolean; target?: CursorTarget; badge?: number }) {
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)361 return (
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers362 <span className="flex w-full flex-col items-center gap-[3px] py-0.5">
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)363 <span
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers364 data-tour={target}
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)365 className={cn(
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers366 "relative flex size-7 items-center justify-center rounded-[8px] transition-colors duration-500",
367 on ? "bg-[#2c2c33] text-fg shadow-[inset_0_0_0_1px_rgba(255,255,255,0.06)]" : "text-muted",
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)368 )}
369 >
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers370 {icon}
371 {badge ? (
372 <span className="absolute -top-1 -right-1.5 min-w-[0.875rem] rounded-full bg-accent px-[3px] text-center text-[8px] leading-[0.875rem] font-bold text-bg tabular-nums ring-[1.5px] ring-[#0b0b0d]">
373 {badge}
374 </span>
375 ) : null}
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)376 </span>
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers377 <span className={cn("text-[9px] leading-none font-medium transition-colors duration-500", on ? "text-fg" : "text-faint")}>{label}</span>
378 </span>
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)379 );
380}
381
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers382/**
383 * The rail, as the app has it: the workspace on the top bar's line (its
384 * switcher), the modes with their names, then the workspace's own pages,
385 * help, and you, with the dot that says how others see you.
386 */
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)387function Rail({ scene }: { scene: Scene }) {
388 return (
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers389 <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)]">
390 <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)]">
391 <Avatar name="acme" size={28} square />
392 </div>
393 <div className="mt-2.5 flex w-full flex-col items-center gap-1.5 px-1">
394 {RAIL.map((item) => (
395 <RailItem key={item.key} icon={item.icon} label={item.label} on={item.scene === scene} target={item.target} badge={item.badge} />
396 ))}
397 </div>
398 <div className="mt-auto flex w-full flex-col items-center gap-1.5 px-1">
399 <RailItem icon={<Building2 size={15} />} label="Workspace" on={false} />
400 <RailItem icon={<CircleHelp size={15} />} label="Help" on={false} />
401 <span className="relative mt-1">
402 <Avatar name="priya" size={26} />
403 <PresenceDot ring="#0b0b0d" />
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)404 </span>
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers405 </div>
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)406 </nav>
407 );
408}
409
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers410/**
411 * The top bar, as the app has it (components/shell.tsx): where you are,
412 * then Explore, Docs, Ask g1t, the inbox and the + menu. Chat has none on
413 * a computer: the conversation's header takes its line.
414 */
415function TopBar({ crumbs }: { crumbs: { label: string; mono?: boolean }[] }) {
416 return (
417 <div className="flex h-11 shrink-0 items-center gap-2 border-b border-line bg-bg px-4">
418 <span className="flex min-w-0 items-center gap-1.5 text-[12.5px]">
419 {crumbs.map((crumb, index) => (
420 <Fragment key={crumb.label}>
421 {index > 0 && <span className="text-line-strong">/</span>}
422 <span className={cn("truncate", index === crumbs.length - 1 ? "font-medium text-fg" : "text-muted", crumb.mono && "font-mono text-[12px]")}>{crumb.label}</span>
423 </Fragment>
424 ))}
425 </span>
426 <span className="ml-auto flex items-center gap-1 text-[12px]">
427 <span className="px-1.5 text-muted">Explore</span>
428 <span className="px-1.5 text-muted">Docs</span>
429 <span className="flex h-7 items-center gap-1.5 rounded-md border border-line px-2 text-fg/90">
430 <G1tMark size={14} />
431 Ask g1t
432 </span>
433 <span className="relative flex size-7 items-center justify-center text-muted">
434 <Bell size={14} />
435 <span className="absolute top-1 right-1 size-1.5 rounded-full bg-accent" />
436 </span>
437 <span className="mx-1 h-4 w-px bg-line" />
438 <span className="flex h-7 items-center gap-0.5 rounded-md border border-line px-1.5 text-fg/90">
439 <Plus size={13} />
440 <ChevronDown size={11} className="text-muted" />
441 </span>
442 </span>
443 </div>
444 );
445}
446
447/** A mode's sidebar heading (ModeHeader): on the top bar's line, with what it makes. */
448function SideHead({ children, action, soon }: { children: ReactNode; action?: ReactNode; soon?: boolean }) {
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)449 return (
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers450 <div className="flex h-11 shrink-0 items-center justify-between gap-2 border-b border-line pr-2 pl-3.5">
451 <span className="flex items-center gap-2 text-[13px] font-semibold text-fg">
452 {children}
453 {soon && <Soon />}
454 </span>
455 {action && <span className="flex size-6 items-center justify-center text-faint">{action}</span>}
456 </div>
457 );
458}
459
460/** A section's heading, with its fold, as Chat's sections have it. */
461function SideLabel({ children, fold = true }: { children: ReactNode; fold?: boolean }) {
462 return (
463 <p className="mt-3 mb-0.5 flex items-center gap-1 px-1 text-[10.5px] font-medium text-faint">
464 {fold && <ChevronDown size={10} />}
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)465 {children}
466 </p>
467 );
468}
469
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers470function SideRow({ on, children, bold }: { on?: boolean; children: ReactNode; bold?: boolean }) {
471 return (
472 <div className={cn("flex h-7 items-center gap-2 rounded-md px-2 text-[12px]", on ? "bg-raised text-fg" : bold ? "text-fg" : "text-muted", bold && "font-semibold")}>
473 {children}
474 </div>
475 );
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)476}
477
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers478/** The search at the top of a sidebar. */
479function SideSearch({ children, keys }: { children: ReactNode; keys?: boolean }) {
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)480 return (
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers481 <span className="flex h-7 items-center gap-2 rounded-md bg-surface px-2 text-[11px] text-faint ring-1 ring-line">
482 <Search size={12} className="shrink-0" />
483 <span className="grow truncate">{children}</span>
484 {keys && <span className="rounded bg-raised px-1 font-mono text-[9px] text-muted ring-1 ring-line">Ctrl K</span>}
485 </span>
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)486 );
487}
488
489/* Chat ---------------------------------------------------------------- */
490
491function ChatSide({ frame }: { frame: Frame }) {
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers492 // Otto works from the handoff until the merge; everyone else is idle.
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)493 const ottoBusy = frame.handoff && !frame.merged;
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers494 const margoBusy = frame.consult && !frame.approved;
Merge branch 'chat-sidebar' into fast-push495 // As the app has it: direct messages with people and agents alike, the
496 // latest first; then the agents not talked to yet.
497 const dms: { who: string; away?: boolean; status?: string; working?: boolean }[] = [
498 { who: "g1t" },
499 { who: "Dana" },
500 { who: "Margo", working: margoBusy },
501 { who: "Alex", away: true, status: "🗓️" },
502 ];
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers503 const agents: { who: string; working: boolean }[] = [
504 { who: "Otto", working: ottoBusy },
505 { who: "Inky", working: frame.docUpdated },
Merge branch 'chat-sidebar' into fast-push506 { who: "Sam", working: frame.shipped && !frame.sam },
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)507 ];
508 return (
509 <>
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers510 <SideHead action={<SquarePen size={14} />}>Chat</SideHead>
511 <div className="space-y-2 px-2 pt-2.5">
512 <SideSearch>Jump to channel or person</SideSearch>
513 <span className="grid grid-cols-3 rounded-md bg-surface p-0.5 text-center text-[10px] font-medium ring-1 ring-line">
514 <span className="rounded-[4px] bg-raised py-0.5 text-fg ring-1 ring-line-strong">All</span>
515 <span className="py-0.5 text-muted">Unread</span>
516 <span className="py-0.5 text-muted">Mentions</span>
517 </span>
518 </div>
519 <div className="px-2">
520 <SideLabel>Pinned</SideLabel>
521 <SideRow>
522 <Hash size={12} className="text-faint" />
523 <span className="flex-1">releases</span>
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)524 </SideRow>
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers525 <SideLabel>Channels</SideLabel>
526 {["web", "support", "general"].map((name) => (
527 <SideRow key={name} on={name === "web"} bold={name === "support"}>
528 <Hash size={12} className="text-faint" />
529 <span className="flex-1">{name}</span>
530 {name === "support" && <span className="min-w-4 rounded-full bg-line-strong px-1 text-center text-[9.5px] font-semibold text-fg">3</span>}
531 </SideRow>
532 ))}
Merge branch 'chat-sidebar' into fast-push533 <div className="flex h-7 items-center gap-2 px-2 text-[12px] text-faint">
534 <Compass size={12} />
535 <span className="flex-1">Browse channels</span>
536 <span className="text-[10.5px]">3</span>
537 </div>
538 <SideLabel>Direct messages</SideLabel>
539 {dms.map((dm) => {
540 const agent = dm.who in AGENTS;
541 return (
542 <SideRow key={dm.who}>
543 <span className="relative flex">
544 <Face who={dm.who} size={16} />
545 {!agent && <PresenceDot away={dm.away} />}
546 {agent && dm.working && <PresenceDot />}
547 </span>
548 <span className="min-w-0 flex-1 truncate">
549 {agent ? dm.who : PEOPLE[dm.who]}
550 {agent && <span className="text-faint"> · {dm.who === "g1t" ? "orchestrator" : AGENTS[dm.who].title}</span>}
551 {dm.status && <span className="ml-1.5 text-[11px]">{dm.status}</span>}
552 </span>
553 </SideRow>
554 );
555 })}
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers556 <SideLabel>Agents</SideLabel>
557 {agents.map((agent) => (
558 <SideRow key={agent.who}>
Merge branch 'chat-sidebar' into fast-push559 <span className="relative flex">
560 <Face who={agent.who} size={16} />
561 {agent.working && <PresenceDot />}
562 </span>
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers563 <span className="min-w-0 flex-1 truncate">
564 {agent.who}
565 <span className="text-faint"> · {AGENTS[agent.who].title}</span>
566 </span>
567 </SideRow>
568 ))}
569 </div>
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)570 </>
571 );
572}
573
574function ChatMain({ frame }: { frame: Frame }) {
575 const typing = ASK.slice(0, frame.typed);
576 return (
577 <div className="flex h-full flex-col">
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers578 <div className="flex h-11 shrink-0 items-center gap-2 border-b border-line px-5">
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)579 <Hash size={15} className="text-faint" />
580 <span className="text-sm font-semibold text-fg">web</span>
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers581 <span aria-hidden="true" className="h-3.5 w-px bg-line" />
582 <span className="truncate text-xs text-muted">The web app, its releases and its bugs</span>
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)583 <span className="ml-auto flex -space-x-1.5" aria-hidden="true">
584 {["Priya", "g1t", "Otto", "Margo", "Dana"].map((who) => (
585 <span key={who} className="rounded-lg ring-2 ring-surface">
586 <Face who={who} size={20} />
587 </span>
588 ))}
589 </span>
590 </div>
591 <div className="flex min-h-0 flex-1 flex-col justify-end overflow-hidden [mask-image:linear-gradient(to_bottom,transparent,black_2.5rem)] pb-1">
592 <div className="flex items-center gap-3 px-5 pt-1 pb-2 text-[11px] text-faint">
593 <span className="h-px flex-1 bg-line" />
594 Today
595 <span className="h-px flex-1 bg-line" />
596 </div>
597 <Message who="Priya" time="09:02">
598 <p>Morning! Thursday&apos;s release closes Wednesday night, so anything for it needs to land by then.</p>
599 </Message>
600 <Message who="Dana" time="09:12">
601 <p>Two enterprise customers hit the CSV export timeout again this morning. Both have over 100k rows.</p>
602 </Message>
603 {frame.sent && (
604 <Message who="Priya" time="09:15" enter>
605 <p>
606 <Rich text={ASK} />
607 </p>
608 </Message>
609 )}
610 {frame.g1tTyping && (
611 <div className="tour-in flex items-center gap-3 px-5 py-2 text-[12px] text-faint">
612 <Face who="g1t" size={28} />
613 <Dots />
614 g1t is typing
615 </div>
616 )}
617 {frame.handoff && (
618 <Message who="g1t" time="09:15" enter>
619 <p>
620 <Rich text="On it. @otto will make the fix and @margo will review it. I'll post here when it ships." />
621 </p>
622 <TaskCard frame={frame} />
623 </Message>
624 )}
625 {frame.shipped && (
626 <Message who="g1t" time="09:41" enter>
627 <p>
Merge branch 'chat-sidebar' into fast-push628 <Rich text="Shipped. A 200,000-row export now finishes in about 3 s. @sam, can you let #support know?" />
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)629 </p>
630 </Message>
631 )}
Merge branch 'chat-sidebar' into fast-push632 {frame.sam && (
633 <Message who="Sam" time="09:42" enter>
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)634 <p>
635 <Rich text="Done. Told #support, and Dana's two customers can export again." />
636 </p>
637 </Message>
638 )}
639 </div>
640 <div className="px-5 pt-1 pb-4">
641 <div
642 data-tour="composer"
643 className={cn(
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers644 "flex items-center gap-2 rounded-xl bg-surface px-3 py-2.5 text-[13px] ring-1 transition-shadow duration-300",
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)645 frame.typed > 0 ? "ring-accent/50 shadow-[0_0_0_3px_rgb(182_168_255/0.08)]" : "ring-line",
646 )}
647 >
648 <span className="min-w-0 flex-1 truncate">
649 {frame.typed > 0 ? (
650 <span className="text-fg">
651 {typing}
652 <span className="tour-caret ml-px inline-block h-[14px] w-px translate-y-[2px] bg-accent" />
653 </span>
654 ) : (
655 <span className="text-faint">Message #web</span>
656 )}
657 </span>
658 <AtSign size={15} className="text-faint" />
659 <Paperclip size={15} className="text-faint" />
660 <span
661 data-tour="send"
662 className={cn(
663 "flex size-7 items-center justify-center rounded-md transition-colors duration-300",
664 frame.typed === ASK.length ? "bg-accent text-bg" : "text-faint",
665 )}
666 >
667 <SendHorizontal size={14} />
668 </span>
669 </div>
670 </div>
671 </div>
672 );
673}
674
675/* Agents -------------------------------------------------------------- */
676
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers677/** The specialists the demo workspace hired, from the role templates, by team (agents-mode.tsx, orgChart). */
678const SPECIALISTS: { who: string; working?: boolean }[] = [
679 { who: "Otto", working: true },
680 { who: "Margo" },
681 { who: "Inky" },
Merge branch 'chat-sidebar' into fast-push682 { who: "Sam" },
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers683 { who: "Dot" },
684];
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)685
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers686/** Agents mode's sidebar, as the app has it: the fleet's pages, g1t, then the specialists by team. */
687function AgentsSide({ frame }: { frame: Frame }) {
688 const link = (icon: ReactNode, label: string, on = false) => (
689 <SideRow on={on}>
690 <span className="text-faint">{icon}</span>
691 {label}
692 </SideRow>
693 );
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)694 return (
695 <>
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers696 <SideHead action={<Plus size={14} />}>Agents</SideHead>
697 <div className="px-2 pt-2.5">
698 {link(<Activity size={13} />, "Overview")}
699 {link(<Network size={13} />, "Context")}
700 {link(<Brain size={13} />, "Memory")}
701 <SideLabel fold={false}>Orchestrator</SideLabel>
702 <AgentRow who="g1t" line="Orchestrator" />
703 <p className="mt-2.5 px-2 text-[10px] leading-snug text-faint">Specialists are colleagues hired into a role. g1t hands them work.</p>
704 {SPECIALISTS.map((agent) => {
705 const working = agent.who === "Otto" ? !frame.merged : agent.who === "Margo" ? frame.consult && !frame.approved : false;
706 return (
707 <Fragment key={agent.who}>
708 <SideLabel>{AGENTS[agent.who].team} · 1</SideLabel>
709 <AgentRow who={agent.who} line={working ? `Working · ${AGENTS[agent.who].title}` : AGENTS[agent.who].title} working={working} on={agent.who === "Otto"} />
710 </Fragment>
711 );
712 })}
713 <div className="mt-2 flex h-7 items-center gap-2 rounded-md border border-dashed border-line px-2 text-[12px] text-muted">
714 <Plus size={13} />
715 New agent
716 </div>
717 </div>
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)718 </>
719 );
720}
721
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers722/** One agent in Agents mode's sidebar: its face and status, name, and what it is doing. */
723function AgentRow({ who, line, working = false, on = false }: { who: string; line: string; working?: boolean; on?: boolean }) {
724 return (
725 <div className={cn("flex h-9 items-center gap-2 rounded-md px-2", on && "bg-raised")}>
726 <span className="relative shrink-0">
727 <Face who={who} size={20} />
728 {who !== "g1t" && (
729 <span className="absolute -right-0.5 -bottom-0.5 flex rounded-full ring-[1.5px] ring-[var(--tour-side)]">
730 <AgentDot working={working} />
731 </span>
732 )}
733 </span>
734 <span className="min-w-0 flex-1 leading-tight">
735 <span className="block truncate text-[12px] font-medium text-fg">{who}</span>
736 <span className="block truncate text-[10px] text-faint">{line}</span>
737 </span>
738 </div>
739 );
740}
741
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)742const FILES: { path: string; from: number }[] = [
743 { path: "services/export/csv.py", from: 3 },
744 { path: "services/export/query.py", from: 3 },
745 { path: "tests/test_export.py", from: 4 },
746];
747
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers748/** A colleague brought into the session, in the open: a line anyone can open. */
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)749function Consult({ frame, compact }: { frame: Frame; compact?: boolean }) {
750 return (
751 <div
752 className={cn(
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers753 "flex items-center gap-2.5 rounded-xl border border-line bg-surface px-3.5 py-2.5 text-[12px] transition-[opacity,transform] duration-500",
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)754 frame.consult ? "translate-y-0 opacity-100" : "translate-y-1 opacity-0",
755 )}
756 >
757 <span className="flex -space-x-1">
758 <Face who="Otto" size={18} />
759 <Face who="Margo" size={18} />
760 </span>
761 <span className="min-w-0 flex-1 truncate text-muted">
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers762 <span className="text-fg-soft">Otto brought in Margo</span> · Helper session
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)763 {!compact && <span className="text-faint">: &ldquo;Is 1,000 a safe batch size for the 200k test?&rdquo;</span>}
764 </span>
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers765 <CornerDownRight size={13} className="shrink-0 text-faint" />
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)766 </div>
767 );
768}
769
770/** The quiet tag on parts of the story that are not built yet. */
771function Soon() {
772 return (
773 <span className="shrink-0 rounded-full bg-raised px-1.5 py-0.5 font-mono text-[9px] tracking-wide whitespace-nowrap text-muted uppercase ring-1 ring-line-strong">
774 Coming soon
775 </span>
776 );
777}
778
779function AgentsMain({ frame }: { frame: Frame }) {
780 const cost = STEPS[frame.steps - 1]?.cost ?? 0;
781 return (
782 <div className="flex h-full flex-col">
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers783 <div className="flex items-center gap-3 px-5 pt-4 pb-3">
784 <span className="relative">
785 <Face who="Otto" size={36} />
786 <span className="absolute -right-0.5 -bottom-0.5 flex rounded-full ring-2 ring-bg">
787 <AgentDot working={!frame.merged} />
788 </span>
789 </span>
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)790 <div className="min-w-0">
791 <p className="flex items-center gap-2 text-sm font-semibold text-fg">
792 Otto <AgentTag />
793 </p>
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers794 <p className="text-xs text-muted">
795 <span className="font-mono">@otto</span> · Software Engineer, Engineering
796 </p>
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)797 </div>
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers798 <span className="ml-auto flex h-7 items-center gap-1.5 rounded-md bg-accent px-2.5 text-[11.5px] font-medium text-bg">
799 <MessagesSquare size={12} />
800 Message
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)801 </span>
802 </div>
803 <div className="flex gap-5 border-b border-line px-5 text-[12px]">
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers804 {["Sessions", "Memory", "Routines", "Spend", "Profile"].map((tab) => (
805 <span key={tab} className={cn("py-2", tab === "Sessions" ? "border-b-2 border-accent text-fg" : "text-faint")}>
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)806 {tab}
807 </span>
808 ))}
809 </div>
810 <div className="flex-1 space-y-3 overflow-hidden px-5 py-4">
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers811 <div className="rounded-xl border border-line bg-surface">
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)812 <div className="flex items-start gap-3 px-4 pt-3.5 pb-3">
813 <div className="min-w-0 flex-1">
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers814 <p className="flex flex-wrap items-center gap-2 text-[13px] font-medium text-fg">
815 CSV export times out for big accounts
816 <span className="inline-flex h-4 items-center rounded-[5px] bg-raised px-1.5 text-[10px] font-medium text-muted ring-1 ring-line ring-inset">Chat</span>
817 <Badge key={frame.merged ? "done" : "working"} tone={frame.merged ? "success" : "accent"} className="tour-pop">
818 {frame.merged ? "Done" : "Working"}
819 </Badge>
820 </p>
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)821 <p className="mt-0.5 text-[11px] text-faint">
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers822 asked by @priya through @g1t · <span className="text-fg-soft">#web</span> · {frame.steps * 3} steps
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)823 </p>
824 </div>
825 <div className="w-32 text-right">
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers826 <p className="text-[12px] text-fg tabular-nums">
827 ${cost.toFixed(2)} <span className="text-faint">of $5.00</span>
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)828 </p>
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers829 <span className="mt-1.5 block h-1 overflow-hidden rounded-full bg-line">
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)830 <span
831 className="block h-full origin-left rounded-full bg-accent transition-transform duration-700"
832 style={{ transform: `scaleX(${cost / 5})` }}
833 />
834 </span>
835 </div>
836 </div>
837 <ol className="space-y-1.5 border-t border-line px-4 py-3">
838 {STEPS.map((step, index) => {
839 const done = index < frame.steps;
840 const now = index === frame.steps;
841 return (
842 <li key={step.text} className={cn("flex items-center gap-2.5 text-[12.5px] transition-colors duration-300", done ? "text-fg-soft" : now ? "text-fg" : "text-faint")}>
843 <span className="flex size-4 shrink-0 items-center justify-center">
844 {done ? (
845 <span className="tour-pop flex size-4 items-center justify-center rounded-full bg-success/15 text-success">
846 <Check size={10} strokeWidth={3} />
847 </span>
848 ) : now ? (
849 <span className="tour-spin size-3.5 rounded-full border-2 border-accent/25 border-t-accent" />
850 ) : (
851 <span className="size-1.5 rounded-full bg-line-strong" />
852 )}
853 </span>
854 {step.text}
855 </li>
856 );
857 })}
858 </ol>
859 <div className="flex items-center gap-2 border-t border-line px-4 py-2.5">
860 <span className="text-[11px] text-faint">Files</span>
861 <span className="flex min-h-[20px] flex-1 flex-wrap gap-1.5">
862 {FILES.filter((file) => frame.steps >= file.from).map((file) => (
863 <span key={file.path} className="tour-pop rounded bg-raised px-1.5 py-0.5 font-mono text-[10.5px] text-muted ring-1 ring-line">
864 {file.path}
865 </span>
866 ))}
867 </span>
868 <span
869 data-tour="task-pull"
870 className={cn(
871 "flex items-center gap-1.5 rounded-md px-2 py-1 text-[12px] font-medium transition-opacity duration-500",
872 frame.steps >= STEPS.length ? "bg-accent/15 text-accent opacity-100" : "text-faint opacity-0",
873 )}
874 >
875 <GitPullRequest size={13} />
876 Pull request #431
877 <ArrowRight size={12} />
878 </span>
879 </div>
880 </div>
881 <Consult frame={frame} />
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers882 <div className="flex items-center gap-3 rounded-xl border border-line bg-surface px-4 py-3 text-[12px] text-muted">
883 <span className="text-fg-soft">Upgrade the date library across the web app</span>
884 <Badge>Queued</Badge>
885 <span className="ml-auto text-faint">Routine · Mondays</span>
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)886 </div>
887 </div>
888 </div>
889 );
890}
891
892/* Code ---------------------------------------------------------------- */
893
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers894/** Code's sidebar in a project, as the app has it (shell.tsx, RepoMenu): its pages in the order people use them. */
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)895function CodeSide() {
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers896 const row = (icon: ReactNode, label: string, count?: string, on = false) => (
897 <SideRow on={on}>
898 <span className="text-faint">{icon}</span>
899 <span className="flex-1">{label}</span>
900 {count && <span className="text-[10.5px] text-faint">{count}</span>}
901 </SideRow>
902 );
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)903 return (
904 <>
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers905 <SideHead>Code</SideHead>
906 <div className="px-2 pt-2.5">
907 <SideSearch keys>Search or jump to…</SideSearch>
908 <div className="mt-2.5 flex h-7 items-center gap-1 px-1 text-[12px] font-medium text-fg">
909 <ChevronLeft size={14} className="text-faint" />
910 All projects
911 </div>
912 <div className="mt-1 flex items-center gap-2 rounded-md px-2 py-1">
913 <span className="flex size-5 items-center justify-center rounded-md bg-raised text-muted ring-1 ring-line">
914 <Box size={11} />
915 </span>
916 <span className="font-mono text-[12px]">
917 <span className="text-faint">acme/</span>
918 <span className="font-semibold text-fg">web</span>
919 </span>
920 </div>
921 <div className="mx-2 my-2 h-px bg-line" />
922 {row(<Code2 size={13} />, "Code")}
923 {row(<CircleDot size={13} />, "Issues", "12")}
924 {row(<GitPullRequest size={13} />, "Pull requests", "3", true)}
925 {row(<Bot size={13} />, "Agents")}
926 {row(<PlayCircle size={13} />, "Workflows")}
927 <div className="mx-2 my-2 h-px bg-line" />
928 {row(<Rocket size={13} />, "Deployments")}
929 <SideRow>
930 <span className="text-faint">
931 <Activity size={13} />
932 </span>
933 <span className="flex-1">Observability</span>
934 <span className="rounded-full px-1.5 text-[9px] text-faint ring-1 ring-line">Soon</span>
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)935 </SideRow>
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers936 {row(<ShieldCheck size={13} />, "Security")}
937 {row(<BarChart3 size={13} />, "Insights")}
938 <div className="mx-2 my-2 h-px bg-line" />
939 {row(<Settings size={13} />, "Settings")}
940 </div>
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)941 </>
942 );
943}
944
945const DIFF: { sign: " " | "-" | "+"; text: string }[] = [
946 { sign: " ", text: "def export_csv(account, writer):" },
947 { sign: "-", text: " rows = list(rows_for_export(account))" },
948 { sign: "-", text: " for row in rows:" },
949 { sign: "-", text: " writer.writerow(row)" },
950 { sign: "+", text: " for batch in rows_for_export(account, batch_size=1_000):" },
951 { sign: "+", text: " writer.writerows(batch)" },
952 { sign: "+", text: " yield writer.flush()" },
953];
954
955function CodeMain({ frame }: { frame: Frame }) {
956 return (
957 <div className="flex h-full flex-col">
958 <div className="border-b border-line px-5 pt-4 pb-3">
959 <p className="text-[15px] font-semibold text-fg">
960 Stream CSV exports in batches <span className="font-normal text-faint">#431</span>
961 </p>
962 <p className="mt-1.5 flex items-center gap-2 text-[12px] text-muted">
963 <span
964 key={frame.merged ? "merged" : "open"}
965 className={cn(
966 "tour-pop flex items-center gap-1 rounded-full px-2 py-0.5 text-[11px] font-medium",
967 frame.merged ? "bg-merged/15 text-merged" : "bg-success/15 text-success",
968 )}
969 >
970 {frame.merged ? <GitMerge size={11} /> : <GitPullRequest size={11} />}
971 {frame.merged ? "Merged" : "Open"}
972 </span>
973 <span className="text-fg-soft">@otto</span> wants to merge into <span className="font-mono text-[11px] text-fg-soft">main</span> · fixes the
974 request from <span className="text-accent">#web</span>
975 </p>
976 </div>
977 <div className="grid min-h-0 flex-1 grid-cols-[1fr_15rem] gap-4 px-5 py-4">
978 <div className="min-w-0 space-y-3">
979 <div className="flex gap-3 text-[12.5px] leading-[1.6] text-fg-soft">
980 <Face who="Otto" size={24} />
981 <p>
982 Exports loaded every row before sending the first byte, so big accounts hit the 60 s limit. Rows now stream in
983 batches of 1,000. A 200,000-row account takes 3.1 s.
984 </p>
985 </div>
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers986 <div className="overflow-hidden rounded-xl border border-line bg-surface">
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)987 <p className="flex items-center gap-2 border-b border-line px-3.5 py-2 font-mono text-[11px] text-muted">
988 <FileText size={12} />
989 services/export/csv.py
990 <span className="ml-auto">
991 <span className="text-success">+3</span> <span className="text-danger">−3</span>
992 </span>
993 </p>
994 <pre className="py-1.5 font-mono text-[11px] leading-[1.75]">
995 {DIFF.map((line, index) => (
996 <div
997 key={index}
998 className={cn(
999 "px-3.5 transition-opacity duration-500",
1000 line.sign === "+" ? "bg-success/10 text-success" : line.sign === "-" ? "bg-danger/10 text-danger" : "text-muted",
1001 frame.diff ? "opacity-100" : "opacity-0",
1002 )}
1003 style={{ transitionDelay: frame.diff ? `${index * 70}ms` : "0ms" }}
1004 >
1005 <span className="mr-3 inline-block w-2 text-faint select-none">{line.sign}</span>
1006 {line.text}
1007 </div>
1008 ))}
1009 </pre>
1010 </div>
1011 <div
1012 className={cn(
1013 "flex items-center gap-3 rounded-xl px-3.5 py-2.5 text-[12px] ring-1 transition-opacity duration-500",
1014 frame.approved ? "bg-success/5 text-fg-soft opacity-100 ring-success/25" : "opacity-0 ring-line",
1015 )}
1016 >
1017 <Face who="Margo" size={22} />
1018 <span>
1019 <span className="text-fg">@margo</span> approved: batching keeps memory flat, and the 200k-row test covers it.
1020 </span>
1021 <Check size={14} className="ml-auto shrink-0 text-success" />
1022 </div>
1023 </div>
1024 <div className="space-y-3">
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers1025 <div className="rounded-xl border border-line bg-surface">
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)1026 <p className="border-b border-line px-3.5 py-2 text-[11px] font-medium text-muted">
1027 Checks · {frame.checks} of {CHECKS.length} passed
1028 </p>
1029 <ul className="space-y-2 px-3.5 py-2.5">
1030 {CHECKS.map((check, index) => {
1031 const done = index < frame.checks;
1032 return (
1033 <li key={check} className="flex items-center gap-2 text-[12px]">
1034 {done ? (
1035 <span className="tour-pop flex size-4 items-center justify-center rounded-full bg-success/15 text-success">
1036 <Check size={10} strokeWidth={3} />
1037 </span>
1038 ) : (
1039 <span className="tour-spin size-3.5 rounded-full border-2 border-warn/25 border-t-warn" />
1040 )}
1041 <span className={done ? "text-fg-soft" : "text-muted"}>{check}</span>
1042 <span className="ml-auto font-mono text-[10px] text-faint">{done ? `${18 + index * 23}s` : ""}</span>
1043 </li>
1044 );
1045 })}
1046 </ul>
1047 </div>
1048 <div
1049 className={cn(
1050 "rounded-xl px-3.5 py-3 text-[12px] ring-1 transition-colors duration-500",
1051 frame.merged ? "bg-merged/10 text-merged ring-merged/30" : frame.approved ? "bg-success/10 text-success ring-success/30" : "text-muted ring-line",
1052 )}
1053 >
1054 <p className="flex items-center gap-2 font-medium">
1055 <GitMerge size={13} />
1056 {frame.merged ? "Merged by the merge queue" : frame.approved ? "In the merge queue" : "Waiting on checks and review"}
1057 </p>
1058 <p className="mt-1 text-[11px] text-faint">{frame.merged ? "Deploying to production" : "Main only moves to what passed."}</p>
1059 </div>
1060 <div
1061 className={cn(
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers1062 "rounded-xl border border-line bg-surface px-3.5 py-3 text-[12px] transition-opacity duration-500",
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)1063 frame.checks >= CHECKS.length ? "opacity-100" : "opacity-0",
1064 )}
1065 >
1066 <p className="text-[11px] font-medium text-muted">Preview</p>
1067 <p className="mt-1 truncate font-mono text-[11px] text-accent">pr-431.web.acme.g1t.page</p>
1068 </div>
1069 </div>
1070 </div>
1071 </div>
1072 );
1073}
1074
1075/* Docs ---------------------------------------------------------------- */
1076
Docs: a workspace knowledge base people and agents write together1077/** Docs' sidebar: search, then each space's pages. */
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)1078function DocsSide() {
1079 return (
1080 <>
Docs: a workspace knowledge base people and agents write together1081 <SideHead action={<Plus size={14} />}>
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers1082 Docs
1083 </SideHead>
1084 <div className="px-2 pt-2.5">
1085 <SideSearch>Search docs</SideSearch>
1086 <SideLabel>Product</SideLabel>
1087 {["Getting started", "Billing", "Exporting data", "Integrations"].map((page) => (
1088 <SideRow key={page} on={page === "Exporting data"}>
1089 <FileText size={12} className="text-faint" />
1090 {page}
1091 </SideRow>
1092 ))}
1093 <SideLabel>Engineering</SideLabel>
1094 {["Runbooks", "Decisions"].map((page) => (
1095 <SideRow key={page}>
1096 <FileText size={12} className="text-faint" />
1097 {page}
1098 </SideRow>
1099 ))}
1100 </div>
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)1101 </>
1102 );
1103}
1104
Docs: a workspace knowledge base people and agents write together1105function DocsMain({ frame }: { frame: Frame; tag?: boolean }) {
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)1106 return (
1107 <div className="relative h-full px-10 py-7">
1108 <p className="text-[11px] text-faint">Product / Exporting data</p>
1109 <h3 className="mt-2 text-[22px] font-semibold tracking-tight text-fg">Exporting data</h3>
1110 <p className="mt-1.5 flex items-center gap-2 text-[11px] text-faint">
1111 Owned by <span className="text-fg-soft">@inky</span> ·{" "}
1112 <span key={frame.docUpdated ? "now" : "then"} className="tour-fade">
1113 {frame.docUpdated ? "updated just now from #431" : "updated 3 weeks ago"}
1114 </span>
1115 </p>
1116 <div className="mt-5 max-w-xl space-y-3 text-[13px] leading-[1.7] text-fg-soft">
1117 <p>Export any table as CSV from Settings, then Data. An export includes every row you can see, in the order shown.</p>
1118 <p className="pt-2 text-[14px] font-semibold text-fg">Large accounts</p>
1119 <div className="relative">
1120 <p
1121 className={cn(
1122 "rounded-md px-3 py-2 transition-all duration-500",
1123 frame.docUpdated ? "bg-danger/5 text-faint line-through decoration-danger/50" : "text-fg-soft",
1124 )}
1125 >
1126 Exports over 50,000 rows can time out. Split them by date range.
1127 </p>
1128 <div
1129 className={cn(
1130 "mt-1.5 rounded-md border-l-2 border-success bg-success/10 px-3 py-2 text-fg transition-all duration-700",
1131 frame.docUpdated ? "translate-y-0 opacity-100" : "translate-y-1 opacity-0",
1132 )}
1133 >
1134 Exports of any size stream in batches of 1,000 rows. A 200,000-row account finishes in about 3 seconds.
1135 </div>
1136 </div>
1137 <div
1138 className={cn(
1139 "flex items-center gap-2 pt-1 text-[11px] text-muted transition-opacity delay-300 duration-500",
1140 frame.docUpdated ? "opacity-100" : "opacity-0",
1141 )}
1142 >
1143 <Face who="Inky" size={18} />
1144 <span>
1145 Updated by <span className="text-fg">@inky</span> after <span className="text-accent">#431</span> merged
1146 </span>
1147 </div>
1148 <p className="pt-3 text-[14px] font-semibold text-fg">Scheduled exports</p>
1149 <p>Owners can send an export to a storage bucket every day or week. Each run uses the same columns as the last.</p>
1150 </div>
1151 </div>
1152 );
1153}
1154
1155/* The cursor ------------------------------------------------------------ */
1156
1157function Cursor({ frame, canvas, scale }: { frame: Frame; canvas: React.RefObject<HTMLDivElement | null>; scale: number }) {
1158 const [at, setAt] = useState<{ x: number; y: number } | null>(null);
1159 useIsoLayoutEffect(() => {
1160 const root = canvas.current;
1161 if (!root) return;
1162 const box = root.getBoundingClientRect();
1163 const target = frame.cursor === "idle" ? null : root.querySelector<HTMLElement>(`[data-tour="${frame.cursor}"]`);
1164 if (!target) {
1165 setAt({ x: W * 0.64, y: H * 0.88 });
1166 return;
1167 }
1168 const rect = target.getBoundingClientRect();
1169 // The composer is clicked towards its right, clear of the words being typed; everything else in its middle.
1170 const across = frame.cursor === "composer" ? 0.82 : 0.5;
1171 setAt({ x: (rect.left - box.left + rect.width * across) / scale, y: (rect.top - box.top + rect.height * 0.55) / scale });
1172 }, [frame.cursor, scale, canvas]);
1173 if (!at) return null;
1174 return (
1175 <div
1176 aria-hidden="true"
1177 className={cn("pointer-events-none absolute top-0 left-0 z-20 transition-[transform,opacity] duration-[900ms] ease-[cubic-bezier(.45,.05,.2,1)]", frame.cursorShown ? "opacity-100" : "opacity-0")}
1178 style={{ transform: `translate(${at.x}px, ${at.y}px)` }}
1179 >
1180 <span
1181 className={cn(
1182 "absolute -top-4 -left-4 size-8 rounded-full bg-accent/30 transition-[transform,opacity] duration-300",
1183 frame.click ? "scale-100 opacity-100" : "scale-50 opacity-0",
1184 )}
1185 />
1186 <svg
1187 width="18"
1188 height="20"
1189 viewBox="0 0 18 20"
1190 className={cn("relative drop-shadow-[0_2px_6px_rgb(0_0_0/0.5)] transition-transform duration-150", frame.click && "scale-90")}
1191 >
1192 <path d="M1.5 1.5 L1.5 15.5 L5.5 12 L8.3 18 L11 16.8 L8.3 11 L13.5 11 Z" fill="#ededef" stroke="#0f0f11" strokeWidth="1.2" strokeLinejoin="round" />
1193 </svg>
1194 </div>
1195 );
1196}
1197
1198/* Desktop ------------------------------------------------------------- */
1199
1200const SIDES: Record<Scene, (props: { frame: Frame }) => ReactNode> = {
1201 chat: ChatSide,
1202 agents: AgentsSide,
1203 code: CodeSide,
1204 docs: DocsSide,
1205};
1206
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers1207/** Where each page is, as the top bar's trail says it; Chat has no top bar on a computer. */
1208const CRUMBS: Record<Scene, { label: string; mono?: boolean }[] | null> = {
1209 chat: null,
1210 agents: [{ label: "acme", mono: true }, { label: "Agents" }],
1211 code: [{ label: "acme", mono: true }, { label: "web", mono: true }, { label: "Pull request #431" }],
1212 docs: [{ label: "acme", mono: true }, { label: "Docs" }],
1213};
1214
1215/** The sidebar's colour, a shade lighter than the page (shell.tsx, SIDEBAR_BOX). */
1216const SIDE_BG = "color-mix(in srgb, var(--color-surface) 70%, var(--color-bg))";
1217
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)1218function Desktop({ frame, reduced }: { frame: Frame; reduced: boolean }) {
1219 const outer = useRef<HTMLDivElement | null>(null);
1220 const canvas = useRef<HTMLDivElement | null>(null);
1221 const [scale, setScale] = useState(1);
1222 useEffect(() => {
1223 const element = outer.current;
1224 if (!element || typeof ResizeObserver === "undefined") return;
1225 const observer = new ResizeObserver(([entry]) => {
1226 const width = entry?.contentRect.width ?? W;
1227 if (width > 0) setScale(width / W);
1228 });
1229 observer.observe(element);
1230 return () => observer.disconnect();
1231 }, []);
Merge site-audit: SEO routes, analytics with consent first in Europe, security page, founder and X account, docs header; the tour opens on the pull request and leads with Code, on the real shell, with Agents and Docs working today1232 const scenes: Scene[] = ["code", "chat", "agents", "docs"];
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)1233 const mains: Record<Scene, ReactNode> = {
1234 chat: <ChatMain frame={frame} />,
1235 agents: <AgentsMain frame={frame} />,
1236 code: <CodeMain frame={frame} />,
1237 docs: <DocsMain frame={frame} />,
1238 };
1239 return (
1240 <div ref={outer} className="relative w-full overflow-hidden rounded-2xl shadow-2xl shadow-black/50 ring-1 ring-line" style={{ aspectRatio: `${W} / ${H}` }}>
1241 <div
1242 ref={canvas}
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers1243 className="absolute top-0 left-0 flex origin-top-left bg-bg text-left"
1244 style={{ width: W, height: H, transform: scale === 1 ? undefined : `scale(${scale})`, ["--tour-side" as string]: SIDE_BG } as CSSProperties}
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)1245 >
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers1246 <Rail scene={frame.scene} />
1247 <div className="relative min-w-0 flex-1">
1248 {scenes.map((scene) => {
1249 const Side = SIDES[scene];
1250 const on = frame.scene === scene;
1251 const crumbs = CRUMBS[scene];
1252 return (
1253 <div
1254 key={scene}
1255 aria-hidden={!on}
1256 className={cn(
1257 "absolute inset-0 flex transition-[opacity,transform] duration-500 ease-out",
1258 on ? "translate-y-0 opacity-100" : "pointer-events-none translate-y-1 opacity-0",
1259 )}
1260 >
1261 <div className="flex w-52 shrink-0 flex-col overflow-hidden border-r border-line" style={{ background: SIDE_BG }}>
1262 <Side frame={frame} />
1263 </div>
1264 <div className="flex min-w-0 flex-1 flex-col">
1265 {crumbs && <TopBar crumbs={crumbs} />}
1266 <div className="min-h-0 flex-1">{mains[scene]}</div>
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)1267 </div>
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers1268 </div>
1269 );
1270 })}
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)1271 </div>
1272 {!reduced && <Cursor frame={frame} canvas={canvas} scale={scale} />}
1273 </div>
1274 </div>
1275 );
1276}
1277
1278/* Phone --------------------------------------------------------------- */
1279
1280const MODE: Record<Scene, { icon: ReactNode; label: string }> = {
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers1281 chat: { icon: <Hash size={14} />, label: "web" },
1282 agents: { icon: <Sparkles size={14} />, label: "Otto · Sessions" },
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)1283 code: { icon: <GitPullRequest size={14} />, label: "Pull request #431" },
1284 docs: { icon: <BookOpen size={14} />, label: "Exporting data" },
1285};
1286
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers1287/** The phone's tab bar, as the app has it (components/mobile.tsx). */
1288const TABS: { scene: Scene | null; icon: ReactNode; label: string }[] = [
1289 { scene: null, icon: <House size={16} />, label: "Home" },
1290 { scene: "code", icon: <Code2 size={16} />, label: "Code" },
1291 { scene: "chat", icon: <MessagesSquare size={16} />, label: "Chat" },
1292 { scene: "agents", icon: <Sparkles size={16} />, label: "Agents" },
1293 { scene: null, icon: <Inbox size={16} />, label: "Inbox" },
1294];
1295
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)1296function Phone({ frame }: { frame: Frame }) {
Merge site-audit: SEO routes, analytics with consent first in Europe, security page, founder and X account, docs header; the tour opens on the pull request and leads with Code, on the real shell, with Agents and Docs working today1297 const scenes: Scene[] = ["code", "chat", "agents", "docs"];
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)1298 return (
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers1299 <div className="relative flex h-[34rem] w-full flex-col overflow-hidden rounded-2xl bg-bg text-left shadow-2xl shadow-black/50 ring-1 ring-line">
1300 <div className="flex h-12 shrink-0 items-center gap-2 border-b border-line px-3">
1301 <Avatar name="acme" size={26} square />
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)1302 <span key={frame.scene} className="tour-fade flex min-w-0 items-center gap-1.5 text-[13px] font-semibold text-fg">
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers1303 <span className="text-faint">{MODE[frame.scene].icon}</span>
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)1304 <span className="truncate">{MODE[frame.scene].label}</span>
1305 </span>
1306 </div>
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers1307 <div className="relative min-h-0 flex-1">
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)1308 {scenes.map((scene) => {
1309 const on = frame.scene === scene;
1310 return (
1311 <div
1312 key={scene}
1313 aria-hidden={!on}
1314 className={cn("absolute inset-0 transition-opacity duration-500", on ? "opacity-100" : "pointer-events-none opacity-0")}
1315 >
1316 {scene === "chat" && <PhoneChat frame={frame} />}
1317 {scene === "agents" && <PhoneAgents frame={frame} />}
1318 {scene === "code" && <PhoneCode frame={frame} />}
1319 {scene === "docs" && (
1320 <div className="origin-top-left scale-[0.86]" style={{ width: "116%" }}>
1321 <DocsMain frame={frame} tag={false} />
1322 </div>
1323 )}
1324 </div>
1325 );
1326 })}
1327 </div>
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers1328 {/* Inside a conversation the tab bar steps aside, as it does on a phone. */}
1329 {frame.scene !== "chat" && (
1330 <div className="grid h-12 shrink-0 grid-cols-5 border-t border-line bg-[#0b0b0d]">
1331 {TABS.map((tab) => {
1332 const on = tab.scene === frame.scene;
1333 return (
1334 <span key={tab.label} className="flex flex-col items-center justify-center gap-0.5">
1335 <span className={cn("flex h-6 w-10 items-center justify-center rounded-full", on ? "bg-[#2c2c33] text-fg" : "text-muted")}>{tab.icon}</span>
1336 <span className={cn("text-[9.5px] font-medium", on ? "text-fg" : "text-faint")}>{tab.label}</span>
1337 </span>
1338 );
1339 })}
1340 </div>
1341 )}
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)1342 </div>
1343 );
1344}
1345
1346function PhoneChat({ frame }: { frame: Frame }) {
1347 return (
1348 <div className="flex h-full flex-col">
1349 <div className="flex min-h-0 flex-1 flex-col justify-end overflow-hidden [mask-image:linear-gradient(to_bottom,transparent,black_2.5rem)] [&>div]:px-3">
1350 <Message who="Priya" time="09:02">
1351 <p>Morning! Thursday&apos;s release closes Wednesday night.</p>
1352 </Message>
1353 <Message who="Dana" time="09:12">
1354 <p>Two enterprise customers hit the CSV export timeout again this morning.</p>
1355 </Message>
1356 {frame.sent && (
1357 <Message who="Priya" time="09:15" enter>
1358 <p>
1359 <Rich text={ASK} />
1360 </p>
1361 </Message>
1362 )}
1363 {frame.g1tTyping && (
1364 <div className="tour-in flex items-center gap-3 py-2 text-[12px] text-faint">
1365 <Face who="g1t" />
1366 <Dots />
1367 </div>
1368 )}
1369 {frame.handoff && (
1370 <Message who="g1t" time="09:15" enter>
1371 <p>
1372 <Rich text="On it. @otto will fix it and @margo will review." />
1373 </p>
1374 <TaskCard frame={frame} />
1375 </Message>
1376 )}
1377 {frame.shipped && (
1378 <Message who="g1t" time="09:41" enter>
1379 <p>
Merge branch 'chat-sidebar' into fast-push1380 <Rich text="Shipped. @sam, can you tell #support?" />
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)1381 </p>
1382 </Message>
1383 )}
Merge branch 'chat-sidebar' into fast-push1384 {frame.sam && (
1385 <Message who="Sam" time="09:42" enter>
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)1386 <p>
1387 <Rich text="Done. Told #support." />
1388 </p>
1389 </Message>
1390 )}
1391 </div>
1392 <div className="px-3 pt-1 pb-3">
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers1393 <div className={cn("flex items-center gap-2 rounded-xl bg-surface px-3 py-2.5 text-[13px] ring-1", frame.typed > 0 ? "ring-accent/50" : "ring-line")}>
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)1394 <span className="min-w-0 flex-1 truncate">
1395 {frame.typed > 0 ? (
1396 <span className="text-fg">
1397 {ASK.slice(Math.max(0, frame.typed - 34), frame.typed)}
1398 <span className="tour-caret ml-px inline-block h-[14px] w-px translate-y-[2px] bg-accent" />
1399 </span>
1400 ) : (
1401 <span className="text-faint">Message #web</span>
1402 )}
1403 </span>
1404 <SendHorizontal size={15} className={frame.typed === ASK.length ? "text-accent" : "text-faint"} />
1405 </div>
1406 </div>
1407 </div>
1408 );
1409}
1410
1411function PhoneAgents({ frame }: { frame: Frame }) {
1412 const cost = STEPS[frame.steps - 1]?.cost ?? 0;
1413 return (
1414 <div className="space-y-3 px-3 py-4">
1415 <div className="flex items-center gap-3">
1416 <Face who="Otto" size={32} />
1417 <div>
1418 <p className="flex items-center gap-2 text-sm font-semibold text-fg">
1419 Otto <AgentTag />
1420 </p>
Merge site-audit: SEO routes, analytics with consent first in Europe, security page, founder and X account, docs header; the tour opens on the pull request and leads with Code, on the real shell, with Agents and Docs working today1421 <p className="text-[11px] text-muted">{frame.merged ? "Shipped #431" : "Working"} · ${cost.toFixed(2)} of $5</p>
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)1422 </div>
1423 </div>
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers1424 <div className="rounded-xl border border-line bg-surface">
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)1425 <p className="border-b border-line px-3.5 py-2.5 text-[13px] font-medium text-fg">CSV export times out for big accounts</p>
1426 <ol className="space-y-2 px-3.5 py-3">
1427 {STEPS.map((step, index) => {
1428 const done = index < frame.steps;
1429 const now = index === frame.steps;
1430 return (
1431 <li key={step.text} className={cn("flex items-start gap-2.5 text-[12.5px]", done ? "text-fg-soft" : now ? "text-fg" : "text-faint")}>
1432 <span className="mt-0.5 flex size-4 shrink-0 items-center justify-center">
1433 {done ? (
1434 <span className="tour-pop flex size-4 items-center justify-center rounded-full bg-success/15 text-success">
1435 <Check size={10} strokeWidth={3} />
1436 </span>
1437 ) : now ? (
1438 <span className="tour-spin size-3.5 rounded-full border-2 border-accent/25 border-t-accent" />
1439 ) : (
1440 <span className="size-1.5 rounded-full bg-line-strong" />
1441 )}
1442 </span>
1443 {step.text}
1444 </li>
1445 );
1446 })}
1447 </ol>
1448 <div className="flex flex-wrap gap-1.5 border-t border-line px-3.5 py-2.5">
1449 {FILES.filter((file) => frame.steps >= file.from).map((file) => (
1450 <span key={file.path} className="tour-pop rounded bg-raised px-1.5 py-0.5 font-mono text-[10px] text-muted ring-1 ring-line">
1451 {file.path}
1452 </span>
1453 ))}
1454 <span className="min-h-[18px]" />
1455 </div>
1456 </div>
1457 <div
1458 className={cn(
1459 "flex items-center gap-2 rounded-xl bg-accent/10 px-3.5 py-2.5 text-[12px] font-medium text-accent ring-1 ring-accent/25 transition-opacity duration-500",
1460 frame.steps >= STEPS.length ? "opacity-100" : "opacity-0",
1461 )}
1462 >
1463 <GitPullRequest size={13} />
1464 Pull request #431 opened
1465 <ArrowRight size={12} className="ml-auto" />
1466 </div>
1467 <Consult frame={frame} compact />
1468 <div className="flex items-center gap-2 rounded-xl px-3.5 py-2.5 text-[11.5px] text-faint ring-dashed">
1469 <span className="rounded bg-raised px-1.5 py-0.5 text-[9.5px] uppercase">Queued</span>
1470 Upgrade the date library
1471 </div>
1472 </div>
1473 );
1474}
1475
1476function PhoneCode({ frame }: { frame: Frame }) {
1477 return (
1478 <div className="space-y-3 px-3 py-4">
1479 <p className="text-[14px] font-semibold text-fg">
1480 Stream CSV exports in batches <span className="font-normal text-faint">#431</span>
1481 </p>
1482 <p className="text-[12px] leading-[1.6] text-muted">
1483 <span className="text-fg-soft">@otto</span> wants to merge into <span className="font-mono text-[11px] text-fg-soft">main</span>. Rows now
1484 stream in batches of 1,000; a 200,000-row account takes 3.1 s.
1485 </p>
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers1486 <div className="rounded-xl border border-line bg-surface">
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)1487 <ul className="space-y-2 px-3.5 py-3">
1488 {CHECKS.map((check, index) => {
1489 const done = index < frame.checks;
1490 return (
1491 <li key={check} className="flex items-center gap-2 text-[12px]">
1492 {done ? (
1493 <span className="tour-pop flex size-4 items-center justify-center rounded-full bg-success/15 text-success">
1494 <Check size={10} strokeWidth={3} />
1495 </span>
1496 ) : (
1497 <span className="tour-spin size-3.5 rounded-full border-2 border-warn/25 border-t-warn" />
1498 )}
1499 <span className={done ? "text-fg-soft" : "text-muted"}>{check}</span>
1500 </li>
1501 );
1502 })}
1503 </ul>
1504 </div>
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers1505 <pre className={cn("overflow-hidden rounded-xl border border-line bg-surface py-1.5 font-mono text-[10.5px] leading-[1.7] transition-opacity duration-500", frame.diff ? "opacity-100" : "opacity-0")}>
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)1506 {DIFF.slice(1).map((line, index) => (
1507 <div key={index} className={cn("truncate px-3", line.sign === "+" ? "bg-success/10 text-success" : "bg-danger/10 text-danger")}>
1508 {line.sign} {line.text.trim()}
1509 </div>
1510 ))}
1511 </pre>
1512 <div
1513 className={cn(
1514 "flex items-center gap-2 rounded-xl px-3.5 py-2.5 text-[12px] ring-1 transition-colors duration-500",
1515 frame.merged ? "bg-merged/10 text-merged ring-merged/30" : frame.approved ? "bg-success/10 text-success ring-success/30" : "text-muted ring-line",
1516 )}
1517 >
1518 <GitMerge size={13} />
1519 {frame.merged ? "Merged · deploying" : frame.approved ? "Approved by @margo" : "Waiting on checks and review"}
1520 </div>
1521 </div>
1522 );
1523}
1524
1525/* The tour -------------------------------------------------------------- */
1526
1527/** Keyframes the tour's parts use; transforms and opacity only. */
1528const STYLES = `
1529.tour-in { animation: tour-in 420ms cubic-bezier(.2,.7,.2,1) both; }
1530@keyframes tour-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
1531.tour-pop { animation: tour-pop 360ms cubic-bezier(.3,1.4,.5,1) both; }
1532@keyframes tour-pop { from { opacity: 0; transform: scale(.7); } to { opacity: 1; transform: none; } }
1533.tour-fade { animation: tour-fade 400ms ease-out both; }
1534@keyframes tour-fade { from { opacity: 0; } to { opacity: 1; } }
1535.tour-dot { animation: tour-dot 1.1s ease-in-out infinite; }
1536@keyframes tour-dot { 0%, 60%, 100% { opacity: .35; transform: translateY(0); } 30% { opacity: 1; transform: translateY(-2px); } }
1537.tour-caret { animation: tour-caret 1s steps(1) infinite; }
1538@keyframes tour-caret { 50% { opacity: 0; } }
1539.tour-spin { animation: tour-spin .9s linear infinite; }
1540@keyframes tour-spin { to { transform: rotate(360deg); } }
1541.tour-pulse { animation: tour-pulse 1.6s ease-in-out infinite; }
1542@keyframes tour-pulse { 50% { opacity: .35; } }
1543.ring-dashed { box-shadow: none; outline: 1px dashed var(--g1t-line-strong); outline-offset: -1px; }
1544@media (prefers-reduced-motion: reduce) {
1545 .tour-in, .tour-pop, .tour-fade, .tour-dot, .tour-caret, .tour-spin, .tour-pulse { animation: none; }
1546}
1547`;
1548
1549/**
1550 * The hero's tour: the frame (a full app on wider screens, one pane on a
1551 * phone), the step pills under it, and a line saying what is a preview.
1552 */
1553export function ProductTour({ className }: { className?: string }) {
1554 const clock = useTourClock();
1555 const { frame } = clock;
1556 return (
1557 <div ref={clock.rootRef} className={className}>
1558 <style dangerouslySetInnerHTML={{ __html: STYLES }} />
1559 <div
1560 role="img"
1561 aria-label="A tour of g1t: a person asks g1t in a channel to fix a slow export, an agent works the task, the pull request passes its checks and merges, g1t says it shipped, and the docs page updates."
1562 onMouseEnter={() => clock.hover(true)}
1563 onMouseLeave={() => clock.hover(false)}
1564 >
1565 <div className="hidden md:block">
1566 <Desktop frame={frame} reduced={clock.reduced} />
1567 </div>
1568 <div className="md:hidden">
1569 <Phone frame={frame} />
1570 </div>
1571 </div>
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers1572 <div className="mt-4 flex justify-center" aria-live="polite">
1573 <HonestyNote frame={frame} />
1574 </div>
1575 <div className="mx-auto mt-3 grid max-w-xl grid-cols-4 gap-2" role="tablist" aria-label="Steps of the tour">
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)1576 {PILLS.map((pill, index) => {
1577 const on = clock.pill === index;
1578 return (
1579 <button
1580 key={pill.scene}
1581 type="button"
1582 role="tab"
1583 aria-selected={on}
1584 onClick={() => clock.jump(index)}
1585 className={cn(
1586 "group relative overflow-hidden rounded-full px-3 py-1.5 text-xs font-medium ring-1 transition-colors",
1587 on ? "bg-surface text-fg ring-line-strong" : "text-muted ring-line hover:text-fg-soft",
1588 )}
1589 >
1590 <span
1591 ref={(element) => {
1592 clock.barRefs.current[index] = element;
1593 }}
1594 aria-hidden="true"
1595 className="absolute inset-0 origin-left bg-accent/15"
1596 style={{ transform: `scaleX(${clock.reduced && on ? 1 : 0})` }}
1597 />
1598 <span className="relative flex items-center justify-center gap-1.5">
1599 {pill.label}
1600 {pill.soon && (
1601 <>
1602 <span className="hidden text-[10px] font-normal text-faint sm:inline">soon</span>
1603 <span className="size-1 rounded-full bg-faint sm:hidden" aria-label="coming soon" />
1604 </>
1605 )}
1606 </span>
1607 </button>
1608 );
1609 })}
1610 </div>
1611 <p className="mx-auto mt-3 max-w-xl text-center text-xs leading-5 text-faint text-balance">
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers1612 Chat, agents and their sessions (colleagues brought in included), pull requests, checks and the merge queue work
Docs: a workspace knowledge base people and agents write together1613 today, and so does Docs. g1t splitting one request between colleagues on its own, and an agent updating a page after a merge by itself, are coming soon. Otto, Margo and the others are
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers1614 agents this workspace hired from role templates; only @g1t comes built in.
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)1615 </p>
1616 </div>
1617 );
1618}

This file's history is long; its oldest lines are credited to the oldest commit read.