@oxvo/ai-live-assist 7.5.8 → 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 +182 -74
- 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 +182 -74
- 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/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.9";
|
|
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' &&
|
|
@@ -1416,12 +1452,13 @@ export class PageContextCollector {
|
|
|
1416
1452
|
grant.targetId.startsWith(`${BOUND_FIELD_TARGET_PREFIX}_`) &&
|
|
1417
1453
|
Boolean(current && isNativeEditableField(current.element) && this.fieldTargetBindings.has(current.element));
|
|
1418
1454
|
const stablePageScroll = (current) => grant.action === 'scroll' && Boolean(current?.pageScroll && current.element === document.scrollingElement);
|
|
1455
|
+
const stableNavigation = (current) => (grant.action === 'click' || grant.action === 'scroll') &&
|
|
1456
|
+
grant.targetId.startsWith(`${BOUND_NAVIGATION_TARGET_PREFIX}_`) &&
|
|
1457
|
+
current?.element instanceof HTMLAnchorElement &&
|
|
1458
|
+
current.target.consequence === 'navigation' && current.target.navigation !== undefined;
|
|
1419
1459
|
const revisionMatchesOrStableTarget = () => grant.domRevision === this.revision ||
|
|
1420
1460
|
(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')));
|
|
1461
|
+
(stableField(handle) || stablePageScroll(handle) || stableNavigation(handle)));
|
|
1425
1462
|
if (expiresAt <= Date.now() ||
|
|
1426
1463
|
grant.pageId !== this.pageId ||
|
|
1427
1464
|
grant.pageUrl !== safePageUrl() ||
|
|
@@ -1458,7 +1495,7 @@ export class PageContextCollector {
|
|
|
1458
1495
|
grant.pageUrl !== safePageUrl() ||
|
|
1459
1496
|
grant.policyVersion !== this.policyVersion ||
|
|
1460
1497
|
!revisionMatchesOrStableTarget() ||
|
|
1461
|
-
(handle !== fingerprintedHandle && !((stableField(handle) || stablePageScroll(handle)) &&
|
|
1498
|
+
(handle !== fingerprintedHandle && !((stableField(handle) || stablePageScroll(handle) || stableNavigation(handle)) &&
|
|
1462
1499
|
handle?.element === fingerprintedHandle.element &&
|
|
1463
1500
|
actionTargetFingerprintBinding(handle.target) === actionTargetFingerprintBinding(fingerprintedHandle.target))) ||
|
|
1464
1501
|
!handle ||
|
|
@@ -1544,6 +1581,9 @@ export class PageContextCollector {
|
|
|
1544
1581
|
this.releasedNavigation = null;
|
|
1545
1582
|
this.finishReleasedNavigationSettlement('rejected');
|
|
1546
1583
|
let postActionSnapshot = null;
|
|
1584
|
+
let navigationReceipt;
|
|
1585
|
+
const disclosureRoot = handle.target.navigationMenu ? framerNavigationRoot(element) : null;
|
|
1586
|
+
const priorDisclosureLinks = disclosureRoot ? this.exposedNavigationLinks(disclosureRoot) : null;
|
|
1547
1587
|
try {
|
|
1548
1588
|
switch (grant.action) {
|
|
1549
1589
|
case 'scroll': {
|
|
@@ -1654,7 +1694,7 @@ export class PageContextCollector {
|
|
|
1654
1694
|
element.focus({ preventScroll: false });
|
|
1655
1695
|
break;
|
|
1656
1696
|
case 'click':
|
|
1657
|
-
this.click(element, handle, grant.actionId, grant.desiredChecked, grant.desiredExpanded, grant.desiredPressed, grant.destinationPath);
|
|
1697
|
+
navigationReceipt = this.click(element, handle, grant.actionId, grant.desiredChecked, grant.desiredExpanded, grant.desiredPressed, grant.destinationPath);
|
|
1658
1698
|
break;
|
|
1659
1699
|
case 'type':
|
|
1660
1700
|
this.type(element, grant.value ?? '');
|
|
@@ -1676,9 +1716,28 @@ export class PageContextCollector {
|
|
|
1676
1716
|
if (grant.action !== 'scroll') {
|
|
1677
1717
|
await new Promise((resolve) => setTimeout(resolve, 80));
|
|
1678
1718
|
}
|
|
1719
|
+
if (handle.target.navigationMenu && grant.action === 'click') {
|
|
1720
|
+
const deadline = Date.now() + 1000;
|
|
1721
|
+
for (let frames = 0; frames < 60 && Date.now() < deadline && element.isConnected && !executionStale(); frames++) {
|
|
1722
|
+
if (disclosureRoot && priorDisclosureLinks) {
|
|
1723
|
+
const added = new Set([...this.exposedNavigationLinks(disclosureRoot)]
|
|
1724
|
+
.filter(link => !priorDisclosureLinks.has(link)));
|
|
1725
|
+
if (added.size)
|
|
1726
|
+
this.navigationDisclosureLinks.set(element, added);
|
|
1727
|
+
}
|
|
1728
|
+
if (this.actionApplied(grant, element, handle))
|
|
1729
|
+
break;
|
|
1730
|
+
await nextAnimationFrameOrTimeout(Math.min(50, deadline - Date.now()));
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1679
1733
|
const postActionStale = executionStale();
|
|
1680
|
-
if (postActionStale)
|
|
1681
|
-
|
|
1734
|
+
if (postActionStale) {
|
|
1735
|
+
this.deferredNavigation = null;
|
|
1736
|
+
return navigationReceipt ? { ...postActionStale, observed: {
|
|
1737
|
+
...postActionStale.observed, targetState: 'unknown',
|
|
1738
|
+
navigation: { url: navigationReceipt.url, outcome: 'unconfirmed' },
|
|
1739
|
+
} } : postActionStale;
|
|
1740
|
+
}
|
|
1682
1741
|
if (!this.actionApplied(grant, element, handle)) {
|
|
1683
1742
|
throw new Error('The approved value was not applied.');
|
|
1684
1743
|
}
|
|
@@ -1698,16 +1757,16 @@ export class PageContextCollector {
|
|
|
1698
1757
|
const attached = element.isConnected;
|
|
1699
1758
|
const after = attached ? this.safeState(element) : '';
|
|
1700
1759
|
const expanded = attached
|
|
1701
|
-
?
|
|
1760
|
+
? this.expandedState(element)
|
|
1702
1761
|
: undefined;
|
|
1703
1762
|
const pressed = attached
|
|
1704
1763
|
? literalBooleanAttribute(element, 'aria-pressed')
|
|
1705
1764
|
: undefined;
|
|
1706
1765
|
const checked = attached ? checkedStateFor(element) : undefined;
|
|
1707
|
-
this.callbacks.onHighlight(attached && !handle.pageScroll ? element : null, 'success');
|
|
1766
|
+
this.callbacks.onHighlight(attached && !handle.pageScroll ? element : null, navigationReceipt && navigationReceipt.outcome !== 'opened' ? 'failure' : 'success');
|
|
1708
1767
|
return {
|
|
1709
1768
|
...base,
|
|
1710
|
-
status: 'executed',
|
|
1769
|
+
status: navigationReceipt && navigationReceipt.outcome !== 'opened' ? 'blocked' : 'executed',
|
|
1711
1770
|
observed: {
|
|
1712
1771
|
url: safePageUrl(),
|
|
1713
1772
|
revision: snapshot.revision,
|
|
@@ -1716,19 +1775,24 @@ export class PageContextCollector {
|
|
|
1716
1775
|
: grant.action === 'type' || grant.action === 'select' || before !== after || handle.pageScroll
|
|
1717
1776
|
? 'changed'
|
|
1718
1777
|
: 'unchanged',
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
? '
|
|
1723
|
-
:
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1778
|
+
...(navigationReceipt ? { navigation: navigationReceipt } : {}),
|
|
1779
|
+
safeSummary: navigationReceipt
|
|
1780
|
+
? navigationReceipt.outcome === 'opened'
|
|
1781
|
+
? 'Opened a separate tab for the exact approved link. Its contents and arrival are not observed.'
|
|
1782
|
+
: 'The new tab could not be confirmed. No fallback navigation was attempted.'
|
|
1783
|
+
: handle.pageScroll
|
|
1784
|
+
? `The page moved ${handle.pageScroll} by ${Math.round(Math.abs(window.scrollY - scrollStartY))} pixels.`
|
|
1785
|
+
: grant.action === 'scroll'
|
|
1786
|
+
? 'The requested target is visible. This confirms target visibility, not a directional page scroll.'
|
|
1787
|
+
: grant.action === 'type'
|
|
1788
|
+
? 'The requested text was applied to the approved field.'
|
|
1789
|
+
: grant.action === 'select'
|
|
1790
|
+
? 'The requested option was applied to the approved field.'
|
|
1791
|
+
: grant.desiredChecked !== undefined
|
|
1792
|
+
? 'The requested checkbox state was applied.'
|
|
1793
|
+
: grant.desiredExpanded !== undefined || grant.desiredPressed !== undefined
|
|
1794
|
+
? 'The requested button state was applied.'
|
|
1795
|
+
: `${grant.action} executed on the approved ${roleFor(element)} target.`,
|
|
1732
1796
|
...(checked !== undefined ? { checked } : {}),
|
|
1733
1797
|
...(this.statefulButtonActionProof && expanded !== undefined ? { expanded } : {}),
|
|
1734
1798
|
...(this.statefulButtonActionProof && pressed !== undefined ? { pressed } : {}),
|
|
@@ -1736,6 +1800,7 @@ export class PageContextCollector {
|
|
|
1736
1800
|
};
|
|
1737
1801
|
}
|
|
1738
1802
|
catch {
|
|
1803
|
+
this.deferredNavigation = null;
|
|
1739
1804
|
this.callbacks.onHighlight(element, 'failure');
|
|
1740
1805
|
return {
|
|
1741
1806
|
...base,
|
|
@@ -1743,8 +1808,12 @@ export class PageContextCollector {
|
|
|
1743
1808
|
observed: {
|
|
1744
1809
|
url: safePageUrl(),
|
|
1745
1810
|
revision: this.revision,
|
|
1746
|
-
targetState: element.isConnected ? 'unchanged' : 'detached',
|
|
1747
|
-
|
|
1811
|
+
targetState: navigationReceipt ? 'unknown' : element.isConnected ? 'unchanged' : 'detached',
|
|
1812
|
+
...(navigationReceipt ? { navigation: { url: navigationReceipt.url,
|
|
1813
|
+
outcome: 'unconfirmed' } } : {}),
|
|
1814
|
+
safeSummary: navigationReceipt
|
|
1815
|
+
? 'The link action could not be fully confirmed. Do not retry automatically.'
|
|
1816
|
+
: 'The approved page action failed safely.',
|
|
1748
1817
|
},
|
|
1749
1818
|
};
|
|
1750
1819
|
}
|
|
@@ -1804,9 +1873,8 @@ export class PageContextCollector {
|
|
|
1804
1873
|
return 'rejected';
|
|
1805
1874
|
}
|
|
1806
1875
|
if (window.location.href !== navigation.fromUrl) {
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
: 'rejected';
|
|
1876
|
+
// Arrival before dispatch is not evidence that this action navigated.
|
|
1877
|
+
return 'rejected';
|
|
1810
1878
|
}
|
|
1811
1879
|
this.captureSnapshot(false);
|
|
1812
1880
|
const handle = this.targets.get(navigation.targetId);
|
|
@@ -1822,7 +1890,7 @@ export class PageContextCollector {
|
|
|
1822
1890
|
target.origin !== window.location.origin ||
|
|
1823
1891
|
safeDestinationPath(navigation.source) !== target.pathname ||
|
|
1824
1892
|
linkBrowsingTarget(navigation.source) !== navigation.browsingTarget ||
|
|
1825
|
-
!supportsCurrentTabNavigation(navigation.source) ||
|
|
1893
|
+
!supportsCurrentTabNavigation(navigation.source) || navigation.browsingTarget === '_blank' ||
|
|
1826
1894
|
!isRendered(navigation.source) ||
|
|
1827
1895
|
!isVisible(navigation.source) ||
|
|
1828
1896
|
!enabled(navigation.source) ||
|
|
@@ -1836,9 +1904,6 @@ export class PageContextCollector {
|
|
|
1836
1904
|
if (current?.actionId !== navigation.actionId)
|
|
1837
1905
|
return;
|
|
1838
1906
|
this.releasedNavigationSettlement = null;
|
|
1839
|
-
if (this.executingActionId === navigation.actionId) {
|
|
1840
|
-
this.executingActionId = null;
|
|
1841
|
-
}
|
|
1842
1907
|
resolve('timed_out');
|
|
1843
1908
|
}, RELEASED_NAVIGATION_SETTLEMENT_TIMEOUT_MS);
|
|
1844
1909
|
this.releasedNavigationSettlement = {
|
|
@@ -1849,16 +1914,8 @@ export class PageContextCollector {
|
|
|
1849
1914
|
resolve,
|
|
1850
1915
|
};
|
|
1851
1916
|
});
|
|
1852
|
-
const useCurrentTab = navigation.browsingTarget === '_blank';
|
|
1853
1917
|
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) &&
|
|
1918
|
+
if (navigation.source.closest(FRAMER_GENERATED_PAGE_SELECTOR) &&
|
|
1862
1919
|
typeof window.PointerEvent === 'function') {
|
|
1863
1920
|
const preventNativeNavigation = (event) => {
|
|
1864
1921
|
if (event.target === navigation.source ||
|
|
@@ -1896,7 +1953,7 @@ export class PageContextCollector {
|
|
|
1896
1953
|
// failure must not turn the acknowledged action into a failed click.
|
|
1897
1954
|
}
|
|
1898
1955
|
}
|
|
1899
|
-
else if (navigationSettled
|
|
1956
|
+
else if (navigationSettled !== 'settled' &&
|
|
1900
1957
|
this.executingActionId === navigation.actionId) {
|
|
1901
1958
|
// A router may consume the exact click but send the browser somewhere
|
|
1902
1959
|
// other than the approved destination. Complete the context-side action
|
|
@@ -1934,7 +1991,7 @@ export class PageContextCollector {
|
|
|
1934
1991
|
}
|
|
1935
1992
|
}
|
|
1936
1993
|
if (desiredExpanded !== undefined) {
|
|
1937
|
-
const expanded =
|
|
1994
|
+
const expanded = this.expandedState(element);
|
|
1938
1995
|
if (roleFor(element) !== 'button' || expanded === undefined) {
|
|
1939
1996
|
throw new Error('Desired expanded state requires an expandable button target.');
|
|
1940
1997
|
}
|
|
@@ -1949,6 +2006,10 @@ export class PageContextCollector {
|
|
|
1949
2006
|
}
|
|
1950
2007
|
if (desiredStates.length > 0 && desiredStates.every(Boolean))
|
|
1951
2008
|
return;
|
|
2009
|
+
if (desiredExpanded === true && framerNavigationRoot(element)) {
|
|
2010
|
+
dispatchFramerPrimaryPointerSequence(element, this.trustedGuideRoot);
|
|
2011
|
+
return;
|
|
2012
|
+
}
|
|
1952
2013
|
if (desiredChecked !== undefined && framerToggleDescriptor(element)) {
|
|
1953
2014
|
dispatchFramerTogglePrimaryPointer(element, this.trustedGuideRoot);
|
|
1954
2015
|
return;
|
|
@@ -1959,11 +2020,25 @@ export class PageContextCollector {
|
|
|
1959
2020
|
safeDestinationPath(element) !== expectedDestinationPath) {
|
|
1960
2021
|
throw new Error('The navigation destination changed before execution.');
|
|
1961
2022
|
}
|
|
1962
|
-
|
|
1963
|
-
|
|
2023
|
+
const navigation = publicNavigation(element);
|
|
2024
|
+
if (!navigation || canonicalJson(navigation) !== canonicalJson(handle.target.navigation)) {
|
|
2025
|
+
throw new Error('The public link destination or browsing target changed.');
|
|
1964
2026
|
}
|
|
1965
|
-
if (
|
|
1966
|
-
|
|
2027
|
+
if (navigation.target === '_blank' || target.origin !== window.location.origin) {
|
|
2028
|
+
// Keep external pages outside this session's origin/credential boundary.
|
|
2029
|
+
// Only the browser's native noreferrer feature can prevent the source
|
|
2030
|
+
// URL from becoming the Referer on this navigation. With noopener the
|
|
2031
|
+
// return value may be null even when the tab opens, so never call a
|
|
2032
|
+
// null result "blocked" or try the link a second time.
|
|
2033
|
+
try {
|
|
2034
|
+
const popup = window.open(navigation.url, '_blank', 'noopener,noreferrer');
|
|
2035
|
+
return { url: navigation.url, outcome: popup && !popup.closed ? 'opened' : 'unconfirmed' };
|
|
2036
|
+
}
|
|
2037
|
+
catch {
|
|
2038
|
+
// Some browsers throw when popups are disallowed. The exact URL was
|
|
2039
|
+
// attempted at most once, and there is no fallback navigation.
|
|
2040
|
+
return { url: navigation.url, outcome: 'unconfirmed' };
|
|
2041
|
+
}
|
|
1967
2042
|
}
|
|
1968
2043
|
const fromUrl = window.location.href;
|
|
1969
2044
|
if (window.location.href === fromUrl && target.href !== fromUrl) {
|
|
@@ -2061,11 +2136,26 @@ export class PageContextCollector {
|
|
|
2061
2136
|
}
|
|
2062
2137
|
form.requestSubmit(submitter);
|
|
2063
2138
|
}
|
|
2139
|
+
exposedNavigationLinks(nav) {
|
|
2140
|
+
const links = nav.querySelectorAll('a[href]');
|
|
2141
|
+
if (links.length > MAX_TARGETS)
|
|
2142
|
+
return new Set();
|
|
2143
|
+
return new Set(Array.from(links).filter(link => isVisible(link) && publicNavigation(link) &&
|
|
2144
|
+
!isCovered(link, this.trustedGuideRoot)));
|
|
2145
|
+
}
|
|
2146
|
+
expandedState(element) {
|
|
2147
|
+
const explicit = literalBooleanAttribute(element, 'aria-expanded');
|
|
2148
|
+
if (explicit !== undefined)
|
|
2149
|
+
return explicit;
|
|
2150
|
+
if (!framerNavigationRoot(element))
|
|
2151
|
+
return undefined;
|
|
2152
|
+
return [...(this.navigationDisclosureLinks.get(element) ?? [])].some(link => link.isConnected && isRendered(link));
|
|
2153
|
+
}
|
|
2064
2154
|
safeState(element) {
|
|
2065
2155
|
return JSON.stringify({
|
|
2066
2156
|
connected: element.isConnected,
|
|
2067
2157
|
disabled: !enabled(element),
|
|
2068
|
-
expanded: element.getAttribute('aria-expanded'),
|
|
2158
|
+
expanded: this.expandedState(element) ?? element.getAttribute('aria-expanded'),
|
|
2069
2159
|
pressed: element.getAttribute('aria-pressed'),
|
|
2070
2160
|
checked: checkedStateFor(element),
|
|
2071
2161
|
selectedIndex: element instanceof HTMLSelectElement ? element.selectedIndex : undefined,
|
|
@@ -2088,6 +2178,7 @@ export class PageContextCollector {
|
|
|
2088
2178
|
let documentLeft = expectedHandle.element.getBoundingClientRect().left + window.scrollX;
|
|
2089
2179
|
let layoutCorrections = 0;
|
|
2090
2180
|
let priorScrollY = null;
|
|
2181
|
+
let intersectedViewport = false;
|
|
2091
2182
|
let interrupted = false;
|
|
2092
2183
|
const interrupt = () => {
|
|
2093
2184
|
interrupted = true;
|
|
@@ -2161,10 +2252,25 @@ export class PageContextCollector {
|
|
|
2161
2252
|
if (!strictlyIntersectsViewport(rect)) {
|
|
2162
2253
|
priorRect = null;
|
|
2163
2254
|
stableSince = null;
|
|
2164
|
-
//
|
|
2165
|
-
//
|
|
2166
|
-
//
|
|
2167
|
-
|
|
2255
|
+
// A public-link reveal performs exactly one native scroll. If layout
|
|
2256
|
+
// shifts move that link away again, stop instead of chasing it and
|
|
2257
|
+
// accidentally scrolling two or three more times. Other controls
|
|
2258
|
+
// retain the existing bounded lazy-layout correction.
|
|
2259
|
+
if (expectedHandle.target.consequence === 'navigation' && intersectedViewport) {
|
|
2260
|
+
this.captureSnapshot(false);
|
|
2261
|
+
const current = this.targets.get(grant.targetId);
|
|
2262
|
+
if (!current ||
|
|
2263
|
+
current.element !== expectedHandle.element ||
|
|
2264
|
+
actionTargetFingerprintBinding(current.target) !==
|
|
2265
|
+
actionTargetFingerprintBinding(expectedHandle.target) ||
|
|
2266
|
+
grant.pageId !== this.pageId ||
|
|
2267
|
+
grant.pageUrl !== safePageUrl() ||
|
|
2268
|
+
grant.policyVersion !== this.policyVersion) {
|
|
2269
|
+
return { status: 'target_changed' };
|
|
2270
|
+
}
|
|
2271
|
+
return { status: 'unavailable' };
|
|
2272
|
+
}
|
|
2273
|
+
if (layoutShifted && expectedHandle.target.consequence !== 'navigation' &&
|
|
2168
2274
|
layoutCorrections < MAX_SCROLL_LAYOUT_CORRECTIONS) {
|
|
2169
2275
|
this.captureSnapshot(false);
|
|
2170
2276
|
const current = this.targets.get(grant.targetId);
|
|
@@ -2189,6 +2295,7 @@ export class PageContextCollector {
|
|
|
2189
2295
|
}
|
|
2190
2296
|
continue;
|
|
2191
2297
|
}
|
|
2298
|
+
intersectedViewport = true;
|
|
2192
2299
|
if (!priorRect || !sameSettledRect(priorRect, rect))
|
|
2193
2300
|
stableSince = Date.now();
|
|
2194
2301
|
priorRect = rect;
|
|
@@ -2211,7 +2318,7 @@ export class PageContextCollector {
|
|
|
2211
2318
|
return checkedStateFor(element) === grant.desiredChecked;
|
|
2212
2319
|
}
|
|
2213
2320
|
if (grant.desiredExpanded !== undefined &&
|
|
2214
|
-
|
|
2321
|
+
this.expandedState(element) !== grant.desiredExpanded) {
|
|
2215
2322
|
return false;
|
|
2216
2323
|
}
|
|
2217
2324
|
if (grant.desiredPressed !== undefined &&
|
|
@@ -2394,6 +2501,7 @@ export class PageContextCollector {
|
|
|
2394
2501
|
resetBoundTargetIds() {
|
|
2395
2502
|
this.targetIds = new WeakMap();
|
|
2396
2503
|
this.navigationTargetBindings = new WeakMap();
|
|
2504
|
+
this.navigationDisclosureLinks = new WeakMap();
|
|
2397
2505
|
this.pageScrollIds = new WeakMap();
|
|
2398
2506
|
this.fieldTargetBindings = new WeakMap();
|
|
2399
2507
|
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/lib/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;
|
package/lib/ui.js
CHANGED
|
@@ -4882,16 +4882,15 @@ export class WidgetView {
|
|
|
4882
4882
|
this.syncSurfaceEffectOwner();
|
|
4883
4883
|
}
|
|
4884
4884
|
setRemoteStream(stream) {
|
|
4885
|
-
|
|
4886
|
-
|
|
4887
|
-
|
|
4885
|
+
if (this.destroyed || this.audio.srcObject === stream)
|
|
4886
|
+
return;
|
|
4887
|
+
// Rebinding the same stream restarts media loading, including its playout buffer.
|
|
4888
|
+
this.invalidateRemotePlayback();
|
|
4888
4889
|
this.audio.srcObject = stream;
|
|
4889
|
-
|
|
4890
|
-
|
|
4891
|
-
|
|
4892
|
-
|
|
4893
|
-
this.bindRemoteAudioTrackState(stream);
|
|
4894
|
-
}
|
|
4890
|
+
this.boundRemoteAudioStreams = new WeakSet();
|
|
4891
|
+
this.boundRemoteAudioTracks = new WeakSet();
|
|
4892
|
+
this.bindPlaybackState();
|
|
4893
|
+
this.bindRemoteAudioTrackState(stream);
|
|
4895
4894
|
void this.playRemoteAudio();
|
|
4896
4895
|
}
|
|
4897
4896
|
bindRemoteAudioTrackState(stream) {
|