@oxvo/ai-live-assist 7.3.4 → 7.4.23

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.
Files changed (67) hide show
  1. package/cjs/AiLiveAssist.d.ts +588 -6
  2. package/cjs/AiLiveAssist.js +14892 -1240
  3. package/cjs/client.d.ts +71 -10
  4. package/cjs/client.js +316 -58
  5. package/cjs/context.d.ts +15 -5
  6. package/cjs/context.js +568 -73
  7. package/cjs/control.d.ts +72 -7
  8. package/cjs/control.js +594 -71
  9. package/cjs/dialogFocusScope.d.ts +37 -0
  10. package/cjs/dialogFocusScope.js +227 -0
  11. package/cjs/experienceState.js +1 -1
  12. package/cjs/index.d.ts +1 -1
  13. package/cjs/index.js +2 -2
  14. package/cjs/media.d.ts +14 -3
  15. package/cjs/media.js +128 -19
  16. package/cjs/messages.js +26 -3
  17. package/cjs/messengerLoader.d.ts +3 -0
  18. package/cjs/messengerLoader.js +156 -0
  19. package/cjs/pageAssistancePreference.d.ts +78 -0
  20. package/cjs/pageAssistancePreference.js +488 -0
  21. package/cjs/preview.d.ts +33 -0
  22. package/cjs/preview.js +567 -0
  23. package/cjs/tabLock.d.ts +54 -3
  24. package/cjs/tabLock.js +752 -72
  25. package/cjs/types.d.ts +152 -4
  26. package/cjs/ui.d.ts +200 -10
  27. package/cjs/ui.js +6946 -523
  28. package/cjs/version.d.ts +1 -1
  29. package/cjs/version.js +1 -1
  30. package/cjs/voicePresenceUi.d.ts +48 -9
  31. package/cjs/voicePresenceUi.js +863 -125
  32. package/lib/AiLiveAssist.d.ts +588 -6
  33. package/lib/AiLiveAssist.js +14897 -1248
  34. package/lib/client.d.ts +71 -10
  35. package/lib/client.js +313 -57
  36. package/lib/context.d.ts +15 -5
  37. package/lib/context.js +566 -72
  38. package/lib/control.d.ts +72 -7
  39. package/lib/control.js +594 -71
  40. package/lib/dialogFocusScope.d.ts +37 -0
  41. package/lib/dialogFocusScope.js +223 -0
  42. package/lib/experienceState.js +1 -1
  43. package/lib/index.d.ts +1 -1
  44. package/lib/index.js +2 -2
  45. package/lib/media.d.ts +14 -3
  46. package/lib/media.js +128 -19
  47. package/lib/messages.js +26 -3
  48. package/lib/messengerLoader.d.ts +3 -0
  49. package/lib/messengerLoader.js +151 -0
  50. package/lib/pageAssistancePreference.d.ts +78 -0
  51. package/lib/pageAssistancePreference.js +482 -0
  52. package/lib/preview.d.ts +33 -0
  53. package/lib/preview.js +561 -0
  54. package/lib/tabLock.d.ts +54 -3
  55. package/lib/tabLock.js +752 -72
  56. package/lib/types.d.ts +152 -4
  57. package/lib/ui.d.ts +200 -10
  58. package/lib/ui.js +6947 -524
  59. package/lib/version.d.ts +1 -1
  60. package/lib/version.js +1 -1
  61. package/lib/voicePresenceUi.d.ts +48 -9
  62. package/lib/voicePresenceUi.js +863 -125
  63. package/package.json +8 -3
  64. package/cjs/recordingPolicy.d.ts +0 -2
  65. package/cjs/recordingPolicy.js +0 -12
  66. package/lib/recordingPolicy.d.ts +0 -2
  67. package/lib/recordingPolicy.js +0 -8
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.VoicePresenceView = void 0;
4
4
  const lucide_1 = require("lucide");
5
+ const dialogFocusScope_js_1 = require("./dialogFocusScope.js");
5
6
  const experienceState_js_1 = require("./experienceState.js");
6
7
  const messages_js_1 = require("./messages.js");
7
8
  const placement_js_1 = require("./placement.js");
@@ -52,6 +53,12 @@ const confirmationPresentation = (value, fallbackTitle, fallbackApprove) => {
52
53
  const INTRO_WAKE_MS = 620;
53
54
  const MAX_LIVE_ANNOUNCEMENTS = 6;
54
55
  const MAX_VISIBLE_SUBTITLES = 3;
56
+ const PAGE_ASSISTANCE_TITLE = "Let AI Guide help on this page?";
57
+ const PAGE_ASSISTANCE_FALLBACK = "AI Guide wants to navigate or interact with this page.";
58
+ const PAGE_ASSISTANCE_ALLOW = "Allow";
59
+ const PAGE_ASSISTANCE_DECLINE = "Not now";
60
+ const PAGE_ASSISTANCE_ALWAYS = "Always allow on this site";
61
+ const PAGE_ASSISTANCE_REVOKE = "Turn off always allow";
55
62
  const styles = `
56
63
  :host {
57
64
  all: initial;
@@ -136,6 +143,60 @@ const styles = `
136
143
  inset-inline-start: max(var(--voice-inline-offset), env(safe-area-inset-left));
137
144
  inset-inline-end: auto;
138
145
  }
146
+ .page-assistance-revoke {
147
+ position: absolute;
148
+ inset-inline-end: calc(100% + 8px);
149
+ bottom: 9px;
150
+ width: max-content;
151
+ max-width: min(220px, calc(100vw - var(--voice-orb-size) - var(--voice-inline-offset) - 24px));
152
+ min-height: 44px;
153
+ padding: 7px 10px;
154
+ overflow-wrap: anywhere;
155
+ color: var(--voice-text);
156
+ background: var(--voice-surface);
157
+ border: 1px solid var(--voice-border);
158
+ border-radius: 999px;
159
+ box-shadow: 0 7px 20px rgba(15,23,42,.14);
160
+ font-size: 11px;
161
+ font-weight: 620;
162
+ line-height: 16px;
163
+ text-align: center;
164
+ white-space: normal;
165
+ pointer-events: auto;
166
+ }
167
+ .page-assistance-revoke:hover { background: #f7f8f9; }
168
+ :host([data-position="bottom_left"]) .page-assistance-revoke {
169
+ inset-inline-start: calc(100% + 8px);
170
+ inset-inline-end: auto;
171
+ }
172
+ .voice-support-action {
173
+ position: absolute;
174
+ inset-inline-end: 0;
175
+ bottom: calc(100% + 10px);
176
+ width: max-content;
177
+ max-width: min(260px, calc(100vw - 28px));
178
+ min-height: 38px;
179
+ padding: 7px 12px;
180
+ color: var(--voice-text);
181
+ background: rgba(255, 255, 255, .92);
182
+ border: 1px solid var(--voice-border);
183
+ border-radius: 999px;
184
+ box-shadow: 0 8px 24px rgba(15, 23, 42, .14);
185
+ font-size: 12px;
186
+ font-weight: 650;
187
+ line-height: 17px;
188
+ text-align: center;
189
+ pointer-events: auto;
190
+ }
191
+ .voice-support-action:hover { background: #f7f8f9; }
192
+ .voice-support-action.has-reply {
193
+ border-color: color-mix(in srgb, var(--voice-accent) 34%, white);
194
+ box-shadow: 0 8px 24px color-mix(in srgb, var(--voice-accent) 16%, transparent);
195
+ }
196
+ :host([data-position="bottom_left"]) .voice-support-action {
197
+ inset-inline-start: 0;
198
+ inset-inline-end: auto;
199
+ }
139
200
  /* Premium orb themes. Light is the backwards-compatible default. */
140
201
  :host,
141
202
  :host([data-orb-theme="light"]) {
@@ -668,11 +729,16 @@ const styles = `
668
729
  .dialog-head { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 12px; align-items: start; }
669
730
  .dialog h2 { margin: 0; font-size: 18px; line-height: 25px; font-weight: 680; }
670
731
  .dialog p { margin: 7px 0 0; color: var(--voice-muted); font-size: 14px; line-height: 21px; }
732
+ .page-assistance-dialog {
733
+ width: min(360px, calc(var(--voice-viewport-width, 100vw) - 32px));
734
+ padding: 16px;
735
+ }
736
+ .page-assistance-dialog h2 { font-size: 16px; line-height: 22px; }
671
737
  .icon-button {
672
738
  display: grid;
673
- width: 36px;
674
- height: 36px;
675
- min-width: 36px;
739
+ width: 44px;
740
+ height: 44px;
741
+ min-width: 44px;
676
742
  padding: 0;
677
743
  place-items: center;
678
744
  color: #4b5563;
@@ -682,7 +748,14 @@ const styles = `
682
748
  }
683
749
  .icon-button:hover { background: #f3f4f6; }
684
750
  .icon-button svg { width: 18px; height: 18px; }
685
- .agreement {
751
+ .page-assistance-copy { overflow-wrap: anywhere; }
752
+ .page-assistance-target {
753
+ color: var(--voice-text);
754
+ font-size: 12px;
755
+ line-height: 18px;
756
+ font-weight: 620;
757
+ }
758
+ .page-assistance-always {
686
759
  display: grid;
687
760
  grid-template-columns: 18px minmax(0, 1fr);
688
761
  gap: 10px;
@@ -697,16 +770,24 @@ const styles = `
697
770
  line-height: 19px;
698
771
  cursor: pointer;
699
772
  }
700
- .agreement input { width: 17px; height: 17px; margin: 1px 0 0; accent-color: var(--voice-accent); }
773
+ .page-assistance-always span { min-width: 0; overflow-wrap: anywhere; }
774
+ .page-assistance-always input { width: 17px; height: 17px; margin: 1px 0 0; accent-color: var(--voice-accent); }
701
775
  details { margin-top: 12px; color: var(--voice-muted); font-size: 12px; line-height: 18px; }
702
- summary { width: fit-content; color: color-mix(in srgb, var(--voice-accent) 78%, #18212b); cursor: pointer; }
776
+ summary {
777
+ display: inline-flex;
778
+ width: fit-content;
779
+ min-height: 44px;
780
+ align-items: center;
781
+ color: color-mix(in srgb, var(--voice-accent) 78%, #18212b);
782
+ cursor: pointer;
783
+ }
703
784
  .legal-copy { margin-top: 8px; padding: 10px 11px; background: #fafbfc; border: 1px solid #e6e9ed; border-radius: 8px; }
704
785
  .legal-copy p { margin: 0; font-size: 12px; line-height: 18px; }
705
786
  .legal-links { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 8px; }
706
787
  .legal-links a { color: color-mix(in srgb, var(--voice-accent) 82%, #18212b); }
707
788
  .dialog-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 18px; }
708
789
  .button {
709
- min-height: 40px;
790
+ min-height: 44px;
710
791
  padding: 0 14px;
711
792
  border: 1px solid #d7dce1;
712
793
  border-radius: 8px;
@@ -738,10 +819,10 @@ const styles = `
738
819
  .card-title { margin: 0; font-size: 14px; line-height: 20px; font-weight: 680; }
739
820
  .card-copy { margin: 5px 0 0; color: var(--voice-muted); font-size: 12px; line-height: 18px; }
740
821
  .card-actions { display: flex; justify-content: flex-end; gap: 7px; margin-top: 12px; }
741
- .card-actions .button { min-height: 36px; }
822
+ .card-actions .button { min-height: 44px; }
742
823
  .recovery-head {
743
824
  display: grid;
744
- grid-template-columns: 28px minmax(0, 1fr) 32px;
825
+ grid-template-columns: 28px minmax(0, 1fr) 44px;
745
826
  gap: 9px;
746
827
  align-items: center;
747
828
  }
@@ -757,9 +838,9 @@ const styles = `
757
838
  }
758
839
  .recovery-badge svg { width: 16px; height: 16px; }
759
840
  .recovery-close {
760
- width: 32px;
761
- height: 32px;
762
- min-width: 32px;
841
+ width: 44px;
842
+ height: 44px;
843
+ min-width: 44px;
763
844
  justify-self: end;
764
845
  }
765
846
  .recovery-card .card-copy { margin-top: 9px; font-size: 13px; line-height: 19px; }
@@ -834,6 +915,37 @@ const styles = `
834
915
  .orb.wake::after { animation-duration: 120ms; }
835
916
  .presence-field { transition-duration: 120ms; }
836
917
  }
918
+ :host([data-motion="off"]) .orb,
919
+ :host([data-motion="off"]) .orb::before,
920
+ :host([data-motion="off"]) .orb::after,
921
+ :host([data-motion="off"]) .orb-rim,
922
+ :host([data-motion="off"]) .orb-rim::before,
923
+ :host([data-motion="off"]) .orb-rim::after,
924
+ :host([data-motion="off"]) .orb-surface,
925
+ :host([data-motion="off"]) .orb-surface::before,
926
+ :host([data-motion="off"]) .orb-surface::after,
927
+ :host([data-motion="off"]) .orb-core,
928
+ :host([data-motion="off"]) .orb-core::before,
929
+ :host([data-motion="off"]) .orb-core::after {
930
+ animation: none !important;
931
+ transition: none !important;
932
+ will-change: auto !important;
933
+ }
934
+ :host([data-effects-paused="true"]) .orb,
935
+ :host([data-effects-paused="true"]) .orb::before,
936
+ :host([data-effects-paused="true"]) .orb::after,
937
+ :host([data-effects-paused="true"]) .orb-rim,
938
+ :host([data-effects-paused="true"]) .orb-rim::before,
939
+ :host([data-effects-paused="true"]) .orb-rim::after,
940
+ :host([data-effects-paused="true"]) .orb-surface,
941
+ :host([data-effects-paused="true"]) .orb-surface::before,
942
+ :host([data-effects-paused="true"]) .orb-surface::after,
943
+ :host([data-effects-paused="true"]) .orb-core,
944
+ :host([data-effects-paused="true"]) .orb-core::before,
945
+ :host([data-effects-paused="true"]) .orb-core::after {
946
+ animation-play-state: paused !important;
947
+ will-change: auto !important;
948
+ }
837
949
  :host([data-visual-test="true"]) *,
838
950
  :host([data-visual-test="true"]) *::before,
839
951
  :host([data-visual-test="true"]) *::after {
@@ -841,10 +953,71 @@ const styles = `
841
953
  transition: none !important;
842
954
  }
843
955
  :host([data-visual-test="true"]) .caption.entering { animation: none !important; }
956
+ @media (prefers-reduced-transparency: reduce) {
957
+ .presence-field {
958
+ display: none;
959
+ opacity: 0;
960
+ transition: none;
961
+ }
962
+ .orb {
963
+ color: #fff;
964
+ background: #1f2937;
965
+ background-image: none;
966
+ border-color: #4b5563;
967
+ box-shadow: none;
968
+ -webkit-backdrop-filter: none;
969
+ backdrop-filter: none;
970
+ filter: none;
971
+ }
972
+ .orb::before,
973
+ .orb::after,
974
+ .orb-rim,
975
+ .orb-rim::before,
976
+ .orb-rim::after,
977
+ .orb-surface,
978
+ .orb-surface::before,
979
+ .orb-surface::after,
980
+ .orb-core::before,
981
+ .orb-core::after {
982
+ display: none;
983
+ animation: none;
984
+ background: none;
985
+ background-image: none;
986
+ box-shadow: none;
987
+ -webkit-backdrop-filter: none;
988
+ backdrop-filter: none;
989
+ filter: none;
990
+ }
991
+ .orb-core {
992
+ background: #111827;
993
+ background-image: none;
994
+ box-shadow: none;
995
+ -webkit-backdrop-filter: none;
996
+ backdrop-filter: none;
997
+ filter: none;
998
+ }
999
+ .orb-icon { filter: none; }
1000
+ .subtitle-track,
1001
+ .first-label,
1002
+ .status-bubble,
1003
+ .attribution-bubble,
1004
+ .dialog,
1005
+ .safety-card,
1006
+ .recovery-card,
1007
+ .page-assistance-revoke {
1008
+ background: #fff;
1009
+ background-image: none;
1010
+ box-shadow: none;
1011
+ -webkit-backdrop-filter: none;
1012
+ backdrop-filter: none;
1013
+ filter: none;
1014
+ }
1015
+ }
844
1016
  @media (forced-colors: active) {
845
1017
  .orb { color: ButtonText; background: ButtonFace; border: 2px solid ButtonText; forced-color-adjust: auto; }
846
1018
  .orb-surface, .orb-core, .orb-rim { display: none; }
847
1019
  .subtitle-track, .dialog, .safety-card, .recovery-card { color: CanvasText; background: Canvas; border: 1px solid CanvasText; }
1020
+ .page-assistance-revoke { color: ButtonText; background: ButtonFace; border: 1px solid ButtonText; }
848
1021
  .caption-speaker { color: CanvasText; background: Canvas; border-color: CanvasText; }
849
1022
  }
850
1023
  @media (max-width: 520px) {
@@ -853,8 +1026,12 @@ const styles = `
853
1026
  width: 100%;
854
1027
  max-height: calc(var(--voice-viewport-height, 100dvh) - 20px);
855
1028
  }
1029
+ .page-assistance-dialog { border-radius: 14px 14px 8px 8px; }
856
1030
  .dialog-actions { flex-direction: column-reverse; }
857
1031
  .dialog-actions .button { width: 100%; }
1032
+ .page-assistance-revoke {
1033
+ max-width: calc(100vw - var(--voice-orb-size) - 36px);
1034
+ }
858
1035
  }
859
1036
  `;
860
1037
  const icon = (node, className) => {
@@ -863,11 +1040,8 @@ const icon = (node, className) => {
863
1040
  element.setAttribute("aria-hidden", "true");
864
1041
  return element;
865
1042
  };
866
- const isFocusTarget = (value) => value instanceof HTMLElement &&
867
- !value.hasAttribute("disabled") &&
868
- value.getAttribute("aria-hidden") !== "true";
869
1043
  class VoicePresenceView {
870
- constructor(config, overrides, callbacks, tabId, onFallback = () => undefined, visualTestMode = false) {
1044
+ constructor(config, overrides, callbacks, tabId, onFallback = () => undefined, visualTestMode = false, options = {}) {
871
1045
  this.callbacks = callbacks;
872
1046
  this.onFallback = onFallback;
873
1047
  this.captionTimers = new Map();
@@ -879,7 +1053,10 @@ class VoicePresenceView {
879
1053
  this.highlightScrollTargets = [];
880
1054
  this.currentConfirmation = null;
881
1055
  this.currentVisualRequest = null;
882
- this.restoreFocus = null;
1056
+ this.currentHandoffContextRequest = null;
1057
+ this.handoffRequestPreparing = false;
1058
+ this.currentPageAssistanceRequest = null;
1059
+ this.pageAssistancePersistentAllowed = false;
883
1060
  this.active = false;
884
1061
  this.activeElsewhere = false;
885
1062
  this.destroyed = false;
@@ -899,31 +1076,7 @@ class VoicePresenceView {
899
1076
  this.serverPresenceState = "ready";
900
1077
  this.playbackState = "idle";
901
1078
  this.capabilityTier = "standard";
902
- this.onModalKeyDown = (event) => {
903
- if (event.key === "Escape") {
904
- event.preventDefault();
905
- this.closeActivation();
906
- return;
907
- }
908
- if (event.key !== "Tab")
909
- return;
910
- const focusable = Array.from(this.modalLayer.querySelectorAll("button,input,summary,a[href]")).filter(isFocusTarget);
911
- if (focusable.length === 0)
912
- return;
913
- const first = focusable[0];
914
- const last = focusable.at(-1);
915
- if (!first || !last)
916
- return;
917
- const activeElement = this.shadow.activeElement;
918
- if (event.shiftKey && activeElement === first) {
919
- event.preventDefault();
920
- last.focus();
921
- }
922
- else if (!event.shiftKey && activeElement === last) {
923
- event.preventDefault();
924
- first.focus();
925
- }
926
- };
1079
+ this.reducedMotionQuery = null;
927
1080
  this.scheduleCaptionLayout = () => {
928
1081
  if (this.destroyed || this.captionLayoutFrame !== null)
929
1082
  return;
@@ -941,10 +1094,10 @@ class VoicePresenceView {
941
1094
  });
942
1095
  };
943
1096
  this.onVisibilityChange = () => {
944
- if (document.hidden)
945
- this.stopLevelLoop();
946
- else if (this.activeAnalyser())
947
- this.startLevelLoop();
1097
+ this.syncMotionPolicy();
1098
+ };
1099
+ this.onReducedMotionChange = () => {
1100
+ this.syncMotionPolicy();
948
1101
  };
949
1102
  const voiceConfig = config.appearance?.voicePresence;
950
1103
  if (!voiceConfig?.enabled) {
@@ -969,7 +1122,9 @@ class VoicePresenceView {
969
1122
  document.documentElement.append(this.host);
970
1123
  this.orb = this.require('[data-role="orb"]');
971
1124
  this.modalLayer = this.require('[data-role="activation-layer"]');
972
- this.agreement = this.require('[data-role="agreement"]');
1125
+ this.pageAssistanceLayer = this.require('[data-role="page-assistance-layer"]');
1126
+ this.pageAssistanceAlways = this.require('[data-role="page-assistance-always"]');
1127
+ this.pageAssistanceRevoke = this.require('[data-role="page-assistance-revoke"]');
973
1128
  this.captionStage = this.require(".caption-stage");
974
1129
  this.subtitleTrack = this.require('[data-role="subtitle-track"]');
975
1130
  this.statusBubble = this.require('[data-role="status-bubble"]');
@@ -979,6 +1134,10 @@ class VoicePresenceView {
979
1134
  this.liveRegion = this.require('[data-role="live-region"]');
980
1135
  this.audio = this.require("audio");
981
1136
  this.highlight = this.require(".action-highlight");
1137
+ this.dialogFocus = new dialogFocusScope_js_1.DialogFocusScope(this.shadow, this.host);
1138
+ this.reducedMotionQuery =
1139
+ window.matchMedia?.("(prefers-reduced-motion: reduce)") ?? null;
1140
+ this.reducedMotionQuery?.addEventListener?.("change", this.onReducedMotionChange);
982
1141
  if (typeof ResizeObserver !== "undefined") {
983
1142
  this.captionResizeObserver = new ResizeObserver(this.scheduleCaptionLayout);
984
1143
  this.highlightResizeObserver = new ResizeObserver(this.scheduleHighlightLayout);
@@ -988,6 +1147,13 @@ class VoicePresenceView {
988
1147
  this.host.dataset.state = state;
989
1148
  });
990
1149
  this.applyConfig();
1150
+ if (options.previewMode) {
1151
+ for (const link of this.shadow.querySelectorAll("a")) {
1152
+ link.removeAttribute("href");
1153
+ link.removeAttribute("target");
1154
+ link.tabIndex = -1;
1155
+ }
1156
+ }
991
1157
  this.placement = new placement_js_1.LauncherPlacementController({
992
1158
  host: this.host,
993
1159
  preferredSide: this.voiceConfig.launcher.position,
@@ -998,8 +1164,97 @@ class VoicePresenceView {
998
1164
  });
999
1165
  this.bind();
1000
1166
  this.bindPlaybackState();
1167
+ this.syncMotionPolicy();
1001
1168
  this.stateMachine.transition("launcher");
1002
1169
  }
1170
+ open(_mode = "voice") {
1171
+ void _mode;
1172
+ if (!this.active && !this.activeElsewhere)
1173
+ this.openActivation();
1174
+ }
1175
+ close() {
1176
+ if (this.currentPageAssistanceRequest) {
1177
+ this.resolvePageAssistanceRequest("decline");
1178
+ return;
1179
+ }
1180
+ if (this.currentHandoffContextRequest) {
1181
+ this.resolveHandoffContextRequest(false);
1182
+ return;
1183
+ }
1184
+ if (this.currentVisualRequest) {
1185
+ this.resolveVisualRequest(false);
1186
+ return;
1187
+ }
1188
+ if (this.currentConfirmation) {
1189
+ this.resolveConfirmation(false);
1190
+ return;
1191
+ }
1192
+ this.closeActivation();
1193
+ }
1194
+ get isOpen() {
1195
+ return !this.modalLayer.hidden || !this.pageAssistanceLayer.hidden;
1196
+ }
1197
+ rootElement() {
1198
+ return this.host;
1199
+ }
1200
+ setSupportCompanion(linked) {
1201
+ const action = this.require('[data-role="voice-support-action"]');
1202
+ action.hidden = !linked;
1203
+ if (linked && !action.classList.contains("has-reply")) {
1204
+ action.textContent = this.messages.support;
1205
+ action.setAttribute("aria-label", this.messages.support);
1206
+ }
1207
+ }
1208
+ showSupportReplyNotice(visible) {
1209
+ const action = this.require('[data-role="voice-support-action"]');
1210
+ action.classList.toggle("has-reply", visible);
1211
+ action.textContent = visible
1212
+ ? `${this.messages.supportReply} · ${this.messages.viewSupport}`
1213
+ : this.messages.support;
1214
+ action.setAttribute("aria-label", action.textContent);
1215
+ }
1216
+ updatePreviewConfig(config) {
1217
+ const voiceConfig = config.appearance?.voicePresence;
1218
+ if (!voiceConfig?.enabled)
1219
+ return false;
1220
+ this.config = config;
1221
+ this.voiceConfig = voiceConfig;
1222
+ this.host.dataset.position = voiceConfig.launcher.position;
1223
+ this.host.dataset.intensity = voiceConfig.visual.intensity;
1224
+ this.setOrbTheme(voiceConfig.launcher.theme ?? "light");
1225
+ this.applyConfig();
1226
+ this.orb.setAttribute("aria-label", this.launcherLabel());
1227
+ const firstLabel = this.shadow.querySelector('[data-role="first-label"]');
1228
+ if (firstLabel) {
1229
+ firstLabel.textContent = config.appearance?.launcherLabel || "Talk to us";
1230
+ }
1231
+ const title = this.shadow.querySelector("#oxvo-voice-title");
1232
+ const description = this.shadow.querySelector("#oxvo-voice-description");
1233
+ const close = this.shadow.querySelector('[data-role="activation-close"]');
1234
+ const secondary = this.shadow.querySelector('[data-role="activation-secondary"]');
1235
+ const primary = this.shadow.querySelector('[data-role="activation-primary"]');
1236
+ if (title)
1237
+ title.textContent = voiceConfig.activation.title;
1238
+ if (description)
1239
+ description.textContent = voiceConfig.activation.description;
1240
+ close?.setAttribute("aria-label", voiceConfig.activation.secondaryLabel);
1241
+ if (secondary)
1242
+ secondary.textContent = voiceConfig.activation.secondaryLabel;
1243
+ if (primary)
1244
+ primary.textContent = voiceConfig.activation.primaryLabel;
1245
+ const details = this.modalLayer.querySelector("details");
1246
+ const summary = details?.querySelector("summary");
1247
+ if (summary) {
1248
+ summary.textContent =
1249
+ voiceConfig.activation.detailsLabel || this.messages.legalDetails;
1250
+ }
1251
+ const disclosure = details?.querySelector(".legal-copy p");
1252
+ if (disclosure) {
1253
+ disclosure.textContent =
1254
+ config.consent?.disclosure || this.messages.disclosure;
1255
+ }
1256
+ return true;
1257
+ }
1003
1258
  setRemoteStream(stream) {
1004
1259
  const previous = this.audio.srcObject;
1005
1260
  if (previous &&
@@ -1018,6 +1273,7 @@ class VoicePresenceView {
1018
1273
  }
1019
1274
  setCapabilityTier(tier) {
1020
1275
  this.capabilityTier = tier;
1276
+ this.syncMotionPolicy();
1021
1277
  }
1022
1278
  setOrbTheme(theme) {
1023
1279
  this.host.dataset.orbTheme = theme;
@@ -1077,14 +1333,12 @@ class VoicePresenceView {
1077
1333
  setAssistantState(state) {
1078
1334
  if (!this.active)
1079
1335
  return;
1336
+ this.serverPresenceState = state;
1080
1337
  if (state === "thinking" || state === "speaking") {
1081
1338
  this.resumeRemotePlayback();
1082
1339
  }
1083
- this.stateMachine.transition(`active.${state}`);
1084
- if (state === "listening" || state === "speaking")
1085
- this.startLevelLoop();
1086
- else
1087
- this.stopLevelLoop();
1340
+ this.stateMachine.transition(this.effectivePresenceState());
1341
+ this.syncLevelLoop();
1088
1342
  }
1089
1343
  applyPresenceState(state, sequence, sessionId, pageId, fencingToken) {
1090
1344
  this.serverPresenceState = state;
@@ -1109,7 +1363,7 @@ class VoicePresenceView {
1109
1363
  this.closeActivation(false);
1110
1364
  }
1111
1365
  showRequestingMicrophone() {
1112
- this.recoveryCard.hidden = true;
1366
+ this.hideRecoveryCard(false);
1113
1367
  this.orb.hidden = false;
1114
1368
  this.stateMachine.transition("requesting_microphone");
1115
1369
  this.showTransientStatus("Requesting microphone access", 0);
@@ -1139,7 +1393,7 @@ class VoicePresenceView {
1139
1393
  this.stateMachine.transition("active.ready");
1140
1394
  if (!this.introPlayed &&
1141
1395
  this.voiceConfig.visual.introWake &&
1142
- this.capabilityTier === "standard") {
1396
+ this.motionEnabled()) {
1143
1397
  this.introPlayed = true;
1144
1398
  this.orb.classList.add("wake");
1145
1399
  this.wakeTimer = setTimeout(() => {
@@ -1204,12 +1458,29 @@ class VoicePresenceView {
1204
1458
  this.announce(`${role === "assistant" ? "AI" : "You"}: ${normalized}`);
1205
1459
  }
1206
1460
  showConfirmation(value) {
1461
+ this.dismissHandoffRequestPreparing();
1462
+ if (this.currentConfirmation?.actionId === value.actionId &&
1463
+ this.currentConfirmation.proposalChecksum === value.proposalChecksum) {
1464
+ return;
1465
+ }
1466
+ if (this.currentHandoffContextRequest) {
1467
+ this.resolveHandoffContextRequest(false);
1468
+ }
1469
+ if (this.currentVisualRequest)
1470
+ this.resolveVisualRequest(false);
1471
+ if (this.currentConfirmation)
1472
+ this.resolveConfirmation(false);
1207
1473
  this.currentConfirmation = value;
1208
- this.currentVisualRequest = null;
1209
1474
  this.hideStatus();
1210
1475
  this.stateMachine.transition("active.awaiting_confirmation");
1211
1476
  const presentation = confirmationPresentation(value, this.messages.actionRequest, this.messages.approve);
1212
- this.renderSafetyCard(presentation.title, presentation.copy, presentation.approve, this.messages.decline, () => this.resolveConfirmation(true), () => this.resolveConfirmation(false));
1477
+ this.renderSafetyCard(presentation.title, presentation.copy, presentation.approve, this.messages.decline, () => this.resolvePendingConfirmation(true, {
1478
+ actionId: value.actionId,
1479
+ proposalChecksum: value.proposalChecksum,
1480
+ }), () => this.resolvePendingConfirmation(false, {
1481
+ actionId: value.actionId,
1482
+ proposalChecksum: value.proposalChecksum,
1483
+ }));
1213
1484
  }
1214
1485
  showActionState(status) {
1215
1486
  this.currentConfirmation = null;
@@ -1224,7 +1495,9 @@ class VoicePresenceView {
1224
1495
  this.hideStatus();
1225
1496
  return;
1226
1497
  }
1227
- this.showTransientStatus(this.messages.actionFailed, 3500);
1498
+ this.showTransientStatus(status === "unconfirmed"
1499
+ ? this.messages.actionVerificationExpired
1500
+ : this.messages.actionFailed, status === "unconfirmed" ? 6000 : 3500);
1228
1501
  }
1229
1502
  dismissActionState() {
1230
1503
  this.currentConfirmation = null;
@@ -1234,18 +1507,182 @@ class VoicePresenceView {
1234
1507
  this.stateMachine.transition("active.ready");
1235
1508
  }
1236
1509
  showVisualRequest(value) {
1510
+ this.dismissHandoffRequestPreparing();
1511
+ if (this.currentPageAssistanceRequest) {
1512
+ this.resolvePageAssistanceRequest("decline");
1513
+ }
1514
+ if (this.currentHandoffContextRequest) {
1515
+ this.resolveHandoffContextRequest(false);
1516
+ }
1517
+ if (this.currentVisualRequest)
1518
+ this.resolveVisualRequest(false);
1519
+ if (this.currentConfirmation)
1520
+ this.resolveConfirmation(false);
1237
1521
  this.currentVisualRequest = value;
1238
- this.currentConfirmation = null;
1239
1522
  this.hideStatus();
1240
1523
  this.stateMachine.transition("active.awaiting_confirmation");
1241
- this.renderSafetyCard(this.messages.visualRequest, value.reason, this.messages.shareVisual, this.messages.declineVisual, () => this.resolveVisualRequest(true), () => this.resolveVisualRequest(false));
1524
+ this.renderSafetyCard(this.messages.visualRequest, value.reason, this.messages.shareVisual, this.messages.declineVisual, () => {
1525
+ if (this.currentVisualRequest === value)
1526
+ this.resolveVisualRequest(true);
1527
+ }, () => {
1528
+ if (this.currentVisualRequest === value)
1529
+ this.resolveVisualRequest(false);
1530
+ });
1242
1531
  }
1243
1532
  showVisualResult(shared) {
1533
+ if (this.currentHandoffContextRequest)
1534
+ return;
1244
1535
  this.currentVisualRequest = null;
1245
1536
  this.hideSafetyCard();
1246
1537
  this.stateMachine.transition("active.ready");
1247
1538
  this.showTransientStatus(shared ? this.messages.visualShared : this.messages.visualUnavailable, 3500);
1248
1539
  }
1540
+ showHandoffRequestPreparing(title, description) {
1541
+ if (this.currentHandoffContextRequest || this.handoffRequestPreparing) {
1542
+ return;
1543
+ }
1544
+ this.handoffRequestPreparing = true;
1545
+ this.hideStatus();
1546
+ this.stateMachine.transition("active.awaiting_confirmation");
1547
+ const heading = document.createElement("h3");
1548
+ heading.className = "card-title";
1549
+ heading.textContent = title.slice(0, 160);
1550
+ const body = document.createElement("p");
1551
+ body.className = "card-copy";
1552
+ body.textContent = description.slice(0, 600);
1553
+ this.safetyCard.replaceChildren(heading, body);
1554
+ this.safetyCard.setAttribute("role", "status");
1555
+ this.safetyCard.setAttribute("aria-live", "polite");
1556
+ this.safetyCard.setAttribute("aria-busy", "true");
1557
+ this.safetyCard.removeAttribute("aria-modal");
1558
+ this.safetyCard.removeAttribute("aria-labelledby");
1559
+ this.safetyCard.removeAttribute("aria-describedby");
1560
+ this.safetyCard.hidden = false;
1561
+ this.syncSubtitleOcclusion();
1562
+ }
1563
+ dismissHandoffRequestPreparing() {
1564
+ if (!this.handoffRequestPreparing)
1565
+ return;
1566
+ this.handoffRequestPreparing = false;
1567
+ this.hideSafetyCard();
1568
+ if (this.active)
1569
+ this.stateMachine.transition("active.ready");
1570
+ }
1571
+ showHandoffContextRequest(value) {
1572
+ this.dismissHandoffRequestPreparing();
1573
+ const exactRequest = this.currentHandoffContextRequest?.requestId === value.requestId &&
1574
+ this.currentHandoffContextRequest.requestChecksum ===
1575
+ value.requestChecksum;
1576
+ if (exactRequest &&
1577
+ !this.safetyCard.hidden &&
1578
+ this.safetyCard.isConnected &&
1579
+ this.safetyCard.querySelector('[data-role="safety-decline"]') &&
1580
+ this.safetyCard.querySelector('[data-role="safety-approve"]')) {
1581
+ return true;
1582
+ }
1583
+ if (!exactRequest) {
1584
+ if (this.currentPageAssistanceRequest) {
1585
+ this.resolvePageAssistanceRequest("decline");
1586
+ }
1587
+ if (this.currentVisualRequest)
1588
+ this.resolveVisualRequest(false);
1589
+ if (this.currentHandoffContextRequest) {
1590
+ this.resolveHandoffContextRequest(false);
1591
+ }
1592
+ if (this.currentConfirmation)
1593
+ this.resolveConfirmation(false);
1594
+ this.currentHandoffContextRequest = value;
1595
+ }
1596
+ const activeRequest = exactRequest
1597
+ ? this.currentHandoffContextRequest
1598
+ : value;
1599
+ this.hideStatus();
1600
+ this.stateMachine.transition("active.awaiting_confirmation");
1601
+ this.renderSafetyCard(activeRequest.title, activeRequest.description, this.messages.handoffContextShare, this.messages.handoffContextNotNow, () => {
1602
+ if (this.currentHandoffContextRequest === activeRequest)
1603
+ this.resolveHandoffContextRequest(true);
1604
+ }, () => {
1605
+ if (this.currentHandoffContextRequest === activeRequest)
1606
+ this.resolveHandoffContextRequest(false);
1607
+ });
1608
+ return Boolean(this.currentHandoffContextRequest?.requestId === activeRequest.requestId &&
1609
+ this.currentHandoffContextRequest.requestChecksum ===
1610
+ activeRequest.requestChecksum &&
1611
+ !this.safetyCard.hidden &&
1612
+ this.safetyCard.isConnected &&
1613
+ this.safetyCard.querySelector('[data-role="safety-decline"]') &&
1614
+ this.safetyCard.querySelector('[data-role="safety-approve"]'));
1615
+ }
1616
+ dismissHandoffContextRequest() {
1617
+ this.dismissHandoffRequestPreparing();
1618
+ this.currentHandoffContextRequest = null;
1619
+ if (this.currentConfirmation ||
1620
+ this.currentVisualRequest ||
1621
+ this.currentPageAssistanceRequest) {
1622
+ return;
1623
+ }
1624
+ this.hideSafetyCard();
1625
+ if (this.active)
1626
+ this.stateMachine.transition("active.ready");
1627
+ }
1628
+ showHandoffContextResult(status) {
1629
+ this.dismissHandoffContextRequest();
1630
+ if (status === "granted") {
1631
+ this.showTransientStatus(this.messages.humanConnecting, 3500);
1632
+ return;
1633
+ }
1634
+ if (status === "expired" || status === "unavailable") {
1635
+ this.showTransientStatus(this.messages.humanUnavailable, 3500);
1636
+ return;
1637
+ }
1638
+ this.hideStatus();
1639
+ }
1640
+ showPageAssistanceRequest(value) {
1641
+ this.dismissHandoffRequestPreparing();
1642
+ if (this.currentConfirmation)
1643
+ this.resolveConfirmation(false);
1644
+ if (this.currentVisualRequest)
1645
+ this.resolveVisualRequest(false);
1646
+ if (this.currentHandoffContextRequest) {
1647
+ this.resolveHandoffContextRequest(false);
1648
+ }
1649
+ if (this.currentPageAssistanceRequest?.requestId === value.requestId)
1650
+ return;
1651
+ if (this.currentPageAssistanceRequest) {
1652
+ this.resolvePageAssistanceRequest("decline", false);
1653
+ }
1654
+ this.currentPageAssistanceRequest = value;
1655
+ const description = compactUiText(value.safeActionDescription, 360);
1656
+ this.require('[data-role="page-assistance-description"]').textContent =
1657
+ description || PAGE_ASSISTANCE_FALLBACK;
1658
+ this.require('[data-role="page-assistance-target"]').textContent =
1659
+ compactUiText(value.safeTargetLabel, 160);
1660
+ const alwaysLabel = this.require('[data-role="page-assistance-always-label"]');
1661
+ this.pageAssistanceAlways.checked = false;
1662
+ alwaysLabel.hidden = !value.persistenceEligible;
1663
+ this.pageAssistanceLayer.hidden = false;
1664
+ this.syncPageAssistanceRevoke();
1665
+ this.hideStatus();
1666
+ this.syncSubtitleOcclusion();
1667
+ if (this.active)
1668
+ this.stateMachine.transition("active.awaiting_confirmation");
1669
+ this.callbacks.onOpenChange?.(true);
1670
+ this.dialogFocus.present({
1671
+ surface: this.pageAssistanceLayer,
1672
+ initialFocus: () => this.require('[data-role="page-assistance-decline"]'),
1673
+ fallbackFocus: () => (this.orb.hidden ? null : this.orb),
1674
+ onEscape: () => {
1675
+ this.resolvePageAssistanceRequest("decline");
1676
+ },
1677
+ });
1678
+ }
1679
+ dismissPageAssistanceRequest() {
1680
+ this.hidePageAssistanceRequest(true);
1681
+ }
1682
+ setPageAssistancePersistentAllowed(allowed) {
1683
+ this.pageAssistancePersistentAllowed = allowed;
1684
+ this.syncPageAssistanceRevoke();
1685
+ }
1249
1686
  setVisualAvailable(_available, _paused = false) {
1250
1687
  void _available;
1251
1688
  void _paused;
@@ -1281,6 +1718,11 @@ class VoicePresenceView {
1281
1718
  this.stateMachine.transition("active.ready");
1282
1719
  }
1283
1720
  showError(message, retryable = false, sessionActive = this.active) {
1721
+ this.hidePageAssistanceRequest(false);
1722
+ this.currentConfirmation = null;
1723
+ this.currentVisualRequest = null;
1724
+ this.currentHandoffContextRequest = null;
1725
+ this.hideSafetyCard();
1284
1726
  const safeMessage = message || this.messages.error;
1285
1727
  if (sessionActive && this.active) {
1286
1728
  this.stateMachine.transition(retryable ? "recoverable_error" : "terminal_error");
@@ -1293,11 +1735,66 @@ class VoicePresenceView {
1293
1735
  this.orb.hidden = true;
1294
1736
  this.renderRecoveryCard(safeMessage, retryable);
1295
1737
  }
1738
+ showEndingDetached(message) {
1739
+ this.active = false;
1740
+ this.hidePageAssistanceRequest(false);
1741
+ this.currentConfirmation = null;
1742
+ this.currentVisualRequest = null;
1743
+ this.currentHandoffContextRequest = null;
1744
+ this.hideSafetyCard();
1745
+ this.hideRecoveryCard(false);
1746
+ this.clearCaptions();
1747
+ this.hideStatus();
1748
+ this.hideAttribution();
1749
+ this.stopAudio();
1750
+ this.stateMachine.transition("ending");
1751
+ this.orb.dataset.active = "false";
1752
+ this.orb.hidden = true;
1753
+ this.showTransientStatus(message || this.messages.ending, 0);
1754
+ this.placement.refresh();
1755
+ }
1756
+ showEndRetry(message) {
1757
+ this.active = true;
1758
+ this.orb.hidden = false;
1759
+ this.orb.dataset.active = "true";
1760
+ this.stateMachine.transition("terminal_error");
1761
+ this.renderRecoveryCard(message || this.messages.endRetry, false, {
1762
+ label: this.messages.end,
1763
+ onSelect: () => {
1764
+ this.hideRecoveryCard();
1765
+ this.stateMachine.transition("ending");
1766
+ this.callbacks.onEnd();
1767
+ },
1768
+ });
1769
+ this.placement.refresh();
1770
+ }
1771
+ showResumeStopped() {
1772
+ this.active = false;
1773
+ this.hidePageAssistanceRequest(false);
1774
+ this.currentConfirmation = null;
1775
+ this.currentVisualRequest = null;
1776
+ this.currentHandoffContextRequest = null;
1777
+ this.hideSafetyCard();
1778
+ this.hideRecoveryCard(false);
1779
+ this.clearCaptions();
1780
+ this.hideStatus();
1781
+ this.hideAttribution();
1782
+ this.stopAudio();
1783
+ this.orb.dataset.active = "false";
1784
+ this.orb.hidden = false;
1785
+ this.orb.setAttribute("aria-label", this.launcherLabel());
1786
+ this.stateMachine.reset();
1787
+ this.stateMachine.transition("launcher");
1788
+ this.placement.refresh();
1789
+ }
1296
1790
  showEnded(message) {
1297
1791
  this.active = false;
1792
+ this.hidePageAssistanceRequest(false);
1793
+ this.currentConfirmation = null;
1794
+ this.currentVisualRequest = null;
1795
+ this.currentHandoffContextRequest = null;
1298
1796
  this.hideSafetyCard();
1299
- this.recoveryCard.hidden = true;
1300
- this.recoveryCard.replaceChildren();
1797
+ this.hideRecoveryCard(false);
1301
1798
  this.clearCaptions();
1302
1799
  this.hideStatus();
1303
1800
  this.stopAudio();
@@ -1320,9 +1817,13 @@ class VoicePresenceView {
1320
1817
  this.orb.hidden = active;
1321
1818
  this.orb.setAttribute("aria-label", active ? this.messages.activeElsewhere : this.launcherLabel());
1322
1819
  if (active) {
1820
+ this.hidePageAssistanceRequest(false);
1821
+ this.currentConfirmation = null;
1822
+ this.currentVisualRequest = null;
1823
+ this.currentHandoffContextRequest = null;
1323
1824
  this.closeActivation(false);
1324
1825
  this.hideSafetyCard();
1325
- this.recoveryCard.hidden = true;
1826
+ this.hideRecoveryCard(false);
1326
1827
  this.hideStatus();
1327
1828
  return;
1328
1829
  }
@@ -1330,6 +1831,11 @@ class VoicePresenceView {
1330
1831
  }
1331
1832
  showLaunchUnavailable(message) {
1332
1833
  this.active = false;
1834
+ this.hidePageAssistanceRequest(false);
1835
+ this.currentConfirmation = null;
1836
+ this.currentVisualRequest = null;
1837
+ this.currentHandoffContextRequest = null;
1838
+ this.hideSafetyCard();
1333
1839
  this.orb.hidden = true;
1334
1840
  this.renderRecoveryCard(message, false);
1335
1841
  this.stateMachine.reset();
@@ -1379,15 +1885,25 @@ class VoicePresenceView {
1379
1885
  cancelPendingConfirmation() {
1380
1886
  if (this.resolvePendingConfirmation(false))
1381
1887
  return true;
1888
+ if (this.currentHandoffContextRequest) {
1889
+ this.resolveHandoffContextRequest(false);
1890
+ return true;
1891
+ }
1382
1892
  if (this.currentVisualRequest) {
1383
1893
  this.resolveVisualRequest(false);
1384
1894
  return true;
1385
1895
  }
1386
1896
  return false;
1387
1897
  }
1388
- resolvePendingConfirmation(approved) {
1389
- if (!this.currentConfirmation)
1898
+ resolvePendingConfirmation(approved, expected) {
1899
+ const current = this.currentConfirmation;
1900
+ if (!current)
1901
+ return false;
1902
+ if (expected &&
1903
+ (current.actionId !== expected.actionId ||
1904
+ current.proposalChecksum !== expected.proposalChecksum)) {
1390
1905
  return false;
1906
+ }
1391
1907
  this.resolveConfirmation(approved);
1392
1908
  return true;
1393
1909
  }
@@ -1396,11 +1912,11 @@ class VoicePresenceView {
1396
1912
  return;
1397
1913
  this.destroyed = true;
1398
1914
  document.removeEventListener("visibilitychange", this.onVisibilityChange);
1915
+ this.reducedMotionQuery?.removeEventListener?.("change", this.onReducedMotionChange);
1399
1916
  document.removeEventListener("scroll", this.scheduleHighlightLayout, true);
1400
1917
  window.removeEventListener("resize", this.scheduleHighlightLayout);
1401
1918
  window.visualViewport?.removeEventListener("resize", this.scheduleHighlightLayout);
1402
1919
  window.visualViewport?.removeEventListener("scroll", this.scheduleHighlightLayout);
1403
- this.modalLayer.removeEventListener("keydown", this.onModalKeyDown);
1404
1920
  if (this.wakeTimer)
1405
1921
  clearTimeout(this.wakeTimer);
1406
1922
  if (this.statusTimer)
@@ -1414,6 +1930,7 @@ class VoicePresenceView {
1414
1930
  this.captionResizeObserver?.disconnect();
1415
1931
  this.highlightResizeObserver?.disconnect();
1416
1932
  this.unbindHighlightScrollTargets();
1933
+ this.dialogFocus.destroy();
1417
1934
  this.placement.destroy();
1418
1935
  this.clearCaptions();
1419
1936
  this.stopAudio();
@@ -1456,13 +1973,30 @@ class VoicePresenceView {
1456
1973
  safety.dataset.role = "safety-card";
1457
1974
  safety.hidden = true;
1458
1975
  safety.setAttribute("role", "alertdialog");
1459
- safety.setAttribute("aria-modal", "false");
1976
+ safety.setAttribute("aria-modal", "true");
1977
+ safety.setAttribute("aria-labelledby", "oxvo-voice-safety-title");
1978
+ safety.setAttribute("aria-describedby", "oxvo-voice-safety-copy");
1460
1979
  const recovery = document.createElement("div");
1461
1980
  recovery.className = "recovery-card";
1462
1981
  recovery.dataset.role = "recovery-card";
1463
1982
  recovery.hidden = true;
1464
1983
  recovery.setAttribute("role", "alertdialog");
1465
- recovery.setAttribute("aria-modal", "false");
1984
+ recovery.setAttribute("aria-modal", "true");
1985
+ const pageAssistanceRevoke = document.createElement("button");
1986
+ pageAssistanceRevoke.type = "button";
1987
+ pageAssistanceRevoke.className = "page-assistance-revoke";
1988
+ pageAssistanceRevoke.dataset.role = "page-assistance-revoke";
1989
+ pageAssistanceRevoke.textContent = PAGE_ASSISTANCE_REVOKE;
1990
+ pageAssistanceRevoke.setAttribute("aria-label", PAGE_ASSISTANCE_REVOKE);
1991
+ pageAssistanceRevoke.hidden = true;
1992
+ const supportAction = document.createElement("button");
1993
+ supportAction.type = "button";
1994
+ supportAction.className = "voice-support-action";
1995
+ supportAction.dataset.role = "voice-support-action";
1996
+ supportAction.textContent = this.messages.support;
1997
+ supportAction.setAttribute("aria-label", this.messages.support);
1998
+ supportAction.setAttribute("aria-live", "polite");
1999
+ supportAction.hidden = true;
1466
2000
  const orb = document.createElement("button");
1467
2001
  orb.type = "button";
1468
2002
  orb.className = "orb";
@@ -1496,7 +2030,7 @@ class VoicePresenceView {
1496
2030
  label.textContent = this.config.appearance?.launcherLabel || "Talk to us";
1497
2031
  orbWrap.append(label);
1498
2032
  }
1499
- orbWrap.append(captions, status, attribution, safety, recovery, orb);
2033
+ orbWrap.append(captions, status, attribution, safety, recovery, pageAssistanceRevoke, supportAction, orb);
1500
2034
  const modalLayer = document.createElement("div");
1501
2035
  modalLayer.className = "dialog-layer";
1502
2036
  modalLayer.dataset.role = "activation-layer";
@@ -1524,24 +2058,21 @@ class VoicePresenceView {
1524
2058
  close.setAttribute("aria-label", this.voiceConfig.activation.secondaryLabel);
1525
2059
  close.append(icon(lucide_1.X, "close-symbol"));
1526
2060
  head.append(copy, close);
1527
- const agreement = document.createElement("label");
1528
- agreement.className = "agreement";
1529
- const checkbox = document.createElement("input");
1530
- checkbox.type = "checkbox";
1531
- checkbox.checked = true;
1532
- checkbox.dataset.role = "agreement";
1533
- const agreementText = document.createElement("span");
1534
- agreementText.textContent = `I agree to use ${this.config.appearance?.brandName || "AI voice assist"}.`;
1535
- agreement.append(checkbox, agreementText);
1536
- if (this.voiceConfig.activation.detailsLabel) {
2061
+ const pageContextDisclosed = this.config.capabilities?.pageContext === true &&
2062
+ this.config.consent?.requiredScopes.includes("page_context") === true;
2063
+ if (this.voiceConfig.activation.detailsLabel || pageContextDisclosed) {
1537
2064
  const details = document.createElement("details");
1538
2065
  const summary = document.createElement("summary");
1539
- summary.textContent = this.voiceConfig.activation.detailsLabel;
2066
+ summary.textContent =
2067
+ this.voiceConfig.activation.detailsLabel || this.messages.legalDetails;
1540
2068
  const legal = document.createElement("div");
1541
2069
  legal.className = "legal-copy";
1542
2070
  const disclosure = document.createElement("p");
1543
2071
  disclosure.textContent =
1544
2072
  this.config.consent?.disclosure || this.messages.disclosure;
2073
+ const pageContext = document.createElement("p");
2074
+ pageContext.textContent = this.messages.pageConsent;
2075
+ pageContext.hidden = !pageContextDisclosed;
1545
2076
  const boundary = document.createElement("p");
1546
2077
  boundary.textContent = this.messages.safetyBoundary;
1547
2078
  const links = document.createElement("div");
@@ -1552,12 +2083,12 @@ class VoicePresenceView {
1552
2083
  if (this.config.consent?.termsUrl) {
1553
2084
  links.append(this.legalLink(this.config.consent.termsUrl, this.messages.termsOfService));
1554
2085
  }
1555
- legal.append(disclosure, boundary, links);
2086
+ legal.append(disclosure, pageContext, boundary, links);
1556
2087
  details.append(summary, legal);
1557
- dialog.append(head, agreement, details);
2088
+ dialog.append(head, details);
1558
2089
  }
1559
2090
  else {
1560
- dialog.append(head, agreement);
2091
+ dialog.append(head);
1561
2092
  }
1562
2093
  const actions = document.createElement("div");
1563
2094
  actions.className = "dialog-actions";
@@ -1574,6 +2105,63 @@ class VoicePresenceView {
1574
2105
  actions.append(secondary, primary);
1575
2106
  dialog.append(actions);
1576
2107
  modalLayer.append(dialog);
2108
+ const pageAssistanceLayer = document.createElement("div");
2109
+ pageAssistanceLayer.className = "dialog-layer";
2110
+ pageAssistanceLayer.dataset.role = "page-assistance-layer";
2111
+ pageAssistanceLayer.hidden = true;
2112
+ const pageAssistanceDialog = document.createElement("section");
2113
+ pageAssistanceDialog.className = "dialog page-assistance-dialog";
2114
+ pageAssistanceDialog.setAttribute("role", "alertdialog");
2115
+ pageAssistanceDialog.setAttribute("aria-modal", "true");
2116
+ pageAssistanceDialog.setAttribute("aria-labelledby", "oxvo-page-assistance-title");
2117
+ pageAssistanceDialog.setAttribute("aria-describedby", "oxvo-page-assistance-description oxvo-page-assistance-target");
2118
+ const pageAssistanceHead = document.createElement("div");
2119
+ pageAssistanceHead.className = "dialog-head";
2120
+ const pageAssistanceCopy = document.createElement("div");
2121
+ pageAssistanceCopy.className = "page-assistance-copy";
2122
+ const pageAssistanceTitle = document.createElement("h2");
2123
+ pageAssistanceTitle.id = "oxvo-page-assistance-title";
2124
+ pageAssistanceTitle.textContent = PAGE_ASSISTANCE_TITLE;
2125
+ const pageAssistanceDescription = document.createElement("p");
2126
+ pageAssistanceDescription.id = "oxvo-page-assistance-description";
2127
+ pageAssistanceDescription.dataset.role = "page-assistance-description";
2128
+ pageAssistanceDescription.textContent = PAGE_ASSISTANCE_FALLBACK;
2129
+ const pageAssistanceTarget = document.createElement("p");
2130
+ pageAssistanceTarget.id = "oxvo-page-assistance-target";
2131
+ pageAssistanceTarget.className = "page-assistance-target";
2132
+ pageAssistanceTarget.dataset.role = "page-assistance-target";
2133
+ pageAssistanceCopy.append(pageAssistanceTitle, pageAssistanceDescription, pageAssistanceTarget);
2134
+ const pageAssistanceClose = document.createElement("button");
2135
+ pageAssistanceClose.type = "button";
2136
+ pageAssistanceClose.className = "icon-button";
2137
+ pageAssistanceClose.dataset.role = "page-assistance-close";
2138
+ pageAssistanceClose.setAttribute("aria-label", PAGE_ASSISTANCE_DECLINE);
2139
+ pageAssistanceClose.append(icon(lucide_1.X, "close-symbol"));
2140
+ pageAssistanceHead.append(pageAssistanceCopy, pageAssistanceClose);
2141
+ const pageAssistanceAlways = document.createElement("label");
2142
+ pageAssistanceAlways.className = "page-assistance-always";
2143
+ pageAssistanceAlways.dataset.role = "page-assistance-always-label";
2144
+ const pageAssistanceCheckbox = document.createElement("input");
2145
+ pageAssistanceCheckbox.type = "checkbox";
2146
+ pageAssistanceCheckbox.dataset.role = "page-assistance-always";
2147
+ const pageAssistanceAlwaysText = document.createElement("span");
2148
+ pageAssistanceAlwaysText.textContent = PAGE_ASSISTANCE_ALWAYS;
2149
+ pageAssistanceAlways.append(pageAssistanceCheckbox, pageAssistanceAlwaysText);
2150
+ const pageAssistanceActions = document.createElement("div");
2151
+ pageAssistanceActions.className = "dialog-actions";
2152
+ const pageAssistanceDecline = document.createElement("button");
2153
+ pageAssistanceDecline.type = "button";
2154
+ pageAssistanceDecline.className = "button";
2155
+ pageAssistanceDecline.dataset.role = "page-assistance-decline";
2156
+ pageAssistanceDecline.textContent = PAGE_ASSISTANCE_DECLINE;
2157
+ const pageAssistanceAllow = document.createElement("button");
2158
+ pageAssistanceAllow.type = "button";
2159
+ pageAssistanceAllow.className = "button primary";
2160
+ pageAssistanceAllow.dataset.role = "page-assistance-allow";
2161
+ pageAssistanceAllow.textContent = PAGE_ASSISTANCE_ALLOW;
2162
+ pageAssistanceActions.append(pageAssistanceDecline, pageAssistanceAllow);
2163
+ pageAssistanceDialog.append(pageAssistanceHead, pageAssistanceAlways, pageAssistanceActions);
2164
+ pageAssistanceLayer.append(pageAssistanceDialog);
1577
2165
  const live = document.createElement("div");
1578
2166
  live.className = "hidden-live";
1579
2167
  live.dataset.role = "live-region";
@@ -1586,7 +2174,7 @@ class VoicePresenceView {
1586
2174
  const highlight = document.createElement("div");
1587
2175
  highlight.className = "action-highlight";
1588
2176
  highlight.setAttribute("aria-hidden", "true");
1589
- fragment.append(presenceField, orbWrap, modalLayer, live, audio, highlight);
2177
+ fragment.append(presenceField, orbWrap, modalLayer, pageAssistanceLayer, live, audio, highlight);
1590
2178
  return fragment;
1591
2179
  }
1592
2180
  bind() {
@@ -1602,27 +2190,55 @@ class VoicePresenceView {
1602
2190
  });
1603
2191
  this.require('[data-role="activation-close"]').addEventListener("click", () => this.closeActivation());
1604
2192
  this.require('[data-role="activation-secondary"]').addEventListener("click", () => this.closeActivation());
1605
- this.require('[data-role="activation-primary"]').addEventListener("click", () => {
1606
- if (!this.agreement.checked) {
1607
- this.agreement.focus();
2193
+ const activationPrimary = this.require('[data-role="activation-primary"]');
2194
+ activationPrimary.addEventListener("click", () => {
2195
+ if (this.modalLayer.hidden || activationPrimary.disabled)
2196
+ return;
2197
+ activationPrimary.disabled = true;
2198
+ activationPrimary.setAttribute("aria-busy", "true");
2199
+ const requiredScopes = new Set(this.config.consent?.requiredScopes ?? []);
2200
+ const scopes = [
2201
+ "ai_disclosure",
2202
+ "text",
2203
+ "microphone",
2204
+ ];
2205
+ for (const scope of [
2206
+ "page_context",
2207
+ "presentation_guidance",
2208
+ ]) {
2209
+ if (requiredScopes.has(scope))
2210
+ scopes.push(scope);
2211
+ }
2212
+ if (!this.callbacks.onStart("voice", scopes)) {
2213
+ activationPrimary.disabled = false;
2214
+ activationPrimary.removeAttribute("aria-busy");
2215
+ activationPrimary.focus({ preventScroll: true });
1608
2216
  return;
1609
2217
  }
1610
2218
  this.ensureAudioContext();
1611
2219
  this.showPreflight();
1612
- const consent = this.config.consent;
1613
- const scopes = [
1614
- ...new Set([
1615
- ...(consent?.requiredScopes ?? []),
1616
- ...(consent?.optionalScopes ?? []),
1617
- ]),
1618
- ];
1619
- this.callbacks.onStart("voice", scopes);
1620
2220
  });
1621
- this.modalLayer.addEventListener("keydown", this.onModalKeyDown);
1622
2221
  this.modalLayer.addEventListener("pointerdown", (event) => {
1623
2222
  if (event.target === this.modalLayer)
1624
2223
  this.closeActivation();
1625
2224
  });
2225
+ this.pageAssistanceLayer.addEventListener("pointerdown", (event) => {
2226
+ if (event.target === this.pageAssistanceLayer) {
2227
+ this.resolvePageAssistanceRequest("decline");
2228
+ }
2229
+ });
2230
+ this.require('[data-role="page-assistance-close"]').addEventListener("click", () => this.resolvePageAssistanceRequest("decline"));
2231
+ this.require('[data-role="page-assistance-decline"]').addEventListener("click", () => this.resolvePageAssistanceRequest("decline"));
2232
+ this.require('[data-role="page-assistance-allow"]').addEventListener("click", () => {
2233
+ const persistent = this.currentPageAssistanceRequest?.persistenceEligible === true &&
2234
+ this.pageAssistanceAlways.checked;
2235
+ this.resolvePageAssistanceRequest(persistent ? "allow_persistent" : "allow_session");
2236
+ });
2237
+ this.pageAssistanceRevoke.addEventListener("click", () => {
2238
+ this.setPageAssistancePersistentAllowed(false);
2239
+ this.callbacks.onPageAssistanceRevoke?.();
2240
+ });
2241
+ this.require('[data-role="voice-support-action"]').addEventListener("click", () => this.callbacks.onHuman());
1626
2242
  document.addEventListener("visibilitychange", this.onVisibilityChange);
1627
2243
  document.addEventListener("scroll", this.scheduleHighlightLayout, {
1628
2244
  capture: true,
@@ -1687,9 +2303,10 @@ class VoicePresenceView {
1687
2303
  state === "reconnecting") {
1688
2304
  return `active.${state}`;
1689
2305
  }
1690
- if (this.playbackState === "playing")
1691
- return "active.speaking";
1692
2306
  if (state === "speaking") {
2307
+ if (this.playbackState === "playing") {
2308
+ return "active.speaking";
2309
+ }
1693
2310
  return this.playbackState === "paused" || this.playbackState === "ended"
1694
2311
  ? "active.ready"
1695
2312
  : "active.thinking";
@@ -1729,31 +2346,35 @@ class VoicePresenceView {
1729
2346
  this.host.style.setProperty("--voice-orb-size", this.voiceConfig.launcher.size === "compact" ? "52px" : "58px");
1730
2347
  }
1731
2348
  openActivation() {
2349
+ if (!this.modalLayer.hidden)
2350
+ return;
1732
2351
  this.hideAttribution();
1733
- const shadowActiveElement = this.shadow.activeElement;
1734
- const documentActiveElement = document.activeElement;
1735
- this.restoreFocus =
1736
- shadowActiveElement instanceof HTMLElement
1737
- ? shadowActiveElement
1738
- : documentActiveElement instanceof HTMLElement &&
1739
- documentActiveElement !== this.host
1740
- ? documentActiveElement
1741
- : this.orb;
1742
2352
  this.require('[data-role="first-label"]', false)?.remove();
1743
2353
  this.placement.refresh(true);
2354
+ const primary = this.require('[data-role="activation-primary"]');
2355
+ primary.disabled = false;
2356
+ primary.removeAttribute("aria-busy");
1744
2357
  this.modalLayer.hidden = false;
1745
2358
  this.stateMachine.transition("activation_modal");
1746
- queueMicrotask(() => this.agreement.focus());
2359
+ this.callbacks.onOpenChange?.(true);
2360
+ this.dialogFocus.present({
2361
+ surface: this.modalLayer,
2362
+ initialFocus: () => primary,
2363
+ fallbackFocus: () => (this.orb.hidden ? null : this.orb),
2364
+ onEscape: () => this.closeActivation(),
2365
+ });
1747
2366
  }
1748
2367
  closeActivation(restore = true) {
1749
2368
  if (this.modalLayer.hidden)
1750
2369
  return;
1751
2370
  this.modalLayer.hidden = true;
2371
+ const primary = this.require('[data-role="activation-primary"]');
2372
+ primary.disabled = false;
2373
+ primary.removeAttribute("aria-busy");
1752
2374
  if (this.stateMachine.state === "activation_modal")
1753
2375
  this.stateMachine.transition("launcher");
1754
- if (restore)
1755
- this.restoreFocus?.focus();
1756
- this.restoreFocus = null;
2376
+ this.callbacks.onOpenChange?.(false);
2377
+ this.dialogFocus.dismiss(this.modalLayer, { restore });
1757
2378
  this.placement.refresh();
1758
2379
  }
1759
2380
  showAttribution() {
@@ -1772,12 +2393,46 @@ class VoicePresenceView {
1772
2393
  this.attributionTimer = null;
1773
2394
  this.attributionBubble.hidden = true;
1774
2395
  }
2396
+ resolvePageAssistanceRequest(decision, restore = true) {
2397
+ const request = this.currentPageAssistanceRequest;
2398
+ if (!request)
2399
+ return;
2400
+ const normalizedDecision = decision === "allow_persistent" && !request.persistenceEligible
2401
+ ? "allow_session"
2402
+ : decision;
2403
+ this.hidePageAssistanceRequest(restore);
2404
+ this.callbacks.onPageAssistance?.(request, normalizedDecision);
2405
+ }
2406
+ hidePageAssistanceRequest(restore) {
2407
+ if (!this.currentPageAssistanceRequest && this.pageAssistanceLayer.hidden) {
2408
+ return;
2409
+ }
2410
+ const wasOpen = !this.pageAssistanceLayer.hidden;
2411
+ this.currentPageAssistanceRequest = null;
2412
+ this.pageAssistanceAlways.checked = false;
2413
+ this.pageAssistanceLayer.hidden = true;
2414
+ this.syncPageAssistanceRevoke();
2415
+ this.syncSubtitleOcclusion();
2416
+ if (this.active && this.stateMachine.state === "active.awaiting_confirmation") {
2417
+ this.stateMachine.transition("active.ready");
2418
+ }
2419
+ if (wasOpen)
2420
+ this.callbacks.onOpenChange?.(false);
2421
+ this.dialogFocus.dismiss(this.pageAssistanceLayer, { restore });
2422
+ }
2423
+ syncPageAssistanceRevoke() {
2424
+ this.pageAssistanceRevoke.hidden =
2425
+ !this.pageAssistancePersistentAllowed || !this.pageAssistanceLayer.hidden;
2426
+ }
1775
2427
  renderSafetyCard(title, copy, approveLabel, declineLabel, approve, decline) {
2428
+ this.restoreSafetyDialogAttributes();
1776
2429
  const heading = document.createElement("h3");
1777
2430
  heading.className = "card-title";
2431
+ heading.id = "oxvo-voice-safety-title";
1778
2432
  heading.textContent = title.slice(0, 160);
1779
2433
  const body = document.createElement("p");
1780
2434
  body.className = "card-copy";
2435
+ body.id = "oxvo-voice-safety-copy";
1781
2436
  body.textContent = copy.slice(0, 600);
1782
2437
  const actions = document.createElement("div");
1783
2438
  actions.className = "card-actions";
@@ -1797,21 +2452,43 @@ class VoicePresenceView {
1797
2452
  this.safetyCard.replaceChildren(heading, body, actions);
1798
2453
  this.safetyCard.hidden = false;
1799
2454
  this.syncSubtitleOcclusion();
1800
- queueMicrotask(() => no.focus());
2455
+ this.dialogFocus.present({
2456
+ surface: this.safetyCard,
2457
+ initialFocus: () => no,
2458
+ fallbackFocus: () => (this.orb.hidden ? null : this.orb),
2459
+ onEscape: decline,
2460
+ });
1801
2461
  }
1802
- hideSafetyCard() {
2462
+ hideSafetyCard(restore = true) {
2463
+ this.handoffRequestPreparing = false;
1803
2464
  this.safetyCard.hidden = true;
1804
2465
  this.safetyCard.replaceChildren();
2466
+ this.restoreSafetyDialogAttributes();
2467
+ this.dialogFocus.dismiss(this.safetyCard, { restore });
2468
+ if (!restore)
2469
+ this.dialogFocus.clear({ restore: false });
1805
2470
  this.syncSubtitleOcclusion();
1806
2471
  }
2472
+ restoreSafetyDialogAttributes() {
2473
+ this.safetyCard.setAttribute("role", "alertdialog");
2474
+ this.safetyCard.setAttribute("aria-modal", "true");
2475
+ this.safetyCard.setAttribute("aria-labelledby", "oxvo-voice-safety-title");
2476
+ this.safetyCard.setAttribute("aria-describedby", "oxvo-voice-safety-copy");
2477
+ this.safetyCard.removeAttribute("aria-live");
2478
+ this.safetyCard.removeAttribute("aria-busy");
2479
+ }
1807
2480
  resolveConfirmation(approved) {
1808
2481
  const confirmation = this.currentConfirmation;
1809
2482
  if (!confirmation)
1810
2483
  return;
2484
+ const accepted = approved && Date.parse(confirmation.expiresAt) > Date.now();
1811
2485
  this.currentConfirmation = null;
1812
2486
  this.hideSafetyCard();
1813
2487
  this.stateMachine.transition("active.ready");
1814
- this.callbacks.onConfirmation(confirmation.actionId, confirmation.proposalChecksum, approved);
2488
+ if (approved && !accepted) {
2489
+ this.showTransientStatus(this.messages.actionFailed, 3500);
2490
+ }
2491
+ this.callbacks.onConfirmation(confirmation.actionId, confirmation.proposalChecksum, accepted);
1815
2492
  }
1816
2493
  resolveVisualRequest(approved) {
1817
2494
  const request = this.currentVisualRequest;
@@ -1822,7 +2499,20 @@ class VoicePresenceView {
1822
2499
  this.stateMachine.transition("active.ready");
1823
2500
  this.callbacks.onVisualContext(request, approved);
1824
2501
  }
1825
- renderRecoveryCard(message, retryable) {
2502
+ resolveHandoffContextRequest(approved) {
2503
+ const request = this.currentHandoffContextRequest;
2504
+ if (!request)
2505
+ return;
2506
+ this.currentHandoffContextRequest = null;
2507
+ this.hideSafetyCard();
2508
+ if (this.active)
2509
+ this.stateMachine.transition("active.ready");
2510
+ if (approved) {
2511
+ this.showTransientStatus(this.messages.humanConnecting, 3500);
2512
+ }
2513
+ this.callbacks.onHandoffContext?.(request, approved);
2514
+ }
2515
+ renderRecoveryCard(message, retryable, primaryAction) {
1826
2516
  const badge = document.createElement("div");
1827
2517
  badge.className = "recovery-badge";
1828
2518
  badge.setAttribute("aria-hidden", "true");
@@ -1844,21 +2534,27 @@ class VoicePresenceView {
1844
2534
  close.setAttribute("aria-label", this.messages.close);
1845
2535
  close.append(icon(lucide_1.X, "close-symbol"));
1846
2536
  close.addEventListener("click", () => {
1847
- this.recoveryCard.hidden = true;
1848
- this.recoveryCard.replaceChildren();
1849
- this.syncSubtitleOcclusion();
2537
+ this.hideRecoveryCard();
1850
2538
  });
1851
2539
  const heading = document.createElement("div");
1852
2540
  heading.className = "recovery-head";
1853
2541
  heading.append(badge, title, close);
1854
- if (retryable) {
2542
+ let primary = null;
2543
+ if (primaryAction) {
2544
+ primary = document.createElement("button");
2545
+ primary.type = "button";
2546
+ primary.className = "button primary";
2547
+ primary.textContent = primaryAction.label;
2548
+ primary.addEventListener("click", primaryAction.onSelect);
2549
+ actions.append(primary);
2550
+ }
2551
+ else if (retryable) {
1855
2552
  const retry = document.createElement("button");
1856
2553
  retry.type = "button";
1857
2554
  retry.className = "button primary";
1858
2555
  retry.append(icon(lucide_1.RefreshCw, "retry-symbol"), document.createTextNode(this.messages.retry));
1859
2556
  retry.addEventListener("click", () => {
1860
- this.recoveryCard.hidden = true;
1861
- this.syncSubtitleOcclusion();
2557
+ this.hideRecoveryCard();
1862
2558
  this.callbacks.onRetry();
1863
2559
  });
1864
2560
  actions.append(retry);
@@ -1875,9 +2571,7 @@ class VoicePresenceView {
1875
2571
  : this.messages.requestHumanFallback;
1876
2572
  fallbackButton.addEventListener("click", () => {
1877
2573
  fallbackButton.disabled = true;
1878
- this.recoveryCard.hidden = true;
1879
- this.recoveryCard.replaceChildren();
1880
- this.syncSubtitleOcclusion();
2574
+ this.hideRecoveryCard();
1881
2575
  this.onFallback(fallback);
1882
2576
  }, { once: true });
1883
2577
  actions.append(fallbackButton);
@@ -1889,9 +2583,27 @@ class VoicePresenceView {
1889
2583
  this.recoveryCard.append(actions);
1890
2584
  this.recoveryCard.hidden = false;
1891
2585
  this.syncSubtitleOcclusion();
2586
+ this.dialogFocus.present({
2587
+ surface: this.recoveryCard,
2588
+ initialFocus: () => primary ?? close,
2589
+ fallbackFocus: () => (this.orb.hidden ? null : this.orb),
2590
+ onEscape: () => {
2591
+ this.hideRecoveryCard();
2592
+ },
2593
+ });
2594
+ }
2595
+ hideRecoveryCard(restore = true) {
2596
+ this.recoveryCard.hidden = true;
2597
+ this.recoveryCard.replaceChildren();
2598
+ this.dialogFocus.dismiss(this.recoveryCard, { restore });
2599
+ if (!restore)
2600
+ this.dialogFocus.clear({ restore: false });
2601
+ this.syncSubtitleOcclusion();
1892
2602
  }
1893
2603
  syncSubtitleOcclusion() {
1894
- const occluded = !this.safetyCard.hidden || !this.recoveryCard.hidden;
2604
+ const occluded = !this.safetyCard.hidden ||
2605
+ !this.recoveryCard.hidden ||
2606
+ !this.pageAssistanceLayer.hidden;
1895
2607
  this.captionStage.hidden = occluded;
1896
2608
  if (occluded) {
1897
2609
  this.host.style.setProperty("--voice-subtitle-height", "0px");
@@ -2095,6 +2807,7 @@ class VoicePresenceView {
2095
2807
  }
2096
2808
  startLevelLoop() {
2097
2809
  if (this.levelFrame !== null ||
2810
+ !this.motionEnabled() ||
2098
2811
  !this.activeAnalyser() ||
2099
2812
  !this.levelBuffer ||
2100
2813
  document.hidden) {
@@ -2103,7 +2816,10 @@ class VoicePresenceView {
2103
2816
  const frame = (timestamp) => {
2104
2817
  this.levelFrame = null;
2105
2818
  const analyser = this.activeAnalyser();
2106
- if (!this.active || document.hidden || !analyser || !this.levelBuffer) {
2819
+ if (!this.active ||
2820
+ !this.motionEnabled() ||
2821
+ !analyser ||
2822
+ !this.levelBuffer) {
2107
2823
  this.host.style.setProperty("--voice-level", "0");
2108
2824
  return;
2109
2825
  }
@@ -2152,6 +2868,28 @@ class VoicePresenceView {
2152
2868
  void this.audioContext.close().catch(() => undefined);
2153
2869
  this.audioContext = null;
2154
2870
  }
2871
+ motionEnabled() {
2872
+ return (this.capabilityTier === "standard" &&
2873
+ this.voiceConfig.visual.lowPowerMode !== "always" &&
2874
+ !this.isReducedMotionRequested() &&
2875
+ !document.hidden);
2876
+ }
2877
+ isReducedMotionRequested() {
2878
+ return (this.reducedMotionQuery?.matches === true ||
2879
+ window.matchMedia?.("(prefers-reduced-motion: reduce)").matches === true);
2880
+ }
2881
+ syncMotionPolicy() {
2882
+ const staticMotion = this.capabilityTier !== "standard" ||
2883
+ this.voiceConfig.visual.lowPowerMode === "always" ||
2884
+ this.isReducedMotionRequested();
2885
+ this.host.dataset.motion = staticMotion ? "off" : "on";
2886
+ this.host.dataset.effectsPaused = String(staticMotion || document.hidden);
2887
+ if (staticMotion || document.hidden) {
2888
+ this.stopLevelLoop();
2889
+ return;
2890
+ }
2891
+ this.syncLevelLoop();
2892
+ }
2155
2893
  activeAnalyser() {
2156
2894
  if (this.stateMachine.state === "active.listening")
2157
2895
  return this.inputAnalyser;