Fork Horse repository, listed by its owner. Publisher snapshot.

packages/forkhorse-dashboard/src/view.tsx

Snapshot at 34e3175

Containing directory

66,470 bytes · textView raw
1import { FreshnessStamp, WriteHint, snapshotAge, staleReason } from "./freshness.tsx";
2import { judgePromotion } from "./judge.ts";
3import { FirstRunEmpty, overviewFirstRun } from "./empty.tsx";
4import { UsageFigures } from "./usage.tsx";
5import { usagePromotionCaption, usageValues } from "./usage.ts";
6import { useState, useEffect, useRef, useSyncExternalStore, type ReactNode } from "react";
7import {
8  Badge,
9  Banner,
10  Button,
11  ClipboardText,
12  Dialog,
13  Empty,
14  LayerCard,
15  Select,
16  Switch,
17  Table,
18  Tabs,
19  Toasty,
20  Tooltip,
21  TooltipProvider,
22  useKumoToastManager,
23} from "@cloudflare/kumo";
24import { ArrowLeft, ArrowLineUp, ArrowsIn, ArrowsOut, Check, Copy, Pulse, Warning, X } from "@phosphor-icons/react";
25import {
26  checkCount,
27  duration,
28  ReceiptAnnouncements,
29  ReceiptBadge,
30  ReceiptCell,
31  ReceiptDetails,
32} from "./receipts.tsx";
33import {
34  isArchived,
35  workPackets,
36  blockerGatesReview,
37  promotionReceipt,
38  promotionAvailable,
39  recentReceipts,
40  receiptGateCaption,
41  reviewGate,
42  reviewState,
43  reviewQueue,
44  type ViewModel,
45} from "./model.ts";
46import { ArchiveControl, ArchivedBadge } from "./archive.tsx";
47import { CollisionBanner, CollisionRows, ResolveCollisionDialog } from "./collisions.tsx";
48import { RaceView } from "./race.tsx";
49import { Races, openRaceCount } from "./races.tsx";
50import { ReviewView } from "./review.tsx";
51import { ForkGraph } from "./fork-graph.tsx";
52import { NeedsYou } from "./needs-you.tsx";
53import { AgentStatusTable, ResumeCard } from "./agent-status.tsx";
54import type { OwnerNeed } from "./needs-you-model.ts";
55import type { ReviewDetail, ReviewDiff, Activity, OwnerReceipt, ProjectState, Review } from "./contracts.ts";
56import { relativeTime as relative } from "./time.ts";
57
58export type Section = "overview" | "work" | "review" | "agents" | "races";
59/** Local frame presentation only; this carries no project or owner authority. */
60export interface FramePresentation {
61  subscribe(listener: () => void): () => void;
62  getSnapshot(): boolean;
63  toggle(): void;
64  reset(): void;
65}
66export interface ViewActions {
67  framePresentation?: FramePresentation;
68  ownerLink?: string;
69  resolveBlocker?(id: string, reason: string): Promise<import("./contracts.ts").WireBlocker>;
70  openCollision?(blocker: import("./contracts.ts").BlockerView): void;
71  showPacket?(id: string): void;
72  raceId?: string;
73  raceBackSection?: "work" | "races";
74  openRace?(id?: string): void;
75  reviewId?: string;
76  openReview(id?: string): void;
77  openReceipt?(id: string): void;
78  getReview?(id: string): Promise<ReviewDetail>;
79  getDiff?(id: string, path: string): Promise<ReviewDiff>;
80  returnReview?(id: string, reason: string): Promise<boolean>;
81  archivePacket?(id: string): Promise<boolean>;
82  unarchivePacket?(id: string): Promise<boolean>;
83  section: Section;
84  setSection(section: Section): void;
85  owner: boolean;
86  selectProject(id: string): Promise<void>;
87  retry?: () => Promise<void>;
88  reconnect?: () => Promise<void>;
89  agent?: string;
90  ask?: () => Promise<void>;
91  action(kind: "approve" | "promote", review: string): Promise<boolean | void>;
92  getReceipt?(review: string): Promise<OwnerReceipt>;
93  setSettings?(required: boolean): Promise<void>;
94}
95const short = (value: string) => <code title={value}>{value.slice(0, 7)}</code>;
96const empty = (title: string) => <Empty size="sm" title={title} />;
97
98function ExpandControl({ presentation }: { presentation: FramePresentation }) {
99  const expanded = useSyncExternalStore(presentation.subscribe, presentation.getSnapshot, () => false);
100  useEffect(() => {
101    // Capture the dialog state before its own Escape handler can unmount it.
102    const escape = (event: KeyboardEvent) => {
103      if (event.key !== "Escape" || event.defaultPrevented || !presentation.getSnapshot()) return;
104      if (document.querySelector('[role="dialog"]:not([data-closed]):not([hidden]), [role="alertdialog"]:not([data-closed]):not([hidden])')) return;
105      event.preventDefault();
106      event.stopPropagation();
107      presentation.reset();
108    };
109    window.addEventListener("keydown", escape, true);
110    return () => {
111      window.removeEventListener("keydown", escape, true);
112      presentation.reset();
113    };
114  }, [presentation]);
115  const label = expanded ? "Exit full view" : "Expand";
116  return (
117    <Tooltip content={label} render={
118      <Button variant="ghost" shape="square" aria-label={label} aria-pressed={expanded}
119        onClick={presentation.toggle} />
120    }>
121      {expanded ? <ArrowsIn size={16} /> : <ArrowsOut size={16} />}
122    </Tooltip>
123  );
124}
125
126function CopyOwnerLink({ url }: { url: string }) {
127  const [copied, setCopied] = useState(false);
128  const [fallback, setFallback] = useState(false);
129  const code = useRef<HTMLElement>(null);
130  useEffect(() => {
131    if (!copied) return;
132    const timer = setTimeout(() => setCopied(false), 2000);
133    return () => clearTimeout(timer);
134  }, [copied]);
135  useEffect(() => {
136    if (!fallback || !code.current) return;
137    code.current.focus();
138    const range = document.createRange();
139    range.selectNodeContents(code.current);
140    const selection = window.getSelection();
141    selection?.removeAllRanges();
142    selection?.addRange(range);
143  }, [fallback]);
144  async function copy() {
145    try {
146      // ClipboardText swallows failures; use its Copy button pattern so sandbox denial
147      // can reveal a selectable URL instead of claiming the clipboard succeeded.
148      await navigator.clipboard.writeText(url);
149      setCopied(true);
150    } catch {
151      setCopied(false);
152      setFallback(true);
153    }
154  }
155  return (
156    <div className="owner-link-action">
157      {fallback ? <div className="owner-link-fallback">
158        <span className="caption">Owner link:</span>{" "}
159        <code ref={code} tabIndex={-1}>{url}</code>
160      </div> : <Button variant="ghost" size="sm" aria-label="Copy owner link" onClick={() => void copy()}>
161        {copied ? <Check size={16} /> : <Copy size={16} />}
162        {copied ? "Copied" : "Copy owner link"}
163      </Button>}
164      <span className="sr-only" role="status">{copied ? "Owner link copied." : ""}</span>
165    </div>
166  );
167}
168type Tone = "info" | "brand" | "success" | "warning" | "danger";
169function Status({ label, tone = "info" }: { label: string; tone?: Tone }) {
170  // Kumo's neutral dot text meets the host contrast requirement; the dot uses a status token.
171  return (
172    <Badge appearance="dot" variant="neutral" className={`status-dot status-${tone}`}>
173      {label}
174    </Badge>
175  );
176}
177function Events({ value }: { value?: "pending" | "active" }) {
178  return value === "active" ? (
179    <Status label="Active" tone="success" />
180  ) : value === "pending" ? (
181    <Badge variant="secondary">Events pending</Badge>
182  ) : null;
183}
184function TruncatedCode({ value }: { value: string }) {
185  return (
186    <Tooltip content={value} render={<code className="truncated-code" />}>
187      {value}
188    </Tooltip>
189  );
190}
191export function Paths({ paths, event }: { paths: string[]; event?: Activity }) {
192  const total = event?.changedPathCount ?? paths.length;
193  if (event && /^0+$/.test(event.after))
194    return (
195      <span className="path-summary">
196        Deleted <code>{event.ref.replace(/^refs\/heads\//, "")}</code>
197      </span>
198    );
199  if (event && /^0+$/.test(event.before) && total > 1)
200    return <span className="path-summary">First push · {total.toLocaleString()} files</span>;
201  return (
202    <span className="path-summary">
203      {paths[0] ? <TruncatedCode value={paths[0]} /> : <span>No changed paths</span>}
204      {total > 1 && <span className="path-more">+{(total - 1).toLocaleString()} more</span>}
205    </span>
206  );
207}
208export function PathDetails({
209  paths,
210  total = paths.length,
211  partial = false,
212}: {
213  paths: string[];
214  total?: number;
215  partial?: boolean;
216}) {
217  const [expanded, setExpanded] = useState(false);
218  return (
219    <div className="path-details">
220      {(expanded ? paths : paths.slice(0, 20)).map((path, index) => (
221        <p className="caption" key={`${index}-${path}`}>
222          <code>{path}</code>
223        </p>
224      ))}
225      {!expanded && paths.length > 20 && (
226        <Button variant="ghost" size="sm" onClick={() => setExpanded(true)}>
227          Show all {(partial ? paths.length : total).toLocaleString()} paths
228        </Button>
229      )}
230      {partial && (
231        <p className="caption muted">
232          Showing {paths.length.toLocaleString()} of {total.toLocaleString()} paths.
233        </p>
234      )}
235    </div>
236  );
237}
238export function packetStatus(id: string, state: ProjectState): { label: string; tone: Tone } {
239  const race = state.races?.find((entry) => entry.packet === id);
240  if (race?.status === "finished") return { label: "Promoted", tone: "success" };
241  if (race?.status === "open" && race.entrants.length > 0) return { label: "Racing", tone: "brand" };
242
243  if (state.blockers.some((b) => b.pushingPacket === id && !b.resolved))
244    return { label: "Blocked", tone: "warning" };
245  const reviews = state.reviews.filter((r) => r.packet === id);
246  if (reviews.some((r) => r.status === "promoted")) return { label: "Promoted", tone: "success" };
247  if (reviews.some((r) => r.status === "approved" && reviewState(r, state).kind === "current"))
248    return { label: "Approved", tone: "success" };
249  const outdated = reviews.map((r) => reviewState(r, state)).find((cause) => cause.banner);
250  if (outdated) return { label: outdated.label, tone: "warning" };
251  if (reviews.some((r) => r.status === "pending")) return { label: "In review", tone: "info" };
252  if (state.claims.some((c) => c.packet === id && c.active))
253    return { label: "In progress", tone: "brand" };
254  return { label: "Ready", tone: "info" };
255}
256function DataTable({
257  headings,
258  children,
259  kind,
260  labelledBy,
261}: {
262  headings: string[];
263  children: ReactNode;
264  kind: string;
265  labelledBy: string;
266}) {
267  return (
268    <div className={`table-scroll table-${kind}`}>
269      <Table aria-labelledby={labelledBy}>
270        <Table.Header>
271          <Table.Row>
272            {headings.map((text) => (
273              <Table.Head
274                key={text}
275                className={`column-${text.toLowerCase().replaceAll(" ", "-")}`}
276              >
277                {text}
278              </Table.Head>
279            ))}
280          </Table.Row>
281        </Table.Header>
282        <Table.Body>{children}</Table.Body>
283      </Table>
284    </div>
285  );
286}
287function Notice({ message, sha, review, openReview }: {
288  message?: string; sha?: string; review?: string; openReview?(id: string): void;
289}) {
290  const toasts = useKumoToastManager();
291  const previous = useRef<string | undefined>(undefined);
292  useEffect(() => {
293    if (message && message !== previous.current)
294      toasts.add({
295        title: sha ? (
296          <>
297            Approved <code>{sha}</code>
298          </>
299        ) : (
300          message
301        ),
302        variant: "success",
303        ...(review && openReview ? { timeout: 10000, actions: [{ children: "Open review", onClick: () => openReview(review) }] } : {}),
304      });
305    previous.current = message;
306  }, [message, sha, review, openReview, toasts]);
307  return null;
308}
309export function ReviewActions({
310  review,
311  view,
312  actions,
313  openReceipt,
314  advice,
315}: {
316  review: Review;
317  view: ViewModel;
318  actions: ViewActions;
319  openReceipt(): void;
320  advice?: { judge: string; pick: string; at: string };
321}) {
322  const [confirmation, setConfirmation] = useState<{
323    sha: string;
324    base?: string;
325    digest: string | null;
326    branch: string;
327    adviceAt?: string;
328  }>();
329  const [promoting, setPromoting] = useState(false);
330  const [failure, setFailure] = useState(false);
331  const required = view.state!.project.settings?.requireReceipt === true;
332  const matches =
333    !!confirmation &&
334    review.sha === confirmation.sha &&
335    review.base === confirmation.base &&
336    review.receiptDigest === confirmation.digest &&
337    view.state!.project.defaultBranch === confirmation.branch &&
338    (!advice || confirmation.adviceAt === advice.at);
339  const gate = reviewGate(review, view.state!, actions.owner);
340  const race = view.state!.races?.find((entry) => entry.packet === review.packet && entry.status === "open");
341  const advised = race && advice ? judgePromotion({ ...view.state!, recentActivity: view.activity }, race) : undefined;
342  const canPromote = promotionAvailable(review, view.state!, actions.owner) &&
343    (!advice || (advised?.available && advised.review?.id === review.id && race?.verdict?.at === advice.at));
344  const otherEntrants = race?.entrants.filter((entrant) => entrant.repoName !== review.repoName && !entrant.closedAt).map((entrant) => entrant.agent) ?? [];
345  const rechecking =
346    required && review.receipt?.status === "running" && review.receipt.trigger === "before-promote";
347  const usageCaption = usagePromotionCaption(review, view.state!);
348  if (!actions.owner) return usageCaption ? <p className="caption muted usage-promotion-caption">{usageCaption}</p> : null;
349  return (
350    <>
351      {!advice && usageCaption && <p className="caption muted usage-promotion-caption">{usageCaption}</p>}
352      {!advice && gate.approve && (
353        <WriteHint reason={staleReason(view)}><Button
354          size="sm"
355          variant="secondary"
356          data-action="approve"
357          data-review={review.id}
358          disabled={view.busy || view.stale}
359          onClick={() => { if (!view.busy && !view.stale) void actions.action("approve", review.id); }}
360        >
361          Approve
362        </Button></WriteHint>
363      )}
364      {canPromote && !promoting && !rechecking && (
365        <WriteHint reason={staleReason(view)}><Button
366          size="sm"
367          variant="secondary"
368          data-action={advice ? "promote-judge-pick" : "promote"}
369          aria-label={advice ? `Promote ${advice.pick}, the judge's pick` : undefined}
370          data-review={review.id}
371          disabled={view.busy || view.stale}
372          onClick={() => {
373            if (view.busy || view.stale) return;
374            setFailure(false);
375            setConfirmation({
376              sha: review.sha,
377              base: review.base,
378              digest: review.receiptDigest,
379              branch: view.state!.project.defaultBranch,
380              adviceAt: advice?.at,
381            });
382          }}
383        >
384          <ArrowLineUp size={16} />
385          {advice ? "Promote judge's pick" : "Promote"}
386        </Button></WriteHint>
387      )}
388      <Dialog.Root
389        open={!!confirmation}
390        onOpenChange={(open) => {
391          if (!open && !promoting) setConfirmation(undefined);
392        }}
393      >
394        <Dialog className="p-6 promotion-dialog">
395          <Dialog.Title>Promote to {confirmation?.branch}</Dialog.Title>
396          <Dialog.Description>
397            {confirmation?.branch} moves{" "}
398            {confirmation?.base ? <>{short(confirmation.base)} → </> : null}
399            {confirmation ? short(confirmation.sha) : null}. Promote {review.packet} from{" "}
400            {review.repoName}.
401          </Dialog.Description>
402          {confirmation?.base && (
403            <div>
404              <p className="caption muted">Base</p>
405              <ClipboardText text={confirmation.base} size="sm" />
406            </div>
407          )}
408          <div>
409            <p className="caption muted">Head</p>
410            <ClipboardText text={confirmation?.sha ?? review.sha} size="sm" />
411          </div>
412          {confirmation?.digest && (
413            <div>
414              <p className="caption muted">Receipt</p>
415              <ClipboardText text={confirmation.digest} size="sm" />
416            </div>
417          )}
418          {required && !failure && (
419            <p className="caption muted">
420              Checks run again on {short(confirmation?.sha ?? review.sha)} before{" "}
421              {confirmation?.branch} moves.
422            </p>
423          )}
424          {!required && review.receipt?.status === "failed" && (
425            <p className="caption muted">
426              The latest checks failed; receipts are advisory in this project.
427            </p>
428          )}
429          {failure && (
430            <div role="alert" className="receipt-failure">
431              <p className="receipt-error-line">
432                <Warning size={20} aria-hidden="true" />{" "}
433                <span>Checks failed on the re-run. Nothing was promoted.</span>
434              </p>
435              <Button
436                variant="ghost"
437                size="sm"
438                className="receipt-link"
439                onClick={() => {
440                  setConfirmation(undefined);
441                  openReceipt();
442                }}
443              >
444                View receipt
445              </Button>
446            </div>
447          )}
448          {!promoting && !failure && (!matches || !canPromote) && (
449            <Banner
450              variant="alert"
451              size="sm"
452              description="Review changed. Close and review again."
453            />
454          )}
455          {advice && <p className="caption judge-promote-caption">{advice.judge} picked {advice.pick}. This is advice; promoting is your call.</p>}
456          {race && <p className="caption race-promote-warning">Promoting this finishes the race.{otherEntrants.length > 0 && <> {otherEntrants.join(" and ")} will be closed and placed.</>}</p>}
457          <div className="dialog-actions">
458            <Button disabled={promoting} onClick={() => setConfirmation(undefined)}>
459              Cancel
460            </Button>
461            <WriteHint reason={staleReason(view)}><Button
462              variant="primary"
463              data-action="confirm-promote"
464              disabled={!matches || !canPromote || view.busy || view.stale || promoting || failure}
465              aria-busy={promoting}
466              onClick={async () => {
467                if (!matches || !canPromote || promoting || view.busy || view.stale) return;
468                setPromoting(true);
469                try {
470                  if (await actions.action("promote", review.id)) setConfirmation(undefined);
471                } catch {
472                  // Only the transport's exact required-receipt 409 reaches this branch.
473                  if (required) setFailure(true);
474                } finally {
475                  setPromoting(false);
476                }
477              }}
478            >
479              {promoting ? (
480                required ? (
481                  "Re-checking…"
482                ) : (
483                  "Promoting…"
484                )
485              ) : (
486                <>Promote to {confirmation?.branch}</>
487              )}
488            </Button></WriteHint>
489          </div>
490        </Dialog>
491      </Dialog.Root>
492    </>
493  );
494}
495export function DashboardView(props: { view: ViewModel; actions: ViewActions }) {
496  return (
497    <TooltipProvider>
498      <Toasty>
499        <DashboardContent {...props} />
500      </Toasty>
501    </TooltipProvider>
502  );
503}
504function DashboardContent({ view, actions }: { view: ViewModel; actions: ViewActions }) {
505  const [panel, setPanel] = useState(() => {
506    try {
507      const saved = window.sessionStorage?.getItem("dashboard-activity");
508      if (saved != null) return saved === "open";
509    } catch {
510      /* Opaque frames may deny storage. */
511    }
512    return typeof window !== "undefined" && window.innerWidth >= 1280;
513  });
514  const [narrow, setNarrow] = useState(
515    () => typeof window !== "undefined" && window.innerWidth < 1024,
516  );
517  useEffect(() => {
518    const update = () => setNarrow(window.innerWidth < 1024);
519    window.addEventListener("resize", update);
520    return () => window.removeEventListener("resize", update);
521  }, []);
522  const [showArchived, setShowArchived] = useState(false);
523  const [archiveAnnouncement, setArchiveAnnouncement] = useState("");
524  const [packetError, setPacketError] = useState<string>();
525  const collisionHeading = useRef<HTMLHeadingElement>(null);
526  const collisionOrigin = useRef<HTMLElement | null>(null);
527  const [highlightedCollision, setHighlightedCollision] = useState<string>();
528  const [agentRepo, setAgentRepo] = useState<string>();
529  useEffect(() => setAgentRepo(undefined), [view.project]);
530  const [collision, setCollision] = useState<import("./contracts.ts").BlockerView>();
531  useEffect(() => { setCollision(undefined); setHighlightedCollision(undefined); }, [view.project]);
532  const [packetId, setPacketId] = useState<string>();
533  useEffect(() => {
534    setShowArchived(false);
535    setArchiveAnnouncement("");
536    setPacketId(undefined);
537    setPacketError(undefined);
538  }, [view.project]);
539  const [reviewId, setReviewId] = useState<string>();
540  const [receiptPanel, setReceiptPanel] = useState(false);
541  const [asking, setAsking] = useState(false);
542  const [askError, setAskError] = useState<string>();
543  const toggleRef = useRef<HTMLButtonElement>(null);
544  const state = view.state;
545  const overviewEmpty = state ? overviewFirstRun(state) : undefined;
546  const blockers = state?.blockers.filter(
547    (b) => !b.resolved && !isArchived(state, b.packet) &&
548      !(b.pushingPacket && isArchived(state, b.pushingPacket)),
549  ) ?? [];
550  const packets = state ? workPackets(state, showArchived) : [];
551  const archivedCount = state?.packets.filter((p) => p.archived).length ?? 0;
552  const reviews = state ? reviewQueue(state) : [];
553  const reviewCount = reviews.filter(
554    (r) => !r.returned && (r.status === "pending" || r.status === "approved"),
555  ).length;
556  const history = state ? recentReceipts(state) : [];
557  const lastPromotion = state?.ledger.at(-1);
558  const lastReceipt = state && lastPromotion ? promotionReceipt(state, lastPromotion) : undefined;
559  const claims = state?.claims.filter((c) => c.active && !isArchived(state, c.packet)) ?? [];
560  const selected = state?.packets.find((p) => p.id === packetId);
561  const selectedReview = state?.reviews.find((r) => r.id === reviewId);
562  function toggle(value: boolean) {
563    setPanel(value);
564    if (!value && narrow) window.setTimeout(() => toggleRef.current?.focus(), 0);
565    try {
566      window.sessionStorage?.setItem("dashboard-activity", value ? "open" : "closed");
567    } catch {
568      /* Keep the in-frame preference. */
569    }
570  }
571  function showPacket(id: string, details = false) {
572    setPacketError(undefined);
573    const packet = state?.packets.find((entry) => entry.id === id);
574    if (!details && !packet?.archived && packet?.mode === "race") {
575      setPacketId(undefined);
576      setReviewId(undefined);
577      actions.openRace?.(id);
578      return;
579    }
580    setReceiptPanel(false);
581    if (details && actions.raceId) actions.openRace?.();
582    setPacketId(id);
583    setReviewId(undefined);
584    toggle(true);
585  }
586  function openCollision(blocker: import("./contracts.ts").BlockerView) {
587    collisionOrigin.current = document.activeElement === document.body ? null : document.activeElement as HTMLElement;
588    setHighlightedCollision(blocker.id);
589    showPacket(blocker.pushingPacket ?? blocker.packet);
590    setCollision(blocker);
591  }
592  function closeCollision(resolved = false) {
593    setCollision(undefined);
594    if (resolved) setHighlightedCollision(undefined);
595    window.setTimeout(() => {
596      if (resolved) collisionHeading.current?.focus();
597      else if (collisionOrigin.current?.isConnected) collisionOrigin.current.focus();
598      else collisionHeading.current?.focus();
599    }, 0);
600  }
601  function showReceipt(id: string) {
602    setReceiptPanel(true);
603    setReviewId(id);
604    setPacketId(undefined);
605    toggle(true);
606  }
607  function openNeed(target: OwnerNeed["target"]) {
608    if ("review" in target) {
609      if (actions.getReview) actions.openReview(target.review);
610      else actions.setSection("review");
611    } else if ("packet" in target) {
612      actions.setSection("work");
613      setHighlightedCollision(target.collision);
614      showPacket(target.packet);
615    } else if ("repo" in target) {
616      setAgentRepo(target.repo);
617      actions.setSection("agents");
618      requestAnimationFrame(() => {
619        const row = [...document.querySelectorAll<HTMLElement>("[data-agent-repo]")]
620          .find((entry) => entry.dataset.agentRepo === target.repo);
621        (row ?? document.getElementById("agents-heading"))?.focus();
622        row?.scrollIntoView({ block: "nearest" });
623      });
624    } else {
625      actions.setSection("overview");
626      requestAnimationFrame(() => document.getElementById("last-promotion")?.focus());
627    }
628  }
629  function back() {
630    setReceiptPanel(false);
631    setPacketId(undefined);
632    setReviewId(undefined);
633  }
634  const ask = actions.ask && (
635    <Button
636      variant="secondary"
637      data-action="ask"
638      disabled={asking}
639      onClick={async () => {
640        setAsking(true);
641        setAskError(undefined);
642        try {
643          await actions.ask!();
644        } catch {
645          setAskError("Unable to open the agent prompt. Try again.");
646        } finally {
647          setAsking(false);
648        }
649      }}
650    >
651      Ask an agent
652    </Button>
653  );
654  const section = (id: Section, content: ReactNode) => (
655    <div
656      hidden={actions.section !== id}
657      id={`view-${id}`}
658      className="view-stack"
659      role="tabpanel"
660      aria-label={{ overview: "Overview", work: "Work", review: "Review", agents: "Agents", races: "Races" }[id]}
661    >
662      {content}
663    </div>
664  );
665  const panelContent = (
666    <>
667      <div className="section-heading panel-heading">
668        {selected || selectedReview ? (
669          <Button variant="ghost" size="sm" onClick={back}>
670            <ArrowLeft size={16} />
671            Back to activity
672          </Button>
673        ) : (
674          <h2>Activity</h2>
675        )}
676        <Tooltip
677          content="Close activity"
678          render={
679            <Button
680              variant="ghost"
681              size="sm"
682              shape="square"
683              aria-label="Close activity"
684              onClick={() => toggle(false)}
685            />
686          }
687        >
688          <X size={16} />
689        </Tooltip>
690      </div>
691      {selectedReview && receiptPanel ? (
692        <ReceiptDetails
693          key={`${view.project}:${selectedReview.id}`}
694          review={selectedReview}
695          project={view.project}
696          owner={actions.owner}
697          load={actions.getReceipt}
698        />
699      ) : selected || selectedReview ? (
700        <>
701          <div className="section-heading archive-heading">
702            <h3>
703              {selected?.title ?? selectedReview?.packet}{" "}
704              {selected?.archived && <ArchivedBadge at={selected.archived.at} />}
705            </h3>
706            {selected && state && (
707              <ArchiveControl
708                key={`${view.project}:${selected.id}`}
709                packet={selected}
710                state={state}
711                owner={actions.owner}
712                busy={view.busy || !!view.stale}
713                disabledReason={staleReason(view)}
714                archive={actions.archivePacket}
715                unarchive={actions.unarchivePacket}
716                missing={(message) => {
717                  setPacketId(undefined);
718                  setPacketError(message);
719                }}
720              />
721            )}
722          </div>
723          {selected && (
724            <>
725              <p className="caption muted">{selected.id}</p>
726              {selected.archived && (
727                <p className="caption muted">
728                  Archived · {relative(selected.archived.at)} by {actions.owner ? "you" : "the owner"}
729                </p>
730              )}
731              <p>{selected.goal}</p>
732            </>
733          )}
734          {selectedReview && (
735            <>
736              <p>{selectedReview.agent}</p>
737              <p className="caption">
738                <code>{selectedReview.repoName}</code>
739              </p>
740              <ClipboardText text={selectedReview.sha} size="sm" />
741              {selectedReview.receiptDigest && (
742                <ClipboardText text={selectedReview.receiptDigest} size="sm" />
743              )}
744            </>
745          )}
746          <h3>Paths</h3>
747          {(() => {
748            const event =
749              selectedReview &&
750              view.activity.findLast(
751                (e) => e.repoName === selectedReview.repoName && e.after === selectedReview.sha,
752              );
753            return (
754              <>
755                {event && (
756                  <div className="caption">
757                    <Paths paths={event.changedPaths} event={event} />
758                  </div>
759                )}
760                <PathDetails
761                  key={selectedReview?.id ?? selected!.id}
762                  paths={
763                    event
764                      ? event.changedPaths
765                      : (selected?.paths ??
766                        state?.packets.find((p) => p.id === selectedReview?.packet)?.paths ??
767                        [])
768                  }
769                  total={event ? event.changedPathCount : undefined}
770                  partial={event ? event.changedPathsTruncated : false}
771                />
772              </>
773            );
774          })()}
775          {selected && state && (() => {
776            const collisions = state.blockers.filter((b) => b.packet === selected.id || b.pushingPacket === selected.id);
777            return collisions.length > 0 && <section aria-label="Collisions">
778              <h3 ref={collisionHeading} tabIndex={-1} className="collision-heading">Collisions</h3>
779              <CollisionRows blockers={collisions} owner={actions.owner} highlighted={highlightedCollision}
780                reviewLink={actions.getReview ? (b) => {
781                  const review = state.reviews.find((r) => blockerGatesReview(b, r) &&
782                    reviewState(r, state).kind === "current" && r.status !== "stale" &&
783                    state.forks.some((f) => f.repoName === r.repoName && f.head === r.sha));
784                  return review && <button className="claim-link caption" onClick={() => actions.openReview(review.id)}>Open review</button>;
785                } : undefined}
786                open={actions.resolveBlocker ? openCollision : undefined} disabled={view.busy || view.stale} disabledReason={staleReason(view)} />
787            </section>;
788          })()}
789          {selected && (
790            <>
791              <h3>Acceptance checks</h3>
792              <ul>
793                {selected.acceptance.map((check) => (
794                  <li key={check}>{check}</li>
795                ))}
796              </ul>
797            </>
798          )}
799        </>
800      ) : (
801        <section id="activity">
802          {view.activity.toReversed().map((entry) => (
803            <article key={entry.sequence} className="activity">
804              <p>
805                <strong>{entry.agent ?? <code>{entry.repoName}</code>}</strong> ·{" "}
806                {short(entry.after)}
807              </p>
808              <div className="caption">
809                <Paths paths={entry.changedPaths} event={entry} />
810              </div>
811              {entry.ref !== `refs/heads/${state?.project.defaultBranch}` && (
812                <p className="caption">
813                  <code>{entry.ref.replace(/^refs\/heads\//, "")}</code>
814                </p>
815              )}
816              <time className="caption muted" dateTime={entry.recordedAt}>
817                {relative(entry.recordedAt)}
818              </time>
819            </article>
820          ))}
821          {!view.activity.length && state && !view.busy && !view.error && (
822            <p className="caption muted">No pushes yet.</p>
823          )}
824          {view.activity.length === 100 && (
825            <p className="caption muted">Showing the latest 100 events.</p>
826          )}
827        </section>
828      )}
829    </>
830  );
831  return (
832    <>
833      {actions.owner && actions.resolveBlocker && collision &&
834        <ResolveCollisionDialog key={`${view.project}:${collision.id}`} blocker={collision}
835          resolve={actions.resolveBlocker} close={closeCollision} disabled={view.busy || view.stale} disabledReason={staleReason(view)} />}
836      <Notice message={view.notice} sha={view.noticeSha} review={actions.getReview ? view.noticeReview : undefined} openReview={actions.openReview} />
837      <ReceiptAnnouncements project={view.project} reviews={state?.reviews ?? []} />
838      <div className={`app-shell ${panel && !actions.raceId ? "panel-open" : ""} ${actions.raceId ? "race-shell" : ""}`}>
839        <main className="main-pane">
840          <div className="main-container">
841            <header className="page-header">
842              <div>
843                <h1 className="page-title">{view.project || view.brand.name}</h1>
844                <p className="page-description">
845                  Agents work in their own forks. {actions.owner ? "You review and promote." : "The owner reviews and promotes."}
846                </p>
847              </div>
848              <div className="actions">
849                {actions.owner && view.projects.length > 0 && (
850                  <Select
851                    size="sm"
852                    aria-label="Project"
853                    value={view.project}
854                    disabled={view.busy}
855                    items={view.projects.map((p) => ({ value: p.id, label: p.label }))}
856                    onValueChange={(value) => {
857                      if (value) void actions.selectProject(value);
858                    }}
859                  />
860                )}
861                {ask}
862                {!actions.owner && actions.framePresentation && <ExpandControl presentation={actions.framePresentation} />}
863                {!actions.raceId && (
864                <Tooltip
865                  content={panel ? "Hide activity" : "Show activity"}
866                  render={
867                    <Button
868                      ref={toggleRef}
869                      variant="ghost"
870                      shape="square"
871                      data-action="toggle-activity"
872                      aria-label={panel ? "Hide activity" : "Show activity"}
873                      aria-expanded={panel}
874                      aria-controls="activity-panel"
875                      onClick={() => toggle(!panel)}
876                    />
877                  }
878                >
879                  <Pulse size={16} weight="bold" />
880                </Tooltip>
881                )}
882              </div>
883            </header>
884            <div className="tab-gutter">
885              <Tabs
886                variant="underline"
887                value={actions.section}
888                onValueChange={(value) => actions.setSection(value as Section)}
889                tabs={[
890                  { className: "text-[13px]", value: "overview", label: "Overview" },
891                  {
892                    className: "text-[13px]",
893                    value: "work",
894                    label: (
895                      <span className="tab-label">
896                        Work
897                        {blockers.length + claims.length > 0 && (
898                          <><span className="sr-only">, </span><Badge variant="secondary">{blockers.length + claims.length}</Badge></>
899                        )}
900                      </span>
901                    ),
902                  },
903                  {
904                    className: "text-[13px]",
905                    value: "review",
906                    label: (
907                      <span className="tab-label">
908                        Review
909                        {reviewCount > 0 && <><span className="sr-only">, </span><Badge variant="secondary">{reviewCount}</Badge></>}
910                      </span>
911                    ),
912                  },
913                  { className: "text-[13px]", value: "agents", label: "Agents" },
914                  { className: "text-[13px]", value: "races", label: <span className="tab-label">
915                    Races
916                    {openRaceCount(state?.races ?? []) > 0 && <><span className="sr-only">, </span>
917                      <Badge variant="secondary">{openRaceCount(state?.races ?? [])}</Badge></>}
918                  </span> },
919                ]}
920              />
921              <FreshnessStamp view={view} retry={actions.retry} />
922            </div>
923            <div className="content">
924              {view.livePaused && (
925                <Banner
926                  variant="alert"
927                  size="sm"
928                  className="live-paused"
929                  icon={<Warning size={16} />}
930                  role="status"
931                  description={`Live updates paused. Last updated ${snapshotAge(view)}.`}
932                  action={
933                    actions.reconnect && (
934                      <Button
935                        variant="secondary"
936                        size="sm"
937                        onClick={() => void actions.reconnect!()}
938                      >
939                        Reconnect
940                      </Button>
941                    )
942                  }
943                />
944              )}
945              {!actions.owner && (
946                <Banner
947                  variant="secondary"
948                  size="sm"
949                  className="agent-notice"
950                  description={`You're connected as ${actions.agent ? `agent ${actions.agent}` : "an agent"}. Promote and approve stay with the owner; your pushes need approval on your fork.`}
951                  action={actions.ownerLink && <CopyOwnerLink key={actions.ownerLink} url={actions.ownerLink} />}
952                />
953              )}
954              {view.fixture && (
955                <Banner
956                  variant="secondary"
957                  size="sm"
958                  className="fixture-banner"
959                  title="FIXTURE · OFFLINE"
960                  description="Local synthetic snapshots only. No account connection."
961                />
962              )}
963              {(((view.refreshing || view.busy) && !state) || askError) && (
964                <div role="status" aria-live="polite">
965                  {(view.refreshing || view.busy) && !state && !actions.raceId && <p>Loading the latest project snapshot…</p>}
966                  {askError && <p>{askError}</p>}
967                </div>
968              )}
969              {packetError && <Banner variant="error" role="alert" description={packetError} />}
970              {view.error && !view.stale && !actions.raceId && (
971                <Banner
972                  variant="error"
973                  className="text-kumo-default"
974                  icon={<Warning size={20} />}
975                  role="alert"
976                  description={view.error}
977                  action={
978                    actions.retry && (
979                      <Button variant="secondary" size="sm" onClick={() => void actions.retry!()}>
980                        Try again
981                      </Button>
982                    )
983                  }
984                />
985              )}
986              {actions.raceId ? <RaceView key={`${view.project}:${actions.raceId}`} packet={actions.raceId} view={view} actions={{ ...actions, showPacket: (id) => showPacket(id, true), openReceipt: (id) => { actions.openRace?.(); showReceipt(id); } }} /> : !state ? (
987                !view.refreshing &&
988                !view.busy &&
989                !view.error &&
990                (!view.project && !view.projects.length
991                  ? <FirstRunEmpty kind="projects" owner={actions.owner} />
992                  : empty(view.project ? "No snapshot loaded." : "Select a configured project."))
993              ) : (
994                <>
995                  {state.promotionPending && (
996                    <Banner variant="error" className="text-kumo-default" icon={<Warning size={20} />}
997                      title="Canonical reconciliation alert"
998                      description={<>Promotion outcome pending owner inspection. {short(state.promotionPending.after)}</>} />
999                  )}
1000                  {state.reconcilerAlerts.map((alert, index) => (
1001                    <Banner
1002                      key={index}
1003                      variant="error"
1004                      className="text-kumo-default"
1005                      icon={<Warning size={20} />}
1006                      title="Canonical reconciliation alert"
1007                      description={
1008                        <>
1009                          {alert.message} {short(alert.sha)}
1010                        </>
1011                      }
1012                    />
1013                  ))}
1014                  {section(
1015                    "overview",
1016                    <>
1017                      {!actions.owner && <ResumeCard key={`resume:${view.project}`} state={state} activity={view.activity} agent={actions.agent} />}
1018                      {overviewEmpty ? (
1019                        <section aria-label="Forks">
1020                          <FirstRunEmpty kind={overviewEmpty} owner={actions.owner} />
1021                        </section>
1022                      ) : <ForkGraph brand={view.brand} key={`forks:${view.project}`} state={state} owner={actions.owner}
1023                        openReview={actions.openReview} openPacket={showPacket} openReceipt={showReceipt} />}
1024                      {actions.owner && <NeedsYou key={`needs:${view.project}`} state={state} brand={view.brand} open={openNeed} />}
1025                      <LayerCard className="p-4 project-card">
1026                        <div className="section-heading">
1027                          <h2>
1028                            <code>{state.project.repoName}</code>
1029                          </h2>
1030                          <Events value={state.project.events} />
1031                        </div>
1032                        <p>
1033                          Default branch <code>{state.project.defaultBranch}</code>
1034                        </p>
1035                        {state.canonicalHead && <p>Canonical head: {short(state.canonicalHead)}</p>}
1036                        {state.project.remote && (
1037                          <ClipboardText text={state.project.remote} size="sm" />
1038                        )}
1039                        <p className="muted" id="last-promotion" tabIndex={-1}>
1040                          {state.ledger.length ? (
1041                            <>
1042                              Last promotion:{" "}
1043                              <code className="last-promotion-sha" title={lastPromotion!.sha}>
1044                                {lastPromotion!.sha.slice(0, 7)}
1045                              </code>{" "}
1046                              · {relative(lastPromotion!.promotedAt)}
1047                              {lastReceipt?.receipt && (
1048                                <>
1049                                  {" · "}
1050                                  <Button
1051                                    variant="ghost"
1052                                    size="sm"
1053                                    className="promotion-receipt"
1054                                    aria-label={`View receipt for ${lastReceipt.packet}`}
1055                                    onClick={() => showReceipt(lastReceipt.id)}
1056                                  >
1057                                    <ReceiptBadge status={lastReceipt.receipt.status} />
1058                                  </Button>
1059                                </>
1060                              )}
1061                            </>
1062                          ) : (
1063                            "No promotions yet"
1064                          )}
1065                        </p>
1066                        <h3>Settings</h3>
1067                        {state.project.settings ? (
1068                          actions.owner && actions.setSettings ? (
1069                            <>
1070                              <WriteHint reason={staleReason(view)}><Switch
1071                                label="Require passing receipts"
1072                                checked={state.project.settings.requireReceipt}
1073                                disabled={view.busy || view.stale}
1074                                onCheckedChange={(checked) => { if (!view.busy && !view.stale) void actions.setSettings!(checked); }}
1075                              /></WriteHint>
1076                              <p className="caption muted">Approve waits for a passing receipt.</p>
1077                            </>
1078                          ) : (
1079                            <p>
1080                              Require passing receipts:{" "}
1081                              {state.project.settings.requireReceipt ? "On" : "Off"}
1082                            </p>
1083                          )
1084                        ) : (
1085                          <p className="caption muted">Receipt settings unavailable.</p>
1086                        )}
1087                      </LayerCard>
1088                      <section className="metrics" aria-label="Project summary">
1089                        {[
1090                          ["Open packets", workPackets(state).length],
1091                          ["Active claims", claims.length],
1092                          ["In review", reviewCount],
1093                          ["Blockers", blockers.length],
1094                        ].map(([label, count]) => (
1095                          <LayerCard key={label} className="p-4">
1096                            <span className="caption">{label}</span>
1097                            <p className="metric-value">
1098                              {count}
1099                              {label === "Blockers" && blockers.length > 0 && (
1100                                <><span className="warning-dot" aria-hidden="true" /><span className="sr-only"> open blockers</span></>
1101                              )}
1102                            </p>
1103                            {label === "In review" &&
1104                              reviews.some((r) => r.receipt?.status === "running") && (
1105                                <p className="caption muted">
1106                                  {reviews.filter((r) => r.receipt?.status === "running").length}{" "}
1107                                  checking
1108                                </p>
1109                              )}
1110                          </LayerCard>
1111                        ))}
1112                      </section>
1113                    </>,
1114                  )}
1115                  {section(
1116                    "work",
1117                    <>
1118                      {!!blockers.length && (
1119                        <section id="blockers" className="blockers">
1120                          {blockers.map((b) => (
1121                            <CollisionBanner key={b.id} blocker={b} owner={actions.owner}
1122                              open={actions.resolveBlocker ? openCollision : undefined}
1123                              showPacket={showPacket} disabled={view.busy || view.stale} disabledReason={staleReason(view)} />
1124                          ))}
1125                        </section>
1126                      )}
1127                      <section id="packets">
1128                        <h2 id="packets-heading">Packets and claims</h2>
1129                        {packets.length ? (
1130                          <DataTable
1131                            kind="work"
1132                            labelledBy="packets-heading"
1133                            headings={["Packet", "State", "Claimed by", "Fork", "Paths", "Claimed"]}
1134                          >
1135                            {packets.map((p) => {
1136                              const live = claims.filter((c) => c.packet === p.id);
1137                              const race = state.races?.find((entry) => entry.packet === p.id);
1138                              return (
1139                                <Table.Row
1140                                  key={p.id}
1141                                  data-packet-id={p.id}
1142                                  className={selected?.id === p.id ? "selected-row" : ""}
1143                                  onClick={(event) => {
1144                                    if (!(event.target as HTMLElement).closest("button, a")) showPacket(p.id);
1145                                  }}
1146                                >
1147                                  <Table.Cell>
1148                                    <span className="packet-label">
1149                                    <button
1150                                      className="packet-link"
1151                                      title={p.title}
1152                                      onClick={() => showPacket(p.id)}
1153                                    >
1154                                      {p.title}
1155                                    </button>{p.mode === "race" && <Badge variant="secondary" className="packet-race-badge">Race</Badge>}
1156                                    </span>
1157                                    <div className="caption muted">{p.id}</div>
1158                                  </Table.Cell>
1159                                  <Table.Cell>
1160                                    {p.archived ? <ArchivedBadge at={p.archived.at} /> : <Status {...packetStatus(p.id, state)} />}
1161                                    {!p.archived && race?.status === "finished" && race.winner && <div className="caption muted">Won by {race.winner}</div>}
1162                                    <div className="caption muted folded">
1163                                      {live
1164                                        .filter((c) => c.createdAt)
1165                                        .map((c) => (
1166                                          <span key={c.agent}>
1167                                            Claimed {relative(c.createdAt!)}
1168                                          </span>
1169                                        ))}
1170                                    </div>
1171                                  </Table.Cell>
1172                                  <Table.Cell>
1173                                    <Tooltip
1174                                      content={p.mode === "race" ? race?.entrants.map((entrant) => entrant.agent).join(", ") || "No entrants yet" : live.map((c) => c.agent).join(", ") || "Unclaimed"}
1175                                      render={<span className="truncated-name" />}
1176                                    >
1177                                      {p.mode === "race" ? (race?.entrants.length ? `${race.entrants.length} ${race.entrants.length === 1 ? "entrant" : "entrants"}` : "No entrants yet") : live.map((c) => c.agent).join(", ") || "Unclaimed"}
1178                                    </Tooltip>
1179                                    <div className="caption muted folded">
1180                                      {live.map((c) => (
1181                                        <TruncatedCode key={c.repoName} value={c.repoName} />
1182                                      ))}
1183                                    </div>
1184                                  </Table.Cell>
1185                                  <Table.Cell className="column-fork">
1186                                    {live.map((c) => (
1187                                      <TruncatedCode key={c.repoName} value={c.repoName} />
1188                                    ))}
1189                                  </Table.Cell>
1190                                  <Table.Cell className="work-paths">
1191                                    <Paths paths={p.paths} />
1192                                  </Table.Cell>
1193                                  <Table.Cell className="column-claimed">
1194                                    {live
1195                                      .filter((c) => c.createdAt)
1196                                      .map((c) => (
1197                                        <span className="caption muted" key={c.agent}>
1198                                          {relative(c.createdAt!)}
1199                                        </span>
1200                                      ))}
1201                                  </Table.Cell>
1202                                </Table.Row>
1203                              );
1204                            })}
1205                          </DataTable>
1206                        ) : (
1207                          archivedCount ? empty("No active packets.")
1208                            : <FirstRunEmpty kind="packets" owner={actions.owner} />
1209                        )}
1210                        {archivedCount > 0 && (
1211                          <Button
1212                            variant="ghost"
1213                            size="sm"
1214                            data-action="toggle-archived"
1215                            aria-pressed={showArchived}
1216                            onClick={() => {
1217                              setShowArchived(!showArchived);
1218                              setArchiveAnnouncement(showArchived
1219                                ? "Archived packets hidden" : `${archivedCount} archived packets shown`);
1220                            }}
1221                          >
1222                            {showArchived ? "Hide archived" : `Show archived (${archivedCount})`}
1223                          </Button>
1224                        )}
1225                        <span className="sr-only" role="status" aria-live="polite">
1226                          {archiveAnnouncement}
1227                        </span>
1228                      </section>
1229                    </>,
1230                  )}
1231                  {section(
1232                    "review",
1233                    actions.reviewId && actions.getReview ? <ReviewView key={`${view.project}:${actions.reviewId}`} id={actions.reviewId} view={view} actions={{ ...actions, openCollision: actions.resolveBlocker ? openCollision : undefined, showPacket }} openReceipt={showReceipt} /> : <>
1234                      <section id="reviews">
1235                        <h2 id="reviews-heading">Review queue</h2>
1236                        {reviews.length ? (
1237                          <DataTable
1238                            kind="review"
1239                            labelledBy="reviews-heading"
1240                            headings={[
1241                              "Packet",
1242                              "Agent",
1243                              "Fork head",
1244                              "State",
1245                              "Receipts",
1246                              ...(reviews.some((queued) => usageValues(queued.usageAtReview).length) ? ["Tokens"] : []),
1247                              "Requested",
1248                              ...(actions.owner ? ["Actions"] : []),
1249                            ]}
1250                          >
1251                            {reviews.map((r) => {
1252                              const gate = reviewGate(r, state, actions.owner);
1253                              const receiptCaption = receiptGateCaption(r, state.project.settings?.requireReceipt === true);
1254                              // Usage refusals replace Promote in Actions, never repeat them in State.
1255                              const hasStateCaption = receiptCaption || (!gate.approve && !gate.promote && gate.reason);
1256                              const stateCaption = hasStateCaption
1257                                ? (gate.reason === usagePromotionCaption(r, state) ? receiptCaption : gate.reason || receiptCaption)
1258                                : "";
1259                              const cause = reviewState(r, state);
1260                              const stale = !!cause.banner;
1261                              return (
1262                                <Table.Row key={r.id} className="review-card" onClick={actions.getReview ? (event) => {
1263                                  if (!(event.target as HTMLElement).closest("button, a")) actions.openReview(r.id);
1264                                } : undefined}>
1265                                  <Table.Cell>
1266                                    <span className="packet-label">
1267                                    {actions.getReview ? <button
1268                                      className="claim-link"
1269                                      onClick={() => {
1270                                        actions.openReview(r.id);
1271                                      }}
1272                                    >
1273                                      {r.packet}
1274                                    </button> : <span>{r.packet}</span>}{state.packets.find((p) => p.id === r.packet)?.mode === "race" && <Badge variant="secondary" className="packet-race-badge">Race</Badge>}
1275                                    </span>
1276                                  </Table.Cell>
1277                                  <Table.Cell>
1278                                    <Tooltip
1279                                      content={r.agent}
1280                                      render={<span className="truncated-name" />}
1281                                    >
1282                                      {r.agent}
1283                                    </Tooltip>
1284                                    <div className="caption muted">
1285                                      <TruncatedCode value={r.repoName} />
1286                                    </div>
1287                                  </Table.Cell>
1288                                  <Table.Cell>{short(r.sha)}</Table.Cell>
1289                                  <Table.Cell>
1290                                    <Status
1291                                      label={cause.label}
1292                                      tone={
1293                                        cause.kind === "returned" ? "danger" : stale ? "warning"
1294                                          : r.status === "approved" ? "success" : "info"
1295                                      }
1296                                    />
1297                                    <div className="caption muted folded">
1298                                      <ReceiptCell
1299                                        receipt={r.receipt}
1300                                        packet={r.packet}
1301                                        sha={r.sha}
1302                                        required={state.project.settings?.requireReceipt === true}
1303                                        open={() => showReceipt(r.id)}
1304                                      />
1305                                      {r.requestedAt && (
1306                                        <div>Requested {relative(r.requestedAt)}</div>
1307                                      )}
1308                                    </div>
1309                                    {r.returned && <p className="caption muted returned-preview">{r.returned.reason.split(/\r?\n/)[0]}</p>}
1310                                    {stale && (
1311                                      <div className="caption muted state-reason">
1312                                        {cause.caption}
1313                                      </div>
1314                                    )}
1315                                    {!stale && !r.returned &&
1316                                      stateCaption && (
1317                                        <div className="caption muted state-reason">
1318                                          {stateCaption}
1319                                          {actions.owner &&
1320                                            r.receipt?.status === "infrastructure-error" && (
1321                                              <Button
1322                                                variant="ghost"
1323                                                size="sm"
1324                                                className="receipt-link"
1325                                                onClick={() => showReceipt(r.id)}
1326                                              >
1327                                                View receipt
1328                                              </Button>
1329                                            )}
1330                                        </div>
1331                                      )}
1332                                  </Table.Cell>
1333                                  <Table.Cell className="column-receipts">
1334                                    <ReceiptCell
1335                                      receipt={r.receipt}
1336                                      packet={r.packet}
1337                                      sha={r.sha}
1338                                      required={state.project.settings?.requireReceipt === true}
1339                                      open={() => showReceipt(r.id)}
1340                                    />
1341                                  </Table.Cell>
1342                                  {reviews.some((queued) => usageValues(queued.usageAtReview).length) && <Table.Cell className="column-tokens"><UsageFigures usage={r.usageAtReview} agent={r.agent} brand={view.brand} compact /></Table.Cell>}
1343                                  <Table.Cell className="column-requested">
1344                                    {r.requestedAt && (
1345                                      <span className="caption muted">
1346                                        {relative(r.requestedAt)}
1347                                      </span>
1348                                    )}
1349                                  </Table.Cell>
1350                                  {actions.owner && (
1351                                    <Table.Cell>
1352                                      <ReviewActions
1353                                        review={r}
1354                                        view={view}
1355                                        actions={actions}
1356                                        openReceipt={() => showReceipt(r.id)}
1357                                      />
1358                                    </Table.Cell>
1359                                  )}
1360                                </Table.Row>
1361                              );
1362                            })}
1363                          </DataTable>
1364                        ) : (
1365                          state.reviews.length ? empty("No reviews waiting for owner action.")
1366                            : <FirstRunEmpty kind="reviews" owner={actions.owner}
1367                              goToWork={() => actions.setSection("work")} />
1368                        )}
1369                      </section>
1370                      <section id="recent-receipts" aria-labelledby="recent-receipts-heading">
1371                        <h2 id="recent-receipts-heading">Recent receipts</h2>
1372                        {history.length ? (
1373                          <DataTable
1374                            kind="receipt-history"
1375                            labelledBy="recent-receipts-heading"
1376                            headings={[
1377                              "Packet",
1378                              "Head",
1379                              "Outcome",
1380                              "Checks",
1381                              "Trigger",
1382                              "Finished",
1383                            ]}
1384                          >
1385                            {history.map((review) => (
1386                              <Table.Row
1387                                key={review.id}
1388                                className="receipt-history-row"
1389                                onClick={() => showReceipt(review.id)}
1390                              >
1391                                <Table.Cell>
1392                                  <button
1393                                    type="button"
1394                                    className="claim-link receipt-history-link"
1395                                    aria-label={`View receipt for ${review.packet} at ${review.sha.slice(0, 7)}`}
1396                                    onClick={(event) => {
1397                                      event.stopPropagation();
1398                                      showReceipt(review.id);
1399                                    }}
1400                                  >
1401                                    {review.packet}
1402                                  </button>
1403                                </Table.Cell>
1404                                <Table.Cell>{short(review.sha)}</Table.Cell>
1405                                <Table.Cell>
1406                                  <ReceiptBadge
1407                                    status={review.receipt.status}
1408                                    recheck={
1409                                      state.project.settings?.requireReceipt === true &&
1410                                      review.receipt.trigger === "before-promote"
1411                                    }
1412                                  />
1413                                </Table.Cell>
1414                                <Table.Cell className="caption muted tabular">
1415                                  {checkCount(review.receipt.checks.length)}
1416                                  {duration(review.receipt.durationMs) &&
1417                                    ` · ${duration(review.receipt.durationMs)}`}
1418                                </Table.Cell>
1419                                <Table.Cell className="caption muted">
1420                                  {review.receipt.trigger === "before-promote"
1421                                    ? "Before promote"
1422                                    : "On review"}
1423                                </Table.Cell>
1424                                <Table.Cell className="caption muted">
1425                                  {review.receipt.finishedAt && (
1426                                    <time dateTime={review.receipt.finishedAt}>
1427                                      {relative(review.receipt.finishedAt)}
1428                                    </time>
1429                                  )}
1430                                </Table.Cell>
1431                              </Table.Row>
1432                            ))}
1433                          </DataTable>
1434                        ) : (
1435                          <p className="muted">No receipts yet.</p>
1436                        )}
1437                      </section>
1438                    </>,
1439                  )}
1440                  {section(
1441                    "agents",
1442                    <AgentStatusTable key={`agents:${view.project}`} state={state} activity={view.activity}
1443                      brand={view.brand} owner={actions.owner} openPacket={showPacket} revealRepo={agentRepo} />,
1444                  )}
1445                  {section("races", <Races state={state} owner={actions.owner} openRace={(id) => actions.openRace?.(id)} />)}
1446                </>
1447              )}
1448            </div>
1449          </div>
1450        </main>
1451        {!actions.raceId && (narrow ? (
1452          <Dialog.Root open={panel} onOpenChange={toggle}>
1453            <Dialog
1454              className="activity-drawer"
1455              container={typeof document !== "undefined" ? document.body : undefined}
1456            >
1457              <Dialog.Title className="sr-only">
1458                {selected || selectedReview ? "Details" : "Activity"}
1459              </Dialog.Title>
1460              <div id="activity-panel">{panelContent}</div>
1461            </Dialog>
1462          </Dialog.Root>
1463        ) : (
1464          panel && (
1465            <aside
1466              id="activity-panel"
1467              className="activity-panel"
1468              aria-label={selected || selectedReview ? "Details" : "Activity"}
1469            >
1470              {panelContent}
1471            </aside>
1472          )
1473        ))}
1474      </div>
1475    </>
1476  );
1477}
1478