Fork Horse repository, listed by its owner. Publisher snapshot.
packages/forkhorse-dashboard/src/view.tsx
Snapshot at 34e3175
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