@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/version.js CHANGED
@@ -1,4 +1,4 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.VERSION = void 0;
4
- exports.VERSION = '7.5.8';
4
+ exports.VERSION = '7.5.10';
@@ -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
- this.audio.srcObject = stream;
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))
@@ -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.8";
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 safeDestinationPath = (element) => {
246
- // An empty href is a control, not authority to navigate to the current URL.
247
- if (!(element instanceof HTMLAnchorElement) || !element.getAttribute('href')?.trim())
248
- return null;
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 (destination.origin !== window.location.origin ||
252
- destination.username !== '' || destination.password !== '' ||
253
- destination.search ||
254
- destination.hash ||
255
- !destination.pathname.startsWith('/') ||
256
- destination.pathname.startsWith('//') ||
257
- destination.pathname.length > 1024) {
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 null;
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 expanded = literalBooleanAttribute(element, 'aria-expanded');
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
- typeof targetIdentity.destinationPath === 'string';
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 = parent?.closest('[role="img"][aria-label]') ?? null;
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') ?? node.textContent ?? '', 500);
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) || ((grant.action === 'click' || grant.action === 'scroll') &&
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
- return postActionStale;
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
- ? literalBooleanAttribute(element, 'aria-expanded')
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
- safeSummary: handle.pageScroll
1720
- ? `The page moved ${handle.pageScroll} by ${Math.round(Math.abs(window.scrollY - scrollStartY))} pixels.`
1721
- : grant.action === 'scroll'
1722
- ? 'The requested target is visible. This confirms target visibility, not a directional page scroll.'
1723
- : grant.action === 'type'
1724
- ? 'The requested text was applied to the approved field.'
1725
- : grant.action === 'select'
1726
- ? 'The requested option was applied to the approved field.'
1727
- : grant.desiredChecked !== undefined
1728
- ? 'The requested checkbox state was applied.'
1729
- : grant.desiredExpanded !== undefined || grant.desiredPressed !== undefined
1730
- ? 'The requested button state was applied.'
1731
- : `${grant.action} executed on the approved ${roleFor(element)} target.`,
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
- safeSummary: 'The approved page action failed safely.',
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
- return sameNavigationDestination(window.location.href, navigation.toUrl)
1808
- ? 'settled'
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
- // New-window links must stay in the observable tab. Navigate the exact
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 === 'rejected' &&
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 = literalBooleanAttribute(element, 'aria-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
- if (target.origin !== window.location.origin) {
1963
- throw new Error('External navigation is blocked.');
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 (!supportsCurrentTabNavigation(element)) {
1966
- throw new Error('Named-window and download navigation is blocked.');
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
- // Lazy content can move the same approved link after it briefly enters
2165
- // view. Follow only layout shifts, never an unchanged offscreen target
2166
- // or visitor input, within the original deadline and a fixed budget.
2167
- if (layoutShifted &&
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
- literalBooleanAttribute(element, 'aria-expanded') !== grant.desiredExpanded) {
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.8";
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 = {