@oxvo/ai-live-assist 7.3.4 → 7.4.23

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.
Files changed (67) hide show
  1. package/cjs/AiLiveAssist.d.ts +588 -6
  2. package/cjs/AiLiveAssist.js +14892 -1240
  3. package/cjs/client.d.ts +71 -10
  4. package/cjs/client.js +316 -58
  5. package/cjs/context.d.ts +15 -5
  6. package/cjs/context.js +568 -73
  7. package/cjs/control.d.ts +72 -7
  8. package/cjs/control.js +594 -71
  9. package/cjs/dialogFocusScope.d.ts +37 -0
  10. package/cjs/dialogFocusScope.js +227 -0
  11. package/cjs/experienceState.js +1 -1
  12. package/cjs/index.d.ts +1 -1
  13. package/cjs/index.js +2 -2
  14. package/cjs/media.d.ts +14 -3
  15. package/cjs/media.js +128 -19
  16. package/cjs/messages.js +26 -3
  17. package/cjs/messengerLoader.d.ts +3 -0
  18. package/cjs/messengerLoader.js +156 -0
  19. package/cjs/pageAssistancePreference.d.ts +78 -0
  20. package/cjs/pageAssistancePreference.js +488 -0
  21. package/cjs/preview.d.ts +33 -0
  22. package/cjs/preview.js +567 -0
  23. package/cjs/tabLock.d.ts +54 -3
  24. package/cjs/tabLock.js +752 -72
  25. package/cjs/types.d.ts +152 -4
  26. package/cjs/ui.d.ts +200 -10
  27. package/cjs/ui.js +6946 -523
  28. package/cjs/version.d.ts +1 -1
  29. package/cjs/version.js +1 -1
  30. package/cjs/voicePresenceUi.d.ts +48 -9
  31. package/cjs/voicePresenceUi.js +863 -125
  32. package/lib/AiLiveAssist.d.ts +588 -6
  33. package/lib/AiLiveAssist.js +14897 -1248
  34. package/lib/client.d.ts +71 -10
  35. package/lib/client.js +313 -57
  36. package/lib/context.d.ts +15 -5
  37. package/lib/context.js +566 -72
  38. package/lib/control.d.ts +72 -7
  39. package/lib/control.js +594 -71
  40. package/lib/dialogFocusScope.d.ts +37 -0
  41. package/lib/dialogFocusScope.js +223 -0
  42. package/lib/experienceState.js +1 -1
  43. package/lib/index.d.ts +1 -1
  44. package/lib/index.js +2 -2
  45. package/lib/media.d.ts +14 -3
  46. package/lib/media.js +128 -19
  47. package/lib/messages.js +26 -3
  48. package/lib/messengerLoader.d.ts +3 -0
  49. package/lib/messengerLoader.js +151 -0
  50. package/lib/pageAssistancePreference.d.ts +78 -0
  51. package/lib/pageAssistancePreference.js +482 -0
  52. package/lib/preview.d.ts +33 -0
  53. package/lib/preview.js +561 -0
  54. package/lib/tabLock.d.ts +54 -3
  55. package/lib/tabLock.js +752 -72
  56. package/lib/types.d.ts +152 -4
  57. package/lib/ui.d.ts +200 -10
  58. package/lib/ui.js +6947 -524
  59. package/lib/version.d.ts +1 -1
  60. package/lib/version.js +1 -1
  61. package/lib/voicePresenceUi.d.ts +48 -9
  62. package/lib/voicePresenceUi.js +863 -125
  63. package/package.json +8 -3
  64. package/cjs/recordingPolicy.d.ts +0 -2
  65. package/cjs/recordingPolicy.js +0 -12
  66. package/lib/recordingPolicy.d.ts +0 -2
  67. package/lib/recordingPolicy.js +0 -8
package/cjs/context.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.PageContextCollector = void 0;
3
+ exports.PageContextCollector = exports.actionTargetFingerprint = void 0;
4
4
  const visualContext_js_1 = require("./visualContext.js");
5
5
  const MAX_TARGETS = 256;
6
6
  const MAX_TEXT_ITEMS = 256;
@@ -29,12 +29,64 @@ const SENSITIVE_PATTERNS = [
29
29
  ['authentication_secret', /auth.*secret|private.?key|access.?token/i],
30
30
  ['api_secret', /api.?key|client.?secret|secret.?key/i],
31
31
  ];
32
+ const EXPLICIT_SENSITIVITIES = new Set([
33
+ 'private',
34
+ 'password',
35
+ 'otp',
36
+ 'payment',
37
+ 'banking',
38
+ 'recovery',
39
+ 'authentication_secret',
40
+ 'api_secret',
41
+ ]);
32
42
  const id = (prefix) => `${prefix}_${crypto.randomUUID()}`;
33
43
  const normalize = (value, maximum) => value
34
44
  .replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, '')
35
45
  .replace(/\s+/g, ' ')
36
46
  .trim()
37
47
  .slice(0, maximum);
48
+ const canonicalJson = (value) => {
49
+ const sort = (item) => {
50
+ if (Array.isArray(item))
51
+ return item.map(sort);
52
+ if (item !== null && typeof item === 'object') {
53
+ return Object.fromEntries(Object.entries(item)
54
+ .sort(([left], [right]) => left.localeCompare(right))
55
+ .map(([key, nested]) => [key, sort(nested)]));
56
+ }
57
+ return item;
58
+ };
59
+ return JSON.stringify(sort(value));
60
+ };
61
+ const actionTargetFingerprint = async (target, subtle = globalThis.crypto?.subtle) => {
62
+ if (!subtle)
63
+ return null;
64
+ try {
65
+ const canonical = canonicalJson({
66
+ targetId: target.targetId,
67
+ role: target.role,
68
+ name: target.name,
69
+ inputType: target.inputType ?? null,
70
+ autocomplete: target.autocomplete ?? null,
71
+ semanticRegion: target.semanticRegion ?? null,
72
+ destinationPath: target.destinationPath ?? null,
73
+ sensitivity: target.sensitivity,
74
+ protectedRegion: target.protectedRegion ?? null,
75
+ frameOrigin: target.frameOrigin ?? null,
76
+ consequence: target.consequence ?? null,
77
+ regionAssessment: target.regionAssessment ?? null,
78
+ options: target.options ?? null,
79
+ });
80
+ const digest = new Uint8Array(await subtle.digest('SHA-256', new TextEncoder().encode(canonical)));
81
+ if (digest.byteLength !== 32)
82
+ return null;
83
+ return Array.from(digest, byte => byte.toString(16).padStart(2, '0')).join('');
84
+ }
85
+ catch {
86
+ return null;
87
+ }
88
+ };
89
+ exports.actionTargetFingerprint = actionTargetFingerprint;
38
90
  const isHTMLElement = (value) => value instanceof HTMLElement;
39
91
  const isRendered = (element) => {
40
92
  if (!isHTMLElement(element) || !element.isConnected || element.closest(PRIVATE_SELECTOR)) {
@@ -60,11 +112,22 @@ const isVisible = (element) => {
60
112
  rect.top <= window.innerHeight &&
61
113
  rect.left <= window.innerWidth);
62
114
  };
63
- const isCovered = (element) => {
115
+ const isCovered = (element, trustedGuideRoot = null) => {
64
116
  const rect = element.getBoundingClientRect();
65
117
  const x = Math.max(0, Math.min(window.innerWidth - 1, rect.left + rect.width / 2));
66
118
  const y = Math.max(0, Math.min(window.innerHeight - 1, rect.top + rect.height / 2));
67
- const top = document.elementFromPoint(x, y);
119
+ let top;
120
+ if (trustedGuideRoot?.isConnected &&
121
+ typeof document.elementsFromPoint === 'function') {
122
+ top =
123
+ document
124
+ .elementsFromPoint(x, y)
125
+ .find((candidate) => candidate !== trustedGuideRoot) ??
126
+ document.elementFromPoint(x, y);
127
+ }
128
+ else {
129
+ top = document.elementFromPoint(x, y);
130
+ }
68
131
  return Boolean(top && top !== element && !element.contains(top) && !top.contains(element));
69
132
  };
70
133
  const safePageUrl = () => `${window.location.origin}${window.location.pathname}`;
@@ -97,17 +160,22 @@ const sensitivityFor = (element, name) => {
97
160
  return 'otp';
98
161
  if (/^cc-/.test(autocomplete))
99
162
  return 'payment';
163
+ const explicitSensitivity = normalize(element.closest('[data-oxvo-sensitivity]')?.getAttribute('data-oxvo-sensitivity') ?? '', 64);
164
+ if (EXPLICIT_SENSITIVITIES.has(explicitSensitivity))
165
+ return explicitSensitivity;
100
166
  const descriptors = [
101
167
  name,
102
168
  element.getAttribute('name') ?? '',
103
169
  element.id,
104
170
  element.getAttribute('autocomplete') ?? '',
105
- element.getAttribute('data-oxvo-sensitivity') ?? '',
171
+ element.closest('[data-oxvo-sensitivity]')?.getAttribute('data-oxvo-sensitivity') ?? '',
106
172
  ].join(' ');
107
173
  for (const [sensitivity, pattern] of SENSITIVE_PATTERNS) {
108
174
  if (pattern.test(descriptors))
109
175
  return sensitivity;
110
176
  }
177
+ if (element.closest('[data-oxvo-sensitivity]'))
178
+ return 'private';
111
179
  if (element.closest('[data-oxvo-private]'))
112
180
  return 'private';
113
181
  return 'none';
@@ -141,11 +209,6 @@ const roleFor = (element) => {
141
209
  return tag.slice(0, 64);
142
210
  };
143
211
  const nameFor = (element) => {
144
- const labelledBy = element.getAttribute('aria-labelledby');
145
- const labelledText = labelledBy
146
- ?.split(/\s+/)
147
- .map((value) => document.getElementById(value)?.textContent ?? '')
148
- .join(' ');
149
212
  const input = element instanceof HTMLInputElement ? element : null;
150
213
  const formControl = element instanceof HTMLInputElement ||
151
214
  element instanceof HTMLSelectElement ||
@@ -154,49 +217,208 @@ const nameFor = (element) => {
154
217
  : null;
155
218
  const associatedLabel = formControl
156
219
  ? Array.from(formControl.labels ?? [])
157
- .flatMap((label) => {
158
- const walker = document.createTreeWalker(label, NodeFilter.SHOW_TEXT);
159
- const text = [];
160
- let node = walker.nextNode();
161
- while (node) {
162
- if (!node.parentElement?.closest('input,select,textarea,option,button')) {
163
- text.push(node.textContent ?? '');
164
- }
165
- node = walker.nextNode();
166
- }
167
- return text;
168
- })
220
+ .map((label) => safeAccessibleNameText(label, 300))
221
+ .filter(Boolean)
169
222
  .join(' ')
170
223
  : '';
171
- const explicitName = element.getAttribute('aria-label') ??
172
- labelledText ??
173
- element.getAttribute('alt') ??
174
- element.getAttribute('title') ??
175
- element.getAttribute('placeholder');
224
+ const safeAttribute = (attribute, maximum) => {
225
+ const value = normalize(element.getAttribute(attribute) ?? '', maximum);
226
+ return value && sensitivityFor(element, value) === 'none' ? value : '';
227
+ };
228
+ const semanticName = safeAccessibleNameText(element, 300);
229
+ const attributeName = safeAttribute('alt', 300) ||
230
+ safeAttribute('title', 300) ||
231
+ safeAttribute('placeholder', 300);
176
232
  if (element instanceof HTMLFormElement) {
177
- return normalize(explicitName ??
178
- element.getAttribute('data-oxvo-ai-region') ??
179
- element.getAttribute('name') ??
233
+ const explicitSemanticName = element.hasAttribute('aria-label') || element.hasAttribute('aria-labelledby')
234
+ ? semanticName
235
+ : '';
236
+ return normalize(explicitSemanticName ||
237
+ attributeName ||
238
+ safeAttribute('data-oxvo-ai-region', 120) ||
239
+ safeAttribute('name', 120) ||
180
240
  'Form', 120);
181
241
  }
182
- return normalize(explicitName ??
183
- (associatedLabel || null) ??
184
- (input && ['button', 'submit', 'reset'].includes(input.type) ? input.value : null) ??
185
- element.textContent ??
186
- '', 300);
242
+ const buttonValue = input && ['button', 'submit', 'reset'].includes(input.type)
243
+ ? normalize(input.value, 300)
244
+ : '';
245
+ return normalize(semanticName ||
246
+ associatedLabel ||
247
+ attributeName ||
248
+ (buttonValue && sensitivityFor(element, buttonValue) === 'none' ? buttonValue : ''), 300);
249
+ };
250
+ const safeDescendantText = (element, maximum, renderedOnly = false) => {
251
+ const parts = [];
252
+ const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT);
253
+ let node = walker.nextNode();
254
+ while (node) {
255
+ const parent = node.parentElement;
256
+ const text = normalize(node.textContent ?? '', maximum);
257
+ if (parent &&
258
+ text &&
259
+ !parent.closest(PRIVATE_SELECTOR) &&
260
+ !parent.closest('script,style,noscript,template,svg,input,select,textarea,option') &&
261
+ (renderedOnly ? isRendered(parent) : isVisible(parent)) &&
262
+ sensitivityFor(parent, text) === 'none') {
263
+ parts.push(text);
264
+ }
265
+ node = walker.nextNode();
266
+ }
267
+ return normalize(parts.join(' '), maximum);
268
+ };
269
+ const safeSemanticText = (element, maximum) => {
270
+ if (!isVisible(element) || element.closest(PRIVATE_SELECTOR))
271
+ return '';
272
+ const explicit = normalize(element.getAttribute('aria-label') ?? '', maximum);
273
+ if (explicit) {
274
+ return sensitivityFor(element, explicit) === 'none' ? explicit : '';
275
+ }
276
+ const labelledBy = element.getAttribute('aria-labelledby');
277
+ const labelledParts = labelledBy
278
+ ? labelledBy
279
+ .split(/\s+/)
280
+ .map((idValue) => document.getElementById(idValue))
281
+ .filter((label) => Boolean(label))
282
+ .flatMap((label) => {
283
+ if (!isVisible(label) || label.closest(PRIVATE_SELECTOR))
284
+ return [];
285
+ const text = safeDescendantText(label, maximum);
286
+ return text && sensitivityFor(label, text) === 'none' ? [text] : [];
287
+ })
288
+ : [];
289
+ if (labelledParts.length > 0) {
290
+ const text = normalize(labelledParts.join(' '), maximum);
291
+ return sensitivityFor(element, text) === 'none' ? text : '';
292
+ }
293
+ const text = safeDescendantText(element, maximum);
294
+ return text && sensitivityFor(element, text) === 'none' ? text : '';
295
+ };
296
+ const safeAccessibleNameText = (element, maximum) => {
297
+ if (!isRendered(element) || element.closest(PRIVATE_SELECTOR))
298
+ return '';
299
+ const explicit = normalize(element.getAttribute('aria-label') ?? '', maximum);
300
+ if (explicit) {
301
+ return sensitivityFor(element, explicit) === 'none' ? explicit : '';
302
+ }
303
+ const labelledBy = element.getAttribute('aria-labelledby');
304
+ const labelledParts = labelledBy
305
+ ? labelledBy
306
+ .split(/\s+/)
307
+ .map((idValue) => document.getElementById(idValue))
308
+ .filter((label) => Boolean(label))
309
+ .flatMap((label) => {
310
+ if (!isRendered(label) || label.closest(PRIVATE_SELECTOR))
311
+ return [];
312
+ const text = safeDescendantText(label, maximum, true);
313
+ return text && sensitivityFor(label, text) === 'none' ? [text] : [];
314
+ })
315
+ : [];
316
+ if (labelledParts.length > 0) {
317
+ const text = normalize(labelledParts.join(' '), maximum);
318
+ return sensitivityFor(element, text) === 'none' ? text : '';
319
+ }
320
+ const text = safeDescendantText(element, maximum, true);
321
+ return text && sensitivityFor(element, text) === 'none' ? text : '';
322
+ };
323
+ const semanticSourceSensitivity = (source) => {
324
+ if (source.closest(PRIVATE_SELECTOR) || source.querySelector(PRIVATE_SELECTOR)) {
325
+ return 'private';
326
+ }
327
+ const markedSources = [
328
+ ...(source.matches('[data-oxvo-sensitivity]') ? [source] : []),
329
+ ...Array.from(source.querySelectorAll('[data-oxvo-sensitivity]')),
330
+ ];
331
+ for (const marked of markedSources) {
332
+ const marker = normalize(marked.getAttribute('data-oxvo-sensitivity') ?? '', 64);
333
+ if (EXPLICIT_SENSITIVITIES.has(marker))
334
+ return marker;
335
+ const descriptor = normalize(`${marker} ${marked.textContent ?? ''}`, 500);
336
+ for (const [sensitivity, pattern] of SENSITIVE_PATTERNS) {
337
+ if (pattern.test(descriptor))
338
+ return sensitivity;
339
+ }
340
+ return 'private';
341
+ }
342
+ const text = normalize(`${source.getAttribute('label') ?? ''} ${source.textContent ?? ''}`, 500);
343
+ return sensitivityFor(source, text);
344
+ };
345
+ const accessibleNameSourceSensitivity = (element) => {
346
+ const labelledBy = element.getAttribute('aria-labelledby');
347
+ const referencedLabels = labelledBy
348
+ ? labelledBy
349
+ .split(/\s+/)
350
+ .map((idValue) => document.getElementById(idValue))
351
+ .filter((label) => Boolean(label))
352
+ : [];
353
+ const formControl = element instanceof HTMLInputElement ||
354
+ element instanceof HTMLSelectElement ||
355
+ element instanceof HTMLTextAreaElement
356
+ ? element
357
+ : null;
358
+ const sources = [
359
+ ...referencedLabels,
360
+ ...Array.from(formControl?.labels ?? []),
361
+ ];
362
+ for (const source of sources) {
363
+ const sensitivity = semanticSourceSensitivity(source);
364
+ if (sensitivity !== 'none')
365
+ return sensitivity;
366
+ }
367
+ return 'none';
368
+ };
369
+ const safeOptionLabel = (option) => {
370
+ const label = normalize(option.label || option.textContent || '', 300);
371
+ if (!label || semanticSourceSensitivity(option) !== 'none')
372
+ return '';
373
+ const group = option.closest('optgroup');
374
+ if (group && semanticSourceSensitivity(group) !== 'none')
375
+ return '';
376
+ return sensitivityFor(option, label) === 'none' ? label : '';
187
377
  };
188
378
  const isActionTarget = (element) => element.matches('a[href],button,input,select,textarea,summary,form,[contenteditable="true"],[role="button"],[role="link"],[role="checkbox"],[role="radio"],[role="combobox"],[role="textbox"],[tabindex]');
379
+ const READONLY_INPUT_TYPES = new Set([
380
+ 'date',
381
+ 'datetime-local',
382
+ 'email',
383
+ 'month',
384
+ 'number',
385
+ 'password',
386
+ 'search',
387
+ 'tel',
388
+ 'text',
389
+ 'time',
390
+ 'url',
391
+ 'week',
392
+ ]);
393
+ const isReadOnlyTextControl = (element) => (element instanceof HTMLInputElement &&
394
+ READONLY_INPUT_TYPES.has(element.type) &&
395
+ element.readOnly) ||
396
+ (element instanceof HTMLTextAreaElement && element.readOnly);
189
397
  const enabled = (element) => {
190
- if (element.getAttribute('aria-disabled') === 'true')
398
+ if (element.getAttribute('aria-disabled') === 'true' ||
399
+ element.getAttribute('aria-readonly') === 'true' ||
400
+ isReadOnlyTextControl(element)) {
191
401
  return false;
402
+ }
192
403
  if (element instanceof HTMLButtonElement ||
193
404
  element instanceof HTMLInputElement ||
194
405
  element instanceof HTMLSelectElement ||
195
406
  element instanceof HTMLTextAreaElement) {
196
- return !element.disabled;
407
+ return !element.matches(':disabled');
197
408
  }
198
409
  return true;
199
410
  };
411
+ const optionDisabled = (option) => option.disabled ||
412
+ option.closest('optgroup')?.disabled === true ||
413
+ option.matches(':disabled');
414
+ const literalBooleanAttribute = (element, attribute) => {
415
+ const value = element.getAttribute(attribute);
416
+ if (value === 'true')
417
+ return true;
418
+ if (value === 'false')
419
+ return false;
420
+ return undefined;
421
+ };
200
422
  const consequenceFor = (element, role, name) => {
201
423
  const form = element instanceof HTMLFormElement ? element : element.closest('form');
202
424
  const explicit = element
@@ -247,10 +469,12 @@ const consequenceFor = (element, role, name) => {
247
469
  return role === 'button' || role === 'checkbox' || role === 'radio' ? 'state_change' : 'none';
248
470
  };
249
471
  class PageContextCollector {
250
- constructor(callbacks, selectorRegionRules = [], policyVersion = 1) {
472
+ constructor(callbacks, selectorRegionRules = [], policyVersion = 1, trustedGuideRoot = null, statefulButtonActionProof = false) {
251
473
  this.callbacks = callbacks;
252
474
  this.selectorRegionRules = selectorRegionRules;
253
475
  this.policyVersion = policyVersion;
476
+ this.trustedGuideRoot = trustedGuideRoot;
477
+ this.statefulButtonActionProof = statefulButtonActionProof;
254
478
  this.pageId = id('page');
255
479
  this.revision = 0;
256
480
  this.running = false;
@@ -267,10 +491,12 @@ class PageContextCollector {
267
491
  this.pendingNavigation = null;
268
492
  this.pendingCapture = false;
269
493
  this.deferredNavigation = null;
494
+ this.navigationSettlementGeneration = 0;
495
+ this.navigationSettlement = null;
270
496
  this.recentSelection = null;
271
497
  this.latestSemanticSignature = null;
272
498
  this.scheduleCapture = () => {
273
- if (!this.running)
499
+ if (!this.running || this.navigationSettlement)
274
500
  return;
275
501
  if (this.executingActionId) {
276
502
  this.pendingCapture = true;
@@ -287,7 +513,7 @@ class PageContextCollector {
287
513
  }, CAPTURE_SETTLE_MS);
288
514
  };
289
515
  this.scheduleMutationCapture = () => {
290
- if (!this.running)
516
+ if (!this.running || this.navigationSettlement)
291
517
  return;
292
518
  if (this.executingActionId) {
293
519
  this.pendingCapture = true;
@@ -328,6 +554,14 @@ class PageContextCollector {
328
554
  this.handleNavigation = () => {
329
555
  if (!this.running)
330
556
  return;
557
+ const currentUrl = safePageUrl();
558
+ if (this.executingActionId &&
559
+ this.pendingNavigation?.causalActionId === this.executingActionId &&
560
+ this.pendingNavigation.url === currentUrl) {
561
+ this.pendingCapture = true;
562
+ this.reportCausalNavigationObservation();
563
+ return;
564
+ }
331
565
  this.pageId = id('page');
332
566
  this.revision += 1;
333
567
  this.targets.clear();
@@ -336,12 +570,27 @@ class PageContextCollector {
336
570
  this.pendingNavigation = {
337
571
  pageId: this.pageId,
338
572
  revision: this.revision,
339
- url: safePageUrl(),
573
+ url: currentUrl,
574
+ causalActionId: this.executingActionId,
575
+ causalObservationReported: false,
340
576
  };
341
577
  this.pendingCapture = true;
578
+ this.reportCausalNavigationObservation();
342
579
  return;
343
580
  }
344
- void Promise.resolve(this.callbacks.onNavigation(this.pageId, this.revision, safePageUrl())).then(() => this.captureAndPublish());
581
+ const settlement = this.beginNavigationSettlement(this.pageId, this.revision, currentUrl);
582
+ void Promise.resolve()
583
+ .then(() => this.callbacks.onNavigation(settlement.pageId, settlement.revision, settlement.url))
584
+ .then((navigationAcknowledged) => {
585
+ if (navigationAcknowledged === false ||
586
+ !this.running ||
587
+ this.navigationSettlement !== settlement) {
588
+ return undefined;
589
+ }
590
+ this.navigationSettlement = null;
591
+ return this.captureAndPublish();
592
+ })
593
+ .catch(() => undefined);
345
594
  };
346
595
  }
347
596
  get currentRevision() {
@@ -388,6 +637,7 @@ class PageContextCollector {
388
637
  'aria-label',
389
638
  'aria-disabled',
390
639
  'aria-expanded',
640
+ 'aria-pressed',
391
641
  'aria-live',
392
642
  'class',
393
643
  'disabled',
@@ -442,14 +692,19 @@ class PageContextCollector {
442
692
  this.pendingNavigation = null;
443
693
  this.pendingCapture = false;
444
694
  this.deferredNavigation = null;
695
+ this.navigationSettlementGeneration += 1;
696
+ this.navigationSettlement = null;
445
697
  this.recentSelection = null;
446
698
  this.latestSemanticSignature = null;
447
699
  this.callbacks.onHighlight(null, 'focus');
448
700
  }
449
701
  async captureAndPublish() {
702
+ if (this.navigationSettlement)
703
+ return null;
450
704
  const snapshot = this.capture();
451
- if (this.running)
705
+ if (this.running) {
452
706
  await this.callbacks.onSnapshot(snapshot);
707
+ }
453
708
  return snapshot;
454
709
  }
455
710
  capture() {
@@ -468,8 +723,17 @@ class PageContextCollector {
468
723
  }
469
724
  if (!isActionTarget(element) || !isRendered(element))
470
725
  continue;
726
+ const role = roleFor(element);
727
+ const hasStatefulButtonAttribute = role === 'button' &&
728
+ (element.hasAttribute('aria-expanded') || element.hasAttribute('aria-pressed'));
729
+ const hasUnsupportedMixedPressedState = role === 'button' &&
730
+ element.getAttribute('aria-pressed')?.trim().toLowerCase() === 'mixed';
731
+ if (hasUnsupportedMixedPressedState ||
732
+ (!this.statefulButtonActionProof && hasStatefulButtonAttribute)) {
733
+ continue;
734
+ }
471
735
  const name = nameFor(element);
472
- if (!name && !['form', 'textbox'].includes(roleFor(element)))
736
+ if (!name && !['form', 'textbox'].includes(role))
473
737
  continue;
474
738
  const targetId = this.targetIds.get(element) ?? id('target');
475
739
  this.targetIds.set(element, targetId);
@@ -478,15 +742,18 @@ class PageContextCollector {
478
742
  if (element instanceof HTMLSelectElement) {
479
743
  options = Array.from(element.options)
480
744
  .slice(0, MAX_OPTIONS)
481
- .map((option) => {
745
+ .flatMap((option) => {
746
+ const label = safeOptionLabel(option);
747
+ if (!label)
748
+ return [];
482
749
  const optionId = this.optionIds.get(option) ?? id('option');
483
750
  this.optionIds.set(option, optionId);
484
751
  optionMap.set(optionId, option);
485
- return {
486
- id: optionId,
487
- label: normalize(option.label || option.textContent || '', 300),
488
- disabled: option.disabled,
489
- };
752
+ return [{
753
+ id: optionId,
754
+ label,
755
+ disabled: optionDisabled(option),
756
+ }];
490
757
  });
491
758
  }
492
759
  const semanticRegion = normalize(element.closest('[data-oxvo-ai-region]')?.getAttribute('data-oxvo-ai-region') ?? '', 128);
@@ -506,10 +773,14 @@ class PageContextCollector {
506
773
  // Invalid tenant selectors remain unevaluated so server policy fails closed.
507
774
  }
508
775
  }
509
- const role = roleFor(element);
510
- const sensitivity = sensitivityFor(element, name);
776
+ const directSensitivity = sensitivityFor(element, name);
777
+ const sensitivity = directSensitivity !== 'none'
778
+ ? directSensitivity
779
+ : accessibleNameSourceSensitivity(element);
511
780
  const visible = isVisible(element);
512
781
  const destinationPath = safeDestinationPath(element);
782
+ const expanded = literalBooleanAttribute(element, 'aria-expanded');
783
+ const pressed = literalBooleanAttribute(element, 'aria-pressed');
513
784
  const target = {
514
785
  targetId,
515
786
  revision,
@@ -535,10 +806,12 @@ class PageContextCollector {
535
806
  state: {
536
807
  visible,
537
808
  enabled: enabled(element),
538
- covered: visible ? isCovered(element) : false,
809
+ covered: visible ? isCovered(element, this.trustedGuideRoot) : false,
539
810
  ...(element instanceof HTMLInputElement && ['checkbox', 'radio'].includes(element.type)
540
811
  ? { checked: element.checked }
541
812
  : {}),
813
+ ...(this.statefulButtonActionProof && expanded !== undefined ? { expanded } : {}),
814
+ ...(this.statefulButtonActionProof && pressed !== undefined ? { pressed } : {}),
542
815
  },
543
816
  ...(options ? { options } : {}),
544
817
  };
@@ -571,11 +844,12 @@ class PageContextCollector {
571
844
  const landmarks = Array.from(document.querySelectorAll('main,nav,header,footer,aside,[role="main"],[role="navigation"]'))
572
845
  .filter(isVisible)
573
846
  .slice(0, 32)
574
- .map((element) => ({ role: roleFor(element), label: nameFor(element) }));
847
+ .map((element) => ({ role: roleFor(element), label: safeSemanticText(element, 300) }))
848
+ .filter(({ label }) => Boolean(label));
575
849
  const alerts = Array.from(document.querySelectorAll('[role="alert"],[aria-live]'))
576
850
  .filter(isVisible)
577
851
  .slice(0, 16)
578
- .map((element) => normalize(element.textContent ?? '', 500))
852
+ .map((element) => safeSemanticText(element, 500))
579
853
  .filter(Boolean);
580
854
  const activeSelection = this.readSafeSelection();
581
855
  if (activeSelection)
@@ -612,6 +886,8 @@ class PageContextCollector {
612
886
  return snapshot;
613
887
  }
614
888
  captureVisualContext() {
889
+ if (this.navigationSettlement)
890
+ return null;
615
891
  if (this.revision < 1 || this.targets.size === 0)
616
892
  return null;
617
893
  const viewportWidth = Math.max(1, Math.round(window.innerWidth));
@@ -650,14 +926,81 @@ class PageContextCollector {
650
926
  regions,
651
927
  });
652
928
  }
653
- async execute(grant) {
654
- const handle = this.targets.get(grant.targetId);
929
+ async execute(grant, authorityIsCurrent = () => true) {
655
930
  const base = {
656
931
  grant: grant.grant,
657
932
  actionId: grant.actionId,
658
933
  };
659
- if (Date.parse(grant.expiresAt) <= Date.now() ||
660
- grant.domRevision !== this.revision ||
934
+ const authorityStale = () => ({
935
+ ...base,
936
+ status: 'stale',
937
+ observed: {
938
+ url: safePageUrl(),
939
+ revision: this.revision,
940
+ targetState: 'unchanged',
941
+ safeSummary: 'Page assistance permission changed before execution.',
942
+ },
943
+ });
944
+ const expiresAt = Date.parse(grant.expiresAt);
945
+ const approvalExpired = () => ({
946
+ ...base,
947
+ status: 'stale',
948
+ observed: {
949
+ url: safePageUrl(),
950
+ revision: this.revision,
951
+ targetState: 'unchanged',
952
+ safeSummary: 'The approved page action expired before execution.',
953
+ },
954
+ });
955
+ const executionStale = () => {
956
+ if (!authorityIsCurrent())
957
+ return authorityStale();
958
+ return expiresAt <= Date.now() ? approvalExpired() : null;
959
+ };
960
+ const initialStale = executionStale();
961
+ if (initialStale)
962
+ return initialStale;
963
+ this.captureSnapshot(false);
964
+ let handle = this.targets.get(grant.targetId);
965
+ const revisionMatchesOrStableNavigation = grant.domRevision === this.revision ||
966
+ (grant.domRevision < this.revision &&
967
+ grant.action === 'click' &&
968
+ handle?.element instanceof HTMLAnchorElement &&
969
+ handle.target.consequence === 'navigation' &&
970
+ typeof handle.target.destinationPath === 'string');
971
+ if (expiresAt <= Date.now() ||
972
+ grant.pageId !== this.pageId ||
973
+ grant.pageUrl !== safePageUrl() ||
974
+ grant.policyVersion !== this.policyVersion ||
975
+ !revisionMatchesOrStableNavigation ||
976
+ !handle ||
977
+ handle.revision !== this.revision ||
978
+ !handle.element.isConnected) {
979
+ return {
980
+ ...base,
981
+ status: 'stale',
982
+ observed: {
983
+ url: safePageUrl(),
984
+ revision: this.revision,
985
+ targetState: 'detached',
986
+ safeSummary: 'The requested target changed before execution.',
987
+ },
988
+ };
989
+ }
990
+ const fingerprintedHandle = handle;
991
+ const fingerprint = await (0, exports.actionTargetFingerprint)(handle.target);
992
+ const postFingerprintStale = executionStale();
993
+ if (postFingerprintStale)
994
+ return postFingerprintStale;
995
+ this.captureSnapshot(false);
996
+ handle = this.targets.get(grant.targetId);
997
+ if (!fingerprint ||
998
+ fingerprint !== grant.targetFingerprint ||
999
+ grant.pageId !== this.pageId ||
1000
+ grant.pageUrl !== safePageUrl() ||
1001
+ grant.policyVersion !== this.policyVersion ||
1002
+ !revisionMatchesOrStableNavigation ||
1003
+ handle !== fingerprintedHandle ||
661
1004
  !handle ||
662
1005
  handle.revision !== this.revision ||
663
1006
  !handle.element.isConnected) {
@@ -683,7 +1026,9 @@ class PageContextCollector {
683
1026
  !isRendered(element) ||
684
1027
  (grant.action !== 'scroll' && !isVisible(element) && !canRevealDesiredStateTarget) ||
685
1028
  !enabled(element) ||
686
- (grant.action !== 'scroll' && isVisible(element) && isCovered(element))) {
1029
+ (grant.action !== 'scroll' &&
1030
+ isVisible(element) &&
1031
+ isCovered(element, this.trustedGuideRoot))) {
687
1032
  return {
688
1033
  ...base,
689
1034
  status: 'blocked',
@@ -696,9 +1041,17 @@ class PageContextCollector {
696
1041
  };
697
1042
  }
698
1043
  if (!isVisible(element) && canRevealDesiredStateTarget) {
1044
+ const preRevealStale = executionStale();
1045
+ if (preRevealStale)
1046
+ return preRevealStale;
699
1047
  element.scrollIntoView({ behavior: 'auto', block: 'center' });
700
1048
  await new Promise((resolve) => setTimeout(resolve, 80));
701
- if (!isRendered(element) || !isVisible(element) || isCovered(element)) {
1049
+ const postRevealStale = executionStale();
1050
+ if (postRevealStale)
1051
+ return postRevealStale;
1052
+ if (!isRendered(element) ||
1053
+ !isVisible(element) ||
1054
+ isCovered(element, this.trustedGuideRoot)) {
702
1055
  return {
703
1056
  ...base,
704
1057
  status: 'blocked',
@@ -711,11 +1064,18 @@ class PageContextCollector {
711
1064
  };
712
1065
  }
713
1066
  }
1067
+ const preHighlightStale = executionStale();
1068
+ if (preHighlightStale)
1069
+ return preHighlightStale;
714
1070
  const before = this.safeState(element);
1071
+ const beforeActionUrl = safePageUrl();
1072
+ this.callbacks.onHighlight(element, 'focus');
1073
+ const preMutationStale = executionStale();
1074
+ if (preMutationStale)
1075
+ return preMutationStale;
715
1076
  this.executingActionId = grant.actionId;
716
1077
  this.pendingCapture = false;
717
1078
  this.deferredNavigation = null;
718
- this.callbacks.onHighlight(element, 'focus');
719
1079
  try {
720
1080
  switch (grant.action) {
721
1081
  case 'scroll':
@@ -732,7 +1092,7 @@ class PageContextCollector {
732
1092
  element.focus({ preventScroll: false });
733
1093
  break;
734
1094
  case 'click':
735
- this.click(element, grant.actionId, grant.desiredChecked, handle.target.destinationPath);
1095
+ this.click(element, grant.actionId, grant.desiredChecked, grant.desiredExpanded, grant.desiredPressed, grant.destinationPath);
736
1096
  break;
737
1097
  case 'type':
738
1098
  this.type(element, grant.value ?? '');
@@ -744,13 +1104,29 @@ class PageContextCollector {
744
1104
  this.submit(element);
745
1105
  break;
746
1106
  }
1107
+ if (['click', 'type', 'select', 'submit'].includes(grant.action)) {
1108
+ this.callbacks.onActionMutationApplied?.(grant.actionId, this.revision, safePageUrl());
1109
+ }
1110
+ if (grant.action === 'click' &&
1111
+ typeof grant.destinationPath === 'string') {
1112
+ this.observeCausalNavigationAfterMutation(grant.actionId, grant.destinationPath, beforeActionUrl);
1113
+ }
747
1114
  await new Promise((resolve) => setTimeout(resolve, 80));
1115
+ const postActionStale = executionStale();
1116
+ if (postActionStale)
1117
+ return postActionStale;
748
1118
  if (!this.actionApplied(grant, element, handle)) {
749
1119
  throw new Error('The approved value was not applied.');
750
1120
  }
751
1121
  const snapshot = this.capture();
752
1122
  const attached = element.isConnected;
753
1123
  const after = attached ? this.safeState(element) : '';
1124
+ const expanded = attached
1125
+ ? literalBooleanAttribute(element, 'aria-expanded')
1126
+ : undefined;
1127
+ const pressed = attached
1128
+ ? literalBooleanAttribute(element, 'aria-pressed')
1129
+ : undefined;
754
1130
  this.callbacks.onHighlight(attached ? element : null, 'success');
755
1131
  return {
756
1132
  ...base,
@@ -769,10 +1145,14 @@ class PageContextCollector {
769
1145
  ? 'The requested option was applied to the approved field.'
770
1146
  : grant.desiredChecked !== undefined
771
1147
  ? 'The requested checkbox state was applied.'
772
- : `${grant.action} executed on the approved ${roleFor(element)} target.`,
1148
+ : grant.desiredExpanded !== undefined || grant.desiredPressed !== undefined
1149
+ ? 'The requested button state was applied.'
1150
+ : `${grant.action} executed on the approved ${roleFor(element)} target.`,
773
1151
  ...(element instanceof HTMLInputElement && ['checkbox', 'radio'].includes(element.type)
774
1152
  ? { checked: element.checked }
775
1153
  : {}),
1154
+ ...(this.statefulButtonActionProof && expanded !== undefined ? { expanded } : {}),
1155
+ ...(this.statefulButtonActionProof && pressed !== undefined ? { pressed } : {}),
776
1156
  },
777
1157
  };
778
1158
  }
@@ -798,10 +1178,25 @@ class PageContextCollector {
798
1178
  this.pendingNavigation = null;
799
1179
  const shouldCapture = this.pendingCapture || Boolean(navigation);
800
1180
  this.pendingCapture = false;
1181
+ let navigationAcknowledged = true;
1182
+ const settlement = navigation && this.running
1183
+ ? this.beginNavigationSettlement(navigation.pageId, Math.max(navigation.revision, this.revision), navigation.url)
1184
+ : null;
801
1185
  if (navigation && this.running) {
802
- await this.callbacks.onNavigation(navigation.pageId, Math.max(navigation.revision, this.revision), navigation.url);
1186
+ try {
1187
+ navigationAcknowledged =
1188
+ (await this.callbacks.onNavigation(navigation.pageId, Math.max(navigation.revision, this.revision), navigation.url, navigation.causalActionId)) !== false;
1189
+ }
1190
+ catch {
1191
+ navigationAcknowledged = false;
1192
+ }
803
1193
  }
804
- if (this.running && (shouldCapture || !navigation)) {
1194
+ if (this.running &&
1195
+ navigationAcknowledged &&
1196
+ (!settlement || this.navigationSettlement === settlement) &&
1197
+ (shouldCapture || !navigation)) {
1198
+ if (settlement)
1199
+ this.navigationSettlement = null;
805
1200
  await this.captureAndPublish();
806
1201
  }
807
1202
  const deferred = this.deferredNavigation;
@@ -810,19 +1205,39 @@ class PageContextCollector {
810
1205
  ? deferred.toUrl
811
1206
  : null;
812
1207
  }
813
- click(element, actionId, desiredChecked, expectedDestinationPath) {
1208
+ click(element, actionId, desiredChecked, desiredExpanded, desiredPressed, expectedDestinationPath) {
814
1209
  if (!(element instanceof HTMLElement))
815
1210
  throw new Error('Target is not clickable.');
1211
+ if (desiredChecked !== undefined &&
1212
+ (desiredExpanded !== undefined || desiredPressed !== undefined)) {
1213
+ throw new Error('Conflicting desired-state contracts are not allowed.');
1214
+ }
1215
+ const desiredStates = [];
816
1216
  if (desiredChecked !== undefined) {
817
1217
  if (!(element instanceof HTMLInputElement) || !['checkbox', 'radio'].includes(element.type)) {
818
1218
  throw new Error('Desired checked state requires a checkbox or radio target.');
819
1219
  }
820
- if (element.checked === desiredChecked)
821
- return;
822
- if (element.type === 'radio' && !desiredChecked) {
1220
+ desiredStates.push(element.checked === desiredChecked);
1221
+ if (element.type === 'radio' && !desiredChecked && element.checked !== desiredChecked) {
823
1222
  throw new Error('A radio target cannot be cleared by clicking it.');
824
1223
  }
825
1224
  }
1225
+ if (desiredExpanded !== undefined) {
1226
+ const expanded = literalBooleanAttribute(element, 'aria-expanded');
1227
+ if (roleFor(element) !== 'button' || expanded === undefined) {
1228
+ throw new Error('Desired expanded state requires an expandable button target.');
1229
+ }
1230
+ desiredStates.push(expanded === desiredExpanded);
1231
+ }
1232
+ if (desiredPressed !== undefined) {
1233
+ const pressed = literalBooleanAttribute(element, 'aria-pressed');
1234
+ if (roleFor(element) !== 'button' || pressed === undefined) {
1235
+ throw new Error('Desired pressed state requires a pressed button target.');
1236
+ }
1237
+ desiredStates.push(pressed === desiredPressed);
1238
+ }
1239
+ if (desiredStates.length > 0 && desiredStates.every(Boolean))
1240
+ return;
826
1241
  if (element instanceof HTMLAnchorElement) {
827
1242
  const target = new URL(element.href, window.location.href);
828
1243
  if (expectedDestinationPath !== undefined &&
@@ -862,6 +1277,8 @@ class PageContextCollector {
862
1277
  type(element, value) {
863
1278
  if (value.length > 2000)
864
1279
  throw new Error('Value is too long.');
1280
+ if (!enabled(element))
1281
+ throw new Error('Text target is not editable.');
865
1282
  if (element instanceof HTMLInputElement) {
866
1283
  if (!['text', 'search', 'email', 'tel', 'url', 'number'].includes(element.type)) {
867
1284
  throw new Error('Input type is not supported.');
@@ -899,14 +1316,24 @@ class PageContextCollector {
899
1316
  element.dispatchEvent(new Event('change', { bubbles: true }));
900
1317
  }
901
1318
  select(element, handle, optionValueId) {
902
- if (!(element instanceof HTMLSelectElement) || !optionValueId) {
1319
+ if (!(element instanceof HTMLSelectElement) || !optionValueId || !enabled(element)) {
903
1320
  throw new Error('Select target is invalid.');
904
1321
  }
905
1322
  const option = handle.options.get(optionValueId);
906
- if (!option || option.disabled || !option.isConnected) {
1323
+ if (!option ||
1324
+ optionDisabled(option) ||
1325
+ !option.isConnected ||
1326
+ option.closest('select') !== element) {
907
1327
  throw new Error('Select option is unavailable.');
908
1328
  }
909
- element.value = option.value;
1329
+ const selectedIndexSetter = Object.getOwnPropertyDescriptor(HTMLSelectElement.prototype, 'selectedIndex')?.set;
1330
+ if (!selectedIndexSetter) {
1331
+ throw new Error('Select option cannot be updated safely.');
1332
+ }
1333
+ selectedIndexSetter.call(element, option.index);
1334
+ if (element.options[element.selectedIndex] !== option || !option.selected) {
1335
+ throw new Error('Select option could not be applied.');
1336
+ }
910
1337
  element.dispatchEvent(new Event('input', { bubbles: true }));
911
1338
  element.dispatchEvent(new Event('change', { bubbles: true }));
912
1339
  }
@@ -928,6 +1355,7 @@ class PageContextCollector {
928
1355
  connected: element.isConnected,
929
1356
  disabled: !enabled(element),
930
1357
  expanded: element.getAttribute('aria-expanded'),
1358
+ pressed: element.getAttribute('aria-pressed'),
931
1359
  checked: element instanceof HTMLInputElement && ['checkbox', 'radio'].includes(element.type)
932
1360
  ? element.checked
933
1361
  : undefined,
@@ -941,6 +1369,17 @@ class PageContextCollector {
941
1369
  ['checkbox', 'radio'].includes(element.type) &&
942
1370
  element.checked === grant.desiredChecked);
943
1371
  }
1372
+ if (grant.desiredExpanded !== undefined &&
1373
+ literalBooleanAttribute(element, 'aria-expanded') !== grant.desiredExpanded) {
1374
+ return false;
1375
+ }
1376
+ if (grant.desiredPressed !== undefined &&
1377
+ literalBooleanAttribute(element, 'aria-pressed') !== grant.desiredPressed) {
1378
+ return false;
1379
+ }
1380
+ if (grant.desiredExpanded !== undefined || grant.desiredPressed !== undefined) {
1381
+ return true;
1382
+ }
944
1383
  if (grant.action === 'type') {
945
1384
  if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
946
1385
  return element.value === (grant.value ?? '');
@@ -952,7 +1391,13 @@ class PageContextCollector {
952
1391
  }
953
1392
  if (grant.action === 'select') {
954
1393
  const option = grant.optionValueId ? handle.options.get(grant.optionValueId) : undefined;
955
- return (element instanceof HTMLSelectElement && Boolean(option) && element.value === option?.value);
1394
+ if (!(element instanceof HTMLSelectElement) || !option)
1395
+ return false;
1396
+ return (option.isConnected &&
1397
+ option.closest('select') === element &&
1398
+ !optionDisabled(option) &&
1399
+ element.options[element.selectedIndex] === option &&
1400
+ option.selected);
956
1401
  }
957
1402
  return true;
958
1403
  }
@@ -973,6 +1418,8 @@ class PageContextCollector {
973
1418
  };
974
1419
  }
975
1420
  async captureAndPublishIfChanged() {
1421
+ if (this.navigationSettlement)
1422
+ return;
976
1423
  const snapshot = this.captureSnapshot(false);
977
1424
  if (snapshot && this.running)
978
1425
  await this.callbacks.onSnapshot(snapshot);
@@ -1046,6 +1493,54 @@ class PageContextCollector {
1046
1493
  capturedAt: new Date().toISOString(),
1047
1494
  };
1048
1495
  }
1496
+ beginNavigationSettlement(pageId, revision, url) {
1497
+ const settlement = {
1498
+ generation: ++this.navigationSettlementGeneration,
1499
+ pageId,
1500
+ revision,
1501
+ url,
1502
+ };
1503
+ this.navigationSettlement = settlement;
1504
+ if (this.captureTimer)
1505
+ clearTimeout(this.captureTimer);
1506
+ this.captureTimer = null;
1507
+ if (this.mutationCaptureTimer)
1508
+ clearTimeout(this.mutationCaptureTimer);
1509
+ this.mutationCaptureTimer = null;
1510
+ return settlement;
1511
+ }
1512
+ observeCausalNavigationAfterMutation(actionId, destinationPath, beforeActionUrl) {
1513
+ const currentUrl = safePageUrl();
1514
+ if (currentUrl === beforeActionUrl ||
1515
+ window.location.search !== '' ||
1516
+ window.location.hash !== '' ||
1517
+ window.location.pathname !== destinationPath) {
1518
+ return;
1519
+ }
1520
+ if (this.pendingNavigation?.causalActionId !== actionId ||
1521
+ this.pendingNavigation.url !== currentUrl) {
1522
+ this.pageId = id('page');
1523
+ this.revision += 1;
1524
+ this.targets.clear();
1525
+ this.recentSelection = null;
1526
+ this.pendingNavigation = {
1527
+ pageId: this.pageId,
1528
+ revision: this.revision,
1529
+ url: currentUrl,
1530
+ causalActionId: actionId,
1531
+ causalObservationReported: false,
1532
+ };
1533
+ }
1534
+ this.pendingCapture = true;
1535
+ this.reportCausalNavigationObservation();
1536
+ }
1537
+ reportCausalNavigationObservation() {
1538
+ const navigation = this.pendingNavigation;
1539
+ if (!navigation || navigation.causalObservationReported)
1540
+ return;
1541
+ navigation.causalObservationReported = true;
1542
+ this.callbacks.onCausalNavigationObserved?.(navigation.pageId, navigation.revision, navigation.url, navigation.causalActionId);
1543
+ }
1049
1544
  installNavigationHooks() {
1050
1545
  this.originalPushState = history.pushState;
1051
1546
  this.originalReplaceState = history.replaceState;