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 Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts | 1 | import { Bot, CircleCheck, CircleSlash, Pencil, Trash2 } from "lucide-react"; |
| Plan: Projects, the home of everything about running software | 2 | import type { CSSProperties, ReactNode } from "react"; |
| 3 | import { Form, Link } from "react-router"; | |
| 4 | ||
| Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar | 5 | import type { AgentRef, Comment, Issue, Pull, State } from "@g1t/contracts"; |
| Plan: Projects, the home of everything about running software | 6 | |
| Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts | 7 | import { mayChangeComment } from "../lib/comments"; |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 8 | import { chipStyle } from "../lib/labels"; |
| Plan: Projects, the home of everything about running software | 9 | import { repoAt } from "../lib/markdown-plugins"; |
| 10 | import { Markdown } from "./markdown"; | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 11 | import { IssueIcon, PullIcon } from "./work-icons"; |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 12 | import { MentionTextarea } from "./mention-textarea"; |
| Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar | 13 | import { AgentAvatar } from "./agent-avatar"; |
| 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. | 14 | import { ErrorText, SubmitButton, TimeAgo } from "./ui"; |
| Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar | 15 | import { Badge } from "./ui/badge"; |
| Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts | 16 | import { |
| 17 | AlertDialog, | |
| 18 | AlertDialogAction, | |
| 19 | AlertDialogCancel, | |
| 20 | AlertDialogContent, | |
| 21 | AlertDialogDescription, | |
| 22 | AlertDialogFooter, | |
| 23 | AlertDialogHeader, | |
| 24 | AlertDialogTitle, | |
| 25 | AlertDialogTrigger, | |
| 26 | } from "./ui/alert-dialog"; | |
| 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. | 27 | import { Avatar } from "./ui/avatar"; |
| 28 | import { Button } from "./ui/button"; | |
| 29 | import { Card } from "./ui/card"; | |
| Workspace names and icons, and a component kit for every control | 30 | import { CheckboxOption } from "./ui/checkbox"; |
| Merge branch 'worktree-agent-ae1299e92e4462012' | 31 | import { Hint } from "./ui/hint"; |
| Merge commit authors and hovercards: commits show their accounts, every name opens a card (repos 0016) | 32 | import { UserCard } from "./user-card"; |
| Plan: Projects, the home of everything about running software | 33 | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 34 | export { IssueIcon, PullIcon }; |
| 35 | ||
| Plan: Projects, the home of everything about running software | 36 | /** Hues for the labels every repository starts with. */ |
| 37 | const LABEL_HUES: Record<string, number> = { | |
| 38 | bug: 25, | |
| 39 | feature: 150, | |
| 40 | docs: 240, | |
| 41 | chore: 80, | |
| 42 | question: 310, | |
| 43 | }; | |
| 44 | ||
| 45 | /** A stable hue for any other label, from its name. */ | |
| 46 | function hueFor(name: string): number { | |
| 47 | let hash = 0; | |
| 48 | for (const char of name) hash = (hash * 31 + char.charCodeAt(0)) % 360; | |
| 49 | return hash; | |
| 50 | } | |
| 51 | ||
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 52 | /** |
| 53 | * A label's chip: in its own color when the page knows it (the | |
| 54 | * repository's labels, by name), else in a hue chosen from its name. | |
| 55 | */ | |
| 56 | export function Label({ name, color }: { name: string; color?: string | null }) { | |
| Plan: Projects, the home of everything about running software | 57 | const hue = LABEL_HUES[name] ?? hueFor(name); |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 58 | const own = chipStyle(color); |
| 59 | const style: CSSProperties = Object.keys(own).length > 0 ? own : { | |
| 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. | 60 | // Light text on a dark page, deep text on a light one (lib/theme.ts). |
| 61 | color: `light-dark(oklch(0.46 0.13 ${hue}), oklch(0.84 0.11 ${hue}))`, | |
| 62 | borderColor: `light-dark(oklch(0.55 0.13 ${hue} / 0.35), oklch(0.84 0.11 ${hue} / 0.35))`, | |
| 63 | backgroundColor: `light-dark(oklch(0.55 0.13 ${hue} / 0.08), oklch(0.84 0.11 ${hue} / 0.1))`, | |
| Plan: Projects, the home of everything about running software | 64 | }; |
| 65 | return ( | |
| 66 | <span | |
| 67 | style={style} | |
| 68 | className="inline-flex shrink-0 items-center rounded-full border px-2 py-px text-xs font-medium" | |
| 69 | > | |
| 70 | {name} | |
| 71 | </span> | |
| 72 | ); | |
| 73 | } | |
| 74 | ||
| 75 | /** The people an issue is assigned to, as overlapping avatars. */ | |
| 76 | export function AssigneeStack({ people }: { people: string[] }) { | |
| 77 | if (people.length === 0) return null; | |
| 78 | const shown = people.slice(0, 3); | |
| 79 | return ( | |
| Merge branch 'worktree-agent-ae1299e92e4462012' | 80 | <Hint label={`Assigned to ${people.join(", ")}`}> |
| 81 | <span className="flex shrink-0 items-center"> | |
| 82 | <span className="sr-only">Assigned to {people.join(", ")}</span> | |
| 83 | {shown.map((name, index) => ( | |
| 84 | <span key={name} className={`rounded-full ring-2 ring-bg ${index > 0 ? "-ml-1.5" : ""}`} aria-hidden> | |
| 85 | <Avatar name={name} size={18} /> | |
| 86 | </span> | |
| 87 | ))} | |
| 88 | {people.length > shown.length && ( | |
| 89 | <span className="ml-1 text-xs text-faint" aria-hidden> | |
| 90 | +{people.length - shown.length} | |
| 91 | </span> | |
| 92 | )} | |
| 93 | </span> | |
| 94 | </Hint> | |
| Plan: Projects, the home of everything about running software | 95 | ); |
| 96 | } | |
| 97 | ||
| 98 | /** The agent working on an issue now. */ | |
| 99 | export function Assignee({ agent }: { agent: string }) { | |
| 100 | return ( | |
| Merge branch 'worktree-agent-ae1299e92e4462012' | 101 | <Hint label={`Assigned to ${agent}`}> |
| 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. | 102 | <Badge tone="accent" className="text-xs"> |
| Merge branch 'worktree-agent-ae1299e92e4462012' | 103 | <Bot size={12} /> |
| 104 | <span className="sr-only">Assigned to </span> | |
| 105 | {agent} | |
| 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. | 106 | </Badge> |
| Merge branch 'worktree-agent-ae1299e92e4462012' | 107 | </Hint> |
| Plan: Projects, the home of everything about running software | 108 | ); |
| 109 | } | |
| 110 | ||
| 111 | const PILL = "inline-flex shrink-0 items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs font-medium"; | |
| 112 | ||
| 113 | /** The large state badge at the top of an issue. */ | |
| 114 | export function IssueState({ issue }: { issue: Pick<Issue, "state" | "reason"> }) { | |
| 115 | const style = | |
| 116 | issue.state === "open" | |
| Merge branch 'worktree-agent-ad7c6d88d93adc817' | 117 | ? "border-success/30 bg-success/10 text-success" |
| Plan: Projects, the home of everything about running software | 118 | : issue.reason === "not_planned" |
| 119 | ? "border-line bg-surface text-muted" | |
| 120 | : "border-merged/30 bg-merged/10 text-merged"; | |
| 121 | return ( | |
| 122 | <span className={`${PILL} ${style}`}> | |
| 123 | <IssueIcon issue={issue} size={14} /> | |
| 124 | {issue.state === "open" | |
| 125 | ? "Open" | |
| 126 | : issue.reason === "not_planned" | |
| 127 | ? "Closed as not planned" | |
| 128 | : "Closed"} | |
| 129 | </span> | |
| 130 | ); | |
| 131 | } | |
| 132 | ||
| 133 | const PULL_STYLES: Record<Pull["status"], string> = { | |
| 134 | draft: "border-line bg-surface text-muted", | |
| Merge branch 'worktree-agent-ad7c6d88d93adc817' | 135 | open: "border-success/30 bg-success/10 text-success", |
| Plan: Projects, the home of everything about running software | 136 | merged: "border-merged/30 bg-merged/10 text-merged", |
| 137 | closed: "border-danger/30 bg-danger/10 text-danger", | |
| 138 | }; | |
| 139 | ||
| 140 | const PULL_NAMES: Record<Pull["status"], string> = { | |
| 141 | draft: "Draft", | |
| 142 | open: "Open", | |
| 143 | merged: "Merged", | |
| 144 | closed: "Closed", | |
| 145 | }; | |
| 146 | ||
| 147 | export function PullState({ status }: { status: Pull["status"] }) { | |
| 148 | return ( | |
| 149 | <span className={`${PILL} ${PULL_STYLES[status]}`}> | |
| 150 | <PullIcon status={status} size={14} /> | |
| 151 | {PULL_NAMES[status]} | |
| 152 | </span> | |
| 153 | ); | |
| 154 | } | |
| 155 | ||
| 156 | /** The open and closed switch above a list. `to` is the list's own path. */ | |
| 157 | export function StateTabs({ | |
| 158 | to, | |
| 159 | state, | |
| 160 | query = "", | |
| 161 | action, | |
| 162 | }: { | |
| 163 | to: string; | |
| 164 | state: State; | |
| 165 | /** Other query parameters to keep, e.g. `label=bug`. */ | |
| 166 | query?: string; | |
| 167 | action?: ReactNode; | |
| 168 | }) { | |
| 169 | const link = (value: State) => | |
| 170 | `${to}?${[value === "open" ? "" : "state=closed", query].filter(Boolean).join("&")}`; | |
| 171 | const tab = (value: State, label: string) => ( | |
| 172 | <Link | |
| 173 | to={link(value)} | |
| 174 | className={ | |
| 175 | "rounded-md px-3 py-1.5 text-sm transition-colors " + | |
| 176 | (state === value ? "bg-raised font-medium text-fg" : "text-muted hover:text-fg") | |
| 177 | } | |
| 178 | > | |
| 179 | {label} | |
| 180 | </Link> | |
| 181 | ); | |
| 182 | return ( | |
| 183 | <div className="flex flex-wrap items-center justify-between gap-3"> | |
| 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. | 184 | <Card tone="plain" radius="lg" className="flex gap-1 p-1"> |
| Plan: Projects, the home of everything about running software | 185 | {tab("open", "Open")} |
| 186 | {tab("closed", "Closed")} | |
| 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. | 187 | </Card> |
| Plan: Projects, the home of everything about running software | 188 | {action} |
| 189 | </div> | |
| 190 | ); | |
| 191 | } | |
| 192 | ||
| 193 | const VERDICTS = { | |
| Merge branch 'worktree-agent-ad7c6d88d93adc817' | 194 | approve: { label: "approved these changes", style: "text-success" }, |
| Plan: Projects, the home of everything about running software | 195 | request_changes: { label: "requested changes", style: "text-danger" }, |
| 196 | } as const; | |
| 197 | ||
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 198 | /** |
| 199 | * A person's name (or `children`) linking to their profile at `/u/<name>`. | |
| g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent | 200 | * g1t, and names that are not usernames, stay plain text. |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 201 | */ |
| 202 | export function PersonLink({ | |
| 203 | name, | |
| 204 | className, | |
| 205 | label, | |
| 206 | children, | |
| 207 | }: { | |
| 208 | name: string; | |
| 209 | className?: string; | |
| 210 | label?: string; | |
| 211 | children?: ReactNode; | |
| 212 | }) { | |
| Merge commit authors and hovercards: commits show their accounts, every name opens a card (repos 0016) | 213 | // g1t itself has no profile; its name carries a small label instead, and |
| 214 | // its card says what it is. | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 215 | if (name === "g1t" && !children) { |
| 216 | return ( | |
| Merge commit authors and hovercards: commits show their accounts, every name opens a card (repos 0016) | 217 | <UserCard username="g1t"> |
| 218 | <span tabIndex={0} className="inline-flex items-baseline gap-1 rounded outline-none focus-visible:ring-2 focus-visible:ring-accent"> | |
| 219 | <span className={className}>g1t</span> | |
| 220 | <span className="rounded border border-line px-1 text-[0.625rem] leading-[1.35] font-medium text-muted">bot</span> | |
| 221 | </span> | |
| 222 | </UserCard> | |
| 223 | ); | |
| 224 | } | |
| 225 | if (name === "g1t") { | |
| 226 | return ( | |
| 227 | <UserCard username="g1t"> | |
| 228 | <span className={className}>{children}</span> | |
| 229 | </UserCard> | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 230 | ); |
| 231 | } | |
| Merge commit authors and hovercards: commits show their accounts, every name opens a card (repos 0016) | 232 | // Ghost, a deleted account, has no profile and no card. |
| 233 | if (name === "ghost" || !/^[a-z0-9-]{1,39}$/i.test(name)) { | |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 234 | return <span className={className}>{children ?? name}</span>; |
| 235 | } | |
| 236 | return ( | |
| Merge commit authors and hovercards: commits show their accounts, every name opens a card (repos 0016) | 237 | <UserCard username={name}> |
| 238 | <Link to={`/u/${name.toLowerCase()}`} className={className} aria-label={label}> | |
| 239 | {children ?? name} | |
| 240 | </Link> | |
| 241 | </UserCard> | |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 242 | ); |
| 243 | } | |
| 244 | ||
| Plan: Projects, the home of everything about running software | 245 | /** |
| 246 | * Comments in order, then the box to add one. On a pull request, `review` | |
| 247 | * says where its changes are shown and whether the viewer may give a | |
| 248 | * verdict. | |
| 249 | */ | |
| 250 | /** | |
| 251 | * One entry in a conversation: who, what they did and when, then what they | |
| 252 | * wrote. The author's avatar sits beside it, as on any forge. | |
| 253 | */ | |
| 254 | export function TimelineItem({ | |
| 255 | author, | |
| Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar | 256 | agent, |
| Plan: Projects, the home of everything about running software | 257 | action, |
| 258 | at, | |
| Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts | 259 | edited, |
| Plan: Projects, the home of everything about running software | 260 | aside, |
| 261 | children, | |
| 262 | }: { | |
| 263 | author: string; | |
| Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar | 264 | /** |
| 265 | * Set when one of the workspace's agents wrote it, as itself: shown by | |
| 266 | * its face and name with an Agent badge, linked to its page, and whom it | |
| 267 | * acted for, in place of `author`. | |
| 268 | */ | |
| 269 | agent?: { ref: AgentRef; href: string | null; actingFor?: string | null }; | |
| Plan: Projects, the home of everything about running software | 270 | /** What they did, after their name: "commented", "opened this". */ |
| 271 | action: ReactNode; | |
| 272 | at?: string; | |
| Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts | 273 | /** When what they wrote was last edited, if it was. */ |
| 274 | edited?: string | null; | |
| Plan: Projects, the home of everything about running software | 275 | /** Shown at the right of the header. */ |
| 276 | aside?: ReactNode; | |
| 277 | children?: ReactNode; | |
| 278 | }) { | |
| Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar | 279 | const face = (size: number) => |
| 280 | agent ? ( | |
| Agents have faces, and are never mistaken for people. Every agent wears a little bot face drawn from a look it owns, shape, colour, eyes, mouth, antenna, accessory and pattern, chosen in its builder and on its Profile tab with a live preview, Shuffle and a way back to the face its seed gives it; the face blinks on its own time, breathes, narrows its eyes while the agent works, shuts them asleep and bounces when it finishes, all of it still for anyone who asked for less motion. Wherever an agent shows, in chat, in a list, on a mention, on a review or a commit, its avatar carries an agent marker, and the people reading it are told so. In Chat, direct messages are two lists: People, and Agents, which also holds the agents you haven't talked to yet; a conversation with both a person and an agent in it is marked in the list, named in the conversation's header, spelled out by the composer and explained once the first time it opens. Agents keep their look in the agents service, which every service passes along. The chat and agents guides say so, and CONTRIBUTING makes the shared avatar the only way to draw an agent. | 281 | <AgentAvatar agent={{ id: agent.ref.id, handle: agent.ref.handle, avatar_seed: agent.ref.avatarSeed, look: agent.ref.look }} size={size} /> |
| Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar | 282 | ) : ( |
| 283 | <Avatar name={author} size={size} /> | |
| 284 | ); | |
| Plan: Projects, the home of everything about running software | 285 | return ( |
| 286 | <div className="flex gap-3"> | |
| 287 | <span className="mt-1 hidden shrink-0 sm:block"> | |
| Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar | 288 | {agent ? ( |
| 289 | agent.href ? ( | |
| 290 | <Link to={agent.href} aria-label={`${agent.ref.displayName}, an agent`}> | |
| 291 | {face(32)} | |
| 292 | </Link> | |
| 293 | ) : ( | |
| 294 | face(32) | |
| 295 | ) | |
| 296 | ) : ( | |
| 297 | <PersonLink name={author} label={`${author}'s profile`}> | |
| 298 | {face(32)} | |
| 299 | </PersonLink> | |
| 300 | )} | |
| Plan: Projects, the home of everything about running software | 301 | </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. | 302 | <Card asChild className="min-w-0 grow overflow-hidden"> |
| 303 | <article> | |
| 304 | <header className="flex flex-wrap items-center gap-x-2 gap-y-1 border-b border-line bg-raised/40 px-4 py-2 text-sm text-muted"> | |
| 305 | <span className="sm:hidden">{face(18)}</span> | |
| 306 | {agent ? ( | |
| 307 | <AgentName agent={agent.ref} href={agent.href} /> | |
| 308 | ) : ( | |
| 309 | <PersonLink name={author} className="font-medium text-fg hover:underline" /> | |
| 310 | )} | |
| 311 | {action} | |
| 312 | {at && <TimeAgo at={at} />} | |
| 313 | {agent?.actingFor && ( | |
| 314 | <span className="text-xs text-faint"> | |
| 315 | on behalf of{" "} | |
| 316 | <PersonLink name={agent.actingFor} className="hover:text-fg hover:underline"> | |
| 317 | @{agent.actingFor} | |
| 318 | </PersonLink> | |
| Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts | 319 | </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. | 320 | )} |
| 321 | {edited && ( | |
| 322 | <Hint | |
| 323 | label={ | |
| 324 | <> | |
| 325 | Edited <TimeAgo at={edited} /> | |
| 326 | </> | |
| 327 | } | |
| 328 | > | |
| 329 | <span tabIndex={0} className="text-xs text-faint"> | |
| 330 | edited | |
| 331 | </span> | |
| 332 | </Hint> | |
| 333 | )} | |
| 334 | {aside && <span className="ml-auto min-w-0">{aside}</span>} | |
| 335 | </header> | |
| 336 | {children && <div className="px-4 py-3">{children}</div>} | |
| 337 | </article> | |
| 338 | </Card> | |
| Plan: Projects, the home of everything about running software | 339 | </div> |
| 340 | ); | |
| 341 | } | |
| 342 | ||
| Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar | 343 | /** An agent's name, linked to its page when there is one, with an Agent badge. */ |
| 344 | function AgentName({ agent, href }: { agent: AgentRef; href: string | null }) { | |
| 345 | const name = <span className="font-medium text-fg">{agent.displayName}</span>; | |
| 346 | return ( | |
| 347 | <span className="inline-flex items-center gap-1.5"> | |
| 348 | {href ? ( | |
| 349 | <Link to={href} className="hover:underline"> | |
| 350 | {name} | |
| 351 | </Link> | |
| 352 | ) : ( | |
| 353 | name | |
| 354 | )} | |
| 355 | <Badge tone="accent" className="px-1.5 text-[0.625rem] leading-[1.35]"> | |
| 356 | Agent | |
| 357 | </Badge> | |
| 358 | </span> | |
| 359 | ); | |
| 360 | } | |
| 361 | ||
| 362 | /** | |
| 363 | * An agent's page, `/<workspace>/-/agents/<handle>`, from the repository's | |
| 364 | * path (`/acme/web`): its agents are its workspace's. | |
| 365 | */ | |
| 366 | export function agentPage(base: string | undefined, handle: string): string | null { | |
| 367 | const owner = base?.split("/").filter(Boolean)[0]; | |
| 368 | return owner ? `/${owner}/-/agents/${handle}` : null; | |
| 369 | } | |
| 370 | ||
| 371 | /** That an agent's review is advisory, and why it counts toward nothing. */ | |
| 372 | function AdvisoryChip() { | |
| 373 | return ( | |
| 374 | <Hint label="Agent reviews don't count toward required approvals"> | |
| 375 | <span tabIndex={0} className="inline-flex rounded-full outline-none focus-visible:ring-2 focus-visible:ring-accent"> | |
| 376 | <Badge tone="neutral">Advisory</Badge> | |
| 377 | </span> | |
| 378 | </Hint> | |
| 379 | ); | |
| 380 | } | |
| 381 | ||
| Plan: Projects, the home of everything about running software | 382 | type Review = { changesUrl: string; canJudge: boolean }; |
| 383 | ||
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 384 | /** |
| 385 | * Text with each `#12` linked to the issue or pull request of that number, | |
| 386 | * and each file named in backticks shown as code, linked to its diff when | |
| 387 | * `filesUrl` says where the changes are. | |
| 388 | */ | |
| 389 | function WithReferences({ text, base, filesUrl }: { text: string; base?: string; filesUrl?: string }) { | |
| Plan: Projects, the home of everything about running software | 390 | if (!base) return <>{text}</>; |
| 391 | return ( | |
| 392 | <> | |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 393 | {text.split(/(#\d+|`[^`\n]+`)/).map((part, index) => |
| 394 | /^`[^`]+`$/.test(part) ? ( | |
| 395 | filesUrl ? ( | |
| 396 | <Link | |
| 397 | key={index} | |
| 398 | to={`${filesUrl}#file-${part.slice(1, -1)}`} | |
| 399 | className="font-mono text-xs text-fg hover:underline" | |
| 400 | > | |
| 401 | {part.slice(1, -1)} | |
| 402 | </Link> | |
| 403 | ) : ( | |
| 404 | <code key={index} className="font-mono text-xs text-fg"> | |
| 405 | {part.slice(1, -1)} | |
| 406 | </code> | |
| 407 | ) | |
| 408 | ) : /^#\d+$/.test(part) ? ( | |
| Plan: Projects, the home of everything about running software | 409 | // Issues and pull requests share numbers; the issue page forwards. |
| 410 | <Link | |
| 411 | key={index} | |
| 412 | to={`${base}/issues/${part.slice(1)}`} | |
| 413 | className="font-medium text-fg hover:underline" | |
| 414 | > | |
| 415 | {part} | |
| 416 | </Link> | |
| 417 | ) : ( | |
| 418 | part | |
| 419 | ), | |
| 420 | )} | |
| 421 | </> | |
| 422 | ); | |
| 423 | } | |
| 424 | ||
| 425 | /** Something that happened, as one line on the conversation's rail. */ | |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 426 | function TimelineEvent({ comment, base, filesUrl }: { comment: Comment; base?: string; filesUrl?: string }) { |
| Plan: Projects, the home of everything about running software | 427 | return ( |
| 428 | <div className="flex items-center gap-3 text-sm text-muted"> | |
| 429 | <span className="hidden w-8 shrink-0 justify-center sm:flex"> | |
| 430 | <span className="size-2 rounded-full border border-line-strong bg-bg" /> | |
| 431 | </span> | |
| 432 | <span className="flex min-w-0 flex-wrap items-center gap-x-1.5 gap-y-0.5"> | |
| 433 | <Avatar name={comment.author.username} size={16} /> | |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 434 | <PersonLink name={comment.author.username} className="font-medium text-fg hover:underline" /> |
| Plan: Projects, the home of everything about running software | 435 | <span> |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 436 | <WithReferences text={comment.body} base={base} filesUrl={filesUrl} /> |
| Plan: Projects, the home of everything about running software | 437 | </span> |
| 438 | <span className="text-faint"> | |
| 439 | · <TimeAgo at={comment.createdAt} /> | |
| 440 | </span> | |
| 441 | </span> | |
| 442 | </div> | |
| 443 | ); | |
| 444 | } | |
| 445 | ||
| Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts | 446 | /** Edit and Delete, under a comment the viewer may change. */ |
| 447 | function CommentActions({ | |
| 448 | comment, | |
| 449 | can, | |
| 450 | error, | |
| 451 | }: { | |
| 452 | comment: Comment; | |
| 453 | can: { edit: boolean; delete: boolean }; | |
| 454 | error?: string | null; | |
| 455 | }) { | |
| 456 | return ( | |
| 457 | <div className="mt-3 border-t border-line pt-2 text-sm"> | |
| 458 | <div className="flex flex-wrap items-center gap-1"> | |
| 459 | {can.edit && ( | |
| 460 | <details className="open:order-last open:w-full"> | |
| 461 | <summary className="inline-flex cursor-pointer list-none items-center gap-1 rounded-md px-1.5 py-1 text-xs text-faint hover:text-fg"> | |
| 462 | <Pencil size={12} /> | |
| 463 | Edit | |
| 464 | </summary> | |
| 465 | <Form method="post" className="mt-2 space-y-2"> | |
| 466 | <input type="hidden" name="action" value="edit-comment" /> | |
| 467 | <input type="hidden" name="comment" value={comment.id} /> | |
| 468 | <MentionTextarea name="body" rows={4} defaultValue={comment.body} /> | |
| 469 | <SubmitButton match={{ action: "edit-comment", comment: comment.id }} pending="Saving…"> | |
| 470 | Save | |
| 471 | </SubmitButton> | |
| 472 | </Form> | |
| 473 | </details> | |
| 474 | )} | |
| 475 | {can.delete && ( | |
| 476 | <AlertDialog> | |
| 477 | <AlertDialogTrigger asChild> | |
| 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. | 478 | <Button |
| Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts | 479 | type="button" |
| 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. | 480 | variant="ghost" |
| 481 | size="inline" | |
| 482 | className="gap-1 px-1.5 py-1 text-xs text-faint hover:text-danger font-normal" | |
| Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts | 483 | > |
| 484 | <Trash2 size={12} /> | |
| 485 | Delete | |
| 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. | 486 | </Button> |
| Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts | 487 | </AlertDialogTrigger> |
| 488 | <AlertDialogContent> | |
| 489 | <Form method="post" className="grid gap-4"> | |
| 490 | <input type="hidden" name="action" value="delete-comment" /> | |
| 491 | <input type="hidden" name="comment" value={comment.id} /> | |
| 492 | <AlertDialogHeader> | |
| 493 | <AlertDialogTitle>Delete this comment?</AlertDialogTitle> | |
| 494 | <AlertDialogDescription>It is removed from the conversation for everyone. This cannot be undone.</AlertDialogDescription> | |
| 495 | </AlertDialogHeader> | |
| 496 | <AlertDialogFooter> | |
| 497 | <AlertDialogCancel>Cancel</AlertDialogCancel> | |
| 498 | <AlertDialogAction asChild> | |
| 499 | <button type="submit"> | |
| 500 | <Trash2 size={14} /> | |
| 501 | Delete comment | |
| 502 | </button> | |
| 503 | </AlertDialogAction> | |
| 504 | </AlertDialogFooter> | |
| 505 | </Form> | |
| 506 | </AlertDialogContent> | |
| 507 | </AlertDialog> | |
| 508 | )} | |
| 509 | </div> | |
| 510 | {error && <ErrorText>{error}</ErrorText>} | |
| 511 | </div> | |
| 512 | ); | |
| 513 | } | |
| 514 | ||
| Plan: Projects, the home of everything about running software | 515 | /** |
| 516 | * The conversation of an issue or a pull request, oldest first: what | |
| 517 | * people and agents wrote, and between those, what happened. | |
| 518 | */ | |
| 519 | export function CommentList({ | |
| 520 | comments, | |
| 521 | review, | |
| 522 | base, | |
| Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts | 523 | viewerId, |
| 524 | canModerate = false, | |
| 525 | failed, | |
| Plan: Projects, the home of everything about running software | 526 | }: { |
| 527 | comments: Comment[]; | |
| 528 | review?: Review; | |
| 529 | /** The repository's path, so that `#12` in an event can be linked. */ | |
| 530 | base?: string; | |
| Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts | 531 | /** The signed-in viewer, who may edit and delete their own comments. */ |
| 532 | viewerId?: string | null; | |
| 533 | /** Maintain and up: may edit and delete anyone's comments. */ | |
| 534 | canModerate?: boolean; | |
| 535 | /** Why editing or deleting a comment was refused, under that comment. */ | |
| 536 | failed?: { comment: string; error?: string | null } | null; | |
| Plan: Projects, the home of everything about running software | 537 | }) { |
| 538 | return ( | |
| 539 | <> | |
| 540 | {comments.map((comment) => { | |
| 541 | if (comment.kind === "event") { | |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 542 | return <TimelineEvent key={comment.id} comment={comment} base={base} filesUrl={review?.changesUrl} />; |
| Plan: Projects, the home of everything about running software | 543 | } |
| 544 | const verdict = comment.verdict && VERDICTS[comment.verdict]; | |
| Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts | 545 | const can = mayChangeComment(comment, viewerId, canModerate); |
| Plan: Projects, the home of everything about running software | 546 | return ( |
| 547 | <TimelineItem | |
| 548 | key={comment.id} | |
| 549 | author={comment.author.username} | |
| Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar | 550 | agent={ |
| 551 | comment.agent && { | |
| 552 | ref: comment.agent, | |
| 553 | href: agentPage(base, comment.agent.handle), | |
| 554 | actingFor: comment.actingFor?.username, | |
| 555 | } | |
| 556 | } | |
| Plan: Projects, the home of everything about running software | 557 | at={comment.createdAt} |
| 558 | action={ | |
| Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar | 559 | <> |
| 560 | {verdict ? ( | |
| 561 | <span className={`flex items-center gap-1 font-medium ${verdict.style}`}> | |
| 562 | {comment.verdict === "approve" ? ( | |
| 563 | <CircleCheck size={14} /> | |
| 564 | ) : ( | |
| 565 | <CircleSlash size={14} /> | |
| 566 | )} | |
| 567 | {verdict.label} | |
| 568 | </span> | |
| 569 | ) : ( | |
| 570 | <span>{comment.advisory ? "reviewed" : "commented"}</span> | |
| 571 | )} | |
| 572 | {comment.advisory && <AdvisoryChip />} | |
| 573 | </> | |
| Plan: Projects, the home of everything about running software | 574 | } |
| Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts | 575 | edited={comment.editedAt} |
| Plan: Projects, the home of everything about running software | 576 | aside={ |
| 577 | comment.path && | |
| 578 | review && ( | |
| 579 | <Link | |
| 580 | to={`${review.changesUrl}#file-${comment.path}`} | |
| 581 | className="block truncate font-mono text-xs text-faint hover:text-fg" | |
| 582 | > | |
| 583 | {comment.path} | |
| 584 | {comment.line != null && `:${comment.line}`} | |
| 585 | </Link> | |
| 586 | ) | |
| 587 | } | |
| 588 | > | |
| 589 | {comment.body && <Markdown source={comment.body} repo={repoAt(base)} />} | |
| Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts | 590 | {(can.edit || can.delete) && ( |
| 591 | <CommentActions comment={comment} can={can} error={failed?.comment === comment.id ? failed.error : null} /> | |
| 592 | )} | |
| Plan: Projects, the home of everything about running software | 593 | </TimelineItem> |
| 594 | ); | |
| 595 | })} | |
| 596 | </> | |
| 597 | ); | |
| 598 | } | |
| 599 | ||
| 600 | /** Where a signed-in person writes a comment, or a review. */ | |
| 601 | export function CommentForm({ | |
| 602 | author, | |
| 603 | resetKey, | |
| 604 | review, | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 605 | handles, |
| Plan: Projects, the home of everything about running software | 606 | }: { |
| 607 | /** The viewer's username, or null if they are signed out. */ | |
| 608 | author: string | null; | |
| 609 | /** Changes when a comment is added, which clears the box. */ | |
| 610 | resetKey: number; | |
| 611 | review?: Review; | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 612 | /** Teams the box offers to mention, as `@workspace/team`. */ |
| 613 | handles?: readonly string[]; | |
| Plan: Projects, the home of everything about running software | 614 | }) { |
| 615 | if (!author) { | |
| 616 | return ( | |
| 617 | <p className="text-sm text-muted"> | |
| 618 | <Link to="/login" className="text-fg underline underline-offset-4"> | |
| 619 | Sign in | |
| 620 | </Link>{" "} | |
| 621 | to comment. | |
| 622 | </p> | |
| 623 | ); | |
| 624 | } | |
| 625 | return ( | |
| 626 | <div className="flex gap-3"> | |
| 627 | <span className="mt-1 hidden shrink-0 sm:block"> | |
| 628 | <Avatar name={author} size={32} /> | |
| 629 | </span> | |
| 630 | <Form method="post" className="min-w-0 grow space-y-2" key={resetKey}> | |
| 631 | <input type="hidden" name="action" value="comment" /> | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 632 | <MentionTextarea |
| Plan: Projects, the home of everything about running software | 633 | name="body" |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 634 | handles={handles} |
| Plan: Projects, the home of everything about running software | 635 | rows={3} |
| 636 | placeholder={ | |
| 637 | review?.canJudge | |
| 638 | ? "Leave a comment, or a review. Markdown works." | |
| 639 | : "Leave a comment. Markdown works." | |
| 640 | } | |
| 641 | /> | |
| 642 | <div className="flex flex-wrap gap-2"> | |
| Merge branch 'worktree-agent-a985c01f43538af01' | 643 | {/* A plain comment's verdict is none the action knows, so each button tells its own post apart. */} |
| 644 | <SubmitButton name="verdict" value="comment" match={{ action: "comment" }} pending="Commenting…"> | |
| 645 | Comment | |
| 646 | </SubmitButton> | |
| Plan: Projects, the home of everything about running software | 647 | {review?.canJudge && ( |
| 648 | <> | |
| 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. | 649 | <SubmitButton variant="outline" name="verdict" value="approve" match={{ action: "comment" }} pending="Approving…"> |
| Merge branch 'worktree-agent-ad7c6d88d93adc817' | 650 | <CircleCheck size={14} className="text-success" /> |
| Plan: Projects, the home of everything about running software | 651 | Approve |
| Merge branch 'worktree-agent-a985c01f43538af01' | 652 | </SubmitButton> |
| 653 | <SubmitButton | |
| 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. | 654 | variant="outline" |
| Merge branch 'worktree-agent-a985c01f43538af01' | 655 | name="verdict" |
| 656 | value="request_changes" | |
| 657 | match={{ action: "comment" }} | |
| 658 | pending="Requesting changes…" | |
| 659 | > | |
| Plan: Projects, the home of everything about running software | 660 | <CircleSlash size={14} className="text-danger" /> |
| 661 | Request changes | |
| Merge branch 'worktree-agent-a985c01f43538af01' | 662 | </SubmitButton> |
| Plan: Projects, the home of everything about running software | 663 | </> |
| 664 | )} | |
| 665 | </div> | |
| 666 | </Form> | |
| 667 | </div> | |
| 668 | ); | |
| 669 | } | |
| 670 | ||
| 671 | /** | |
| 672 | * Checkboxes for choosing people: the workspace's members, anyone already | |
| 673 | * chosen, and a box for other usernames. Posts `name` for each ticked and | |
| 674 | * `others` for the rest. | |
| 675 | */ | |
| 676 | export function PeoplePicker({ | |
| 677 | name, | |
| 678 | members, | |
| 679 | chosen, | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 680 | placeholder = "Other usernames, comma separated", |
| Plan: Projects, the home of everything about running software | 681 | }: { |
| 682 | name: string; | |
| 683 | members: string[]; | |
| 684 | chosen: string[]; | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 685 | placeholder?: string; |
| Plan: Projects, the home of everything about running software | 686 | }) { |
| 687 | const people = [...new Set([...members, ...chosen])]; | |
| 688 | return ( | |
| 689 | <> | |
| 690 | {people.length > 0 && ( | |
| 691 | <div className="space-y-1.5"> | |
| 692 | {people.map((person) => ( | |
| Workspace names and icons, and a component kit for every control | 693 | <CheckboxOption |
| 694 | key={person} | |
| 695 | name={name} | |
| 696 | value={person} | |
| 697 | defaultChecked={chosen.includes(person)} | |
| 698 | className="items-center" | |
| 699 | labelClassName="flex items-center gap-2" | |
| 700 | label={ | |
| 701 | <> | |
| 702 | <Avatar name={person} size={18} /> | |
| 703 | <span className="font-mono text-xs">{person}</span> | |
| 704 | </> | |
| 705 | } | |
| 706 | /> | |
| Plan: Projects, the home of everything about running software | 707 | ))} |
| 708 | </div> | |
| 709 | )} | |
| 710 | <input | |
| 711 | name="others" | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 712 | placeholder={placeholder} |
| Plan: Projects, the home of everything about running software | 713 | autoComplete="off" |
| 714 | data-1p-ignore | |
| 715 | className="w-full rounded-md border border-line bg-bg px-3 py-2 text-sm outline-none placeholder:text-faint hover:border-line-strong focus:border-accent-dim" | |
| 716 | /> | |
| 717 | </> | |
| 718 | ); | |
| 719 | } | |
| 720 | ||
| Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar | 721 | /** |
| 722 | * Where each reviewer stands: their most recent verdict. An agent's review | |
| 723 | * is advisory and is not one of them. | |
| 724 | */ | |
| Plan: Projects, the home of everything about running software | 725 | export function verdicts(comments: Comment[]): { reviewer: string; verdict: NonNullable<Comment["verdict"]> }[] { |
| 726 | const latest = new Map<string, NonNullable<Comment["verdict"]>>(); | |
| 727 | for (const comment of comments) { | |
| Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar | 728 | if (comment.verdict && !comment.agent && !comment.advisory) latest.set(comment.author.username, comment.verdict); |
| Plan: Projects, the home of everything about running software | 729 | } |
| 730 | return [...latest].map(([reviewer, verdict]) => ({ reviewer, verdict })); | |
| 731 | } | |
| 732 | ||
| Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar | 733 | /** |
| 734 | * Each agent's latest review, advisory: shown beside people's verdicts but | |
| 735 | * never one of them. `verdict` is null for a review that only commented. | |
| 736 | */ | |
| 737 | export function agentReviews(comments: Comment[]): { agent: AgentRef; verdict: Comment["verdict"] }[] { | |
| 738 | const latest = new Map<string, { agent: AgentRef; verdict: Comment["verdict"] }>(); | |
| 739 | for (const comment of comments) { | |
| 740 | if (comment.agent && comment.advisory) { | |
| 741 | latest.delete(comment.agent.id); | |
| 742 | latest.set(comment.agent.id, { agent: comment.agent, verdict: comment.verdict }); | |
| 743 | } | |
| 744 | } | |
| 745 | return [...latest.values()]; | |
| 746 | } | |
| 747 | ||
| 748 | /** An agent's advisory review, in a pull request's summary of reviews. */ | |
| 749 | export function AgentReviewLine({ agent, verdict, base }: { agent: AgentRef; verdict: Comment["verdict"]; base?: string }) { | |
| 750 | const href = agentPage(base, agent.handle); | |
| 751 | const said = verdict === "approve" ? "approved" : verdict === "request_changes" ? "requested changes" : "reviewed"; | |
| 752 | return ( | |
| 753 | <span className="flex items-center gap-1.5 text-muted"> | |
| Agents have faces, and are never mistaken for people. Every agent wears a little bot face drawn from a look it owns, shape, colour, eyes, mouth, antenna, accessory and pattern, chosen in its builder and on its Profile tab with a live preview, Shuffle and a way back to the face its seed gives it; the face blinks on its own time, breathes, narrows its eyes while the agent works, shuts them asleep and bounces when it finishes, all of it still for anyone who asked for less motion. Wherever an agent shows, in chat, in a list, on a mention, on a review or a commit, its avatar carries an agent marker, and the people reading it are told so. In Chat, direct messages are two lists: People, and Agents, which also holds the agents you haven't talked to yet; a conversation with both a person and an agent in it is marked in the list, named in the conversation's header, spelled out by the composer and explained once the first time it opens. Agents keep their look in the agents service, which every service passes along. The chat and agents guides say so, and CONTRIBUTING makes the shared avatar the only way to draw an agent. | 754 | <AgentAvatar agent={{ id: agent.id, handle: agent.handle, avatar_seed: agent.avatarSeed, look: agent.look }} size={16} /> |
| Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar | 755 | <AgentName agent={agent} href={href} /> |
| 756 | <span>{said}</span> | |
| 757 | <AdvisoryChip /> | |
| 758 | </span> | |
| 759 | ); | |
| 760 | } | |
| 761 | ||
| Plan: Projects, the home of everything about running software | 762 | /** `2 files +12 −3`: the size of a pull request's change. */ |
| 763 | export function ChangeSize({ files }: { files: Pull["files"] }) { | |
| 764 | if (files.length === 0) return null; | |
| 765 | const additions = files.reduce((sum, file) => sum + file.additions, 0); | |
| 766 | const deletions = files.reduce((sum, file) => sum + file.deletions, 0); | |
| 767 | return ( | |
| 768 | <span className="flex shrink-0 items-center gap-1.5 font-mono text-xs text-faint"> | |
| 769 | <span> | |
| 770 | {files.length} {files.length === 1 ? "file" : "files"} | |
| 771 | </span> | |
| Merge branch 'worktree-agent-ad7c6d88d93adc817' | 772 | <span className="text-success">+{additions}</span> |
| Plan: Projects, the home of everything about running software | 773 | <span className="text-danger">−{deletions}</span> |
| 774 | </span> | |
| 775 | ); | |
| 776 | } | |
| 777 | ||
| 778 | /** The words of a Markdown text, without its markup, for a one-line preview. */ | |
| 779 | export function plainText(markdown: string): string { | |
| 780 | return markdown | |
| 781 | .replace(/```[\s\S]*?```/g, " ") | |
| 782 | .replace(/^\s{0,3}(#{1,6}|[-*+]|\d+\.)\s+/gm, "") | |
| 783 | .replace(/!?\[([^\]]*)\]\([^)]*\)/g, "$1") | |
| 784 | .replace(/[*_`~]/g, "") | |
| 785 | .replace(/\s+/g, " ") | |
| 786 | .trim(); | |
| 787 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.