diff --git a/apps/fabro-web/app/components/size-chip.test.tsx b/apps/fabro-web/app/components/size-chip.test.tsx
new file mode 100644
index 000000000..b355f5db1
--- /dev/null
+++ b/apps/fabro-web/app/components/size-chip.test.tsx
@@ -0,0 +1,40 @@
+import { describe, expect, test } from "bun:test";
+import TestRenderer, { act } from "react-test-renderer";
+
+import { SizeChip } from "./size-chip";
+import { Tooltip } from "./ui";
+
+function tooltipLabel(element: React.ReactElement): string {
+ let renderer: TestRenderer.ReactTestRenderer | undefined;
+ act(() => {
+ renderer = TestRenderer.create(element);
+ });
+ return renderer!.root.findByType(Tooltip).props.label as string;
+}
+
+describe("SizeChip", () => {
+ test("renders the size letter", () => {
+ let renderer: TestRenderer.ReactTestRenderer | undefined;
+ act(() => {
+ renderer = TestRenderer.create();
+ });
+
+ expect(JSON.stringify(renderer!.toJSON())).toContain("M");
+ });
+
+ test("appends the cost to the tooltip", () => {
+ expect(tooltipLabel())
+ .toBe("Size M · $12.34");
+ });
+
+ test("omits the cost when the run has no billing yet", () => {
+ expect(tooltipLabel()).toBe("Size M");
+ expect(tooltipLabel()).toBe("Size M");
+ });
+
+ test("calls out the tiers that warrant attention", () => {
+ expect(tooltipLabel())
+ .toBe("Size L (risky) · $150.00");
+ expect(tooltipLabel()).toBe("Size XL (unhealthy)");
+ });
+});
diff --git a/apps/fabro-web/app/routes/runs.tsx b/apps/fabro-web/app/routes/runs.tsx
index 7da0719ea..fe21e36c8 100644
--- a/apps/fabro-web/app/routes/runs.tsx
+++ b/apps/fabro-web/app/routes/runs.tsx
@@ -26,6 +26,7 @@ import { ciConfig, columnForRun, columnStatusDisplay, columnStatuses, deriveCiSt
import type { CiStatus, CheckRun, CheckStatus, RunItem } from "../data/runs";
import { EmptyState } from "../components/state";
import { PullRequestChip } from "../components/pull-request-chip";
+import { SizeChip } from "../components/size-chip";
import {
summarizeBatchLifecycleAction,
} from "../components/runs-list/batch-lifecycle";
@@ -345,7 +346,7 @@ function PrCard({
// All inline footer metadata on PrCard belongs in this one row. Adding a new
// piece as a sibling `
` below the card body recreates a recurring bug
-// where stats stack onto separate lines instead of sitting next to elapsed/actions.
+// where stats stack onto separate lines instead of sitting next to size/actions.
function PrCardFooter({ pr, actions }: { pr: RunItem; actions?: string[] }) {
const hasActions = actions != null && actions.length > 0;
const hasStats =
@@ -354,7 +355,7 @@ function PrCardFooter({ pr, actions }: { pr: RunItem; actions?: string[] }) {
(pr.additions != null && pr.additions !== 0) ||
(pr.deletions != null && pr.deletions !== 0);
- if (!hasStats && !hasActions && pr.elapsed == null) return null;
+ if (!hasStats && !hasActions && pr.size == null) return null;
return (
@@ -416,9 +417,9 @@ function PrCardFooter({ pr, actions }: { pr: RunItem; actions?: string[] }) {
))}
)}
- {pr.elapsed != null && (
-
- {pr.elapsed}
+ {pr.size != null && (
+
+
)}