diff --git a/evalboard/app/_components/__tests__/harness-selector.test.tsx b/evalboard/app/_components/__tests__/harness-selector.test.tsx
index 95b1057c..6a6d020f 100644
--- a/evalboard/app/_components/__tests__/harness-selector.test.tsx
+++ b/evalboard/app/_components/__tests__/harness-selector.test.tsx
@@ -62,6 +62,57 @@ describe("HarnessSelector", () => {
).toHaveAttribute("aria-pressed", "true");
});
+ test("a known harness absent from the discovery window still shows", () => {
+ // Delegate runs weekly, so it falls out of the 12-run discovery window
+ // between firings. The segment must survive that, or the filter reads as
+ // "delegate was removed" rather than "delegate hasn't run lately".
+ render(
+ ,
+ );
+ for (const h of KNOWN_HARNESSES) {
+ expect(
+ screen.getByRole("button", { name: harnessShortLabel(h) }),
+ ).toBeInTheDocument();
+ }
+ });
+
+ test("a discovered newcomer is kept, and sorts after the known set", () => {
+ render(
+ ,
+ );
+ const names = screen
+ .getAllByRole("button")
+ .map((b) => b.getAttribute("aria-label"));
+ expect(names).toContain(harnessShortLabel("zzz-new-harness"));
+ // Known harnesses hold display order; the newcomer lands last.
+ expect(names[names.length - 1]).toBe(
+ harnessShortLabel("zzz-new-harness"),
+ );
+ });
+
+ test("no segment is duplicated when discovery and the known set overlap", () => {
+ render(
+ ,
+ );
+ const labels = screen
+ .getAllByRole("button")
+ .map((b) => b.getAttribute("aria-label"))
+ .filter((n): n is string => n != null);
+ expect(new Set(labels).size).toBe(labels.length);
+ });
+
test("every segment is named for screen readers, not color-only", () => {
render(
("*")) {
+ out.push(...el.className.split(/\s+/).filter(Boolean));
+ }
+ return out;
+}
+
+// Tailwind's numeric width scale is quarter-rem, so `w-24` is 96px. Arbitrary
+// values carry their own unit.
+function fixedWidthPx(token: string): number | null {
+ const scale = /^(?:min-)?w-(\d+(?:\.\d+)?)$/.exec(token);
+ if (scale) return Number(scale[1]) * 4;
+ const arb = /^(?:min-)?w-\[(\d+(?:\.\d+)?)px\]$/.exec(token);
+ if (arb) return Number(arb[1]);
+ return null;
+}
+
+// The narrowest phone the rest of the app is built to survive. A skeleton that
+// overflows it would introduce a horizontal scrollbar that disappears the
+// moment the real content arrives.
+const MIN_VIEWPORT_PX = 320;
+// Room for the page gutters (`px-4` each side in the root layout).
+const CONTENT_BUDGET_PX = MIN_VIEWPORT_PX - 32;
+
+describe.each(CASES)("%s loading skeleton", (_name, Loading, label) => {
+ test("announces itself to a screen reader", () => {
+ const { getByRole } = render();
+ const status = getByRole("status");
+ expect(status).toHaveAttribute("aria-busy", "true");
+ expect(status).toHaveAttribute("aria-label", label);
+ // The blocks are decorative, so the label has to carry the meaning.
+ expect(status.textContent).toContain(label);
+ });
+
+ test("the pulse is dropped under prefers-reduced-motion", () => {
+ const { getByRole } = render();
+ const cls = getByRole("status").className;
+ expect(cls).toContain("animate-pulse");
+ expect(cls).toContain("motion-reduce:animate-none");
+ });
+
+ test("no fixed width can overflow a 320px phone", () => {
+ const { container } = render();
+ const offenders = classTokens(container)
+ .map((t) => [t, fixedWidthPx(t)] as const)
+ .filter(([, px]) => px != null && px > CONTENT_BUDGET_PX);
+ expect(offenders.map(([t]) => t)).toEqual([]);
+ });
+
+ test("every wide block is capped rather than left to run", () => {
+ const { container } = render();
+ // `w-full` on its own is fine (it fills the parent); paired with a
+ // `max-w-*` it is also fine. What must not appear is a fixed width
+ // wider than the phone, which the test above covers, or a horizontal
+ // overflow container, which would scroll instead of wrap.
+ expect(classTokens(container)).not.toContain("overflow-x-auto");
+ });
+
+ test("the skeleton is small enough to be free", () => {
+ const { container } = render();
+ // The pages these stand in for render thousands of nodes; the point of
+ // the skeleton is to appear instantly, so it must stay tiny.
+ expect(container.querySelectorAll("*").length).toBeLessThan(200);
+ });
+});
+
+describe("run skeleton", () => {
+ test("mirrors the grid's own md breakpoint instead of picking one layout", () => {
+ // task-grid renders a table at md and up and a card list below it. If
+ // the skeleton showed only one, a phone (or a desktop) would watch the
+ // layout jump the moment the real content landed.
+ const { container } = render();
+ expect(container.querySelector(".hidden.md\\:block")).not.toBeNull();
+ expect(container.querySelector(".md\\:hidden")).not.toBeNull();
+ });
+
+ test("the stat tiles match the real two-column phone grid", () => {
+ const { container } = render();
+ const grid = container.querySelector(".grid");
+ expect(grid?.className).toContain("grid-cols-2");
+ expect(grid?.className).toContain("md:grid-cols-5");
+ });
+});
diff --git a/evalboard/app/_components/harness-selector.tsx b/evalboard/app/_components/harness-selector.tsx
index 9e7acc0a..0646d17f 100644
--- a/evalboard/app/_components/harness-selector.tsx
+++ b/evalboard/app/_components/harness-selector.tsx
@@ -1,6 +1,7 @@
"use client";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
+import { KNOWN_HARNESSES, orderHarnesses } from "@/lib/harness";
import { HarnessBadge, harnessShortLabel } from "./harness-badge";
// Segmented control for a page's harness scope. Sets `?h=` while
@@ -41,12 +42,18 @@ export function HarnessSelector({
const qs = p.toString();
router.replace(qs ? `${pathname}?${qs}` : pathname, { scroll: false });
};
- // Always show the active harness, even if it has aged out of the recent
- // window (so a deep-linked `?h=` still reads as selected rather than absent).
- const opts =
- current == null || harnesses.includes(current)
- ? harnesses
- : [current, ...harnesses];
+ // Every known harness gets a segment whether or not it turned up in the
+ // discovery window. A weekly harness (delegate) drops out of that window
+ // between firings, and a control that quietly loses an option reads as
+ // "removed" rather than "hasn't run lately"; an empty result is honest and
+ // one click from recoverable. `current` is unioned in so a deep-linked `?h=`
+ // outside the known set still reads as selected, and orderHarnesses fixes
+ // the order so segments never reshuffle.
+ const opts = orderHarnesses([
+ ...KNOWN_HARNESSES,
+ ...harnesses,
+ ...(current == null ? [] : [current]),
+ ]);
// shrink-0 + whitespace-nowrap: a segment must keep its label on one line
// and never compress, so on a narrow screen the control scrolls (see the
// wrapper) instead of wrapping "Claude Code" onto two lines and stretching
diff --git a/evalboard/app/_components/skeleton.tsx b/evalboard/app/_components/skeleton.tsx
new file mode 100644
index 00000000..11ae1068
--- /dev/null
+++ b/evalboard/app/_components/skeleton.tsx
@@ -0,0 +1,171 @@
+// Shared skeleton primitives for the route-level `loading.tsx` files.
+//
+// These exist because every heavy route here is force-dynamic: on navigation
+// Next has to run the server component before it can render anything, and with
+// no Suspense boundary the browser sits on the PREVIOUS page for the whole of
+// it. Measured warm, that is 1.4-1.6s on the front page and 1.7-2.2s on a
+// 1,296-task run, with no indication the click registered.
+//
+// Blocks are sized to the element they stand in for and mirror the same
+// breakpoints, so the page doesn't jump when the content lands. Nothing here is
+// pixel-exact; it only has to read as "this shape, still loading".
+//
+// Widths are fractional or capped so a phone never scrolls sideways, and the
+// pulse is on the wrapper rather than each block: one animated element instead
+// of dozens, all in phase. `motion-reduce` drops it entirely.
+
+// One grey block. `className` carries the size, so callers read as a layout.
+export function SkelBar({ className = "" }: { className?: string }) {
+ return ;
+}
+
+// A bordered card matching the stat tiles and panels: same border, radius and
+// padding, so the skeleton occupies the height the real tile will.
+export function SkelTile({
+ className = "",
+ children,
+}: {
+ className?: string;
+ children?: React.ReactNode;
+}) {
+ return (
+