@burdenoff/fe-libs 2026.1024.3 → 2026.1026.1

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 (33) hide show
  1. package/dist/shared/assistant/avatar/live/LiveAvatarMinutes.d.ts +24 -0
  2. package/dist/shared/assistant/avatar/live/LiveAvatarMinutes.js +161 -0
  3. package/dist/shared/assistant/avatar/live/LiveAvatarStage.js +95 -74
  4. package/dist/shared/assistant/avatar/live/LiveAvatarTabView.d.ts +6 -0
  5. package/dist/shared/assistant/avatar/live/LiveAvatarTabView.js +195 -172
  6. package/dist/shared/assistant/avatar/live/avatarPorts.d.ts +9 -0
  7. package/dist/shared/assistant/avatar/live/avatarSessionClient.d.ts +77 -1
  8. package/dist/shared/assistant/avatar/live/avatarSessionClient.js +193 -72
  9. package/dist/shared/assistant/avatar/live/avatarTransport.d.ts +25 -3
  10. package/dist/shared/assistant/avatar/live/avatarTransport.js +29 -2
  11. package/dist/shared/assistant/avatar/live/createLiveAvatarSeam.d.ts +35 -4
  12. package/dist/shared/assistant/avatar/live/createLiveAvatarSeam.js +136 -79
  13. package/dist/shared/assistant/avatar/live/liveAvatarController.d.ts +79 -3
  14. package/dist/shared/assistant/avatar/live/liveAvatarController.js +424 -245
  15. package/dist/shared/assistant/avatar/live/liveAvatarMinutesView.d.ts +127 -0
  16. package/dist/shared/assistant/avatar/live/liveAvatarMinutesView.js +168 -0
  17. package/dist/shared/assistant/avatar/live/liveAvatarStatus.d.ts +3 -0
  18. package/dist/shared/assistant/avatar/live/liveAvatarStatus.js +11 -7
  19. package/dist/shared/assistant/avatar/live/liveKitAvatarRoom.js +6 -1
  20. package/dist/shared/assistant/avatar/useAvatarCopy.d.ts +127 -2
  21. package/dist/shared/assistant/avatar/useAvatarCopy.js +116 -7
  22. package/dist/shared/assistant/i18nManifest.d.ts +32 -1
  23. package/dist/shared/assistant/i18nManifest.js +55 -24
  24. package/dist/shared/assistant/index.d.ts +2 -0
  25. package/dist/shared/assistant/ui/AssistantChatArea.js +17 -17
  26. package/dist/shared/assistant/ui/AssistantMarkdownRenderer.d.ts +5 -2
  27. package/dist/shared/assistant/ui/assistantLiveAvatarSeam.d.ts +11 -3
  28. package/dist/shared/assistant/ui/avatar/AssistantAvatarTab.js +78 -76
  29. package/dist/shared/assistant/ui/avatar/avatarAskGate.d.ts +2 -0
  30. package/dist/shared/assistant/ui/avatar/avatarAskGate.js +1 -0
  31. package/dist/shared/events/index.d.ts +2 -1
  32. package/dist/shared-assistant.js +227 -225
  33. package/package.json +1 -1
@@ -65,6 +65,7 @@ var p = class {
65
65
  videoPresent = !1;
66
66
  degraded = !1;
67
67
  audioElement = null;
68
+ elementPaused = !1;
68
69
  disposed = !1;
69
70
  rejectReady = null;
70
71
  mediaReady = !1;
@@ -85,7 +86,10 @@ var p = class {
85
86
  return this.readinessStatus;
86
87
  }
87
88
  get audioBlocked() {
88
- return this.state === "ready" && !this.options.room.canPlaybackAudio();
89
+ return this.state === "ready" && (!this.options.room.canPlaybackAudio() || this.elementPaused);
90
+ }
91
+ get videoBlocked() {
92
+ return this.state === "ready" && this.options.room.canPlaybackVideo?.() === !1;
89
93
  }
90
94
  subscribe(e) {
91
95
  return this.listeners.add(e), () => {
@@ -100,6 +104,26 @@ var p = class {
100
104
  async resumeAudio() {
101
105
  await this.options.room.startAudio();
102
106
  }
107
+ async resumeVideo() {
108
+ await this.options.room.startVideo?.();
109
+ }
110
+ watchAudioElement(e) {
111
+ if (e === null || typeof e.addEventListener != "function") return;
112
+ let t = () => {
113
+ this.disposed || this.state !== "ready" || e.srcObject === null || this.elementPaused || (this.elementPaused = !0, this.emit({
114
+ type: "audioPlayback",
115
+ canPlay: !1
116
+ }));
117
+ }, n = () => {
118
+ this.elementPaused && (this.elementPaused = !1, this.emit({
119
+ type: "audioPlayback",
120
+ canPlay: this.options.room.canPlaybackAudio()
121
+ }));
122
+ };
123
+ e.addEventListener("pause", t), e.addEventListener("playing", n), this.unsubscribes.push(() => {
124
+ e.removeEventListener("pause", t), e.removeEventListener("playing", n);
125
+ });
126
+ }
103
127
  setOutputMuted(e) {
104
128
  this.audioElement && (this.audioElement.muted = e);
105
129
  }
@@ -107,7 +131,7 @@ var p = class {
107
131
  if (this.state !== "idle") throw Error("transport already used");
108
132
  this.state = "connecting";
109
133
  let { room: t, avatarIdentity: i } = this.options;
110
- this.audioElement = e.audioElement ?? null, t.registerRpc(n, (e, t) => this.handleFinished(e, t)), t.registerRpc(r, (e) => this.handleStarted(e)), this.unsubscribes.push(t.onParticipantConnected((e) => {
134
+ this.audioElement = e.audioElement ?? null, this.watchAudioElement(this.audioElement), t.registerRpc(n, (e, t) => this.handleFinished(e, t)), t.registerRpc(r, (e) => this.handleStarted(e)), this.unsubscribes.push(t.onParticipantConnected((e) => {
111
135
  e === i && (this.avatarPresent = !0, this.checkReady());
112
136
  }), t.onParticipantDisconnected((e) => {
113
137
  e === i && (this.avatarPresent = !1, this.videoPresent = !1, this.avatarDepartures += 1, this.owedFinishes = 0, this.endAllSegments(), this.emit({ type: "avatarLeft" }));
@@ -122,6 +146,9 @@ var p = class {
122
146
  }), t.onAudioPlaybackChanged((e) => this.emit({
123
147
  type: "audioPlayback",
124
148
  canPlay: e
149
+ }))), t.onVideoPlaybackChanged && this.unsubscribes.push(t.onVideoPlaybackChanged((e) => this.emit({
150
+ type: "videoPlayback",
151
+ canPlay: e
125
152
  })));
126
153
  try {
127
154
  await t.connect(e.url, e.token);
@@ -7,6 +7,7 @@ import { AvatarRoom, AvatarScheduler } from './avatarPorts';
7
7
  import { AvatarSessionClient } from './avatarSessionClient';
8
8
  import { LiveAvatarVoiceStore } from './LiveAvatarTabView';
9
9
  import { LiveAvatarController, LiveAvatarControllerDeps, LiveAvatarLogEvent, LiveAvatarPreferences } from './liveAvatarController';
10
+ import { LiveAvatarMinutesPurchase } from './liveAvatarMinutesView';
10
11
  export interface LiveAvatarSeamOptions {
11
12
  /** Display names for the avatar ids the server offers (it sends ids only); without one the chooser numbers them. */
12
13
  readonly avatarNames?: Readonly<Record<string, string>>;
@@ -33,6 +34,18 @@ export interface LiveAvatarSeamOptions {
33
34
  * person's stored choice always wins.
34
35
  */
35
36
  readonly defaultAvatarVoiceMode?: AvatarVoiceDefault;
37
+ /**
38
+ * ★ BOFF-9446: whether this person may buy a plan (`access`, the host's purchase check, which follows `billing:create` on the
39
+ * organization's billing account): fe-libs cannot import billing. The Avatar tab asks it only where "See plans" could be offered (none
40
+ * left, or running low, while the server says a plan with more credits exists), and opens the app's plans page with the panel's own
41
+ * navigator (`onNavigate`), never by itself. Absent: nobody is offered "See plans" in the tab, and a person who has run out is told to
42
+ * ask their billing administrator about a plan with more avatar credits. Only `canPurchase` is read. Copied once, here: a host that
43
+ * changes its object later changes nothing.
44
+ *
45
+ * The host knows its own routes, so it says when the person comes back: from any billing page, `controller.refreshMinutes()`; from a
46
+ * plan checkout, `controller.checkMinutesPurchase()` (the tab then says "Checking your plan…" while it checks).
47
+ */
48
+ readonly minutesPurchase?: LiveAvatarMinutesPurchase;
36
49
  readonly onLog?: (event: LiveAvatarLogEvent) => void;
37
50
  /** Overrides, for tests and for hosts with their own plumbing. Each defaults to the assistant's own. */
38
51
  readonly client?: () => AvatarSessionClient | null;
@@ -45,7 +58,7 @@ export interface LiveAvatarSeamOptions {
45
58
  readonly avatarVoiceStore?: LiveAvatarVoiceStore;
46
59
  readonly scheduler?: AvatarScheduler;
47
60
  readonly browserSupported?: () => boolean;
48
- readonly controller?: Partial<Pick<LiveAvatarControllerDeps, 'createAudioElement' | 'releaseAudioElement' | 'startMeter' | 'decode' | 'now' | 'newIdempotencyKey' | 'subscribeWorkspaceChange' | 'subscribePageHide' | 'subscribePageShow' | 'idleMs' | 'endBeforeDeadlineMs' | 'connectTimeoutMs' | 'trackerConfig' | 'maxDrainMs' | 'stallMs' | 'readinessProbe'>>;
61
+ readonly controller?: Partial<Pick<LiveAvatarControllerDeps, 'createAudioElement' | 'releaseAudioElement' | 'startMeter' | 'decode' | 'now' | 'newIdempotencyKey' | 'subscribeWorkspaceChange' | 'subscribePageHide' | 'subscribePageHidden' | 'endOnHide' | 'subscribeUserGesture' | 'isPageVisible' | 'subscribePageVisible' | 'subscribePageShow' | 'idleMs' | 'endBeforeDeadlineMs' | 'connectTimeoutMs' | 'trackerConfig' | 'maxDrainMs' | 'stallMs' | 'readinessProbe'>>;
49
62
  }
50
63
  /** The seam, plus what a host or a test may need of it. */
51
64
  export interface LiveAvatarSeamHandle extends AssistantLiveAvatarSeam {
@@ -58,9 +71,12 @@ export interface LiveAvatarSeamHandle extends AssistantLiveAvatarSeam {
58
71
  dispose(): void;
59
72
  }
60
73
  /**
61
- * Whether this browser may show the avatar. Desktop Chrome first (GATE1B §10: no other browser or phone is offered until it is tested,
62
- * Chromium builds included, other Chromium-based browsers not), with the platform pieces the avatar needs: WebRTC, Web Audio (the level
63
- * meter and the MP3 decode) and MediaStream.
74
+ * Whether this browser can show the avatar: a check of what the avatar needs, on every browser and phone (BOFF-9696, the operator's
75
+ * decision of 2026-10-25; until then desktop Chrome only, GATE1B §10). The pieces: WebRTC (the room), an AudioContext (the level meter),
76
+ * MediaStream, and an OfflineAudioContext at the reply audio's rate (the MP3 decode makes one: avatarAudioDecode.ts). All four are
77
+ * unprefixed: Safari shipped the unprefixed audio contexts together (14.1), and the prefixed offline context's callback-only decode
78
+ * could not serve the avatar anyway. The user agent is never read: a browser that has these runs the avatar, one that lacks any cannot.
79
+ * Where autoplay needs a tap for sound (Safari and every iOS browser), the sound-blocked state asks for it.
64
80
  */
65
81
  export declare function isLiveAvatarBrowserSupported(scope?: typeof globalThis): boolean;
66
82
  /** The `Authorization` the hosted TTS requests carry (the host's `getHeaders`), else the stored access token. Nothing else is read. */
@@ -78,4 +94,19 @@ export declare function liveAvatarWorkspaceAuthorization(options: {
78
94
  readonly fresh: boolean;
79
95
  readonly refused?: string;
80
96
  }): Promise<string | null>;
97
+ /**
98
+ * ★ BOFF-9696 (S5): a touch-first device (a phone or tablet without a hovering pointer), where a hidden page ends the session. A
99
+ * capability query, never the user agent. Exported for tests only.
100
+ */
101
+ export declare function isTouchFirstDevice(scope?: {
102
+ readonly matchMedia?: (query: string) => {
103
+ readonly matches: boolean;
104
+ };
105
+ }): boolean;
106
+ /**
107
+ * ★ BOFF-9696: the person's taps, clicks and keys anywhere on the page, in the capture phase so no handler can stop them first. Each
108
+ * is a user gesture, inside which a browser lets a refused video start (iOS unlocks media on `touchend` and `click`). Only trusted
109
+ * events count: a script's synthetic click is no gesture. Exported for tests only.
110
+ */
111
+ export declare function documentUserGesture(doc?: Pick<Document, 'addEventListener' | 'removeEventListener'> | null): ((listener: () => void) => () => void) | undefined;
81
112
  export declare function createLiveAvatarSeam(options?: LiveAvatarSeamOptions): LiveAvatarSeamHandle;
@@ -4,36 +4,41 @@ import { readSpeechEngineConfig as a } from "../../speech/createSpeechEngine.js"
4
4
  import { useAvatarPreferencesStore as o } from "../avatarPreferences.js";
5
5
  import { realAvatarScheduler as s } from "./avatarPorts.js";
6
6
  import { startAvatarAudioMeter as c } from "./avatarAudioMeter.js";
7
- import { decodeMp3ToPcm16k as l } from "./avatarAudioDecode.js";
8
- import { avatarApiBaseFromTtsEndpoint as u, avatarIdempotencyKey as d, createAvatarSessionClient as f } from "./avatarSessionClient.js";
9
- import { createLiveAvatarController as p } from "./liveAvatarController.js";
10
- import { openLiveKitAvatarRoom as m } from "./liveKitAvatarRoom.js";
11
- import { liveAvatarAskState as h } from "./liveAvatarStatus.js";
12
- import { LiveAvatarStageView as g } from "./LiveAvatarStage.js";
13
- import { sanitizeAvatarImages as _ } from "./avatarFaces.js";
14
- import { avatarSpeakerFor as v, hostedAvatarVoice as y, liveAvatarVoicesFrom as b, restoreVoiceWhenOutputLeaves as x } from "./avatarVoice.js";
15
- import { LiveAvatarTabView as S } from "./LiveAvatarTabView.js";
16
- import { jsx as C } from "react/jsx-runtime";
7
+ import { AVATAR_SAMPLE_RATE as l } from "./avatarPcm16.js";
8
+ import { decodeMp3ToPcm16k as u } from "./avatarAudioDecode.js";
9
+ import { avatarApiBaseFromTtsEndpoint as d, avatarIdempotencyKey as f, createAvatarSessionClient as p } from "./avatarSessionClient.js";
10
+ import { createLiveAvatarController as m } from "./liveAvatarController.js";
11
+ import { openLiveKitAvatarRoom as h } from "./liveKitAvatarRoom.js";
12
+ import { liveAvatarAskState as g } from "./liveAvatarStatus.js";
13
+ import { LiveAvatarStageView as _ } from "./LiveAvatarStage.js";
14
+ import { sanitizeAvatarImages as v } from "./avatarFaces.js";
15
+ import { avatarSpeakerFor as y, hostedAvatarVoice as b, liveAvatarVoicesFrom as x, restoreVoiceWhenOutputLeaves as S } from "./avatarVoice.js";
16
+ import { LiveAvatarTabView as C } from "./LiveAvatarTabView.js";
17
+ import { jsx as w } from "react/jsx-runtime";
17
18
  //#region src/shared/assistant/avatar/live/createLiveAvatarSeam.tsx
18
- function w(e = globalThis) {
19
- let t = e;
20
- if (typeof t.RTCPeerConnection != "function" || typeof t.OfflineAudioContext != "function" || typeof t.AudioContext != "function" || typeof t.MediaStream != "function") return !1;
21
- let n = t.navigator, r = n?.userAgentData;
22
- if (r?.brands !== void 0) {
23
- let e = r.brands.map((e) => e.brand);
24
- return r.mobile === !1 && e.some((e) => e === "Google Chrome" || e === "Chromium") && !e.some((e) => /Edge|Opera|Brave|Vivaldi|Samsung/i.test(e));
19
+ var T = /* @__PURE__ */ new WeakMap();
20
+ function E(e = globalThis) {
21
+ let t = T.get(e);
22
+ if (t !== void 0) return t;
23
+ let n = D(e);
24
+ return T.set(e, n), n;
25
+ }
26
+ function D(e) {
27
+ if (typeof e.RTCPeerConnection != "function" || typeof e.AudioContext != "function" || typeof e.MediaStream != "function" || typeof e.OfflineAudioContext != "function") return !1;
28
+ try {
29
+ return new e.OfflineAudioContext(1, 1, l), !0;
30
+ } catch {
31
+ return !1;
25
32
  }
26
- let i = n?.userAgent ?? "";
27
- return /\b(?:Chrome|Chromium)\//.test(i) && !/\b(?:Edg|OPR|SamsungBrowser)\/|Mobile|Android|iPhone|iPad/.test(i);
28
33
  }
29
- function T() {
34
+ function O() {
30
35
  let e = a().getHeaders?.() ?? {};
31
36
  for (let [t, n] of Object.entries(e)) if (t.toLowerCase() === "authorization" && typeof n == "string" && n.trim() !== "") return n.trim();
32
37
  let n = t();
33
38
  return n ? `Bearer ${n}` : null;
34
39
  }
35
- async function E(t) {
36
- let i = n(), a = T(), o = a?.replace(/^Bearer\s+/i, "") ?? null;
40
+ async function k(t) {
41
+ let i = n(), a = O(), o = a?.replace(/^Bearer\s+/i, "") ?? null;
37
42
  if (!i || !o) return null;
38
43
  let s = t.refused?.replace(/^Bearer\s+/i, ""), c = await e({
39
44
  url: r("workspace"),
@@ -42,38 +47,38 @@ async function E(t) {
42
47
  currentQuery: "liveAvatarSession",
43
48
  ...t.fresh ? { issuanceGuard: {
44
49
  identityKey: "liveAvatarSession",
45
- isCurrent: () => n() === i && T() === a,
50
+ isCurrent: () => n() === i && O() === a,
46
51
  accepts: (e, t) => e !== s && t === i
47
52
  } } : {}
48
53
  });
49
54
  return c ? `Bearer ${c}` : null;
50
55
  }
51
- function D() {
56
+ function A() {
52
57
  let e = null;
53
58
  return () => {
54
- let t = typeof location > "u" ? void 0 : location.href, n = u(a().endpoint, t);
59
+ let t = typeof location > "u" ? void 0 : location.href, n = d(a().endpoint, t);
55
60
  return n === null ? null : (e?.base !== n && (e = {
56
61
  base: n,
57
- client: f({
62
+ client: p({
58
63
  baseUrl: n,
59
- getAuthorization: T,
60
- getWorkspaceAuthorization: E
64
+ getAuthorization: O,
65
+ getWorkspaceAuthorization: k
61
66
  })
62
67
  }), e.client);
63
68
  };
64
69
  }
65
- var O = {
70
+ var j = {
66
71
  getState: () => o.getState(),
67
72
  setAvatarEnabled: (e) => o.getState().setAvatarEnabled(e),
68
73
  setAvatarId: (e) => o.getState().setAvatarId(e),
69
74
  subscribe: (e) => o.subscribe(() => e())
70
- }, k = {
75
+ }, M = {
71
76
  getState: () => o.getState(),
72
77
  subscribe: (e) => o.subscribe(() => e()),
73
78
  setVoiceMode: (e) => o.getState().setVoiceMode(e),
74
79
  setAvatarSpeaker: (e) => o.getState().setAvatarSpeaker(e)
75
80
  };
76
- function A(e, t) {
81
+ function N(e, t) {
77
82
  let n = (e) => {
78
83
  i(e) || t();
79
84
  };
@@ -90,15 +95,61 @@ function A(e, t) {
90
95
  cancel: () => e.cancel()
91
96
  };
92
97
  }
93
- function j() {
98
+ function P(e) {
99
+ if (e === void 0) return;
100
+ let { access: t } = e;
101
+ return { access: () => t.call(e) };
102
+ }
103
+ function F() {
94
104
  if (typeof document > "u") return null;
95
105
  let e = document.createElement("audio");
96
106
  return e.autoplay = !0, e.hidden = !0, e.setAttribute("data-testid", "assistant-live-avatar-audio"), document.body.appendChild(e), e;
97
107
  }
98
- function M(e) {
108
+ function I(e) {
99
109
  if (!(typeof window > "u")) return (t) => (window.addEventListener(e, t), () => window.removeEventListener(e, t));
100
110
  }
101
- function N() {
111
+ function L() {
112
+ if (typeof document > "u" || typeof document.addEventListener != "function") return;
113
+ let e = () => document.visibilityState !== "hidden";
114
+ return {
115
+ isVisible: e,
116
+ onHidden: (t) => {
117
+ let n = () => {
118
+ e() || t();
119
+ };
120
+ return document.addEventListener("visibilitychange", n), () => {
121
+ document.removeEventListener("visibilitychange", n);
122
+ };
123
+ },
124
+ onShown: (t) => {
125
+ let n = () => {
126
+ e() && t();
127
+ }, r = (n) => {
128
+ "persisted" in n && n.persisted === !0 && e() && t();
129
+ }, i = typeof window < "u" && typeof window.addEventListener == "function" ? window : void 0;
130
+ return document.addEventListener("visibilitychange", n), i?.addEventListener("pageshow", r), () => {
131
+ document.removeEventListener("visibilitychange", n), i?.removeEventListener("pageshow", r);
132
+ };
133
+ }
134
+ };
135
+ }
136
+ function R(e = globalThis) {
137
+ return typeof e.matchMedia == "function" && e.matchMedia("(hover: none) and (pointer: coarse)").matches;
138
+ }
139
+ function z(e = typeof document > "u" ? null : document) {
140
+ if (!(e === null || typeof e.addEventListener != "function")) return (t) => {
141
+ let n = (e) => {
142
+ e.isTrusted && t();
143
+ };
144
+ return e.addEventListener("touchend", n, {
145
+ capture: !0,
146
+ passive: !0
147
+ }), e.addEventListener("click", n, !0), e.addEventListener("keydown", n, !0), () => {
148
+ e.removeEventListener("touchend", n, { capture: !0 }), e.removeEventListener("click", n, !0), e.removeEventListener("keydown", n, !0);
149
+ };
150
+ };
151
+ }
152
+ function B() {
102
153
  if (!(typeof window > "u" || typeof document > "u")) return (e) => {
103
154
  let t = window, n = document, r = () => {
104
155
  n.visibilityState === "visible" && e();
@@ -108,24 +159,29 @@ function N() {
108
159
  };
109
160
  };
110
161
  }
111
- function P(e = {}) {
112
- let t = e.preferences ?? O, n = e.controller ?? {}, r = n.subscribeWorkspaceChange ?? M("burdenoff:workspace-changed"), i = n.subscribePageHide ?? M("pagehide"), a = n.subscribePageShow ?? N(), o = n.now ?? (() => Date.now()), u = p({
113
- client: e.client ?? D(),
114
- openRoom: e.openRoom ?? m,
162
+ function V(e = {}) {
163
+ let t = e.preferences ?? j, n = e.controller ?? {}, r = n.subscribeWorkspaceChange ?? I("burdenoff:workspace-changed"), i = n.subscribePageHide ?? I("pagehide"), a = L(), o = n.isPageVisible ?? a?.isVisible, l = n.subscribePageVisible ?? a?.onShown, d = n.subscribePageHidden ?? a?.onHidden, p = n.endOnHide ?? R, T = n.subscribeUserGesture ?? z(), D = n.subscribePageShow ?? B(), O = n.now ?? (() => Date.now()), k = m({
164
+ client: e.client ?? A(),
165
+ openRoom: e.openRoom ?? h,
115
166
  scheduler: e.scheduler ?? s,
116
167
  preferences: t,
117
- decode: n.decode ?? ((e, t) => l(e, { signal: t })),
118
- createAudioElement: n.createAudioElement ?? j,
168
+ decode: n.decode ?? ((e, t) => u(e, { signal: t })),
169
+ createAudioElement: n.createAudioElement ?? F,
119
170
  releaseAudioElement: n.releaseAudioElement ?? ((e) => {
120
171
  e.srcObject = null, e.remove();
121
172
  }),
122
173
  startMeter: n.startMeter ?? ((e, t) => typeof MediaStream > "u" ? null : c(new MediaStream([e]), t)),
123
- browserSupported: e.browserSupported ?? (() => w()),
124
- now: o,
125
- newIdempotencyKey: n.newIdempotencyKey ?? (() => d()),
174
+ browserSupported: e.browserSupported ?? (() => E()),
175
+ now: O,
176
+ newIdempotencyKey: n.newIdempotencyKey ?? (() => f()),
126
177
  ...r ? { subscribeWorkspaceChange: r } : {},
127
178
  ...i ? { subscribePageHide: i } : {},
128
- ...a ? { subscribePageShow: a } : {},
179
+ ...o ? { isPageVisible: o } : {},
180
+ ...l ? { subscribePageVisible: l } : {},
181
+ ...d ? { subscribePageHidden: d } : {},
182
+ endOnHide: p,
183
+ ...T ? { subscribeUserGesture: T } : {},
184
+ ...D ? { subscribePageShow: D } : {},
129
185
  ...n.idleMs === void 0 ? {} : { idleMs: n.idleMs },
130
186
  ...n.endBeforeDeadlineMs === void 0 ? {} : { endBeforeDeadlineMs: n.endBeforeDeadlineMs },
131
187
  ...n.connectTimeoutMs === void 0 ? {} : { connectTimeoutMs: n.connectTimeoutMs },
@@ -134,34 +190,34 @@ function P(e = {}) {
134
190
  ...n.stallMs === void 0 ? {} : { stallMs: n.stallMs },
135
191
  ...n.readinessProbe === void 0 ? {} : { readinessProbe: n.readinessProbe },
136
192
  ...e.onLog ? { onLog: e.onLog } : {}
137
- }), f = b(e.avatarVoices), T = e.avatarImages === void 0 ? null : _(e.avatarImages), E = e.avatarVoiceStore ?? k, P = () => E.getState(), F = (n, r) => {
138
- let i = v(P(), {
139
- avatarVoices: f,
193
+ }), V = x(e.avatarVoices), H = e.avatarImages === void 0 ? null : v(e.avatarImages), U = e.avatarVoiceStore ?? M, W = () => U.getState(), G = P(e.minutesPurchase), K = (n, r) => {
194
+ let i = y(W(), {
195
+ avatarVoices: V,
140
196
  avatarId: t.getState().avatarId,
141
197
  ...e.defaultAvatarVoiceMode ? { hostDefault: e.defaultAvatarVoiceMode } : {}
142
198
  });
143
- return i === null ? null : y(i, n, r.listVoices());
144
- }, I = (e, t) => {
199
+ return i === null ? null : b(i, n, r.listVoices());
200
+ }, q = (e, t) => {
145
201
  let n = {
146
202
  ...e,
147
203
  voice: t
148
204
  };
149
- return n.audioOutput = x(u.createOutput(), () => {
205
+ return n.audioOutput = S(k.createOutput(), () => {
150
206
  n.voice = e.voice;
151
- }, (e) => u.subscribe(() => {
152
- u.getSnapshot().phase !== "ready" && e();
207
+ }, (e) => k.subscribe(() => {
208
+ k.getSnapshot().phase !== "ready" && e();
153
209
  })), n;
154
210
  };
155
211
  return {
156
212
  decorateSpeechEngine: (e) => {
157
213
  let t = (t) => {
158
- let n = F(t.lang, e), r = n === null ? t : {
214
+ let n = K(t.lang, e), r = n === null ? t : {
159
215
  ...t,
160
216
  voice: n
161
217
  };
162
218
  return {
163
219
  voice: n,
164
- target: u.route(r, e)
220
+ target: k.route(r, e)
165
221
  };
166
222
  }, n = null, r = () => {
167
223
  let e = n;
@@ -169,12 +225,12 @@ function P(e = {}) {
169
225
  }, i = (t, r, i) => {
170
226
  let a = !0, o = !1, s = () => void 0, c = () => {
171
227
  a && (a = !1, n === c && (n = null), s());
172
- }, l = () => o ? !1 : (o = !0, c(), !0), d = () => {
228
+ }, l = () => o ? !1 : (o = !0, c(), !0), u = () => {
173
229
  !a || !l() || (e.cancel(), i?.onError?.("output-left"));
174
230
  };
175
- s = u.subscribe(() => {
176
- u.getSnapshot().phase !== "ready" && d();
177
- }), n = c, r.audioOutput &&= A(r.audioOutput, d), r.fallback = !1, e.speak(t, r, {
231
+ s = k.subscribe(() => {
232
+ k.getSnapshot().phase !== "ready" && u();
233
+ }), n = c, r.audioOutput &&= N(r.audioOutput, u), r.fallback = !1, e.speak(t, r, {
178
234
  ...i,
179
235
  onEnd: () => {
180
236
  l() && i?.onEnd?.();
@@ -203,8 +259,8 @@ function P(e = {}) {
203
259
  }
204
260
  let l = s === null ? {
205
261
  ...a,
206
- audioOutput: u.createOutput()
207
- } : I(a, s);
262
+ audioOutput: k.createOutput()
263
+ } : q(a, s);
208
264
  if (a.avatarOnly === !0) {
209
265
  i(n, l, o);
210
266
  return;
@@ -216,8 +272,8 @@ function P(e = {}) {
216
272
  }
217
273
  };
218
274
  },
219
- Stage: ({ expanded: t, fallback: n }) => /* @__PURE__ */ C(g, {
220
- controller: u,
275
+ Stage: ({ expanded: t, fallback: n }) => /* @__PURE__ */ w(_, {
276
+ controller: k,
221
277
  expanded: t,
222
278
  fallback: n,
223
279
  ...e.avatarNames ? { avatarNames: e.avatarNames } : {}
@@ -225,33 +281,34 @@ function P(e = {}) {
225
281
  enabled: {
226
282
  subscribe: (e) => t.subscribe(e),
227
283
  getSnapshot: () => t.getState().avatarEnabled,
228
- switchOff: () => u.switchOff(),
229
- voiceLive: (e) => u.setVoiceLive(e)
284
+ switchOff: () => k.switchOff(),
285
+ voiceLive: (e) => k.setVoiceLive(e)
230
286
  },
231
287
  tab: {
232
288
  offered: {
233
- subscribe: (e) => u.subscribe(e),
234
- getSnapshot: () => u.getSnapshot().tabOffered
289
+ subscribe: (e) => k.subscribe(e),
290
+ getSnapshot: () => k.getSnapshot().tabOffered
235
291
  },
236
292
  ask: {
237
- subscribe: (e) => u.subscribe(e),
238
- getSnapshot: () => h(u.getSnapshot(), o())
293
+ subscribe: (e) => k.subscribe(e),
294
+ getSnapshot: () => g(k.getSnapshot(), O())
239
295
  },
240
- setOpen: (e) => u.setTabOpen(e),
241
- heldByVoice: () => u.getSnapshot().heldByVoice,
242
- View: (t) => /* @__PURE__ */ C(S, {
296
+ setOpen: (e) => k.setTabOpen(e),
297
+ heldByVoice: () => k.getSnapshot().heldByVoice,
298
+ View: (t) => /* @__PURE__ */ w(C, {
243
299
  ...t,
244
- controller: u,
245
- avatarVoices: f,
246
- voiceStore: E,
300
+ controller: k,
301
+ avatarVoices: V,
302
+ voiceStore: U,
247
303
  ...e.avatarNames ? { avatarNames: e.avatarNames } : {},
248
- ...T ? { avatarImages: T } : {},
249
- ...e.defaultAvatarVoiceMode ? { defaultAvatarVoiceMode: e.defaultAvatarVoiceMode } : {}
304
+ ...H ? { avatarImages: H } : {},
305
+ ...e.defaultAvatarVoiceMode ? { defaultAvatarVoiceMode: e.defaultAvatarVoiceMode } : {},
306
+ ...G ? { minutesPurchase: G } : {}
250
307
  })
251
308
  },
252
- controller: u,
253
- dispose: () => u.dispose()
309
+ controller: k,
310
+ dispose: () => k.dispose()
254
311
  };
255
312
  }
256
313
  //#endregion
257
- export { P as createLiveAvatarSeam, w as isLiveAvatarBrowserSupported, T as liveAvatarAuthorization, E as liveAvatarWorkspaceAuthorization };
314
+ export { V as createLiveAvatarSeam, z as documentUserGesture, E as isLiveAvatarBrowserSupported, R as isTouchFirstDevice, O as liveAvatarAuthorization, k as liveAvatarWorkspaceAuthorization };
@@ -4,7 +4,7 @@ import { AvatarMeterHandle } from './avatarAudioMeter';
4
4
  import { AvatarRoom, AvatarScheduler } from './avatarPorts';
5
5
  import { AvatarTrackerConfig } from './avatarPlaybackTracker';
6
6
  import { AvatarDecodedPiece, AvatarNotice, AvatarReplyOutputs } from './avatarReplyOutputs';
7
- import { AvatarBudgetScope, AvatarClientEndReason, AvatarSessionClient } from './avatarSessionClient';
7
+ import { AvatarBudgetScope, AvatarClientEndReason, AvatarMinutes, AvatarMinutesReason, AvatarSessionClient, AvatarSessionMinutes } from './avatarSessionClient';
8
8
  import { AvatarReadinessProbeOptions } from './avatarTransport';
9
9
  /** End after this long without a reply on the avatar while it is on (OD8, BACKEND-DESIGN §8.7). */
10
10
  export declare const LIVE_AVATAR_IDLE_MS = 120000;
@@ -14,6 +14,20 @@ export declare const LIVE_AVATAR_END_BEFORE_DEADLINE_MS = 10000;
14
14
  export declare const LIVE_AVATAR_REQUEST_TIMEOUT_MS = 8000;
15
15
  /** The longest a connect may take (the avatar's own wait for its video, 8 s, plus the readiness probe, 6 s, plus slack). */
16
16
  export declare const LIVE_AVATAR_CONNECT_TIMEOUT_MS = 20000;
17
+ /**
18
+ * ★ BOFF-9446: quiet minutes questions are at least this far apart, counted from the last availability question of any kind; one asked
19
+ * sooner waits for it. The check after a purchase keeps its own bounded pace (`LIVE_AVATAR_PURCHASE_POLL_MS`).
20
+ */
21
+ export declare const LIVE_AVATAR_MINUTES_REFRESH_MS = 15000;
22
+ /** ★ BOFF-9446: a quiet minutes question answered 429 is asked again after its Retry-After, at most this many times in a row. */
23
+ export declare const LIVE_AVATAR_MINUTES_BUSY_RETRIES = 2;
24
+ /** ★ BOFF-9446: the check after a plan purchase asks at most this many times… */
25
+ export declare const LIVE_AVATAR_PURCHASE_POLLS = 5;
26
+ /**
27
+ * …this far apart (0, 6, 12, 18 and 24 s from its first question, which itself comes at least this long after the last availability
28
+ * question of any kind), and stops as soon as the credits available rise.
29
+ */
30
+ export declare const LIVE_AVATAR_PURCHASE_POLL_MS = 6000;
17
31
  export type LiveAvatarPhase =
18
32
  /** Nothing running. */
19
33
  'off'
@@ -29,7 +43,10 @@ export type LiveAvatarPhase =
29
43
  | 'ready'
30
44
  /** Ending the session. */
31
45
  | 'ending';
32
- export type LiveAvatarNotice = 'interrupted' | 'voice-only' | 'time-limit';
46
+ /** `minutes-ended` (★ BOFF-9446): the time limit of a session whose length was its minutes (D' < D): its minutes ran out. */
47
+ export type LiveAvatarNotice = 'interrupted' | 'voice-only' | 'time-limit' | 'minutes-ended';
48
+ /** ★ BOFF-9446: the bounded check after a plan purchase (`checkMinutesPurchase`): `checking` while it runs. */
49
+ export type LiveAvatarPurchaseCheck = 'idle' | 'checking';
33
50
  /** A Turn off this soon after the tab's start click is the second click of a double click, not a choice (BOFF-9341, critic #2). */
34
51
  export declare const LIVE_AVATAR_TAB_OFF_GUARD_MS = 1000;
35
52
  export interface LiveAvatarSnapshot {
@@ -76,6 +93,30 @@ export interface LiveAvatarSnapshot {
76
93
  readonly audioBlocked: boolean;
77
94
  /** The avatar's video has arrived. */
78
95
  readonly videoReady: boolean;
96
+ /**
97
+ * ★ BOFF-9696: the browser refuses to play the avatar's video (iOS in Low Power Mode, until a user gesture): the views show the stage's
98
+ * placeholder instead of a frozen face. The sound tap (`resumeAudio`) plays it too, and so does the person's next tap or key on the page.
99
+ */
100
+ readonly videoBlocked: boolean;
101
+ /**
102
+ * ★ BOFF-9446: the minutes block of the latest availability answer (the server sends one in `enforce` only), or null: no answer with
103
+ * one yet (today's answers never have one), an answer with none since, or cleared by a workspace switch or a "not offered" answer.
104
+ * A quiet question (`refreshMinutes`) replaces it; it never changes the phase, and changes the problem only to settle a minutes
105
+ * problem that has no time of its own (see `refreshMinutes`).
106
+ */
107
+ readonly minutes: AvatarMinutes | null;
108
+ /** ★ BOFF-9446: with the problem `minutes-exhausted`, the reason the server or the minutes answer gave; null otherwise. Goes with it. */
109
+ readonly minutesReason: AvatarMinutesReason | null;
110
+ /** ★ BOFF-9446: the running session's minutes, from its start answer (enforce only); null otherwise. Set at `ready`. */
111
+ readonly sessionMinutes: AvatarSessionMinutes | null;
112
+ /**
113
+ * ★ BOFF-9446: wall-clock ms at which the server ends the running session (create sent + D' - 5 s). Measured on the local scheduler
114
+ * from the moment the create was sent, and placed on the local clock once, at `ready`: never the server's wall clock. Null unless the
115
+ * start answered D'.
116
+ */
117
+ readonly sessionEndsAt: number | null;
118
+ /** ★ BOFF-9446: the bounded check after a plan purchase (`checkMinutesPurchase`). */
119
+ readonly purchase: LiveAvatarPurchaseCheck;
79
120
  }
80
121
  /** The person's stored choices (P4), read and written through this narrow port so tests need no storage. */
81
122
  export interface LiveAvatarPreferences {
@@ -115,7 +156,7 @@ export interface LiveAvatarControllerDeps {
115
156
  readonly releaseAudioElement: (element: HTMLAudioElement) => void;
116
157
  /** A level meter on the avatar's received audio; null when none can be built (the output then cannot prove silence). */
117
158
  readonly startMeter: (track: MediaStreamTrack, onChange: (audible: boolean) => void) => AvatarMeterHandle | null;
118
- /** The browser can show the avatar at all (desktop Chromium first: GATE1B §10). */
159
+ /** The browser can show the avatar at all: what it needs is there (any browser or phone since BOFF-9696). */
119
160
  readonly browserSupported: () => boolean;
120
161
  /**
121
162
  * Wall-clock milliseconds: a refusal's retry time (`retryAt`), its lock and its re-check (`recheckRetry`); also the fallback for
@@ -125,6 +166,19 @@ export interface LiveAvatarControllerDeps {
125
166
  readonly newIdempotencyKey: () => string;
126
167
  readonly subscribeWorkspaceChange?: (listener: () => void) => () => void;
127
168
  readonly subscribePageHide?: (listener: () => void) => () => void;
169
+ /**
170
+ * ★ BOFF-9696 (S5): the page was hidden (its document hidden). A phone hides a page without `pagehide` when the person switches apps or
171
+ * locks the screen, and freezes its timers then, so a session left on there would run on unseen until its idle rule or deadline.
172
+ */
173
+ readonly subscribePageHidden?: (listener: () => void) => () => void;
174
+ /** ★ BOFF-9696 (S5): whether a hidden page ends the session at once (touch devices); absent or false: only `pagehide` does. */
175
+ readonly endOnHide?: () => boolean;
176
+ /** ★ BOFF-9696: the person's next tap or key anywhere on the page (a user gesture), once: it plays a refused video. */
177
+ readonly subscribeUserGesture?: (listener: () => void) => () => void;
178
+ /** ★ BOFF-9446: the page is shown now; no quiet minutes question is asked while it is hidden. Absent: always shown. */
179
+ readonly isPageVisible?: () => boolean;
180
+ /** ★ BOFF-9446: called when the page is shown again (after it was hidden): the minutes are asked again then, quietly. */
181
+ readonly subscribePageVisible?: (listener: () => void) => () => void;
128
182
  /**
129
183
  * The page is shown again (its document visible again, or shown from the back/forward cache): the listener is `recheckRetry`. The
130
184
  * scheduler's monotonic clock does not count a system sleep, so after one a refusal's timer fires late; the wall clock (`now`) does.
@@ -180,6 +234,28 @@ export interface LiveAvatarController {
180
234
  setTabOpen(open: boolean): void;
181
235
  /** The person declined the disclosure. */
182
236
  cancelChoice(): void;
237
+ /**
238
+ * ★ BOFF-9446: ask availability again, QUIETLY, for its minutes only (the host's own reasons, such as the app coming back from its
239
+ * billing pages; the controller asks by itself after an end, when the tab opens and when the page is shown again). The phase and the
240
+ * chooser never change, no problem is raised, and a failure, a 429 included, keeps the last answer. The newer minutes settle a minutes
241
+ * problem that has no time of its own (none left, in use, not set up: Turn on cannot start), as `confirm` would find them: it says
242
+ * what they rule out, or goes when they leave room for a start (AVAILABLE, UNAVAILABLE, no block). A refusal with a time (`retryAt`)
243
+ * waits for it, and the zero-session limit stays (no minutes answer reports it). It asks only once the server has sent minutes (with
244
+ * today's answers it asks nothing), only while the avatar is offered and off or choosing, never while the page is hidden, never before
245
+ * the Retry-After of the last 429 to an availability question, and at most once per `LIVE_AVATAR_MINUTES_REFRESH_MS` (one asked sooner
246
+ * waits for it).
247
+ */
248
+ refreshMinutes(): void;
249
+ /**
250
+ * ★ BOFF-9446: the person came back from a plan checkout. `purchase` is `checking` while up to `LIVE_AVATAR_PURCHASE_POLLS`
251
+ * quiet questions are asked, `LIVE_AVATAR_PURCHASE_POLL_MS` apart, the first no sooner than that after the last availability question
252
+ * of any kind; it stops as soon as the credits available rise above what they were when it began. Its answers settle the credits
253
+ * problem as a quiet question's do (`refreshMinutes`). Called while a check runs, the check starts again with all its questions, at
254
+ * the same pace. A tick is skipped, and counted, while the page is hidden, while another availability question is out, or before the
255
+ * Retry-After of the last 429 to one. Only once the server has sent minutes (`minutes` not null): a call before then is dropped, not
256
+ * remembered, so the host calls it once the snapshot has minutes.
257
+ */
258
+ checkMinutesPurchase(): void;
183
259
  selectAvatar(avatarId: string): void;
184
260
  /**
185
261
  * Avatar OFF: the toggle, the compact view's stop, the tab's Turn off, the end of the voice conversation (P2). Idempotent. In the tab,