@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.
@@ -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.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 safeDestinationPath = (element) => {
250
- // An empty href is a control, not authority to navigate to the current URL.
251
- if (!(element instanceof HTMLAnchorElement) || !element.getAttribute('href')?.trim())
252
- return null;
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 (destination.origin !== window.location.origin ||
256
- destination.username !== '' || destination.password !== '' ||
257
- destination.search ||
258
- destination.hash ||
259
- !destination.pathname.startsWith('/') ||
260
- destination.pathname.startsWith('//') ||
261
- destination.pathname.length > 1024) {
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 null;
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
  };
@@ -768,6 +796,7 @@ class PageContextCollector {
768
796
  this.targetIds = new WeakMap();
769
797
  this.pageScrollIds = new WeakMap();
770
798
  this.navigationTargetBindings = new WeakMap();
799
+ this.navigationDisclosureLinks = new WeakMap();
771
800
  this.fieldTargetBindings = new WeakMap();
772
801
  this.fieldAppliedRevisions = new WeakMap();
773
802
  this.textIds = new WeakMap();
@@ -991,6 +1020,9 @@ class PageContextCollector {
991
1020
  'data-framer-name',
992
1021
  'disabled',
993
1022
  'href',
1023
+ 'target',
1024
+ 'download',
1025
+ 'data-highlight',
994
1026
  'hidden',
995
1027
  'inert',
996
1028
  'role',
@@ -1037,6 +1069,7 @@ class PageContextCollector {
1037
1069
  document.removeEventListener('change', this.handleControlStateChange, true);
1038
1070
  this.restoreNavigationHooks();
1039
1071
  this.targets.clear();
1072
+ this.navigationDisclosureLinks = new WeakMap();
1040
1073
  this.executingActionId = null;
1041
1074
  this.pendingNavigation = null;
1042
1075
  this.pendingCapture = false;
@@ -1077,7 +1110,8 @@ class PageContextCollector {
1077
1110
  continue;
1078
1111
  const role = roleFor(element);
1079
1112
  const hasStatefulButtonAttribute = role === 'button' &&
1080
- (element.hasAttribute('aria-expanded') || element.hasAttribute('aria-pressed'));
1113
+ (element.hasAttribute('aria-expanded') || element.hasAttribute('aria-pressed') ||
1114
+ Boolean(framerNavigationRoot(element)));
1081
1115
  const hasUnsupportedMixedPressedState = role === 'button' &&
1082
1116
  element.getAttribute('aria-pressed')?.trim().toLowerCase() === 'mixed';
1083
1117
  if (hasUnsupportedMixedPressedState ||
@@ -1114,7 +1148,8 @@ class PageContextCollector {
1114
1148
  : accessibleNameSourceSensitivity(element);
1115
1149
  const visible = isVisible(element);
1116
1150
  const destinationPath = safeDestinationPath(element);
1117
- const expanded = literalBooleanAttribute(element, 'aria-expanded');
1151
+ const navigation = publicNavigation(element);
1152
+ const expanded = this.expandedState(element);
1118
1153
  const pressed = literalBooleanAttribute(element, 'aria-pressed');
1119
1154
  const checked = checkedStateFor(element);
1120
1155
  const targetIdentity = {
@@ -1131,6 +1166,7 @@ class PageContextCollector {
1131
1166
  ...(semanticRegion ? { semanticRegion } : {}),
1132
1167
  ...(this.statefulButtonActionProof && isNavigationMenu(element, role) ? { navigationMenu: true } : {}),
1133
1168
  ...(destinationPath ? { destinationPath } : {}),
1169
+ ...(navigation ? { navigation } : {}),
1134
1170
  sensitivity,
1135
1171
  protectedRegion: sensitivity !== 'none' || regionAssessment.matchedRuleDigests.length > 0,
1136
1172
  frameOrigin: 'same_origin',
@@ -1148,7 +1184,7 @@ class PageContextCollector {
1148
1184
  };
1149
1185
  const priorTargetId = this.targetIds.get(element);
1150
1186
  const boundNavigation = targetIdentity.consequence === 'navigation' &&
1151
- typeof targetIdentity.destinationPath === 'string';
1187
+ targetIdentity.navigation !== undefined;
1152
1188
  const binding = boundNavigation ? navigationTargetBinding(targetIdentity) : null;
1153
1189
  const priorBinding = this.navigationTargetBindings.get(element);
1154
1190
  const boundField = isNativeEditableField(element) && sensitivity === 'none' &&
@@ -1420,12 +1456,13 @@ class PageContextCollector {
1420
1456
  grant.targetId.startsWith(`${BOUND_FIELD_TARGET_PREFIX}_`) &&
1421
1457
  Boolean(current && isNativeEditableField(current.element) && this.fieldTargetBindings.has(current.element));
1422
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;
1423
1463
  const revisionMatchesOrStableTarget = () => grant.domRevision === this.revision ||
1424
1464
  (grant.domRevision < this.revision &&
1425
- (stableField(handle) || stablePageScroll(handle) || ((grant.action === 'click' || grant.action === 'scroll') &&
1426
- handle?.element instanceof HTMLAnchorElement &&
1427
- handle.target.consequence === 'navigation' &&
1428
- typeof handle.target.destinationPath === 'string')));
1465
+ (stableField(handle) || stablePageScroll(handle) || stableNavigation(handle)));
1429
1466
  if (expiresAt <= Date.now() ||
1430
1467
  grant.pageId !== this.pageId ||
1431
1468
  grant.pageUrl !== safePageUrl() ||
@@ -1462,7 +1499,7 @@ class PageContextCollector {
1462
1499
  grant.pageUrl !== safePageUrl() ||
1463
1500
  grant.policyVersion !== this.policyVersion ||
1464
1501
  !revisionMatchesOrStableTarget() ||
1465
- (handle !== fingerprintedHandle && !((stableField(handle) || stablePageScroll(handle)) &&
1502
+ (handle !== fingerprintedHandle && !((stableField(handle) || stablePageScroll(handle) || stableNavigation(handle)) &&
1466
1503
  handle?.element === fingerprintedHandle.element &&
1467
1504
  actionTargetFingerprintBinding(handle.target) === actionTargetFingerprintBinding(fingerprintedHandle.target))) ||
1468
1505
  !handle ||
@@ -1548,6 +1585,9 @@ class PageContextCollector {
1548
1585
  this.releasedNavigation = null;
1549
1586
  this.finishReleasedNavigationSettlement('rejected');
1550
1587
  let postActionSnapshot = null;
1588
+ let navigationReceipt;
1589
+ const disclosureRoot = handle.target.navigationMenu ? framerNavigationRoot(element) : null;
1590
+ const priorDisclosureLinks = disclosureRoot ? this.exposedNavigationLinks(disclosureRoot) : null;
1551
1591
  try {
1552
1592
  switch (grant.action) {
1553
1593
  case 'scroll': {
@@ -1658,7 +1698,7 @@ class PageContextCollector {
1658
1698
  element.focus({ preventScroll: false });
1659
1699
  break;
1660
1700
  case 'click':
1661
- 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);
1662
1702
  break;
1663
1703
  case 'type':
1664
1704
  this.type(element, grant.value ?? '');
@@ -1680,9 +1720,28 @@ class PageContextCollector {
1680
1720
  if (grant.action !== 'scroll') {
1681
1721
  await new Promise((resolve) => setTimeout(resolve, 80));
1682
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
+ }
1683
1737
  const postActionStale = executionStale();
1684
- if (postActionStale)
1685
- return postActionStale;
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
+ }
1686
1745
  if (!this.actionApplied(grant, element, handle)) {
1687
1746
  throw new Error('The approved value was not applied.');
1688
1747
  }
@@ -1702,16 +1761,16 @@ class PageContextCollector {
1702
1761
  const attached = element.isConnected;
1703
1762
  const after = attached ? this.safeState(element) : '';
1704
1763
  const expanded = attached
1705
- ? literalBooleanAttribute(element, 'aria-expanded')
1764
+ ? this.expandedState(element)
1706
1765
  : undefined;
1707
1766
  const pressed = attached
1708
1767
  ? literalBooleanAttribute(element, 'aria-pressed')
1709
1768
  : undefined;
1710
1769
  const checked = attached ? checkedStateFor(element) : undefined;
1711
- this.callbacks.onHighlight(attached && !handle.pageScroll ? element : null, 'success');
1770
+ this.callbacks.onHighlight(attached && !handle.pageScroll ? element : null, navigationReceipt && navigationReceipt.outcome !== 'opened' ? 'failure' : 'success');
1712
1771
  return {
1713
1772
  ...base,
1714
- status: 'executed',
1773
+ status: navigationReceipt && navigationReceipt.outcome !== 'opened' ? 'blocked' : 'executed',
1715
1774
  observed: {
1716
1775
  url: safePageUrl(),
1717
1776
  revision: snapshot.revision,
@@ -1720,19 +1779,24 @@ class PageContextCollector {
1720
1779
  : grant.action === 'type' || grant.action === 'select' || before !== after || handle.pageScroll
1721
1780
  ? 'changed'
1722
1781
  : 'unchanged',
1723
- safeSummary: handle.pageScroll
1724
- ? `The page moved ${handle.pageScroll} by ${Math.round(Math.abs(window.scrollY - scrollStartY))} pixels.`
1725
- : grant.action === 'scroll'
1726
- ? 'The requested target is visible. This confirms target visibility, not a directional page scroll.'
1727
- : grant.action === 'type'
1728
- ? 'The requested text was applied to the approved field.'
1729
- : grant.action === 'select'
1730
- ? 'The requested option was applied to the approved field.'
1731
- : grant.desiredChecked !== undefined
1732
- ? 'The requested checkbox state was applied.'
1733
- : grant.desiredExpanded !== undefined || grant.desiredPressed !== undefined
1734
- ? 'The requested button state was applied.'
1735
- : `${grant.action} executed on the approved ${roleFor(element)} target.`,
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.`,
1736
1800
  ...(checked !== undefined ? { checked } : {}),
1737
1801
  ...(this.statefulButtonActionProof && expanded !== undefined ? { expanded } : {}),
1738
1802
  ...(this.statefulButtonActionProof && pressed !== undefined ? { pressed } : {}),
@@ -1740,6 +1804,7 @@ class PageContextCollector {
1740
1804
  };
1741
1805
  }
1742
1806
  catch {
1807
+ this.deferredNavigation = null;
1743
1808
  this.callbacks.onHighlight(element, 'failure');
1744
1809
  return {
1745
1810
  ...base,
@@ -1747,8 +1812,12 @@ class PageContextCollector {
1747
1812
  observed: {
1748
1813
  url: safePageUrl(),
1749
1814
  revision: this.revision,
1750
- targetState: element.isConnected ? 'unchanged' : 'detached',
1751
- safeSummary: 'The approved page action failed safely.',
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.',
1752
1821
  },
1753
1822
  };
1754
1823
  }
@@ -1808,9 +1877,8 @@ class PageContextCollector {
1808
1877
  return 'rejected';
1809
1878
  }
1810
1879
  if (window.location.href !== navigation.fromUrl) {
1811
- return sameNavigationDestination(window.location.href, navigation.toUrl)
1812
- ? 'settled'
1813
- : 'rejected';
1880
+ // Arrival before dispatch is not evidence that this action navigated.
1881
+ return 'rejected';
1814
1882
  }
1815
1883
  this.captureSnapshot(false);
1816
1884
  const handle = this.targets.get(navigation.targetId);
@@ -1826,7 +1894,7 @@ class PageContextCollector {
1826
1894
  target.origin !== window.location.origin ||
1827
1895
  safeDestinationPath(navigation.source) !== target.pathname ||
1828
1896
  linkBrowsingTarget(navigation.source) !== navigation.browsingTarget ||
1829
- !supportsCurrentTabNavigation(navigation.source) ||
1897
+ !supportsCurrentTabNavigation(navigation.source) || navigation.browsingTarget === '_blank' ||
1830
1898
  !isRendered(navigation.source) ||
1831
1899
  !isVisible(navigation.source) ||
1832
1900
  !enabled(navigation.source) ||
@@ -1840,9 +1908,6 @@ class PageContextCollector {
1840
1908
  if (current?.actionId !== navigation.actionId)
1841
1909
  return;
1842
1910
  this.releasedNavigationSettlement = null;
1843
- if (this.executingActionId === navigation.actionId) {
1844
- this.executingActionId = null;
1845
- }
1846
1911
  resolve('timed_out');
1847
1912
  }, RELEASED_NAVIGATION_SETTLEMENT_TIMEOUT_MS);
1848
1913
  this.releasedNavigationSettlement = {
@@ -1853,16 +1918,8 @@ class PageContextCollector {
1853
1918
  resolve,
1854
1919
  };
1855
1920
  });
1856
- const useCurrentTab = navigation.browsingTarget === '_blank';
1857
1921
  try {
1858
- // New-window links must stay in the observable tab. Navigate the exact
1859
- // approved URL only after durable release; the normal document-navigation
1860
- // recovery handles the new page. Dispatching their handlers can open a
1861
- // popup even if the DOM target is temporarily changed (e.g. React props).
1862
- if (useCurrentTab) {
1863
- window.location.assign(navigation.toUrl);
1864
- }
1865
- else if (navigation.source.closest(FRAMER_GENERATED_PAGE_SELECTOR) &&
1922
+ if (navigation.source.closest(FRAMER_GENERATED_PAGE_SELECTOR) &&
1866
1923
  typeof window.PointerEvent === 'function') {
1867
1924
  const preventNativeNavigation = (event) => {
1868
1925
  if (event.target === navigation.source ||
@@ -1900,7 +1957,7 @@ class PageContextCollector {
1900
1957
  // failure must not turn the acknowledged action into a failed click.
1901
1958
  }
1902
1959
  }
1903
- else if (navigationSettled === 'rejected' &&
1960
+ else if (navigationSettled !== 'settled' &&
1904
1961
  this.executingActionId === navigation.actionId) {
1905
1962
  // A router may consume the exact click but send the browser somewhere
1906
1963
  // other than the approved destination. Complete the context-side action
@@ -1938,7 +1995,7 @@ class PageContextCollector {
1938
1995
  }
1939
1996
  }
1940
1997
  if (desiredExpanded !== undefined) {
1941
- const expanded = literalBooleanAttribute(element, 'aria-expanded');
1998
+ const expanded = this.expandedState(element);
1942
1999
  if (roleFor(element) !== 'button' || expanded === undefined) {
1943
2000
  throw new Error('Desired expanded state requires an expandable button target.');
1944
2001
  }
@@ -1953,6 +2010,10 @@ class PageContextCollector {
1953
2010
  }
1954
2011
  if (desiredStates.length > 0 && desiredStates.every(Boolean))
1955
2012
  return;
2013
+ if (desiredExpanded === true && framerNavigationRoot(element)) {
2014
+ dispatchFramerPrimaryPointerSequence(element, this.trustedGuideRoot);
2015
+ return;
2016
+ }
1956
2017
  if (desiredChecked !== undefined && framerToggleDescriptor(element)) {
1957
2018
  dispatchFramerTogglePrimaryPointer(element, this.trustedGuideRoot);
1958
2019
  return;
@@ -1963,11 +2024,25 @@ class PageContextCollector {
1963
2024
  safeDestinationPath(element) !== expectedDestinationPath) {
1964
2025
  throw new Error('The navigation destination changed before execution.');
1965
2026
  }
1966
- if (target.origin !== window.location.origin) {
1967
- throw new Error('External navigation is blocked.');
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.');
1968
2030
  }
1969
- if (!supportsCurrentTabNavigation(element)) {
1970
- throw new Error('Named-window and download navigation is blocked.');
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
+ }
1971
2046
  }
1972
2047
  const fromUrl = window.location.href;
1973
2048
  if (window.location.href === fromUrl && target.href !== fromUrl) {
@@ -2065,11 +2140,26 @@ class PageContextCollector {
2065
2140
  }
2066
2141
  form.requestSubmit(submitter);
2067
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
+ }
2068
2158
  safeState(element) {
2069
2159
  return JSON.stringify({
2070
2160
  connected: element.isConnected,
2071
2161
  disabled: !enabled(element),
2072
- expanded: element.getAttribute('aria-expanded'),
2162
+ expanded: this.expandedState(element) ?? element.getAttribute('aria-expanded'),
2073
2163
  pressed: element.getAttribute('aria-pressed'),
2074
2164
  checked: checkedStateFor(element),
2075
2165
  selectedIndex: element instanceof HTMLSelectElement ? element.selectedIndex : undefined,
@@ -2092,6 +2182,7 @@ class PageContextCollector {
2092
2182
  let documentLeft = expectedHandle.element.getBoundingClientRect().left + window.scrollX;
2093
2183
  let layoutCorrections = 0;
2094
2184
  let priorScrollY = null;
2185
+ let intersectedViewport = false;
2095
2186
  let interrupted = false;
2096
2187
  const interrupt = () => {
2097
2188
  interrupted = true;
@@ -2165,10 +2256,25 @@ class PageContextCollector {
2165
2256
  if (!strictlyIntersectsViewport(rect)) {
2166
2257
  priorRect = null;
2167
2258
  stableSince = null;
2168
- // Lazy content can move the same approved link after it briefly enters
2169
- // view. Follow only layout shifts, never an unchanged offscreen target
2170
- // or visitor input, within the original deadline and a fixed budget.
2171
- if (layoutShifted &&
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' &&
2172
2278
  layoutCorrections < MAX_SCROLL_LAYOUT_CORRECTIONS) {
2173
2279
  this.captureSnapshot(false);
2174
2280
  const current = this.targets.get(grant.targetId);
@@ -2193,6 +2299,7 @@ class PageContextCollector {
2193
2299
  }
2194
2300
  continue;
2195
2301
  }
2302
+ intersectedViewport = true;
2196
2303
  if (!priorRect || !sameSettledRect(priorRect, rect))
2197
2304
  stableSince = Date.now();
2198
2305
  priorRect = rect;
@@ -2215,7 +2322,7 @@ class PageContextCollector {
2215
2322
  return checkedStateFor(element) === grant.desiredChecked;
2216
2323
  }
2217
2324
  if (grant.desiredExpanded !== undefined &&
2218
- literalBooleanAttribute(element, 'aria-expanded') !== grant.desiredExpanded) {
2325
+ this.expandedState(element) !== grant.desiredExpanded) {
2219
2326
  return false;
2220
2327
  }
2221
2328
  if (grant.desiredPressed !== undefined &&
@@ -2398,6 +2505,7 @@ class PageContextCollector {
2398
2505
  resetBoundTargetIds() {
2399
2506
  this.targetIds = new WeakMap();
2400
2507
  this.navigationTargetBindings = new WeakMap();
2508
+ this.navigationDisclosureLinks = new WeakMap();
2401
2509
  this.pageScrollIds = new WeakMap();
2402
2510
  this.fieldTargetBindings = new WeakMap();
2403
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.8";
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;
package/cjs/ui.js CHANGED
@@ -4885,16 +4885,15 @@ class WidgetView {
4885
4885
  this.syncSurfaceEffectOwner();
4886
4886
  }
4887
4887
  setRemoteStream(stream) {
4888
- const streamChanged = this.audio.srcObject !== stream;
4889
- if (streamChanged)
4890
- this.invalidateRemotePlayback();
4888
+ if (this.destroyed || this.audio.srcObject === stream)
4889
+ return;
4890
+ // Rebinding the same stream restarts media loading, including its playout buffer.
4891
+ this.invalidateRemotePlayback();
4891
4892
  this.audio.srcObject = stream;
4892
- if (streamChanged) {
4893
- this.boundRemoteAudioStreams = new WeakSet();
4894
- this.boundRemoteAudioTracks = new WeakSet();
4895
- this.bindPlaybackState();
4896
- this.bindRemoteAudioTrackState(stream);
4897
- }
4893
+ this.boundRemoteAudioStreams = new WeakSet();
4894
+ this.boundRemoteAudioTracks = new WeakSet();
4895
+ this.bindPlaybackState();
4896
+ this.bindRemoteAudioTrackState(stream);
4898
4897
  void this.playRemoteAudio();
4899
4898
  }
4900
4899
  bindRemoteAudioTrackState(stream) {
package/cjs/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "7.5.8";
1
+ export declare const VERSION = "7.5.9";
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.9';