@oxvo/ai-live-assist 7.5.6 → 7.5.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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.6";
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
  };
@@ -640,6 +668,18 @@ const safeOptionLabel = (option) => {
640
668
  return sensitivityFor(option, label) === 'none' ? label : '';
641
669
  };
642
670
  const isActionTarget = (element) => element.matches('a[href],button,input,select,textarea,summary,form,[contenteditable="true"],[role="button"],[role="link"],[role="checkbox"],[role="radio"],[role="combobox"],[role="textbox"],[tabindex]');
671
+ const belongsToInteractiveControl = (element) => {
672
+ for (let current = element; current; current = current.parentElement) {
673
+ // Form copy can carry useful public instructions. Only controls and links
674
+ // already represented in targets are omitted from visible page text.
675
+ if (current.matches('form'))
676
+ continue;
677
+ // A bare tabindex is often a focusable Framer content card, not an action.
678
+ if (current.matches('a[href],button,input,select,textarea,summary,[contenteditable="true"],[role="button"],[role="link"],[role="checkbox"],[role="radio"],[role="combobox"],[role="textbox"]'))
679
+ return true;
680
+ }
681
+ return false;
682
+ };
643
683
  const READONLY_INPUT_TYPES = new Set([
644
684
  'date',
645
685
  'datetime-local',
@@ -756,6 +796,7 @@ class PageContextCollector {
756
796
  this.targetIds = new WeakMap();
757
797
  this.pageScrollIds = new WeakMap();
758
798
  this.navigationTargetBindings = new WeakMap();
799
+ this.navigationDisclosureLinks = new WeakMap();
759
800
  this.fieldTargetBindings = new WeakMap();
760
801
  this.fieldAppliedRevisions = new WeakMap();
761
802
  this.textIds = new WeakMap();
@@ -979,6 +1020,9 @@ class PageContextCollector {
979
1020
  'data-framer-name',
980
1021
  'disabled',
981
1022
  'href',
1023
+ 'target',
1024
+ 'download',
1025
+ 'data-highlight',
982
1026
  'hidden',
983
1027
  'inert',
984
1028
  'role',
@@ -1025,6 +1069,7 @@ class PageContextCollector {
1025
1069
  document.removeEventListener('change', this.handleControlStateChange, true);
1026
1070
  this.restoreNavigationHooks();
1027
1071
  this.targets.clear();
1072
+ this.navigationDisclosureLinks = new WeakMap();
1028
1073
  this.executingActionId = null;
1029
1074
  this.pendingNavigation = null;
1030
1075
  this.pendingCapture = false;
@@ -1065,7 +1110,8 @@ class PageContextCollector {
1065
1110
  continue;
1066
1111
  const role = roleFor(element);
1067
1112
  const hasStatefulButtonAttribute = role === 'button' &&
1068
- (element.hasAttribute('aria-expanded') || element.hasAttribute('aria-pressed'));
1113
+ (element.hasAttribute('aria-expanded') || element.hasAttribute('aria-pressed') ||
1114
+ Boolean(framerNavigationRoot(element)));
1069
1115
  const hasUnsupportedMixedPressedState = role === 'button' &&
1070
1116
  element.getAttribute('aria-pressed')?.trim().toLowerCase() === 'mixed';
1071
1117
  if (hasUnsupportedMixedPressedState ||
@@ -1102,7 +1148,8 @@ class PageContextCollector {
1102
1148
  : accessibleNameSourceSensitivity(element);
1103
1149
  const visible = isVisible(element);
1104
1150
  const destinationPath = safeDestinationPath(element);
1105
- const expanded = literalBooleanAttribute(element, 'aria-expanded');
1151
+ const navigation = publicNavigation(element);
1152
+ const expanded = this.expandedState(element);
1106
1153
  const pressed = literalBooleanAttribute(element, 'aria-pressed');
1107
1154
  const checked = checkedStateFor(element);
1108
1155
  const targetIdentity = {
@@ -1119,6 +1166,7 @@ class PageContextCollector {
1119
1166
  ...(semanticRegion ? { semanticRegion } : {}),
1120
1167
  ...(this.statefulButtonActionProof && isNavigationMenu(element, role) ? { navigationMenu: true } : {}),
1121
1168
  ...(destinationPath ? { destinationPath } : {}),
1169
+ ...(navigation ? { navigation } : {}),
1122
1170
  sensitivity,
1123
1171
  protectedRegion: sensitivity !== 'none' || regionAssessment.matchedRuleDigests.length > 0,
1124
1172
  frameOrigin: 'same_origin',
@@ -1136,7 +1184,7 @@ class PageContextCollector {
1136
1184
  };
1137
1185
  const priorTargetId = this.targetIds.get(element);
1138
1186
  const boundNavigation = targetIdentity.consequence === 'navigation' &&
1139
- typeof targetIdentity.destinationPath === 'string';
1187
+ targetIdentity.navigation !== undefined;
1140
1188
  const binding = boundNavigation ? navigationTargetBinding(targetIdentity) : null;
1141
1189
  const priorBinding = this.navigationTargetBindings.get(element);
1142
1190
  const boundField = isNativeEditableField(element) && sensitivity === 'none' &&
@@ -1185,23 +1233,33 @@ class PageContextCollector {
1185
1233
  });
1186
1234
  }
1187
1235
  const visibleText = [];
1236
+ const capturedSemanticText = new Set();
1188
1237
  let totalText = 0;
1189
1238
  const walker = document.createTreeWalker(document.body ?? document.documentElement, NodeFilter.SHOW_TEXT);
1190
1239
  let node = walker.nextNode();
1191
1240
  while (node && visibleText.length < MAX_TEXT_ITEMS && totalText < MAX_TOTAL_TEXT) {
1192
1241
  const parent = node.parentElement;
1193
- const text = normalize(node.textContent ?? '', 500);
1242
+ const semanticTextElement = parent?.closest('[role="img"][aria-label]') ?? null;
1243
+ const textElement = semanticTextElement && isRendered(semanticTextElement)
1244
+ ? semanticTextElement
1245
+ : parent;
1246
+ const text = normalize(semanticTextElement?.getAttribute('aria-label') ?? node.textContent ?? '', 500);
1194
1247
  if (parent &&
1248
+ textElement &&
1195
1249
  text &&
1196
1250
  !parent.closest(PRIVATE_SELECTOR) &&
1197
1251
  !parent.closest('script,style,noscript,template,svg') &&
1198
1252
  !parent.closest('input,select,textarea,option') &&
1199
- isVisible(parent) &&
1200
- sensitivityFor(parent, nameFor(parent)) === 'none') {
1253
+ (!semanticTextElement || !capturedSemanticText.has(semanticTextElement)) &&
1254
+ !belongsToInteractiveControl(textElement) &&
1255
+ isRendered(textElement) &&
1256
+ sensitivityFor(textElement, nameFor(textElement)) === 'none') {
1201
1257
  const textId = this.textIds.get(node) ?? id('text');
1202
1258
  this.textIds.set(node, textId);
1203
1259
  visibleText.push({ id: textId, text, provenance: 'page' });
1204
1260
  totalText += text.length;
1261
+ if (semanticTextElement)
1262
+ capturedSemanticText.add(semanticTextElement);
1205
1263
  }
1206
1264
  node = walker.nextNode();
1207
1265
  }
@@ -1398,12 +1456,13 @@ class PageContextCollector {
1398
1456
  grant.targetId.startsWith(`${BOUND_FIELD_TARGET_PREFIX}_`) &&
1399
1457
  Boolean(current && isNativeEditableField(current.element) && this.fieldTargetBindings.has(current.element));
1400
1458
  const stablePageScroll = (current) => grant.action === 'scroll' && Boolean(current?.pageScroll && current.element === document.scrollingElement);
1459
+ const stableNavigation = (current) => (grant.action === 'click' || grant.action === 'scroll') &&
1460
+ grant.targetId.startsWith(`${BOUND_NAVIGATION_TARGET_PREFIX}_`) &&
1461
+ current?.element instanceof HTMLAnchorElement &&
1462
+ current.target.consequence === 'navigation' && current.target.navigation !== undefined;
1401
1463
  const revisionMatchesOrStableTarget = () => grant.domRevision === this.revision ||
1402
1464
  (grant.domRevision < this.revision &&
1403
- (stableField(handle) || stablePageScroll(handle) || ((grant.action === 'click' || grant.action === 'scroll') &&
1404
- handle?.element instanceof HTMLAnchorElement &&
1405
- handle.target.consequence === 'navigation' &&
1406
- typeof handle.target.destinationPath === 'string')));
1465
+ (stableField(handle) || stablePageScroll(handle) || stableNavigation(handle)));
1407
1466
  if (expiresAt <= Date.now() ||
1408
1467
  grant.pageId !== this.pageId ||
1409
1468
  grant.pageUrl !== safePageUrl() ||
@@ -1440,7 +1499,7 @@ class PageContextCollector {
1440
1499
  grant.pageUrl !== safePageUrl() ||
1441
1500
  grant.policyVersion !== this.policyVersion ||
1442
1501
  !revisionMatchesOrStableTarget() ||
1443
- (handle !== fingerprintedHandle && !((stableField(handle) || stablePageScroll(handle)) &&
1502
+ (handle !== fingerprintedHandle && !((stableField(handle) || stablePageScroll(handle) || stableNavigation(handle)) &&
1444
1503
  handle?.element === fingerprintedHandle.element &&
1445
1504
  actionTargetFingerprintBinding(handle.target) === actionTargetFingerprintBinding(fingerprintedHandle.target))) ||
1446
1505
  !handle ||
@@ -1526,6 +1585,9 @@ class PageContextCollector {
1526
1585
  this.releasedNavigation = null;
1527
1586
  this.finishReleasedNavigationSettlement('rejected');
1528
1587
  let postActionSnapshot = null;
1588
+ let navigationReceipt;
1589
+ const disclosureRoot = handle.target.navigationMenu ? framerNavigationRoot(element) : null;
1590
+ const priorDisclosureLinks = disclosureRoot ? this.exposedNavigationLinks(disclosureRoot) : null;
1529
1591
  try {
1530
1592
  switch (grant.action) {
1531
1593
  case 'scroll': {
@@ -1636,7 +1698,7 @@ class PageContextCollector {
1636
1698
  element.focus({ preventScroll: false });
1637
1699
  break;
1638
1700
  case 'click':
1639
- this.click(element, handle, grant.actionId, grant.desiredChecked, grant.desiredExpanded, grant.desiredPressed, grant.destinationPath);
1701
+ navigationReceipt = this.click(element, handle, grant.actionId, grant.desiredChecked, grant.desiredExpanded, grant.desiredPressed, grant.destinationPath);
1640
1702
  break;
1641
1703
  case 'type':
1642
1704
  this.type(element, grant.value ?? '');
@@ -1658,9 +1720,28 @@ class PageContextCollector {
1658
1720
  if (grant.action !== 'scroll') {
1659
1721
  await new Promise((resolve) => setTimeout(resolve, 80));
1660
1722
  }
1723
+ if (handle.target.navigationMenu && grant.action === 'click') {
1724
+ const deadline = Date.now() + 1000;
1725
+ for (let frames = 0; frames < 60 && Date.now() < deadline && element.isConnected && !executionStale(); frames++) {
1726
+ if (disclosureRoot && priorDisclosureLinks) {
1727
+ const added = new Set([...this.exposedNavigationLinks(disclosureRoot)]
1728
+ .filter(link => !priorDisclosureLinks.has(link)));
1729
+ if (added.size)
1730
+ this.navigationDisclosureLinks.set(element, added);
1731
+ }
1732
+ if (this.actionApplied(grant, element, handle))
1733
+ break;
1734
+ await nextAnimationFrameOrTimeout(Math.min(50, deadline - Date.now()));
1735
+ }
1736
+ }
1661
1737
  const postActionStale = executionStale();
1662
- if (postActionStale)
1663
- 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
+ }
1664
1745
  if (!this.actionApplied(grant, element, handle)) {
1665
1746
  throw new Error('The approved value was not applied.');
1666
1747
  }
@@ -1680,16 +1761,16 @@ class PageContextCollector {
1680
1761
  const attached = element.isConnected;
1681
1762
  const after = attached ? this.safeState(element) : '';
1682
1763
  const expanded = attached
1683
- ? literalBooleanAttribute(element, 'aria-expanded')
1764
+ ? this.expandedState(element)
1684
1765
  : undefined;
1685
1766
  const pressed = attached
1686
1767
  ? literalBooleanAttribute(element, 'aria-pressed')
1687
1768
  : undefined;
1688
1769
  const checked = attached ? checkedStateFor(element) : undefined;
1689
- this.callbacks.onHighlight(attached && !handle.pageScroll ? element : null, 'success');
1770
+ this.callbacks.onHighlight(attached && !handle.pageScroll ? element : null, navigationReceipt && navigationReceipt.outcome !== 'opened' ? 'failure' : 'success');
1690
1771
  return {
1691
1772
  ...base,
1692
- status: 'executed',
1773
+ status: navigationReceipt && navigationReceipt.outcome !== 'opened' ? 'blocked' : 'executed',
1693
1774
  observed: {
1694
1775
  url: safePageUrl(),
1695
1776
  revision: snapshot.revision,
@@ -1698,19 +1779,24 @@ class PageContextCollector {
1698
1779
  : grant.action === 'type' || grant.action === 'select' || before !== after || handle.pageScroll
1699
1780
  ? 'changed'
1700
1781
  : 'unchanged',
1701
- safeSummary: handle.pageScroll
1702
- ? `The page moved ${handle.pageScroll} by ${Math.round(Math.abs(window.scrollY - scrollStartY))} pixels.`
1703
- : grant.action === 'scroll'
1704
- ? 'The requested target is visible. This confirms target visibility, not a directional page scroll.'
1705
- : grant.action === 'type'
1706
- ? 'The requested text was applied to the approved field.'
1707
- : grant.action === 'select'
1708
- ? 'The requested option was applied to the approved field.'
1709
- : grant.desiredChecked !== undefined
1710
- ? 'The requested checkbox state was applied.'
1711
- : grant.desiredExpanded !== undefined || grant.desiredPressed !== undefined
1712
- ? 'The requested button state was applied.'
1713
- : `${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.`,
1714
1800
  ...(checked !== undefined ? { checked } : {}),
1715
1801
  ...(this.statefulButtonActionProof && expanded !== undefined ? { expanded } : {}),
1716
1802
  ...(this.statefulButtonActionProof && pressed !== undefined ? { pressed } : {}),
@@ -1718,6 +1804,7 @@ class PageContextCollector {
1718
1804
  };
1719
1805
  }
1720
1806
  catch {
1807
+ this.deferredNavigation = null;
1721
1808
  this.callbacks.onHighlight(element, 'failure');
1722
1809
  return {
1723
1810
  ...base,
@@ -1725,8 +1812,12 @@ class PageContextCollector {
1725
1812
  observed: {
1726
1813
  url: safePageUrl(),
1727
1814
  revision: this.revision,
1728
- targetState: element.isConnected ? 'unchanged' : 'detached',
1729
- 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.',
1730
1821
  },
1731
1822
  };
1732
1823
  }
@@ -1786,9 +1877,8 @@ class PageContextCollector {
1786
1877
  return 'rejected';
1787
1878
  }
1788
1879
  if (window.location.href !== navigation.fromUrl) {
1789
- return sameNavigationDestination(window.location.href, navigation.toUrl)
1790
- ? 'settled'
1791
- : 'rejected';
1880
+ // Arrival before dispatch is not evidence that this action navigated.
1881
+ return 'rejected';
1792
1882
  }
1793
1883
  this.captureSnapshot(false);
1794
1884
  const handle = this.targets.get(navigation.targetId);
@@ -1804,7 +1894,7 @@ class PageContextCollector {
1804
1894
  target.origin !== window.location.origin ||
1805
1895
  safeDestinationPath(navigation.source) !== target.pathname ||
1806
1896
  linkBrowsingTarget(navigation.source) !== navigation.browsingTarget ||
1807
- !supportsCurrentTabNavigation(navigation.source) ||
1897
+ !supportsCurrentTabNavigation(navigation.source) || navigation.browsingTarget === '_blank' ||
1808
1898
  !isRendered(navigation.source) ||
1809
1899
  !isVisible(navigation.source) ||
1810
1900
  !enabled(navigation.source) ||
@@ -1818,9 +1908,6 @@ class PageContextCollector {
1818
1908
  if (current?.actionId !== navigation.actionId)
1819
1909
  return;
1820
1910
  this.releasedNavigationSettlement = null;
1821
- if (this.executingActionId === navigation.actionId) {
1822
- this.executingActionId = null;
1823
- }
1824
1911
  resolve('timed_out');
1825
1912
  }, RELEASED_NAVIGATION_SETTLEMENT_TIMEOUT_MS);
1826
1913
  this.releasedNavigationSettlement = {
@@ -1831,16 +1918,8 @@ class PageContextCollector {
1831
1918
  resolve,
1832
1919
  };
1833
1920
  });
1834
- const useCurrentTab = navigation.browsingTarget === '_blank';
1835
1921
  try {
1836
- // New-window links must stay in the observable tab. Navigate the exact
1837
- // approved URL only after durable release; the normal document-navigation
1838
- // recovery handles the new page. Dispatching their handlers can open a
1839
- // popup even if the DOM target is temporarily changed (e.g. React props).
1840
- if (useCurrentTab) {
1841
- window.location.assign(navigation.toUrl);
1842
- }
1843
- else if (navigation.source.closest(FRAMER_GENERATED_PAGE_SELECTOR) &&
1922
+ if (navigation.source.closest(FRAMER_GENERATED_PAGE_SELECTOR) &&
1844
1923
  typeof window.PointerEvent === 'function') {
1845
1924
  const preventNativeNavigation = (event) => {
1846
1925
  if (event.target === navigation.source ||
@@ -1878,7 +1957,7 @@ class PageContextCollector {
1878
1957
  // failure must not turn the acknowledged action into a failed click.
1879
1958
  }
1880
1959
  }
1881
- else if (navigationSettled === 'rejected' &&
1960
+ else if (navigationSettled !== 'settled' &&
1882
1961
  this.executingActionId === navigation.actionId) {
1883
1962
  // A router may consume the exact click but send the browser somewhere
1884
1963
  // other than the approved destination. Complete the context-side action
@@ -1916,7 +1995,7 @@ class PageContextCollector {
1916
1995
  }
1917
1996
  }
1918
1997
  if (desiredExpanded !== undefined) {
1919
- const expanded = literalBooleanAttribute(element, 'aria-expanded');
1998
+ const expanded = this.expandedState(element);
1920
1999
  if (roleFor(element) !== 'button' || expanded === undefined) {
1921
2000
  throw new Error('Desired expanded state requires an expandable button target.');
1922
2001
  }
@@ -1931,6 +2010,10 @@ class PageContextCollector {
1931
2010
  }
1932
2011
  if (desiredStates.length > 0 && desiredStates.every(Boolean))
1933
2012
  return;
2013
+ if (desiredExpanded === true && framerNavigationRoot(element)) {
2014
+ dispatchFramerPrimaryPointerSequence(element, this.trustedGuideRoot);
2015
+ return;
2016
+ }
1934
2017
  if (desiredChecked !== undefined && framerToggleDescriptor(element)) {
1935
2018
  dispatchFramerTogglePrimaryPointer(element, this.trustedGuideRoot);
1936
2019
  return;
@@ -1941,11 +2024,25 @@ class PageContextCollector {
1941
2024
  safeDestinationPath(element) !== expectedDestinationPath) {
1942
2025
  throw new Error('The navigation destination changed before execution.');
1943
2026
  }
1944
- if (target.origin !== window.location.origin) {
1945
- 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.');
1946
2030
  }
1947
- if (!supportsCurrentTabNavigation(element)) {
1948
- 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
+ }
1949
2046
  }
1950
2047
  const fromUrl = window.location.href;
1951
2048
  if (window.location.href === fromUrl && target.href !== fromUrl) {
@@ -2043,11 +2140,26 @@ class PageContextCollector {
2043
2140
  }
2044
2141
  form.requestSubmit(submitter);
2045
2142
  }
2143
+ exposedNavigationLinks(nav) {
2144
+ const links = nav.querySelectorAll('a[href]');
2145
+ if (links.length > MAX_TARGETS)
2146
+ return new Set();
2147
+ return new Set(Array.from(links).filter(link => isVisible(link) && publicNavigation(link) &&
2148
+ !isCovered(link, this.trustedGuideRoot)));
2149
+ }
2150
+ expandedState(element) {
2151
+ const explicit = literalBooleanAttribute(element, 'aria-expanded');
2152
+ if (explicit !== undefined)
2153
+ return explicit;
2154
+ if (!framerNavigationRoot(element))
2155
+ return undefined;
2156
+ return [...(this.navigationDisclosureLinks.get(element) ?? [])].some(link => link.isConnected && isRendered(link));
2157
+ }
2046
2158
  safeState(element) {
2047
2159
  return JSON.stringify({
2048
2160
  connected: element.isConnected,
2049
2161
  disabled: !enabled(element),
2050
- expanded: element.getAttribute('aria-expanded'),
2162
+ expanded: this.expandedState(element) ?? element.getAttribute('aria-expanded'),
2051
2163
  pressed: element.getAttribute('aria-pressed'),
2052
2164
  checked: checkedStateFor(element),
2053
2165
  selectedIndex: element instanceof HTMLSelectElement ? element.selectedIndex : undefined,
@@ -2070,6 +2182,7 @@ class PageContextCollector {
2070
2182
  let documentLeft = expectedHandle.element.getBoundingClientRect().left + window.scrollX;
2071
2183
  let layoutCorrections = 0;
2072
2184
  let priorScrollY = null;
2185
+ let intersectedViewport = false;
2073
2186
  let interrupted = false;
2074
2187
  const interrupt = () => {
2075
2188
  interrupted = true;
@@ -2143,10 +2256,25 @@ class PageContextCollector {
2143
2256
  if (!strictlyIntersectsViewport(rect)) {
2144
2257
  priorRect = null;
2145
2258
  stableSince = null;
2146
- // Lazy content can move the same approved link after it briefly enters
2147
- // view. Follow only layout shifts, never an unchanged offscreen target
2148
- // or visitor input, within the original deadline and a fixed budget.
2149
- 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' &&
2150
2278
  layoutCorrections < MAX_SCROLL_LAYOUT_CORRECTIONS) {
2151
2279
  this.captureSnapshot(false);
2152
2280
  const current = this.targets.get(grant.targetId);
@@ -2171,6 +2299,7 @@ class PageContextCollector {
2171
2299
  }
2172
2300
  continue;
2173
2301
  }
2302
+ intersectedViewport = true;
2174
2303
  if (!priorRect || !sameSettledRect(priorRect, rect))
2175
2304
  stableSince = Date.now();
2176
2305
  priorRect = rect;
@@ -2193,7 +2322,7 @@ class PageContextCollector {
2193
2322
  return checkedStateFor(element) === grant.desiredChecked;
2194
2323
  }
2195
2324
  if (grant.desiredExpanded !== undefined &&
2196
- literalBooleanAttribute(element, 'aria-expanded') !== grant.desiredExpanded) {
2325
+ this.expandedState(element) !== grant.desiredExpanded) {
2197
2326
  return false;
2198
2327
  }
2199
2328
  if (grant.desiredPressed !== undefined &&
@@ -2376,6 +2505,7 @@ class PageContextCollector {
2376
2505
  resetBoundTargetIds() {
2377
2506
  this.targetIds = new WeakMap();
2378
2507
  this.navigationTargetBindings = new WeakMap();
2508
+ this.navigationDisclosureLinks = new WeakMap();
2379
2509
  this.pageScrollIds = new WeakMap();
2380
2510
  this.fieldTargetBindings = new WeakMap();
2381
2511
  this.fieldAppliedRevisions = new WeakMap();
@@ -1,5 +1,5 @@
1
1
  import { type LiveDuplexMediaOptions, type LiveDuplexMediaDependencies } from './liveDuplexMedia.js';
2
- export declare const LIVE_CONTROL_CLIENT_VERSION = "7.5.6";
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;