Fork Horse repository, listed by its owner. Publisher snapshot.
packages/forkhorse-dashboard/src/race-usage.tsx
Snapshot at 34e3175
1import { assertDetail } from "./review.tsx";
2import { useEffect, useState, type ReactNode } from "react";
3import type { Brand, ProjectState, Race, ReviewDetail } from "./contracts.ts";
4import { entrantChecks, raceEntrants, raceStart, raceSplit } from "./race-model.ts";
5import { oldestLoadedActivity, packetPushes, timestamp } from "./fork-graph-model.ts";
6import { BudgetUsage } from "./usage.tsx";
7import { totalTokens } from "./usage.ts";
8import { agentIdentities, Saddlecloth, type AgentIdentity } from "./lane.tsx";
9
10const elapsed = (start: number | undefined, end?: string | null) => {
11 const at = Date.parse(end ?? "");
12 return start !== undefined && Number.isFinite(start) && Number.isFinite(at) && at >= start
13 ? at - start
14 : undefined;
15};
16const best = (values: (number | undefined)[], index: number) =>
17 values.length > 1 &&
18 values.every((value) => value !== undefined) &&
19 new Set(values).size > 1 &&
20 values[index] === Math.min(...(values as number[]));
21
22function CompareCell({ agent, identity, children }: { agent: string; identity?: AgentIdentity; children: ReactNode }) {
23 return <td>
24 <span className="compare-entrant" aria-hidden="true"><Saddlecloth identity={identity} /><span title={agent}>{agent}</span></span>
25 <span className="compare-value">{children}</span>
26 </td>;
27}
28
29/** Facts below the accepted FH-055 track, using its claim order and recorded race clock. */
30export function RaceCompare({
31 race: rawRace,
32 state,
33 brand,
34 getReview,
35 advice,
36}: {
37 race: Race;
38 state: ProjectState;
39 brand: Brand;
40 advice?: ReactNode;
41 getReview?: (id: string) => Promise<ReviewDetail>;
42}) {
43 const race = { ...rawRace, entrants: raceEntrants(rawRace) };
44 const reviews = race.entrants.map((entrant) =>
45 state.reviews
46 .filter(
47 (r) =>
48 r.packet === race.packet && r.repoName === entrant.repoName && r.agent === entrant.agent,
49 )
50 .toSorted((a, b) => (b.requestedAt ?? "").localeCompare(a.requestedAt ?? ""))
51 .at(0),
52 );
53 const keys = JSON.stringify(reviews.map((r) => (r ? [r.id, r.sha] : null)));
54 const [details, setDetails] = useState<Record<string, ReviewDetail>>({});
55 useEffect(() => {
56 let active = true;
57 setDetails({});
58 if (getReview)
59 void Promise.allSettled(
60 reviews
61 .filter((r) => !!r)
62 .map(async (review) => {
63 const detail = await getReview(review.id);
64 assertDetail(detail, review.id);
65 if (
66 active &&
67 detail.review.id === review.id &&
68 detail.review.sha === review.sha &&
69 Number.isSafeInteger(detail.changes.fileCount)
70 )
71 setDetails((loaded) => ({ ...loaded, [review.id]: detail }));
72 }),
73 );
74 return () => {
75 active = false;
76 };
77 // Exact review identities trigger reads; host action objects need not be stable.
78 // eslint-disable-next-line react-hooks/exhaustive-deps
79 }, [keys, getReview]);
80 const start = raceStart(race);
81 const activity = state.recentActivity ?? [];
82 const oldestLoaded = oldestLoadedActivity(activity);
83 const pushesAvailable = oldestLoaded !== Infinity && race.entrants.every((entrant) => {
84 const claimed = timestamp(entrant.claimedAt);
85 return claimed !== undefined && oldestLoaded <= claimed;
86 });
87 const now = Date.now();
88 const approved = race.entrants.map((e) => elapsed(start, e.approvedAt));
89 const promoted = race.entrants.map((e) => elapsed(start, e.promotedAt));
90 const changes = reviews.map((r) => (r ? details[r.id]?.changes.fileCount : undefined));
91 const tokenValues = (source: "reported" | "metered") =>
92 race.entrants.map((e) => (e.usage?.[source] ? totalTokens(e.usage[source]!) : undefined));
93 const tokens = (index: number, compare = true) => {
94 const e = race.entrants[index];
95 return (
96 <>
97 <BudgetUsage
98 usage={e.usage}
99 budget={race.tokenBudget}
100 status={e.budget}
101 agent={e.agent}
102 brand={brand}
103 />
104 {compare &&
105 (["reported", "metered"] as const).map(
106 (source) =>
107 best(tokenValues(source), index) && (
108 <span className="caption muted" key={source}>
109 fewest ({source})
110 </span>
111 ),
112 )}
113 </>
114 );
115 };
116 const identities = agentIdentities(state.agents);
117 return (
118 <section className="race-usage" aria-labelledby="race-compare-title">
119 <h3 id="race-compare-title">Compare</h3>
120 {advice}
121 <p className="caption muted">
122 Times since the first entrant claimed. Files come from the latest review detail.
123 {pushesAvailable && " Push counts cover each entrant's race interval in loaded activity."}
124 </p>
125 <div className="table-scroll">
126 <table className="race-compare">
127 <thead>
128 <tr>
129 <th scope="col">Fact</th>
130 {race.entrants.map((e) => (
131 <th scope="col" key={e.repoName}>
132 {e.agent}
133 </th>
134 ))}
135 </tr>
136 </thead>
137 <tbody>
138 {(
139 [
140 ["Time to Approved", approved, "approvedAt"],
141 ["Time to Promoted", promoted, "promotedAt"],
142 ] as const
143 ).map(([label, values, key]) => (
144 <tr key={label}>
145 <th scope="row">{label}</th>
146 {values.map((_value, i) => (
147 <CompareCell key={race.entrants[i].repoName} agent={race.entrants[i].agent} identity={identities.get(race.entrants[i].agent)}>
148 {raceSplit(race.entrants[i][key], start) || "Not yet"}
149 {best([...values], i) && <span className="caption muted">fastest</span>}
150 </CompareCell>
151 ))}
152 </tr>
153 ))}
154 <tr>
155 <th scope="row">Checks passed</th>
156 {race.entrants.map((e) => (
157 <CompareCell key={e.repoName} agent={e.agent} identity={identities.get(e.agent)}>{entrantChecks(state, race, e)}</CompareCell>
158 ))}
159 </tr>
160 <tr>
161 <th scope="row">Files changed</th>
162 {changes.map((value, i) => (
163 <CompareCell key={race.entrants[i].repoName} agent={race.entrants[i].agent} identity={identities.get(race.entrants[i].agent)}>
164 {value ?? (reviews[i] ? "Unavailable" : "No review yet")}
165 {best(changes, i) && <span className="caption muted">fewest</span>}
166 </CompareCell>
167 ))}
168 </tr>
169 {pushesAvailable && <tr>
170 <th scope="row">Recorded pushes</th>
171 {race.entrants.map((e) => (
172 <CompareCell key={e.repoName} agent={e.agent} identity={identities.get(e.agent)}>
173 {packetPushes(
174 activity, e.repoName, state.project.defaultBranch,
175 timestamp(e.claimedAt) ?? Infinity,
176 timestamp(e.closedAt) ?? timestamp(race.finishedAt) ?? now,
177 ).length}
178 </CompareCell>
179 ))}
180 </tr>}
181 <tr>
182 <th scope="row">Tokens</th>
183 {race.entrants.map((e, i) => (
184 <CompareCell key={e.repoName} agent={e.agent} identity={identities.get(e.agent)}>{tokens(i)}</CompareCell>
185 ))}
186 </tr>
187 </tbody>
188 </table>
189 </div>
190 </section>
191 );
192}
193