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