@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/AiLiveAssist.d.ts +2 -1
- package/cjs/AiLiveAssist.js +56 -8
- package/cjs/context.d.ts +2 -0
- package/cjs/context.js +125 -59
- package/cjs/control.d.ts +9 -1
- package/cjs/control.js +81 -3
- package/cjs/liveControlChannel.d.ts +1 -1
- package/cjs/media.d.ts +1 -0
- package/cjs/media.js +3 -0
- package/cjs/types.d.ts +2 -0
- package/cjs/ui.js +2 -0
- package/cjs/version.d.ts +1 -1
- package/cjs/version.js +1 -1
- package/lib/AiLiveAssist.d.ts +2 -1
- package/lib/AiLiveAssist.js +56 -8
- package/lib/context.d.ts +2 -0
- package/lib/context.js +125 -59
- package/lib/control.d.ts +9 -1
- package/lib/control.js +81 -3
- package/lib/liveControlChannel.d.ts +1 -1
- package/lib/media.d.ts +1 -0
- package/lib/media.js +3 -0
- package/lib/types.d.ts +2 -0
- package/lib/ui.js +2 -0
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +1 -1
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.
|
|
1
|
+
export declare const VERSION = "7.5.6";
|
package/cjs/version.js
CHANGED
package/lib/AiLiveAssist.d.ts
CHANGED
|
@@ -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.
|
|
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;
|
package/lib/AiLiveAssist.js
CHANGED
|
@@ -7937,7 +7937,28 @@ export default class AiLiveAssist {
|
|
|
7937
7937
|
? this.navigationConsentResetReady
|
|
7938
7938
|
: null;
|
|
7939
7939
|
const resumeRecordAtConnect = this.readResume();
|
|
7940
|
-
|
|
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?.
|
|
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
|
-
|
|
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
|
-
|
|
11994
|
-
return deliveryResult;
|
|
11995
|
-
}
|
|
11996
|
-
deliveryResult = this.markDeferredNavigationUnconfirmed(pendingExecution, context.currentRevision);
|
|
12023
|
+
navigationReleased = await this.navigateTo(deferredNavigation);
|
|
11997
12024
|
}
|
|
11998
12025
|
catch {
|
|
11999
|
-
|
|
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
|
-
|
|
150
|
-
|
|
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
|
|
187
|
+
const visibleRect = (element) => {
|
|
160
188
|
if (!isRendered(element))
|
|
161
|
-
return
|
|
162
|
-
const rect = element
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
|
169
|
-
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
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
|
-
|
|
448
|
-
|
|
449
|
-
const
|
|
450
|
-
|
|
451
|
-
|
|
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
|
-
|
|
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
|
-
},
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|