@oxvo/ai-live-assist 7.5.8 → 7.5.10
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 +1 -1
- package/cjs/context.d.ts +3 -0
- package/cjs/context.js +190 -79
- package/cjs/liveControlChannel.d.ts +1 -1
- package/cjs/types.d.ts +8 -0
- package/cjs/ui.js +8 -9
- package/cjs/version.d.ts +1 -1
- package/cjs/version.js +1 -1
- package/cjs/voicePresenceUi.js +8 -2
- package/lib/AiLiveAssist.d.ts +1 -1
- package/lib/context.d.ts +3 -0
- package/lib/context.js +190 -79
- package/lib/liveControlChannel.d.ts +1 -1
- package/lib/types.d.ts +8 -0
- package/lib/ui.js +8 -9
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/lib/voicePresenceUi.js +8 -2
- package/package.json +1 -1
package/cjs/version.js
CHANGED
package/cjs/voicePresenceUi.js
CHANGED
|
@@ -1309,6 +1309,8 @@ class VoicePresenceView {
|
|
|
1309
1309
|
const previous = this.audio.srcObject;
|
|
1310
1310
|
const streamChanged = previous !== stream;
|
|
1311
1311
|
const analysisChanged = !this.outputAnalyserMatchesStream(stream);
|
|
1312
|
+
if (!streamChanged && !analysisChanged)
|
|
1313
|
+
return;
|
|
1312
1314
|
if (streamChanged) {
|
|
1313
1315
|
this.playbackGeneration += 1;
|
|
1314
1316
|
this.playbackResumePending = false;
|
|
@@ -1329,9 +1331,11 @@ class VoicePresenceView {
|
|
|
1329
1331
|
this.emitPlaybackState("waiting");
|
|
1330
1332
|
this.syncPresenceFromPlayback();
|
|
1331
1333
|
}
|
|
1332
|
-
|
|
1333
|
-
if (streamChanged)
|
|
1334
|
+
// Track-set changes can rebuild analysis without reloading ongoing playout.
|
|
1335
|
+
if (streamChanged) {
|
|
1336
|
+
this.audio.srcObject = stream;
|
|
1334
1337
|
this.bindPlaybackState();
|
|
1338
|
+
}
|
|
1335
1339
|
if (previous &&
|
|
1336
1340
|
typeof MediaStream !== "undefined" &&
|
|
1337
1341
|
previous instanceof MediaStream &&
|
|
@@ -1344,6 +1348,8 @@ class VoicePresenceView {
|
|
|
1344
1348
|
this.bindRemoteAudioTrackState(stream);
|
|
1345
1349
|
this.resumeOutputAudioContext();
|
|
1346
1350
|
this.startOutputEvidenceSampling();
|
|
1351
|
+
if (!streamChanged)
|
|
1352
|
+
return;
|
|
1347
1353
|
const generation = this.playbackGeneration;
|
|
1348
1354
|
void this.audio.play().catch(() => {
|
|
1349
1355
|
if (!this.playbackSourceIsCurrent(generation, stream))
|
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.10";
|
|
38
38
|
private readonly host;
|
|
39
39
|
private readonly guideAdmission;
|
|
40
40
|
private readonly standaloneAdmissionPending;
|
package/lib/context.d.ts
CHANGED
|
@@ -27,6 +27,7 @@ export declare class PageContextCollector {
|
|
|
27
27
|
private targetIds;
|
|
28
28
|
private pageScrollIds;
|
|
29
29
|
private navigationTargetBindings;
|
|
30
|
+
private navigationDisclosureLinks;
|
|
30
31
|
private fieldTargetBindings;
|
|
31
32
|
private fieldAppliedRevisions;
|
|
32
33
|
private textIds;
|
|
@@ -70,6 +71,8 @@ export declare class PageContextCollector {
|
|
|
70
71
|
private type;
|
|
71
72
|
private select;
|
|
72
73
|
private submit;
|
|
74
|
+
private exposedNavigationLinks;
|
|
75
|
+
private expandedState;
|
|
73
76
|
private safeState;
|
|
74
77
|
private scrollTargetIntoView;
|
|
75
78
|
private waitForScrollSettlement;
|
package/lib/context.js
CHANGED
|
@@ -17,7 +17,7 @@ const RELEASED_NAVIGATION_SETTLEMENT_TIMEOUT_MS = 5000;
|
|
|
17
17
|
const FRAMER_GENERATED_PAGE_SELECTOR = '[data-framer-generated-page]';
|
|
18
18
|
const FRAMER_TOGGLE_ROOT_SELECTOR = '[data-framer-name="Toggle"]';
|
|
19
19
|
const FRAMER_TOGGLE_CONTROL_SELECTOR = 'div[name="Toggle Switch"][data-highlight="true"][tabindex]';
|
|
20
|
-
const ACTIONABLE_MUTATION_SELECTOR = `a[href],button,input,select,textarea,summary,[role="button"],[role="link"],[role="tab"],[role="menuitem"],[role="option"],[role="checkbox"],[role="radio"],[role="switch"],[role="combobox"],[role="listbox"],[role="treeitem"],[role="slider"],[role="spinbutton"],[role="searchbox"],[role="textbox"],${FRAMER_TOGGLE_CONTROL_SELECTOR}`;
|
|
20
|
+
const ACTIONABLE_MUTATION_SELECTOR = `a[href],button,input,select,textarea,summary,[role="button"],[role="link"],[role="tab"],[role="menuitem"],[role="option"],[role="checkbox"],[role="radio"],[role="switch"],[role="combobox"],[role="listbox"],[role="treeitem"],[role="slider"],[role="spinbutton"],[role="searchbox"],[role="textbox"],[data-highlight="true"][tabindex],${FRAMER_TOGGLE_CONTROL_SELECTOR}`;
|
|
21
21
|
const MAX_FRAMER_TOGGLE_LABEL = 120;
|
|
22
22
|
const PRIVATE_SELECTOR = [
|
|
23
23
|
'[data-oxvo-ai-live-assist-root]',
|
|
@@ -77,6 +77,7 @@ const navigationTargetBinding = (target) => canonicalJson({
|
|
|
77
77
|
autocomplete: target.autocomplete ?? null,
|
|
78
78
|
semanticRegion: target.semanticRegion ?? null,
|
|
79
79
|
...(target.navigationMenu === true ? { navigationMenu: true } : {}),
|
|
80
|
+
...(target.navigation ? { navigation: target.navigation } : {}),
|
|
80
81
|
destinationPath: target.destinationPath ?? null,
|
|
81
82
|
sensitivity: target.sensitivity,
|
|
82
83
|
protectedRegion: target.protectedRegion ?? null,
|
|
@@ -122,6 +123,7 @@ const actionTargetFingerprintBinding = (target) => canonicalJson({
|
|
|
122
123
|
autocomplete: target.autocomplete ?? null,
|
|
123
124
|
semanticRegion: target.semanticRegion ?? null,
|
|
124
125
|
...(target.navigationMenu === true ? { navigationMenu: true } : {}),
|
|
126
|
+
...(target.navigation ? { navigation: target.navigation } : {}),
|
|
125
127
|
destinationPath: target.destinationPath ?? null,
|
|
126
128
|
sensitivity: target.sensitivity,
|
|
127
129
|
protectedRegion: target.protectedRegion ?? null,
|
|
@@ -218,8 +220,27 @@ const interactionPoint = (element, trustedGuideRoot = null) => {
|
|
|
218
220
|
return null;
|
|
219
221
|
};
|
|
220
222
|
const isCovered = (element, trustedGuideRoot = null) => interactionPoint(element, trustedGuideRoot) === null;
|
|
223
|
+
const FRAMER_NAVIGATION_CONTROL_SELECTOR = 'div[data-highlight="true"][tabindex]';
|
|
224
|
+
const framerNavigationRoot = (element) => {
|
|
225
|
+
if (!(element instanceof HTMLElement) || !element.matches(FRAMER_NAVIGATION_CONTROL_SELECTOR) ||
|
|
226
|
+
element.tabIndex < 0 || element.hasAttribute('role') || element.hasAttribute('aria-pressed') ||
|
|
227
|
+
element.hasAttribute('aria-checked') || !element.getAttribute('data-framer-name')?.trim() ||
|
|
228
|
+
!element.closest(FRAMER_GENERATED_PAGE_SELECTOR) || element.closest(`form,${PRIVATE_SELECTOR}`) ||
|
|
229
|
+
element.querySelector('a,button,input,select,textarea,summary,form,[role],[tabindex],[contenteditable]') ||
|
|
230
|
+
element.textContent?.trim())
|
|
231
|
+
return null;
|
|
232
|
+
const nav = element.closest('nav,[role="navigation"]');
|
|
233
|
+
if (!nav)
|
|
234
|
+
return null;
|
|
235
|
+
const candidates = nav.querySelectorAll(FRAMER_NAVIGATION_CONTROL_SELECTOR);
|
|
236
|
+
if (candidates.length > MAX_TARGETS)
|
|
237
|
+
return null;
|
|
238
|
+
const rendered = Array.from(candidates).filter(candidate => isRendered(candidate) &&
|
|
239
|
+
candidate.closest('nav,[role="navigation"]') === nav);
|
|
240
|
+
return rendered.length === 1 && rendered[0] === element ? nav : null;
|
|
241
|
+
};
|
|
221
242
|
const isNavigationMenu = (element, role) => {
|
|
222
|
-
if (role !== 'button' || literalBooleanAttribute(element, 'aria-expanded') === undefined ||
|
|
243
|
+
if (role !== 'button' || (literalBooleanAttribute(element, 'aria-expanded') === undefined && !framerNavigationRoot(element)) ||
|
|
223
244
|
element.hasAttribute('aria-pressed') || element.hasAttribute('aria-checked') ||
|
|
224
245
|
element instanceof HTMLButtonElement && element.form !== null && element.type !== 'button')
|
|
225
246
|
return false;
|
|
@@ -242,27 +263,31 @@ const sameNavigationDestination = (actual, expected) => {
|
|
|
242
263
|
const left = new URL(actual), right = new URL(expected);
|
|
243
264
|
return left.origin === right.origin && left.pathname === right.pathname && left.search === right.search;
|
|
244
265
|
};
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
return
|
|
266
|
+
const publicNavigation = (element) => {
|
|
267
|
+
if (!(element instanceof HTMLAnchorElement) || !element.getAttribute('href')?.trim() ||
|
|
268
|
+
!supportsCurrentTabNavigation(element))
|
|
269
|
+
return undefined;
|
|
249
270
|
try {
|
|
250
271
|
const destination = new URL(element.href, window.location.href);
|
|
251
|
-
if (
|
|
252
|
-
destination.
|
|
253
|
-
destination.search ||
|
|
254
|
-
destination.
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
return null;
|
|
259
|
-
}
|
|
260
|
-
return destination.pathname;
|
|
272
|
+
if (!['http:', 'https:'].includes(destination.protocol) ||
|
|
273
|
+
(destination.origin !== window.location.origin && destination.protocol !== 'https:') ||
|
|
274
|
+
destination.username || destination.password || destination.search || destination.hash ||
|
|
275
|
+
destination.href.length > 2048 || destination.pathname.length > 1024 ||
|
|
276
|
+
!/^\/(?:[A-Za-z0-9_~-]+(?:\.[A-Za-z0-9_~-]+)*\/?)*$/.test(destination.pathname))
|
|
277
|
+
return undefined;
|
|
278
|
+
return { url: destination.href, target: linkBrowsingTarget(element) === '_blank' ? '_blank' : '_self' };
|
|
261
279
|
}
|
|
262
280
|
catch {
|
|
263
|
-
return
|
|
281
|
+
return undefined;
|
|
264
282
|
}
|
|
265
283
|
};
|
|
284
|
+
const safeDestinationPath = (element) => {
|
|
285
|
+
const navigation = publicNavigation(element);
|
|
286
|
+
if (!navigation)
|
|
287
|
+
return null;
|
|
288
|
+
const destination = new URL(navigation.url);
|
|
289
|
+
return destination.origin === window.location.origin ? destination.pathname : null;
|
|
290
|
+
};
|
|
266
291
|
const sensitivityFor = (element, name) => {
|
|
267
292
|
const input = element instanceof HTMLInputElement ? element : null;
|
|
268
293
|
const type = input?.type.toLowerCase() ?? '';
|
|
@@ -296,6 +321,8 @@ const sensitivityFor = (element, name) => {
|
|
|
296
321
|
const roleFor = (element) => {
|
|
297
322
|
if (framerToggleDescriptor(element))
|
|
298
323
|
return 'checkbox';
|
|
324
|
+
if (framerNavigationRoot(element))
|
|
325
|
+
return 'button';
|
|
299
326
|
const explicit = normalize(element.getAttribute('role') ?? '', 64);
|
|
300
327
|
if (explicit)
|
|
301
328
|
return explicit;
|
|
@@ -363,6 +390,7 @@ const nameFor = (element) => {
|
|
|
363
390
|
return normalize(semanticName ||
|
|
364
391
|
associatedLabel ||
|
|
365
392
|
attributeName ||
|
|
393
|
+
(framerNavigationRoot(element) ? safeAttribute('data-framer-name', 120) : '') ||
|
|
366
394
|
(buttonValue && sensitivityFor(element, buttonValue) === 'none' ? buttonValue : '') ||
|
|
367
395
|
(safeDestinationPath(element) === '/' ? 'Home' : ''), 300);
|
|
368
396
|
};
|
|
@@ -764,6 +792,7 @@ export class PageContextCollector {
|
|
|
764
792
|
this.targetIds = new WeakMap();
|
|
765
793
|
this.pageScrollIds = new WeakMap();
|
|
766
794
|
this.navigationTargetBindings = new WeakMap();
|
|
795
|
+
this.navigationDisclosureLinks = new WeakMap();
|
|
767
796
|
this.fieldTargetBindings = new WeakMap();
|
|
768
797
|
this.fieldAppliedRevisions = new WeakMap();
|
|
769
798
|
this.textIds = new WeakMap();
|
|
@@ -987,6 +1016,9 @@ export class PageContextCollector {
|
|
|
987
1016
|
'data-framer-name',
|
|
988
1017
|
'disabled',
|
|
989
1018
|
'href',
|
|
1019
|
+
'target',
|
|
1020
|
+
'download',
|
|
1021
|
+
'data-highlight',
|
|
990
1022
|
'hidden',
|
|
991
1023
|
'inert',
|
|
992
1024
|
'role',
|
|
@@ -1033,6 +1065,7 @@ export class PageContextCollector {
|
|
|
1033
1065
|
document.removeEventListener('change', this.handleControlStateChange, true);
|
|
1034
1066
|
this.restoreNavigationHooks();
|
|
1035
1067
|
this.targets.clear();
|
|
1068
|
+
this.navigationDisclosureLinks = new WeakMap();
|
|
1036
1069
|
this.executingActionId = null;
|
|
1037
1070
|
this.pendingNavigation = null;
|
|
1038
1071
|
this.pendingCapture = false;
|
|
@@ -1073,7 +1106,8 @@ export class PageContextCollector {
|
|
|
1073
1106
|
continue;
|
|
1074
1107
|
const role = roleFor(element);
|
|
1075
1108
|
const hasStatefulButtonAttribute = role === 'button' &&
|
|
1076
|
-
(element.hasAttribute('aria-expanded') || element.hasAttribute('aria-pressed')
|
|
1109
|
+
(element.hasAttribute('aria-expanded') || element.hasAttribute('aria-pressed') ||
|
|
1110
|
+
Boolean(framerNavigationRoot(element)));
|
|
1077
1111
|
const hasUnsupportedMixedPressedState = role === 'button' &&
|
|
1078
1112
|
element.getAttribute('aria-pressed')?.trim().toLowerCase() === 'mixed';
|
|
1079
1113
|
if (hasUnsupportedMixedPressedState ||
|
|
@@ -1110,7 +1144,8 @@ export class PageContextCollector {
|
|
|
1110
1144
|
: accessibleNameSourceSensitivity(element);
|
|
1111
1145
|
const visible = isVisible(element);
|
|
1112
1146
|
const destinationPath = safeDestinationPath(element);
|
|
1113
|
-
const
|
|
1147
|
+
const navigation = publicNavigation(element);
|
|
1148
|
+
const expanded = this.expandedState(element);
|
|
1114
1149
|
const pressed = literalBooleanAttribute(element, 'aria-pressed');
|
|
1115
1150
|
const checked = checkedStateFor(element);
|
|
1116
1151
|
const targetIdentity = {
|
|
@@ -1127,6 +1162,7 @@ export class PageContextCollector {
|
|
|
1127
1162
|
...(semanticRegion ? { semanticRegion } : {}),
|
|
1128
1163
|
...(this.statefulButtonActionProof && isNavigationMenu(element, role) ? { navigationMenu: true } : {}),
|
|
1129
1164
|
...(destinationPath ? { destinationPath } : {}),
|
|
1165
|
+
...(navigation ? { navigation } : {}),
|
|
1130
1166
|
sensitivity,
|
|
1131
1167
|
protectedRegion: sensitivity !== 'none' || regionAssessment.matchedRuleDigests.length > 0,
|
|
1132
1168
|
frameOrigin: 'same_origin',
|
|
@@ -1144,7 +1180,7 @@ export class PageContextCollector {
|
|
|
1144
1180
|
};
|
|
1145
1181
|
const priorTargetId = this.targetIds.get(element);
|
|
1146
1182
|
const boundNavigation = targetIdentity.consequence === 'navigation' &&
|
|
1147
|
-
|
|
1183
|
+
targetIdentity.navigation !== undefined;
|
|
1148
1184
|
const binding = boundNavigation ? navigationTargetBinding(targetIdentity) : null;
|
|
1149
1185
|
const priorBinding = this.navigationTargetBindings.get(element);
|
|
1150
1186
|
const boundField = isNativeEditableField(element) && sensitivity === 'none' &&
|
|
@@ -1195,15 +1231,18 @@ export class PageContextCollector {
|
|
|
1195
1231
|
const visibleText = [];
|
|
1196
1232
|
const capturedSemanticText = new Set();
|
|
1197
1233
|
let totalText = 0;
|
|
1198
|
-
const walker = document.createTreeWalker(document.body ?? document.documentElement, NodeFilter.SHOW_TEXT);
|
|
1234
|
+
const walker = document.createTreeWalker(document.body ?? document.documentElement, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT);
|
|
1199
1235
|
let node = walker.nextNode();
|
|
1200
1236
|
while (node && visibleText.length < MAX_TEXT_ITEMS && totalText < MAX_TOTAL_TEXT) {
|
|
1201
|
-
const parent = node.parentElement;
|
|
1202
|
-
const semanticTextElement =
|
|
1237
|
+
const parent = node instanceof Element ? node : node.parentElement;
|
|
1238
|
+
const semanticTextElement = node instanceof Element
|
|
1239
|
+
? node.matches('[role="img"][aria-label]') ? node : null
|
|
1240
|
+
: parent?.closest('[role="img"][aria-label]') ?? null;
|
|
1203
1241
|
const textElement = semanticTextElement && isRendered(semanticTextElement)
|
|
1204
1242
|
? semanticTextElement
|
|
1205
|
-
: parent;
|
|
1206
|
-
const text = normalize(semanticTextElement?.getAttribute('aria-label') ??
|
|
1243
|
+
: node.nodeType === Node.TEXT_NODE ? parent : null;
|
|
1244
|
+
const text = normalize(semanticTextElement?.getAttribute('aria-label') ??
|
|
1245
|
+
(node.nodeType === Node.TEXT_NODE ? node.textContent ?? '' : ''), 500);
|
|
1207
1246
|
if (parent &&
|
|
1208
1247
|
textElement &&
|
|
1209
1248
|
text &&
|
|
@@ -1416,12 +1455,13 @@ export class PageContextCollector {
|
|
|
1416
1455
|
grant.targetId.startsWith(`${BOUND_FIELD_TARGET_PREFIX}_`) &&
|
|
1417
1456
|
Boolean(current && isNativeEditableField(current.element) && this.fieldTargetBindings.has(current.element));
|
|
1418
1457
|
const stablePageScroll = (current) => grant.action === 'scroll' && Boolean(current?.pageScroll && current.element === document.scrollingElement);
|
|
1458
|
+
const stableNavigation = (current) => (grant.action === 'click' || grant.action === 'scroll') &&
|
|
1459
|
+
grant.targetId.startsWith(`${BOUND_NAVIGATION_TARGET_PREFIX}_`) &&
|
|
1460
|
+
current?.element instanceof HTMLAnchorElement &&
|
|
1461
|
+
current.target.consequence === 'navigation' && current.target.navigation !== undefined;
|
|
1419
1462
|
const revisionMatchesOrStableTarget = () => grant.domRevision === this.revision ||
|
|
1420
1463
|
(grant.domRevision < this.revision &&
|
|
1421
|
-
(stableField(handle) || stablePageScroll(handle) || (
|
|
1422
|
-
handle?.element instanceof HTMLAnchorElement &&
|
|
1423
|
-
handle.target.consequence === 'navigation' &&
|
|
1424
|
-
typeof handle.target.destinationPath === 'string')));
|
|
1464
|
+
(stableField(handle) || stablePageScroll(handle) || stableNavigation(handle)));
|
|
1425
1465
|
if (expiresAt <= Date.now() ||
|
|
1426
1466
|
grant.pageId !== this.pageId ||
|
|
1427
1467
|
grant.pageUrl !== safePageUrl() ||
|
|
@@ -1458,7 +1498,7 @@ export class PageContextCollector {
|
|
|
1458
1498
|
grant.pageUrl !== safePageUrl() ||
|
|
1459
1499
|
grant.policyVersion !== this.policyVersion ||
|
|
1460
1500
|
!revisionMatchesOrStableTarget() ||
|
|
1461
|
-
(handle !== fingerprintedHandle && !((stableField(handle) || stablePageScroll(handle)) &&
|
|
1501
|
+
(handle !== fingerprintedHandle && !((stableField(handle) || stablePageScroll(handle) || stableNavigation(handle)) &&
|
|
1462
1502
|
handle?.element === fingerprintedHandle.element &&
|
|
1463
1503
|
actionTargetFingerprintBinding(handle.target) === actionTargetFingerprintBinding(fingerprintedHandle.target))) ||
|
|
1464
1504
|
!handle ||
|
|
@@ -1544,6 +1584,9 @@ export class PageContextCollector {
|
|
|
1544
1584
|
this.releasedNavigation = null;
|
|
1545
1585
|
this.finishReleasedNavigationSettlement('rejected');
|
|
1546
1586
|
let postActionSnapshot = null;
|
|
1587
|
+
let navigationReceipt;
|
|
1588
|
+
const disclosureRoot = handle.target.navigationMenu ? framerNavigationRoot(element) : null;
|
|
1589
|
+
const priorDisclosureLinks = disclosureRoot ? this.exposedNavigationLinks(disclosureRoot) : null;
|
|
1547
1590
|
try {
|
|
1548
1591
|
switch (grant.action) {
|
|
1549
1592
|
case 'scroll': {
|
|
@@ -1654,7 +1697,7 @@ export class PageContextCollector {
|
|
|
1654
1697
|
element.focus({ preventScroll: false });
|
|
1655
1698
|
break;
|
|
1656
1699
|
case 'click':
|
|
1657
|
-
this.click(element, handle, grant.actionId, grant.desiredChecked, grant.desiredExpanded, grant.desiredPressed, grant.destinationPath);
|
|
1700
|
+
navigationReceipt = this.click(element, handle, grant.actionId, grant.desiredChecked, grant.desiredExpanded, grant.desiredPressed, grant.destinationPath);
|
|
1658
1701
|
break;
|
|
1659
1702
|
case 'type':
|
|
1660
1703
|
this.type(element, grant.value ?? '');
|
|
@@ -1676,9 +1719,28 @@ export class PageContextCollector {
|
|
|
1676
1719
|
if (grant.action !== 'scroll') {
|
|
1677
1720
|
await new Promise((resolve) => setTimeout(resolve, 80));
|
|
1678
1721
|
}
|
|
1722
|
+
if (handle.target.navigationMenu && grant.action === 'click') {
|
|
1723
|
+
const deadline = Date.now() + 1000;
|
|
1724
|
+
for (let frames = 0; frames < 60 && Date.now() < deadline && element.isConnected && !executionStale(); frames++) {
|
|
1725
|
+
if (disclosureRoot && priorDisclosureLinks) {
|
|
1726
|
+
const added = new Set([...this.exposedNavigationLinks(disclosureRoot)]
|
|
1727
|
+
.filter(link => !priorDisclosureLinks.has(link)));
|
|
1728
|
+
if (added.size)
|
|
1729
|
+
this.navigationDisclosureLinks.set(element, added);
|
|
1730
|
+
}
|
|
1731
|
+
if (this.actionApplied(grant, element, handle))
|
|
1732
|
+
break;
|
|
1733
|
+
await nextAnimationFrameOrTimeout(Math.min(50, deadline - Date.now()));
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1679
1736
|
const postActionStale = executionStale();
|
|
1680
|
-
if (postActionStale)
|
|
1681
|
-
|
|
1737
|
+
if (postActionStale) {
|
|
1738
|
+
this.deferredNavigation = null;
|
|
1739
|
+
return navigationReceipt ? { ...postActionStale, observed: {
|
|
1740
|
+
...postActionStale.observed, targetState: 'unknown',
|
|
1741
|
+
navigation: { url: navigationReceipt.url, outcome: 'unconfirmed' },
|
|
1742
|
+
} } : postActionStale;
|
|
1743
|
+
}
|
|
1682
1744
|
if (!this.actionApplied(grant, element, handle)) {
|
|
1683
1745
|
throw new Error('The approved value was not applied.');
|
|
1684
1746
|
}
|
|
@@ -1698,16 +1760,16 @@ export class PageContextCollector {
|
|
|
1698
1760
|
const attached = element.isConnected;
|
|
1699
1761
|
const after = attached ? this.safeState(element) : '';
|
|
1700
1762
|
const expanded = attached
|
|
1701
|
-
?
|
|
1763
|
+
? this.expandedState(element)
|
|
1702
1764
|
: undefined;
|
|
1703
1765
|
const pressed = attached
|
|
1704
1766
|
? literalBooleanAttribute(element, 'aria-pressed')
|
|
1705
1767
|
: undefined;
|
|
1706
1768
|
const checked = attached ? checkedStateFor(element) : undefined;
|
|
1707
|
-
this.callbacks.onHighlight(attached && !handle.pageScroll ? element : null, 'success');
|
|
1769
|
+
this.callbacks.onHighlight(attached && !handle.pageScroll ? element : null, navigationReceipt && navigationReceipt.outcome !== 'opened' ? 'failure' : 'success');
|
|
1708
1770
|
return {
|
|
1709
1771
|
...base,
|
|
1710
|
-
status: 'executed',
|
|
1772
|
+
status: navigationReceipt && navigationReceipt.outcome !== 'opened' ? 'blocked' : 'executed',
|
|
1711
1773
|
observed: {
|
|
1712
1774
|
url: safePageUrl(),
|
|
1713
1775
|
revision: snapshot.revision,
|
|
@@ -1716,19 +1778,24 @@ export class PageContextCollector {
|
|
|
1716
1778
|
: grant.action === 'type' || grant.action === 'select' || before !== after || handle.pageScroll
|
|
1717
1779
|
? 'changed'
|
|
1718
1780
|
: 'unchanged',
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
? '
|
|
1723
|
-
:
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1781
|
+
...(navigationReceipt ? { navigation: navigationReceipt } : {}),
|
|
1782
|
+
safeSummary: navigationReceipt
|
|
1783
|
+
? navigationReceipt.outcome === 'opened'
|
|
1784
|
+
? 'Opened a separate tab for the exact approved link. Its contents and arrival are not observed.'
|
|
1785
|
+
: 'The new tab could not be confirmed. No fallback navigation was attempted.'
|
|
1786
|
+
: handle.pageScroll
|
|
1787
|
+
? `The page moved ${handle.pageScroll} by ${Math.round(Math.abs(window.scrollY - scrollStartY))} pixels.`
|
|
1788
|
+
: grant.action === 'scroll'
|
|
1789
|
+
? 'The requested target is visible. This confirms target visibility, not a directional page scroll.'
|
|
1790
|
+
: grant.action === 'type'
|
|
1791
|
+
? 'The requested text was applied to the approved field.'
|
|
1792
|
+
: grant.action === 'select'
|
|
1793
|
+
? 'The requested option was applied to the approved field.'
|
|
1794
|
+
: grant.desiredChecked !== undefined
|
|
1795
|
+
? 'The requested checkbox state was applied.'
|
|
1796
|
+
: grant.desiredExpanded !== undefined || grant.desiredPressed !== undefined
|
|
1797
|
+
? 'The requested button state was applied.'
|
|
1798
|
+
: `${grant.action} executed on the approved ${roleFor(element)} target.`,
|
|
1732
1799
|
...(checked !== undefined ? { checked } : {}),
|
|
1733
1800
|
...(this.statefulButtonActionProof && expanded !== undefined ? { expanded } : {}),
|
|
1734
1801
|
...(this.statefulButtonActionProof && pressed !== undefined ? { pressed } : {}),
|
|
@@ -1736,6 +1803,7 @@ export class PageContextCollector {
|
|
|
1736
1803
|
};
|
|
1737
1804
|
}
|
|
1738
1805
|
catch {
|
|
1806
|
+
this.deferredNavigation = null;
|
|
1739
1807
|
this.callbacks.onHighlight(element, 'failure');
|
|
1740
1808
|
return {
|
|
1741
1809
|
...base,
|
|
@@ -1743,8 +1811,12 @@ export class PageContextCollector {
|
|
|
1743
1811
|
observed: {
|
|
1744
1812
|
url: safePageUrl(),
|
|
1745
1813
|
revision: this.revision,
|
|
1746
|
-
targetState: element.isConnected ? 'unchanged' : 'detached',
|
|
1747
|
-
|
|
1814
|
+
targetState: navigationReceipt ? 'unknown' : element.isConnected ? 'unchanged' : 'detached',
|
|
1815
|
+
...(navigationReceipt ? { navigation: { url: navigationReceipt.url,
|
|
1816
|
+
outcome: 'unconfirmed' } } : {}),
|
|
1817
|
+
safeSummary: navigationReceipt
|
|
1818
|
+
? 'The link action could not be fully confirmed. Do not retry automatically.'
|
|
1819
|
+
: 'The approved page action failed safely.',
|
|
1748
1820
|
},
|
|
1749
1821
|
};
|
|
1750
1822
|
}
|
|
@@ -1804,9 +1876,8 @@ export class PageContextCollector {
|
|
|
1804
1876
|
return 'rejected';
|
|
1805
1877
|
}
|
|
1806
1878
|
if (window.location.href !== navigation.fromUrl) {
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
: 'rejected';
|
|
1879
|
+
// Arrival before dispatch is not evidence that this action navigated.
|
|
1880
|
+
return 'rejected';
|
|
1810
1881
|
}
|
|
1811
1882
|
this.captureSnapshot(false);
|
|
1812
1883
|
const handle = this.targets.get(navigation.targetId);
|
|
@@ -1822,7 +1893,7 @@ export class PageContextCollector {
|
|
|
1822
1893
|
target.origin !== window.location.origin ||
|
|
1823
1894
|
safeDestinationPath(navigation.source) !== target.pathname ||
|
|
1824
1895
|
linkBrowsingTarget(navigation.source) !== navigation.browsingTarget ||
|
|
1825
|
-
!supportsCurrentTabNavigation(navigation.source) ||
|
|
1896
|
+
!supportsCurrentTabNavigation(navigation.source) || navigation.browsingTarget === '_blank' ||
|
|
1826
1897
|
!isRendered(navigation.source) ||
|
|
1827
1898
|
!isVisible(navigation.source) ||
|
|
1828
1899
|
!enabled(navigation.source) ||
|
|
@@ -1836,9 +1907,6 @@ export class PageContextCollector {
|
|
|
1836
1907
|
if (current?.actionId !== navigation.actionId)
|
|
1837
1908
|
return;
|
|
1838
1909
|
this.releasedNavigationSettlement = null;
|
|
1839
|
-
if (this.executingActionId === navigation.actionId) {
|
|
1840
|
-
this.executingActionId = null;
|
|
1841
|
-
}
|
|
1842
1910
|
resolve('timed_out');
|
|
1843
1911
|
}, RELEASED_NAVIGATION_SETTLEMENT_TIMEOUT_MS);
|
|
1844
1912
|
this.releasedNavigationSettlement = {
|
|
@@ -1849,16 +1917,8 @@ export class PageContextCollector {
|
|
|
1849
1917
|
resolve,
|
|
1850
1918
|
};
|
|
1851
1919
|
});
|
|
1852
|
-
const useCurrentTab = navigation.browsingTarget === '_blank';
|
|
1853
1920
|
try {
|
|
1854
|
-
|
|
1855
|
-
// approved URL only after durable release; the normal document-navigation
|
|
1856
|
-
// recovery handles the new page. Dispatching their handlers can open a
|
|
1857
|
-
// popup even if the DOM target is temporarily changed (e.g. React props).
|
|
1858
|
-
if (useCurrentTab) {
|
|
1859
|
-
window.location.assign(navigation.toUrl);
|
|
1860
|
-
}
|
|
1861
|
-
else if (navigation.source.closest(FRAMER_GENERATED_PAGE_SELECTOR) &&
|
|
1921
|
+
if (navigation.source.closest(FRAMER_GENERATED_PAGE_SELECTOR) &&
|
|
1862
1922
|
typeof window.PointerEvent === 'function') {
|
|
1863
1923
|
const preventNativeNavigation = (event) => {
|
|
1864
1924
|
if (event.target === navigation.source ||
|
|
@@ -1896,7 +1956,7 @@ export class PageContextCollector {
|
|
|
1896
1956
|
// failure must not turn the acknowledged action into a failed click.
|
|
1897
1957
|
}
|
|
1898
1958
|
}
|
|
1899
|
-
else if (navigationSettled
|
|
1959
|
+
else if (navigationSettled !== 'settled' &&
|
|
1900
1960
|
this.executingActionId === navigation.actionId) {
|
|
1901
1961
|
// A router may consume the exact click but send the browser somewhere
|
|
1902
1962
|
// other than the approved destination. Complete the context-side action
|
|
@@ -1934,7 +1994,7 @@ export class PageContextCollector {
|
|
|
1934
1994
|
}
|
|
1935
1995
|
}
|
|
1936
1996
|
if (desiredExpanded !== undefined) {
|
|
1937
|
-
const expanded =
|
|
1997
|
+
const expanded = this.expandedState(element);
|
|
1938
1998
|
if (roleFor(element) !== 'button' || expanded === undefined) {
|
|
1939
1999
|
throw new Error('Desired expanded state requires an expandable button target.');
|
|
1940
2000
|
}
|
|
@@ -1949,6 +2009,10 @@ export class PageContextCollector {
|
|
|
1949
2009
|
}
|
|
1950
2010
|
if (desiredStates.length > 0 && desiredStates.every(Boolean))
|
|
1951
2011
|
return;
|
|
2012
|
+
if (desiredExpanded === true && framerNavigationRoot(element)) {
|
|
2013
|
+
dispatchFramerPrimaryPointerSequence(element, this.trustedGuideRoot);
|
|
2014
|
+
return;
|
|
2015
|
+
}
|
|
1952
2016
|
if (desiredChecked !== undefined && framerToggleDescriptor(element)) {
|
|
1953
2017
|
dispatchFramerTogglePrimaryPointer(element, this.trustedGuideRoot);
|
|
1954
2018
|
return;
|
|
@@ -1959,11 +2023,25 @@ export class PageContextCollector {
|
|
|
1959
2023
|
safeDestinationPath(element) !== expectedDestinationPath) {
|
|
1960
2024
|
throw new Error('The navigation destination changed before execution.');
|
|
1961
2025
|
}
|
|
1962
|
-
|
|
1963
|
-
|
|
2026
|
+
const navigation = publicNavigation(element);
|
|
2027
|
+
if (!navigation || canonicalJson(navigation) !== canonicalJson(handle.target.navigation)) {
|
|
2028
|
+
throw new Error('The public link destination or browsing target changed.');
|
|
1964
2029
|
}
|
|
1965
|
-
if (
|
|
1966
|
-
|
|
2030
|
+
if (navigation.target === '_blank' || target.origin !== window.location.origin) {
|
|
2031
|
+
// Keep external pages outside this session's origin/credential boundary.
|
|
2032
|
+
// Only the browser's native noreferrer feature can prevent the source
|
|
2033
|
+
// URL from becoming the Referer on this navigation. With noopener the
|
|
2034
|
+
// return value may be null even when the tab opens, so never call a
|
|
2035
|
+
// null result "blocked" or try the link a second time.
|
|
2036
|
+
try {
|
|
2037
|
+
const popup = window.open(navigation.url, '_blank', 'noopener,noreferrer');
|
|
2038
|
+
return { url: navigation.url, outcome: popup && !popup.closed ? 'opened' : 'unconfirmed' };
|
|
2039
|
+
}
|
|
2040
|
+
catch {
|
|
2041
|
+
// Some browsers throw when popups are disallowed. The exact URL was
|
|
2042
|
+
// attempted at most once, and there is no fallback navigation.
|
|
2043
|
+
return { url: navigation.url, outcome: 'unconfirmed' };
|
|
2044
|
+
}
|
|
1967
2045
|
}
|
|
1968
2046
|
const fromUrl = window.location.href;
|
|
1969
2047
|
if (window.location.href === fromUrl && target.href !== fromUrl) {
|
|
@@ -2061,11 +2139,26 @@ export class PageContextCollector {
|
|
|
2061
2139
|
}
|
|
2062
2140
|
form.requestSubmit(submitter);
|
|
2063
2141
|
}
|
|
2142
|
+
exposedNavigationLinks(nav) {
|
|
2143
|
+
const links = nav.querySelectorAll('a[href]');
|
|
2144
|
+
if (links.length > MAX_TARGETS)
|
|
2145
|
+
return new Set();
|
|
2146
|
+
return new Set(Array.from(links).filter(link => isVisible(link) && publicNavigation(link) &&
|
|
2147
|
+
!isCovered(link, this.trustedGuideRoot)));
|
|
2148
|
+
}
|
|
2149
|
+
expandedState(element) {
|
|
2150
|
+
const explicit = literalBooleanAttribute(element, 'aria-expanded');
|
|
2151
|
+
if (explicit !== undefined)
|
|
2152
|
+
return explicit;
|
|
2153
|
+
if (!framerNavigationRoot(element))
|
|
2154
|
+
return undefined;
|
|
2155
|
+
return [...(this.navigationDisclosureLinks.get(element) ?? [])].some(link => link.isConnected && isRendered(link));
|
|
2156
|
+
}
|
|
2064
2157
|
safeState(element) {
|
|
2065
2158
|
return JSON.stringify({
|
|
2066
2159
|
connected: element.isConnected,
|
|
2067
2160
|
disabled: !enabled(element),
|
|
2068
|
-
expanded: element.getAttribute('aria-expanded'),
|
|
2161
|
+
expanded: this.expandedState(element) ?? element.getAttribute('aria-expanded'),
|
|
2069
2162
|
pressed: element.getAttribute('aria-pressed'),
|
|
2070
2163
|
checked: checkedStateFor(element),
|
|
2071
2164
|
selectedIndex: element instanceof HTMLSelectElement ? element.selectedIndex : undefined,
|
|
@@ -2088,6 +2181,7 @@ export class PageContextCollector {
|
|
|
2088
2181
|
let documentLeft = expectedHandle.element.getBoundingClientRect().left + window.scrollX;
|
|
2089
2182
|
let layoutCorrections = 0;
|
|
2090
2183
|
let priorScrollY = null;
|
|
2184
|
+
let intersectedViewport = false;
|
|
2091
2185
|
let interrupted = false;
|
|
2092
2186
|
const interrupt = () => {
|
|
2093
2187
|
interrupted = true;
|
|
@@ -2161,10 +2255,25 @@ export class PageContextCollector {
|
|
|
2161
2255
|
if (!strictlyIntersectsViewport(rect)) {
|
|
2162
2256
|
priorRect = null;
|
|
2163
2257
|
stableSince = null;
|
|
2164
|
-
//
|
|
2165
|
-
//
|
|
2166
|
-
//
|
|
2167
|
-
|
|
2258
|
+
// A public-link reveal performs exactly one native scroll. If layout
|
|
2259
|
+
// shifts move that link away again, stop instead of chasing it and
|
|
2260
|
+
// accidentally scrolling two or three more times. Other controls
|
|
2261
|
+
// retain the existing bounded lazy-layout correction.
|
|
2262
|
+
if (expectedHandle.target.consequence === 'navigation' && intersectedViewport) {
|
|
2263
|
+
this.captureSnapshot(false);
|
|
2264
|
+
const current = this.targets.get(grant.targetId);
|
|
2265
|
+
if (!current ||
|
|
2266
|
+
current.element !== expectedHandle.element ||
|
|
2267
|
+
actionTargetFingerprintBinding(current.target) !==
|
|
2268
|
+
actionTargetFingerprintBinding(expectedHandle.target) ||
|
|
2269
|
+
grant.pageId !== this.pageId ||
|
|
2270
|
+
grant.pageUrl !== safePageUrl() ||
|
|
2271
|
+
grant.policyVersion !== this.policyVersion) {
|
|
2272
|
+
return { status: 'target_changed' };
|
|
2273
|
+
}
|
|
2274
|
+
return { status: 'unavailable' };
|
|
2275
|
+
}
|
|
2276
|
+
if (layoutShifted && expectedHandle.target.consequence !== 'navigation' &&
|
|
2168
2277
|
layoutCorrections < MAX_SCROLL_LAYOUT_CORRECTIONS) {
|
|
2169
2278
|
this.captureSnapshot(false);
|
|
2170
2279
|
const current = this.targets.get(grant.targetId);
|
|
@@ -2189,6 +2298,7 @@ export class PageContextCollector {
|
|
|
2189
2298
|
}
|
|
2190
2299
|
continue;
|
|
2191
2300
|
}
|
|
2301
|
+
intersectedViewport = true;
|
|
2192
2302
|
if (!priorRect || !sameSettledRect(priorRect, rect))
|
|
2193
2303
|
stableSince = Date.now();
|
|
2194
2304
|
priorRect = rect;
|
|
@@ -2211,7 +2321,7 @@ export class PageContextCollector {
|
|
|
2211
2321
|
return checkedStateFor(element) === grant.desiredChecked;
|
|
2212
2322
|
}
|
|
2213
2323
|
if (grant.desiredExpanded !== undefined &&
|
|
2214
|
-
|
|
2324
|
+
this.expandedState(element) !== grant.desiredExpanded) {
|
|
2215
2325
|
return false;
|
|
2216
2326
|
}
|
|
2217
2327
|
if (grant.desiredPressed !== undefined &&
|
|
@@ -2394,6 +2504,7 @@ export class PageContextCollector {
|
|
|
2394
2504
|
resetBoundTargetIds() {
|
|
2395
2505
|
this.targetIds = new WeakMap();
|
|
2396
2506
|
this.navigationTargetBindings = new WeakMap();
|
|
2507
|
+
this.navigationDisclosureLinks = new WeakMap();
|
|
2397
2508
|
this.pageScrollIds = new WeakMap();
|
|
2398
2509
|
this.fieldTargetBindings = new WeakMap();
|
|
2399
2510
|
this.fieldAppliedRevisions = new WeakMap();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type LiveDuplexMediaOptions, type LiveDuplexMediaDependencies } from './liveDuplexMedia.js';
|
|
2
|
-
export declare const LIVE_CONTROL_CLIENT_VERSION = "7.5.
|
|
2
|
+
export declare const LIVE_CONTROL_CLIENT_VERSION = "7.5.10";
|
|
3
3
|
export declare const LIVE_CONTROL_CLIENT_CAPABILITIES: readonly ["live_duplex_v1", "live_local_wake_v1", "live_fragment_captions_v1"];
|
|
4
4
|
declare const phases: readonly ["voice_off", "local_listening", "wake_buffering", "starting", "active", "tentative_idle", "closing", "uncertain", "muted", "suspended", "budget_blocked", "ended"];
|
|
5
5
|
export type LiveControlState = {
|