@syrin/iris 0.6.10 → 0.8.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 CHANGED
@@ -1,32 +1,4 @@
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 TRANSPORT_LIMITS = {
6
- MAX_MESSAGE_BYTES: 1024 * 1024,
7
- MAX_MESSAGES_PER_SECOND: 1e3,
8
- MAX_SESSIONS: 32,
9
- MAX_PENDING_CONNECTIONS: 16,
10
- HELLO_TIMEOUT_MS: 5e3,
11
- MAX_BUFFER_BYTES: 8 * 1024 * 1024,
12
- MAX_SESSION_ID_LENGTH: 128,
13
- MAX_URL_LENGTH: 4096,
14
- MAX_TITLE_LENGTH: 512,
15
- MAX_ADAPTERS: 32,
16
- MAX_ADAPTER_NAME_LENGTH: 128,
17
- MAX_TOKEN_LENGTH: 512,
18
- MAX_COMMAND_ID_LENGTH: 128,
19
- MAX_COMMAND_NAME_LENGTH: 128,
20
- MAX_REF_LENGTH: 128,
21
- MAX_ERROR_LENGTH: 4096,
22
- MAX_SERIALIZE_DEPTH: 8,
23
- MAX_COLLECTION_ITEMS: 200,
24
- MAX_OBJECT_KEYS: 200,
25
- MAX_STRING_LENGTH: 64 * 1024
26
- };
27
- var REDACTED_VALUE = "[REDACTED]";
28
- var DANGEROUS_ACTION_CONFIRM_ARG = "confirmDangerous";
29
- var UpdateCheckIntervalMs = 24 * 60 * 60 * 1e3;
1
+ // ../protocol/dist/flow-constants.js
30
2
  var RunKind = {
31
3
  FLOW_REPLAY: "flow_replay",
32
4
  // auto-recorded by iris_flow_replay
@@ -45,8 +17,10 @@ var AnchorKind = {
45
17
  // { kind:'testid', value }
46
18
  ROLE: "role",
47
19
  // { kind:'role', role, name? } — best-effort fallback
48
- SIGNAL: "signal"
20
+ SIGNAL: "signal",
49
21
  // { kind:'signal', name } — wait/assert anchors
22
+ COMPONENT: "component"
23
+ // { kind:'component', component?, source?, role?, name? } — auto-anchor (no testid)
50
24
  };
51
25
  var DEGRADED_ANCHOR_ROLE = "unresolved";
52
26
  var AnnotationKind = {
@@ -54,10 +28,14 @@ var AnnotationKind = {
54
28
  // → step.expect.signal (invariant)
55
29
  ASSERT_VISIBLE: "assert-visible",
56
30
  // → step.expect.element (invariant)
31
+ ASSERT_STATE: "assert-state",
32
+ // → step.expect.state (store-truth invariant on the last step)
57
33
  MARK_DYNAMIC: "mark-dynamic",
58
34
  // → flow.dynamic[] (don't assert words/content)
59
- SUCCESS_STATE: "success-state"
35
+ SUCCESS_STATE: "success-state",
60
36
  // → flow.success (golden end condition)
37
+ INTENT: "intent"
38
+ // → flow.intent (the business goal this flow exists to verify)
61
39
  };
62
40
  var RecorderPhase = {
63
41
  IDLE: "idle",
@@ -67,6 +45,40 @@ var RecorderPhase = {
67
45
  ANNOTATING: "annotating"
68
46
  // recording paused, awaiting an annotation target/kind
69
47
  };
48
+
49
+ // ../protocol/dist/constants.js
50
+ var IRIS_DEFAULT_PORT = 4400;
51
+ var IRIS_WS_PATH = "/iris";
52
+ var IRIS_PROTOCOL_VERSION = 1;
53
+ var TRANSPORT_LIMITS = {
54
+ MAX_MESSAGE_BYTES: 1024 * 1024,
55
+ MAX_MESSAGES_PER_SECOND: 1e3,
56
+ MAX_SESSIONS: 32,
57
+ MAX_PENDING_CONNECTIONS: 16,
58
+ HELLO_TIMEOUT_MS: 5e3,
59
+ MAX_BUFFER_BYTES: 8 * 1024 * 1024,
60
+ MAX_SESSION_ID_LENGTH: 128,
61
+ MAX_URL_LENGTH: 4096,
62
+ MAX_TITLE_LENGTH: 512,
63
+ MAX_ADAPTERS: 32,
64
+ MAX_ADAPTER_NAME_LENGTH: 128,
65
+ MAX_TOKEN_LENGTH: 512,
66
+ MAX_COMMAND_ID_LENGTH: 128,
67
+ MAX_COMMAND_NAME_LENGTH: 128,
68
+ MAX_REF_LENGTH: 128,
69
+ MAX_ERROR_LENGTH: 4096,
70
+ MAX_SERIALIZE_DEPTH: 8,
71
+ MAX_COLLECTION_ITEMS: 200,
72
+ MAX_OBJECT_KEYS: 200,
73
+ MAX_STRING_LENGTH: 64 * 1024,
74
+ /** Human review marks: the note the human types when flagging a mistake on the page. */
75
+ MAX_MARK_NOTE_LENGTH: 2e3,
76
+ /** Human review marks: the legible element label that pins the mark (e.g. "Submit button"). */
77
+ MAX_MARK_LABEL_LENGTH: 256
78
+ };
79
+ var REDACTED_VALUE = "[REDACTED]";
80
+ var DANGEROUS_ACTION_CONFIRM_ARG = "confirmDangerous";
81
+ var UpdateCheckIntervalMs = 24 * 60 * 60 * 1e3;
70
82
  var RING_BUFFER_DEFAULTS = {
71
83
  MAX_EVENTS: 2e3,
72
84
  MAX_AGE_MS: 6e4,
@@ -102,19 +114,37 @@ var EventType = {
102
114
  * Live-control: browser → bridge. A human acted on the presenter panel.
103
115
  * `data: { kind: HumanControlKind; text?: string }`. Rides the existing EventMessage.
104
116
  */
105
- HUMAN_CONTROL: "human.control"
117
+ HUMAN_CONTROL: "human.control",
118
+ /**
119
+ * Human review: browser → bridge. A human pinned a mistake to an element on the running page
120
+ * (the "annotate the bug where you see it" loop). `data` narrows to HumanMarkDataSchema — a note
121
+ * plus a re-resolvable element anchor (and its source file:line when the framework stamped one) so
122
+ * the agent that drains the mark knows exactly which element and which source to fix.
123
+ */
124
+ HUMAN_MARK: "human.mark"
106
125
  };
107
126
  var SessionState = {
108
127
  ACTIVE: "active",
109
128
  PAUSED: "paused",
110
129
  ENDED: "ended"
111
130
  };
131
+ function isSessionState(value) {
132
+ return value === SessionState.ACTIVE || value === SessionState.PAUSED || value === SessionState.ENDED;
133
+ }
112
134
  var SESSION_AUTO = "auto";
113
135
  var HumanControlKind = {
114
136
  PAUSE: "pause",
115
137
  RESUME: "resume",
116
138
  END: "end",
117
- MESSAGE: "message"
139
+ MESSAGE: "message",
140
+ /** Human clicked ▶ on a saved flow in the panel — replay it (no agent). `text` carries the name. */
141
+ REPLAY: "replay"
142
+ };
143
+ var MarkAnchorStrategy = {
144
+ TESTID: "testid",
145
+ COMPONENT: "component",
146
+ ROLE: "role",
147
+ POSITION: "position"
118
148
  };
119
149
  var SESSION_HEALTH = {
120
150
  HEARTBEAT_MS: 5e3,
@@ -122,7 +152,11 @@ var SESSION_HEALTH = {
122
152
  STALE_THRESHOLD_MS: 12e3
123
153
  };
124
154
  var SESSION_LIFECYCLE = {
125
- /** Default agent-idle window before the server reaps a session. Agent-tunable via iris_session. */
155
+ /**
156
+ * Default agent-idle window before the panel hands back to the human as WAITING. The agent signals
157
+ * this IMMEDIATELY via iris_yield; this reaper is only the slow backstop for a forgotten yield, so
158
+ * it's deliberately long (a short window would auto-end a session mid slow-step). iris_session-tunable.
159
+ */
126
160
  IDLE_END_MS: 3e5,
127
161
  /** Floor for a tuned idle window (so an agent can't disable the safety net). */
128
162
  IDLE_END_MIN_MS: 5e3,
@@ -191,7 +225,10 @@ var QueryBy = {
191
225
  LABEL: "label",
192
226
  PLACEHOLDER: "placeholder",
193
227
  TESTID: "testid",
194
- ALT: "alt"
228
+ ALT: "alt",
229
+ /** Resolve by component identity / source location (auto-anchors — addresses any element with
230
+ * no hand-added testid). Pair with ElementQuery.component and/or .source. */
231
+ COMPONENT: "component"
195
232
  };
196
233
  var IrisCommand = {
197
234
  SNAPSHOT: "snapshot",
@@ -217,7 +254,9 @@ var IrisCommand = {
217
254
  /** Navigate the page to a new URL. `args: { url: string }`. */
218
255
  NAVIGATE: "navigate",
219
256
  /** Reload the page. `args: { hard?: boolean }` — hard clears the cache via location replace trick. */
220
- REFRESH: "refresh"
257
+ REFRESH: "refresh",
258
+ /** Bridge → browser: the saved flows the human can replay from the panel. `args: { flows: [{name}] }`. */
259
+ FLOWS: "flows"
221
260
  };
222
261
  var PresenterMode = {
223
262
  IDLE: "idle",
@@ -236,6 +275,17 @@ var MessageKind = {
236
275
  EVENT: "event"
237
276
  };
238
277
 
278
+ // ../protocol/dist/notices.js
279
+ var PresenterTone = {
280
+ CALM: "calm",
281
+ WAITING: "waiting",
282
+ ASK: "ask",
283
+ WARN: "warn"
284
+ };
285
+ function isPresenterTone(value) {
286
+ return value === PresenterTone.CALM || value === PresenterTone.WAITING || value === PresenterTone.ASK || value === PresenterTone.WARN;
287
+ }
288
+
239
289
  // ../protocol/dist/messages.js
240
290
  import { z } from "zod";
241
291
  var sessionIdSchema = z.string().min(1).max(TRANSPORT_LIMITS.MAX_SESSION_ID_LENGTH);
@@ -244,6 +294,20 @@ var HumanControlDataSchema = z.object({
244
294
  kind: z.nativeEnum(HumanControlKind),
245
295
  text: z.string().optional()
246
296
  });
297
+ var HumanMarkDataSchema = z.object({
298
+ note: z.string().min(1).max(TRANSPORT_LIMITS.MAX_MARK_NOTE_LENGTH),
299
+ anchor: z.string().max(TRANSPORT_LIMITS.MAX_REF_LENGTH),
300
+ strategy: z.nativeEnum(MarkAnchorStrategy),
301
+ /** Human-legible element label (role + accessible name / text), to show the agent what was flagged. */
302
+ label: z.string().max(TRANSPORT_LIMITS.MAX_MARK_LABEL_LENGTH).optional(),
303
+ /** Source file:line stamped by the framework's compiler/plugin, when available. */
304
+ source: z.object({
305
+ file: z.string().max(TRANSPORT_LIMITS.MAX_URL_LENGTH),
306
+ line: z.number().int().min(0)
307
+ }).optional(),
308
+ /** Route/URL the mark was made on, so the agent can reproduce the context. */
309
+ route: z.string().max(TRANSPORT_LIMITS.MAX_URL_LENGTH).optional()
310
+ });
247
311
  var IrisEventSchema = z.object({
248
312
  t: z.number(),
249
313
  type: z.nativeEnum(EventType),
@@ -309,6 +373,54 @@ function isDangerousActionText(text) {
309
373
  return DANGEROUS_ACTION.test(text.replace(/[_-]+/g, " "));
310
374
  }
311
375
 
376
+ // ../protocol/dist/state-select.js
377
+ function keysOf(value) {
378
+ if (Array.isArray(value))
379
+ return value.map((_, i) => String(i));
380
+ if (typeof value === "object" && value !== null)
381
+ return Object.keys(value);
382
+ return [];
383
+ }
384
+ function selectPath(root, path) {
385
+ const segments = path.split(".").filter((s) => s.length > 0);
386
+ let current = root;
387
+ for (const segment of segments) {
388
+ if (Array.isArray(current)) {
389
+ const index = Number(segment);
390
+ if (!Number.isInteger(index) || index < 0 || index >= current.length) {
391
+ return { found: false, value: null, availableKeys: keysOf(current) };
392
+ }
393
+ current = current[index];
394
+ continue;
395
+ }
396
+ if (typeof current === "object" && current !== null && segment in current) {
397
+ current = current[segment];
398
+ continue;
399
+ }
400
+ return { found: false, value: null, availableKeys: keysOf(current) };
401
+ }
402
+ return { found: true, value: current };
403
+ }
404
+ function capDepth(value, maxDepth) {
405
+ if (maxDepth < 0)
406
+ return value;
407
+ if (Array.isArray(value)) {
408
+ if (maxDepth === 0)
409
+ return `[Array(${String(value.length)})]`;
410
+ return value.map((v) => capDepth(v, maxDepth - 1));
411
+ }
412
+ if (typeof value === "object" && value !== null) {
413
+ const keys = Object.keys(value);
414
+ if (maxDepth === 0)
415
+ return `{\u2026${String(keys.length)} keys}`;
416
+ const out = {};
417
+ for (const key of keys)
418
+ out[key] = capDepth(value[key], maxDepth - 1);
419
+ return out;
420
+ }
421
+ return value;
422
+ }
423
+
312
424
  // ../protocol/dist/types.js
313
425
  import { z as z2 } from "zod";
314
426
  var ElementQuerySchema = z2.object({
@@ -321,6 +433,10 @@ var ElementQuerySchema = z2.object({
321
433
  placeholder: z2.string().optional(),
322
434
  testid: z2.string().optional(),
323
435
  alt: z2.string().optional(),
436
+ /** Component display name (auto-anchor resolution). The nearest enclosing component of the target. */
437
+ component: z2.string().optional(),
438
+ /** Source location of the target element (auto-anchor resolution) — the precise, granular match. */
439
+ source: z2.object({ file: z2.string(), line: z2.number(), column: z2.number().optional() }).optional(),
324
440
  /** CSS selector or ref to scope the search. */
325
441
  scope: z2.string().optional()
326
442
  });
@@ -369,7 +485,16 @@ var FlowAnchorSchema = z2.discriminatedUnion("kind", [
369
485
  role: z2.string().min(1),
370
486
  name: z2.string().optional()
371
487
  }),
372
- z2.object({ kind: z2.literal(AnchorKind.SIGNAL), name: z2.string().min(1) })
488
+ z2.object({ kind: z2.literal(AnchorKind.SIGNAL), name: z2.string().min(1) }),
489
+ // Auto-anchor: re-find an element by component identity / source location when it has no testid.
490
+ // component or source carries the durable signal; role/name are disambiguating extras.
491
+ z2.object({
492
+ kind: z2.literal(AnchorKind.COMPONENT),
493
+ component: z2.string().optional(),
494
+ source: z2.object({ file: z2.string(), line: z2.number(), column: z2.number().optional() }).optional(),
495
+ role: z2.string().optional(),
496
+ name: z2.string().optional()
497
+ })
373
498
  ]);
374
499
  var FlowExpectSchema = z2.object({
375
500
  signal: z2.string().optional(),
@@ -382,12 +507,45 @@ var FlowExpectSchema = z2.object({
382
507
  net: z2.object({
383
508
  method: z2.string().optional(),
384
509
  urlContains: z2.string().optional(),
385
- status: z2.number().optional()
510
+ status: z2.number().optional(),
511
+ /**
512
+ * Exact number of matching requests since the action — turns presence into a cardinality
513
+ * assertion. Catches the double-submit / useEffect-double-fire / retry-storm regression class:
514
+ * the request fired (presence passes) but fired the WRONG number of times. Omit = presence (≥1).
515
+ */
516
+ count: z2.number().int().nonnegative().optional()
517
+ }).optional(),
518
+ /**
519
+ * Console golden end-condition: assert the action logged (or, with absent:true, did NOT log) a
520
+ * console message at `level` (default 'error'). `absent:true` is the common case — "the action
521
+ * completed with a clean console" — catching the regression where an action throws a caught error
522
+ * / logs an uncaught rejection while the UI still renders fine (a presence check passes it).
523
+ */
524
+ console: z2.object({
525
+ level: z2.string().optional(),
526
+ absent: z2.boolean().optional()
386
527
  }).optional(),
387
528
  element: z2.object({
388
529
  testid: z2.string().optional(),
389
530
  role: z2.string().optional(),
390
531
  name: z2.string().optional()
532
+ }).optional(),
533
+ /**
534
+ * Assert a registered store's value — the source of truth no DOM/network read can reach. Compiles
535
+ * to the predicate engine's `state` predicate. Additive/optional — a flow without it still parses
536
+ * and the on-disk version stays FLOW_FILE_VERSION 1. `equals` accepts a literal, omitted = presence,
537
+ * or a `{ $gte | $contains | $length }` operator pattern.
538
+ */
539
+ state: z2.object({
540
+ store: z2.string().optional(),
541
+ path: z2.string(),
542
+ equals: z2.unknown().optional(),
543
+ /**
544
+ * Treat this as an INVARIANT that must still hold AFTER the action settles, rather than a
545
+ * condition to wait for. Set it for a blast-radius check ("this unrelated path must NOT have
546
+ * moved") — without it a wait-until-true read passes before an over-reaching side-effect lands.
547
+ */
548
+ hold: z2.boolean().optional()
391
549
  }).optional()
392
550
  });
393
551
  var baseFlowStep = z2.object({
@@ -404,6 +562,14 @@ var FlowStepSchema = baseFlowStep.extend({
404
562
  var FlowFileSchema = z2.object({
405
563
  version: z2.literal(FLOW_FILE_VERSION),
406
564
  name: z2.string(),
565
+ /**
566
+ * The business goal this flow exists to verify, one line (e.g. "ship a deploy to production").
567
+ * Optional + back-compat (a flow without it still parses). Set via an `intent` annotation. The
568
+ * point of "intent + outcome oracle": a flow that declares an intent should also assert an
569
+ * observable business OUTCOME (a consequence success-state), or it claims to verify a goal it
570
+ * cannot actually check — flow-classify flags that gap.
571
+ */
572
+ intent: z2.string().optional(),
407
573
  // FUTURE: fixtures/preconditions — schema slot reserved, unpopulated this cut. The recorder
408
574
  // never writes it and no fixture runner exists.
409
575
  fixture: z2.string().optional(),
@@ -431,6 +597,12 @@ var AnnotationSchema = z2.discriminatedUnion("kind", [
431
597
  kind: z2.literal(AnnotationKind.ASSERT_VISIBLE),
432
598
  testid: z2.string().min(1)
433
599
  }),
600
+ z2.object({
601
+ kind: z2.literal(AnnotationKind.ASSERT_STATE),
602
+ statePath: z2.string().min(1),
603
+ store: z2.string().min(1).optional(),
604
+ equals: z2.unknown().optional()
605
+ }),
434
606
  z2.object({
435
607
  kind: z2.literal(AnnotationKind.MARK_DYNAMIC),
436
608
  testid: z2.string().min(1)
@@ -438,7 +610,34 @@ var AnnotationSchema = z2.discriminatedUnion("kind", [
438
610
  z2.object({
439
611
  kind: z2.literal(AnnotationKind.SUCCESS_STATE),
440
612
  signal: z2.string().min(1).optional(),
441
- testid: z2.string().min(1).optional()
613
+ testid: z2.string().min(1).optional(),
614
+ // A store-truth golden end-condition: the flow succeeds when this store path holds (e.g. the
615
+ // created deployment actually reached status 'live' in the store, not just on screen).
616
+ statePath: z2.string().min(1).optional(),
617
+ store: z2.string().min(1).optional(),
618
+ equals: z2.unknown().optional(),
619
+ // Treat the statePath as an INVARIANT that must hold AFTER settle (a blast-radius "this unrelated
620
+ // path must not have moved" check), not a condition to wait for.
621
+ hold: z2.boolean().optional(),
622
+ // A network-cardinality golden end-condition: the flow succeeds only when EXACTLY `count` matching
623
+ // requests fired (omit count = presence). Catches the double-submit / retry-storm regression class.
624
+ net: z2.object({
625
+ method: z2.string().min(1).optional(),
626
+ urlContains: z2.string().min(1).optional(),
627
+ status: z2.number().optional(),
628
+ count: z2.number().int().nonnegative().optional()
629
+ }).optional(),
630
+ // A console golden end-condition: with absent:true, "the action completed with a clean console"
631
+ // (no message at `level`, default 'error') — catches an action that logs a caught error / rejection
632
+ // while the UI still renders fine.
633
+ console: z2.object({
634
+ level: z2.string().min(1).optional(),
635
+ absent: z2.boolean().optional()
636
+ }).optional()
637
+ }),
638
+ z2.object({
639
+ kind: z2.literal(AnnotationKind.INTENT),
640
+ text: z2.string().min(1)
442
641
  })
443
642
  ]);
444
643
 
@@ -480,7 +679,7 @@ var OMIT_VALUE = /* @__PURE__ */ Symbol("omit");
480
679
  var MAX_KEY_LENGTH = 256;
481
680
  var MAX_TOTAL_CHARACTERS = Math.floor(TRANSPORT_LIMITS.MAX_MESSAGE_BYTES / 8);
482
681
  var MAX_TOTAL_NODES = TRANSPORT_LIMITS.MAX_COLLECTION_ITEMS * 5;
483
- var SENSITIVE_KEY = /password|passwd|passcode|secret|token|authorization|api[-_]?key|access[-_]?key|private[-_]?key|client[-_]?secret|credit[-_]?card|card[-_]?number|cvv|cvc|ssn/i;
682
+ var SENSITIVE_KEY = /password|passwd|passcode|secret|(?:(?:access|refresh|auth|bearer|api|id|session|csrf|client)[-_]?tokens?|(?:^|[-_])tokens?(?=$|[-_]))|authorization|api[-_]?key|access[-_]?key|private[-_]?key|client[-_]?secret|credit[-_]?card|card[-_]?number|cvv|cvc|ssn/i;
484
683
  function isSensitiveKey(key) {
485
684
  return SENSITIVE_KEY.test(key);
486
685
  }
@@ -801,8 +1000,8 @@ function isIrisOverlay(el) {
801
1000
  return el.closest(IRIS_OVERLAY) !== null;
802
1001
  }
803
1002
  function isIgnored(el) {
804
- const sel = extraIgnore.length > 0 ? `${IRIS_OVERLAY},${DEV_OVERLAYS},${extraIgnore}` : `${IRIS_OVERLAY},${DEV_OVERLAYS}`;
805
- return el.closest(sel) !== null;
1003
+ const sel2 = extraIgnore.length > 0 ? `${IRIS_OVERLAY},${DEV_OVERLAYS},${extraIgnore}` : `${IRIS_OVERLAY},${DEV_OVERLAYS}`;
1004
+ return el.closest(sel2) !== null;
806
1005
  }
807
1006
 
808
1007
  // ../browser/dist/dom/snapshot.js
@@ -822,6 +1021,16 @@ var INTERACTIVE = /* @__PURE__ */ new Set([
822
1021
  "option"
823
1022
  ]);
824
1023
  var SKIP_TAGS = /* @__PURE__ */ new Set(["script", "style", "noscript", "template", "head", "meta", "link"]);
1024
+ var TEXT_MAX = 80;
1025
+ function directText(el) {
1026
+ let out = "";
1027
+ for (const node of el.childNodes) {
1028
+ if (node.nodeType === 3)
1029
+ out += node.textContent ?? "";
1030
+ }
1031
+ const collapsed = out.replace(/\s+/g, " ").trim();
1032
+ return collapsed.length > TEXT_MAX ? `${collapsed.slice(0, TEXT_MAX)}\u2026` : collapsed;
1033
+ }
825
1034
  function skip(el) {
826
1035
  if (SKIP_TAGS.has(el.tagName.toLowerCase()))
827
1036
  return true;
@@ -843,13 +1052,29 @@ function stateSuffix(el) {
843
1052
  const states = getStates(el).filter((s) => s === ElementState.DISABLED || s === ElementState.CHECKED || s === ElementState.EXPANDED || s === ElementState.FOCUSED);
844
1053
  return states.length > 0 ? ` [${states.join(",")}]` : "";
845
1054
  }
846
- function formatLine(el, depth, role, name) {
1055
+ function formatLine(el, depth, role, name, layout) {
847
1056
  const indent = " ".repeat(depth);
848
1057
  const value = getValue(el);
849
1058
  const namePart = name.length > 0 ? ` "${name}"` : "";
850
1059
  const refPart = INTERACTIVE.has(role) || name.length > 0 ? ` (ref=${refs.refFor(el)})` : "";
851
1060
  const valuePart = value !== void 0 && value.length > 0 ? ` [value="${value}"]` : "";
852
- return `${indent}- ${role}${namePart}${refPart}${valuePart}${stateSuffix(el)}`;
1061
+ const layoutPart = layout.length > 0 ? ` [${layout}]` : "";
1062
+ return `${indent}- ${role}${namePart}${refPart}${valuePart}${layoutPart}${stateSuffix(el)}`;
1063
+ }
1064
+ function formatTextLine(depth, text) {
1065
+ return `${" ".repeat(depth)}- text "${text}"`;
1066
+ }
1067
+ function layoutSignature(el) {
1068
+ const view = el.ownerDocument.defaultView;
1069
+ if (view === null)
1070
+ return "";
1071
+ const style = view.getComputedStyle(el);
1072
+ const display = style.display;
1073
+ if (display === "grid" || display === "inline-grid") {
1074
+ const cols = style.gridTemplateColumns;
1075
+ return cols !== "" && cols !== "none" ? `grid-cols:${cols}` : "grid";
1076
+ }
1077
+ return "";
853
1078
  }
854
1079
  function walk(parent, depth, ctx) {
855
1080
  if (depth > ctx.maxDepth)
@@ -864,11 +1089,14 @@ function walk(parent, depth, ctx) {
864
1089
  const role = getRole(child);
865
1090
  const name = getAccessibleName(child);
866
1091
  const interactive = INTERACTIVE.has(role);
867
- const meaningful = interactive || role !== "generic" || name.length > 0;
868
- const include = ctx.mode === SnapshotMode.INTERACTIVE ? interactive : meaningful;
1092
+ const lean = ctx.mode === SnapshotMode.INTERACTIVE;
1093
+ const text = !lean && role === "generic" && name.length === 0 ? directText(child) : "";
1094
+ const layout = lean ? "" : layoutSignature(child);
1095
+ const meaningful = interactive || role !== "generic" || name.length > 0 || text.length > 0 || layout.length > 0;
1096
+ const include = lean ? interactive : meaningful;
869
1097
  if (include) {
870
1098
  ctx.nodes += 1;
871
- ctx.lines.push(formatLine(child, depth, role, name));
1099
+ ctx.lines.push(text.length > 0 && name.length === 0 && layout.length === 0 ? formatTextLine(depth, text) : formatLine(child, depth, role, name, layout));
872
1100
  walk(child, depth + 1, ctx);
873
1101
  } else {
874
1102
  walk(child, depth, ctx);
@@ -959,9 +1187,50 @@ function hasCapabilities() {
959
1187
  return capabilities.testids.length > 0 || capabilities.signals.length > 0 || capabilities.stores.length > 0 || capabilities.flows.length > 0;
960
1188
  }
961
1189
 
1190
+ // ../browser/dist/registry/adapters.js
1191
+ var globalStore2 = globalThis;
1192
+ var adapters = globalStore2.__irisAdapters ??= [];
1193
+ function registerAdapter(adapter) {
1194
+ if (!adapters.some((a) => a.name === adapter.name))
1195
+ adapters.push(adapter);
1196
+ }
1197
+ function identifyComponent(el) {
1198
+ for (const adapter of adapters) {
1199
+ const info = adapter.identify(el);
1200
+ if (info !== null)
1201
+ return info;
1202
+ }
1203
+ return null;
1204
+ }
1205
+ function readComponentState(el) {
1206
+ for (const adapter of adapters) {
1207
+ if (adapter.readState === void 0)
1208
+ continue;
1209
+ const state = adapter.readState(el);
1210
+ if (state !== void 0)
1211
+ return state;
1212
+ }
1213
+ return void 0;
1214
+ }
1215
+ function elementHasHoverHandlers(el) {
1216
+ for (const adapter of adapters) {
1217
+ if (adapter.hasHoverHandlers === void 0)
1218
+ continue;
1219
+ if (adapter.hasHoverHandlers(el))
1220
+ return true;
1221
+ }
1222
+ return false;
1223
+ }
1224
+ function adapterNames() {
1225
+ return adapters.map((a) => a.name);
1226
+ }
1227
+
962
1228
  // ../browser/dist/dom/query.js
963
1229
  var TESTID_ATTR = "data-testid";
1230
+ var SOURCE_ATTR = "data-iris-source";
964
1231
  var MAX_PRESENT_TESTIDS = 12;
1232
+ var MAX_COMPONENT_CANDIDATES = 2e3;
1233
+ var COMPONENT_CANDIDATE_SELECTOR = "[data-iris-source], [data-testid], button, a, input, select, textarea, [role]";
965
1234
  function resolveContainer(scope) {
966
1235
  const body = document.body;
967
1236
  if (scope === void 0)
@@ -977,6 +1246,38 @@ function resolveContainer(scope) {
977
1246
  }
978
1247
  return body;
979
1248
  }
1249
+ function findBySource(container, source) {
1250
+ const prefix = `${source.file}:${source.line}:`.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
1251
+ try {
1252
+ return Array.from(container.querySelectorAll(`[${SOURCE_ATTR}^="${prefix}"]`));
1253
+ } catch {
1254
+ return [];
1255
+ }
1256
+ }
1257
+ function findByComponentName(container, component) {
1258
+ const out = [];
1259
+ let scanned = 0;
1260
+ for (const el of Array.from(container.querySelectorAll(COMPONENT_CANDIDATE_SELECTOR))) {
1261
+ if (scanned >= MAX_COMPONENT_CANDIDATES)
1262
+ break;
1263
+ scanned += 1;
1264
+ const info = identifyComponent(el);
1265
+ if (info !== null && info.componentStack[0] === component)
1266
+ out.push(el);
1267
+ }
1268
+ return out;
1269
+ }
1270
+ function findByComponent(container, query) {
1271
+ if (query.source !== void 0) {
1272
+ const bySource = findBySource(container, query.source);
1273
+ if (bySource.length > 0)
1274
+ return bySource;
1275
+ }
1276
+ if (query.component !== void 0 && query.component.length > 0) {
1277
+ return findByComponentName(container, query.component);
1278
+ }
1279
+ return [];
1280
+ }
980
1281
  function findCandidates(query) {
981
1282
  const container = resolveContainer(query.scope);
982
1283
  const by = query.by;
@@ -995,10 +1296,15 @@ function findCandidates(query) {
995
1296
  return queryAllByTestId(container, value, { exact: true });
996
1297
  case QueryBy.ALT:
997
1298
  return queryAllByAltText(container, value, { exact: false });
1299
+ case QueryBy.COMPONENT:
1300
+ return findByComponent(container, { ...query, component: query.component ?? value });
998
1301
  default:
999
1302
  return [];
1000
1303
  }
1001
1304
  }
1305
+ if (query.component !== void 0 || query.source !== void 0) {
1306
+ return findByComponent(container, query);
1307
+ }
1002
1308
  if (query.role !== void 0) {
1003
1309
  const options = query.name !== void 0 ? { hidden: true, name: query.name } : { hidden: true };
1004
1310
  return queryAllByRole(container, query.role, options);
@@ -1112,44 +1418,6 @@ function runQuery(query) {
1112
1418
  return { elements: result2.elements };
1113
1419
  }
1114
1420
 
1115
- // ../browser/dist/registry/adapters.js
1116
- var globalStore2 = globalThis;
1117
- var adapters = globalStore2.__irisAdapters ??= [];
1118
- function registerAdapter(adapter) {
1119
- if (!adapters.some((a) => a.name === adapter.name))
1120
- adapters.push(adapter);
1121
- }
1122
- function identifyComponent(el) {
1123
- for (const adapter of adapters) {
1124
- const info = adapter.identify(el);
1125
- if (info !== null)
1126
- return info;
1127
- }
1128
- return null;
1129
- }
1130
- function readComponentState(el) {
1131
- for (const adapter of adapters) {
1132
- if (adapter.readState === void 0)
1133
- continue;
1134
- const state = adapter.readState(el);
1135
- if (state !== void 0)
1136
- return state;
1137
- }
1138
- return void 0;
1139
- }
1140
- function elementHasHoverHandlers(el) {
1141
- for (const adapter of adapters) {
1142
- if (adapter.hasHoverHandlers === void 0)
1143
- continue;
1144
- if (adapter.hasHoverHandlers(el))
1145
- return true;
1146
- }
1147
- return false;
1148
- }
1149
- function adapterNames() {
1150
- return adapters.map((a) => a.name);
1151
- }
1152
-
1153
1421
  // ../browser/dist/timers/native-timers.js
1154
1422
  var g = globalThis;
1155
1423
  var realSetTimeout = typeof g.setTimeout === "function" ? g.setTimeout.bind(g) : null;
@@ -1224,8 +1492,20 @@ function requireElement(ref) {
1224
1492
  throw new Error(`ref '${ref}' is not an HTMLElement`);
1225
1493
  return el;
1226
1494
  }
1227
- var result = (ref, action, effect, settled, settleReason, warning) => {
1228
- const testid = refs.resolve(ref)?.getAttribute("data-testid") ?? void 0;
1495
+ function anchorOf(el) {
1496
+ const testid = el.getAttribute("data-testid") ?? void 0;
1497
+ if (testid !== void 0)
1498
+ return { testid };
1499
+ const info = identifyComponent(el);
1500
+ const out = {};
1501
+ const component = info?.componentStack[0];
1502
+ if (component !== void 0)
1503
+ out.component = component;
1504
+ if (info?.source !== void 0)
1505
+ out.source = info.source;
1506
+ return out;
1507
+ }
1508
+ var result = (ref, action, effect, settled, settleReason, anchor, warning) => {
1229
1509
  const base = {
1230
1510
  ok: true,
1231
1511
  ref,
@@ -1235,8 +1515,14 @@ var result = (ref, action, effect, settled, settleReason, warning) => {
1235
1515
  settleReason,
1236
1516
  effect
1237
1517
  };
1238
- if (testid !== void 0)
1239
- base.testid = testid;
1518
+ if (anchor.testid !== void 0) {
1519
+ base.testid = anchor.testid;
1520
+ } else {
1521
+ if (anchor.component !== void 0)
1522
+ base.component = anchor.component;
1523
+ if (anchor.source !== void 0)
1524
+ base.source = anchor.source;
1525
+ }
1240
1526
  if (warning !== void 0)
1241
1527
  base.warning = warning;
1242
1528
  return base;
@@ -1435,6 +1721,7 @@ async function dispatchFor(el, action, args) {
1435
1721
  async function executeAction(ref, action, args = {}) {
1436
1722
  const el = requireElement(ref);
1437
1723
  assertActionAllowed(el, action, args);
1724
+ const anchor = anchorOf(el);
1438
1725
  const visible = isVisible(el);
1439
1726
  const enabled = enabledOf(el);
1440
1727
  const prevFocus = activeRef(el);
@@ -1477,7 +1764,7 @@ async function executeAction(ref, action, args = {}) {
1477
1764
  scrolledIntoView: geometry.scrolledIntoView
1478
1765
  };
1479
1766
  const warning = geometry.occluded ? ActionWarning.CLICK_OCCLUDED : action === ActionType.HOVER && elementHasHoverHandlers(el) ? ActionWarning.HOVER_NATIVE_ENTER_LEAVE : void 0;
1480
- return result(ref, action, effect, settled, settleReason, warning);
1767
+ return result(ref, action, effect, settled, settleReason, anchor, warning);
1481
1768
  }
1482
1769
  var sleep = (ms) => new Promise((r) => nativeSetTimeout(r, ms));
1483
1770
  function firePointer(el, type, relatedTarget = null) {
@@ -1573,6 +1860,77 @@ async function executeSequence(steps) {
1573
1860
  return { ok: true, count: steps.length, effects, steps: stepResults };
1574
1861
  }
1575
1862
 
1863
+ // ../browser/dist/dom/theme.js
1864
+ var cached = null;
1865
+ function toRgb(value) {
1866
+ if (value.length === 0)
1867
+ return null;
1868
+ const probe = document.createElement("span");
1869
+ probe.style.color = "";
1870
+ probe.style.color = value;
1871
+ if (probe.style.color === "")
1872
+ return null;
1873
+ probe.style.position = "absolute";
1874
+ probe.style.pointerEvents = "none";
1875
+ document.body.appendChild(probe);
1876
+ const rgb = getComputedStyle(probe).color;
1877
+ probe.remove();
1878
+ return rgb;
1879
+ }
1880
+ function collectTokens() {
1881
+ const out = {};
1882
+ for (const sheet of Array.from(document.styleSheets)) {
1883
+ let rules = null;
1884
+ try {
1885
+ rules = sheet.cssRules;
1886
+ } catch {
1887
+ continue;
1888
+ }
1889
+ if (rules === null)
1890
+ continue;
1891
+ for (const rule of Array.from(rules)) {
1892
+ if (!(rule instanceof CSSStyleRule))
1893
+ continue;
1894
+ if (!/(^|,)\s*(:root|html)\b/.test(rule.selectorText))
1895
+ continue;
1896
+ for (const prop of Array.from(rule.style)) {
1897
+ if (prop.startsWith("--"))
1898
+ out[prop] = rule.style.getPropertyValue(prop).trim();
1899
+ }
1900
+ }
1901
+ }
1902
+ return out;
1903
+ }
1904
+ function palette() {
1905
+ if (cached !== null)
1906
+ return cached;
1907
+ const byColor = /* @__PURE__ */ new Map();
1908
+ for (const [name, value] of Object.entries(collectTokens())) {
1909
+ const rgb = toRgb(value);
1910
+ if (rgb !== null && !byColor.has(rgb))
1911
+ byColor.set(rgb, name);
1912
+ }
1913
+ cached = { byColor };
1914
+ return cached;
1915
+ }
1916
+ function isTransparent(rgb) {
1917
+ return rgb === "rgba(0, 0, 0, 0)" || rgb === "transparent";
1918
+ }
1919
+ function themeReport(cs) {
1920
+ const p = palette();
1921
+ const colorToken = p.byColor.get(cs.color) ?? null;
1922
+ const backgroundToken = p.byColor.get(cs.backgroundColor) ?? null;
1923
+ const colorOff = !isTransparent(cs.color) && colorToken === null;
1924
+ const bgOff = !isTransparent(cs.backgroundColor) && backgroundToken === null;
1925
+ return {
1926
+ colorToken,
1927
+ backgroundToken,
1928
+ // Only meaningful when a palette exists; an app with no tokens can't violate one.
1929
+ offTheme: p.byColor.size > 0 && (colorOff || bgOff),
1930
+ tokenCount: p.byColor.size
1931
+ };
1932
+ }
1933
+
1576
1934
  // ../browser/dist/registry/stores.js
1577
1935
  var globalStore3 = globalThis;
1578
1936
  var stores = globalStore3.__irisStores ??= /* @__PURE__ */ new Map();
@@ -1711,6 +2069,9 @@ function scrollContainer(ref, dy, fraction) {
1711
2069
  function str(value) {
1712
2070
  return typeof value === "string" ? value : void 0;
1713
2071
  }
2072
+ function num(value) {
2073
+ return typeof value === "number" ? value : void 0;
2074
+ }
1714
2075
  function record(value) {
1715
2076
  return typeof value === "object" && value !== null ? value : {};
1716
2077
  }
@@ -1736,7 +2097,14 @@ function inspect(ref) {
1736
2097
  const component = identifyComponent(el);
1737
2098
  const view = el.ownerDocument.defaultView;
1738
2099
  const cs = view !== null ? view.getComputedStyle(el) : null;
1739
- const styles = cs !== null ? { color: cs.color, backgroundColor: cs.backgroundColor, opacity: cs.opacity } : null;
2100
+ const styles = cs !== null ? {
2101
+ color: cs.color,
2102
+ backgroundColor: cs.backgroundColor,
2103
+ opacity: cs.opacity,
2104
+ cursor: cs.cursor,
2105
+ display: cs.display,
2106
+ visibility: cs.visibility
2107
+ } : null;
1740
2108
  return {
1741
2109
  ...describe(el),
1742
2110
  tag: el.tagName.toLowerCase(),
@@ -1744,10 +2112,24 @@ function inspect(ref) {
1744
2112
  formAction: el instanceof HTMLButtonElement || el instanceof HTMLInputElement ? el.form?.getAttribute("action") ?? void 0 : void 0,
1745
2113
  formText: el instanceof HTMLButtonElement || el instanceof HTMLInputElement ? el.form?.textContent ?? void 0 : void 0,
1746
2114
  box: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
2115
+ // True when another element sits over this one's center point — the click would hit the overlay,
2116
+ // not this control (a z-index/overlay bug the DOM tree cannot show).
2117
+ occluded: isOccluded(el, rect),
1747
2118
  styles,
2119
+ // Theme compliance vs the app's design tokens (off-theme colors a DOM tool can't judge).
2120
+ theme: cs !== null ? themeReport(cs) : null,
1748
2121
  component
1749
2122
  };
1750
2123
  }
2124
+ function isOccluded(el, rect) {
2125
+ if (rect.width === 0 || rect.height === 0)
2126
+ return false;
2127
+ const doc = el.ownerDocument;
2128
+ if (typeof doc.elementFromPoint !== "function")
2129
+ return false;
2130
+ const top = doc.elementFromPoint(rect.x + rect.width / 2, rect.y + rect.height / 2);
2131
+ return top !== null && top !== el && !el.contains(top);
2132
+ }
1751
2133
  function isComponentStateResult(value) {
1752
2134
  return typeof value === "object" && value !== null && "ok" in value && typeof value.ok === "boolean";
1753
2135
  }
@@ -1755,11 +2137,25 @@ var COMPONENT_UNAVAILABLE = {
1755
2137
  ok: false,
1756
2138
  reason: ComponentStateReason.UNAVAILABLE
1757
2139
  };
1758
- function readState(ref, store) {
2140
+ function readState(ref, store, path, depth) {
1759
2141
  const stores2 = readStores(store);
2142
+ const names = storeNames();
2143
+ if (path !== void 0 || depth !== void 0) {
2144
+ const base = store !== void 0 ? stores2[store] : { stores: stores2, storeNames: names };
2145
+ const selection = path !== void 0 ? selectPath(base, path) : { found: true, value: base };
2146
+ const value = selection.found && depth !== void 0 ? capDepth(selection.value, depth) : selection.value;
2147
+ return {
2148
+ store,
2149
+ path,
2150
+ found: selection.found,
2151
+ value,
2152
+ ..."availableKeys" in selection ? { availableKeys: selection.availableKeys } : {},
2153
+ storeNames: names
2154
+ };
2155
+ }
1760
2156
  const result2 = {
1761
2157
  stores: stores2,
1762
- storeNames: storeNames()
2158
+ storeNames: names
1763
2159
  };
1764
2160
  if (ref !== void 0 && ref.length > 0) {
1765
2161
  const el = refs.resolve(ref);
@@ -1828,7 +2224,7 @@ function createCommandRegistry() {
1828
2224
  }
1829
2225
  return { frozen: isClockFrozen() };
1830
2226
  });
1831
- reg.set(IrisCommand.STATE_READ, (args) => readState(str(args["ref"]), str(args["store"])));
2227
+ reg.set(IrisCommand.STATE_READ, (args) => readState(str(args["ref"]), str(args["store"]), str(args["path"]), num(args["depth"])));
1832
2228
  reg.set(IrisCommand.CAPABILITIES, () => getCapabilities());
1833
2229
  reg.set(IrisCommand.SCROLL, (args) => {
1834
2230
  const dy = args["dy"];
@@ -2080,13 +2476,18 @@ function methodOf(input, init) {
2080
2476
  function installNetwork(emit) {
2081
2477
  const origFetch = window.fetch;
2082
2478
  const callFetch = origFetch.bind(window);
2479
+ let seq2 = 0;
2480
+ const nextId = () => `n${++seq2}`;
2083
2481
  window.fetch = async (input, init) => {
2482
+ const id = nextId();
2084
2483
  const start = performance.now();
2085
2484
  const method = methodOf(input, init);
2086
2485
  const url = urlOf(input);
2486
+ emit(EventType.NET_PENDING, { id, method, url, initiator: "fetch" });
2087
2487
  try {
2088
2488
  const res = await callFetch(input, init);
2089
2489
  emit(EventType.NET_REQUEST, {
2490
+ id,
2090
2491
  method,
2091
2492
  url,
2092
2493
  status: res.status,
@@ -2097,6 +2498,7 @@ function installNetwork(emit) {
2097
2498
  return res;
2098
2499
  } catch (error) {
2099
2500
  emit(EventType.NET_REQUEST, {
2501
+ id,
2100
2502
  method,
2101
2503
  url,
2102
2504
  status: 0,
@@ -2114,15 +2516,17 @@ function installNetwork(emit) {
2114
2516
  const origSend = proto.send;
2115
2517
  const callOpen = origOpen;
2116
2518
  proto.open = function(method, url, ...rest) {
2117
- meta.set(this, { method: method.toUpperCase(), url: String(url), start: 0 });
2519
+ meta.set(this, { id: nextId(), method: method.toUpperCase(), url: String(url), start: 0 });
2118
2520
  callOpen.call(this, method, url, ...rest);
2119
2521
  };
2120
2522
  proto.send = function(body) {
2121
2523
  const m = meta.get(this);
2122
2524
  if (m !== void 0) {
2123
2525
  m.start = performance.now();
2526
+ emit(EventType.NET_PENDING, { id: m.id, method: m.method, url: m.url, initiator: "xhr" });
2124
2527
  this.addEventListener("loadend", () => {
2125
2528
  emit(EventType.NET_REQUEST, {
2529
+ id: m.id,
2126
2530
  method: m.method,
2127
2531
  url: m.url,
2128
2532
  status: this.status,
@@ -2496,6 +2900,7 @@ function appendLogRow(container, kind, text, ts, logMax) {
2496
2900
 
2497
2901
  // ../browser/dist/presenter/presenter-controls.js
2498
2902
  var DATA_IRIS_STATE = "data-iris-state";
2903
+ var DATA_IRIS_TONE = "data-iris-tone";
2499
2904
  var DATA_ON = "data-on";
2500
2905
  var GLOW_OFF = "0";
2501
2906
  var CONTROL_LABEL = {
@@ -2509,6 +2914,7 @@ var PAUSED_BADGE_TEXT = "PAUSED";
2509
2914
  var ENDED_BANNER_TEXT = "Session ended";
2510
2915
  var COPY_LABEL = "Copy run";
2511
2916
  var EXPORT_LABEL = "Export";
2917
+ var FLOWS_LABEL = "Replay a flow";
2512
2918
  var COPIED_TEXT = "Copied \u2713";
2513
2919
  var RUN_FILENAME = "iris-run.json";
2514
2920
  var ENDED_FADE_MS = 4e3;
@@ -2526,11 +2932,12 @@ var CONTROLS_CSS = `
2526
2932
  color:var(--iris-accent);border:1px solid var(--iris-accent);background:var(--iris-accent-soft);padding:2px 8px;border-radius:999px;}
2527
2933
  [data-iris-overlay][data-iris-state="paused"] [data-iris-badge]{display:inline-flex;}
2528
2934
  [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);}
2529
- [data-iris-hud] .iris-composer{display:flex;align-items:center;gap:6px;background:rgba(255,255,255,.05);
2935
+ [data-iris-hud] .iris-composer{display:flex;align-items:flex-end;gap:6px;background:rgba(255,255,255,.05);
2530
2936
  border:1px solid var(--iris-line);border-radius:14px;padding:5px 6px 5px 14px;transition:border-color .15s,box-shadow .15s;}
2531
2937
  [data-iris-hud] .iris-composer:focus-within{border-color:var(--iris-accent);box-shadow:0 0 0 3px var(--iris-accent-soft);}
2532
- [data-iris-hud] .iris-msg{flex:1;min-width:0;pointer-events:auto;background:transparent;border:none;outline:none;
2533
- color:var(--iris-fg);font-family:var(--iris-font);font-size:13px;height:28px;padding:0;}
2938
+ [data-iris-hud] .iris-msg{flex:1;min-width:0;pointer-events:auto;background:transparent;border:none;outline:none;resize:none;
2939
+ color:var(--iris-fg);font-family:var(--iris-font);font-size:13px;line-height:18px;height:18px;min-height:18px;max-height:96px;
2940
+ padding:5px 0;overflow-y:auto;}
2534
2941
  [data-iris-hud] .iris-msg::placeholder{color:var(--iris-faint);}
2535
2942
  [data-iris-hud] .iris-msg:disabled{opacity:.5;}
2536
2943
  [data-iris-hud] .iris-send{flex:none;width:30px;height:30px;padding:0;border-radius:10px;border:none;cursor:pointer;pointer-events:auto;
@@ -2551,11 +2958,40 @@ var CONTROLS_CSS = `
2551
2958
  box-shadow:inset 0 0 0 3px rgba(246,180,76,.9),inset 0 0 30px 6px rgba(246,180,76,.4);}
2552
2959
  [data-iris-overlay][data-iris-state="ended"] [data-iris-glow][data-on="1"]{animation:none;
2553
2960
  box-shadow:inset 0 0 0 2px rgba(61,215,166,.55);}
2961
+ /* Handoff tones tell the human the agent's mode at a glance. waiting = calm teal "your turn" (no
2962
+ alarm); ask = amber "answer me" with a pulse; warn = amber "agent crashed" with a pulse. Each leads
2963
+ the banner with an icon and overrides the calm ended-green accent. */
2964
+ [data-iris-overlay][data-iris-tone="waiting"] [data-iris-hud]{--iris-accent:#38bdf8;--iris-accent-soft:rgba(56,189,248,.16);}
2965
+ [data-iris-overlay][data-iris-tone="waiting"] [data-iris-banner]{font-weight:600;color:#7dd3fc;}
2966
+ [data-iris-overlay][data-iris-tone="waiting"] [data-iris-banner]::before{content:"\\270B ";}
2967
+ [data-iris-overlay][data-iris-tone="waiting"] [data-iris-glow][data-on="1"]{animation:none;
2968
+ box-shadow:inset 0 0 0 2px rgba(56,189,248,.5);}
2969
+ [data-iris-overlay][data-iris-tone="ask"] [data-iris-hud],
2970
+ [data-iris-overlay][data-iris-tone="warn"] [data-iris-hud]{--iris-accent:#fb923c;--iris-accent-soft:rgba(251,146,60,.18);}
2971
+ [data-iris-overlay][data-iris-tone="ask"] [data-iris-banner],
2972
+ [data-iris-overlay][data-iris-tone="warn"] [data-iris-banner]{font-weight:600;color:#fdba74;}
2973
+ [data-iris-overlay][data-iris-tone="ask"] [data-iris-banner]::before{content:"\\2753 ";}
2974
+ [data-iris-overlay][data-iris-tone="warn"] [data-iris-banner]::before{content:"\\26A0\\FE0F ";}
2975
+ [data-iris-overlay][data-iris-tone="ask"] [data-iris-glow][data-on="1"],
2976
+ [data-iris-overlay][data-iris-tone="warn"] [data-iris-glow][data-on="1"]{animation:iris-warn-pulse 1.5s ease-in-out infinite;
2977
+ box-shadow:inset 0 0 0 2px rgba(251,146,60,.7);}
2978
+ @keyframes iris-warn-pulse{0%,100%{box-shadow:inset 0 0 0 2px rgba(251,146,60,.32);}
2979
+ 50%{box-shadow:inset 0 0 0 3px rgba(251,146,60,.85),inset 0 0 26px 5px rgba(251,146,60,.34);}}
2980
+ /* Replay-a-flow row: the human re-runs a saved flow with no agent. Hidden until flows are pushed. */
2981
+ [data-iris-hud] .iris-flows{display:none;flex-wrap:wrap;gap:6px;padding:9px 12px;border-top:1px solid var(--iris-line2);}
2982
+ [data-iris-hud] .iris-flows[data-has="1"]{display:flex;}
2983
+ [data-iris-hud] .iris-flows-cap{flex:0 0 100%;margin-bottom:1px;color:var(--iris-faint);font-size:9.5px;letter-spacing:.09em;text-transform:uppercase;}
2984
+ [data-iris-hud] .iris-flow{pointer-events:auto;cursor:pointer;display:inline-flex;align-items:center;gap:5px;height:24px;padding:0 10px;
2985
+ border-radius:7px;border:1px solid var(--iris-line);background:rgba(255,255,255,.04);color:var(--iris-muted);
2986
+ font-family:var(--iris-font);font-size:11px;font-weight:500;transition:background .15s,color .15s,border-color .15s,transform .1s;}
2987
+ [data-iris-hud] .iris-flow:hover{color:var(--iris-fg);background:var(--iris-accent-soft);border-color:var(--iris-accent);}
2988
+ [data-iris-hud] .iris-flow:active{transform:scale(.95);}
2554
2989
  `;
2555
2990
  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>`;
2556
2991
  var CONTROLS_BANNER_HTML = `<div data-iris-banner class="iris-banner">${ENDED_BANNER_TEXT}</div>`;
2992
+ var CONTROLS_FLOWS_HTML = `<div data-iris-flows class="iris-flows"><span class="iris-flows-cap">${FLOWS_LABEL}</span></div>`;
2557
2993
  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>`;
2558
- 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>`;
2994
+ var CONTROLS_FOOT_HTML = `<div data-iris-foot><div class="iris-composer"><textarea data-iris-input class="iris-msg" rows="1" placeholder="${INPUT_PLACEHOLDER}"></textarea><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>`;
2559
2995
  function queryControlRefs(root) {
2560
2996
  return {
2561
2997
  pauseBtn: root.querySelector("[data-iris-pause]") ?? void 0,
@@ -2565,7 +3001,8 @@ function queryControlRefs(root) {
2565
3001
  banner: root.querySelector("[data-iris-banner]") ?? void 0,
2566
3002
  copyBtn: root.querySelector("[data-iris-copy]") ?? void 0,
2567
3003
  exportBtn: root.querySelector("[data-iris-export]") ?? void 0,
2568
- exportMsg: root.querySelector("[data-iris-export-msg]") ?? void 0
3004
+ exportMsg: root.querySelector("[data-iris-export-msg]") ?? void 0,
3005
+ flows: root.querySelector("[data-iris-flows]") ?? void 0
2569
3006
  };
2570
3007
  }
2571
3008
  var ControlPanel = class {
@@ -2577,7 +3014,8 @@ var ControlPanel = class {
2577
3014
  banner: void 0,
2578
3015
  copyBtn: void 0,
2579
3016
  exportBtn: void 0,
2580
- exportMsg: void 0
3017
+ exportMsg: void 0,
3018
+ flows: void 0
2581
3019
  };
2582
3020
  #state = SessionState.ACTIVE;
2583
3021
  #fadeTimer;
@@ -2599,8 +3037,20 @@ var ControlPanel = class {
2599
3037
  this.#refs.endBtn?.addEventListener("click", () => this.#onEnd());
2600
3038
  this.#refs.sendBtn?.addEventListener("click", () => this.#onSend());
2601
3039
  this.#refs.input?.addEventListener("keydown", (e) => {
2602
- if (e instanceof KeyboardEvent && e.key === "Enter")
3040
+ if (e instanceof KeyboardEvent && e.key === "Enter" && !e.shiftKey) {
3041
+ e.preventDefault();
2603
3042
  this.#onSend();
3043
+ }
3044
+ });
3045
+ this.#refs.input?.addEventListener("input", () => this.#autosize());
3046
+ this.#refs.flows?.addEventListener("click", (e) => {
3047
+ const target = e.target;
3048
+ if (!(target instanceof HTMLElement))
3049
+ return;
3050
+ const name = target.closest("[data-iris-replay]")?.getAttribute("data-iris-replay");
3051
+ if (name !== null && name !== void 0 && name.length > 0) {
3052
+ this.#host.emit(HumanControlKind.REPLAY, name);
3053
+ }
2604
3054
  });
2605
3055
  this.#refs.copyBtn?.addEventListener("click", () => this.#onCopy());
2606
3056
  this.#refs.exportBtn?.addEventListener("click", () => this.#onExport());
@@ -2661,15 +3111,49 @@ var ControlPanel = class {
2661
3111
  this.#host.logHuman(text);
2662
3112
  if (this.#refs.input !== void 0)
2663
3113
  this.#refs.input.value = "";
3114
+ this.#autosize();
3115
+ }
3116
+ /** Grow the composer to fit its content (up to the CSS max-height), then shrink back — soothing,
3117
+ * no scrollbar until it's genuinely long. Driven on input and after a send clears the field. */
3118
+ #autosize() {
3119
+ const el = this.#refs.input;
3120
+ if (el === void 0)
3121
+ return;
3122
+ el.style.height = "auto";
3123
+ el.style.height = `${String(Math.min(el.scrollHeight, 96))}px`;
3124
+ }
3125
+ /** Render the replayable-flow chips from the server push. Each ▶ click re-runs that flow, no agent.
3126
+ * Takes the raw wire value and narrows it here (the panel is the consumer of this push). */
3127
+ setFlows(flows) {
3128
+ const el = this.#refs.flows;
3129
+ if (el === void 0)
3130
+ return;
3131
+ const list = Array.isArray(flows) ? flows : [];
3132
+ const names = list.map((f) => typeof f === "object" && f !== null ? f["name"] : f).filter((n) => typeof n === "string" && n.length > 0);
3133
+ el.querySelectorAll("[data-iris-replay]").forEach((b) => b.remove());
3134
+ for (const name of names) {
3135
+ const btn = el.ownerDocument.createElement("button");
3136
+ btn.type = "button";
3137
+ btn.className = "iris-flow";
3138
+ btn.setAttribute("data-iris-replay", name);
3139
+ btn.textContent = `\u25B6 ${name}`;
3140
+ el.appendChild(btn);
3141
+ }
3142
+ el.setAttribute("data-has", names.length > 0 ? "1" : "0");
2664
3143
  }
2665
3144
  /**
2666
3145
  * Drive the panel's visual state. Idempotent; NEVER emits a control — the shared path for both the
2667
3146
  * optimistic local click and the authoritative server PRESENTER echo. Only the ended-border fade
2668
3147
  * touches a clock, via the injected native timer.
2669
3148
  */
2670
- setState(state, text) {
3149
+ setState(state, text, tone) {
2671
3150
  this.#state = state;
2672
3151
  this.#root?.setAttribute(DATA_IRIS_STATE, state);
3152
+ const handoff = tone !== void 0 && tone !== PresenterTone.CALM;
3153
+ if (handoff)
3154
+ this.#root?.setAttribute(DATA_IRIS_TONE, tone);
3155
+ else
3156
+ this.#root?.removeAttribute(DATA_IRIS_TONE);
2673
3157
  if (this.#fadeTimer !== void 0) {
2674
3158
  nativeClearTimeout(this.#fadeTimer);
2675
3159
  this.#fadeTimer = void 0;
@@ -2687,8 +3171,8 @@ var ControlPanel = class {
2687
3171
  if (refs2.input !== void 0)
2688
3172
  refs2.input.disabled = ended;
2689
3173
  if (refs2.banner !== void 0) {
2690
- const summary = text !== void 0 && text.trim().length > 0 ? ` \xB7 ${text.trim()}` : "";
2691
- refs2.banner.textContent = `${ENDED_BANNER_TEXT}${summary}`;
3174
+ const summary = text !== void 0 && text.trim().length > 0 ? text.trim() : "";
3175
+ refs2.banner.textContent = handoff && summary.length > 0 ? summary : `${ENDED_BANNER_TEXT}${summary.length > 0 ? ` \xB7 ${summary}` : ""}`;
2692
3176
  }
2693
3177
  if (ended) {
2694
3178
  const glow = this.#glow;
@@ -2699,8 +3183,8 @@ var ControlPanel = class {
2699
3183
  }
2700
3184
  };
2701
3185
 
2702
- // ../browser/dist/presenter/presenter.js
2703
- var CSS = `
3186
+ // ../browser/dist/presenter/presenter-styles.js
3187
+ var PRESENTER_CSS = `
2704
3188
  @import url("https://fonts.googleapis.com/css2?family=IBM+Plex+Serif:wght@400;500&family=Inter:wght@400;450;500;600&display=swap");
2705
3189
  [data-iris-glow]{position:fixed;inset:0;pointer-events:none;z-index:2147483600;opacity:0;
2706
3190
  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);}
@@ -2772,6 +3256,14 @@ var CSS = `
2772
3256
  [data-iris-hud] [data-iris-min-btn]:hover{color:var(--iris-fg);background:rgba(255,255,255,.08);}
2773
3257
  [data-iris-hud] [data-iris-min-btn]:active{transform:scale(.94);}
2774
3258
  [data-iris-hud] .iris-pass{color:var(--iris-ok);}[data-iris-hud] .iris-fail{color:var(--iris-bad);}
3259
+ [data-iris-hud] .iris-tally{flex:none;display:inline-flex;align-items:center;gap:8px;font-size:11.5px;
3260
+ font-weight:600;font-variant-numeric:tabular-nums;letter-spacing:.01em;}
3261
+ [data-iris-hud] .iris-tally[hidden]{display:none;}
3262
+ [data-iris-hud] .iris-tally .iris-t-pass{color:var(--iris-ok);}
3263
+ [data-iris-hud] .iris-tally .iris-t-fail{color:var(--iris-bad);}
3264
+ [data-iris-hud] .iris-tally [data-z="1"]{opacity:.4;}
3265
+ @keyframes iris-tally-pop{0%{transform:scale(1)}38%{transform:scale(1.3)}100%{transform:scale(1)}}
3266
+ [data-iris-hud] .iris-tally [data-bump="1"]{display:inline-block;animation:iris-tally-pop .36s cubic-bezier(.16,1,.3,1);}
2775
3267
  [data-iris-hud] .iris-chip{display:none;flex:none;font-size:9px;font-weight:600;letter-spacing:.08em;
2776
3268
  padding:2px 7px;border-radius:6px;vertical-align:middle;}
2777
3269
  [data-iris-hud] .iris-chip[data-mode="reading"]{display:inline-block;color:var(--iris-read);
@@ -2802,6 +3294,8 @@ var CSS = `
2802
3294
  [data-iris-overlay][data-iris-throttled="1"] [data-iris-hud]{--iris-accent:#fbbf24;--iris-accent-soft:rgba(251,191,36,.16);}
2803
3295
  ${LOG_CSS}
2804
3296
  ${CONTROLS_CSS}`;
3297
+
3298
+ // ../browser/dist/presenter/presenter-config.js
2805
3299
  var BorderMode = { SESSION: "session", BUSY: "busy" };
2806
3300
  var DEFAULT_BORDER_MODE = BorderMode.SESSION;
2807
3301
  var DATA_BUSY = "data-busy";
@@ -2824,27 +3318,224 @@ var GLOW_OFF2 = "0";
2824
3318
  var DATA_ON2 = "data-on";
2825
3319
  var MIN_ATTR = "data-iris-min";
2826
3320
  var THROTTLED_ATTR = "data-iris-throttled";
2827
- var Presenter = class {
2828
- #paceMs;
2829
- #root;
2830
- #glow;
2831
- #cursor;
2832
- #ring;
2833
- #hud;
2834
- #actLine;
2835
- #chip;
2836
- #liveLine;
2837
- #mode = PresenterMode.IDLE;
3321
+
3322
+ // ../browser/dist/presenter/presenter-run-state.js
3323
+ function buildRunState(input) {
3324
+ const start = input.startMs ?? input.now;
3325
+ const counts = { reads: 0, acts: 0, narrations: 0, human: 0, passes: 0, fails: 0 };
3326
+ for (const e of input.runLog) {
3327
+ if (e.kind === LOG_KIND.READ)
3328
+ counts.reads += 1;
3329
+ else if (e.kind === LOG_KIND.ACT)
3330
+ counts.acts += 1;
3331
+ else if (e.kind === LOG_KIND.NARRATION)
3332
+ counts.narrations += 1;
3333
+ else if (e.kind === LOG_KIND.HUMAN)
3334
+ counts.human += 1;
3335
+ if (e.result === LOG_RESULT.PASS)
3336
+ counts.passes += 1;
3337
+ else if (e.result === LOG_RESULT.FAIL)
3338
+ counts.fails += 1;
3339
+ }
3340
+ return {
3341
+ session: input.sessionId,
3342
+ url: typeof location === "undefined" ? "" : location.href,
3343
+ state: input.state,
3344
+ startedMs: start,
3345
+ durationMs: Math.max(0, (input.endMs ?? input.now) - start),
3346
+ counts,
3347
+ capabilities: getCapabilities(),
3348
+ log: input.runLog.map((e) => ({ ...e }))
3349
+ };
3350
+ }
3351
+
3352
+ // ../browser/dist/presenter/presenter-effects.js
3353
+ function moveCursor(cursor, x, y) {
3354
+ if (cursor === void 0)
3355
+ return;
3356
+ cursor.setAttribute("data-on", "1");
3357
+ cursor.style.transform = `translate(${String(x)}px, ${String(y)}px)`;
3358
+ }
3359
+ function ringAround(ring, rect) {
3360
+ if (ring === void 0)
3361
+ return;
3362
+ ring.style.left = `${String(rect.left - 4)}px`;
3363
+ ring.style.top = `${String(rect.top - 4)}px`;
3364
+ ring.style.width = `${String(rect.width + 8)}px`;
3365
+ ring.style.height = `${String(rect.height + 8)}px`;
3366
+ ring.setAttribute("data-on", "1");
3367
+ nativeSetTimeout(() => ring.setAttribute("data-on", "0"), 700);
3368
+ }
3369
+ function spawnRipple(root, x, y) {
3370
+ if (root === void 0)
3371
+ return;
3372
+ const r = document.createElement("div");
3373
+ r.setAttribute("data-iris-ripple", "");
3374
+ r.style.left = `${String(x)}px`;
3375
+ r.style.top = `${String(y)}px`;
3376
+ root.appendChild(r);
3377
+ nativeSetTimeout(() => r.remove(), 520);
3378
+ }
3379
+ function pace(ms) {
3380
+ return new Promise((res) => nativeSetTimeout(res, ms));
3381
+ }
3382
+
3383
+ // ../browser/dist/presenter/presenter-glow.js
3384
+ var GlowController = class {
3385
+ #phase = GlowPhase.IDLE;
3386
+ #lastActivityMs = 0;
3387
+ #idleCheckTimer;
3388
+ #fadeTimer;
3389
+ #glow;
3390
+ #cursor;
2838
3391
  #now;
2839
3392
  #idleAfterMs;
2840
3393
  #glowFadeMs;
3394
+ #borderMode;
3395
+ #setMode;
3396
+ constructor(deps) {
3397
+ this.#now = deps.now;
3398
+ this.#idleAfterMs = deps.idleAfterMs;
3399
+ this.#glowFadeMs = deps.glowFadeMs;
3400
+ this.#borderMode = deps.borderMode;
3401
+ this.#setMode = deps.setMode;
3402
+ }
3403
+ /** Wire the glow + cursor elements after the Presenter mounts the DOM. */
3404
+ setElements(glow, cursor) {
3405
+ this.#glow = glow;
3406
+ this.#cursor = cursor;
3407
+ }
3408
+ /** Current glow phase (test/diagnostic accessor). */
3409
+ phase() {
3410
+ return this.#phase;
3411
+ }
3412
+ /** Last activity timestamp — read by the Presenter's liveness heartbeat. */
3413
+ lastActivityMs() {
3414
+ return this.#lastActivityMs;
3415
+ }
3416
+ /** Set the activity baseline WITHOUT entering busy (sessionStart / revive). */
3417
+ resetActivity(ms) {
3418
+ this.#lastActivityMs = ms;
3419
+ }
3420
+ /**
3421
+ * Record agent activity. Idempotent while busy — only the first activity from idle/fading flips the
3422
+ * glow on (no strobe). `ms` lets log() read the clock exactly once per row.
3423
+ */
3424
+ markActivity(ms = this.#now()) {
3425
+ this.#lastActivityMs = ms;
3426
+ if (this.#phase === GlowPhase.IDLE || this.#phase === GlowPhase.FADING)
3427
+ this.#enterBusy();
3428
+ this.#armIdleCheck();
3429
+ }
3430
+ /** Re-arm the quiet-window idle check (kept for iris.ts's finally block). */
3431
+ scheduleIdle() {
3432
+ this.#armIdleCheck();
3433
+ }
3434
+ /** Clear both timers (called from Presenter.destroy). */
3435
+ teardown() {
3436
+ if (this.#idleCheckTimer !== void 0)
3437
+ nativeClearTimeout(this.#idleCheckTimer);
3438
+ if (this.#fadeTimer !== void 0)
3439
+ nativeClearTimeout(this.#fadeTimer);
3440
+ this.#idleCheckTimer = void 0;
3441
+ this.#fadeTimer = void 0;
3442
+ }
3443
+ #enterBusy() {
3444
+ if (this.#fadeTimer !== void 0) {
3445
+ nativeClearTimeout(this.#fadeTimer);
3446
+ this.#fadeTimer = void 0;
3447
+ }
3448
+ this.#phase = GlowPhase.BUSY;
3449
+ if (this.#borderMode === BorderMode.SESSION) {
3450
+ this.#glow?.setAttribute(DATA_BUSY, BUSY_ON);
3451
+ } else {
3452
+ this.#glow?.setAttribute(DATA_ON2, GLOW_ON);
3453
+ }
3454
+ this.#cursor?.setAttribute(DATA_ON2, GLOW_ON);
3455
+ }
3456
+ #armIdleCheck() {
3457
+ if (this.#idleCheckTimer !== void 0)
3458
+ nativeClearTimeout(this.#idleCheckTimer);
3459
+ this.#idleCheckTimer = nativeSetTimeout(() => this.#checkIdle(), this.#idleAfterMs);
3460
+ }
3461
+ #checkIdle() {
3462
+ this.#idleCheckTimer = void 0;
3463
+ if (this.#phase !== GlowPhase.BUSY)
3464
+ return;
3465
+ const quietFor = this.#now() - this.#lastActivityMs;
3466
+ if (quietFor < this.#idleAfterMs) {
3467
+ this.#idleCheckTimer = nativeSetTimeout(() => this.#checkIdle(), this.#idleAfterMs - quietFor);
3468
+ return;
3469
+ }
3470
+ this.#beginFade();
3471
+ }
3472
+ #beginFade() {
3473
+ this.#phase = GlowPhase.FADING;
3474
+ if (this.#borderMode === BorderMode.SESSION) {
3475
+ this.#glow?.setAttribute(DATA_BUSY, BUSY_OFF);
3476
+ } else {
3477
+ this.#glow?.setAttribute(DATA_ON2, GLOW_OFF2);
3478
+ }
3479
+ this.#cursor?.setAttribute(DATA_ON2, GLOW_OFF2);
3480
+ this.#setMode(PresenterMode.IDLE);
3481
+ this.#fadeTimer = nativeSetTimeout(() => {
3482
+ this.#fadeTimer = void 0;
3483
+ if (this.#phase === GlowPhase.FADING)
3484
+ this.#phase = GlowPhase.IDLE;
3485
+ }, this.#glowFadeMs);
3486
+ }
3487
+ };
3488
+
3489
+ // ../browser/dist/presenter/presenter-tally.js
3490
+ function countVerdicts(runLog) {
3491
+ let passes = 0;
3492
+ let fails = 0;
3493
+ for (const e of runLog) {
3494
+ if (e.result === LOG_RESULT.PASS)
3495
+ passes += 1;
3496
+ else if (e.result === LOG_RESULT.FAIL)
3497
+ fails += 1;
3498
+ }
3499
+ return { passes, fails };
3500
+ }
3501
+ function renderTally(el, runLog, prev) {
3502
+ const next = countVerdicts(runLog);
3503
+ if (el === void 0)
3504
+ return next;
3505
+ if (next.passes === 0 && next.fails === 0) {
3506
+ el.setAttribute("hidden", "");
3507
+ return next;
3508
+ }
3509
+ const bumpPass = next.passes > prev.passes ? ' data-bump="1"' : "";
3510
+ const bumpFail = next.fails > prev.fails ? ' data-bump="1"' : "";
3511
+ const dimP = next.passes === 0 ? ' data-z="1"' : "";
3512
+ const dimF = next.fails === 0 ? ' data-z="1"' : "";
3513
+ el.removeAttribute("hidden");
3514
+ el.innerHTML = `<span class="iris-t-pass"${dimP}${bumpPass}>\u2713 ${String(next.passes)}</span><span class="iris-t-fail"${dimF}${bumpFail}>\u2717 ${String(next.fails)}</span>`;
3515
+ return next;
3516
+ }
3517
+
3518
+ // ../browser/dist/presenter/presenter.js
3519
+ var Presenter = class {
3520
+ #paceMs;
3521
+ #root;
3522
+ #glow;
3523
+ #cursor;
3524
+ #ring;
3525
+ #hud;
3526
+ #actLine;
3527
+ #chip;
3528
+ /** Live verdict tally (✓N ✗M) in the header — the running testing score the human watches. */
3529
+ #tally;
3530
+ #tallied = { passes: 0, fails: 0 };
3531
+ #liveLine;
3532
+ #mode = PresenterMode.IDLE;
3533
+ #now;
2841
3534
  #heartbeatMs;
2842
3535
  #idleNoticeMs;
2843
3536
  #borderMode;
2844
- #phase = GlowPhase.IDLE;
2845
- #lastActivityMs = 0;
2846
- #idleCheckTimer;
2847
- #fadeTimer;
3537
+ /** The glow / activity state machine (border shimmer + cursor visibility from activity timing). */
3538
+ #glowCtl;
2848
3539
  /** Liveness: the most recent action text + a 1s ticker that ages it into an "idle · {dur}" clock. */
2849
3540
  #lastActionText = "";
2850
3541
  #heartbeatTimer;
@@ -2867,13 +3558,18 @@ var Presenter = class {
2867
3558
  constructor(options = {}) {
2868
3559
  this.#paceMs = options.paceMs ?? DEFAULT_PACE;
2869
3560
  this.#now = options.now ?? nativeNow;
2870
- this.#idleAfterMs = options.idleAfterMs ?? IDLE_AFTER_MS;
2871
- this.#glowFadeMs = options.glowFadeMs ?? GLOW_FADE_MS;
2872
3561
  this.#heartbeatMs = options.heartbeatMs ?? HEARTBEAT_MS;
2873
3562
  this.#idleNoticeMs = options.idleNoticeMs ?? IDLE_NOTICE_MS;
2874
3563
  this.#idleEndMs = options.idleEndMs ?? IDLE_END_MS;
2875
3564
  this.#sessionId = options.sessionId ?? "";
2876
3565
  this.#borderMode = options.border ?? DEFAULT_BORDER_MODE;
3566
+ this.#glowCtl = new GlowController({
3567
+ now: this.#now,
3568
+ idleAfterMs: options.idleAfterMs ?? IDLE_AFTER_MS,
3569
+ glowFadeMs: options.glowFadeMs ?? GLOW_FADE_MS,
3570
+ borderMode: this.#borderMode,
3571
+ setMode: (mode) => this.setMode(mode)
3572
+ });
2877
3573
  this.#logMax = clampLogMax(options.logMax);
2878
3574
  this.#onControl = options.onControl;
2879
3575
  this.#panel = new ControlPanel({
@@ -2898,8 +3594,21 @@ var Presenter = class {
2898
3594
  return this.#sessionActive;
2899
3595
  }
2900
3596
  /** Drive the panel's live-control visual state (server-push / agent path; never emits). */
2901
- setState(state, text) {
2902
- this.#panel.setState(state, text);
3597
+ setState(state, text, tone) {
3598
+ this.#panel.setState(state, text, tone);
3599
+ }
3600
+ /** Apply a bridge→browser presenter push: PRESENTER (state echo) or FLOWS (replay list, the human's
3601
+ * no-agent replay surface). Owns the wire parsing so the SDK dispatcher stays a thin router;
3602
+ * setState-only so an echo can't re-emit. */
3603
+ handlePush(command) {
3604
+ const a = command.args;
3605
+ if (command.name === IrisCommand.FLOWS)
3606
+ return void this.#panel.setFlows(a["flows"]);
3607
+ const state = a["state"];
3608
+ const tone = a["tone"];
3609
+ const text = typeof a["text"] === "string" && a["text"].length > 0 ? a["text"] : void 0;
3610
+ if (isSessionState(state))
3611
+ this.setState(state, text, isPresenterTone(tone) ? tone : void 0);
2903
3612
  }
2904
3613
  /** Current cap on accumulated log rows. */
2905
3614
  get logMax() {
@@ -2914,7 +3623,7 @@ var Presenter = class {
2914
3623
  return;
2915
3624
  const style = document.createElement("style");
2916
3625
  style.setAttribute("data-iris-overlay", "");
2917
- style.textContent = CSS;
3626
+ style.textContent = PRESENTER_CSS;
2918
3627
  document.head.appendChild(style);
2919
3628
  const root = document.createElement("div");
2920
3629
  root.setAttribute("data-iris-overlay", "");
@@ -2923,10 +3632,11 @@ var Presenter = class {
2923
3632
  <div data-iris-cursor></div>
2924
3633
  <div data-iris-ring></div>
2925
3634
  <div data-iris-hud>
2926
- <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>
3635
+ <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-tally" data-iris-tally hidden></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>
2927
3636
  <div class="iris-act-strip"><span class="iris-act">idle</span></div>
2928
3637
  ${CONTROLS_BANNER_HTML}
2929
3638
  <div ${DATA_IRIS_LOG}></div>
3639
+ ${CONTROLS_FLOWS_HTML}
2930
3640
  ${CONTROLS_FOOT_HTML}
2931
3641
  </div>`;
2932
3642
  document.body.appendChild(root);
@@ -2938,6 +3648,7 @@ var Presenter = class {
2938
3648
  this.#actLine = root.querySelector(".iris-act") ?? void 0;
2939
3649
  this.#log = root.querySelector(`[${DATA_IRIS_LOG}]`) ?? void 0;
2940
3650
  this.#chip = root.querySelector("[data-iris-chip]") ?? void 0;
3651
+ this.#tally = root.querySelector("[data-iris-tally]") ?? void 0;
2941
3652
  this.#liveLine = root.querySelector(".iris-live") ?? void 0;
2942
3653
  const setMin = (on) => root.setAttribute(MIN_ATTR, on ? "1" : "0");
2943
3654
  root.querySelector("[data-iris-min-btn]")?.addEventListener("click", (e) => {
@@ -2948,20 +3659,16 @@ var Presenter = class {
2948
3659
  if (root.getAttribute(MIN_ATTR) === "1")
2949
3660
  setMin(false);
2950
3661
  });
3662
+ this.#glowCtl.setElements(this.#glow, this.#cursor);
2951
3663
  this.#panel.mount(root, this.#glow);
2952
3664
  this.setMode(this.#mode);
2953
3665
  }
2954
3666
  destroy() {
2955
- if (this.#idleCheckTimer !== void 0)
2956
- nativeClearTimeout(this.#idleCheckTimer);
2957
- if (this.#fadeTimer !== void 0)
2958
- nativeClearTimeout(this.#fadeTimer);
3667
+ this.#glowCtl.teardown();
2959
3668
  if (this.#heartbeatTimer !== void 0)
2960
3669
  nativeClearTimeout(this.#heartbeatTimer);
2961
3670
  this.#heartbeatTimer = void 0;
2962
3671
  this.#panel.teardown();
2963
- this.#idleCheckTimer = void 0;
2964
- this.#fadeTimer = void 0;
2965
3672
  this.#sessionActive = false;
2966
3673
  this.#logBaseMs = void 0;
2967
3674
  this.#log = void 0;
@@ -2984,7 +3691,7 @@ var Presenter = class {
2984
3691
  this.#startMs ??= this.#now();
2985
3692
  this.#endMs = void 0;
2986
3693
  this.#showSession();
2987
- this.#lastActivityMs = this.#now();
3694
+ this.#glowCtl.resetActivity(this.#now());
2988
3695
  this.#startHeartbeat();
2989
3696
  }
2990
3697
  /** Turn the base border (session mode) + the HUD/log on — the visible "session is live" state. */
@@ -2998,7 +3705,7 @@ var Presenter = class {
2998
3705
  this.#panel.setState(SessionState.ACTIVE);
2999
3706
  this.#endMs = void 0;
3000
3707
  this.#showSession();
3001
- this.#lastActivityMs = this.#now();
3708
+ this.#glowCtl.resetActivity(this.#now());
3002
3709
  this.#startHeartbeat();
3003
3710
  }
3004
3711
  /**
@@ -3025,23 +3732,15 @@ var Presenter = class {
3025
3732
  * just refresh the last-activity timestamp and re-arm the idle check.
3026
3733
  */
3027
3734
  markActivity() {
3028
- this.#markActivityAt(this.#now());
3029
- }
3030
- /** markActivity with a caller-supplied timestamp so log() reads the clock exactly once per row. */
3031
- #markActivityAt(ms) {
3032
- this.#lastActivityMs = ms;
3033
- if (this.#phase === GlowPhase.IDLE || this.#phase === GlowPhase.FADING) {
3034
- this.#enterBusy();
3035
- }
3036
- this.#armIdleCheck();
3735
+ this.#glowCtl.markActivity();
3037
3736
  }
3038
3737
  /** Re-arm the quiet-window idle check (kept for iris.ts's finally block). */
3039
3738
  scheduleIdle() {
3040
- this.#armIdleCheck();
3739
+ this.#glowCtl.scheduleIdle();
3041
3740
  }
3042
3741
  /** Test/diagnostic accessor for the current glow phase. */
3043
3742
  glowPhase() {
3044
- return this.#phase;
3743
+ return this.#glowCtl.phase();
3045
3744
  }
3046
3745
  /** Current intent (reading vs acting), exposed for tests + the watcher. */
3047
3746
  get mode() {
@@ -3061,50 +3760,6 @@ var Presenter = class {
3061
3760
  if (mode === PresenterMode.READING)
3062
3761
  this.#cursor?.setAttribute(DATA_ON2, GLOW_OFF2);
3063
3762
  }
3064
- #enterBusy() {
3065
- if (this.#fadeTimer !== void 0) {
3066
- nativeClearTimeout(this.#fadeTimer);
3067
- this.#fadeTimer = void 0;
3068
- }
3069
- this.#phase = GlowPhase.BUSY;
3070
- if (this.#borderMode === BorderMode.SESSION) {
3071
- this.#glow?.setAttribute(DATA_BUSY, BUSY_ON);
3072
- } else {
3073
- this.#glow?.setAttribute(DATA_ON2, GLOW_ON);
3074
- }
3075
- this.#cursor?.setAttribute(DATA_ON2, GLOW_ON);
3076
- }
3077
- #armIdleCheck() {
3078
- if (this.#idleCheckTimer !== void 0)
3079
- nativeClearTimeout(this.#idleCheckTimer);
3080
- this.#idleCheckTimer = nativeSetTimeout(() => this.#checkIdle(), this.#idleAfterMs);
3081
- }
3082
- #checkIdle() {
3083
- this.#idleCheckTimer = void 0;
3084
- if (this.#phase !== GlowPhase.BUSY)
3085
- return;
3086
- const quietFor = this.#now() - this.#lastActivityMs;
3087
- if (quietFor < this.#idleAfterMs) {
3088
- this.#idleCheckTimer = nativeSetTimeout(() => this.#checkIdle(), this.#idleAfterMs - quietFor);
3089
- return;
3090
- }
3091
- this.#beginFade();
3092
- }
3093
- #beginFade() {
3094
- this.#phase = GlowPhase.FADING;
3095
- if (this.#borderMode === BorderMode.SESSION) {
3096
- this.#glow?.setAttribute(DATA_BUSY, BUSY_OFF);
3097
- } else {
3098
- this.#glow?.setAttribute(DATA_ON2, GLOW_OFF2);
3099
- }
3100
- this.#cursor?.setAttribute(DATA_ON2, GLOW_OFF2);
3101
- this.setMode(PresenterMode.IDLE);
3102
- this.#fadeTimer = nativeSetTimeout(() => {
3103
- this.#fadeTimer = void 0;
3104
- if (this.#phase === GlowPhase.FADING)
3105
- this.#phase = GlowPhase.IDLE;
3106
- }, this.#glowFadeMs);
3107
- }
3108
3763
  status(text) {
3109
3764
  this.markActivity();
3110
3765
  this.#lastActionText = text;
@@ -3131,7 +3786,7 @@ var Presenter = class {
3131
3786
  return;
3132
3787
  if (this.state === SessionState.ENDED)
3133
3788
  return;
3134
- const idleMs = this.#now() - this.#lastActivityMs;
3789
+ const idleMs = this.#now() - this.#glowCtl.lastActivityMs();
3135
3790
  if (idleMs >= this.#idleEndMs) {
3136
3791
  this.#endIdle(idleMs);
3137
3792
  return;
@@ -3162,33 +3817,14 @@ var Presenter = class {
3162
3817
  * (The full network/console ring-buffer lives server-side; this is the in-page run summary.)
3163
3818
  */
3164
3819
  runState() {
3165
- const now = this.#now();
3166
- const start = this.#startMs ?? now;
3167
- const counts = { reads: 0, acts: 0, narrations: 0, human: 0, passes: 0, fails: 0 };
3168
- for (const e of this.#runLog) {
3169
- if (e.kind === LOG_KIND.READ)
3170
- counts.reads += 1;
3171
- else if (e.kind === LOG_KIND.ACT)
3172
- counts.acts += 1;
3173
- else if (e.kind === LOG_KIND.NARRATION)
3174
- counts.narrations += 1;
3175
- else if (e.kind === LOG_KIND.HUMAN)
3176
- counts.human += 1;
3177
- if (e.result === LOG_RESULT.PASS)
3178
- counts.passes += 1;
3179
- else if (e.result === LOG_RESULT.FAIL)
3180
- counts.fails += 1;
3181
- }
3182
- return {
3183
- session: this.#sessionId,
3184
- url: typeof location === "undefined" ? "" : location.href,
3820
+ return buildRunState({
3821
+ sessionId: this.#sessionId,
3185
3822
  state: this.state,
3186
- startedMs: start,
3187
- durationMs: Math.max(0, (this.#endMs ?? now) - start),
3188
- counts,
3189
- capabilities: getCapabilities(),
3190
- log: this.#runLog.map((e) => ({ ...e }))
3191
- };
3823
+ startMs: this.#startMs,
3824
+ endMs: this.#endMs,
3825
+ now: this.#now(),
3826
+ runLog: this.#runLog
3827
+ });
3192
3828
  }
3193
3829
  /**
3194
3830
  * Append an activity-log row. Accumulates (never overwrites): each call adds a timestamped row
@@ -3197,7 +3833,7 @@ var Presenter = class {
3197
3833
  */
3198
3834
  log(kind, text, result2) {
3199
3835
  const ms = this.#now();
3200
- this.#markActivityAt(ms);
3836
+ this.#glowCtl.markActivity(ms);
3201
3837
  if (this.#log === void 0)
3202
3838
  return void 0;
3203
3839
  const trimmed = text.trim();
@@ -3214,13 +3850,19 @@ var Presenter = class {
3214
3850
  handle.result(result2);
3215
3851
  if (this.#liveLine !== void 0)
3216
3852
  this.#liveLine.textContent = trimmed;
3853
+ this.#renderTally();
3217
3854
  return {
3218
3855
  result: (r) => {
3219
3856
  handle.result(r);
3220
3857
  entry.result = r;
3858
+ this.#renderTally();
3221
3859
  }
3222
3860
  };
3223
3861
  }
3862
+ /** Repaint the header verdict tally from the run log; the side that grew gets a one-shot pop. */
3863
+ #renderTally() {
3864
+ this.#tallied = renderTally(this.#tally, this.#runLog, this.#tallied);
3865
+ }
3224
3866
  /** Back-compat: narration appends to the live log (append-only, never overwrites). */
3225
3867
  narrate(text, level = "info") {
3226
3868
  const line = level === "info" ? text : `[${level}] ${text}`;
@@ -3252,46 +3894,17 @@ var Presenter = class {
3252
3894
  const el = refs.resolve(refId);
3253
3895
  this.status(`${actionVerb(action)} ${label}`);
3254
3896
  if (!(el instanceof HTMLElement)) {
3255
- await this.#pause();
3897
+ await pace(this.#paceMs);
3256
3898
  return;
3257
3899
  }
3258
3900
  const rect = el.getBoundingClientRect();
3259
3901
  const cx = rect.left + rect.width / 2;
3260
3902
  const cy = rect.top + rect.height / 2;
3261
- this.#moveCursor(cx, cy);
3262
- this.#ringAround(rect);
3263
- await this.#pause();
3903
+ moveCursor(this.#cursor, cx, cy);
3904
+ ringAround(this.#ring, rect);
3905
+ await pace(this.#paceMs);
3264
3906
  if (action === "click" || action === "dblclick" || action === "submit")
3265
- this.#ripple(cx, cy);
3266
- }
3267
- #moveCursor(x, y) {
3268
- if (this.#cursor === void 0)
3269
- return;
3270
- this.#cursor.setAttribute("data-on", "1");
3271
- this.#cursor.style.transform = `translate(${String(x)}px, ${String(y)}px)`;
3272
- }
3273
- #ringAround(rect) {
3274
- if (this.#ring === void 0)
3275
- return;
3276
- this.#ring.style.left = `${String(rect.left - 4)}px`;
3277
- this.#ring.style.top = `${String(rect.top - 4)}px`;
3278
- this.#ring.style.width = `${String(rect.width + 8)}px`;
3279
- this.#ring.style.height = `${String(rect.height + 8)}px`;
3280
- this.#ring.setAttribute("data-on", "1");
3281
- nativeSetTimeout(() => this.#ring?.setAttribute("data-on", "0"), 700);
3282
- }
3283
- #ripple(x, y) {
3284
- if (this.#root === void 0)
3285
- return;
3286
- const r = document.createElement("div");
3287
- r.setAttribute("data-iris-ripple", "");
3288
- r.style.left = `${String(x)}px`;
3289
- r.style.top = `${String(y)}px`;
3290
- this.#root.appendChild(r);
3291
- nativeSetTimeout(() => r.remove(), 520);
3292
- }
3293
- #pause() {
3294
- return new Promise((res) => nativeSetTimeout(res, this.#paceMs));
3907
+ spawnRipple(this.#root, cx, cy);
3295
3908
  }
3296
3909
  };
3297
3910
 
@@ -3350,8 +3963,10 @@ var BUTTON_LABEL = {
3350
3963
  var ANNOTATION_LABEL = {
3351
3964
  [AnnotationKind.ASSERT_SIGNAL]: "assert signal",
3352
3965
  [AnnotationKind.ASSERT_VISIBLE]: "assert visible",
3966
+ [AnnotationKind.ASSERT_STATE]: "assert state",
3353
3967
  [AnnotationKind.MARK_DYNAMIC]: "mark dynamic",
3354
- [AnnotationKind.SUCCESS_STATE]: "success state"
3968
+ [AnnotationKind.SUCCESS_STATE]: "success state",
3969
+ [AnnotationKind.INTENT]: "intent"
3355
3970
  };
3356
3971
  var NEEDS_SIGNAL = /* @__PURE__ */ new Set([
3357
3972
  AnnotationKind.ASSERT_SIGNAL,
@@ -3670,6 +4285,8 @@ var Recorder = class {
3670
4285
  return;
3671
4286
  menu.textContent = "";
3672
4287
  for (const kind of Object.values(AnnotationKind)) {
4288
+ if (kind === AnnotationKind.INTENT)
4289
+ continue;
3673
4290
  const item = document.createElement("button");
3674
4291
  item.setAttribute("data-iris-annkind", kind);
3675
4292
  item.textContent = ANNOTATION_LABEL[kind];
@@ -3720,6 +4337,410 @@ function installRecorder(deps) {
3720
4337
  return new Recorder(deps);
3721
4338
  }
3722
4339
 
4340
+ // ../browser/dist/dom/auto-anchor.js
4341
+ var AnchorStrategy = {
4342
+ TESTID: "testid",
4343
+ COMPONENT: "component",
4344
+ ROLE: "role",
4345
+ POSITION: "position"
4346
+ };
4347
+ function nonEmpty(s) {
4348
+ return typeof s === "string" && s.length > 0;
4349
+ }
4350
+ function sourceTag(source) {
4351
+ const base = source.file.split("/").pop() ?? source.file;
4352
+ return `${base}:${source.line}`;
4353
+ }
4354
+ function synthesizeAnchor(input) {
4355
+ if (nonEmpty(input.testid)) {
4356
+ return { strategy: AnchorStrategy.TESTID, value: input.testid, stable: true };
4357
+ }
4358
+ if (nonEmpty(input.component) && input.source !== void 0) {
4359
+ return {
4360
+ strategy: AnchorStrategy.COMPONENT,
4361
+ value: `${input.component}@${sourceTag(input.source)}`,
4362
+ stable: true
4363
+ };
4364
+ }
4365
+ if (nonEmpty(input.component) && (nonEmpty(input.role) || nonEmpty(input.name))) {
4366
+ const qualifier = nonEmpty(input.name) ? input.name : input.role;
4367
+ return {
4368
+ strategy: AnchorStrategy.COMPONENT,
4369
+ value: `${input.component}[${qualifier ?? ""}]`,
4370
+ stable: true
4371
+ };
4372
+ }
4373
+ if (nonEmpty(input.role) && nonEmpty(input.name)) {
4374
+ return { strategy: AnchorStrategy.ROLE, value: `${input.role}:${input.name}`, stable: false };
4375
+ }
4376
+ if (nonEmpty(input.role)) {
4377
+ const suffix = input.nth !== void 0 ? `#${input.nth}` : "";
4378
+ return { strategy: AnchorStrategy.ROLE, value: `${input.role}${suffix}`, stable: false };
4379
+ }
4380
+ return { strategy: AnchorStrategy.POSITION, value: `el#${input.nth ?? 0}`, stable: false };
4381
+ }
4382
+
4383
+ // ../browser/dist/review/mark-anchor.js
4384
+ var TESTID_ATTR3 = "data-testid";
4385
+ var SOURCE_ATTR2 = "data-iris-source";
4386
+ var STRATEGY = {
4387
+ [AnchorStrategy.TESTID]: MarkAnchorStrategy.TESTID,
4388
+ [AnchorStrategy.COMPONENT]: MarkAnchorStrategy.COMPONENT,
4389
+ [AnchorStrategy.ROLE]: MarkAnchorStrategy.ROLE,
4390
+ [AnchorStrategy.POSITION]: MarkAnchorStrategy.POSITION
4391
+ };
4392
+ function parseSourceAttr(value) {
4393
+ if (value === null)
4394
+ return void 0;
4395
+ const m = /^(.*):(\d+):(\d+)$/.exec(value);
4396
+ if (m === null)
4397
+ return void 0;
4398
+ const file = m[1];
4399
+ const line = Number(m[2]);
4400
+ if (file === void 0 || file.length === 0 || !Number.isFinite(line))
4401
+ return void 0;
4402
+ return { file, line };
4403
+ }
4404
+ function sourceFor(el, adapterSource) {
4405
+ if (adapterSource !== void 0)
4406
+ return { file: adapterSource.file, line: adapterSource.line };
4407
+ const host = el.closest(`[${SOURCE_ATTR2}]`);
4408
+ return host !== null ? parseSourceAttr(host.getAttribute(SOURCE_ATTR2)) : void 0;
4409
+ }
4410
+ function labelFor(el, role, name) {
4411
+ if (name.length > 0)
4412
+ return role.length > 0 ? `${role} "${name}"` : `"${name}"`;
4413
+ return role.length > 0 ? role : el.tagName.toLowerCase();
4414
+ }
4415
+ function resolveMarkAnchor(el) {
4416
+ const testid = el.getAttribute(TESTID_ATTR3) ?? void 0;
4417
+ const info = identifyComponent(el);
4418
+ const component = info?.componentStack[0];
4419
+ const source = sourceFor(el, info?.source);
4420
+ const role = getRole(el);
4421
+ const name = getAccessibleName(el);
4422
+ const input = {};
4423
+ if (testid !== void 0)
4424
+ input.testid = testid;
4425
+ if (component !== void 0)
4426
+ input.component = component;
4427
+ if (source !== void 0)
4428
+ input.source = source;
4429
+ if (role.length > 0)
4430
+ input.role = role;
4431
+ if (name.length > 0)
4432
+ input.name = name;
4433
+ const synthesized = synthesizeAnchor(input);
4434
+ const out = {
4435
+ anchor: synthesized.value,
4436
+ strategy: STRATEGY[synthesized.strategy],
4437
+ label: labelFor(el, role, name)
4438
+ };
4439
+ if (source !== void 0)
4440
+ out.source = source;
4441
+ return out;
4442
+ }
4443
+
4444
+ // ../browser/dist/review/annotator.js
4445
+ var HIGHLIGHT_REST_MS = 130;
4446
+ var MARK_ATTR = "data-iris-mark";
4447
+ var ACTIVE_ATTR = "data-iris-mark-active";
4448
+ var Z = 2147483640;
4449
+ var sel = (role) => `[${MARK_ATTR}="${role}"]`;
4450
+ var CSS = `
4451
+ ${sel("fab")}{position:fixed;left:18px;bottom:18px;z-index:${String(Z + 2)};
4452
+ font:500 13px/1 "Inter",system-ui,sans-serif;display:inline-flex;align-items:center;gap:7px;
4453
+ padding:9px 13px;border-radius:11px;cursor:pointer;color:#e9ebf2;
4454
+ background:linear-gradient(180deg,rgba(19,22,32,.92),rgba(13,15,22,.92));
4455
+ border:1px solid rgba(255,255,255,.12);box-shadow:0 10px 30px -10px rgba(0,0,0,.6);
4456
+ -webkit-backdrop-filter:blur(20px);backdrop-filter:blur(20px);transition:transform .12s,border-color .15s;}
4457
+ ${sel("fab")}:hover{transform:translateY(-1px);border-color:rgba(124,131,255,.55);}
4458
+ ${sel("fab")}[data-on="1"]{color:#fff;border-color:#7c83ff;background:linear-gradient(180deg,#6366f1,#4f46e5);}
4459
+ ${sel("dot")}{width:8px;height:8px;border-radius:50%;background:#ff7a7a;flex:none;}
4460
+ ${sel("fab")}[data-on="1"] ${sel("dot")}{background:#fff;}
4461
+ html[${ACTIVE_ATTR}] *{cursor:crosshair !important;}
4462
+ /* The Flag button + its popover are interactive \u2014 keep the pointer cursor over them, not crosshair. */
4463
+ html[${ACTIVE_ATTR}] ${sel("fab")},html[${ACTIVE_ATTR}] ${sel("fab")} *,html[${ACTIVE_ATTR}] ${sel("pop")},html[${ACTIVE_ATTR}] ${sel("pop")} *{cursor:pointer !important;}
4464
+ /* The outline glides to the rested element with an ease (soothing), and fades rather than snapping. */
4465
+ ${sel("hi")}{position:fixed;z-index:${String(Z + 1)};pointer-events:none;opacity:0;box-sizing:border-box;
4466
+ border:2px solid #7c83ff;border-radius:6px;background:rgba(124,131,255,.12);box-shadow:0 0 0 2px rgba(124,131,255,.22);
4467
+ transition:left .22s cubic-bezier(.22,1,.36,1),top .22s cubic-bezier(.22,1,.36,1),width .22s cubic-bezier(.22,1,.36,1),height .22s cubic-bezier(.22,1,.36,1),opacity .18s ease;}
4468
+ ${sel("hi")}[data-on="1"]{opacity:1;}
4469
+ ${sel("hilabel")}{position:absolute;top:-21px;left:-2px;background:#6366f1;color:#fff;
4470
+ font:600 10.5px/1 "Inter",system-ui,sans-serif;padding:3px 6px;border-radius:5px;white-space:nowrap;
4471
+ max-width:300px;overflow:hidden;text-overflow:ellipsis;}
4472
+ ${sel("pin")}{position:fixed;z-index:${String(Z + 1)};width:22px;height:22px;margin:-11px 0 0 -11px;
4473
+ border-radius:50% 50% 50% 2px;background:#ff5d5d;border:2px solid #fff;box-shadow:0 4px 12px -2px rgba(0,0,0,.5);
4474
+ color:#fff;font:700 11px/18px "Inter",system-ui,sans-serif;text-align:center;pointer-events:none;}
4475
+ ${sel("pop")}{position:fixed;z-index:${String(Z + 3)};width:280px;box-sizing:border-box;
4476
+ background:linear-gradient(180deg,rgba(19,22,32,.96),rgba(13,15,22,.96));border:1px solid rgba(255,255,255,.14);
4477
+ border-radius:14px;padding:12px;box-shadow:0 24px 60px -16px rgba(0,0,0,.7);
4478
+ -webkit-backdrop-filter:blur(24px);backdrop-filter:blur(24px);font:13px/1.5 "Inter",system-ui,sans-serif;color:#e9ebf2;}
4479
+ ${sel("pop")} .iris-mark-where{color:#9aa0b2;font-size:11.5px;margin-bottom:7px;
4480
+ white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
4481
+ ${sel("pop")} textarea{width:100%;box-sizing:border-box;min-height:62px;resize:vertical;
4482
+ background:rgba(0,0,0,.3);border:1px solid rgba(255,255,255,.12);border-radius:9px;color:#e9ebf2;
4483
+ font:13px/1.45 "Inter",system-ui,sans-serif;padding:8px;outline:none;}
4484
+ ${sel("pop")} textarea:focus{border-color:#7c83ff;}
4485
+ ${sel("pop")} .iris-mark-row{display:flex;gap:8px;align-items:center;margin-top:9px;}
4486
+ ${sel("pop")} .iris-mark-hint{margin-right:auto;color:#6a7186;font-size:10.5px;letter-spacing:.02em;}
4487
+ ${sel("pop")} button{font:600 12px/1 "Inter",system-ui,sans-serif;padding:8px 12px;border-radius:9px;cursor:pointer;border:1px solid rgba(255,255,255,.12);background:rgba(255,255,255,.05);color:#cdd2e2;}
4488
+ ${sel("pop")} button[data-send]{background:#6366f1;border-color:#7c83ff;color:#fff;}
4489
+ ${sel("pop")} button[data-send]:disabled{opacity:.5;cursor:default;}`;
4490
+ var Annotator = class {
4491
+ #emit;
4492
+ #now;
4493
+ #onMark;
4494
+ #root;
4495
+ #fab;
4496
+ #pop;
4497
+ /** Hover outline box that shows WHICH element a click would flag (agentation-style). */
4498
+ #hi;
4499
+ #hiLabel;
4500
+ /** Debounce timer — boxes the element only once the cursor rests, so a fast sweep doesn't flicker. */
4501
+ #hiTimer;
4502
+ #active = false;
4503
+ #markCount = 0;
4504
+ #onClick;
4505
+ #onKeydown;
4506
+ #onMove;
4507
+ constructor(deps) {
4508
+ this.#emit = deps.emit;
4509
+ this.#now = deps.now;
4510
+ this.#onMark = deps.onMark;
4511
+ }
4512
+ /** Whether annotate mode is currently capturing clicks. */
4513
+ get active() {
4514
+ return this.#active;
4515
+ }
4516
+ /** Number of marks sent this session (drives the pin numbering). */
4517
+ get markCount() {
4518
+ return this.#markCount;
4519
+ }
4520
+ mount() {
4521
+ if (this.#root !== void 0 || typeof document === "undefined")
4522
+ return;
4523
+ const style = document.createElement("style");
4524
+ style.setAttribute(MARK_ATTR, "style");
4525
+ style.textContent = CSS;
4526
+ document.head.appendChild(style);
4527
+ const root = document.createElement("div");
4528
+ root.setAttribute(MARK_ATTR, "root");
4529
+ root.innerHTML = `<button type="button" ${MARK_ATTR}="fab" aria-label="Flag a bug for the agent">
4530
+ <span ${MARK_ATTR}="dot"></span><span>Flag a bug</span></button>
4531
+ <div ${MARK_ATTR}="hi"><span ${MARK_ATTR}="hilabel"></span></div>`;
4532
+ document.body.appendChild(root);
4533
+ this.#root = root;
4534
+ this.#fab = root.querySelector(sel("fab")) ?? void 0;
4535
+ this.#hi = root.querySelector(sel("hi")) ?? void 0;
4536
+ this.#hiLabel = root.querySelector(sel("hilabel")) ?? void 0;
4537
+ this.#fab?.addEventListener("click", (e) => {
4538
+ e.stopPropagation();
4539
+ this.toggle();
4540
+ });
4541
+ this.#onClick = (ev) => this.#handleClick(ev);
4542
+ document.addEventListener("click", this.#onClick, { capture: true });
4543
+ this.#onMove = (ev) => this.#scheduleMove(ev);
4544
+ document.addEventListener("mousemove", this.#onMove, { passive: true, capture: true });
4545
+ this.#onKeydown = (ev) => {
4546
+ if (ev.key !== "Escape" || !this.#active)
4547
+ return;
4548
+ if (this.#pop !== void 0)
4549
+ this.#closePopover();
4550
+ else
4551
+ this.toggle(false);
4552
+ };
4553
+ document.addEventListener("keydown", this.#onKeydown);
4554
+ }
4555
+ destroy() {
4556
+ if (this.#onClick !== void 0) {
4557
+ document.removeEventListener("click", this.#onClick, { capture: true });
4558
+ this.#onClick = void 0;
4559
+ }
4560
+ if (this.#onKeydown !== void 0) {
4561
+ document.removeEventListener("keydown", this.#onKeydown);
4562
+ this.#onKeydown = void 0;
4563
+ }
4564
+ if (this.#onMove !== void 0) {
4565
+ document.removeEventListener("mousemove", this.#onMove, { capture: true });
4566
+ this.#onMove = void 0;
4567
+ }
4568
+ if (this.#hiTimer !== void 0) {
4569
+ nativeClearTimeout(this.#hiTimer);
4570
+ this.#hiTimer = void 0;
4571
+ }
4572
+ this.#closePopover();
4573
+ document.documentElement.removeAttribute(ACTIVE_ATTR);
4574
+ this.#root?.remove();
4575
+ document.querySelectorAll(`style[${MARK_ATTR}="style"]`).forEach((s) => s.remove());
4576
+ this.#root = void 0;
4577
+ this.#fab = void 0;
4578
+ this.#active = false;
4579
+ }
4580
+ /** Turn annotate mode on/off. With no argument, flips the current state. */
4581
+ toggle(on) {
4582
+ this.#active = on ?? !this.#active;
4583
+ this.#fab?.setAttribute("data-on", this.#active ? "1" : "0");
4584
+ if (this.#active)
4585
+ document.documentElement.setAttribute(ACTIVE_ATTR, "1");
4586
+ else {
4587
+ document.documentElement.removeAttribute(ACTIVE_ATTR);
4588
+ this.#hideHighlight();
4589
+ this.#closePopover();
4590
+ }
4591
+ }
4592
+ #handleClick(ev) {
4593
+ if (!this.#active)
4594
+ return;
4595
+ const target = ev.target;
4596
+ if (!(target instanceof Element))
4597
+ return;
4598
+ if (target.closest(`[${MARK_ATTR}]`) !== null)
4599
+ return;
4600
+ ev.preventDefault();
4601
+ ev.stopPropagation();
4602
+ this.#openPopover(target, ev.clientX, ev.clientY);
4603
+ }
4604
+ /**
4605
+ * Debounce the outline: while the cursor is moving the human is still travelling, so we wait for a
4606
+ * brief rest before boxing the element under it (the box then eases into place via CSS). A pending
4607
+ * timer is replaced on every move, so only the resting position ever paints.
4608
+ */
4609
+ #scheduleMove(ev) {
4610
+ if (this.#hiTimer !== void 0)
4611
+ nativeClearTimeout(this.#hiTimer);
4612
+ this.#hiTimer = nativeSetTimeout(() => {
4613
+ this.#hiTimer = void 0;
4614
+ this.#handleMove(ev);
4615
+ }, HIGHLIGHT_REST_MS);
4616
+ }
4617
+ /** Box the element under the cursor (when active, no popover open) so you see what a click flags. */
4618
+ #handleMove(ev) {
4619
+ if (this.#hi === void 0)
4620
+ return;
4621
+ const target = ev.target;
4622
+ const skip2 = !this.#active || this.#pop !== void 0 || !(target instanceof Element) || target.closest(`[${MARK_ATTR}]`) !== null;
4623
+ if (skip2) {
4624
+ this.#hi.setAttribute("data-on", "0");
4625
+ return;
4626
+ }
4627
+ const rect = target.getBoundingClientRect();
4628
+ if (rect.width === 0 && rect.height === 0) {
4629
+ this.#hi.setAttribute("data-on", "0");
4630
+ return;
4631
+ }
4632
+ this.#hi.style.left = `${String(rect.left)}px`;
4633
+ this.#hi.style.top = `${String(rect.top)}px`;
4634
+ this.#hi.style.width = `${String(rect.width)}px`;
4635
+ this.#hi.style.height = `${String(rect.height)}px`;
4636
+ this.#hi.setAttribute("data-on", "1");
4637
+ if (this.#hiLabel !== void 0)
4638
+ this.#hiLabel.textContent = describeEl(target);
4639
+ }
4640
+ /** Hide the hover outline (annotate mode off, or a popover took over). Cancels any pending box. */
4641
+ #hideHighlight() {
4642
+ if (this.#hiTimer !== void 0) {
4643
+ nativeClearTimeout(this.#hiTimer);
4644
+ this.#hiTimer = void 0;
4645
+ }
4646
+ this.#hi?.setAttribute("data-on", "0");
4647
+ }
4648
+ #openPopover(el, x, y) {
4649
+ this.#closePopover();
4650
+ this.#hideHighlight();
4651
+ const resolved = resolveMarkAnchor(el);
4652
+ const pop = document.createElement("div");
4653
+ pop.setAttribute(MARK_ATTR, "pop");
4654
+ const where = resolved.source !== void 0 ? `${resolved.label} \xB7 ${resolved.source.file}:${String(resolved.source.line)}` : resolved.label;
4655
+ pop.innerHTML = `<div class="iris-mark-where"></div>
4656
+ <textarea placeholder="What's wrong here? The agent will read this and fix it."></textarea>
4657
+ <div class="iris-mark-row"><span class="iris-mark-hint">\u2318\u21B5 send \xB7 esc cancel</span><button type="button" data-cancel>Cancel</button>
4658
+ <button type="button" data-send disabled>Send to agent</button></div>`;
4659
+ const whereEl = pop.querySelector(".iris-mark-where");
4660
+ if (whereEl !== null)
4661
+ whereEl.textContent = where;
4662
+ const left = Math.min(x, window.innerWidth - 296);
4663
+ const top = Math.min(y + 12, window.innerHeight - 170);
4664
+ pop.style.left = `${String(Math.max(8, left))}px`;
4665
+ pop.style.top = `${String(Math.max(8, top))}px`;
4666
+ document.body.appendChild(pop);
4667
+ this.#pop = pop;
4668
+ const textarea = pop.querySelector("textarea");
4669
+ const send = pop.querySelector("button[data-send]");
4670
+ const submit = () => {
4671
+ const note = textarea?.value.trim() ?? "";
4672
+ if (note.length === 0)
4673
+ return;
4674
+ this.#sendMark(note, resolved, x, y);
4675
+ this.#closePopover();
4676
+ };
4677
+ textarea?.addEventListener("input", () => {
4678
+ if (send !== null)
4679
+ send.disabled = textarea.value.trim().length === 0;
4680
+ });
4681
+ textarea?.addEventListener("keydown", (e) => {
4682
+ if (e.key === "Escape") {
4683
+ e.preventDefault();
4684
+ this.#closePopover();
4685
+ } else if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
4686
+ e.preventDefault();
4687
+ submit();
4688
+ }
4689
+ });
4690
+ textarea?.focus();
4691
+ pop.querySelector("button[data-cancel]")?.addEventListener("click", () => this.#closePopover());
4692
+ send?.addEventListener("click", submit);
4693
+ }
4694
+ #sendMark(note, resolved, x, y) {
4695
+ const data = {
4696
+ note,
4697
+ anchor: resolved.anchor,
4698
+ strategy: resolved.strategy,
4699
+ label: resolved.label,
4700
+ route: typeof location === "undefined" ? "" : location.pathname + location.search
4701
+ };
4702
+ if (resolved.source !== void 0)
4703
+ data["source"] = resolved.source;
4704
+ this.#emit(EventType.HUMAN_MARK, data);
4705
+ this.#onMark?.(note, resolved.label);
4706
+ this.#markCount += 1;
4707
+ this.#dropPin(x, y, this.#markCount);
4708
+ }
4709
+ #dropPin(x, y, n) {
4710
+ if (this.#root === void 0)
4711
+ return;
4712
+ const pin = document.createElement("div");
4713
+ pin.setAttribute(MARK_ATTR, "pin");
4714
+ pin.style.left = `${String(x)}px`;
4715
+ pin.style.top = `${String(y)}px`;
4716
+ pin.textContent = String(n);
4717
+ this.#root.appendChild(pin);
4718
+ const ref = pin;
4719
+ this.#now();
4720
+ window.setTimeout(() => ref.remove(), 2600);
4721
+ }
4722
+ #closePopover() {
4723
+ this.#pop?.remove();
4724
+ this.#pop = void 0;
4725
+ }
4726
+ };
4727
+ function describeEl(el) {
4728
+ const testid = el.getAttribute("data-testid");
4729
+ if (testid !== null && testid.length > 0)
4730
+ return testid;
4731
+ const tag = el.tagName.toLowerCase();
4732
+ const aria = el.getAttribute("aria-label");
4733
+ if (aria !== null && aria.length > 0)
4734
+ return `${tag} "${aria}"`;
4735
+ const text = (el.textContent ?? "").trim().replace(/\s+/g, " ").slice(0, 40);
4736
+ return text.length > 0 ? `${tag} "${text}"` : tag;
4737
+ }
4738
+ function installAnnotator(deps) {
4739
+ const annotator = new Annotator(deps);
4740
+ annotator.mount();
4741
+ return annotator;
4742
+ }
4743
+
3723
4744
  // ../browser/dist/iris.js
3724
4745
  function connectionPolicy(pageHostname, bridgeUrl, allowNonLocalhost, token) {
3725
4746
  let bridge;
@@ -3762,9 +4783,6 @@ var BRIDGE_LOST_SUMMARY = "Session ended \u2014 lost connection to Iris (the age
3762
4783
  function resolveSessionLabel(option, gen) {
3763
4784
  return option === void 0 || option === SESSION_AUTO ? gen() : option;
3764
4785
  }
3765
- function isSessionState(value) {
3766
- return value === SessionState.ACTIVE || value === SessionState.PAUSED || value === SessionState.ENDED;
3767
- }
3768
4786
  function refLabel(refId) {
3769
4787
  const el = refs.resolve(refId);
3770
4788
  if (!(el instanceof Element))
@@ -3782,6 +4800,7 @@ var Iris = class {
3782
4800
  #overlay;
3783
4801
  #presenter;
3784
4802
  #recorder;
4803
+ #annotator;
3785
4804
  #eventCount = 0;
3786
4805
  #token;
3787
4806
  /** Act-row log handle for the in-flight act/act_sequence, so its outcome stamps the right row. */
@@ -3856,6 +4875,16 @@ var Iris = class {
3856
4875
  this.#recorder = installRecorder({ emit, now: () => Date.now() });
3857
4876
  this.#recorder.mount();
3858
4877
  }
4878
+ if (options.annotate ?? options.present === true) {
4879
+ const presenter = this.#presenter;
4880
+ this.#annotator = new Annotator({
4881
+ emit,
4882
+ now: () => Date.now(),
4883
+ // Echo the flag into the live panel so the human watches their bug report land in the log.
4884
+ onMark: (note, label) => presenter?.log(LOG_KIND.HUMAN, `\u{1F6A9} ${label}: ${note}`)
4885
+ });
4886
+ this.#annotator.mount();
4887
+ }
3859
4888
  this.#transport.connect();
3860
4889
  this.#connected = true;
3861
4890
  }
@@ -3894,6 +4923,8 @@ var Iris = class {
3894
4923
  this.#presenter = void 0;
3895
4924
  this.#recorder?.destroy();
3896
4925
  this.#recorder = void 0;
4926
+ this.#annotator?.destroy();
4927
+ this.#annotator = void 0;
3897
4928
  resetClock();
3898
4929
  this.#connected = false;
3899
4930
  }
@@ -3933,11 +4964,8 @@ var Iris = class {
3933
4964
  this.#presenter?.setIdleEndMs(idleEndMs);
3934
4965
  return { ok: true, result: { applied: this.#presenter !== void 0, idleEndMs } };
3935
4966
  }
3936
- if (command.name === IrisCommand.PRESENTER) {
3937
- const state = command.args["state"];
3938
- if (isSessionState(state)) {
3939
- this.#presenter?.setState(state, str2(command.args["text"]) || void 0);
3940
- }
4967
+ if (command.name === IrisCommand.PRESENTER || command.name === IrisCommand.FLOWS) {
4968
+ this.#presenter?.handlePush(command);
3941
4969
  return { ok: true, result: { applied: this.#presenter !== void 0 } };
3942
4970
  }
3943
4971
  const handler = this.#registry.get(command.name);
@@ -4080,6 +5108,52 @@ function registerIrisDomain(domain) {
4080
5108
  var globalStore4 = globalThis;
4081
5109
  var iris = globalStore4.__irisInstance ??= new Iris();
4082
5110
 
5111
+ // ../react/dist/render-meter.js
5112
+ var HOOK_KEY = "__REACT_DEVTOOLS_GLOBAL_HOOK__";
5113
+ var RENDER_STORE = "__iris_renders";
5114
+ var commits = 0;
5115
+ var installed2 = false;
5116
+ function noop() {
5117
+ }
5118
+ function getRenderStats() {
5119
+ return { commits };
5120
+ }
5121
+ function installRenderMeter() {
5122
+ if (installed2)
5123
+ return;
5124
+ installed2 = true;
5125
+ try {
5126
+ const root = globalThis;
5127
+ const existing = root[HOOK_KEY];
5128
+ if (existing === void 0) {
5129
+ root[HOOK_KEY] = {
5130
+ supportsFiber: true,
5131
+ renderers: /* @__PURE__ */ new Map(),
5132
+ inject: () => 1,
5133
+ onScheduleFiberRoot: noop,
5134
+ onCommitFiberRoot: () => {
5135
+ commits += 1;
5136
+ },
5137
+ onPostCommitFiberRoot: noop,
5138
+ onCommitFiberUnmount: noop
5139
+ };
5140
+ } else {
5141
+ const original = typeof existing.onCommitFiberRoot === "function" ? existing.onCommitFiberRoot.bind(existing) : void 0;
5142
+ existing.onCommitFiberRoot = (...args) => {
5143
+ commits += 1;
5144
+ if (original !== void 0) {
5145
+ try {
5146
+ original(...args);
5147
+ } catch {
5148
+ }
5149
+ }
5150
+ };
5151
+ }
5152
+ registerStore(RENDER_STORE, () => getRenderStats());
5153
+ } catch {
5154
+ }
5155
+ }
5156
+
4083
5157
  // ../react/dist/index.js
4084
5158
  var FIBER_PREFIXES = ["__reactFiber$", "__reactInternalInstance$"];
4085
5159
  var MAX_DEPTH = 200;
@@ -4243,14 +5317,15 @@ function hasHoverHandlers(el) {
4243
5317
  const p = props;
4244
5318
  return HOVER_HANDLER_KEYS.some((k) => typeof p[k] === "function");
4245
5319
  }
4246
- var installed2 = false;
5320
+ var installed3 = false;
4247
5321
  function install() {
4248
- if (installed2)
5322
+ if (installed3)
4249
5323
  return;
4250
- installed2 = true;
5324
+ installed3 = true;
4251
5325
  registerAdapter({ name: "react", identify, readState: readState2, hasHoverHandlers });
4252
5326
  }
4253
5327
  export {
5328
+ Annotator,
4254
5329
  Iris,
4255
5330
  RefRegistry,
4256
5331
  SESSION_AUTO,
@@ -4271,6 +5346,8 @@ export {
4271
5346
  identify,
4272
5347
  identifyComponent,
4273
5348
  install,
5349
+ installAnnotator,
5350
+ installRenderMeter,
4274
5351
  iris,
4275
5352
  isVisible,
4276
5353
  matchQuery,
@@ -4282,6 +5359,7 @@ export {
4282
5359
  registerCapabilities,
4283
5360
  registerIrisDomain,
4284
5361
  registerStore,
5362
+ resolveMarkAnchor,
4285
5363
  runQuery,
4286
5364
  setIgnoreSelectors,
4287
5365
  storeNames,