Skip to content
45 changes: 45 additions & 0 deletions evalboard/app/_components/__tests__/harness-badge.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { describe, expect, test } from "vitest";
import { render, screen } from "@testing-library/react";
import { KNOWN_HARNESSES } from "@/lib/harness";
import { HarnessBadge, harnessShortLabel } from "../harness-badge";

describe("harnessShortLabel", () => {
test("every known harness has a human label", () => {
// A missing entry falls through to the raw id, which would put
// "delegate-sdk" in a legend next to "Codex" and "Antigravity".
for (const h of KNOWN_HARNESSES) {
expect(harnessShortLabel(h)).not.toBe(h);
}
});

test("the UiPath harness reads as Delegate", () => {
// The run data's id stays `delegate-sdk` (it's the registered
// `agent.type`); only the label people read is the short one.
expect(harnessShortLabel("delegate-sdk")).toBe("Delegate");
});

test("an unknown harness falls back to its id rather than a wrong name", () => {
expect(harnessShortLabel("some-new-agent")).toBe("some-new-agent");
});
});

describe("HarnessBadge", () => {
test("names the vendor in the alt text, not just the product", () => {
render(<HarnessBadge harness="delegate-sdk" />);
expect(screen.getByAltText("Delegate · UiPath")).toBeInTheDocument();
});

test("renders the id as text when there is no logo for it", () => {
// Better a raw id than another vendor's mark on someone else's run.
render(<HarnessBadge harness="some-new-agent" />);
expect(screen.getByText("some-new-agent")).toBeInTheDocument();
});

test("takes a size so the chart legend can sit inside 11px text", () => {
render(<HarnessBadge harness="codex" size={14} />);
expect(screen.getByAltText("Codex · OpenAI")).toHaveAttribute(
"width",
"14",
);
});
});
81 changes: 81 additions & 0 deletions evalboard/app/_components/__tests__/harness-selector.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { describe, expect, test, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import { KNOWN_HARNESSES } from "@/lib/harness";
import { harnessShortLabel } from "../harness-badge";

// The selector reads router/params hooks; stub them so it renders in jsdom
// without a router provider.
const replace = vi.fn();
vi.mock("next/navigation", () => ({
useRouter: () => ({ replace }),
usePathname: () => "/",
useSearchParams: () => new URLSearchParams(),
}));

const { HarnessSelector } = await import("../harness-selector");

describe("HarnessSelector", () => {
test("the all-harness segment is present and selected by default", () => {
render(
<HarnessSelector
current={null}
harnesses={[...KNOWN_HARNESSES]}
includeAll
/>,
);
expect(screen.getByRole("button", { name: "All" })).toHaveAttribute(
"aria-pressed",
"true",
);
});

test("a scoped harness reads as pressed, and All does not", () => {
render(
<HarnessSelector
current="codex"
harnesses={[...KNOWN_HARNESSES]}
includeAll
/>,
);
expect(screen.getByRole("button", { name: "Codex" })).toHaveAttribute(
"aria-pressed",
"true",
);
expect(screen.getByRole("button", { name: "All" })).toHaveAttribute(
"aria-pressed",
"false",
);
});

test("a deep-linked harness that has aged out still shows as a segment", () => {
// Otherwise `?h=delegate-sdk` after a quiet fortnight renders a control
// with nothing selected, which reads as an unscoped page.
render(
<HarnessSelector
current="delegate-sdk"
harnesses={["claude-code", "codex"]}
includeAll
/>,
);
expect(
screen.getByRole("button", { name: "Delegate" }),
).toHaveAttribute("aria-pressed", "true");
});

test("every segment is named for screen readers, not color-only", () => {
render(
<HarnessSelector
current={null}
harnesses={[...KNOWN_HARNESSES]}
includeAll
/>,
);
// The accessible name comes from aria-label, so it survives the
// below-`sm` breakpoint that hides the visible text.
for (const h of KNOWN_HARNESSES) {
expect(
screen.getByRole("button", { name: harnessShortLabel(h) }),
).toBeInTheDocument();
}
});
});
24 changes: 21 additions & 3 deletions evalboard/app/_components/harness-badge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,16 @@ const HARNESS_LOGO: Record<string, { src: string; label: string; short: string }
label: "Antigravity · Google Gemini",
short: "Antigravity",
},
// The Delegate harness is UiPath's own (the coder_eval_uipath plugin), so
// its vendor mark is the UiPath logo already served for the site header
// rather than a per-harness file under /harness. The key stays `delegate-sdk`
// because that is the `agent.type` the plugin registers and therefore what
// run.json carries; only the label people read is shortened.
"delegate-sdk": {
src: "/uipath.png",
label: "Delegate · UiPath",
short: "Delegate",
},
};

// Short human label for a harness id ("Claude Code"), for selectors and prose.
Expand All @@ -30,7 +40,15 @@ export function harnessShortLabel(harness: string): string {
return HARNESS_LOGO[harness]?.short ?? harness;
}

export function HarnessBadge({ harness }: { harness?: string | null }) {
export function HarnessBadge({
harness,
// Square edge in px. Defaults to the runs-table size; the chart legend asks
// for a smaller mark so the logo sits inside a line of 11px text.
size = 20,
}: {
harness?: string | null;
size?: number;
}) {
const key = harness ?? "claude-code";
const logo = HARNESS_LOGO[key];
// Unknown harness: show the raw id rather than a misleading logo.
Expand All @@ -42,8 +60,8 @@ export function HarnessBadge({ harness }: { harness?: string | null }) {
src={logo.src}
alt={logo.label}
title={logo.label}
width={20}
height={20}
width={size}
height={size}
className="rounded-sm"
/>
);
Expand Down
82 changes: 67 additions & 15 deletions evalboard/app/_components/harness-selector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,44 +3,96 @@
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { HarnessBadge, harnessShortLabel } from "./harness-badge";

// Segmented control for the trends page's harness scope. Sets `?h=<harness>`
// while preserving the active q/tag params, mirroring WindowSelector. Each
// segment shows the vendor logo + short label so it reads like the per-run
// harness badge on the runs tables.
// Segmented control for a page's harness scope. Sets `?h=<harness>` while
// preserving the active q/tag params, mirroring WindowSelector. Each segment
// shows the vendor logo + short label so it reads like the per-run harness badge
// on the runs tables.
//
// `current` is null only on pages that support the all-harness view (the
// overview, whose charts draw a line per harness and whose tiles and run list
// then cover every harness). Pages that need exactly one harness — trends
// collapses per-task history across runs, which only means something inside one
// harness — pass a concrete harness and leave `includeAll` off.
export function HarnessSelector({
current,
harnesses,
includeAll = false,
}: {
current: string;
current: string | null;
harnesses: readonly string[];
includeAll?: boolean;
}) {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const set = (h: string) => {
const set = (h: string | null) => {
const p = new URLSearchParams(searchParams.toString());
p.set("h", h);
router.replace(`${pathname}?${p.toString()}`, { scroll: false });
if (h == null) {
// The unscoped view is the default, so it's the absence of the param
// rather than `h=all` — keeps the canonical URL clean.
p.delete("h");
} else {
p.set("h", h);
}
// Changing scope changes which runs are in play, so the paged-out row
// counts no longer describe the new set; drop them back to page one.
p.delete("limit");
p.delete("alimit");
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 = harnesses.includes(current)
? harnesses
: [current, ...harnesses];
const opts =
current == null || harnesses.includes(current)
? harnesses
: [current, ...harnesses];
// 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
// the whole page past the viewport.
const segment = (active: boolean) =>
`flex shrink-0 items-center gap-1.5 whitespace-nowrap px-2.5 py-1 sm:px-3 ${
active
? "bg-studio-blue text-white"
: "bg-white text-gray-700 hover:bg-gray-50"
}`;
// Below `sm` the vendor logo carries the identity on its own (each button
// keeps the full name as its accessible label and tooltip) — five spelled-out
// segments don't fit on a phone, and the chart legend below names every line.
const label = (text: string) => (
<span className="hidden sm:inline">{text}</span>
);
return (
<div className="inline-flex border border-gray-200 rounded-md overflow-hidden text-sm">
{opts.map((h) => {
<div className="flex max-w-full overflow-x-auto rounded-md border border-gray-200 text-sm">
{includeAll && (
<button
type="button"
onClick={() => set(null)}
aria-pressed={current == null}
className={`${segment(current == null)} rounded-l-md`}
title="Every harness, one line each"
>
All
</button>
)}
{opts.map((h, i) => {
const active = h === current;
const name = harnessShortLabel(h);
return (
<button
key={h}
type="button"
onClick={() => set(h)}
aria-pressed={active}
className={`flex items-center gap-1.5 px-3 py-1 ${active ? "bg-studio-blue text-white" : "bg-white text-gray-700 hover:bg-gray-50"}`}
aria-label={name}
title={name}
className={`${segment(active)} ${
!includeAll && i === 0 ? "rounded-l-md" : ""
} ${i === opts.length - 1 ? "rounded-r-md" : ""}`}
>
<HarnessBadge harness={h} />
{harnessShortLabel(h)}
{label(name)}
</button>
);
})}
Expand Down
33 changes: 0 additions & 33 deletions evalboard/app/_components/window-selector.tsx

This file was deleted.

Loading
Loading