@syrin/iris 0.4.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.
package/dist/index.js ADDED
@@ -0,0 +1,3984 @@
1
+ // ../protocol/dist/constants.js
2
+ var IRIS_DEFAULT_PORT = 4400;
3
+ var IRIS_WS_PATH = "/iris";
4
+ var IRIS_PROTOCOL_VERSION = 1;
5
+ var RunKind = {
6
+ FLOW_REPLAY: "flow_replay",
7
+ // auto-recorded by iris_flow_replay
8
+ MANUAL: "manual"
9
+ // explicitly recorded via iris_run_record
10
+ };
11
+ var RunStatus = {
12
+ PASS: "pass",
13
+ DRIFT: "drift",
14
+ ERROR: "error",
15
+ FAIL: "fail"
16
+ };
17
+ var FLOW_FILE_VERSION = 1;
18
+ var AnchorKind = {
19
+ TESTID: "testid",
20
+ // { kind:'testid', value }
21
+ ROLE: "role",
22
+ // { kind:'role', role, name? } — best-effort fallback
23
+ SIGNAL: "signal"
24
+ // { kind:'signal', name } — wait/assert anchors
25
+ };
26
+ var DEGRADED_ANCHOR_ROLE = "unresolved";
27
+ var AnnotationKind = {
28
+ ASSERT_SIGNAL: "assert-signal",
29
+ // → step.expect.signal (invariant)
30
+ ASSERT_VISIBLE: "assert-visible",
31
+ // → step.expect.element (invariant)
32
+ MARK_DYNAMIC: "mark-dynamic",
33
+ // → flow.dynamic[] (don't assert words/content)
34
+ SUCCESS_STATE: "success-state"
35
+ // → flow.success (golden end condition)
36
+ };
37
+ var RecorderPhase = {
38
+ IDLE: "idle",
39
+ // listeners inert, no steps captured
40
+ RECORDING: "recording",
41
+ // capture-phase listeners live
42
+ ANNOTATING: "annotating"
43
+ // recording paused, awaiting an annotation target/kind
44
+ };
45
+ var EventType = {
46
+ DOM_ADDED: "dom.added",
47
+ DOM_REMOVED: "dom.removed",
48
+ DOM_ATTR: "dom.attr",
49
+ DOM_TEXT: "dom.text",
50
+ NET_REQUEST: "net.request",
51
+ NET_PENDING: "net.pending",
52
+ ROUTE_CHANGE: "route.change",
53
+ CONSOLE_LOG: "console.log",
54
+ CONSOLE_WARN: "console.warn",
55
+ CONSOLE_ERROR: "console.error",
56
+ ERROR_UNCAUGHT: "error.uncaught",
57
+ VISIBLE_SHOWN: "visible.shown",
58
+ VISIBLE_HIDDEN: "visible.hidden",
59
+ ANIM_START: "anim.start",
60
+ ANIM_END: "anim.end",
61
+ SCROLL_POSITION: "scroll.position",
62
+ REVEAL_SHOWN: "reveal.shown",
63
+ SIGNAL: "signal",
64
+ STATE_CHANGE: "state.change",
65
+ /** page-level visibility/focus health (distinct from element-level VISIBLE_*). */
66
+ PAGE_HEALTH: "page.health",
67
+ /** browser → bridge: a human recording compiled in-page. */
68
+ FLOW_RECORDED: "flow.recorded",
69
+ /** synthetic: browser transport queue overflowed; events were dropped. `data: { dropped: number }`. */
70
+ TRANSPORT_OVERFLOW: "transport.overflow",
71
+ /**
72
+ * Live-control: browser → bridge. A human acted on the presenter panel.
73
+ * `data: { kind: HumanControlKind; text?: string }`. Rides the existing EventMessage.
74
+ */
75
+ HUMAN_CONTROL: "human.control"
76
+ };
77
+ var SessionState = {
78
+ ACTIVE: "active",
79
+ PAUSED: "paused",
80
+ ENDED: "ended"
81
+ };
82
+ var SESSION_AUTO = "auto";
83
+ var HumanControlKind = {
84
+ PAUSE: "pause",
85
+ RESUME: "resume",
86
+ END: "end",
87
+ MESSAGE: "message"
88
+ };
89
+ var SESSION_HEALTH = {
90
+ HEARTBEAT_MS: 5e3,
91
+ /** lastSeenMs beyond this ⇒ throttled (≈ 2 missed heartbeats). */
92
+ STALE_THRESHOLD_MS: 12e3
93
+ };
94
+ var SESSION_LIFECYCLE = {
95
+ /** Default agent-idle window before the server reaps a session. Agent-tunable via iris_session. */
96
+ IDLE_END_MS: 3e5,
97
+ /** Floor for a tuned idle window (so an agent can't disable the safety net). */
98
+ IDLE_END_MIN_MS: 5e3,
99
+ /** How often the server reaper sweeps sessions for idle/disconnected ones. */
100
+ REAP_INTERVAL_MS: 5e3,
101
+ /** Browser fallback: continuous failure to reach the bridge for this long ⇒ self-end the session. */
102
+ BRIDGE_LOST_MS: 15e3
103
+ };
104
+ var HealthReason = {
105
+ VISIBILITY: "visibilitychange",
106
+ FOCUS: "focus",
107
+ BLUR: "blur",
108
+ HEARTBEAT: "heartbeat",
109
+ INITIAL: "initial"
110
+ };
111
+ var ActionWarning = {
112
+ HOVER_NATIVE_ENTER_LEAVE: "target has enter/leave handlers; synthetic hover may not trigger them \u2014 expect no state change",
113
+ /** real-input provider was available but failed; the action fell back to synthetic dispatch. */
114
+ REAL_INPUT_FELL_BACK: "real-input provider was available but failed; fell back to synthetic dispatch",
115
+ /**
116
+ * The click point was covered by another element. Synthetic dispatch still delivered the event to
117
+ * your target, but a real user could NOT click it — treat the target as visually blocked, not
118
+ * actionable. Scroll it into a clear area or dismiss the overlay on top.
119
+ */
120
+ CLICK_OCCLUDED: "target is visually occluded by another element; a real user could not click it (synthetic dispatch still delivered the event) \u2014 dismiss the overlay or scroll the target clear"
121
+ };
122
+ var ActionType = {
123
+ CLICK: "click",
124
+ DBLCLICK: "dblclick",
125
+ HOVER: "hover",
126
+ FOCUS: "focus",
127
+ BLUR: "blur",
128
+ FILL: "fill",
129
+ TYPE: "type",
130
+ CLEAR: "clear",
131
+ SELECT: "select",
132
+ CHECK: "check",
133
+ UNCHECK: "uncheck",
134
+ SUBMIT: "submit",
135
+ PRESS: "press",
136
+ UPLOAD: "upload",
137
+ SCROLL_INTO_VIEW: "scrollIntoView",
138
+ DRAG: "drag",
139
+ WEBMCP: "webmcp"
140
+ };
141
+ var SettleReason = {
142
+ TIMEOUT: "timeout",
143
+ THROTTLED: "throttled"
144
+ };
145
+ var ComponentStateReason = {
146
+ UNAVAILABLE: "component-state-unavailable"
147
+ };
148
+ var ElementState = {
149
+ VISIBLE: "visible",
150
+ HIDDEN: "hidden",
151
+ ENABLED: "enabled",
152
+ DISABLED: "disabled",
153
+ CHECKED: "checked",
154
+ EXPANDED: "expanded",
155
+ FOCUSED: "focused",
156
+ PRESENT: "present"
157
+ };
158
+ var QueryBy = {
159
+ ROLE: "role",
160
+ TEXT: "text",
161
+ LABEL: "label",
162
+ PLACEHOLDER: "placeholder",
163
+ TESTID: "testid",
164
+ ALT: "alt"
165
+ };
166
+ var IrisCommand = {
167
+ SNAPSHOT: "snapshot",
168
+ QUERY: "query",
169
+ MATCH: "match",
170
+ INSPECT: "inspect",
171
+ ACT: "act",
172
+ ACT_SEQUENCE: "act_sequence",
173
+ ANIMATIONS: "animations",
174
+ NARRATE: "narrate",
175
+ CLOCK: "clock",
176
+ CAPABILITIES: "capabilities",
177
+ STATE_READ: "state_read",
178
+ /** scroll a ref's nearest scrollable container by ~a viewport (virtualized lists). */
179
+ SCROLL: "scroll",
180
+ /** Session lifecycle: agent tunes the presenter session (e.g. idle-end timeout) for the app's needs. */
181
+ SESSION_CONFIG: "session_config",
182
+ /**
183
+ * Live-control: bridge → browser. Pushes the current session state to the panel so an
184
+ * AGENT-driven pause/end keeps the presenter in sync. `args: { state, text? }`.
185
+ */
186
+ PRESENTER: "presenter",
187
+ /** Navigate the page to a new URL. `args: { url: string }`. */
188
+ NAVIGATE: "navigate",
189
+ /** Reload the page. `args: { hard?: boolean }` — hard clears the cache via location replace trick. */
190
+ REFRESH: "refresh"
191
+ };
192
+ var PresenterMode = {
193
+ IDLE: "idle",
194
+ READING: "reading",
195
+ ACTING: "acting"
196
+ };
197
+ var SnapshotMode = {
198
+ FULL: "full",
199
+ INTERACTIVE: "interactive",
200
+ STATUS: "status"
201
+ };
202
+ var MessageKind = {
203
+ HELLO: "hello",
204
+ COMMAND: "command",
205
+ COMMAND_RESULT: "command_result",
206
+ EVENT: "event"
207
+ };
208
+
209
+ // ../protocol/dist/types.js
210
+ import { z } from "zod";
211
+ var ElementQuerySchema = z.object({
212
+ by: z.nativeEnum(QueryBy).optional(),
213
+ value: z.string().optional(),
214
+ role: z.string().optional(),
215
+ name: z.string().optional(),
216
+ text: z.string().optional(),
217
+ label: z.string().optional(),
218
+ placeholder: z.string().optional(),
219
+ testid: z.string().optional(),
220
+ alt: z.string().optional(),
221
+ /** CSS selector or ref to scope the search. */
222
+ scope: z.string().optional()
223
+ });
224
+ var CapabilityFlowSchema = z.object({
225
+ name: z.string(),
226
+ steps: z.array(z.string())
227
+ });
228
+ var CapabilitiesSchema = z.object({
229
+ testids: z.array(z.string()),
230
+ signals: z.array(z.string()),
231
+ stores: z.array(z.string()),
232
+ flows: z.array(CapabilityFlowSchema)
233
+ });
234
+ var ContractFileSchema = z.object({
235
+ version: z.number(),
236
+ generatedAt: z.number(),
237
+ capabilities: CapabilitiesSchema
238
+ });
239
+ var RunEvidenceSchema = z.object({
240
+ consoleErrors: z.number().optional(),
241
+ networkErrors: z.number().optional(),
242
+ driftSteps: z.number().optional()
243
+ });
244
+ var RunRecordSchema = z.object({
245
+ kind: z.nativeEnum(RunKind),
246
+ name: z.string(),
247
+ status: z.nativeEnum(RunStatus),
248
+ at: z.number(),
249
+ summary: z.string().optional(),
250
+ evidence: RunEvidenceSchema.optional(),
251
+ durationMs: z.number().optional()
252
+ });
253
+ var ProjectLearnedSchema = z.object({
254
+ flows: z.array(z.string()).optional(),
255
+ routes: z.array(z.string()).optional()
256
+ });
257
+ var ProjectFileSchema = z.object({
258
+ version: z.number(),
259
+ learned: ProjectLearnedSchema.optional(),
260
+ runs: z.array(RunRecordSchema)
261
+ });
262
+ var FlowAnchorSchema = z.discriminatedUnion("kind", [
263
+ z.object({ kind: z.literal(AnchorKind.TESTID), value: z.string().min(1) }),
264
+ z.object({
265
+ kind: z.literal(AnchorKind.ROLE),
266
+ role: z.string().min(1),
267
+ name: z.string().optional()
268
+ }),
269
+ z.object({ kind: z.literal(AnchorKind.SIGNAL), name: z.string().min(1) })
270
+ ]);
271
+ var FlowExpectSchema = z.object({
272
+ signal: z.string().optional(),
273
+ /**
274
+ * Optional payload shape an `assert-signal` annotation requires the signal
275
+ * to match (the predicate DSL's signal.dataMatches). Additive/optional — a flow file with a
276
+ * bare `signal` still parses, and the on-disk version stays FLOW_FILE_VERSION 1.
277
+ */
278
+ signalData: z.record(z.unknown()).optional(),
279
+ net: z.object({
280
+ method: z.string().optional(),
281
+ urlContains: z.string().optional(),
282
+ status: z.number().optional()
283
+ }).optional(),
284
+ element: z.object({
285
+ testid: z.string().optional(),
286
+ role: z.string().optional(),
287
+ name: z.string().optional()
288
+ }).optional()
289
+ });
290
+ var baseFlowStep = z.object({
291
+ tool: z.string(),
292
+ anchor: FlowAnchorSchema,
293
+ action: z.nativeEnum(ActionType).optional(),
294
+ args: z.record(z.unknown()).optional(),
295
+ expect: FlowExpectSchema.optional(),
296
+ degraded: z.boolean().optional()
297
+ });
298
+ var FlowStepSchema = baseFlowStep.extend({
299
+ steps: z.lazy(() => z.array(FlowStepSchema).optional())
300
+ });
301
+ var FlowFileSchema = z.object({
302
+ version: z.literal(FLOW_FILE_VERSION),
303
+ name: z.string(),
304
+ // FUTURE: fixtures/preconditions — schema slot reserved, unpopulated this cut. The recorder
305
+ // never writes it and no fixture runner exists.
306
+ fixture: z.string().optional(),
307
+ /** From the injected clock (ms) — deterministic in tests, byte-stable on disk. */
308
+ createdAt: z.number(),
309
+ steps: z.array(FlowStepSchema),
310
+ success: FlowExpectSchema.optional(),
311
+ /**
312
+ * Anchors whose CONTENT must not be asserted (e.g. LLM output). Replay asserts
313
+ * presence, not words. Compiled from a `mark-dynamic` annotation.
314
+ */
315
+ dynamic: z.array(FlowAnchorSchema).optional()
316
+ });
317
+ var RecordedFlowSchema = z.object({
318
+ name: z.string(),
319
+ flow: FlowFileSchema
320
+ });
321
+ var AnnotationSchema = z.discriminatedUnion("kind", [
322
+ z.object({
323
+ kind: z.literal(AnnotationKind.ASSERT_SIGNAL),
324
+ name: z.string().min(1),
325
+ dataMatches: z.record(z.unknown()).optional()
326
+ }),
327
+ z.object({
328
+ kind: z.literal(AnnotationKind.ASSERT_VISIBLE),
329
+ testid: z.string().min(1)
330
+ }),
331
+ z.object({
332
+ kind: z.literal(AnnotationKind.MARK_DYNAMIC),
333
+ testid: z.string().min(1)
334
+ }),
335
+ z.object({
336
+ kind: z.literal(AnnotationKind.SUCCESS_STATE),
337
+ signal: z.string().min(1).optional(),
338
+ testid: z.string().min(1).optional()
339
+ })
340
+ ]);
341
+
342
+ // ../browser/dist/dom/refs.js
343
+ var RefRegistry = class {
344
+ #toRef = /* @__PURE__ */ new WeakMap();
345
+ #fromRef = /* @__PURE__ */ new Map();
346
+ #seq = 0;
347
+ /** Get the existing ref for an element, or mint a new one. */
348
+ refFor(el) {
349
+ const existing = this.#toRef.get(el);
350
+ if (existing !== void 0)
351
+ return existing;
352
+ this.#seq += 1;
353
+ const ref = `e${String(this.#seq)}`;
354
+ this.#toRef.set(el, ref);
355
+ this.#fromRef.set(ref, new WeakRef(el));
356
+ return ref;
357
+ }
358
+ /** Resolve a ref back to its element, or null if it's gone/detached. */
359
+ resolve(ref) {
360
+ const weak = this.#fromRef.get(ref);
361
+ if (weak === void 0)
362
+ return null;
363
+ const el = weak.deref();
364
+ if (el === void 0 || !el.isConnected) {
365
+ this.#fromRef.delete(ref);
366
+ return null;
367
+ }
368
+ return el;
369
+ }
370
+ };
371
+ var refs = new RefRegistry();
372
+
373
+ // ../browser/dist/dom/a11y.js
374
+ var NAME_FROM_CONTENT = /* @__PURE__ */ new Set([
375
+ "button",
376
+ "link",
377
+ "heading",
378
+ "option",
379
+ "listitem",
380
+ "cell",
381
+ "columnheader",
382
+ "rowheader",
383
+ "tab",
384
+ "menuitem",
385
+ "menuitemcheckbox",
386
+ "menuitemradio",
387
+ "treeitem",
388
+ "gridcell",
389
+ "switch",
390
+ "status",
391
+ "alert"
392
+ ]);
393
+ var INPUT_TEXT_TYPES = /* @__PURE__ */ new Set(["text", "email", "tel", "url", "search", "password", ""]);
394
+ function inputRole(input) {
395
+ const type = input.type.toLowerCase();
396
+ if (INPUT_TEXT_TYPES.has(type))
397
+ return "textbox";
398
+ if (type === "checkbox")
399
+ return "checkbox";
400
+ if (type === "radio")
401
+ return "radio";
402
+ if (type === "range")
403
+ return "slider";
404
+ if (type === "number")
405
+ return "spinbutton";
406
+ if (type === "submit" || type === "button" || type === "reset")
407
+ return "button";
408
+ return "textbox";
409
+ }
410
+ function getRole(el) {
411
+ const explicit = el.getAttribute("role");
412
+ if (explicit !== null && explicit.trim().length > 0)
413
+ return explicit.trim();
414
+ const tag = el.tagName.toLowerCase();
415
+ switch (tag) {
416
+ case "a":
417
+ return el.hasAttribute("href") ? "link" : "generic";
418
+ case "button":
419
+ return "button";
420
+ case "input":
421
+ return inputRole(el);
422
+ case "textarea":
423
+ return "textbox";
424
+ case "select":
425
+ return el.multiple ? "listbox" : "combobox";
426
+ case "h1":
427
+ case "h2":
428
+ case "h3":
429
+ case "h4":
430
+ case "h5":
431
+ case "h6":
432
+ return "heading";
433
+ case "ul":
434
+ case "ol":
435
+ return "list";
436
+ case "li":
437
+ return "listitem";
438
+ case "nav":
439
+ return "navigation";
440
+ case "main":
441
+ return "main";
442
+ case "aside":
443
+ return "complementary";
444
+ case "dialog":
445
+ return "dialog";
446
+ case "img":
447
+ return "img";
448
+ case "table":
449
+ return "table";
450
+ case "form":
451
+ return "form";
452
+ case "p":
453
+ return "paragraph";
454
+ case "header":
455
+ return "banner";
456
+ case "footer":
457
+ return "contentinfo";
458
+ default:
459
+ return "generic";
460
+ }
461
+ }
462
+ function collapse(text) {
463
+ return text.replace(/\s+/g, " ").trim();
464
+ }
465
+ function labelledByText(el) {
466
+ const ids = el.getAttribute("aria-labelledby");
467
+ if (ids === null)
468
+ return null;
469
+ const parts = [];
470
+ for (const id of ids.split(/\s+/)) {
471
+ const ref = el.ownerDocument.getElementById(id);
472
+ if (ref !== null)
473
+ parts.push(collapse(ref.textContent ?? ""));
474
+ }
475
+ const joined = parts.join(" ").trim();
476
+ return joined.length > 0 ? joined : null;
477
+ }
478
+ function getAccessibleName(el) {
479
+ const labelled = labelledByText(el);
480
+ if (labelled !== null)
481
+ return labelled;
482
+ const ariaLabel = el.getAttribute("aria-label");
483
+ if (ariaLabel !== null && ariaLabel.trim().length > 0)
484
+ return ariaLabel.trim();
485
+ if (el instanceof HTMLImageElement) {
486
+ const alt = el.getAttribute("alt");
487
+ if (alt !== null)
488
+ return alt.trim();
489
+ }
490
+ if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement) {
491
+ const labels = el.labels;
492
+ if (labels !== null && labels.length > 0) {
493
+ const text = [...labels].map((l) => collapse(l.textContent ?? "")).join(" ").trim();
494
+ if (text.length > 0)
495
+ return text;
496
+ }
497
+ if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
498
+ const placeholder = el.getAttribute("placeholder");
499
+ if (placeholder !== null && placeholder.trim().length > 0)
500
+ return placeholder.trim();
501
+ }
502
+ }
503
+ if (NAME_FROM_CONTENT.has(getRole(el))) {
504
+ const text = collapse(el.textContent ?? "");
505
+ if (text.length > 0)
506
+ return text;
507
+ }
508
+ const title = el.getAttribute("title");
509
+ if (title !== null && title.trim().length > 0)
510
+ return title.trim();
511
+ return "";
512
+ }
513
+ function ariaBool(el, attr) {
514
+ const value = el.getAttribute(attr);
515
+ if (value === null)
516
+ return void 0;
517
+ return value === "true";
518
+ }
519
+ function getStates(el) {
520
+ const states = [ElementState.PRESENT];
521
+ if (isVisible(el))
522
+ states.push(ElementState.VISIBLE);
523
+ else
524
+ states.push(ElementState.HIDDEN);
525
+ const disabledProp = (el instanceof HTMLButtonElement || el instanceof HTMLInputElement || el instanceof HTMLSelectElement || el instanceof HTMLTextAreaElement) && el.disabled;
526
+ const disabled = disabledProp || ariaBool(el, "aria-disabled") === true;
527
+ states.push(disabled ? ElementState.DISABLED : ElementState.ENABLED);
528
+ const checkedProp = el instanceof HTMLInputElement && (el.type === "checkbox" || el.type === "radio") && el.checked;
529
+ if (checkedProp || ariaBool(el, "aria-checked") === true)
530
+ states.push(ElementState.CHECKED);
531
+ if (ariaBool(el, "aria-expanded") === true)
532
+ states.push(ElementState.EXPANDED);
533
+ if (el.ownerDocument.activeElement === el)
534
+ states.push(ElementState.FOCUSED);
535
+ return states;
536
+ }
537
+ function getValue(el) {
538
+ if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement) {
539
+ return el.value;
540
+ }
541
+ const valueNow = el.getAttribute("aria-valuenow");
542
+ return valueNow ?? void 0;
543
+ }
544
+ function isVisible(el) {
545
+ if (!el.isConnected)
546
+ return false;
547
+ let node = el;
548
+ while (node !== null) {
549
+ if (node.getAttribute("aria-hidden") === "true")
550
+ return false;
551
+ if (node instanceof HTMLElement && node.hidden)
552
+ return false;
553
+ const view = node.ownerDocument.defaultView;
554
+ if (view !== null) {
555
+ const style = view.getComputedStyle(node);
556
+ if (style.display === "none" || style.visibility === "hidden" || style.visibility === "collapse") {
557
+ return false;
558
+ }
559
+ if (Number.parseFloat(style.opacity || "1") === 0)
560
+ return false;
561
+ }
562
+ node = node.parentElement;
563
+ }
564
+ return true;
565
+ }
566
+ var MAX_TEXT = 80;
567
+ function getVisibleText(el) {
568
+ const text = collapse(el.textContent ?? "");
569
+ return text.length > MAX_TEXT ? `${text.slice(0, MAX_TEXT)}\u2026` : text;
570
+ }
571
+ function describe(el) {
572
+ const value = getValue(el);
573
+ const text = getVisibleText(el);
574
+ const name = getAccessibleName(el);
575
+ const base = {
576
+ ref: refs.refFor(el),
577
+ role: getRole(el),
578
+ name,
579
+ states: getStates(el),
580
+ visible: isVisible(el)
581
+ };
582
+ if (value !== void 0 && value.length > 0)
583
+ base.value = value;
584
+ if (text.length > 0 && text !== name)
585
+ base.text = text;
586
+ return base;
587
+ }
588
+
589
+ // ../browser/dist/dom/dom-ignore.js
590
+ var IRIS_OVERLAY = "[data-iris-overlay],[data-iris-cursor],[data-iris-hud],[data-iris-glow]";
591
+ var DEV_OVERLAYS = "[data-agentation],#__next-build-watcher,nextjs-portal,[data-nextjs-dialog],[data-nextjs-toast]";
592
+ var extraIgnore = "";
593
+ function setIgnoreSelectors(selectors) {
594
+ extraIgnore = selectors.join(",");
595
+ }
596
+ function isIrisOverlay(el) {
597
+ return el.closest(IRIS_OVERLAY) !== null;
598
+ }
599
+ function isIgnored(el) {
600
+ const sel = extraIgnore.length > 0 ? `${IRIS_OVERLAY},${DEV_OVERLAYS},${extraIgnore}` : `${IRIS_OVERLAY},${DEV_OVERLAYS}`;
601
+ return el.closest(sel) !== null;
602
+ }
603
+
604
+ // ../browser/dist/dom/snapshot.js
605
+ var INTERACTIVE = /* @__PURE__ */ new Set([
606
+ "button",
607
+ "link",
608
+ "textbox",
609
+ "checkbox",
610
+ "radio",
611
+ "combobox",
612
+ "listbox",
613
+ "slider",
614
+ "spinbutton",
615
+ "switch",
616
+ "tab",
617
+ "menuitem",
618
+ "option"
619
+ ]);
620
+ var SKIP_TAGS = /* @__PURE__ */ new Set(["script", "style", "noscript", "template", "head", "meta", "link"]);
621
+ function skip(el) {
622
+ if (SKIP_TAGS.has(el.tagName.toLowerCase()))
623
+ return true;
624
+ if (isIgnored(el))
625
+ return true;
626
+ if (el.getAttribute("aria-hidden") === "true")
627
+ return true;
628
+ if (el instanceof HTMLElement && el.hidden)
629
+ return true;
630
+ const view = el.ownerDocument.defaultView;
631
+ if (view !== null) {
632
+ const style = view.getComputedStyle(el);
633
+ if (style.display === "none")
634
+ return true;
635
+ }
636
+ return false;
637
+ }
638
+ function stateSuffix(el) {
639
+ const states = getStates(el).filter((s) => s === ElementState.DISABLED || s === ElementState.CHECKED || s === ElementState.EXPANDED || s === ElementState.FOCUSED);
640
+ return states.length > 0 ? ` [${states.join(",")}]` : "";
641
+ }
642
+ function formatLine(el, depth, role, name) {
643
+ const indent = " ".repeat(depth);
644
+ const value = getValue(el);
645
+ const namePart = name.length > 0 ? ` "${name}"` : "";
646
+ const refPart = INTERACTIVE.has(role) || name.length > 0 ? ` (ref=${refs.refFor(el)})` : "";
647
+ const valuePart = value !== void 0 && value.length > 0 ? ` [value="${value}"]` : "";
648
+ return `${indent}- ${role}${namePart}${refPart}${valuePart}${stateSuffix(el)}`;
649
+ }
650
+ function walk(parent, depth, ctx) {
651
+ if (depth > ctx.maxDepth)
652
+ return;
653
+ for (const child of parent.children) {
654
+ if (ctx.nodes >= ctx.maxNodes) {
655
+ ctx.truncated = true;
656
+ return;
657
+ }
658
+ if (skip(child))
659
+ continue;
660
+ const role = getRole(child);
661
+ const name = getAccessibleName(child);
662
+ const interactive = INTERACTIVE.has(role);
663
+ const meaningful = interactive || role !== "generic" || name.length > 0;
664
+ const include = ctx.mode === SnapshotMode.INTERACTIVE ? interactive : meaningful;
665
+ if (include) {
666
+ ctx.nodes += 1;
667
+ ctx.lines.push(formatLine(child, depth, role, name));
668
+ walk(child, depth + 1, ctx);
669
+ } else {
670
+ walk(child, depth, ctx);
671
+ }
672
+ }
673
+ }
674
+ function collectDialogs(root) {
675
+ const nodes = root.querySelectorAll('[role="dialog"], dialog[open], [aria-modal="true"]');
676
+ const names = [];
677
+ for (const node of nodes) {
678
+ if (isVisible(node))
679
+ names.push(getAccessibleName(node) || "(unnamed dialog)");
680
+ }
681
+ return names;
682
+ }
683
+ function buildStatus(root) {
684
+ return {
685
+ route: `${location.pathname}${location.search}${location.hash}`,
686
+ title: document.title,
687
+ visibleDialogs: collectDialogs(root)
688
+ };
689
+ }
690
+ function buildSnapshot(options = {}) {
691
+ const mode = options.mode ?? SnapshotMode.FULL;
692
+ const scopeEl = options.scope !== void 0 ? refs.resolve(options.scope) ?? document.querySelector(options.scope) : document.body;
693
+ const root = scopeEl ?? document.body;
694
+ const status = buildStatus(root);
695
+ if (mode === SnapshotMode.STATUS) {
696
+ return { tree: "", status, nodes: 0, truncated: false };
697
+ }
698
+ const ctx = {
699
+ lines: [],
700
+ nodes: 0,
701
+ truncated: false,
702
+ mode,
703
+ maxNodes: options.maxNodes ?? 400,
704
+ maxDepth: options.maxDepth ?? 20
705
+ };
706
+ walk(root, 0, ctx);
707
+ return {
708
+ tree: ctx.lines.join("\n"),
709
+ status,
710
+ nodes: ctx.nodes,
711
+ truncated: ctx.truncated
712
+ };
713
+ }
714
+
715
+ // ../browser/dist/dom/query.js
716
+ import { queryAllByRole, queryAllByText, queryAllByLabelText, queryAllByPlaceholderText, queryAllByTestId, queryAllByAltText } from "@testing-library/dom";
717
+
718
+ // ../browser/dist/registry/capabilities.js
719
+ var globalStore = globalThis;
720
+ function empty() {
721
+ return { testids: [], signals: [], stores: [], flows: [] };
722
+ }
723
+ var capabilities = globalStore.__irisCapabilities ??= empty();
724
+ function mergeUnique(into, add) {
725
+ if (add === void 0)
726
+ return;
727
+ for (const v of add)
728
+ if (!into.includes(v))
729
+ into.push(v);
730
+ }
731
+ function registerCapabilities(input) {
732
+ mergeUnique(capabilities.testids, input.testids);
733
+ mergeUnique(capabilities.signals, input.signals);
734
+ mergeUnique(capabilities.stores, input.stores);
735
+ if (input.flows !== void 0) {
736
+ for (const flow of input.flows) {
737
+ const existing = capabilities.flows.find((f) => f.name === flow.name);
738
+ if (existing === void 0) {
739
+ capabilities.flows.push({ name: flow.name, steps: [...flow.steps] });
740
+ } else {
741
+ existing.steps = [...flow.steps];
742
+ }
743
+ }
744
+ }
745
+ }
746
+ function getCapabilities() {
747
+ return {
748
+ testids: [...capabilities.testids],
749
+ signals: [...capabilities.signals],
750
+ stores: [...capabilities.stores],
751
+ flows: capabilities.flows.map((f) => ({ name: f.name, steps: [...f.steps] }))
752
+ };
753
+ }
754
+ function hasCapabilities() {
755
+ return capabilities.testids.length > 0 || capabilities.signals.length > 0 || capabilities.stores.length > 0 || capabilities.flows.length > 0;
756
+ }
757
+
758
+ // ../browser/dist/dom/query.js
759
+ var TESTID_ATTR = "data-testid";
760
+ var MAX_PRESENT_TESTIDS = 12;
761
+ function resolveContainer(scope) {
762
+ const body = document.body;
763
+ if (scope === void 0)
764
+ return body;
765
+ const byRef = refs.resolve(scope);
766
+ if (byRef instanceof HTMLElement)
767
+ return byRef;
768
+ try {
769
+ const found = document.querySelector(scope);
770
+ if (found instanceof HTMLElement)
771
+ return found;
772
+ } catch {
773
+ }
774
+ return body;
775
+ }
776
+ function findCandidates(query) {
777
+ const container = resolveContainer(query.scope);
778
+ const by = query.by;
779
+ const value = query.value;
780
+ if (by !== void 0 && value !== void 0) {
781
+ switch (by) {
782
+ case QueryBy.ROLE:
783
+ return queryAllByRole(container, value, query.name !== void 0 ? { hidden: true, name: query.name } : { hidden: true });
784
+ case QueryBy.TEXT:
785
+ return queryAllByText(container, value, { exact: false });
786
+ case QueryBy.LABEL:
787
+ return queryAllByLabelText(container, value, { exact: false });
788
+ case QueryBy.PLACEHOLDER:
789
+ return queryAllByPlaceholderText(container, value, { exact: false });
790
+ case QueryBy.TESTID:
791
+ return queryAllByTestId(container, value, { exact: true });
792
+ case QueryBy.ALT:
793
+ return queryAllByAltText(container, value, { exact: false });
794
+ default:
795
+ return [];
796
+ }
797
+ }
798
+ if (query.role !== void 0) {
799
+ const options = query.name !== void 0 ? { hidden: true, name: query.name } : { hidden: true };
800
+ return queryAllByRole(container, query.role, options);
801
+ }
802
+ if (query.text !== void 0)
803
+ return queryAllByText(container, query.text, { exact: false });
804
+ if (query.label !== void 0) {
805
+ return queryAllByLabelText(container, query.label, { exact: false });
806
+ }
807
+ if (query.placeholder !== void 0) {
808
+ return queryAllByPlaceholderText(container, query.placeholder, { exact: false });
809
+ }
810
+ if (query.testid !== void 0)
811
+ return queryAllByTestId(container, query.testid, { exact: true });
812
+ if (query.alt !== void 0)
813
+ return queryAllByAltText(container, query.alt, { exact: false });
814
+ return [];
815
+ }
816
+ function inState(el, state) {
817
+ return getStates(el).includes(state);
818
+ }
819
+ function matchQuery(query, state) {
820
+ let elements;
821
+ try {
822
+ elements = findCandidates(query);
823
+ } catch {
824
+ elements = [];
825
+ }
826
+ const filtered = state === void 0 ? elements : elements.filter((el) => inState(el, state));
827
+ const descriptors = filtered.map((el) => describe(el));
828
+ return { matched: descriptors.length > 0, count: descriptors.length, elements: descriptors };
829
+ }
830
+ function buildPresentRegions(query) {
831
+ const container = resolveContainer(query.scope);
832
+ const regions = [];
833
+ const CONTAINER_ROLES = [
834
+ "list",
835
+ "listbox",
836
+ "grid",
837
+ "table",
838
+ "tree",
839
+ "treegrid",
840
+ "dialog",
841
+ "alertdialog",
842
+ "navigation",
843
+ "main",
844
+ "banner",
845
+ "form",
846
+ "search",
847
+ "menu",
848
+ "menubar",
849
+ "tablist"
850
+ ];
851
+ for (const role of CONTAINER_ROLES) {
852
+ let containers;
853
+ try {
854
+ containers = queryAllByRole(container, role, { hidden: true });
855
+ } catch {
856
+ continue;
857
+ }
858
+ for (const el of containers) {
859
+ const name = el.getAttribute("aria-label") ?? el.getAttribute("aria-labelledby") ?? el.getAttribute("data-testid") ?? void 0;
860
+ const children = el.querySelectorAll("[role]");
861
+ const sample = [];
862
+ for (const child of Array.from(children)) {
863
+ if (sample.length >= 3)
864
+ break;
865
+ const childRole = child.getAttribute("role");
866
+ const childName = child.getAttribute("aria-label") ?? child.getAttribute("data-testid") ?? child.textContent?.trim().slice(0, 40) ?? "";
867
+ if (childRole !== null && childName.length > 0) {
868
+ sample.push(`${childRole}[${childName}]`);
869
+ }
870
+ }
871
+ const region = { role, childCount: children.length, sample };
872
+ if (name !== void 0 && name.length > 0)
873
+ region.name = name;
874
+ regions.push(region);
875
+ if (regions.length >= 10)
876
+ return regions;
877
+ }
878
+ }
879
+ return regions;
880
+ }
881
+ function buildEmptyHint(query) {
882
+ const container = resolveContainer(query.scope);
883
+ const all = container.querySelectorAll(`[${TESTID_ATTR}]`);
884
+ const present = [];
885
+ for (const el of Array.from(all)) {
886
+ const id = el.getAttribute(TESTID_ATTR);
887
+ if (id !== null && id.length > 0 && !present.includes(id)) {
888
+ present.push(id);
889
+ if (present.length >= MAX_PRESENT_TESTIDS)
890
+ break;
891
+ }
892
+ }
893
+ const registered = getCapabilities().testids;
894
+ const knownEmptyState = present.some((id) => registered.includes(id));
895
+ const route = `${location.pathname}${location.search}`;
896
+ return {
897
+ route,
898
+ presentTestids: present,
899
+ presentRegions: buildPresentRegions(query),
900
+ knownEmptyState
901
+ };
902
+ }
903
+ function runQuery(query) {
904
+ const result2 = matchQuery(query);
905
+ if (result2.elements.length === 0) {
906
+ return { elements: result2.elements, hint: buildEmptyHint(query) };
907
+ }
908
+ return { elements: result2.elements };
909
+ }
910
+
911
+ // ../browser/dist/registry/adapters.js
912
+ var globalStore2 = globalThis;
913
+ var adapters = globalStore2.__irisAdapters ??= [];
914
+ function registerAdapter(adapter) {
915
+ if (!adapters.some((a) => a.name === adapter.name))
916
+ adapters.push(adapter);
917
+ }
918
+ function identifyComponent(el) {
919
+ for (const adapter of adapters) {
920
+ const info = adapter.identify(el);
921
+ if (info !== null)
922
+ return info;
923
+ }
924
+ return null;
925
+ }
926
+ function readComponentState(el) {
927
+ for (const adapter of adapters) {
928
+ if (adapter.readState === void 0)
929
+ continue;
930
+ const state = adapter.readState(el);
931
+ if (state !== void 0)
932
+ return state;
933
+ }
934
+ return void 0;
935
+ }
936
+ function elementHasHoverHandlers(el) {
937
+ for (const adapter of adapters) {
938
+ if (adapter.hasHoverHandlers === void 0)
939
+ continue;
940
+ if (adapter.hasHoverHandlers(el))
941
+ return true;
942
+ }
943
+ return false;
944
+ }
945
+ function adapterNames() {
946
+ return adapters.map((a) => a.name);
947
+ }
948
+
949
+ // ../browser/dist/timers/native-timers.js
950
+ var g = globalThis;
951
+ var realSetTimeout = typeof g.setTimeout === "function" ? g.setTimeout.bind(g) : null;
952
+ var realClearTimeout = typeof g.clearTimeout === "function" ? g.clearTimeout.bind(g) : null;
953
+ var realRaf = typeof g.requestAnimationFrame === "function" ? g.requestAnimationFrame.bind(g) : null;
954
+ var realPerfNow = typeof g.performance?.now === "function" ? g.performance.now.bind(g.performance) : null;
955
+ var nativeNow = () => realPerfNow ? realPerfNow() : 0;
956
+ var nativeSetTimeout = (cb, ms = 0) => realSetTimeout ? realSetTimeout(cb, ms) : 0;
957
+ var nativeClearTimeout = (id) => {
958
+ realClearTimeout?.(id);
959
+ };
960
+ var nativeSetInterval = (cb, ms) => {
961
+ let stopped = false;
962
+ let id = 0;
963
+ const tick = () => {
964
+ if (stopped)
965
+ return;
966
+ cb();
967
+ if (stopped)
968
+ return;
969
+ id = nativeSetTimeout(tick, ms);
970
+ };
971
+ id = nativeSetTimeout(tick, ms);
972
+ return () => {
973
+ stopped = true;
974
+ nativeClearTimeout(id);
975
+ };
976
+ };
977
+ var FRAME_BUDGET_MS = 200;
978
+ var boundedFrame = (budgetMs = FRAME_BUDGET_MS) => new Promise((resolve) => {
979
+ let done = false;
980
+ const finish = (settled) => {
981
+ if (done)
982
+ return;
983
+ done = true;
984
+ nativeClearTimeout(timer);
985
+ resolve({ settled });
986
+ };
987
+ const timer = nativeSetTimeout(() => finish(false), budgetMs);
988
+ if (realRaf)
989
+ realRaf(() => finish(true));
990
+ else
991
+ nativeSetTimeout(() => finish(true), 0);
992
+ });
993
+ var nativeFrame = () => boundedFrame().then(() => void 0);
994
+ var settle = async (budgetMs = FRAME_BUDGET_MS) => {
995
+ await Promise.resolve();
996
+ return boundedFrame(budgetMs);
997
+ };
998
+
999
+ // ../browser/dist/actions/actions.js
1000
+ function setNativeValue(el, value) {
1001
+ const proto = el instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
1002
+ const setter = Object.getOwnPropertyDescriptor(proto, "value")?.set;
1003
+ if (setter !== void 0) {
1004
+ setter.call(el, value);
1005
+ } else {
1006
+ el.value = value;
1007
+ }
1008
+ const notPrevented = el.dispatchEvent(new Event("input", { bubbles: true, cancelable: true }));
1009
+ el.dispatchEvent(new Event("change", { bubbles: true }));
1010
+ return !notPrevented;
1011
+ }
1012
+ function asString(value, fallback = "") {
1013
+ return typeof value === "string" ? value : fallback;
1014
+ }
1015
+ function requireElement(ref) {
1016
+ const el = refs.resolve(ref);
1017
+ if (el === null)
1018
+ throw new Error(`ref '${ref}' no longer resolves to an element`);
1019
+ if (!(el instanceof HTMLElement))
1020
+ throw new Error(`ref '${ref}' is not an HTMLElement`);
1021
+ return el;
1022
+ }
1023
+ var result = (ref, action, effect, settled, settleReason, warning) => {
1024
+ const testid = refs.resolve(ref)?.getAttribute("data-testid") ?? void 0;
1025
+ const base = {
1026
+ ok: true,
1027
+ ref,
1028
+ action,
1029
+ dispatched: true,
1030
+ settled,
1031
+ settleReason,
1032
+ effect
1033
+ };
1034
+ if (testid !== void 0)
1035
+ base.testid = testid;
1036
+ if (warning !== void 0)
1037
+ base.warning = warning;
1038
+ return base;
1039
+ };
1040
+ var FILL_LIKE = /* @__PURE__ */ new Set([ActionType.FILL, ActionType.TYPE, ActionType.CLEAR]);
1041
+ var isFillLike = (action) => FILL_LIKE.has(action);
1042
+ var CLICK_LIKE = /* @__PURE__ */ new Set([ActionType.CLICK, ActionType.DBLCLICK]);
1043
+ var NO_GEOMETRY = { occluded: false, occludedBy: null, scrolledIntoView: false };
1044
+ function fireClickSequence(el) {
1045
+ const doc = el.ownerDocument;
1046
+ const from = doc.activeElement ?? doc.body;
1047
+ firePointer(el, "pointerdown", from);
1048
+ el.dispatchEvent(new MouseEvent("mousedown", { bubbles: true, cancelable: true }));
1049
+ if (el.tabIndex >= 0 && typeof el.focus === "function")
1050
+ el.focus();
1051
+ firePointer(el, "pointerup", from);
1052
+ el.dispatchEvent(new MouseEvent("mouseup", { bubbles: true, cancelable: true }));
1053
+ const notPrevented = el.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true }));
1054
+ return !notPrevented;
1055
+ }
1056
+ function isMeasurable(rect) {
1057
+ return rect.width > 0 || rect.height > 0;
1058
+ }
1059
+ function isOffViewport(el, rect) {
1060
+ const win = el.ownerDocument.defaultView;
1061
+ if (win === null)
1062
+ return false;
1063
+ const cx = rect.left + rect.width / 2;
1064
+ const cy = rect.top + rect.height / 2;
1065
+ return cx < 0 || cy < 0 || cx > win.innerWidth || cy > win.innerHeight;
1066
+ }
1067
+ function hitTest(el, rect) {
1068
+ const doc = el.ownerDocument;
1069
+ if (typeof doc.elementFromPoint !== "function")
1070
+ return { occluded: false, occludedBy: null };
1071
+ const top = doc.elementFromPoint(rect.left + rect.width / 2, rect.top + rect.height / 2);
1072
+ if (top === null)
1073
+ return { occluded: false, occludedBy: null };
1074
+ const lands = top === el || el.contains(top) || top.contains(el);
1075
+ return lands ? { occluded: false, occludedBy: null } : { occluded: true, occludedBy: refs.refFor(top) };
1076
+ }
1077
+ function clickGeometry(el) {
1078
+ if (typeof el.getBoundingClientRect !== "function")
1079
+ return NO_GEOMETRY;
1080
+ let rect = el.getBoundingClientRect();
1081
+ if (!isMeasurable(rect))
1082
+ return NO_GEOMETRY;
1083
+ let scrolledIntoView = false;
1084
+ if (isOffViewport(el, rect) && typeof el.scrollIntoView === "function") {
1085
+ el.scrollIntoView({ block: "center", inline: "center" });
1086
+ scrolledIntoView = true;
1087
+ rect = el.getBoundingClientRect();
1088
+ }
1089
+ return { ...hitTest(el, rect), scrolledIntoView };
1090
+ }
1091
+ function enabledOf(el) {
1092
+ return !getStates(el).includes(ElementState.DISABLED);
1093
+ }
1094
+ function valueOf(el) {
1095
+ if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement) {
1096
+ return el.value;
1097
+ }
1098
+ return void 0;
1099
+ }
1100
+ function activeRef(el) {
1101
+ const active = el.ownerDocument.activeElement;
1102
+ if (active === null || active === el.ownerDocument.body)
1103
+ return null;
1104
+ return refs.refFor(active);
1105
+ }
1106
+ async function dispatchFor(el, action, args) {
1107
+ switch (action) {
1108
+ case ActionType.CLICK:
1109
+ return fireClickSequence(el);
1110
+ case ActionType.DBLCLICK:
1111
+ return !el.dispatchEvent(new MouseEvent("dblclick", { bubbles: true, cancelable: true }));
1112
+ case ActionType.HOVER: {
1113
+ const doc = el.ownerDocument;
1114
+ const from = doc.activeElement ?? doc.body;
1115
+ firePointer(el, "pointerover", from);
1116
+ el.dispatchEvent(new MouseEvent("mouseover", { bubbles: true, relatedTarget: from }));
1117
+ firePointerNonBubbling(el, "pointerenter", from);
1118
+ el.dispatchEvent(new MouseEvent("mouseenter", { relatedTarget: from }));
1119
+ firePointer(el, "pointermove", from);
1120
+ const moved = el.dispatchEvent(new MouseEvent("mousemove", { bubbles: true, cancelable: true }));
1121
+ const holdMs = typeof args["holdMs"] === "number" ? args["holdMs"] : 0;
1122
+ if (holdMs > 0)
1123
+ await sleep(holdMs);
1124
+ return !moved;
1125
+ }
1126
+ case ActionType.FOCUS:
1127
+ el.focus();
1128
+ el.dispatchEvent(new FocusEvent("focusin", { bubbles: true }));
1129
+ return false;
1130
+ // FocusEvents are not cancelable.
1131
+ case ActionType.BLUR:
1132
+ el.blur();
1133
+ el.dispatchEvent(new FocusEvent("blur"));
1134
+ el.dispatchEvent(new FocusEvent("focusout", { bubbles: true }));
1135
+ return false;
1136
+ case ActionType.FILL:
1137
+ if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
1138
+ el.focus();
1139
+ return setNativeValue(el, asString(args["value"]));
1140
+ }
1141
+ throw new Error(`cannot fill a <${el.tagName.toLowerCase()}>`);
1142
+ case ActionType.TYPE:
1143
+ if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
1144
+ el.focus();
1145
+ return setNativeValue(el, el.value + asString(args["text"]));
1146
+ }
1147
+ throw new Error(`cannot type into a <${el.tagName.toLowerCase()}>`);
1148
+ case ActionType.CLEAR:
1149
+ if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
1150
+ return setNativeValue(el, "");
1151
+ }
1152
+ return false;
1153
+ case ActionType.SELECT:
1154
+ if (el instanceof HTMLSelectElement) {
1155
+ el.value = asString(args["value"]);
1156
+ el.dispatchEvent(new Event("change", { bubbles: true }));
1157
+ return false;
1158
+ }
1159
+ throw new Error(`cannot select on a <${el.tagName.toLowerCase()}>`);
1160
+ case ActionType.CHECK:
1161
+ case ActionType.UNCHECK:
1162
+ if (el instanceof HTMLInputElement) {
1163
+ el.checked = action === ActionType.CHECK;
1164
+ el.dispatchEvent(new Event("change", { bubbles: true }));
1165
+ return false;
1166
+ }
1167
+ throw new Error(`cannot (un)check a <${el.tagName.toLowerCase()}>`);
1168
+ case ActionType.SUBMIT: {
1169
+ const form = el instanceof HTMLFormElement ? el : el.closest("form");
1170
+ if (form === null)
1171
+ throw new Error("no form to submit");
1172
+ form.requestSubmit();
1173
+ return false;
1174
+ }
1175
+ case ActionType.PRESS: {
1176
+ const key = asString(args["key"], "Enter");
1177
+ const down = el.dispatchEvent(new KeyboardEvent("keydown", { key, bubbles: true, cancelable: true }));
1178
+ el.dispatchEvent(new KeyboardEvent("keyup", { key, bubbles: true }));
1179
+ return !down;
1180
+ }
1181
+ case ActionType.SCROLL_INTO_VIEW:
1182
+ el.scrollIntoView();
1183
+ return false;
1184
+ case ActionType.UPLOAD: {
1185
+ if (!(el instanceof HTMLInputElement) || el.type !== "file") {
1186
+ throw new Error('upload target must be a <input type="file">');
1187
+ }
1188
+ const file = new File([asString(args["content"], "iris test file")], asString(args["name"], "file.txt"), {
1189
+ type: asString(args["type"], "text/plain")
1190
+ });
1191
+ const dt = new DataTransfer();
1192
+ dt.items.add(file);
1193
+ el.files = dt.files;
1194
+ const inputOk = el.dispatchEvent(new Event("input", { bubbles: true, cancelable: true }));
1195
+ el.dispatchEvent(new Event("change", { bubbles: true }));
1196
+ return !inputOk;
1197
+ }
1198
+ case ActionType.DRAG: {
1199
+ const toRef = asString(args["toRef"]);
1200
+ const resolved = toRef !== void 0 ? refs.resolve(toRef) : null;
1201
+ return await dragElement(el, resolved instanceof HTMLElement ? resolved : null, args["data"]);
1202
+ }
1203
+ default:
1204
+ throw new Error(`unknown action '${action}'`);
1205
+ }
1206
+ }
1207
+ async function executeAction(ref, action, args = {}) {
1208
+ const el = requireElement(ref);
1209
+ const visible = isVisible(el);
1210
+ const enabled = enabledOf(el);
1211
+ const prevFocus = activeRef(el);
1212
+ const valueBefore = valueOf(el);
1213
+ const geometry = CLICK_LIKE.has(action) ? clickGeometry(el) : NO_GEOMETRY;
1214
+ let mutated = 0;
1215
+ const obs = new MutationObserver((records) => {
1216
+ mutated += records.length;
1217
+ });
1218
+ obs.observe(el.ownerDocument.documentElement, {
1219
+ subtree: true,
1220
+ childList: true,
1221
+ attributes: true,
1222
+ characterData: true
1223
+ });
1224
+ let defaultPrevented = false;
1225
+ let settled = false;
1226
+ let settleReason = null;
1227
+ try {
1228
+ defaultPrevented = await dispatchFor(el, action, args);
1229
+ } finally {
1230
+ const outcome = await settle();
1231
+ settled = outcome.settled;
1232
+ settleReason = outcome.settled ? null : SettleReason.TIMEOUT;
1233
+ obs.disconnect();
1234
+ }
1235
+ const valueAfter = valueOf(el);
1236
+ const nextFocus = activeRef(el);
1237
+ const effect = {
1238
+ dispatched: true,
1239
+ targetMatched: el.isConnected,
1240
+ visible,
1241
+ enabled,
1242
+ defaultPrevented,
1243
+ focusMoved: prevFocus !== nextFocus ? `${prevFocus ?? "null"}->${nextFocus ?? "null"}` : null,
1244
+ valueChanged: isFillLike(action) ? valueBefore !== valueAfter : false,
1245
+ domMutatedWithin: mutated,
1246
+ occluded: geometry.occluded,
1247
+ occludedBy: geometry.occludedBy,
1248
+ scrolledIntoView: geometry.scrolledIntoView
1249
+ };
1250
+ const warning = geometry.occluded ? ActionWarning.CLICK_OCCLUDED : action === ActionType.HOVER && elementHasHoverHandlers(el) ? ActionWarning.HOVER_NATIVE_ENTER_LEAVE : void 0;
1251
+ return result(ref, action, effect, settled, settleReason, warning);
1252
+ }
1253
+ var sleep = (ms) => new Promise((r) => nativeSetTimeout(r, ms));
1254
+ function firePointer(el, type, relatedTarget = null) {
1255
+ if (typeof PointerEvent === "function") {
1256
+ el.dispatchEvent(new PointerEvent(type, { bubbles: true, cancelable: true, relatedTarget }));
1257
+ } else {
1258
+ el.dispatchEvent(new MouseEvent(type, { bubbles: true, cancelable: true, relatedTarget }));
1259
+ }
1260
+ }
1261
+ function firePointerNonBubbling(el, type, relatedTarget = null) {
1262
+ if (typeof PointerEvent === "function") {
1263
+ el.dispatchEvent(new PointerEvent(type, { bubbles: false, cancelable: true, relatedTarget }));
1264
+ } else {
1265
+ el.dispatchEvent(new MouseEvent(type, { bubbles: false, cancelable: true, relatedTarget }));
1266
+ }
1267
+ }
1268
+ function makeDataTransfer(data) {
1269
+ if (typeof DataTransfer !== "function")
1270
+ return null;
1271
+ const dt = new DataTransfer();
1272
+ const entries = Array.isArray(data) ? data : data !== void 0 ? [data] : [];
1273
+ for (const entry of entries) {
1274
+ if (typeof entry === "object" && entry !== null) {
1275
+ const e = entry;
1276
+ if (typeof e.mime === "string" && typeof e.value === "string")
1277
+ dt.setData(e.mime, e.value);
1278
+ }
1279
+ }
1280
+ return dt;
1281
+ }
1282
+ async function dragElement(source, target, data) {
1283
+ const dest = target ?? source;
1284
+ const fire = (el, type) => {
1285
+ if (typeof PointerEvent === "function" && type.startsWith("pointer")) {
1286
+ el.dispatchEvent(new PointerEvent(type, { bubbles: true, cancelable: true }));
1287
+ } else {
1288
+ el.dispatchEvent(new MouseEvent(type, { bubbles: true, cancelable: true }));
1289
+ }
1290
+ };
1291
+ fire(source, "pointerdown");
1292
+ fire(source, "mousedown");
1293
+ await nativeFrame();
1294
+ fire(dest, "pointermove");
1295
+ fire(dest, "mousemove");
1296
+ await nativeFrame();
1297
+ fire(dest, "pointerup");
1298
+ fire(dest, "mouseup");
1299
+ let dropPrevented = false;
1300
+ if (typeof DragEvent === "function") {
1301
+ const dataTransfer = makeDataTransfer(data);
1302
+ const init = { bubbles: true, cancelable: true };
1303
+ if (dataTransfer !== null)
1304
+ init.dataTransfer = dataTransfer;
1305
+ source.dispatchEvent(new DragEvent("dragstart", init));
1306
+ await nativeFrame();
1307
+ dest.dispatchEvent(new DragEvent("dragenter", init));
1308
+ dest.dispatchEvent(new DragEvent("dragover", init));
1309
+ await nativeFrame();
1310
+ dropPrevented = !dest.dispatchEvent(new DragEvent("drop", init));
1311
+ source.dispatchEvent(new DragEvent("dragend", init));
1312
+ }
1313
+ return dropPrevented;
1314
+ }
1315
+ async function dispatchWebMcp(tool, params) {
1316
+ const mc = navigator.modelContext;
1317
+ if (mc === void 0 || typeof mc.callTool !== "function") {
1318
+ throw new Error("WebMCP (navigator.modelContext) not available on this page");
1319
+ }
1320
+ return await mc.callTool(tool, params);
1321
+ }
1322
+ async function executeSequence(steps) {
1323
+ const effects = [];
1324
+ const stepResults = [];
1325
+ for (const step of steps) {
1326
+ const res = await executeAction(step.ref, step.action, step.args ?? {});
1327
+ effects.push(res.effect);
1328
+ const stepBase = {
1329
+ ref: res.ref,
1330
+ action: res.action,
1331
+ dispatched: res.dispatched,
1332
+ settled: res.settled,
1333
+ settleReason: res.settleReason
1334
+ };
1335
+ if (res.testid !== void 0)
1336
+ stepBase.testid = res.testid;
1337
+ if (res.warning !== void 0)
1338
+ stepBase.warning = res.warning;
1339
+ stepResults.push(stepBase);
1340
+ }
1341
+ return { ok: true, count: steps.length, effects, steps: stepResults };
1342
+ }
1343
+
1344
+ // ../browser/dist/registry/stores.js
1345
+ var globalStore3 = globalThis;
1346
+ var stores = globalStore3.__irisStores ??= /* @__PURE__ */ new Map();
1347
+ function registerStore(name, getter) {
1348
+ stores.set(name, getter);
1349
+ }
1350
+ function unregisterStore(name) {
1351
+ stores.delete(name);
1352
+ }
1353
+ function storeNames() {
1354
+ return [...stores.keys()];
1355
+ }
1356
+ function readStores(only) {
1357
+ const out = {};
1358
+ for (const [name, getter] of stores) {
1359
+ if (only !== void 0 && name !== only)
1360
+ continue;
1361
+ try {
1362
+ out[name] = getter();
1363
+ } catch (error) {
1364
+ out[name] = { __error: error instanceof Error ? error.message : String(error) };
1365
+ }
1366
+ }
1367
+ return out;
1368
+ }
1369
+
1370
+ // ../browser/dist/timers/clock.js
1371
+ var installed = false;
1372
+ var virtualNow = 0;
1373
+ var realBase = 0;
1374
+ var seq = 1;
1375
+ var tasks = [];
1376
+ var originals = null;
1377
+ function isClockFrozen() {
1378
+ return installed;
1379
+ }
1380
+ function freezeClock() {
1381
+ if (installed || typeof window === "undefined")
1382
+ return;
1383
+ installed = true;
1384
+ virtualNow = 0;
1385
+ realBase = Date.now();
1386
+ originals = {
1387
+ setTimeout: window.setTimeout,
1388
+ clearTimeout: window.clearTimeout,
1389
+ setInterval: window.setInterval,
1390
+ clearInterval: window.clearInterval,
1391
+ dateNow: Date.now
1392
+ };
1393
+ const schedule = (cb, delay, interval) => {
1394
+ const id = seq;
1395
+ seq += 1;
1396
+ tasks.push({ id, time: virtualNow + Math.max(0, delay), cb, interval });
1397
+ return id;
1398
+ };
1399
+ const cancel = (id) => {
1400
+ tasks = tasks.filter((t) => t.id !== id);
1401
+ };
1402
+ window.setTimeout = ((cb, delay = 0) => schedule(cb, delay));
1403
+ window.clearTimeout = ((id) => cancel(id));
1404
+ window.setInterval = ((cb, delay = 0) => schedule(cb, delay, Math.max(1, delay)));
1405
+ window.clearInterval = ((id) => cancel(id));
1406
+ Date.now = () => realBase + virtualNow;
1407
+ }
1408
+ function advanceClock(ms) {
1409
+ if (!installed)
1410
+ return;
1411
+ const target = virtualNow + Math.max(0, ms);
1412
+ let guard = 0;
1413
+ for (; ; ) {
1414
+ guard += 1;
1415
+ if (guard > 1e5)
1416
+ break;
1417
+ const due = tasks.filter((t) => t.time <= target).sort((a, b) => a.time - b.time);
1418
+ const next = due[0];
1419
+ if (next === void 0)
1420
+ break;
1421
+ tasks = tasks.filter((t) => t !== next);
1422
+ virtualNow = next.time;
1423
+ next.cb();
1424
+ if (next.interval !== void 0) {
1425
+ tasks.push({ ...next, id: seq++, time: virtualNow + next.interval });
1426
+ }
1427
+ }
1428
+ virtualNow = target;
1429
+ }
1430
+ function resetClock() {
1431
+ if (!installed || originals === null)
1432
+ return;
1433
+ window.setTimeout = originals.setTimeout;
1434
+ window.clearTimeout = originals.clearTimeout;
1435
+ window.setInterval = originals.setInterval;
1436
+ window.clearInterval = originals.clearInterval;
1437
+ Date.now = originals.dateNow;
1438
+ originals = null;
1439
+ tasks = [];
1440
+ installed = false;
1441
+ virtualNow = 0;
1442
+ }
1443
+
1444
+ // ../browser/dist/actions/scroll.js
1445
+ var FALLBACK_STEP_PX = 400;
1446
+ var VIEWPORT_FRACTION = 0.8;
1447
+ function nearestScrollable(el) {
1448
+ let cur = el;
1449
+ while (cur !== null && cur !== document.body) {
1450
+ const oy = getComputedStyle(cur).overflowY;
1451
+ if ((oy === "auto" || oy === "scroll") && cur.scrollHeight > cur.clientHeight)
1452
+ return cur;
1453
+ cur = cur.parentElement;
1454
+ }
1455
+ return document.scrollingElement ?? document.documentElement;
1456
+ }
1457
+ function scrollContainer(ref, dy, fraction) {
1458
+ const base = ref !== void 0 ? refs.resolve(ref) : null;
1459
+ const target = base instanceof Element ? nearestScrollable(base) : document.scrollingElement ?? document.documentElement;
1460
+ const before = target.scrollTop;
1461
+ if (fraction !== void 0 && fraction >= 0 && fraction <= 1) {
1462
+ target.scrollTop = Math.round(target.scrollHeight * fraction);
1463
+ } else {
1464
+ const step = dy ?? (Math.round(target.clientHeight * VIEWPORT_FRACTION) || FALLBACK_STEP_PX);
1465
+ target.scrollTop = before + step;
1466
+ }
1467
+ target.dispatchEvent(new Event("scroll", { bubbles: false }));
1468
+ const after = target.scrollTop;
1469
+ return {
1470
+ scrolled: after !== before,
1471
+ scrollTop: after,
1472
+ scrollHeight: target.scrollHeight,
1473
+ clientHeight: target.clientHeight,
1474
+ atEnd: after + target.clientHeight >= target.scrollHeight - 1
1475
+ };
1476
+ }
1477
+
1478
+ // ../browser/dist/commands/commands.js
1479
+ function str(value) {
1480
+ return typeof value === "string" ? value : void 0;
1481
+ }
1482
+ function record(value) {
1483
+ return typeof value === "object" && value !== null ? value : {};
1484
+ }
1485
+ function queryFromArgs(args) {
1486
+ return {
1487
+ by: str(args["by"]),
1488
+ value: str(args["value"]),
1489
+ role: str(args["role"]),
1490
+ name: str(args["name"]),
1491
+ text: str(args["text"]),
1492
+ label: str(args["label"]),
1493
+ placeholder: str(args["placeholder"]),
1494
+ testid: str(args["testid"]),
1495
+ alt: str(args["alt"]),
1496
+ scope: str(args["scope"])
1497
+ };
1498
+ }
1499
+ function inspect(ref) {
1500
+ const el = refs.resolve(ref);
1501
+ if (el === null)
1502
+ return { error: `ref '${ref}' no longer resolves` };
1503
+ const rect = el.getBoundingClientRect();
1504
+ const component = identifyComponent(el);
1505
+ const view = el.ownerDocument.defaultView;
1506
+ const cs = view !== null ? view.getComputedStyle(el) : null;
1507
+ const styles = cs !== null ? { color: cs.color, backgroundColor: cs.backgroundColor, opacity: cs.opacity } : null;
1508
+ return {
1509
+ ...describe(el),
1510
+ tag: el.tagName.toLowerCase(),
1511
+ box: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
1512
+ styles,
1513
+ component
1514
+ };
1515
+ }
1516
+ function isComponentStateResult(value) {
1517
+ return typeof value === "object" && value !== null && "ok" in value && typeof value.ok === "boolean";
1518
+ }
1519
+ var COMPONENT_UNAVAILABLE = {
1520
+ ok: false,
1521
+ reason: ComponentStateReason.UNAVAILABLE
1522
+ };
1523
+ function readState(ref, store) {
1524
+ const stores2 = readStores(store);
1525
+ const result2 = {
1526
+ stores: stores2,
1527
+ storeNames: storeNames()
1528
+ };
1529
+ if (ref !== void 0 && ref.length > 0) {
1530
+ const el = refs.resolve(ref);
1531
+ if (el === null) {
1532
+ result2.component = COMPONENT_UNAVAILABLE;
1533
+ } else {
1534
+ const state = readComponentState(el);
1535
+ result2.component = isComponentStateResult(state) ? state : COMPONENT_UNAVAILABLE;
1536
+ }
1537
+ }
1538
+ return result2;
1539
+ }
1540
+ function listAnimations() {
1541
+ const doc = document;
1542
+ if (typeof doc.getAnimations !== "function")
1543
+ return { animations: [] };
1544
+ const animations = doc.getAnimations().map((a) => {
1545
+ const effect = a.effect;
1546
+ const timing = effect?.getTiming();
1547
+ return {
1548
+ playState: a.playState,
1549
+ currentTime: a.currentTime,
1550
+ duration: timing?.duration
1551
+ };
1552
+ });
1553
+ return { animations };
1554
+ }
1555
+ function createCommandRegistry() {
1556
+ const reg = /* @__PURE__ */ new Map();
1557
+ reg.set(IrisCommand.SNAPSHOT, (args) => buildSnapshot({
1558
+ scope: str(args["scope"]),
1559
+ mode: str(args["mode"]) ?? SnapshotMode.FULL
1560
+ }));
1561
+ reg.set(IrisCommand.QUERY, (args) => runQuery(queryFromArgs(args)));
1562
+ reg.set(IrisCommand.MATCH, (args) => matchQuery(ElementQuerySchema.parse(record(args["query"])), str(args["state"])));
1563
+ reg.set(IrisCommand.ACT, (args) => {
1564
+ const action = str(args["action"]) ?? "";
1565
+ if (action === ActionType.WEBMCP) {
1566
+ const inner = record(args["args"]);
1567
+ return dispatchWebMcp(str(inner["tool"]) ?? "", record(inner["params"]));
1568
+ }
1569
+ return executeAction(str(args["ref"]) ?? "", action, record(args["args"]));
1570
+ });
1571
+ reg.set(IrisCommand.ACT_SEQUENCE, (args) => executeSequence(Array.isArray(args["steps"]) ? args["steps"] : []));
1572
+ reg.set(IrisCommand.INSPECT, (args) => inspect(str(args["ref"]) ?? ""));
1573
+ reg.set(IrisCommand.ANIMATIONS, () => listAnimations());
1574
+ reg.set(IrisCommand.CLOCK, (args) => {
1575
+ if (args["reset"] === true) {
1576
+ resetClock();
1577
+ } else {
1578
+ if (args["freeze"] === true)
1579
+ freezeClock();
1580
+ const adv = args["advanceMs"];
1581
+ if (typeof adv === "number")
1582
+ advanceClock(adv);
1583
+ }
1584
+ return { frozen: isClockFrozen() };
1585
+ });
1586
+ reg.set(IrisCommand.STATE_READ, (args) => readState(str(args["ref"]), str(args["store"])));
1587
+ reg.set(IrisCommand.CAPABILITIES, () => getCapabilities());
1588
+ reg.set(IrisCommand.SCROLL, (args) => {
1589
+ const dy = args["dy"];
1590
+ const fraction = args["fraction"];
1591
+ return scrollContainer(str(args["ref"]), typeof dy === "number" ? dy : void 0, typeof fraction === "number" ? fraction : void 0);
1592
+ });
1593
+ reg.set(IrisCommand.NAVIGATE, (args) => {
1594
+ const url = str(args["url"]);
1595
+ if (url === void 0 || url.length === 0)
1596
+ return { ok: false, reason: "url required" };
1597
+ window.location.assign(url);
1598
+ return { ok: true, url };
1599
+ });
1600
+ reg.set(IrisCommand.REFRESH, (args) => {
1601
+ if (args["hard"] === true) {
1602
+ const url = new URL(window.location.href);
1603
+ url.searchParams.set("_iris_reload", String(Date.now()));
1604
+ window.location.replace(url.toString());
1605
+ } else {
1606
+ window.location.reload();
1607
+ }
1608
+ return { ok: true };
1609
+ });
1610
+ return reg;
1611
+ }
1612
+
1613
+ // ../browser/dist/transport/transport.js
1614
+ var RECONNECT_DELAY_MS = 1e3;
1615
+ var MAX_QUEUE = 500;
1616
+ var Transport = class {
1617
+ #ws;
1618
+ #queue = [];
1619
+ #closed = false;
1620
+ /** When the current continuous outage began (nativeNow), or undefined while connected. */
1621
+ #disconnectedSince;
1622
+ /** Whether onConnectionLost has already fired for the current outage (fire-once). */
1623
+ #lost = false;
1624
+ #overflowCount = 0;
1625
+ #deps;
1626
+ #now;
1627
+ constructor(deps) {
1628
+ this.#deps = deps;
1629
+ this.#now = deps.now ?? nativeNow;
1630
+ }
1631
+ connect() {
1632
+ if (typeof WebSocket === "undefined")
1633
+ return;
1634
+ this.#closed = false;
1635
+ this.#open();
1636
+ }
1637
+ #open() {
1638
+ const ws = new WebSocket(this.#deps.url);
1639
+ this.#ws = ws;
1640
+ ws.onopen = () => {
1641
+ this.#disconnectedSince = void 0;
1642
+ this.#lost = false;
1643
+ ws.send(JSON.stringify(this.#deps.hello()));
1644
+ for (const msg of this.#queue)
1645
+ ws.send(msg);
1646
+ this.#queue = [];
1647
+ };
1648
+ ws.onmessage = (event) => {
1649
+ const data = event.data;
1650
+ void this.#onMessage(typeof data === "string" ? data : String(data));
1651
+ };
1652
+ ws.onclose = () => {
1653
+ this.#ws = void 0;
1654
+ this.#noteOutage();
1655
+ if (!this.#closed)
1656
+ nativeSetTimeout(() => this.#reopen(), RECONNECT_DELAY_MS);
1657
+ };
1658
+ ws.onerror = () => {
1659
+ ws.close();
1660
+ };
1661
+ }
1662
+ /** A scheduled reconnect — skipped if the SDK has since been torn down. */
1663
+ #reopen() {
1664
+ if (this.#closed)
1665
+ return;
1666
+ this.#open();
1667
+ }
1668
+ /**
1669
+ * Track how long the bridge has been unreachable. Once the outage exceeds BRIDGE_LOST_MS, fire
1670
+ * onConnectionLost exactly once so the SDK can end the session (the server/agent is gone and can
1671
+ * no longer push an end itself).
1672
+ */
1673
+ #noteOutage() {
1674
+ const at = this.#now();
1675
+ this.#disconnectedSince ??= at;
1676
+ if (!this.#lost && at - this.#disconnectedSince >= SESSION_LIFECYCLE.BRIDGE_LOST_MS) {
1677
+ this.#lost = true;
1678
+ this.#deps.onConnectionLost?.();
1679
+ }
1680
+ }
1681
+ async #onMessage(text) {
1682
+ let parsed;
1683
+ try {
1684
+ parsed = JSON.parse(text);
1685
+ } catch {
1686
+ return;
1687
+ }
1688
+ const msg = parsed;
1689
+ if (msg.kind !== MessageKind.COMMAND)
1690
+ return;
1691
+ const command = parsed;
1692
+ const outcome = await this.#deps.handleCommand(command);
1693
+ this.#sendRaw(JSON.stringify({
1694
+ kind: MessageKind.COMMAND_RESULT,
1695
+ id: command.id,
1696
+ ok: outcome.ok,
1697
+ result: outcome.result,
1698
+ error: outcome.error
1699
+ }));
1700
+ }
1701
+ sendEvent(event) {
1702
+ this.#sendRaw(JSON.stringify({ kind: MessageKind.EVENT, event }));
1703
+ }
1704
+ #sendRaw(text) {
1705
+ if (this.#ws !== void 0 && this.#ws.readyState === WebSocket.OPEN) {
1706
+ this.#ws.send(text);
1707
+ } else if (this.#queue.length < MAX_QUEUE) {
1708
+ this.#queue.push(text);
1709
+ } else {
1710
+ this.#overflowCount += 1;
1711
+ this.#deps.onOverflow?.(this.#overflowCount);
1712
+ }
1713
+ }
1714
+ close() {
1715
+ this.#closed = true;
1716
+ this.#ws?.close();
1717
+ this.#ws = void 0;
1718
+ }
1719
+ };
1720
+
1721
+ // ../browser/dist/observers/dom.js
1722
+ var WATCHED_ATTRS = [
1723
+ "class",
1724
+ "hidden",
1725
+ "disabled",
1726
+ "open",
1727
+ "aria-hidden",
1728
+ "aria-expanded",
1729
+ "aria-selected",
1730
+ "aria-checked",
1731
+ "data-state"
1732
+ ];
1733
+ var DIALOG_ROLES = /* @__PURE__ */ new Set(["dialog", "alertdialog"]);
1734
+ var LIVE_ROLES = /* @__PURE__ */ new Set(["alert", "status"]);
1735
+ var MAX_PER_BATCH = 40;
1736
+ function isMeaningful(role, name) {
1737
+ return role !== "generic" || name.length > 0;
1738
+ }
1739
+ function installDom(emit) {
1740
+ const observer = new MutationObserver((records) => {
1741
+ let added = 0;
1742
+ let removed = 0;
1743
+ let changed = 0;
1744
+ for (const record2 of records) {
1745
+ if (record2.type === "attributes") {
1746
+ const target = record2.target;
1747
+ if (target instanceof Element && record2.attributeName !== null && changed < MAX_PER_BATCH && !isIrisOverlay(target)) {
1748
+ changed += 1;
1749
+ emit(EventType.DOM_ATTR, { attr: record2.attributeName, value: target.getAttribute(record2.attributeName) }, refs.refFor(target));
1750
+ }
1751
+ continue;
1752
+ }
1753
+ if (record2.type === "characterData") {
1754
+ const parent = record2.target.parentElement;
1755
+ if (parent !== null && changed < MAX_PER_BATCH && !isIrisOverlay(parent)) {
1756
+ changed += 1;
1757
+ const text = (record2.target.textContent ?? "").trim().slice(0, 80);
1758
+ emit(EventType.DOM_TEXT, { text }, refs.refFor(parent));
1759
+ }
1760
+ continue;
1761
+ }
1762
+ for (const node of record2.addedNodes) {
1763
+ if (!(node instanceof Element) || added >= MAX_PER_BATCH)
1764
+ continue;
1765
+ if (isIrisOverlay(node))
1766
+ continue;
1767
+ const role = getRole(node);
1768
+ const name = getAccessibleName(node);
1769
+ if (!isMeaningful(role, name))
1770
+ continue;
1771
+ added += 1;
1772
+ const ref = refs.refFor(node);
1773
+ emit(EventType.DOM_ADDED, { role, name }, ref);
1774
+ if (DIALOG_ROLES.has(role) || LIVE_ROLES.has(role) || node.getAttribute("aria-modal") === "true") {
1775
+ if (isVisible(node))
1776
+ emit(EventType.VISIBLE_SHOWN, { role, name }, ref);
1777
+ }
1778
+ }
1779
+ for (const node of record2.removedNodes) {
1780
+ if (!(node instanceof Element) || removed >= MAX_PER_BATCH)
1781
+ continue;
1782
+ if (isIrisOverlay(node))
1783
+ continue;
1784
+ const role = getRole(node);
1785
+ const name = getAccessibleName(node);
1786
+ if (!isMeaningful(role, name))
1787
+ continue;
1788
+ removed += 1;
1789
+ emit(EventType.DOM_REMOVED, { role, name });
1790
+ }
1791
+ }
1792
+ });
1793
+ observer.observe(document.documentElement, {
1794
+ subtree: true,
1795
+ childList: true,
1796
+ attributes: true,
1797
+ attributeFilter: WATCHED_ATTRS,
1798
+ characterData: true
1799
+ });
1800
+ return () => {
1801
+ observer.disconnect();
1802
+ };
1803
+ }
1804
+
1805
+ // ../browser/dist/observers/network.js
1806
+ function urlOf(input) {
1807
+ if (typeof input === "string")
1808
+ return input;
1809
+ if (input instanceof URL)
1810
+ return input.href;
1811
+ return input.url;
1812
+ }
1813
+ function methodOf(input, init) {
1814
+ if (init?.method !== void 0)
1815
+ return init.method.toUpperCase();
1816
+ if (input instanceof Request)
1817
+ return input.method.toUpperCase();
1818
+ return "GET";
1819
+ }
1820
+ function installNetwork(emit) {
1821
+ const origFetch = window.fetch.bind(window);
1822
+ window.fetch = async (input, init) => {
1823
+ const start = performance.now();
1824
+ const method = methodOf(input, init);
1825
+ const url = urlOf(input);
1826
+ try {
1827
+ const res = await origFetch(input, init);
1828
+ emit(EventType.NET_REQUEST, {
1829
+ method,
1830
+ url,
1831
+ status: res.status,
1832
+ ok: res.ok,
1833
+ durationMs: Math.round(performance.now() - start),
1834
+ initiator: "fetch"
1835
+ });
1836
+ return res;
1837
+ } catch (error) {
1838
+ emit(EventType.NET_REQUEST, {
1839
+ method,
1840
+ url,
1841
+ status: 0,
1842
+ ok: false,
1843
+ error: error instanceof Error ? error.message : String(error),
1844
+ durationMs: Math.round(performance.now() - start),
1845
+ initiator: "fetch"
1846
+ });
1847
+ throw error;
1848
+ }
1849
+ };
1850
+ const meta = /* @__PURE__ */ new WeakMap();
1851
+ const proto = XMLHttpRequest.prototype;
1852
+ const origOpen = proto.open;
1853
+ const origSend = proto.send;
1854
+ const callOpen = origOpen;
1855
+ proto.open = function(method, url, ...rest) {
1856
+ meta.set(this, { method: method.toUpperCase(), url: String(url), start: 0 });
1857
+ callOpen.call(this, method, url, ...rest);
1858
+ };
1859
+ proto.send = function(body) {
1860
+ const m = meta.get(this);
1861
+ if (m !== void 0) {
1862
+ m.start = performance.now();
1863
+ this.addEventListener("loadend", () => {
1864
+ emit(EventType.NET_REQUEST, {
1865
+ method: m.method,
1866
+ url: m.url,
1867
+ status: this.status,
1868
+ ok: this.status >= 200 && this.status < 400,
1869
+ durationMs: Math.round(performance.now() - m.start),
1870
+ initiator: "xhr"
1871
+ });
1872
+ });
1873
+ }
1874
+ origSend.call(this, body ?? null);
1875
+ };
1876
+ return () => {
1877
+ window.fetch = origFetch;
1878
+ proto.open = origOpen;
1879
+ proto.send = origSend;
1880
+ };
1881
+ }
1882
+
1883
+ // ../browser/dist/observers/route.js
1884
+ function snapshotLocation() {
1885
+ return {
1886
+ pathname: location.pathname,
1887
+ search: location.search,
1888
+ hash: location.hash,
1889
+ href: location.href
1890
+ };
1891
+ }
1892
+ function installRoute(emit) {
1893
+ const origPush = history.pushState.bind(history);
1894
+ const origReplace = history.replaceState.bind(history);
1895
+ const fire = (from) => {
1896
+ const to = snapshotLocation();
1897
+ if (to.href === from)
1898
+ return;
1899
+ emit(EventType.ROUTE_CHANGE, {
1900
+ from,
1901
+ to: to.href,
1902
+ pathname: to.pathname,
1903
+ search: to.search,
1904
+ hash: to.hash
1905
+ });
1906
+ };
1907
+ history.pushState = (data, unused, url) => {
1908
+ const from = location.href;
1909
+ origPush(data, unused, url ?? null);
1910
+ fire(from);
1911
+ };
1912
+ history.replaceState = (data, unused, url) => {
1913
+ const from = location.href;
1914
+ origReplace(data, unused, url ?? null);
1915
+ fire(from);
1916
+ };
1917
+ let lastHref = location.href;
1918
+ const onNav = () => {
1919
+ fire(lastHref);
1920
+ lastHref = location.href;
1921
+ };
1922
+ window.addEventListener("popstate", onNav);
1923
+ window.addEventListener("hashchange", onNav);
1924
+ return () => {
1925
+ history.pushState = origPush;
1926
+ history.replaceState = origReplace;
1927
+ window.removeEventListener("popstate", onNav);
1928
+ window.removeEventListener("hashchange", onNav);
1929
+ };
1930
+ }
1931
+
1932
+ // ../browser/dist/observers/console.js
1933
+ var METHOD_EVENT = {
1934
+ log: EventType.CONSOLE_LOG,
1935
+ warn: EventType.CONSOLE_WARN,
1936
+ error: EventType.CONSOLE_ERROR
1937
+ };
1938
+ function stringifyArgs(args) {
1939
+ return args.map((a) => {
1940
+ if (typeof a === "string")
1941
+ return a;
1942
+ if (a instanceof Error)
1943
+ return a.message;
1944
+ try {
1945
+ return JSON.stringify(a);
1946
+ } catch {
1947
+ return String(a);
1948
+ }
1949
+ }).join(" ");
1950
+ }
1951
+ function installConsole(emit) {
1952
+ const methods = ["log", "warn", "error"];
1953
+ const originals2 = /* @__PURE__ */ new Map();
1954
+ for (const method of methods) {
1955
+ const original = console[method].bind(console);
1956
+ originals2.set(method, original);
1957
+ console[method] = (...args) => {
1958
+ emit(METHOD_EVENT[method], { message: stringifyArgs(args) });
1959
+ original(...args);
1960
+ };
1961
+ }
1962
+ const onError = (event) => {
1963
+ emit(EventType.ERROR_UNCAUGHT, {
1964
+ message: event.message,
1965
+ source: event.filename,
1966
+ line: event.lineno
1967
+ });
1968
+ };
1969
+ const onRejection = (event) => {
1970
+ const reason = event.reason;
1971
+ emit(EventType.ERROR_UNCAUGHT, {
1972
+ message: reason instanceof Error ? reason.message : String(reason),
1973
+ kind: "unhandledrejection"
1974
+ });
1975
+ };
1976
+ window.addEventListener("error", onError);
1977
+ window.addEventListener("unhandledrejection", onRejection);
1978
+ return () => {
1979
+ for (const [method, original] of originals2) {
1980
+ console[method] = original;
1981
+ }
1982
+ window.removeEventListener("error", onError);
1983
+ window.removeEventListener("unhandledrejection", onRejection);
1984
+ };
1985
+ }
1986
+
1987
+ // ../browser/dist/observers/animation.js
1988
+ function installAnimation(emit) {
1989
+ const onStart = (event) => {
1990
+ const target = event.target;
1991
+ if (target instanceof Element && !isIrisOverlay(target)) {
1992
+ emit(EventType.ANIM_START, { name: event.animationName }, refs.refFor(target));
1993
+ }
1994
+ };
1995
+ const onEnd = (event) => {
1996
+ const target = event.target;
1997
+ if (target instanceof Element && !isIrisOverlay(target)) {
1998
+ emit(EventType.ANIM_END, { name: event.animationName }, refs.refFor(target));
1999
+ }
2000
+ };
2001
+ const onTransitionEnd = (event) => {
2002
+ const target = event.target;
2003
+ if (target instanceof Element && !isIrisOverlay(target)) {
2004
+ emit(EventType.ANIM_END, { name: event.propertyName, kind: "transition" }, refs.refFor(target));
2005
+ }
2006
+ };
2007
+ document.addEventListener("animationstart", onStart, true);
2008
+ document.addEventListener("animationend", onEnd, true);
2009
+ document.addEventListener("transitionend", onTransitionEnd, true);
2010
+ return () => {
2011
+ document.removeEventListener("animationstart", onStart, true);
2012
+ document.removeEventListener("animationend", onEnd, true);
2013
+ document.removeEventListener("transitionend", onTransitionEnd, true);
2014
+ };
2015
+ }
2016
+
2017
+ // ../browser/dist/observers/scroll.js
2018
+ var THROTTLE_MS = 100;
2019
+ var REVEAL_SELECTOR = "[data-iris-reveal], [data-reveal], section";
2020
+ function installScroll(emit) {
2021
+ let lastEmit = 0;
2022
+ let lastY = 0;
2023
+ const onScroll = () => {
2024
+ const now = performance.now();
2025
+ if (now - lastEmit < THROTTLE_MS)
2026
+ return;
2027
+ lastEmit = now;
2028
+ const y = window.scrollY;
2029
+ const max = Math.max(1, document.documentElement.scrollHeight - window.innerHeight);
2030
+ emit(EventType.SCROLL_POSITION, {
2031
+ x: window.scrollX,
2032
+ y,
2033
+ percent: Math.round(y / max * 100),
2034
+ direction: y >= lastY ? "down" : "up"
2035
+ });
2036
+ lastY = y;
2037
+ };
2038
+ window.addEventListener("scroll", onScroll, { passive: true });
2039
+ let io;
2040
+ if (typeof IntersectionObserver === "function") {
2041
+ io = new IntersectionObserver((entries) => {
2042
+ for (const entry of entries) {
2043
+ if (entry.isIntersecting) {
2044
+ emit(EventType.REVEAL_SHOWN, { ratio: entry.intersectionRatio }, refs.refFor(entry.target));
2045
+ }
2046
+ }
2047
+ }, { threshold: 0.25 });
2048
+ for (const el of document.querySelectorAll(REVEAL_SELECTOR))
2049
+ io.observe(el);
2050
+ }
2051
+ return () => {
2052
+ window.removeEventListener("scroll", onScroll);
2053
+ io?.disconnect();
2054
+ };
2055
+ }
2056
+
2057
+ // ../browser/dist/observers/health.js
2058
+ function snapshotHealth() {
2059
+ return {
2060
+ hidden: document.visibilityState === "hidden",
2061
+ focused: document.hasFocus()
2062
+ };
2063
+ }
2064
+ function installHealth(emit) {
2065
+ const report = (reason) => {
2066
+ emit(EventType.PAGE_HEALTH, { ...snapshotHealth(), reason });
2067
+ };
2068
+ const onVisibility = () => report(HealthReason.VISIBILITY);
2069
+ const onFocus = () => report(HealthReason.FOCUS);
2070
+ const onBlur = () => report(HealthReason.BLUR);
2071
+ document.addEventListener("visibilitychange", onVisibility);
2072
+ window.addEventListener("focus", onFocus);
2073
+ window.addEventListener("blur", onBlur);
2074
+ report(HealthReason.INITIAL);
2075
+ const stopHeartbeat = nativeSetInterval(() => report(HealthReason.HEARTBEAT), SESSION_HEALTH.HEARTBEAT_MS);
2076
+ return () => {
2077
+ stopHeartbeat();
2078
+ document.removeEventListener("visibilitychange", onVisibility);
2079
+ window.removeEventListener("focus", onFocus);
2080
+ window.removeEventListener("blur", onBlur);
2081
+ };
2082
+ }
2083
+
2084
+ // ../browser/dist/presenter/overlay.js
2085
+ var STYLE = [
2086
+ "position:fixed",
2087
+ "bottom:8px",
2088
+ "right:8px",
2089
+ "z-index:2147483647",
2090
+ "font:11px ui-monospace,SFMono-Regular,Menlo,monospace",
2091
+ "background:#151823",
2092
+ "color:#e6e9f0",
2093
+ "border:1px solid #2a2f3d",
2094
+ "border-radius:8px",
2095
+ "padding:6px 10px",
2096
+ "pointer-events:none",
2097
+ "opacity:0.85"
2098
+ ].join(";");
2099
+ function installOverlay() {
2100
+ const el = document.createElement("div");
2101
+ el.setAttribute("data-iris-overlay", "");
2102
+ el.style.cssText = STYLE;
2103
+ el.textContent = "iris: connecting\u2026";
2104
+ document.body.appendChild(el);
2105
+ return {
2106
+ update: (stats) => {
2107
+ el.textContent = `iris ${stats.connected ? "\u25CF" : "\u25CB"} ${String(stats.events)} events`;
2108
+ },
2109
+ destroy: () => {
2110
+ el.remove();
2111
+ }
2112
+ };
2113
+ }
2114
+
2115
+ // ../browser/dist/presenter/presenter-verbs.js
2116
+ function actionVerb(action) {
2117
+ switch (action) {
2118
+ case "click":
2119
+ case "dblclick":
2120
+ return "Clicking";
2121
+ case "fill":
2122
+ case "type":
2123
+ return "Typing into";
2124
+ case "hover":
2125
+ return "Hovering";
2126
+ case "select":
2127
+ return "Selecting";
2128
+ case "submit":
2129
+ return "Submitting";
2130
+ case "check":
2131
+ case "uncheck":
2132
+ return "Toggling";
2133
+ case "upload":
2134
+ return "Uploading to";
2135
+ case "drag":
2136
+ return "Dragging";
2137
+ default:
2138
+ return action;
2139
+ }
2140
+ }
2141
+
2142
+ // ../browser/dist/presenter/presenter-log.js
2143
+ var DEFAULT_LOG_MAX = 50;
2144
+ var LOG_KIND = {
2145
+ READ: "read",
2146
+ ACT: "act",
2147
+ NARRATION: "narration",
2148
+ HUMAN: "human"
2149
+ };
2150
+ var HUMAN_ROW_PREFIX = "\u{1F9D1} you: ";
2151
+ var LOG_RESULT = { PASS: "pass", FAIL: "fail" };
2152
+ var LOG_CHIP = { read: "READ", act: "ACT", narration: "", human: "" };
2153
+ var CHIP_LABEL = {
2154
+ [PresenterMode.IDLE]: "",
2155
+ [PresenterMode.READING]: "READING",
2156
+ [PresenterMode.ACTING]: "ACTING"
2157
+ };
2158
+ var LOG_CHIP_MODE = {
2159
+ read: PresenterMode.READING,
2160
+ act: PresenterMode.ACTING,
2161
+ narration: PresenterMode.IDLE,
2162
+ human: PresenterMode.IDLE
2163
+ };
2164
+ var RESULT_GLYPH = { pass: "\u2713", fail: "\u2717" };
2165
+ var RESULT_CLASS = { pass: "iris-pass", fail: "iris-fail" };
2166
+ var DATA_IRIS_LOG = "data-iris-log";
2167
+ var DATA_IRIS_LOG_ROW = "data-iris-log-row";
2168
+ var DATA_IRIS_LOG_TS = "data-iris-log-ts";
2169
+ var DATA_KIND = "data-kind";
2170
+ var LOG_TEXT_CLASS = "iris-log-text";
2171
+ var LOG_RES_CLASS = "iris-res";
2172
+ var LOG_CHIP_CLASS = "iris-chip";
2173
+ var LOG_CSS = `
2174
+ [data-iris-log]{flex:1;min-height:0;overflow-y:auto;overscroll-behavior:contain;display:flex;flex-direction:column;
2175
+ gap:7px;padding:12px 14px;scrollbar-width:thin;scrollbar-color:rgba(255,255,255,.16) transparent;}
2176
+ [data-iris-log]::-webkit-scrollbar{width:9px;}
2177
+ [data-iris-log]::-webkit-scrollbar-thumb{background:rgba(255,255,255,.14);border-radius:9px;border:2px solid transparent;background-clip:content-box;}
2178
+ [data-iris-log]::-webkit-scrollbar-thumb:hover{background:rgba(255,255,255,.26);background-clip:content-box;}
2179
+ [data-iris-log-row]{display:flex;align-items:baseline;gap:8px;font-size:12px;line-height:1.45;
2180
+ animation:iris-row-in .26s cubic-bezier(.16,1,.3,1);}
2181
+ @keyframes iris-row-in{from{opacity:0;transform:translateY(5px);}to{opacity:1;transform:none;}}
2182
+ [data-iris-log-ts]{flex:none;color:var(--iris-faint);font-size:10px;font-variant-numeric:tabular-nums;padding-top:1px;}
2183
+ [data-iris-log] .iris-log-text{flex:1;min-width:0;color:#d6dae4;overflow-wrap:anywhere;word-break:break-word;}
2184
+ [data-iris-log] .iris-res{flex:none;font-weight:700;}
2185
+ [data-iris-log-row][data-kind="human"]{align-self:flex-end;max-width:88%;
2186
+ background:var(--iris-accent-soft);border:1px solid var(--iris-accent);border-radius:13px 13px 4px 13px;padding:6px 11px;}
2187
+ [data-iris-log-row][data-kind="human"] [data-iris-log-ts]{display:none;}
2188
+ [data-iris-log-row][data-kind="human"] .iris-log-text{color:var(--iris-fg);}
2189
+ `;
2190
+ function clampLogMax(n) {
2191
+ if (n === void 0 || !Number.isFinite(n) || n <= 0)
2192
+ return DEFAULT_LOG_MAX;
2193
+ return Math.floor(n);
2194
+ }
2195
+ function humanDuration(ms) {
2196
+ const s = Math.max(0, Math.floor(ms / 1e3));
2197
+ if (s < 60)
2198
+ return `${s}s`;
2199
+ const m = Math.floor(s / 60);
2200
+ if (m < 60)
2201
+ return s % 60 === 0 ? `${m}m` : `${m}m ${s % 60}s`;
2202
+ const h = Math.floor(m / 60);
2203
+ return m % 60 === 0 ? `${h}h` : `${h}h ${m % 60}m`;
2204
+ }
2205
+ function formatElapsed(ms) {
2206
+ return humanDuration(ms);
2207
+ }
2208
+ function appendLogRow(container, kind, text, ts, logMax) {
2209
+ const row = document.createElement("div");
2210
+ row.setAttribute(DATA_IRIS_LOG_ROW, "");
2211
+ row.setAttribute(DATA_KIND, kind);
2212
+ const tsEl = document.createElement("span");
2213
+ tsEl.setAttribute(DATA_IRIS_LOG_TS, "");
2214
+ tsEl.textContent = ts;
2215
+ const chip = document.createElement("span");
2216
+ chip.className = LOG_CHIP_CLASS;
2217
+ chip.setAttribute("data-mode", LOG_CHIP_MODE[kind]);
2218
+ chip.textContent = LOG_CHIP[kind];
2219
+ const textEl = document.createElement("span");
2220
+ textEl.className = LOG_TEXT_CLASS;
2221
+ textEl.textContent = text;
2222
+ const resEl = document.createElement("span");
2223
+ resEl.className = LOG_RES_CLASS;
2224
+ row.append(tsEl, chip, textEl, resEl);
2225
+ container.appendChild(row);
2226
+ while (container.childElementCount > logMax)
2227
+ container.firstElementChild?.remove();
2228
+ container.scrollTop = container.scrollHeight;
2229
+ return {
2230
+ result: (r) => {
2231
+ resEl.textContent = ` ${RESULT_GLYPH[r]}`;
2232
+ resEl.className = `${LOG_RES_CLASS} ${RESULT_CLASS[r]}`;
2233
+ }
2234
+ };
2235
+ }
2236
+
2237
+ // ../browser/dist/presenter/presenter-controls.js
2238
+ var DATA_IRIS_STATE = "data-iris-state";
2239
+ var DATA_ON = "data-on";
2240
+ var GLOW_OFF = "0";
2241
+ var CONTROL_LABEL = {
2242
+ PAUSE: "Pause",
2243
+ RESUME: "Resume",
2244
+ END: "End",
2245
+ SEND: "Send"
2246
+ };
2247
+ var INPUT_PLACEHOLDER = "Tell the agent something\u2026";
2248
+ var PAUSED_BADGE_TEXT = "PAUSED";
2249
+ var ENDED_BANNER_TEXT = "Session ended";
2250
+ var COPY_LABEL = "Copy run";
2251
+ var EXPORT_LABEL = "Export";
2252
+ var COPIED_TEXT = "Copied \u2713";
2253
+ var RUN_FILENAME = "iris-run.json";
2254
+ var ENDED_FADE_MS = 4e3;
2255
+ var CONTROLS_CSS = `
2256
+ [data-iris-hud] .iris-ctl{pointer-events:auto;cursor:pointer;flex:none;display:inline-flex;align-items:center;justify-content:center;
2257
+ height:26px;padding:0 11px;border-radius:8px;border:1px solid var(--iris-line);background:rgba(255,255,255,.04);
2258
+ color:var(--iris-muted);font-family:var(--iris-font);font-size:11px;font-weight:500;letter-spacing:.01em;line-height:1;
2259
+ transition:background .15s,color .15s,border-color .15s,transform .1s;}
2260
+ [data-iris-hud] .iris-ctl:hover{color:var(--iris-fg);background:rgba(255,255,255,.09);}
2261
+ [data-iris-hud] .iris-ctl:active{transform:scale(.95);}
2262
+ [data-iris-hud] .iris-ctl:disabled{opacity:.35;cursor:default;}
2263
+ [data-iris-hud] [data-iris-end]{color:#ff9aa2;border-color:rgba(255,107,107,.22);}
2264
+ [data-iris-hud] [data-iris-end]:hover{color:#ff7a7a;border-color:rgba(255,107,107,.5);background:rgba(255,107,107,.1);}
2265
+ [data-iris-hud] .iris-badge{display:none;align-items:center;flex:none;font-weight:600;letter-spacing:.1em;font-size:9px;
2266
+ color:var(--iris-accent);border:1px solid var(--iris-accent);background:var(--iris-accent-soft);padding:2px 8px;border-radius:999px;}
2267
+ [data-iris-overlay][data-iris-state="paused"] [data-iris-badge]{display:inline-flex;}
2268
+ [data-iris-hud] [data-iris-foot]{flex:none;padding:10px 12px 12px;border-top:1px solid var(--iris-line2);background:rgba(0,0,0,.16);}
2269
+ [data-iris-hud] .iris-composer{display:flex;align-items:center;gap:6px;background:rgba(255,255,255,.05);
2270
+ border:1px solid var(--iris-line);border-radius:14px;padding:5px 6px 5px 14px;transition:border-color .15s,box-shadow .15s;}
2271
+ [data-iris-hud] .iris-composer:focus-within{border-color:var(--iris-accent);box-shadow:0 0 0 3px var(--iris-accent-soft);}
2272
+ [data-iris-hud] .iris-msg{flex:1;min-width:0;pointer-events:auto;background:transparent;border:none;outline:none;
2273
+ color:var(--iris-fg);font-family:var(--iris-font);font-size:13px;height:28px;padding:0;}
2274
+ [data-iris-hud] .iris-msg::placeholder{color:var(--iris-faint);}
2275
+ [data-iris-hud] .iris-msg:disabled{opacity:.5;}
2276
+ [data-iris-hud] .iris-send{flex:none;width:30px;height:30px;padding:0;border-radius:10px;border:none;cursor:pointer;pointer-events:auto;
2277
+ background:var(--iris-accent);color:#0b0d14;display:inline-flex;align-items:center;justify-content:center;transition:filter .15s,transform .1s;}
2278
+ [data-iris-hud] .iris-send svg{display:block;}
2279
+ [data-iris-hud] .iris-send:hover{filter:brightness(1.12);}
2280
+ [data-iris-hud] .iris-send:active{transform:scale(.9);}
2281
+ [data-iris-hud] .iris-send:disabled{opacity:.4;cursor:default;}
2282
+ [data-iris-hud] .iris-banner{display:none;flex:none;padding:8px 15px;color:var(--iris-accent);
2283
+ font-size:11.5px;font-weight:500;border-bottom:1px solid var(--iris-line2);background:var(--iris-accent-soft);}
2284
+ [data-iris-overlay][data-iris-state="ended"] [data-iris-banner]{display:block;}
2285
+ /* Export row: hidden during a live session; revealed when ended so the run can be copied/saved. */
2286
+ [data-iris-hud] .iris-export{display:none;align-items:center;gap:8px;margin-top:9px;}
2287
+ [data-iris-overlay][data-iris-state="ended"] [data-iris-hud] .iris-export{display:flex;}
2288
+ [data-iris-hud] .iris-export-msg{color:var(--iris-ok);font-size:11px;opacity:0;transition:opacity .15s;}
2289
+ [data-iris-hud] .iris-export-msg[data-show="1"]{opacity:1;}
2290
+ [data-iris-overlay][data-iris-state="paused"] [data-iris-glow][data-on="1"]{animation:none;
2291
+ box-shadow:inset 0 0 0 3px rgba(246,180,76,.9),inset 0 0 30px 6px rgba(246,180,76,.4);}
2292
+ [data-iris-overlay][data-iris-state="ended"] [data-iris-glow][data-on="1"]{animation:none;
2293
+ box-shadow:inset 0 0 0 2px rgba(61,215,166,.55);}
2294
+ `;
2295
+ var CONTROLS_HEAD_HTML = `<button type="button" data-iris-pause class="iris-ctl">${CONTROL_LABEL.PAUSE}</button><button type="button" data-iris-end class="iris-ctl">${CONTROL_LABEL.END}</button><span data-iris-badge class="iris-badge">${PAUSED_BADGE_TEXT}</span>`;
2296
+ var CONTROLS_BANNER_HTML = `<div data-iris-banner class="iris-banner">${ENDED_BANNER_TEXT}</div>`;
2297
+ var SEND_ICON = `<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="22" y1="2" x2="11" y2="13"></line><polygon points="22 2 15 22 11 13 2 9 22 2"></polygon></svg>`;
2298
+ var CONTROLS_FOOT_HTML = `<div data-iris-foot><div class="iris-composer"><input data-iris-input class="iris-msg" type="text" placeholder="${INPUT_PLACEHOLDER}" /><button type="button" data-iris-send class="iris-send" aria-label="${CONTROL_LABEL.SEND}">${SEND_ICON}</button></div><div class="iris-export"><button type="button" data-iris-copy class="iris-ctl">${COPY_LABEL}</button><button type="button" data-iris-export class="iris-ctl">${EXPORT_LABEL}</button><span data-iris-export-msg class="iris-export-msg"></span></div></div>`;
2299
+ function queryControlRefs(root) {
2300
+ return {
2301
+ pauseBtn: root.querySelector("[data-iris-pause]") ?? void 0,
2302
+ endBtn: root.querySelector("[data-iris-end]") ?? void 0,
2303
+ input: root.querySelector("[data-iris-input]") ?? void 0,
2304
+ sendBtn: root.querySelector("[data-iris-send]") ?? void 0,
2305
+ banner: root.querySelector("[data-iris-banner]") ?? void 0,
2306
+ copyBtn: root.querySelector("[data-iris-copy]") ?? void 0,
2307
+ exportBtn: root.querySelector("[data-iris-export]") ?? void 0,
2308
+ exportMsg: root.querySelector("[data-iris-export-msg]") ?? void 0
2309
+ };
2310
+ }
2311
+ var ControlPanel = class {
2312
+ #refs = {
2313
+ pauseBtn: void 0,
2314
+ endBtn: void 0,
2315
+ input: void 0,
2316
+ sendBtn: void 0,
2317
+ banner: void 0,
2318
+ copyBtn: void 0,
2319
+ exportBtn: void 0,
2320
+ exportMsg: void 0
2321
+ };
2322
+ #state = SessionState.ACTIVE;
2323
+ #fadeTimer;
2324
+ #root;
2325
+ #glow;
2326
+ #host;
2327
+ constructor(host) {
2328
+ this.#host = host;
2329
+ }
2330
+ get state() {
2331
+ return this.#state;
2332
+ }
2333
+ /** Query control refs out of the mounted root and bind the DOM listeners, then paint active. */
2334
+ mount(root, glow) {
2335
+ this.#root = root;
2336
+ this.#glow = glow;
2337
+ this.#refs = queryControlRefs(root);
2338
+ this.#refs.pauseBtn?.addEventListener("click", () => this.#onPauseToggle());
2339
+ this.#refs.endBtn?.addEventListener("click", () => this.#onEnd());
2340
+ this.#refs.sendBtn?.addEventListener("click", () => this.#onSend());
2341
+ this.#refs.input?.addEventListener("keydown", (e) => {
2342
+ if (e instanceof KeyboardEvent && e.key === "Enter")
2343
+ this.#onSend();
2344
+ });
2345
+ this.#refs.copyBtn?.addEventListener("click", () => this.#onCopy());
2346
+ this.#refs.exportBtn?.addEventListener("click", () => this.#onExport());
2347
+ this.setState(SessionState.ACTIVE);
2348
+ }
2349
+ /** Serialize the run state to pretty JSON for Copy/Export. */
2350
+ #runJson() {
2351
+ return JSON.stringify(this.#host.runState(), null, 2);
2352
+ }
2353
+ /** Copy the run state to the clipboard (with a brief "Copied ✓" flash). */
2354
+ #onCopy() {
2355
+ void navigator.clipboard?.writeText(this.#runJson());
2356
+ const msg = this.#refs.exportMsg;
2357
+ if (msg !== void 0) {
2358
+ msg.textContent = COPIED_TEXT;
2359
+ msg.setAttribute("data-show", "1");
2360
+ nativeSetTimeout(() => msg.setAttribute("data-show", "0"), 1600);
2361
+ }
2362
+ }
2363
+ /** Download the run state as iris-run.json. */
2364
+ #onExport() {
2365
+ const blob = new Blob([this.#runJson()], { type: "application/json" });
2366
+ const url = URL.createObjectURL(blob);
2367
+ const a = document.createElement("a");
2368
+ a.href = url;
2369
+ a.download = RUN_FILENAME;
2370
+ a.click();
2371
+ URL.revokeObjectURL(url);
2372
+ }
2373
+ /** Clear any pending ended-fade timer (called from Presenter.destroy). */
2374
+ teardown() {
2375
+ if (this.#fadeTimer !== void 0)
2376
+ nativeClearTimeout(this.#fadeTimer);
2377
+ this.#fadeTimer = void 0;
2378
+ }
2379
+ #onPauseToggle() {
2380
+ if (this.#state === SessionState.PAUSED) {
2381
+ this.#host.emit(HumanControlKind.RESUME);
2382
+ this.setState(SessionState.ACTIVE);
2383
+ } else if (this.#state === SessionState.ACTIVE) {
2384
+ this.#host.emit(HumanControlKind.PAUSE);
2385
+ this.setState(SessionState.PAUSED);
2386
+ }
2387
+ }
2388
+ #onEnd() {
2389
+ if (this.#state === SessionState.ENDED)
2390
+ return;
2391
+ this.#host.emit(HumanControlKind.END);
2392
+ this.setState(SessionState.ENDED);
2393
+ }
2394
+ #onSend() {
2395
+ if (this.#state === SessionState.ENDED)
2396
+ return;
2397
+ const text = (this.#refs.input?.value ?? "").trim();
2398
+ if (text.length === 0)
2399
+ return;
2400
+ this.#host.emit(HumanControlKind.MESSAGE, text);
2401
+ this.#host.logHuman(text);
2402
+ if (this.#refs.input !== void 0)
2403
+ this.#refs.input.value = "";
2404
+ }
2405
+ /**
2406
+ * Drive the panel's visual state. Idempotent; NEVER emits a control — the shared path for both the
2407
+ * optimistic local click and the authoritative server PRESENTER echo. Only the ended-border fade
2408
+ * touches a clock, via the injected native timer.
2409
+ */
2410
+ setState(state, text) {
2411
+ this.#state = state;
2412
+ this.#root?.setAttribute(DATA_IRIS_STATE, state);
2413
+ if (this.#fadeTimer !== void 0) {
2414
+ nativeClearTimeout(this.#fadeTimer);
2415
+ this.#fadeTimer = void 0;
2416
+ }
2417
+ const refs2 = this.#refs;
2418
+ const ended = state === SessionState.ENDED;
2419
+ if (refs2.pauseBtn !== void 0) {
2420
+ refs2.pauseBtn.textContent = state === SessionState.PAUSED ? CONTROL_LABEL.RESUME : CONTROL_LABEL.PAUSE;
2421
+ refs2.pauseBtn.disabled = ended;
2422
+ }
2423
+ if (refs2.endBtn !== void 0)
2424
+ refs2.endBtn.disabled = ended;
2425
+ if (refs2.sendBtn !== void 0)
2426
+ refs2.sendBtn.disabled = ended;
2427
+ if (refs2.input !== void 0)
2428
+ refs2.input.disabled = ended;
2429
+ if (refs2.banner !== void 0) {
2430
+ const summary = text !== void 0 && text.trim().length > 0 ? ` \xB7 ${text.trim()}` : "";
2431
+ refs2.banner.textContent = `${ENDED_BANNER_TEXT}${summary}`;
2432
+ }
2433
+ if (ended) {
2434
+ const glow = this.#glow;
2435
+ this.#fadeTimer = nativeSetTimeout(() => {
2436
+ glow?.setAttribute(DATA_ON, GLOW_OFF);
2437
+ }, this.#host.endedFadeMs);
2438
+ }
2439
+ }
2440
+ };
2441
+
2442
+ // ../browser/dist/presenter/presenter.js
2443
+ var CSS = `
2444
+ @import url("https://fonts.googleapis.com/css2?family=IBM+Plex+Serif:wght@400;500&family=Inter:wght@400;450;500;600&display=swap");
2445
+ [data-iris-glow]{position:fixed;inset:0;pointer-events:none;z-index:2147483600;opacity:0;
2446
+ transition:opacity .25s ease;box-shadow:inset 0 0 0 3px rgba(99,102,241,.9),inset 0 0 28px 6px rgba(99,102,241,.45);}
2447
+ [data-iris-glow][data-on="1"]{opacity:1;animation:iris-pulse 1.6s ease-in-out infinite;}
2448
+ [data-iris-glow][data-on="1"][data-busy="1"]{animation:iris-shimmer 1.1s ease-in-out infinite;}
2449
+ @keyframes iris-pulse{0%,100%{box-shadow:inset 0 0 0 3px rgba(99,102,241,.9),inset 0 0 22px 4px rgba(99,102,241,.35)}
2450
+ 50%{box-shadow:inset 0 0 0 3px rgba(124,127,242,1),inset 0 0 40px 10px rgba(99,102,241,.6)}}
2451
+ @keyframes iris-shimmer{0%,100%{box-shadow:inset 0 0 0 3px rgba(124,127,242,1),inset 0 0 34px 8px rgba(99,102,241,.55)}
2452
+ 50%{box-shadow:inset 0 0 0 3px rgba(140,142,255,1),inset 0 0 48px 12px rgba(99,102,241,.7)}}
2453
+ [data-iris-cursor]{position:fixed;top:0;left:0;width:22px;height:22px;margin:-11px 0 0 -11px;
2454
+ border:2px solid #6366f1;border-radius:50%;background:rgba(99,102,241,.25);pointer-events:none;
2455
+ z-index:2147483646;opacity:0;transition:transform .32s cubic-bezier(.22,1,.36,1),opacity .2s ease;}
2456
+ [data-iris-cursor][data-on="1"]{opacity:1;}
2457
+ [data-iris-cursor]::after{content:"";position:absolute;inset:7px;border-radius:50%;background:#6366f1;}
2458
+ [data-iris-ripple]{position:fixed;width:14px;height:14px;margin:-7px 0 0 -7px;border-radius:50%;
2459
+ background:rgba(99,102,241,.5);pointer-events:none;z-index:2147483645;animation:iris-ripple .5s ease-out forwards;}
2460
+ @keyframes iris-ripple{from{transform:scale(.4);opacity:.8}to{transform:scale(5);opacity:0}}
2461
+ [data-iris-ring]{position:fixed;pointer-events:none;z-index:2147483644;border:2px solid #22c55e;border-radius:8px;
2462
+ box-shadow:0 0 0 3px rgba(34,197,94,.25);opacity:0;transition:opacity .15s ease;}
2463
+ [data-iris-ring][data-on="1"]{opacity:1;}
2464
+ [data-iris-hud]{
2465
+ --iris-accent:#7c83ff;--iris-accent-soft:rgba(124,131,255,.16);
2466
+ --iris-bg:rgba(13,15,22,.80);--iris-bg2:rgba(19,22,32,.74);
2467
+ --iris-fg:#e9ebf2;--iris-muted:#9aa0b2;--iris-faint:#6a7186;
2468
+ --iris-line:rgba(255,255,255,.09);--iris-line2:rgba(255,255,255,.05);
2469
+ --iris-read:#54d2e6;--iris-ok:#3dd7a6;--iris-bad:#ff7a7a;
2470
+ --iris-font:"Inter",system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
2471
+ --iris-serif:"IBM Plex Serif",Georgia,"Times New Roman",serif;
2472
+ position:fixed;left:50%;right:auto;bottom:20px;box-sizing:border-box;
2473
+ width:384px;height:468px;max-width:calc(100vw - 32px);max-height:calc(100vh - 32px);
2474
+ display:flex;flex-direction:column;overflow:hidden;text-align:left;z-index:2147483647;pointer-events:none;
2475
+ font-family:var(--iris-font);font-size:13px;line-height:1.5;color:var(--iris-fg);-webkit-font-smoothing:antialiased;
2476
+ background:linear-gradient(180deg,var(--iris-bg),var(--iris-bg2));
2477
+ -webkit-backdrop-filter:blur(24px) saturate(1.5);backdrop-filter:blur(24px) saturate(1.5);
2478
+ border:1px solid var(--iris-line);border-radius:20px;
2479
+ box-shadow:0 28px 70px -18px rgba(0,0,0,.66),0 0 0 1px rgba(0,0,0,.35),inset 0 1px 0 rgba(255,255,255,.07),0 0 54px -22px var(--iris-accent);
2480
+ opacity:0;transform:translateX(-50%) translateY(14px) scale(.985);
2481
+ transition:opacity .3s ease,transform .42s cubic-bezier(.16,1,.3,1),height .42s cubic-bezier(.16,1,.3,1),border-radius .42s ease,box-shadow .35s ease;}
2482
+ [data-iris-overlay][data-iris-state="paused"] [data-iris-hud]{--iris-accent:#f6b44c;--iris-accent-soft:rgba(246,180,76,.16);}
2483
+ [data-iris-overlay][data-iris-state="ended"] [data-iris-hud]{--iris-accent:#3dd7a6;--iris-accent-soft:rgba(61,215,166,.14);}
2484
+ [data-iris-hud]::before{content:"";position:absolute;inset:0;border-radius:inherit;pointer-events:none;
2485
+ background:radial-gradient(130% 90% at 50% 0%,var(--iris-accent-soft),transparent 60%);}
2486
+ [data-iris-hud]>*{position:relative;}
2487
+ [data-iris-hud][data-on="1"]{opacity:1;transform:translateX(-50%) translateY(0) scale(1);}
2488
+ /* Click-through: the glassy panel itself never blocks the app \u2014 only its interactive controls
2489
+ capture clicks (buttons / inputs), so a human can click straight through the HUD to the page.
2490
+ The log auto-scrolls, so it stays click-through too (drag-scroll is traded for click-through). */
2491
+ [data-iris-hud] button,[data-iris-hud] input,[data-iris-hud] textarea,
2492
+ [data-iris-hud] select,[data-iris-hud] [contenteditable]{pointer-events:auto;}
2493
+ /* When minimised to a pill, the whole bar is the (single) click target to restore. */
2494
+ [data-iris-overlay][data-iris-min="1"] [data-iris-hud][data-on="1"]{pointer-events:auto;}
2495
+ [data-iris-hud] .iris-hud-head{display:flex;align-items:center;gap:8px;flex:none;
2496
+ padding:12px 12px 12px 15px;border-bottom:1px solid var(--iris-line2);}
2497
+ [data-iris-hud] .iris-dot{width:9px;height:9px;border-radius:50%;flex:none;background:var(--iris-accent);
2498
+ animation:iris-breathe 2.6s ease-in-out infinite;}
2499
+ @keyframes iris-breathe{0%,100%{box-shadow:0 0 0 0 var(--iris-accent),0 0 7px 1px var(--iris-accent);opacity:.85}
2500
+ 50%{box-shadow:0 0 0 4px var(--iris-accent-soft),0 0 15px 3px var(--iris-accent);opacity:1}}
2501
+ [data-iris-hud] .iris-brand{font-family:var(--iris-serif);font-weight:500;font-size:15px;letter-spacing:.01em;color:var(--iris-fg);}
2502
+ [data-iris-hud] .iris-head-sp{flex:1;}
2503
+ [data-iris-hud] .iris-live{display:none;flex:1;min-width:0;color:var(--iris-muted);font-size:12.5px;
2504
+ white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
2505
+ [data-iris-hud] .iris-maxhint{display:none;flex:none;color:var(--iris-faint);font-size:13px;line-height:1;}
2506
+ [data-iris-hud] .iris-act-strip{flex:none;padding:7px 15px;border-bottom:1px solid var(--iris-line2);background:rgba(0,0,0,.14);}
2507
+ [data-iris-hud] .iris-act{display:block;color:var(--iris-muted);font-size:11.5px;
2508
+ white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
2509
+ [data-iris-hud] [data-iris-min-btn]{flex:none;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;
2510
+ width:26px;height:26px;border-radius:8px;border:1px solid var(--iris-line);background:rgba(255,255,255,.04);
2511
+ color:var(--iris-muted);font-size:15px;line-height:1;transition:background .15s,color .15s,transform .1s;}
2512
+ [data-iris-hud] [data-iris-min-btn]:hover{color:var(--iris-fg);background:rgba(255,255,255,.08);}
2513
+ [data-iris-hud] [data-iris-min-btn]:active{transform:scale(.94);}
2514
+ [data-iris-hud] .iris-pass{color:var(--iris-ok);}[data-iris-hud] .iris-fail{color:var(--iris-bad);}
2515
+ [data-iris-hud] .iris-chip{display:none;flex:none;font-size:9px;font-weight:600;letter-spacing:.08em;
2516
+ padding:2px 7px;border-radius:6px;vertical-align:middle;}
2517
+ [data-iris-hud] .iris-chip[data-mode="reading"]{display:inline-block;color:var(--iris-read);
2518
+ background:rgba(84,210,230,.12);border:1px solid rgba(84,210,230,.32);}
2519
+ [data-iris-hud] .iris-chip[data-mode="acting"]{display:inline-block;color:var(--iris-accent);
2520
+ background:var(--iris-accent-soft);border:1px solid var(--iris-accent);}
2521
+ [data-iris-hud] .iris-chip[data-mode="idle"]{display:none;}
2522
+ [data-iris-overlay][data-iris-min="1"] [data-iris-hud]{height:50px;border-radius:25px;cursor:pointer;}
2523
+ [data-iris-overlay][data-iris-min="1"] [data-iris-hud] .iris-hud-head{border-bottom:none;height:50px;padding:0 12px 0 16px;}
2524
+ [data-iris-overlay][data-iris-min="1"] [data-iris-hud] .iris-brand,
2525
+ [data-iris-overlay][data-iris-min="1"] [data-iris-hud] .iris-chip,
2526
+ [data-iris-overlay][data-iris-min="1"] [data-iris-hud] .iris-head-sp,
2527
+ [data-iris-overlay][data-iris-min="1"] [data-iris-hud] [data-iris-min-btn],
2528
+ [data-iris-overlay][data-iris-min="1"] [data-iris-hud] .iris-ctl,
2529
+ [data-iris-overlay][data-iris-min="1"] [data-iris-hud] .iris-badge,
2530
+ [data-iris-overlay][data-iris-min="1"] [data-iris-hud] .iris-act-strip,
2531
+ [data-iris-overlay][data-iris-min="1"] [data-iris-hud] [data-iris-log],
2532
+ [data-iris-overlay][data-iris-min="1"] [data-iris-hud] [data-iris-foot],
2533
+ [data-iris-overlay][data-iris-min="1"] [data-iris-hud] .iris-banner{display:none;}
2534
+ [data-iris-overlay][data-iris-min="1"] [data-iris-hud] .iris-live{display:block;}
2535
+ [data-iris-overlay][data-iris-min="1"] [data-iris-hud] .iris-maxhint{display:inline-flex;}
2536
+ [data-iris-mode="reading"] [data-iris-glow][data-on="1"]{
2537
+ box-shadow:inset 0 0 0 3px rgba(34,211,238,.9),inset 0 0 28px 6px rgba(34,211,238,.4);}
2538
+ [data-iris-mode="reading"] [data-iris-ring]{border-color:#22d3ee;
2539
+ box-shadow:0 0 0 3px rgba(34,211,238,.25);}
2540
+ [data-iris-overlay][data-iris-throttled="1"] [data-iris-glow][data-on="1"]{
2541
+ box-shadow:inset 0 0 0 3px rgba(251,191,36,.9),inset 0 0 28px 6px rgba(251,191,36,.45);}
2542
+ [data-iris-overlay][data-iris-throttled="1"] [data-iris-hud]{--iris-accent:#fbbf24;--iris-accent-soft:rgba(251,191,36,.16);}
2543
+ ${LOG_CSS}
2544
+ ${CONTROLS_CSS}`;
2545
+ var BorderMode = { SESSION: "session", BUSY: "busy" };
2546
+ var DEFAULT_BORDER_MODE = BorderMode.SESSION;
2547
+ var DATA_BUSY = "data-busy";
2548
+ var BUSY_ON = "1";
2549
+ var BUSY_OFF = "0";
2550
+ var DEFAULT_PACE = 450;
2551
+ var GlowPhase = {
2552
+ IDLE: "idle",
2553
+ BUSY: "busy",
2554
+ FADING: "fading"
2555
+ };
2556
+ var IDLE_AFTER_MS = 700;
2557
+ var HEARTBEAT_MS = 1e3;
2558
+ var IDLE_NOTICE_MS = 4e3;
2559
+ var IDLE_END_MS = 3e5;
2560
+ var IDLE_END_MIN_MS = 5e3;
2561
+ var GLOW_FADE_MS = 250;
2562
+ var GLOW_ON = "1";
2563
+ var GLOW_OFF2 = "0";
2564
+ var DATA_ON2 = "data-on";
2565
+ var MIN_ATTR = "data-iris-min";
2566
+ var THROTTLED_ATTR = "data-iris-throttled";
2567
+ var Presenter = class {
2568
+ #paceMs;
2569
+ #root;
2570
+ #glow;
2571
+ #cursor;
2572
+ #ring;
2573
+ #hud;
2574
+ #actLine;
2575
+ #chip;
2576
+ #liveLine;
2577
+ #mode = PresenterMode.IDLE;
2578
+ #now;
2579
+ #idleAfterMs;
2580
+ #glowFadeMs;
2581
+ #heartbeatMs;
2582
+ #idleNoticeMs;
2583
+ #borderMode;
2584
+ #phase = GlowPhase.IDLE;
2585
+ #lastActivityMs = 0;
2586
+ #idleCheckTimer;
2587
+ #fadeTimer;
2588
+ /** Liveness: the most recent action text + a 1s ticker that ages it into an "idle · {dur}" clock. */
2589
+ #lastActionText = "";
2590
+ #heartbeatTimer;
2591
+ /** Session lifecycle: idle-end window (tweakable), session id, start/end cursors, structured run log. */
2592
+ #idleEndMs;
2593
+ #sessionId;
2594
+ #startMs;
2595
+ #endMs;
2596
+ #runLog = [];
2597
+ /** Tracks sessionStart/sessionEnd so both are idempotent (no strobe / no spurious off-write). */
2598
+ #sessionActive = false;
2599
+ // v2: narration + action status accumulate in a persistent, timestamped, scrollable log.
2600
+ #logMax;
2601
+ #log;
2602
+ /** now() of the first row, the baseline for the +elapsed timestamps. */
2603
+ #logBaseMs;
2604
+ // Live-control panel: the two-way control surface (Pause/Resume + End + message Send).
2605
+ #onControl;
2606
+ #panel;
2607
+ constructor(options = {}) {
2608
+ this.#paceMs = options.paceMs ?? DEFAULT_PACE;
2609
+ this.#now = options.now ?? nativeNow;
2610
+ this.#idleAfterMs = options.idleAfterMs ?? IDLE_AFTER_MS;
2611
+ this.#glowFadeMs = options.glowFadeMs ?? GLOW_FADE_MS;
2612
+ this.#heartbeatMs = options.heartbeatMs ?? HEARTBEAT_MS;
2613
+ this.#idleNoticeMs = options.idleNoticeMs ?? IDLE_NOTICE_MS;
2614
+ this.#idleEndMs = options.idleEndMs ?? IDLE_END_MS;
2615
+ this.#sessionId = options.sessionId ?? "";
2616
+ this.#borderMode = options.border ?? DEFAULT_BORDER_MODE;
2617
+ this.#logMax = clampLogMax(options.logMax);
2618
+ this.#onControl = options.onControl;
2619
+ this.#panel = new ControlPanel({
2620
+ emit: (kind, text) => this.#onControl?.(text !== void 0 ? { kind, text } : { kind }),
2621
+ logHuman: (text) => {
2622
+ this.log(LOG_KIND.HUMAN, HUMAN_ROW_PREFIX + text);
2623
+ },
2624
+ endedFadeMs: options.endedFadeMs ?? ENDED_FADE_MS,
2625
+ runState: () => this.runState()
2626
+ });
2627
+ }
2628
+ /** Setter so iris.ts can wire the control callback after construction. */
2629
+ setControlHandler(handler) {
2630
+ this.#onControl = handler;
2631
+ }
2632
+ /** Current live-control session state mirrored onto the panel (data-iris-state). */
2633
+ get state() {
2634
+ return this.#panel.state;
2635
+ }
2636
+ /** Whether a run is currently being presented (false before the agent's first activity / after end). */
2637
+ get sessionActive() {
2638
+ return this.#sessionActive;
2639
+ }
2640
+ /** Drive the panel's live-control visual state (server-push / agent path; never emits). */
2641
+ setState(state, text) {
2642
+ this.#panel.setState(state, text);
2643
+ }
2644
+ /** Current cap on accumulated log rows. */
2645
+ get logMax() {
2646
+ return this.#logMax;
2647
+ }
2648
+ set logMax(n) {
2649
+ this.#logMax = clampLogMax(n);
2650
+ this.#pruneLog();
2651
+ }
2652
+ mount() {
2653
+ if (this.#root !== void 0 || typeof document === "undefined")
2654
+ return;
2655
+ const style = document.createElement("style");
2656
+ style.setAttribute("data-iris-overlay", "");
2657
+ style.textContent = CSS;
2658
+ document.head.appendChild(style);
2659
+ const root = document.createElement("div");
2660
+ root.setAttribute("data-iris-overlay", "");
2661
+ root.innerHTML = `
2662
+ <div data-iris-glow></div>
2663
+ <div data-iris-cursor></div>
2664
+ <div data-iris-ring></div>
2665
+ <div data-iris-hud>
2666
+ <div class="iris-hud-head"><span class="iris-dot"></span><span class="iris-brand">iris</span><span class="iris-chip" data-iris-chip></span><span class="iris-live"></span><span class="iris-head-sp"></span><button type="button" data-iris-min-btn title="Minimise" aria-label="Minimise the panel">\u2304</button>${CONTROLS_HEAD_HTML}<span class="iris-maxhint" aria-hidden="true">\u2303</span></div>
2667
+ <div class="iris-act-strip"><span class="iris-act">idle</span></div>
2668
+ ${CONTROLS_BANNER_HTML}
2669
+ <div ${DATA_IRIS_LOG}></div>
2670
+ ${CONTROLS_FOOT_HTML}
2671
+ </div>`;
2672
+ document.body.appendChild(root);
2673
+ this.#root = root;
2674
+ this.#glow = root.querySelector("[data-iris-glow]") ?? void 0;
2675
+ this.#cursor = root.querySelector("[data-iris-cursor]") ?? void 0;
2676
+ this.#ring = root.querySelector("[data-iris-ring]") ?? void 0;
2677
+ this.#hud = root.querySelector("[data-iris-hud]") ?? void 0;
2678
+ this.#actLine = root.querySelector(".iris-act") ?? void 0;
2679
+ this.#log = root.querySelector(`[${DATA_IRIS_LOG}]`) ?? void 0;
2680
+ this.#chip = root.querySelector("[data-iris-chip]") ?? void 0;
2681
+ this.#liveLine = root.querySelector(".iris-live") ?? void 0;
2682
+ const setMin = (on) => root.setAttribute(MIN_ATTR, on ? "1" : "0");
2683
+ root.querySelector("[data-iris-min-btn]")?.addEventListener("click", (e) => {
2684
+ e.stopPropagation();
2685
+ setMin(true);
2686
+ });
2687
+ root.querySelector(".iris-hud-head")?.addEventListener("click", () => {
2688
+ if (root.getAttribute(MIN_ATTR) === "1")
2689
+ setMin(false);
2690
+ });
2691
+ this.#panel.mount(root, this.#glow);
2692
+ this.setMode(this.#mode);
2693
+ }
2694
+ destroy() {
2695
+ if (this.#idleCheckTimer !== void 0)
2696
+ nativeClearTimeout(this.#idleCheckTimer);
2697
+ if (this.#fadeTimer !== void 0)
2698
+ nativeClearTimeout(this.#fadeTimer);
2699
+ if (this.#heartbeatTimer !== void 0)
2700
+ nativeClearTimeout(this.#heartbeatTimer);
2701
+ this.#heartbeatTimer = void 0;
2702
+ this.#panel.teardown();
2703
+ this.#idleCheckTimer = void 0;
2704
+ this.#fadeTimer = void 0;
2705
+ this.#sessionActive = false;
2706
+ this.#logBaseMs = void 0;
2707
+ this.#log = void 0;
2708
+ this.#root?.remove();
2709
+ document.querySelectorAll("style[data-iris-overlay]").forEach((s) => s.remove());
2710
+ this.#root = void 0;
2711
+ }
2712
+ /**
2713
+ * Session start: in 'session' border mode this fades the base border IN and keeps it on until
2714
+ * sessionEnd(). Idempotent, and a no-op when unmounted or in 'busy' border mode.
2715
+ */
2716
+ sessionStart() {
2717
+ if (this.state === SessionState.ENDED) {
2718
+ this.#revive();
2719
+ return;
2720
+ }
2721
+ if (this.#sessionActive)
2722
+ return;
2723
+ this.#sessionActive = true;
2724
+ this.#startMs ??= this.#now();
2725
+ this.#endMs = void 0;
2726
+ this.#showSession();
2727
+ this.#lastActivityMs = this.#now();
2728
+ this.#startHeartbeat();
2729
+ }
2730
+ /** Turn the base border (session mode) + the HUD/log on — the visible "session is live" state. */
2731
+ #showSession() {
2732
+ this.#hud?.setAttribute(DATA_ON2, GLOW_ON);
2733
+ if (this.#borderMode === BorderMode.SESSION)
2734
+ this.#glow?.setAttribute(DATA_ON2, GLOW_ON);
2735
+ }
2736
+ /** Revive after an ended session (new agent activity): clear the ended state + glow back on. */
2737
+ #revive() {
2738
+ this.#panel.setState(SessionState.ACTIVE);
2739
+ this.#endMs = void 0;
2740
+ this.#showSession();
2741
+ this.#lastActivityMs = this.#now();
2742
+ this.#startHeartbeat();
2743
+ }
2744
+ /**
2745
+ * Session end: hides the log/HUD and (in 'session' mode) clears the base border. Idempotent; a
2746
+ * no-op without a prior sessionStart or when unmounted.
2747
+ */
2748
+ sessionEnd() {
2749
+ if (!this.#sessionActive)
2750
+ return;
2751
+ this.#sessionActive = false;
2752
+ if (this.#heartbeatTimer !== void 0) {
2753
+ nativeClearTimeout(this.#heartbeatTimer);
2754
+ this.#heartbeatTimer = void 0;
2755
+ }
2756
+ this.#hud?.setAttribute(DATA_ON2, GLOW_OFF2);
2757
+ if (this.#borderMode === BorderMode.SESSION) {
2758
+ this.#glow?.setAttribute(DATA_ON2, GLOW_OFF2);
2759
+ this.#glow?.setAttribute(DATA_BUSY, BUSY_OFF);
2760
+ }
2761
+ }
2762
+ /**
2763
+ * Record agent activity. Idempotent while busy — only the first activity from idle/fading flips
2764
+ * the glow on, so a burst never restarts the iris-pulse animation (no strobe). Subsequent calls
2765
+ * just refresh the last-activity timestamp and re-arm the idle check.
2766
+ */
2767
+ markActivity() {
2768
+ this.#markActivityAt(this.#now());
2769
+ }
2770
+ /** markActivity with a caller-supplied timestamp so log() reads the clock exactly once per row. */
2771
+ #markActivityAt(ms) {
2772
+ this.#lastActivityMs = ms;
2773
+ if (this.#phase === GlowPhase.IDLE || this.#phase === GlowPhase.FADING) {
2774
+ this.#enterBusy();
2775
+ }
2776
+ this.#armIdleCheck();
2777
+ }
2778
+ /** Re-arm the quiet-window idle check (kept for iris.ts's finally block). */
2779
+ scheduleIdle() {
2780
+ this.#armIdleCheck();
2781
+ }
2782
+ /** Test/diagnostic accessor for the current glow phase. */
2783
+ glowPhase() {
2784
+ return this.#phase;
2785
+ }
2786
+ /** Current intent (reading vs acting), exposed for tests + the watcher. */
2787
+ get mode() {
2788
+ return this.#mode;
2789
+ }
2790
+ /**
2791
+ * Set the presenter intent. READING shows a cyan scan + chip and hides the cursor; ACTING
2792
+ * keeps the warm cursor/ripple + chip; IDLE clears the chip. Drives color via data-iris-mode.
2793
+ */
2794
+ setMode(mode) {
2795
+ this.#mode = mode;
2796
+ this.#root?.setAttribute("data-iris-mode", mode);
2797
+ if (this.#chip !== void 0) {
2798
+ this.#chip.textContent = CHIP_LABEL[mode];
2799
+ this.#chip.setAttribute("data-mode", mode);
2800
+ }
2801
+ if (mode === PresenterMode.READING)
2802
+ this.#cursor?.setAttribute(DATA_ON2, GLOW_OFF2);
2803
+ }
2804
+ #enterBusy() {
2805
+ if (this.#fadeTimer !== void 0) {
2806
+ nativeClearTimeout(this.#fadeTimer);
2807
+ this.#fadeTimer = void 0;
2808
+ }
2809
+ this.#phase = GlowPhase.BUSY;
2810
+ if (this.#borderMode === BorderMode.SESSION) {
2811
+ this.#glow?.setAttribute(DATA_BUSY, BUSY_ON);
2812
+ } else {
2813
+ this.#glow?.setAttribute(DATA_ON2, GLOW_ON);
2814
+ }
2815
+ this.#cursor?.setAttribute(DATA_ON2, GLOW_ON);
2816
+ }
2817
+ #armIdleCheck() {
2818
+ if (this.#idleCheckTimer !== void 0)
2819
+ nativeClearTimeout(this.#idleCheckTimer);
2820
+ this.#idleCheckTimer = nativeSetTimeout(() => this.#checkIdle(), this.#idleAfterMs);
2821
+ }
2822
+ #checkIdle() {
2823
+ this.#idleCheckTimer = void 0;
2824
+ if (this.#phase !== GlowPhase.BUSY)
2825
+ return;
2826
+ const quietFor = this.#now() - this.#lastActivityMs;
2827
+ if (quietFor < this.#idleAfterMs) {
2828
+ this.#idleCheckTimer = nativeSetTimeout(() => this.#checkIdle(), this.#idleAfterMs - quietFor);
2829
+ return;
2830
+ }
2831
+ this.#beginFade();
2832
+ }
2833
+ #beginFade() {
2834
+ this.#phase = GlowPhase.FADING;
2835
+ if (this.#borderMode === BorderMode.SESSION) {
2836
+ this.#glow?.setAttribute(DATA_BUSY, BUSY_OFF);
2837
+ } else {
2838
+ this.#glow?.setAttribute(DATA_ON2, GLOW_OFF2);
2839
+ }
2840
+ this.#cursor?.setAttribute(DATA_ON2, GLOW_OFF2);
2841
+ this.setMode(PresenterMode.IDLE);
2842
+ this.#fadeTimer = nativeSetTimeout(() => {
2843
+ this.#fadeTimer = void 0;
2844
+ if (this.#phase === GlowPhase.FADING)
2845
+ this.#phase = GlowPhase.IDLE;
2846
+ }, this.#glowFadeMs);
2847
+ }
2848
+ status(text) {
2849
+ this.markActivity();
2850
+ this.#lastActionText = text;
2851
+ if (this.#actLine !== void 0)
2852
+ this.#actLine.textContent = text;
2853
+ }
2854
+ /**
2855
+ * Liveness heartbeat (native 1s timer — never rAF, so it ticks in a foreground tab regardless of
2856
+ * agent activity). Once the agent has been quiet for IDLE_NOTICE_MS, the act strip shows a LIVE,
2857
+ * growing "◌ idle · {duration} since last action" — the signal that was missing when a stopped
2858
+ * agent left the panel frozen and indistinguishable from one still thinking.
2859
+ */
2860
+ #startHeartbeat() {
2861
+ if (this.#heartbeatTimer !== void 0)
2862
+ nativeClearTimeout(this.#heartbeatTimer);
2863
+ const tick = () => {
2864
+ this.#tickLiveness();
2865
+ this.#heartbeatTimer = nativeSetTimeout(tick, this.#heartbeatMs);
2866
+ };
2867
+ this.#heartbeatTimer = nativeSetTimeout(tick, this.#heartbeatMs);
2868
+ }
2869
+ #tickLiveness() {
2870
+ if (!this.#sessionActive || this.#actLine === void 0)
2871
+ return;
2872
+ if (this.state === SessionState.ENDED)
2873
+ return;
2874
+ const idleMs = this.#now() - this.#lastActivityMs;
2875
+ if (idleMs >= this.#idleEndMs) {
2876
+ this.#endIdle(idleMs);
2877
+ return;
2878
+ }
2879
+ if (idleMs < this.#idleNoticeMs)
2880
+ return;
2881
+ const since = this.#lastActionText !== "" ? ` since last action` : "";
2882
+ this.#actLine.textContent = `\u25CC idle \xB7 ${humanDuration(idleMs)}${since}`;
2883
+ }
2884
+ /** Auto-end after the idle window: stamp the end, drive the panel to ENDED, stop the heartbeat. */
2885
+ #endIdle(idleMs) {
2886
+ this.#endMs = this.#now();
2887
+ this.#panel.setState(SessionState.ENDED, `idle ${humanDuration(idleMs)}`);
2888
+ if (this.#heartbeatTimer !== void 0) {
2889
+ nativeClearTimeout(this.#heartbeatTimer);
2890
+ this.#heartbeatTimer = void 0;
2891
+ }
2892
+ }
2893
+ /** Agent-tunable idle-end window (iris_session). Floored so it can't be set uselessly small. */
2894
+ setIdleEndMs(ms) {
2895
+ if (!Number.isFinite(ms))
2896
+ return;
2897
+ this.#idleEndMs = Math.max(IDLE_END_MIN_MS, Math.floor(ms));
2898
+ }
2899
+ /**
2900
+ * The exported "run state" for the Copy/Export buttons — everything the page holds about this
2901
+ * run: session id, url, duration, capability surface, per-kind counts, and the full activity log.
2902
+ * (The full network/console ring-buffer lives server-side; this is the in-page run summary.)
2903
+ */
2904
+ runState() {
2905
+ const now = this.#now();
2906
+ const start = this.#startMs ?? now;
2907
+ const counts = { reads: 0, acts: 0, narrations: 0, human: 0, passes: 0, fails: 0 };
2908
+ for (const e of this.#runLog) {
2909
+ if (e.kind === LOG_KIND.READ)
2910
+ counts.reads += 1;
2911
+ else if (e.kind === LOG_KIND.ACT)
2912
+ counts.acts += 1;
2913
+ else if (e.kind === LOG_KIND.NARRATION)
2914
+ counts.narrations += 1;
2915
+ else if (e.kind === LOG_KIND.HUMAN)
2916
+ counts.human += 1;
2917
+ if (e.result === LOG_RESULT.PASS)
2918
+ counts.passes += 1;
2919
+ else if (e.result === LOG_RESULT.FAIL)
2920
+ counts.fails += 1;
2921
+ }
2922
+ return {
2923
+ session: this.#sessionId,
2924
+ url: typeof location === "undefined" ? "" : location.href,
2925
+ state: this.state,
2926
+ startedMs: start,
2927
+ durationMs: Math.max(0, (this.#endMs ?? now) - start),
2928
+ counts,
2929
+ capabilities: getCapabilities(),
2930
+ log: this.#runLog.map((e) => ({ ...e }))
2931
+ };
2932
+ }
2933
+ /**
2934
+ * Append an activity-log row. Accumulates (never overwrites): each call adds a timestamped row
2935
+ * with a mode chip + text. Returns a handle to stamp the row's outcome glyph (✓/✗) later, or
2936
+ * undefined when unmounted / when the text is empty after trimming.
2937
+ */
2938
+ log(kind, text, result2) {
2939
+ const ms = this.#now();
2940
+ this.#markActivityAt(ms);
2941
+ if (this.#log === void 0)
2942
+ return void 0;
2943
+ const trimmed = text.trim();
2944
+ if (trimmed.length === 0)
2945
+ return void 0;
2946
+ this.#logBaseMs ??= ms;
2947
+ const entry = result2 !== void 0 ? { at: ms - this.#logBaseMs, kind, text: trimmed, result: result2 } : { at: ms - this.#logBaseMs, kind, text: trimmed };
2948
+ this.#runLog.push(entry);
2949
+ while (this.#runLog.length > this.#logMax)
2950
+ this.#runLog.shift();
2951
+ const ts = formatElapsed(ms - this.#logBaseMs);
2952
+ const handle = appendLogRow(this.#log, kind, trimmed, ts, this.#logMax);
2953
+ if (result2 !== void 0)
2954
+ handle.result(result2);
2955
+ if (this.#liveLine !== void 0)
2956
+ this.#liveLine.textContent = trimmed;
2957
+ return {
2958
+ result: (r) => {
2959
+ handle.result(r);
2960
+ entry.result = r;
2961
+ }
2962
+ };
2963
+ }
2964
+ /** Back-compat: narration appends to the live log (append-only, never overwrites). */
2965
+ narrate(text, level = "info") {
2966
+ const line = level === "info" ? text : `[${level}] ${text}`;
2967
+ return this.log(LOG_KIND.NARRATION, line);
2968
+ }
2969
+ #pruneLog() {
2970
+ if (this.#log === void 0)
2971
+ return;
2972
+ while (this.#log.childElementCount > this.#logMax) {
2973
+ this.#log.firstElementChild?.remove();
2974
+ }
2975
+ }
2976
+ /** Legacy no-op kept for source compat; outcomes now flow through LogHandle.result(). */
2977
+ result(_ok) {
2978
+ }
2979
+ /**
2980
+ * Mirror the server's session.throttled() state onto the HUD border. When throttled (tab
2981
+ * backgrounded or stale), the border turns amber so the developer knows actions are no-oping —
2982
+ * the same signal the agent already reads from result.session.throttled.
2983
+ */
2984
+ setThrottled(throttled) {
2985
+ this.#root?.setAttribute(THROTTLED_ATTR, throttled ? "1" : "0");
2986
+ if (throttled && this.#actLine !== void 0) {
2987
+ this.#actLine.textContent = "Tab backgrounded \u2014 actions throttled. Bring tab to front or use `iris drive`.";
2988
+ }
2989
+ }
2990
+ /** Fly the cursor to an element, play the action's effect, then pace for the human. */
2991
+ async beforeAct(refId, action, label) {
2992
+ const el = refs.resolve(refId);
2993
+ this.status(`${actionVerb(action)} ${label}`);
2994
+ if (!(el instanceof HTMLElement)) {
2995
+ await this.#pause();
2996
+ return;
2997
+ }
2998
+ const rect = el.getBoundingClientRect();
2999
+ const cx = rect.left + rect.width / 2;
3000
+ const cy = rect.top + rect.height / 2;
3001
+ this.#moveCursor(cx, cy);
3002
+ this.#ringAround(rect);
3003
+ await this.#pause();
3004
+ if (action === "click" || action === "dblclick" || action === "submit")
3005
+ this.#ripple(cx, cy);
3006
+ }
3007
+ #moveCursor(x, y) {
3008
+ if (this.#cursor === void 0)
3009
+ return;
3010
+ this.#cursor.setAttribute("data-on", "1");
3011
+ this.#cursor.style.transform = `translate(${String(x)}px, ${String(y)}px)`;
3012
+ }
3013
+ #ringAround(rect) {
3014
+ if (this.#ring === void 0)
3015
+ return;
3016
+ this.#ring.style.left = `${String(rect.left - 4)}px`;
3017
+ this.#ring.style.top = `${String(rect.top - 4)}px`;
3018
+ this.#ring.style.width = `${String(rect.width + 8)}px`;
3019
+ this.#ring.style.height = `${String(rect.height + 8)}px`;
3020
+ this.#ring.setAttribute("data-on", "1");
3021
+ nativeSetTimeout(() => this.#ring?.setAttribute("data-on", "0"), 700);
3022
+ }
3023
+ #ripple(x, y) {
3024
+ if (this.#root === void 0)
3025
+ return;
3026
+ const r = document.createElement("div");
3027
+ r.setAttribute("data-iris-ripple", "");
3028
+ r.style.left = `${String(x)}px`;
3029
+ r.style.top = `${String(y)}px`;
3030
+ this.#root.appendChild(r);
3031
+ nativeSetTimeout(() => r.remove(), 520);
3032
+ }
3033
+ #pause() {
3034
+ return new Promise((res) => nativeSetTimeout(res, this.#paceMs));
3035
+ }
3036
+ };
3037
+
3038
+ // ../browser/dist/recorder/recorder-styles.js
3039
+ var TOOLBAR_CSS = [
3040
+ "position:fixed",
3041
+ "top:8px",
3042
+ "left:50%",
3043
+ "transform:translateX(-50%)",
3044
+ "z-index:2147483647",
3045
+ "display:flex",
3046
+ "gap:6px",
3047
+ "align-items:center",
3048
+ "flex-wrap:wrap",
3049
+ "max-width:90vw",
3050
+ "font:12px ui-sans-serif,system-ui,sans-serif",
3051
+ "background:#151823",
3052
+ "color:#e6e9f0",
3053
+ "border:1px solid #2a2f3d",
3054
+ "border-radius:10px",
3055
+ "padding:6px 10px",
3056
+ "box-shadow:0 8px 30px rgba(0,0,0,.5)"
3057
+ ].join(";");
3058
+ var BTN_CSS = [
3059
+ "font:inherit",
3060
+ "cursor:pointer",
3061
+ "background:#262b3a",
3062
+ "color:#e6e9f0",
3063
+ "border:1px solid #3a4151",
3064
+ "border-radius:7px",
3065
+ "padding:3px 9px"
3066
+ ].join(";");
3067
+ var NAME_CSS = [
3068
+ "font:inherit",
3069
+ "background:#0e1018",
3070
+ "color:#e6e9f0",
3071
+ "border:1px solid #3a4151",
3072
+ "border-radius:7px",
3073
+ "padding:3px 8px"
3074
+ ].join(";");
3075
+ var STATUS_CSS = ["opacity:.75", "margin-left:4px"].join(";");
3076
+ var MENU_CSS = ["display:flex", "gap:6px", "align-items:center", "flex-wrap:wrap"].join(";");
3077
+
3078
+ // ../browser/dist/recorder/recorder.js
3079
+ var RECORDER_EMPTY_MSG = "recorded 0 steps";
3080
+ var STATUS_RECORDING = "recording\u2026";
3081
+ var STATUS_IDLE = "ready";
3082
+ var STATUS_ANNOTATE = "pick an annotation";
3083
+ var TESTID_ATTR2 = "data-testid";
3084
+ var TOOL = "iris_act";
3085
+ var BUTTON_LABEL = {
3086
+ record: "Record",
3087
+ stop: "Stop",
3088
+ annotate: "Annotate"
3089
+ };
3090
+ var ANNOTATION_LABEL = {
3091
+ [AnnotationKind.ASSERT_SIGNAL]: "assert signal",
3092
+ [AnnotationKind.ASSERT_VISIBLE]: "assert visible",
3093
+ [AnnotationKind.MARK_DYNAMIC]: "mark dynamic",
3094
+ [AnnotationKind.SUCCESS_STATE]: "success state"
3095
+ };
3096
+ var NEEDS_SIGNAL = /* @__PURE__ */ new Set([
3097
+ AnnotationKind.ASSERT_SIGNAL,
3098
+ AnnotationKind.SUCCESS_STATE
3099
+ ]);
3100
+ var DEFAULT_NAME = "recorded-flow";
3101
+ function anchorFor(el) {
3102
+ const testid = el.getAttribute(TESTID_ATTR2);
3103
+ if (testid !== null && testid.length > 0) {
3104
+ return { anchor: { kind: AnchorKind.TESTID, value: testid }, degraded: false };
3105
+ }
3106
+ const role = getRole(el);
3107
+ const name = getAccessibleName(el);
3108
+ if (role !== "generic" && (role.length > 0 || name.length > 0)) {
3109
+ const anchor = name.length > 0 ? { kind: AnchorKind.ROLE, role, name } : { kind: AnchorKind.ROLE, role };
3110
+ return { anchor, degraded: false };
3111
+ }
3112
+ return { anchor: { kind: AnchorKind.ROLE, role: DEGRADED_ANCHOR_ROLE }, degraded: true };
3113
+ }
3114
+ function isTextbox(el) {
3115
+ if (el instanceof HTMLTextAreaElement)
3116
+ return true;
3117
+ return el instanceof HTMLInputElement && inputRole2(el) === "textbox";
3118
+ }
3119
+ function inputRole2(el) {
3120
+ const type = el.type.toLowerCase();
3121
+ if (type === "checkbox")
3122
+ return "checkbox";
3123
+ if (type === "radio")
3124
+ return "radio";
3125
+ if (["text", "email", "tel", "url", "search", "password", ""].includes(type))
3126
+ return "textbox";
3127
+ return "other";
3128
+ }
3129
+ function buildStep(el, action, args) {
3130
+ const { anchor, degraded } = anchorFor(el);
3131
+ const step = { tool: TOOL, anchor, action };
3132
+ if (args !== void 0)
3133
+ step.args = args;
3134
+ if (degraded)
3135
+ step.degraded = true;
3136
+ return step;
3137
+ }
3138
+ function compileRecording(name, steps, annotations, createdAt) {
3139
+ const out = steps.map((s) => ({ ...s }));
3140
+ const dynamic = [];
3141
+ let success;
3142
+ for (const ann of annotations) {
3143
+ if (ann.kind === AnnotationKind.MARK_DYNAMIC) {
3144
+ dynamic.push(ann.anchor);
3145
+ continue;
3146
+ }
3147
+ if (ann.kind === AnnotationKind.SUCCESS_STATE) {
3148
+ if (ann.signal !== void 0)
3149
+ success = { signal: ann.signal };
3150
+ continue;
3151
+ }
3152
+ const target = out.at(-1);
3153
+ if (target === void 0)
3154
+ continue;
3155
+ if (ann.kind === AnnotationKind.ASSERT_SIGNAL && ann.signal !== void 0) {
3156
+ target.expect = { ...target.expect, signal: ann.signal };
3157
+ } else if (ann.kind === AnnotationKind.ASSERT_VISIBLE) {
3158
+ target.expect = { ...target.expect, element: anchorToElement(ann.anchor) };
3159
+ }
3160
+ }
3161
+ const flow = { version: FLOW_FILE_VERSION, name, createdAt, steps: out };
3162
+ if (dynamic.length > 0)
3163
+ flow.dynamic = dynamic;
3164
+ if (success !== void 0)
3165
+ flow.success = success;
3166
+ return flow;
3167
+ }
3168
+ function anchorToElement(anchor) {
3169
+ if (anchor.kind === AnchorKind.TESTID)
3170
+ return { testid: anchor.value };
3171
+ if (anchor.kind === AnchorKind.ROLE) {
3172
+ return anchor.name !== void 0 ? { role: anchor.role, name: anchor.name } : { role: anchor.role };
3173
+ }
3174
+ return {};
3175
+ }
3176
+ var Recorder = class {
3177
+ #deps;
3178
+ #phase = RecorderPhase.IDLE;
3179
+ #steps = [];
3180
+ #annotations = [];
3181
+ #pendingFill;
3182
+ #teardowns = [];
3183
+ #root;
3184
+ #statusEl;
3185
+ #menuEl;
3186
+ /** The annotation being assembled in ANNOTATING phase. */
3187
+ #draft;
3188
+ constructor(deps) {
3189
+ this.#deps = deps;
3190
+ }
3191
+ phase() {
3192
+ return this.#phase;
3193
+ }
3194
+ steps() {
3195
+ return this.#steps.map((s) => ({ ...s }));
3196
+ }
3197
+ mount() {
3198
+ if (this.#root !== void 0)
3199
+ return;
3200
+ if (typeof document === "undefined")
3201
+ return;
3202
+ this.#buildToolbar();
3203
+ this.#installCapture();
3204
+ }
3205
+ destroy() {
3206
+ for (const t of this.#teardowns)
3207
+ t();
3208
+ this.#teardowns = [];
3209
+ this.#root?.remove();
3210
+ this.#root = void 0;
3211
+ this.#statusEl = void 0;
3212
+ this.#menuEl = void 0;
3213
+ this.#phase = RecorderPhase.IDLE;
3214
+ this.#steps = [];
3215
+ this.#annotations = [];
3216
+ this.#draft = void 0;
3217
+ this.#pendingFill = void 0;
3218
+ }
3219
+ // ---- capture ----
3220
+ #installCapture() {
3221
+ const onClick = (ev) => this.#onClick(ev);
3222
+ const onInput = (ev) => this.#onInput(ev);
3223
+ const onChange = (ev) => this.#onChange(ev);
3224
+ const onSubmit = (ev) => this.#onSubmit(ev);
3225
+ document.addEventListener("click", onClick, true);
3226
+ document.addEventListener("input", onInput, true);
3227
+ document.addEventListener("change", onChange, true);
3228
+ document.addEventListener("submit", onSubmit, true);
3229
+ this.#teardowns.push(() => {
3230
+ document.removeEventListener("click", onClick, true);
3231
+ document.removeEventListener("input", onInput, true);
3232
+ document.removeEventListener("change", onChange, true);
3233
+ document.removeEventListener("submit", onSubmit, true);
3234
+ });
3235
+ }
3236
+ /** True if the event should be ignored (toolbar self-click or non-element target). */
3237
+ #ignore(ev) {
3238
+ const target = ev.target;
3239
+ if (!(target instanceof Element))
3240
+ return void 0;
3241
+ if (isIrisOverlay(target))
3242
+ return void 0;
3243
+ return target;
3244
+ }
3245
+ #onClick(ev) {
3246
+ const target = this.#ignore(ev);
3247
+ if (target === void 0)
3248
+ return;
3249
+ if (this.#phase === RecorderPhase.ANNOTATING) {
3250
+ this.#captureAnnotationTarget(target);
3251
+ return;
3252
+ }
3253
+ if (this.#phase !== RecorderPhase.RECORDING)
3254
+ return;
3255
+ this.#flushPendingFill();
3256
+ this.#steps.push(buildStep(target, ActionType.CLICK));
3257
+ }
3258
+ #onInput(ev) {
3259
+ if (this.#phase !== RecorderPhase.RECORDING)
3260
+ return;
3261
+ const target = this.#ignore(ev);
3262
+ if (target === void 0 || !isTextbox(target))
3263
+ return;
3264
+ this.#pendingFill = { el: target, value: target.value };
3265
+ }
3266
+ #onChange(ev) {
3267
+ if (this.#phase !== RecorderPhase.RECORDING)
3268
+ return;
3269
+ const target = this.#ignore(ev);
3270
+ if (target === void 0)
3271
+ return;
3272
+ if (target instanceof HTMLInputElement && inputRole2(target) === "checkbox") {
3273
+ this.#flushPendingFill();
3274
+ this.#steps.push(buildStep(target, target.checked ? ActionType.CHECK : ActionType.UNCHECK));
3275
+ return;
3276
+ }
3277
+ if (target instanceof HTMLInputElement && inputRole2(target) === "radio") {
3278
+ this.#flushPendingFill();
3279
+ this.#steps.push(buildStep(target, ActionType.CHECK));
3280
+ return;
3281
+ }
3282
+ if (isTextbox(target)) {
3283
+ this.#pendingFill = { el: target, value: target.value };
3284
+ this.#flushPendingFill();
3285
+ }
3286
+ }
3287
+ #onSubmit(ev) {
3288
+ if (this.#phase !== RecorderPhase.RECORDING)
3289
+ return;
3290
+ const target = this.#ignore(ev);
3291
+ if (target === void 0)
3292
+ return;
3293
+ this.#flushPendingFill();
3294
+ this.#steps.push(buildStep(target, ActionType.SUBMIT));
3295
+ }
3296
+ #flushPendingFill() {
3297
+ const pending = this.#pendingFill;
3298
+ if (pending === void 0)
3299
+ return;
3300
+ this.#pendingFill = void 0;
3301
+ this.#steps.push(buildStep(pending.el, ActionType.FILL, { value: pending.value }));
3302
+ }
3303
+ // ---- annotation ----
3304
+ #captureAnnotationTarget(target) {
3305
+ const draft = this.#draft;
3306
+ if (draft === void 0)
3307
+ return;
3308
+ const { anchor } = anchorFor(target);
3309
+ const ann = { kind: draft.kind, anchor };
3310
+ const signal = this.#selectedSignal();
3311
+ if (signal !== void 0)
3312
+ ann.signal = signal;
3313
+ this.#annotations.push(ann);
3314
+ this.#draft = void 0;
3315
+ this.#setPhase(RecorderPhase.RECORDING);
3316
+ this.#closeMenu();
3317
+ }
3318
+ /** Annotate-on-prior: confirm the most-recent step as the target (no extra click needed). */
3319
+ #confirmAnnotationOnPrior() {
3320
+ const draft = this.#draft;
3321
+ if (draft === void 0)
3322
+ return;
3323
+ const last = this.#steps.at(-1);
3324
+ if (last === void 0) {
3325
+ this.#draft = void 0;
3326
+ this.#setPhase(RecorderPhase.RECORDING);
3327
+ this.#closeMenu();
3328
+ return;
3329
+ }
3330
+ const ann = { kind: draft.kind, anchor: last.anchor };
3331
+ const signal = this.#selectedSignal();
3332
+ if (signal !== void 0)
3333
+ ann.signal = signal;
3334
+ this.#annotations.push(ann);
3335
+ this.#draft = void 0;
3336
+ this.#setPhase(RecorderPhase.RECORDING);
3337
+ this.#closeMenu();
3338
+ }
3339
+ #selectedSignal() {
3340
+ const select = this.#menuEl?.querySelector("[data-iris-signal]");
3341
+ const value = select?.value ?? "";
3342
+ return value.length > 0 ? value : void 0;
3343
+ }
3344
+ // ---- toolbar lifecycle actions ----
3345
+ #start() {
3346
+ this.#steps = [];
3347
+ this.#annotations = [];
3348
+ this.#pendingFill = void 0;
3349
+ this.#draft = void 0;
3350
+ this.#setPhase(RecorderPhase.RECORDING);
3351
+ this.#setStatus(STATUS_RECORDING);
3352
+ }
3353
+ #stop() {
3354
+ this.#flushPendingFill();
3355
+ const name = this.#nameField() ?? this.#deps.defaultName ?? DEFAULT_NAME;
3356
+ const flow = compileRecording(name, this.#steps, this.#annotations, this.#deps.now());
3357
+ this.#deps.emit(EventType.FLOW_RECORDED, { name, flow });
3358
+ this.#setStatus(this.#steps.length === 0 ? RECORDER_EMPTY_MSG : STATUS_IDLE);
3359
+ this.#setPhase(RecorderPhase.IDLE);
3360
+ this.#closeMenu();
3361
+ }
3362
+ #openAnnotateMenu() {
3363
+ if (this.#phase !== RecorderPhase.RECORDING)
3364
+ return;
3365
+ this.#setPhase(RecorderPhase.ANNOTATING);
3366
+ this.#setStatus(STATUS_ANNOTATE);
3367
+ this.#renderMenu();
3368
+ }
3369
+ #setPhase(phase) {
3370
+ this.#phase = phase;
3371
+ }
3372
+ // ---- toolbar DOM (all nodes data-iris-overlay → snapshot-excluded via dom-ignore.ts) ----
3373
+ #buildToolbar() {
3374
+ const root = document.createElement("div");
3375
+ root.setAttribute("data-iris-overlay", "");
3376
+ root.style.cssText = TOOLBAR_CSS;
3377
+ const name = document.createElement("input");
3378
+ name.setAttribute("data-iris-name", "");
3379
+ name.setAttribute("placeholder", "flow name");
3380
+ name.style.cssText = NAME_CSS;
3381
+ root.appendChild(name);
3382
+ root.appendChild(this.#button("record", BUTTON_LABEL.record, () => this.#start()));
3383
+ root.appendChild(this.#button("stop", BUTTON_LABEL.stop, () => this.#stop()));
3384
+ root.appendChild(this.#button("annotate", BUTTON_LABEL.annotate, () => this.#openAnnotateMenu()));
3385
+ const status = document.createElement("span");
3386
+ status.setAttribute("data-iris-status", "");
3387
+ status.style.cssText = STATUS_CSS;
3388
+ status.textContent = STATUS_IDLE;
3389
+ root.appendChild(status);
3390
+ const menu = document.createElement("div");
3391
+ menu.setAttribute("data-iris-menu", "");
3392
+ menu.style.cssText = MENU_CSS;
3393
+ root.appendChild(menu);
3394
+ document.body.appendChild(root);
3395
+ this.#root = root;
3396
+ this.#statusEl = status;
3397
+ this.#menuEl = menu;
3398
+ }
3399
+ #button(action, label, onClick) {
3400
+ const btn = document.createElement("button");
3401
+ btn.setAttribute("data-iris-action", action);
3402
+ btn.textContent = label;
3403
+ btn.style.cssText = BTN_CSS;
3404
+ btn.addEventListener("click", onClick);
3405
+ return btn;
3406
+ }
3407
+ #renderMenu() {
3408
+ const menu = this.#menuEl;
3409
+ if (menu === void 0)
3410
+ return;
3411
+ menu.textContent = "";
3412
+ for (const kind of Object.values(AnnotationKind)) {
3413
+ const item = document.createElement("button");
3414
+ item.setAttribute("data-iris-annkind", kind);
3415
+ item.textContent = ANNOTATION_LABEL[kind];
3416
+ item.style.cssText = BTN_CSS;
3417
+ item.addEventListener("click", () => this.#chooseKind(kind));
3418
+ menu.appendChild(item);
3419
+ }
3420
+ }
3421
+ #chooseKind(kind) {
3422
+ this.#draft = { kind };
3423
+ const menu = this.#menuEl;
3424
+ if (menu === void 0)
3425
+ return;
3426
+ if (NEEDS_SIGNAL.has(kind)) {
3427
+ const select = document.createElement("select");
3428
+ select.setAttribute("data-iris-signal", "");
3429
+ select.style.cssText = NAME_CSS;
3430
+ for (const sig of getCapabilities().signals) {
3431
+ const opt = document.createElement("option");
3432
+ opt.value = sig;
3433
+ opt.textContent = sig;
3434
+ select.appendChild(opt);
3435
+ }
3436
+ menu.appendChild(select);
3437
+ }
3438
+ const confirm = document.createElement("button");
3439
+ confirm.setAttribute("data-iris-action", "annotate-confirm");
3440
+ confirm.textContent = "on prior step";
3441
+ confirm.style.cssText = BTN_CSS;
3442
+ confirm.addEventListener("click", () => this.#confirmAnnotationOnPrior());
3443
+ menu.appendChild(confirm);
3444
+ }
3445
+ #closeMenu() {
3446
+ if (this.#menuEl !== void 0)
3447
+ this.#menuEl.textContent = "";
3448
+ }
3449
+ #setStatus(text) {
3450
+ if (this.#statusEl !== void 0)
3451
+ this.#statusEl.textContent = text;
3452
+ }
3453
+ #nameField() {
3454
+ const input = this.#root?.querySelector("[data-iris-name]");
3455
+ const value = input?.value.trim() ?? "";
3456
+ return value.length > 0 ? value : void 0;
3457
+ }
3458
+ };
3459
+ function installRecorder(deps) {
3460
+ return new Recorder(deps);
3461
+ }
3462
+
3463
+ // ../browser/dist/iris.js
3464
+ function str2(value, fallback = "") {
3465
+ return typeof value === "string" ? value : fallback;
3466
+ }
3467
+ var BRIDGE_LOST_SUMMARY = "Session ended \u2014 lost connection to Iris (the agent is no longer running).";
3468
+ function resolveSessionLabel(option, gen) {
3469
+ return option === void 0 || option === SESSION_AUTO ? gen() : option;
3470
+ }
3471
+ function isSessionState(value) {
3472
+ return value === SessionState.ACTIVE || value === SessionState.PAUSED || value === SessionState.ENDED;
3473
+ }
3474
+ function refLabel(refId) {
3475
+ const el = refs.resolve(refId);
3476
+ if (!(el instanceof Element))
3477
+ return refId;
3478
+ const d = describe(el);
3479
+ return d.name.length > 0 ? `${d.role} "${d.name}"` : `${d.role} (${refId})`;
3480
+ }
3481
+ var Iris = class {
3482
+ #transport;
3483
+ #registry = /* @__PURE__ */ new Map();
3484
+ #teardowns = [];
3485
+ #connected = false;
3486
+ #session = "default";
3487
+ #start = 0;
3488
+ #overlay;
3489
+ #presenter;
3490
+ #recorder;
3491
+ #eventCount = 0;
3492
+ /** Act-row log handle for the in-flight act/act_sequence, so its outcome stamps the right row. */
3493
+ #actHandle;
3494
+ connect(options = {}) {
3495
+ if (this.#connected)
3496
+ return;
3497
+ if (typeof window === "undefined" || typeof document === "undefined")
3498
+ return;
3499
+ this.#session = resolveSessionLabel(options.session, () => `s${Date.now().toString(36)}`);
3500
+ this.#start = performance.now();
3501
+ this.#registry = createCommandRegistry();
3502
+ const url = options.url ?? `ws://localhost:${String(IRIS_DEFAULT_PORT)}${IRIS_WS_PATH}`;
3503
+ this.#transport = new Transport({
3504
+ url,
3505
+ hello: () => this.#hello(),
3506
+ handleCommand: (command) => this.#handleCommand(command),
3507
+ // Liveness fallback: if the bridge stays unreachable (the agent killed the server process),
3508
+ // no server-pushed end can arrive — so end the run we're presenting ourselves. A returning
3509
+ // agent revives it via the normal sessionStart() path on its next command.
3510
+ onConnectionLost: () => {
3511
+ if (this.#presenter?.sessionActive === true) {
3512
+ this.#presenter.setState(SessionState.ENDED, BRIDGE_LOST_SUMMARY);
3513
+ }
3514
+ }
3515
+ });
3516
+ const emit = this.#emit;
3517
+ this.#teardowns = [
3518
+ installNetwork(emit),
3519
+ installRoute(emit),
3520
+ installConsole(emit),
3521
+ installAnimation(emit),
3522
+ installScroll(emit),
3523
+ installDom(emit),
3524
+ installHealth(emit)
3525
+ // page visibility/focus health + heartbeat
3526
+ ];
3527
+ if (options.overlay === true) {
3528
+ this.#overlay = installOverlay();
3529
+ this.#overlay.update({ connected: true, events: 0 });
3530
+ }
3531
+ if (options.present === true) {
3532
+ const presenterOptions = {};
3533
+ if (options.pace !== void 0)
3534
+ presenterOptions.paceMs = options.pace;
3535
+ if (options.narrationDwellMs !== void 0) {
3536
+ presenterOptions.narrationDwellMs = options.narrationDwellMs;
3537
+ }
3538
+ if (options.border !== void 0)
3539
+ presenterOptions.border = options.border;
3540
+ if (options.logMax !== void 0)
3541
+ presenterOptions.logMax = options.logMax;
3542
+ if (options.endedFadeMs !== void 0)
3543
+ presenterOptions.endedFadeMs = options.endedFadeMs;
3544
+ if (options.idleEndMs !== void 0)
3545
+ presenterOptions.idleEndMs = options.idleEndMs;
3546
+ presenterOptions.sessionId = this.#session;
3547
+ presenterOptions.onControl = (intent) => this.#emit(EventType.HUMAN_CONTROL, intent.text !== void 0 ? { kind: intent.kind, text: intent.text } : { kind: intent.kind });
3548
+ this.#presenter = new Presenter(presenterOptions);
3549
+ this.#presenter.mount();
3550
+ }
3551
+ if (options.recorder === true) {
3552
+ this.#recorder = installRecorder({ emit, now: () => Date.now() });
3553
+ this.#recorder.mount();
3554
+ }
3555
+ this.#transport.connect();
3556
+ this.#connected = true;
3557
+ }
3558
+ /** Whether the in-page SDK is connected to the bridge (read by createIrisEmitter, P5a). */
3559
+ get connected() {
3560
+ return this.#connected;
3561
+ }
3562
+ /** Surface an arbitrary app-domain observation the DOM can't express (plan/03 §7). */
3563
+ signal(name, data = {}) {
3564
+ this.#emit(EventType.SIGNAL, { name, data });
3565
+ }
3566
+ /** Report a framework/store state change the agent can observe and assert on. */
3567
+ state(name, value) {
3568
+ this.#emit(EventType.STATE_CHANGE, { name, value });
3569
+ }
3570
+ /** Advertise the app's testable surface so the agent learns it without reading source. */
3571
+ describe(input) {
3572
+ registerCapabilities(input);
3573
+ }
3574
+ /** Live-control: end the session programmatically from the host app (drives the panel to ended). */
3575
+ endSession() {
3576
+ this.#presenter?.setState(SessionState.ENDED);
3577
+ }
3578
+ disconnect() {
3579
+ if (!this.#connected)
3580
+ return;
3581
+ for (const teardown of this.#teardowns)
3582
+ teardown();
3583
+ this.#teardowns = [];
3584
+ this.#transport?.close();
3585
+ this.#transport = void 0;
3586
+ this.#overlay?.destroy();
3587
+ this.#overlay = void 0;
3588
+ this.#presenter?.sessionEnd();
3589
+ this.#presenter?.destroy();
3590
+ this.#presenter = void 0;
3591
+ this.#recorder?.destroy();
3592
+ this.#recorder = void 0;
3593
+ resetClock();
3594
+ this.#connected = false;
3595
+ }
3596
+ #emit = (type, data, ref) => {
3597
+ const event = {
3598
+ t: Math.round(performance.now() - this.#start),
3599
+ type,
3600
+ sessionId: this.#session,
3601
+ ref,
3602
+ data
3603
+ };
3604
+ this.#transport?.sendEvent(event);
3605
+ this.#eventCount += 1;
3606
+ this.#overlay?.update({ connected: true, events: this.#eventCount });
3607
+ };
3608
+ #hello() {
3609
+ return {
3610
+ kind: MessageKind.HELLO,
3611
+ protocolVersion: IRIS_PROTOCOL_VERSION,
3612
+ sessionId: this.#session,
3613
+ url: location.href,
3614
+ title: document.title,
3615
+ adapters: adapterNames(),
3616
+ hasCapabilities: hasCapabilities()
3617
+ };
3618
+ }
3619
+ async #handleCommand(command) {
3620
+ if (command.name === IrisCommand.NARRATE) {
3621
+ this.#presenter?.sessionStart();
3622
+ this.#presenter?.narrate(str2(command.args["text"]), str2(command.args["level"], "info"));
3623
+ return { ok: true, result: { shown: this.#presenter !== void 0 } };
3624
+ }
3625
+ if (command.name === IrisCommand.SESSION_CONFIG) {
3626
+ const idleEndMs = command.args["idleEndMs"];
3627
+ if (typeof idleEndMs === "number")
3628
+ this.#presenter?.setIdleEndMs(idleEndMs);
3629
+ return { ok: true, result: { applied: this.#presenter !== void 0, idleEndMs } };
3630
+ }
3631
+ if (command.name === IrisCommand.PRESENTER) {
3632
+ const state = command.args["state"];
3633
+ if (isSessionState(state)) {
3634
+ this.#presenter?.setState(state, str2(command.args["text"]) || void 0);
3635
+ }
3636
+ return { ok: true, result: { applied: this.#presenter !== void 0 } };
3637
+ }
3638
+ const handler = this.#registry.get(command.name);
3639
+ if (handler === void 0) {
3640
+ return { ok: false, error: `unknown command '${command.name}'` };
3641
+ }
3642
+ this.#presenter?.sessionStart();
3643
+ await this.#presentBefore(command);
3644
+ try {
3645
+ const result2 = await handler(command.args);
3646
+ this.#actHandle?.result(LOG_RESULT.PASS);
3647
+ return { ok: true, result: result2 };
3648
+ } catch (error) {
3649
+ this.#actHandle?.result(LOG_RESULT.FAIL);
3650
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
3651
+ } finally {
3652
+ this.#actHandle = void 0;
3653
+ this.#presenter?.scheduleIdle();
3654
+ }
3655
+ }
3656
+ /** Drive the presenter (cursor/effects/status) before the real action runs. */
3657
+ async #presentBefore(command) {
3658
+ const p = this.#presenter;
3659
+ if (p === void 0)
3660
+ return;
3661
+ p.setMode(modeForCommand(command.name));
3662
+ this.#actHandle = void 0;
3663
+ if (command.name === IrisCommand.ACT) {
3664
+ const ref = str2(command.args["ref"]);
3665
+ const label = refLabel(ref);
3666
+ this.#actHandle = p.log(LOG_KIND.ACT, `${actionVerb(str2(command.args["action"]))} ${label}`);
3667
+ await p.beforeAct(ref, str2(command.args["action"]), label);
3668
+ } else if (command.name === IrisCommand.ACT_SEQUENCE) {
3669
+ const steps = Array.isArray(command.args["steps"]) ? command.args["steps"] : [];
3670
+ for (const step of steps) {
3671
+ const s = step;
3672
+ const ref = str2(s.ref);
3673
+ const label = refLabel(ref);
3674
+ this.#actHandle = p.log(LOG_KIND.ACT, `${actionVerb(str2(s.action))} ${label}`);
3675
+ await p.beforeAct(ref, str2(s.action), label);
3676
+ }
3677
+ } else {
3678
+ const label = presentStatus(command.name, command.args);
3679
+ p.status(label);
3680
+ p.log(LOG_KIND.READ, label);
3681
+ }
3682
+ }
3683
+ };
3684
+ function modeForCommand(commandName) {
3685
+ switch (commandName) {
3686
+ case IrisCommand.ACT:
3687
+ case IrisCommand.ACT_SEQUENCE:
3688
+ return PresenterMode.ACTING;
3689
+ case IrisCommand.SNAPSHOT:
3690
+ case IrisCommand.QUERY:
3691
+ case IrisCommand.MATCH:
3692
+ case IrisCommand.INSPECT:
3693
+ case IrisCommand.ANIMATIONS:
3694
+ case IrisCommand.STATE_READ:
3695
+ case IrisCommand.CAPABILITIES:
3696
+ return PresenterMode.READING;
3697
+ default:
3698
+ return PresenterMode.IDLE;
3699
+ }
3700
+ }
3701
+ function presentStatus(commandName, args = {}) {
3702
+ switch (commandName) {
3703
+ case IrisCommand.SNAPSHOT:
3704
+ return "Looking at the page";
3705
+ case IrisCommand.QUERY:
3706
+ case IrisCommand.MATCH: {
3707
+ const q = commandName === IrisCommand.MATCH ? args["query"] ?? {} : args;
3708
+ const target = queryTarget(q);
3709
+ return target !== void 0 ? `Finding ${target}` : "Finding an element";
3710
+ }
3711
+ case IrisCommand.INSPECT: {
3712
+ const ref = str2(args["ref"]);
3713
+ return ref !== void 0 ? `Inspecting ${refLabel(ref)}` : "Inspecting an element";
3714
+ }
3715
+ case IrisCommand.ANIMATIONS:
3716
+ return "Reading animations";
3717
+ case IrisCommand.STATE_READ: {
3718
+ const store = str2(args["store"]);
3719
+ return store !== void 0 ? `Reading state: ${store}` : "Reading state";
3720
+ }
3721
+ case IrisCommand.CAPABILITIES:
3722
+ return "Reading capabilities";
3723
+ default:
3724
+ return commandName;
3725
+ }
3726
+ }
3727
+ function queryTarget(q) {
3728
+ const testid = str2(q["testid"]) ?? (str2(q["by"]) === "testid" ? str2(q["value"]) : void 0);
3729
+ if (testid !== void 0)
3730
+ return `[testid=${testid}]`;
3731
+ const name = str2(q["name"]);
3732
+ const value = str2(q["value"]) ?? str2(q["text"]) ?? str2(q["label"]) ?? str2(q["role"]);
3733
+ if (value !== void 0)
3734
+ return name !== void 0 ? `"${value}" (${name})` : `"${value}"`;
3735
+ return name !== void 0 ? `"${name}"` : void 0;
3736
+ }
3737
+
3738
+ // ../browser/dist/registry/emitter.js
3739
+ function createIrisEmitter(options = {}) {
3740
+ const target = options.target ?? iris;
3741
+ return {
3742
+ signal(name, data = {}) {
3743
+ if (!target.connected)
3744
+ return;
3745
+ target.signal(name, data);
3746
+ },
3747
+ state(name, value) {
3748
+ if (!target.connected)
3749
+ return;
3750
+ target.state(name, value);
3751
+ }
3752
+ };
3753
+ }
3754
+
3755
+ // ../browser/dist/registry/commit-and-signal.js
3756
+ function commitAndSignal(emitter, mutate, name, data) {
3757
+ const result2 = mutate();
3758
+ emitter.signal(name, data);
3759
+ return result2;
3760
+ }
3761
+
3762
+ // ../browser/dist/registry/domains.js
3763
+ function registerIrisDomain(domain) {
3764
+ const input = {};
3765
+ if (domain.testids !== void 0)
3766
+ input.testids = [...domain.testids];
3767
+ if (domain.signals !== void 0)
3768
+ input.signals = [...domain.signals];
3769
+ if (domain.stores !== void 0)
3770
+ input.stores = [...domain.stores];
3771
+ registerCapabilities(input);
3772
+ }
3773
+
3774
+ // ../browser/dist/index.js
3775
+ var globalStore4 = globalThis;
3776
+ var iris = globalStore4.__irisInstance ??= new Iris();
3777
+
3778
+ // ../react/dist/index.js
3779
+ var FIBER_PREFIXES = ["__reactFiber$", "__reactInternalInstance$"];
3780
+ var MAX_DEPTH = 200;
3781
+ var MAX_HOOKS = 100;
3782
+ var MAX_SERIALIZE_DEPTH = 4;
3783
+ var MAX_SERIALIZE_KEYS = 50;
3784
+ var MAX_SERIALIZE_ITEMS = 50;
3785
+ var DOM_NODE_MARKER = "[Node]";
3786
+ var FRAMEWORK_NOISE = /(Context|Boundary|Provider|Router|Handler)$|^(Root|ServerRoot|HotReload|Fragment|__next)/;
3787
+ function isFrameworkNoise(name) {
3788
+ return FRAMEWORK_NOISE.test(name);
3789
+ }
3790
+ function getFiber(el) {
3791
+ const key = Object.keys(el).find((k) => FIBER_PREFIXES.some((p) => k.startsWith(p)));
3792
+ if (key === void 0)
3793
+ return null;
3794
+ const value = el[key];
3795
+ return value ?? null;
3796
+ }
3797
+ function componentName(type) {
3798
+ if (typeof type === "function") {
3799
+ const fn = type;
3800
+ if (fn.displayName !== void 0 && fn.displayName.length > 0)
3801
+ return fn.displayName;
3802
+ return fn.name !== void 0 && fn.name.length > 0 ? fn.name : null;
3803
+ }
3804
+ if (typeof type === "object" && type !== null) {
3805
+ const obj = type;
3806
+ return obj.displayName !== void 0 && obj.displayName.length > 0 ? obj.displayName : null;
3807
+ }
3808
+ return null;
3809
+ }
3810
+ function identify(el) {
3811
+ let fiber = getFiber(el);
3812
+ const stack = [];
3813
+ const rawStack = [];
3814
+ let source;
3815
+ let depth = 0;
3816
+ while (fiber !== null && depth < MAX_DEPTH) {
3817
+ depth += 1;
3818
+ const name = componentName(fiber.elementType ?? fiber.type);
3819
+ if (name !== null && rawStack[rawStack.length - 1] !== name) {
3820
+ rawStack.push(name);
3821
+ if (!isFrameworkNoise(name) && stack[stack.length - 1] !== name)
3822
+ stack.push(name);
3823
+ }
3824
+ if (source === void 0 && fiber._debugSource !== void 0) {
3825
+ source = { file: fiber._debugSource.fileName, line: fiber._debugSource.lineNumber };
3826
+ if (fiber._debugSource.columnNumber !== void 0) {
3827
+ source.column = fiber._debugSource.columnNumber;
3828
+ }
3829
+ }
3830
+ fiber = fiber.return;
3831
+ }
3832
+ if (source === void 0) {
3833
+ source = sourceFromAttribute(el);
3834
+ }
3835
+ const componentStack = stack.length > 0 ? stack : rawStack.slice(0, 1);
3836
+ if (componentStack.length === 0 && source === void 0)
3837
+ return null;
3838
+ const info = { componentStack };
3839
+ if (source !== void 0)
3840
+ info.source = source;
3841
+ return info;
3842
+ }
3843
+ function sourceFromAttribute(el) {
3844
+ const stamped = el.closest("[data-iris-source]");
3845
+ const raw = stamped?.getAttribute("data-iris-source");
3846
+ if (raw === null || raw === void 0)
3847
+ return void 0;
3848
+ const match = /^(.*):(\d+):(\d+)$/.exec(raw);
3849
+ if (match === null)
3850
+ return void 0;
3851
+ const [, file, line, column] = match;
3852
+ if (file === void 0 || line === void 0 || column === void 0)
3853
+ return void 0;
3854
+ return { file, line: Number(line), column: Number(column) };
3855
+ }
3856
+ function nearestComponentFiber(el) {
3857
+ let fiber = getFiber(el);
3858
+ let depth = 0;
3859
+ while (fiber !== null && depth < MAX_DEPTH) {
3860
+ depth += 1;
3861
+ if (typeof fiber.type === "function" || typeof fiber.elementType === "function")
3862
+ return fiber;
3863
+ fiber = fiber.return;
3864
+ }
3865
+ return null;
3866
+ }
3867
+ function safeValue(value, depth, seen) {
3868
+ if (value === null)
3869
+ return null;
3870
+ const t = typeof value;
3871
+ if (t === "string" || t === "number" || t === "boolean")
3872
+ return value;
3873
+ if (t === "undefined" || t === "function" || t === "symbol" || t === "bigint")
3874
+ return null;
3875
+ if (typeof Node !== "undefined" && value instanceof Node)
3876
+ return DOM_NODE_MARKER;
3877
+ if (depth >= MAX_SERIALIZE_DEPTH)
3878
+ return null;
3879
+ const obj = value;
3880
+ if (seen.has(obj))
3881
+ return null;
3882
+ seen.add(obj);
3883
+ if (Array.isArray(value)) {
3884
+ const out2 = value.slice(0, MAX_SERIALIZE_ITEMS).map((v) => safeValue(v, depth + 1, seen));
3885
+ seen.delete(obj);
3886
+ return out2;
3887
+ }
3888
+ const out = {};
3889
+ const keys = Object.keys(obj).slice(0, MAX_SERIALIZE_KEYS);
3890
+ for (const key of keys) {
3891
+ out[key] = safeValue(obj[key], depth + 1, seen);
3892
+ }
3893
+ seen.delete(obj);
3894
+ return out;
3895
+ }
3896
+ function ok(name, hooks) {
3897
+ return name === null ? { ok: true, hooks } : { ok: true, component: name, hooks };
3898
+ }
3899
+ function readState2(el) {
3900
+ try {
3901
+ const fiber = nearestComponentFiber(el);
3902
+ if (fiber === null) {
3903
+ return { ok: false, reason: ComponentStateReason.UNAVAILABLE };
3904
+ }
3905
+ const name = componentName(fiber.elementType ?? fiber.type);
3906
+ const head = fiber.memoizedState;
3907
+ if (typeof head !== "object" || head === null) {
3908
+ return ok(name, []);
3909
+ }
3910
+ const hooks = [];
3911
+ const seen = /* @__PURE__ */ new WeakSet();
3912
+ let hook = head;
3913
+ let i = 0;
3914
+ while (typeof hook === "object" && i < MAX_HOOKS) {
3915
+ hooks.push(safeValue(hook.memoizedState, 0, seen));
3916
+ const next = hook.next;
3917
+ if (next === null || typeof next !== "object")
3918
+ break;
3919
+ hook = next;
3920
+ i += 1;
3921
+ }
3922
+ return ok(name, hooks);
3923
+ } catch {
3924
+ return { ok: false, reason: ComponentStateReason.UNAVAILABLE };
3925
+ }
3926
+ }
3927
+ var HOVER_HANDLER_KEYS = [
3928
+ "onMouseEnter",
3929
+ "onMouseLeave",
3930
+ "onPointerEnter",
3931
+ "onPointerLeave"
3932
+ ];
3933
+ function hasHoverHandlers(el) {
3934
+ const fiber = getFiber(el);
3935
+ const props = fiber?.memoizedProps;
3936
+ if (typeof props !== "object" || props === null)
3937
+ return false;
3938
+ const p = props;
3939
+ return HOVER_HANDLER_KEYS.some((k) => typeof p[k] === "function");
3940
+ }
3941
+ var installed2 = false;
3942
+ function install() {
3943
+ if (installed2)
3944
+ return;
3945
+ installed2 = true;
3946
+ registerAdapter({ name: "react", identify, readState: readState2, hasHoverHandlers });
3947
+ }
3948
+ export {
3949
+ Iris,
3950
+ RefRegistry,
3951
+ SESSION_AUTO,
3952
+ adapterNames,
3953
+ buildSnapshot,
3954
+ commitAndSignal,
3955
+ createIrisEmitter,
3956
+ describe,
3957
+ elementHasHoverHandlers,
3958
+ executeAction,
3959
+ executeSequence,
3960
+ getAccessibleName,
3961
+ getCapabilities,
3962
+ getRole,
3963
+ getStates,
3964
+ hasCapabilities,
3965
+ hasHoverHandlers,
3966
+ identify,
3967
+ identifyComponent,
3968
+ install,
3969
+ iris,
3970
+ isVisible,
3971
+ matchQuery,
3972
+ readComponentState,
3973
+ readState2 as readState,
3974
+ readStores,
3975
+ refs,
3976
+ registerAdapter,
3977
+ registerCapabilities,
3978
+ registerIrisDomain,
3979
+ registerStore,
3980
+ runQuery,
3981
+ setIgnoreSelectors,
3982
+ storeNames,
3983
+ unregisterStore
3984
+ };