@oxvo/ai-live-assist 7.5.6 → 7.5.9
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 +207 -77
- 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 +207 -77
- 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/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.9";
|
|
38
38
|
private readonly host;
|
|
39
39
|
private readonly guideAdmission;
|
|
40
40
|
private readonly standaloneAdmissionPending;
|
package/cjs/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/cjs/context.js
CHANGED
|
@@ -20,7 +20,7 @@ const RELEASED_NAVIGATION_SETTLEMENT_TIMEOUT_MS = 5000;
|
|
|
20
20
|
const FRAMER_GENERATED_PAGE_SELECTOR = '[data-framer-generated-page]';
|
|
21
21
|
const FRAMER_TOGGLE_ROOT_SELECTOR = '[data-framer-name="Toggle"]';
|
|
22
22
|
const FRAMER_TOGGLE_CONTROL_SELECTOR = 'div[name="Toggle Switch"][data-highlight="true"][tabindex]';
|
|
23
|
-
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}`;
|
|
23
|
+
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}`;
|
|
24
24
|
const MAX_FRAMER_TOGGLE_LABEL = 120;
|
|
25
25
|
const PRIVATE_SELECTOR = [
|
|
26
26
|
'[data-oxvo-ai-live-assist-root]',
|
|
@@ -80,6 +80,7 @@ const navigationTargetBinding = (target) => canonicalJson({
|
|
|
80
80
|
autocomplete: target.autocomplete ?? null,
|
|
81
81
|
semanticRegion: target.semanticRegion ?? null,
|
|
82
82
|
...(target.navigationMenu === true ? { navigationMenu: true } : {}),
|
|
83
|
+
...(target.navigation ? { navigation: target.navigation } : {}),
|
|
83
84
|
destinationPath: target.destinationPath ?? null,
|
|
84
85
|
sensitivity: target.sensitivity,
|
|
85
86
|
protectedRegion: target.protectedRegion ?? null,
|
|
@@ -125,6 +126,7 @@ const actionTargetFingerprintBinding = (target) => canonicalJson({
|
|
|
125
126
|
autocomplete: target.autocomplete ?? null,
|
|
126
127
|
semanticRegion: target.semanticRegion ?? null,
|
|
127
128
|
...(target.navigationMenu === true ? { navigationMenu: true } : {}),
|
|
129
|
+
...(target.navigation ? { navigation: target.navigation } : {}),
|
|
128
130
|
destinationPath: target.destinationPath ?? null,
|
|
129
131
|
sensitivity: target.sensitivity,
|
|
130
132
|
protectedRegion: target.protectedRegion ?? null,
|
|
@@ -222,8 +224,27 @@ const interactionPoint = (element, trustedGuideRoot = null) => {
|
|
|
222
224
|
return null;
|
|
223
225
|
};
|
|
224
226
|
const isCovered = (element, trustedGuideRoot = null) => interactionPoint(element, trustedGuideRoot) === null;
|
|
227
|
+
const FRAMER_NAVIGATION_CONTROL_SELECTOR = 'div[data-highlight="true"][tabindex]';
|
|
228
|
+
const framerNavigationRoot = (element) => {
|
|
229
|
+
if (!(element instanceof HTMLElement) || !element.matches(FRAMER_NAVIGATION_CONTROL_SELECTOR) ||
|
|
230
|
+
element.tabIndex < 0 || element.hasAttribute('role') || element.hasAttribute('aria-pressed') ||
|
|
231
|
+
element.hasAttribute('aria-checked') || !element.getAttribute('data-framer-name')?.trim() ||
|
|
232
|
+
!element.closest(FRAMER_GENERATED_PAGE_SELECTOR) || element.closest(`form,${PRIVATE_SELECTOR}`) ||
|
|
233
|
+
element.querySelector('a,button,input,select,textarea,summary,form,[role],[tabindex],[contenteditable]') ||
|
|
234
|
+
element.textContent?.trim())
|
|
235
|
+
return null;
|
|
236
|
+
const nav = element.closest('nav,[role="navigation"]');
|
|
237
|
+
if (!nav)
|
|
238
|
+
return null;
|
|
239
|
+
const candidates = nav.querySelectorAll(FRAMER_NAVIGATION_CONTROL_SELECTOR);
|
|
240
|
+
if (candidates.length > MAX_TARGETS)
|
|
241
|
+
return null;
|
|
242
|
+
const rendered = Array.from(candidates).filter(candidate => isRendered(candidate) &&
|
|
243
|
+
candidate.closest('nav,[role="navigation"]') === nav);
|
|
244
|
+
return rendered.length === 1 && rendered[0] === element ? nav : null;
|
|
245
|
+
};
|
|
225
246
|
const isNavigationMenu = (element, role) => {
|
|
226
|
-
if (role !== 'button' || literalBooleanAttribute(element, 'aria-expanded') === undefined ||
|
|
247
|
+
if (role !== 'button' || (literalBooleanAttribute(element, 'aria-expanded') === undefined && !framerNavigationRoot(element)) ||
|
|
227
248
|
element.hasAttribute('aria-pressed') || element.hasAttribute('aria-checked') ||
|
|
228
249
|
element instanceof HTMLButtonElement && element.form !== null && element.type !== 'button')
|
|
229
250
|
return false;
|
|
@@ -246,27 +267,31 @@ const sameNavigationDestination = (actual, expected) => {
|
|
|
246
267
|
const left = new URL(actual), right = new URL(expected);
|
|
247
268
|
return left.origin === right.origin && left.pathname === right.pathname && left.search === right.search;
|
|
248
269
|
};
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
return
|
|
270
|
+
const publicNavigation = (element) => {
|
|
271
|
+
if (!(element instanceof HTMLAnchorElement) || !element.getAttribute('href')?.trim() ||
|
|
272
|
+
!supportsCurrentTabNavigation(element))
|
|
273
|
+
return undefined;
|
|
253
274
|
try {
|
|
254
275
|
const destination = new URL(element.href, window.location.href);
|
|
255
|
-
if (
|
|
256
|
-
destination.
|
|
257
|
-
destination.search ||
|
|
258
|
-
destination.
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
return null;
|
|
263
|
-
}
|
|
264
|
-
return destination.pathname;
|
|
276
|
+
if (!['http:', 'https:'].includes(destination.protocol) ||
|
|
277
|
+
(destination.origin !== window.location.origin && destination.protocol !== 'https:') ||
|
|
278
|
+
destination.username || destination.password || destination.search || destination.hash ||
|
|
279
|
+
destination.href.length > 2048 || destination.pathname.length > 1024 ||
|
|
280
|
+
!/^\/(?:[A-Za-z0-9_~-]+(?:\.[A-Za-z0-9_~-]+)*\/?)*$/.test(destination.pathname))
|
|
281
|
+
return undefined;
|
|
282
|
+
return { url: destination.href, target: linkBrowsingTarget(element) === '_blank' ? '_blank' : '_self' };
|
|
265
283
|
}
|
|
266
284
|
catch {
|
|
267
|
-
return
|
|
285
|
+
return undefined;
|
|
268
286
|
}
|
|
269
287
|
};
|
|
288
|
+
const safeDestinationPath = (element) => {
|
|
289
|
+
const navigation = publicNavigation(element);
|
|
290
|
+
if (!navigation)
|
|
291
|
+
return null;
|
|
292
|
+
const destination = new URL(navigation.url);
|
|
293
|
+
return destination.origin === window.location.origin ? destination.pathname : null;
|
|
294
|
+
};
|
|
270
295
|
const sensitivityFor = (element, name) => {
|
|
271
296
|
const input = element instanceof HTMLInputElement ? element : null;
|
|
272
297
|
const type = input?.type.toLowerCase() ?? '';
|
|
@@ -300,6 +325,8 @@ const sensitivityFor = (element, name) => {
|
|
|
300
325
|
const roleFor = (element) => {
|
|
301
326
|
if (framerToggleDescriptor(element))
|
|
302
327
|
return 'checkbox';
|
|
328
|
+
if (framerNavigationRoot(element))
|
|
329
|
+
return 'button';
|
|
303
330
|
const explicit = normalize(element.getAttribute('role') ?? '', 64);
|
|
304
331
|
if (explicit)
|
|
305
332
|
return explicit;
|
|
@@ -367,6 +394,7 @@ const nameFor = (element) => {
|
|
|
367
394
|
return normalize(semanticName ||
|
|
368
395
|
associatedLabel ||
|
|
369
396
|
attributeName ||
|
|
397
|
+
(framerNavigationRoot(element) ? safeAttribute('data-framer-name', 120) : '') ||
|
|
370
398
|
(buttonValue && sensitivityFor(element, buttonValue) === 'none' ? buttonValue : '') ||
|
|
371
399
|
(safeDestinationPath(element) === '/' ? 'Home' : ''), 300);
|
|
372
400
|
};
|
|
@@ -640,6 +668,18 @@ const safeOptionLabel = (option) => {
|
|
|
640
668
|
return sensitivityFor(option, label) === 'none' ? label : '';
|
|
641
669
|
};
|
|
642
670
|
const isActionTarget = (element) => element.matches('a[href],button,input,select,textarea,summary,form,[contenteditable="true"],[role="button"],[role="link"],[role="checkbox"],[role="radio"],[role="combobox"],[role="textbox"],[tabindex]');
|
|
671
|
+
const belongsToInteractiveControl = (element) => {
|
|
672
|
+
for (let current = element; current; current = current.parentElement) {
|
|
673
|
+
// Form copy can carry useful public instructions. Only controls and links
|
|
674
|
+
// already represented in targets are omitted from visible page text.
|
|
675
|
+
if (current.matches('form'))
|
|
676
|
+
continue;
|
|
677
|
+
// A bare tabindex is often a focusable Framer content card, not an action.
|
|
678
|
+
if (current.matches('a[href],button,input,select,textarea,summary,[contenteditable="true"],[role="button"],[role="link"],[role="checkbox"],[role="radio"],[role="combobox"],[role="textbox"]'))
|
|
679
|
+
return true;
|
|
680
|
+
}
|
|
681
|
+
return false;
|
|
682
|
+
};
|
|
643
683
|
const READONLY_INPUT_TYPES = new Set([
|
|
644
684
|
'date',
|
|
645
685
|
'datetime-local',
|
|
@@ -756,6 +796,7 @@ class PageContextCollector {
|
|
|
756
796
|
this.targetIds = new WeakMap();
|
|
757
797
|
this.pageScrollIds = new WeakMap();
|
|
758
798
|
this.navigationTargetBindings = new WeakMap();
|
|
799
|
+
this.navigationDisclosureLinks = new WeakMap();
|
|
759
800
|
this.fieldTargetBindings = new WeakMap();
|
|
760
801
|
this.fieldAppliedRevisions = new WeakMap();
|
|
761
802
|
this.textIds = new WeakMap();
|
|
@@ -979,6 +1020,9 @@ class PageContextCollector {
|
|
|
979
1020
|
'data-framer-name',
|
|
980
1021
|
'disabled',
|
|
981
1022
|
'href',
|
|
1023
|
+
'target',
|
|
1024
|
+
'download',
|
|
1025
|
+
'data-highlight',
|
|
982
1026
|
'hidden',
|
|
983
1027
|
'inert',
|
|
984
1028
|
'role',
|
|
@@ -1025,6 +1069,7 @@ class PageContextCollector {
|
|
|
1025
1069
|
document.removeEventListener('change', this.handleControlStateChange, true);
|
|
1026
1070
|
this.restoreNavigationHooks();
|
|
1027
1071
|
this.targets.clear();
|
|
1072
|
+
this.navigationDisclosureLinks = new WeakMap();
|
|
1028
1073
|
this.executingActionId = null;
|
|
1029
1074
|
this.pendingNavigation = null;
|
|
1030
1075
|
this.pendingCapture = false;
|
|
@@ -1065,7 +1110,8 @@ class PageContextCollector {
|
|
|
1065
1110
|
continue;
|
|
1066
1111
|
const role = roleFor(element);
|
|
1067
1112
|
const hasStatefulButtonAttribute = role === 'button' &&
|
|
1068
|
-
(element.hasAttribute('aria-expanded') || element.hasAttribute('aria-pressed')
|
|
1113
|
+
(element.hasAttribute('aria-expanded') || element.hasAttribute('aria-pressed') ||
|
|
1114
|
+
Boolean(framerNavigationRoot(element)));
|
|
1069
1115
|
const hasUnsupportedMixedPressedState = role === 'button' &&
|
|
1070
1116
|
element.getAttribute('aria-pressed')?.trim().toLowerCase() === 'mixed';
|
|
1071
1117
|
if (hasUnsupportedMixedPressedState ||
|
|
@@ -1102,7 +1148,8 @@ class PageContextCollector {
|
|
|
1102
1148
|
: accessibleNameSourceSensitivity(element);
|
|
1103
1149
|
const visible = isVisible(element);
|
|
1104
1150
|
const destinationPath = safeDestinationPath(element);
|
|
1105
|
-
const
|
|
1151
|
+
const navigation = publicNavigation(element);
|
|
1152
|
+
const expanded = this.expandedState(element);
|
|
1106
1153
|
const pressed = literalBooleanAttribute(element, 'aria-pressed');
|
|
1107
1154
|
const checked = checkedStateFor(element);
|
|
1108
1155
|
const targetIdentity = {
|
|
@@ -1119,6 +1166,7 @@ class PageContextCollector {
|
|
|
1119
1166
|
...(semanticRegion ? { semanticRegion } : {}),
|
|
1120
1167
|
...(this.statefulButtonActionProof && isNavigationMenu(element, role) ? { navigationMenu: true } : {}),
|
|
1121
1168
|
...(destinationPath ? { destinationPath } : {}),
|
|
1169
|
+
...(navigation ? { navigation } : {}),
|
|
1122
1170
|
sensitivity,
|
|
1123
1171
|
protectedRegion: sensitivity !== 'none' || regionAssessment.matchedRuleDigests.length > 0,
|
|
1124
1172
|
frameOrigin: 'same_origin',
|
|
@@ -1136,7 +1184,7 @@ class PageContextCollector {
|
|
|
1136
1184
|
};
|
|
1137
1185
|
const priorTargetId = this.targetIds.get(element);
|
|
1138
1186
|
const boundNavigation = targetIdentity.consequence === 'navigation' &&
|
|
1139
|
-
|
|
1187
|
+
targetIdentity.navigation !== undefined;
|
|
1140
1188
|
const binding = boundNavigation ? navigationTargetBinding(targetIdentity) : null;
|
|
1141
1189
|
const priorBinding = this.navigationTargetBindings.get(element);
|
|
1142
1190
|
const boundField = isNativeEditableField(element) && sensitivity === 'none' &&
|
|
@@ -1185,23 +1233,33 @@ class PageContextCollector {
|
|
|
1185
1233
|
});
|
|
1186
1234
|
}
|
|
1187
1235
|
const visibleText = [];
|
|
1236
|
+
const capturedSemanticText = new Set();
|
|
1188
1237
|
let totalText = 0;
|
|
1189
1238
|
const walker = document.createTreeWalker(document.body ?? document.documentElement, NodeFilter.SHOW_TEXT);
|
|
1190
1239
|
let node = walker.nextNode();
|
|
1191
1240
|
while (node && visibleText.length < MAX_TEXT_ITEMS && totalText < MAX_TOTAL_TEXT) {
|
|
1192
1241
|
const parent = node.parentElement;
|
|
1193
|
-
const
|
|
1242
|
+
const semanticTextElement = parent?.closest('[role="img"][aria-label]') ?? null;
|
|
1243
|
+
const textElement = semanticTextElement && isRendered(semanticTextElement)
|
|
1244
|
+
? semanticTextElement
|
|
1245
|
+
: parent;
|
|
1246
|
+
const text = normalize(semanticTextElement?.getAttribute('aria-label') ?? node.textContent ?? '', 500);
|
|
1194
1247
|
if (parent &&
|
|
1248
|
+
textElement &&
|
|
1195
1249
|
text &&
|
|
1196
1250
|
!parent.closest(PRIVATE_SELECTOR) &&
|
|
1197
1251
|
!parent.closest('script,style,noscript,template,svg') &&
|
|
1198
1252
|
!parent.closest('input,select,textarea,option') &&
|
|
1199
|
-
|
|
1200
|
-
|
|
1253
|
+
(!semanticTextElement || !capturedSemanticText.has(semanticTextElement)) &&
|
|
1254
|
+
!belongsToInteractiveControl(textElement) &&
|
|
1255
|
+
isRendered(textElement) &&
|
|
1256
|
+
sensitivityFor(textElement, nameFor(textElement)) === 'none') {
|
|
1201
1257
|
const textId = this.textIds.get(node) ?? id('text');
|
|
1202
1258
|
this.textIds.set(node, textId);
|
|
1203
1259
|
visibleText.push({ id: textId, text, provenance: 'page' });
|
|
1204
1260
|
totalText += text.length;
|
|
1261
|
+
if (semanticTextElement)
|
|
1262
|
+
capturedSemanticText.add(semanticTextElement);
|
|
1205
1263
|
}
|
|
1206
1264
|
node = walker.nextNode();
|
|
1207
1265
|
}
|
|
@@ -1398,12 +1456,13 @@ class PageContextCollector {
|
|
|
1398
1456
|
grant.targetId.startsWith(`${BOUND_FIELD_TARGET_PREFIX}_`) &&
|
|
1399
1457
|
Boolean(current && isNativeEditableField(current.element) && this.fieldTargetBindings.has(current.element));
|
|
1400
1458
|
const stablePageScroll = (current) => grant.action === 'scroll' && Boolean(current?.pageScroll && current.element === document.scrollingElement);
|
|
1459
|
+
const stableNavigation = (current) => (grant.action === 'click' || grant.action === 'scroll') &&
|
|
1460
|
+
grant.targetId.startsWith(`${BOUND_NAVIGATION_TARGET_PREFIX}_`) &&
|
|
1461
|
+
current?.element instanceof HTMLAnchorElement &&
|
|
1462
|
+
current.target.consequence === 'navigation' && current.target.navigation !== undefined;
|
|
1401
1463
|
const revisionMatchesOrStableTarget = () => grant.domRevision === this.revision ||
|
|
1402
1464
|
(grant.domRevision < this.revision &&
|
|
1403
|
-
(stableField(handle) || stablePageScroll(handle) || (
|
|
1404
|
-
handle?.element instanceof HTMLAnchorElement &&
|
|
1405
|
-
handle.target.consequence === 'navigation' &&
|
|
1406
|
-
typeof handle.target.destinationPath === 'string')));
|
|
1465
|
+
(stableField(handle) || stablePageScroll(handle) || stableNavigation(handle)));
|
|
1407
1466
|
if (expiresAt <= Date.now() ||
|
|
1408
1467
|
grant.pageId !== this.pageId ||
|
|
1409
1468
|
grant.pageUrl !== safePageUrl() ||
|
|
@@ -1440,7 +1499,7 @@ class PageContextCollector {
|
|
|
1440
1499
|
grant.pageUrl !== safePageUrl() ||
|
|
1441
1500
|
grant.policyVersion !== this.policyVersion ||
|
|
1442
1501
|
!revisionMatchesOrStableTarget() ||
|
|
1443
|
-
(handle !== fingerprintedHandle && !((stableField(handle) || stablePageScroll(handle)) &&
|
|
1502
|
+
(handle !== fingerprintedHandle && !((stableField(handle) || stablePageScroll(handle) || stableNavigation(handle)) &&
|
|
1444
1503
|
handle?.element === fingerprintedHandle.element &&
|
|
1445
1504
|
actionTargetFingerprintBinding(handle.target) === actionTargetFingerprintBinding(fingerprintedHandle.target))) ||
|
|
1446
1505
|
!handle ||
|
|
@@ -1526,6 +1585,9 @@ class PageContextCollector {
|
|
|
1526
1585
|
this.releasedNavigation = null;
|
|
1527
1586
|
this.finishReleasedNavigationSettlement('rejected');
|
|
1528
1587
|
let postActionSnapshot = null;
|
|
1588
|
+
let navigationReceipt;
|
|
1589
|
+
const disclosureRoot = handle.target.navigationMenu ? framerNavigationRoot(element) : null;
|
|
1590
|
+
const priorDisclosureLinks = disclosureRoot ? this.exposedNavigationLinks(disclosureRoot) : null;
|
|
1529
1591
|
try {
|
|
1530
1592
|
switch (grant.action) {
|
|
1531
1593
|
case 'scroll': {
|
|
@@ -1636,7 +1698,7 @@ class PageContextCollector {
|
|
|
1636
1698
|
element.focus({ preventScroll: false });
|
|
1637
1699
|
break;
|
|
1638
1700
|
case 'click':
|
|
1639
|
-
this.click(element, handle, grant.actionId, grant.desiredChecked, grant.desiredExpanded, grant.desiredPressed, grant.destinationPath);
|
|
1701
|
+
navigationReceipt = this.click(element, handle, grant.actionId, grant.desiredChecked, grant.desiredExpanded, grant.desiredPressed, grant.destinationPath);
|
|
1640
1702
|
break;
|
|
1641
1703
|
case 'type':
|
|
1642
1704
|
this.type(element, grant.value ?? '');
|
|
@@ -1658,9 +1720,28 @@ class PageContextCollector {
|
|
|
1658
1720
|
if (grant.action !== 'scroll') {
|
|
1659
1721
|
await new Promise((resolve) => setTimeout(resolve, 80));
|
|
1660
1722
|
}
|
|
1723
|
+
if (handle.target.navigationMenu && grant.action === 'click') {
|
|
1724
|
+
const deadline = Date.now() + 1000;
|
|
1725
|
+
for (let frames = 0; frames < 60 && Date.now() < deadline && element.isConnected && !executionStale(); frames++) {
|
|
1726
|
+
if (disclosureRoot && priorDisclosureLinks) {
|
|
1727
|
+
const added = new Set([...this.exposedNavigationLinks(disclosureRoot)]
|
|
1728
|
+
.filter(link => !priorDisclosureLinks.has(link)));
|
|
1729
|
+
if (added.size)
|
|
1730
|
+
this.navigationDisclosureLinks.set(element, added);
|
|
1731
|
+
}
|
|
1732
|
+
if (this.actionApplied(grant, element, handle))
|
|
1733
|
+
break;
|
|
1734
|
+
await nextAnimationFrameOrTimeout(Math.min(50, deadline - Date.now()));
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1661
1737
|
const postActionStale = executionStale();
|
|
1662
|
-
if (postActionStale)
|
|
1663
|
-
|
|
1738
|
+
if (postActionStale) {
|
|
1739
|
+
this.deferredNavigation = null;
|
|
1740
|
+
return navigationReceipt ? { ...postActionStale, observed: {
|
|
1741
|
+
...postActionStale.observed, targetState: 'unknown',
|
|
1742
|
+
navigation: { url: navigationReceipt.url, outcome: 'unconfirmed' },
|
|
1743
|
+
} } : postActionStale;
|
|
1744
|
+
}
|
|
1664
1745
|
if (!this.actionApplied(grant, element, handle)) {
|
|
1665
1746
|
throw new Error('The approved value was not applied.');
|
|
1666
1747
|
}
|
|
@@ -1680,16 +1761,16 @@ class PageContextCollector {
|
|
|
1680
1761
|
const attached = element.isConnected;
|
|
1681
1762
|
const after = attached ? this.safeState(element) : '';
|
|
1682
1763
|
const expanded = attached
|
|
1683
|
-
?
|
|
1764
|
+
? this.expandedState(element)
|
|
1684
1765
|
: undefined;
|
|
1685
1766
|
const pressed = attached
|
|
1686
1767
|
? literalBooleanAttribute(element, 'aria-pressed')
|
|
1687
1768
|
: undefined;
|
|
1688
1769
|
const checked = attached ? checkedStateFor(element) : undefined;
|
|
1689
|
-
this.callbacks.onHighlight(attached && !handle.pageScroll ? element : null, 'success');
|
|
1770
|
+
this.callbacks.onHighlight(attached && !handle.pageScroll ? element : null, navigationReceipt && navigationReceipt.outcome !== 'opened' ? 'failure' : 'success');
|
|
1690
1771
|
return {
|
|
1691
1772
|
...base,
|
|
1692
|
-
status: 'executed',
|
|
1773
|
+
status: navigationReceipt && navigationReceipt.outcome !== 'opened' ? 'blocked' : 'executed',
|
|
1693
1774
|
observed: {
|
|
1694
1775
|
url: safePageUrl(),
|
|
1695
1776
|
revision: snapshot.revision,
|
|
@@ -1698,19 +1779,24 @@ class PageContextCollector {
|
|
|
1698
1779
|
: grant.action === 'type' || grant.action === 'select' || before !== after || handle.pageScroll
|
|
1699
1780
|
? 'changed'
|
|
1700
1781
|
: 'unchanged',
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
? '
|
|
1705
|
-
:
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1782
|
+
...(navigationReceipt ? { navigation: navigationReceipt } : {}),
|
|
1783
|
+
safeSummary: navigationReceipt
|
|
1784
|
+
? navigationReceipt.outcome === 'opened'
|
|
1785
|
+
? 'Opened a separate tab for the exact approved link. Its contents and arrival are not observed.'
|
|
1786
|
+
: 'The new tab could not be confirmed. No fallback navigation was attempted.'
|
|
1787
|
+
: handle.pageScroll
|
|
1788
|
+
? `The page moved ${handle.pageScroll} by ${Math.round(Math.abs(window.scrollY - scrollStartY))} pixels.`
|
|
1789
|
+
: grant.action === 'scroll'
|
|
1790
|
+
? 'The requested target is visible. This confirms target visibility, not a directional page scroll.'
|
|
1791
|
+
: grant.action === 'type'
|
|
1792
|
+
? 'The requested text was applied to the approved field.'
|
|
1793
|
+
: grant.action === 'select'
|
|
1794
|
+
? 'The requested option was applied to the approved field.'
|
|
1795
|
+
: grant.desiredChecked !== undefined
|
|
1796
|
+
? 'The requested checkbox state was applied.'
|
|
1797
|
+
: grant.desiredExpanded !== undefined || grant.desiredPressed !== undefined
|
|
1798
|
+
? 'The requested button state was applied.'
|
|
1799
|
+
: `${grant.action} executed on the approved ${roleFor(element)} target.`,
|
|
1714
1800
|
...(checked !== undefined ? { checked } : {}),
|
|
1715
1801
|
...(this.statefulButtonActionProof && expanded !== undefined ? { expanded } : {}),
|
|
1716
1802
|
...(this.statefulButtonActionProof && pressed !== undefined ? { pressed } : {}),
|
|
@@ -1718,6 +1804,7 @@ class PageContextCollector {
|
|
|
1718
1804
|
};
|
|
1719
1805
|
}
|
|
1720
1806
|
catch {
|
|
1807
|
+
this.deferredNavigation = null;
|
|
1721
1808
|
this.callbacks.onHighlight(element, 'failure');
|
|
1722
1809
|
return {
|
|
1723
1810
|
...base,
|
|
@@ -1725,8 +1812,12 @@ class PageContextCollector {
|
|
|
1725
1812
|
observed: {
|
|
1726
1813
|
url: safePageUrl(),
|
|
1727
1814
|
revision: this.revision,
|
|
1728
|
-
targetState: element.isConnected ? 'unchanged' : 'detached',
|
|
1729
|
-
|
|
1815
|
+
targetState: navigationReceipt ? 'unknown' : element.isConnected ? 'unchanged' : 'detached',
|
|
1816
|
+
...(navigationReceipt ? { navigation: { url: navigationReceipt.url,
|
|
1817
|
+
outcome: 'unconfirmed' } } : {}),
|
|
1818
|
+
safeSummary: navigationReceipt
|
|
1819
|
+
? 'The link action could not be fully confirmed. Do not retry automatically.'
|
|
1820
|
+
: 'The approved page action failed safely.',
|
|
1730
1821
|
},
|
|
1731
1822
|
};
|
|
1732
1823
|
}
|
|
@@ -1786,9 +1877,8 @@ class PageContextCollector {
|
|
|
1786
1877
|
return 'rejected';
|
|
1787
1878
|
}
|
|
1788
1879
|
if (window.location.href !== navigation.fromUrl) {
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
: 'rejected';
|
|
1880
|
+
// Arrival before dispatch is not evidence that this action navigated.
|
|
1881
|
+
return 'rejected';
|
|
1792
1882
|
}
|
|
1793
1883
|
this.captureSnapshot(false);
|
|
1794
1884
|
const handle = this.targets.get(navigation.targetId);
|
|
@@ -1804,7 +1894,7 @@ class PageContextCollector {
|
|
|
1804
1894
|
target.origin !== window.location.origin ||
|
|
1805
1895
|
safeDestinationPath(navigation.source) !== target.pathname ||
|
|
1806
1896
|
linkBrowsingTarget(navigation.source) !== navigation.browsingTarget ||
|
|
1807
|
-
!supportsCurrentTabNavigation(navigation.source) ||
|
|
1897
|
+
!supportsCurrentTabNavigation(navigation.source) || navigation.browsingTarget === '_blank' ||
|
|
1808
1898
|
!isRendered(navigation.source) ||
|
|
1809
1899
|
!isVisible(navigation.source) ||
|
|
1810
1900
|
!enabled(navigation.source) ||
|
|
@@ -1818,9 +1908,6 @@ class PageContextCollector {
|
|
|
1818
1908
|
if (current?.actionId !== navigation.actionId)
|
|
1819
1909
|
return;
|
|
1820
1910
|
this.releasedNavigationSettlement = null;
|
|
1821
|
-
if (this.executingActionId === navigation.actionId) {
|
|
1822
|
-
this.executingActionId = null;
|
|
1823
|
-
}
|
|
1824
1911
|
resolve('timed_out');
|
|
1825
1912
|
}, RELEASED_NAVIGATION_SETTLEMENT_TIMEOUT_MS);
|
|
1826
1913
|
this.releasedNavigationSettlement = {
|
|
@@ -1831,16 +1918,8 @@ class PageContextCollector {
|
|
|
1831
1918
|
resolve,
|
|
1832
1919
|
};
|
|
1833
1920
|
});
|
|
1834
|
-
const useCurrentTab = navigation.browsingTarget === '_blank';
|
|
1835
1921
|
try {
|
|
1836
|
-
|
|
1837
|
-
// approved URL only after durable release; the normal document-navigation
|
|
1838
|
-
// recovery handles the new page. Dispatching their handlers can open a
|
|
1839
|
-
// popup even if the DOM target is temporarily changed (e.g. React props).
|
|
1840
|
-
if (useCurrentTab) {
|
|
1841
|
-
window.location.assign(navigation.toUrl);
|
|
1842
|
-
}
|
|
1843
|
-
else if (navigation.source.closest(FRAMER_GENERATED_PAGE_SELECTOR) &&
|
|
1922
|
+
if (navigation.source.closest(FRAMER_GENERATED_PAGE_SELECTOR) &&
|
|
1844
1923
|
typeof window.PointerEvent === 'function') {
|
|
1845
1924
|
const preventNativeNavigation = (event) => {
|
|
1846
1925
|
if (event.target === navigation.source ||
|
|
@@ -1878,7 +1957,7 @@ class PageContextCollector {
|
|
|
1878
1957
|
// failure must not turn the acknowledged action into a failed click.
|
|
1879
1958
|
}
|
|
1880
1959
|
}
|
|
1881
|
-
else if (navigationSettled
|
|
1960
|
+
else if (navigationSettled !== 'settled' &&
|
|
1882
1961
|
this.executingActionId === navigation.actionId) {
|
|
1883
1962
|
// A router may consume the exact click but send the browser somewhere
|
|
1884
1963
|
// other than the approved destination. Complete the context-side action
|
|
@@ -1916,7 +1995,7 @@ class PageContextCollector {
|
|
|
1916
1995
|
}
|
|
1917
1996
|
}
|
|
1918
1997
|
if (desiredExpanded !== undefined) {
|
|
1919
|
-
const expanded =
|
|
1998
|
+
const expanded = this.expandedState(element);
|
|
1920
1999
|
if (roleFor(element) !== 'button' || expanded === undefined) {
|
|
1921
2000
|
throw new Error('Desired expanded state requires an expandable button target.');
|
|
1922
2001
|
}
|
|
@@ -1931,6 +2010,10 @@ class PageContextCollector {
|
|
|
1931
2010
|
}
|
|
1932
2011
|
if (desiredStates.length > 0 && desiredStates.every(Boolean))
|
|
1933
2012
|
return;
|
|
2013
|
+
if (desiredExpanded === true && framerNavigationRoot(element)) {
|
|
2014
|
+
dispatchFramerPrimaryPointerSequence(element, this.trustedGuideRoot);
|
|
2015
|
+
return;
|
|
2016
|
+
}
|
|
1934
2017
|
if (desiredChecked !== undefined && framerToggleDescriptor(element)) {
|
|
1935
2018
|
dispatchFramerTogglePrimaryPointer(element, this.trustedGuideRoot);
|
|
1936
2019
|
return;
|
|
@@ -1941,11 +2024,25 @@ class PageContextCollector {
|
|
|
1941
2024
|
safeDestinationPath(element) !== expectedDestinationPath) {
|
|
1942
2025
|
throw new Error('The navigation destination changed before execution.');
|
|
1943
2026
|
}
|
|
1944
|
-
|
|
1945
|
-
|
|
2027
|
+
const navigation = publicNavigation(element);
|
|
2028
|
+
if (!navigation || canonicalJson(navigation) !== canonicalJson(handle.target.navigation)) {
|
|
2029
|
+
throw new Error('The public link destination or browsing target changed.');
|
|
1946
2030
|
}
|
|
1947
|
-
if (
|
|
1948
|
-
|
|
2031
|
+
if (navigation.target === '_blank' || target.origin !== window.location.origin) {
|
|
2032
|
+
// Keep external pages outside this session's origin/credential boundary.
|
|
2033
|
+
// Only the browser's native noreferrer feature can prevent the source
|
|
2034
|
+
// URL from becoming the Referer on this navigation. With noopener the
|
|
2035
|
+
// return value may be null even when the tab opens, so never call a
|
|
2036
|
+
// null result "blocked" or try the link a second time.
|
|
2037
|
+
try {
|
|
2038
|
+
const popup = window.open(navigation.url, '_blank', 'noopener,noreferrer');
|
|
2039
|
+
return { url: navigation.url, outcome: popup && !popup.closed ? 'opened' : 'unconfirmed' };
|
|
2040
|
+
}
|
|
2041
|
+
catch {
|
|
2042
|
+
// Some browsers throw when popups are disallowed. The exact URL was
|
|
2043
|
+
// attempted at most once, and there is no fallback navigation.
|
|
2044
|
+
return { url: navigation.url, outcome: 'unconfirmed' };
|
|
2045
|
+
}
|
|
1949
2046
|
}
|
|
1950
2047
|
const fromUrl = window.location.href;
|
|
1951
2048
|
if (window.location.href === fromUrl && target.href !== fromUrl) {
|
|
@@ -2043,11 +2140,26 @@ class PageContextCollector {
|
|
|
2043
2140
|
}
|
|
2044
2141
|
form.requestSubmit(submitter);
|
|
2045
2142
|
}
|
|
2143
|
+
exposedNavigationLinks(nav) {
|
|
2144
|
+
const links = nav.querySelectorAll('a[href]');
|
|
2145
|
+
if (links.length > MAX_TARGETS)
|
|
2146
|
+
return new Set();
|
|
2147
|
+
return new Set(Array.from(links).filter(link => isVisible(link) && publicNavigation(link) &&
|
|
2148
|
+
!isCovered(link, this.trustedGuideRoot)));
|
|
2149
|
+
}
|
|
2150
|
+
expandedState(element) {
|
|
2151
|
+
const explicit = literalBooleanAttribute(element, 'aria-expanded');
|
|
2152
|
+
if (explicit !== undefined)
|
|
2153
|
+
return explicit;
|
|
2154
|
+
if (!framerNavigationRoot(element))
|
|
2155
|
+
return undefined;
|
|
2156
|
+
return [...(this.navigationDisclosureLinks.get(element) ?? [])].some(link => link.isConnected && isRendered(link));
|
|
2157
|
+
}
|
|
2046
2158
|
safeState(element) {
|
|
2047
2159
|
return JSON.stringify({
|
|
2048
2160
|
connected: element.isConnected,
|
|
2049
2161
|
disabled: !enabled(element),
|
|
2050
|
-
expanded: element.getAttribute('aria-expanded'),
|
|
2162
|
+
expanded: this.expandedState(element) ?? element.getAttribute('aria-expanded'),
|
|
2051
2163
|
pressed: element.getAttribute('aria-pressed'),
|
|
2052
2164
|
checked: checkedStateFor(element),
|
|
2053
2165
|
selectedIndex: element instanceof HTMLSelectElement ? element.selectedIndex : undefined,
|
|
@@ -2070,6 +2182,7 @@ class PageContextCollector {
|
|
|
2070
2182
|
let documentLeft = expectedHandle.element.getBoundingClientRect().left + window.scrollX;
|
|
2071
2183
|
let layoutCorrections = 0;
|
|
2072
2184
|
let priorScrollY = null;
|
|
2185
|
+
let intersectedViewport = false;
|
|
2073
2186
|
let interrupted = false;
|
|
2074
2187
|
const interrupt = () => {
|
|
2075
2188
|
interrupted = true;
|
|
@@ -2143,10 +2256,25 @@ class PageContextCollector {
|
|
|
2143
2256
|
if (!strictlyIntersectsViewport(rect)) {
|
|
2144
2257
|
priorRect = null;
|
|
2145
2258
|
stableSince = null;
|
|
2146
|
-
//
|
|
2147
|
-
//
|
|
2148
|
-
//
|
|
2149
|
-
|
|
2259
|
+
// A public-link reveal performs exactly one native scroll. If layout
|
|
2260
|
+
// shifts move that link away again, stop instead of chasing it and
|
|
2261
|
+
// accidentally scrolling two or three more times. Other controls
|
|
2262
|
+
// retain the existing bounded lazy-layout correction.
|
|
2263
|
+
if (expectedHandle.target.consequence === 'navigation' && intersectedViewport) {
|
|
2264
|
+
this.captureSnapshot(false);
|
|
2265
|
+
const current = this.targets.get(grant.targetId);
|
|
2266
|
+
if (!current ||
|
|
2267
|
+
current.element !== expectedHandle.element ||
|
|
2268
|
+
actionTargetFingerprintBinding(current.target) !==
|
|
2269
|
+
actionTargetFingerprintBinding(expectedHandle.target) ||
|
|
2270
|
+
grant.pageId !== this.pageId ||
|
|
2271
|
+
grant.pageUrl !== safePageUrl() ||
|
|
2272
|
+
grant.policyVersion !== this.policyVersion) {
|
|
2273
|
+
return { status: 'target_changed' };
|
|
2274
|
+
}
|
|
2275
|
+
return { status: 'unavailable' };
|
|
2276
|
+
}
|
|
2277
|
+
if (layoutShifted && expectedHandle.target.consequence !== 'navigation' &&
|
|
2150
2278
|
layoutCorrections < MAX_SCROLL_LAYOUT_CORRECTIONS) {
|
|
2151
2279
|
this.captureSnapshot(false);
|
|
2152
2280
|
const current = this.targets.get(grant.targetId);
|
|
@@ -2171,6 +2299,7 @@ class PageContextCollector {
|
|
|
2171
2299
|
}
|
|
2172
2300
|
continue;
|
|
2173
2301
|
}
|
|
2302
|
+
intersectedViewport = true;
|
|
2174
2303
|
if (!priorRect || !sameSettledRect(priorRect, rect))
|
|
2175
2304
|
stableSince = Date.now();
|
|
2176
2305
|
priorRect = rect;
|
|
@@ -2193,7 +2322,7 @@ class PageContextCollector {
|
|
|
2193
2322
|
return checkedStateFor(element) === grant.desiredChecked;
|
|
2194
2323
|
}
|
|
2195
2324
|
if (grant.desiredExpanded !== undefined &&
|
|
2196
|
-
|
|
2325
|
+
this.expandedState(element) !== grant.desiredExpanded) {
|
|
2197
2326
|
return false;
|
|
2198
2327
|
}
|
|
2199
2328
|
if (grant.desiredPressed !== undefined &&
|
|
@@ -2376,6 +2505,7 @@ class PageContextCollector {
|
|
|
2376
2505
|
resetBoundTargetIds() {
|
|
2377
2506
|
this.targetIds = new WeakMap();
|
|
2378
2507
|
this.navigationTargetBindings = new WeakMap();
|
|
2508
|
+
this.navigationDisclosureLinks = new WeakMap();
|
|
2379
2509
|
this.pageScrollIds = new WeakMap();
|
|
2380
2510
|
this.fieldTargetBindings = new WeakMap();
|
|
2381
2511
|
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.9";
|
|
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 = {
|
package/cjs/types.d.ts
CHANGED
|
@@ -332,6 +332,10 @@ export type SanitizedTarget = {
|
|
|
332
332
|
autocomplete?: string;
|
|
333
333
|
semanticRegion?: string;
|
|
334
334
|
navigationMenu?: true;
|
|
335
|
+
navigation?: {
|
|
336
|
+
url: string;
|
|
337
|
+
target: '_self' | '_blank';
|
|
338
|
+
};
|
|
335
339
|
destinationPath?: string;
|
|
336
340
|
sensitivity: Sensitivity;
|
|
337
341
|
protectedRegion?: boolean;
|
|
@@ -441,6 +445,10 @@ export type ActionExecutionResult = {
|
|
|
441
445
|
revision: number;
|
|
442
446
|
targetState: "unchanged" | "changed" | "detached" | "unknown";
|
|
443
447
|
safeSummary: string;
|
|
448
|
+
navigation?: {
|
|
449
|
+
url: string;
|
|
450
|
+
outcome: 'opened' | 'blocked' | 'unconfirmed';
|
|
451
|
+
};
|
|
444
452
|
checked?: boolean;
|
|
445
453
|
expanded?: boolean;
|
|
446
454
|
pressed?: boolean;
|