@syrin/iris 0.5.0 → 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,8 +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 UpdateCheckIntervalMs = 24 * 60 * 60 * 1e3;
1
+ // ../protocol/dist/flow-constants.js
6
2
  var RunKind = {
7
3
  FLOW_REPLAY: "flow_replay",
8
4
  // auto-recorded by iris_flow_replay
@@ -21,8 +17,10 @@ var AnchorKind = {
21
17
  // { kind:'testid', value }
22
18
  ROLE: "role",
23
19
  // { kind:'role', role, name? } — best-effort fallback
24
- SIGNAL: "signal"
20
+ SIGNAL: "signal",
25
21
  // { kind:'signal', name } — wait/assert anchors
22
+ COMPONENT: "component"
23
+ // { kind:'component', component?, source?, role?, name? } — auto-anchor (no testid)
26
24
  };
27
25
  var DEGRADED_ANCHOR_ROLE = "unresolved";
28
26
  var AnnotationKind = {
@@ -30,10 +28,14 @@ var AnnotationKind = {
30
28
  // → step.expect.signal (invariant)
31
29
  ASSERT_VISIBLE: "assert-visible",
32
30
  // → step.expect.element (invariant)
31
+ ASSERT_STATE: "assert-state",
32
+ // → step.expect.state (store-truth invariant on the last step)
33
33
  MARK_DYNAMIC: "mark-dynamic",
34
34
  // → flow.dynamic[] (don't assert words/content)
35
- SUCCESS_STATE: "success-state"
35
+ SUCCESS_STATE: "success-state",
36
36
  // → flow.success (golden end condition)
37
+ INTENT: "intent"
38
+ // → flow.intent (the business goal this flow exists to verify)
37
39
  };
38
40
  var RecorderPhase = {
39
41
  IDLE: "idle",
@@ -43,6 +45,45 @@ var RecorderPhase = {
43
45
  ANNOTATING: "annotating"
44
46
  // recording paused, awaiting an annotation target/kind
45
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;
82
+ var RING_BUFFER_DEFAULTS = {
83
+ MAX_EVENTS: 2e3,
84
+ MAX_AGE_MS: 6e4,
85
+ MAX_BYTES: TRANSPORT_LIMITS.MAX_BUFFER_BYTES
86
+ };
46
87
  var EventType = {
47
88
  DOM_ADDED: "dom.added",
48
89
  DOM_REMOVED: "dom.removed",
@@ -73,19 +114,37 @@ var EventType = {
73
114
  * Live-control: browser → bridge. A human acted on the presenter panel.
74
115
  * `data: { kind: HumanControlKind; text?: string }`. Rides the existing EventMessage.
75
116
  */
76
- 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"
77
125
  };
78
126
  var SessionState = {
79
127
  ACTIVE: "active",
80
128
  PAUSED: "paused",
81
129
  ENDED: "ended"
82
130
  };
131
+ function isSessionState(value) {
132
+ return value === SessionState.ACTIVE || value === SessionState.PAUSED || value === SessionState.ENDED;
133
+ }
83
134
  var SESSION_AUTO = "auto";
84
135
  var HumanControlKind = {
85
136
  PAUSE: "pause",
86
137
  RESUME: "resume",
87
138
  END: "end",
88
- 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"
89
148
  };
90
149
  var SESSION_HEALTH = {
91
150
  HEARTBEAT_MS: 5e3,
@@ -93,7 +152,11 @@ var SESSION_HEALTH = {
93
152
  STALE_THRESHOLD_MS: 12e3
94
153
  };
95
154
  var SESSION_LIFECYCLE = {
96
- /** 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
+ */
97
160
  IDLE_END_MS: 3e5,
98
161
  /** Floor for a tuned idle window (so an agent can't disable the safety net). */
99
162
  IDLE_END_MIN_MS: 5e3,
@@ -162,7 +225,10 @@ var QueryBy = {
162
225
  LABEL: "label",
163
226
  PLACEHOLDER: "placeholder",
164
227
  TESTID: "testid",
165
- 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"
166
232
  };
167
233
  var IrisCommand = {
168
234
  SNAPSHOT: "snapshot",
@@ -188,7 +254,9 @@ var IrisCommand = {
188
254
  /** Navigate the page to a new URL. `args: { url: string }`. */
189
255
  NAVIGATE: "navigate",
190
256
  /** Reload the page. `args: { hard?: boolean }` — hard clears the cache via location replace trick. */
191
- REFRESH: "refresh"
257
+ REFRESH: "refresh",
258
+ /** Bridge → browser: the saved flows the human can replay from the panel. `args: { flows: [{name}] }`. */
259
+ FLOWS: "flows"
192
260
  };
193
261
  var PresenterMode = {
194
262
  IDLE: "idle",
@@ -207,136 +275,369 @@ var MessageKind = {
207
275
  EVENT: "event"
208
276
  };
209
277
 
210
- // ../protocol/dist/types.js
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
+
289
+ // ../protocol/dist/messages.js
211
290
  import { z } from "zod";
212
- var ElementQuerySchema = z.object({
213
- by: z.nativeEnum(QueryBy).optional(),
214
- value: z.string().optional(),
215
- role: z.string().optional(),
216
- name: z.string().optional(),
217
- text: z.string().optional(),
218
- label: z.string().optional(),
219
- placeholder: z.string().optional(),
220
- testid: z.string().optional(),
221
- alt: z.string().optional(),
291
+ var sessionIdSchema = z.string().min(1).max(TRANSPORT_LIMITS.MAX_SESSION_ID_LENGTH);
292
+ var refSchema = z.string().max(TRANSPORT_LIMITS.MAX_REF_LENGTH);
293
+ var HumanControlDataSchema = z.object({
294
+ kind: z.nativeEnum(HumanControlKind),
295
+ text: z.string().optional()
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
+ });
311
+ var IrisEventSchema = z.object({
312
+ t: z.number(),
313
+ type: z.nativeEnum(EventType),
314
+ sessionId: sessionIdSchema,
315
+ /** Stable element reference this event concerns, when applicable (e.g. "e7"). */
316
+ ref: refSchema.optional(),
317
+ /** Event-type-specific payload. Kept open here; refined per observer at the edges. */
318
+ data: z.record(z.unknown()).default({})
319
+ });
320
+ var HelloMessageSchema = z.object({
321
+ kind: z.literal(MessageKind.HELLO),
322
+ protocolVersion: z.literal(IRIS_PROTOCOL_VERSION),
323
+ sessionId: sessionIdSchema,
324
+ url: z.string().max(TRANSPORT_LIMITS.MAX_URL_LENGTH),
325
+ title: z.string().max(TRANSPORT_LIMITS.MAX_TITLE_LENGTH),
326
+ adapters: z.array(z.string().max(TRANSPORT_LIMITS.MAX_ADAPTER_NAME_LENGTH)).max(TRANSPORT_LIMITS.MAX_ADAPTERS),
327
+ /** Optional browser/bridge pairing token. Required when the bridge configures one. */
328
+ token: z.string().max(TRANSPORT_LIMITS.MAX_TOKEN_LENGTH).optional(),
329
+ /** Whether the app has advertised a capability registry (iris.describe). */
330
+ hasCapabilities: z.boolean().optional()
331
+ });
332
+ var CommandMessageSchema = z.object({
333
+ kind: z.literal(MessageKind.COMMAND),
334
+ id: z.string().min(1).max(TRANSPORT_LIMITS.MAX_COMMAND_ID_LENGTH),
335
+ sessionId: sessionIdSchema.optional(),
336
+ name: z.string().min(1).max(TRANSPORT_LIMITS.MAX_COMMAND_NAME_LENGTH),
337
+ args: z.record(z.unknown()).default({})
338
+ });
339
+ var CommandResultSchema = z.object({
340
+ kind: z.literal(MessageKind.COMMAND_RESULT),
341
+ id: z.string().min(1).max(TRANSPORT_LIMITS.MAX_COMMAND_ID_LENGTH),
342
+ ok: z.boolean(),
343
+ result: z.unknown().optional(),
344
+ error: z.string().max(TRANSPORT_LIMITS.MAX_ERROR_LENGTH).optional()
345
+ });
346
+ var EventMessageSchema = z.object({
347
+ kind: z.literal(MessageKind.EVENT),
348
+ event: IrisEventSchema
349
+ });
350
+ var IrisMessageSchema = z.discriminatedUnion("kind", [
351
+ HelloMessageSchema,
352
+ CommandMessageSchema,
353
+ CommandResultSchema,
354
+ EventMessageSchema
355
+ ]);
356
+
357
+ // ../protocol/dist/security.js
358
+ var DANGEROUS_ACTION = /\b(delete|remove|destroy|erase|drop|terminate|revoke|reset|logout|log out|sign out|close account|cancel subscription|purchase|buy|pay|place order|confirm order|deploy|publish|send|transfer|withdraw|refund)\b/i;
359
+ function isLoopbackHostname(hostname) {
360
+ const normalized = hostname.toLowerCase().replace(/^\[|\]$/g, "");
361
+ if (normalized === "localhost" || normalized === "::1" || normalized === "0:0:0:0:0:0:0:1") {
362
+ return true;
363
+ }
364
+ const octets = normalized.split(".");
365
+ return octets.length === 4 && octets[0] === "127" && octets.every((octet) => {
366
+ if (!/^\d{1,3}$/.test(octet))
367
+ return false;
368
+ const value = Number(octet);
369
+ return value >= 0 && value <= 255;
370
+ });
371
+ }
372
+ function isDangerousActionText(text) {
373
+ return DANGEROUS_ACTION.test(text.replace(/[_-]+/g, " "));
374
+ }
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
+
424
+ // ../protocol/dist/types.js
425
+ import { z as z2 } from "zod";
426
+ var ElementQuerySchema = z2.object({
427
+ by: z2.nativeEnum(QueryBy).optional(),
428
+ value: z2.string().optional(),
429
+ role: z2.string().optional(),
430
+ name: z2.string().optional(),
431
+ text: z2.string().optional(),
432
+ label: z2.string().optional(),
433
+ placeholder: z2.string().optional(),
434
+ testid: z2.string().optional(),
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(),
222
440
  /** CSS selector or ref to scope the search. */
223
- scope: z.string().optional()
441
+ scope: z2.string().optional()
224
442
  });
225
- var CapabilityFlowSchema = z.object({
226
- name: z.string(),
227
- steps: z.array(z.string())
443
+ var CapabilityFlowSchema = z2.object({
444
+ name: z2.string(),
445
+ steps: z2.array(z2.string())
228
446
  });
229
- var CapabilitiesSchema = z.object({
230
- testids: z.array(z.string()),
231
- signals: z.array(z.string()),
232
- stores: z.array(z.string()),
233
- flows: z.array(CapabilityFlowSchema)
447
+ var CapabilitiesSchema = z2.object({
448
+ testids: z2.array(z2.string()),
449
+ signals: z2.array(z2.string()),
450
+ stores: z2.array(z2.string()),
451
+ flows: z2.array(CapabilityFlowSchema)
234
452
  });
235
- var ContractFileSchema = z.object({
236
- version: z.number(),
237
- generatedAt: z.number(),
453
+ var ContractFileSchema = z2.object({
454
+ version: z2.number(),
455
+ generatedAt: z2.number(),
238
456
  capabilities: CapabilitiesSchema
239
457
  });
240
- var RunEvidenceSchema = z.object({
241
- consoleErrors: z.number().optional(),
242
- networkErrors: z.number().optional(),
243
- driftSteps: z.number().optional()
458
+ var RunEvidenceSchema = z2.object({
459
+ consoleErrors: z2.number().optional(),
460
+ networkErrors: z2.number().optional(),
461
+ driftSteps: z2.number().optional()
244
462
  });
245
- var RunRecordSchema = z.object({
246
- kind: z.nativeEnum(RunKind),
247
- name: z.string(),
248
- status: z.nativeEnum(RunStatus),
249
- at: z.number(),
250
- summary: z.string().optional(),
463
+ var RunRecordSchema = z2.object({
464
+ kind: z2.nativeEnum(RunKind),
465
+ name: z2.string(),
466
+ status: z2.nativeEnum(RunStatus),
467
+ at: z2.number(),
468
+ summary: z2.string().optional(),
251
469
  evidence: RunEvidenceSchema.optional(),
252
- durationMs: z.number().optional()
470
+ durationMs: z2.number().optional()
253
471
  });
254
- var ProjectLearnedSchema = z.object({
255
- flows: z.array(z.string()).optional(),
256
- routes: z.array(z.string()).optional()
472
+ var ProjectLearnedSchema = z2.object({
473
+ flows: z2.array(z2.string()).optional(),
474
+ routes: z2.array(z2.string()).optional()
257
475
  });
258
- var ProjectFileSchema = z.object({
259
- version: z.number(),
476
+ var ProjectFileSchema = z2.object({
477
+ version: z2.number(),
260
478
  learned: ProjectLearnedSchema.optional(),
261
- runs: z.array(RunRecordSchema)
479
+ runs: z2.array(RunRecordSchema)
262
480
  });
263
- var FlowAnchorSchema = z.discriminatedUnion("kind", [
264
- z.object({ kind: z.literal(AnchorKind.TESTID), value: z.string().min(1) }),
265
- z.object({
266
- kind: z.literal(AnchorKind.ROLE),
267
- role: z.string().min(1),
268
- name: z.string().optional()
481
+ var FlowAnchorSchema = z2.discriminatedUnion("kind", [
482
+ z2.object({ kind: z2.literal(AnchorKind.TESTID), value: z2.string().min(1) }),
483
+ z2.object({
484
+ kind: z2.literal(AnchorKind.ROLE),
485
+ role: z2.string().min(1),
486
+ name: z2.string().optional()
269
487
  }),
270
- z.object({ kind: z.literal(AnchorKind.SIGNAL), name: z.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
+ })
271
498
  ]);
272
- var FlowExpectSchema = z.object({
273
- signal: z.string().optional(),
499
+ var FlowExpectSchema = z2.object({
500
+ signal: z2.string().optional(),
274
501
  /**
275
502
  * Optional payload shape an `assert-signal` annotation requires the signal
276
503
  * to match (the predicate DSL's signal.dataMatches). Additive/optional — a flow file with a
277
504
  * bare `signal` still parses, and the on-disk version stays FLOW_FILE_VERSION 1.
278
505
  */
279
- signalData: z.record(z.unknown()).optional(),
280
- net: z.object({
281
- method: z.string().optional(),
282
- urlContains: z.string().optional(),
283
- status: z.number().optional()
506
+ signalData: z2.record(z2.unknown()).optional(),
507
+ net: z2.object({
508
+ method: z2.string().optional(),
509
+ urlContains: z2.string().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()
527
+ }).optional(),
528
+ element: z2.object({
529
+ testid: z2.string().optional(),
530
+ role: z2.string().optional(),
531
+ name: z2.string().optional()
284
532
  }).optional(),
285
- element: z.object({
286
- testid: z.string().optional(),
287
- role: z.string().optional(),
288
- name: z.string().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()
289
549
  }).optional()
290
550
  });
291
- var baseFlowStep = z.object({
292
- tool: z.string(),
551
+ var baseFlowStep = z2.object({
552
+ tool: z2.string(),
293
553
  anchor: FlowAnchorSchema,
294
- action: z.nativeEnum(ActionType).optional(),
295
- args: z.record(z.unknown()).optional(),
554
+ action: z2.nativeEnum(ActionType).optional(),
555
+ args: z2.record(z2.unknown()).optional(),
296
556
  expect: FlowExpectSchema.optional(),
297
- degraded: z.boolean().optional()
557
+ degraded: z2.boolean().optional()
298
558
  });
299
559
  var FlowStepSchema = baseFlowStep.extend({
300
- steps: z.lazy(() => z.array(FlowStepSchema).optional())
560
+ steps: z2.lazy(() => z2.array(FlowStepSchema).optional())
301
561
  });
302
- var FlowFileSchema = z.object({
303
- version: z.literal(FLOW_FILE_VERSION),
304
- name: z.string(),
562
+ var FlowFileSchema = z2.object({
563
+ version: z2.literal(FLOW_FILE_VERSION),
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(),
305
573
  // FUTURE: fixtures/preconditions — schema slot reserved, unpopulated this cut. The recorder
306
574
  // never writes it and no fixture runner exists.
307
- fixture: z.string().optional(),
575
+ fixture: z2.string().optional(),
308
576
  /** From the injected clock (ms) — deterministic in tests, byte-stable on disk. */
309
- createdAt: z.number(),
310
- steps: z.array(FlowStepSchema),
577
+ createdAt: z2.number(),
578
+ steps: z2.array(FlowStepSchema),
311
579
  success: FlowExpectSchema.optional(),
312
580
  /**
313
581
  * Anchors whose CONTENT must not be asserted (e.g. LLM output). Replay asserts
314
582
  * presence, not words. Compiled from a `mark-dynamic` annotation.
315
583
  */
316
- dynamic: z.array(FlowAnchorSchema).optional()
584
+ dynamic: z2.array(FlowAnchorSchema).optional()
317
585
  });
318
- var RecordedFlowSchema = z.object({
319
- name: z.string(),
586
+ var RecordedFlowSchema = z2.object({
587
+ name: z2.string(),
320
588
  flow: FlowFileSchema
321
589
  });
322
- var AnnotationSchema = z.discriminatedUnion("kind", [
323
- z.object({
324
- kind: z.literal(AnnotationKind.ASSERT_SIGNAL),
325
- name: z.string().min(1),
326
- dataMatches: z.record(z.unknown()).optional()
590
+ var AnnotationSchema = z2.discriminatedUnion("kind", [
591
+ z2.object({
592
+ kind: z2.literal(AnnotationKind.ASSERT_SIGNAL),
593
+ name: z2.string().min(1),
594
+ dataMatches: z2.record(z2.unknown()).optional()
327
595
  }),
328
- z.object({
329
- kind: z.literal(AnnotationKind.ASSERT_VISIBLE),
330
- testid: z.string().min(1)
596
+ z2.object({
597
+ kind: z2.literal(AnnotationKind.ASSERT_VISIBLE),
598
+ testid: z2.string().min(1)
331
599
  }),
332
- z.object({
333
- kind: z.literal(AnnotationKind.MARK_DYNAMIC),
334
- testid: z.string().min(1)
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()
335
605
  }),
336
- z.object({
337
- kind: z.literal(AnnotationKind.SUCCESS_STATE),
338
- signal: z.string().min(1).optional(),
339
- testid: z.string().min(1).optional()
606
+ z2.object({
607
+ kind: z2.literal(AnnotationKind.MARK_DYNAMIC),
608
+ testid: z2.string().min(1)
609
+ }),
610
+ z2.object({
611
+ kind: z2.literal(AnnotationKind.SUCCESS_STATE),
612
+ signal: 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)
340
641
  })
341
642
  ]);
342
643
 
@@ -371,6 +672,96 @@ var RefRegistry = class {
371
672
  };
372
673
  var refs = new RefRegistry();
373
674
 
675
+ // ../browser/dist/security/serialization.js
676
+ var TRUNCATED_VALUE = "[TRUNCATED]";
677
+ var UNSERIALIZABLE_VALUE = "[UNSERIALIZABLE]";
678
+ var OMIT_VALUE = /* @__PURE__ */ Symbol("omit");
679
+ var MAX_KEY_LENGTH = 256;
680
+ var MAX_TOTAL_CHARACTERS = Math.floor(TRANSPORT_LIMITS.MAX_MESSAGE_BYTES / 8);
681
+ var MAX_TOTAL_NODES = TRANSPORT_LIMITS.MAX_COLLECTION_ITEMS * 5;
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;
683
+ function isSensitiveKey(key) {
684
+ return SENSITIVE_KEY.test(key);
685
+ }
686
+ function boundedString(value, state, max) {
687
+ const allowed = Math.max(0, Math.min(max, state.remainingCharacters));
688
+ if (value.length <= allowed) {
689
+ state.remainingCharacters -= value.length;
690
+ return value;
691
+ }
692
+ const truncated = allowed <= TRUNCATED_VALUE.length ? TRUNCATED_VALUE.slice(0, allowed) : `${value.slice(0, allowed - TRUNCATED_VALUE.length)}${TRUNCATED_VALUE}`;
693
+ state.remainingCharacters -= truncated.length;
694
+ return truncated;
695
+ }
696
+ function sanitize(value, state, depth, key) {
697
+ if (key !== void 0 && isSensitiveKey(key))
698
+ return REDACTED_VALUE;
699
+ if (depth > TRANSPORT_LIMITS.MAX_SERIALIZE_DEPTH || state.nodes >= MAX_TOTAL_NODES) {
700
+ return TRUNCATED_VALUE;
701
+ }
702
+ state.nodes += 1;
703
+ if (value === null || typeof value === "boolean")
704
+ return value;
705
+ if (typeof value === "string") {
706
+ return boundedString(value, state, key?.toLowerCase() === "error" ? TRANSPORT_LIMITS.MAX_ERROR_LENGTH : TRANSPORT_LIMITS.MAX_STRING_LENGTH);
707
+ }
708
+ if (typeof value === "number")
709
+ return Number.isFinite(value) ? value : null;
710
+ if (typeof value === "bigint")
711
+ return value.toString();
712
+ if (typeof value === "undefined" || typeof value === "function" || typeof value === "symbol") {
713
+ return OMIT_VALUE;
714
+ }
715
+ if (value instanceof Date)
716
+ return value.toISOString();
717
+ if (value instanceof Error) {
718
+ return {
719
+ name: boundedString(value.name, state, 256),
720
+ message: boundedString(value.message, state, TRANSPORT_LIMITS.MAX_ERROR_LENGTH)
721
+ };
722
+ }
723
+ if (state.seen.has(value))
724
+ return "[CIRCULAR]";
725
+ state.seen.add(value);
726
+ try {
727
+ if (Array.isArray(value)) {
728
+ return value.slice(0, TRANSPORT_LIMITS.MAX_COLLECTION_ITEMS).map((item) => {
729
+ const sanitized = sanitize(item, state, depth + 1);
730
+ return sanitized === OMIT_VALUE ? null : sanitized;
731
+ });
732
+ }
733
+ const out = /* @__PURE__ */ Object.create(null);
734
+ for (const rawKey of Object.keys(value).slice(0, TRANSPORT_LIMITS.MAX_OBJECT_KEYS)) {
735
+ const safeKey = boundedString(rawKey, state, MAX_KEY_LENGTH);
736
+ try {
737
+ const sanitized = sanitize(value[rawKey], state, depth + 1, rawKey);
738
+ if (sanitized !== OMIT_VALUE)
739
+ out[safeKey] = sanitized;
740
+ } catch {
741
+ out[safeKey] = UNSERIALIZABLE_VALUE;
742
+ }
743
+ }
744
+ return out;
745
+ } finally {
746
+ state.seen.delete(value);
747
+ }
748
+ }
749
+ function sanitizeForTransport(value) {
750
+ const sanitized = sanitize(value, {
751
+ seen: /* @__PURE__ */ new WeakSet(),
752
+ remainingCharacters: MAX_TOTAL_CHARACTERS,
753
+ nodes: 0
754
+ }, 0);
755
+ return sanitized === OMIT_VALUE ? null : sanitized;
756
+ }
757
+ function safeStringify(value) {
758
+ try {
759
+ return JSON.stringify(sanitizeForTransport(value));
760
+ } catch {
761
+ return JSON.stringify(UNSERIALIZABLE_VALUE);
762
+ }
763
+ }
764
+
374
765
  // ../browser/dist/dom/a11y.js
375
766
  var NAME_FROM_CONTENT = /* @__PURE__ */ new Set([
376
767
  "button",
@@ -537,6 +928,17 @@ function getStates(el) {
537
928
  }
538
929
  function getValue(el) {
539
930
  if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement) {
931
+ const autocomplete = el.getAttribute("autocomplete") ?? "";
932
+ const identifiers = [
933
+ el.getAttribute("name") ?? "",
934
+ el.id,
935
+ el.getAttribute("data-testid") ?? "",
936
+ el.getAttribute("aria-label") ?? ""
937
+ ];
938
+ const sensitiveAutocomplete = /current-password|new-password|cc-number|cc-csc|one-time-code/i.test(autocomplete);
939
+ if (el instanceof HTMLInputElement && el.type.toLowerCase() === "password" || sensitiveAutocomplete || identifiers.some(isSensitiveKey)) {
940
+ return REDACTED_VALUE;
941
+ }
540
942
  return el.value;
541
943
  }
542
944
  const valueNow = el.getAttribute("aria-valuenow");
@@ -598,8 +1000,8 @@ function isIrisOverlay(el) {
598
1000
  return el.closest(IRIS_OVERLAY) !== null;
599
1001
  }
600
1002
  function isIgnored(el) {
601
- const sel = extraIgnore.length > 0 ? `${IRIS_OVERLAY},${DEV_OVERLAYS},${extraIgnore}` : `${IRIS_OVERLAY},${DEV_OVERLAYS}`;
602
- 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;
603
1005
  }
604
1006
 
605
1007
  // ../browser/dist/dom/snapshot.js
@@ -619,6 +1021,16 @@ var INTERACTIVE = /* @__PURE__ */ new Set([
619
1021
  "option"
620
1022
  ]);
621
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
+ }
622
1034
  function skip(el) {
623
1035
  if (SKIP_TAGS.has(el.tagName.toLowerCase()))
624
1036
  return true;
@@ -640,13 +1052,29 @@ function stateSuffix(el) {
640
1052
  const states = getStates(el).filter((s) => s === ElementState.DISABLED || s === ElementState.CHECKED || s === ElementState.EXPANDED || s === ElementState.FOCUSED);
641
1053
  return states.length > 0 ? ` [${states.join(",")}]` : "";
642
1054
  }
643
- function formatLine(el, depth, role, name) {
1055
+ function formatLine(el, depth, role, name, layout) {
644
1056
  const indent = " ".repeat(depth);
645
1057
  const value = getValue(el);
646
1058
  const namePart = name.length > 0 ? ` "${name}"` : "";
647
1059
  const refPart = INTERACTIVE.has(role) || name.length > 0 ? ` (ref=${refs.refFor(el)})` : "";
648
1060
  const valuePart = value !== void 0 && value.length > 0 ? ` [value="${value}"]` : "";
649
- 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 "";
650
1078
  }
651
1079
  function walk(parent, depth, ctx) {
652
1080
  if (depth > ctx.maxDepth)
@@ -661,11 +1089,14 @@ function walk(parent, depth, ctx) {
661
1089
  const role = getRole(child);
662
1090
  const name = getAccessibleName(child);
663
1091
  const interactive = INTERACTIVE.has(role);
664
- const meaningful = interactive || role !== "generic" || name.length > 0;
665
- 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;
666
1097
  if (include) {
667
1098
  ctx.nodes += 1;
668
- 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));
669
1100
  walk(child, depth + 1, ctx);
670
1101
  } else {
671
1102
  walk(child, depth, ctx);
@@ -756,9 +1187,50 @@ function hasCapabilities() {
756
1187
  return capabilities.testids.length > 0 || capabilities.signals.length > 0 || capabilities.stores.length > 0 || capabilities.flows.length > 0;
757
1188
  }
758
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
+
759
1228
  // ../browser/dist/dom/query.js
760
1229
  var TESTID_ATTR = "data-testid";
1230
+ var SOURCE_ATTR = "data-iris-source";
761
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]";
762
1234
  function resolveContainer(scope) {
763
1235
  const body = document.body;
764
1236
  if (scope === void 0)
@@ -774,6 +1246,38 @@ function resolveContainer(scope) {
774
1246
  }
775
1247
  return body;
776
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
+ }
777
1281
  function findCandidates(query) {
778
1282
  const container = resolveContainer(query.scope);
779
1283
  const by = query.by;
@@ -792,10 +1296,15 @@ function findCandidates(query) {
792
1296
  return queryAllByTestId(container, value, { exact: true });
793
1297
  case QueryBy.ALT:
794
1298
  return queryAllByAltText(container, value, { exact: false });
1299
+ case QueryBy.COMPONENT:
1300
+ return findByComponent(container, { ...query, component: query.component ?? value });
795
1301
  default:
796
1302
  return [];
797
1303
  }
798
1304
  }
1305
+ if (query.component !== void 0 || query.source !== void 0) {
1306
+ return findByComponent(container, query);
1307
+ }
799
1308
  if (query.role !== void 0) {
800
1309
  const options = query.name !== void 0 ? { hidden: true, name: query.name } : { hidden: true };
801
1310
  return queryAllByRole(container, query.role, options);
@@ -909,44 +1418,6 @@ function runQuery(query) {
909
1418
  return { elements: result2.elements };
910
1419
  }
911
1420
 
912
- // ../browser/dist/registry/adapters.js
913
- var globalStore2 = globalThis;
914
- var adapters = globalStore2.__irisAdapters ??= [];
915
- function registerAdapter(adapter) {
916
- if (!adapters.some((a) => a.name === adapter.name))
917
- adapters.push(adapter);
918
- }
919
- function identifyComponent(el) {
920
- for (const adapter of adapters) {
921
- const info = adapter.identify(el);
922
- if (info !== null)
923
- return info;
924
- }
925
- return null;
926
- }
927
- function readComponentState(el) {
928
- for (const adapter of adapters) {
929
- if (adapter.readState === void 0)
930
- continue;
931
- const state = adapter.readState(el);
932
- if (state !== void 0)
933
- return state;
934
- }
935
- return void 0;
936
- }
937
- function elementHasHoverHandlers(el) {
938
- for (const adapter of adapters) {
939
- if (adapter.hasHoverHandlers === void 0)
940
- continue;
941
- if (adapter.hasHoverHandlers(el))
942
- return true;
943
- }
944
- return false;
945
- }
946
- function adapterNames() {
947
- return adapters.map((a) => a.name);
948
- }
949
-
950
1421
  // ../browser/dist/timers/native-timers.js
951
1422
  var g = globalThis;
952
1423
  var realSetTimeout = typeof g.setTimeout === "function" ? g.setTimeout.bind(g) : null;
@@ -1021,8 +1492,20 @@ function requireElement(ref) {
1021
1492
  throw new Error(`ref '${ref}' is not an HTMLElement`);
1022
1493
  return el;
1023
1494
  }
1024
- var result = (ref, action, effect, settled, settleReason, warning) => {
1025
- 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) => {
1026
1509
  const base = {
1027
1510
  ok: true,
1028
1511
  ref,
@@ -1032,8 +1515,14 @@ var result = (ref, action, effect, settled, settleReason, warning) => {
1032
1515
  settleReason,
1033
1516
  effect
1034
1517
  };
1035
- if (testid !== void 0)
1036
- 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
+ }
1037
1526
  if (warning !== void 0)
1038
1527
  base.warning = warning;
1039
1528
  return base;
@@ -1041,6 +1530,30 @@ var result = (ref, action, effect, settled, settleReason, warning) => {
1041
1530
  var FILL_LIKE = /* @__PURE__ */ new Set([ActionType.FILL, ActionType.TYPE, ActionType.CLEAR]);
1042
1531
  var isFillLike = (action) => FILL_LIKE.has(action);
1043
1532
  var CLICK_LIKE = /* @__PURE__ */ new Set([ActionType.CLICK, ActionType.DBLCLICK]);
1533
+ function dangerousActionContext(el) {
1534
+ const form = el.closest("form");
1535
+ return [
1536
+ getAccessibleName(el),
1537
+ el.textContent ?? "",
1538
+ el.getAttribute("value") ?? "",
1539
+ el.getAttribute("title") ?? "",
1540
+ el.getAttribute("aria-label") ?? "",
1541
+ el.getAttribute("href") ?? "",
1542
+ form?.getAttribute("action") ?? "",
1543
+ form?.textContent ?? ""
1544
+ ].join(" ");
1545
+ }
1546
+ function requiresDangerousConfirmation(text) {
1547
+ return isDangerousActionText(text);
1548
+ }
1549
+ function assertActionAllowed(el, action, args) {
1550
+ const canTrigger = action === ActionType.CLICK || action === ActionType.DBLCLICK || action === ActionType.DRAG || action === ActionType.SUBMIT || action === ActionType.PRESS && asString(args["key"], "Enter") === "Enter";
1551
+ const dragTarget = action === ActionType.DRAG ? refs.resolve(asString(args["toRef"])) : null;
1552
+ const context = dragTarget instanceof HTMLElement ? `${dangerousActionContext(el)} ${dangerousActionContext(dragTarget)}` : dangerousActionContext(el);
1553
+ if (canTrigger && requiresDangerousConfirmation(context) && args[DANGEROUS_ACTION_CONFIRM_ARG] !== true) {
1554
+ throw new Error(`potentially destructive action blocked; retry with args.${DANGEROUS_ACTION_CONFIRM_ARG}=true`);
1555
+ }
1556
+ }
1044
1557
  var NO_GEOMETRY = { occluded: false, occludedBy: null, scrolledIntoView: false };
1045
1558
  function fireClickSequence(el) {
1046
1559
  const doc = el.ownerDocument;
@@ -1207,6 +1720,8 @@ async function dispatchFor(el, action, args) {
1207
1720
  }
1208
1721
  async function executeAction(ref, action, args = {}) {
1209
1722
  const el = requireElement(ref);
1723
+ assertActionAllowed(el, action, args);
1724
+ const anchor = anchorOf(el);
1210
1725
  const visible = isVisible(el);
1211
1726
  const enabled = enabledOf(el);
1212
1727
  const prevFocus = activeRef(el);
@@ -1249,7 +1764,7 @@ async function executeAction(ref, action, args = {}) {
1249
1764
  scrolledIntoView: geometry.scrolledIntoView
1250
1765
  };
1251
1766
  const warning = geometry.occluded ? ActionWarning.CLICK_OCCLUDED : action === ActionType.HOVER && elementHasHoverHandlers(el) ? ActionWarning.HOVER_NATIVE_ENTER_LEAVE : void 0;
1252
- return result(ref, action, effect, settled, settleReason, warning);
1767
+ return result(ref, action, effect, settled, settleReason, anchor, warning);
1253
1768
  }
1254
1769
  var sleep = (ms) => new Promise((r) => nativeSetTimeout(r, ms));
1255
1770
  function firePointer(el, type, relatedTarget = null) {
@@ -1313,7 +1828,10 @@ async function dragElement(source, target, data) {
1313
1828
  }
1314
1829
  return dropPrevented;
1315
1830
  }
1316
- async function dispatchWebMcp(tool, params) {
1831
+ async function dispatchWebMcp(tool, params, confirmDangerous = false) {
1832
+ if (requiresDangerousConfirmation(tool) && !confirmDangerous) {
1833
+ throw new Error(`potentially destructive WebMCP tool blocked; retry with ${DANGEROUS_ACTION_CONFIRM_ARG}=true`);
1834
+ }
1317
1835
  const mc = navigator.modelContext;
1318
1836
  if (mc === void 0 || typeof mc.callTool !== "function") {
1319
1837
  throw new Error("WebMCP (navigator.modelContext) not available on this page");
@@ -1342,6 +1860,77 @@ async function executeSequence(steps) {
1342
1860
  return { ok: true, count: steps.length, effects, steps: stepResults };
1343
1861
  }
1344
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
+
1345
1934
  // ../browser/dist/registry/stores.js
1346
1935
  var globalStore3 = globalThis;
1347
1936
  var stores = globalStore3.__irisStores ??= /* @__PURE__ */ new Map();
@@ -1360,7 +1949,7 @@ function readStores(only) {
1360
1949
  if (only !== void 0 && name !== only)
1361
1950
  continue;
1362
1951
  try {
1363
- out[name] = getter();
1952
+ out[name] = sanitizeForTransport(getter());
1364
1953
  } catch (error) {
1365
1954
  out[name] = { __error: error instanceof Error ? error.message : String(error) };
1366
1955
  }
@@ -1480,6 +2069,9 @@ function scrollContainer(ref, dy, fraction) {
1480
2069
  function str(value) {
1481
2070
  return typeof value === "string" ? value : void 0;
1482
2071
  }
2072
+ function num(value) {
2073
+ return typeof value === "number" ? value : void 0;
2074
+ }
1483
2075
  function record(value) {
1484
2076
  return typeof value === "object" && value !== null ? value : {};
1485
2077
  }
@@ -1505,15 +2097,39 @@ function inspect(ref) {
1505
2097
  const component = identifyComponent(el);
1506
2098
  const view = el.ownerDocument.defaultView;
1507
2099
  const cs = view !== null ? view.getComputedStyle(el) : null;
1508
- 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;
1509
2108
  return {
1510
2109
  ...describe(el),
1511
2110
  tag: el.tagName.toLowerCase(),
2111
+ href: el.getAttribute("href") ?? void 0,
2112
+ formAction: el instanceof HTMLButtonElement || el instanceof HTMLInputElement ? el.form?.getAttribute("action") ?? void 0 : void 0,
2113
+ formText: el instanceof HTMLButtonElement || el instanceof HTMLInputElement ? el.form?.textContent ?? void 0 : void 0,
1512
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),
1513
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,
1514
2121
  component
1515
2122
  };
1516
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
+ }
1517
2133
  function isComponentStateResult(value) {
1518
2134
  return typeof value === "object" && value !== null && "ok" in value && typeof value.ok === "boolean";
1519
2135
  }
@@ -1521,11 +2137,25 @@ var COMPONENT_UNAVAILABLE = {
1521
2137
  ok: false,
1522
2138
  reason: ComponentStateReason.UNAVAILABLE
1523
2139
  };
1524
- function readState(ref, store) {
2140
+ function readState(ref, store, path, depth) {
1525
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
+ }
1526
2156
  const result2 = {
1527
2157
  stores: stores2,
1528
- storeNames: storeNames()
2158
+ storeNames: names
1529
2159
  };
1530
2160
  if (ref !== void 0 && ref.length > 0) {
1531
2161
  const el = refs.resolve(ref);
@@ -1553,6 +2183,16 @@ function listAnimations() {
1553
2183
  });
1554
2184
  return { animations };
1555
2185
  }
2186
+ function resolveNavigationUrl(rawUrl, baseUrl) {
2187
+ if (rawUrl.length === 0 || rawUrl.length > TRANSPORT_LIMITS.MAX_URL_LENGTH)
2188
+ return null;
2189
+ try {
2190
+ const url = new URL(rawUrl, baseUrl);
2191
+ return url.protocol === "http:" || url.protocol === "https:" ? url.toString() : null;
2192
+ } catch {
2193
+ return null;
2194
+ }
2195
+ }
1556
2196
  function createCommandRegistry() {
1557
2197
  const reg = /* @__PURE__ */ new Map();
1558
2198
  reg.set(IrisCommand.SNAPSHOT, (args) => buildSnapshot({
@@ -1565,7 +2205,7 @@ function createCommandRegistry() {
1565
2205
  const action = str(args["action"]) ?? "";
1566
2206
  if (action === ActionType.WEBMCP) {
1567
2207
  const inner = record(args["args"]);
1568
- return dispatchWebMcp(str(inner["tool"]) ?? "", record(inner["params"]));
2208
+ return dispatchWebMcp(str(inner["tool"]) ?? "", record(inner["params"]), inner[DANGEROUS_ACTION_CONFIRM_ARG] === true);
1569
2209
  }
1570
2210
  return executeAction(str(args["ref"]) ?? "", action, record(args["args"]));
1571
2211
  });
@@ -1584,7 +2224,7 @@ function createCommandRegistry() {
1584
2224
  }
1585
2225
  return { frozen: isClockFrozen() };
1586
2226
  });
1587
- 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"])));
1588
2228
  reg.set(IrisCommand.CAPABILITIES, () => getCapabilities());
1589
2229
  reg.set(IrisCommand.SCROLL, (args) => {
1590
2230
  const dy = args["dy"];
@@ -1592,9 +2232,12 @@ function createCommandRegistry() {
1592
2232
  return scrollContainer(str(args["ref"]), typeof dy === "number" ? dy : void 0, typeof fraction === "number" ? fraction : void 0);
1593
2233
  });
1594
2234
  reg.set(IrisCommand.NAVIGATE, (args) => {
1595
- const url = str(args["url"]);
1596
- if (url === void 0 || url.length === 0)
2235
+ const rawUrl = str(args["url"]);
2236
+ if (rawUrl === void 0 || rawUrl.length === 0)
1597
2237
  return { ok: false, reason: "url required" };
2238
+ const url = resolveNavigationUrl(rawUrl, window.location.href);
2239
+ if (url === null)
2240
+ return { ok: false, reason: "only http(s) navigation is allowed" };
1598
2241
  window.location.assign(url);
1599
2242
  return { ok: true, url };
1600
2243
  });
@@ -1687,12 +2330,23 @@ var Transport = class {
1687
2330
  } catch {
1688
2331
  return;
1689
2332
  }
1690
- const msg = parsed;
1691
- if (msg.kind !== MessageKind.COMMAND)
2333
+ const result2 = CommandMessageSchema.safeParse(parsed);
2334
+ if (!result2.success)
1692
2335
  return;
1693
- const command = parsed;
1694
- const outcome = await this.#deps.handleCommand(command);
1695
- this.#sendRaw(JSON.stringify({
2336
+ const command = result2.data;
2337
+ const currentSessionId = this.#deps.hello().sessionId;
2338
+ if (command.sessionId !== void 0 && command.sessionId !== currentSessionId)
2339
+ return;
2340
+ let outcome;
2341
+ try {
2342
+ outcome = await this.#deps.handleCommand(command);
2343
+ } catch (error) {
2344
+ outcome = {
2345
+ ok: false,
2346
+ error: error instanceof Error ? error.message : String(error)
2347
+ };
2348
+ }
2349
+ this.#sendRaw(safeStringify({
1696
2350
  kind: MessageKind.COMMAND_RESULT,
1697
2351
  id: command.id,
1698
2352
  ok: outcome.ok,
@@ -1701,7 +2355,7 @@ var Transport = class {
1701
2355
  }));
1702
2356
  }
1703
2357
  sendEvent(event) {
1704
- this.#sendRaw(JSON.stringify({ kind: MessageKind.EVENT, event }));
2358
+ this.#sendRaw(safeStringify({ kind: MessageKind.EVENT, event }));
1705
2359
  }
1706
2360
  #sendRaw(text) {
1707
2361
  if (this.#ws !== void 0 && this.#ws.readyState === WebSocket.OPEN) {
@@ -1820,14 +2474,20 @@ function methodOf(input, init) {
1820
2474
  return "GET";
1821
2475
  }
1822
2476
  function installNetwork(emit) {
1823
- const origFetch = window.fetch.bind(window);
2477
+ const origFetch = window.fetch;
2478
+ const callFetch = origFetch.bind(window);
2479
+ let seq2 = 0;
2480
+ const nextId = () => `n${++seq2}`;
1824
2481
  window.fetch = async (input, init) => {
2482
+ const id = nextId();
1825
2483
  const start = performance.now();
1826
2484
  const method = methodOf(input, init);
1827
2485
  const url = urlOf(input);
2486
+ emit(EventType.NET_PENDING, { id, method, url, initiator: "fetch" });
1828
2487
  try {
1829
- const res = await origFetch(input, init);
2488
+ const res = await callFetch(input, init);
1830
2489
  emit(EventType.NET_REQUEST, {
2490
+ id,
1831
2491
  method,
1832
2492
  url,
1833
2493
  status: res.status,
@@ -1838,6 +2498,7 @@ function installNetwork(emit) {
1838
2498
  return res;
1839
2499
  } catch (error) {
1840
2500
  emit(EventType.NET_REQUEST, {
2501
+ id,
1841
2502
  method,
1842
2503
  url,
1843
2504
  status: 0,
@@ -1855,15 +2516,17 @@ function installNetwork(emit) {
1855
2516
  const origSend = proto.send;
1856
2517
  const callOpen = origOpen;
1857
2518
  proto.open = function(method, url, ...rest) {
1858
- 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 });
1859
2520
  callOpen.call(this, method, url, ...rest);
1860
2521
  };
1861
2522
  proto.send = function(body) {
1862
2523
  const m = meta.get(this);
1863
2524
  if (m !== void 0) {
1864
2525
  m.start = performance.now();
2526
+ emit(EventType.NET_PENDING, { id: m.id, method: m.method, url: m.url, initiator: "xhr" });
1865
2527
  this.addEventListener("loadend", () => {
1866
2528
  emit(EventType.NET_REQUEST, {
2529
+ id: m.id,
1867
2530
  method: m.method,
1868
2531
  url: m.url,
1869
2532
  status: this.status,
@@ -1892,8 +2555,10 @@ function snapshotLocation() {
1892
2555
  };
1893
2556
  }
1894
2557
  function installRoute(emit) {
1895
- const origPush = history.pushState.bind(history);
1896
- const origReplace = history.replaceState.bind(history);
2558
+ const origPush = history.pushState;
2559
+ const origReplace = history.replaceState;
2560
+ const callPush = origPush.bind(history);
2561
+ const callReplace = origReplace.bind(history);
1897
2562
  const fire = (from) => {
1898
2563
  const to = snapshotLocation();
1899
2564
  if (to.href === from)
@@ -1908,12 +2573,12 @@ function installRoute(emit) {
1908
2573
  };
1909
2574
  history.pushState = (data, unused, url) => {
1910
2575
  const from = location.href;
1911
- origPush(data, unused, url ?? null);
2576
+ callPush(data, unused, url ?? null);
1912
2577
  fire(from);
1913
2578
  };
1914
2579
  history.replaceState = (data, unused, url) => {
1915
2580
  const from = location.href;
1916
- origReplace(data, unused, url ?? null);
2581
+ callReplace(data, unused, url ?? null);
1917
2582
  fire(from);
1918
2583
  };
1919
2584
  let lastHref = location.href;
@@ -1943,22 +2608,19 @@ function stringifyArgs(args) {
1943
2608
  return a;
1944
2609
  if (a instanceof Error)
1945
2610
  return a.message;
1946
- try {
1947
- return JSON.stringify(a);
1948
- } catch {
1949
- return String(a);
1950
- }
2611
+ return safeStringify(a);
1951
2612
  }).join(" ");
1952
2613
  }
1953
2614
  function installConsole(emit) {
1954
2615
  const methods = ["log", "warn", "error"];
1955
2616
  const originals2 = /* @__PURE__ */ new Map();
1956
2617
  for (const method of methods) {
1957
- const original = console[method].bind(console);
2618
+ const original = console[method];
1958
2619
  originals2.set(method, original);
2620
+ const callOriginal = original.bind(console);
1959
2621
  console[method] = (...args) => {
1960
2622
  emit(METHOD_EVENT[method], { message: stringifyArgs(args) });
1961
- original(...args);
2623
+ callOriginal(...args);
1962
2624
  };
1963
2625
  }
1964
2626
  const onError = (event) => {
@@ -2238,6 +2900,7 @@ function appendLogRow(container, kind, text, ts, logMax) {
2238
2900
 
2239
2901
  // ../browser/dist/presenter/presenter-controls.js
2240
2902
  var DATA_IRIS_STATE = "data-iris-state";
2903
+ var DATA_IRIS_TONE = "data-iris-tone";
2241
2904
  var DATA_ON = "data-on";
2242
2905
  var GLOW_OFF = "0";
2243
2906
  var CONTROL_LABEL = {
@@ -2251,6 +2914,7 @@ var PAUSED_BADGE_TEXT = "PAUSED";
2251
2914
  var ENDED_BANNER_TEXT = "Session ended";
2252
2915
  var COPY_LABEL = "Copy run";
2253
2916
  var EXPORT_LABEL = "Export";
2917
+ var FLOWS_LABEL = "Replay a flow";
2254
2918
  var COPIED_TEXT = "Copied \u2713";
2255
2919
  var RUN_FILENAME = "iris-run.json";
2256
2920
  var ENDED_FADE_MS = 4e3;
@@ -2268,11 +2932,12 @@ var CONTROLS_CSS = `
2268
2932
  color:var(--iris-accent);border:1px solid var(--iris-accent);background:var(--iris-accent-soft);padding:2px 8px;border-radius:999px;}
2269
2933
  [data-iris-overlay][data-iris-state="paused"] [data-iris-badge]{display:inline-flex;}
2270
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);}
2271
- [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);
2272
2936
  border:1px solid var(--iris-line);border-radius:14px;padding:5px 6px 5px 14px;transition:border-color .15s,box-shadow .15s;}
2273
2937
  [data-iris-hud] .iris-composer:focus-within{border-color:var(--iris-accent);box-shadow:0 0 0 3px var(--iris-accent-soft);}
2274
- [data-iris-hud] .iris-msg{flex:1;min-width:0;pointer-events:auto;background:transparent;border:none;outline:none;
2275
- 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;}
2276
2941
  [data-iris-hud] .iris-msg::placeholder{color:var(--iris-faint);}
2277
2942
  [data-iris-hud] .iris-msg:disabled{opacity:.5;}
2278
2943
  [data-iris-hud] .iris-send{flex:none;width:30px;height:30px;padding:0;border-radius:10px;border:none;cursor:pointer;pointer-events:auto;
@@ -2293,11 +2958,40 @@ var CONTROLS_CSS = `
2293
2958
  box-shadow:inset 0 0 0 3px rgba(246,180,76,.9),inset 0 0 30px 6px rgba(246,180,76,.4);}
2294
2959
  [data-iris-overlay][data-iris-state="ended"] [data-iris-glow][data-on="1"]{animation:none;
2295
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);}
2296
2989
  `;
2297
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>`;
2298
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>`;
2299
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>`;
2300
- 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>`;
2301
2995
  function queryControlRefs(root) {
2302
2996
  return {
2303
2997
  pauseBtn: root.querySelector("[data-iris-pause]") ?? void 0,
@@ -2307,7 +3001,8 @@ function queryControlRefs(root) {
2307
3001
  banner: root.querySelector("[data-iris-banner]") ?? void 0,
2308
3002
  copyBtn: root.querySelector("[data-iris-copy]") ?? void 0,
2309
3003
  exportBtn: root.querySelector("[data-iris-export]") ?? void 0,
2310
- 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
2311
3006
  };
2312
3007
  }
2313
3008
  var ControlPanel = class {
@@ -2319,7 +3014,8 @@ var ControlPanel = class {
2319
3014
  banner: void 0,
2320
3015
  copyBtn: void 0,
2321
3016
  exportBtn: void 0,
2322
- exportMsg: void 0
3017
+ exportMsg: void 0,
3018
+ flows: void 0
2323
3019
  };
2324
3020
  #state = SessionState.ACTIVE;
2325
3021
  #fadeTimer;
@@ -2341,8 +3037,20 @@ var ControlPanel = class {
2341
3037
  this.#refs.endBtn?.addEventListener("click", () => this.#onEnd());
2342
3038
  this.#refs.sendBtn?.addEventListener("click", () => this.#onSend());
2343
3039
  this.#refs.input?.addEventListener("keydown", (e) => {
2344
- if (e instanceof KeyboardEvent && e.key === "Enter")
3040
+ if (e instanceof KeyboardEvent && e.key === "Enter" && !e.shiftKey) {
3041
+ e.preventDefault();
2345
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
+ }
2346
3054
  });
2347
3055
  this.#refs.copyBtn?.addEventListener("click", () => this.#onCopy());
2348
3056
  this.#refs.exportBtn?.addEventListener("click", () => this.#onExport());
@@ -2403,15 +3111,49 @@ var ControlPanel = class {
2403
3111
  this.#host.logHuman(text);
2404
3112
  if (this.#refs.input !== void 0)
2405
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");
2406
3143
  }
2407
3144
  /**
2408
3145
  * Drive the panel's visual state. Idempotent; NEVER emits a control — the shared path for both the
2409
3146
  * optimistic local click and the authoritative server PRESENTER echo. Only the ended-border fade
2410
3147
  * touches a clock, via the injected native timer.
2411
3148
  */
2412
- setState(state, text) {
3149
+ setState(state, text, tone) {
2413
3150
  this.#state = state;
2414
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);
2415
3157
  if (this.#fadeTimer !== void 0) {
2416
3158
  nativeClearTimeout(this.#fadeTimer);
2417
3159
  this.#fadeTimer = void 0;
@@ -2429,8 +3171,8 @@ var ControlPanel = class {
2429
3171
  if (refs2.input !== void 0)
2430
3172
  refs2.input.disabled = ended;
2431
3173
  if (refs2.banner !== void 0) {
2432
- const summary = text !== void 0 && text.trim().length > 0 ? ` \xB7 ${text.trim()}` : "";
2433
- 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}` : ""}`;
2434
3176
  }
2435
3177
  if (ended) {
2436
3178
  const glow = this.#glow;
@@ -2441,8 +3183,8 @@ var ControlPanel = class {
2441
3183
  }
2442
3184
  };
2443
3185
 
2444
- // ../browser/dist/presenter/presenter.js
2445
- var CSS = `
3186
+ // ../browser/dist/presenter/presenter-styles.js
3187
+ var PRESENTER_CSS = `
2446
3188
  @import url("https://fonts.googleapis.com/css2?family=IBM+Plex+Serif:wght@400;500&family=Inter:wght@400;450;500;600&display=swap");
2447
3189
  [data-iris-glow]{position:fixed;inset:0;pointer-events:none;z-index:2147483600;opacity:0;
2448
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);}
@@ -2514,6 +3256,14 @@ var CSS = `
2514
3256
  [data-iris-hud] [data-iris-min-btn]:hover{color:var(--iris-fg);background:rgba(255,255,255,.08);}
2515
3257
  [data-iris-hud] [data-iris-min-btn]:active{transform:scale(.94);}
2516
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);}
2517
3267
  [data-iris-hud] .iris-chip{display:none;flex:none;font-size:9px;font-weight:600;letter-spacing:.08em;
2518
3268
  padding:2px 7px;border-radius:6px;vertical-align:middle;}
2519
3269
  [data-iris-hud] .iris-chip[data-mode="reading"]{display:inline-block;color:var(--iris-read);
@@ -2544,6 +3294,8 @@ var CSS = `
2544
3294
  [data-iris-overlay][data-iris-throttled="1"] [data-iris-hud]{--iris-accent:#fbbf24;--iris-accent-soft:rgba(251,191,36,.16);}
2545
3295
  ${LOG_CSS}
2546
3296
  ${CONTROLS_CSS}`;
3297
+
3298
+ // ../browser/dist/presenter/presenter-config.js
2547
3299
  var BorderMode = { SESSION: "session", BUSY: "busy" };
2548
3300
  var DEFAULT_BORDER_MODE = BorderMode.SESSION;
2549
3301
  var DATA_BUSY = "data-busy";
@@ -2566,6 +3318,204 @@ var GLOW_OFF2 = "0";
2566
3318
  var DATA_ON2 = "data-on";
2567
3319
  var MIN_ATTR = "data-iris-min";
2568
3320
  var THROTTLED_ATTR = "data-iris-throttled";
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;
3391
+ #now;
3392
+ #idleAfterMs;
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
2569
3519
  var Presenter = class {
2570
3520
  #paceMs;
2571
3521
  #root;
@@ -2575,18 +3525,17 @@ var Presenter = class {
2575
3525
  #hud;
2576
3526
  #actLine;
2577
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 };
2578
3531
  #liveLine;
2579
3532
  #mode = PresenterMode.IDLE;
2580
3533
  #now;
2581
- #idleAfterMs;
2582
- #glowFadeMs;
2583
3534
  #heartbeatMs;
2584
3535
  #idleNoticeMs;
2585
3536
  #borderMode;
2586
- #phase = GlowPhase.IDLE;
2587
- #lastActivityMs = 0;
2588
- #idleCheckTimer;
2589
- #fadeTimer;
3537
+ /** The glow / activity state machine (border shimmer + cursor visibility from activity timing). */
3538
+ #glowCtl;
2590
3539
  /** Liveness: the most recent action text + a 1s ticker that ages it into an "idle · {dur}" clock. */
2591
3540
  #lastActionText = "";
2592
3541
  #heartbeatTimer;
@@ -2609,13 +3558,18 @@ var Presenter = class {
2609
3558
  constructor(options = {}) {
2610
3559
  this.#paceMs = options.paceMs ?? DEFAULT_PACE;
2611
3560
  this.#now = options.now ?? nativeNow;
2612
- this.#idleAfterMs = options.idleAfterMs ?? IDLE_AFTER_MS;
2613
- this.#glowFadeMs = options.glowFadeMs ?? GLOW_FADE_MS;
2614
3561
  this.#heartbeatMs = options.heartbeatMs ?? HEARTBEAT_MS;
2615
3562
  this.#idleNoticeMs = options.idleNoticeMs ?? IDLE_NOTICE_MS;
2616
3563
  this.#idleEndMs = options.idleEndMs ?? IDLE_END_MS;
2617
3564
  this.#sessionId = options.sessionId ?? "";
2618
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
+ });
2619
3573
  this.#logMax = clampLogMax(options.logMax);
2620
3574
  this.#onControl = options.onControl;
2621
3575
  this.#panel = new ControlPanel({
@@ -2640,8 +3594,21 @@ var Presenter = class {
2640
3594
  return this.#sessionActive;
2641
3595
  }
2642
3596
  /** Drive the panel's live-control visual state (server-push / agent path; never emits). */
2643
- setState(state, text) {
2644
- 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);
2645
3612
  }
2646
3613
  /** Current cap on accumulated log rows. */
2647
3614
  get logMax() {
@@ -2656,7 +3623,7 @@ var Presenter = class {
2656
3623
  return;
2657
3624
  const style = document.createElement("style");
2658
3625
  style.setAttribute("data-iris-overlay", "");
2659
- style.textContent = CSS;
3626
+ style.textContent = PRESENTER_CSS;
2660
3627
  document.head.appendChild(style);
2661
3628
  const root = document.createElement("div");
2662
3629
  root.setAttribute("data-iris-overlay", "");
@@ -2665,10 +3632,11 @@ var Presenter = class {
2665
3632
  <div data-iris-cursor></div>
2666
3633
  <div data-iris-ring></div>
2667
3634
  <div data-iris-hud>
2668
- <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>
2669
3636
  <div class="iris-act-strip"><span class="iris-act">idle</span></div>
2670
3637
  ${CONTROLS_BANNER_HTML}
2671
3638
  <div ${DATA_IRIS_LOG}></div>
3639
+ ${CONTROLS_FLOWS_HTML}
2672
3640
  ${CONTROLS_FOOT_HTML}
2673
3641
  </div>`;
2674
3642
  document.body.appendChild(root);
@@ -2680,6 +3648,7 @@ var Presenter = class {
2680
3648
  this.#actLine = root.querySelector(".iris-act") ?? void 0;
2681
3649
  this.#log = root.querySelector(`[${DATA_IRIS_LOG}]`) ?? void 0;
2682
3650
  this.#chip = root.querySelector("[data-iris-chip]") ?? void 0;
3651
+ this.#tally = root.querySelector("[data-iris-tally]") ?? void 0;
2683
3652
  this.#liveLine = root.querySelector(".iris-live") ?? void 0;
2684
3653
  const setMin = (on) => root.setAttribute(MIN_ATTR, on ? "1" : "0");
2685
3654
  root.querySelector("[data-iris-min-btn]")?.addEventListener("click", (e) => {
@@ -2690,20 +3659,16 @@ var Presenter = class {
2690
3659
  if (root.getAttribute(MIN_ATTR) === "1")
2691
3660
  setMin(false);
2692
3661
  });
3662
+ this.#glowCtl.setElements(this.#glow, this.#cursor);
2693
3663
  this.#panel.mount(root, this.#glow);
2694
3664
  this.setMode(this.#mode);
2695
3665
  }
2696
3666
  destroy() {
2697
- if (this.#idleCheckTimer !== void 0)
2698
- nativeClearTimeout(this.#idleCheckTimer);
2699
- if (this.#fadeTimer !== void 0)
2700
- nativeClearTimeout(this.#fadeTimer);
3667
+ this.#glowCtl.teardown();
2701
3668
  if (this.#heartbeatTimer !== void 0)
2702
3669
  nativeClearTimeout(this.#heartbeatTimer);
2703
3670
  this.#heartbeatTimer = void 0;
2704
3671
  this.#panel.teardown();
2705
- this.#idleCheckTimer = void 0;
2706
- this.#fadeTimer = void 0;
2707
3672
  this.#sessionActive = false;
2708
3673
  this.#logBaseMs = void 0;
2709
3674
  this.#log = void 0;
@@ -2726,7 +3691,7 @@ var Presenter = class {
2726
3691
  this.#startMs ??= this.#now();
2727
3692
  this.#endMs = void 0;
2728
3693
  this.#showSession();
2729
- this.#lastActivityMs = this.#now();
3694
+ this.#glowCtl.resetActivity(this.#now());
2730
3695
  this.#startHeartbeat();
2731
3696
  }
2732
3697
  /** Turn the base border (session mode) + the HUD/log on — the visible "session is live" state. */
@@ -2740,7 +3705,7 @@ var Presenter = class {
2740
3705
  this.#panel.setState(SessionState.ACTIVE);
2741
3706
  this.#endMs = void 0;
2742
3707
  this.#showSession();
2743
- this.#lastActivityMs = this.#now();
3708
+ this.#glowCtl.resetActivity(this.#now());
2744
3709
  this.#startHeartbeat();
2745
3710
  }
2746
3711
  /**
@@ -2767,23 +3732,15 @@ var Presenter = class {
2767
3732
  * just refresh the last-activity timestamp and re-arm the idle check.
2768
3733
  */
2769
3734
  markActivity() {
2770
- this.#markActivityAt(this.#now());
2771
- }
2772
- /** markActivity with a caller-supplied timestamp so log() reads the clock exactly once per row. */
2773
- #markActivityAt(ms) {
2774
- this.#lastActivityMs = ms;
2775
- if (this.#phase === GlowPhase.IDLE || this.#phase === GlowPhase.FADING) {
2776
- this.#enterBusy();
2777
- }
2778
- this.#armIdleCheck();
3735
+ this.#glowCtl.markActivity();
2779
3736
  }
2780
3737
  /** Re-arm the quiet-window idle check (kept for iris.ts's finally block). */
2781
3738
  scheduleIdle() {
2782
- this.#armIdleCheck();
3739
+ this.#glowCtl.scheduleIdle();
2783
3740
  }
2784
3741
  /** Test/diagnostic accessor for the current glow phase. */
2785
3742
  glowPhase() {
2786
- return this.#phase;
3743
+ return this.#glowCtl.phase();
2787
3744
  }
2788
3745
  /** Current intent (reading vs acting), exposed for tests + the watcher. */
2789
3746
  get mode() {
@@ -2803,50 +3760,6 @@ var Presenter = class {
2803
3760
  if (mode === PresenterMode.READING)
2804
3761
  this.#cursor?.setAttribute(DATA_ON2, GLOW_OFF2);
2805
3762
  }
2806
- #enterBusy() {
2807
- if (this.#fadeTimer !== void 0) {
2808
- nativeClearTimeout(this.#fadeTimer);
2809
- this.#fadeTimer = void 0;
2810
- }
2811
- this.#phase = GlowPhase.BUSY;
2812
- if (this.#borderMode === BorderMode.SESSION) {
2813
- this.#glow?.setAttribute(DATA_BUSY, BUSY_ON);
2814
- } else {
2815
- this.#glow?.setAttribute(DATA_ON2, GLOW_ON);
2816
- }
2817
- this.#cursor?.setAttribute(DATA_ON2, GLOW_ON);
2818
- }
2819
- #armIdleCheck() {
2820
- if (this.#idleCheckTimer !== void 0)
2821
- nativeClearTimeout(this.#idleCheckTimer);
2822
- this.#idleCheckTimer = nativeSetTimeout(() => this.#checkIdle(), this.#idleAfterMs);
2823
- }
2824
- #checkIdle() {
2825
- this.#idleCheckTimer = void 0;
2826
- if (this.#phase !== GlowPhase.BUSY)
2827
- return;
2828
- const quietFor = this.#now() - this.#lastActivityMs;
2829
- if (quietFor < this.#idleAfterMs) {
2830
- this.#idleCheckTimer = nativeSetTimeout(() => this.#checkIdle(), this.#idleAfterMs - quietFor);
2831
- return;
2832
- }
2833
- this.#beginFade();
2834
- }
2835
- #beginFade() {
2836
- this.#phase = GlowPhase.FADING;
2837
- if (this.#borderMode === BorderMode.SESSION) {
2838
- this.#glow?.setAttribute(DATA_BUSY, BUSY_OFF);
2839
- } else {
2840
- this.#glow?.setAttribute(DATA_ON2, GLOW_OFF2);
2841
- }
2842
- this.#cursor?.setAttribute(DATA_ON2, GLOW_OFF2);
2843
- this.setMode(PresenterMode.IDLE);
2844
- this.#fadeTimer = nativeSetTimeout(() => {
2845
- this.#fadeTimer = void 0;
2846
- if (this.#phase === GlowPhase.FADING)
2847
- this.#phase = GlowPhase.IDLE;
2848
- }, this.#glowFadeMs);
2849
- }
2850
3763
  status(text) {
2851
3764
  this.markActivity();
2852
3765
  this.#lastActionText = text;
@@ -2873,7 +3786,7 @@ var Presenter = class {
2873
3786
  return;
2874
3787
  if (this.state === SessionState.ENDED)
2875
3788
  return;
2876
- const idleMs = this.#now() - this.#lastActivityMs;
3789
+ const idleMs = this.#now() - this.#glowCtl.lastActivityMs();
2877
3790
  if (idleMs >= this.#idleEndMs) {
2878
3791
  this.#endIdle(idleMs);
2879
3792
  return;
@@ -2904,33 +3817,14 @@ var Presenter = class {
2904
3817
  * (The full network/console ring-buffer lives server-side; this is the in-page run summary.)
2905
3818
  */
2906
3819
  runState() {
2907
- const now = this.#now();
2908
- const start = this.#startMs ?? now;
2909
- const counts = { reads: 0, acts: 0, narrations: 0, human: 0, passes: 0, fails: 0 };
2910
- for (const e of this.#runLog) {
2911
- if (e.kind === LOG_KIND.READ)
2912
- counts.reads += 1;
2913
- else if (e.kind === LOG_KIND.ACT)
2914
- counts.acts += 1;
2915
- else if (e.kind === LOG_KIND.NARRATION)
2916
- counts.narrations += 1;
2917
- else if (e.kind === LOG_KIND.HUMAN)
2918
- counts.human += 1;
2919
- if (e.result === LOG_RESULT.PASS)
2920
- counts.passes += 1;
2921
- else if (e.result === LOG_RESULT.FAIL)
2922
- counts.fails += 1;
2923
- }
2924
- return {
2925
- session: this.#sessionId,
2926
- url: typeof location === "undefined" ? "" : location.href,
3820
+ return buildRunState({
3821
+ sessionId: this.#sessionId,
2927
3822
  state: this.state,
2928
- startedMs: start,
2929
- durationMs: Math.max(0, (this.#endMs ?? now) - start),
2930
- counts,
2931
- capabilities: getCapabilities(),
2932
- log: this.#runLog.map((e) => ({ ...e }))
2933
- };
3823
+ startMs: this.#startMs,
3824
+ endMs: this.#endMs,
3825
+ now: this.#now(),
3826
+ runLog: this.#runLog
3827
+ });
2934
3828
  }
2935
3829
  /**
2936
3830
  * Append an activity-log row. Accumulates (never overwrites): each call adds a timestamped row
@@ -2939,7 +3833,7 @@ var Presenter = class {
2939
3833
  */
2940
3834
  log(kind, text, result2) {
2941
3835
  const ms = this.#now();
2942
- this.#markActivityAt(ms);
3836
+ this.#glowCtl.markActivity(ms);
2943
3837
  if (this.#log === void 0)
2944
3838
  return void 0;
2945
3839
  const trimmed = text.trim();
@@ -2956,13 +3850,19 @@ var Presenter = class {
2956
3850
  handle.result(result2);
2957
3851
  if (this.#liveLine !== void 0)
2958
3852
  this.#liveLine.textContent = trimmed;
3853
+ this.#renderTally();
2959
3854
  return {
2960
3855
  result: (r) => {
2961
3856
  handle.result(r);
2962
3857
  entry.result = r;
3858
+ this.#renderTally();
2963
3859
  }
2964
3860
  };
2965
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
+ }
2966
3866
  /** Back-compat: narration appends to the live log (append-only, never overwrites). */
2967
3867
  narrate(text, level = "info") {
2968
3868
  const line = level === "info" ? text : `[${level}] ${text}`;
@@ -2994,46 +3894,17 @@ var Presenter = class {
2994
3894
  const el = refs.resolve(refId);
2995
3895
  this.status(`${actionVerb(action)} ${label}`);
2996
3896
  if (!(el instanceof HTMLElement)) {
2997
- await this.#pause();
3897
+ await pace(this.#paceMs);
2998
3898
  return;
2999
3899
  }
3000
3900
  const rect = el.getBoundingClientRect();
3001
3901
  const cx = rect.left + rect.width / 2;
3002
3902
  const cy = rect.top + rect.height / 2;
3003
- this.#moveCursor(cx, cy);
3004
- this.#ringAround(rect);
3005
- await this.#pause();
3903
+ moveCursor(this.#cursor, cx, cy);
3904
+ ringAround(this.#ring, rect);
3905
+ await pace(this.#paceMs);
3006
3906
  if (action === "click" || action === "dblclick" || action === "submit")
3007
- this.#ripple(cx, cy);
3008
- }
3009
- #moveCursor(x, y) {
3010
- if (this.#cursor === void 0)
3011
- return;
3012
- this.#cursor.setAttribute("data-on", "1");
3013
- this.#cursor.style.transform = `translate(${String(x)}px, ${String(y)}px)`;
3014
- }
3015
- #ringAround(rect) {
3016
- if (this.#ring === void 0)
3017
- return;
3018
- this.#ring.style.left = `${String(rect.left - 4)}px`;
3019
- this.#ring.style.top = `${String(rect.top - 4)}px`;
3020
- this.#ring.style.width = `${String(rect.width + 8)}px`;
3021
- this.#ring.style.height = `${String(rect.height + 8)}px`;
3022
- this.#ring.setAttribute("data-on", "1");
3023
- nativeSetTimeout(() => this.#ring?.setAttribute("data-on", "0"), 700);
3024
- }
3025
- #ripple(x, y) {
3026
- if (this.#root === void 0)
3027
- return;
3028
- const r = document.createElement("div");
3029
- r.setAttribute("data-iris-ripple", "");
3030
- r.style.left = `${String(x)}px`;
3031
- r.style.top = `${String(y)}px`;
3032
- this.#root.appendChild(r);
3033
- nativeSetTimeout(() => r.remove(), 520);
3034
- }
3035
- #pause() {
3036
- return new Promise((res) => nativeSetTimeout(res, this.#paceMs));
3907
+ spawnRipple(this.#root, cx, cy);
3037
3908
  }
3038
3909
  };
3039
3910
 
@@ -3092,8 +3963,10 @@ var BUTTON_LABEL = {
3092
3963
  var ANNOTATION_LABEL = {
3093
3964
  [AnnotationKind.ASSERT_SIGNAL]: "assert signal",
3094
3965
  [AnnotationKind.ASSERT_VISIBLE]: "assert visible",
3966
+ [AnnotationKind.ASSERT_STATE]: "assert state",
3095
3967
  [AnnotationKind.MARK_DYNAMIC]: "mark dynamic",
3096
- [AnnotationKind.SUCCESS_STATE]: "success state"
3968
+ [AnnotationKind.SUCCESS_STATE]: "success state",
3969
+ [AnnotationKind.INTENT]: "intent"
3097
3970
  };
3098
3971
  var NEEDS_SIGNAL = /* @__PURE__ */ new Set([
3099
3972
  AnnotationKind.ASSERT_SIGNAL,
@@ -3412,6 +4285,8 @@ var Recorder = class {
3412
4285
  return;
3413
4286
  menu.textContent = "";
3414
4287
  for (const kind of Object.values(AnnotationKind)) {
4288
+ if (kind === AnnotationKind.INTENT)
4289
+ continue;
3415
4290
  const item = document.createElement("button");
3416
4291
  item.setAttribute("data-iris-annkind", kind);
3417
4292
  item.textContent = ANNOTATION_LABEL[kind];
@@ -3462,7 +4337,445 @@ function installRecorder(deps) {
3462
4337
  return new Recorder(deps);
3463
4338
  }
3464
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
+
3465
4744
  // ../browser/dist/iris.js
4745
+ function connectionPolicy(pageHostname, bridgeUrl, allowNonLocalhost, token) {
4746
+ let bridge;
4747
+ try {
4748
+ bridge = new URL(bridgeUrl);
4749
+ } catch {
4750
+ return { allowed: false, reason: "invalid Iris bridge URL" };
4751
+ }
4752
+ if (bridge.protocol !== "ws:" && bridge.protocol !== "wss:") {
4753
+ return { allowed: false, reason: "Iris bridge URL must use ws:// or wss://" };
4754
+ }
4755
+ if ((token?.length ?? 0) > TRANSPORT_LIMITS.MAX_TOKEN_LENGTH) {
4756
+ return {
4757
+ allowed: false,
4758
+ reason: `Iris pairing token exceeds ${String(TRANSPORT_LIMITS.MAX_TOKEN_LENGTH)} characters`
4759
+ };
4760
+ }
4761
+ const remoteBridge = !isLoopbackHostname(bridge.hostname);
4762
+ if (remoteBridge && bridge.protocol !== "wss:") {
4763
+ return { allowed: false, reason: "a non-local Iris bridge must use wss://" };
4764
+ }
4765
+ const remote = !isLoopbackHostname(pageHostname) || remoteBridge;
4766
+ if (!remote)
4767
+ return { allowed: true };
4768
+ if (!allowNonLocalhost) {
4769
+ return {
4770
+ allowed: false,
4771
+ reason: "Iris is disabled outside localhost unless allowNonLocalhost is explicitly enabled"
4772
+ };
4773
+ }
4774
+ if (token === void 0 || token.length === 0) {
4775
+ return { allowed: false, reason: "a pairing token is required outside localhost" };
4776
+ }
4777
+ return { allowed: true };
4778
+ }
3466
4779
  function str2(value, fallback = "") {
3467
4780
  return typeof value === "string" ? value : fallback;
3468
4781
  }
@@ -3470,9 +4783,6 @@ var BRIDGE_LOST_SUMMARY = "Session ended \u2014 lost connection to Iris (the age
3470
4783
  function resolveSessionLabel(option, gen) {
3471
4784
  return option === void 0 || option === SESSION_AUTO ? gen() : option;
3472
4785
  }
3473
- function isSessionState(value) {
3474
- return value === SessionState.ACTIVE || value === SessionState.PAUSED || value === SessionState.ENDED;
3475
- }
3476
4786
  function refLabel(refId) {
3477
4787
  const el = refs.resolve(refId);
3478
4788
  if (!(el instanceof Element))
@@ -3490,7 +4800,9 @@ var Iris = class {
3490
4800
  #overlay;
3491
4801
  #presenter;
3492
4802
  #recorder;
4803
+ #annotator;
3493
4804
  #eventCount = 0;
4805
+ #token;
3494
4806
  /** Act-row log handle for the in-flight act/act_sequence, so its outcome stamps the right row. */
3495
4807
  #actHandle;
3496
4808
  connect(options = {}) {
@@ -3498,10 +4810,16 @@ var Iris = class {
3498
4810
  return;
3499
4811
  if (typeof window === "undefined" || typeof document === "undefined")
3500
4812
  return;
3501
- this.#session = resolveSessionLabel(options.session, () => `s${Date.now().toString(36)}`);
4813
+ const url = options.url ?? `ws://localhost:${String(IRIS_DEFAULT_PORT)}${IRIS_WS_PATH}`;
4814
+ const policy = connectionPolicy(window.location.hostname, url, options.allowNonLocalhost === true, options.token);
4815
+ if (!policy.allowed) {
4816
+ globalThis.console.warn(`[Iris] ${policy.reason ?? "connection blocked"}`);
4817
+ return;
4818
+ }
4819
+ this.#session = resolveSessionLabel(options.session, () => typeof globalThis.crypto?.randomUUID === "function" ? `s${globalThis.crypto.randomUUID()}` : `s${Date.now().toString(36)}`);
4820
+ this.#token = options.token !== void 0 && options.token.length > 0 ? options.token : void 0;
3502
4821
  this.#start = performance.now();
3503
4822
  this.#registry = createCommandRegistry();
3504
- const url = options.url ?? `ws://localhost:${String(IRIS_DEFAULT_PORT)}${IRIS_WS_PATH}`;
3505
4823
  this.#transport = new Transport({
3506
4824
  url,
3507
4825
  hello: () => this.#hello(),
@@ -3557,6 +4875,16 @@ var Iris = class {
3557
4875
  this.#recorder = installRecorder({ emit, now: () => Date.now() });
3558
4876
  this.#recorder.mount();
3559
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
+ }
3560
4888
  this.#transport.connect();
3561
4889
  this.#connected = true;
3562
4890
  }
@@ -3595,6 +4923,8 @@ var Iris = class {
3595
4923
  this.#presenter = void 0;
3596
4924
  this.#recorder?.destroy();
3597
4925
  this.#recorder = void 0;
4926
+ this.#annotator?.destroy();
4927
+ this.#annotator = void 0;
3598
4928
  resetClock();
3599
4929
  this.#connected = false;
3600
4930
  }
@@ -3618,6 +4948,7 @@ var Iris = class {
3618
4948
  url: location.href,
3619
4949
  title: document.title,
3620
4950
  adapters: adapterNames(),
4951
+ ...this.#token === void 0 ? {} : { token: this.#token },
3621
4952
  hasCapabilities: hasCapabilities()
3622
4953
  };
3623
4954
  }
@@ -3633,11 +4964,8 @@ var Iris = class {
3633
4964
  this.#presenter?.setIdleEndMs(idleEndMs);
3634
4965
  return { ok: true, result: { applied: this.#presenter !== void 0, idleEndMs } };
3635
4966
  }
3636
- if (command.name === IrisCommand.PRESENTER) {
3637
- const state = command.args["state"];
3638
- if (isSessionState(state)) {
3639
- this.#presenter?.setState(state, str2(command.args["text"]) || void 0);
3640
- }
4967
+ if (command.name === IrisCommand.PRESENTER || command.name === IrisCommand.FLOWS) {
4968
+ this.#presenter?.handlePush(command);
3641
4969
  return { ok: true, result: { applied: this.#presenter !== void 0 } };
3642
4970
  }
3643
4971
  const handler = this.#registry.get(command.name);
@@ -3780,6 +5108,52 @@ function registerIrisDomain(domain) {
3780
5108
  var globalStore4 = globalThis;
3781
5109
  var iris = globalStore4.__irisInstance ??= new Iris();
3782
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
+
3783
5157
  // ../react/dist/index.js
3784
5158
  var FIBER_PREFIXES = ["__reactFiber$", "__reactInternalInstance$"];
3785
5159
  var MAX_DEPTH = 200;
@@ -3943,14 +5317,15 @@ function hasHoverHandlers(el) {
3943
5317
  const p = props;
3944
5318
  return HOVER_HANDLER_KEYS.some((k) => typeof p[k] === "function");
3945
5319
  }
3946
- var installed2 = false;
5320
+ var installed3 = false;
3947
5321
  function install() {
3948
- if (installed2)
5322
+ if (installed3)
3949
5323
  return;
3950
- installed2 = true;
5324
+ installed3 = true;
3951
5325
  registerAdapter({ name: "react", identify, readState: readState2, hasHoverHandlers });
3952
5326
  }
3953
5327
  export {
5328
+ Annotator,
3954
5329
  Iris,
3955
5330
  RefRegistry,
3956
5331
  SESSION_AUTO,
@@ -3971,6 +5346,8 @@ export {
3971
5346
  identify,
3972
5347
  identifyComponent,
3973
5348
  install,
5349
+ installAnnotator,
5350
+ installRenderMeter,
3974
5351
  iris,
3975
5352
  isVisible,
3976
5353
  matchQuery,
@@ -3982,6 +5359,7 @@ export {
3982
5359
  registerCapabilities,
3983
5360
  registerIrisDomain,
3984
5361
  registerStore,
5362
+ resolveMarkAnchor,
3985
5363
  runQuery,
3986
5364
  setIgnoreSelectors,
3987
5365
  storeNames,