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 today | 4 | * 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 managers | 15 | * It is an honest miniature of the app: the same rail (the workspace on |
| g1t is described as the product it is in alpha, and its shell takes the new shape: the README, the docs home and docs/PLAN.md label every feature Live, Preview or Coming; a floating dock with the g1t mark, Today, Chat, Notifications, Agents, Code, Artifacts, your pinned apps and the Apps launcher, with People, Workspace and the account menu (now holding help) at its foot; the workspace's logo and switcher heading a flat sidebar that folds away with Ctrl B, or leading the header's breadcrumbs where there is none; the page in a rounded panel with a full-width header; Today as the front page, with first-time acceptance of agents' pull requests, what needs you and what's at stake, spend today and one item to start with, every number from a service; Notifications in place of the Inbox at /notifications; a bottom bar and More sheet on phones; and sign-in, sign-up, two-factor, reset, invites and choosing a workspace on standalone pages with no app around them. | 16 | * the top bar's line, then Today, Code, Chat, Docs, Agents and Notifications, |
| One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers | 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 | */ |
| 24 | import { | |
| One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers | 25 | 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 managers | 28 | 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 managers | 32 | 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 managers | 37 | 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 managers | 41 | 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, | |
| g1t is described as the product it is in alpha, and its shell takes the new shape: the README, the docs home and docs/PLAN.md label every feature Live, Preview or Coming; a floating dock with the g1t mark, Today, Chat, Notifications, Agents, Code, Artifacts, your pinned apps and the Apps launcher, with People, Workspace and the account menu (now holding help) at its foot; the workspace's logo and switcher heading a flat sidebar that folds away with Ctrl B, or leading the header's breadcrumbs where there is none; the page in a rounded panel with a full-width header; Today as the front page, with first-time acceptance of agents' pull requests, what needs you and what's at stake, spend today and one item to start with, every number from a service; Notifications in place of the Inbox at /notifications; a bottom bar and More sheet on phones; and sign-in, sign-up, two-factor, reset, invites and choosing a workspace on standalone pages with no app around them. | 47 | Sun, |
| 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 | 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 managers | 49 | 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) | 50 | 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 managers | 51 | 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) | 52 | 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 managers | 53 | 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) | 54 | Search, |
| 55 | 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 managers | 56 | Settings, |
| 57 | ShieldCheck, | |
| Artifacts is a mode in the rail, the shell and the phone's tab bar in place of Docs: its home lists everything you can open by day or as cards under All, Yours and Shared with you, with search, filters and tiles to start a doc (slides, designs and dashboards say they are coming soon), and its sidebar holds favorites, spaces, Private, Shared, projects' docs and the trash. | 58 | Shapes, |
| One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers | 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 managers | 62 | import { 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 | |
| 64 | import { cn } from "../lib/cn"; | |
| 65 | import { ASK, CHECKS, type CursorTarget, type Frame, LOOP_MS, PILLS, STEPS, STILLS, type Scene, frameAt, pillAt, pillProgress, sameFrame } from "../lib/tour"; | |
| 66 | import { 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 managers | 67 | import { G1tMark } from "./orchestrator"; |
| 68 | import { Avatar } from "./ui"; | |
| 69 | import { 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. */ | |
| 72 | const W = 992; | |
| 73 | const H = 600; | |
| 74 | ||
| 75 | const useIsoLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect; | |
| 76 | ||
| 77 | /* ------------------------------------------------------------------ */ | |
| 78 | /* The clock */ | |
| 79 | /* ------------------------------------------------------------------ */ | |
| 80 | ||
| 81 | type 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 | ||
| 94 | function 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). */ | |
| 185 | function 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 today | 186 | 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 managers | 189 | /** 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) | 190 | function honesty(frame: Frame): { today: boolean; label: string } { |
| Artifacts is a mode in the rail, the shell and the phone's tab bar in place of Docs: its home lists everything you can open by day or as cards under All, Yours and Shared with you, with search, filters and tiles to start a doc (slides, designs and dashboards say they are coming soon), and its sidebar holds favorites, spaces, Private, Shared, projects' docs and the trash. | 191 | if (frame.scene === "docs") return { today: false, label: "Docs in Artifacts work today; an agent updating one 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 managers | 192 | 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 managers | 194 | 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 managers | 198 | /** The line under the frame: today, or soon. Not part of the app, so it sits outside it. */ |
| 199 | function 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-push | 221 | const 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. */ | |
| 224 | const 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-push | 229 | 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 managers | 235 | /** 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) | 236 | function 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 managers | 237 | 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. */ | |
| 242 | function 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. */ | |
| 252 | function 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 managers | 255 | 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 | ||
| 263 | function 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. */ | |
| 268 | function 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 | ||
| 285 | function 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 | ||
| 303 | function 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 managers | 313 | /** |
| 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 calendar | 315 | * (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 managers | 316 | * 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) | 318 | function 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 managers | 319 | 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 managers | 322 | <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 managers | 339 | {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 managers | 349 | /** The rail's modes, in the app's order (components/rail.tsx), with its icons. */ |
| 350 | const RAIL: { key: string; icon: ReactNode; label: string; target?: CursorTarget; scene?: Scene; badge?: number }[] = [ | |
| g1t is described as the product it is in alpha, and its shell takes the new shape: the README, the docs home and docs/PLAN.md label every feature Live, Preview or Coming; a floating dock with the g1t mark, Today, Chat, Notifications, Agents, Code, Artifacts, your pinned apps and the Apps launcher, with People, Workspace and the account menu (now holding help) at its foot; the workspace's logo and switcher heading a flat sidebar that folds away with Ctrl B, or leading the header's breadcrumbs where there is none; the page in a rounded panel with a full-width header; Today as the front page, with first-time acceptance of agents' pull requests, what needs you and what's at stake, spend today and one item to start with, every number from a service; Notifications in place of the Inbox at /notifications; a bottom bar and More sheet on phones; and sign-in, sign-up, two-factor, reset, invites and choosing a workspace on standalone pages with no app around them. | 351 | { key: "home", icon: <Sun size={15} />, label: "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 managers | 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 }, | |
| Artifacts is a mode in the rail, the shell and the phone's tab bar in place of Docs: its home lists everything you can open by day or as cards under All, Yours and Shared with you, with search, filters and tiles to start a doc (slides, designs and dashboards say they are coming soon), and its sidebar holds favorites, spaces, Private, Shared, projects' docs and the trash. | 354 | { key: "artifacts", icon: <Shapes size={15} />, label: "Artifacts", target: "rail-docs", scene: "docs" }, |
| One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers | 355 | { key: "agents", icon: <Sparkles size={15} />, label: "Agents", target: "rail-agents", scene: "agents" }, |
| g1t is described as the product it is in alpha, and its shell takes the new shape: the README, the docs home and docs/PLAN.md label every feature Live, Preview or Coming; a floating dock with the g1t mark, Today, Chat, Notifications, Agents, Code, Artifacts, your pinned apps and the Apps launcher, with People, Workspace and the account menu (now holding help) at its foot; the workspace's logo and switcher heading a flat sidebar that folds away with Ctrl B, or leading the header's breadcrumbs where there is none; the page in a rounded panel with a full-width header; Today as the front page, with first-time acceptance of agents' pull requests, what needs you and what's at stake, spend today and one item to start with, every number from a service; Notifications in place of the Inbox at /notifications; a bottom bar and More sheet on phones; and sign-in, sign-up, two-factor, reset, invites and choosing a workspace on standalone pages with no app around them. | 356 | { key: "inbox", icon: <Bell size={15} />, label: "Notifications", 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 managers | 359 | /** One rail item: the square behind its icon, filled for the mode you are in, and its name under it. */ |
| 360 | function 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 managers | 362 | <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 managers | 364 | 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 managers | 366 | "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 managers | 370 | {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 managers | 377 | <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 managers | 382 | /** |
| 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) | 387 | function 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 managers | 389 | <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 managers | 405 | </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 managers | 410 | /** |
| 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 | */ | |
| 415 | function 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. */ | |
| 448 | function 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 managers | 450 | <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. */ | |
| 461 | function 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 managers | 470 | function 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 managers | 478 | /** The search at the top of a sidebar. */ |
| 479 | function 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 managers | 481 | <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 | ||
| 491 | function 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 managers | 492 | // 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 managers | 494 | const margoBusy = frame.consult && !frame.approved; |
| Merge branch 'chat-sidebar' into fast-push | 495 | // 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 managers | 503 | const agents: { who: string; working: boolean }[] = [ |
| 504 | { who: "Otto", working: ottoBusy }, | |
| 505 | { who: "Inky", working: frame.docUpdated }, | |
| Merge branch 'chat-sidebar' into fast-push | 506 | { 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 managers | 510 | <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 managers | 525 | <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-push | 533 | <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 managers | 556 | <SideLabel>Agents</SideLabel> |
| 557 | {agents.map((agent) => ( | |
| 558 | <SideRow key={agent.who}> | |
| Merge branch 'chat-sidebar' into fast-push | 559 | <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 managers | 563 | <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 | ||
| 574 | function 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 managers | 578 | <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 managers | 581 | <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'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-push | 628 | <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-push | 632 | {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 managers | 644 | "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 managers | 677 | /** The specialists the demo workspace hired, from the role templates, by team (agents-mode.tsx, orgChart). */ |
| 678 | const SPECIALISTS: { who: string; working?: boolean }[] = [ | |
| 679 | { who: "Otto", working: true }, | |
| 680 | { who: "Margo" }, | |
| 681 | { who: "Inky" }, | |
| Merge branch 'chat-sidebar' into fast-push | 682 | { 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 managers | 683 | { 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 managers | 686 | /** Agents mode's sidebar, as the app has it: the fleet's pages, g1t, then the specialists by team. */ |
| 687 | function 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 managers | 696 | <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 managers | 722 | /** One agent in Agents mode's sidebar: its face and status, name, and what it is doing. */ |
| 723 | function 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) | 742 | const 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 managers | 748 | /** 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) | 749 | function 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 managers | 753 | "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 managers | 762 | <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">: “Is 1,000 a safe batch size for the 200k test?”</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 managers | 765 | <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. */ | |
| 771 | function 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 | ||
| 779 | function 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 managers | 783 | <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 managers | 794 | <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 managers | 798 | <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 managers | 804 | {["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 managers | 811 | <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 managers | 814 | <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 managers | 822 | 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 managers | 826 | <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 managers | 829 | <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 managers | 882 | <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 managers | 894 | /** 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) | 895 | function 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 managers | 896 | 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 managers | 905 | <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 managers | 936 | {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 | ||
| 945 | const 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 | ||
| 955 | function 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 managers | 986 | <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 managers | 1025 | <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 managers | 1062 | "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 together | 1077 | /** 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) | 1078 | function DocsSide() { |
| 1079 | return ( | |
| 1080 | <> | |
| Docs: a workspace knowledge base people and agents write together | 1081 | <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 managers | 1082 | 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 together | 1105 | function 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 | ||
| 1157 | function 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 | ||
| 1200 | const 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 managers | 1207 | /** Where each page is, as the top bar's trail says it; Chat has no top bar on a computer. */ |
| 1208 | const 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). */ | |
| 1216 | const 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) | 1218 | function 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 today | 1232 | 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 ( | |
| You choose how g1t looks: Auto follows your system, Light and Dark stay put, set from the account menu, the phone's You and help sheet, Settings → Account or the command palette; the page is drawn in your choice from the first byte, every colour has a light value that reads at 4.5:1 or better, code and diffs are highlighted for both, diagrams and the document editor follow along, the product tour and the g1t mark's avatar stay dark, and Dark is what you see until you choose; the workspaces guide says how. | 1240 | <div ref={outer} className="relative w-full overflow-hidden rounded-2xl text-fg shadow-2xl shadow-black/50 ring-1 ring-line scheme-dark" style={{ aspectRatio: `${W} / ${H}` }}> |
| 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) | 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 managers | 1243 | 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 managers | 1246 | <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 managers | 1268 | </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 | ||
| 1280 | const 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 managers | 1281 | 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 managers | 1287 | /** The phone's tab bar, as the app has it (components/mobile.tsx). */ |
| 1288 | const TABS: { scene: Scene | null; icon: ReactNode; label: string }[] = [ | |
| g1t is described as the product it is in alpha, and its shell takes the new shape: the README, the docs home and docs/PLAN.md label every feature Live, Preview or Coming; a floating dock with the g1t mark, Today, Chat, Notifications, Agents, Code, Artifacts, your pinned apps and the Apps launcher, with People, Workspace and the account menu (now holding help) at its foot; the workspace's logo and switcher heading a flat sidebar that folds away with Ctrl B, or leading the header's breadcrumbs where there is none; the page in a rounded panel with a full-width header; Today as the front page, with first-time acceptance of agents' pull requests, what needs you and what's at stake, spend today and one item to start with, every number from a service; Notifications in place of the Inbox at /notifications; a bottom bar and More sheet on phones; and sign-in, sign-up, two-factor, reset, invites and choosing a workspace on standalone pages with no app around them. | 1289 | { scene: null, icon: <Sun size={16} />, label: "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 managers | 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" }, | |
| g1t is described as the product it is in alpha, and its shell takes the new shape: the README, the docs home and docs/PLAN.md label every feature Live, Preview or Coming; a floating dock with the g1t mark, Today, Chat, Notifications, Agents, Code, Artifacts, your pinned apps and the Apps launcher, with People, Workspace and the account menu (now holding help) at its foot; the workspace's logo and switcher heading a flat sidebar that folds away with Ctrl B, or leading the header's breadcrumbs where there is none; the page in a rounded panel with a full-width header; Today as the front page, with first-time acceptance of agents' pull requests, what needs you and what's at stake, spend today and one item to start with, every number from a service; Notifications in place of the Inbox at /notifications; a bottom bar and More sheet on phones; and sign-in, sign-up, two-factor, reset, invites and choosing a workspace on standalone pages with no app around them. | 1293 | { scene: null, icon: <Bell size={16} />, label: "Notifications" }, |
| One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers | 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) | 1296 | function 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 today | 1297 | 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 ( |
| You choose how g1t looks: Auto follows your system, Light and Dark stay put, set from the account menu, the phone's You and help sheet, Settings → Account or the command palette; the page is drawn in your choice from the first byte, every colour has a light value that reads at 4.5:1 or better, code and diffs are highlighted for both, diagrams and the document editor follow along, the product tour and the g1t mark's avatar stay dark, and Dark is what you see until you choose; the workspaces guide says how. | 1299 | <div className="relative flex h-[34rem] w-full flex-col overflow-hidden rounded-2xl bg-bg text-left text-fg shadow-2xl shadow-black/50 ring-1 ring-line scheme-dark"> |
| One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers | 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 managers | 1303 | <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 managers | 1307 | <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 managers | 1328 | {/* 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 | ||
| 1346 | function 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'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-push | 1380 | <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-push | 1384 | {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 managers | 1393 | <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 | ||
| 1411 | function 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 today | 1421 | <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 managers | 1424 | <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 | ||
| 1476 | function 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 managers | 1486 | <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 managers | 1505 | <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. */ | |
| 1528 | const 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 | */ | |
| 1553 | export 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 managers | 1572 | <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 managers | 1612 | 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 together | 1613 | 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 managers | 1614 | 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.