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

packages/forkhorse-dashboard/src/race.tsx

Snapshot at 34e3175

Containing directory

18,577 bytes · textView raw
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