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 and memory, checks and conflicts, profiles, slug renames, custom domains | 1 | /** |
| 2 | * The parts of a pull request's merge box that say what stands between it | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 3 | * and a merge: the checks the branch requires and every other check, job by |
| 4 | * job, and whether | |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 5 | * the change merges cleanly into its target, with what to do when not. |
| 6 | */ | |
| 7 | import { | |
| 8 | Check, | |
| 9 | ChevronRight, | |
| 10 | CircleAlert, | |
| 11 | CircleCheck, | |
| 12 | CircleX, | |
| 13 | Copy, | |
| 14 | GitCommitHorizontal, | |
| 15 | History, | |
| 16 | LoaderCircle, | |
| 17 | RotateCw, | |
| 18 | Sparkles, | |
| 19 | Terminal, | |
| 20 | TriangleAlert, | |
| 21 | } from "lucide-react"; | |
| Catching up with main takes seconds when the two sides touched different files | 22 | import { useEffect, useMemo, useState } from "react"; |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 23 | import { Form, Link } from "react-router"; |
| 24 | ||
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 25 | import type { CheckResult, CheckRun, CommitStatus, Job, Mergeable, PullBranchUpdate, Pull, RequiredCheck } from "@g1t/contracts"; |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 26 | |
| Merge branch 'worktree-agent-aaf03bdceac799c89' | 27 | import { useAddresses } from "../lib/addresses"; |
| Catching up with main takes seconds when the two sides touched different files | 28 | import { catchUpPhase, catchUpRun, catchUpTitle, catchUpWhy } from "../lib/catch-up"; |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 29 | import { duration } from "./actions"; |
| Merge branch 'worktree-agent-aca4790ea223aa235' | 30 | import { Elapsed, type Live, useRuns } from "./agents"; |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 31 | import { CopyLine, ErrorText, SubmitButton, TimeAgo } from "./ui"; |
| 32 | import { Badge } from "./ui/badge"; | |
| 33 | import { Button } from "./ui/button"; | |
| 34 | import { Card } from "./ui/card"; | |
| Merge branch 'worktree-agent-ae1299e92e4462012' | 35 | import { Hint } from "./ui/hint"; |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 36 | import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip"; |
| Merge branch 'worktree-agent-aca4790ea223aa235' | 37 | import { SkeletonLine } from "./ui/skeleton"; |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 38 | |
| 39 | // --- Output --------------------------------------------------------------- | |
| 40 | ||
| 41 | /** What the sandbox puts first when it kept only the end of the output. */ | |
| 42 | const CUT_SHORT = "… (earlier output not shown)"; | |
| 43 | ||
| 44 | /** One run of text in one style, from a line with ANSI colour codes. */ | |
| 45 | type Segment = { text: string; className: string }; | |
| 46 | ||
| 47 | const ANSI_COLOURS: Record<number, string> = { | |
| 48 | 30: "text-faint", | |
| 49 | 31: "text-danger", | |
| Merge branch 'worktree-agent-ad7c6d88d93adc817' | 50 | 32: "text-success", |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 51 | 33: "text-warn", |
| 52 | 34: "text-info", | |
| 53 | 35: "text-merged", | |
| 54 | 36: "text-info", | |
| 55 | 37: "text-fg", | |
| 56 | 90: "text-faint", | |
| 57 | 91: "text-danger", | |
| Merge branch 'worktree-agent-ad7c6d88d93adc817' | 58 | 92: "text-success", |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 59 | 93: "text-warn", |
| 60 | 94: "text-info", | |
| 61 | 95: "text-merged", | |
| 62 | 96: "text-info", | |
| 63 | 97: "text-fg", | |
| 64 | }; | |
| 65 | ||
| 66 | /** | |
| 67 | * A line's text split where its colour changes. Colours and bold are kept; | |
| 68 | * every other escape sequence (cursor movement, erasing, links) is dropped. | |
| 69 | */ | |
| 70 | export function ansiSegments(line: string): Segment[] { | |
| 71 | const segments: Segment[] = []; | |
| 72 | let colour = ""; | |
| 73 | let bold = false; | |
| 74 | // SGR sequences are kept apart; anything else that starts with ESC goes. | |
| 75 | const pattern = /\x1b\[([0-9;]*)m|\x1b\[[0-9;?]*[A-Za-z]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[@-_]/g; | |
| 76 | let at = 0; | |
| 77 | const push = (text: string) => { | |
| 78 | if (text) segments.push({ text, className: [colour, bold ? "font-semibold" : ""].filter(Boolean).join(" ") }); | |
| 79 | }; | |
| 80 | for (const match of line.matchAll(pattern)) { | |
| 81 | push(line.slice(at, match.index)); | |
| 82 | at = match.index + match[0].length; | |
| 83 | if (match[1] === undefined) continue; | |
| 84 | const codes = match[1] === "" ? [0] : match[1].split(";").map(Number); | |
| 85 | for (const code of codes) { | |
| 86 | if (code === 0) { | |
| 87 | colour = ""; | |
| 88 | bold = false; | |
| 89 | } else if (code === 1) bold = true; | |
| 90 | else if (code === 22) bold = false; | |
| 91 | else if (code === 39) colour = ""; | |
| 92 | else if (ANSI_COLOURS[code]) colour = ANSI_COLOURS[code]; | |
| 93 | } | |
| 94 | } | |
| 95 | push(line.slice(at)); | |
| 96 | return segments; | |
| 97 | } | |
| 98 | ||
| 99 | /** Text without any ANSI escape sequences, for copying. */ | |
| 100 | export function stripAnsi(text: string): string { | |
| 101 | return ansiSegments(text.replace(/\r\n/g, "\n")) | |
| 102 | .map((segment) => segment.text) | |
| 103 | .join(""); | |
| 104 | } | |
| 105 | ||
| 106 | /** Lines as a terminal would show them: a carriage return starts the line over. */ | |
| 107 | function terminalLines(text: string): string[] { | |
| 108 | return text | |
| 109 | .replace(/\r\n/g, "\n") | |
| 110 | .split("\n") | |
| 111 | .map((line) => { | |
| 112 | const parts = line.split("\r"); | |
| 113 | return parts[parts.length - 1] || parts.filter(Boolean).pop() || ""; | |
| 114 | }); | |
| 115 | } | |
| 116 | ||
| 117 | /** | |
| 118 | * A command's output as a log: numbered lines, colours rendered, a copy | |
| 119 | * button, and a note when only the end of it was kept. | |
| 120 | */ | |
| 121 | export function LogViewer({ text }: { text: string }) { | |
| 122 | const cut = text.startsWith(CUT_SHORT); | |
| 123 | const body = cut ? text.slice(CUT_SHORT.length).replace(/^\n/, "") : text; | |
| 124 | const lines = useMemo(() => { | |
| 125 | const all = terminalLines(body); | |
| 126 | while (all.length > 0 && all[all.length - 1].trim() === "") all.pop(); | |
| 127 | return all; | |
| 128 | }, [body]); | |
| 129 | const [copied, setCopied] = useState(false); | |
| 130 | if (lines.length === 0) { | |
| 131 | return <p className="px-3 py-2 font-mono text-xs text-faint">No output.</p>; | |
| 132 | } | |
| 133 | return ( | |
| 134 | <div className="relative"> | |
| Merge branch 'worktree-agent-ae1299e92e4462012' | 135 | <Hint label="Copy the output"> |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 136 | <Button |
| Merge branch 'worktree-agent-ae1299e92e4462012' | 137 | type="button" |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 138 | aria-label="Copy the output" variant="outline" size="inline" className="absolute top-1.5 right-3 z-10 bg-surface p-1.5 text-faint" onClick={() => { |
| Merge branch 'worktree-agent-ae1299e92e4462012' | 139 | void navigator.clipboard.writeText(stripAnsi(body)); |
| 140 | setCopied(true); | |
| 141 | setTimeout(() => setCopied(false), 1500); | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 142 | }}> |
| Merge branch 'worktree-agent-ad7c6d88d93adc817' | 143 | {copied ? <Check size={13} className="text-success" /> : <Copy size={13} />} |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 144 | </Button> |
| Merge branch 'worktree-agent-ae1299e92e4462012' | 145 | </Hint> |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 146 | {cut && ( |
| 147 | <p className="border-b border-line px-3 py-1.5 text-xs text-faint"> | |
| 148 | Showing the end of the output. Earlier lines were not kept. | |
| 149 | </p> | |
| 150 | )} | |
| 151 | <div className="max-h-96 overflow-auto py-1.5 font-mono text-xs leading-5"> | |
| 152 | {lines.map((line, index) => ( | |
| 153 | <div key={index} className="flex gap-3 pr-12 pl-2 hover:bg-raised/40"> | |
| 154 | <span className="w-9 shrink-0 text-right text-faint/70 select-none">{index + 1}</span> | |
| 155 | <span className="min-w-0 break-all whitespace-pre-wrap text-fg/85"> | |
| 156 | {ansiSegments(line).map((segment, at) => | |
| 157 | segment.className ? ( | |
| 158 | <span key={at} className={segment.className}> | |
| 159 | {segment.text} | |
| 160 | </span> | |
| 161 | ) : ( | |
| 162 | segment.text | |
| 163 | ), | |
| 164 | )} | |
| 165 | </span> | |
| 166 | </div> | |
| 167 | ))} | |
| 168 | </div> | |
| 169 | </div> | |
| 170 | ); | |
| 171 | } | |
| 172 | ||
| 173 | // --- Checks --------------------------------------------------------------- | |
| 174 | ||
| 175 | function seconds(ms: number): string { | |
| 176 | if (ms < 1000) return `${ms}ms`; | |
| 177 | const s = Math.round(ms / 1000); | |
| 178 | return s < 60 ? `${(ms / 1000).toFixed(ms < 10_000 ? 1 : 0)}s` : `${Math.floor(s / 60)}m ${s % 60}s`; | |
| 179 | } | |
| 180 | ||
| 181 | type Standing = "passed" | "failed" | "running" | "queued" | "skipped"; | |
| 182 | ||
| 183 | function StandingIcon({ standing, size = 15 }: { standing: Standing; size?: number }) { | |
| Merge branch 'worktree-agent-ad7c6d88d93adc817' | 184 | if (standing === "passed") return <CircleCheck size={size} className="shrink-0 text-success" aria-label="Passed" />; |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 185 | if (standing === "failed") return <CircleX size={size} className="shrink-0 text-danger" aria-label="Failed" />; |
| 186 | if (standing === "running") | |
| 187 | return <LoaderCircle size={size} className="shrink-0 animate-spin text-warn" aria-label="Running" />; | |
| 188 | if (standing === "skipped") return <CircleAlert size={size} className="shrink-0 text-faint" aria-label="Skipped" />; | |
| 189 | return <LoaderCircle size={size} className="shrink-0 text-faint" aria-label="Queued" />; | |
| 190 | } | |
| 191 | ||
| 192 | /** The id of the workflow run a status links to, if it links to one. */ | |
| 193 | export function runIdOf(status: CommitStatus): string | null { | |
| 194 | return /\/actions\/runs\/([^/?#]+)/.exec(status.targetUrl ?? "")?.[1] ?? null; | |
| 195 | } | |
| 196 | ||
| 197 | /** Where a job stands, in the box's terms. */ | |
| 198 | function jobStanding(job: Job): Standing { | |
| 199 | if (job.status !== "completed") return job.status === "in_progress" || job.status === "calling" ? "running" : "queued"; | |
| 200 | if (job.conclusion === "success") return "passed"; | |
| 201 | if (job.conclusion === "skipped" || job.conclusion === "cancelled") return "skipped"; | |
| 202 | return "failed"; | |
| 203 | } | |
| 204 | ||
| 205 | function statusStanding(status: CommitStatus): Standing { | |
| 206 | if (status.state === "pending") return "running"; | |
| 207 | return status.state === "success" ? "passed" : "failed"; | |
| 208 | } | |
| 209 | ||
| 210 | /** One row of the checks list: an icon, a name, how long, and a way in. */ | |
| 211 | function CheckLine({ | |
| 212 | standing, | |
| 213 | name, | |
| 214 | detail, | |
| 215 | time, | |
| Merge branch 'worktree-agent-aca4790ea223aa235' | 216 | timing = false, |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 217 | to, |
| 218 | }: { | |
| 219 | standing: Standing; | |
| 220 | name: React.ReactNode; | |
| 221 | detail?: React.ReactNode; | |
| 222 | time?: string; | |
| Merge branch 'worktree-agent-aca4790ea223aa235' | 223 | /** Its jobs and their times are still being read. */ |
| 224 | timing?: boolean; | |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 225 | to?: string; |
| 226 | }) { | |
| 227 | return ( | |
| 228 | <div className="flex items-center gap-2.5 px-4 py-2 text-sm"> | |
| 229 | <StandingIcon standing={standing} /> | |
| 230 | <span className="min-w-0 grow truncate"> | |
| 231 | {name} | |
| 232 | {detail && <span className="text-muted"> — {detail}</span>} | |
| 233 | </span> | |
| Merge remote-tracking branch 'origin/main' into workspace-chat | 234 | {time && ( |
| 235 | // A running job's time counts to now, which differs between server and browser. | |
| 236 | <span className="shrink-0 animate-fade-in font-mono text-xs text-faint" suppressHydrationWarning> | |
| 237 | {time} | |
| 238 | </span> | |
| 239 | )} | |
| Merge branch 'worktree-agent-aca4790ea223aa235' | 240 | {timing && <SkeletonLine className="w-10 shrink-0 text-xs" />} |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 241 | {to && ( |
| 242 | <Link to={to} className="shrink-0 text-xs text-muted hover:text-fg hover:underline"> | |
| 243 | Details | |
| 244 | </Link> | |
| 245 | )} | |
| 246 | </div> | |
| 247 | ); | |
| 248 | } | |
| 249 | ||
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 250 | /** A command from a run recorded before checks were workflows: what it printed, open when it failed. */ |
| 251 | function CommandResult({ result }: { result: CheckResult }) { | |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 252 | const timedOut = result.exitCode == null && !result.passed; |
| 253 | return ( | |
| 254 | <details className="group" open={!result.passed}> | |
| 255 | <summary className="flex cursor-pointer list-none items-center gap-2.5 px-4 py-2 text-sm hover:bg-raised/40"> | |
| 256 | <StandingIcon standing={result.passed ? "passed" : "failed"} /> | |
| 257 | <span className="min-w-0 grow truncate"> | |
| 258 | <code className="font-mono text-xs">{result.command}</code> | |
| 259 | {!result.passed && ( | |
| 260 | <span className="text-muted"> | |
| 261 | {" — "} | |
| 262 | {timedOut ? "stopped for taking too long" : `exited with code ${result.exitCode}`} | |
| 263 | </span> | |
| 264 | )} | |
| 265 | </span> | |
| 266 | <span className="shrink-0 font-mono text-xs text-faint">{seconds(result.durationMs)}</span> | |
| 267 | <ChevronRight size={14} className="shrink-0 text-faint transition-transform group-open:rotate-90" /> | |
| 268 | </summary> | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 269 | <Card tone="bg" radius="lg" className="mx-4 mb-3 overflow-hidden"> |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 270 | <LogViewer text={result.output} /> |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 271 | </Card> |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 272 | </details> |
| 273 | ); | |
| 274 | } | |
| 275 | ||
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 276 | /** Where a required check stands, in the box's terms. */ |
| 277 | function requiredStanding(check: RequiredCheck): Standing { | |
| 278 | if (check.state === "success") return "passed"; | |
| 279 | if (check.state === "failure") return "failed"; | |
| 280 | return check.state === "pending" ? "running" : "queued"; | |
| 281 | } | |
| 282 | ||
| 283 | /** How the required checks stand together: "2 of 3 passing", and what holds the rest. */ | |
| 284 | export function requiredSummary(required: RequiredCheck[]): { title: string; sub: string; standing: Standing } { | |
| 285 | const count = (state: RequiredCheck["state"]) => required.filter((check) => check.state === state).length; | |
| 286 | const passing = count("success"); | |
| 287 | const failing = count("failure"); | |
| 288 | const running = count("pending"); | |
| 289 | const expected = count("expected"); | |
| 290 | const sub = [ | |
| 291 | failing > 0 && `${failing} failing`, | |
| 292 | running > 0 && `${running} running`, | |
| 293 | expected > 0 && `${expected} not reported yet`, | |
| 294 | ] | |
| 295 | .filter(Boolean) | |
| 296 | .join(", "); | |
| 297 | return { | |
| 298 | title: `Required checks: ${passing} of ${required.length} passing`, | |
| 299 | sub, | |
| 300 | standing: failing > 0 ? "failed" : running + expected > 0 ? "running" : "passed", | |
| 301 | }; | |
| 302 | } | |
| 303 | ||
| 304 | /** The repository-relative address of a status's link, when it is on g1t. */ | |
| 305 | function onSite(url: string | null): string | undefined { | |
| 306 | if (!url) return undefined; | |
| 307 | const path = url.replace(/^https:\/\/g1t\.sh(?=\/)/, ""); | |
| 308 | return path.startsWith("/") ? path : undefined; | |
| 309 | } | |
| 310 | ||
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 311 | /** |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 312 | * Every check on a pull request, at a glance and in detail: first the |
| 313 | * checks the default branch requires, each as it stands on the head (one | |
| 314 | * nothing has reported is waited for), then every workflow run on the | |
| 315 | * head, job by job. A record of the merge queue taking it out comes last. | |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 316 | */ |
| 317 | export function ChecksSection({ | |
| 318 | run, | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 319 | required, |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 320 | statuses, |
| 321 | jobs, | |
| 322 | pull, | |
| 323 | base, | |
| 324 | earlier, | |
| 325 | canRerunWorkflows, | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 326 | settingsUrl, |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 327 | error, |
| Merge branch 'worktree-agent-aca4790ea223aa235' | 328 | loading = false, |
| Merge demo dry run fixes: settled checks, other attempts, queue links, clone box, activity feed, DEMO.md | 329 | settled = false, |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 330 | }: { |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 331 | /** Set when the merge queue took it out, or for a run from before checks were workflows. */ |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 332 | run: CheckRun | null; |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 333 | required: RequiredCheck[]; |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 334 | statuses: CommitStatus[]; |
| 335 | /** Each workflow run's jobs, by run id, where they could be read. */ | |
| 336 | jobs: Record<string, Job[]>; | |
| 337 | pull: Pull; | |
| 338 | /** The repository's path, `/<owner>/<repo>`. */ | |
| 339 | base: string; | |
| 340 | earlier: CheckRun[]; | |
| 341 | canRerunWorkflows: boolean; | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 342 | /** Where the required checks are chosen, for those who may. */ |
| 343 | settingsUrl?: string | null; | |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 344 | error?: string | null; |
| Merge branch 'worktree-agent-aca4790ea223aa235' | 345 | /** The workflow runs' jobs are still being read: their rows wait, busy. */ |
| 346 | loading?: boolean; | |
| Merge demo dry run fixes: settled checks, other attempts, queue links, clone box, activity feed, DEMO.md | 347 | /** |
| 348 | * The pull request is closed or merged: its head's checks as they ended, | |
| 349 | * to read only. Nothing is said of holding a merge. | |
| 350 | */ | |
| 351 | settled?: boolean; | |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 352 | }) { |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 353 | if (!run && statuses.length === 0 && required.length === 0) return null; |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 354 | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 355 | // Every workflow row, so they can be counted the same way they are shown. |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 356 | const workflow: Standing[] = statuses.flatMap((status) => { |
| 357 | const id = runIdOf(status); | |
| 358 | const theirs = id ? jobs[id] : undefined; | |
| 359 | return theirs && theirs.length > 0 ? theirs.map(jobStanding) : [statusStanding(status)]; | |
| 360 | }); | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 361 | const failed = workflow.filter((standing) => standing === "failed").length; |
| 362 | const pending = workflow.filter((standing) => standing === "running" || standing === "queued").length; | |
| 363 | const passed = workflow.filter((standing) => standing === "passed").length; | |
| 364 | const skipped = workflow.filter((standing) => standing === "skipped").length; | |
| 365 | const total = workflow.length; | |
| 366 | const queueFailed = run?.status === "failed" && run.results.length === 0; | |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 367 | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 368 | const summary = required.length > 0 ? requiredSummary(required) : null; |
| 369 | const headline = summary | |
| 370 | ? summary.title | |
| 371 | : failed > 0 | |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 372 | ? `${failed} of ${total} ${total === 1 ? "check" : "checks"} failed` |
| 373 | : pending > 0 | |
| 374 | ? `${pending} of ${total} ${total === 1 ? "check is" : "checks are"} still running` | |
| 375 | : total > 0 | |
| 376 | ? "All checks have passed" | |
| 377 | : "Checks"; | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 378 | const others = [ |
| 379 | failed > 0 && `${failed} failed`, | |
| 380 | pending > 0 && `${pending} running`, | |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 381 | passed > 0 && `${passed} passed`, |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 382 | skipped > 0 && `${skipped} skipped or cancelled`, |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 383 | ] |
| 384 | .filter(Boolean) | |
| 385 | .join(", "); | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 386 | const sub = summary |
| 387 | ? [summary.sub, others && `all checks: ${others}`].filter(Boolean).join(" · ") | |
| Merge demo dry run fixes: settled checks, other attempts, queue links, clone box, activity feed, DEMO.md | 388 | : settled |
| 389 | ? [others, pull.headCommit && `on ${pull.headCommit.slice(0, 7)}`].filter(Boolean).join(" · ") | |
| 390 | : [others, "none are required, so none hold the merge"].filter(Boolean).join(" · "); | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 391 | const standing: Standing = summary |
| 392 | ? summary.standing | |
| 393 | : failed > 0 | |
| 394 | ? "failed" | |
| 395 | : pending > 0 | |
| 396 | ? "running" | |
| 397 | : "passed"; | |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 398 | |
| 399 | const commitPath = (sha: string) => | |
| 400 | pull.fork ? `/${pull.fork.namespace}/${pull.fork.name}/commit/${sha}` : `${base}/commit/${sha}`; | |
| 401 | const older = run != null && pull.headCommit != null && run.headCommit !== pull.headCommit; | |
| 402 | const failedRuns = statuses.filter((status) => statusStanding(status) === "failed" && runIdOf(status)); | |
| 403 | ||
| 404 | return ( | |
| Merge branch 'worktree-agent-aca4790ea223aa235' | 405 | <div aria-busy={loading || undefined}> |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 406 | <div className="flex items-start gap-3 px-4 py-3"> |
| 407 | <span className="mt-0.5 shrink-0"> | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 408 | {standing === "failed" || queueFailed ? ( |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 409 | <CircleX size={16} className="text-danger" /> |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 410 | ) : standing === "running" ? ( |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 411 | <LoaderCircle size={16} className="animate-spin text-warn" /> |
| 412 | ) : ( | |
| Merge branch 'worktree-agent-ad7c6d88d93adc817' | 413 | <CircleCheck size={16} className="text-success" /> |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 414 | )} |
| 415 | </span> | |
| 416 | <div className="min-w-0 grow"> | |
| 417 | <p className="text-sm font-medium">{headline}</p> | |
| 418 | {sub && <p className="mt-0.5 text-xs text-muted">{sub}</p>} | |
| Merge demo dry run fixes: settled checks, other attempts, queue links, clone box, activity feed, DEMO.md | 419 | {!summary && !settled && settingsUrl && ( |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 420 | <p className="mt-0.5 text-xs text-muted"> |
| 421 | <Link to={settingsUrl} className="text-fg hover:underline"> | |
| 422 | Choose required checks | |
| 423 | </Link>{" "} | |
| 424 | to hold merges until they pass. | |
| 425 | </p> | |
| 426 | )} | |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 427 | </div> |
| 428 | <div className="flex shrink-0 flex-wrap items-center justify-end gap-2"> | |
| 429 | {canRerunWorkflows && | |
| 430 | failedRuns.map((status) => ( | |
| 431 | <Form method="post" key={status.context}> | |
| 432 | <input type="hidden" name="action" value="rerun-workflow" /> | |
| 433 | <input type="hidden" name="run" value={runIdOf(status) ?? ""} /> | |
| Merge branch 'worktree-agent-ae1299e92e4462012' | 434 | <Hint label={`Re-run the failed jobs of ${status.context}`}> |
| 435 | <SubmitButton | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 436 | variant="outline" |
| Merge branch 'worktree-agent-ae1299e92e4462012' | 437 | match={{ action: "rerun-workflow", run: runIdOf(status) ?? "" }} |
| 438 | pending="Re-running…" | |
| 439 | > | |
| 440 | <RotateCw size={13} /> | |
| 441 | Re-run failed jobs{failedRuns.length > 1 ? ` of ${status.context}` : ""} | |
| 442 | </SubmitButton> | |
| 443 | </Hint> | |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 444 | </Form> |
| 445 | ))} | |
| 446 | </div> | |
| 447 | </div> | |
| 448 | {error && ( | |
| 449 | <div className="px-4 pb-2"> | |
| 450 | <ErrorText>{error}</ErrorText> | |
| 451 | </div> | |
| 452 | )} | |
| 453 | ||
| 454 | <div className="divide-y divide-line border-t border-line"> | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 455 | {required.length > 0 && ( |
| 456 | <section aria-label="Required checks" className="py-1"> | |
| 457 | <p className="px-4 pt-1.5 pb-1 text-xs font-medium text-muted">Required</p> | |
| 458 | {required.map((check) => ( | |
| 459 | <CheckLine | |
| 460 | key={check.name} | |
| 461 | standing={requiredStanding(check)} | |
| 462 | name={ | |
| 463 | <> | |
| 464 | {check.name} | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 465 | <Badge className="ml-1.5 px-1.5 text-[0.625rem] font-normal text-faint">Required</Badge> |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 466 | </> |
| 467 | } | |
| 468 | detail={ | |
| 469 | check.state === "expected" | |
| 470 | ? "Waiting for status to be reported" | |
| 471 | : (check.description ?? (check.state === "pending" ? "running" : undefined)) | |
| 472 | } | |
| 473 | to={onSite(check.targetUrl)} | |
| 474 | /> | |
| 475 | ))} | |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 476 | </section> |
| 477 | )} | |
| 478 | ||
| 479 | {statuses.length > 0 && ( | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 480 | <section aria-label="All checks" className="py-1"> |
| 481 | <p className="px-4 pt-1.5 pb-1 text-xs font-medium text-muted">{required.length > 0 ? "All checks" : "Workflows"}</p> | |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 482 | {statuses.map((status) => { |
| 483 | const id = runIdOf(status); | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 484 | const runPath = id ? `${base}/actions/runs/${id}` : onSite(status.targetUrl); |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 485 | const theirs = id ? jobs[id] : undefined; |
| 486 | if (!theirs || theirs.length === 0) { | |
| 487 | return ( | |
| 488 | <CheckLine | |
| 489 | key={status.context} | |
| 490 | standing={statusStanding(status)} | |
| 491 | name={status.context} | |
| 492 | detail={status.description ?? undefined} | |
| Merge branch 'worktree-agent-aca4790ea223aa235' | 493 | timing={loading && id != null} |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 494 | to={runPath} |
| 495 | /> | |
| 496 | ); | |
| 497 | } | |
| 498 | return theirs.map((job) => ( | |
| 499 | <CheckLine | |
| 500 | key={`${status.context}-${job.id}`} | |
| 501 | standing={jobStanding(job)} | |
| 502 | name={ | |
| 503 | <> | |
| 504 | <span className="text-muted">{status.context} / </span> | |
| 505 | {job.name} | |
| 506 | </> | |
| 507 | } | |
| 508 | detail={job.reason ?? (jobStanding(job) === "failed" ? "failed" : undefined)} | |
| 509 | time={duration(job.startedAt, job.finishedAt) || undefined} | |
| 510 | to={`${runPath}?job=${job.id}`} | |
| 511 | /> | |
| 512 | )); | |
| 513 | })} | |
| 514 | </section> | |
| 515 | )} | |
| 516 | ||
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 517 | {run && ( |
| 518 | <section aria-label={queueFailed ? "Merge queue" : "Earlier commands"} className="py-1"> | |
| 519 | <p className="flex flex-wrap items-center gap-x-1.5 px-4 pt-1.5 pb-1 text-xs text-faint"> | |
| 520 | <span className="font-medium text-muted">{queueFailed ? "Merge queue" : "Commands from the issue"}</span> | |
| 521 | <span>· on</span> | |
| 522 | <Link to={commitPath(run.headCommit)} className="inline-flex items-center gap-1 font-mono hover:text-fg"> | |
| 523 | <GitCommitHorizontal size={12} /> | |
| 524 | {run.headCommit.slice(0, 7)} | |
| 525 | </Link> | |
| 526 | {run.finishedAt ? ( | |
| 527 | <span> | |
| 528 | · <TimeAgo at={run.finishedAt} /> | |
| 529 | </span> | |
| 530 | ) : ( | |
| 531 | <span> | |
| 532 | · started <TimeAgo at={run.createdAt} /> | |
| 533 | </span> | |
| 534 | )} | |
| 535 | </p> | |
| 536 | {older && !queueFailed && ( | |
| 537 | <p className="mx-4 my-1.5 flex items-start gap-2 rounded-md bg-warn/10 px-2.5 py-1.5 text-xs text-warn"> | |
| 538 | <TriangleAlert size={13} className="mt-0.5 shrink-0" /> | |
| 539 | These ran on an older commit, before the latest push,{" "} | |
| 540 | <span className="font-mono">{pull.headCommit?.slice(0, 7)}</span>. | |
| 541 | </p> | |
| 542 | )} | |
| 543 | {run.error && ( | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 544 | <Card asChild tone="bg" radius="lg" className="mx-4 my-1.5 px-3 py-2 text-sm text-muted"> |
| 545 | <p>{run.error}</p> | |
| 546 | </Card> | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 547 | )} |
| 548 | {run.results.map((result, index) => ( | |
| 549 | <CommandResult key={`${result.command}-${index}`} result={result} /> | |
| 550 | ))} | |
| 551 | </section> | |
| 552 | )} | |
| 553 | ||
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 554 | {earlier.length > 0 && ( |
| 555 | <details className="group"> | |
| 556 | <summary className="flex cursor-pointer list-none items-center gap-2 px-4 py-2 text-xs text-muted hover:text-fg"> | |
| 557 | <History size={13} /> | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 558 | Earlier records ({earlier.length}) |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 559 | <ChevronRight size={13} className="transition-transform group-open:rotate-90" /> |
| 560 | </summary> | |
| 561 | <ul className="space-y-1 px-4 pb-3 text-xs"> | |
| 562 | {earlier.map((before) => ( | |
| 563 | <li key={before.id} className="flex items-center gap-2 text-muted"> | |
| 564 | <StandingIcon | |
| 565 | size={13} | |
| 566 | standing={ | |
| 567 | before.status === "passed" | |
| 568 | ? "passed" | |
| 569 | : before.status === "queued" || before.status === "running" | |
| 570 | ? "queued" | |
| 571 | : "failed" | |
| 572 | } | |
| 573 | /> | |
| 574 | <Link to={commitPath(before.headCommit)} className="font-mono hover:text-fg"> | |
| 575 | {before.headCommit.slice(0, 7)} | |
| 576 | </Link> | |
| 577 | <span className="min-w-0 truncate"> | |
| 578 | {before.status === "errored" | |
| 579 | ? `could not run${before.error ? `: ${before.error}` : ""}` | |
| 580 | : before.results.length > 0 | |
| 581 | ? `${before.results.filter((result) => result.passed).length} of ${before.results.length} passed` | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 582 | : (before.error ?? before.status)} |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 583 | </span> |
| 584 | <span className="ml-auto shrink-0 text-faint"> | |
| 585 | <TimeAgo at={before.createdAt} /> | |
| 586 | </span> | |
| 587 | </li> | |
| 588 | ))} | |
| 589 | </ul> | |
| 590 | </details> | |
| 591 | )} | |
| 592 | </div> | |
| 593 | </div> | |
| 594 | ); | |
| 595 | } | |
| 596 | ||
| 597 | // --- Mergeability --------------------------------------------------------- | |
| 598 | ||
| 599 | /** How to resolve the conflicts by hand, step by step, for a branch or a fork. */ | |
| Merge branch 'worktree-agent-aaf03bdceac799c89' | 600 | export function commandLineSteps( |
| 601 | pull: Pull, | |
| 602 | owner: string, | |
| 603 | repo: string, | |
| 604 | defaultBranch: string, | |
| 605 | files: string[], | |
| 606 | site = "https://g1t.sh", | |
| 607 | ): string[] { | |
| 608 | const upstream = `${site}/${owner}/${repo}.git`; | |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 609 | const add = files.length > 0 && files.length <= 6 ? `git add ${files.join(" ")}` : "git add -A"; |
| 610 | if (pull.branch) { | |
| 611 | return [ | |
| 612 | `git fetch origin`, | |
| 613 | `git checkout ${pull.branch}`, | |
| 614 | `git merge origin/${defaultBranch}`, | |
| 615 | `${add} && git commit --no-edit`, | |
| 616 | `git push origin ${pull.branch}`, | |
| 617 | ]; | |
| 618 | } | |
| 619 | const fork = pull.fork!; | |
| 620 | return [ | |
| Merge branch 'worktree-agent-aaf03bdceac799c89' | 621 | `git clone ${site}/${fork.namespace}/${fork.name}.git && cd ${fork.name}`, |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 622 | `git pull --no-rebase ${upstream} ${defaultBranch}`, |
| 623 | `${add} && git commit --no-edit`, | |
| 624 | `git push`, | |
| 625 | ]; | |
| 626 | } | |
| 627 | ||
| 628 | /** | |
| 629 | * The branch conflicts with its target: which files, and three ways out — | |
| g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent | 630 | * g1t, the browser (soon), or the command line. |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 631 | */ |
| 632 | export function ConflictsSection({ | |
| 633 | conflicts, | |
| 634 | pull, | |
| 635 | owner, | |
| 636 | repo, | |
| 637 | defaultBranch, | |
| 638 | changesUrl, | |
| 639 | canResolve, | |
| 640 | resolving, | |
| 641 | error, | |
| 642 | }: { | |
| 643 | conflicts: string[]; | |
| 644 | pull: Pull; | |
| 645 | owner: string; | |
| 646 | repo: string; | |
| 647 | defaultBranch: string; | |
| 648 | /** Where the pull request's changes are shown, to link each file. */ | |
| 649 | changesUrl: string; | |
| g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent | 650 | /** Whether the viewer may have g1t resolve them. */ |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 651 | canResolve: boolean; |
| Catching up with main takes seconds when the two sides touched different files | 652 | /** Whether asking for it is on its way. */ |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 653 | resolving: boolean; |
| 654 | error?: string | null; | |
| 655 | }) { | |
| Merge branch 'worktree-agent-aaf03bdceac799c89' | 656 | const { site } = useAddresses(); |
| 657 | const steps = commandLineSteps(pull, owner, repo, defaultBranch, conflicts, site); | |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 658 | return ( |
| 659 | <div className="bg-danger/5"> | |
| 660 | <div className="flex gap-3 px-4 py-3"> | |
| 661 | <CircleAlert size={16} className="mt-0.5 shrink-0 text-danger" /> | |
| 662 | <div className="min-w-0 grow"> | |
| 663 | <p className="text-sm font-medium">This branch has conflicts that must be resolved</p> | |
| 664 | <p className="mt-0.5 text-sm text-muted"> | |
| 665 | Merging <span className="font-mono">{defaultBranch}</span> into it stops on{" "} | |
| 666 | {conflicts.length === 1 ? "one file" : `${conflicts.length} files`}. It cannot merge until they are resolved. | |
| 667 | </p> | |
| 668 | {conflicts.length > 0 && ( | |
| 669 | <> | |
| 670 | <p className="mt-3 text-xs font-medium text-muted">Conflicting files</p> | |
| 671 | <ul className="mt-1.5 space-y-1"> | |
| 672 | {conflicts.map((path) => ( | |
| 673 | <li key={path}> | |
| 674 | <Link | |
| 675 | to={`${changesUrl}#file-${path}`} | |
| 676 | className="font-mono text-xs text-fg hover:text-danger hover:underline" | |
| 677 | > | |
| 678 | {path} | |
| 679 | </Link> | |
| 680 | </li> | |
| 681 | ))} | |
| 682 | </ul> | |
| 683 | </> | |
| 684 | )} | |
| 685 | <div className="mt-3 flex flex-wrap items-center gap-2"> | |
| 686 | {canResolve && ( | |
| Catching up with main takes seconds when the two sides touched different files | 687 | <Form method="post"> |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 688 | <input type="hidden" name="action" value="update" /> |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 689 | <Button type="submit" disabled={resolving}> |
| Catching up with main takes seconds when the two sides touched different files | 690 | {resolving ? <LoaderCircle size={14} className="animate-spin" /> : <Sparkles size={14} />} |
| g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent | 691 | {resolving ? "Starting g1t…" : "Resolve with g1t"} |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 692 | </Button> |
| 693 | </Form> | |
| 694 | )} | |
| 695 | <Tooltip> | |
| 696 | <TooltipTrigger asChild> | |
| 697 | {/* A disabled button gets no pointer events; the span carries the tooltip. */} | |
| 698 | <span tabIndex={0} className="inline-flex"> | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 699 | <Button variant="outline" type="button" disabled aria-disabled="true"> |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 700 | Resolve in the browser |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 701 | <Badge className="px-1.5 py-0 text-[0.625rem] font-normal text-faint">Soon</Badge> |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 702 | </Button> |
| 703 | </span> | |
| 704 | </TooltipTrigger> | |
| 705 | <TooltipContent> | |
| g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent | 706 | An editor for conflicts in the browser is coming. Until then, have g1t resolve them, or use the |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 707 | command line. |
| 708 | </TooltipContent> | |
| 709 | </Tooltip> | |
| 710 | </div> | |
| 711 | {error && ( | |
| 712 | <div className="mt-2"> | |
| 713 | <ErrorText>{error}</ErrorText> | |
| 714 | </div> | |
| 715 | )} | |
| 716 | <details className="group mt-3"> | |
| 717 | <summary className="flex cursor-pointer list-none items-center gap-1.5 text-xs text-muted hover:text-fg"> | |
| 718 | <Terminal size={13} /> | |
| 719 | Resolve on the command line | |
| 720 | <ChevronRight size={13} className="transition-transform group-open:rotate-90" /> | |
| 721 | </summary> | |
| 722 | <ol className="mt-2 space-y-2 text-xs text-muted"> | |
| 723 | {steps.map((step, index) => ( | |
| 724 | <li key={step}> | |
| 725 | <p className="mb-1"> | |
| 726 | {index + 1}.{" "} | |
| 727 | {step.startsWith("git add") | |
| 728 | ? "Fix the conflicts in each file, removing the conflict markers, then commit the merge:" | |
| 729 | : step.startsWith("git push") | |
| 730 | ? "Push it. The pull request updates, and its checks run again:" | |
| 731 | : step.includes("merge") || step.includes("pull") | |
| 732 | ? `Merge ${defaultBranch} in. Git stops on the conflicting files:` | |
| 733 | : step.startsWith("git clone") | |
| 734 | ? "Clone the pull request's fork:" | |
| 735 | : step.startsWith("git checkout") | |
| 736 | ? "Check out the pull request's branch:" | |
| 737 | : "Get the latest of both:"} | |
| 738 | </p> | |
| 739 | <CopyLine text={step} prompt /> | |
| 740 | </li> | |
| 741 | ))} | |
| 742 | </ol> | |
| 743 | {pull.fork && ( | |
| 744 | <p className="mt-2 text-xs text-faint"> | |
| 745 | A pull request's fork takes pushes from whoever opened it. | |
| 746 | </p> | |
| 747 | )} | |
| 748 | </details> | |
| 749 | </div> | |
| 750 | </div> | |
| 751 | </div> | |
| 752 | ); | |
| 753 | } | |
| 754 | ||
| 755 | /** Whether the change merges cleanly, when that is not a conflict. */ | |
| 756 | export function MergeabilityRow({ | |
| 757 | mergeable, | |
| 758 | defaultBranch, | |
| 759 | }: { | |
| 760 | mergeable: Mergeable; | |
| 761 | defaultBranch: string; | |
| 762 | }) { | |
| 763 | if (mergeable === "checking") { | |
| 764 | return ( | |
| 765 | <div className="flex gap-3 px-4 py-3 text-sm"> | |
| 766 | <LoaderCircle size={16} className="mt-0.5 shrink-0 animate-spin text-faint" /> | |
| 767 | <div> | |
| 768 | <p className="font-medium">Checking whether this merges cleanly…</p> | |
| 769 | <p className="mt-0.5 text-muted"> | |
| 770 | It and <span className="font-mono">{defaultBranch}</span> changed some of the same files, so g1t is merging | |
| 771 | the two in a sandbox to see. | |
| 772 | </p> | |
| 773 | </div> | |
| 774 | </div> | |
| 775 | ); | |
| 776 | } | |
| 777 | if (mergeable === "clean") { | |
| 778 | return ( | |
| 779 | <div className="flex gap-3 px-4 py-3 text-sm"> | |
| Merge branch 'worktree-agent-ad7c6d88d93adc817' | 780 | <CircleCheck size={16} className="mt-0.5 shrink-0 text-success" /> |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 781 | <p className="font-medium"> |
| 782 | This branch has no conflicts with <span className="font-mono">{defaultBranch}</span> | |
| 783 | </p> | |
| 784 | </div> | |
| 785 | ); | |
| 786 | } | |
| 787 | return null; | |
| 788 | } | |
| Catching up with main takes seconds when the two sides touched different files | 789 | |
| 790 | /** | |
| 791 | * A catch-up handed to a sandbox, while it lasts: who is doing what, the | |
| 792 | * run's live step, and how long it has taken. It ends with the pull request | |
| 793 | * up to date (and this disappears), or with a plain failure and a way to | |
| 794 | * try again. It never spins on with no end. | |
| 795 | */ | |
| 796 | export function CatchUpProgress({ | |
| 797 | owner, | |
| 798 | repo, | |
| 799 | number, | |
| 800 | defaultBranch, | |
| 801 | behind, | |
| 802 | update, | |
| 803 | startedAt, | |
| 804 | retrying, | |
| Merge branch 'worktree-agent-aca4790ea223aa235' | 805 | runs: initial, |
| Catching up with main takes seconds when the two sides touched different files | 806 | }: { |
| 807 | owner: string; | |
| 808 | repo: string; | |
| 809 | number: number; | |
| 810 | defaultBranch: string; | |
| 811 | behind: boolean; | |
| 812 | update: Extract<PullBranchUpdate, { outcome: "needs_agent" }>; | |
| 813 | /** When it was asked for, in ms since the epoch. */ | |
| 814 | startedAt: number; | |
| 815 | /** Whether a retry is on its way. */ | |
| 816 | retrying: boolean; | |
| Merge branch 'worktree-agent-aca4790ea223aa235' | 817 | /** The pull request's latest runs, from the page's loader. */ |
| 818 | runs?: Live | null; | |
| Catching up with main takes seconds when the two sides touched different files | 819 | }) { |
| 820 | // The page revalidates while this is working, which reloads the runs too. | |
| Merge branch 'worktree-agent-aca4790ea223aa235' | 821 | const data = useRuns(owner, repo, { number: String(number), limit: "5" }, initial); |
| Catching up with main takes seconds when the two sides touched different files | 822 | const [now, setNow] = useState(() => Date.now()); |
| 823 | useEffect(() => { | |
| 824 | const timer = setInterval(() => setNow(Date.now()), 5000); | |
| 825 | return () => clearInterval(timer); | |
| 826 | }, []); | |
| 827 | const run = catchUpRun(data?.runs ?? [], startedAt); | |
| 828 | const phase = catchUpPhase({ behind, run, startedAt, now }); | |
| 829 | if (phase === "done") return null; | |
| 830 | const session = `/${owner}/${repo}/sessions/${number}`; | |
| 831 | if (phase !== "working") { | |
| 832 | return ( | |
| 833 | <div className="flex gap-3 px-4 py-3 text-sm"> | |
| 834 | <TriangleAlert size={16} className={`mt-0.5 shrink-0 ${phase === "failed" ? "text-danger" : "text-warn"}`} /> | |
| 835 | <div className="min-w-0"> | |
| 836 | <p className="font-medium"> | |
| 837 | {phase === "failed" | |
| 838 | ? `Catching up with ${defaultBranch} failed` | |
| 839 | : `Catching up with ${defaultBranch} is taking longer than it should`} | |
| 840 | </p> | |
| 841 | <p className="mt-0.5 text-muted"> | |
| 842 | {phase === "failed" | |
| 843 | ? `${run?.error ? `${run.error} ` : ""}Nothing was pushed, so the pull request is as it was.` | |
| 844 | : "It usually takes about a minute. It may still finish; this page updates if it does."}{" "} | |
| 845 | <Link to={session} className="text-fg hover:underline"> | |
| 846 | See the session | |
| 847 | </Link> | |
| 848 | . | |
| 849 | </p> | |
| 850 | <Form method="post" className="mt-2"> | |
| 851 | <input type="hidden" name="action" value="update" /> | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 852 | <Button variant="outline" type="submit" disabled={retrying}> |
| Catching up with main takes seconds when the two sides touched different files | 853 | {retrying ? <LoaderCircle size={14} className="animate-spin" /> : <RotateCw size={14} />} |
| 854 | {retrying ? "Trying again…" : "Try again"} | |
| 855 | </Button> | |
| 856 | </Form> | |
| 857 | </div> | |
| 858 | </div> | |
| 859 | ); | |
| 860 | } | |
| 861 | return ( | |
| 862 | <div className="flex gap-3 px-4 py-3 text-sm"> | |
| Merge branch 'worktree-agent-ad7c6d88d93adc817' | 863 | <LoaderCircle size={16} className="mt-0.5 shrink-0 animate-spin text-accent" /> |
| Catching up with main takes seconds when the two sides touched different files | 864 | <div className="min-w-0 grow"> |
| 865 | <p className="font-medium">{catchUpTitle(update.reason, defaultBranch)}</p> | |
| 866 | <p className="mt-0.5 text-muted"> | |
| 867 | {catchUpWhy(update, defaultBranch)} This usually takes about a minute; the pull request updates here when it | |
| 868 | is pushed. | |
| 869 | </p> | |
| 870 | {update.paths.length > 0 && ( | |
| Merge branch 'worktree-agent-ae1299e92e4462012' | 871 | <Hint label={update.paths.join(", ")}> |
| 872 | <p className="mt-1 truncate font-mono text-xs text-faint"> | |
| 873 | {update.paths.slice(0, 5).join(", ")} | |
| 874 | {update.paths.length > 5 && ` and ${update.paths.length - 5} more`} | |
| 875 | </p> | |
| 876 | </Hint> | |
| Catching up with main takes seconds when the two sides touched different files | 877 | )} |
| Merge branch 'worktree-agent-ae1299e92e4462012' | 878 | <Hint label={run?.step}> |
| 879 | <p className="mt-2 truncate rounded-lg bg-bg px-3 py-2 font-mono text-xs text-fg/85 ring-1 ring-line"> | |
| Merge branch 'worktree-agent-ad7c6d88d93adc817' | 880 | <span className="mr-2 inline-block size-1.5 animate-pulse rounded-full bg-accent align-middle" /> |
| Merge branch 'worktree-agent-ae1299e92e4462012' | 881 | {run?.step ?? (run ? "Starting a sandbox…" : "Waiting for a sandbox…")} |
| 882 | </p> | |
| 883 | </Hint> | |
| Catching up with main takes seconds when the two sides touched different files | 884 | <p className="mt-2 flex flex-wrap items-center gap-x-3 text-xs text-muted"> |
| 885 | <Elapsed from={new Date(startedAt).toISOString()} /> | |
| 886 | <Link to={session} className="hover:text-fg"> | |
| 887 | Watch the session | |
| 888 | </Link> | |
| 889 | </p> | |
| 890 | </div> | |
| 891 | </div> | |
| 892 | ); | |
| 893 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.