Fork Horse repository, listed by its owner. Publisher snapshot.
packages/forkhorse-dashboard/src/race.tsx
Snapshot at 34e3175
1import { JudgeAdvice, JudgePickBadge } from "./judge.tsx";
2import { ArchivedBadge } from "./archive.tsx";
3import { RaceCompare } from "./race-usage.tsx";
4import { BudgetUsage, UsageFigures } from "./usage.tsx";
5import { LaneRunner, laneSilkStyle, LaneMarker, LaneConnector } from "./lane.tsx";
6import { Badge, Button, Tooltip } from "@cloudflare/kumo";
7import { useEffect, useRef, useState, type CSSProperties } from "react";
8import type { Brand, ProjectState, Race, RaceEntrant } from "./contracts.ts";
9import type { ViewModel } from "./model.ts";
10import type { ViewActions } from "./view.tsx";
11import { duration, receiptWord } from "./receipts.tsx";
12import { oldestLoadedActivity, packetPushes } from "./fork-graph-model.ts";
13import { milestoneDescription, raceAnnouncementChanges, raceAnnouncementSnapshot } from "./race-announcements.ts";
14import {
15 entrantResult,
16 entrantChecks,
17 entrantReviews,
18 ordinal,
19 photoFinish,
20 raceEntrants,
21 raceMilestones,
22 raceSplit,
23 raceStart,
24 rejectionDelta,
25 timestamp,
26} from "./race-model.ts";
27
28function Place({ entrant }: { entrant: RaceEntrant }) {
29 return (
30 entrant.place !== null && (
31 <Badge
32 variant={entrant.place === 1 ? "neutral" : "secondary"}
33 appearance={entrant.place === 1 ? "dot" : undefined}
34 className={entrant.place === 1 ? "race-place status-dot status-success" : "race-place"}
35 >
36 {ordinal(entrant.place)}
37 </Badge>
38 )
39 );
40}
41function Lane({
42 entrant,
43 race,
44 state,
45 brand,
46 owner,
47 ledger,
48}: {
49 entrant: RaceEntrant;
50 race: Race;
51 state: ProjectState;
52 brand: Brand;
53 owner: boolean;
54 ledger?: ProjectState["ledger"][number];
55}) {
56 const start = raceStart(race);
57 const reviews = entrantReviews(state, race, entrant);
58 const receipt = reviews.at(-1)?.receipt;
59 const failed =
60 receipt && ["failed", "timed-out", "infrastructure-error"].includes(receipt.status);
61 const failureAt = failed ? receipt.finishedAt : null;
62 const priorFailures = reviews.flatMap((review) =>
63 review.receipt && review.receipt.status !== "passed" && review.receipt.status !== "running"
64 ? [review.receipt]
65 : [],
66 );
67 const tooltip = [
68 ...priorFailures.flatMap((r) =>
69 r.checks
70 .filter((check) => check.outcome !== "passed")
71 .map(
72 (check) =>
73 `${check.name} · ${receiptWord(check.outcome).label.toLowerCase()}${duration(check.durationMs) ? ` · ${duration(check.durationMs)}` : ""}`,
74 ),
75 ),
76 ...(receipt?.status === "passed" && priorFailures.length && entrant.receiptPassedAt
77 ? [`retried, passed at +${raceSplit(entrant.receiptPassedAt, start)}`]
78 : []),
79 ].join("; ");
80 let previous = -1;
81 return (
82 <tr style={laneSilkStyle(state, entrant.agent)} data-race-agent={entrant.agent}>
83 <th scope="row">
84 <LaneRunner entrant={entrant} state={state} />
85 </th>
86 {raceMilestones.map((milestone, index) => {
87 const at = entrant[milestone.key];
88 const reached = timestamp(at) !== undefined;
89 const gap = reached && previous >= 0 ? index - previous : 0;
90 if (reached) previous = index;
91 const hurdle = milestone.key === "receiptPassedAt";
92 const failure = hurdle && failed;
93 const promoted = milestone.key === "promotedAt" && reached;
94 const label = failure ? receiptWord(receipt!.status).label : milestone.label;
95 const eventAt = failure ? failureAt : at;
96 const split = raceSplit(eventAt, start);
97 const { description } = milestoneDescription(state, race, entrant, milestone);
98 const marker = (
99 <LaneMarker
100 kind={
101 failure
102 ? receipt!.status === "failed" ? "failed" : "interrupted"
103 : promoted
104 ? "promoted"
105 : hurdle && reached && receipt?.status === "passed"
106 ? "passed"
107 : reached
108 ? milestone.key === "claimedAt"
109 ? "claim"
110 : milestone.key === "firstPushAt"
111 ? "push"
112 : milestone.key === "reviewRequestedAt"
113 ? "review"
114 : milestone.key === "approvedAt"
115 ? "approved"
116 : "reached"
117 : "unreached"
118 }
119 />
120 );
121 return (
122 <td
123 key={milestone.key}
124 className={`race-step ${milestone.key === "promotedAt" ? "race-finish" : ""}`}
125 data-milestone={milestone.key}
126 style={{ "--race-gap": gap } as CSSProperties}
127 >
128 <span className="race-phone-label" aria-hidden="true">
129 {milestone.label}
130 </span>
131 <span className="sr-only">{description}</span>
132 <LaneConnector reached={gap > 0} />
133 <span className="race-mark-box">
134 {hurdle && tooltip ? (
135 <Tooltip
136 content={tooltip}
137 render={
138 <button
139 type="button"
140 className="race-hurdle-button"
141 aria-label={`${description}. ${tooltip}`}
142 />
143 }
144 >
145 {marker}
146 </Tooltip>
147 ) : (
148 marker
149 )}
150 </span>
151 <span className="race-split caption" aria-hidden="true">
152 {split && <time dateTime={eventAt!}>+{split}</time>}
153 {failure && <span>{label}</span>}
154 </span>
155 </td>
156 );
157 })}
158 <td className="race-tokens" data-label="Tokens">
159 <span className="race-phone-label" aria-hidden="true">Tokens</span>
160 <BudgetUsage usage={entrant.usage} budget={race.tokenBudget} status={entrant.budget} agent={entrant.agent} brand={brand} />
161 </td>
162 <td className="race-result">
163 <Place entrant={entrant} />
164 <JudgePickBadge race={race} agent={entrant.agent} />
165 <strong>{entrantResult(entrant, owner)}</strong>
166 {entrant.promotedAt && (
167 <span className="caption">
168 {raceSplit(entrant.promotedAt, start)}
169 {ledger && (
170 <>
171 {" "}
172 · <code title={ledger.sha}>{ledger.sha.slice(0, 7)}</code>
173 </>
174 )}
175 </span>
176 )}
177 {entrant.closedAt && !entrant.promotedAt && (
178 <span className="caption muted">Closed +{raceSplit(entrant.closedAt, start)}</span>
179 )}
180 {entrant.promotedAt && ledger?.before && (
181 <span className="caption muted">
182 Promoted to main{" "}
183 <code>
184 {ledger.before.slice(0, 7)} → {ledger.sha.slice(0, 7)}
185 </code>
186 </span>
187 )}
188 </td>
189 </tr>
190 );
191}
192export function RaceView({
193 packet,
194 view,
195 actions,
196}: {
197 packet: string;
198 view: ViewModel;
199 actions: ViewActions;
200}) {
201 const state = view.state;
202 const observedRace = state?.races?.find((entry) => entry.packet === packet);
203 const previous = useRef<{ scope: string; snapshot: ReturnType<typeof raceAnnouncementSnapshot> } | null>(null);
204 const [announcement, setAnnouncement] = useState("");
205 useEffect(() => {
206 if (!state || !observedRace || view.error) {
207 previous.current = null;
208 setAnnouncement("");
209 return;
210 }
211 const scope = `${view.project}:${packet}`;
212 const snapshot = raceAnnouncementSnapshot(state, observedRace);
213 if (previous.current?.scope === scope) {
214 const message = raceAnnouncementChanges(previous.current.snapshot, snapshot);
215 if (message) setAnnouncement(message);
216 } else {
217 // Establish a baseline on load or switch; don't narrate historical milestones.
218 setAnnouncement("");
219 }
220 previous.current = { scope, snapshot };
221 }, [packet, view.project, state, observedRace, view.error]);
222 const title = useRef<HTMLHeadingElement>(null);
223 useEffect(() => {
224 // Focus after the triggering click completes, including tooltip focus restoration.
225 const frame = requestAnimationFrame(() => title.current?.focus());
226 return () => cancelAnimationFrame(frame);
227 }, [packet, !!state]);
228 const back = (
229 <Button variant="ghost" size="sm" onClick={() => actions.openRace?.()}>
230 ← {actions.raceBackSection === "races" ? "Races" : "Work"}
231 </Button>
232 );
233 if (view.error && !view.stale)
234 return (
235 <section className="race-page">
236 {back}
237 <p role="alert">Couldn't load this race.</p>
238 <Button variant="secondary" onClick={() => void actions.retry?.()}>
239 Try again
240 </Button>
241 </section>
242 );
243 if (!state)
244 return (
245 <section className="race-page">
246 {back}
247 <p className="caption muted" role="status">
248 Loading race…
249 </p>
250 </section>
251 );
252 const work = state.packets.find((entry) => entry.id === packet);
253 const race = state.races?.find((entry) => entry.packet === packet);
254 if (!race)
255 return (
256 <section className="race-page">
257 {back}
258 <p>
259 {work && work.mode !== "race"
260 ? "This packet isn't a race."
261 : "This race doesn't exist anymore."}
262 </p>
263 </section>
264 );
265 const entrants = raceEntrants(race),
266 start = raceStart(race),
267 photo = photoFinish(race);
268 // Match only an explicit ledger reference; never use response array position.
269 const ledger =
270 race.ledgerEntryId === undefined || race.ledgerEntryId === null
271 ? undefined
272 : state.ledger.find((entry) => entry.id === race.ledgerEntryId);
273 const placed = entrants
274 .filter((entry) => entry.place !== null)
275 .toSorted((a, b) => a.place! - b.place!);
276 const oldestLoaded = oldestLoadedActivity(view.activity);
277 const pushesAvailable = oldestLoaded !== Infinity && entrants.every((entrant) => {
278 const claimed = timestamp(entrant.claimedAt);
279 return claimed !== undefined && oldestLoaded <= claimed;
280 });
281 const now = Date.now();
282 return (
283 <article className="race-page" aria-labelledby="race-title">
284 <div className="sr-only" role="status" aria-live="polite">{announcement}</div>
285 <div className="race-navigation flex items-center gap-3">
286 {back}
287 {work && actions.showPacket && <Button variant="ghost" size="sm"
288 onClick={() => actions.showPacket!(packet)}>Packet details</Button>}
289 </div>
290 <header className="race-header">
291 <div>
292 <h2 ref={title} tabIndex={-1} id="race-title" className="heading-1">
293 {work?.title ?? packet}{" "}
294 {work?.archived && <ArchivedBadge at={work.archived.at} />}
295 </h2>
296 <p className="race-flag">
297 <span aria-hidden="true">⚑</span> {race.status === "finished" ? "Finished" : "Racing"}
298 {photo >= 2 && " · photo finish"}
299 </p>
300 <p className="caption muted">
301 <code>{packet}</code> · Race · {entrants.length}{" "}
302 {entrants.length === 1 ? "entrant" : "entrants"}
303 {race.judge && <> · Judge {race.judge}</>}
304 {start !== undefined && (
305 <>
306 {" "}
307 · Started{" "}
308 {new Date(start).toLocaleTimeString(undefined, {
309 hour: "2-digit",
310 minute: "2-digit",
311 hour12: false,
312 })}
313 </>
314 )}
315 {race.finishedAt && start !== undefined && (
316 <> · Finished {duration(timestamp(race.finishedAt)! - start)} later</>
317 )}
318 {placed.map((entrant) => (
319 <span key={entrant.repoName}>
320 {" "}
321 · {ordinal(entrant.place!)} {entrant.agent}
322 </span>
323 ))}
324 </p>
325 </div>
326 {start !== undefined && (
327 <div className="race-clock">
328 <span className="caption muted">Post time</span>
329 <strong>
330 {new Date(start).toLocaleTimeString(undefined, {
331 hour: "2-digit",
332 minute: "2-digit",
333 hour12: false,
334 })}
335 </strong>
336 {race.finishedAt && race.winner && (
337 <>
338 <span className="caption muted">Won in</span>
339 <strong>{raceSplit(race.finishedAt, start)}</strong>
340 </>
341 )}
342 </div>
343 )}
344 </header>
345 {photo >= 2 && (
346 <p className="race-photo-caption">
347 <Badge variant="secondary">Photo finish</Badge> {photo} entrants were approved; the first
348 promote to <code>main</code> won. The others' reviews closed when the race finished.
349 </p>
350 )}
351 <table className={`race-track ${entrants.length ? "" : "race-track-empty"}`}>
352 <caption className="sr-only">Race progress for {packet}</caption>
353 <thead>
354 <tr>
355 <th scope="col">Runner</th>
356 {raceMilestones.map((step) => (
357 <th
358 key={step.key}
359 scope="col"
360 className={step.key === "promotedAt" ? "race-finish" : ""}
361 >
362 {step.label}
363 </th>
364 ))}
365 <th scope="col" className="race-tokens-heading">Tokens</th>
366 <th scope="col">Result</th>
367 </tr>
368 </thead>
369 <tbody>
370 {entrants.map((entrant) => (
371 <Lane
372 key={entrant.repoName}
373 entrant={entrant}
374 race={race}
375 state={state}
376 brand={view.brand}
377 owner={actions.owner}
378 ledger={ledger}
379 />
380 ))}
381 </tbody>
382 </table>
383 {(!!entrants.length || !!race.judge) && <RaceCompare race={race} state={{ ...state, recentActivity: view.activity }} brand={view.brand} getReview={actions.getReview} advice={<JudgeAdvice race={race} view={view} actions={actions} />} />}
384 {!entrants.length && (
385 <p className="race-empty">No entrants yet. Agents join by claiming {packet}.</p>
386 )}
387 {!!entrants.length && (
388 <section className="race-results" aria-labelledby="race-results-title">
389 <h3 id="race-results-title">Results</h3>
390 <table>
391 <caption className="sr-only">Recorded results for {packet}</caption>
392 <thead>
393 <tr>
394 <th scope="col">Pos</th>
395 <th scope="col">Runner</th>
396 <th scope="col">Time to promote</th>
397 {pushesAvailable && <th scope="col">Pushes</th>}
398 <th scope="col">Checks passed</th>
399 <th scope="col">Tokens</th>
400 <th scope="col">Result</th>
401 </tr>
402 </thead>
403 <tbody>
404 {[...placed, ...entrants.filter((entry) => entry.place === null)].map((entrant) => {
405 const pushes = packetPushes(
406 view.activity, entrant.repoName, state.project.defaultBranch,
407 timestamp(entrant.claimedAt) ?? Infinity,
408 timestamp(entrant.closedAt) ?? timestamp(race.finishedAt) ?? now,
409 ).length;
410 return (
411 <tr key={entrant.repoName} style={laneSilkStyle(state, entrant.agent)}>
412 <td data-label="Pos">
413 {entrant.place !== null ? ordinal(entrant.place) : "—"}
414 </td>
415 <th scope="row">
416 <LaneRunner entrant={entrant} state={state} />
417 </th>
418 <td data-label="Time to promote">
419 {entrant.promotedAt ? raceSplit(entrant.promotedAt, start) : "—"}
420 </td>
421 {pushesAvailable && <td data-label="Pushes">{pushes}</td>}
422 <td data-label="Checks passed">
423 {entrantChecks(state, race, entrant)}
424 </td>
425 <td data-label="Tokens"><UsageFigures usage={entrant.usage} agent={entrant.agent} brand={view.brand} /></td>
426 <td data-label="Result">{entrantResult(entrant, actions.owner)}</td>
427 </tr>
428 );
429 })}
430 </tbody>
431 </table>
432 {pushesAvailable && <p className="caption muted">Pushes from loaded activity.</p>}
433 </section>
434 )}
435 {photo >= 2 && (
436 <section className="race-photo" aria-labelledby="race-photo-title">
437 <div>
438 <h3 id="race-photo-title">Photo finish</h3>
439 <p>
440 Promote is compare-and-swap, so exactly one can move <code>main</code>.
441 </p>
442 <p className="caption muted">The others' reviews closed when the race finished.</p>
443 </div>
444 <div className="race-photo-events">
445 {entrants
446 .filter((entrant) => entrant.promotedAt)
447 .map((entrant) => (
448 <p key={entrant.repoName}>
449 <strong>{entrant.agent}</strong> promoted
450 {ledger?.before && (
451 <>
452 {" "}
453 · <code>main</code>{" "}
454 <code>
455 {ledger.before.slice(0, 7)} → {ledger.sha.slice(0, 7)}
456 </code>
457 </>
458 )}{" "}
459 · {raceSplit(entrant.promotedAt, start)}
460 </p>
461 ))}
462 {entrants
463 .filter((entrant) => entrant.promoteRejected)
464 .map((entrant) => (
465 <div key={entrant.repoName}>
466 <p>
467 <strong>{entrant.agent}</strong> tried to promote · expected{" "}
468 <code>{entrant.promoteRejected!.expected.slice(0, 7)}</code>, found{" "}
469 <code>{entrant.promoteRejected!.found.slice(0, 7)}</code>
470 {rejectionDelta(entrant.promoteRejected!.at, race.finishedAt) && (
471 <> · {rejectionDelta(entrant.promoteRejected!.at, race.finishedAt)}</>
472 )}
473 </p>
474 <Badge variant="neutral" appearance="dot" className="status-dot status-warning">
475 Main moved
476 </Badge>
477 </div>
478 ))}
479 </div>
480 </section>
481 )}
482 </article>
483 );
484}
485