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.
| Fixes from the page sweep | 1 | /** |
| 2 | * The pieces mission control and a project's overview are made of: panels | |
| 3 | * with a "View all", what needs the viewer, the activity feed with its | |
| 4 | * filters, the week's pulse with its sparklines, a project's pipeline and | |
| 5 | * its deploy history. | |
| 6 | */ | |
| 7 | import { | |
| 8 | ArrowRight, | |
| 9 | CircleCheck, | |
| 10 | CircleDot, | |
| 11 | CircleSlash, | |
| 12 | Eye, | |
| Merge demo dry run fixes: settled checks, other attempts, queue links, clone box, activity feed, DEMO.md | 13 | GitCommitHorizontal, |
| Fixes from the page sweep | 14 | GitMerge, |
| 15 | GitPullRequest, | |
| 16 | Hand, | |
| 17 | MessageSquare, | |
| 18 | Play, | |
| 19 | Rocket, | |
| 20 | Swords, | |
| 21 | Terminal, | |
| 22 | TimerOff, | |
| 23 | TriangleAlert, | |
| 24 | UserPlus, | |
| 25 | } from "lucide-react"; | |
| 26 | import { type ReactNode, useMemo, useState } from "react"; | |
| 27 | import { Link } from "react-router"; | |
| 28 | ||
| 29 | import type { DeployStatus } from "@g1t/contracts"; | |
| 30 | ||
| 31 | import { cn } from "../lib/cn"; | |
| 32 | import { type ActivityGroup, type Need, type NeedKind, type Verb, isAgent, sparkPoints } from "../lib/mission"; | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 33 | import { TimeAgo } from "./ui"; |
| 34 | import { Avatar } from "./ui/avatar"; | |
| 35 | import { Button } from "./ui/button"; | |
| 36 | import { Card } from "./ui/card"; | |
| Merge branch 'worktree-agent-ae1299e92e4462012' | 37 | import { Hint } from "./ui/hint"; |
| Fixes from the page sweep | 38 | import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"; |
| 39 | ||
| 40 | // --- Panels ------------------------------------------------------------------- | |
| 41 | ||
| 42 | /** A titled section, with a count and a link to everything it summarises. */ | |
| 43 | export function Panel({ | |
| 44 | id, | |
| 45 | title, | |
| 46 | icon, | |
| 47 | count, | |
| 48 | all, | |
| 49 | extra, | |
| 50 | children, | |
| 51 | className, | |
| 52 | }: { | |
| 53 | id?: string; | |
| 54 | title: ReactNode; | |
| 55 | icon?: ReactNode; | |
| 56 | count?: number | null; | |
| 57 | all?: { to: string; label?: string } | null; | |
| 58 | extra?: ReactNode; | |
| 59 | children: ReactNode; | |
| 60 | className?: string; | |
| 61 | }) { | |
| 62 | return ( | |
| 63 | <section id={id} className={cn("scroll-mt-20", className)}> | |
| 64 | <div className="flex min-h-7 flex-wrap items-center gap-x-3 gap-y-2"> | |
| 65 | <h2 className="flex items-center gap-2 text-sm font-semibold tracking-tight"> | |
| 66 | {icon && <span className="text-faint">{icon}</span>} | |
| 67 | {title} | |
| 68 | {count != null && count > 0 && ( | |
| 69 | <span className="rounded-full bg-line px-1.5 text-[0.6875rem] font-medium tabular-nums text-muted">{count}</span> | |
| 70 | )} | |
| 71 | </h2> | |
| 72 | <span className="grow" /> | |
| 73 | {extra} | |
| 74 | {all && ( | |
| 75 | <Link to={all.to} prefetch="intent" className="inline-flex items-center gap-1 text-xs text-muted hover:text-fg"> | |
| 76 | {all.label ?? "View all"} | |
| 77 | <ArrowRight size={12} /> | |
| 78 | </Link> | |
| 79 | )} | |
| 80 | </div> | |
| 81 | <div className="mt-3">{children}</div> | |
| 82 | </section> | |
| 83 | ); | |
| 84 | } | |
| 85 | ||
| 86 | /** Says a section could not be loaded, without taking the page down. */ | |
| 87 | export function Unavailable({ what }: { what: string }) { | |
| 88 | return ( | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 89 | <Card asChild tone="plain" className="flex items-center gap-2 border-dashed px-4 py-5 text-sm text-muted"> |
| 90 | <p> | |
| 91 | <TriangleAlert size={14} className="text-warn" /> | |
| 92 | {what} could not be loaded just now. It will be back on the next refresh. | |
| 93 | </p> | |
| 94 | </Card> | |
| Fixes from the page sweep | 95 | ); |
| 96 | } | |
| 97 | ||
| 98 | /** A quiet, dashed empty state with an optional call to action. */ | |
| 99 | export function Quiet({ children, action }: { children: ReactNode; action?: ReactNode }) { | |
| 100 | return ( | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 101 | <Card tone="plain" className="flex flex-wrap items-center gap-3 border-dashed px-4 py-5 text-sm text-muted"> |
| Fixes from the page sweep | 102 | <span className="min-w-0 grow">{children}</span> |
| 103 | {action} | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 104 | </Card> |
| Fixes from the page sweep | 105 | ); |
| 106 | } | |
| 107 | ||
| 108 | // --- Needs you ---------------------------------------------------------------- | |
| 109 | ||
| 110 | const NEED: Record<NeedKind, { icon: ReactNode; tone: string; label: string }> = { | |
| 111 | deploy: { icon: <Rocket size={15} />, tone: "text-danger", label: "Production" }, | |
| 112 | invitation: { icon: <UserPlus size={15} />, tone: "text-accent", label: "Invitation" }, | |
| 113 | conflict: { icon: <Swords size={15} />, tone: "text-warn", label: "Conflict" }, | |
| 114 | stalled: { icon: <Hand size={15} />, tone: "text-warn", label: "Stopped" }, | |
| The site no longer has g1t's own agents. g1t is where the coding agents you already use do their work, so the Agents mode is gone from the rail, with every page under /<workspace>/-/agents: personas, faces, skills, abilities, routines, computers, sessions and agent spend. Customize (skills, connectors, plugins, marketplaces), the context hub, model providers, the AI Gateway page, the Spend page and pill, Ask g1t, and the repository's Agents, Plans and agent settings pages go too, and their addresses now answer 404. | 115 | stuck: { icon: <TimerOff size={15} />, tone: "text-warn", label: "Quiet run" }, |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 116 | runner: { icon: <TimerOff size={15} />, tone: "text-warn", label: "No runner" }, |
| Fixes from the page sweep | 117 | review: { icon: <Eye size={15} />, tone: "text-info", label: "Review" }, |
| 118 | checks: { icon: <Terminal size={15} />, tone: "text-danger", label: "Checks" }, | |
| Merge branch 'worktree-agent-ad7c6d88d93adc817' | 119 | ready: { icon: <GitMerge size={15} />, tone: "text-success", label: "Ready" }, |
| Fixes from the page sweep | 120 | }; |
| 121 | ||
| 122 | /** What is waiting on the viewer, most urgent first, each with its next step. */ | |
| 123 | export function NeedsList({ needs, limit = 8 }: { needs: Need[]; limit?: number }) { | |
| 124 | const [all, setAll] = useState(false); | |
| 125 | const shown = all ? needs : needs.slice(0, limit); | |
| 126 | return ( | |
| 127 | <> | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 128 | <Card asChild divided className="overflow-hidden"> |
| 129 | <ul> | |
| 130 | {shown.map((need) => { | |
| 131 | const look = NEED[need.kind]; | |
| 132 | return ( | |
| 133 | <li key={need.key} className="flex items-start gap-3 px-4 py-3"> | |
| 134 | <Hint label={look.label}> | |
| 135 | <span className={cn("mt-0.5 shrink-0", look.tone)}> | |
| 136 | {look.icon} | |
| 137 | <span className="sr-only">{look.label}</span> | |
| 138 | </span> | |
| 139 | </Hint> | |
| 140 | <span className="min-w-0 grow"> | |
| 141 | <Link to={need.to} prefetch="intent" className="block truncate text-sm font-medium hover:text-accent"> | |
| 142 | {need.title} | |
| 143 | </Link> | |
| 144 | <span className="mt-0.5 block text-xs leading-5 text-muted"> | |
| 145 | {need.where && <span className="font-mono text-faint">{need.where} · </span>} | |
| 146 | {need.detail} | |
| 147 | </span> | |
| Merge branch 'worktree-agent-ae1299e92e4462012' | 148 | </span> |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 149 | <span className="hidden shrink-0 pt-0.5 text-xs text-faint sm:block"> |
| 150 | <TimeAgo at={need.at} /> | |
| 151 | </span> | |
| 152 | <Link | |
| 153 | to={need.to} | |
| 154 | prefetch="intent" | |
| 155 | className="shrink-0 rounded-md border border-line px-2.5 py-1 text-xs font-medium text-fg/85 transition-colors hover:border-line-strong hover:bg-raised hover:text-fg" | |
| 156 | > | |
| 157 | {need.action} | |
| Fixes from the page sweep | 158 | </Link> |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 159 | </li> |
| 160 | ); | |
| 161 | })} | |
| 162 | </ul> | |
| 163 | </Card> | |
| Fixes from the page sweep | 164 | {needs.length > limit && ( |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 165 | <Button type="button" onClick={() => setAll(!all)} variant="link" size="inline" className="mt-2 text-xs text-muted hover:text-fg font-normal"> |
| Fixes from the page sweep | 166 | {all ? "Show fewer" : `Show ${needs.length - limit} more`} |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 167 | </Button> |
| Fixes from the page sweep | 168 | )} |
| 169 | </> | |
| 170 | ); | |
| 171 | } | |
| 172 | ||
| 173 | // --- Activity ----------------------------------------------------------------- | |
| 174 | ||
| 175 | const VERB: Record<Verb, { icon: ReactNode; tone: string; text: (n: string) => string }> = { | |
| 176 | landed: { icon: <GitMerge size={14} />, tone: "text-merged", text: (n) => `landed ${n}` }, | |
| Merge branch 'worktree-agent-ad7c6d88d93adc817' | 177 | opened_issue: { icon: <CircleDot size={14} />, tone: "text-success", text: (n) => `opened ${n}` }, |
| Fixes from the page sweep | 178 | closed_issue: { icon: <CircleSlash size={14} />, tone: "text-faint", text: (n) => `closed ${n}` }, |
| Merge branch 'worktree-agent-ad7c6d88d93adc817' | 179 | started: { icon: <Play size={14} />, tone: "text-accent", text: (n) => `started on ${n}` }, |
| Fixes from the page sweep | 180 | ready: { icon: <GitPullRequest size={14} />, tone: "text-info", text: (n) => `marked ${n} ready for review` }, |
| Merge branch 'worktree-agent-ad7c6d88d93adc817' | 181 | checks_passed: { icon: <Terminal size={14} />, tone: "text-success", text: (n) => `checks passed on ${n}` }, |
| Fixes from the page sweep | 182 | checks_failed: { icon: <Terminal size={14} />, tone: "text-danger", text: (n) => `checks failed on ${n}` }, |
| Merge branch 'worktree-agent-ad7c6d88d93adc817' | 183 | approved: { icon: <CircleCheck size={14} />, tone: "text-success", text: (n) => `approved ${n}` }, |
| Fixes from the page sweep | 184 | changes_requested: { icon: <CircleSlash size={14} />, tone: "text-warn", text: (n) => `asked for changes on ${n}` }, |
| 185 | commented: { icon: <MessageSquare size={14} />, tone: "text-muted", text: (n) => `commented on ${n}` }, | |
| Merge branch 'worktree-agent-ad7c6d88d93adc817' | 186 | deployed: { icon: <Rocket size={14} />, tone: "text-success", text: () => "deployed production" }, |
| Fixes from the page sweep | 187 | deploy_failed: { icon: <Rocket size={14} />, tone: "text-danger", text: () => "production build failed" }, |
| Merge demo dry run fixes: settled checks, other attempts, queue links, clone box, activity feed, DEMO.md | 188 | pushed: { icon: <GitCommitHorizontal size={14} />, tone: "text-muted", text: () => "pushed to the default branch" }, |
| Fixes from the page sweep | 189 | }; |
| 190 | ||
| 191 | function Refs({ numbers, base }: { numbers: number[]; base: string }) { | |
| 192 | const shown = numbers.slice(0, 4); | |
| 193 | return ( | |
| 194 | <> | |
| 195 | {shown.map((number, index) => ( | |
| 196 | <span key={number}> | |
| 197 | {index > 0 && (index === shown.length - 1 && numbers.length <= 4 ? " and " : ", ")} | |
| 198 | <Link to={`${base}/issues/${number}`} prefetch="intent" className="font-medium text-fg hover:underline"> | |
| 199 | #{number} | |
| 200 | </Link> | |
| 201 | </span> | |
| 202 | ))} | |
| 203 | {numbers.length > 4 && ` and ${numbers.length - 4} more`} | |
| 204 | </> | |
| 205 | ); | |
| 206 | } | |
| 207 | ||
| 208 | /** One group as a sentence: "landed #4 and #3; started on #5". */ | |
| 209 | function GroupLine({ group }: { group: ActivityGroup }) { | |
| 210 | const base = `/${group.repo.namespace}/${group.repo.name}`; | |
| 211 | return ( | |
| 212 | <> | |
| 213 | {group.parts.map((part, index) => { | |
| 214 | const look = VERB[part.verb]; | |
| 215 | const [before, after] = look.text("\u0000").split("\u0000"); | |
| 216 | return ( | |
| 217 | <span key={part.verb}> | |
| 218 | {index > 0 && "; "} | |
| The site no longer has g1t's own agents. g1t is where the coding agents you already use do their work, so the Agents mode is gone from the rail, with every page under /<workspace>/-/agents: personas, faces, skills, abilities, routines, computers, sessions and agent spend. Customize (skills, connectors, plugins, marketplaces), the context hub, model providers, the AI Gateway page, the Spend page and pill, Ask g1t, and the repository's Agents, Plans and agent settings pages go too, and their addresses now answer 404. | 219 | {part.numbers.length > 0 ? ( |
| Fixes from the page sweep | 220 | <> |
| 221 | {before} | |
| 222 | <Refs numbers={part.numbers} base={base} /> | |
| 223 | {after} | |
| 224 | </> | |
| 225 | ) : part.to ? ( | |
| 226 | <Link to={part.to} className="hover:text-fg"> | |
| 227 | {look.text("")} | |
| 228 | </Link> | |
| 229 | ) : ( | |
| 230 | look.text("") | |
| 231 | )} | |
| 232 | </span> | |
| 233 | ); | |
| 234 | })} | |
| 235 | </> | |
| 236 | ); | |
| 237 | } | |
| 238 | ||
| 239 | /** | |
| 240 | * What moved across projects, newest first, a burst of one actor's work | |
| 241 | * as one line, filterable by project and by who did it. | |
| 242 | */ | |
| 243 | export function ActivityFeed({ | |
| 244 | groups, | |
| 245 | showRepo = true, | |
| 246 | limit = 14, | |
| 247 | empty, | |
| 248 | }: { | |
| 249 | groups: ActivityGroup[]; | |
| 250 | showRepo?: boolean; | |
| 251 | limit?: number; | |
| 252 | empty: ReactNode; | |
| 253 | }) { | |
| 254 | const [project, setProject] = useState("all"); | |
| 255 | const [person, setPerson] = useState("all"); | |
| 256 | const [more, setMore] = useState(false); | |
| 257 | const projects = useMemo( | |
| 258 | () => [...new Set(groups.map((g) => `${g.repo.namespace}/${g.repo.name}`))].sort(), | |
| 259 | [groups], | |
| 260 | ); | |
| 261 | const people = useMemo( | |
| 262 | () => [...new Set(groups.map((g) => g.actor).filter((a): a is string => a != null))].sort(), | |
| 263 | [groups], | |
| 264 | ); | |
| 265 | const filtered = groups.filter( | |
| 266 | (g) => | |
| 267 | (project === "all" || `${g.repo.namespace}/${g.repo.name}` === project) && | |
| The site no longer has g1t's own agents. g1t is where the coding agents you already use do their work, so the Agents mode is gone from the rail, with every page under /<workspace>/-/agents: personas, faces, skills, abilities, routines, computers, sessions and agent spend. Customize (skills, connectors, plugins, marketplaces), the context hub, model providers, the AI Gateway page, the Spend page and pill, Ask g1t, and the repository's Agents, Plans and agent settings pages go too, and their addresses now answer 404. | 268 | (person === "all" || g.actor === person), |
| Fixes from the page sweep | 269 | ); |
| 270 | const shown = more ? filtered : filtered.slice(0, limit); | |
| 271 | return ( | |
| 272 | <div> | |
| The site no longer has g1t's own agents. g1t is where the coding agents you already use do their work, so the Agents mode is gone from the rail, with every page under /<workspace>/-/agents: personas, faces, skills, abilities, routines, computers, sessions and agent spend. Customize (skills, connectors, plugins, marketplaces), the context hub, model providers, the AI Gateway page, the Spend page and pill, Ask g1t, and the repository's Agents, Plans and agent settings pages go too, and their addresses now answer 404. | 273 | {((showRepo && projects.length > 1) || people.length > 1) && ( |
| Fixes from the page sweep | 274 | <div className="mb-3 flex flex-wrap items-center gap-2"> |
| 275 | {showRepo && projects.length > 1 && ( | |
| 276 | <Select value={project} onValueChange={setProject}> | |
| 277 | <SelectTrigger size="sm" className="w-auto max-w-56" aria-label="Project"> | |
| 278 | <SelectValue /> | |
| 279 | </SelectTrigger> | |
| 280 | <SelectContent> | |
| 281 | <SelectItem value="all">All projects</SelectItem> | |
| 282 | {projects.map((name) => ( | |
| 283 | <SelectItem key={name} value={name}> | |
| 284 | {name.split("/")[1]} | |
| 285 | </SelectItem> | |
| 286 | ))} | |
| 287 | </SelectContent> | |
| 288 | </Select> | |
| 289 | )} | |
| 290 | {people.length > 1 && ( | |
| 291 | <Select value={person} onValueChange={setPerson}> | |
| 292 | <SelectTrigger size="sm" className="w-auto max-w-48" aria-label="Who"> | |
| 293 | <SelectValue /> | |
| 294 | </SelectTrigger> | |
| 295 | <SelectContent> | |
| 296 | <SelectItem value="all">Anyone</SelectItem> | |
| 297 | {people.map((name) => ( | |
| 298 | <SelectItem key={name} value={name} icon={<Avatar name={name} size={16} />}> | |
| 299 | {name} | |
| 300 | </SelectItem> | |
| 301 | ))} | |
| 302 | </SelectContent> | |
| 303 | </Select> | |
| 304 | )} | |
| 305 | </div> | |
| 306 | )} | |
| 307 | {shown.length === 0 ? ( | |
| 308 | <Quiet>{groups.length === 0 ? empty : "Nothing matches these filters."}</Quiet> | |
| 309 | ) : ( | |
| 310 | <ol className="relative space-y-0.5 before:absolute before:top-3 before:bottom-3 before:left-[0.9375rem] before:w-px before:bg-line"> | |
| 311 | {shown.map((group) => { | |
| 312 | const look = VERB[group.parts[0].verb]; | |
| 313 | const agent = group.actor == null || isAgent(group.actor); | |
| 314 | return ( | |
| 315 | <li key={group.id} className="relative flex items-start gap-3 rounded-lg px-1 py-1.5 text-sm"> | |
| 316 | <span | |
| 317 | className={cn( | |
| 318 | "relative z-10 mt-0.5 flex size-[1.375rem] shrink-0 items-center justify-center rounded-full bg-bg ring-1 ring-line", | |
| 319 | look.tone, | |
| 320 | )} | |
| 321 | > | |
| 322 | {look.icon} | |
| 323 | </span> | |
| 324 | <span className="min-w-0 grow leading-6 text-muted"> | |
| 325 | {group.actor ? ( | |
| Merge branch 'worktree-agent-ad7c6d88d93adc817' | 326 | <span className={cn("mr-1.5 inline-flex items-center gap-1.5 align-middle font-medium", agent ? "text-accent" : "text-fg")}> |
| Fixes from the page sweep | 327 | <Avatar name={group.actor} size={16} /> |
| 328 | {group.actor} | |
| 329 | </span> | |
| 330 | ) : ( | |
| 331 | <span className="mr-1 text-fg-soft">g1t:</span> | |
| 332 | )} | |
| 333 | <GroupLine group={group} /> | |
| 334 | {showRepo && ( | |
| 335 | <Link | |
| 336 | to={`/${group.repo.namespace}/${group.repo.name}`} | |
| 337 | className="ml-1.5 font-mono text-xs text-faint hover:text-muted" | |
| 338 | > | |
| 339 | {group.repo.name} | |
| 340 | </Link> | |
| 341 | )} | |
| 342 | {group.count > 2 && <span className="ml-1.5 text-xs text-faint">· {group.count} events</span>} | |
| 343 | </span> | |
| 344 | <span className="shrink-0 pt-0.5 text-xs text-faint"> | |
| 345 | <TimeAgo at={group.at} /> | |
| 346 | </span> | |
| 347 | </li> | |
| 348 | ); | |
| 349 | })} | |
| 350 | </ol> | |
| 351 | )} | |
| 352 | {filtered.length > limit && ( | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 353 | <Button type="button" onClick={() => setMore(!more)} variant="link" size="inline" className="mt-2 text-xs text-muted hover:text-fg font-normal"> |
| Fixes from the page sweep | 354 | {more ? "Show fewer" : `Show ${filtered.length - limit} more`} |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 355 | </Button> |
| Fixes from the page sweep | 356 | )} |
| 357 | </div> | |
| 358 | ); | |
| 359 | } | |
| 360 | ||
| 361 | // --- Pulse -------------------------------------------------------------------- | |
| 362 | ||
| 363 | const DAY_LABEL = (offset: number) => { | |
| 364 | const date = new Date(Date.now() - offset * 86_400_000); | |
| 365 | return date.toLocaleDateString("en-US", { weekday: "short", month: "short", day: "numeric", timeZone: "UTC" }); | |
| 366 | }; | |
| 367 | ||
| 368 | /** | |
| 369 | * A week of one measure as a small line, the last day marked. Each day | |
| 370 | * has a hover target that says its value. | |
| 371 | */ | |
| 372 | export function Sparkline({ | |
| 373 | values, | |
| 374 | format, | |
| 375 | width = 96, | |
| 376 | height = 28, | |
| 377 | }: { | |
| 378 | values: number[]; | |
| 379 | format: (value: number) => string; | |
| 380 | width?: number; | |
| 381 | height?: number; | |
| 382 | }) { | |
| 383 | const points = sparkPoints(values, width, height, 3); | |
| 384 | if (points.length === 0) return null; | |
| 385 | const line = points.map(([x, y], i) => `${i ? "L" : "M"}${x},${y}`).join(" "); | |
| 386 | const area = `${line} L${points[points.length - 1][0]},${height} L${points[0][0]},${height} Z`; | |
| 387 | const [lastX, lastY] = points[points.length - 1]; | |
| 388 | const slot = width / values.length; | |
| 389 | return ( | |
| 390 | <svg width={width} height={height} viewBox={`0 0 ${width} ${height}`} className="overflow-visible text-accent" role="img" aria-label={values.map(format).join(", ")}> | |
| 391 | <path d={area} fill="currentColor" opacity={0.12} /> | |
| 392 | <path d={line} fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" /> | |
| 393 | <circle cx={lastX} cy={lastY} r={3} fill="currentColor" stroke="var(--color-surface)" strokeWidth={2} /> | |
| 394 | {values.map((value, index) => ( | |
| 395 | <rect key={index} x={index * slot} y={0} width={slot} height={height} fill="transparent" className="hover:fill-fg/5"> | |
| 396 | <title>{`${DAY_LABEL(values.length - 1 - index)}: ${format(value)}`}</title> | |
| 397 | </rect> | |
| 398 | ))} | |
| 399 | </svg> | |
| 400 | ); | |
| 401 | } | |
| 402 | ||
| 403 | /** One measure of the week: a headline number, what it means, and its line. */ | |
| 404 | export function PulseTile({ | |
| 405 | label, | |
| 406 | value, | |
| 407 | hint, | |
| 408 | values, | |
| 409 | format, | |
| 410 | to, | |
| 411 | }: { | |
| 412 | label: string; | |
| 413 | value: string; | |
| 414 | hint?: string; | |
| 415 | values?: number[]; | |
| 416 | format?: (value: number) => string; | |
| 417 | to?: string; | |
| 418 | }) { | |
| 419 | const body = ( | |
| 420 | <> | |
| 421 | <p className="truncate text-xs text-muted">{label}</p> | |
| 422 | <div className="mt-1 flex items-end justify-between gap-2"> | |
| 423 | <p className="text-2xl font-semibold tracking-tight tabular-nums">{value}</p> | |
| 424 | {values && format && values.some((v) => v > 0) && ( | |
| 425 | <span className="mb-1"> | |
| 426 | <Sparkline values={values} format={format} width={56} height={22} /> | |
| 427 | </span> | |
| 428 | )} | |
| 429 | </div> | |
| Merge branch 'worktree-agent-ae1299e92e4462012' | 430 | <Hint label={hint}> |
| 431 | <p className="mt-1.5 truncate text-[0.6875rem] text-faint">{hint}</p> | |
| 432 | </Hint> | |
| Fixes from the page sweep | 433 | </> |
| 434 | ); | |
| 435 | const box = "block rounded-xl border border-line bg-surface p-4 transition-colors"; | |
| 436 | return to ? ( | |
| 437 | <Link to={to} className={`${box} hover:border-line-strong`}> | |
| 438 | {body} | |
| 439 | </Link> | |
| 440 | ) : ( | |
| 441 | <div className={box}>{body}</div> | |
| 442 | ); | |
| 443 | } | |
| 444 | ||
| 445 | // --- Deploys ------------------------------------------------------------------ | |
| 446 | ||
| 447 | const STRIP_TONE: Record<DeployStatus, string> = { | |
| Merge branch 'worktree-agent-ad7c6d88d93adc817' | 448 | ready: "bg-success", |
| Fixes from the page sweep | 449 | // Served once, until a newer build or a take-down: it worked. |
| Merge branch 'worktree-agent-ad7c6d88d93adc817' | 450 | replaced: "bg-success/45", |
| Fixes from the page sweep | 451 | down: "bg-line-strong", |
| 452 | failed: "bg-danger", | |
| 453 | building: "bg-warn animate-pulse", | |
| 454 | queued: "bg-line-strong animate-pulse", | |
| 455 | skipped: "bg-line", | |
| 456 | }; | |
| 457 | ||
| 458 | /** The last builds as a row of bars, oldest on the left; each links to its build. */ | |
| 459 | export function DeployStrip({ | |
| 460 | builds, | |
| 461 | base, | |
| 462 | slots = 20, | |
| 463 | }: { | |
| 464 | builds: { id: string; status: DeployStatus; kind: string; commit: string; createdAt: string }[]; | |
| 465 | base: string; | |
| 466 | slots?: number; | |
| 467 | }) { | |
| 468 | const shown = builds.slice(0, slots).reverse(); | |
| 469 | return ( | |
| 470 | <div className="flex h-8 items-end gap-[3px]" aria-label="Recent builds, oldest first"> | |
| 471 | {Array.from({ length: slots - shown.length }, (_, i) => ( | |
| 472 | <span key={`empty-${i}`} className="h-2 flex-1 rounded-sm bg-line/60" /> | |
| 473 | ))} | |
| Merge branch 'worktree-agent-ae1299e92e4462012' | 474 | {shown.map((build) => { |
| 475 | const about = `${build.kind === "production" ? "Production" : "Preview"} · ${build.commit.slice(0, 7)} · ${build.status} · ${new Date(build.createdAt).toLocaleString("en-US")}`; | |
| 476 | return ( | |
| 477 | <Hint key={build.id} label={about}> | |
| 478 | <Link | |
| 479 | to={`${base}/deployments/${build.id}`} | |
| 480 | aria-label={about} | |
| 481 | className={cn("flex-1 rounded-sm transition-opacity hover:opacity-80", STRIP_TONE[build.status], build.kind === "production" ? "h-8" : "h-5")} | |
| 482 | /> | |
| 483 | </Hint> | |
| 484 | ); | |
| 485 | })} | |
| Fixes from the page sweep | 486 | </div> |
| 487 | ); | |
| 488 | } | |
| 489 | ||
| 490 | /** A small meter for a share, 0 to 1. */ | |
| Merge branch 'worktree-agent-ad7c6d88d93adc817' | 491 | export function Meter({ value, tone = "bg-success" }: { value: number | null; tone?: string }) { |
| Fixes from the page sweep | 492 | return ( |
| 493 | <span className="block h-1.5 w-full overflow-hidden rounded-full bg-line"> | |
| 494 | {value != null && <span className={cn("block h-full rounded-full", tone)} style={{ width: `${Math.round(value * 100)}%` }} />} | |
| 495 | </span> | |
| 496 | ); | |
| 497 | } | |
| 498 | ||
| 499 | export function percent(value: number | null): string { | |
| 500 | return value == null ? "—" : `${Math.round(value * 100)}%`; | |
| 501 | } | |
| 502 |
This file's history is long; its oldest lines are credited to the oldest commit read.