executable-stories-react 0.12.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,13 +2,14 @@
2
2
 
3
3
  // src/interactive/ReportInteractive.tsx
4
4
  import { Monitor, Moon, Sun } from "lucide-react";
5
+ import { useTools } from "webmcpable/react";
5
6
  import {
6
7
  useCallback as useCallback5,
7
8
  useDeferredValue,
8
9
  useId as useId2,
9
10
  useMemo as useMemo3,
10
11
  useRef as useRef5,
11
- useState as useState9
12
+ useState as useState10
12
13
  } from "react";
13
14
 
14
15
  // src/components/ui/empty.tsx
@@ -205,7 +206,7 @@ function formatValue(value) {
205
206
  function DocKv({ entry }) {
206
207
  return /* @__PURE__ */ jsxs("dl", { className: "my-2 flex flex-wrap items-baseline gap-x-2 text-sm", children: [
207
208
  /* @__PURE__ */ jsx6("dt", { className: "font-semibold text-muted-foreground", children: entry.label }),
208
- /* @__PURE__ */ jsx6("dd", { className: "font-mono text-foreground", children: formatValue(entry.value) })
209
+ /* @__PURE__ */ jsx6("dd", { className: "min-w-0 break-words font-mono text-foreground", children: formatValue(entry.value) })
209
210
  ] });
210
211
  }
211
212
 
@@ -413,7 +414,7 @@ function DocHtml({ entry }) {
413
414
  loading: "lazy",
414
415
  title,
415
416
  className: "block w-full border-0",
416
- style: { height }
417
+ style: { height, colorScheme: "light", background: "#fff" }
417
418
  }
418
419
  )
419
420
  ] });
@@ -1137,6 +1138,24 @@ function scenarioToPrompt(scenario) {
1137
1138
  }
1138
1139
  return lines.join("\n");
1139
1140
  }
1141
+ function failuresToPrompt(scenarios) {
1142
+ if (scenarios.length === 0) return "";
1143
+ const noun = scenarios.length === 1 ? "test scenario is" : "test scenarios are";
1144
+ const lines = [
1145
+ `${scenarios.length} ${noun} failing. Investigate the likely cause (they may share one) and propose a fix.`
1146
+ ];
1147
+ scenarios.forEach((scenario, i) => {
1148
+ lines.push("", `${i + 1}. ${scenario.title}`, "Steps:");
1149
+ for (const step of scenario.steps) {
1150
+ const mark = step.status === "failed" ? " \u27F5 failed here" : "";
1151
+ lines.push(` ${step.keyword} ${step.text}${mark}`);
1152
+ }
1153
+ if (scenario.errorMessage) {
1154
+ lines.push("Error:", scenario.errorMessage.trim());
1155
+ }
1156
+ });
1157
+ return lines.join("\n");
1158
+ }
1140
1159
  function scenarioPermalink(scenario) {
1141
1160
  if (typeof location === "undefined") return `#${scenario.id}`;
1142
1161
  return `${location.href.split("#")[0]}#${scenario.id}`;
@@ -1262,57 +1281,68 @@ function ReportScenario({ scenario, hideTitle = false }) {
1262
1281
  className: "gap-0 py-0",
1263
1282
  children: [
1264
1283
  /* @__PURE__ */ jsxs17(CardHeader, { className: "px-4 py-3", children: [
1265
- /* @__PURE__ */ jsxs17("div", { className: cn("flex items-start gap-4", hideTitle ? "justify-end" : "justify-between"), children: [
1266
- hideTitle ? null : /* @__PURE__ */ jsxs17("h3", { id: titleId, className: "flex items-center gap-2 text-sm font-medium text-foreground", children: [
1267
- collapsible ? /* @__PURE__ */ jsx26(
1268
- "button",
1269
- {
1270
- type: "button",
1271
- onClick: () => collapse.toggle(scenario.id),
1272
- "aria-expanded": !collapsed,
1273
- "aria-controls": bodyId,
1274
- "aria-label": `Toggle ${scenario.title}`,
1275
- className: "shrink-0 cursor-pointer text-muted-foreground hover:text-foreground",
1276
- children: /* @__PURE__ */ jsx26("span", { "aria-hidden": true, className: cn("inline-block text-xs transition-transform", !collapsed && "rotate-90"), children: "\u25B8" })
1277
- }
1278
- ) : null,
1279
- /* @__PURE__ */ jsx26("span", { "aria-hidden": true, className: cn("text-sm leading-none", meta.glyphColor), children: meta.glyph }),
1280
- /* @__PURE__ */ jsx26("span", { children: scenario.title })
1281
- ] }),
1282
- /* @__PURE__ */ jsxs17("div", { className: "flex shrink-0 items-center gap-2", children: [
1283
- /* @__PURE__ */ jsx26(ScenarioRunHistory, { scenarioId: scenario.id }),
1284
- /* @__PURE__ */ jsx26(ScenarioStaleness, { scenario }),
1285
- scenario.durationMs > 0 ? /* @__PURE__ */ jsx26("span", { className: "whitespace-nowrap font-mono text-[0.6875rem] text-muted-foreground", children: formatDuration3(scenario.durationMs) }) : null,
1286
- meta.showPill ? /* @__PURE__ */ jsx26(Badge, { variant: meta.badgeVariant, "aria-label": `Status: ${label}`, children: label }) : /* @__PURE__ */ jsx26("span", { className: "sr-only", children: `Status: ${label}` }),
1287
- actions ? /* @__PURE__ */ jsxs17(DropdownMenu, { children: [
1288
- /* @__PURE__ */ jsx26(
1289
- DropdownMenuTrigger,
1290
- {
1291
- "aria-label": `Actions for ${scenario.title}`,
1292
- className: "cursor-pointer rounded p-1 text-muted-foreground hover:bg-muted hover:text-foreground [&_svg]:size-4",
1293
- children: /* @__PURE__ */ jsx26(MoreHorizontalIcon, { "aria-hidden": true })
1294
- }
1295
- ),
1296
- /* @__PURE__ */ jsxs17(DropdownMenuContent, { align: "end", children: [
1297
- /* @__PURE__ */ jsxs17(DropdownMenuItem, { onClick: () => actions.copyLink(scenario), children: [
1298
- /* @__PURE__ */ jsx26(LinkIcon, {}),
1299
- " Copy link"
1300
- ] }),
1301
- /* @__PURE__ */ jsxs17(DropdownMenuItem, { onClick: () => actions.copyMarkdown(scenario), children: [
1302
- /* @__PURE__ */ jsx26(FileTextIcon, {}),
1303
- " Copy as Markdown"
1304
- ] }),
1305
- /* @__PURE__ */ jsxs17(DropdownMenuItem, { onClick: () => actions.copyPrompt(scenario), children: [
1306
- /* @__PURE__ */ jsx26(SparklesIcon, {}),
1307
- " Explain with AI"
1308
- ] })
1284
+ /* @__PURE__ */ jsxs17(
1285
+ "div",
1286
+ {
1287
+ className: cn(
1288
+ // Wraps rather than squeezing: on a phone the status/duration group
1289
+ // is ~160px, which leaves a title column a few characters wide.
1290
+ "flex flex-wrap items-start gap-x-4 gap-y-1",
1291
+ hideTitle ? "justify-end" : "justify-between"
1292
+ ),
1293
+ children: [
1294
+ hideTitle ? null : /* @__PURE__ */ jsxs17("h3", { id: titleId, className: "flex min-w-0 flex-1 basis-48 items-center gap-2 text-sm font-medium text-foreground", children: [
1295
+ collapsible ? /* @__PURE__ */ jsx26(
1296
+ "button",
1297
+ {
1298
+ type: "button",
1299
+ onClick: () => collapse.toggle(scenario.id),
1300
+ "aria-expanded": !collapsed,
1301
+ "aria-controls": bodyId,
1302
+ "aria-label": `Toggle ${scenario.title}`,
1303
+ className: "shrink-0 cursor-pointer text-muted-foreground hover:text-foreground",
1304
+ children: /* @__PURE__ */ jsx26("span", { "aria-hidden": true, className: cn("inline-block text-xs transition-transform", !collapsed && "rotate-90"), children: "\u25B8" })
1305
+ }
1306
+ ) : null,
1307
+ /* @__PURE__ */ jsx26("span", { "aria-hidden": true, className: cn("text-sm leading-none", meta.glyphColor), children: meta.glyph }),
1308
+ /* @__PURE__ */ jsx26("span", { children: scenario.title })
1309
+ ] }),
1310
+ /* @__PURE__ */ jsxs17("div", { className: "flex shrink-0 items-center gap-2", children: [
1311
+ /* @__PURE__ */ jsx26(ScenarioRunHistory, { scenarioId: scenario.id }),
1312
+ /* @__PURE__ */ jsx26(ScenarioStaleness, { scenario }),
1313
+ scenario.durationMs > 0 ? /* @__PURE__ */ jsx26("span", { className: "whitespace-nowrap font-mono text-[0.6875rem] text-muted-foreground", children: formatDuration3(scenario.durationMs) }) : null,
1314
+ meta.showPill ? /* @__PURE__ */ jsx26(Badge, { variant: meta.badgeVariant, "aria-label": `Status: ${label}`, children: label }) : /* @__PURE__ */ jsx26("span", { className: "sr-only", children: `Status: ${label}` }),
1315
+ actions ? /* @__PURE__ */ jsxs17(DropdownMenu, { children: [
1316
+ /* @__PURE__ */ jsx26(
1317
+ DropdownMenuTrigger,
1318
+ {
1319
+ "aria-label": `Actions for ${scenario.title}`,
1320
+ className: "cursor-pointer rounded p-1 text-muted-foreground hover:bg-muted hover:text-foreground [&_svg]:size-4",
1321
+ children: /* @__PURE__ */ jsx26(MoreHorizontalIcon, { "aria-hidden": true })
1322
+ }
1323
+ ),
1324
+ /* @__PURE__ */ jsxs17(DropdownMenuContent, { align: "end", children: [
1325
+ /* @__PURE__ */ jsxs17(DropdownMenuItem, { onClick: () => actions.copyLink(scenario), children: [
1326
+ /* @__PURE__ */ jsx26(LinkIcon, {}),
1327
+ " Copy link"
1328
+ ] }),
1329
+ /* @__PURE__ */ jsxs17(DropdownMenuItem, { onClick: () => actions.copyMarkdown(scenario), children: [
1330
+ /* @__PURE__ */ jsx26(FileTextIcon, {}),
1331
+ " Copy as Markdown"
1332
+ ] }),
1333
+ /* @__PURE__ */ jsxs17(DropdownMenuItem, { onClick: () => actions.copyPrompt(scenario), children: [
1334
+ /* @__PURE__ */ jsx26(SparklesIcon, {}),
1335
+ " Explain with AI"
1336
+ ] })
1337
+ ] })
1338
+ ] }) : null
1309
1339
  ] })
1310
- ] }) : null
1311
- ] })
1312
- ] }),
1340
+ ]
1341
+ }
1342
+ ),
1313
1343
  scenario.tags.length > 0 || (scenario.tickets?.length ?? 0) > 0 ? /* @__PURE__ */ jsxs17("ul", { "aria-label": "Tags and tickets", className: "flex flex-wrap gap-1.5", children: [
1314
1344
  scenario.tags.map((t) => /* @__PURE__ */ jsx26("li", { children: /* @__PURE__ */ jsx26(Badge, { variant: "tag", children: t }) }, `tag-${t}`)),
1315
- (scenario.tickets ?? []).map((ticket) => /* @__PURE__ */ jsx26("li", { children: ticket.url ? /* @__PURE__ */ jsx26("a", { href: ticket.url, target: "_blank", rel: "noreferrer noopener", children: /* @__PURE__ */ jsx26(Badge, { variant: "outline", children: ticket.id }) }) : /* @__PURE__ */ jsx26(Badge, { variant: "outline", children: ticket.id }) }, `ticket-${ticket.id}`))
1345
+ (scenario.tickets ?? []).map((ticket) => /* @__PURE__ */ jsx26("li", { children: ticket.url ? /* @__PURE__ */ jsx26("a", { href: ticket.url, target: "_blank", rel: "noreferrer noopener", className: "inline-flex min-h-6 items-center", children: /* @__PURE__ */ jsx26(Badge, { variant: "outline", children: ticket.id }) }) : /* @__PURE__ */ jsx26(Badge, { variant: "outline", children: ticket.id }) }, `ticket-${ticket.id}`))
1316
1346
  ] }) : null
1317
1347
  ] }),
1318
1348
  /* @__PURE__ */ jsxs17(CardContent, { id: bodyId, hidden: collapsed, className: "px-4 pt-0 pb-4", children: [
@@ -1455,6 +1485,7 @@ function ReportFeatureList() {
1455
1485
 
1456
1486
  // src/components/ReportMeta.tsx
1457
1487
  import { formatDuration as formatDuration4 } from "executable-stories-core/utils/duration";
1488
+ import { slowestScenarios } from "executable-stories-core/report-queries";
1458
1489
  import { jsx as jsx31, jsxs as jsxs20 } from "react/jsx-runtime";
1459
1490
  function ReportMeta() {
1460
1491
  const report = useReport();
@@ -1498,7 +1529,9 @@ function ReportMeta() {
1498
1529
  value: url ? /* @__PURE__ */ jsx31("a", { href: url, target: "_blank", rel: "noreferrer noopener", className: linkClass, children: label }) : label
1499
1530
  });
1500
1531
  }
1501
- if (items.length === 0) return null;
1532
+ const slowest = slowestScenarios(report, 5);
1533
+ const showSlowest = slowest.length >= 3;
1534
+ if (items.length === 0 && !showSlowest) return null;
1502
1535
  return (
1503
1536
  // Run metadata is useful but secondary to "what failed" — tuck it into a
1504
1537
  // collapsed disclosure so it doesn't push the result down. Native <details>
@@ -1511,7 +1544,14 @@ function ReportMeta() {
1511
1544
  /* @__PURE__ */ jsx31("dl", { className: "flex flex-wrap gap-x-5 gap-y-2 border-t border-border px-4 py-3 text-muted-foreground", "aria-label": "Run metadata", children: items.map((item) => /* @__PURE__ */ jsxs20("div", { className: "flex items-baseline gap-1.5", children: [
1512
1545
  /* @__PURE__ */ jsx31("dt", { className: "font-medium", children: item.label }),
1513
1546
  /* @__PURE__ */ jsx31("dd", { className: "font-mono text-foreground", children: item.value })
1514
- ] }, item.label)) })
1547
+ ] }, item.label)) }),
1548
+ showSlowest ? /* @__PURE__ */ jsxs20("div", { className: "border-t border-border px-4 py-3", children: [
1549
+ /* @__PURE__ */ jsx31("div", { className: "mb-1.5 font-medium text-muted-foreground", children: "Slowest scenarios" }),
1550
+ /* @__PURE__ */ jsx31("ol", { className: "flex flex-col gap-1", "aria-label": "Slowest scenarios", children: slowest.map((s) => /* @__PURE__ */ jsxs20("li", { className: "flex items-baseline justify-between gap-4", children: [
1551
+ /* @__PURE__ */ jsx31("a", { href: `#${s.id}`, className: "min-w-0 text-primary underline underline-offset-2", children: s.title }),
1552
+ /* @__PURE__ */ jsx31("span", { className: "shrink-0 font-mono tabular-nums text-muted-foreground", children: formatDuration4(s.durationMs) })
1553
+ ] }, s.id)) })
1554
+ ] }) : null
1515
1555
  ] })
1516
1556
  );
1517
1557
  }
@@ -1987,21 +2027,26 @@ function allTags(report) {
1987
2027
  }
1988
2028
  return [...seen];
1989
2029
  }
1990
- function queryMatches(scenario, q) {
2030
+ function queryMatches(scenario, q, sourceFile) {
1991
2031
  if (q === "") return true;
1992
2032
  if (scenario.title.toLowerCase().includes(q)) return true;
2033
+ if (sourceFile?.toLowerCase().includes(q)) return true;
2034
+ if (scenario.errorMessage?.toLowerCase().includes(q)) return true;
1993
2035
  for (const tag of scenario.tags) {
1994
2036
  if (tag.toLowerCase().includes(q)) return true;
1995
2037
  }
2038
+ for (const ticket of scenario.tickets ?? []) {
2039
+ if (ticket.id.toLowerCase().includes(q)) return true;
2040
+ }
1996
2041
  for (const step of scenario.steps) {
1997
2042
  if (step.text.toLowerCase().includes(q)) return true;
1998
2043
  }
1999
2044
  return false;
2000
2045
  }
2001
- function scenarioMatches(scenario, q, status, tags) {
2046
+ function scenarioMatches(scenario, q, status, tags, sourceFile) {
2002
2047
  if (status !== "all" && scenario.status !== status) return false;
2003
2048
  if (tags.length > 0 && !tags.some((t) => scenario.tags.includes(t))) return false;
2004
- return queryMatches(scenario, q);
2049
+ return queryMatches(scenario, q, sourceFile);
2005
2050
  }
2006
2051
  function summarizeScenarios(scenarios) {
2007
2052
  let total = 0, passed = 0, failed = 0, skipped = 0, pending = 0, durationMs = 0;
@@ -2024,7 +2069,9 @@ function filterReport(report, criteria) {
2024
2069
  const features = [];
2025
2070
  let topTotal = 0, topPassed = 0, topFailed = 0, topSkipped = 0, topPending = 0, topDuration = 0;
2026
2071
  for (const feature of report.features) {
2027
- const matched = feature.scenarios.filter((s) => scenarioMatches(s, q, status, tags));
2072
+ const matched = feature.scenarios.filter(
2073
+ (s) => scenarioMatches(s, q, status, tags, feature.sourceFile)
2074
+ );
2028
2075
  if (matched.length === 0) continue;
2029
2076
  const summary = summarizeScenarios(matched);
2030
2077
  features.push({ ...feature, summary, scenarios: matched });
@@ -2048,6 +2095,9 @@ function filterReport(report, criteria) {
2048
2095
  features
2049
2096
  };
2050
2097
  }
2098
+ function failedScenarios(report) {
2099
+ return report.features.flatMap((f) => f.scenarios.filter((s) => s.status === "failed"));
2100
+ }
2051
2101
  function listFailures(report) {
2052
2102
  const out = [];
2053
2103
  for (const feature of report.features) {
@@ -2068,17 +2118,77 @@ function listFailures(report) {
2068
2118
  return out;
2069
2119
  }
2070
2120
 
2121
+ // src/interactive/ReportAgentFilterNotice.tsx
2122
+ import { Bot, X } from "lucide-react";
2123
+ import { Fragment as Fragment11, jsx as jsx39, jsxs as jsxs25 } from "react/jsx-runtime";
2124
+ function ReportAgentFilterNotice({
2125
+ applied,
2126
+ onReset,
2127
+ onDismiss
2128
+ }) {
2129
+ const terms = [];
2130
+ if (applied.search) terms.push(`matching \u201C${applied.search}\u201D`);
2131
+ if (applied.status !== "all") terms.push(applied.status);
2132
+ if (applied.tags.length > 0) terms.push(applied.tags.join(", "));
2133
+ return /* @__PURE__ */ jsxs25(
2134
+ "div",
2135
+ {
2136
+ role: "status",
2137
+ "aria-live": "polite",
2138
+ className: "flex flex-wrap items-center gap-x-3 gap-y-1 rounded-md border border-primary/40 bg-primary/5 px-3 py-2 text-sm",
2139
+ children: [
2140
+ /* @__PURE__ */ jsx39(Bot, { "aria-hidden": true, className: "size-4 shrink-0 text-primary" }),
2141
+ /* @__PURE__ */ jsxs25("span", { className: "min-w-0 flex-1", children: [
2142
+ "An agent filtered this report",
2143
+ terms.length > 0 ? /* @__PURE__ */ jsxs25(Fragment11, { children: [
2144
+ " to ",
2145
+ terms.join(" \xB7 ")
2146
+ ] }) : null,
2147
+ " \u2014",
2148
+ " ",
2149
+ /* @__PURE__ */ jsxs25("span", { className: "tabular-nums", children: [
2150
+ applied.matched,
2151
+ " of ",
2152
+ applied.total,
2153
+ " scenarios"
2154
+ ] }),
2155
+ "."
2156
+ ] }),
2157
+ /* @__PURE__ */ jsx39(
2158
+ "button",
2159
+ {
2160
+ type: "button",
2161
+ onClick: onReset,
2162
+ className: "cursor-pointer rounded-md border border-border px-2 py-1 text-xs hover:bg-muted",
2163
+ children: "Show all"
2164
+ }
2165
+ ),
2166
+ /* @__PURE__ */ jsx39(
2167
+ "button",
2168
+ {
2169
+ type: "button",
2170
+ onClick: onDismiss,
2171
+ "aria-label": "Dismiss",
2172
+ className: "cursor-pointer rounded-md p-1 text-muted-foreground hover:text-foreground",
2173
+ children: /* @__PURE__ */ jsx39(X, { "aria-hidden": true, className: "size-4" })
2174
+ }
2175
+ )
2176
+ ]
2177
+ }
2178
+ );
2179
+ }
2180
+
2071
2181
  // src/interactive/ReportFailureBanner.tsx
2072
2182
  import { useCallback as useCallback2 } from "react";
2073
- import { jsx as jsx39, jsxs as jsxs25 } from "react/jsx-runtime";
2074
- function ReportFailureBanner({ failures }) {
2183
+ import { jsx as jsx40, jsxs as jsxs26 } from "react/jsx-runtime";
2184
+ function ReportFailureBanner({ failures, onCopyAll }) {
2075
2185
  const first = failures[0];
2076
2186
  const jumpToFirst = useCallback2(() => {
2077
2187
  if (!first) return;
2078
2188
  scrollToScenarioId(first.scenarioId);
2079
2189
  }, [first]);
2080
2190
  if (failures.length === 0) return null;
2081
- return /* @__PURE__ */ jsxs25(
2191
+ return /* @__PURE__ */ jsxs26(
2082
2192
  "div",
2083
2193
  {
2084
2194
  className: "es-failure-banner",
@@ -2086,13 +2196,13 @@ function ReportFailureBanner({ failures }) {
2086
2196
  "aria-live": "polite",
2087
2197
  "aria-label": "Failure summary",
2088
2198
  children: [
2089
- /* @__PURE__ */ jsxs25("span", { className: "es-failure-banner-text", children: [
2090
- /* @__PURE__ */ jsx39("strong", { children: failures.length }),
2199
+ /* @__PURE__ */ jsxs26("span", { className: "es-failure-banner-text", children: [
2200
+ /* @__PURE__ */ jsx40("strong", { children: failures.length }),
2091
2201
  " ",
2092
2202
  "failure",
2093
2203
  failures.length === 1 ? "" : "s"
2094
2204
  ] }),
2095
- /* @__PURE__ */ jsx39(
2205
+ /* @__PURE__ */ jsx40(
2096
2206
  "button",
2097
2207
  {
2098
2208
  type: "button",
@@ -2101,7 +2211,23 @@ function ReportFailureBanner({ failures }) {
2101
2211
  "aria-label": "View first failure",
2102
2212
  children: "View first failure \u2193"
2103
2213
  }
2104
- )
2214
+ ),
2215
+ onCopyAll ? /* @__PURE__ */ jsxs26(
2216
+ "button",
2217
+ {
2218
+ type: "button",
2219
+ className: "es-failure-banner-jump",
2220
+ onClick: onCopyAll,
2221
+ "aria-label": "Copy failures for an agent",
2222
+ children: [
2223
+ "Copy ",
2224
+ failures.length,
2225
+ " failure",
2226
+ failures.length === 1 ? "" : "s",
2227
+ " for an agent"
2228
+ ]
2229
+ }
2230
+ ) : null
2105
2231
  ]
2106
2232
  }
2107
2233
  );
@@ -2109,7 +2235,7 @@ function ReportFailureBanner({ failures }) {
2109
2235
 
2110
2236
  // src/interactive/ReportFilters.tsx
2111
2237
  import { useState as useState3 } from "react";
2112
- import { jsx as jsx40, jsxs as jsxs26 } from "react/jsx-runtime";
2238
+ import { jsx as jsx41, jsxs as jsxs27 } from "react/jsx-runtime";
2113
2239
  var TAG_LIMIT = 12;
2114
2240
  function ReportFilters({
2115
2241
  statuses,
@@ -2123,10 +2249,10 @@ function ReportFilters({
2123
2249
  if (statuses.length === 0 && tags.length === 0) return null;
2124
2250
  const tagsCollapsed = tags.length > TAG_LIMIT && !showAllTags;
2125
2251
  const visibleTags = tagsCollapsed ? tags.slice(0, TAG_LIMIT) : tags;
2126
- return /* @__PURE__ */ jsxs26("div", { className: "flex flex-col gap-2", children: [
2127
- statuses.length > 0 ? /* @__PURE__ */ jsx40("div", { role: "group", "aria-label": "Filter by status", className: "flex flex-wrap gap-1", children: statuses.map((opt) => {
2252
+ return /* @__PURE__ */ jsxs27("div", { className: "flex flex-col gap-2", children: [
2253
+ statuses.length > 0 ? /* @__PURE__ */ jsx41("div", { role: "group", "aria-label": "Filter by status", className: "flex flex-wrap gap-1", children: statuses.map((opt) => {
2128
2254
  const active = status === opt.key;
2129
- return /* @__PURE__ */ jsxs26(
2255
+ return /* @__PURE__ */ jsxs27(
2130
2256
  "button",
2131
2257
  {
2132
2258
  type: "button",
@@ -2138,23 +2264,23 @@ function ReportFilters({
2138
2264
  ),
2139
2265
  children: [
2140
2266
  opt.label,
2141
- /* @__PURE__ */ jsx40("span", { className: "ml-1 font-mono opacity-70", children: opt.count })
2267
+ /* @__PURE__ */ jsx41("span", { className: "ml-1 font-mono opacity-70", children: opt.count })
2142
2268
  ]
2143
2269
  },
2144
2270
  opt.key
2145
2271
  );
2146
2272
  }) }) : null,
2147
- tags.length > 0 ? /* @__PURE__ */ jsxs26("ul", { "aria-label": "Filter by tag", className: "flex flex-wrap items-center gap-1.5", children: [
2273
+ tags.length > 0 ? /* @__PURE__ */ jsxs27("ul", { "aria-label": "Filter by tag", className: "flex flex-wrap items-center gap-1.5", children: [
2148
2274
  visibleTags.map((tag) => {
2149
2275
  const active = activeTags.includes(tag);
2150
- return /* @__PURE__ */ jsx40("li", { children: /* @__PURE__ */ jsx40(
2276
+ return /* @__PURE__ */ jsx41("li", { children: /* @__PURE__ */ jsx41(
2151
2277
  "button",
2152
2278
  {
2153
2279
  type: "button",
2154
2280
  "aria-pressed": active,
2155
2281
  onClick: () => onToggleTag(tag),
2156
2282
  className: "cursor-pointer",
2157
- children: /* @__PURE__ */ jsx40(
2283
+ children: /* @__PURE__ */ jsx41(
2158
2284
  Badge,
2159
2285
  {
2160
2286
  variant: "tag",
@@ -2167,7 +2293,7 @@ function ReportFilters({
2167
2293
  }
2168
2294
  ) }, tag);
2169
2295
  }),
2170
- tags.length > TAG_LIMIT ? /* @__PURE__ */ jsx40("li", { children: /* @__PURE__ */ jsx40(
2296
+ tags.length > TAG_LIMIT ? /* @__PURE__ */ jsx41("li", { children: /* @__PURE__ */ jsx41(
2171
2297
  "button",
2172
2298
  {
2173
2299
  type: "button",
@@ -2183,7 +2309,7 @@ function ReportFilters({
2183
2309
 
2184
2310
  // src/interactive/ReportFreshness.tsx
2185
2311
  import { useEffect as useEffect2, useState as useState4 } from "react";
2186
- import { Fragment as Fragment11, jsx as jsx41, jsxs as jsxs27 } from "react/jsx-runtime";
2312
+ import { Fragment as Fragment12, jsx as jsx42, jsxs as jsxs28 } from "react/jsx-runtime";
2187
2313
  function ReportFreshness({ lastRunMs, ciUrl, staleAfterDays, nowMs }) {
2188
2314
  const [now, setNow] = useState4(nowMs);
2189
2315
  useEffect2(() => {
@@ -2191,7 +2317,7 @@ function ReportFreshness({ lastRunMs, ciUrl, staleAfterDays, nowMs }) {
2191
2317
  }, [nowMs]);
2192
2318
  if (lastRunMs === void 0 || now === void 0) return null;
2193
2319
  const age = formatRelativeAge(lastRunMs, now);
2194
- const runLink = ciUrl ? /* @__PURE__ */ jsx41(
2320
+ const runLink = ciUrl ? /* @__PURE__ */ jsx42(
2195
2321
  "a",
2196
2322
  {
2197
2323
  href: ciUrl,
@@ -2202,9 +2328,9 @@ function ReportFreshness({ lastRunMs, ciUrl, staleAfterDays, nowMs }) {
2202
2328
  }
2203
2329
  ) : null;
2204
2330
  if (isReportStale(lastRunMs, staleAfterDays, now)) {
2205
- return /* @__PURE__ */ jsxs27("div", { className: "es-stale-banner", role: "status", "aria-label": "Report freshness", children: [
2206
- /* @__PURE__ */ jsxs27("span", { children: [
2207
- /* @__PURE__ */ jsxs27("strong", { children: [
2331
+ return /* @__PURE__ */ jsxs28("div", { className: "es-stale-banner", role: "status", "aria-label": "Report freshness", children: [
2332
+ /* @__PURE__ */ jsxs28("span", { children: [
2333
+ /* @__PURE__ */ jsxs28("strong", { children: [
2208
2334
  "Last verified ",
2209
2335
  age,
2210
2336
  "."
@@ -2214,10 +2340,10 @@ function ReportFreshness({ lastRunMs, ciUrl, staleAfterDays, nowMs }) {
2214
2340
  runLink
2215
2341
  ] });
2216
2342
  }
2217
- return /* @__PURE__ */ jsxs27("p", { className: "es-freshness", "aria-label": "Report freshness", children: [
2343
+ return /* @__PURE__ */ jsxs28("p", { className: "es-freshness", "aria-label": "Report freshness", children: [
2218
2344
  "Verified ",
2219
2345
  age,
2220
- runLink ? /* @__PURE__ */ jsxs27(Fragment11, { children: [
2346
+ runLink ? /* @__PURE__ */ jsxs28(Fragment12, { children: [
2221
2347
  " \xB7 ",
2222
2348
  runLink
2223
2349
  ] }) : null
@@ -2225,15 +2351,15 @@ function ReportFreshness({ lastRunMs, ciUrl, staleAfterDays, nowMs }) {
2225
2351
  }
2226
2352
 
2227
2353
  // src/interactive/ReportLastRunDelta.tsx
2228
- import { Fragment as Fragment12, jsx as jsx42, jsxs as jsxs28 } from "react/jsx-runtime";
2354
+ import { Fragment as Fragment13, jsx as jsx43, jsxs as jsxs29 } from "react/jsx-runtime";
2229
2355
  var MAX_LINKS = 5;
2230
2356
  function ScenarioLinks({ scenarios, tone }) {
2231
- return /* @__PURE__ */ jsxs28(Fragment12, { children: [
2232
- scenarios.slice(0, MAX_LINKS).map((s, i) => /* @__PURE__ */ jsxs28("span", { children: [
2357
+ return /* @__PURE__ */ jsxs29(Fragment13, { children: [
2358
+ scenarios.slice(0, MAX_LINKS).map((s, i) => /* @__PURE__ */ jsxs29("span", { children: [
2233
2359
  i > 0 ? ", " : "",
2234
- /* @__PURE__ */ jsx42("a", { href: `#${s.id}`, className: cn("underline underline-offset-2 hover:no-underline", tone), children: s.title })
2360
+ /* @__PURE__ */ jsx43("a", { href: `#${s.id}`, className: cn("underline underline-offset-2 hover:no-underline", tone), children: s.title })
2235
2361
  ] }, s.id)),
2236
- scenarios.length > MAX_LINKS ? /* @__PURE__ */ jsxs28("span", { children: [
2362
+ scenarios.length > MAX_LINKS ? /* @__PURE__ */ jsxs29("span", { children: [
2237
2363
  " and ",
2238
2364
  scenarios.length - MAX_LINKS,
2239
2365
  " more"
@@ -2250,23 +2376,23 @@ function ReportLastRunDelta({
2250
2376
  if (!delta) return null;
2251
2377
  const { newlyFailing, newlyPassing, added } = delta;
2252
2378
  if (newlyFailing.length === 0 && newlyPassing.length === 0 && added.length === 0) {
2253
- return /* @__PURE__ */ jsx42("p", { className: "text-xs text-muted-foreground", "data-testid": "es-last-run-delta", children: "Since last run: no behavior changes." });
2379
+ return /* @__PURE__ */ jsx43("p", { className: "text-xs text-muted-foreground", "data-testid": "es-last-run-delta", children: "Since last run: no behavior changes." });
2254
2380
  }
2255
- return /* @__PURE__ */ jsxs28("p", { className: "text-xs text-muted-foreground", "data-testid": "es-last-run-delta", children: [
2256
- /* @__PURE__ */ jsx42("span", { className: "font-medium text-foreground", children: "Since last run:" }),
2257
- newlyFailing.length > 0 ? /* @__PURE__ */ jsxs28("span", { className: "ml-2 text-fail", children: [
2381
+ return /* @__PURE__ */ jsxs29("p", { className: "text-xs text-muted-foreground", "data-testid": "es-last-run-delta", children: [
2382
+ /* @__PURE__ */ jsx43("span", { className: "font-medium text-foreground", children: "Since last run:" }),
2383
+ newlyFailing.length > 0 ? /* @__PURE__ */ jsxs29("span", { className: "ml-2 text-fail", children: [
2258
2384
  newlyFailing.length,
2259
2385
  " newly failing (",
2260
- /* @__PURE__ */ jsx42(ScenarioLinks, { scenarios: newlyFailing, tone: "text-fail" }),
2386
+ /* @__PURE__ */ jsx43(ScenarioLinks, { scenarios: newlyFailing, tone: "text-fail" }),
2261
2387
  ")"
2262
2388
  ] }) : null,
2263
- newlyPassing.length > 0 ? /* @__PURE__ */ jsxs28("span", { className: "ml-2 text-pass", children: [
2389
+ newlyPassing.length > 0 ? /* @__PURE__ */ jsxs29("span", { className: "ml-2 text-pass", children: [
2264
2390
  newlyPassing.length,
2265
2391
  " fixed (",
2266
- /* @__PURE__ */ jsx42(ScenarioLinks, { scenarios: newlyPassing, tone: "text-pass" }),
2392
+ /* @__PURE__ */ jsx43(ScenarioLinks, { scenarios: newlyPassing, tone: "text-pass" }),
2267
2393
  ")"
2268
2394
  ] }) : null,
2269
- added.length > 0 ? /* @__PURE__ */ jsxs28("span", { className: "ml-2", children: [
2395
+ added.length > 0 ? /* @__PURE__ */ jsxs29("span", { className: "ml-2", children: [
2270
2396
  added.length,
2271
2397
  " new"
2272
2398
  ] }) : null
@@ -2282,9 +2408,9 @@ import { Search } from "lucide-react";
2282
2408
 
2283
2409
  // src/components/ui/input.tsx
2284
2410
  import "react";
2285
- import { jsx as jsx43 } from "react/jsx-runtime";
2411
+ import { jsx as jsx44 } from "react/jsx-runtime";
2286
2412
  function Input({ className, type, ...props }) {
2287
- return /* @__PURE__ */ jsx43(
2413
+ return /* @__PURE__ */ jsx44(
2288
2414
  "input",
2289
2415
  {
2290
2416
  type,
@@ -2301,7 +2427,7 @@ function Input({ className, type, ...props }) {
2301
2427
  }
2302
2428
 
2303
2429
  // src/interactive/ReportSearch.tsx
2304
- import { jsx as jsx44, jsxs as jsxs29 } from "react/jsx-runtime";
2430
+ import { jsx as jsx45, jsxs as jsxs30 } from "react/jsx-runtime";
2305
2431
  var ReportSearch = forwardRef(
2306
2432
  function ReportSearch2(props, ref) {
2307
2433
  const {
@@ -2309,7 +2435,7 @@ var ReportSearch = forwardRef(
2309
2435
  onChange,
2310
2436
  matchedCount,
2311
2437
  totalCount,
2312
- placeholder = "Search scenarios or tags\u2026",
2438
+ placeholder = "Search scenarios, tags, tickets, errors\u2026",
2313
2439
  className
2314
2440
  } = props;
2315
2441
  const inputId = useId();
@@ -2323,17 +2449,17 @@ var ReportSearch = forwardRef(
2323
2449
  e.preventDefault();
2324
2450
  }
2325
2451
  }
2326
- return /* @__PURE__ */ jsxs29("div", { className: cn("es-search flex flex-col gap-1", className), children: [
2327
- /* @__PURE__ */ jsx44("label", { htmlFor: inputId, className: "sr-only", children: "Search" }),
2328
- /* @__PURE__ */ jsxs29("div", { className: "relative w-full sm:w-72", children: [
2329
- /* @__PURE__ */ jsx44(
2452
+ return /* @__PURE__ */ jsxs30("div", { className: cn("es-search flex flex-col gap-1", className), children: [
2453
+ /* @__PURE__ */ jsx45("label", { htmlFor: inputId, className: "sr-only", children: "Search" }),
2454
+ /* @__PURE__ */ jsxs30("div", { className: "relative w-full sm:w-72", children: [
2455
+ /* @__PURE__ */ jsx45(
2330
2456
  Search,
2331
2457
  {
2332
2458
  "aria-hidden": true,
2333
2459
  className: "pointer-events-none absolute top-1/2 left-2.5 size-4 -translate-y-1/2 text-muted-foreground"
2334
2460
  }
2335
2461
  ),
2336
- /* @__PURE__ */ jsx44(
2462
+ /* @__PURE__ */ jsx45(
2337
2463
  Input,
2338
2464
  {
2339
2465
  ref,
@@ -2350,7 +2476,7 @@ var ReportSearch = forwardRef(
2350
2476
  }
2351
2477
  )
2352
2478
  ] }),
2353
- showCounts ? /* @__PURE__ */ jsxs29(
2479
+ showCounts ? /* @__PURE__ */ jsxs30(
2354
2480
  "span",
2355
2481
  {
2356
2482
  className: "text-xs tabular-nums text-muted-foreground",
@@ -2367,8 +2493,118 @@ var ReportSearch = forwardRef(
2367
2493
  }
2368
2494
  );
2369
2495
 
2496
+ // src/interactive/ReportSinceLastVisit.tsx
2497
+ import { useEffect as useEffect3, useState as useState5 } from "react";
2498
+
2499
+ // src/interactive/last-visit.ts
2500
+ function snapshotOf(report, atMs) {
2501
+ const statuses = {};
2502
+ for (const feature of report.features) {
2503
+ for (const scenario of feature.scenarios) {
2504
+ statuses[scenario.id] = scenario.status;
2505
+ }
2506
+ }
2507
+ return { runId: report.runId, atMs, statuses };
2508
+ }
2509
+ function diffSinceVisit(previous, report) {
2510
+ if (!previous) return null;
2511
+ if (previous.runId === report.runId) return null;
2512
+ const newlyFailing = [];
2513
+ const newlyPassing = [];
2514
+ const added = [];
2515
+ for (const feature of report.features) {
2516
+ for (const scenario of feature.scenarios) {
2517
+ const ref = { id: scenario.id, title: scenario.title };
2518
+ const before = previous.statuses[scenario.id];
2519
+ if (before === void 0) {
2520
+ added.push(ref);
2521
+ } else if (before !== "failed" && scenario.status === "failed") {
2522
+ newlyFailing.push(ref);
2523
+ } else if (before === "failed" && scenario.status === "passed") {
2524
+ newlyPassing.push(ref);
2525
+ }
2526
+ }
2527
+ }
2528
+ if (newlyFailing.length === 0 && newlyPassing.length === 0 && added.length === 0) return null;
2529
+ return { sinceMs: previous.atMs, newlyFailing, newlyPassing, added };
2530
+ }
2531
+
2532
+ // src/interactive/ReportSinceLastVisit.tsx
2533
+ import { Fragment as Fragment14, jsx as jsx46, jsxs as jsxs31 } from "react/jsx-runtime";
2534
+ var KEY = "es-last-visit";
2535
+ var MAX_LINKS2 = 5;
2536
+ function readVisit() {
2537
+ try {
2538
+ const raw = localStorage.getItem(KEY);
2539
+ if (!raw) return null;
2540
+ const parsed = JSON.parse(raw);
2541
+ if (typeof parsed !== "object" || parsed === null) return null;
2542
+ const v = parsed;
2543
+ if (typeof v.runId !== "string" || typeof v.atMs !== "number" || typeof v.statuses !== "object") {
2544
+ return null;
2545
+ }
2546
+ return { runId: v.runId, atMs: v.atMs, statuses: v.statuses };
2547
+ } catch {
2548
+ return null;
2549
+ }
2550
+ }
2551
+ function writeVisit(snapshot) {
2552
+ try {
2553
+ localStorage.setItem(KEY, JSON.stringify(snapshot));
2554
+ } catch {
2555
+ }
2556
+ }
2557
+ function ScenarioLinks2({ scenarios, tone }) {
2558
+ return /* @__PURE__ */ jsxs31(Fragment14, { children: [
2559
+ scenarios.slice(0, MAX_LINKS2).map((s, i) => /* @__PURE__ */ jsxs31("span", { children: [
2560
+ i > 0 ? ", " : "",
2561
+ /* @__PURE__ */ jsx46("a", { href: `#${s.id}`, className: cn("underline underline-offset-2 hover:no-underline", tone), children: s.title })
2562
+ ] }, s.id)),
2563
+ scenarios.length > MAX_LINKS2 ? /* @__PURE__ */ jsxs31("span", { children: [
2564
+ " and ",
2565
+ scenarios.length - MAX_LINKS2,
2566
+ " more"
2567
+ ] }) : null
2568
+ ] });
2569
+ }
2570
+ function ReportSinceLastVisit({ report }) {
2571
+ const [delta, setDelta] = useState5(null);
2572
+ const [nowMs, setNowMs] = useState5(0);
2573
+ useEffect3(() => {
2574
+ const now = Date.now();
2575
+ setDelta(diffSinceVisit(readVisit(), report));
2576
+ setNowMs(now);
2577
+ writeVisit(snapshotOf(report, now));
2578
+ }, [report]);
2579
+ if (!delta) return null;
2580
+ const { newlyFailing, newlyPassing, added } = delta;
2581
+ return /* @__PURE__ */ jsxs31("p", { className: "text-xs text-muted-foreground", "data-testid": "es-since-last-visit", children: [
2582
+ /* @__PURE__ */ jsxs31("span", { className: "font-medium text-foreground", children: [
2583
+ "Since your last visit (",
2584
+ formatRelativeAge(delta.sinceMs, nowMs),
2585
+ "):"
2586
+ ] }),
2587
+ newlyFailing.length > 0 ? /* @__PURE__ */ jsxs31("span", { className: "ml-2 text-fail", children: [
2588
+ newlyFailing.length,
2589
+ " started failing (",
2590
+ /* @__PURE__ */ jsx46(ScenarioLinks2, { scenarios: newlyFailing, tone: "text-fail" }),
2591
+ ")"
2592
+ ] }) : null,
2593
+ newlyPassing.length > 0 ? /* @__PURE__ */ jsxs31("span", { className: "ml-2 text-pass", children: [
2594
+ newlyPassing.length,
2595
+ " fixed (",
2596
+ /* @__PURE__ */ jsx46(ScenarioLinks2, { scenarios: newlyPassing, tone: "text-pass" }),
2597
+ ")"
2598
+ ] }) : null,
2599
+ added.length > 0 ? /* @__PURE__ */ jsxs31("span", { className: "ml-2", children: [
2600
+ added.length,
2601
+ " new"
2602
+ ] }) : null
2603
+ ] });
2604
+ }
2605
+
2370
2606
  // src/interactive/ReportShortcutsHelp.tsx
2371
- import { useEffect as useEffect3, useRef } from "react";
2607
+ import { useEffect as useEffect4, useRef } from "react";
2372
2608
 
2373
2609
  // src/components/ui/button.tsx
2374
2610
  import "react";
@@ -2424,7 +2660,7 @@ function Button({
2424
2660
  }
2425
2661
 
2426
2662
  // src/interactive/ReportShortcutsHelp.tsx
2427
- import { jsx as jsx45, jsxs as jsxs30 } from "react/jsx-runtime";
2663
+ import { jsx as jsx47, jsxs as jsxs32 } from "react/jsx-runtime";
2428
2664
  var SHORTCUTS = [
2429
2665
  { keys: "/", description: "Focus search" },
2430
2666
  { keys: "f", description: "Jump to next failure" },
@@ -2436,20 +2672,20 @@ var SHORTCUTS = [
2436
2672
  ];
2437
2673
  function Keys({ keys }) {
2438
2674
  const parts = keys.split(" ");
2439
- return /* @__PURE__ */ jsx45("span", { className: "inline-flex items-center gap-1", children: parts.map((k, i) => /* @__PURE__ */ jsxs30("span", { className: "inline-flex items-center gap-1", children: [
2440
- i > 0 ? /* @__PURE__ */ jsx45("span", { className: "text-xs text-muted-foreground", children: "+" }) : null,
2441
- /* @__PURE__ */ jsx45("kbd", { className: "inline-flex h-6 min-w-6 items-center justify-center rounded border border-border bg-muted px-1.5 font-mono text-xs font-medium text-muted-foreground", children: k })
2675
+ return /* @__PURE__ */ jsx47("span", { className: "inline-flex items-center gap-1", children: parts.map((k, i) => /* @__PURE__ */ jsxs32("span", { className: "inline-flex items-center gap-1", children: [
2676
+ i > 0 ? /* @__PURE__ */ jsx47("span", { className: "text-xs text-muted-foreground", children: "+" }) : null,
2677
+ /* @__PURE__ */ jsx47("kbd", { className: "inline-flex h-6 min-w-6 items-center justify-center rounded border border-border bg-muted px-1.5 font-mono text-xs font-medium text-muted-foreground", children: k })
2442
2678
  ] }, k)) });
2443
2679
  }
2444
2680
  function ReportShortcutsHelp({ open, onClose }) {
2445
2681
  const dialogRef = useRef(null);
2446
- useEffect3(() => {
2682
+ useEffect4(() => {
2447
2683
  const dialog = dialogRef.current;
2448
2684
  if (!dialog) return;
2449
2685
  if (open && !dialog.open) dialog.showModal();
2450
2686
  else if (!open && dialog.open) dialog.close();
2451
2687
  }, [open]);
2452
- return /* @__PURE__ */ jsxs30(
2688
+ return /* @__PURE__ */ jsxs32(
2453
2689
  "dialog",
2454
2690
  {
2455
2691
  ref: dialogRef,
@@ -2457,20 +2693,20 @@ function ReportShortcutsHelp({ open, onClose }) {
2457
2693
  "aria-label": "Keyboard shortcuts",
2458
2694
  onClose,
2459
2695
  children: [
2460
- /* @__PURE__ */ jsx45("h2", { className: "text-base font-semibold", children: "Keyboard shortcuts" }),
2461
- /* @__PURE__ */ jsx45("dl", { className: "mt-4 grid grid-cols-[auto_1fr] items-center gap-x-4 gap-y-2.5", children: SHORTCUTS.map((s) => /* @__PURE__ */ jsxs30("div", { className: "col-span-2 grid grid-cols-subgrid items-center", children: [
2462
- /* @__PURE__ */ jsx45("dt", { className: "justify-self-start", children: /* @__PURE__ */ jsx45(Keys, { keys: s.keys }) }),
2463
- /* @__PURE__ */ jsx45("dd", { className: "text-sm text-muted-foreground", children: s.description })
2696
+ /* @__PURE__ */ jsx47("h2", { className: "text-base font-semibold", children: "Keyboard shortcuts" }),
2697
+ /* @__PURE__ */ jsx47("dl", { className: "mt-4 grid grid-cols-[auto_1fr] items-center gap-x-4 gap-y-2.5", children: SHORTCUTS.map((s) => /* @__PURE__ */ jsxs32("div", { className: "col-span-2 grid grid-cols-subgrid items-center", children: [
2698
+ /* @__PURE__ */ jsx47("dt", { className: "justify-self-start", children: /* @__PURE__ */ jsx47(Keys, { keys: s.keys }) }),
2699
+ /* @__PURE__ */ jsx47("dd", { className: "text-sm text-muted-foreground", children: s.description })
2464
2700
  ] }, s.keys)) }),
2465
- /* @__PURE__ */ jsx45("form", { method: "dialog", className: "mt-6", children: /* @__PURE__ */ jsx45(Button, { type: "submit", variant: "outline", size: "sm", children: "Close" }) })
2701
+ /* @__PURE__ */ jsx47("form", { method: "dialog", className: "mt-6", children: /* @__PURE__ */ jsx47(Button, { type: "submit", variant: "outline", size: "sm", children: "Close" }) })
2466
2702
  ]
2467
2703
  }
2468
2704
  );
2469
2705
  }
2470
2706
 
2471
2707
  // src/interactive/ReportToc.tsx
2472
- import { useEffect as useEffect4, useMemo as useMemo2, useState as useState5 } from "react";
2473
- import { jsx as jsx46, jsxs as jsxs31 } from "react/jsx-runtime";
2708
+ import { useEffect as useEffect5, useMemo as useMemo2, useState as useState6 } from "react";
2709
+ import { jsx as jsx48, jsxs as jsxs33 } from "react/jsx-runtime";
2474
2710
  var DOT = {
2475
2711
  passed: "bg-pass",
2476
2712
  failed: "bg-fail",
@@ -2479,12 +2715,12 @@ var DOT = {
2479
2715
  };
2480
2716
  function TocContent({ onNavigate }) {
2481
2717
  const report = useReport();
2482
- const [activeId, setActiveId] = useState5(null);
2718
+ const [activeId, setActiveId] = useState6(null);
2483
2719
  const scenarioIds = useMemo2(
2484
2720
  () => report.features.flatMap((f) => f.scenarios.map((s) => s.id)),
2485
2721
  [report]
2486
2722
  );
2487
- useEffect4(() => {
2723
+ useEffect5(() => {
2488
2724
  if (typeof IntersectionObserver === "undefined") return;
2489
2725
  const els = scenarioIds.map((id) => document.getElementById(id)).filter((el) => el !== null);
2490
2726
  if (els.length === 0) return;
@@ -2499,11 +2735,11 @@ function TocContent({ onNavigate }) {
2499
2735
  return () => observer.disconnect();
2500
2736
  }, [scenarioIds]);
2501
2737
  if (report.features.length === 0) return null;
2502
- return /* @__PURE__ */ jsx46("ul", { className: "flex list-none flex-col gap-4 pl-0", children: report.features.map((feature) => /* @__PURE__ */ jsxs31("li", { children: [
2503
- /* @__PURE__ */ jsx46("p", { className: "mb-1.5 font-semibold leading-snug text-foreground", children: feature.title }),
2504
- /* @__PURE__ */ jsx46("ul", { className: "flex list-none flex-col gap-0.5 pl-0", children: feature.scenarios.map((scenario) => {
2738
+ return /* @__PURE__ */ jsx48("ul", { className: "flex list-none flex-col gap-4 pl-0", children: report.features.map((feature) => /* @__PURE__ */ jsxs33("li", { children: [
2739
+ /* @__PURE__ */ jsx48("p", { className: "mb-1.5 font-semibold leading-snug text-foreground", children: feature.title }),
2740
+ /* @__PURE__ */ jsx48("ul", { className: "flex list-none flex-col gap-0.5 pl-0", children: feature.scenarios.map((scenario) => {
2505
2741
  const active = activeId === scenario.id;
2506
- return /* @__PURE__ */ jsx46("li", { children: /* @__PURE__ */ jsxs31(
2742
+ return /* @__PURE__ */ jsx48("li", { children: /* @__PURE__ */ jsxs33(
2507
2743
  "a",
2508
2744
  {
2509
2745
  href: `#${scenario.id}`,
@@ -2518,14 +2754,14 @@ function TocContent({ onNavigate }) {
2518
2754
  active ? "border-primary font-medium text-foreground" : "border-border text-muted-foreground"
2519
2755
  ),
2520
2756
  children: [
2521
- /* @__PURE__ */ jsx46(
2757
+ /* @__PURE__ */ jsx48(
2522
2758
  "span",
2523
2759
  {
2524
2760
  "aria-hidden": true,
2525
2761
  className: cn("mt-1 size-1.5 shrink-0 rounded-full", DOT[scenario.status])
2526
2762
  }
2527
2763
  ),
2528
- /* @__PURE__ */ jsx46("span", { className: "break-words", children: scenario.title })
2764
+ /* @__PURE__ */ jsx48("span", { className: "break-words", children: scenario.title })
2529
2765
  ]
2530
2766
  }
2531
2767
  ) }, scenario.id);
@@ -2535,48 +2771,48 @@ function TocContent({ onNavigate }) {
2535
2771
  function ReportToc() {
2536
2772
  const report = useReport();
2537
2773
  if (report.features.length === 0) return null;
2538
- return /* @__PURE__ */ jsxs31(
2774
+ return /* @__PURE__ */ jsxs33(
2539
2775
  "nav",
2540
2776
  {
2541
2777
  "aria-label": "Table of contents",
2542
2778
  className: "sticky top-4 hidden h-fit w-64 shrink-0 self-start text-xs lg:block",
2543
2779
  children: [
2544
- /* @__PURE__ */ jsx46("p", { className: "mb-3 text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: "Contents" }),
2545
- /* @__PURE__ */ jsx46(TocContent, {})
2780
+ /* @__PURE__ */ jsx48("p", { className: "mb-3 text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: "Contents" }),
2781
+ /* @__PURE__ */ jsx48(TocContent, {})
2546
2782
  ]
2547
2783
  }
2548
2784
  );
2549
2785
  }
2550
2786
 
2551
2787
  // src/interactive/ReportTocDrawer.tsx
2552
- import { useState as useState6 } from "react";
2788
+ import { useState as useState7 } from "react";
2553
2789
  import { Dialog as DialogPrimitive } from "@base-ui/react/dialog";
2554
2790
  import { ListIcon, XIcon } from "lucide-react";
2555
- import { jsx as jsx47, jsxs as jsxs32 } from "react/jsx-runtime";
2791
+ import { jsx as jsx49, jsxs as jsxs34 } from "react/jsx-runtime";
2556
2792
  function ReportTocDrawer() {
2557
2793
  const report = useReport();
2558
- const [open, setOpen] = useState6(false);
2794
+ const [open, setOpen] = useState7(false);
2559
2795
  if (report.features.length === 0) return null;
2560
- return /* @__PURE__ */ jsxs32(DialogPrimitive.Root, { open, onOpenChange: setOpen, children: [
2561
- /* @__PURE__ */ jsxs32(DialogPrimitive.Trigger, { className: "flex cursor-pointer items-center gap-1 rounded-md border border-border px-2 py-1 text-xs text-muted-foreground hover:bg-muted hover:text-foreground lg:hidden [&_svg]:size-3.5", children: [
2562
- /* @__PURE__ */ jsx47(ListIcon, { "aria-hidden": true }),
2796
+ return /* @__PURE__ */ jsxs34(DialogPrimitive.Root, { open, onOpenChange: setOpen, children: [
2797
+ /* @__PURE__ */ jsxs34(DialogPrimitive.Trigger, { className: "flex cursor-pointer items-center gap-1 rounded-md border border-border px-2 py-1 text-xs text-muted-foreground hover:bg-muted hover:text-foreground lg:hidden [&_svg]:size-3.5", children: [
2798
+ /* @__PURE__ */ jsx49(ListIcon, { "aria-hidden": true }),
2563
2799
  " Contents"
2564
2800
  ] }),
2565
- /* @__PURE__ */ jsxs32(DialogPrimitive.Portal, { children: [
2566
- /* @__PURE__ */ jsx47(DialogPrimitive.Backdrop, { className: "fixed inset-0 z-50 bg-black/50 transition-opacity duration-200 data-[ending-style]:opacity-0 data-[starting-style]:opacity-0" }),
2567
- /* @__PURE__ */ jsxs32(DialogPrimitive.Popup, { className: "fixed inset-y-0 left-0 z-50 flex w-72 max-w-[80vw] flex-col gap-3 overflow-y-auto border-r border-border bg-background p-4 font-sans text-xs shadow-lg outline-none transition-transform duration-200 data-[ending-style]:-translate-x-full data-[starting-style]:-translate-x-full", children: [
2568
- /* @__PURE__ */ jsxs32("div", { className: "flex items-center justify-between", children: [
2569
- /* @__PURE__ */ jsx47(DialogPrimitive.Title, { className: "text-xs font-semibold tracking-wide text-muted-foreground uppercase", children: "Contents" }),
2570
- /* @__PURE__ */ jsx47(
2801
+ /* @__PURE__ */ jsxs34(DialogPrimitive.Portal, { children: [
2802
+ /* @__PURE__ */ jsx49(DialogPrimitive.Backdrop, { className: "fixed inset-0 z-50 bg-black/50 transition-opacity duration-200 data-[ending-style]:opacity-0 data-[starting-style]:opacity-0" }),
2803
+ /* @__PURE__ */ jsxs34(DialogPrimitive.Popup, { className: "fixed inset-y-0 left-0 z-50 flex w-72 max-w-[80vw] flex-col gap-3 overflow-y-auto border-r border-border bg-background p-4 font-sans text-xs shadow-lg outline-none transition-transform duration-200 data-[ending-style]:-translate-x-full data-[starting-style]:-translate-x-full", children: [
2804
+ /* @__PURE__ */ jsxs34("div", { className: "flex items-center justify-between", children: [
2805
+ /* @__PURE__ */ jsx49(DialogPrimitive.Title, { className: "text-xs font-semibold tracking-wide text-muted-foreground uppercase", children: "Contents" }),
2806
+ /* @__PURE__ */ jsx49(
2571
2807
  DialogPrimitive.Close,
2572
2808
  {
2573
2809
  "aria-label": "Close contents",
2574
2810
  className: "cursor-pointer rounded p-1 text-muted-foreground hover:bg-muted hover:text-foreground [&_svg]:size-4",
2575
- children: /* @__PURE__ */ jsx47(XIcon, { "aria-hidden": true })
2811
+ children: /* @__PURE__ */ jsx49(XIcon, { "aria-hidden": true })
2576
2812
  }
2577
2813
  )
2578
2814
  ] }),
2579
- /* @__PURE__ */ jsx47(TocContent, { onNavigate: () => setOpen(false) })
2815
+ /* @__PURE__ */ jsx49(TocContent, { onNavigate: () => setOpen(false) })
2580
2816
  ] })
2581
2817
  ] })
2582
2818
  ] });
@@ -2610,10 +2846,10 @@ function failuresFirst(report) {
2610
2846
  }
2611
2847
 
2612
2848
  // src/interactive/use-deep-link-scroll.ts
2613
- import { useEffect as useEffect5, useRef as useRef2 } from "react";
2849
+ import { useEffect as useEffect6, useRef as useRef2 } from "react";
2614
2850
  function useDeepLinkScroll() {
2615
2851
  const lastAnchor = useRef2(null);
2616
- useEffect5(() => {
2852
+ useEffect6(() => {
2617
2853
  function scrollToHash() {
2618
2854
  const { anchor } = splitHash(window.location.hash);
2619
2855
  if (!anchor || anchor === lastAnchor.current) return;
@@ -2629,7 +2865,7 @@ function useDeepLinkScroll() {
2629
2865
  }
2630
2866
 
2631
2867
  // src/interactive/use-keyboard-shortcuts.ts
2632
- import { useEffect as useEffect6, useRef as useRef3 } from "react";
2868
+ import { useEffect as useEffect7, useRef as useRef3 } from "react";
2633
2869
  function isEditableTarget(target) {
2634
2870
  if (!(target instanceof HTMLElement)) return false;
2635
2871
  const tag = target.tagName;
@@ -2640,7 +2876,7 @@ function isEditableTarget(target) {
2640
2876
  function useKeyboardShortcuts(handlers) {
2641
2877
  const ref = useRef3(handlers);
2642
2878
  ref.current = handlers;
2643
- useEffect6(() => {
2879
+ useEffect7(() => {
2644
2880
  function onKeyDown(e) {
2645
2881
  const h = ref.current;
2646
2882
  if (e.key === "Escape") {
@@ -2684,8 +2920,8 @@ function useKeyboardShortcuts(handlers) {
2684
2920
  }
2685
2921
 
2686
2922
  // src/interactive/use-theme.ts
2687
- import { useCallback as useCallback3, useEffect as useEffect7, useState as useState7 } from "react";
2688
- var KEY = "es-theme";
2923
+ import { useCallback as useCallback3, useEffect as useEffect8, useState as useState8 } from "react";
2924
+ var KEY2 = "es-theme";
2689
2925
  function systemDark() {
2690
2926
  return typeof matchMedia !== "undefined" && matchMedia("(prefers-color-scheme: dark)").matches;
2691
2927
  }
@@ -2699,19 +2935,19 @@ function applyTheme(pref) {
2699
2935
  }
2700
2936
  function initialPref() {
2701
2937
  if (typeof localStorage !== "undefined") {
2702
- const stored = localStorage.getItem(KEY);
2938
+ const stored = localStorage.getItem(KEY2);
2703
2939
  if (stored === "light" || stored === "dark" || stored === "system") return stored;
2704
2940
  }
2705
2941
  return "system";
2706
2942
  }
2707
2943
  function useTheme() {
2708
- const [pref, setPrefState] = useState7("system");
2709
- useEffect7(() => {
2944
+ const [pref, setPrefState] = useState8("system");
2945
+ useEffect8(() => {
2710
2946
  const p = initialPref();
2711
2947
  setPrefState(p);
2712
2948
  applyTheme(p);
2713
2949
  }, []);
2714
- useEffect7(() => {
2950
+ useEffect8(() => {
2715
2951
  if (typeof matchMedia === "undefined") return;
2716
2952
  const mq = matchMedia("(prefers-color-scheme: dark)");
2717
2953
  const onChange = () => {
@@ -2724,7 +2960,7 @@ function useTheme() {
2724
2960
  setPrefState(p);
2725
2961
  applyTheme(p);
2726
2962
  try {
2727
- localStorage.setItem(KEY, p);
2963
+ localStorage.setItem(KEY2, p);
2728
2964
  } catch {
2729
2965
  }
2730
2966
  }, []);
@@ -2732,19 +2968,19 @@ function useTheme() {
2732
2968
  }
2733
2969
 
2734
2970
  // src/interactive/use-url-state.ts
2735
- import { useCallback as useCallback4, useEffect as useEffect8, useRef as useRef4, useState as useState8 } from "react";
2971
+ import { useCallback as useCallback4, useEffect as useEffect9, useRef as useRef4, useState as useState9 } from "react";
2736
2972
  var WRITE_DELAY_MS = 250;
2737
2973
  function useUrlState() {
2738
- const [state, setState] = useState8(DEFAULT_URL_STATE);
2974
+ const [state, setState] = useState9(DEFAULT_URL_STATE);
2739
2975
  const ready = useRef4(false);
2740
- useEffect8(() => {
2976
+ useEffect9(() => {
2741
2977
  const read = () => setState(decodeUrlState(splitHash(window.location.hash).params));
2742
2978
  read();
2743
2979
  ready.current = true;
2744
2980
  window.addEventListener("hashchange", read);
2745
2981
  return () => window.removeEventListener("hashchange", read);
2746
2982
  }, []);
2747
- useEffect8(() => {
2983
+ useEffect9(() => {
2748
2984
  if (!ready.current) return;
2749
2985
  const timer = setTimeout(
2750
2986
  () => writeHash(splitHash(window.location.hash).anchor, encodeUrlState(state)),
@@ -2759,13 +2995,233 @@ function useUrlState() {
2759
2995
  return [state, update];
2760
2996
  }
2761
2997
 
2998
+ // src/interactive/webmcp-tools.ts
2999
+ import {
3000
+ getFeatureSummary,
3001
+ getScenario,
3002
+ listScenarioSummaries,
3003
+ runProvenance,
3004
+ toScenarioSummary
3005
+ } from "executable-stories-core/report-queries";
3006
+ var STATUSES2 = [
3007
+ "passed",
3008
+ "failed",
3009
+ "skipped",
3010
+ "pending"
3011
+ ];
3012
+ var VIEW_STATUSES = ["all", ...STATUSES2];
3013
+ var ToolArgumentError = class extends Error {
3014
+ };
3015
+ function asRecord(input) {
3016
+ return typeof input === "object" && input !== null ? input : {};
3017
+ }
3018
+ function readString(input, key) {
3019
+ const value = input[key];
3020
+ if (value === void 0 || value === null) return void 0;
3021
+ if (typeof value !== "string") {
3022
+ throw new ToolArgumentError(`"${key}" must be a string, received ${typeof value}.`);
3023
+ }
3024
+ return value;
3025
+ }
3026
+ function readStringArray(input, key) {
3027
+ const value = input[key];
3028
+ if (value === void 0 || value === null) return void 0;
3029
+ if (typeof value === "string") return [value];
3030
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
3031
+ throw new ToolArgumentError(`"${key}" must be an array of strings.`);
3032
+ }
3033
+ return value;
3034
+ }
3035
+ function readEnum(input, key, allowed) {
3036
+ const value = readString(input, key);
3037
+ if (value === void 0) return void 0;
3038
+ if (!allowed.includes(value)) {
3039
+ throw new ToolArgumentError(
3040
+ `"${key}" must be one of: ${allowed.join(", ")}. Received "${value}".`
3041
+ );
3042
+ }
3043
+ return value;
3044
+ }
3045
+ function readStatuses(input) {
3046
+ const values = readStringArray(input, "statuses");
3047
+ if (values === void 0) return void 0;
3048
+ const invalid = values.filter((v) => !STATUSES2.includes(v));
3049
+ if (invalid.length > 0) {
3050
+ throw new ToolArgumentError(
3051
+ `"statuses" must contain only: ${STATUSES2.join(", ")}. Received "${invalid.join('", "')}".`
3052
+ );
3053
+ }
3054
+ return values;
3055
+ }
3056
+ function guarded(run) {
3057
+ try {
3058
+ return run();
3059
+ } catch (error) {
3060
+ if (error instanceof ToolArgumentError) return `Invalid input \u2014 ${error.message}`;
3061
+ throw error;
3062
+ }
3063
+ }
3064
+ var scenarioListSchema = {
3065
+ type: "object",
3066
+ properties: {
3067
+ statuses: {
3068
+ type: "array",
3069
+ items: { type: "string", enum: [...STATUSES2] },
3070
+ description: "Keep only scenarios with any of these statuses."
3071
+ },
3072
+ tags: {
3073
+ type: "array",
3074
+ items: { type: "string" },
3075
+ description: "Keep only scenarios carrying any of these tags."
3076
+ },
3077
+ sourceFiles: {
3078
+ type: "array",
3079
+ items: { type: "string" },
3080
+ description: "Keep only scenarios whose source file path contains any of these."
3081
+ }
3082
+ }
3083
+ };
3084
+ var READ_ONLY = { readOnlyHint: true, untrustedContentHint: true };
3085
+ function reportTools(deps) {
3086
+ const { report, setView, onAgentFilter } = deps;
3087
+ const now = deps.now ?? Date.now;
3088
+ const provenance = () => ({ run: runProvenance(report, now()) });
3089
+ const list = (filters) => {
3090
+ const scenarios = listScenarioSummaries(report, filters);
3091
+ return { ...provenance(), total: scenarios.length, scenarios };
3092
+ };
3093
+ return {
3094
+ list_scenarios: {
3095
+ title: "List scenarios",
3096
+ description: "List the behaviour scenarios in this report, optionally filtered by status, tag, or source file. Returns each scenario with its steps, tags, timing and failure message.",
3097
+ annotations: READ_ONLY,
3098
+ input: scenarioListSchema,
3099
+ execute: (input) => guarded(() => {
3100
+ const args = asRecord(input);
3101
+ const filters = {};
3102
+ const statuses = readStatuses(args);
3103
+ const tags = readStringArray(args, "tags");
3104
+ const sourceFiles = readStringArray(args, "sourceFiles");
3105
+ if (statuses) filters.statuses = statuses;
3106
+ if (tags) filters.tags = tags;
3107
+ if (sourceFiles) filters.sourceFiles = sourceFiles;
3108
+ return list(filters);
3109
+ })
3110
+ },
3111
+ get_failing_scenarios: {
3112
+ title: "Get failing scenarios",
3113
+ description: "List only the scenarios that failed in this run, each with the step that failed and its error message. Use this first when asked what is broken.",
3114
+ annotations: READ_ONLY,
3115
+ execute: () => list({ statuses: ["failed"] })
3116
+ },
3117
+ get_feature_summary: {
3118
+ title: "Get feature summary",
3119
+ description: 'Summarise each feature in the report with its scenario counts by status. Use this for "how are we doing" questions rather than listing every scenario.',
3120
+ annotations: READ_ONLY,
3121
+ execute: () => ({
3122
+ ...provenance(),
3123
+ summary: report.summary,
3124
+ features: getFeatureSummary(report)
3125
+ })
3126
+ },
3127
+ get_scenario: {
3128
+ title: "Get scenario",
3129
+ description: "Get one scenario by its id or its exact title, with every step and any failure detail. Attachment and document bodies are omitted \u2014 `docKinds` names what the page shows.",
3130
+ annotations: READ_ONLY,
3131
+ input: {
3132
+ type: "object",
3133
+ properties: {
3134
+ idOrTitle: {
3135
+ type: "string",
3136
+ description: "The scenario id, or its exact title as shown in the report."
3137
+ }
3138
+ },
3139
+ required: ["idOrTitle"]
3140
+ },
3141
+ execute: (input) => guarded(() => {
3142
+ const idOrTitle = readString(asRecord(input), "idOrTitle") ?? "";
3143
+ const found = getScenario(report, idOrTitle);
3144
+ if (!found) {
3145
+ return {
3146
+ ...provenance(),
3147
+ error: `No scenario found with id or title "${idOrTitle}". Call list_scenarios to see what this report contains.`
3148
+ };
3149
+ }
3150
+ return {
3151
+ ...provenance(),
3152
+ feature: {
3153
+ id: found.feature.id,
3154
+ title: found.feature.title,
3155
+ sourceFile: found.feature.sourceFile
3156
+ },
3157
+ // The summary projection, not the raw scenario: doc entry bodies and
3158
+ // base64 attachments are unbounded, and this payload goes straight
3159
+ // into a model's context window.
3160
+ scenario: toScenarioSummary(found.feature, found.scenario)
3161
+ };
3162
+ })
3163
+ },
3164
+ filter_scenarios: {
3165
+ title: "Filter the report",
3166
+ description: `Change what the reader is looking at: set the report's search text, status filter and tag filter. Omitted fields are left as they are; pass an empty string, "all", or an empty array to clear one.`,
3167
+ input: {
3168
+ type: "object",
3169
+ properties: {
3170
+ search: {
3171
+ type: "string",
3172
+ description: "Free text matched against scenario titles, tags and step text. Empty string clears it."
3173
+ },
3174
+ status: {
3175
+ type: "string",
3176
+ enum: [...VIEW_STATUSES],
3177
+ description: 'Show only scenarios with this status. "all" clears the filter.'
3178
+ },
3179
+ tags: {
3180
+ type: "array",
3181
+ items: { type: "string" },
3182
+ description: "Show only scenarios carrying any of these tags. Empty array clears the filter."
3183
+ }
3184
+ }
3185
+ },
3186
+ execute: (input) => guarded(() => {
3187
+ const args = asRecord(input);
3188
+ const search = readString(args, "search");
3189
+ const status = readEnum(args, "status", VIEW_STATUSES);
3190
+ const tags = readStringArray(args, "tags");
3191
+ const applied = {
3192
+ search: search ?? deps.view.query,
3193
+ status: status ?? deps.view.status,
3194
+ tags: tags ?? deps.view.tags
3195
+ };
3196
+ const patch = {};
3197
+ if (search !== void 0) patch.query = search;
3198
+ if (status !== void 0) patch.status = status;
3199
+ if (tags !== void 0) patch.tags = tags;
3200
+ setView(patch);
3201
+ const matched = filterReport(report, {
3202
+ query: applied.search,
3203
+ status: applied.status,
3204
+ tags: applied.tags
3205
+ }).summary.total;
3206
+ const result = {
3207
+ ...applied,
3208
+ matched,
3209
+ total: report.summary.total
3210
+ };
3211
+ onAgentFilter?.(result);
3212
+ return { ...provenance(), applied: result };
3213
+ })
3214
+ }
3215
+ };
3216
+ }
3217
+
2762
3218
  // src/interactive/ReportInteractive.tsx
2763
- import { Fragment as Fragment13, jsx as jsx48, jsxs as jsxs33 } from "react/jsx-runtime";
3219
+ import { Fragment as Fragment15, jsx as jsx50, jsxs as jsxs35 } from "react/jsx-runtime";
2764
3220
  function ReportInteractive(props) {
2765
3221
  const { report, className, title, dataTheme } = props;
2766
3222
  const result = unwrapReport(report);
2767
3223
  if (!result.ok) {
2768
- return /* @__PURE__ */ jsx48(
3224
+ return /* @__PURE__ */ jsx50(
2769
3225
  ReportErrorShell,
2770
3226
  {
2771
3227
  error: result.error,
@@ -2775,7 +3231,7 @@ function ReportInteractive(props) {
2775
3231
  }
2776
3232
  );
2777
3233
  }
2778
- return /* @__PURE__ */ jsx48(ReportInteractiveView, { ...props, report: result.data });
3234
+ return /* @__PURE__ */ jsx50(ReportInteractiveView, { ...props, report: result.data });
2779
3235
  }
2780
3236
  var THEME_OPTIONS = [
2781
3237
  { value: "light", label: "Light", Icon: Sun },
@@ -2786,7 +3242,7 @@ function ThemeSegment({
2786
3242
  pref,
2787
3243
  onPref
2788
3244
  }) {
2789
- return /* @__PURE__ */ jsx48(
3245
+ return /* @__PURE__ */ jsx50(
2790
3246
  ToggleGroup,
2791
3247
  {
2792
3248
  size: "sm",
@@ -2796,14 +3252,14 @@ function ThemeSegment({
2796
3252
  if (next) onPref(next);
2797
3253
  },
2798
3254
  "aria-label": "Color theme",
2799
- children: THEME_OPTIONS.map(({ value, label, Icon }) => /* @__PURE__ */ jsx48(
3255
+ children: THEME_OPTIONS.map(({ value, label, Icon }) => /* @__PURE__ */ jsx50(
2800
3256
  ToggleGroupItem,
2801
3257
  {
2802
3258
  value,
2803
3259
  "aria-label": label,
2804
3260
  title: label,
2805
3261
  className: "px-2",
2806
- children: /* @__PURE__ */ jsx48(Icon, {})
3262
+ children: /* @__PURE__ */ jsx50(Icon, {})
2807
3263
  },
2808
3264
  value
2809
3265
  ))
@@ -2836,7 +3292,7 @@ function ReportInteractiveView({
2836
3292
  (next) => setUrlState({ detail: next }),
2837
3293
  [setUrlState]
2838
3294
  );
2839
- const [helpOpen, setHelpOpen] = useState9(false);
3295
+ const [helpOpen, setHelpOpen] = useState10(false);
2840
3296
  const { pref: themePref, setPref: setThemePref } = useTheme();
2841
3297
  const searchRef = useRef5(null);
2842
3298
  const expandId = useId2();
@@ -2878,7 +3334,7 @@ function ReportInteractiveView({
2878
3334
  }),
2879
3335
  [activeTags, setUrlState]
2880
3336
  );
2881
- const [toast, setToast] = useState9(null);
3337
+ const [toast, setToast] = useState10(null);
2882
3338
  const toastTimer = useRef5(null);
2883
3339
  const showToast = useCallback5((msg) => {
2884
3340
  setToast(msg);
@@ -2904,6 +3360,10 @@ function ReportInteractiveView({
2904
3360
  }),
2905
3361
  [copy]
2906
3362
  );
3363
+ const copyAllFailures = useCallback5(
3364
+ () => copy(failuresToPrompt(failedScenarios(filtered)), "Failures copied"),
3365
+ [copy, filtered]
3366
+ );
2907
3367
  const failureIndexRef = useRef5(0);
2908
3368
  const allCollapsibleIds = useMemo3(
2909
3369
  () => report.features.flatMap((f) => [f.id, ...f.scenarios.map((s) => s.id)]),
@@ -2921,7 +3381,7 @@ function ReportInteractiveView({
2921
3381
  () => collapse.collapseAll(allCollapsibleIds),
2922
3382
  [collapse, allCollapsibleIds]
2923
3383
  );
2924
- const [expandedAll, setExpandedAll] = useState9(
3384
+ const [expandedAll, setExpandedAll] = useState10(
2925
3385
  () => report.summary.failed === 0
2926
3386
  );
2927
3387
  const setExpanded = useCallback5(
@@ -2932,6 +3392,20 @@ function ReportInteractiveView({
2932
3392
  },
2933
3393
  [collapse, collapseAll]
2934
3394
  );
3395
+ const [agentFilter, setAgentFilter] = useState10(null);
3396
+ useTools(
3397
+ reportTools({
3398
+ report,
3399
+ view: urlState,
3400
+ setView: setUrlState,
3401
+ onAgentFilter: setAgentFilter
3402
+ })
3403
+ );
3404
+ const showAgentFilter = agentFilter !== null && agentFilter.search === query && agentFilter.status === statusFilter && agentFilter.tags.length === activeTags.length && agentFilter.tags.every((tag) => activeTags.includes(tag));
3405
+ const clearFilters = useCallback5(
3406
+ () => setUrlState({ query: "", status: "all", tags: [] }),
3407
+ [setUrlState]
3408
+ );
2935
3409
  const focusSearch = useCallback5(() => {
2936
3410
  searchRef.current?.focus();
2937
3411
  }, []);
@@ -2965,13 +3439,13 @@ function ReportInteractiveView({
2965
3439
  const totalScenarios = report.summary.total;
2966
3440
  const matchedScenarios = filtered.summary.total;
2967
3441
  const showToc = !hideToc && totalScenarios > 3;
2968
- return /* @__PURE__ */ jsx48(CollapseProvider, { value: visibleCollapse, children: /* @__PURE__ */ jsx48(ScenarioActionsProvider, { value: scenarioActions, children: /* @__PURE__ */ jsx48(ScenarioHistoryProvider, { value: scenarioHistory ?? null, children: /* @__PURE__ */ jsx48(ScenarioFreshnessProvider, { value: { staleAfterDays, report }, children: /* @__PURE__ */ jsx48(
3442
+ return /* @__PURE__ */ jsx50(CollapseProvider, { value: visibleCollapse, children: /* @__PURE__ */ jsx50(ScenarioActionsProvider, { value: scenarioActions, children: /* @__PURE__ */ jsx50(ScenarioHistoryProvider, { value: scenarioHistory ?? null, children: /* @__PURE__ */ jsx50(ScenarioFreshnessProvider, { value: { staleAfterDays, report }, children: /* @__PURE__ */ jsx50(
2969
3443
  ReportRoot,
2970
3444
  {
2971
3445
  report: filtered,
2972
3446
  customRenderers,
2973
3447
  renderers,
2974
- children: /* @__PURE__ */ jsxs33(
3448
+ children: /* @__PURE__ */ jsxs35(
2975
3449
  "main",
2976
3450
  {
2977
3451
  className: cn("es-report", "es-report-interactive", className),
@@ -2980,8 +3454,8 @@ function ReportInteractiveView({
2980
3454
  "data-theme": dataTheme,
2981
3455
  "data-detail-level": detail,
2982
3456
  children: [
2983
- /* @__PURE__ */ jsxs33("header", { className: "es-report-header", children: [
2984
- /* @__PURE__ */ jsxs33(
3457
+ /* @__PURE__ */ jsxs35("header", { className: "es-report-header", children: [
3458
+ /* @__PURE__ */ jsxs35(
2985
3459
  "div",
2986
3460
  {
2987
3461
  "data-es-navbar": true,
@@ -2990,9 +3464,9 @@ function ReportInteractiveView({
2990
3464
  hideHeader ? "justify-end" : "justify-between"
2991
3465
  ),
2992
3466
  children: [
2993
- hideHeader ? null : /* @__PURE__ */ jsx48("h1", { className: "min-w-0", children: title ?? "Story Report" }),
2994
- /* @__PURE__ */ jsxs33("div", { className: "flex items-center gap-2", children: [
2995
- /* @__PURE__ */ jsx48(
3467
+ hideHeader ? null : /* @__PURE__ */ jsx50("h1", { className: "min-w-0", children: title ?? "Story Report" }),
3468
+ /* @__PURE__ */ jsxs35("div", { className: "flex items-center gap-2", children: [
3469
+ /* @__PURE__ */ jsx50(
2996
3470
  ReportSearch,
2997
3471
  {
2998
3472
  ref: searchRef,
@@ -3002,16 +3476,16 @@ function ReportInteractiveView({
3002
3476
  totalCount: totalScenarios
3003
3477
  }
3004
3478
  ),
3005
- /* @__PURE__ */ jsx48(ThemeSegment, { pref: themePref, onPref: setThemePref })
3479
+ /* @__PURE__ */ jsx50(ThemeSegment, { pref: themePref, onPref: setThemePref })
3006
3480
  ] })
3007
3481
  ]
3008
3482
  }
3009
3483
  ),
3010
- hideHeader ? null : /* @__PURE__ */ jsxs33(Fragment13, { children: [
3011
- /* @__PURE__ */ jsx48(ReportSummary, {}),
3012
- /* @__PURE__ */ jsx48(ReportMeta, {})
3484
+ hideHeader ? null : /* @__PURE__ */ jsxs35(Fragment15, { children: [
3485
+ /* @__PURE__ */ jsx50(ReportSummary, {}),
3486
+ /* @__PURE__ */ jsx50(ReportMeta, {})
3013
3487
  ] }),
3014
- /* @__PURE__ */ jsx48(
3488
+ /* @__PURE__ */ jsx50(
3015
3489
  ReportFreshness,
3016
3490
  {
3017
3491
  lastRunMs: reportLastRunMs(report),
@@ -3019,14 +3493,15 @@ function ReportInteractiveView({
3019
3493
  staleAfterDays
3020
3494
  }
3021
3495
  ),
3022
- /* @__PURE__ */ jsx48(
3496
+ /* @__PURE__ */ jsx50(
3023
3497
  ReportLastRunDelta,
3024
3498
  {
3025
3499
  history: scenarioHistory,
3026
3500
  report
3027
3501
  }
3028
3502
  ),
3029
- /* @__PURE__ */ jsx48(
3503
+ /* @__PURE__ */ jsx50(ReportSinceLastVisit, { report }),
3504
+ /* @__PURE__ */ jsx50(
3030
3505
  ReportFilters,
3031
3506
  {
3032
3507
  statuses: statusOptions,
@@ -3037,10 +3512,10 @@ function ReportInteractiveView({
3037
3512
  onToggleTag: toggleTag
3038
3513
  }
3039
3514
  ),
3040
- /* @__PURE__ */ jsxs33("div", { className: "flex flex-wrap items-center gap-x-6 gap-y-2 border-t border-border/60 pt-3", children: [
3041
- showToc ? /* @__PURE__ */ jsx48(ReportTocDrawer, {}) : null,
3042
- /* @__PURE__ */ jsxs33("div", { className: "flex items-center gap-2", children: [
3043
- /* @__PURE__ */ jsx48(
3515
+ /* @__PURE__ */ jsxs35("div", { className: "flex flex-wrap items-center gap-x-6 gap-y-2 border-t border-border/60 pt-3", children: [
3516
+ showToc ? /* @__PURE__ */ jsx50(ReportTocDrawer, {}) : null,
3517
+ /* @__PURE__ */ jsxs35("div", { className: "flex items-center gap-2", children: [
3518
+ /* @__PURE__ */ jsx50(
3044
3519
  Switch,
3045
3520
  {
3046
3521
  id: expandId,
@@ -3048,7 +3523,7 @@ function ReportInteractiveView({
3048
3523
  onCheckedChange: setExpanded
3049
3524
  }
3050
3525
  ),
3051
- /* @__PURE__ */ jsx48(
3526
+ /* @__PURE__ */ jsx50(
3052
3527
  "label",
3053
3528
  {
3054
3529
  htmlFor: expandId,
@@ -3057,8 +3532,8 @@ function ReportInteractiveView({
3057
3532
  }
3058
3533
  )
3059
3534
  ] }),
3060
- /* @__PURE__ */ jsxs33("div", { className: "flex items-center gap-2", children: [
3061
- /* @__PURE__ */ jsx48(
3535
+ /* @__PURE__ */ jsxs35("div", { className: "flex items-center gap-2", children: [
3536
+ /* @__PURE__ */ jsx50(
3062
3537
  Switch,
3063
3538
  {
3064
3539
  id: docsId,
@@ -3066,7 +3541,7 @@ function ReportInteractiveView({
3066
3541
  onCheckedChange: (checked) => setDetail(checked ? "full" : "minimal")
3067
3542
  }
3068
3543
  ),
3069
- /* @__PURE__ */ jsx48(
3544
+ /* @__PURE__ */ jsx50(
3070
3545
  "label",
3071
3546
  {
3072
3547
  htmlFor: docsId,
@@ -3077,17 +3552,25 @@ function ReportInteractiveView({
3077
3552
  ] })
3078
3553
  ] })
3079
3554
  ] }),
3080
- /* @__PURE__ */ jsx48(ReportFailureBanner, { failures }),
3081
- hasContent ? showToc ? /* @__PURE__ */ jsxs33("div", { className: "flex gap-6", children: [
3082
- /* @__PURE__ */ jsx48(ReportToc, {}),
3083
- /* @__PURE__ */ jsx48("div", { className: "flex min-w-0 flex-1 flex-col gap-4", children: /* @__PURE__ */ jsx48(ReportFeatureList, {}) })
3084
- ] }) : /* @__PURE__ */ jsx48("div", { className: "flex flex-col gap-4", children: /* @__PURE__ */ jsx48(ReportFeatureList, {}) }) : /* @__PURE__ */ jsx48(
3555
+ showAgentFilter ? /* @__PURE__ */ jsx50(
3556
+ ReportAgentFilterNotice,
3557
+ {
3558
+ applied: agentFilter,
3559
+ onReset: clearFilters,
3560
+ onDismiss: () => setAgentFilter(null)
3561
+ }
3562
+ ) : null,
3563
+ /* @__PURE__ */ jsx50(ReportFailureBanner, { failures, onCopyAll: copyAllFailures }),
3564
+ hasContent ? showToc ? /* @__PURE__ */ jsxs35("div", { className: "flex gap-6", children: [
3565
+ /* @__PURE__ */ jsx50(ReportToc, {}),
3566
+ /* @__PURE__ */ jsx50("div", { className: "flex min-w-0 flex-1 flex-col gap-4", children: /* @__PURE__ */ jsx50(ReportFeatureList, {}) })
3567
+ ] }) : /* @__PURE__ */ jsx50("div", { className: "flex flex-col gap-4", children: /* @__PURE__ */ jsx50(ReportFeatureList, {}) }) : /* @__PURE__ */ jsx50(
3085
3568
  ReportEmpty,
3086
3569
  {
3087
3570
  message: query ? "No scenarios match the search." : void 0
3088
3571
  }
3089
3572
  ),
3090
- /* @__PURE__ */ jsx48(
3573
+ /* @__PURE__ */ jsx50(
3091
3574
  "button",
3092
3575
  {
3093
3576
  type: "button",
@@ -3098,14 +3581,14 @@ function ReportInteractiveView({
3098
3581
  children: "?"
3099
3582
  }
3100
3583
  ),
3101
- /* @__PURE__ */ jsx48(
3584
+ /* @__PURE__ */ jsx50(
3102
3585
  ReportShortcutsHelp,
3103
3586
  {
3104
3587
  open: helpOpen,
3105
3588
  onClose: () => setHelpOpen(false)
3106
3589
  }
3107
3590
  ),
3108
- toast ? /* @__PURE__ */ jsx48(
3591
+ toast ? /* @__PURE__ */ jsx50(
3109
3592
  "div",
3110
3593
  {
3111
3594
  role: "status",
@@ -3125,11 +3608,11 @@ function ReportInteractiveView({
3125
3608
  import { useMemo as useMemo4 } from "react";
3126
3609
 
3127
3610
  // src/interactive/island-renderers.tsx
3128
- import { createElement, useEffect as useEffect10, useState as useState11 } from "react";
3611
+ import { createElement, useEffect as useEffect11, useState as useState12 } from "react";
3129
3612
 
3130
3613
  // src/components/doc/MermaidView.tsx
3131
- import { useEffect as useEffect9, useRef as useRef6, useState as useState10 } from "react";
3132
- import { Fragment as Fragment14, jsx as jsx49, jsxs as jsxs34 } from "react/jsx-runtime";
3614
+ import { useEffect as useEffect10, useRef as useRef6, useState as useState11 } from "react";
3615
+ import { Fragment as Fragment16, jsx as jsx51, jsxs as jsxs36 } from "react/jsx-runtime";
3133
3616
  function prefersDark(el) {
3134
3617
  const themed = el?.closest("[data-theme]");
3135
3618
  if (themed?.dataset.theme === "dark") return true;
@@ -3138,9 +3621,9 @@ function prefersDark(el) {
3138
3621
  }
3139
3622
  function MermaidView({ entry, load }) {
3140
3623
  const ref = useRef6(null);
3141
- const [svg, setSvg] = useState10(null);
3142
- const [error, setError] = useState10(null);
3143
- useEffect9(() => {
3624
+ const [svg, setSvg] = useState11(null);
3625
+ const [error, setError] = useState11(null);
3626
+ useEffect10(() => {
3144
3627
  let cancelled = false;
3145
3628
  const draw = async () => {
3146
3629
  let mermaid;
@@ -3183,9 +3666,9 @@ function MermaidView({ entry, load }) {
3183
3666
  };
3184
3667
  }, [entry.code, load]);
3185
3668
  if (svg) {
3186
- return /* @__PURE__ */ jsxs34("figure", { ref, className: "my-3", children: [
3187
- entry.title ? /* @__PURE__ */ jsx49("figcaption", { className: "mb-1.5 text-xs font-medium text-muted-foreground", children: entry.title }) : null,
3188
- /* @__PURE__ */ jsx49(
3669
+ return /* @__PURE__ */ jsxs36("figure", { ref, className: "my-3", children: [
3670
+ entry.title ? /* @__PURE__ */ jsx51("figcaption", { className: "mb-1.5 text-xs font-medium text-muted-foreground", children: entry.title }) : null,
3671
+ /* @__PURE__ */ jsx51(
3189
3672
  "div",
3190
3673
  {
3191
3674
  role: "img",
@@ -3196,8 +3679,8 @@ function MermaidView({ entry, load }) {
3196
3679
  )
3197
3680
  ] });
3198
3681
  }
3199
- return /* @__PURE__ */ jsxs34(Fragment14, { children: [
3200
- error ? /* @__PURE__ */ jsxs34(
3682
+ return /* @__PURE__ */ jsxs36(Fragment16, { children: [
3683
+ error ? /* @__PURE__ */ jsxs36(
3201
3684
  "p",
3202
3685
  {
3203
3686
  role: "alert",
@@ -3209,12 +3692,12 @@ function MermaidView({ entry, load }) {
3209
3692
  ]
3210
3693
  }
3211
3694
  ) : null,
3212
- /* @__PURE__ */ jsx49(MermaidSource, { entry, ref })
3695
+ /* @__PURE__ */ jsx51(MermaidSource, { entry, ref })
3213
3696
  ] });
3214
3697
  }
3215
3698
 
3216
3699
  // src/interactive/island-renderers.tsx
3217
- import { jsx as jsx50 } from "react/jsx-runtime";
3700
+ import { jsx as jsx52 } from "react/jsx-runtime";
3218
3701
  var HLJS_ESM = "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/es/highlight.min.js";
3219
3702
  var MERMAID_ESM = "https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs";
3220
3703
  function cachedCdnModule(url) {
@@ -3231,8 +3714,8 @@ function cachedCdnModule(url) {
3231
3714
  var loadMermaidFromCdn = cachedCdnModule(MERMAID_ESM);
3232
3715
  var loadHljs = cachedCdnModule(HLJS_ESM);
3233
3716
  function CdnHighlightedCode({ entry }) {
3234
- const [html, setHtml] = useState11(null);
3235
- useEffect10(() => {
3717
+ const [html, setHtml] = useState12(null);
3718
+ useEffect11(() => {
3236
3719
  let cancelled = false;
3237
3720
  void (async () => {
3238
3721
  try {
@@ -3248,13 +3731,13 @@ function CdnHighlightedCode({ entry }) {
3248
3731
  cancelled = true;
3249
3732
  };
3250
3733
  }, [entry.content, entry.lang]);
3251
- return /* @__PURE__ */ jsx50(CodeFigure, { label: entry.label, children: html !== null ? /* @__PURE__ */ jsx50(
3734
+ return /* @__PURE__ */ jsx52(CodeFigure, { label: entry.label, children: html !== null ? /* @__PURE__ */ jsx52(
3252
3735
  "code",
3253
3736
  {
3254
3737
  className: cn("hljs font-mono text-xs", entry.lang && `language-${entry.lang}`),
3255
3738
  dangerouslySetInnerHTML: { __html: html }
3256
3739
  }
3257
- ) : /* @__PURE__ */ jsx50("code", { className: cn("font-mono text-xs", entry.lang && `language-${entry.lang}`), children: entry.content }) });
3740
+ ) : /* @__PURE__ */ jsx52("code", { className: cn("font-mono text-xs", entry.lang && `language-${entry.lang}`), children: entry.content }) });
3258
3741
  }
3259
3742
  function buildIslandRenderers(opts) {
3260
3743
  const renderers = {};
@@ -3268,7 +3751,7 @@ function buildIslandRenderers(opts) {
3268
3751
  }
3269
3752
 
3270
3753
  // src/interactive/ReportInteractiveIsland.tsx
3271
- import { jsx as jsx51 } from "react/jsx-runtime";
3754
+ import { jsx as jsx53 } from "react/jsx-runtime";
3272
3755
  function ReportInteractiveIsland({
3273
3756
  report,
3274
3757
  title,
@@ -3282,7 +3765,7 @@ function ReportInteractiveIsland({
3282
3765
  () => buildIslandRenderers({ syntaxHighlighting, mermaid }),
3283
3766
  [syntaxHighlighting, mermaid]
3284
3767
  );
3285
- return /* @__PURE__ */ jsx51(
3768
+ return /* @__PURE__ */ jsx53(
3286
3769
  ReportInteractive,
3287
3770
  {
3288
3771
  report,