executable-stories-react 0.11.0 → 0.13.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.
@@ -1,45 +1,146 @@
1
1
  "use client";
2
2
 
3
3
  // src/interactive/ReportInteractive.tsx
4
+ import { Monitor, Moon, Sun } from "lucide-react";
5
+ import { useTools } from "webmcpable/react";
4
6
  import {
5
7
  useCallback as useCallback5,
6
8
  useDeferredValue,
7
9
  useId as useId2,
8
10
  useMemo as useMemo3,
9
11
  useRef as useRef5,
10
- useState as useState8
12
+ useState as useState9
11
13
  } from "react";
12
14
 
13
- // src/result.ts
14
- var ok = (data) => ({ ok: true, data });
15
- function unwrapReport(value) {
16
- if (typeof value === "object" && value !== null && "ok" in value && typeof value.ok === "boolean") {
17
- return value;
15
+ // src/components/ui/empty.tsx
16
+ import { cva } from "class-variance-authority";
17
+
18
+ // src/lib/utils.ts
19
+ import { clsx } from "clsx";
20
+ import { twMerge } from "tailwind-merge";
21
+ function cn(...inputs) {
22
+ return twMerge(clsx(inputs));
23
+ }
24
+
25
+ // src/components/ui/empty.tsx
26
+ import { jsx } from "react/jsx-runtime";
27
+ function Empty({ className, ...props }) {
28
+ return /* @__PURE__ */ jsx(
29
+ "div",
30
+ {
31
+ "data-slot": "empty",
32
+ className: cn(
33
+ "flex min-w-0 flex-1 flex-col items-center justify-center gap-6 rounded-lg border-dashed p-6 text-center text-balance md:p-12",
34
+ className
35
+ ),
36
+ ...props
37
+ }
38
+ );
39
+ }
40
+ function EmptyHeader({ className, ...props }) {
41
+ return /* @__PURE__ */ jsx(
42
+ "div",
43
+ {
44
+ "data-slot": "empty-header",
45
+ className: cn(
46
+ "flex max-w-sm flex-col items-center gap-2 text-center",
47
+ className
48
+ ),
49
+ ...props
50
+ }
51
+ );
52
+ }
53
+ var emptyMediaVariants = cva(
54
+ "mb-2 flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0",
55
+ {
56
+ variants: {
57
+ variant: {
58
+ default: "bg-transparent",
59
+ icon: "flex size-10 shrink-0 items-center justify-center rounded-lg bg-muted text-foreground [&_svg:not([class*='size-'])]:size-6"
60
+ }
61
+ },
62
+ defaultVariants: {
63
+ variant: "default"
64
+ }
18
65
  }
19
- return ok(value);
66
+ );
67
+ function EmptyTitle({ className, ...props }) {
68
+ return /* @__PURE__ */ jsx(
69
+ "div",
70
+ {
71
+ "data-slot": "empty-title",
72
+ className: cn("text-lg font-medium tracking-tight", className),
73
+ ...props
74
+ }
75
+ );
20
76
  }
21
77
 
22
- // src/context/ReportRoot.tsx
23
- import { useMemo } from "react";
78
+ // src/components/ReportEmpty.tsx
79
+ import { jsx as jsx2 } from "react/jsx-runtime";
80
+ function ReportEmpty({ message }) {
81
+ return /* @__PURE__ */ jsx2(Empty, { "aria-live": "polite", className: "border", children: /* @__PURE__ */ jsx2(EmptyHeader, { children: /* @__PURE__ */ jsx2(EmptyTitle, { children: message ?? "No scenarios in this report." }) }) });
82
+ }
83
+
84
+ // src/hooks/useReport.ts
85
+ import { useContext } from "react";
24
86
 
25
87
  // src/context/ReportContext.ts
26
88
  import { createContext } from "react";
27
89
  var ReportContext = createContext(null);
28
90
 
29
- // src/components/ui/badge.tsx
30
- import "react";
31
- import { cva } from "class-variance-authority";
32
- import { useRender } from "@base-ui/react/use-render";
91
+ // src/hooks/useReport.ts
92
+ function useReport() {
93
+ const ctx = useContext(ReportContext);
94
+ if (!ctx) {
95
+ throw new Error(
96
+ "useReport must be used inside <ReportRoot> or <Report>. Wrap your tree with one of those."
97
+ );
98
+ }
99
+ return ctx.report;
100
+ }
33
101
 
34
- // src/lib/utils.ts
35
- import { clsx } from "clsx";
36
- import { twMerge } from "tailwind-merge";
37
- function cn(...inputs) {
38
- return twMerge(clsx(inputs));
102
+ // src/components/ReportScenario.tsx
103
+ import { formatDuration as formatDuration3 } from "executable-stories-core/utils/duration";
104
+
105
+ // src/lib/step-params.tsx
106
+ import { jsx as jsx3 } from "react/jsx-runtime";
107
+ var STEP_PARAM_PATTERN = /"[^"]*"|(?<![\w.-])\d+(?:\.\d+)?(?![\w.-])/g;
108
+ function highlightStepParams(text) {
109
+ const matches = Array.from(text.matchAll(STEP_PARAM_PATTERN));
110
+ if (matches.length === 0) return text;
111
+ const nodes = [];
112
+ let lastIndex = 0;
113
+ matches.forEach((match, i) => {
114
+ const start = match.index;
115
+ const end = start + match[0].length;
116
+ if (start > lastIndex) nodes.push(text.slice(lastIndex, start));
117
+ nodes.push(
118
+ /* @__PURE__ */ jsx3("span", { className: "font-medium text-step-param italic", children: match[0] }, i)
119
+ );
120
+ lastIndex = end;
121
+ });
122
+ if (lastIndex < text.length) nodes.push(text.slice(lastIndex));
123
+ return nodes;
124
+ }
125
+
126
+ // src/components/ReportSteps.tsx
127
+ import {
128
+ assertionState,
129
+ assertiveSteps
130
+ } from "executable-stories-core/utils/assertive-steps";
131
+ import { formatDuration } from "executable-stories-core/utils/duration";
132
+
133
+ // src/components/doc/DocNote.tsx
134
+ import { jsx as jsx4 } from "react/jsx-runtime";
135
+ function DocNote({ entry }) {
136
+ return /* @__PURE__ */ jsx4("p", { className: "my-2 text-sm text-muted-foreground", children: entry.text });
39
137
  }
40
138
 
41
139
  // src/components/ui/badge.tsx
42
- var badgeVariants = cva(
140
+ import "react";
141
+ import { cva as cva2 } from "class-variance-authority";
142
+ import { useRender } from "@base-ui/react/use-render";
143
+ var badgeVariants = cva2(
43
144
  "inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3",
44
145
  {
45
146
  variants: {
@@ -84,342 +185,119 @@ function Badge({
84
185
  });
85
186
  }
86
187
 
87
- // src/components/doc/NarrativeBlocks.tsx
88
- import { jsx, jsxs } from "react/jsx-runtime";
89
- var FILE_TREE_TYPE = "file-tree";
90
- var DATA_MODEL_TYPE = "data-model";
91
- function isChangeKind(value) {
92
- return value === "added" || value === "modified" || value === "removed" || value === "renamed";
188
+ // src/components/doc/DocTag.tsx
189
+ import { jsx as jsx5 } from "react/jsx-runtime";
190
+ function DocTag({ entry }) {
191
+ return /* @__PURE__ */ jsx5("ul", { className: "my-2 flex flex-wrap gap-1.5", "aria-label": "Tags", children: entry.names.map((n) => /* @__PURE__ */ jsx5("li", { children: /* @__PURE__ */ jsx5(Badge, { variant: "tag", children: n }) }, n)) });
93
192
  }
94
- function isRecord(value) {
95
- return typeof value === "object" && value !== null && !Array.isArray(value);
193
+
194
+ // src/components/doc/DocKv.tsx
195
+ import { jsx as jsx6, jsxs } from "react/jsx-runtime";
196
+ function formatValue(value) {
197
+ if (value === null) return "null";
198
+ if (typeof value === "string") return value;
199
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
200
+ try {
201
+ return JSON.stringify(value);
202
+ } catch {
203
+ return String(value);
204
+ }
96
205
  }
97
- function str(value) {
98
- return typeof value === "string" && value !== "" ? value : void 0;
206
+ function DocKv({ entry }) {
207
+ return /* @__PURE__ */ jsxs("dl", { className: "my-2 flex flex-wrap items-baseline gap-x-2 text-sm", children: [
208
+ /* @__PURE__ */ jsx6("dt", { className: "font-semibold text-muted-foreground", children: entry.label }),
209
+ /* @__PURE__ */ jsx6("dd", { className: "font-mono text-foreground", children: formatValue(entry.value) })
210
+ ] });
99
211
  }
100
- function AuthoredMarker({ authored }) {
101
- if (authored !== "agent") return null;
102
- return /* @__PURE__ */ jsx(Badge, { variant: "outline", className: "border-border text-[10px] text-muted-foreground", children: "AI-authored, not verified by a run" });
212
+
213
+ // src/hooks/useRenderers.ts
214
+ import { useContext as useContext2 } from "react";
215
+ var EMPTY_CUSTOM = {};
216
+ var EMPTY_RENDERERS = {};
217
+ function useCustomRenderers() {
218
+ const ctx = useContext2(ReportContext);
219
+ return ctx?.customRenderers ?? EMPTY_CUSTOM;
103
220
  }
104
- function BlockFrame({
105
- title,
106
- authored,
107
- children
108
- }) {
109
- return /* @__PURE__ */ jsxs("figure", { className: "my-3", children: [
110
- title || authored === "agent" ? /* @__PURE__ */ jsxs("figcaption", { className: "mb-1.5 flex flex-wrap items-center gap-2", children: [
111
- title ? /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-muted-foreground", children: title }) : null,
112
- /* @__PURE__ */ jsx(AuthoredMarker, { authored })
113
- ] }) : null,
114
- children
221
+ function useBuiltinRenderers() {
222
+ const ctx = useContext2(ReportContext);
223
+ return ctx?.renderers ?? EMPTY_RENDERERS;
224
+ }
225
+
226
+ // src/components/doc/DocCode.tsx
227
+ import { Fragment, jsx as jsx7, jsxs as jsxs2 } from "react/jsx-runtime";
228
+ function CodeFigure({ label, children }) {
229
+ return /* @__PURE__ */ jsxs2("figure", { className: "my-3", children: [
230
+ label ? /* @__PURE__ */ jsx7("figcaption", { className: "mb-1.5 text-xs font-medium text-muted-foreground", children: label }) : null,
231
+ /* @__PURE__ */ jsx7("pre", { tabIndex: 0, className: "overflow-x-auto rounded-md bg-muted p-3", children })
115
232
  ] });
116
233
  }
117
- function ChangeBadge({ change }) {
118
- return /* @__PURE__ */ jsx(Badge, { variant: "tag", className: "text-[10px]", children: change });
234
+ function DocCode({ entry }) {
235
+ const renderers = useBuiltinRenderers();
236
+ if (renderers.code) {
237
+ return /* @__PURE__ */ jsx7(Fragment, { children: renderers.code(entry) });
238
+ }
239
+ return /* @__PURE__ */ jsx7(CodeFigure, { label: entry.label, children: /* @__PURE__ */ jsx7("code", { className: cn("font-mono text-xs", entry.lang && `language-${entry.lang}`), children: entry.content }) });
119
240
  }
120
- function parseFileTree(data) {
121
- if (!isRecord(data) || !Array.isArray(data.files)) return void 0;
122
- const files = [];
123
- for (const raw of data.files) {
124
- if (typeof raw === "string") {
125
- if (raw) files.push({ path: raw });
126
- continue;
241
+
242
+ // src/components/doc/DocTable.tsx
243
+ import { jsx as jsx8, jsxs as jsxs3 } from "react/jsx-runtime";
244
+ function DocTable({ entry }) {
245
+ return /* @__PURE__ */ jsxs3("figure", { className: "my-3", children: [
246
+ entry.label ? /* @__PURE__ */ jsx8("figcaption", { className: "mb-1.5 text-xs font-medium text-muted-foreground", children: entry.label }) : null,
247
+ /* @__PURE__ */ jsx8("div", { tabIndex: 0, className: "overflow-x-auto rounded-md border border-border", children: /* @__PURE__ */ jsxs3("table", { className: "w-full border-collapse text-sm", children: [
248
+ /* @__PURE__ */ jsx8("thead", { children: /* @__PURE__ */ jsx8("tr", { className: "bg-muted/50", children: entry.columns.map((c) => /* @__PURE__ */ jsx8("th", { scope: "col", className: "border-b border-border px-3 py-2 text-left font-semibold", children: c }, c)) }) }),
249
+ /* @__PURE__ */ jsx8("tbody", { children: entry.rows.map((row, i) => /* @__PURE__ */ jsx8("tr", { children: row.map((cell, j) => /* @__PURE__ */ jsx8("td", { className: "border-b border-border px-3 py-2 text-foreground", children: cell }, j)) }, i)) })
250
+ ] }) })
251
+ ] });
252
+ }
253
+
254
+ // src/components/doc/DocLink.tsx
255
+ import { jsx as jsx9 } from "react/jsx-runtime";
256
+ function DocLink({ entry }) {
257
+ return /* @__PURE__ */ jsx9(
258
+ "a",
259
+ {
260
+ className: "text-sm font-medium text-primary underline underline-offset-2 hover:no-underline",
261
+ href: entry.url,
262
+ rel: "noreferrer noopener",
263
+ target: "_blank",
264
+ children: entry.label
127
265
  }
128
- if (!isRecord(raw)) continue;
129
- const path = str(raw.path);
130
- if (!path) continue;
131
- files.push({
132
- path,
133
- change: isChangeKind(raw.change) ? raw.change : void 0,
134
- note: str(raw.note)
135
- });
136
- }
137
- if (files.length === 0) return void 0;
138
- return { title: str(data.title), authored: str(data.authored), files };
266
+ );
139
267
  }
140
- function buildTree(files) {
141
- const root = { name: "", children: /* @__PURE__ */ new Map() };
142
- for (const file of files) {
143
- const segments = file.path.split("/").filter(Boolean);
144
- let node = root;
145
- segments.forEach((segment, index) => {
146
- let child = node.children.get(segment);
147
- if (!child) {
148
- child = { name: segment, children: /* @__PURE__ */ new Map() };
149
- node.children.set(segment, child);
150
- }
151
- if (index === segments.length - 1) child.file = file;
152
- node = child;
153
- });
154
- }
155
- return root;
268
+
269
+ // src/lib/markdown.ts
270
+ import { marked } from "marked";
271
+
272
+ // src/lib/url.ts
273
+ import { safeUrl, safeImageUrl, isLocalFsPath } from "executable-stories-core/utils/url";
274
+
275
+ // src/lib/markdown.ts
276
+ function safeMarkdownHtml(markdown) {
277
+ const raw = marked.parse(markdown, { async: false });
278
+ return raw.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, "").replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, "").replace(/<\/?(?:iframe|object|embed|form|base)\b[^>]*>/gi, "").replace(/\son[a-z]+\s*=\s*"[^"]*"/gi, "").replace(/\son[a-z]+\s*=\s*'[^']*'/gi, "").replace(/\son[a-z]+\s*=\s*[^\s>]+/gi, "").replace(/(href|src)\s*=\s*"([^"]*)"/gi, (_m, attr, val) => `${attr}="${neutralizeUrl(val)}"`).replace(/(href|src)\s*=\s*'([^']*)'/gi, (_m, attr, val) => `${attr}='${neutralizeUrl(val)}'`);
156
279
  }
157
- function sortedChildren(node) {
158
- return [...node.children.values()].sort((a, b) => {
159
- const aDir = a.children.size > 0;
160
- const bDir = b.children.size > 0;
161
- if (aDir !== bDir) return aDir ? -1 : 1;
162
- return a.name.localeCompare(b.name);
163
- });
164
- }
165
- function TreeBranch({ node, depth }) {
166
- const children = sortedChildren(node);
167
- return /* @__PURE__ */ jsx("ul", { className: cn("space-y-0.5", depth > 0 && "ml-3 border-l border-border/60 pl-3"), children: children.map((child) => {
168
- const isDir = child.children.size > 0;
169
- return /* @__PURE__ */ jsxs("li", { children: [
170
- /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-baseline gap-2", children: [
171
- /* @__PURE__ */ jsx(
172
- "span",
173
- {
174
- className: cn(
175
- "font-mono text-xs",
176
- isDir ? "text-muted-foreground" : "text-foreground"
177
- ),
178
- children: isDir ? `${child.name}/` : child.name
179
- }
180
- ),
181
- child.file?.change ? /* @__PURE__ */ jsx(ChangeBadge, { change: child.file.change }) : null,
182
- child.file?.note ? /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: child.file.note }) : null
183
- ] }),
184
- isDir ? /* @__PURE__ */ jsx(TreeBranch, { node: child, depth: depth + 1 }) : null
185
- ] }, child.name);
186
- }) });
187
- }
188
- function FileTreeBlock({ entry }) {
189
- const data = parseFileTree(entry.data);
190
- if (!data) return /* @__PURE__ */ jsx(UnreadableBlock, { entry });
191
- return /* @__PURE__ */ jsx(BlockFrame, { title: data.title ?? "Files changed", authored: data.authored, children: /* @__PURE__ */ jsx("div", { tabIndex: 0, className: "overflow-x-auto rounded-md border border-border bg-card p-3", children: /* @__PURE__ */ jsx(TreeBranch, { node: buildTree(data.files), depth: 0 }) }) });
192
- }
193
- function parseDataModel(data) {
194
- if (!isRecord(data) || !Array.isArray(data.fields)) return void 0;
195
- const fields = [];
196
- for (const raw of data.fields) {
197
- if (!isRecord(raw)) continue;
198
- const name = str(raw.name);
199
- if (!name) continue;
200
- fields.push({
201
- name,
202
- type: str(raw.type),
203
- note: str(raw.note),
204
- change: isChangeKind(raw.change) ? raw.change : void 0
205
- });
206
- }
207
- if (fields.length === 0) return void 0;
208
- return {
209
- title: str(data.title),
210
- authored: str(data.authored),
211
- name: str(data.name),
212
- fields
213
- };
214
- }
215
- function DataModelBlock({ entry }) {
216
- const data = parseDataModel(entry.data);
217
- if (!data) return /* @__PURE__ */ jsx(UnreadableBlock, { entry });
218
- const hasNotes = data.fields.some((f) => f.note);
219
- return /* @__PURE__ */ jsx(BlockFrame, { title: data.title ?? data.name ?? "Data model", authored: data.authored, children: /* @__PURE__ */ jsx("div", { tabIndex: 0, className: "overflow-x-auto rounded-md border border-border", children: /* @__PURE__ */ jsxs("table", { className: "w-full border-collapse text-sm", children: [
220
- /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { className: "bg-muted/50", children: [
221
- /* @__PURE__ */ jsx("th", { scope: "col", className: "border-b border-border px-3 py-2 text-left font-semibold", children: "Field" }),
222
- /* @__PURE__ */ jsx("th", { scope: "col", className: "border-b border-border px-3 py-2 text-left font-semibold", children: "Type" }),
223
- hasNotes ? /* @__PURE__ */ jsx("th", { scope: "col", className: "border-b border-border px-3 py-2 text-left font-semibold", children: "Note" }) : null
224
- ] }) }),
225
- /* @__PURE__ */ jsx("tbody", { children: data.fields.map((field) => /* @__PURE__ */ jsxs("tr", { children: [
226
- /* @__PURE__ */ jsx("td", { className: "border-b border-border px-3 py-2", children: /* @__PURE__ */ jsxs("span", { className: "flex flex-wrap items-center gap-2", children: [
227
- /* @__PURE__ */ jsx("span", { className: "font-mono text-xs text-foreground", children: field.name }),
228
- field.change ? /* @__PURE__ */ jsx(ChangeBadge, { change: field.change }) : null
229
- ] }) }),
230
- /* @__PURE__ */ jsx("td", { className: "border-b border-border px-3 py-2 font-mono text-xs text-muted-foreground", children: field.type ?? "" }),
231
- hasNotes ? /* @__PURE__ */ jsx("td", { className: "border-b border-border px-3 py-2 text-foreground", children: field.note ?? "" }) : null
232
- ] }, field.name)) })
233
- ] }) }) });
234
- }
235
- function UnreadableBlock({ entry }) {
236
- return /* @__PURE__ */ jsxs("div", { className: "my-2", "data-type": entry.type, children: [
237
- /* @__PURE__ */ jsxs("p", { className: "mb-1 text-xs font-medium text-muted-foreground", children: [
238
- entry.type,
239
- " (unrecognised shape)"
240
- ] }),
241
- /* @__PURE__ */ jsx(
242
- "pre",
243
- {
244
- tabIndex: 0,
245
- className: "overflow-x-auto rounded-md bg-muted p-3 font-mono text-xs text-foreground",
246
- children: safeStringify(entry.data)
247
- }
248
- )
249
- ] });
250
- }
251
- function safeStringify(value) {
252
- try {
253
- return JSON.stringify(value, null, 2);
254
- } catch {
255
- return String(value);
256
- }
257
- }
258
- var narrativeBlockRenderers = {
259
- [FILE_TREE_TYPE]: (entry) => /* @__PURE__ */ jsx(FileTreeBlock, { entry }),
260
- [DATA_MODEL_TYPE]: (entry) => /* @__PURE__ */ jsx(DataModelBlock, { entry })
261
- };
262
-
263
- // src/context/ReportRoot.tsx
264
- import { jsx as jsx2 } from "react/jsx-runtime";
265
- var EMPTY_RENDERERS = {};
266
- function ReportRoot({
267
- report,
268
- customRenderers,
269
- renderers,
270
- children
271
- }) {
272
- const value = useMemo(
273
- () => ({
274
- report,
275
- // Narrative blocks render everywhere the report does (static SSR, the
276
- // island, Astro) without every host wiring them up. A host renderer for
277
- // the same type still wins.
278
- customRenderers: { ...narrativeBlockRenderers, ...customRenderers },
279
- renderers: renderers ?? EMPTY_RENDERERS
280
- }),
281
- [report, customRenderers, renderers]
282
- );
283
- return /* @__PURE__ */ jsx2(ReportContext.Provider, { value, children });
284
- }
285
-
286
- // src/hooks/useReport.ts
287
- import { useContext } from "react";
288
- function useReport() {
289
- const ctx = useContext(ReportContext);
290
- if (!ctx) {
291
- throw new Error(
292
- "useReport must be used inside <ReportRoot> or <Report>. Wrap your tree with one of those."
293
- );
294
- }
295
- return ctx.report;
296
- }
297
-
298
- // src/components/ReportScenario.tsx
299
- import { formatDuration as formatDuration3 } from "executable-stories-core/utils/duration";
300
-
301
- // src/components/ReportSteps.tsx
302
- import { formatDuration } from "executable-stories-core/utils/duration";
303
-
304
- // src/components/doc/DocNote.tsx
305
- import { jsx as jsx3 } from "react/jsx-runtime";
306
- function DocNote({ entry }) {
307
- return /* @__PURE__ */ jsx3("p", { className: "my-2 text-sm text-muted-foreground", children: entry.text });
308
- }
309
-
310
- // src/components/doc/DocTag.tsx
311
- import { jsx as jsx4 } from "react/jsx-runtime";
312
- function DocTag({ entry }) {
313
- return /* @__PURE__ */ jsx4("ul", { className: "my-2 flex flex-wrap gap-1.5", "aria-label": "Tags", children: entry.names.map((n) => /* @__PURE__ */ jsx4("li", { children: /* @__PURE__ */ jsx4(Badge, { variant: "tag", children: n }) }, n)) });
314
- }
315
-
316
- // src/components/doc/DocKv.tsx
317
- import { jsx as jsx5, jsxs as jsxs2 } from "react/jsx-runtime";
318
- function formatValue(value) {
319
- if (value === null) return "null";
320
- if (typeof value === "string") return value;
321
- if (typeof value === "number" || typeof value === "boolean") return String(value);
322
- try {
323
- return JSON.stringify(value);
324
- } catch {
325
- return String(value);
326
- }
327
- }
328
- function DocKv({ entry }) {
329
- return /* @__PURE__ */ jsxs2("dl", { className: "my-2 flex flex-wrap items-baseline gap-x-2 text-sm", children: [
330
- /* @__PURE__ */ jsx5("dt", { className: "font-semibold text-muted-foreground", children: entry.label }),
331
- /* @__PURE__ */ jsx5("dd", { className: "font-mono text-foreground", children: formatValue(entry.value) })
332
- ] });
333
- }
334
-
335
- // src/hooks/useRenderers.ts
336
- import { useContext as useContext2 } from "react";
337
- var EMPTY_CUSTOM = {};
338
- var EMPTY_RENDERERS2 = {};
339
- function useCustomRenderers() {
340
- const ctx = useContext2(ReportContext);
341
- return ctx?.customRenderers ?? EMPTY_CUSTOM;
342
- }
343
- function useBuiltinRenderers() {
344
- const ctx = useContext2(ReportContext);
345
- return ctx?.renderers ?? EMPTY_RENDERERS2;
346
- }
347
-
348
- // src/components/doc/DocCode.tsx
349
- import { Fragment, jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
350
- function CodeFigure({ label, children }) {
351
- return /* @__PURE__ */ jsxs3("figure", { className: "my-3", children: [
352
- label ? /* @__PURE__ */ jsx6("figcaption", { className: "mb-1.5 text-xs font-medium text-muted-foreground", children: label }) : null,
353
- /* @__PURE__ */ jsx6("pre", { tabIndex: 0, className: "overflow-x-auto rounded-md bg-muted p-3", children })
354
- ] });
355
- }
356
- function DocCode({ entry }) {
357
- const renderers = useBuiltinRenderers();
358
- if (renderers.code) {
359
- return /* @__PURE__ */ jsx6(Fragment, { children: renderers.code(entry) });
360
- }
361
- return /* @__PURE__ */ jsx6(CodeFigure, { label: entry.label, children: /* @__PURE__ */ jsx6("code", { className: cn("font-mono text-xs", entry.lang && `language-${entry.lang}`), children: entry.content }) });
362
- }
363
-
364
- // src/components/doc/DocTable.tsx
365
- import { jsx as jsx7, jsxs as jsxs4 } from "react/jsx-runtime";
366
- function DocTable({ entry }) {
367
- return /* @__PURE__ */ jsxs4("figure", { className: "my-3", children: [
368
- entry.label ? /* @__PURE__ */ jsx7("figcaption", { className: "mb-1.5 text-xs font-medium text-muted-foreground", children: entry.label }) : null,
369
- /* @__PURE__ */ jsx7("div", { tabIndex: 0, className: "overflow-x-auto rounded-md border border-border", children: /* @__PURE__ */ jsxs4("table", { className: "w-full border-collapse text-sm", children: [
370
- /* @__PURE__ */ jsx7("thead", { children: /* @__PURE__ */ jsx7("tr", { className: "bg-muted/50", children: entry.columns.map((c) => /* @__PURE__ */ jsx7("th", { scope: "col", className: "border-b border-border px-3 py-2 text-left font-semibold", children: c }, c)) }) }),
371
- /* @__PURE__ */ jsx7("tbody", { children: entry.rows.map((row, i) => /* @__PURE__ */ jsx7("tr", { children: row.map((cell, j) => /* @__PURE__ */ jsx7("td", { className: "border-b border-border px-3 py-2 text-foreground", children: cell }, j)) }, i)) })
372
- ] }) })
373
- ] });
374
- }
375
-
376
- // src/components/doc/DocLink.tsx
377
- import { jsx as jsx8 } from "react/jsx-runtime";
378
- function DocLink({ entry }) {
379
- return /* @__PURE__ */ jsx8(
380
- "a",
381
- {
382
- className: "text-sm font-medium text-primary underline underline-offset-2 hover:no-underline",
383
- href: entry.url,
384
- rel: "noreferrer noopener",
385
- target: "_blank",
386
- children: entry.label
387
- }
388
- );
389
- }
390
-
391
- // src/lib/markdown.ts
392
- import { marked } from "marked";
393
-
394
- // src/lib/url.ts
395
- import { safeUrl, safeImageUrl, isLocalFsPath } from "executable-stories-core/utils/url";
396
-
397
- // src/lib/markdown.ts
398
- function safeMarkdownHtml(markdown) {
399
- const raw = marked.parse(markdown, { async: false });
400
- return raw.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, "").replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, "").replace(/<\/?(?:iframe|object|embed|form|base)\b[^>]*>/gi, "").replace(/\son[a-z]+\s*=\s*"[^"]*"/gi, "").replace(/\son[a-z]+\s*=\s*'[^']*'/gi, "").replace(/\son[a-z]+\s*=\s*[^\s>]+/gi, "").replace(/(href|src)\s*=\s*"([^"]*)"/gi, (_m, attr, val) => `${attr}="${neutralizeUrl(val)}"`).replace(/(href|src)\s*=\s*'([^']*)'/gi, (_m, attr, val) => `${attr}='${neutralizeUrl(val)}'`);
401
- }
402
- function dedent(text) {
403
- const lines = text.replace(/\t/g, " ").split("\n");
404
- const indents = lines.filter((line) => line.trim().length > 0).map((line) => line.length - line.trimStart().length);
405
- const shortest = indents.length > 0 ? Math.min(...indents) : 0;
406
- return lines.map((line) => line.slice(shortest)).join("\n").trim();
280
+ function dedent(text) {
281
+ const lines = text.replace(/\t/g, " ").split("\n");
282
+ const indents = lines.filter((line) => line.trim().length > 0).map((line) => line.length - line.trimStart().length);
283
+ const shortest = indents.length > 0 ? Math.min(...indents) : 0;
284
+ return lines.map((line) => line.slice(shortest)).join("\n").trim();
407
285
  }
408
286
  function neutralizeUrl(value) {
409
287
  return safeUrl(value) ?? "#";
410
288
  }
411
289
 
412
290
  // src/components/doc/DocSection.tsx
413
- import { Fragment as Fragment2, jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
291
+ import { Fragment as Fragment2, jsx as jsx10, jsxs as jsxs4 } from "react/jsx-runtime";
414
292
  function DocSection({ entry }) {
415
293
  const renderers = useBuiltinRenderers();
416
294
  if (renderers.section) {
417
- return /* @__PURE__ */ jsx9(Fragment2, { children: renderers.section(entry) });
295
+ return /* @__PURE__ */ jsx10(Fragment2, { children: renderers.section(entry) });
418
296
  }
419
297
  const html = safeMarkdownHtml(entry.markdown);
420
- return /* @__PURE__ */ jsxs5("section", { className: "my-3 text-sm", "aria-label": entry.title, children: [
421
- entry.title ? /* @__PURE__ */ jsx9("h4", { className: "mb-1 font-semibold text-foreground", children: entry.title }) : null,
422
- /* @__PURE__ */ jsx9(
298
+ return /* @__PURE__ */ jsxs4("section", { className: "my-3 text-sm", "aria-label": entry.title, children: [
299
+ entry.title ? /* @__PURE__ */ jsx10("h4", { className: "mb-1 font-semibold text-foreground", children: entry.title }) : null,
300
+ /* @__PURE__ */ jsx10(
423
301
  "div",
424
302
  {
425
303
  className: "es-doc-prose prose prose-sm max-w-none",
@@ -430,11 +308,11 @@ function DocSection({ entry }) {
430
308
  }
431
309
 
432
310
  // src/components/doc/DocMermaid.tsx
433
- import { Fragment as Fragment3, jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
311
+ import { Fragment as Fragment3, jsx as jsx11, jsxs as jsxs5 } from "react/jsx-runtime";
434
312
  function MermaidSource({ entry, ref }) {
435
- return /* @__PURE__ */ jsxs6("figure", { ref, className: "my-3", "aria-label": entry.title ?? "Diagram", children: [
436
- entry.title ? /* @__PURE__ */ jsx10("figcaption", { className: "mb-1.5 text-xs font-medium text-muted-foreground", children: entry.title }) : null,
437
- /* @__PURE__ */ jsx10(
313
+ return /* @__PURE__ */ jsxs5("figure", { ref, className: "my-3", "aria-label": entry.title ?? "Diagram", children: [
314
+ entry.title ? /* @__PURE__ */ jsx11("figcaption", { className: "mb-1.5 text-xs font-medium text-muted-foreground", children: entry.title }) : null,
315
+ /* @__PURE__ */ jsx11(
438
316
  "pre",
439
317
  {
440
318
  "data-mermaid": true,
@@ -448,24 +326,24 @@ function MermaidSource({ entry, ref }) {
448
326
  function DocMermaid({ entry }) {
449
327
  const renderers = useBuiltinRenderers();
450
328
  if (renderers.mermaid) {
451
- return /* @__PURE__ */ jsx10(Fragment3, { children: renderers.mermaid(entry) });
329
+ return /* @__PURE__ */ jsx11(Fragment3, { children: renderers.mermaid(entry) });
452
330
  }
453
- return /* @__PURE__ */ jsx10(MermaidSource, { entry });
331
+ return /* @__PURE__ */ jsx11(MermaidSource, { entry });
454
332
  }
455
333
 
456
334
  // src/components/doc/DocScreenshot.tsx
457
- import { jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
335
+ import { jsx as jsx12, jsxs as jsxs6 } from "react/jsx-runtime";
458
336
  function DocScreenshot({ entry }) {
459
337
  const src = isLocalFsPath(entry.path) ? void 0 : safeImageUrl(entry.path);
460
338
  if (!src) {
461
- return /* @__PURE__ */ jsxs7("figure", { className: "my-3 rounded-md border border-dashed border-border p-3 text-sm text-muted-foreground", children: [
462
- /* @__PURE__ */ jsx11("div", { className: "font-medium", children: "Screenshot unavailable" }),
463
- /* @__PURE__ */ jsx11("div", { className: "mt-1 break-all font-mono text-xs", children: entry.path }),
464
- entry.alt ? /* @__PURE__ */ jsx11("figcaption", { className: "mt-1.5 text-xs", children: entry.alt }) : null
339
+ return /* @__PURE__ */ jsxs6("figure", { className: "my-3 rounded-md border border-dashed border-border p-3 text-sm text-muted-foreground", children: [
340
+ /* @__PURE__ */ jsx12("div", { className: "font-medium", children: "Screenshot unavailable" }),
341
+ /* @__PURE__ */ jsx12("div", { className: "mt-1 break-all font-mono text-xs", children: entry.path }),
342
+ entry.alt ? /* @__PURE__ */ jsx12("figcaption", { className: "mt-1.5 text-xs", children: entry.alt }) : null
465
343
  ] });
466
344
  }
467
- return /* @__PURE__ */ jsxs7("figure", { className: "my-3", children: [
468
- /* @__PURE__ */ jsx11(
345
+ return /* @__PURE__ */ jsxs6("figure", { className: "my-3", children: [
346
+ /* @__PURE__ */ jsx12(
469
347
  "img",
470
348
  {
471
349
  src,
@@ -474,24 +352,24 @@ function DocScreenshot({ entry }) {
474
352
  className: "max-w-full rounded-md border border-border"
475
353
  }
476
354
  ),
477
- entry.alt ? /* @__PURE__ */ jsx11("figcaption", { className: "mt-1.5 text-xs text-muted-foreground", children: entry.alt }) : null
355
+ entry.alt ? /* @__PURE__ */ jsx12("figcaption", { className: "mt-1.5 text-xs text-muted-foreground", children: entry.alt }) : null
478
356
  ] });
479
357
  }
480
358
 
481
359
  // src/components/doc/DocVideo.tsx
482
- import { jsx as jsx12, jsxs as jsxs8 } from "react/jsx-runtime";
360
+ import { jsx as jsx13, jsxs as jsxs7 } from "react/jsx-runtime";
483
361
  function DocVideo({ entry }) {
484
362
  if (isLocalFsPath(entry.path)) {
485
- return /* @__PURE__ */ jsxs8("figure", { className: "my-3 rounded-md border border-dashed border-border p-3 text-sm text-muted-foreground", children: [
486
- /* @__PURE__ */ jsx12("div", { className: "font-medium", children: "Video unavailable" }),
487
- /* @__PURE__ */ jsx12("div", { className: "mt-1 break-all font-mono text-xs", children: entry.path }),
488
- entry.caption ? /* @__PURE__ */ jsx12("figcaption", { className: "mt-1.5 text-xs", children: entry.caption }) : null
363
+ return /* @__PURE__ */ jsxs7("figure", { className: "my-3 rounded-md border border-dashed border-border p-3 text-sm text-muted-foreground", children: [
364
+ /* @__PURE__ */ jsx13("div", { className: "font-medium", children: "Video unavailable" }),
365
+ /* @__PURE__ */ jsx13("div", { className: "mt-1 break-all font-mono text-xs", children: entry.path }),
366
+ entry.caption ? /* @__PURE__ */ jsx13("figcaption", { className: "mt-1.5 text-xs", children: entry.caption }) : null
489
367
  ] });
490
368
  }
491
369
  const src = safeUrl(entry.path);
492
370
  const poster = safeUrl(entry.poster);
493
- return /* @__PURE__ */ jsxs8("figure", { className: "my-3", children: [
494
- /* @__PURE__ */ jsx12(
371
+ return /* @__PURE__ */ jsxs7("figure", { className: "my-3", children: [
372
+ /* @__PURE__ */ jsx13(
495
373
  "video",
496
374
  {
497
375
  controls: true,
@@ -501,21 +379,21 @@ function DocVideo({ entry }) {
501
379
  className: "max-w-full rounded-md border border-border"
502
380
  }
503
381
  ),
504
- entry.caption ? /* @__PURE__ */ jsx12("figcaption", { className: "mt-1 text-xs text-muted-foreground", children: entry.caption }) : null
382
+ entry.caption ? /* @__PURE__ */ jsx13("figcaption", { className: "mt-1 text-xs text-muted-foreground", children: entry.caption }) : null
505
383
  ] });
506
384
  }
507
385
 
508
386
  // src/components/doc/DocHtml.tsx
509
- import { jsx as jsx13, jsxs as jsxs9 } from "react/jsx-runtime";
387
+ import { jsx as jsx14, jsxs as jsxs8 } from "react/jsx-runtime";
510
388
  function DocHtml({ entry }) {
511
389
  const height = typeof entry.height === "number" ? `${entry.height}px` : entry.height ?? "400px";
512
390
  const title = entry.title ?? "Embedded HTML";
513
391
  const safeHref = safeUrl(entry.url ?? entry.path);
514
392
  const frameProps = entry.content !== void 0 ? { srcDoc: entry.content } : { src: safeHref ?? "" };
515
- return /* @__PURE__ */ jsxs9("figure", { className: "my-3 overflow-hidden rounded-md border border-border", children: [
516
- /* @__PURE__ */ jsxs9("figcaption", { className: "flex items-center justify-between border-b border-border bg-muted/40 px-3 py-1.5 text-xs font-medium text-muted-foreground", children: [
517
- /* @__PURE__ */ jsx13("span", { children: entry.title ?? "HTML" }),
518
- safeHref ? /* @__PURE__ */ jsx13(
393
+ return /* @__PURE__ */ jsxs8("figure", { className: "my-3 overflow-hidden rounded-md border border-border", children: [
394
+ /* @__PURE__ */ jsxs8("figcaption", { className: "flex items-center justify-between border-b border-border bg-muted/40 px-3 py-1.5 text-xs font-medium text-muted-foreground", children: [
395
+ /* @__PURE__ */ jsx14("span", { children: entry.title ?? "HTML" }),
396
+ safeHref ? /* @__PURE__ */ jsx14(
519
397
  "a",
520
398
  {
521
399
  href: safeHref,
@@ -528,7 +406,7 @@ function DocHtml({ entry }) {
528
406
  }
529
407
  ) : null
530
408
  ] }),
531
- /* @__PURE__ */ jsx13(
409
+ /* @__PURE__ */ jsx14(
532
410
  "iframe",
533
411
  {
534
412
  ...frameProps,
@@ -543,7 +421,7 @@ function DocHtml({ entry }) {
543
421
  }
544
422
 
545
423
  // src/components/doc/DocState.tsx
546
- import { jsx as jsx14 } from "react/jsx-runtime";
424
+ import { jsx as jsx15 } from "react/jsx-runtime";
547
425
  function DocState({ entry }) {
548
426
  let json;
549
427
  try {
@@ -551,23 +429,23 @@ function DocState({ entry }) {
551
429
  } catch {
552
430
  json = String(entry.value);
553
431
  }
554
- return /* @__PURE__ */ jsx14(CodeFigure, { label: entry.label ?? "State", children: /* @__PURE__ */ jsx14("code", { className: "font-mono text-xs language-json", children: json }) });
432
+ return /* @__PURE__ */ jsx15(CodeFigure, { label: entry.label ?? "State", children: /* @__PURE__ */ jsx15("code", { className: "font-mono text-xs language-json", children: json }) });
555
433
  }
556
434
 
557
435
  // src/components/doc/DocCustom.tsx
558
- import { Fragment as Fragment4, jsx as jsx15, jsxs as jsxs10 } from "react/jsx-runtime";
436
+ import { Fragment as Fragment4, jsx as jsx16, jsxs as jsxs9 } from "react/jsx-runtime";
559
437
  function DocCustom({ entry }) {
560
438
  const renderers = useCustomRenderers();
561
439
  const renderer = renderers[entry.type];
562
440
  if (renderer) {
563
- return /* @__PURE__ */ jsx15(Fragment4, { children: renderer(entry) });
441
+ return /* @__PURE__ */ jsx16(Fragment4, { children: renderer(entry) });
564
442
  }
565
- return /* @__PURE__ */ jsxs10("div", { className: "my-2", "data-type": entry.type, children: [
566
- /* @__PURE__ */ jsx15("p", { className: "mb-1 text-xs font-medium text-muted-foreground", children: entry.type }),
567
- /* @__PURE__ */ jsx15("pre", { tabIndex: 0, className: "overflow-x-auto rounded-md bg-muted p-3 font-mono text-xs text-foreground", children: safeStringify2(entry.data) })
443
+ return /* @__PURE__ */ jsxs9("div", { className: "my-2", "data-type": entry.type, children: [
444
+ /* @__PURE__ */ jsx16("p", { className: "mb-1 text-xs font-medium text-muted-foreground", children: entry.type }),
445
+ /* @__PURE__ */ jsx16("pre", { tabIndex: 0, className: "overflow-x-auto rounded-md bg-muted p-3 font-mono text-xs text-foreground", children: safeStringify(entry.data) })
568
446
  ] });
569
447
  }
570
- function safeStringify2(value) {
448
+ function safeStringify(value) {
571
449
  try {
572
450
  return JSON.stringify(value, null, 2);
573
451
  } catch {
@@ -576,68 +454,47 @@ function safeStringify2(value) {
576
454
  }
577
455
 
578
456
  // src/components/doc/DocEntry.tsx
579
- import { jsx as jsx16 } from "react/jsx-runtime";
457
+ import { jsx as jsx17 } from "react/jsx-runtime";
580
458
  function DocEntry({ entry }) {
581
459
  switch (entry.kind) {
582
460
  case "note":
583
- return /* @__PURE__ */ jsx16(DocNote, { entry });
461
+ return /* @__PURE__ */ jsx17(DocNote, { entry });
584
462
  case "tag":
585
- return /* @__PURE__ */ jsx16(DocTag, { entry });
463
+ return /* @__PURE__ */ jsx17(DocTag, { entry });
586
464
  case "kv":
587
- return /* @__PURE__ */ jsx16(DocKv, { entry });
465
+ return /* @__PURE__ */ jsx17(DocKv, { entry });
588
466
  case "code":
589
- return /* @__PURE__ */ jsx16(DocCode, { entry });
467
+ return /* @__PURE__ */ jsx17(DocCode, { entry });
590
468
  case "table":
591
- return /* @__PURE__ */ jsx16(DocTable, { entry });
469
+ return /* @__PURE__ */ jsx17(DocTable, { entry });
592
470
  case "link":
593
- return /* @__PURE__ */ jsx16(DocLink, { entry });
471
+ return /* @__PURE__ */ jsx17(DocLink, { entry });
594
472
  case "section":
595
- return /* @__PURE__ */ jsx16(DocSection, { entry });
473
+ return /* @__PURE__ */ jsx17(DocSection, { entry });
596
474
  case "mermaid":
597
- return /* @__PURE__ */ jsx16(DocMermaid, { entry });
475
+ return /* @__PURE__ */ jsx17(DocMermaid, { entry });
598
476
  case "screenshot":
599
- return /* @__PURE__ */ jsx16(DocScreenshot, { entry });
477
+ return /* @__PURE__ */ jsx17(DocScreenshot, { entry });
600
478
  case "video":
601
- return /* @__PURE__ */ jsx16(DocVideo, { entry });
479
+ return /* @__PURE__ */ jsx17(DocVideo, { entry });
602
480
  case "html":
603
- return /* @__PURE__ */ jsx16(DocHtml, { entry });
481
+ return /* @__PURE__ */ jsx17(DocHtml, { entry });
604
482
  case "state":
605
- return /* @__PURE__ */ jsx16(DocState, { entry });
483
+ return /* @__PURE__ */ jsx17(DocState, { entry });
606
484
  case "custom":
607
- return /* @__PURE__ */ jsx16(DocCustom, { entry });
485
+ return /* @__PURE__ */ jsx17(DocCustom, { entry });
608
486
  }
609
487
  }
610
488
 
611
489
  // src/components/ReportDocEntries.tsx
612
- import { Fragment as Fragment5, jsx as jsx17 } from "react/jsx-runtime";
490
+ import { Fragment as Fragment5, jsx as jsx18 } from "react/jsx-runtime";
613
491
  function ReportDocEntries({ entries }) {
614
492
  if (entries.length === 0) return null;
615
- return /* @__PURE__ */ jsx17(Fragment5, { children: entries.map((entry, i) => /* @__PURE__ */ jsx17(DocEntry, { entry }, i)) });
616
- }
617
-
618
- // src/lib/step-params.tsx
619
- import { jsx as jsx18 } from "react/jsx-runtime";
620
- var STEP_PARAM_PATTERN = /"[^"]*"|(?<![\w.-])\d+(?:\.\d+)?(?![\w.-])/g;
621
- function highlightStepParams(text) {
622
- const matches = Array.from(text.matchAll(STEP_PARAM_PATTERN));
623
- if (matches.length === 0) return text;
624
- const nodes = [];
625
- let lastIndex = 0;
626
- matches.forEach((match, i) => {
627
- const start = match.index;
628
- const end = start + match[0].length;
629
- if (start > lastIndex) nodes.push(text.slice(lastIndex, start));
630
- nodes.push(
631
- /* @__PURE__ */ jsx18("span", { className: "font-medium text-step-param italic", children: match[0] }, i)
632
- );
633
- lastIndex = end;
634
- });
635
- if (lastIndex < text.length) nodes.push(text.slice(lastIndex));
636
- return nodes;
493
+ return /* @__PURE__ */ jsx18(Fragment5, { children: entries.map((entry, i) => /* @__PURE__ */ jsx18(DocEntry, { entry }, i)) });
637
494
  }
638
495
 
639
496
  // src/components/ReportSteps.tsx
640
- import { jsx as jsx19, jsxs as jsxs11 } from "react/jsx-runtime";
497
+ import { jsx as jsx19, jsxs as jsxs10 } from "react/jsx-runtime";
641
498
  var STEP_GLYPH = {
642
499
  passed: "\u2713",
643
500
  failed: "\u2717",
@@ -652,20 +509,46 @@ var STEP_GLYPH_COLOR = {
652
509
  };
653
510
  function ReportSteps({ scenario }) {
654
511
  if (scenario.steps.length === 0) return null;
655
- return /* @__PURE__ */ jsx19("ol", { "data-slot": "steps", className: "flex flex-col", children: scenario.steps.map((step) => /* @__PURE__ */ jsx19(ReportStepItem, { step }, step.id)) });
512
+ const unasserted = new Set(
513
+ assertionState(scenario.steps) === "unasserted" ? assertiveSteps(scenario.steps).map((step) => step.id) : []
514
+ );
515
+ return /* @__PURE__ */ jsx19("ol", { "data-slot": "steps", className: "flex flex-col", children: scenario.steps.map((step) => /* @__PURE__ */ jsx19(
516
+ ReportStepItem,
517
+ {
518
+ step,
519
+ unasserted: unasserted.has(step.id)
520
+ },
521
+ step.id
522
+ )) });
656
523
  }
657
- function ReportStepItem({ step }) {
524
+ function ReportStepItem({
525
+ step,
526
+ unasserted = false
527
+ }) {
658
528
  const kw = step.keyword.toLowerCase();
659
529
  const continuation = kw === "and" || kw === "but";
660
- return /* @__PURE__ */ jsxs11(
530
+ return /* @__PURE__ */ jsxs10(
661
531
  "li",
662
532
  {
663
533
  id: step.id,
664
534
  "data-status": step.status,
665
- className: cn("py-1.5 text-[0.8125rem] leading-normal", continuation && "pl-5"),
535
+ className: cn(
536
+ "py-1.5 text-[0.8125rem] leading-normal",
537
+ continuation && "pl-5"
538
+ ),
666
539
  children: [
667
- /* @__PURE__ */ jsxs11("div", { className: "flex items-baseline gap-2", children: [
668
- /* @__PURE__ */ jsx19("span", { "aria-hidden": true, className: cn("w-4 shrink-0 text-center text-xs", STEP_GLYPH_COLOR[step.status]), children: STEP_GLYPH[step.status] ?? "\u25CB" }),
540
+ /* @__PURE__ */ jsxs10("div", { className: "flex items-baseline gap-2", children: [
541
+ /* @__PURE__ */ jsx19(
542
+ "span",
543
+ {
544
+ "aria-hidden": true,
545
+ className: cn(
546
+ "w-4 shrink-0 text-center text-xs",
547
+ STEP_GLYPH_COLOR[step.status]
548
+ ),
549
+ children: STEP_GLYPH[step.status] ?? "\u25CB"
550
+ }
551
+ ),
669
552
  /* @__PURE__ */ jsx19(
670
553
  "span",
671
554
  {
@@ -677,6 +560,7 @@ function ReportStepItem({ step }) {
677
560
  }
678
561
  ),
679
562
  /* @__PURE__ */ jsx19("span", { className: "flex-1 text-foreground", children: highlightStepParams(step.text) }),
563
+ unasserted ? /* @__PURE__ */ jsx19("span", { className: "shrink-0 rounded-full border border-pend-border bg-pend-bg px-2 py-0.5 text-[0.6875rem] font-medium text-pend", children: "No assertion" }) : null,
680
564
  step.durationMs > 0 ? /* @__PURE__ */ jsx19("span", { className: "shrink-0 whitespace-nowrap font-mono text-[0.6875rem] text-muted-foreground", children: formatDuration(step.durationMs) }) : null
681
565
  ] }),
682
566
  step.errorMessage ? /* @__PURE__ */ jsx19(
@@ -697,7 +581,7 @@ function ReportStepItem({ step }) {
697
581
  import { extractStoryboardFrames } from "executable-stories-core/storyboard";
698
582
  import { summarizeStateChanges } from "executable-stories-core/state-diff";
699
583
  import { isLocalFsPath as isLocalFsPath2, safeImageUrl as safeImageUrl2 } from "executable-stories-core/utils/url";
700
- import { jsx as jsx20, jsxs as jsxs12 } from "react/jsx-runtime";
584
+ import { jsx as jsx20, jsxs as jsxs11 } from "react/jsx-runtime";
701
585
  var FRAME_GLYPH = {
702
586
  passed: "\u2713",
703
587
  failed: "\u2717",
@@ -713,7 +597,7 @@ var FRAME_GLYPH_COLOR = {
713
597
  function ReportStoryboard({ scenario }) {
714
598
  const frames = extractStoryboardFrames(scenario);
715
599
  if (frames.length < 2) return null;
716
- return /* @__PURE__ */ jsxs12("section", { "data-slot": "storyboard", "aria-label": "Storyboard", className: "mb-3", children: [
600
+ return /* @__PURE__ */ jsxs11("section", { "data-slot": "storyboard", "aria-label": "Storyboard", className: "mb-3", children: [
717
601
  /* @__PURE__ */ jsx20("h4", { className: "mb-1.5 text-xs font-semibold tracking-wide text-muted-foreground uppercase", children: "Storyboard" }),
718
602
  /* @__PURE__ */ jsx20("ol", { className: "flex items-start gap-3 overflow-x-auto pb-2", children: frames.map((frame) => /* @__PURE__ */ jsx20(StoryboardFrameItem, { frame }, frame.stepId)) })
719
603
  ] });
@@ -721,9 +605,9 @@ function ReportStoryboard({ scenario }) {
721
605
  function StoryboardFrameItem({ frame }) {
722
606
  const label = `${frame.keyword} ${frame.text}`;
723
607
  const hasStates = frame.states.length > 0;
724
- const caption = /* @__PURE__ */ jsxs12("p", { className: "mt-1 flex items-baseline gap-1.5 text-[0.6875rem] leading-snug", children: [
608
+ const caption = /* @__PURE__ */ jsxs11("p", { className: "mt-1 flex items-baseline gap-1.5 text-[0.6875rem] leading-snug", children: [
725
609
  /* @__PURE__ */ jsx20("span", { "aria-hidden": true, className: cn("shrink-0", FRAME_GLYPH_COLOR[frame.status]), children: FRAME_GLYPH[frame.status] ?? "\u25CB" }),
726
- /* @__PURE__ */ jsxs12("span", { className: "min-w-0", children: [
610
+ /* @__PURE__ */ jsxs11("span", { className: "min-w-0", children: [
727
611
  /* @__PURE__ */ jsx20("span", { className: "font-mono font-semibold text-keyword", children: frame.keyword }),
728
612
  " ",
729
613
  /* @__PURE__ */ jsx20("span", { className: "text-muted-foreground", children: frame.screenshot?.alt ?? frame.text })
@@ -732,7 +616,7 @@ function StoryboardFrameItem({ frame }) {
732
616
  return (
733
617
  // State cards need room for diff rows; screenshot-only frames keep the
734
618
  // original narrow film-cell width (and DOM) unchanged.
735
- /* @__PURE__ */ jsxs12("li", { className: cn("shrink-0", hasStates ? "w-64" : "w-40"), children: [
619
+ /* @__PURE__ */ jsxs11("li", { className: cn("shrink-0", hasStates ? "w-64" : "w-40"), children: [
736
620
  frame.screenshot ? /* @__PURE__ */ jsx20(FrameScreenshot, { frame, label }) : null,
737
621
  hasStates ? /* @__PURE__ */ jsx20("div", { className: cn("flex flex-col gap-1.5", frame.screenshot && "mt-1.5"), children: frame.states.map((card, i) => /* @__PURE__ */ jsx20(StateCardView, { card }, card.label ?? i)) }) : null,
738
622
  frame.screenshot ? caption : (
@@ -779,8 +663,8 @@ function prettyJson(value) {
779
663
  function StateCardView({ card }) {
780
664
  const json = prettyJson(card.value);
781
665
  const firstCapture = card.changes === void 0;
782
- return /* @__PURE__ */ jsxs12("div", { "data-slot": "state-card", className: "rounded-md border border-border bg-card p-2", children: [
783
- /* @__PURE__ */ jsxs12("p", { className: "flex flex-wrap items-baseline gap-1.5", children: [
666
+ return /* @__PURE__ */ jsxs11("div", { "data-slot": "state-card", className: "rounded-md border border-border bg-card p-2", children: [
667
+ /* @__PURE__ */ jsxs11("p", { className: "flex flex-wrap items-baseline gap-1.5", children: [
784
668
  /* @__PURE__ */ jsx20("span", { className: "inline-flex rounded-full border border-border px-1.5 font-mono text-[0.625rem] font-medium text-muted-foreground", children: card.label ?? "State" }),
785
669
  firstCapture ? /* @__PURE__ */ jsx20("span", { className: "text-[0.625rem] text-muted-foreground italic", children: "first capture" }) : null
786
670
  ] }),
@@ -795,7 +679,7 @@ function StateCardView({ card }) {
795
679
  },
796
680
  i
797
681
  )) }) : /* @__PURE__ */ jsx20("p", { className: "mt-1.5 text-[0.625rem] text-muted-foreground", children: "No changes" })),
798
- /* @__PURE__ */ jsxs12(
682
+ /* @__PURE__ */ jsxs11(
799
683
  "details",
800
684
  {
801
685
  open: firstCapture && json.length <= 240,
@@ -810,7 +694,7 @@ function StateCardView({ card }) {
810
694
  }
811
695
 
812
696
  // src/components/ReportAttachments.tsx
813
- import { Fragment as Fragment6, jsx as jsx21, jsxs as jsxs13 } from "react/jsx-runtime";
697
+ import { Fragment as Fragment6, jsx as jsx21, jsxs as jsxs12 } from "react/jsx-runtime";
814
698
  function ReportAttachments({ attachments }) {
815
699
  if (attachments.length === 0) return null;
816
700
  return (
@@ -819,7 +703,7 @@ function ReportAttachments({ attachments }) {
819
703
  /* @__PURE__ */ jsx21("div", { role: "group", "aria-label": "Attachments", className: "mt-3 flex flex-col gap-2", children: attachments.map((att, i) => {
820
704
  const dataUri = att.contentEncoding === "BASE64" ? `data:${att.mediaType};base64,${att.body}` : `data:${att.mediaType},${encodeURIComponent(att.body)}`;
821
705
  const isImage = att.mediaType.startsWith("image/");
822
- return /* @__PURE__ */ jsx21("figure", { className: "m-0", children: isImage ? /* @__PURE__ */ jsxs13(Fragment6, { children: [
706
+ return /* @__PURE__ */ jsx21("figure", { className: "m-0", children: isImage ? /* @__PURE__ */ jsxs12(Fragment6, { children: [
823
707
  /* @__PURE__ */ jsx21(
824
708
  "img",
825
709
  {
@@ -830,7 +714,7 @@ function ReportAttachments({ attachments }) {
830
714
  }
831
715
  ),
832
716
  /* @__PURE__ */ jsx21("figcaption", { className: "mt-1 text-xs text-muted-foreground", children: att.name })
833
- ] }) : /* @__PURE__ */ jsxs13(
717
+ ] }) : /* @__PURE__ */ jsxs12(
834
718
  "a",
835
719
  {
836
720
  href: dataUri,
@@ -839,7 +723,7 @@ function ReportAttachments({ attachments }) {
839
723
  children: [
840
724
  att.name,
841
725
  " ",
842
- /* @__PURE__ */ jsxs13("span", { className: "text-muted-foreground", children: [
726
+ /* @__PURE__ */ jsxs12("span", { className: "text-muted-foreground", children: [
843
727
  "(",
844
728
  att.mediaType,
845
729
  ")"
@@ -930,7 +814,7 @@ function traceWindow(rows) {
930
814
  }
931
815
 
932
816
  // src/components/ReportTrace.tsx
933
- import { jsx as jsx22, jsxs as jsxs14 } from "react/jsx-runtime";
817
+ import { jsx as jsx22, jsxs as jsxs13 } from "react/jsx-runtime";
934
818
  var BAR_COLOR = {
935
819
  ok: "bg-pass",
936
820
  error: "bg-fail",
@@ -941,8 +825,8 @@ function ReportTrace({ spans }) {
941
825
  const rows = toTraceRows(spans);
942
826
  if (rows.length === 0) return null;
943
827
  const { minStartMs, totalMs } = traceWindow(rows);
944
- return /* @__PURE__ */ jsxs14("details", { className: "mt-3 rounded-md border border-border bg-muted/30", children: [
945
- /* @__PURE__ */ jsxs14("summary", { className: "cursor-pointer px-3 py-2 text-xs font-medium text-muted-foreground", children: [
828
+ return /* @__PURE__ */ jsxs13("details", { className: "mt-3 rounded-md border border-border bg-muted/30", children: [
829
+ /* @__PURE__ */ jsxs13("summary", { className: "cursor-pointer px-3 py-2 text-xs font-medium text-muted-foreground", children: [
946
830
  "Trace (",
947
831
  rows.length,
948
832
  " span",
@@ -954,7 +838,7 @@ function ReportTrace({ spans }) {
954
838
  /* @__PURE__ */ jsx22("ul", { className: "flex flex-col gap-0.5 px-3 pb-3 font-mono text-[0.6875rem]", children: rows.map((row, i) => {
955
839
  const leftPct = (row.startTimeMs - minStartMs) / totalMs * 100;
956
840
  const widthPct = Math.max(row.durationMs / totalMs * 100, 0.5);
957
- return /* @__PURE__ */ jsxs14("li", { className: "flex items-center gap-2", children: [
841
+ return /* @__PURE__ */ jsxs13("li", { className: "flex items-center gap-2", children: [
958
842
  /* @__PURE__ */ jsx22(
959
843
  "span",
960
844
  {
@@ -1040,7 +924,7 @@ function diffSinceLastRun(history, scenarios) {
1040
924
  }
1041
925
 
1042
926
  // src/components/ScenarioRunHistory.tsx
1043
- import { Fragment as Fragment7, jsx as jsx23, jsxs as jsxs15 } from "react/jsx-runtime";
927
+ import { Fragment as Fragment7, jsx as jsx23, jsxs as jsxs14 } from "react/jsx-runtime";
1044
928
  var MAX_DOTS = 10;
1045
929
  var DOT_COLOR = {
1046
930
  passed: "bg-pass",
@@ -1060,7 +944,7 @@ function ScenarioRunHistory({ scenarioId }) {
1060
944
  const recent = entries.slice(-MAX_DOTS);
1061
945
  const flaky = flakinessOf(recent) === "flaky";
1062
946
  const summary = flaky ? `${describeRunHistory(recent)} \xB7 Flaky` : describeRunHistory(recent);
1063
- return /* @__PURE__ */ jsxs15(Fragment7, { children: [
947
+ return /* @__PURE__ */ jsxs14(Fragment7, { children: [
1064
948
  flaky ? /* @__PURE__ */ jsx23(Badge, { variant: "pending", "aria-label": "Flaky: status flips between recent runs", title: summary, children: "Flaky" }) : null,
1065
949
  /* @__PURE__ */ jsx23(
1066
950
  "span",
@@ -1090,24 +974,103 @@ function ScenarioRunHistory({ scenarioId }) {
1090
974
  ] });
1091
975
  }
1092
976
 
1093
- // src/interactive/collapse-context.ts
1094
- import { createContext as createContext3, useCallback, useContext as useContext4, useState } from "react";
1095
- var CollapseContext = createContext3(null);
1096
- var CollapseProvider = CollapseContext.Provider;
1097
- function useCollapse() {
1098
- return useContext4(CollapseContext);
1099
- }
1100
- var STORAGE_KEY = "es-collapsed-ids";
1101
- function readPersisted() {
1102
- if (typeof localStorage === "undefined") return null;
1103
- try {
1104
- const raw = localStorage.getItem(STORAGE_KEY);
1105
- return raw ? new Set(JSON.parse(raw)) : null;
1106
- } catch {
1107
- return null;
1108
- }
977
+ // src/components/ScenarioStaleness.tsx
978
+ import { useEffect, useState } from "react";
979
+
980
+ // src/lib/provenance.ts
981
+ function reportLastRunMs(report) {
982
+ if (report.finishedAtMs && report.finishedAtMs > 0) return report.finishedAtMs;
983
+ if (report.startedAtMs && report.startedAtMs > 0) return report.startedAtMs;
984
+ return void 0;
1109
985
  }
1110
- function persist(ids) {
986
+ function formatRelativeAge(thenMs, nowMs) {
987
+ const deltaMs = Math.max(0, nowMs - thenMs);
988
+ const minutes = Math.floor(deltaMs / 6e4);
989
+ if (minutes < 1) return "just now";
990
+ if (minutes < 60) return `${minutes} minute${minutes === 1 ? "" : "s"} ago`;
991
+ const hours = Math.floor(minutes / 60);
992
+ if (hours < 24) return `${hours} hour${hours === 1 ? "" : "s"} ago`;
993
+ const days = Math.floor(hours / 24);
994
+ return `${days} day${days === 1 ? "" : "s"} ago`;
995
+ }
996
+ function isReportStale(lastRunMs, staleAfterDays, nowMs) {
997
+ if (lastRunMs === void 0 || staleAfterDays <= 0) return false;
998
+ return nowMs - lastRunMs >= staleAfterDays * 864e5;
999
+ }
1000
+ var CI_DISPLAY_NAMES = {
1001
+ github: "GitHub Actions",
1002
+ gitlab: "GitLab CI",
1003
+ circleci: "CircleCI",
1004
+ jenkins: "Jenkins",
1005
+ azure: "Azure DevOps",
1006
+ buildkite: "Buildkite",
1007
+ travis: "Travis CI",
1008
+ ci: "CI"
1009
+ };
1010
+ function ciDisplayName(name) {
1011
+ return CI_DISPLAY_NAMES[name] ?? name;
1012
+ }
1013
+ function commitUrl(ci, sha) {
1014
+ const commitSha = ci?.commitSha ?? sha;
1015
+ if (!ci?.url || !commitSha) return void 0;
1016
+ const github = ci.url.match(/^(https?:\/\/[^/]+\/[^/]+\/[^/]+)\/actions\/runs\//);
1017
+ if (github) return `${github[1]}/commit/${commitSha}`;
1018
+ const gitlab = ci.url.match(/^(https?:\/\/.+)\/-\/pipelines\//);
1019
+ if (gitlab) return `${gitlab[1]}/-/commit/${commitSha}`;
1020
+ return void 0;
1021
+ }
1022
+ function prUrl(ci) {
1023
+ if (!ci?.url || !ci.prNumber) return void 0;
1024
+ const github = ci.url.match(/^(https?:\/\/[^/]+\/[^/]+\/[^/]+)\/actions\/runs\//);
1025
+ if (github) return `${github[1]}/pull/${ci.prNumber}`;
1026
+ return void 0;
1027
+ }
1028
+
1029
+ // src/interactive/scenario-freshness-context.tsx
1030
+ import { createContext as createContext3, useContext as useContext4 } from "react";
1031
+ var ScenarioFreshnessContext = createContext3(null);
1032
+ var ScenarioFreshnessProvider = ScenarioFreshnessContext.Provider;
1033
+ function useScenarioFreshness() {
1034
+ return useContext4(ScenarioFreshnessContext);
1035
+ }
1036
+
1037
+ // src/components/ScenarioStaleness.tsx
1038
+ import { jsxs as jsxs15 } from "react/jsx-runtime";
1039
+ function ScenarioStaleness({ scenario }) {
1040
+ const freshness = useScenarioFreshness();
1041
+ const fixedNow = freshness?.nowMs;
1042
+ const [now, setNow] = useState(fixedNow);
1043
+ useEffect(() => {
1044
+ if (fixedNow === void 0) setNow(Date.now());
1045
+ }, [fixedNow]);
1046
+ if (!freshness || now === void 0) return null;
1047
+ const lastRunAtMs = scenario.lastRunAtMs;
1048
+ if (!lastRunAtMs || lastRunAtMs <= 0) return null;
1049
+ if (!isReportStale(lastRunAtMs, freshness.staleAfterDays, now)) return null;
1050
+ return /* @__PURE__ */ jsxs15(Badge, { variant: "outline", "aria-label": "Scenario freshness", children: [
1051
+ "Last ran ",
1052
+ formatRelativeAge(lastRunAtMs, now)
1053
+ ] });
1054
+ }
1055
+
1056
+ // src/interactive/collapse-context.ts
1057
+ import { createContext as createContext4, useCallback, useContext as useContext5, useState as useState2 } from "react";
1058
+ var CollapseContext = createContext4(null);
1059
+ var CollapseProvider = CollapseContext.Provider;
1060
+ function useCollapse() {
1061
+ return useContext5(CollapseContext);
1062
+ }
1063
+ var STORAGE_KEY = "es-collapsed-ids";
1064
+ function readPersisted() {
1065
+ if (typeof localStorage === "undefined") return null;
1066
+ try {
1067
+ const raw = localStorage.getItem(STORAGE_KEY);
1068
+ return raw ? new Set(JSON.parse(raw)) : null;
1069
+ } catch {
1070
+ return null;
1071
+ }
1072
+ }
1073
+ function persist(ids) {
1111
1074
  if (typeof localStorage === "undefined") return;
1112
1075
  try {
1113
1076
  localStorage.setItem(STORAGE_KEY, JSON.stringify([...ids]));
@@ -1115,7 +1078,7 @@ function persist(ids) {
1115
1078
  }
1116
1079
  }
1117
1080
  function useCollapseState(computeDefault) {
1118
- const [collapsed, setCollapsed] = useState(
1081
+ const [collapsed, setCollapsed] = useState2(
1119
1082
  () => readPersisted() ?? computeDefault?.() ?? /* @__PURE__ */ new Set()
1120
1083
  );
1121
1084
  const isCollapsed = useCallback((id) => collapsed.has(id), [collapsed]);
@@ -1146,12 +1109,12 @@ function useCollapseState(computeDefault) {
1146
1109
  }
1147
1110
 
1148
1111
  // src/interactive/scenario-actions.tsx
1149
- import { createContext as createContext4, useContext as useContext5 } from "react";
1112
+ import { createContext as createContext5, useContext as useContext6 } from "react";
1150
1113
  import { scenarioToMarkdown as coreScenarioToMarkdown } from "executable-stories-core/utils/scenario-markdown";
1151
- var ScenarioActionsContext = createContext4(null);
1114
+ var ScenarioActionsContext = createContext5(null);
1152
1115
  var ScenarioActionsProvider = ScenarioActionsContext.Provider;
1153
1116
  function useScenarioActions() {
1154
- return useContext5(ScenarioActionsContext);
1117
+ return useContext6(ScenarioActionsContext);
1155
1118
  }
1156
1119
  function scenarioToMarkdown(scenario) {
1157
1120
  return coreScenarioToMarkdown(scenario, { variant: "compact" });
@@ -1319,6 +1282,7 @@ function ReportScenario({ scenario, hideTitle = false }) {
1319
1282
  ] }),
1320
1283
  /* @__PURE__ */ jsxs17("div", { className: "flex shrink-0 items-center gap-2", children: [
1321
1284
  /* @__PURE__ */ jsx26(ScenarioRunHistory, { scenarioId: scenario.id }),
1285
+ /* @__PURE__ */ jsx26(ScenarioStaleness, { scenario }),
1322
1286
  scenario.durationMs > 0 ? /* @__PURE__ */ jsx26("span", { className: "whitespace-nowrap font-mono text-[0.6875rem] text-muted-foreground", children: formatDuration3(scenario.durationMs) }) : null,
1323
1287
  meta.showPill ? /* @__PURE__ */ jsx26(Badge, { variant: meta.badgeVariant, "aria-label": `Status: ${label}`, children: label }) : /* @__PURE__ */ jsx26("span", { className: "sr-only", children: `Status: ${label}` }),
1324
1288
  actions ? /* @__PURE__ */ jsxs17(DropdownMenu, { children: [
@@ -1490,71 +1454,74 @@ function ReportFeatureList() {
1490
1454
  return /* @__PURE__ */ jsx30("div", { className: "flex flex-col gap-8", children: report.features.map((feature) => /* @__PURE__ */ jsx30(ReportFeature, { feature }, feature.id)) });
1491
1455
  }
1492
1456
 
1493
- // src/components/ui/empty.tsx
1494
- import { cva as cva2 } from "class-variance-authority";
1495
- import { jsx as jsx31 } from "react/jsx-runtime";
1496
- function Empty({ className, ...props }) {
1497
- return /* @__PURE__ */ jsx31(
1498
- "div",
1499
- {
1500
- "data-slot": "empty",
1501
- className: cn(
1502
- "flex min-w-0 flex-1 flex-col items-center justify-center gap-6 rounded-lg border-dashed p-6 text-center text-balance md:p-12",
1503
- className
1504
- ),
1505
- ...props
1506
- }
1507
- );
1508
- }
1509
- function EmptyHeader({ className, ...props }) {
1510
- return /* @__PURE__ */ jsx31(
1511
- "div",
1512
- {
1513
- "data-slot": "empty-header",
1514
- className: cn(
1515
- "flex max-w-sm flex-col items-center gap-2 text-center",
1516
- className
1517
- ),
1518
- ...props
1519
- }
1520
- );
1521
- }
1522
- var emptyMediaVariants = cva2(
1523
- "mb-2 flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0",
1524
- {
1525
- variants: {
1526
- variant: {
1527
- default: "bg-transparent",
1528
- icon: "flex size-10 shrink-0 items-center justify-center rounded-lg bg-muted text-foreground [&_svg:not([class*='size-'])]:size-6"
1529
- }
1530
- },
1531
- defaultVariants: {
1532
- variant: "default"
1533
- }
1457
+ // src/components/ReportMeta.tsx
1458
+ import { formatDuration as formatDuration4 } from "executable-stories-core/utils/duration";
1459
+ import { jsx as jsx31, jsxs as jsxs20 } from "react/jsx-runtime";
1460
+ function ReportMeta() {
1461
+ const report = useReport();
1462
+ const items = [];
1463
+ const linkClass = "text-primary underline underline-offset-2";
1464
+ if (report.startedAtMs) {
1465
+ items.push({ label: "Started", value: new Date(report.startedAtMs).toLocaleString() });
1534
1466
  }
1535
- );
1536
- function EmptyTitle({ className, ...props }) {
1537
- return /* @__PURE__ */ jsx31(
1538
- "div",
1539
- {
1540
- "data-slot": "empty-title",
1541
- className: cn("text-lg font-medium tracking-tight", className),
1542
- ...props
1543
- }
1467
+ if (report.durationMs) {
1468
+ items.push({ label: "Duration", value: formatDuration4(report.durationMs) });
1469
+ }
1470
+ if (report.packageVersion) {
1471
+ items.push({ label: "Version", value: report.packageVersion });
1472
+ }
1473
+ if (report.ci?.branch) {
1474
+ items.push({ label: "Branch", value: report.ci.branch });
1475
+ }
1476
+ const sha = report.ci?.commitSha ?? report.gitSha;
1477
+ if (sha) {
1478
+ const url = commitUrl(report.ci, report.gitSha);
1479
+ const short = sha.slice(0, 8);
1480
+ items.push({
1481
+ label: "Commit",
1482
+ value: url ? /* @__PURE__ */ jsx31("a", { href: url, target: "_blank", rel: "noreferrer noopener", className: linkClass, children: short }) : short
1483
+ });
1484
+ }
1485
+ if (report.ci) {
1486
+ items.push({
1487
+ label: "CI",
1488
+ value: report.ci.url ? /* @__PURE__ */ jsxs20("a", { href: report.ci.url, target: "_blank", rel: "noreferrer noopener", className: linkClass, children: [
1489
+ ciDisplayName(report.ci.name),
1490
+ report.ci.buildNumber ? ` #${report.ci.buildNumber}` : ""
1491
+ ] }) : `${ciDisplayName(report.ci.name)}${report.ci.buildNumber ? ` #${report.ci.buildNumber}` : ""}`
1492
+ });
1493
+ }
1494
+ if (report.ci?.prNumber) {
1495
+ const url = prUrl(report.ci);
1496
+ const label = `#${report.ci.prNumber}`;
1497
+ items.push({
1498
+ label: "PR",
1499
+ value: url ? /* @__PURE__ */ jsx31("a", { href: url, target: "_blank", rel: "noreferrer noopener", className: linkClass, children: label }) : label
1500
+ });
1501
+ }
1502
+ if (items.length === 0) return null;
1503
+ return (
1504
+ // Run metadata is useful but secondary to "what failed" — tuck it into a
1505
+ // collapsed disclosure so it doesn't push the result down. Native <details>
1506
+ // so it works with no JS (the SSR/no-hydration report still toggles).
1507
+ /* @__PURE__ */ jsxs20("details", { "data-es-meta-card": true, className: "es-run-details group rounded-lg border border-border bg-card text-xs", children: [
1508
+ /* @__PURE__ */ jsxs20("summary", { className: "flex cursor-pointer list-none select-none items-center gap-1.5 px-4 py-2 font-medium text-muted-foreground [&::-webkit-details-marker]:hidden", children: [
1509
+ /* @__PURE__ */ jsx31("span", { "aria-hidden": true, className: "text-xs transition-transform group-open:rotate-90", children: "\u25B8" }),
1510
+ "Run details"
1511
+ ] }),
1512
+ /* @__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: [
1513
+ /* @__PURE__ */ jsx31("dt", { className: "font-medium", children: item.label }),
1514
+ /* @__PURE__ */ jsx31("dd", { className: "font-mono text-foreground", children: item.value })
1515
+ ] }, item.label)) })
1516
+ ] })
1544
1517
  );
1545
1518
  }
1546
1519
 
1547
- // src/components/ReportEmpty.tsx
1548
- import { jsx as jsx32 } from "react/jsx-runtime";
1549
- function ReportEmpty({ message }) {
1550
- return /* @__PURE__ */ jsx32(Empty, { "aria-live": "polite", className: "border", children: /* @__PURE__ */ jsx32(EmptyHeader, { children: /* @__PURE__ */ jsx32(EmptyTitle, { children: message ?? "No scenarios in this report." }) }) });
1551
- }
1552
-
1553
1520
  // src/components/ReportSummary.tsx
1554
- import { jsx as jsx33, jsxs as jsxs20 } from "react/jsx-runtime";
1521
+ import { jsx as jsx32, jsxs as jsxs21 } from "react/jsx-runtime";
1555
1522
  function ReportSummary({ className }) {
1556
1523
  const report = useReport();
1557
- return /* @__PURE__ */ jsx33(
1524
+ return /* @__PURE__ */ jsx32(
1558
1525
  ReportSummaryView,
1559
1526
  {
1560
1527
  summary: report.summary,
@@ -1580,7 +1547,7 @@ function ReportSummaryView({ summary, className, ariaLabel }) {
1580
1547
  if (summary.pending > 0) {
1581
1548
  cards.push({ status: "pending", label: "Pending", value: summary.pending });
1582
1549
  }
1583
- return /* @__PURE__ */ jsx33(
1550
+ return /* @__PURE__ */ jsx32(
1584
1551
  "dl",
1585
1552
  {
1586
1553
  className: cn(
@@ -1593,17 +1560,17 @@ function ReportSummaryView({ summary, className, ariaLabel }) {
1593
1560
  "aria-label": ariaLabel,
1594
1561
  children: cards.map((c) => {
1595
1562
  const tone = TONE[c.status];
1596
- return /* @__PURE__ */ jsxs20(
1563
+ return /* @__PURE__ */ jsxs21(
1597
1564
  "div",
1598
1565
  {
1599
1566
  "data-status": c.status,
1600
1567
  className: "rounded-lg border bg-card px-4 py-3",
1601
1568
  children: [
1602
- /* @__PURE__ */ jsxs20("dt", { className: "flex items-center gap-1.5 font-mono text-[11px] font-medium uppercase tracking-wider text-muted-foreground", children: [
1603
- /* @__PURE__ */ jsx33("span", { className: cn("size-1.5 shrink-0 rounded-full", tone.dot), "aria-hidden": "true" }),
1569
+ /* @__PURE__ */ jsxs21("dt", { className: "flex items-center gap-1.5 font-mono text-[11px] font-medium uppercase tracking-wider text-muted-foreground", children: [
1570
+ /* @__PURE__ */ jsx32("span", { className: cn("size-1.5 shrink-0 rounded-full", tone.dot), "aria-hidden": "true" }),
1604
1571
  c.label
1605
1572
  ] }),
1606
- /* @__PURE__ */ jsx33("dd", { className: cn("mt-2 text-3xl font-semibold leading-none tracking-tight tabular-nums", tone.num), children: c.value })
1573
+ /* @__PURE__ */ jsx32("dd", { className: cn("mt-2 text-3xl font-semibold leading-none tracking-tight tabular-nums", tone.num), children: c.value })
1607
1574
  ]
1608
1575
  },
1609
1576
  c.status
@@ -1613,165 +1580,48 @@ function ReportSummaryView({ summary, className, ariaLabel }) {
1613
1580
  );
1614
1581
  }
1615
1582
 
1616
- // src/components/ReportMeta.tsx
1617
- import { formatDuration as formatDuration4 } from "executable-stories-core/utils/duration";
1618
-
1619
- // src/lib/provenance.ts
1620
- function reportLastRunMs(report) {
1621
- if (report.finishedAtMs && report.finishedAtMs > 0) return report.finishedAtMs;
1622
- if (report.startedAtMs && report.startedAtMs > 0) return report.startedAtMs;
1623
- return void 0;
1624
- }
1625
- function formatRelativeAge(thenMs, nowMs) {
1626
- const deltaMs = Math.max(0, nowMs - thenMs);
1627
- const minutes = Math.floor(deltaMs / 6e4);
1628
- if (minutes < 1) return "just now";
1629
- if (minutes < 60) return `${minutes} minute${minutes === 1 ? "" : "s"} ago`;
1630
- const hours = Math.floor(minutes / 60);
1631
- if (hours < 24) return `${hours} hour${hours === 1 ? "" : "s"} ago`;
1632
- const days = Math.floor(hours / 24);
1633
- return `${days} day${days === 1 ? "" : "s"} ago`;
1634
- }
1635
- function isReportStale(lastRunMs, staleAfterDays, nowMs) {
1636
- if (lastRunMs === void 0 || staleAfterDays <= 0) return false;
1637
- return nowMs - lastRunMs >= staleAfterDays * 864e5;
1638
- }
1639
- var CI_DISPLAY_NAMES = {
1640
- github: "GitHub Actions",
1641
- gitlab: "GitLab CI",
1642
- circleci: "CircleCI",
1643
- jenkins: "Jenkins",
1644
- azure: "Azure DevOps",
1645
- buildkite: "Buildkite",
1646
- travis: "Travis CI",
1647
- ci: "CI"
1648
- };
1649
- function ciDisplayName(name) {
1650
- return CI_DISPLAY_NAMES[name] ?? name;
1651
- }
1652
- function commitUrl(ci, sha) {
1653
- const commitSha = ci?.commitSha ?? sha;
1654
- if (!ci?.url || !commitSha) return void 0;
1655
- const github = ci.url.match(/^(https?:\/\/[^/]+\/[^/]+\/[^/]+)\/actions\/runs\//);
1656
- if (github) return `${github[1]}/commit/${commitSha}`;
1657
- const gitlab = ci.url.match(/^(https?:\/\/.+)\/-\/pipelines\//);
1658
- if (gitlab) return `${gitlab[1]}/-/commit/${commitSha}`;
1659
- return void 0;
1583
+ // src/components/ReportSchemaError.tsx
1584
+ import { jsx as jsx33, jsxs as jsxs22 } from "react/jsx-runtime";
1585
+ function ReportSchemaError({ error }) {
1586
+ return /* @__PURE__ */ jsxs22("section", { className: "es-schema-error", role: "alert", "aria-live": "assertive", children: [
1587
+ /* @__PURE__ */ jsx33("p", { children: /* @__PURE__ */ jsx33("strong", { children: "Report could not be displayed." }) }),
1588
+ /* @__PURE__ */ jsx33("p", { children: error.message }),
1589
+ error.code === "SCHEMA_VERSION_MISMATCH" ? /* @__PURE__ */ jsxs22("p", { children: [
1590
+ "The report bundle is newer than this version of ",
1591
+ /* @__PURE__ */ jsx33("code", { children: "executable-stories-react" }),
1592
+ ". Upgrade the package, or regenerate the report with an older formatters CLI."
1593
+ ] }) : null,
1594
+ error.issues && error.issues.length > 0 ? /* @__PURE__ */ jsxs22("details", { children: [
1595
+ /* @__PURE__ */ jsxs22("summary", { children: [
1596
+ error.issues.length,
1597
+ " validation issue",
1598
+ error.issues.length === 1 ? "" : "s"
1599
+ ] }),
1600
+ /* @__PURE__ */ jsx33("pre", { children: error.issues.slice(0, 20).map((i) => `${i.path}: ${i.message}`).join("\n") })
1601
+ ] }) : null
1602
+ ] });
1660
1603
  }
1661
- function prUrl(ci) {
1662
- if (!ci?.url || !ci.prNumber) return void 0;
1663
- const github = ci.url.match(/^(https?:\/\/[^/]+\/[^/]+\/[^/]+)\/actions\/runs\//);
1664
- if (github) return `${github[1]}/pull/${ci.prNumber}`;
1665
- return void 0;
1604
+
1605
+ // src/components/ReportShell.tsx
1606
+ import { Fragment as Fragment10, jsx as jsx34, jsxs as jsxs23 } from "react/jsx-runtime";
1607
+ function ReportErrorShell({
1608
+ error,
1609
+ className,
1610
+ title,
1611
+ dataTheme
1612
+ }) {
1613
+ return /* @__PURE__ */ jsx34("main", { className: cn("es-report", className), "aria-label": title ?? "Test report", "data-theme": dataTheme, children: /* @__PURE__ */ jsx34(ReportSchemaError, { error }) });
1666
1614
  }
1667
1615
 
1668
- // src/components/ReportMeta.tsx
1669
- import { jsx as jsx34, jsxs as jsxs21 } from "react/jsx-runtime";
1670
- function ReportMeta() {
1671
- const report = useReport();
1672
- const items = [];
1673
- const linkClass = "text-primary underline underline-offset-2";
1674
- if (report.startedAtMs) {
1675
- items.push({ label: "Started", value: new Date(report.startedAtMs).toLocaleString() });
1676
- }
1677
- if (report.durationMs) {
1678
- items.push({ label: "Duration", value: formatDuration4(report.durationMs) });
1679
- }
1680
- if (report.packageVersion) {
1681
- items.push({ label: "Version", value: report.packageVersion });
1682
- }
1683
- if (report.ci?.branch) {
1684
- items.push({ label: "Branch", value: report.ci.branch });
1685
- }
1686
- const sha = report.ci?.commitSha ?? report.gitSha;
1687
- if (sha) {
1688
- const url = commitUrl(report.ci, report.gitSha);
1689
- const short = sha.slice(0, 8);
1690
- items.push({
1691
- label: "Commit",
1692
- value: url ? /* @__PURE__ */ jsx34("a", { href: url, target: "_blank", rel: "noreferrer noopener", className: linkClass, children: short }) : short
1693
- });
1694
- }
1695
- if (report.ci) {
1696
- items.push({
1697
- label: "CI",
1698
- value: report.ci.url ? /* @__PURE__ */ jsxs21("a", { href: report.ci.url, target: "_blank", rel: "noreferrer noopener", className: linkClass, children: [
1699
- ciDisplayName(report.ci.name),
1700
- report.ci.buildNumber ? ` #${report.ci.buildNumber}` : ""
1701
- ] }) : `${ciDisplayName(report.ci.name)}${report.ci.buildNumber ? ` #${report.ci.buildNumber}` : ""}`
1702
- });
1703
- }
1704
- if (report.ci?.prNumber) {
1705
- const url = prUrl(report.ci);
1706
- const label = `#${report.ci.prNumber}`;
1707
- items.push({
1708
- label: "PR",
1709
- value: url ? /* @__PURE__ */ jsx34("a", { href: url, target: "_blank", rel: "noreferrer noopener", className: linkClass, children: label }) : label
1710
- });
1711
- }
1712
- if (items.length === 0) return null;
1713
- return (
1714
- // Run metadata is useful but secondary to "what failed" — tuck it into a
1715
- // collapsed disclosure so it doesn't push the result down. Native <details>
1716
- // so it works with no JS (the SSR/no-hydration report still toggles).
1717
- /* @__PURE__ */ jsxs21("details", { "data-es-meta-card": true, className: "es-run-details group rounded-lg border border-border bg-card text-xs", children: [
1718
- /* @__PURE__ */ jsxs21("summary", { className: "flex cursor-pointer list-none select-none items-center gap-1.5 px-4 py-2 font-medium text-muted-foreground [&::-webkit-details-marker]:hidden", children: [
1719
- /* @__PURE__ */ jsx34("span", { "aria-hidden": true, className: "text-xs transition-transform group-open:rotate-90", children: "\u25B8" }),
1720
- "Run details"
1721
- ] }),
1722
- /* @__PURE__ */ jsx34("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__ */ jsxs21("div", { className: "flex items-baseline gap-1.5", children: [
1723
- /* @__PURE__ */ jsx34("dt", { className: "font-medium", children: item.label }),
1724
- /* @__PURE__ */ jsx34("dd", { className: "font-mono text-foreground", children: item.value })
1725
- ] }, item.label)) })
1726
- ] })
1727
- );
1728
- }
1729
-
1730
- // src/components/ReportSchemaError.tsx
1731
- import { jsx as jsx35, jsxs as jsxs22 } from "react/jsx-runtime";
1732
- function ReportSchemaError({ error }) {
1733
- return /* @__PURE__ */ jsxs22("section", { className: "es-schema-error", role: "alert", "aria-live": "assertive", children: [
1734
- /* @__PURE__ */ jsx35("p", { children: /* @__PURE__ */ jsx35("strong", { children: "Report could not be displayed." }) }),
1735
- /* @__PURE__ */ jsx35("p", { children: error.message }),
1736
- error.code === "SCHEMA_VERSION_MISMATCH" ? /* @__PURE__ */ jsxs22("p", { children: [
1737
- "The report bundle is newer than this version of ",
1738
- /* @__PURE__ */ jsx35("code", { children: "executable-stories-react" }),
1739
- ". Upgrade the package, or regenerate the report with an older formatters CLI."
1740
- ] }) : null,
1741
- error.issues && error.issues.length > 0 ? /* @__PURE__ */ jsxs22("details", { children: [
1742
- /* @__PURE__ */ jsxs22("summary", { children: [
1743
- error.issues.length,
1744
- " validation issue",
1745
- error.issues.length === 1 ? "" : "s"
1746
- ] }),
1747
- /* @__PURE__ */ jsx35("pre", { children: error.issues.slice(0, 20).map((i) => `${i.path}: ${i.message}`).join("\n") })
1748
- ] }) : null
1749
- ] });
1750
- }
1751
-
1752
- // src/components/ReportShell.tsx
1753
- import { Fragment as Fragment10, jsx as jsx36, jsxs as jsxs23 } from "react/jsx-runtime";
1754
- function ReportErrorShell({
1755
- error,
1756
- className,
1757
- title,
1758
- dataTheme
1759
- }) {
1760
- return /* @__PURE__ */ jsx36("main", { className: cn("es-report", className), "aria-label": title ?? "Test report", "data-theme": dataTheme, children: /* @__PURE__ */ jsx36(ReportSchemaError, { error }) });
1761
- }
1762
-
1763
- // src/interactive/ReportInteractive.tsx
1764
- import { Monitor, Moon, Sun } from "lucide-react";
1765
-
1766
1616
  // src/components/ui/switch.tsx
1767
1617
  import "react";
1768
1618
  import { Switch as SwitchPrimitive } from "@base-ui/react/switch";
1769
- import { jsx as jsx37 } from "react/jsx-runtime";
1619
+ import { jsx as jsx35 } from "react/jsx-runtime";
1770
1620
  function Switch({
1771
1621
  className,
1772
1622
  ...props
1773
1623
  }) {
1774
- return /* @__PURE__ */ jsx37(
1624
+ return /* @__PURE__ */ jsx35(
1775
1625
  SwitchPrimitive.Root,
1776
1626
  {
1777
1627
  "data-slot": "switch",
@@ -1780,7 +1630,7 @@ function Switch({
1780
1630
  className
1781
1631
  ),
1782
1632
  ...props,
1783
- children: /* @__PURE__ */ jsx37(
1633
+ children: /* @__PURE__ */ jsx35(
1784
1634
  SwitchPrimitive.Thumb,
1785
1635
  {
1786
1636
  "data-slot": "switch-thumb",
@@ -1796,7 +1646,7 @@ import * as React5 from "react";
1796
1646
  import { ToggleGroup as ToggleGroupPrimitive } from "@base-ui/react/toggle-group";
1797
1647
  import { Toggle as TogglePrimitive } from "@base-ui/react/toggle";
1798
1648
  import { cva as cva3 } from "class-variance-authority";
1799
- import { jsx as jsx38 } from "react/jsx-runtime";
1649
+ import { jsx as jsx36 } from "react/jsx-runtime";
1800
1650
  var toggleItemVariants = cva3(
1801
1651
  "inline-flex items-center justify-center gap-1.5 rounded-md font-medium whitespace-nowrap text-muted-foreground transition-[color,background-color,box-shadow] outline-none hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 data-[pressed]:bg-background data-[pressed]:text-foreground data-[pressed]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
1802
1652
  {
@@ -1818,13 +1668,13 @@ function ToggleGroup({
1818
1668
  children,
1819
1669
  ...props
1820
1670
  }) {
1821
- return /* @__PURE__ */ jsx38(
1671
+ return /* @__PURE__ */ jsx36(
1822
1672
  ToggleGroupPrimitive,
1823
1673
  {
1824
1674
  "data-slot": "toggle-group",
1825
1675
  className: cn("flex w-fit items-center gap-0.5 rounded-lg bg-muted p-0.5", className),
1826
1676
  ...props,
1827
- children: /* @__PURE__ */ jsx38(ToggleGroupContext.Provider, { value: { size }, children })
1677
+ children: /* @__PURE__ */ jsx36(ToggleGroupContext.Provider, { value: { size }, children })
1828
1678
  }
1829
1679
  );
1830
1680
  }
@@ -1835,7 +1685,7 @@ function ToggleGroupItem({
1835
1685
  ...props
1836
1686
  }) {
1837
1687
  const context = React5.useContext(ToggleGroupContext);
1838
- return /* @__PURE__ */ jsx38(
1688
+ return /* @__PURE__ */ jsx36(
1839
1689
  TogglePrimitive,
1840
1690
  {
1841
1691
  "data-slot": "toggle-group-item",
@@ -1846,474 +1696,283 @@ function ToggleGroupItem({
1846
1696
  );
1847
1697
  }
1848
1698
 
1849
- // src/interactive/ReportSearch.tsx
1850
- import {
1851
- forwardRef,
1852
- useId
1853
- } from "react";
1854
- import { Search } from "lucide-react";
1699
+ // src/context/ReportRoot.tsx
1700
+ import { useMemo } from "react";
1855
1701
 
1856
- // src/components/ui/input.tsx
1857
- import "react";
1858
- import { jsx as jsx39 } from "react/jsx-runtime";
1859
- function Input({ className, type, ...props }) {
1860
- return /* @__PURE__ */ jsx39(
1861
- "input",
1862
- {
1863
- type,
1864
- "data-slot": "input",
1865
- className: cn(
1866
- "h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30",
1867
- "focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
1868
- "aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
1869
- className
1870
- ),
1871
- ...props
1872
- }
1873
- );
1702
+ // src/components/doc/NarrativeBlocks.tsx
1703
+ import { jsx as jsx37, jsxs as jsxs24 } from "react/jsx-runtime";
1704
+ var FILE_TREE_TYPE = "file-tree";
1705
+ var DATA_MODEL_TYPE = "data-model";
1706
+ function isChangeKind(value) {
1707
+ return value === "added" || value === "modified" || value === "removed" || value === "renamed";
1874
1708
  }
1875
-
1876
- // src/interactive/ReportSearch.tsx
1877
- import { jsx as jsx40, jsxs as jsxs24 } from "react/jsx-runtime";
1878
- var ReportSearch = forwardRef(
1879
- function ReportSearch2(props, ref) {
1880
- const {
1881
- value,
1882
- onChange,
1883
- matchedCount,
1884
- totalCount,
1885
- placeholder = "Search scenarios or tags\u2026",
1886
- className
1887
- } = props;
1888
- const inputId = useId();
1889
- const showCounts = value.trim().length > 0 && typeof matchedCount === "number" && typeof totalCount === "number";
1890
- function handleChange(e) {
1891
- onChange(e.target.value);
1892
- }
1893
- function handleKeyDown(e) {
1894
- if (e.key === "Escape" && value !== "") {
1895
- onChange("");
1896
- e.preventDefault();
1897
- }
1898
- }
1899
- return /* @__PURE__ */ jsxs24("div", { className: cn("es-search flex flex-col gap-1", className), children: [
1900
- /* @__PURE__ */ jsx40("label", { htmlFor: inputId, className: "sr-only", children: "Search" }),
1901
- /* @__PURE__ */ jsxs24("div", { className: "relative w-full sm:w-72", children: [
1902
- /* @__PURE__ */ jsx40(
1903
- Search,
1904
- {
1905
- "aria-hidden": true,
1906
- className: "pointer-events-none absolute top-1/2 left-2.5 size-4 -translate-y-1/2 text-muted-foreground"
1907
- }
1908
- ),
1909
- /* @__PURE__ */ jsx40(
1910
- Input,
1911
- {
1912
- ref,
1913
- id: inputId,
1914
- type: "search",
1915
- value,
1916
- onChange: handleChange,
1917
- onKeyDown: handleKeyDown,
1918
- placeholder,
1919
- autoComplete: "off",
1920
- spellCheck: false,
1921
- "aria-keyshortcuts": "/",
1922
- className: "pl-8"
1923
- }
1924
- )
1925
- ] }),
1926
- showCounts ? /* @__PURE__ */ jsxs24(
1927
- "span",
1928
- {
1929
- className: "text-xs tabular-nums text-muted-foreground",
1930
- "aria-live": "polite",
1931
- children: [
1932
- matchedCount,
1933
- " of ",
1934
- totalCount,
1935
- " scenarios"
1936
- ]
1937
- }
1938
- ) : null
1939
- ] });
1940
- }
1941
- );
1942
-
1943
- // src/interactive/ReportFailureBanner.tsx
1944
- import { useCallback as useCallback2 } from "react";
1945
-
1946
- // src/lib/hash-state.ts
1947
- var DEFAULT_URL_STATE = {
1948
- query: "",
1949
- status: "all",
1950
- tags: [],
1951
- detail: "full"
1952
- };
1953
- var STATUSES = ["all", "passed", "failed", "skipped", "pending"];
1954
- function splitHash(hash) {
1955
- const raw = hash.replace(/^#/, "");
1956
- const split = raw.indexOf("?");
1957
- if (split === -1) return { anchor: raw, params: new URLSearchParams() };
1958
- return { anchor: raw.slice(0, split), params: new URLSearchParams(raw.slice(split + 1)) };
1709
+ function isRecord(value) {
1710
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1959
1711
  }
1960
- function decodeUrlState(params) {
1961
- const status = params.get("status");
1962
- const tags = params.get("tags");
1963
- return {
1964
- query: params.get("q") ?? DEFAULT_URL_STATE.query,
1965
- status: STATUSES.includes(status) ? status : DEFAULT_URL_STATE.status,
1966
- tags: tags ? tags.split(",").filter(Boolean) : [],
1967
- detail: params.get("docs") === "0" ? "minimal" : "full"
1968
- };
1712
+ function str(value) {
1713
+ return typeof value === "string" && value !== "" ? value : void 0;
1969
1714
  }
1970
- function encodeUrlState(state) {
1971
- const params = new URLSearchParams();
1972
- if (state.query) params.set("q", state.query);
1973
- if (state.status !== "all") params.set("status", state.status);
1974
- if (state.tags.length > 0) params.set("tags", state.tags.join(","));
1975
- if (state.detail === "minimal") params.set("docs", "0");
1976
- return params.toString();
1715
+ function AuthoredMarker({ authored }) {
1716
+ if (authored !== "agent") return null;
1717
+ return /* @__PURE__ */ jsx37(Badge, { variant: "outline", className: "border-border text-[10px] text-muted-foreground", children: "AI-authored, not verified by a run" });
1977
1718
  }
1978
- function formatHash(anchor, params) {
1979
- if (params) return `#${anchor}?${params}`;
1980
- return anchor ? `#${anchor}` : "";
1719
+ function BlockFrame({
1720
+ title,
1721
+ authored,
1722
+ children
1723
+ }) {
1724
+ return /* @__PURE__ */ jsxs24("figure", { className: "my-3", children: [
1725
+ title || authored === "agent" ? /* @__PURE__ */ jsxs24("figcaption", { className: "mb-1.5 flex flex-wrap items-center gap-2", children: [
1726
+ title ? /* @__PURE__ */ jsx37("span", { className: "text-xs font-medium text-muted-foreground", children: title }) : null,
1727
+ /* @__PURE__ */ jsx37(AuthoredMarker, { authored })
1728
+ ] }) : null,
1729
+ children
1730
+ ] });
1981
1731
  }
1982
- function writeHash(anchor, params) {
1983
- if (typeof window === "undefined") return;
1984
- const next = formatHash(anchor, params);
1985
- if (next === window.location.hash) return;
1986
- const url = next || window.location.href.split("#")[0];
1987
- try {
1988
- window.history.replaceState(window.history.state, "", url);
1989
- } catch {
1990
- window.location.hash = next.slice(1);
1732
+ function ChangeBadge({ change }) {
1733
+ return /* @__PURE__ */ jsx37(Badge, { variant: "tag", className: "text-[10px]", children: change });
1734
+ }
1735
+ function parseFileTree(data) {
1736
+ if (!isRecord(data) || !Array.isArray(data.files)) return void 0;
1737
+ const files = [];
1738
+ for (const raw of data.files) {
1739
+ if (typeof raw === "string") {
1740
+ if (raw) files.push({ path: raw });
1741
+ continue;
1742
+ }
1743
+ if (!isRecord(raw)) continue;
1744
+ const path = str(raw.path);
1745
+ if (!path) continue;
1746
+ files.push({
1747
+ path,
1748
+ change: isChangeKind(raw.change) ? raw.change : void 0,
1749
+ note: str(raw.note)
1750
+ });
1991
1751
  }
1752
+ if (files.length === 0) return void 0;
1753
+ return { title: str(data.title), authored: str(data.authored), files };
1992
1754
  }
1993
-
1994
- // src/lib/scroll.ts
1995
- function prefersReducedMotion() {
1996
- if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
1997
- return false;
1755
+ function buildTree(files) {
1756
+ const root = { name: "", children: /* @__PURE__ */ new Map() };
1757
+ for (const file of files) {
1758
+ const segments = file.path.split("/").filter(Boolean);
1759
+ let node = root;
1760
+ segments.forEach((segment, index) => {
1761
+ let child = node.children.get(segment);
1762
+ if (!child) {
1763
+ child = { name: segment, children: /* @__PURE__ */ new Map() };
1764
+ node.children.set(segment, child);
1765
+ }
1766
+ if (index === segments.length - 1) child.file = file;
1767
+ node = child;
1768
+ });
1998
1769
  }
1999
- return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
1770
+ return root;
2000
1771
  }
2001
- function scrollToScenarioId(scenarioId, options = {}) {
2002
- if (typeof document === "undefined") return;
2003
- const el = document.getElementById(scenarioId);
2004
- if (!el) return;
2005
- el.scrollIntoView({
2006
- behavior: prefersReducedMotion() ? "auto" : "smooth",
2007
- block: "start"
1772
+ function sortedChildren(node) {
1773
+ return [...node.children.values()].sort((a, b) => {
1774
+ const aDir = a.children.size > 0;
1775
+ const bDir = b.children.size > 0;
1776
+ if (aDir !== bDir) return aDir ? -1 : 1;
1777
+ return a.name.localeCompare(b.name);
2008
1778
  });
2009
- if (options.updateHash !== false) {
2010
- writeHash(scenarioId, splitHash(window.location.hash).params.toString());
2011
- }
2012
1779
  }
2013
-
2014
- // src/interactive/ReportFailureBanner.tsx
2015
- import { jsx as jsx41, jsxs as jsxs25 } from "react/jsx-runtime";
2016
- function ReportFailureBanner({ failures }) {
2017
- const first = failures[0];
2018
- const jumpToFirst = useCallback2(() => {
2019
- if (!first) return;
2020
- scrollToScenarioId(first.scenarioId);
2021
- }, [first]);
2022
- if (failures.length === 0) return null;
2023
- return /* @__PURE__ */ jsxs25(
2024
- "div",
2025
- {
2026
- className: "es-failure-banner",
2027
- role: "status",
2028
- "aria-live": "polite",
2029
- "aria-label": "Failure summary",
2030
- children: [
2031
- /* @__PURE__ */ jsxs25("span", { className: "es-failure-banner-text", children: [
2032
- /* @__PURE__ */ jsx41("strong", { children: failures.length }),
2033
- " ",
2034
- "failure",
2035
- failures.length === 1 ? "" : "s"
2036
- ] }),
2037
- /* @__PURE__ */ jsx41(
2038
- "button",
1780
+ function TreeBranch({ node, depth }) {
1781
+ const children = sortedChildren(node);
1782
+ return /* @__PURE__ */ jsx37("ul", { className: cn("space-y-0.5", depth > 0 && "ml-3 border-l border-border/60 pl-3"), children: children.map((child) => {
1783
+ const isDir = child.children.size > 0;
1784
+ return /* @__PURE__ */ jsxs24("li", { children: [
1785
+ /* @__PURE__ */ jsxs24("div", { className: "flex flex-wrap items-baseline gap-2", children: [
1786
+ /* @__PURE__ */ jsx37(
1787
+ "span",
2039
1788
  {
2040
- type: "button",
2041
- className: "es-failure-banner-jump",
2042
- onClick: jumpToFirst,
2043
- "aria-label": "View first failure",
2044
- children: "View first failure \u2193"
1789
+ className: cn(
1790
+ "font-mono text-xs",
1791
+ isDir ? "text-muted-foreground" : "text-foreground"
1792
+ ),
1793
+ children: isDir ? `${child.name}/` : child.name
2045
1794
  }
2046
- )
2047
- ]
2048
- }
2049
- );
2050
- }
2051
-
2052
- // src/interactive/ReportFreshness.tsx
2053
- import { useEffect, useState as useState2 } from "react";
2054
- import { Fragment as Fragment11, jsx as jsx42, jsxs as jsxs26 } from "react/jsx-runtime";
2055
- function ReportFreshness({ lastRunMs, ciUrl, staleAfterDays, nowMs }) {
2056
- const [now, setNow] = useState2(nowMs);
2057
- useEffect(() => {
2058
- if (nowMs === void 0) setNow(Date.now());
2059
- }, [nowMs]);
2060
- if (lastRunMs === void 0 || now === void 0) return null;
2061
- const age = formatRelativeAge(lastRunMs, now);
2062
- const runLink = ciUrl ? /* @__PURE__ */ jsx42(
2063
- "a",
2064
- {
2065
- href: ciUrl,
2066
- target: "_blank",
2067
- rel: "noreferrer noopener",
2068
- className: "underline underline-offset-2",
2069
- children: "View CI run"
2070
- }
2071
- ) : null;
2072
- if (isReportStale(lastRunMs, staleAfterDays, now)) {
2073
- return /* @__PURE__ */ jsxs26("div", { className: "es-stale-banner", role: "status", "aria-label": "Report freshness", children: [
2074
- /* @__PURE__ */ jsxs26("span", { children: [
2075
- /* @__PURE__ */ jsxs26("strong", { children: [
2076
- "Last verified ",
2077
- age,
2078
- "."
2079
- ] }),
2080
- " This report may no longer match the current behavior. Look for a newer run."
1795
+ ),
1796
+ child.file?.change ? /* @__PURE__ */ jsx37(ChangeBadge, { change: child.file.change }) : null,
1797
+ child.file?.note ? /* @__PURE__ */ jsx37("span", { className: "text-xs text-muted-foreground", children: child.file.note }) : null
2081
1798
  ] }),
2082
- runLink
2083
- ] });
1799
+ isDir ? /* @__PURE__ */ jsx37(TreeBranch, { node: child, depth: depth + 1 }) : null
1800
+ ] }, child.name);
1801
+ }) });
1802
+ }
1803
+ function FileTreeBlock({ entry }) {
1804
+ const data = parseFileTree(entry.data);
1805
+ if (!data) return /* @__PURE__ */ jsx37(UnreadableBlock, { entry });
1806
+ return /* @__PURE__ */ jsx37(BlockFrame, { title: data.title ?? "Files changed", authored: data.authored, children: /* @__PURE__ */ jsx37("div", { tabIndex: 0, className: "overflow-x-auto rounded-md border border-border bg-card p-3", children: /* @__PURE__ */ jsx37(TreeBranch, { node: buildTree(data.files), depth: 0 }) }) });
1807
+ }
1808
+ function parseDataModel(data) {
1809
+ if (!isRecord(data) || !Array.isArray(data.fields)) return void 0;
1810
+ const fields = [];
1811
+ for (const raw of data.fields) {
1812
+ if (!isRecord(raw)) continue;
1813
+ const name = str(raw.name);
1814
+ if (!name) continue;
1815
+ fields.push({
1816
+ name,
1817
+ type: str(raw.type),
1818
+ note: str(raw.note),
1819
+ change: isChangeKind(raw.change) ? raw.change : void 0
1820
+ });
2084
1821
  }
2085
- return /* @__PURE__ */ jsxs26("p", { className: "es-freshness", "aria-label": "Report freshness", children: [
2086
- "Verified ",
2087
- age,
2088
- runLink ? /* @__PURE__ */ jsxs26(Fragment11, { children: [
2089
- " \xB7 ",
2090
- runLink
2091
- ] }) : null
2092
- ] });
1822
+ if (fields.length === 0) return void 0;
1823
+ return {
1824
+ title: str(data.title),
1825
+ authored: str(data.authored),
1826
+ name: str(data.name),
1827
+ fields
1828
+ };
2093
1829
  }
2094
-
2095
- // src/interactive/ReportLastRunDelta.tsx
2096
- import { Fragment as Fragment12, jsx as jsx43, jsxs as jsxs27 } from "react/jsx-runtime";
2097
- var MAX_LINKS = 5;
2098
- function ScenarioLinks({ scenarios, tone }) {
2099
- return /* @__PURE__ */ jsxs27(Fragment12, { children: [
2100
- scenarios.slice(0, MAX_LINKS).map((s, i) => /* @__PURE__ */ jsxs27("span", { children: [
2101
- i > 0 ? ", " : "",
2102
- /* @__PURE__ */ jsx43("a", { href: `#${s.id}`, className: cn("underline underline-offset-2 hover:no-underline", tone), children: s.title })
2103
- ] }, s.id)),
2104
- scenarios.length > MAX_LINKS ? /* @__PURE__ */ jsxs27("span", { children: [
2105
- " and ",
2106
- scenarios.length - MAX_LINKS,
2107
- " more"
2108
- ] }) : null
1830
+ function DataModelBlock({ entry }) {
1831
+ const data = parseDataModel(entry.data);
1832
+ if (!data) return /* @__PURE__ */ jsx37(UnreadableBlock, { entry });
1833
+ const hasNotes = data.fields.some((f) => f.note);
1834
+ return /* @__PURE__ */ jsx37(BlockFrame, { title: data.title ?? data.name ?? "Data model", authored: data.authored, children: /* @__PURE__ */ jsx37("div", { tabIndex: 0, className: "overflow-x-auto rounded-md border border-border", children: /* @__PURE__ */ jsxs24("table", { className: "w-full border-collapse text-sm", children: [
1835
+ /* @__PURE__ */ jsx37("thead", { children: /* @__PURE__ */ jsxs24("tr", { className: "bg-muted/50", children: [
1836
+ /* @__PURE__ */ jsx37("th", { scope: "col", className: "border-b border-border px-3 py-2 text-left font-semibold", children: "Field" }),
1837
+ /* @__PURE__ */ jsx37("th", { scope: "col", className: "border-b border-border px-3 py-2 text-left font-semibold", children: "Type" }),
1838
+ hasNotes ? /* @__PURE__ */ jsx37("th", { scope: "col", className: "border-b border-border px-3 py-2 text-left font-semibold", children: "Note" }) : null
1839
+ ] }) }),
1840
+ /* @__PURE__ */ jsx37("tbody", { children: data.fields.map((field) => /* @__PURE__ */ jsxs24("tr", { children: [
1841
+ /* @__PURE__ */ jsx37("td", { className: "border-b border-border px-3 py-2", children: /* @__PURE__ */ jsxs24("span", { className: "flex flex-wrap items-center gap-2", children: [
1842
+ /* @__PURE__ */ jsx37("span", { className: "font-mono text-xs text-foreground", children: field.name }),
1843
+ field.change ? /* @__PURE__ */ jsx37(ChangeBadge, { change: field.change }) : null
1844
+ ] }) }),
1845
+ /* @__PURE__ */ jsx37("td", { className: "border-b border-border px-3 py-2 font-mono text-xs text-muted-foreground", children: field.type ?? "" }),
1846
+ hasNotes ? /* @__PURE__ */ jsx37("td", { className: "border-b border-border px-3 py-2 text-foreground", children: field.note ?? "" }) : null
1847
+ ] }, field.name)) })
1848
+ ] }) }) });
1849
+ }
1850
+ function UnreadableBlock({ entry }) {
1851
+ return /* @__PURE__ */ jsxs24("div", { className: "my-2", "data-type": entry.type, children: [
1852
+ /* @__PURE__ */ jsxs24("p", { className: "mb-1 text-xs font-medium text-muted-foreground", children: [
1853
+ entry.type,
1854
+ " (unrecognised shape)"
1855
+ ] }),
1856
+ /* @__PURE__ */ jsx37(
1857
+ "pre",
1858
+ {
1859
+ tabIndex: 0,
1860
+ className: "overflow-x-auto rounded-md bg-muted p-3 font-mono text-xs text-foreground",
1861
+ children: safeStringify2(entry.data)
1862
+ }
1863
+ )
2109
1864
  ] });
2110
1865
  }
2111
- function ReportLastRunDelta({
2112
- history,
2113
- report
2114
- }) {
2115
- if (!history) return null;
2116
- const scenarios = report.features.flatMap((f) => f.scenarios.map((s) => ({ id: s.id, title: s.title })));
2117
- const delta = diffSinceLastRun(history, scenarios);
2118
- if (!delta) return null;
2119
- const { newlyFailing, newlyPassing, added } = delta;
2120
- if (newlyFailing.length === 0 && newlyPassing.length === 0 && added.length === 0) {
2121
- return /* @__PURE__ */ jsx43("p", { className: "text-xs text-muted-foreground", "data-testid": "es-last-run-delta", children: "Since last run: no behavior changes." });
1866
+ function safeStringify2(value) {
1867
+ try {
1868
+ return JSON.stringify(value, null, 2);
1869
+ } catch {
1870
+ return String(value);
2122
1871
  }
2123
- return /* @__PURE__ */ jsxs27("p", { className: "text-xs text-muted-foreground", "data-testid": "es-last-run-delta", children: [
2124
- /* @__PURE__ */ jsx43("span", { className: "font-medium text-foreground", children: "Since last run:" }),
2125
- newlyFailing.length > 0 ? /* @__PURE__ */ jsxs27("span", { className: "ml-2 text-fail", children: [
2126
- newlyFailing.length,
2127
- " newly failing (",
2128
- /* @__PURE__ */ jsx43(ScenarioLinks, { scenarios: newlyFailing, tone: "text-fail" }),
2129
- ")"
2130
- ] }) : null,
2131
- newlyPassing.length > 0 ? /* @__PURE__ */ jsxs27("span", { className: "ml-2 text-pass", children: [
2132
- newlyPassing.length,
2133
- " fixed (",
2134
- /* @__PURE__ */ jsx43(ScenarioLinks, { scenarios: newlyPassing, tone: "text-pass" }),
2135
- ")"
2136
- ] }) : null,
2137
- added.length > 0 ? /* @__PURE__ */ jsxs27("span", { className: "ml-2", children: [
2138
- added.length,
2139
- " new"
2140
- ] }) : null
2141
- ] });
2142
1872
  }
1873
+ var narrativeBlockRenderers = {
1874
+ [FILE_TREE_TYPE]: (entry) => /* @__PURE__ */ jsx37(FileTreeBlock, { entry }),
1875
+ [DATA_MODEL_TYPE]: (entry) => /* @__PURE__ */ jsx37(DataModelBlock, { entry })
1876
+ };
2143
1877
 
2144
- // src/interactive/ReportShortcutsHelp.tsx
2145
- import { useEffect as useEffect2, useRef } from "react";
2146
-
2147
- // src/components/ui/button.tsx
2148
- import "react";
2149
- import { cva as cva4 } from "class-variance-authority";
2150
- import { useRender as useRender2 } from "@base-ui/react/use-render";
2151
- var buttonVariants = cva4(
2152
- "inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-[color,background-color,border-color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
2153
- {
2154
- variants: {
2155
- variant: {
2156
- default: "bg-primary text-primary-foreground hover:bg-primary/90",
2157
- destructive: "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40",
2158
- outline: "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
2159
- secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
2160
- ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
2161
- link: "text-primary underline-offset-4 hover:underline"
2162
- },
2163
- size: {
2164
- default: "h-9 px-4 py-2 has-[>svg]:px-3",
2165
- xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
2166
- sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
2167
- lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
2168
- icon: "size-9",
2169
- "icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
2170
- "icon-sm": "size-8",
2171
- "icon-lg": "size-10"
2172
- }
2173
- },
2174
- defaultVariants: {
2175
- variant: "default",
2176
- size: "default"
2177
- }
2178
- }
2179
- );
2180
- function Button({
2181
- className,
2182
- variant = "default",
2183
- size = "default",
2184
- render,
2185
- ...props
1878
+ // src/context/ReportRoot.tsx
1879
+ import { jsx as jsx38 } from "react/jsx-runtime";
1880
+ var EMPTY_RENDERERS2 = {};
1881
+ function ReportRoot({
1882
+ report,
1883
+ customRenderers,
1884
+ renderers,
1885
+ children
2186
1886
  }) {
2187
- return useRender2({
2188
- defaultTagName: "button",
2189
- render,
2190
- props: {
2191
- "data-slot": "button",
2192
- "data-variant": variant,
2193
- "data-size": size,
2194
- className: cn(buttonVariants({ variant, size, className })),
2195
- ...props
2196
- }
2197
- });
1887
+ const value = useMemo(
1888
+ () => ({
1889
+ report,
1890
+ // Narrative blocks render everywhere the report does (static SSR, the
1891
+ // island, Astro) without every host wiring them up. A host renderer for
1892
+ // the same type still wins.
1893
+ customRenderers: { ...narrativeBlockRenderers, ...customRenderers },
1894
+ renderers: renderers ?? EMPTY_RENDERERS2
1895
+ }),
1896
+ [report, customRenderers, renderers]
1897
+ );
1898
+ return /* @__PURE__ */ jsx38(ReportContext.Provider, { value, children });
2198
1899
  }
2199
1900
 
2200
- // src/interactive/ReportShortcutsHelp.tsx
2201
- import { jsx as jsx44, jsxs as jsxs28 } from "react/jsx-runtime";
2202
- var SHORTCUTS = [
2203
- { keys: "/", description: "Focus search" },
2204
- { keys: "f", description: "Jump to next failure" },
2205
- { keys: "Shift F", description: "Jump to previous failure" },
2206
- { keys: "e", description: "Expand all" },
2207
- { keys: "c", description: "Collapse all" },
2208
- { keys: "?", description: "Toggle this help" },
2209
- { keys: "Esc", description: "Clear search / close" }
2210
- ];
2211
- function Keys({ keys }) {
2212
- const parts = keys.split(" ");
2213
- return /* @__PURE__ */ jsx44("span", { className: "inline-flex items-center gap-1", children: parts.map((k, i) => /* @__PURE__ */ jsxs28("span", { className: "inline-flex items-center gap-1", children: [
2214
- i > 0 ? /* @__PURE__ */ jsx44("span", { className: "text-xs text-muted-foreground", children: "+" }) : null,
2215
- /* @__PURE__ */ jsx44("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 })
2216
- ] }, k)) });
1901
+ // src/lib/hash-state.ts
1902
+ var DEFAULT_URL_STATE = {
1903
+ query: "",
1904
+ status: "all",
1905
+ tags: [],
1906
+ detail: "full"
1907
+ };
1908
+ var STATUSES = ["all", "passed", "failed", "skipped", "pending"];
1909
+ function splitHash(hash) {
1910
+ const raw = hash.replace(/^#/, "");
1911
+ const split = raw.indexOf("?");
1912
+ if (split === -1) return { anchor: raw, params: new URLSearchParams() };
1913
+ return { anchor: raw.slice(0, split), params: new URLSearchParams(raw.slice(split + 1)) };
2217
1914
  }
2218
- function ReportShortcutsHelp({ open, onClose }) {
2219
- const dialogRef = useRef(null);
2220
- useEffect2(() => {
2221
- const dialog = dialogRef.current;
2222
- if (!dialog) return;
2223
- if (open && !dialog.open) dialog.showModal();
2224
- else if (!open && dialog.open) dialog.close();
2225
- }, [open]);
2226
- return /* @__PURE__ */ jsxs28(
2227
- "dialog",
2228
- {
2229
- ref: dialogRef,
2230
- className: "es-shortcuts-help m-auto w-[min(90vw,28rem)] rounded-lg border border-border bg-card p-6 text-card-foreground shadow-lg [overscroll-behavior:contain] backdrop:bg-black/40",
2231
- "aria-label": "Keyboard shortcuts",
2232
- onClose,
2233
- children: [
2234
- /* @__PURE__ */ jsx44("h2", { className: "text-base font-semibold", children: "Keyboard shortcuts" }),
2235
- /* @__PURE__ */ jsx44("dl", { className: "mt-4 grid grid-cols-[auto_1fr] items-center gap-x-4 gap-y-2.5", children: SHORTCUTS.map((s) => /* @__PURE__ */ jsxs28("div", { className: "col-span-2 grid grid-cols-subgrid items-center", children: [
2236
- /* @__PURE__ */ jsx44("dt", { className: "justify-self-start", children: /* @__PURE__ */ jsx44(Keys, { keys: s.keys }) }),
2237
- /* @__PURE__ */ jsx44("dd", { className: "text-sm text-muted-foreground", children: s.description })
2238
- ] }, s.keys)) }),
2239
- /* @__PURE__ */ jsx44("form", { method: "dialog", className: "mt-6", children: /* @__PURE__ */ jsx44(Button, { type: "submit", variant: "outline", size: "sm", children: "Close" }) })
2240
- ]
2241
- }
2242
- );
1915
+ function decodeUrlState(params) {
1916
+ const status = params.get("status");
1917
+ const tags = params.get("tags");
1918
+ return {
1919
+ query: params.get("q") ?? DEFAULT_URL_STATE.query,
1920
+ status: STATUSES.includes(status) ? status : DEFAULT_URL_STATE.status,
1921
+ tags: tags ? tags.split(",").filter(Boolean) : [],
1922
+ detail: params.get("docs") === "0" ? "minimal" : "full"
1923
+ };
1924
+ }
1925
+ function encodeUrlState(state) {
1926
+ const params = new URLSearchParams();
1927
+ if (state.query) params.set("q", state.query);
1928
+ if (state.status !== "all") params.set("status", state.status);
1929
+ if (state.tags.length > 0) params.set("tags", state.tags.join(","));
1930
+ if (state.detail === "minimal") params.set("docs", "0");
1931
+ return params.toString();
1932
+ }
1933
+ function formatHash(anchor, params) {
1934
+ if (params) return `#${anchor}?${params}`;
1935
+ return anchor ? `#${anchor}` : "";
1936
+ }
1937
+ function writeHash(anchor, params) {
1938
+ if (typeof window === "undefined") return;
1939
+ const next = formatHash(anchor, params);
1940
+ if (next === window.location.hash) return;
1941
+ const url = next || window.location.href.split("#")[0];
1942
+ try {
1943
+ window.history.replaceState(window.history.state, "", url);
1944
+ } catch {
1945
+ window.location.hash = next.slice(1);
1946
+ }
2243
1947
  }
2244
1948
 
2245
- // src/interactive/use-keyboard-shortcuts.ts
2246
- import { useEffect as useEffect3, useRef as useRef2 } from "react";
2247
- function isEditableTarget(target) {
2248
- if (!(target instanceof HTMLElement)) return false;
2249
- const tag = target.tagName;
2250
- if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return true;
2251
- if (target.isContentEditable) return true;
2252
- return false;
1949
+ // src/lib/scroll.ts
1950
+ function prefersReducedMotion() {
1951
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
1952
+ return false;
1953
+ }
1954
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
2253
1955
  }
2254
- function useKeyboardShortcuts(handlers) {
2255
- const ref = useRef2(handlers);
2256
- ref.current = handlers;
2257
- useEffect3(() => {
2258
- function onKeyDown(e) {
2259
- const h = ref.current;
2260
- if (e.key === "Escape") {
2261
- h.onEscape?.();
2262
- return;
2263
- }
2264
- if (isEditableTarget(e.target)) return;
2265
- if (e.metaKey || e.ctrlKey || e.altKey) return;
2266
- switch (e.key) {
2267
- case "/":
2268
- h.onFocusSearch?.();
2269
- e.preventDefault();
2270
- return;
2271
- case "?":
2272
- h.onToggleHelp?.();
2273
- e.preventDefault();
2274
- return;
2275
- case "f":
2276
- h.onNextFailure?.();
2277
- e.preventDefault();
2278
- return;
2279
- case "F":
2280
- h.onPrevFailure?.();
2281
- e.preventDefault();
2282
- return;
2283
- case "e":
2284
- h.onExpandAll?.();
2285
- e.preventDefault();
2286
- return;
2287
- case "c":
2288
- h.onCollapseAll?.();
2289
- e.preventDefault();
2290
- return;
2291
- default:
2292
- return;
2293
- }
2294
- }
2295
- window.addEventListener("keydown", onKeyDown);
2296
- return () => window.removeEventListener("keydown", onKeyDown);
2297
- }, []);
1956
+ function scrollToScenarioId(scenarioId, options = {}) {
1957
+ if (typeof document === "undefined") return;
1958
+ const el = document.getElementById(scenarioId);
1959
+ if (!el) return;
1960
+ el.scrollIntoView({
1961
+ behavior: prefersReducedMotion() ? "auto" : "smooth",
1962
+ block: "start"
1963
+ });
1964
+ if (options.updateHash !== false) {
1965
+ writeHash(scenarioId, splitHash(window.location.hash).params.toString());
1966
+ }
2298
1967
  }
2299
1968
 
2300
- // src/interactive/use-deep-link-scroll.ts
2301
- import { useEffect as useEffect4, useRef as useRef3 } from "react";
2302
- function useDeepLinkScroll() {
2303
- const lastAnchor = useRef3(null);
2304
- useEffect4(() => {
2305
- function scrollToHash() {
2306
- const { anchor } = splitHash(window.location.hash);
2307
- if (!anchor || anchor === lastAnchor.current) return;
2308
- lastAnchor.current = anchor;
2309
- requestAnimationFrame(() => {
2310
- scrollToScenarioId(anchor, { updateHash: false });
2311
- });
2312
- }
2313
- scrollToHash();
2314
- window.addEventListener("hashchange", scrollToHash);
2315
- return () => window.removeEventListener("hashchange", scrollToHash);
2316
- }, []);
1969
+ // src/result.ts
1970
+ var ok = (data) => ({ ok: true, data });
1971
+ function unwrapReport(value) {
1972
+ if (typeof value === "object" && value !== null && "ok" in value && typeof value.ok === "boolean") {
1973
+ return value;
1974
+ }
1975
+ return ok(value);
2317
1976
  }
2318
1977
 
2319
1978
  // src/interactive/filter.ts
@@ -2406,40 +2065,112 @@ function listFailures(report) {
2406
2065
  out.push(ref);
2407
2066
  }
2408
2067
  }
2409
- }
2410
- return out;
2068
+ }
2069
+ return out;
2070
+ }
2071
+
2072
+ // src/interactive/ReportAgentFilterNotice.tsx
2073
+ import { Bot, X } from "lucide-react";
2074
+ import { Fragment as Fragment11, jsx as jsx39, jsxs as jsxs25 } from "react/jsx-runtime";
2075
+ function ReportAgentFilterNotice({
2076
+ applied,
2077
+ onReset,
2078
+ onDismiss
2079
+ }) {
2080
+ const terms = [];
2081
+ if (applied.search) terms.push(`matching \u201C${applied.search}\u201D`);
2082
+ if (applied.status !== "all") terms.push(applied.status);
2083
+ if (applied.tags.length > 0) terms.push(applied.tags.join(", "));
2084
+ return /* @__PURE__ */ jsxs25(
2085
+ "div",
2086
+ {
2087
+ role: "status",
2088
+ "aria-live": "polite",
2089
+ 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",
2090
+ children: [
2091
+ /* @__PURE__ */ jsx39(Bot, { "aria-hidden": true, className: "size-4 shrink-0 text-primary" }),
2092
+ /* @__PURE__ */ jsxs25("span", { className: "min-w-0 flex-1", children: [
2093
+ "An agent filtered this report",
2094
+ terms.length > 0 ? /* @__PURE__ */ jsxs25(Fragment11, { children: [
2095
+ " to ",
2096
+ terms.join(" \xB7 ")
2097
+ ] }) : null,
2098
+ " \u2014",
2099
+ " ",
2100
+ /* @__PURE__ */ jsxs25("span", { className: "tabular-nums", children: [
2101
+ applied.matched,
2102
+ " of ",
2103
+ applied.total,
2104
+ " scenarios"
2105
+ ] }),
2106
+ "."
2107
+ ] }),
2108
+ /* @__PURE__ */ jsx39(
2109
+ "button",
2110
+ {
2111
+ type: "button",
2112
+ onClick: onReset,
2113
+ className: "cursor-pointer rounded-md border border-border px-2 py-1 text-xs hover:bg-muted",
2114
+ children: "Show all"
2115
+ }
2116
+ ),
2117
+ /* @__PURE__ */ jsx39(
2118
+ "button",
2119
+ {
2120
+ type: "button",
2121
+ onClick: onDismiss,
2122
+ "aria-label": "Dismiss",
2123
+ className: "cursor-pointer rounded-md p-1 text-muted-foreground hover:text-foreground",
2124
+ children: /* @__PURE__ */ jsx39(X, { "aria-hidden": true, className: "size-4" })
2125
+ }
2126
+ )
2127
+ ]
2128
+ }
2129
+ );
2411
2130
  }
2412
2131
 
2413
- // src/interactive/triage-default.ts
2414
- function needsAttention(status) {
2415
- return status === "failed";
2416
- }
2417
- function defaultCollapsedIds(report) {
2418
- const collapsed = /* @__PURE__ */ new Set();
2419
- if (report.summary.failed === 0) return collapsed;
2420
- for (const feature of report.features) {
2421
- const featureHasFailure = feature.scenarios.some((s) => needsAttention(s.status));
2422
- if (!featureHasFailure) collapsed.add(feature.id);
2423
- for (const scenario of feature.scenarios) {
2424
- if (!needsAttention(scenario.status)) collapsed.add(scenario.id);
2132
+ // src/interactive/ReportFailureBanner.tsx
2133
+ import { useCallback as useCallback2 } from "react";
2134
+ import { jsx as jsx40, jsxs as jsxs26 } from "react/jsx-runtime";
2135
+ function ReportFailureBanner({ failures }) {
2136
+ const first = failures[0];
2137
+ const jumpToFirst = useCallback2(() => {
2138
+ if (!first) return;
2139
+ scrollToScenarioId(first.scenarioId);
2140
+ }, [first]);
2141
+ if (failures.length === 0) return null;
2142
+ return /* @__PURE__ */ jsxs26(
2143
+ "div",
2144
+ {
2145
+ className: "es-failure-banner",
2146
+ role: "status",
2147
+ "aria-live": "polite",
2148
+ "aria-label": "Failure summary",
2149
+ children: [
2150
+ /* @__PURE__ */ jsxs26("span", { className: "es-failure-banner-text", children: [
2151
+ /* @__PURE__ */ jsx40("strong", { children: failures.length }),
2152
+ " ",
2153
+ "failure",
2154
+ failures.length === 1 ? "" : "s"
2155
+ ] }),
2156
+ /* @__PURE__ */ jsx40(
2157
+ "button",
2158
+ {
2159
+ type: "button",
2160
+ className: "es-failure-banner-jump",
2161
+ onClick: jumpToFirst,
2162
+ "aria-label": "View first failure",
2163
+ children: "View first failure \u2193"
2164
+ }
2165
+ )
2166
+ ]
2425
2167
  }
2426
- }
2427
- return collapsed;
2428
- }
2429
- function failuresFirst(report) {
2430
- if (report.summary.failed === 0) return report;
2431
- const withFailures = [];
2432
- const rest = [];
2433
- for (const feature of report.features) {
2434
- (feature.scenarios.some((s) => needsAttention(s.status)) ? withFailures : rest).push(feature);
2435
- }
2436
- if (withFailures.length === 0) return report;
2437
- return { ...report, features: [...withFailures, ...rest] };
2168
+ );
2438
2169
  }
2439
2170
 
2440
2171
  // src/interactive/ReportFilters.tsx
2441
2172
  import { useState as useState3 } from "react";
2442
- import { jsx as jsx45, jsxs as jsxs29 } from "react/jsx-runtime";
2173
+ import { jsx as jsx41, jsxs as jsxs27 } from "react/jsx-runtime";
2443
2174
  var TAG_LIMIT = 12;
2444
2175
  function ReportFilters({
2445
2176
  statuses,
@@ -2453,10 +2184,10 @@ function ReportFilters({
2453
2184
  if (statuses.length === 0 && tags.length === 0) return null;
2454
2185
  const tagsCollapsed = tags.length > TAG_LIMIT && !showAllTags;
2455
2186
  const visibleTags = tagsCollapsed ? tags.slice(0, TAG_LIMIT) : tags;
2456
- return /* @__PURE__ */ jsxs29("div", { className: "flex flex-col gap-2", children: [
2457
- statuses.length > 0 ? /* @__PURE__ */ jsx45("div", { role: "group", "aria-label": "Filter by status", className: "flex flex-wrap gap-1", children: statuses.map((opt) => {
2187
+ return /* @__PURE__ */ jsxs27("div", { className: "flex flex-col gap-2", children: [
2188
+ statuses.length > 0 ? /* @__PURE__ */ jsx41("div", { role: "group", "aria-label": "Filter by status", className: "flex flex-wrap gap-1", children: statuses.map((opt) => {
2458
2189
  const active = status === opt.key;
2459
- return /* @__PURE__ */ jsxs29(
2190
+ return /* @__PURE__ */ jsxs27(
2460
2191
  "button",
2461
2192
  {
2462
2193
  type: "button",
@@ -2468,23 +2199,23 @@ function ReportFilters({
2468
2199
  ),
2469
2200
  children: [
2470
2201
  opt.label,
2471
- /* @__PURE__ */ jsx45("span", { className: "ml-1 font-mono opacity-70", children: opt.count })
2202
+ /* @__PURE__ */ jsx41("span", { className: "ml-1 font-mono opacity-70", children: opt.count })
2472
2203
  ]
2473
2204
  },
2474
2205
  opt.key
2475
2206
  );
2476
2207
  }) }) : null,
2477
- tags.length > 0 ? /* @__PURE__ */ jsxs29("ul", { "aria-label": "Filter by tag", className: "flex flex-wrap items-center gap-1.5", children: [
2208
+ tags.length > 0 ? /* @__PURE__ */ jsxs27("ul", { "aria-label": "Filter by tag", className: "flex flex-wrap items-center gap-1.5", children: [
2478
2209
  visibleTags.map((tag) => {
2479
2210
  const active = activeTags.includes(tag);
2480
- return /* @__PURE__ */ jsx45("li", { children: /* @__PURE__ */ jsx45(
2211
+ return /* @__PURE__ */ jsx41("li", { children: /* @__PURE__ */ jsx41(
2481
2212
  "button",
2482
2213
  {
2483
2214
  type: "button",
2484
2215
  "aria-pressed": active,
2485
2216
  onClick: () => onToggleTag(tag),
2486
2217
  className: "cursor-pointer",
2487
- children: /* @__PURE__ */ jsx45(
2218
+ children: /* @__PURE__ */ jsx41(
2488
2219
  Badge,
2489
2220
  {
2490
2221
  variant: "tag",
@@ -2497,7 +2228,7 @@ function ReportFilters({
2497
2228
  }
2498
2229
  ) }, tag);
2499
2230
  }),
2500
- tags.length > TAG_LIMIT ? /* @__PURE__ */ jsx45("li", { children: /* @__PURE__ */ jsx45(
2231
+ tags.length > TAG_LIMIT ? /* @__PURE__ */ jsx41("li", { children: /* @__PURE__ */ jsx41(
2501
2232
  "button",
2502
2233
  {
2503
2234
  type: "button",
@@ -2511,9 +2242,296 @@ function ReportFilters({
2511
2242
  ] });
2512
2243
  }
2513
2244
 
2245
+ // src/interactive/ReportFreshness.tsx
2246
+ import { useEffect as useEffect2, useState as useState4 } from "react";
2247
+ import { Fragment as Fragment12, jsx as jsx42, jsxs as jsxs28 } from "react/jsx-runtime";
2248
+ function ReportFreshness({ lastRunMs, ciUrl, staleAfterDays, nowMs }) {
2249
+ const [now, setNow] = useState4(nowMs);
2250
+ useEffect2(() => {
2251
+ if (nowMs === void 0) setNow(Date.now());
2252
+ }, [nowMs]);
2253
+ if (lastRunMs === void 0 || now === void 0) return null;
2254
+ const age = formatRelativeAge(lastRunMs, now);
2255
+ const runLink = ciUrl ? /* @__PURE__ */ jsx42(
2256
+ "a",
2257
+ {
2258
+ href: ciUrl,
2259
+ target: "_blank",
2260
+ rel: "noreferrer noopener",
2261
+ className: "underline underline-offset-2",
2262
+ children: "View CI run"
2263
+ }
2264
+ ) : null;
2265
+ if (isReportStale(lastRunMs, staleAfterDays, now)) {
2266
+ return /* @__PURE__ */ jsxs28("div", { className: "es-stale-banner", role: "status", "aria-label": "Report freshness", children: [
2267
+ /* @__PURE__ */ jsxs28("span", { children: [
2268
+ /* @__PURE__ */ jsxs28("strong", { children: [
2269
+ "Last verified ",
2270
+ age,
2271
+ "."
2272
+ ] }),
2273
+ " This report may no longer match the current behavior. Look for a newer run."
2274
+ ] }),
2275
+ runLink
2276
+ ] });
2277
+ }
2278
+ return /* @__PURE__ */ jsxs28("p", { className: "es-freshness", "aria-label": "Report freshness", children: [
2279
+ "Verified ",
2280
+ age,
2281
+ runLink ? /* @__PURE__ */ jsxs28(Fragment12, { children: [
2282
+ " \xB7 ",
2283
+ runLink
2284
+ ] }) : null
2285
+ ] });
2286
+ }
2287
+
2288
+ // src/interactive/ReportLastRunDelta.tsx
2289
+ import { Fragment as Fragment13, jsx as jsx43, jsxs as jsxs29 } from "react/jsx-runtime";
2290
+ var MAX_LINKS = 5;
2291
+ function ScenarioLinks({ scenarios, tone }) {
2292
+ return /* @__PURE__ */ jsxs29(Fragment13, { children: [
2293
+ scenarios.slice(0, MAX_LINKS).map((s, i) => /* @__PURE__ */ jsxs29("span", { children: [
2294
+ i > 0 ? ", " : "",
2295
+ /* @__PURE__ */ jsx43("a", { href: `#${s.id}`, className: cn("underline underline-offset-2 hover:no-underline", tone), children: s.title })
2296
+ ] }, s.id)),
2297
+ scenarios.length > MAX_LINKS ? /* @__PURE__ */ jsxs29("span", { children: [
2298
+ " and ",
2299
+ scenarios.length - MAX_LINKS,
2300
+ " more"
2301
+ ] }) : null
2302
+ ] });
2303
+ }
2304
+ function ReportLastRunDelta({
2305
+ history,
2306
+ report
2307
+ }) {
2308
+ if (!history) return null;
2309
+ const scenarios = report.features.flatMap((f) => f.scenarios.map((s) => ({ id: s.id, title: s.title })));
2310
+ const delta = diffSinceLastRun(history, scenarios);
2311
+ if (!delta) return null;
2312
+ const { newlyFailing, newlyPassing, added } = delta;
2313
+ if (newlyFailing.length === 0 && newlyPassing.length === 0 && added.length === 0) {
2314
+ return /* @__PURE__ */ jsx43("p", { className: "text-xs text-muted-foreground", "data-testid": "es-last-run-delta", children: "Since last run: no behavior changes." });
2315
+ }
2316
+ return /* @__PURE__ */ jsxs29("p", { className: "text-xs text-muted-foreground", "data-testid": "es-last-run-delta", children: [
2317
+ /* @__PURE__ */ jsx43("span", { className: "font-medium text-foreground", children: "Since last run:" }),
2318
+ newlyFailing.length > 0 ? /* @__PURE__ */ jsxs29("span", { className: "ml-2 text-fail", children: [
2319
+ newlyFailing.length,
2320
+ " newly failing (",
2321
+ /* @__PURE__ */ jsx43(ScenarioLinks, { scenarios: newlyFailing, tone: "text-fail" }),
2322
+ ")"
2323
+ ] }) : null,
2324
+ newlyPassing.length > 0 ? /* @__PURE__ */ jsxs29("span", { className: "ml-2 text-pass", children: [
2325
+ newlyPassing.length,
2326
+ " fixed (",
2327
+ /* @__PURE__ */ jsx43(ScenarioLinks, { scenarios: newlyPassing, tone: "text-pass" }),
2328
+ ")"
2329
+ ] }) : null,
2330
+ added.length > 0 ? /* @__PURE__ */ jsxs29("span", { className: "ml-2", children: [
2331
+ added.length,
2332
+ " new"
2333
+ ] }) : null
2334
+ ] });
2335
+ }
2336
+
2337
+ // src/interactive/ReportSearch.tsx
2338
+ import {
2339
+ forwardRef,
2340
+ useId
2341
+ } from "react";
2342
+ import { Search } from "lucide-react";
2343
+
2344
+ // src/components/ui/input.tsx
2345
+ import "react";
2346
+ import { jsx as jsx44 } from "react/jsx-runtime";
2347
+ function Input({ className, type, ...props }) {
2348
+ return /* @__PURE__ */ jsx44(
2349
+ "input",
2350
+ {
2351
+ type,
2352
+ "data-slot": "input",
2353
+ className: cn(
2354
+ "h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30",
2355
+ "focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
2356
+ "aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
2357
+ className
2358
+ ),
2359
+ ...props
2360
+ }
2361
+ );
2362
+ }
2363
+
2364
+ // src/interactive/ReportSearch.tsx
2365
+ import { jsx as jsx45, jsxs as jsxs30 } from "react/jsx-runtime";
2366
+ var ReportSearch = forwardRef(
2367
+ function ReportSearch2(props, ref) {
2368
+ const {
2369
+ value,
2370
+ onChange,
2371
+ matchedCount,
2372
+ totalCount,
2373
+ placeholder = "Search scenarios or tags\u2026",
2374
+ className
2375
+ } = props;
2376
+ const inputId = useId();
2377
+ const showCounts = value.trim().length > 0 && typeof matchedCount === "number" && typeof totalCount === "number";
2378
+ function handleChange(e) {
2379
+ onChange(e.target.value);
2380
+ }
2381
+ function handleKeyDown(e) {
2382
+ if (e.key === "Escape" && value !== "") {
2383
+ onChange("");
2384
+ e.preventDefault();
2385
+ }
2386
+ }
2387
+ return /* @__PURE__ */ jsxs30("div", { className: cn("es-search flex flex-col gap-1", className), children: [
2388
+ /* @__PURE__ */ jsx45("label", { htmlFor: inputId, className: "sr-only", children: "Search" }),
2389
+ /* @__PURE__ */ jsxs30("div", { className: "relative w-full sm:w-72", children: [
2390
+ /* @__PURE__ */ jsx45(
2391
+ Search,
2392
+ {
2393
+ "aria-hidden": true,
2394
+ className: "pointer-events-none absolute top-1/2 left-2.5 size-4 -translate-y-1/2 text-muted-foreground"
2395
+ }
2396
+ ),
2397
+ /* @__PURE__ */ jsx45(
2398
+ Input,
2399
+ {
2400
+ ref,
2401
+ id: inputId,
2402
+ type: "search",
2403
+ value,
2404
+ onChange: handleChange,
2405
+ onKeyDown: handleKeyDown,
2406
+ placeholder,
2407
+ autoComplete: "off",
2408
+ spellCheck: false,
2409
+ "aria-keyshortcuts": "/",
2410
+ className: "pl-8"
2411
+ }
2412
+ )
2413
+ ] }),
2414
+ showCounts ? /* @__PURE__ */ jsxs30(
2415
+ "span",
2416
+ {
2417
+ className: "text-xs tabular-nums text-muted-foreground",
2418
+ "aria-live": "polite",
2419
+ children: [
2420
+ matchedCount,
2421
+ " of ",
2422
+ totalCount,
2423
+ " scenarios"
2424
+ ]
2425
+ }
2426
+ ) : null
2427
+ ] });
2428
+ }
2429
+ );
2430
+
2431
+ // src/interactive/ReportShortcutsHelp.tsx
2432
+ import { useEffect as useEffect3, useRef } from "react";
2433
+
2434
+ // src/components/ui/button.tsx
2435
+ import "react";
2436
+ import { cva as cva4 } from "class-variance-authority";
2437
+ import { useRender as useRender2 } from "@base-ui/react/use-render";
2438
+ var buttonVariants = cva4(
2439
+ "inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-[color,background-color,border-color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
2440
+ {
2441
+ variants: {
2442
+ variant: {
2443
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
2444
+ destructive: "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40",
2445
+ outline: "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
2446
+ secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
2447
+ ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
2448
+ link: "text-primary underline-offset-4 hover:underline"
2449
+ },
2450
+ size: {
2451
+ default: "h-9 px-4 py-2 has-[>svg]:px-3",
2452
+ xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
2453
+ sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
2454
+ lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
2455
+ icon: "size-9",
2456
+ "icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
2457
+ "icon-sm": "size-8",
2458
+ "icon-lg": "size-10"
2459
+ }
2460
+ },
2461
+ defaultVariants: {
2462
+ variant: "default",
2463
+ size: "default"
2464
+ }
2465
+ }
2466
+ );
2467
+ function Button({
2468
+ className,
2469
+ variant = "default",
2470
+ size = "default",
2471
+ render,
2472
+ ...props
2473
+ }) {
2474
+ return useRender2({
2475
+ defaultTagName: "button",
2476
+ render,
2477
+ props: {
2478
+ "data-slot": "button",
2479
+ "data-variant": variant,
2480
+ "data-size": size,
2481
+ className: cn(buttonVariants({ variant, size, className })),
2482
+ ...props
2483
+ }
2484
+ });
2485
+ }
2486
+
2487
+ // src/interactive/ReportShortcutsHelp.tsx
2488
+ import { jsx as jsx46, jsxs as jsxs31 } from "react/jsx-runtime";
2489
+ var SHORTCUTS = [
2490
+ { keys: "/", description: "Focus search" },
2491
+ { keys: "f", description: "Jump to next failure" },
2492
+ { keys: "Shift F", description: "Jump to previous failure" },
2493
+ { keys: "e", description: "Expand all" },
2494
+ { keys: "c", description: "Collapse all" },
2495
+ { keys: "?", description: "Toggle this help" },
2496
+ { keys: "Esc", description: "Clear search / close" }
2497
+ ];
2498
+ function Keys({ keys }) {
2499
+ const parts = keys.split(" ");
2500
+ return /* @__PURE__ */ jsx46("span", { className: "inline-flex items-center gap-1", children: parts.map((k, i) => /* @__PURE__ */ jsxs31("span", { className: "inline-flex items-center gap-1", children: [
2501
+ i > 0 ? /* @__PURE__ */ jsx46("span", { className: "text-xs text-muted-foreground", children: "+" }) : null,
2502
+ /* @__PURE__ */ jsx46("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 })
2503
+ ] }, k)) });
2504
+ }
2505
+ function ReportShortcutsHelp({ open, onClose }) {
2506
+ const dialogRef = useRef(null);
2507
+ useEffect3(() => {
2508
+ const dialog = dialogRef.current;
2509
+ if (!dialog) return;
2510
+ if (open && !dialog.open) dialog.showModal();
2511
+ else if (!open && dialog.open) dialog.close();
2512
+ }, [open]);
2513
+ return /* @__PURE__ */ jsxs31(
2514
+ "dialog",
2515
+ {
2516
+ ref: dialogRef,
2517
+ className: "es-shortcuts-help m-auto w-[min(90vw,28rem)] rounded-lg border border-border bg-card p-6 text-card-foreground shadow-lg [overscroll-behavior:contain] backdrop:bg-black/40",
2518
+ "aria-label": "Keyboard shortcuts",
2519
+ onClose,
2520
+ children: [
2521
+ /* @__PURE__ */ jsx46("h2", { className: "text-base font-semibold", children: "Keyboard shortcuts" }),
2522
+ /* @__PURE__ */ jsx46("dl", { className: "mt-4 grid grid-cols-[auto_1fr] items-center gap-x-4 gap-y-2.5", children: SHORTCUTS.map((s) => /* @__PURE__ */ jsxs31("div", { className: "col-span-2 grid grid-cols-subgrid items-center", children: [
2523
+ /* @__PURE__ */ jsx46("dt", { className: "justify-self-start", children: /* @__PURE__ */ jsx46(Keys, { keys: s.keys }) }),
2524
+ /* @__PURE__ */ jsx46("dd", { className: "text-sm text-muted-foreground", children: s.description })
2525
+ ] }, s.keys)) }),
2526
+ /* @__PURE__ */ jsx46("form", { method: "dialog", className: "mt-6", children: /* @__PURE__ */ jsx46(Button, { type: "submit", variant: "outline", size: "sm", children: "Close" }) })
2527
+ ]
2528
+ }
2529
+ );
2530
+ }
2531
+
2514
2532
  // src/interactive/ReportToc.tsx
2515
- import { useEffect as useEffect5, useMemo as useMemo2, useState as useState4 } from "react";
2516
- import { jsx as jsx46, jsxs as jsxs30 } from "react/jsx-runtime";
2533
+ import { useEffect as useEffect4, useMemo as useMemo2, useState as useState5 } from "react";
2534
+ import { jsx as jsx47, jsxs as jsxs32 } from "react/jsx-runtime";
2517
2535
  var DOT = {
2518
2536
  passed: "bg-pass",
2519
2537
  failed: "bg-fail",
@@ -2522,12 +2540,12 @@ var DOT = {
2522
2540
  };
2523
2541
  function TocContent({ onNavigate }) {
2524
2542
  const report = useReport();
2525
- const [activeId, setActiveId] = useState4(null);
2543
+ const [activeId, setActiveId] = useState5(null);
2526
2544
  const scenarioIds = useMemo2(
2527
2545
  () => report.features.flatMap((f) => f.scenarios.map((s) => s.id)),
2528
2546
  [report]
2529
2547
  );
2530
- useEffect5(() => {
2548
+ useEffect4(() => {
2531
2549
  if (typeof IntersectionObserver === "undefined") return;
2532
2550
  const els = scenarioIds.map((id) => document.getElementById(id)).filter((el) => el !== null);
2533
2551
  if (els.length === 0) return;
@@ -2542,11 +2560,11 @@ function TocContent({ onNavigate }) {
2542
2560
  return () => observer.disconnect();
2543
2561
  }, [scenarioIds]);
2544
2562
  if (report.features.length === 0) return null;
2545
- return /* @__PURE__ */ jsx46("ul", { className: "flex list-none flex-col gap-4 pl-0", children: report.features.map((feature) => /* @__PURE__ */ jsxs30("li", { children: [
2546
- /* @__PURE__ */ jsx46("p", { className: "mb-1.5 font-semibold leading-snug text-foreground", children: feature.title }),
2547
- /* @__PURE__ */ jsx46("ul", { className: "flex list-none flex-col gap-0.5 pl-0", children: feature.scenarios.map((scenario) => {
2563
+ return /* @__PURE__ */ jsx47("ul", { className: "flex list-none flex-col gap-4 pl-0", children: report.features.map((feature) => /* @__PURE__ */ jsxs32("li", { children: [
2564
+ /* @__PURE__ */ jsx47("p", { className: "mb-1.5 font-semibold leading-snug text-foreground", children: feature.title }),
2565
+ /* @__PURE__ */ jsx47("ul", { className: "flex list-none flex-col gap-0.5 pl-0", children: feature.scenarios.map((scenario) => {
2548
2566
  const active = activeId === scenario.id;
2549
- return /* @__PURE__ */ jsx46("li", { children: /* @__PURE__ */ jsxs30(
2567
+ return /* @__PURE__ */ jsx47("li", { children: /* @__PURE__ */ jsxs32(
2550
2568
  "a",
2551
2569
  {
2552
2570
  href: `#${scenario.id}`,
@@ -2561,14 +2579,14 @@ function TocContent({ onNavigate }) {
2561
2579
  active ? "border-primary font-medium text-foreground" : "border-border text-muted-foreground"
2562
2580
  ),
2563
2581
  children: [
2564
- /* @__PURE__ */ jsx46(
2582
+ /* @__PURE__ */ jsx47(
2565
2583
  "span",
2566
2584
  {
2567
2585
  "aria-hidden": true,
2568
2586
  className: cn("mt-1 size-1.5 shrink-0 rounded-full", DOT[scenario.status])
2569
2587
  }
2570
2588
  ),
2571
- /* @__PURE__ */ jsx46("span", { className: "break-words", children: scenario.title })
2589
+ /* @__PURE__ */ jsx47("span", { className: "break-words", children: scenario.title })
2572
2590
  ]
2573
2591
  }
2574
2592
  ) }, scenario.id);
@@ -2578,55 +2596,156 @@ function TocContent({ onNavigate }) {
2578
2596
  function ReportToc() {
2579
2597
  const report = useReport();
2580
2598
  if (report.features.length === 0) return null;
2581
- return /* @__PURE__ */ jsxs30(
2599
+ return /* @__PURE__ */ jsxs32(
2582
2600
  "nav",
2583
2601
  {
2584
2602
  "aria-label": "Table of contents",
2585
2603
  className: "sticky top-4 hidden h-fit w-64 shrink-0 self-start text-xs lg:block",
2586
2604
  children: [
2587
- /* @__PURE__ */ jsx46("p", { className: "mb-3 text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: "Contents" }),
2588
- /* @__PURE__ */ jsx46(TocContent, {})
2605
+ /* @__PURE__ */ jsx47("p", { className: "mb-3 text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: "Contents" }),
2606
+ /* @__PURE__ */ jsx47(TocContent, {})
2589
2607
  ]
2590
2608
  }
2591
2609
  );
2592
2610
  }
2593
2611
 
2594
2612
  // src/interactive/ReportTocDrawer.tsx
2595
- import { useState as useState5 } from "react";
2613
+ import { useState as useState6 } from "react";
2596
2614
  import { Dialog as DialogPrimitive } from "@base-ui/react/dialog";
2597
2615
  import { ListIcon, XIcon } from "lucide-react";
2598
- import { jsx as jsx47, jsxs as jsxs31 } from "react/jsx-runtime";
2616
+ import { jsx as jsx48, jsxs as jsxs33 } from "react/jsx-runtime";
2599
2617
  function ReportTocDrawer() {
2600
2618
  const report = useReport();
2601
- const [open, setOpen] = useState5(false);
2619
+ const [open, setOpen] = useState6(false);
2602
2620
  if (report.features.length === 0) return null;
2603
- return /* @__PURE__ */ jsxs31(DialogPrimitive.Root, { open, onOpenChange: setOpen, children: [
2604
- /* @__PURE__ */ jsxs31(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: [
2605
- /* @__PURE__ */ jsx47(ListIcon, { "aria-hidden": true }),
2621
+ return /* @__PURE__ */ jsxs33(DialogPrimitive.Root, { open, onOpenChange: setOpen, children: [
2622
+ /* @__PURE__ */ jsxs33(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: [
2623
+ /* @__PURE__ */ jsx48(ListIcon, { "aria-hidden": true }),
2606
2624
  " Contents"
2607
2625
  ] }),
2608
- /* @__PURE__ */ jsxs31(DialogPrimitive.Portal, { children: [
2609
- /* @__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" }),
2610
- /* @__PURE__ */ jsxs31(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: [
2611
- /* @__PURE__ */ jsxs31("div", { className: "flex items-center justify-between", children: [
2612
- /* @__PURE__ */ jsx47(DialogPrimitive.Title, { className: "text-xs font-semibold tracking-wide text-muted-foreground uppercase", children: "Contents" }),
2613
- /* @__PURE__ */ jsx47(
2626
+ /* @__PURE__ */ jsxs33(DialogPrimitive.Portal, { children: [
2627
+ /* @__PURE__ */ jsx48(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" }),
2628
+ /* @__PURE__ */ jsxs33(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: [
2629
+ /* @__PURE__ */ jsxs33("div", { className: "flex items-center justify-between", children: [
2630
+ /* @__PURE__ */ jsx48(DialogPrimitive.Title, { className: "text-xs font-semibold tracking-wide text-muted-foreground uppercase", children: "Contents" }),
2631
+ /* @__PURE__ */ jsx48(
2614
2632
  DialogPrimitive.Close,
2615
2633
  {
2616
2634
  "aria-label": "Close contents",
2617
2635
  className: "cursor-pointer rounded p-1 text-muted-foreground hover:bg-muted hover:text-foreground [&_svg]:size-4",
2618
- children: /* @__PURE__ */ jsx47(XIcon, { "aria-hidden": true })
2636
+ children: /* @__PURE__ */ jsx48(XIcon, { "aria-hidden": true })
2619
2637
  }
2620
2638
  )
2621
2639
  ] }),
2622
- /* @__PURE__ */ jsx47(TocContent, { onNavigate: () => setOpen(false) })
2640
+ /* @__PURE__ */ jsx48(TocContent, { onNavigate: () => setOpen(false) })
2623
2641
  ] })
2624
2642
  ] })
2625
2643
  ] });
2626
2644
  }
2627
2645
 
2646
+ // src/interactive/triage-default.ts
2647
+ function needsAttention(status) {
2648
+ return status === "failed";
2649
+ }
2650
+ function defaultCollapsedIds(report) {
2651
+ const collapsed = /* @__PURE__ */ new Set();
2652
+ if (report.summary.failed === 0) return collapsed;
2653
+ for (const feature of report.features) {
2654
+ const featureHasFailure = feature.scenarios.some((s) => needsAttention(s.status));
2655
+ if (!featureHasFailure) collapsed.add(feature.id);
2656
+ for (const scenario of feature.scenarios) {
2657
+ if (!needsAttention(scenario.status)) collapsed.add(scenario.id);
2658
+ }
2659
+ }
2660
+ return collapsed;
2661
+ }
2662
+ function failuresFirst(report) {
2663
+ if (report.summary.failed === 0) return report;
2664
+ const withFailures = [];
2665
+ const rest = [];
2666
+ for (const feature of report.features) {
2667
+ (feature.scenarios.some((s) => needsAttention(s.status)) ? withFailures : rest).push(feature);
2668
+ }
2669
+ if (withFailures.length === 0) return report;
2670
+ return { ...report, features: [...withFailures, ...rest] };
2671
+ }
2672
+
2673
+ // src/interactive/use-deep-link-scroll.ts
2674
+ import { useEffect as useEffect5, useRef as useRef2 } from "react";
2675
+ function useDeepLinkScroll() {
2676
+ const lastAnchor = useRef2(null);
2677
+ useEffect5(() => {
2678
+ function scrollToHash() {
2679
+ const { anchor } = splitHash(window.location.hash);
2680
+ if (!anchor || anchor === lastAnchor.current) return;
2681
+ lastAnchor.current = anchor;
2682
+ requestAnimationFrame(() => {
2683
+ scrollToScenarioId(anchor, { updateHash: false });
2684
+ });
2685
+ }
2686
+ scrollToHash();
2687
+ window.addEventListener("hashchange", scrollToHash);
2688
+ return () => window.removeEventListener("hashchange", scrollToHash);
2689
+ }, []);
2690
+ }
2691
+
2692
+ // src/interactive/use-keyboard-shortcuts.ts
2693
+ import { useEffect as useEffect6, useRef as useRef3 } from "react";
2694
+ function isEditableTarget(target) {
2695
+ if (!(target instanceof HTMLElement)) return false;
2696
+ const tag = target.tagName;
2697
+ if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return true;
2698
+ if (target.isContentEditable) return true;
2699
+ return false;
2700
+ }
2701
+ function useKeyboardShortcuts(handlers) {
2702
+ const ref = useRef3(handlers);
2703
+ ref.current = handlers;
2704
+ useEffect6(() => {
2705
+ function onKeyDown(e) {
2706
+ const h = ref.current;
2707
+ if (e.key === "Escape") {
2708
+ h.onEscape?.();
2709
+ return;
2710
+ }
2711
+ if (isEditableTarget(e.target)) return;
2712
+ if (e.metaKey || e.ctrlKey || e.altKey) return;
2713
+ switch (e.key) {
2714
+ case "/":
2715
+ h.onFocusSearch?.();
2716
+ e.preventDefault();
2717
+ return;
2718
+ case "?":
2719
+ h.onToggleHelp?.();
2720
+ e.preventDefault();
2721
+ return;
2722
+ case "f":
2723
+ h.onNextFailure?.();
2724
+ e.preventDefault();
2725
+ return;
2726
+ case "F":
2727
+ h.onPrevFailure?.();
2728
+ e.preventDefault();
2729
+ return;
2730
+ case "e":
2731
+ h.onExpandAll?.();
2732
+ e.preventDefault();
2733
+ return;
2734
+ case "c":
2735
+ h.onCollapseAll?.();
2736
+ e.preventDefault();
2737
+ return;
2738
+ default:
2739
+ return;
2740
+ }
2741
+ }
2742
+ window.addEventListener("keydown", onKeyDown);
2743
+ return () => window.removeEventListener("keydown", onKeyDown);
2744
+ }, []);
2745
+ }
2746
+
2628
2747
  // src/interactive/use-theme.ts
2629
- import { useCallback as useCallback3, useEffect as useEffect6, useState as useState6 } from "react";
2748
+ import { useCallback as useCallback3, useEffect as useEffect7, useState as useState7 } from "react";
2630
2749
  var KEY = "es-theme";
2631
2750
  function systemDark() {
2632
2751
  return typeof matchMedia !== "undefined" && matchMedia("(prefers-color-scheme: dark)").matches;
@@ -2647,13 +2766,13 @@ function initialPref() {
2647
2766
  return "system";
2648
2767
  }
2649
2768
  function useTheme() {
2650
- const [pref, setPrefState] = useState6("system");
2651
- useEffect6(() => {
2769
+ const [pref, setPrefState] = useState7("system");
2770
+ useEffect7(() => {
2652
2771
  const p = initialPref();
2653
2772
  setPrefState(p);
2654
2773
  applyTheme(p);
2655
2774
  }, []);
2656
- useEffect6(() => {
2775
+ useEffect7(() => {
2657
2776
  if (typeof matchMedia === "undefined") return;
2658
2777
  const mq = matchMedia("(prefers-color-scheme: dark)");
2659
2778
  const onChange = () => {
@@ -2674,19 +2793,19 @@ function useTheme() {
2674
2793
  }
2675
2794
 
2676
2795
  // src/interactive/use-url-state.ts
2677
- import { useCallback as useCallback4, useEffect as useEffect7, useRef as useRef4, useState as useState7 } from "react";
2796
+ import { useCallback as useCallback4, useEffect as useEffect8, useRef as useRef4, useState as useState8 } from "react";
2678
2797
  var WRITE_DELAY_MS = 250;
2679
2798
  function useUrlState() {
2680
- const [state, setState] = useState7(DEFAULT_URL_STATE);
2799
+ const [state, setState] = useState8(DEFAULT_URL_STATE);
2681
2800
  const ready = useRef4(false);
2682
- useEffect7(() => {
2801
+ useEffect8(() => {
2683
2802
  const read = () => setState(decodeUrlState(splitHash(window.location.hash).params));
2684
2803
  read();
2685
2804
  ready.current = true;
2686
2805
  window.addEventListener("hashchange", read);
2687
2806
  return () => window.removeEventListener("hashchange", read);
2688
2807
  }, []);
2689
- useEffect7(() => {
2808
+ useEffect8(() => {
2690
2809
  if (!ready.current) return;
2691
2810
  const timer = setTimeout(
2692
2811
  () => writeHash(splitHash(window.location.hash).anchor, encodeUrlState(state)),
@@ -2701,23 +2820,254 @@ function useUrlState() {
2701
2820
  return [state, update];
2702
2821
  }
2703
2822
 
2823
+ // src/interactive/webmcp-tools.ts
2824
+ import {
2825
+ getFeatureSummary,
2826
+ getScenario,
2827
+ listScenarioSummaries,
2828
+ runProvenance,
2829
+ toScenarioSummary
2830
+ } from "executable-stories-core/report-queries";
2831
+ var STATUSES2 = [
2832
+ "passed",
2833
+ "failed",
2834
+ "skipped",
2835
+ "pending"
2836
+ ];
2837
+ var VIEW_STATUSES = ["all", ...STATUSES2];
2838
+ var ToolArgumentError = class extends Error {
2839
+ };
2840
+ function asRecord(input) {
2841
+ return typeof input === "object" && input !== null ? input : {};
2842
+ }
2843
+ function readString(input, key) {
2844
+ const value = input[key];
2845
+ if (value === void 0 || value === null) return void 0;
2846
+ if (typeof value !== "string") {
2847
+ throw new ToolArgumentError(`"${key}" must be a string, received ${typeof value}.`);
2848
+ }
2849
+ return value;
2850
+ }
2851
+ function readStringArray(input, key) {
2852
+ const value = input[key];
2853
+ if (value === void 0 || value === null) return void 0;
2854
+ if (typeof value === "string") return [value];
2855
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
2856
+ throw new ToolArgumentError(`"${key}" must be an array of strings.`);
2857
+ }
2858
+ return value;
2859
+ }
2860
+ function readEnum(input, key, allowed) {
2861
+ const value = readString(input, key);
2862
+ if (value === void 0) return void 0;
2863
+ if (!allowed.includes(value)) {
2864
+ throw new ToolArgumentError(
2865
+ `"${key}" must be one of: ${allowed.join(", ")}. Received "${value}".`
2866
+ );
2867
+ }
2868
+ return value;
2869
+ }
2870
+ function readStatuses(input) {
2871
+ const values = readStringArray(input, "statuses");
2872
+ if (values === void 0) return void 0;
2873
+ const invalid = values.filter((v) => !STATUSES2.includes(v));
2874
+ if (invalid.length > 0) {
2875
+ throw new ToolArgumentError(
2876
+ `"statuses" must contain only: ${STATUSES2.join(", ")}. Received "${invalid.join('", "')}".`
2877
+ );
2878
+ }
2879
+ return values;
2880
+ }
2881
+ function guarded(run) {
2882
+ try {
2883
+ return run();
2884
+ } catch (error) {
2885
+ if (error instanceof ToolArgumentError) return `Invalid input \u2014 ${error.message}`;
2886
+ throw error;
2887
+ }
2888
+ }
2889
+ var scenarioListSchema = {
2890
+ type: "object",
2891
+ properties: {
2892
+ statuses: {
2893
+ type: "array",
2894
+ items: { type: "string", enum: [...STATUSES2] },
2895
+ description: "Keep only scenarios with any of these statuses."
2896
+ },
2897
+ tags: {
2898
+ type: "array",
2899
+ items: { type: "string" },
2900
+ description: "Keep only scenarios carrying any of these tags."
2901
+ },
2902
+ sourceFiles: {
2903
+ type: "array",
2904
+ items: { type: "string" },
2905
+ description: "Keep only scenarios whose source file path contains any of these."
2906
+ }
2907
+ }
2908
+ };
2909
+ var READ_ONLY = { readOnlyHint: true, untrustedContentHint: true };
2910
+ function reportTools(deps) {
2911
+ const { report, setView, onAgentFilter } = deps;
2912
+ const now = deps.now ?? Date.now;
2913
+ const provenance = () => ({ run: runProvenance(report, now()) });
2914
+ const list = (filters) => {
2915
+ const scenarios = listScenarioSummaries(report, filters);
2916
+ return { ...provenance(), total: scenarios.length, scenarios };
2917
+ };
2918
+ return {
2919
+ list_scenarios: {
2920
+ title: "List scenarios",
2921
+ 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.",
2922
+ annotations: READ_ONLY,
2923
+ input: scenarioListSchema,
2924
+ execute: (input) => guarded(() => {
2925
+ const args = asRecord(input);
2926
+ const filters = {};
2927
+ const statuses = readStatuses(args);
2928
+ const tags = readStringArray(args, "tags");
2929
+ const sourceFiles = readStringArray(args, "sourceFiles");
2930
+ if (statuses) filters.statuses = statuses;
2931
+ if (tags) filters.tags = tags;
2932
+ if (sourceFiles) filters.sourceFiles = sourceFiles;
2933
+ return list(filters);
2934
+ })
2935
+ },
2936
+ get_failing_scenarios: {
2937
+ title: "Get failing scenarios",
2938
+ 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.",
2939
+ annotations: READ_ONLY,
2940
+ execute: () => list({ statuses: ["failed"] })
2941
+ },
2942
+ get_feature_summary: {
2943
+ title: "Get feature summary",
2944
+ 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.',
2945
+ annotations: READ_ONLY,
2946
+ execute: () => ({
2947
+ ...provenance(),
2948
+ summary: report.summary,
2949
+ features: getFeatureSummary(report)
2950
+ })
2951
+ },
2952
+ get_scenario: {
2953
+ title: "Get scenario",
2954
+ 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.",
2955
+ annotations: READ_ONLY,
2956
+ input: {
2957
+ type: "object",
2958
+ properties: {
2959
+ idOrTitle: {
2960
+ type: "string",
2961
+ description: "The scenario id, or its exact title as shown in the report."
2962
+ }
2963
+ },
2964
+ required: ["idOrTitle"]
2965
+ },
2966
+ execute: (input) => guarded(() => {
2967
+ const idOrTitle = readString(asRecord(input), "idOrTitle") ?? "";
2968
+ const found = getScenario(report, idOrTitle);
2969
+ if (!found) {
2970
+ return {
2971
+ ...provenance(),
2972
+ error: `No scenario found with id or title "${idOrTitle}". Call list_scenarios to see what this report contains.`
2973
+ };
2974
+ }
2975
+ return {
2976
+ ...provenance(),
2977
+ feature: {
2978
+ id: found.feature.id,
2979
+ title: found.feature.title,
2980
+ sourceFile: found.feature.sourceFile
2981
+ },
2982
+ // The summary projection, not the raw scenario: doc entry bodies and
2983
+ // base64 attachments are unbounded, and this payload goes straight
2984
+ // into a model's context window.
2985
+ scenario: toScenarioSummary(found.feature, found.scenario)
2986
+ };
2987
+ })
2988
+ },
2989
+ filter_scenarios: {
2990
+ title: "Filter the report",
2991
+ 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.`,
2992
+ input: {
2993
+ type: "object",
2994
+ properties: {
2995
+ search: {
2996
+ type: "string",
2997
+ description: "Free text matched against scenario titles, tags and step text. Empty string clears it."
2998
+ },
2999
+ status: {
3000
+ type: "string",
3001
+ enum: [...VIEW_STATUSES],
3002
+ description: 'Show only scenarios with this status. "all" clears the filter.'
3003
+ },
3004
+ tags: {
3005
+ type: "array",
3006
+ items: { type: "string" },
3007
+ description: "Show only scenarios carrying any of these tags. Empty array clears the filter."
3008
+ }
3009
+ }
3010
+ },
3011
+ execute: (input) => guarded(() => {
3012
+ const args = asRecord(input);
3013
+ const search = readString(args, "search");
3014
+ const status = readEnum(args, "status", VIEW_STATUSES);
3015
+ const tags = readStringArray(args, "tags");
3016
+ const applied = {
3017
+ search: search ?? deps.view.query,
3018
+ status: status ?? deps.view.status,
3019
+ tags: tags ?? deps.view.tags
3020
+ };
3021
+ const patch = {};
3022
+ if (search !== void 0) patch.query = search;
3023
+ if (status !== void 0) patch.status = status;
3024
+ if (tags !== void 0) patch.tags = tags;
3025
+ setView(patch);
3026
+ const matched = filterReport(report, {
3027
+ query: applied.search,
3028
+ status: applied.status,
3029
+ tags: applied.tags
3030
+ }).summary.total;
3031
+ const result = {
3032
+ ...applied,
3033
+ matched,
3034
+ total: report.summary.total
3035
+ };
3036
+ onAgentFilter?.(result);
3037
+ return { ...provenance(), applied: result };
3038
+ })
3039
+ }
3040
+ };
3041
+ }
3042
+
2704
3043
  // src/interactive/ReportInteractive.tsx
2705
- import { Fragment as Fragment13, jsx as jsx48, jsxs as jsxs32 } from "react/jsx-runtime";
3044
+ import { Fragment as Fragment14, jsx as jsx49, jsxs as jsxs34 } from "react/jsx-runtime";
2706
3045
  function ReportInteractive(props) {
2707
3046
  const { report, className, title, dataTheme } = props;
2708
3047
  const result = unwrapReport(report);
2709
3048
  if (!result.ok) {
2710
- return /* @__PURE__ */ jsx48(ReportErrorShell, { error: result.error, className, title, dataTheme });
3049
+ return /* @__PURE__ */ jsx49(
3050
+ ReportErrorShell,
3051
+ {
3052
+ error: result.error,
3053
+ className,
3054
+ title,
3055
+ dataTheme
3056
+ }
3057
+ );
2711
3058
  }
2712
- return /* @__PURE__ */ jsx48(ReportInteractiveView, { ...props, report: result.data });
3059
+ return /* @__PURE__ */ jsx49(ReportInteractiveView, { ...props, report: result.data });
2713
3060
  }
2714
3061
  var THEME_OPTIONS = [
2715
3062
  { value: "light", label: "Light", Icon: Sun },
2716
3063
  { value: "system", label: "System", Icon: Monitor },
2717
3064
  { value: "dark", label: "Dark", Icon: Moon }
2718
3065
  ];
2719
- function ThemeSegment({ pref, onPref }) {
2720
- return /* @__PURE__ */ jsx48(
3066
+ function ThemeSegment({
3067
+ pref,
3068
+ onPref
3069
+ }) {
3070
+ return /* @__PURE__ */ jsx49(
2721
3071
  ToggleGroup,
2722
3072
  {
2723
3073
  size: "sm",
@@ -2727,7 +3077,17 @@ function ThemeSegment({ pref, onPref }) {
2727
3077
  if (next) onPref(next);
2728
3078
  },
2729
3079
  "aria-label": "Color theme",
2730
- children: THEME_OPTIONS.map(({ value, label, Icon }) => /* @__PURE__ */ jsx48(ToggleGroupItem, { value, "aria-label": label, title: label, className: "px-2", children: /* @__PURE__ */ jsx48(Icon, {}) }, value))
3080
+ children: THEME_OPTIONS.map(({ value, label, Icon }) => /* @__PURE__ */ jsx49(
3081
+ ToggleGroupItem,
3082
+ {
3083
+ value,
3084
+ "aria-label": label,
3085
+ title: label,
3086
+ className: "px-2",
3087
+ children: /* @__PURE__ */ jsx49(Icon, {})
3088
+ },
3089
+ value
3090
+ ))
2731
3091
  }
2732
3092
  );
2733
3093
  }
@@ -2745,7 +3105,10 @@ function ReportInteractiveView({
2745
3105
  }) {
2746
3106
  const [urlState, setUrlState] = useUrlState();
2747
3107
  const { query, status: statusFilter, tags: activeTags, detail } = urlState;
2748
- const setQuery = useCallback5((next) => setUrlState({ query: next }), [setUrlState]);
3108
+ const setQuery = useCallback5(
3109
+ (next) => setUrlState({ query: next }),
3110
+ [setUrlState]
3111
+ );
2749
3112
  const setStatusFilter = useCallback5(
2750
3113
  (next) => setUrlState({ status: next }),
2751
3114
  [setUrlState]
@@ -2754,7 +3117,7 @@ function ReportInteractiveView({
2754
3117
  (next) => setUrlState({ detail: next }),
2755
3118
  [setUrlState]
2756
3119
  );
2757
- const [helpOpen, setHelpOpen] = useState8(false);
3120
+ const [helpOpen, setHelpOpen] = useState9(false);
2758
3121
  const { pref: themePref, setPref: setThemePref } = useTheme();
2759
3122
  const searchRef = useRef5(null);
2760
3123
  const expandId = useId2();
@@ -2764,7 +3127,13 @@ function ReportInteractiveView({
2764
3127
  const filtered = useMemo3(
2765
3128
  // Features carrying failures float to the top so the first thing on screen
2766
3129
  // is the thing that needs attention; an all-green run keeps source order.
2767
- () => failuresFirst(filterReport(report, { query: deferredQuery, status: statusFilter, tags: activeTags })),
3130
+ () => failuresFirst(
3131
+ filterReport(report, {
3132
+ query: deferredQuery,
3133
+ status: statusFilter,
3134
+ tags: activeTags
3135
+ })
3136
+ ),
2768
3137
  [report, deferredQuery, statusFilter, activeTags]
2769
3138
  );
2770
3139
  const isFiltering = query !== deferredQuery;
@@ -2773,10 +3142,14 @@ function ReportInteractiveView({
2773
3142
  const opts = [
2774
3143
  { key: "all", label: "All", count: s.total }
2775
3144
  ];
2776
- if (s.passed) opts.push({ key: "passed", label: "Passed", count: s.passed });
2777
- if (s.failed) opts.push({ key: "failed", label: "Failed", count: s.failed });
2778
- if (s.skipped) opts.push({ key: "skipped", label: "Skipped", count: s.skipped });
2779
- if (s.pending) opts.push({ key: "pending", label: "Pending", count: s.pending });
3145
+ if (s.passed)
3146
+ opts.push({ key: "passed", label: "Passed", count: s.passed });
3147
+ if (s.failed)
3148
+ opts.push({ key: "failed", label: "Failed", count: s.failed });
3149
+ if (s.skipped)
3150
+ opts.push({ key: "skipped", label: "Skipped", count: s.skipped });
3151
+ if (s.pending)
3152
+ opts.push({ key: "pending", label: "Pending", count: s.pending });
2780
3153
  return opts;
2781
3154
  }, [report]);
2782
3155
  const tagOptions = useMemo3(() => allTags(report), [report]);
@@ -2786,7 +3159,7 @@ function ReportInteractiveView({
2786
3159
  }),
2787
3160
  [activeTags, setUrlState]
2788
3161
  );
2789
- const [toast, setToast] = useState8(null);
3162
+ const [toast, setToast] = useState9(null);
2790
3163
  const toastTimer = useRef5(null);
2791
3164
  const showToast = useCallback5((msg) => {
2792
3165
  setToast(msg);
@@ -2796,7 +3169,10 @@ function ReportInteractiveView({
2796
3169
  const copy = useCallback5(
2797
3170
  (text, msg) => {
2798
3171
  if (typeof navigator !== "undefined" && navigator.clipboard) {
2799
- navigator.clipboard.writeText(text).then(() => showToast(msg), () => showToast("Copy failed"));
3172
+ navigator.clipboard.writeText(text).then(
3173
+ () => showToast(msg),
3174
+ () => showToast("Copy failed")
3175
+ );
2800
3176
  }
2801
3177
  },
2802
3178
  [showToast]
@@ -2814,9 +3190,21 @@ function ReportInteractiveView({
2814
3190
  () => report.features.flatMap((f) => [f.id, ...f.scenarios.map((s) => s.id)]),
2815
3191
  [report]
2816
3192
  );
2817
- const collapse = useCollapseState(useCallback5(() => defaultCollapsedIds(report), [report]));
2818
- const collapseAll = useCallback5(() => collapse.collapseAll(allCollapsibleIds), [collapse, allCollapsibleIds]);
2819
- const [expandedAll, setExpandedAll] = useState8(() => report.summary.failed === 0);
3193
+ const collapse = useCollapseState(
3194
+ useCallback5(() => defaultCollapsedIds(report), [report])
3195
+ );
3196
+ const revealFilteredResults = deferredQuery.trim() !== "" || statusFilter !== "all" || activeTags.length > 0;
3197
+ const visibleCollapse = useMemo3(
3198
+ () => revealFilteredResults ? { ...collapse.api, isCollapsed: () => false } : collapse.api,
3199
+ [collapse.api, revealFilteredResults]
3200
+ );
3201
+ const collapseAll = useCallback5(
3202
+ () => collapse.collapseAll(allCollapsibleIds),
3203
+ [collapse, allCollapsibleIds]
3204
+ );
3205
+ const [expandedAll, setExpandedAll] = useState9(
3206
+ () => report.summary.failed === 0
3207
+ );
2820
3208
  const setExpanded = useCallback5(
2821
3209
  (expanded) => {
2822
3210
  if (expanded) collapse.expandAll();
@@ -2825,6 +3213,20 @@ function ReportInteractiveView({
2825
3213
  },
2826
3214
  [collapse, collapseAll]
2827
3215
  );
3216
+ const [agentFilter, setAgentFilter] = useState9(null);
3217
+ useTools(
3218
+ reportTools({
3219
+ report,
3220
+ view: urlState,
3221
+ setView: setUrlState,
3222
+ onAgentFilter: setAgentFilter
3223
+ })
3224
+ );
3225
+ const showAgentFilter = agentFilter !== null && agentFilter.search === query && agentFilter.status === statusFilter && agentFilter.tags.length === activeTags.length && agentFilter.tags.every((tag) => activeTags.includes(tag));
3226
+ const clearFilters = useCallback5(
3227
+ () => setUrlState({ query: "", status: "all", tags: [] }),
3228
+ [setUrlState]
3229
+ );
2828
3230
  const focusSearch = useCallback5(() => {
2829
3231
  searchRef.current?.focus();
2830
3232
  }, []);
@@ -2843,7 +3245,7 @@ function ReportInteractiveView({
2843
3245
  const escape = useCallback5(() => {
2844
3246
  if (helpOpen) setHelpOpen(false);
2845
3247
  else if (query !== "") setQuery("");
2846
- }, [helpOpen, query]);
3248
+ }, [helpOpen, query, setQuery]);
2847
3249
  useKeyboardShortcuts({
2848
3250
  onFocusSearch: focusSearch,
2849
3251
  onNextFailure: () => stepFailure(1),
@@ -2858,13 +3260,13 @@ function ReportInteractiveView({
2858
3260
  const totalScenarios = report.summary.total;
2859
3261
  const matchedScenarios = filtered.summary.total;
2860
3262
  const showToc = !hideToc && totalScenarios > 3;
2861
- return /* @__PURE__ */ jsx48(CollapseProvider, { value: collapse.api, children: /* @__PURE__ */ jsx48(ScenarioActionsProvider, { value: scenarioActions, children: /* @__PURE__ */ jsx48(ScenarioHistoryProvider, { value: scenarioHistory ?? null, children: /* @__PURE__ */ jsx48(
3263
+ return /* @__PURE__ */ jsx49(CollapseProvider, { value: visibleCollapse, children: /* @__PURE__ */ jsx49(ScenarioActionsProvider, { value: scenarioActions, children: /* @__PURE__ */ jsx49(ScenarioHistoryProvider, { value: scenarioHistory ?? null, children: /* @__PURE__ */ jsx49(ScenarioFreshnessProvider, { value: { staleAfterDays, report }, children: /* @__PURE__ */ jsx49(
2862
3264
  ReportRoot,
2863
3265
  {
2864
3266
  report: filtered,
2865
3267
  customRenderers,
2866
3268
  renderers,
2867
- children: /* @__PURE__ */ jsxs32(
3269
+ children: /* @__PURE__ */ jsxs34(
2868
3270
  "main",
2869
3271
  {
2870
3272
  className: cn("es-report", "es-report-interactive", className),
@@ -2873,8 +3275,8 @@ function ReportInteractiveView({
2873
3275
  "data-theme": dataTheme,
2874
3276
  "data-detail-level": detail,
2875
3277
  children: [
2876
- /* @__PURE__ */ jsxs32("header", { className: "es-report-header", children: [
2877
- /* @__PURE__ */ jsxs32(
3278
+ /* @__PURE__ */ jsxs34("header", { className: "es-report-header", children: [
3279
+ /* @__PURE__ */ jsxs34(
2878
3280
  "div",
2879
3281
  {
2880
3282
  "data-es-navbar": true,
@@ -2883,9 +3285,9 @@ function ReportInteractiveView({
2883
3285
  hideHeader ? "justify-end" : "justify-between"
2884
3286
  ),
2885
3287
  children: [
2886
- hideHeader ? null : /* @__PURE__ */ jsx48("h1", { className: "min-w-0", children: title ?? "Story Report" }),
2887
- /* @__PURE__ */ jsxs32("div", { className: "flex items-center gap-2", children: [
2888
- /* @__PURE__ */ jsx48(
3288
+ hideHeader ? null : /* @__PURE__ */ jsx49("h1", { className: "min-w-0", children: title ?? "Story Report" }),
3289
+ /* @__PURE__ */ jsxs34("div", { className: "flex items-center gap-2", children: [
3290
+ /* @__PURE__ */ jsx49(
2889
3291
  ReportSearch,
2890
3292
  {
2891
3293
  ref: searchRef,
@@ -2895,16 +3297,16 @@ function ReportInteractiveView({
2895
3297
  totalCount: totalScenarios
2896
3298
  }
2897
3299
  ),
2898
- /* @__PURE__ */ jsx48(ThemeSegment, { pref: themePref, onPref: setThemePref })
3300
+ /* @__PURE__ */ jsx49(ThemeSegment, { pref: themePref, onPref: setThemePref })
2899
3301
  ] })
2900
3302
  ]
2901
3303
  }
2902
3304
  ),
2903
- hideHeader ? null : /* @__PURE__ */ jsxs32(Fragment13, { children: [
2904
- /* @__PURE__ */ jsx48(ReportSummary, {}),
2905
- /* @__PURE__ */ jsx48(ReportMeta, {})
3305
+ hideHeader ? null : /* @__PURE__ */ jsxs34(Fragment14, { children: [
3306
+ /* @__PURE__ */ jsx49(ReportSummary, {}),
3307
+ /* @__PURE__ */ jsx49(ReportMeta, {})
2906
3308
  ] }),
2907
- /* @__PURE__ */ jsx48(
3309
+ /* @__PURE__ */ jsx49(
2908
3310
  ReportFreshness,
2909
3311
  {
2910
3312
  lastRunMs: reportLastRunMs(report),
@@ -2912,8 +3314,14 @@ function ReportInteractiveView({
2912
3314
  staleAfterDays
2913
3315
  }
2914
3316
  ),
2915
- /* @__PURE__ */ jsx48(ReportLastRunDelta, { history: scenarioHistory, report }),
2916
- /* @__PURE__ */ jsx48(
3317
+ /* @__PURE__ */ jsx49(
3318
+ ReportLastRunDelta,
3319
+ {
3320
+ history: scenarioHistory,
3321
+ report
3322
+ }
3323
+ ),
3324
+ /* @__PURE__ */ jsx49(
2917
3325
  ReportFilters,
2918
3326
  {
2919
3327
  statuses: statusOptions,
@@ -2924,14 +3332,28 @@ function ReportInteractiveView({
2924
3332
  onToggleTag: toggleTag
2925
3333
  }
2926
3334
  ),
2927
- /* @__PURE__ */ jsxs32("div", { className: "flex flex-wrap items-center gap-x-6 gap-y-2 border-t border-border/60 pt-3", children: [
2928
- showToc ? /* @__PURE__ */ jsx48(ReportTocDrawer, {}) : null,
2929
- /* @__PURE__ */ jsxs32("div", { className: "flex items-center gap-2", children: [
2930
- /* @__PURE__ */ jsx48(Switch, { id: expandId, checked: expandedAll, onCheckedChange: setExpanded }),
2931
- /* @__PURE__ */ jsx48("label", { htmlFor: expandId, className: "cursor-pointer text-sm text-muted-foreground select-none", children: "Expand all" })
3335
+ /* @__PURE__ */ jsxs34("div", { className: "flex flex-wrap items-center gap-x-6 gap-y-2 border-t border-border/60 pt-3", children: [
3336
+ showToc ? /* @__PURE__ */ jsx49(ReportTocDrawer, {}) : null,
3337
+ /* @__PURE__ */ jsxs34("div", { className: "flex items-center gap-2", children: [
3338
+ /* @__PURE__ */ jsx49(
3339
+ Switch,
3340
+ {
3341
+ id: expandId,
3342
+ checked: expandedAll,
3343
+ onCheckedChange: setExpanded
3344
+ }
3345
+ ),
3346
+ /* @__PURE__ */ jsx49(
3347
+ "label",
3348
+ {
3349
+ htmlFor: expandId,
3350
+ className: "cursor-pointer text-sm text-muted-foreground select-none",
3351
+ children: "Expand all"
3352
+ }
3353
+ )
2932
3354
  ] }),
2933
- /* @__PURE__ */ jsxs32("div", { className: "flex items-center gap-2", children: [
2934
- /* @__PURE__ */ jsx48(
3355
+ /* @__PURE__ */ jsxs34("div", { className: "flex items-center gap-2", children: [
3356
+ /* @__PURE__ */ jsx49(
2935
3357
  Switch,
2936
3358
  {
2937
3359
  id: docsId,
@@ -2939,16 +3361,36 @@ function ReportInteractiveView({
2939
3361
  onCheckedChange: (checked) => setDetail(checked ? "full" : "minimal")
2940
3362
  }
2941
3363
  ),
2942
- /* @__PURE__ */ jsx48("label", { htmlFor: docsId, className: "cursor-pointer text-sm text-muted-foreground select-none", children: "Show documentation" })
3364
+ /* @__PURE__ */ jsx49(
3365
+ "label",
3366
+ {
3367
+ htmlFor: docsId,
3368
+ className: "cursor-pointer text-sm text-muted-foreground select-none",
3369
+ children: "Show documentation"
3370
+ }
3371
+ )
2943
3372
  ] })
2944
3373
  ] })
2945
3374
  ] }),
2946
- /* @__PURE__ */ jsx48(ReportFailureBanner, { failures }),
2947
- hasContent ? showToc ? /* @__PURE__ */ jsxs32("div", { className: "flex gap-6", children: [
2948
- /* @__PURE__ */ jsx48(ReportToc, {}),
2949
- /* @__PURE__ */ jsx48("div", { className: "flex min-w-0 flex-1 flex-col gap-4", children: /* @__PURE__ */ jsx48(ReportFeatureList, {}) })
2950
- ] }) : /* @__PURE__ */ jsx48("div", { className: "flex flex-col gap-4", children: /* @__PURE__ */ jsx48(ReportFeatureList, {}) }) : /* @__PURE__ */ jsx48(ReportEmpty, { message: query ? "No scenarios match the search." : void 0 }),
2951
- /* @__PURE__ */ jsx48(
3375
+ showAgentFilter ? /* @__PURE__ */ jsx49(
3376
+ ReportAgentFilterNotice,
3377
+ {
3378
+ applied: agentFilter,
3379
+ onReset: clearFilters,
3380
+ onDismiss: () => setAgentFilter(null)
3381
+ }
3382
+ ) : null,
3383
+ /* @__PURE__ */ jsx49(ReportFailureBanner, { failures }),
3384
+ hasContent ? showToc ? /* @__PURE__ */ jsxs34("div", { className: "flex gap-6", children: [
3385
+ /* @__PURE__ */ jsx49(ReportToc, {}),
3386
+ /* @__PURE__ */ jsx49("div", { className: "flex min-w-0 flex-1 flex-col gap-4", children: /* @__PURE__ */ jsx49(ReportFeatureList, {}) })
3387
+ ] }) : /* @__PURE__ */ jsx49("div", { className: "flex flex-col gap-4", children: /* @__PURE__ */ jsx49(ReportFeatureList, {}) }) : /* @__PURE__ */ jsx49(
3388
+ ReportEmpty,
3389
+ {
3390
+ message: query ? "No scenarios match the search." : void 0
3391
+ }
3392
+ ),
3393
+ /* @__PURE__ */ jsx49(
2952
3394
  "button",
2953
3395
  {
2954
3396
  type: "button",
@@ -2959,8 +3401,14 @@ function ReportInteractiveView({
2959
3401
  children: "?"
2960
3402
  }
2961
3403
  ),
2962
- /* @__PURE__ */ jsx48(ReportShortcutsHelp, { open: helpOpen, onClose: () => setHelpOpen(false) }),
2963
- toast ? /* @__PURE__ */ jsx48(
3404
+ /* @__PURE__ */ jsx49(
3405
+ ReportShortcutsHelp,
3406
+ {
3407
+ open: helpOpen,
3408
+ onClose: () => setHelpOpen(false)
3409
+ }
3410
+ ),
3411
+ toast ? /* @__PURE__ */ jsx49(
2964
3412
  "div",
2965
3413
  {
2966
3414
  role: "status",
@@ -2973,18 +3421,18 @@ function ReportInteractiveView({
2973
3421
  }
2974
3422
  )
2975
3423
  }
2976
- ) }) }) });
3424
+ ) }) }) }) });
2977
3425
  }
2978
3426
 
2979
3427
  // src/interactive/ReportInteractiveIsland.tsx
2980
3428
  import { useMemo as useMemo4 } from "react";
2981
3429
 
2982
3430
  // src/interactive/island-renderers.tsx
2983
- import { createElement, useEffect as useEffect9, useState as useState10 } from "react";
3431
+ import { createElement, useEffect as useEffect10, useState as useState11 } from "react";
2984
3432
 
2985
3433
  // src/components/doc/MermaidView.tsx
2986
- import { useEffect as useEffect8, useRef as useRef6, useState as useState9 } from "react";
2987
- import { Fragment as Fragment14, jsx as jsx49, jsxs as jsxs33 } from "react/jsx-runtime";
3434
+ import { useEffect as useEffect9, useRef as useRef6, useState as useState10 } from "react";
3435
+ import { Fragment as Fragment15, jsx as jsx50, jsxs as jsxs35 } from "react/jsx-runtime";
2988
3436
  function prefersDark(el) {
2989
3437
  const themed = el?.closest("[data-theme]");
2990
3438
  if (themed?.dataset.theme === "dark") return true;
@@ -2993,9 +3441,9 @@ function prefersDark(el) {
2993
3441
  }
2994
3442
  function MermaidView({ entry, load }) {
2995
3443
  const ref = useRef6(null);
2996
- const [svg, setSvg] = useState9(null);
2997
- const [error, setError] = useState9(null);
2998
- useEffect8(() => {
3444
+ const [svg, setSvg] = useState10(null);
3445
+ const [error, setError] = useState10(null);
3446
+ useEffect9(() => {
2999
3447
  let cancelled = false;
3000
3448
  const draw = async () => {
3001
3449
  let mermaid;
@@ -3038,9 +3486,9 @@ function MermaidView({ entry, load }) {
3038
3486
  };
3039
3487
  }, [entry.code, load]);
3040
3488
  if (svg) {
3041
- return /* @__PURE__ */ jsxs33("figure", { ref, className: "my-3", children: [
3042
- entry.title ? /* @__PURE__ */ jsx49("figcaption", { className: "mb-1.5 text-xs font-medium text-muted-foreground", children: entry.title }) : null,
3043
- /* @__PURE__ */ jsx49(
3489
+ return /* @__PURE__ */ jsxs35("figure", { ref, className: "my-3", children: [
3490
+ entry.title ? /* @__PURE__ */ jsx50("figcaption", { className: "mb-1.5 text-xs font-medium text-muted-foreground", children: entry.title }) : null,
3491
+ /* @__PURE__ */ jsx50(
3044
3492
  "div",
3045
3493
  {
3046
3494
  role: "img",
@@ -3051,8 +3499,8 @@ function MermaidView({ entry, load }) {
3051
3499
  )
3052
3500
  ] });
3053
3501
  }
3054
- return /* @__PURE__ */ jsxs33(Fragment14, { children: [
3055
- error ? /* @__PURE__ */ jsxs33(
3502
+ return /* @__PURE__ */ jsxs35(Fragment15, { children: [
3503
+ error ? /* @__PURE__ */ jsxs35(
3056
3504
  "p",
3057
3505
  {
3058
3506
  role: "alert",
@@ -3064,12 +3512,12 @@ function MermaidView({ entry, load }) {
3064
3512
  ]
3065
3513
  }
3066
3514
  ) : null,
3067
- /* @__PURE__ */ jsx49(MermaidSource, { entry, ref })
3515
+ /* @__PURE__ */ jsx50(MermaidSource, { entry, ref })
3068
3516
  ] });
3069
3517
  }
3070
3518
 
3071
3519
  // src/interactive/island-renderers.tsx
3072
- import { jsx as jsx50 } from "react/jsx-runtime";
3520
+ import { jsx as jsx51 } from "react/jsx-runtime";
3073
3521
  var HLJS_ESM = "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/es/highlight.min.js";
3074
3522
  var MERMAID_ESM = "https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs";
3075
3523
  function cachedCdnModule(url) {
@@ -3086,8 +3534,8 @@ function cachedCdnModule(url) {
3086
3534
  var loadMermaidFromCdn = cachedCdnModule(MERMAID_ESM);
3087
3535
  var loadHljs = cachedCdnModule(HLJS_ESM);
3088
3536
  function CdnHighlightedCode({ entry }) {
3089
- const [html, setHtml] = useState10(null);
3090
- useEffect9(() => {
3537
+ const [html, setHtml] = useState11(null);
3538
+ useEffect10(() => {
3091
3539
  let cancelled = false;
3092
3540
  void (async () => {
3093
3541
  try {
@@ -3103,13 +3551,13 @@ function CdnHighlightedCode({ entry }) {
3103
3551
  cancelled = true;
3104
3552
  };
3105
3553
  }, [entry.content, entry.lang]);
3106
- return /* @__PURE__ */ jsx50(CodeFigure, { label: entry.label, children: html !== null ? /* @__PURE__ */ jsx50(
3554
+ return /* @__PURE__ */ jsx51(CodeFigure, { label: entry.label, children: html !== null ? /* @__PURE__ */ jsx51(
3107
3555
  "code",
3108
3556
  {
3109
3557
  className: cn("hljs font-mono text-xs", entry.lang && `language-${entry.lang}`),
3110
3558
  dangerouslySetInnerHTML: { __html: html }
3111
3559
  }
3112
- ) : /* @__PURE__ */ jsx50("code", { className: cn("font-mono text-xs", entry.lang && `language-${entry.lang}`), children: entry.content }) });
3560
+ ) : /* @__PURE__ */ jsx51("code", { className: cn("font-mono text-xs", entry.lang && `language-${entry.lang}`), children: entry.content }) });
3113
3561
  }
3114
3562
  function buildIslandRenderers(opts) {
3115
3563
  const renderers = {};
@@ -3123,7 +3571,7 @@ function buildIslandRenderers(opts) {
3123
3571
  }
3124
3572
 
3125
3573
  // src/interactive/ReportInteractiveIsland.tsx
3126
- import { jsx as jsx51 } from "react/jsx-runtime";
3574
+ import { jsx as jsx52 } from "react/jsx-runtime";
3127
3575
  function ReportInteractiveIsland({
3128
3576
  report,
3129
3577
  title,
@@ -3137,7 +3585,7 @@ function ReportInteractiveIsland({
3137
3585
  () => buildIslandRenderers({ syntaxHighlighting, mermaid }),
3138
3586
  [syntaxHighlighting, mermaid]
3139
3587
  );
3140
- return /* @__PURE__ */ jsx51(
3588
+ return /* @__PURE__ */ jsx52(
3141
3589
  ReportInteractive,
3142
3590
  {
3143
3591
  report,