Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions evalboard/app/_components/harness-badge.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,35 @@
import Image from "next/image";

// Vendor logo for a run's harness (RunConfig). Renders the recognizable
// Canonical harness constants live in the leaf lib/harness module (shared by
// the server data layer); re-exported here so existing badge importers are
// unaffected.
export { KNOWN_HARNESSES, type KnownHarness } from "@/lib/harness";

// Vendor logo + labels for a run's harness (RunConfig). Renders the recognizable
// vendor mark instead of raw "claude-code"/"codex"/"antigravity" text,
// mirroring the Slack rollup's vendor emoji. A missing harness defaults to
// claude-code (the nightly). This is an internal-only column — the caller
// gates it behind isInternal (see lib/edition.ts).
const HARNESS_LOGO: Record<string, { src: string; label: string }> = {
const HARNESS_LOGO: Record<string, { src: string; label: string; short: string }> = {
"claude-code": {
src: "/harness/claude-code.png",
label: "Claude Code · Anthropic",
short: "Claude Code",
},
codex: { src: "/harness/codex.png", label: "Codex · OpenAI" },
codex: { src: "/harness/codex.png", label: "Codex · OpenAI", short: "Codex" },
antigravity: {
src: "/harness/antigravity.png",
label: "Antigravity · Google Gemini",
short: "Antigravity",
},
};

// Short human label for a harness id ("Claude Code"), for selectors and prose.
// Unknown ids fall through to the raw id.
export function harnessShortLabel(harness: string): string {
return HARNESS_LOGO[harness]?.short ?? harness;
}

export function HarnessBadge({ harness }: { harness?: string | null }) {
const key = harness ?? "claude-code";
const logo = HARNESS_LOGO[key];
Expand Down
49 changes: 49 additions & 0 deletions evalboard/app/_components/harness-selector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"use client";

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.
export function HarnessSelector({
current,
harnesses,
}: {
current: string;
harnesses: readonly string[];
}) {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const set = (h: string) => {
const p = new URLSearchParams(searchParams.toString());
p.set("h", h);
router.replace(`${pathname}?${p.toString()}`, { 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];
return (
<div className="inline-flex border border-gray-200 rounded-md overflow-hidden text-sm">
{opts.map((h) => {
const active = h === current;
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"}`}
>
<HarnessBadge harness={h} />
{harnessShortLabel(h)}
</button>
);
})}
</div>
);
}
16 changes: 15 additions & 1 deletion evalboard/app/_overview/tag-rail.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Link from "next/link";
import type { TagCount } from "@/lib/overview";
import { DEFAULT_HARNESS } from "@/lib/harness";
import type { Window } from "@/lib/reviews-types";

type Variant = "neutral" | "rose" | "indigo";
Expand Down Expand Up @@ -35,11 +36,16 @@ function hrefForTag(
tag: string | null,
window: Window | null,
q: string | null,
harness: string | null,
): string {
const params = new URLSearchParams();
if (window) params.set("window", window);
if (tag) params.set("tag", tag);
if (q) params.set("q", q);
// Preserve the active harness scope across tag clicks (omit the default to
// keep URLs clean). Without this, filtering by a tag would silently reset a
// codex/antigravity view back to claude-code.
if (harness && harness !== DEFAULT_HARNESS) params.set("h", harness);
const qs = params.toString();
return qs ? `${basePath}?${qs}` : basePath;
}
Expand All @@ -52,6 +58,7 @@ function TagChip({
basePath,
window,
q,
harness,
}: {
tag: string;
count: number;
Expand All @@ -60,11 +67,12 @@ function TagChip({
basePath: string;
window: Window | null;
q: string | null;
harness: string | null;
}) {
const s = STYLES[variant];
return (
<Link
href={hrefForTag(basePath, active ? null : tag, window, q)}
href={hrefForTag(basePath, active ? null : tag, window, q, harness)}
scroll={false}
className={`inline-flex items-center gap-1 text-[11px] leading-none px-2 py-1 rounded border transition-colors ${active ? s.chipActive : s.chip}`}
>
Expand Down Expand Up @@ -111,6 +119,7 @@ export function MergedTagRail({
basePath = "/",
window = null,
q = null,
harness = null,
limit = 24,
}: {
skills: TagCount[];
Expand All @@ -123,6 +132,8 @@ export function MergedTagRail({
// Null on pages that don't expose a window selector (e.g. /trends).
window?: Window | null;
q?: string | null;
// Active harness scope to preserve in chip links (null = not harness-scoped).
harness?: string | null;
limit?: number;
}) {
const s = pickShown(skills, limit, activeTag);
Expand All @@ -146,6 +157,7 @@ export function MergedTagRail({
basePath={basePath}
window={window}
q={q}
harness={harness}
/>
))}
{r.shown.map((tc) => (
Expand All @@ -158,6 +170,7 @@ export function MergedTagRail({
basePath={basePath}
window={window}
q={q}
harness={harness}
/>
))}
{t.shown.map((tc) => (
Expand All @@ -170,6 +183,7 @@ export function MergedTagRail({
basePath={basePath}
window={window}
q={q}
harness={harness}
/>
))}
{totalRemaining > 0 && (
Expand Down
38 changes: 30 additions & 8 deletions evalboard/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ import {
getAdhocRunListing,
getOverview,
getRunListing,
listRecentHarnesses,
type TagCount,
} from "@/lib/overview";
import { parseHarnessParam, DEFAULT_HARNESS } from "@/lib/harness";
import { fmtDuration, fmtRunTime, fmtTimestamp, passClass } from "@/lib/format";
import { WindowSelector } from "./_components/window-selector";
import { WINDOWS, type Window } from "@/lib/reviews-types";
Expand All @@ -15,7 +17,8 @@ import { ChipLegend, MergedTagRail } from "./_overview/tag-rail";
import { TableScroll } from "./_components/scroll-table";
import { CollapsibleRail } from "./_components/collapsible-rail";
import { isInternal } from "@/lib/edition";
import { HarnessBadge } from "@/app/_components/harness-badge";
import { HarnessBadge, harnessShortLabel } from "@/app/_components/harness-badge";
import { HarnessSelector } from "@/app/_components/harness-selector";

export const dynamic = "force-dynamic";

Expand Down Expand Up @@ -98,6 +101,7 @@ export default async function Page({
window?: string;
tag?: string;
q?: string;
h?: string;
limit?: string;
alimit?: string;
}>;
Expand All @@ -106,14 +110,20 @@ export default async function Page({
const window = parseWindow(params.window);
const activeTag = parseTag(params.tag);
const q = parseQ(params.q);
const harness = parseHarnessParam(params.h);
const limit = parseLimit(params.limit);
const adhocLimit = parseAdhocLimit(params.alimit);
const isFiltered = activeTag != null || q != null;

const [overview, listing, adhoc] = await Promise.all([
getOverview(window, activeTag, q),
// The analytics block (chart + rails) is scoped to one harness so the
// success line stops zigzagging across incomparable harnesses. The run
// LIST stays all-harness — seeing every recent run is the page's job, and
// the Harness column already disambiguates each row.
const [overview, listing, adhoc, harnesses] = await Promise.all([
getOverview(window, activeTag, q, harness),
getRunListing(window, activeTag, q, limit),
getAdhocRunListing(adhocLimit),
listRecentHarnesses(),
]);

const skills = filterTagsByQuery(overview.skills, q);
Expand All @@ -133,10 +143,14 @@ export default async function Page({
const rawAlimit = Array.isArray(params.alimit)
? params.alimit[0]
: params.alimit;
// Omit the default harness from URLs to keep them clean; carry a non-default
// scope through every self-link so it isn't reset by pagination/clear.
const hParam = harness === DEFAULT_HARNESS ? undefined : harness;
const base = {
window,
tag: activeTag,
q,
h: hParam,
limit: rawLimit,
alimit: rawAlimit,
};
Expand All @@ -146,7 +160,7 @@ export default async function Page({
limit: Math.min(tableTotalLabel, shownCount + DEFAULT_LIMIT),
});
const showAllHref = buildHref({ ...base, limit: "all" });
const clearAllHref = buildHref({ window });
const clearAllHref = buildHref({ window, h: hParam });

// Ad-hoc section disclosure: rows are filtered (by `q`) then capped to
// adhocLimit; offer "Show all" while more match than are shown, and a
Expand Down Expand Up @@ -212,7 +226,7 @@ export default async function Page({
{overview.runs.length === 1 ? "" : "s"}
{" · "}
<Link
href={buildHref({ window })}
href={buildHref({ window, h: hParam })}
scroll={false}
className="text-studio-blue hover:underline"
>
Expand All @@ -221,14 +235,21 @@ export default async function Page({
</>
) : (
<>
Success rate per run across the last{" "}
{window} · {overview.runs.length} run
Success rate per{" "}
{harnessShortLabel(harness)} run across the
last {window} · {overview.runs.length} run
{overview.runs.length === 1 ? "" : "s"}
</>
)}
</p>
</div>
<WindowSelector current={window} />
<div className="flex items-center gap-3">
<HarnessSelector
current={harness}
harnesses={harnesses}
/>
<WindowSelector current={window} />
</div>
</div>
<DailySuccessChart
data={overview.runs}
Expand Down Expand Up @@ -261,6 +282,7 @@ export default async function Page({
activeTag={activeTag}
window={window}
q={q}
harness={harness}
limit={24}
/>
</CollapsibleRail>
Expand Down
29 changes: 22 additions & 7 deletions evalboard/app/path-to-ga/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import Link from "next/link";
import { getOverview, getTagTaskBreakdown } from "@/lib/overview";
import {
getOverview,
getTagTaskBreakdown,
listRecentHarnesses,
} from "@/lib/overview";
import { parseHarnessParam } from "@/lib/harness";
import { humanizeTaskId } from "@/lib/format";
import { WindowSelector } from "../_components/window-selector";
import { HarnessSelector } from "../_components/harness-selector";
import { harnessShortLabel } from "../_components/harness-badge";
import { WINDOWS, type Window } from "@/lib/reviews-types";
import { DailySuccessChart } from "../_overview/daily-chart";
import { TableScroll } from "../_components/scroll-table";
Expand All @@ -26,14 +33,18 @@ function passClass(pct: number | null): string {
export default async function PathToGaPage({
searchParams,
}: {
searchParams: Promise<{ window?: string }>;
searchParams: Promise<{ window?: string; h?: string }>;
}) {
const params = await searchParams;
const window = parseWindow(params.window);
const harness = parseHarnessParam(params.h);

const [overview, taskRows] = await Promise.all([
getOverview(window, TAG, null),
getTagTaskBreakdown(window, TAG),
// Scope to one harness — readiness of a task is per-harness, and a blended
// chart/pass-rate mixes incomparable regimes.
const [overview, taskRows, harnesses] = await Promise.all([
getOverview(window, TAG, null, harness),
getTagTaskBreakdown(window, TAG, harness),
listRecentHarnesses(),
]);

const runsInWindow = overview.runs.length;
Expand All @@ -52,10 +63,14 @@ export default async function PathToGaPage({
</h1>
<p className="text-sm text-gray-500">
Score for every task tagged{" "}
<span className="font-mono text-gray-700">{TAG}</span>.
<span className="font-mono text-gray-700">{TAG}</span> on{" "}
{harnessShortLabel(harness)}.
</p>
</div>
<WindowSelector current={window} />
<div className="flex items-center gap-3">
<HarnessSelector current={harness} harnesses={harnesses} />
<WindowSelector current={window} />
</div>
</div>

<section className="border border-gray-200 rounded-lg bg-white p-4 space-y-4">
Expand Down
11 changes: 11 additions & 0 deletions evalboard/app/trends/__tests__/trends-view.test.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
import { describe, expect, test, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import type { TaskTrend } from "@/lib/trends";
import { KNOWN_HARNESSES } from "@/app/_components/harness-badge";

// The view imports the server action for the expandable history rows; stub it
// so rendering doesn't pull the blob-backed loader into a jsdom test.
vi.mock("../actions", () => ({
fetchTaskHistoryAction: vi.fn(async () => []),
}));

// The header's HarnessSelector reads router/params hooks; stub them so the view
// renders in jsdom without a router provider.
vi.mock("next/navigation", () => ({
useRouter: () => ({ replace: vi.fn() }),
usePathname: () => "/trends",
useSearchParams: () => new URLSearchParams(),
}));

const { TrendsView } = await import("../trends-view");

function trend(overrides: Partial<TaskTrend>): TaskTrend {
Expand Down Expand Up @@ -35,6 +44,8 @@ function renderView(tasks: TaskTrend[], runIds: string[]) {
runIds={runIds}
q={null}
activeTag={null}
activeHarness="claude-code"
harnesses={KNOWN_HARNESSES}
skills={[]}
taskTags={[]}
reviewTags={[]}
Expand Down
Loading
Loading