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.
| Agents as a team: lifecycle, merge queue, billing and a new shell | 1 | import { |
| 2 | ChevronDown, | |
| 3 | ChevronRight, | |
| 4 | Columns2, | |
| 5 | FileDiff as FileIcon, | |
| 6 | FileMinus, | |
| 7 | FilePlus, | |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 8 | Files, |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 9 | Folder, |
| 10 | MessageSquarePlus, | |
| 11 | Rows3, | |
| 12 | Search, | |
| 13 | } from "lucide-react"; | |
| 14 | import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from "react"; | |
| 15 | import { Form, Link } from "react-router"; | |
| API and MCP server in Rust; a public index at the API root | 16 | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 17 | import type { Comment, Comparison, DiffLine } from "@g1t/contracts"; |
| API and MCP server in Rust; a public index at the API root | 18 | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 19 | import type { |
| 20 | HighlightedComparison, | |
| 21 | HighlightedFile, | |
| 22 | HighlightedLine, | |
| 23 | } from "../lib/diff"; | |
| Acceptance checks in sandboxes, line comments and review verdicts | 24 | import { Markdown } from "./markdown"; |
| Merge branch 'worktree-agent-a985c01f43538af01' | 25 | import { Avatar, Button, EmptyState, SubmitButton, Textarea, TimeAgo } from "./ui"; |
| Workspace names and icons, and a component kit for every control | 26 | import { Checkbox } from "./ui/checkbox"; |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 27 | import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "./ui/sheet"; |
| API and MCP server in Rust; a public index at the API root | 28 | |
| 29 | const ROW_STYLES: Record<DiffLine["kind"], string> = { | |
| 30 | context: "", | |
| Merge branch 'worktree-agent-ad7c6d88d93adc817' | 31 | add: "bg-success/[0.09]", |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 32 | delete: "bg-danger/[0.09]", |
| 33 | }; | |
| 34 | ||
| 35 | const GUTTER_STYLES: Record<DiffLine["kind"], string> = { | |
| 36 | context: "text-faint", | |
| Merge branch 'worktree-agent-ad7c6d88d93adc817' | 37 | add: "bg-success/[0.14] text-success/70", |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 38 | delete: "bg-danger/[0.14] text-danger/70", |
| API and MCP server in Rust; a public index at the API root | 39 | }; |
| 40 | ||
| 41 | const MARKERS: Record<DiffLine["kind"], string> = { | |
| 42 | context: " ", | |
| 43 | add: "+", | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 44 | delete: "−", |
| API and MCP server in Rust; a public index at the API root | 45 | }; |
| 46 | ||
| Acceptance checks in sandboxes, line comments and review verdicts | 47 | /** Comments on lines of the change, and whether the viewer may add one. */ |
| 48 | export type LineReview = { comments: Comment[]; canComment: boolean }; | |
| 49 | ||
| Agents as a team: lifecycle, merge queue, billing and a new shell | 50 | type Layout = "unified" | "split"; |
| 51 | ||
| 52 | /** Reads a per-viewer preference; storage can be missing or refuse. */ | |
| 53 | function stored(key: string): string | null { | |
| 54 | try { | |
| 55 | return window.localStorage.getItem(key); | |
| 56 | } catch { | |
| 57 | return null; | |
| 58 | } | |
| 59 | } | |
| 60 | ||
| 61 | function store(key: string, value: string | null) { | |
| 62 | try { | |
| 63 | if (value == null) window.localStorage.removeItem(key); | |
| 64 | else window.localStorage.setItem(key, value); | |
| 65 | } catch { | |
| 66 | // A private window: the preference lasts until the page is left. | |
| 67 | } | |
| 68 | } | |
| 69 | ||
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 70 | /** Whether the screen is phone-sized: a change is shown unified there, never split. */ |
| 71 | function useNarrow(): boolean { | |
| 72 | const [narrow, setNarrow] = useState(false); | |
| 73 | useEffect(() => { | |
| 74 | const query = window.matchMedia("(max-width: 639px)"); | |
| 75 | const update = () => setNarrow(query.matches); | |
| 76 | update(); | |
| 77 | query.addEventListener("change", update); | |
| 78 | return () => query.removeEventListener("change", update); | |
| 79 | }, []); | |
| 80 | return narrow; | |
| 81 | } | |
| 82 | ||
| 83 | /** `+12 −3` with a five-block bar (the bar from `sm` up). */ | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 84 | export function Stat({ additions, deletions }: { additions: number; deletions: number }) { |
| API and MCP server in Rust; a public index at the API root | 85 | const total = additions + deletions; |
| 86 | const green = total === 0 ? 0 : Math.round((additions / total) * 5); | |
| 87 | return ( | |
| 88 | <span className="flex shrink-0 items-center gap-2 font-mono text-xs"> | |
| Merge branch 'worktree-agent-ad7c6d88d93adc817' | 89 | <span className="text-success">+{additions}</span> |
| API and MCP server in Rust; a public index at the API root | 90 | <span className="text-danger">−{deletions}</span> |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 91 | <span className="hidden gap-px sm:flex" aria-hidden="true"> |
| API and MCP server in Rust; a public index at the API root | 92 | {Array.from({ length: 5 }, (_, i) => ( |
| 93 | <span | |
| 94 | key={i} | |
| 95 | className={`size-2 rounded-xs ${ | |
| Merge branch 'worktree-agent-ad7c6d88d93adc817' | 96 | total === 0 ? "bg-line" : i < green ? "bg-success" : "bg-danger" |
| API and MCP server in Rust; a public index at the API root | 97 | }`} |
| 98 | /> | |
| 99 | ))} | |
| 100 | </span> | |
| 101 | </span> | |
| 102 | ); | |
| 103 | } | |
| 104 | ||
| Agents as a team: lifecycle, merge queue, billing and a new shell | 105 | function fileIcon(status: HighlightedFile["status"]) { |
| 106 | return status === "added" ? FilePlus : status === "deleted" ? FileMinus : FileIcon; | |
| 107 | } | |
| 108 | ||
| 109 | function fileTone(status: HighlightedFile["status"]) { | |
| Merge branch 'worktree-agent-ad7c6d88d93adc817' | 110 | return status === "added" ? "text-success" : status === "deleted" ? "text-danger" : "text-faint"; |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 111 | } |
| 112 | ||
| Acceptance checks in sandboxes, line comments and review verdicts | 113 | /** One comment made on a line, shown under that line. */ |
| 114 | function LineComment({ comment }: { comment: Comment }) { | |
| 115 | return ( | |
| 116 | <div className="rounded-lg border border-line bg-bg font-sans"> | |
| 117 | <p className="flex items-center gap-2 border-b border-line px-3 py-1.5 text-xs text-muted"> | |
| 118 | <Avatar name={comment.author.username} size={16} /> | |
| 119 | <span className="font-medium text-fg">{comment.author.username}</span> | |
| 120 | <TimeAgo at={comment.createdAt} /> | |
| 121 | </p> | |
| 122 | <div className="px-3 py-2 text-sm"> | |
| 123 | <Markdown source={comment.body} /> | |
| 124 | </div> | |
| 125 | </div> | |
| 126 | ); | |
| 127 | } | |
| 128 | ||
| Agents as a team: lifecycle, merge queue, billing and a new shell | 129 | /** A line's text, highlighted when the server could. */ |
| 130 | function Code({ line }: { line: HighlightedLine | null }) { | |
| 131 | if (!line) return null; | |
| 132 | if (line.html != null) { | |
| 133 | return <span dangerouslySetInnerHTML={{ __html: line.html || " " }} />; | |
| 134 | } | |
| 135 | return <>{line.text || " "}</>; | |
| 136 | } | |
| 137 | ||
| 138 | /** The comments on a line and, when open, the box to add one. */ | |
| 139 | function Thread({ | |
| 140 | path, | |
| 141 | line, | |
| 142 | comments, | |
| 143 | open, | |
| 144 | onClose, | |
| 145 | }: { | |
| 146 | path: string; | |
| 147 | line: number; | |
| 148 | comments: Comment[]; | |
| 149 | open: boolean; | |
| 150 | onClose: () => void; | |
| 151 | }) { | |
| Merge branch 'worktree-agent-a985c01f43538af01' | 152 | // The box stays, saying it is posting, until the new comment shows above it. |
| 153 | const count = useRef(comments.length); | |
| 154 | useEffect(() => { | |
| 155 | if (comments.length > count.current) onClose(); | |
| 156 | count.current = comments.length; | |
| 157 | // eslint-disable-next-line react-hooks/exhaustive-deps -- only a new comment closes it | |
| 158 | }, [comments.length]); | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 159 | return ( |
| 160 | <div className="max-w-2xl space-y-2"> | |
| 161 | {comments.map((comment) => ( | |
| 162 | <LineComment key={comment.id} comment={comment} /> | |
| 163 | ))} | |
| 164 | {open && ( | |
| Merge branch 'worktree-agent-a985c01f43538af01' | 165 | <Form method="post" className="space-y-2 font-sans"> |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 166 | <input type="hidden" name="action" value="comment" /> |
| 167 | <input type="hidden" name="path" value={path} /> | |
| 168 | <input type="hidden" name="line" value={line} /> | |
| 169 | <Textarea name="body" rows={3} required autoFocus placeholder={`Comment on line ${line}`} /> | |
| 170 | <div className="flex gap-2"> | |
| Merge branch 'worktree-agent-a985c01f43538af01' | 171 | <SubmitButton match={{ action: "comment", path, line: String(line) }} pending="Commenting…"> |
| 172 | Comment | |
| 173 | </SubmitButton> | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 174 | <Button variant="quiet" type="button" onClick={onClose}> |
| 175 | Cancel | |
| 176 | </Button> | |
| 177 | </div> | |
| 178 | </Form> | |
| 179 | )} | |
| 180 | </div> | |
| Acceptance checks in sandboxes, line comments and review verdicts | 181 | ); |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 182 | } |
| 183 | ||
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 184 | function CommentButton({ line, onClick }: { line: number; onClick: (event: React.MouseEvent) => void }) { |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 185 | return ( |
| 186 | <button | |
| 187 | type="button" | |
| 188 | aria-label={`Comment on line ${line}`} | |
| 189 | onClick={onClick} | |
| Phones reach every page: each mode's menu from the top bar or by tapping the tab you're on, Docs first in the avatar sheet, a project's pages as a swipeable strip; dialogs as bottom sheets above the keyboard, safe areas, menus kept on screen, 40px targets, touch-visible controls, save bars above the tab bar | 190 | className="absolute top-0 -right-2.5 z-10 hidden size-5 items-center justify-center rounded bg-accent text-bg group-hover:flex focus-visible:flex pointer-coarse:-right-1 pointer-coarse:flex pointer-coarse:size-7" |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 191 | > |
| 192 | <MessageSquarePlus size={12} /> | |
| 193 | </button> | |
| Acceptance checks in sandboxes, line comments and review verdicts | 194 | ); |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 195 | } |
| 196 | ||
| 197 | type FileProps = { | |
| 198 | file: HighlightedFile; | |
| 199 | comments: Comment[]; | |
| 200 | canComment: boolean; | |
| 201 | }; | |
| 202 | ||
| 203 | function UnifiedRows({ file, comments, canComment }: FileProps) { | |
| 204 | const [writing, setWriting] = useState<number | null>(null); | |
| API and MCP server in Rust; a public index at the API root | 205 | return ( |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 206 | <table className="w-full border-collapse font-mono text-xs leading-5"> |
| 207 | <tbody> | |
| 208 | {file.hunks.map((hunk, h) => ( | |
| 209 | <Fragment key={h}> | |
| 210 | {h > 0 && <HunkGap colSpan={4} />} | |
| 211 | {hunk.lines.map((line, index) => { | |
| 212 | const here = comments.filter((c) => line.new != null && c.line === line.new); | |
| 213 | const open = line.new != null && writing === line.new; | |
| 214 | return ( | |
| 215 | <Fragment key={index}> | |
| 216 | <tr className={`group ${ROW_STYLES[line.kind]}`}> | |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 217 | <td className={`w-9 px-1 text-right select-none sm:w-12 sm:px-2 ${GUTTER_STYLES[line.kind]}`}> |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 218 | {line.old} |
| 219 | </td> | |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 220 | <td |
| 221 | // A tap on the number comments on the line: a touch screen has no hover for the button. | |
| 222 | onClick={canComment && line.new != null ? () => setWriting(open ? null : line.new) : undefined} | |
| 223 | className={`relative w-9 px-1 text-right select-none sm:w-12 sm:px-2 ${canComment && line.new != null ? "cursor-pointer" : ""} ${GUTTER_STYLES[line.kind]}`} | |
| 224 | > | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 225 | {line.new} |
| 226 | {canComment && line.new != null && ( | |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 227 | <CommentButton |
| 228 | line={line.new} | |
| 229 | onClick={(event) => { | |
| 230 | event.stopPropagation(); | |
| 231 | setWriting(open ? null : line.new); | |
| 232 | }} | |
| 233 | /> | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 234 | )} |
| 235 | </td> | |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 236 | <td className={`w-4 text-center select-none sm:w-5 ${fileToneFor(line.kind)}`}> |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 237 | {MARKERS[line.kind]} |
| 238 | </td> | |
| 239 | <td className="pr-4 whitespace-pre"> | |
| 240 | <Code line={line} /> | |
| 241 | </td> | |
| 242 | </tr> | |
| 243 | {(here.length > 0 || open) && line.new != null && ( | |
| 244 | <tr> | |
| 245 | <td colSpan={4} className="border-y border-line bg-surface p-3"> | |
| 246 | <Thread | |
| 247 | path={file.path} | |
| 248 | line={line.new} | |
| 249 | comments={here} | |
| 250 | open={open} | |
| 251 | onClose={() => setWriting(null)} | |
| 252 | /> | |
| 253 | </td> | |
| 254 | </tr> | |
| 255 | )} | |
| 256 | </Fragment> | |
| 257 | ); | |
| 258 | })} | |
| 259 | </Fragment> | |
| 260 | ))} | |
| 261 | </tbody> | |
| 262 | </table> | |
| 263 | ); | |
| 264 | } | |
| 265 | ||
| 266 | function fileToneFor(kind: DiffLine["kind"]) { | |
| Merge branch 'worktree-agent-ad7c6d88d93adc817' | 267 | return kind === "add" ? "text-success" : kind === "delete" ? "text-danger" : "text-faint"; |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 268 | } |
| 269 | ||
| 270 | /** Lines side by side: a run of deletions beside the additions that replaced it. */ | |
| 271 | function pairs(lines: HighlightedLine[]): [HighlightedLine | null, HighlightedLine | null][] { | |
| 272 | const rows: [HighlightedLine | null, HighlightedLine | null][] = []; | |
| 273 | let index = 0; | |
| 274 | while (index < lines.length) { | |
| 275 | const line = lines[index]!; | |
| 276 | if (line.kind === "context") { | |
| 277 | rows.push([line, line]); | |
| 278 | index++; | |
| 279 | continue; | |
| 280 | } | |
| 281 | const removed: HighlightedLine[] = []; | |
| 282 | const added: HighlightedLine[] = []; | |
| 283 | while (index < lines.length && lines[index]!.kind === "delete") removed.push(lines[index++]!); | |
| 284 | while (index < lines.length && lines[index]!.kind === "add") added.push(lines[index++]!); | |
| 285 | for (let n = 0; n < Math.max(removed.length, added.length); n++) { | |
| 286 | rows.push([removed[n] ?? null, added[n] ?? null]); | |
| 287 | } | |
| 288 | } | |
| 289 | return rows; | |
| 290 | } | |
| 291 | ||
| 292 | function SplitRows({ file, comments, canComment }: FileProps) { | |
| 293 | const [writing, setWriting] = useState<number | null>(null); | |
| 294 | const side = (line: HighlightedLine | null, which: "old" | "new") => { | |
| 295 | const kind = line ? (line.kind === "context" ? "context" : line.kind) : null; | |
| 296 | const empty = "bg-surface/60"; | |
| 297 | return ( | |
| 298 | <> | |
| 299 | <td | |
| 300 | className={`relative w-12 px-2 text-right align-top select-none ${kind ? GUTTER_STYLES[kind] : empty} ${ | |
| 301 | which === "new" ? "border-l border-line" : "" | |
| 302 | }`} | |
| 303 | > | |
| 304 | {line?.[which]} | |
| 305 | {which === "new" && canComment && line?.new != null && ( | |
| 306 | <CommentButton line={line.new} onClick={() => setWriting(writing === line.new ? null : line.new)} /> | |
| 307 | )} | |
| 308 | </td> | |
| 309 | <td className={`pr-3 pl-2 align-top break-all whitespace-pre-wrap ${kind ? ROW_STYLES[kind] : empty}`}> | |
| 310 | <Code line={line} /> | |
| 311 | </td> | |
| 312 | </> | |
| 313 | ); | |
| 314 | }; | |
| 315 | return ( | |
| 316 | <table className="w-full table-fixed border-collapse font-mono text-xs leading-5"> | |
| 317 | <colgroup> | |
| 318 | <col className="w-12" /> | |
| 319 | <col /> | |
| 320 | <col className="w-12" /> | |
| 321 | <col /> | |
| 322 | </colgroup> | |
| 323 | <tbody> | |
| 324 | {file.hunks.map((hunk, h) => ( | |
| 325 | <Fragment key={h}> | |
| 326 | {h > 0 && <HunkGap colSpan={4} />} | |
| 327 | {pairs(hunk.lines).map(([before, after], index) => { | |
| 328 | const line = after?.new ?? null; | |
| 329 | const here = comments.filter((c) => line != null && c.line === line); | |
| 330 | const open = line != null && writing === line; | |
| 331 | return ( | |
| 332 | <Fragment key={index}> | |
| 333 | <tr className="group"> | |
| 334 | {side(before && before.kind !== "add" ? before : null, "old")} | |
| 335 | {side(after && after.kind !== "delete" ? after : null, "new")} | |
| 336 | </tr> | |
| 337 | {(here.length > 0 || open) && line != null && ( | |
| 338 | <tr> | |
| 339 | <td colSpan={4} className="border-y border-line bg-surface p-3"> | |
| 340 | <Thread | |
| 341 | path={file.path} | |
| 342 | line={line} | |
| 343 | comments={here} | |
| 344 | open={open} | |
| 345 | onClose={() => setWriting(null)} | |
| 346 | /> | |
| 347 | </td> | |
| 348 | </tr> | |
| 349 | )} | |
| 350 | </Fragment> | |
| 351 | ); | |
| 352 | })} | |
| 353 | </Fragment> | |
| 354 | ))} | |
| 355 | </tbody> | |
| 356 | </table> | |
| 357 | ); | |
| 358 | } | |
| 359 | ||
| 360 | function HunkGap({ colSpan }: { colSpan: number }) { | |
| 361 | return ( | |
| 362 | <tr aria-hidden="true"> | |
| 363 | <td colSpan={colSpan} className="border-y border-line bg-surface py-0.5 text-center text-faint"> | |
| 364 | ⋯ | |
| 365 | </td> | |
| 366 | </tr> | |
| 367 | ); | |
| 368 | } | |
| 369 | ||
| 370 | function FileSection({ | |
| 371 | file, | |
| 372 | review, | |
| 373 | layout, | |
| 374 | collapsed, | |
| 375 | viewed, | |
| 376 | onToggle, | |
| 377 | onViewed, | |
| 378 | fileBase, | |
| 379 | }: { | |
| 380 | file: HighlightedFile; | |
| 381 | review?: LineReview; | |
| 382 | layout: Layout; | |
| 383 | collapsed: boolean; | |
| 384 | viewed: boolean; | |
| 385 | onToggle: () => void; | |
| 386 | onViewed: (viewed: boolean) => void; | |
| 387 | /** Where the file can be read as of the change, e.g. `/acme/web/blob/<hash>`. */ | |
| 388 | fileBase?: string; | |
| 389 | }) { | |
| 390 | const Icon = fileIcon(file.status); | |
| 391 | const comments = review?.comments.filter((comment) => comment.path === file.path) ?? []; | |
| 392 | const shown = new Set(file.hunks.flatMap((hunk) => hunk.lines.map((line) => line.new))); | |
| 393 | // Made against an earlier version of the change, on a line no longer in it. | |
| 394 | const outdated = comments.filter((comment) => comment.line == null || !shown.has(comment.line)); | |
| 395 | // Shown plain at once, and highlighted in the browser when it comes near | |
| 396 | // the screen: the page never waits for colours. | |
| 397 | const section = useRef<HTMLElement>(null); | |
| 398 | const [highlighted, setHighlighted] = useState<HighlightedFile | null>(null); | |
| 399 | useEffect(() => { | |
| 400 | setHighlighted(null); | |
| 401 | const element = section.current; | |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 402 | // Highlighted by the server already: one of the change's first files. |
| 403 | const done = file.hunks.some((hunk) => hunk.lines.some((line) => (line as HighlightedLine).html != null)); | |
| 404 | if (!element || collapsed || done) return; | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 405 | let cancelled = false; |
| 406 | const observer = new IntersectionObserver( | |
| 407 | ([entry]) => { | |
| 408 | if (!entry?.isIntersecting) return; | |
| 409 | observer.disconnect(); | |
| 410 | void import("../lib/shiki") | |
| 411 | .then(({ highlightFile }) => highlightFile(file)) | |
| 412 | .then((result) => { | |
| 413 | if (!cancelled && result) setHighlighted(result); | |
| 414 | }) | |
| 415 | .catch(() => {}); | |
| 416 | }, | |
| 417 | { rootMargin: "600px 0px" }, | |
| 418 | ); | |
| 419 | observer.observe(element); | |
| 420 | return () => { | |
| 421 | cancelled = true; | |
| 422 | observer.disconnect(); | |
| 423 | }; | |
| 424 | }, [file, collapsed]); | |
| 425 | const props = { file: highlighted ?? file, comments, canComment: review?.canComment ?? false }; | |
| 426 | return ( | |
| API and MCP server in Rust; a public index at the API root | 427 | <section |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 428 | ref={section} |
| API and MCP server in Rust; a public index at the API root | 429 | id={`file-${file.path}`} |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 430 | data-diff-file={file.path} |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 431 | // Lands below the site bar and the change's toolbar, whatever their height. |
| 432 | className="scroll-mt-[calc(4rem+var(--diff-toolbar,3rem))] rounded-xl border border-line" | |
| API and MCP server in Rust; a public index at the API root | 433 | > |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 434 | <header |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 435 | // Sticks under the change's toolbar (its height is --diff-toolbar), not behind it. |
| 436 | className={`sticky top-[calc(3.5rem+var(--diff-toolbar,0px))] z-20 flex items-center gap-2 border-line bg-surface/95 px-2 py-2 backdrop-blur sm:gap-2.5 sm:px-3 ${ | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 437 | collapsed ? "rounded-xl" : "rounded-t-xl border-b" |
| 438 | }`} | |
| 439 | > | |
| 440 | <button | |
| 441 | type="button" | |
| 442 | onClick={onToggle} | |
| 443 | aria-label={collapsed ? `Show ${file.path}` : `Hide ${file.path}`} | |
| 444 | aria-expanded={!collapsed} | |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 445 | className="-m-1.5 shrink-0 rounded p-2 text-faint hover:bg-raised hover:text-fg sm:m-0 sm:p-0.5" |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 446 | > |
| 447 | {collapsed ? <ChevronRight size={15} /> : <ChevronDown size={15} />} | |
| 448 | </button> | |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 449 | <Icon size={15} className={`hidden shrink-0 sm:block ${fileTone(file.status)}`} /> |
| 450 | {/* A long path is cut at its start, so the file's own name shows. */} | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 451 | <button |
| 452 | type="button" | |
| 453 | onClick={onToggle} | |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 454 | dir="rtl" |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 455 | className={`min-w-0 grow truncate text-left font-mono text-[0.8125rem] ${viewed ? "text-muted" : ""}`} |
| 456 | > | |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 457 | <bdi>{file.path}</bdi> |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 458 | </button> |
| 459 | {comments.length > 0 && ( | |
| 460 | <span className="flex items-center gap-1 text-xs text-muted"> | |
| 461 | <MessageSquarePlus size={12} /> | |
| 462 | {comments.length} | |
| 463 | </span> | |
| 464 | )} | |
| API and MCP server in Rust; a public index at the API root | 465 | <Stat additions={file.additions} deletions={file.deletions} /> |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 466 | {fileBase && file.status !== "deleted" && ( |
| 467 | <span className="hidden items-center gap-0.5 text-xs sm:flex"> | |
| 468 | <Link to={`${fileBase}/${file.path}`} className="rounded px-1.5 py-0.5 text-muted hover:bg-raised hover:text-fg"> | |
| 469 | View | |
| 470 | </Link> | |
| 471 | <Link | |
| 472 | to={`${fileBase}/${file.path}?blame=1`} | |
| 473 | className="rounded px-1.5 py-0.5 text-muted hover:bg-raised hover:text-fg" | |
| 474 | > | |
| 475 | Blame | |
| 476 | </Link> | |
| 477 | </span> | |
| 478 | )} | |
| 479 | <label | |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 480 | className={`ml-1 flex shrink-0 cursor-pointer items-center gap-1.5 rounded-md border px-2 py-1.5 text-xs transition-colors select-none sm:py-0.5 ${ |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 481 | viewed ? "border-accent/40 bg-accent/10 text-accent" : "border-line text-muted hover:text-fg" |
| 482 | }`} | |
| 483 | > | |
| Workspace names and icons, and a component kit for every control | 484 | <Checkbox |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 485 | checked={viewed} |
| Workspace names and icons, and a component kit for every control | 486 | onCheckedChange={(checked) => onViewed(checked === true)} |
| 487 | className="size-3.5 rounded-[4px] [&_svg]:size-2.5" | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 488 | /> |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 489 | <span className="sr-only sm:not-sr-only">Viewed</span> |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 490 | </label> |
| API and MCP server in Rust; a public index at the API root | 491 | </header> |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 492 | {!collapsed && ( |
| 493 | <div className="overflow-hidden rounded-b-xl"> | |
| 494 | {outdated.length > 0 && ( | |
| 495 | <div className="space-y-2 border-b border-line bg-surface/50 p-3"> | |
| 496 | <p className="text-xs text-faint">On lines that have since changed</p> | |
| 497 | {outdated.map((comment) => ( | |
| 498 | <LineComment key={comment.id} comment={comment} /> | |
| API and MCP server in Rust; a public index at the API root | 499 | ))} |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 500 | </div> |
| 501 | )} | |
| 502 | {file.binary ? ( | |
| 503 | <p className="px-4 py-6 text-sm text-muted">Binary or large file; its contents are not shown.</p> | |
| 504 | ) : file.hunks.length === 0 ? ( | |
| 505 | <p className="px-4 py-6 text-sm text-muted">No line changes{file.status === "added" ? ": an empty file" : ""}.</p> | |
| 506 | ) : ( | |
| 507 | <div className="overflow-x-auto"> | |
| 508 | {layout === "split" ? <SplitRows {...props} /> : <UnifiedRows {...props} />} | |
| 509 | </div> | |
| 510 | )} | |
| API and MCP server in Rust; a public index at the API root | 511 | </div> |
| 512 | )} | |
| 513 | </section> | |
| 514 | ); | |
| 515 | } | |
| 516 | ||
| Agents as a team: lifecycle, merge queue, billing and a new shell | 517 | type TreeNode = { name: string; path: string; children: TreeNode[]; file?: HighlightedFile }; |
| 518 | ||
| 519 | /** The changed files as folders, with folders of one folder merged: `src/lib`. */ | |
| 520 | function treeOf(files: HighlightedFile[]): TreeNode[] { | |
| 521 | const root: TreeNode = { name: "", path: "", children: [] }; | |
| 522 | for (const file of files) { | |
| 523 | const parts = file.path.split("/"); | |
| 524 | let node = root; | |
| 525 | parts.forEach((part, index) => { | |
| 526 | const path = parts.slice(0, index + 1).join("/"); | |
| 527 | let child = node.children.find((c) => c.name === part && !c.file === (index < parts.length - 1)); | |
| 528 | if (!child) { | |
| 529 | child = { name: part, path, children: [] }; | |
| 530 | if (index === parts.length - 1) child.file = file; | |
| 531 | node.children.push(child); | |
| 532 | } | |
| 533 | node = child; | |
| 534 | }); | |
| 535 | } | |
| 536 | const squash = (node: TreeNode): TreeNode => { | |
| 537 | let current = node; | |
| 538 | while (!current.file && current.children.length === 1 && !current.children[0]!.file) { | |
| 539 | const only = current.children[0]!; | |
| 540 | current = { ...only, name: `${current.name}/${only.name}` }; | |
| 541 | } | |
| 542 | const children = current.children | |
| 543 | .map(squash) | |
| 544 | .sort((a, b) => Number(!!a.file) - Number(!!b.file) || a.name.localeCompare(b.name)); | |
| 545 | return { ...current, children }; | |
| 546 | }; | |
| 547 | return squash(root).children; | |
| 548 | } | |
| 549 | ||
| 550 | function Tree({ | |
| 551 | nodes, | |
| 552 | depth, | |
| 553 | active, | |
| 554 | viewed, | |
| 555 | onPick, | |
| Acceptance checks in sandboxes, line comments and review verdicts | 556 | }: { |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 557 | nodes: TreeNode[]; |
| 558 | depth: number; | |
| 559 | active: string | null; | |
| 560 | viewed: Set<string>; | |
| 561 | onPick: (path: string) => void; | |
| Acceptance checks in sandboxes, line comments and review verdicts | 562 | }) { |
| API and MCP server in Rust; a public index at the API root | 563 | return ( |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 564 | <ul> |
| 565 | {nodes.map((node) => { | |
| 566 | if (!node.file) { | |
| 567 | return ( | |
| 568 | <li key={node.path}> | |
| 569 | <p | |
| 570 | className="flex items-center gap-1.5 truncate py-1 font-mono text-xs text-faint" | |
| 571 | style={{ paddingLeft: depth * 12 + 8 }} | |
| 572 | > | |
| 573 | <Folder size={13} className="shrink-0" /> | |
| 574 | {node.name} | |
| 575 | </p> | |
| 576 | <Tree nodes={node.children} depth={depth + 1} active={active} viewed={viewed} onPick={onPick} /> | |
| 577 | </li> | |
| 578 | ); | |
| 579 | } | |
| 580 | const file = node.file; | |
| 581 | const Icon = fileIcon(file.status); | |
| Acceptance checks in sandboxes, line comments and review verdicts | 582 | return ( |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 583 | <li key={node.path}> |
| 584 | <button | |
| 585 | type="button" | |
| 586 | onClick={() => onPick(file.path)} | |
| 587 | style={{ paddingLeft: depth * 12 + 8 }} | |
| 588 | className={`flex w-full items-center gap-1.5 rounded-md py-1 pr-2 text-left font-mono text-xs transition-colors ${ | |
| 589 | active === file.path ? "bg-raised text-fg" : "text-muted hover:bg-raised/60 hover:text-fg" | |
| 590 | } ${viewed.has(file.path) ? "opacity-55" : ""}`} | |
| 591 | > | |
| 592 | <Icon size={13} className={`shrink-0 ${fileTone(file.status)}`} /> | |
| 593 | <span className="min-w-0 grow truncate">{node.name}</span> | |
| 594 | <span className="shrink-0 text-[0.625rem] tabular-nums"> | |
| Merge branch 'worktree-agent-ad7c6d88d93adc817' | 595 | {file.additions > 0 && <span className="text-success">+{file.additions}</span>} |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 596 | {file.deletions > 0 && <span className="ml-1 text-danger">−{file.deletions}</span>} |
| 597 | </span> | |
| 598 | </button> | |
| 599 | </li> | |
| Acceptance checks in sandboxes, line comments and review verdicts | 600 | ); |
| 601 | })} | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 602 | </ul> |
| API and MCP server in Rust; a public index at the API root | 603 | ); |
| 604 | } | |
| 605 | ||
| Agents as a team: lifecycle, merge queue, billing and a new shell | 606 | /** |
| 607 | * A change, file by file: a tree of what changed beside the diffs, unified | |
| 608 | * or side by side, each file foldable and markable as viewed. `j` and `k` | |
| 609 | * move between files and `t` filters them. | |
| 610 | */ | |
| Acceptance checks in sandboxes, line comments and review verdicts | 611 | export function DiffView({ |
| 612 | comparison, | |
| 613 | review, | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 614 | empty, |
| 615 | fileBase, | |
| Acceptance checks in sandboxes, line comments and review verdicts | 616 | }: { |
| 617 | comparison: Comparison; | |
| 618 | review?: LineReview; | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 619 | /** What to say when nothing changed. */ |
| 620 | empty?: React.ReactNode; | |
| 621 | /** Where each file can be read as of the change, to link to it. */ | |
| 622 | fileBase?: string; | |
| Acceptance checks in sandboxes, line comments and review verdicts | 623 | }) { |
| API and MCP server in Rust; a public index at the API root | 624 | const { files, truncated } = comparison; |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 625 | const [chosenLayout, setLayout] = useState<Layout>("unified"); |
| 626 | // Side by side has no room on a phone. | |
| 627 | const narrow = useNarrow(); | |
| 628 | const layout: Layout = narrow ? "unified" : chosenLayout; | |
| 629 | const [picking, setPicking] = useState(false); | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 630 | const [collapsed, setCollapsed] = useState<Set<string>>(new Set()); |
| 631 | const [viewed, setViewed] = useState<Set<string>>(new Set()); | |
| 632 | const [active, setActive] = useState<string | null>(files[0]?.path ?? null); | |
| 633 | const [filter, setFilter] = useState(""); | |
| 634 | const filterInput = useRef<HTMLInputElement>(null); | |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 635 | // The toolbar's height, as it wraps at each width, so the files' own |
| 636 | // sticky headers sit below it rather than under it. | |
| 637 | const frame = useRef<HTMLDivElement>(null); | |
| 638 | const toolbar = useRef<HTMLDivElement>(null); | |
| 639 | const noFiles = files.length === 0; | |
| 640 | useEffect(() => { | |
| 641 | const bar = toolbar.current; | |
| 642 | const root = frame.current; | |
| 643 | if (!bar || !root) return; | |
| 644 | const measure = () => root.style.setProperty("--diff-toolbar", `${bar.offsetHeight}px`); | |
| 645 | measure(); | |
| 646 | if (typeof ResizeObserver === "undefined") return; | |
| 647 | const observer = new ResizeObserver(measure); | |
| 648 | observer.observe(bar); | |
| 649 | return () => observer.disconnect(); | |
| 650 | }, [noFiles]); | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 651 | const viewedKey = `g1t:viewed:${comparison.head}`; |
| 652 | ||
| 653 | // Preferences live in the browser: read them once it is there. | |
| 654 | useEffect(() => { | |
| 655 | if (stored("g1t:diff-layout") === "split") setLayout("split"); | |
| 656 | const saved = stored(viewedKey); | |
| 657 | if (saved) { | |
| 658 | try { | |
| 659 | const paths = new Set<string>(JSON.parse(saved)); | |
| 660 | setViewed(paths); | |
| 661 | setCollapsed(new Set(paths)); | |
| 662 | } catch { | |
| 663 | // Something else wrote it. | |
| 664 | } | |
| 665 | } | |
| 666 | }, [viewedKey]); | |
| 667 | ||
| 668 | const changeLayout = (next: Layout) => { | |
| 669 | setLayout(next); | |
| 670 | store("g1t:diff-layout", next); | |
| 671 | }; | |
| 672 | const markViewed = (path: string, on: boolean) => { | |
| 673 | setViewed((previous) => { | |
| 674 | const next = new Set(previous); | |
| 675 | if (on) next.add(path); | |
| 676 | else next.delete(path); | |
| 677 | store(viewedKey, next.size ? JSON.stringify([...next]) : null); | |
| 678 | return next; | |
| 679 | }); | |
| 680 | // Marking a file viewed folds it away, as a reviewer moves on. | |
| 681 | setCollapsed((previous) => { | |
| 682 | const next = new Set(previous); | |
| 683 | if (on) next.add(path); | |
| 684 | else next.delete(path); | |
| 685 | return next; | |
| 686 | }); | |
| 687 | }; | |
| 688 | const toggle = (path: string) => | |
| 689 | setCollapsed((previous) => { | |
| 690 | const next = new Set(previous); | |
| 691 | if (next.has(path)) next.delete(path); | |
| 692 | else next.add(path); | |
| 693 | return next; | |
| 694 | }); | |
| 695 | ||
| 696 | const shownFiles = useMemo(() => { | |
| 697 | const words = filter.toLowerCase().split(/\s+/).filter(Boolean); | |
| 698 | return words.length === 0 | |
| 699 | ? files | |
| 700 | : files.filter((file) => words.every((word) => file.path.toLowerCase().includes(word))); | |
| 701 | }, [files, filter]); | |
| 702 | const tree = useMemo(() => treeOf(shownFiles), [shownFiles]); | |
| 703 | ||
| 704 | const jump = useCallback((path: string) => { | |
| 705 | setCollapsed((previous) => { | |
| 706 | if (!previous.has(path)) return previous; | |
| 707 | const next = new Set(previous); | |
| 708 | next.delete(path); | |
| 709 | return next; | |
| 710 | }); | |
| 711 | setActive(path); | |
| 712 | requestAnimationFrame(() => | |
| 713 | document.getElementById(`file-${path}`)?.scrollIntoView({ behavior: "smooth", block: "start" }), | |
| 714 | ); | |
| 715 | }, []); | |
| 716 | ||
| 717 | // The file at the top of the screen is the current one. | |
| 718 | useEffect(() => { | |
| 719 | const sections = [...document.querySelectorAll<HTMLElement>("[data-diff-file]")]; | |
| 720 | if (sections.length === 0) return; | |
| 721 | const observer = new IntersectionObserver( | |
| 722 | (entries) => { | |
| 723 | const top = entries | |
| 724 | .filter((entry) => entry.isIntersecting) | |
| 725 | .sort((a, b) => a.boundingClientRect.top - b.boundingClientRect.top)[0]; | |
| 726 | if (top) setActive((top.target as HTMLElement).dataset.diffFile ?? null); | |
| 727 | }, | |
| 728 | { rootMargin: "-112px 0px -60% 0px" }, | |
| 729 | ); | |
| 730 | sections.forEach((section) => observer.observe(section)); | |
| 731 | return () => observer.disconnect(); | |
| 732 | }, [shownFiles, collapsed]); | |
| 733 | ||
| 734 | useEffect(() => { | |
| 735 | const onKey = (event: KeyboardEvent) => { | |
| 736 | const target = event.target as HTMLElement | null; | |
| 737 | if (event.metaKey || event.ctrlKey || event.altKey) return; | |
| 738 | if (target && (target.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(target.tagName))) return; | |
| 739 | const index = shownFiles.findIndex((file) => file.path === active); | |
| 740 | if (event.key === "j" || event.key === "k") { | |
| 741 | const next = shownFiles[Math.max(0, Math.min(shownFiles.length - 1, index + (event.key === "j" ? 1 : -1)))]; | |
| 742 | if (next) { | |
| 743 | event.preventDefault(); | |
| 744 | jump(next.path); | |
| 745 | } | |
| 746 | } else if (event.key === "t") { | |
| 747 | event.preventDefault(); | |
| 748 | filterInput.current?.focus(); | |
| 749 | } else if (event.key === "v" && active) { | |
| 750 | event.preventDefault(); | |
| 751 | markViewed(active, !viewed.has(active)); | |
| 752 | } | |
| 753 | }; | |
| 754 | window.addEventListener("keydown", onKey); | |
| 755 | return () => window.removeEventListener("keydown", onKey); | |
| 756 | }); | |
| 757 | ||
| API and MCP server in Rust; a public index at the API root | 758 | if (files.length === 0) { |
| 759 | return ( | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 760 | <EmptyState title="No changes"> |
| 761 | {empty ?? "Nothing has been pushed to this pull request yet, or it matches the branch it would merge into."} | |
| API and MCP server in Rust; a public index at the API root | 762 | </EmptyState> |
| 763 | ); | |
| 764 | } | |
| 765 | const additions = files.reduce((sum, file) => sum + file.additions, 0); | |
| 766 | const deletions = files.reduce((sum, file) => sum + file.deletions, 0); | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 767 | const allCollapsed = files.every((file) => collapsed.has(file.path)); |
| API and MCP server in Rust; a public index at the API root | 768 | return ( |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 769 | <div ref={frame}> |
| Phones reach every page: each mode's menu from the top bar or by tapping the tab you're on, Docs first in the avatar sheet, a project's pages as a swipeable strip; dialogs as bottom sheets above the keyboard, safe areas, menus kept on screen, 40px targets, touch-visible controls, save bars above the tab bar | 770 | <div ref={toolbar} className="sticky top-(--topbar-h) z-30 -mx-1 mb-3 flex flex-wrap items-center gap-x-4 gap-y-2 bg-bg/90 px-1 py-2 backdrop-blur"> |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 771 | <span className="text-sm text-muted"> |
| 772 | <span className="font-medium text-fg">{files.length}</span> {files.length === 1 ? "file" : "files"} | |
| API and MCP server in Rust; a public index at the API root | 773 | </span> |
| 774 | <Stat additions={additions} deletions={deletions} /> | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 775 | <span className="flex items-center gap-2 text-xs text-muted"> |
| 776 | <span className="h-1.5 w-20 overflow-hidden rounded-full bg-line"> | |
| 777 | <span | |
| 778 | className="block h-full rounded-full bg-accent transition-all" | |
| 779 | style={{ width: `${(viewed.size / files.length) * 100}%` }} | |
| 780 | /> | |
| Acceptance checks in sandboxes, line comments and review verdicts | 781 | </span> |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 782 | {viewed.size}/{files.length} viewed |
| 783 | </span> | |
| 784 | <div className="ml-auto flex items-center gap-2"> | |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 785 | {files.length > 1 && ( |
| 786 | // Below xl the tree is not beside the diffs: it opens as a sheet. | |
| 787 | <button | |
| 788 | type="button" | |
| 789 | onClick={() => setPicking(true)} | |
| 790 | className="flex min-h-9 items-center gap-1.5 rounded-md border border-line px-2.5 text-xs text-muted transition-colors hover:border-line-strong hover:text-fg xl:hidden" | |
| 791 | > | |
| 792 | <Files size={13} /> | |
| 793 | Files | |
| 794 | </button> | |
| 795 | )} | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 796 | <button |
| 797 | type="button" | |
| 798 | onClick={() => setCollapsed(allCollapsed ? new Set() : new Set(files.map((file) => file.path)))} | |
| 799 | className="rounded-md px-2 py-1 text-xs text-muted hover:bg-raised hover:text-fg" | |
| 800 | > | |
| 801 | {allCollapsed ? "Expand all" : "Collapse all"} | |
| 802 | </button> | |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 803 | <div className="hidden rounded-md border border-line p-0.5 sm:flex" role="radiogroup" aria-label="Layout"> |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 804 | {( |
| 805 | [ | |
| 806 | ["unified", <Rows3 key="u" size={13} />, "Unified"], | |
| 807 | ["split", <Columns2 key="s" size={13} />, "Split"], | |
| 808 | ] as const | |
| 809 | ).map(([value, icon, label]) => ( | |
| 810 | <button | |
| 811 | key={value} | |
| 812 | type="button" | |
| 813 | role="radio" | |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 814 | aria-checked={chosenLayout === value} |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 815 | onClick={() => changeLayout(value)} |
| 816 | className={`flex items-center gap-1.5 rounded px-2 py-0.5 text-xs transition-colors ${ | |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 817 | chosenLayout === value ? "bg-raised text-fg" : "text-muted hover:text-fg" |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 818 | }`} |
| 819 | > | |
| 820 | {icon} | |
| 821 | {label} | |
| 822 | </button> | |
| 823 | ))} | |
| 824 | </div> | |
| 825 | </div> | |
| API and MCP server in Rust; a public index at the API root | 826 | </div> |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 827 | {files.length > 1 && ( |
| 828 | <Sheet open={picking} onOpenChange={setPicking}> | |
| 829 | <SheetContent | |
| 830 | side="left" | |
| 831 | // No keyboard popping up over the list on a phone: the filter is a tap away. | |
| 832 | onOpenAutoFocus={(event) => event.preventDefault()} | |
| Phones reach every page: each mode's menu from the top bar or by tapping the tab you're on, Docs first in the avatar sheet, a project's pages as a swipeable strip; dialogs as bottom sheets above the keyboard, safe areas, menus kept on screen, 40px targets, touch-visible controls, save bars above the tab bar | 833 | className="inset-x-0 top-auto bottom-0 h-[80dvh] max-w-none rounded-t-2xl pt-0 border-t border-r-0 data-[state=closed]:animate-fade-out data-[state=open]:animate-fade-in sm:max-w-none xl:hidden" |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 834 | > |
| 835 | <SheetHeader> | |
| 836 | <SheetTitle>Files changed</SheetTitle> | |
| 837 | <SheetDescription> | |
| 838 | {files.length} files · {viewed.size} viewed | |
| 839 | </SheetDescription> | |
| 840 | </SheetHeader> | |
| 841 | <div className="relative px-4 pt-3"> | |
| 842 | <Search size={14} className="pointer-events-none absolute top-1/2 left-6.5 mt-1.5 -translate-y-1/2 text-faint" /> | |
| 843 | <input | |
| 844 | value={filter} | |
| 845 | onChange={(event) => setFilter(event.target.value)} | |
| 846 | placeholder="Filter files" | |
| 847 | aria-label="Filter files" | |
| 848 | autoComplete="off" | |
| 849 | data-1p-ignore | |
| 850 | className="h-10 w-full rounded-md border border-line bg-bg pr-3 pl-8 text-sm outline-none placeholder:text-faint focus:border-accent-dim" | |
| 851 | /> | |
| 852 | </div> | |
| 853 | <nav aria-label="Files changed" className="min-h-0 grow overflow-y-auto px-2 py-2 [&_button]:min-h-10 [&_button]:text-[0.8125rem] [&_p]:min-h-8 [&_p]:text-[0.8125rem]"> | |
| 854 | <Tree | |
| 855 | nodes={tree} | |
| 856 | depth={0} | |
| 857 | active={active} | |
| 858 | viewed={viewed} | |
| 859 | onPick={(path) => { | |
| 860 | setPicking(false); | |
| 861 | // Once the sheet has let the page go, so its scroll is not undone. | |
| 862 | setTimeout(() => jump(path), 50); | |
| 863 | }} | |
| 864 | /> | |
| 865 | {shownFiles.length === 0 && <p className="px-2 py-6 text-center text-sm text-muted">No file matches.</p>} | |
| 866 | </nav> | |
| 867 | </SheetContent> | |
| 868 | </Sheet> | |
| 869 | )} | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 870 | <div className="flex gap-4"> |
| 871 | {files.length > 1 && ( | |
| 872 | <aside className="sticky top-28 hidden max-h-[calc(100vh-8rem)] w-60 shrink-0 flex-col self-start xl:flex"> | |
| 873 | <div className="relative"> | |
| 874 | <Search size={13} className="pointer-events-none absolute top-1/2 left-2.5 -translate-y-1/2 text-faint" /> | |
| 875 | <input | |
| 876 | ref={filterInput} | |
| 877 | value={filter} | |
| 878 | onChange={(event) => setFilter(event.target.value)} | |
| 879 | onKeyDown={(event) => { | |
| 880 | if (event.key === "Escape") { | |
| 881 | setFilter(""); | |
| 882 | event.currentTarget.blur(); | |
| 883 | } else if (event.key === "Enter" && shownFiles[0]) { | |
| 884 | jump(shownFiles[0].path); | |
| 885 | event.currentTarget.blur(); | |
| 886 | } | |
| 887 | }} | |
| 888 | placeholder="Filter files" | |
| 889 | autoComplete="off" | |
| 890 | data-1p-ignore | |
| 891 | className="h-8 w-full rounded-md border border-line bg-surface/60 pr-7 pl-7 text-xs outline-none placeholder:text-faint focus:border-accent-dim" | |
| 892 | /> | |
| 893 | <kbd className="absolute top-1/2 right-2 -translate-y-1/2 rounded border border-line px-1 font-mono text-[0.625rem] text-faint"> | |
| 894 | t | |
| 895 | </kbd> | |
| 896 | </div> | |
| 897 | <nav className="mt-2 min-h-0 overflow-y-auto pb-2"> | |
| 898 | <Tree nodes={tree} depth={0} active={active} viewed={viewed} onPick={jump} /> | |
| 899 | </nav> | |
| 900 | <p className="mt-auto border-t border-line pt-2 text-[0.6875rem] text-faint"> | |
| 901 | <kbd className="font-mono">j</kbd>/<kbd className="font-mono">k</kbd> next and previous file,{" "} | |
| 902 | <kbd className="font-mono">v</kbd> mark viewed | |
| 903 | </p> | |
| 904 | </aside> | |
| 905 | )} | |
| 906 | <div className="min-w-0 grow space-y-3"> | |
| 907 | {review?.canComment && ( | |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 908 | <p className="text-xs text-faint"> |
| 909 | <span className="pointer-coarse:hidden">Hover a line and press the button beside its number to comment on it.</span> | |
| 910 | <span className="hidden pointer-coarse:inline">Tap a line number to comment on it.</span> | |
| 911 | </p> | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 912 | )} |
| 913 | {shownFiles.map((file) => ( | |
| 914 | <FileSection | |
| 915 | key={file.path} | |
| 916 | file={file} | |
| 917 | review={review} | |
| 918 | layout={layout} | |
| 919 | collapsed={collapsed.has(file.path)} | |
| 920 | viewed={viewed.has(file.path)} | |
| 921 | onToggle={() => toggle(file.path)} | |
| 922 | onViewed={(on) => markViewed(file.path, on)} | |
| 923 | fileBase={fileBase} | |
| 924 | /> | |
| API and MCP server in Rust; a public index at the API root | 925 | ))} |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 926 | {shownFiles.length === 0 && <p className="py-8 text-center text-sm text-muted">No file matches.</p>} |
| 927 | {truncated && <p className="text-center text-sm text-muted">This change is too large to show in full.</p>} | |
| 928 | </div> | |
| 929 | </div> | |
| API and MCP server in Rust; a public index at the API root | 930 | </div> |
| 931 | ); | |
| 932 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.