@oxvo/ai-live-assist 7.5.0 → 7.5.6

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/cjs/media.d.ts CHANGED
@@ -36,6 +36,7 @@ export declare class RealtimeMedia {
36
36
  connect(result: LaunchResult, mode: AssistMode, signal: AbortSignal, offerIdempotencyKey?: string, expectedSessionState?: "active_text" | "active_voice"): Promise<number>;
37
37
  setMuted(muted: boolean): void;
38
38
  get isMuted(): boolean;
39
+ get isConnected(): boolean;
39
40
  setAssistantPlaybackAudible(audible: boolean): void;
40
41
  setManualTurn(manual: boolean): void;
41
42
  beginPushToTalk(): void;
package/cjs/media.js CHANGED
@@ -137,6 +137,9 @@ class RealtimeMedia {
137
137
  get isMuted() {
138
138
  return this.muted;
139
139
  }
140
+ get isConnected() {
141
+ return !this.closed && this.connection?.connectionState === "connected";
142
+ }
140
143
  setAssistantPlaybackAudible(audible) {
141
144
  if (this.closed)
142
145
  return;
package/cjs/types.d.ts CHANGED
@@ -331,6 +331,7 @@ export type SanitizedTarget = {
331
331
  inputType?: string;
332
332
  autocomplete?: string;
333
333
  semanticRegion?: string;
334
+ navigationMenu?: true;
334
335
  destinationPath?: string;
335
336
  sensitivity: Sensitivity;
336
337
  protectedRegion?: boolean;
@@ -356,6 +357,7 @@ export type PageContextSnapshot = {
356
357
  pageId: string;
357
358
  revision: number;
358
359
  url: string;
360
+ locationHasQuery?: boolean;
359
361
  title: string;
360
362
  locale: string;
361
363
  viewport: {
package/cjs/ui.js CHANGED
@@ -6793,7 +6793,9 @@ class WidgetView {
6793
6793
  this.setActionStateRunning(status === "running");
6794
6794
  if (this.actionStateRunning) {
6795
6795
  this.clearAssistantReadyVisualTimer();
6796
+ const previousState = this.assistantState;
6796
6797
  this.assistantState = "thinking";
6798
+ this.updateRemoteAudioEvidenceExpectation(previousState, "thinking");
6797
6799
  const assistantCaption = this.captions.querySelector('[data-caption="assistant-partial"]');
6798
6800
  if (!assistantCaption || assistantCaption.classList.contains("pending")) {
6799
6801
  this.ensurePendingCaption("assistant", this.messages.thinking);
package/cjs/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "7.5.0";
1
+ export declare const VERSION = "7.5.6";
package/cjs/version.js CHANGED
@@ -1,4 +1,4 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.VERSION = void 0;
4
- exports.VERSION = '7.5.0';
4
+ exports.VERSION = '7.5.6';
@@ -34,7 +34,7 @@ export declare const normalizeAiGuideOpenInput: (input?: AiGuideOpenInput) => Re
34
34
  export declare const resolveAiGuideProtocolVersion: (bootstrap: Pick<BootstrapConfig, "experienceMode" | "minimumProtocolVersion" | "protocol">) => 1 | 2;
35
35
  export default class AiLiveAssist {
36
36
  private readonly options;
37
- readonly version = "7.5.0";
37
+ readonly version = "7.5.6";
38
38
  private readonly host;
39
39
  private readonly guideAdmission;
40
40
  private readonly standaloneAdmissionPending;
@@ -627,6 +627,7 @@ export default class AiLiveAssist {
627
627
  private submitFeedback;
628
628
  private retry;
629
629
  private scheduleReconnect;
630
+ private canRecoverControlTransport;
630
631
  private reconnectPendingHandoffControlInMemory;
631
632
  private reconnectMessengerTerminalControl;
632
633
  private clearReconnectTimer;
@@ -7937,7 +7937,28 @@ export default class AiLiveAssist {
7937
7937
  ? this.navigationConsentResetReady
7938
7938
  : null;
7939
7939
  const resumeRecordAtConnect = this.readResume();
7940
- const channel = new ControlChannel(result, this.tabId, VERSION, () => this.scheduleReconnect(true), this.readResume()?.clientSequence ?? 0, (sequence) => this.updateResumeSequence(sequence), () => this.handleControlSequenceGap());
7940
+ let recoveryPath = null;
7941
+ const channel = new ControlChannel(result, this.tabId, VERSION, () => this.scheduleReconnect(true), this.readResume()?.clientSequence ?? 0, (sequence) => this.updateResumeSequence(sequence), () => this.handleControlSequenceGap(), {
7942
+ canRecover: () => {
7943
+ const currentPath = path();
7944
+ if (!this.canRecoverControlTransport(channel, result, media) ||
7945
+ (recoveryPath !== null && recoveryPath !== currentPath))
7946
+ return false;
7947
+ recoveryPath = currentPath;
7948
+ return true;
7949
+ },
7950
+ onRecovered: async () => {
7951
+ assertCurrent();
7952
+ const fencingToken = channel.leaseToken;
7953
+ channel.startHeartbeat(this.heartbeatSeconds);
7954
+ await this.replayPreFenceAuthorityEnvelopes(channel, fencingToken, assertCurrent);
7955
+ assertCurrent();
7956
+ await this.context?.captureAndPublish();
7957
+ assertCurrent();
7958
+ this.flushPlaybackState();
7959
+ this.reapplyConservativePreFencePresence(channel, fencingToken);
7960
+ },
7961
+ });
7941
7962
  const pauseHelloStateSignal = deferredSignal();
7942
7963
  (this.pauseHelloStateSignals ?? (this.pauseHelloStateSignals = new WeakMap())).set(channel, pauseHelloStateSignal);
7943
7964
  this.handoffContextPromptReceiptChannel = null;
@@ -8307,7 +8328,7 @@ export default class AiLiveAssist {
8307
8328
  this.context === context;
8308
8329
  context = new PageContextCollector({
8309
8330
  onSnapshot: (snapshot) => {
8310
- if (isCurrent() && this.channel?.leaseToken) {
8331
+ if (isCurrent() && this.channel?.hasFencedAuthority) {
8311
8332
  this.send("context.snapshot", snapshot);
8312
8333
  }
8313
8334
  },
@@ -9666,7 +9687,15 @@ export default class AiLiveAssist {
9666
9687
  this.activePageAssistanceAuthorityEpoch === null &&
9667
9688
  this.activeActionAuthorityEpoch === null;
9668
9689
  if (!exactApplied || !localAuthorityInactive) {
9669
- this.invalidatePageAssistanceAuthority(false);
9690
+ // A resumed document still owes the original action's terminal result.
9691
+ // Preserve only its grantless outbox, never browser-control authority.
9692
+ // Delivery remains fenced by the server's exact already_started receipt.
9693
+ const preserveNavigationResult = source === "navigation_resume" &&
9694
+ this.awaitingNavigationConsentReset &&
9695
+ structurallyValid &&
9696
+ localAuthorityInactive &&
9697
+ this.pendingNavigationActionResult?.grant === null;
9698
+ this.invalidatePageAssistanceAuthority(false, preserveNavigationResult);
9670
9699
  }
9671
9700
  if (!structurallyValid ||
9672
9701
  identity.authorityEpoch < this.minimumPageAssistanceAuthorityEpoch) {
@@ -11989,15 +12018,19 @@ export default class AiLiveAssist {
11989
12018
  // intentionally result-less until navigation settles, preserving the
11990
12019
  // cross-document recovery contract if this click unloads the page.
11991
12020
  pendingExecution.result = deliveryResult;
12021
+ let navigationReleased = false;
11992
12022
  try {
11993
- if (await this.navigateTo(deferredNavigation)) {
11994
- return deliveryResult;
11995
- }
11996
- deliveryResult = this.markDeferredNavigationUnconfirmed(pendingExecution, context.currentRevision);
12023
+ navigationReleased = await this.navigateTo(deferredNavigation);
11997
12024
  }
11998
12025
  catch {
11999
- deliveryResult = this.markDeferredNavigationUnconfirmed(pendingExecution, context.currentRevision);
12026
+ // A rejected router and an outgoing document both settle here.
12000
12027
  }
12028
+ // Pagehide stops the old collector and settles its pending router
12029
+ // callback. It must not replace the destination document's grantless
12030
+ // recovery record with an unconfirmed result from the outgoing page.
12031
+ if (navigationReleased || !authorityIsCurrent())
12032
+ return deliveryResult;
12033
+ deliveryResult = this.markDeferredNavigationUnconfirmed(pendingExecution, context.currentRevision);
12001
12034
  }
12002
12035
  else if (deferredNavigation) {
12003
12036
  deliveryResult = this.markDeferredNavigationUnconfirmed(pendingExecution, context.currentRevision);
@@ -14661,6 +14694,21 @@ export default class AiLiveAssist {
14661
14694
  }, delay + Math.floor(Math.random() * 250));
14662
14695
  this.reconnectTimer = timer;
14663
14696
  }
14697
+ canRecoverControlTransport(channel, result, media) {
14698
+ return (this.channel === channel &&
14699
+ this.result === result &&
14700
+ this.media === media && media.isConnected && navigator.onLine &&
14701
+ !this.ended && !this.destroyed && !this.terminalRequestPending &&
14702
+ !this.reconnecting && !this.launchInFlight && !this.isVoicePresence() &&
14703
+ (this.lifecyclePhase === "active_text" || this.lifecyclePhase === "active_voice") &&
14704
+ !this.pendingAction && !this.pendingConfirmation && !this.pendingPageAssistance &&
14705
+ !this.pendingNavigationActionResult && !this.legacyActionExecution &&
14706
+ this.actionGrantInFlight.size === 0 &&
14707
+ !this.pendingModeUpgrade && !this.pendingTextModeTransition &&
14708
+ !this.messengerTransferPending && !this.messengerTransferred &&
14709
+ !this.handoffRequestLifecycleOwner && !this.readResume()?.pauseCommand &&
14710
+ this.modeUpgradePathIsCurrent(path()));
14711
+ }
14664
14712
  async reconnectPendingHandoffControlInMemory(result, record, clientSequence, fencingToken, reconnectGeneration) {
14665
14713
  if (reconnectGeneration !== this.reconnectGeneration ||
14666
14714
  this.reconnecting ||
package/lib/context.d.ts CHANGED
@@ -23,6 +23,7 @@ export declare class PageContextCollector {
23
23
  private mutationObserver;
24
24
  private captureTimer;
25
25
  private mutationCaptureTimer;
26
+ private mutationCaptureStartedAt;
26
27
  private targetIds;
27
28
  private pageScrollIds;
28
29
  private navigationTargetBindings;
@@ -74,6 +75,7 @@ export declare class PageContextCollector {
74
75
  private waitForScrollSettlement;
75
76
  private actionApplied;
76
77
  private clippedGeometry;
78
+ private clearMutationCapture;
77
79
  private scheduleCapture;
78
80
  private scheduleMutationCapture;
79
81
  private scheduleViewportCapture;
package/lib/context.js CHANGED
@@ -7,6 +7,7 @@ const MAX_SELECTED_TEXT = 2000;
7
7
  const SELECTION_RETENTION_MS = 120000;
8
8
  const CAPTURE_SETTLE_MS = 250;
9
9
  const MUTATION_CAPTURE_DEBOUNCE_MS = 350;
10
+ const MUTATION_CAPTURE_MAX_WAIT_MS = 1000;
10
11
  const SCROLL_SETTLEMENT_TIMEOUT_MS = 2000;
11
12
  const SCROLL_SETTLEMENT_FRAME_FALLBACK_MS = 50;
12
13
  const SCROLL_SETTLEMENT_POSITION_TOLERANCE_PX = 1;
@@ -75,6 +76,7 @@ const navigationTargetBinding = (target) => canonicalJson({
75
76
  inputType: target.inputType ?? null,
76
77
  autocomplete: target.autocomplete ?? null,
77
78
  semanticRegion: target.semanticRegion ?? null,
79
+ ...(target.navigationMenu === true ? { navigationMenu: true } : {}),
78
80
  destinationPath: target.destinationPath ?? null,
79
81
  sensitivity: target.sensitivity,
80
82
  protectedRegion: target.protectedRegion ?? null,
@@ -119,6 +121,7 @@ const actionTargetFingerprintBinding = (target) => canonicalJson({
119
121
  inputType: target.inputType ?? null,
120
122
  autocomplete: target.autocomplete ?? null,
121
123
  semanticRegion: target.semanticRegion ?? null,
124
+ ...(target.navigationMenu === true ? { navigationMenu: true } : {}),
122
125
  destinationPath: target.destinationPath ?? null,
123
126
  sensitivity: target.sensitivity,
124
127
  protectedRegion: target.protectedRegion ?? null,
@@ -141,47 +144,94 @@ export const actionTargetFingerprint = async (target, subtle = globalThis.crypto
141
144
  }
142
145
  };
143
146
  const isHTMLElement = (value) => value instanceof HTMLElement;
147
+ // Clipping is checked in document geometry, not against the viewport: a
148
+ // rendered footer link remains discoverable before it is scrolled into view.
149
+ // No hidden descendant text is inspected to discover a navigation menu.
150
+ const clippedRect = (element) => {
151
+ const rect = element.getBoundingClientRect();
152
+ let { left, top, right, bottom } = rect;
153
+ let depth = 0;
154
+ for (let parent = element.parentElement; parent; parent = parent.parentElement) {
155
+ if (++depth > 64)
156
+ return null;
157
+ const style = window.getComputedStyle(parent);
158
+ if (Number.parseFloat(style.opacity) === 0 || style.visibility === 'hidden' || style.display === 'none' || style.contentVisibility === 'hidden')
159
+ return null;
160
+ if (parent === document.body || parent === document.documentElement)
161
+ continue;
162
+ const bounds = parent.getBoundingClientRect();
163
+ // Overflow scrolling can reveal an offscreen descendant. A collapsed or
164
+ // clipped disclosure cannot be revealed by scrolling the document to it.
165
+ if (['hidden', 'clip'].includes(style.overflowX)) {
166
+ left = Math.max(left, bounds.left);
167
+ right = Math.min(right, bounds.right);
168
+ }
169
+ if (['hidden', 'clip'].includes(style.overflowY)) {
170
+ top = Math.max(top, bounds.top);
171
+ bottom = Math.min(bottom, bounds.bottom);
172
+ }
173
+ if (right <= left || bottom <= top)
174
+ return null;
175
+ }
176
+ return right > left && bottom > top ? { left, top, right, bottom } : null;
177
+ };
144
178
  const isRendered = (element) => {
145
- if (!isHTMLElement(element) || !element.isConnected || element.closest(PRIVATE_SELECTOR)) {
179
+ if (!isHTMLElement(element) || !element.isConnected || element.closest(PRIVATE_SELECTOR))
146
180
  return false;
147
- }
148
181
  const style = window.getComputedStyle(element);
149
- const opacity = Number.parseFloat(style.opacity);
150
- if (style.display === 'none' ||
151
- style.visibility === 'hidden' ||
152
- (Number.isFinite(opacity) && opacity === 0) ||
153
- style.pointerEvents === 'none') {
182
+ if (style.display === 'none' || style.visibility === 'hidden' ||
183
+ Number.parseFloat(style.opacity) === 0 || style.pointerEvents === 'none')
154
184
  return false;
155
- }
156
- const rect = element.getBoundingClientRect();
157
- return rect.width > 0 && rect.height > 0;
185
+ return clippedRect(element) !== null;
158
186
  };
159
- const isVisible = (element) => {
187
+ const visibleRect = (element) => {
160
188
  if (!isRendered(element))
161
- return false;
162
- const rect = element.getBoundingClientRect();
163
- return (rect.bottom >= 0 &&
164
- rect.right >= 0 &&
165
- rect.top <= window.innerHeight &&
166
- rect.left <= window.innerWidth);
189
+ return null;
190
+ const rect = clippedRect(element);
191
+ if (!rect)
192
+ return null;
193
+ const left = Math.max(0, rect.left), top = Math.max(0, rect.top);
194
+ const right = Math.min(window.innerWidth, rect.right), bottom = Math.min(window.innerHeight, rect.bottom);
195
+ return right > left && bottom > top ? { left, top, right, bottom } : null;
167
196
  };
168
- const isCovered = (element, trustedGuideRoot = null) => {
169
- const rect = element.getBoundingClientRect();
170
- const x = Math.max(0, Math.min(window.innerWidth - 1, rect.left + rect.width / 2));
171
- const y = Math.max(0, Math.min(window.innerHeight - 1, rect.top + rect.height / 2));
172
- let top;
173
- if (trustedGuideRoot?.isConnected &&
174
- typeof document.elementsFromPoint === 'function') {
175
- top =
176
- document
177
- .elementsFromPoint(x, y)
178
- .find((candidate) => candidate !== trustedGuideRoot) ??
179
- document.elementFromPoint(x, y);
180
- }
181
- else {
182
- top = document.elementFromPoint(x, y);
183
- }
184
- return Boolean(top && top !== element && !element.contains(top) && !top.contains(element));
197
+ const isVisible = (element) => visibleRect(element) !== null;
198
+ const interactionPoint = (element, trustedGuideRoot = null) => {
199
+ const rect = visibleRect(element);
200
+ if (!rect)
201
+ return null;
202
+ // Five bounded hit tests permit an exposed part of a link without treating
203
+ // an ancestor, empty hit, or a covering sibling as an interactable target.
204
+ const insetX = Math.min(1, (rect.right - rect.left) / 4);
205
+ const insetY = Math.min(1, (rect.bottom - rect.top) / 4);
206
+ const points = [
207
+ [(rect.left + rect.right) / 2, (rect.top + rect.bottom) / 2],
208
+ [rect.left + insetX, rect.top + insetY], [rect.right - insetX, rect.top + insetY],
209
+ [rect.left + insetX, rect.bottom - insetY], [rect.right - insetX, rect.bottom - insetY],
210
+ ];
211
+ for (const [x, y] of points) {
212
+ const top = trustedGuideRoot?.isConnected && typeof document.elementsFromPoint === 'function'
213
+ ? document.elementsFromPoint(x, y).find(candidate => candidate !== trustedGuideRoot) ?? null
214
+ : document.elementFromPoint(x, y);
215
+ if (top !== null && (top === element || element.contains(top)))
216
+ return { x: x, y: y, hit: top };
217
+ }
218
+ return null;
219
+ };
220
+ const isCovered = (element, trustedGuideRoot = null) => interactionPoint(element, trustedGuideRoot) === null;
221
+ const isNavigationMenu = (element, role) => {
222
+ if (role !== 'button' || literalBooleanAttribute(element, 'aria-expanded') === undefined ||
223
+ element.hasAttribute('aria-pressed') || element.hasAttribute('aria-checked') ||
224
+ element instanceof HTMLButtonElement && element.form !== null && element.type !== 'button')
225
+ return false;
226
+ if (element.closest('nav,[role="navigation"]'))
227
+ return true;
228
+ // aria-controls supplies identity only. Do not capture hidden menu labels,
229
+ // links, values or instructions before native expansion and recapture.
230
+ const controls = (element.getAttribute('aria-controls') ?? '').trim().split(/\s+/).slice(0, 4);
231
+ return controls.some(control => {
232
+ const menu = document.getElementById(control);
233
+ return menu !== null && menu.matches('nav,[role="navigation"],[role="menu"]');
234
+ });
185
235
  };
186
236
  const safePageUrl = () => `${window.location.origin}${window.location.pathname}`;
187
237
  const linkBrowsingTarget = (link) => (link.getAttribute('target') ?? document.querySelector('base[target]')?.getAttribute('target') ?? '').toLowerCase();
@@ -193,11 +243,13 @@ const sameNavigationDestination = (actual, expected) => {
193
243
  return left.origin === right.origin && left.pathname === right.pathname && left.search === right.search;
194
244
  };
195
245
  const safeDestinationPath = (element) => {
196
- if (!(element instanceof HTMLAnchorElement))
246
+ // An empty href is a control, not authority to navigate to the current URL.
247
+ if (!(element instanceof HTMLAnchorElement) || !element.getAttribute('href')?.trim())
197
248
  return null;
198
249
  try {
199
250
  const destination = new URL(element.href, window.location.href);
200
251
  if (destination.origin !== window.location.origin ||
252
+ destination.username !== '' || destination.password !== '' ||
201
253
  destination.search ||
202
254
  destination.hash ||
203
255
  !destination.pathname.startsWith('/') ||
@@ -440,15 +492,17 @@ const checkedStateFor = (element) => {
440
492
  }
441
493
  return framerToggleDescriptor(element)?.checked;
442
494
  };
443
- const dispatchFramerPrimaryPointerSequence = (element) => {
495
+ const dispatchFramerPrimaryPointerSequence = (element, trustedGuideRoot = null) => {
444
496
  if (typeof window.PointerEvent !== 'function') {
445
497
  throw new Error('Framer pointer interaction is unavailable.');
446
498
  }
447
- const rect = element.getBoundingClientRect();
448
- const clientX = rect.left + rect.width / 2;
449
- const clientY = rect.top + rect.height / 2;
450
- const hit = document.elementFromPoint(clientX, clientY);
451
- const eventTarget = element instanceof HTMLAnchorElement && hit && element.contains(hit) ? hit : element;
499
+ // Capture and dispatch must agree on the exposed point and exact descendant,
500
+ // including when our own mobile surface covers a link's router-owned logo.
501
+ const point = interactionPoint(element, trustedGuideRoot);
502
+ if (!point)
503
+ throw new Error('The approved pointer target is covered.');
504
+ const { x: clientX, y: clientY } = point;
505
+ const eventTarget = element instanceof HTMLAnchorElement ? point.hit : element;
452
506
  const nestedLink = eventTarget.closest('a[href]');
453
507
  if (element instanceof HTMLAnchorElement && nestedLink instanceof HTMLAnchorElement && nestedLink !== element &&
454
508
  (!sameNavigationDestination(nestedLink.href, element.href) || nestedLink.hasAttribute('download') ||
@@ -520,11 +574,11 @@ const dispatchFramerPrimaryPointerSequence = (element) => {
520
574
  detail: 1,
521
575
  }));
522
576
  };
523
- const dispatchFramerTogglePrimaryPointer = (element) => {
577
+ const dispatchFramerTogglePrimaryPointer = (element, trustedGuideRoot = null) => {
524
578
  if (!framerToggleDescriptor(element)) {
525
579
  throw new Error('Framer toggle pointer interaction is unavailable.');
526
580
  }
527
- dispatchFramerPrimaryPointerSequence(element);
581
+ dispatchFramerPrimaryPointerSequence(element, trustedGuideRoot);
528
582
  };
529
583
  const semanticSourceSensitivity = (source) => {
530
584
  if (source.closest(PRIVATE_SELECTOR) || source.querySelector(PRIVATE_SELECTOR)) {
@@ -694,6 +748,7 @@ export class PageContextCollector {
694
748
  this.mutationObserver = null;
695
749
  this.captureTimer = null;
696
750
  this.mutationCaptureTimer = null;
751
+ this.mutationCaptureStartedAt = null;
697
752
  this.targetIds = new WeakMap();
698
753
  this.pageScrollIds = new WeakMap();
699
754
  this.navigationTargetBindings = new WeakMap();
@@ -721,9 +776,7 @@ export class PageContextCollector {
721
776
  this.pendingCapture = true;
722
777
  return;
723
778
  }
724
- if (this.mutationCaptureTimer)
725
- clearTimeout(this.mutationCaptureTimer);
726
- this.mutationCaptureTimer = null;
779
+ this.clearMutationCapture();
727
780
  if (this.captureTimer)
728
781
  return;
729
782
  this.captureTimer = setTimeout(() => {
@@ -740,12 +793,19 @@ export class PageContextCollector {
740
793
  }
741
794
  if (this.captureTimer)
742
795
  return;
796
+ // Decorative animation must not indefinitely hide newly available links.
797
+ // Retain trailing-edge batching, with at most one forced scan per second
798
+ // during continuous churn. Unchanged semantic snapshots are still deduped.
799
+ const now = performance.now();
800
+ this.mutationCaptureStartedAt ?? (this.mutationCaptureStartedAt = now);
801
+ const delay = Math.max(0, Math.min(MUTATION_CAPTURE_DEBOUNCE_MS, MUTATION_CAPTURE_MAX_WAIT_MS - (now - this.mutationCaptureStartedAt)));
743
802
  if (this.mutationCaptureTimer)
744
803
  clearTimeout(this.mutationCaptureTimer);
745
804
  this.mutationCaptureTimer = setTimeout(() => {
746
805
  this.mutationCaptureTimer = null;
806
+ this.mutationCaptureStartedAt = null;
747
807
  void this.captureAndPublishIfChanged();
748
- }, MUTATION_CAPTURE_DEBOUNCE_MS);
808
+ }, delay);
749
809
  };
750
810
  this.scheduleViewportCapture = () => {
751
811
  this.scheduleMutationCapture();
@@ -818,9 +878,7 @@ export class PageContextCollector {
818
878
  if (this.captureTimer)
819
879
  clearTimeout(this.captureTimer);
820
880
  this.captureTimer = null;
821
- if (this.mutationCaptureTimer)
822
- clearTimeout(this.mutationCaptureTimer);
823
- this.mutationCaptureTimer = null;
881
+ this.clearMutationCapture();
824
882
  if (this.releasedNavigationSettlement &&
825
883
  window.location.href !== this.releasedNavigationSettlement.fromUrl) {
826
884
  this.finishReleasedNavigationSettlement(sameNavigationDestination(window.location.href, this.releasedNavigationSettlement.toUrl)
@@ -951,9 +1009,7 @@ export class PageContextCollector {
951
1009
  if (this.captureTimer)
952
1010
  clearTimeout(this.captureTimer);
953
1011
  this.captureTimer = null;
954
- if (this.mutationCaptureTimer)
955
- clearTimeout(this.mutationCaptureTimer);
956
- this.mutationCaptureTimer = null;
1012
+ this.clearMutationCapture();
957
1013
  window.removeEventListener('resize', this.scheduleViewportCapture);
958
1014
  document.removeEventListener('scroll', this.scheduleViewportCapture, true);
959
1015
  window.visualViewport?.removeEventListener('resize', this.scheduleViewportCapture);
@@ -1057,6 +1113,7 @@ export class PageContextCollector {
1057
1113
  }
1058
1114
  : {}),
1059
1115
  ...(semanticRegion ? { semanticRegion } : {}),
1116
+ ...(this.statefulButtonActionProof && isNavigationMenu(element, role) ? { navigationMenu: true } : {}),
1060
1117
  ...(destinationPath ? { destinationPath } : {}),
1061
1118
  sensitivity,
1062
1119
  protectedRegion: sensitivity !== 'none' || regionAssessment.matchedRuleDigests.length > 0,
@@ -1168,6 +1225,7 @@ export class PageContextCollector {
1168
1225
  pageId: this.pageId,
1169
1226
  revision,
1170
1227
  url: safePageUrl(),
1228
+ locationHasQuery: window.location.search !== '',
1171
1229
  title: normalize(document.title, 512),
1172
1230
  locale: normalize(document.documentElement.lang || navigator.language || 'en', 16),
1173
1231
  viewport: {
@@ -1788,7 +1846,7 @@ export class PageContextCollector {
1788
1846
  };
1789
1847
  window.addEventListener('click', preventNativeNavigation);
1790
1848
  try {
1791
- dispatchFramerPrimaryPointerSequence(navigation.source);
1849
+ dispatchFramerPrimaryPointerSequence(navigation.source, this.trustedGuideRoot);
1792
1850
  }
1793
1851
  finally {
1794
1852
  window.removeEventListener('click', preventNativeNavigation);
@@ -1870,7 +1928,7 @@ export class PageContextCollector {
1870
1928
  if (desiredStates.length > 0 && desiredStates.every(Boolean))
1871
1929
  return;
1872
1930
  if (desiredChecked !== undefined && framerToggleDescriptor(element)) {
1873
- dispatchFramerTogglePrimaryPointer(element);
1931
+ dispatchFramerTogglePrimaryPointer(element, this.trustedGuideRoot);
1874
1932
  return;
1875
1933
  }
1876
1934
  if (element instanceof HTMLAnchorElement) {
@@ -2178,9 +2236,19 @@ export class PageContextCollector {
2178
2236
  height,
2179
2237
  };
2180
2238
  }
2239
+ clearMutationCapture() {
2240
+ if (this.mutationCaptureTimer)
2241
+ clearTimeout(this.mutationCaptureTimer);
2242
+ this.mutationCaptureTimer = null;
2243
+ this.mutationCaptureStartedAt = null;
2244
+ }
2181
2245
  async captureAndPublishIfChanged() {
2182
- if (this.navigationSettlement)
2246
+ if (!this.running || this.navigationSettlement)
2247
+ return;
2248
+ if (this.executingActionId) {
2249
+ this.pendingCapture = true;
2183
2250
  return;
2251
+ }
2184
2252
  const snapshot = this.captureSnapshot(false);
2185
2253
  if (snapshot && this.running)
2186
2254
  await this.callbacks.onSnapshot(snapshot);
@@ -2265,9 +2333,7 @@ export class PageContextCollector {
2265
2333
  if (this.captureTimer)
2266
2334
  clearTimeout(this.captureTimer);
2267
2335
  this.captureTimer = null;
2268
- if (this.mutationCaptureTimer)
2269
- clearTimeout(this.mutationCaptureTimer);
2270
- this.mutationCaptureTimer = null;
2336
+ this.clearMutationCapture();
2271
2337
  return settlement;
2272
2338
  }
2273
2339
  observeCausalNavigationAfterMutation(actionId, destinationPath, beforeActionUrl) {
package/lib/control.d.ts CHANGED
@@ -24,6 +24,10 @@ export type ControlOutboundOperation = {
24
24
  type: string;
25
25
  };
26
26
  export type ReservedControlOperation = Readonly<ControlOutboundOperation>;
27
+ type TransportRecovery = {
28
+ canRecover: () => boolean;
29
+ onRecovered: () => Promise<void>;
30
+ };
27
31
  export declare class ControlChannel {
28
32
  private readonly result;
29
33
  private readonly tabId;
@@ -31,6 +35,7 @@ export declare class ControlChannel {
31
35
  private readonly onDisconnect;
32
36
  private readonly onClientSequence;
33
37
  private readonly onUnsafeDisconnect;
38
+ private readonly transportRecovery?;
34
39
  private socket;
35
40
  private clientSequence;
36
41
  private serverSequence;
@@ -54,7 +59,9 @@ export declare class ControlChannel {
54
59
  private pendingConnection;
55
60
  private helloOperation;
56
61
  private fencedAuthorityConfirmed;
57
- constructor(result: RuntimeAuthorization, tabId: string, clientVersion: string, onDisconnect: () => void, initialClientSequence?: number, onClientSequence?: (sequence: number) => void, onUnsafeDisconnect?: () => void);
62
+ private transportRecoveryAttempted;
63
+ private recoveringTransport;
64
+ constructor(result: RuntimeAuthorization, tabId: string, clientVersion: string, onDisconnect: () => void, initialClientSequence?: number, onClientSequence?: (sequence: number) => void, onUnsafeDisconnect?: () => void, transportRecovery?: TransportRecovery | undefined);
58
65
  get leaseToken(): number;
59
66
  get lastClientSequence(): number;
60
67
  get isOpen(): boolean;
@@ -78,6 +85,7 @@ export declare class ControlChannel {
78
85
  private handleMessage;
79
86
  private recoverFromSequenceGap;
80
87
  private notifyUnsafeDisconnect;
88
+ private recoverTransport;
81
89
  private enqueueHandlers;
82
90
  private dispatchHandlers;
83
91
  private handleHandlerFailure;