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"; |
| Sidebar: the panels really slide | 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"; |
| Sidebar: the panels really slide | 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"; |
| Sidebar: the panels really slide | 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"; |
| Sidebar: the panels really slide | 30 | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 31 | export { IssueIcon, PullIcon }; |
| 32 | ||
| Sidebar: the panels really slide | 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 }) { | |
| Sidebar: the panels really slide | 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))`, | |
| Sidebar: the panels really slide | 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> | |
| Sidebar: the panels really slide | 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> | |
| Sidebar: the panels really slide | 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" |
| Sidebar: the panels really slide | 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", |
| Sidebar: the panels really slide | 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" }, |
| Sidebar: the panels really slide | 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 | ||
| Sidebar: the panels really slide | 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, |
| Sidebar: the panels really slide | 254 | action, |
| 255 | at, | |
| Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts | 256 | edited, |
| Sidebar: the panels really slide | 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 }; | |
| Sidebar: the panels really slide | 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; | |
| Sidebar: the panels really slide | 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 | ); | |
| Sidebar: the panels really slide | 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 | )} | |
| Sidebar: the panels really slide | 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 | )} | |
| Sidebar: the panels really slide | 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 | )} | |
| Sidebar: the panels really slide | 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 | ||
| Sidebar: the panels really slide | 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 }) { | |
| Sidebar: the panels really slide | 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) ? ( | |
| Sidebar: the panels really slide | 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 }) { |
| Sidebar: the panels really slide | 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" /> |
| Sidebar: the panels really slide | 430 | <span> |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 431 | <WithReferences text={comment.body} base={base} filesUrl={filesUrl} /> |
| Sidebar: the panels really slide | 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 | ||
| Sidebar: the panels really slide | 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, | |
| Sidebar: the panels really slide | 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; | |
| Sidebar: the panels really slide | 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} />; |
| Sidebar: the panels really slide | 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); |
| Sidebar: the panels really slide | 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 | } | |
| Sidebar: the panels really slide | 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 | </> | |
| Sidebar: the panels really slide | 567 | } |
| Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts | 568 | edited={comment.editedAt} |
| Sidebar: the panels really slide | 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 | )} | |
| Sidebar: the panels really slide | 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, |
| Sidebar: the panels really slide | 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[]; | |
| Sidebar: the panels really slide | 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 |
| Sidebar: the panels really slide | 626 | name="body" |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 627 | handles={handles} |
| Sidebar: the panels really slide | 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> | |
| Sidebar: the panels really slide | 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" /> |
| Sidebar: the panels really slide | 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 | > | |
| Sidebar: the panels really slide | 653 | <CircleSlash size={14} className="text-danger" /> |
| 654 | Request changes | |
| Merge branch 'worktree-agent-a985c01f43538af01' | 655 | </SubmitButton> |
| Sidebar: the panels really slide | 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", |
| Sidebar: the panels really slide | 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; |
| Sidebar: the panels really slide | 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 | /> | |
| Sidebar: the panels really slide | 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} |
| Sidebar: the panels really slide | 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 | */ | |
| Sidebar: the panels really slide | 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); |
| Sidebar: the panels really slide | 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 | ||
| Sidebar: the panels really slide | 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> |
| Sidebar: the panels really slide | 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.