@ateam-ai-mobile/ui 0.1.5 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -34,9 +34,13 @@ __export(index_exports, {
34
34
  ChatInput: () => ChatInput,
35
35
  DaySeparator: () => DaySeparator,
36
36
  EmbeddedPluginWebView: () => EmbeddedPluginWebView,
37
+ HandoffScreen: () => HandoffScreen,
37
38
  JobProgressBar: () => JobProgressBar,
39
+ JoinScreen: () => JoinScreen,
38
40
  Lightbox: () => Lightbox,
41
+ LoginScreen: () => LoginScreen,
39
42
  MessageBubble: () => MessageBubble,
43
+ OpenJoinScreen: () => OpenJoinScreen,
40
44
  Plugin: () => Plugin,
41
45
  PluginSurface: () => PluginSurface,
42
46
  TableCardBlock: () => TableCardBlock,
@@ -44,17 +48,31 @@ __export(index_exports, {
44
48
  TypingIndicator: () => TypingIndicator,
45
49
  UIActionsRow: () => UIActionsRow,
46
50
  VoiceCallSheet: () => VoiceCallSheet,
51
+ VoiceMicIndicator: () => VoiceMicIndicator,
52
+ VoiceSessionHost: () => VoiceSessionHost,
47
53
  WidgetHost: () => WidgetHost,
48
54
  WidgetWebView: () => WidgetWebView,
55
+ WorkspaceBanner: () => WorkspaceBanner,
49
56
  buildPluginBridgeJS: () => buildPluginBridgeJS,
57
+ completeInviteJoin: () => completeInviteJoin,
58
+ completeOpenJoin: () => completeOpenJoin,
59
+ completeProviderSignIn: () => completeProviderSignIn,
60
+ googleClientIds: () => googleClientIds,
61
+ handoffErrorMessage: () => handoffErrorMessage,
62
+ inviteHeadline: () => inviteHeadline,
63
+ openJoinErrorMessage: () => openJoinErrorMessage,
50
64
  openWidget: () => openWidget,
65
+ runHandoffExchange: () => runHandoffExchange,
51
66
  setClipboardProvider: () => setClipboardProvider,
67
+ setOAuthProvider: () => setOAuthProvider,
52
68
  setPluginWebViewExtraHosts: () => setPluginWebViewExtraHosts,
53
69
  setUIActionNavigator: () => setUIActionNavigator,
70
+ shouldRecoverToDefaultTenant: () => shouldRecoverToDefaultTenant,
54
71
  useLightboxStore: () => useLightboxStore,
55
72
  usePluginBridge: () => usePluginBridge,
56
73
  useVoiceCall: () => useVoiceCall,
57
- useWidgetHostStore: () => useWidgetHostStore
74
+ useWidgetHostStore: () => useWidgetHostStore,
75
+ voiceSession: () => voiceSession
58
76
  });
59
77
  module.exports = __toCommonJS(index_exports);
60
78
 
@@ -261,6 +279,13 @@ var import_core7 = require("@ateam-ai-mobile/core");
261
279
  var import_plugin_runtime5 = require("@ateam-ai-mobile/plugin-runtime");
262
280
 
263
281
  // src/providers.ts
282
+ var _oauth = null;
283
+ function setOAuthProvider(oauth) {
284
+ _oauth = oauth;
285
+ }
286
+ function getOAuthProvider() {
287
+ return _oauth;
288
+ }
264
289
  var _clipboard = null;
265
290
  function setClipboardProvider(clipboard) {
266
291
  _clipboard = clipboard;
@@ -785,8 +810,10 @@ function Plugin({ id, manifest, commandBufferRef, props: engineProps }) {
785
810
  const { plugin, loading, error, bundleMissing } = (0, import_plugin_runtime2.usePlugin)(id, manifest);
786
811
  const connectorId = id.startsWith("mcp:") ? id.split(":")[1] : manifest._connectorId;
787
812
  const bridge = (0, import_plugin_runtime2.useNativeBridge)(id, connectorId, commandBufferRef);
788
- const declaredCaps = manifest?.native;
789
- const native = (0, import_react4.useMemo)(() => (0, import_plugin_runtime2.gatePluginNative)((0, import_plugin_runtime2.getPluginNative)(), declaredCaps), [declaredCaps]);
813
+ const { device, native, deviceState, voice } = (0, import_react4.useMemo)(
814
+ () => (0, import_plugin_runtime2.resolvePluginRuntimeProps)({ pluginId: id, manifest, plugin }),
815
+ [id, manifest, plugin]
816
+ );
790
817
  const theme = (0, import_plugin_runtime2.usePluginTheme)();
791
818
  const user = (0, import_core4.getUser)();
792
819
  const context = (0, import_react4.useMemo)(() => ({
@@ -809,7 +836,7 @@ function Plugin({ id, manifest, commandBufferRef, props: engineProps }) {
809
836
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native3.View, { style: styles3.center, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native3.Text, { style: styles3.errorMessage, children: error || "Plugin not found" }) });
810
837
  }
811
838
  const { Component } = plugin;
812
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(PluginErrorBoundary, { pluginId: id, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Component, { bridge, native, theme, context, props: engineProps }) });
839
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(PluginErrorBoundary, { pluginId: id, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Component, { bridge, device, native, deviceState, voice, theme, context, props: engineProps }) });
813
840
  }
814
841
  var styles3 = import_react_native3.StyleSheet.create({
815
842
  center: {
@@ -1306,7 +1333,10 @@ function MessageBubble({ message }) {
1306
1333
  message.toolCalls && message.toolCalls.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(ToolCallsBadge, { calls: message.toolCalls }),
1307
1334
  message.jobProgress && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(JobProgressBar, { progress: message.jobProgress, standalone: isProgressOnly }),
1308
1335
  !isUser && message.actions && message.actions.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(UIActionsRow, { actions: message.actions }),
1309
- pq && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(QuestionChips, { inputType: pq.inputType, options: pq.options }),
1336
+ pq && // Per-chain: this bubble's OWN chain has a pending question. Parallel
1337
+ // pending chains each render their own chips (jobProgress is per
1338
+ // bubble); answered questions have no jobProgress → no chips.
1339
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(QuestionChips, { inputType: pq.inputType, options: pq.options }),
1310
1340
  !isProgressOnly && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react_native8.Text, { style: [styles7.timeText, isUser && styles7.timeTextUser], children: formatTime(message.timestamp) })
1311
1341
  ]
1312
1342
  }
@@ -2144,17 +2174,522 @@ var styles13 = import_react_native14.StyleSheet.create({
2144
2174
  }
2145
2175
  });
2146
2176
 
2147
- // src/voice/VoiceCallSheet.tsx
2148
- var import_react13 = require("react");
2177
+ // src/WorkspaceBanner.tsx
2149
2178
  var import_react_native15 = require("react-native");
2150
- var import_react_native_svg2 = __toESM(require("react-native-svg"));
2151
- var import_react_native_webview3 = require("react-native-webview");
2179
+ var import_core17 = require("@ateam-ai-mobile/core");
2180
+ var import_jsx_runtime16 = require("react/jsx-runtime");
2181
+ var WARM_AMBER2 = "#D49A4E";
2182
+ var WARM_AMBER_SOFT2 = "rgba(212, 154, 78, 0.16)";
2183
+ var WARM_AMBER_BORDER = "rgba(212, 154, 78, 0.4)";
2184
+ function WorkspaceBanner({ topInset = 0 }) {
2185
+ const snapshot = (0, import_core17.useWorkspaceStore)((s) => s.snapshot);
2186
+ if (!snapshot) return null;
2187
+ if (snapshot.state === "active" || snapshot.state === "unknown") return null;
2188
+ if (!snapshot.message) return null;
2189
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
2190
+ import_react_native15.View,
2191
+ {
2192
+ style: [styles14.container, { paddingTop: import_core17.spacing.sm + topInset }],
2193
+ accessibilityRole: "alert",
2194
+ accessibilityLabel: snapshot.message,
2195
+ children: [
2196
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_react_native15.View, { style: styles14.dot }),
2197
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_react_native15.Text, { style: styles14.message, children: snapshot.message })
2198
+ ]
2199
+ }
2200
+ );
2201
+ }
2202
+ var styles14 = import_react_native15.StyleSheet.create({
2203
+ container: {
2204
+ flexDirection: "row",
2205
+ alignItems: "flex-start",
2206
+ gap: import_core17.spacing.sm,
2207
+ paddingHorizontal: import_core17.spacing.md,
2208
+ paddingBottom: import_core17.spacing.sm,
2209
+ backgroundColor: WARM_AMBER_SOFT2,
2210
+ borderBottomWidth: import_react_native15.StyleSheet.hairlineWidth,
2211
+ borderBottomColor: WARM_AMBER_BORDER
2212
+ },
2213
+ dot: {
2214
+ width: 8,
2215
+ height: 8,
2216
+ borderRadius: 4,
2217
+ marginTop: 5,
2218
+ backgroundColor: WARM_AMBER2
2219
+ },
2220
+ message: {
2221
+ flex: 1,
2222
+ color: import_core17.ada.ink,
2223
+ fontSize: import_core17.fontSize.sm,
2224
+ lineHeight: import_core17.fontSize.sm * 1.35
2225
+ }
2226
+ });
2227
+
2228
+ // src/auth/HandoffScreen.tsx
2229
+ var import_react12 = require("react");
2230
+ var import_react_native16 = require("react-native");
2152
2231
  var import_core18 = require("@ateam-ai-mobile/core");
2153
2232
  var import_plugin_runtime9 = require("@ateam-ai-mobile/plugin-runtime");
2154
2233
 
2234
+ // src/auth/handoffExchange.ts
2235
+ async function runHandoffExchange(code, deps) {
2236
+ const res = await deps.exchange(code);
2237
+ if (res.tenant) await deps.onAdoptTenant(res.tenant);
2238
+ await deps.onSignedIn(res.jwt);
2239
+ deps.onDone();
2240
+ }
2241
+ function handoffErrorMessage(msg) {
2242
+ const m = (msg || "").toLowerCase();
2243
+ if (m.includes("429") || m.includes("rate") || m.includes("too many")) {
2244
+ return "Too many attempts \u2014 wait a moment, then get a new sign-in link on the web.";
2245
+ }
2246
+ return "This sign-in link is no longer valid. Get a new one on the web and try again.";
2247
+ }
2248
+
2249
+ // src/auth/HandoffScreen.tsx
2250
+ var import_jsx_runtime17 = require("react/jsx-runtime");
2251
+ var fontFam7 = import_react_native16.Platform.OS === "ios" ? "SF Pro Rounded" : void 0;
2252
+ function HandoffScreen({ code, onAdoptTenant, onSignedIn, onDone, onCancel }) {
2253
+ const [err, setErr] = (0, import_react12.useState)(null);
2254
+ const startedForRef = (0, import_react12.useRef)(null);
2255
+ (0, import_react12.useEffect)(() => {
2256
+ if (!code || startedForRef.current === code) return;
2257
+ startedForRef.current = code;
2258
+ runHandoffExchange(code, { exchange: import_core18.exchangeHandoff, onAdoptTenant, onSignedIn, onDone }).catch((e) => setErr(handoffErrorMessage(e?.message)));
2259
+ }, [code, onAdoptTenant, onSignedIn, onDone]);
2260
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_react_native16.View, { style: styles15.container, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_react_native16.View, { style: styles15.center, children: [
2261
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_plugin_runtime9.AdaOrb, { size: 72, state: err ? "idle" : "thinking" }),
2262
+ !err ? /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_jsx_runtime17.Fragment, { children: [
2263
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_react_native16.Text, { style: styles15.title, children: "Signing you in\u2026" }),
2264
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_react_native16.ActivityIndicator, { color: import_core18.ada.ink, style: { marginTop: 6 } })
2265
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_jsx_runtime17.Fragment, { children: [
2266
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_react_native16.Text, { style: styles15.title, children: "Couldn\u2019t finish sign-in" }),
2267
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_react_native16.Text, { style: styles15.sub, children: err }),
2268
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_react_native16.Pressable, { onPress: onCancel, hitSlop: 8, style: styles15.back, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_react_native16.Text, { style: styles15.backText, children: "Back to sign in" }) })
2269
+ ] })
2270
+ ] }) });
2271
+ }
2272
+ var styles15 = import_react_native16.StyleSheet.create({
2273
+ container: { flex: 1, backgroundColor: import_core18.ada.paper },
2274
+ center: { flex: 1, alignItems: "center", justifyContent: "center", gap: 14, paddingHorizontal: 32 },
2275
+ title: { fontSize: 20, fontWeight: "600", color: import_core18.ada.ink, fontFamily: fontFam7, marginTop: 8 },
2276
+ sub: { fontSize: 14, lineHeight: 20, color: import_core18.ada.inkSoft, textAlign: "center", fontFamily: fontFam7 },
2277
+ back: { marginTop: 10, paddingVertical: 10 },
2278
+ backText: { fontSize: 15, fontWeight: "600", color: import_core18.ada.orangeDeep, fontFamily: fontFam7 }
2279
+ });
2280
+
2281
+ // src/auth/OpenJoinScreen.tsx
2282
+ var import_react13 = require("react");
2283
+ var import_react_native17 = require("react-native");
2284
+ var import_core19 = require("@ateam-ai-mobile/core");
2285
+ var import_plugin_runtime10 = require("@ateam-ai-mobile/plugin-runtime");
2286
+
2287
+ // src/auth/openJoin.ts
2288
+ async function completeOpenJoin(slug, idToken, deps) {
2289
+ if (!idToken) return false;
2290
+ const consumer = await deps.join(slug, idToken);
2291
+ await deps.onAdoptTenant(slug);
2292
+ await deps.onSignedIn(consumer.jwt);
2293
+ return true;
2294
+ }
2295
+ function openJoinErrorMessage(msg) {
2296
+ const m = (msg || "").toLowerCase();
2297
+ if (m.includes("invite-only") || m.includes("invite only")) {
2298
+ return "This team is invite-only \u2014 ask an admin for an invite link.";
2299
+ }
2300
+ if (m.includes("not found") || m.includes("unknown tenant")) {
2301
+ return "No team with that handle.";
2302
+ }
2303
+ return msg || "Could not join";
2304
+ }
2305
+
2306
+ // src/auth/OpenJoinScreen.tsx
2307
+ var import_jsx_runtime18 = require("react/jsx-runtime");
2308
+ var fontFam8 = import_react_native17.Platform.OS === "ios" ? "SF Pro Rounded" : void 0;
2309
+ function OpenJoinScreen({ onAdoptTenant, onSignedIn, onBack }) {
2310
+ const [team, setTeam] = (0, import_react13.useState)("");
2311
+ const [busy, setBusy] = (0, import_react13.useState)(false);
2312
+ const [err, setErr] = (0, import_react13.useState)(null);
2313
+ const [appleAvailable, setAppleAvailable] = (0, import_react13.useState)(false);
2314
+ (0, import_react13.useEffect)(() => {
2315
+ getOAuthProvider()?.isAppleAvailable().then(setAppleAvailable).catch(() => setAppleAvailable(false));
2316
+ }, []);
2317
+ const slug = team.trim().toLowerCase();
2318
+ const canSubmit = slug.length > 0 && !busy;
2319
+ async function run(acquire, join) {
2320
+ if (!slug) return;
2321
+ const oauth = getOAuthProvider();
2322
+ if (!oauth) {
2323
+ setErr("Sign-in is unavailable on this device.");
2324
+ return;
2325
+ }
2326
+ setErr(null);
2327
+ setBusy(true);
2328
+ try {
2329
+ const idToken = await acquire(oauth);
2330
+ await completeOpenJoin(slug, idToken, { join, onAdoptTenant, onSignedIn });
2331
+ } catch (e) {
2332
+ setErr(openJoinErrorMessage(e?.message));
2333
+ } finally {
2334
+ setBusy(false);
2335
+ }
2336
+ }
2337
+ const handleGoogle = () => run((oauth) => oauth.signInWithGoogle(), import_core19.openJoinWithGoogle);
2338
+ const handleApple = () => run((oauth) => oauth.signInWithApple(), import_core19.openJoinWithApple);
2339
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(import_react_native17.View, { style: styles16.container, children: [
2340
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(import_react_native17.View, { style: styles16.hero, children: [
2341
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_plugin_runtime10.AdaOrb, { size: 110 }),
2342
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react_native17.Text, { style: styles16.wordmark, children: "Join a team" }),
2343
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react_native17.Text, { style: styles16.tagline, children: "Enter the team's handle. If it's open to join, you're in." })
2344
+ ] }),
2345
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(import_react_native17.View, { style: styles16.form, children: [
2346
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
2347
+ import_react_native17.TextInput,
2348
+ {
2349
+ style: styles16.input,
2350
+ value: team,
2351
+ onChangeText: setTeam,
2352
+ placeholder: "team handle (e.g. acme)",
2353
+ placeholderTextColor: import_core19.ada.inkMuted,
2354
+ autoCapitalize: "none",
2355
+ autoCorrect: false,
2356
+ returnKeyType: "done"
2357
+ }
2358
+ ),
2359
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
2360
+ import_react_native17.Pressable,
2361
+ {
2362
+ style: [styles16.googleBtn, !canSubmit && styles16.btnDisabled],
2363
+ onPress: handleGoogle,
2364
+ disabled: !canSubmit,
2365
+ children: [
2366
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react_native17.Text, { style: styles16.googleGlyph, children: "G" }),
2367
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react_native17.Text, { style: styles16.googleText, children: "Continue with Google" })
2368
+ ]
2369
+ }
2370
+ ),
2371
+ appleAvailable && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react_native17.View, { style: { opacity: canSubmit ? 1 : 0.5 }, pointerEvents: canSubmit ? "auto" : "none", children: getOAuthProvider()?.renderAppleButton({ onPress: handleApple, disabled: !canSubmit, style: styles16.appleBtn }) }),
2372
+ busy && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react_native17.ActivityIndicator, { color: import_core19.ada.ink, style: { marginTop: 10 } }),
2373
+ !!err && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react_native17.Text, { style: styles16.err, children: err }),
2374
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react_native17.Pressable, { onPress: onBack, hitSlop: 8, style: styles16.back, children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react_native17.Text, { style: styles16.backText, children: "Back to sign in" }) })
2375
+ ] })
2376
+ ] });
2377
+ }
2378
+ var styles16 = import_react_native17.StyleSheet.create({
2379
+ container: { flex: 1, backgroundColor: import_core19.ada.paper, justifyContent: "space-between" },
2380
+ hero: { alignItems: "center", marginTop: import_core19.spacing.xxl },
2381
+ wordmark: { fontSize: 26, fontWeight: "800", color: import_core19.ada.ink, marginTop: 14, fontFamily: fontFam8 },
2382
+ tagline: { fontSize: 15, color: import_core19.ada.inkSoft, marginTop: 8, textAlign: "center", paddingHorizontal: 28, fontFamily: fontFam8 },
2383
+ form: { paddingHorizontal: 24, paddingBottom: 40, gap: 12 },
2384
+ input: {
2385
+ backgroundColor: import_core19.ada.paperSoft,
2386
+ borderWidth: 1,
2387
+ borderColor: import_core19.ada.border,
2388
+ borderRadius: 14,
2389
+ paddingHorizontal: 16,
2390
+ paddingVertical: 14,
2391
+ fontSize: 16,
2392
+ color: import_core19.ada.ink
2393
+ },
2394
+ googleBtn: {
2395
+ flexDirection: "row",
2396
+ alignItems: "center",
2397
+ justifyContent: "center",
2398
+ gap: 10,
2399
+ backgroundColor: import_core19.ada.ink,
2400
+ paddingVertical: 15,
2401
+ borderRadius: 999
2402
+ },
2403
+ btnDisabled: { opacity: 0.5 },
2404
+ googleGlyph: { color: "#fff", fontSize: 18, fontWeight: "800" },
2405
+ googleText: { color: "#fff", fontSize: 16, fontWeight: "600", fontFamily: fontFam8 },
2406
+ appleBtn: { height: 52, width: "100%" },
2407
+ err: { color: "#C15545", fontSize: 13, textAlign: "center", paddingHorizontal: 16, marginTop: 4 },
2408
+ back: { alignItems: "center", paddingVertical: 10 },
2409
+ backText: { color: import_core19.ada.inkMuted, fontSize: 15, fontWeight: "500" }
2410
+ });
2411
+
2412
+ // src/auth/JoinScreen.tsx
2413
+ var import_react14 = require("react");
2414
+ var import_react_native18 = require("react-native");
2415
+ var import_core20 = require("@ateam-ai-mobile/core");
2416
+ var import_plugin_runtime11 = require("@ateam-ai-mobile/plugin-runtime");
2417
+
2418
+ // src/auth/inviteJoin.ts
2419
+ async function completeInviteJoin(code, idToken, deps) {
2420
+ if (!idToken) return false;
2421
+ const consumer = await deps.join(code, idToken);
2422
+ deps.onDone();
2423
+ await deps.onSignedIn(consumer.jwt);
2424
+ return true;
2425
+ }
2426
+ function inviteHeadline(preview) {
2427
+ if (!preview) return "";
2428
+ return preview.role ? `Join ${preview.org} as ${preview.role}?` : `Join ${preview.org}?`;
2429
+ }
2430
+
2431
+ // src/auth/JoinScreen.tsx
2432
+ var import_jsx_runtime19 = require("react/jsx-runtime");
2433
+ var fontFam9 = import_react_native18.Platform.OS === "ios" ? "SF Pro Rounded" : void 0;
2434
+ function JoinScreen({ code, onSignedIn, onDone, onCancel }) {
2435
+ const [preview, setPreview] = (0, import_react14.useState)(null);
2436
+ const [loadErr, setLoadErr] = (0, import_react14.useState)(null);
2437
+ const [busy, setBusy] = (0, import_react14.useState)(false);
2438
+ const [err, setErr] = (0, import_react14.useState)(null);
2439
+ const [appleAvailable, setAppleAvailable] = (0, import_react14.useState)(false);
2440
+ (0, import_react14.useEffect)(() => {
2441
+ if (!code) return;
2442
+ setPreview(null);
2443
+ setLoadErr(null);
2444
+ (0, import_core20.previewInvite)(code).then(setPreview).catch((e) => setLoadErr(e?.message || "Could not load invite"));
2445
+ }, [code]);
2446
+ (0, import_react14.useEffect)(() => {
2447
+ getOAuthProvider()?.isAppleAvailable().then(setAppleAvailable).catch(() => setAppleAvailable(false));
2448
+ }, []);
2449
+ const orgLine = (0, import_react14.useMemo)(() => inviteHeadline(preview), [preview]);
2450
+ async function run(acquire, join) {
2451
+ if (!code) return;
2452
+ const oauth = getOAuthProvider();
2453
+ if (!oauth) {
2454
+ setErr("Sign-in is unavailable on this device.");
2455
+ return;
2456
+ }
2457
+ setErr(null);
2458
+ setBusy(true);
2459
+ try {
2460
+ const idToken = await acquire(oauth);
2461
+ await completeInviteJoin(code, idToken, { join, onDone, onSignedIn });
2462
+ } catch (e) {
2463
+ setErr(e?.message || "Sign-in failed");
2464
+ } finally {
2465
+ setBusy(false);
2466
+ }
2467
+ }
2468
+ const handleGoogle = () => run((oauth) => oauth.signInWithGoogle(), import_core20.joinWithGoogle);
2469
+ const handleApple = () => run((oauth) => oauth.signInWithApple(), import_core20.joinWithApple);
2470
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(import_react_native18.View, { style: styles17.container, children: [
2471
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(import_react_native18.View, { style: styles17.hero, children: [
2472
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_plugin_runtime11.AdaOrb, { size: 120 }),
2473
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_react_native18.Text, { style: styles17.wordmark, children: "ada" }),
2474
+ loadErr ? /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_react_native18.Text, { style: styles17.err, children: loadErr }) : preview ? /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_react_native18.Text, { style: styles17.tagline, children: orgLine }) : /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_react_native18.ActivityIndicator, { color: import_core20.ada.ink, style: { marginTop: 12 } })
2475
+ ] }),
2476
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(import_react_native18.View, { style: styles17.buttons, children: [
2477
+ preview && !loadErr && /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(import_jsx_runtime19.Fragment, { children: [
2478
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
2479
+ import_react_native18.Pressable,
2480
+ {
2481
+ style: [styles17.googleBtn, busy && styles17.btnDisabled],
2482
+ onPress: handleGoogle,
2483
+ disabled: busy,
2484
+ children: [
2485
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_react_native18.Text, { style: styles17.googleGlyph, children: "G" }),
2486
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_react_native18.Text, { style: styles17.googleText, children: "Continue with Google" })
2487
+ ]
2488
+ }
2489
+ ),
2490
+ appleAvailable && getOAuthProvider()?.renderAppleButton({ onPress: handleApple, disabled: busy, style: styles17.appleBtn })
2491
+ ] }),
2492
+ loadErr && /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_react_native18.Pressable, { style: styles17.dismiss, onPress: onCancel, hitSlop: 8, children: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_react_native18.Text, { style: styles17.dismissText, children: "Back to sign in" }) }),
2493
+ !!err && /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_react_native18.Text, { style: styles17.err, children: err }),
2494
+ busy && /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_react_native18.ActivityIndicator, { color: import_core20.ada.ink, style: { marginTop: 12 } }),
2495
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_react_native18.Text, { style: styles17.disclaimer, children: "By continuing you agree to Ada's Terms and Privacy Policy." })
2496
+ ] })
2497
+ ] });
2498
+ }
2499
+ var styles17 = import_react_native18.StyleSheet.create({
2500
+ container: { flex: 1, backgroundColor: import_core20.ada.paper, justifyContent: "space-between" },
2501
+ hero: { alignItems: "center", marginTop: import_core20.spacing.xxl * 1.4 },
2502
+ wordmark: { fontSize: 34, fontWeight: "800", color: import_core20.ada.ink, letterSpacing: 4, marginTop: 12, fontFamily: fontFam9 },
2503
+ tagline: { fontSize: 17, color: import_core20.ada.inkSoft, marginTop: 10, textAlign: "center", paddingHorizontal: 24, fontFamily: fontFam9 },
2504
+ buttons: { paddingHorizontal: 24, paddingBottom: 40, gap: 12 },
2505
+ googleBtn: {
2506
+ flexDirection: "row",
2507
+ alignItems: "center",
2508
+ justifyContent: "center",
2509
+ gap: 10,
2510
+ backgroundColor: import_core20.ada.ink,
2511
+ paddingVertical: 15,
2512
+ borderRadius: 999
2513
+ },
2514
+ btnDisabled: { opacity: 0.5 },
2515
+ googleGlyph: { color: "#fff", fontSize: 18, fontWeight: "800" },
2516
+ googleText: { color: "#fff", fontSize: 16, fontWeight: "600", fontFamily: fontFam9 },
2517
+ appleBtn: { height: 52, width: "100%" },
2518
+ dismiss: { alignItems: "center", paddingVertical: 10 },
2519
+ dismissText: { color: import_core20.ada.inkMuted, fontSize: 15, fontWeight: "500" },
2520
+ err: { color: "#C15545", fontSize: 13, textAlign: "center", paddingHorizontal: 16, marginTop: 8 },
2521
+ disclaimer: { fontSize: 12, color: import_core20.ada.inkMuted, textAlign: "center", marginTop: 8 }
2522
+ });
2523
+
2524
+ // src/auth/LoginScreen.tsx
2525
+ var import_react15 = require("react");
2526
+ var import_react_native19 = require("react-native");
2527
+ var import_core21 = require("@ateam-ai-mobile/core");
2528
+ var import_plugin_runtime12 = require("@ateam-ai-mobile/plugin-runtime");
2529
+
2530
+ // src/auth/login.ts
2531
+ function googleClientIds(config) {
2532
+ const g = config.providers.google || {};
2533
+ return {
2534
+ webClientId: g.web_client_id || g.client_id || "",
2535
+ iosClientId: g.ios_client_id || ""
2536
+ };
2537
+ }
2538
+ function shouldRecoverToDefaultTenant(errorMsg, currentTenant, defaultTenant) {
2539
+ return /capability not enabled/i.test(errorMsg || "") && currentTenant !== defaultTenant;
2540
+ }
2541
+ async function completeProviderSignIn(idToken, deps) {
2542
+ if (!idToken) return false;
2543
+ const consumer = await deps.exchange(idToken);
2544
+ await deps.onSignedIn(consumer.jwt);
2545
+ return true;
2546
+ }
2547
+
2548
+ // src/auth/LoginScreen.tsx
2549
+ var import_jsx_runtime20 = require("react/jsx-runtime");
2550
+ var fontFam10 = import_react_native19.Platform.OS === "ios" ? "SF Pro Rounded" : void 0;
2551
+ function LoginScreen({ activeTenant, defaultTenant, onConsumerDisabled, onSignedIn, onJoinTeam }) {
2552
+ const [config, setConfig] = (0, import_react15.useState)(null);
2553
+ const [error, setError] = (0, import_react15.useState)(null);
2554
+ (0, import_react15.useEffect)(() => {
2555
+ setConfig(null);
2556
+ setError(null);
2557
+ (0, import_core21.fetchConsumerConfig)().then(setConfig).catch(async (err) => {
2558
+ const msg = err?.message || "Failed to load sign-in config";
2559
+ if (shouldRecoverToDefaultTenant(msg, activeTenant, defaultTenant)) {
2560
+ await onConsumerDisabled();
2561
+ return;
2562
+ }
2563
+ setError(msg);
2564
+ });
2565
+ }, [activeTenant, defaultTenant, onConsumerDisabled]);
2566
+ return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(import_react_native19.View, { style: styles18.container, children: [
2567
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(import_react_native19.View, { style: styles18.hero, children: [
2568
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_plugin_runtime12.AdaOrb, { size: 140 }),
2569
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_react_native19.Text, { style: styles18.wordmark, children: "ada" }),
2570
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_react_native19.Text, { style: styles18.tagline, children: "Your private personal AI-Team" })
2571
+ ] }),
2572
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(import_react_native19.View, { style: styles18.buttonSection, children: [
2573
+ !config && !error && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_react_native19.ActivityIndicator, { color: import_core21.ada.ink }),
2574
+ !!error && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_react_native19.Text, { style: styles18.errorText, children: error }),
2575
+ !!config && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(SignInButtons, { config, onSignedIn }),
2576
+ onJoinTeam && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_react_native19.Pressable, { onPress: onJoinTeam, hitSlop: 8, style: { paddingVertical: 8 }, children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_react_native19.Text, { style: styles18.joinLink, children: "Join a team by name" }) }),
2577
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_react_native19.Text, { style: styles18.disclaimer, children: "By continuing you agree to Ada's Terms and Privacy Policy." })
2578
+ ] })
2579
+ ] });
2580
+ }
2581
+ function SignInButtons({ config, onSignedIn }) {
2582
+ const [busy, setBusy] = (0, import_react15.useState)(false);
2583
+ const [error, setError] = (0, import_react15.useState)(null);
2584
+ const [appleAvailable, setAppleAvailable] = (0, import_react15.useState)(false);
2585
+ const { webClientId, iosClientId } = (0, import_react15.useMemo)(() => googleClientIds(config), [config]);
2586
+ (0, import_react15.useEffect)(() => {
2587
+ getOAuthProvider()?.isAppleAvailable().then(setAppleAvailable).catch(() => setAppleAvailable(false));
2588
+ }, []);
2589
+ const handleGoogleLogin = async () => {
2590
+ setError(null);
2591
+ if (!webClientId) {
2592
+ setError("Google sign-in is not configured for this tenant");
2593
+ return;
2594
+ }
2595
+ const oauth = getOAuthProvider();
2596
+ if (!oauth) {
2597
+ setError("Sign-in is unavailable on this device.");
2598
+ return;
2599
+ }
2600
+ setBusy(true);
2601
+ try {
2602
+ const idToken = await oauth.signInWithGoogle({ webClientId, iosClientId });
2603
+ await completeProviderSignIn(idToken, { exchange: import_core21.signInWithGoogle, onSignedIn });
2604
+ } catch (err) {
2605
+ setError(err?.message || "Sign-in failed");
2606
+ } finally {
2607
+ setBusy(false);
2608
+ }
2609
+ };
2610
+ const handleAppleLogin = async () => {
2611
+ setError(null);
2612
+ const oauth = getOAuthProvider();
2613
+ if (!oauth) {
2614
+ setError("Sign-in is unavailable on this device.");
2615
+ return;
2616
+ }
2617
+ setBusy(true);
2618
+ try {
2619
+ const idToken = await oauth.signInWithApple();
2620
+ await completeProviderSignIn(idToken, { exchange: import_core21.signInWithApple, onSignedIn });
2621
+ } catch (err) {
2622
+ setError(err?.message || "Apple sign-in failed");
2623
+ } finally {
2624
+ setBusy(false);
2625
+ }
2626
+ };
2627
+ const googleEnabled = !!webClientId && !busy;
2628
+ const appleEnabled = appleAvailable && !!config.providers.apple?.service_id && !busy;
2629
+ return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(import_jsx_runtime20.Fragment, { children: [
2630
+ !!config.providers.google && /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(
2631
+ import_react_native19.Pressable,
2632
+ {
2633
+ style: [styles18.signInBtn, !googleEnabled && styles18.signInBtnDisabled],
2634
+ onPress: handleGoogleLogin,
2635
+ disabled: !googleEnabled,
2636
+ children: [
2637
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_react_native19.Text, { style: styles18.signInGlyph, children: "G" }),
2638
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_react_native19.Text, { style: styles18.signInText, children: "Continue with Google" })
2639
+ ]
2640
+ }
2641
+ ),
2642
+ appleEnabled && getOAuthProvider()?.renderAppleButton({ onPress: handleAppleLogin, disabled: busy, style: styles18.appleBtn }),
2643
+ !!error && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_react_native19.Text, { style: styles18.errorText, children: error }),
2644
+ busy && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_react_native19.ActivityIndicator, { color: import_core21.ada.ink })
2645
+ ] });
2646
+ }
2647
+ var styles18 = import_react_native19.StyleSheet.create({
2648
+ container: {
2649
+ flex: 1,
2650
+ backgroundColor: import_core21.ada.paper,
2651
+ justifyContent: "space-between",
2652
+ paddingHorizontal: 32,
2653
+ paddingBottom: 24
2654
+ },
2655
+ hero: { flex: 1, alignItems: "center", justifyContent: "center", gap: 18 },
2656
+ wordmark: { fontSize: 32, fontWeight: "600", color: import_core21.ada.ink, letterSpacing: 4, fontFamily: fontFam10, marginTop: 8 },
2657
+ tagline: { fontSize: 16, fontWeight: "500", color: import_core21.ada.inkSoft, letterSpacing: -0.1, fontFamily: fontFam10 },
2658
+ buttonSection: { alignItems: "center", gap: 12 },
2659
+ signInBtn: {
2660
+ flexDirection: "row",
2661
+ alignItems: "center",
2662
+ justifyContent: "center",
2663
+ backgroundColor: import_core21.ada.ink,
2664
+ paddingVertical: 16,
2665
+ paddingHorizontal: 24,
2666
+ borderRadius: 999,
2667
+ width: "100%",
2668
+ gap: 12
2669
+ },
2670
+ signInBtnDisabled: { opacity: 0.5 },
2671
+ signInGlyph: { fontSize: 18, fontWeight: "700", color: import_core21.ada.paper, fontFamily: fontFam10 },
2672
+ signInText: { fontSize: 16, fontWeight: "600", color: import_core21.ada.paper, letterSpacing: -0.2, fontFamily: fontFam10 },
2673
+ appleBtn: { width: "100%", height: 52 },
2674
+ errorText: { fontSize: 13, color: "#c43d3d", textAlign: "center", paddingHorizontal: 16 },
2675
+ disclaimer: { fontSize: 12, color: import_core21.ada.inkMuted, textAlign: "center", marginTop: 8, lineHeight: 18, fontFamily: fontFam10 },
2676
+ joinLink: { fontSize: 14, fontWeight: "600", color: import_core21.ada.orangeDeep, textAlign: "center", fontFamily: fontFam10 }
2677
+ });
2678
+
2679
+ // src/voice/VoiceCallSheet.tsx
2680
+ var import_react17 = require("react");
2681
+ var import_react_native20 = require("react-native");
2682
+ var import_react_native_svg2 = __toESM(require("react-native-svg"));
2683
+ var import_core23 = require("@ateam-ai-mobile/core");
2684
+ var import_plugin_runtime14 = require("@ateam-ai-mobile/plugin-runtime");
2685
+
2155
2686
  // src/voice/useVoiceCall.ts
2156
- var import_react12 = require("react");
2157
- var import_core17 = require("@ateam-ai-mobile/core");
2687
+ var import_react16 = require("react");
2688
+
2689
+ // src/voice/voiceSession.ts
2690
+ var import_core22 = require("@ateam-ai-mobile/core");
2691
+ var import_plugin_runtime13 = require("@ateam-ai-mobile/plugin-runtime");
2692
+ var CONNECT_TIMEOUT_MS = 3e4;
2158
2693
  var TOKEN_REFRESH_SKEW_MS = 9e4;
2159
2694
  function tokenNeedsRefresh(token) {
2160
2695
  if (!token) return true;
@@ -2182,7 +2717,6 @@ var INJECT_TRACK_HOOK = `
2182
2717
  }
2183
2718
  }
2184
2719
 
2185
- // 1) Hook getUserMedia \u2014 captures mic streams at request time.
2186
2720
  try {
2187
2721
  if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
2188
2722
  const origGUM = navigator.mediaDevices.getUserMedia.bind(navigator.mediaDevices);
@@ -2196,7 +2730,6 @@ var INJECT_TRACK_HOOK = `
2196
2730
  }
2197
2731
  } catch (e) {}
2198
2732
 
2199
- // 2) Hook RTCPeerConnection.addTrack \u2014 catches whatever the app sends.
2200
2733
  try {
2201
2734
  if (window.RTCPeerConnection) {
2202
2735
  const origPC = window.RTCPeerConnection;
@@ -2216,9 +2749,7 @@ var INJECT_TRACK_HOOK = `
2216
2749
  }
2217
2750
  if (origAddTransceiver) {
2218
2751
  origPC.prototype.addTransceiver = function(arg) {
2219
- if (arg && arg.kind === 'audio') {
2220
- // Transceiver created with kind 'audio' \u2014 sender track set later.
2221
- } else if (arg && arg.kind === undefined && arg.track && arg.track.kind === 'audio') {
2752
+ if (arg && arg.kind === undefined && arg.track && arg.track.kind === 'audio') {
2222
2753
  track(arg.track);
2223
2754
  }
2224
2755
  const tx = origAddTransceiver.apply(this, arguments);
@@ -2231,7 +2762,6 @@ var INJECT_TRACK_HOOK = `
2231
2762
  }
2232
2763
  } catch (e) {}
2233
2764
 
2234
- // 3) Helper exposed for direct calls.
2235
2765
  window._adaSetMuted = function(muted) {
2236
2766
  let n = 0;
2237
2767
  if (Array.isArray(window._allAudioTracks)) {
@@ -2250,7 +2780,6 @@ var buildMuteScript = (muted) => `
2250
2780
  if (typeof window._adaSetMuted === 'function') {
2251
2781
  n = window._adaSetMuted(muted);
2252
2782
  }
2253
- // Last-resort scan if hook somehow missed.
2254
2783
  if (n === 0 && Array.isArray(window._allAudioTracks)) {
2255
2784
  window._allAudioTracks.forEach(function(t) {
2256
2785
  try { t.enabled = !muted; n++; } catch (e) {}
@@ -2259,104 +2788,557 @@ var buildMuteScript = (muted) => `
2259
2788
  console.log('[ada-mute]', muted ? 'MUTED' : 'UNMUTED', 'tracks=', n);
2260
2789
  })(${muted}); true;
2261
2790
  `;
2262
- function useVoiceCall({ skillSlug } = {}) {
2263
- const [callState, setCallState] = (0, import_react12.useState)("idle");
2264
- const [sessionId, setSessionId] = (0, import_react12.useState)(null);
2265
- const [error, setError] = (0, import_react12.useState)(null);
2266
- const [transcript, setTranscript] = (0, import_react12.useState)([]);
2267
- const [aiPartial, setAiPartial] = (0, import_react12.useState)("");
2268
- const [userPartial, setUserPartial] = (0, import_react12.useState)("");
2269
- const [muted, setMuted] = (0, import_react12.useState)(false);
2270
- const webViewRef = (0, import_react12.useRef)(null);
2271
- const handleWebViewMessage = (0, import_react12.useCallback)((event) => {
2791
+ var buildAgentTurnScript = (input) => `
2792
+ (function(input) {
2793
+ if (typeof window.__adasAgentTurn === 'function') {
2794
+ try { window.__adasAgentTurn(input); }
2795
+ catch (e) {
2796
+ window.ReactNativeWebView && window.ReactNativeWebView.postMessage(JSON.stringify({
2797
+ type: 'agentTurnFailed', message: String(e && e.message || e),
2798
+ }));
2799
+ }
2800
+ } else if (window.ReactNativeWebView) {
2801
+ window.ReactNativeWebView.postMessage(JSON.stringify({
2802
+ type: 'agentTurnUnsupported',
2803
+ message: 'voice app does not support agent turns (__adasAgentTurn missing) \u2014 update the voice page',
2804
+ }));
2805
+ }
2806
+ })(${JSON.stringify(input)}); true;
2807
+ `;
2808
+ var buildObserveScript = (text) => `
2809
+ (function(text) {
2810
+ if (typeof window.__adasObserve === 'function') {
2811
+ try { window.__adasObserve(text); }
2812
+ catch (e) {
2813
+ window.ReactNativeWebView && window.ReactNativeWebView.postMessage(JSON.stringify({
2814
+ type: 'observeFailed', message: String(e && e.message || e),
2815
+ }));
2816
+ }
2817
+ } else if (window.ReactNativeWebView) {
2818
+ window.ReactNativeWebView.postMessage(JSON.stringify({
2819
+ type: 'observeUnsupported',
2820
+ message: 'voice app does not support observe (__adasObserve missing) \u2014 update the voice page',
2821
+ }));
2822
+ }
2823
+ })(${JSON.stringify(text)}); true;
2824
+ `;
2825
+ function originOf(url) {
2826
+ if (!url) return null;
2827
+ const m = /^([a-z][a-z0-9+.-]*:\/\/[^/?#]+)/i.exec(url);
2828
+ return m ? m[1].toLowerCase() : null;
2829
+ }
2830
+ var IDLE = {
2831
+ callState: "idle",
2832
+ sessionId: null,
2833
+ error: null,
2834
+ transcript: [],
2835
+ aiPartial: "",
2836
+ userPartial: "",
2837
+ muted: false,
2838
+ userSpeaking: false,
2839
+ lastUserSpeechEndedAt: null,
2840
+ lastDeviceTool: null,
2841
+ mounted: false,
2842
+ webViewSource: null,
2843
+ skillSlug: void 0
2844
+ };
2845
+ var VoiceSessionManager = class {
2846
+ constructor() {
2847
+ this.state = { ...IDLE };
2848
+ this.listeners = /* @__PURE__ */ new Set();
2849
+ this.webView = null;
2850
+ this.pending = [];
2851
+ /**
2852
+ * WHO owns the one physical session. Set to the caller that started it from
2853
+ * idle (a plugin's pluginId, or the PLATFORM symbol for the built-in sheet),
2854
+ * and kept after stop until the next start reassigns it — so a plugin can read
2855
+ * its own just-ended transcript but another plugin never can.
2856
+ *
2857
+ * ISOLATION (GPT blocker on 8881225): every access is scoped through this. A
2858
+ * different plugin must not read, control, or attach to another plugin's live
2859
+ * conversation just because it declared voice.
2860
+ */
2861
+ this.owner = null;
2862
+ /** EVERY start() still in its pre-mount awaits (tool preload, token refresh).
2863
+ * The session is not 'connecting' yet, so a stop() arriving now would otherwise
2864
+ * be an idle no-op — and a start would then mount a call for a screen that has
2865
+ * already gone. A set, not one slot: two concurrent starts must BOTH be
2866
+ * cancellable. stop() cancels every pending start of its caller (the platform:
2867
+ * all of them); another plugin's stop cancels none. */
2868
+ this.pendingStarts = /* @__PURE__ */ new Set();
2869
+ /**
2870
+ * The device-tool names Core ALLOWED for this voice session — Core's sanitized
2871
+ * allowset, delivered to the phone in a `deviceToolAllowset` message.
2872
+ * Authorization gates against THIS (necessary, alongside the registry's can-do).
2873
+ *
2874
+ * WHY (GPT gate 1, corrected): the phone only PROPOSES its available tools
2875
+ * (sendDeviceToolList); CORE is the session authority and decides which are
2876
+ * accepted (session.device_tools). Freezing the phone's own proposal would let
2877
+ * a tool Core REJECTED still run locally. So the authz set is populated from
2878
+ * Core's accepted set, never from listDeviceTools(). Until Core's set arrives
2879
+ * this is null and every call FAILS CLOSED. Pinned once per session; the live
2880
+ * registry (which drifts via lazy loads / host registration / tenant changes)
2881
+ * never feeds it.
2882
+ */
2883
+ this.frozenTools = null;
2884
+ /** The voice `sessionId` the current allowset is BOUND to. Authorization
2885
+ * requires it to equal the active session's id (state.sessionId), so a stale
2886
+ * allowset from a previous/other session is refused. */
2887
+ this.frozenSessionId = null;
2888
+ /** A CLEAN call (skills:'none') started by a plugin offers and runs ONLY that
2889
+ * plugin's own device tools — not other solutions' tools that happen to be
2890
+ * registered on the same phone. null = every registered tool (a normal call,
2891
+ * or the platform's). Set by every start(); like `owner`, kept after stop.
2892
+ * ONE identity: the caller is the TRUSTED manifest pluginId gateVoice stamps,
2893
+ * and the bundleManager registers a plugin's tools under that same id (never
2894
+ * the bundle's own short `id`), so exact equality is the whole check. */
2895
+ this.toolScope = null;
2896
+ this.injectedTrackHook = INJECT_TRACK_HOOK;
2897
+ }
2898
+ /** The platform (built-in sheet, mic indicator, session host) sees and
2899
+ * controls any session — it is the user's own surface. A plugin caller may
2900
+ * only touch a session it owns. */
2901
+ canAccess(caller) {
2902
+ return caller === import_plugin_runtime13.PLATFORM_VOICE_CALLER || this.owner === caller;
2903
+ }
2904
+ foreignError() {
2905
+ const err = new Error(
2906
+ "voice: another solution is using the microphone \u2014 one live session per device."
2907
+ );
2908
+ err.code = "RESTRICTED";
2909
+ return err;
2910
+ }
2911
+ // ── Host wiring (used only by VoiceSessionHost) ──────────────
2912
+ /** The offscreen WebView sets its ref here so the session can inject into it. */
2913
+ setWebView(ref) {
2914
+ this.webView = ref;
2915
+ }
2916
+ /** Whole state — the host reads `mounted` + `webViewSource` to decide what to
2917
+ * render, and every reader derives its snapshot from this. */
2918
+ getInternal() {
2919
+ return this.state;
2920
+ }
2921
+ // ── VoiceAPI (what props.voice and useVoiceCall call) ────────
2922
+ async start(opts, caller = import_plugin_runtime13.PLATFORM_VOICE_CALLER) {
2923
+ if (this.state.callState === "connected") {
2924
+ if (!this.canAccess(caller)) throw this.foreignError();
2925
+ return this.realSnapshot();
2926
+ }
2927
+ if (this.state.callState === "connecting") {
2928
+ if (!this.canAccess(caller)) throw this.foreignError();
2929
+ return this.waitForConnect();
2930
+ }
2931
+ if (opts?.skills === "none" && opts?.skillSlug) {
2932
+ throw new Error("voice: skills:'none' (a clean call) cannot also hand the mic to a skill (skillSlug).");
2933
+ }
2934
+ const ticket = { caller, cancelled: false };
2935
+ this.pendingStarts.add(ticket);
2272
2936
  try {
2273
- const data = JSON.parse(event.nativeEvent.data);
2274
- console.log("[VoiceWebView] message:", data.type, data.text?.slice?.(0, 50) || data.state || "");
2275
- switch (data.type) {
2276
- case "callState":
2277
- case "call-state":
2278
- setCallState(data.state);
2279
- if (data.sessionId) setSessionId(data.sessionId);
2280
- if (data.error) setError(data.error);
2281
- break;
2282
- case "transcript":
2283
- setTranscript((prev) => [...prev, { role: data.role, text: data.text, ts: Date.now() }]);
2284
- break;
2285
- case "aiPartial":
2286
- setAiPartial(data.text);
2287
- break;
2288
- case "userPartial":
2289
- setUserPartial(data.text);
2290
- break;
2291
- case "clearPartials":
2292
- setAiPartial("");
2293
- setUserPartial("");
2937
+ await (0, import_plugin_runtime13.ensureDeviceToolsLoaded)();
2938
+ } catch (e) {
2939
+ console.warn("[voiceSession] device tool preload failed:", e?.message || String(e));
2940
+ }
2941
+ try {
2942
+ if (tokenNeedsRefresh((0, import_core22.getToken)())) await (0, import_core22.refreshToken)();
2943
+ } catch {
2944
+ }
2945
+ this.pendingStarts.delete(ticket);
2946
+ if (ticket.cancelled) throw new Error("voice: stopped before the session started");
2947
+ const after = this.getInternal().callState;
2948
+ if (after === "connected") {
2949
+ if (!this.canAccess(caller)) throw this.foreignError();
2950
+ return this.realSnapshot();
2951
+ }
2952
+ if (after === "connecting") {
2953
+ if (!this.canAccess(caller)) throw this.foreignError();
2954
+ return this.waitForConnect();
2955
+ }
2956
+ this.owner = caller;
2957
+ this.toolScope = opts?.skills === "none" && typeof caller === "string" ? caller : null;
2958
+ this.frozenTools = null;
2959
+ this.frozenSessionId = null;
2960
+ const skillSlug = opts?.skillSlug;
2961
+ this.setState({
2962
+ callState: "connecting",
2963
+ error: null,
2964
+ transcript: [],
2965
+ aiPartial: "",
2966
+ userPartial: "",
2967
+ muted: false,
2968
+ mounted: true,
2969
+ skillSlug,
2970
+ persona: opts?.persona,
2971
+ webViewSource: { uri: this.buildUri(skillSlug, opts?.persona, opts?.skills) }
2972
+ });
2973
+ return this.waitForConnect();
2974
+ }
2975
+ stop(caller = import_plugin_runtime13.PLATFORM_VOICE_CALLER) {
2976
+ if (this.state.callState !== "idle" && !this.canAccess(caller)) throw this.foreignError();
2977
+ for (const t of [...this.pendingStarts]) {
2978
+ if (caller === import_plugin_runtime13.PLATFORM_VOICE_CALLER || caller === t.caller) {
2979
+ t.cancelled = true;
2980
+ this.pendingStarts.delete(t);
2981
+ }
2982
+ }
2983
+ this.webView?.injectJavaScript?.("window.hangUp && window.hangUp(); true;");
2984
+ this.rejectPending(new Error("voice: session stopped before it connected"));
2985
+ this.frozenTools = null;
2986
+ this.frozenSessionId = null;
2987
+ this.setState({
2988
+ ...IDLE,
2989
+ // keep the transcript of the call that just ended for any reader still up
2990
+ transcript: this.state.transcript
2991
+ });
2992
+ }
2993
+ setMuted(muted, caller = import_plugin_runtime13.PLATFORM_VOICE_CALLER) {
2994
+ if (this.state.callState !== "idle" && !this.canAccess(caller)) throw this.foreignError();
2995
+ this.webView?.injectJavaScript?.(buildMuteScript(muted));
2996
+ this.setState({ muted });
2997
+ }
2998
+ prompt(input, caller = import_plugin_runtime13.PLATFORM_VOICE_CALLER) {
2999
+ if (this.state.callState !== "connected") {
3000
+ throw new Error("voice: session is not connected \u2014 call start() and await it before prompt().");
3001
+ }
3002
+ if (!this.canAccess(caller)) throw this.foreignError();
3003
+ this.webView?.injectJavaScript?.(buildAgentTurnScript(input));
3004
+ }
3005
+ /**
3006
+ * Stream a silent observation into the live session — tell the model without
3007
+ * asking it to speak. The cheap channel behind the gated props.voice.observe();
3008
+ * every item stays in the model's context for the call, so a caller sends only
3009
+ * material changes. Owner-scoped like every other control.
3010
+ */
3011
+ observe(text, caller = import_plugin_runtime13.PLATFORM_VOICE_CALLER) {
3012
+ if (this.state.callState !== "connected") {
3013
+ throw new Error("voice: session is not connected \u2014 observe() needs a live session.");
3014
+ }
3015
+ if (!this.canAccess(caller)) throw this.foreignError();
3016
+ this.webView?.injectJavaScript?.(buildObserveScript(text));
3017
+ }
3018
+ getSnapshot(caller = import_plugin_runtime13.PLATFORM_VOICE_CALLER) {
3019
+ return this.canAccess(caller) ? this.realSnapshot() : this.foreignSnapshot();
3020
+ }
3021
+ realSnapshot() {
3022
+ const s = this.state;
3023
+ return {
3024
+ state: s.callState,
3025
+ sessionId: s.sessionId,
3026
+ muted: s.muted,
3027
+ speaking: !!s.aiPartial,
3028
+ listening: !!s.userPartial,
3029
+ userSpeaking: s.userSpeaking,
3030
+ lastUserSpeechEndedAt: s.lastUserSpeechEndedAt,
3031
+ lastDeviceTool: s.lastDeviceTool,
3032
+ authorizedSessionId: this.frozenSessionId,
3033
+ lastError: s.error
3034
+ };
3035
+ }
3036
+ foreignSnapshot() {
3037
+ return {
3038
+ state: "idle",
3039
+ sessionId: null,
3040
+ muted: false,
3041
+ speaking: false,
3042
+ listening: false,
3043
+ userSpeaking: false,
3044
+ lastUserSpeechEndedAt: null,
3045
+ lastDeviceTool: null,
3046
+ authorizedSessionId: null,
3047
+ lastError: null
3048
+ };
3049
+ }
3050
+ subscribe(cb, caller = import_plugin_runtime13.PLATFORM_VOICE_CALLER) {
3051
+ const wrapped = (event) => {
3052
+ if (this.canAccess(caller)) cb(event);
3053
+ };
3054
+ this.listeners.add(wrapped);
3055
+ return { unsubscribe: () => {
3056
+ this.listeners.delete(wrapped);
3057
+ } };
3058
+ }
3059
+ transcript(caller = import_plugin_runtime13.PLATFORM_VOICE_CALLER) {
3060
+ return this.canAccess(caller) ? this.state.transcript.slice() : [];
3061
+ }
3062
+ // ── WebView plumbing ─────────────────────────────────────────
3063
+ buildUri(skillSlug, persona, skills) {
3064
+ const token = (0, import_core22.getToken)();
3065
+ const tenant = (0, import_core22.getTenant)();
3066
+ const actorId = (0, import_core22.getUser)()?.actorId || "";
3067
+ const skillParam = skillSlug ? `&skillSlug=${encodeURIComponent(skillSlug)}` : "";
3068
+ const p = persona && (persona.name || persona.style) ? { ...persona.name ? { name: persona.name } : {}, ...persona.style ? { style: persona.style } : {} } : null;
3069
+ const personaParam = p ? `&persona=${encodeURIComponent(JSON.stringify(p))}` : "";
3070
+ const skillsParam = skills === "none" ? "&skills=none" : "";
3071
+ return `${(0, import_core22.getVoiceApiUrl)()}/app?mode=mini&embedded=rn&authToken=${encodeURIComponent(token || "")}&tenant=${encodeURIComponent(tenant)}&actorId=${encodeURIComponent(actorId)}${skillParam}${personaParam}${skillsParam}`;
3072
+ }
3073
+ /** Handle a postMessage from the WebView. `event` is the RN WebView event. */
3074
+ handleMessage(event) {
3075
+ let data;
3076
+ try {
3077
+ data = JSON.parse(event.nativeEvent.data);
3078
+ } catch {
3079
+ return;
3080
+ }
3081
+ switch (data.type) {
3082
+ case "callState":
3083
+ case "call-state":
3084
+ if (data.state) this.setState({ callState: data.state });
3085
+ if (data.sessionId) this.setState({ sessionId: data.sessionId });
3086
+ if (data.error) this.setState({ error: data.error });
3087
+ if (data.state === "connected") this.resolvePending();
3088
+ if (data.state === "error") this.rejectPending(new Error(data.error || "voice: connection error"));
3089
+ break;
3090
+ case "transcript":
3091
+ this.appendTranscript({ role: data.role, text: data.text, ts: Date.now() });
3092
+ break;
3093
+ case "aiPartial":
3094
+ this.setState({ aiPartial: data.text });
3095
+ break;
3096
+ case "userPartial":
3097
+ this.setState({ userPartial: data.text });
3098
+ break;
3099
+ case "clearPartials":
3100
+ this.setState({ aiPartial: "", userPartial: "" });
3101
+ break;
3102
+ // User VAD from the mini-app. `at` is when it happened; on speaking:false
3103
+ // we stamp lastUserSpeechEndedAt so a solution can gate "speak after N s of
3104
+ // silence" on the first real stop, never before the user has spoken.
3105
+ case "speechActivity": {
3106
+ const speaking = !!data.speaking;
3107
+ const at = typeof data.at === "number" ? data.at : Date.now();
3108
+ this.setState(
3109
+ speaking ? { userSpeaking: true } : { userSpeaking: false, lastUserSpeechEndedAt: at }
3110
+ );
3111
+ break;
3112
+ }
3113
+ case "error":
3114
+ this.setState({ error: data.message, callState: "error" });
3115
+ this.rejectPending(new Error(data.message || "voice: error"));
3116
+ break;
3117
+ // A proactive turn couldn't run (older voice page / hook threw). NON-FATAL:
3118
+ // the session stays live; we only surface why the guide didn't speak.
3119
+ case "agentTurnUnsupported":
3120
+ case "agentTurnFailed":
3121
+ case "observeUnsupported":
3122
+ case "observeFailed":
3123
+ this.setState({ error: data.message || "voice: channel notice" });
3124
+ break;
3125
+ // Device tools — executed locally, origin-checked. The model's function
3126
+ // call already arrived on this phone; only the voice app's own origin may
3127
+ // drive the camera/GPS.
3128
+ case "device-tool-list": {
3129
+ const listSender = originOf(event?.nativeEvent?.url);
3130
+ const listExpected = originOf((0, import_core22.getVoiceApiUrl)());
3131
+ if (!listSender || !listExpected || listSender !== listExpected) break;
3132
+ this.sendDeviceToolList(data.correlationId);
3133
+ break;
3134
+ }
3135
+ // CORE's ALLOWSET for this session — what the model may ASK for. Core sends
3136
+ // it (even empty, on purpose) at session start. It is a NECESSARY gate, not
3137
+ // the whole story: a call must be in the allowset AND the device must
3138
+ // actually have the tool (the registry check inside callDeviceTool) — Core
3139
+ // says what may be asked, the device decides what it will do.
3140
+ //
3141
+ // Origin-checked (only the voice app may set it) and pinned ONCE per session
3142
+ // (a later message cannot widen it). An EMPTY allowset (arrived) refuses
3143
+ // everything but is distinct from NEVER-arrived (frozenTools stays null);
3144
+ // both fail closed here — we never fall back to the phone's own registry,
3145
+ // which is the seam this closes.
3146
+ case "deviceToolAllowset": {
3147
+ const sender = originOf(event?.nativeEvent?.url);
3148
+ const expected = originOf((0, import_core22.getVoiceApiUrl)());
3149
+ if (!sender || !expected || sender !== expected) break;
3150
+ if (Array.isArray(data.tools) && typeof data.sessionId === "string" && data.sessionId !== this.frozenSessionId) {
3151
+ this.frozenTools = new Set(data.tools.filter((n) => typeof n === "string"));
3152
+ this.frozenSessionId = data.sessionId;
3153
+ }
3154
+ break;
3155
+ }
3156
+ case "device-tool-call": {
3157
+ const sender = originOf(event?.nativeEvent?.url);
3158
+ const expected = originOf((0, import_core22.getVoiceApiUrl)());
3159
+ if (!sender || !expected || sender !== expected) {
3160
+ console.warn(`[voiceSession] REFUSED device tool call from "${sender || "unknown origin"}" (expected "${expected}")`);
2294
3161
  break;
2295
- case "error":
2296
- setError(data.message);
2297
- setCallState("error");
3162
+ }
3163
+ if (data.name) this.setState({ lastDeviceTool: { name: data.name, at: Date.now() } });
3164
+ if (!this.isToolAuthorized(data.name)) {
3165
+ const notReady = this.frozenTools === null;
3166
+ console.warn(`[voiceSession] REFUSED "${data.name}" \u2014 ${notReady ? "allowset not received yet" : "not authorized for this session"}`);
3167
+ this.postDeviceToolRefusal(data.correlationId, data.name, notReady);
2298
3168
  break;
3169
+ }
3170
+ void this.runDeviceTool(data);
3171
+ break;
2299
3172
  }
2300
- } catch {
2301
3173
  }
2302
- }, []);
2303
- const startCall = (0, import_react12.useCallback)(async () => {
2304
- setError(null);
2305
- setTranscript([]);
2306
- setAiPartial("");
2307
- setUserPartial("");
3174
+ }
3175
+ /**
3176
+ * Final authorization = Core's frozen allowset ∩ the current valid local
3177
+ * registry, bound to the ACTIVE session. All must hold:
3178
+ * - a Core allowset arrived and is bound to THIS session (frozenSessionId ===
3179
+ * the active state.sessionId) — a missing or stale/wrong-session allowset
3180
+ * fails closed;
3181
+ * - the tool is IN that allowset (Core approved it); AND
3182
+ * - the tool is STILL registered locally right now (removed-after-approval is
3183
+ * refused; a lazily-added tool Core never approved is refused) — and, on a
3184
+ * clean call, registered by the call's owner.
3185
+ * No fallback to the live registry alone, ever.
3186
+ */
3187
+ isToolAuthorized(name) {
3188
+ if (typeof name !== "string") return false;
3189
+ if (this.frozenTools === null || this.frozenSessionId === null) return false;
3190
+ if (this.frozenSessionId !== this.state.sessionId) return false;
3191
+ if (!this.frozenTools.has(name)) return false;
3192
+ return (0, import_plugin_runtime13.hasDeviceTool)(name, this.toolScope ?? void 0);
3193
+ }
3194
+ /** Tell the mini-app the call was refused — a stated reason, never a silent drop.
3195
+ * `notReady` distinguishes a transient "allowset not here yet" (retry) from a
3196
+ * final "not allowed in this session". */
3197
+ postDeviceToolRefusal(correlationId, name, notReady) {
3198
+ const payload = notReady ? {
3199
+ correlationId,
3200
+ ok: false,
3201
+ reason: "not_ready",
3202
+ error: "The voice session is still starting up \u2014 its tool allowset has not arrived yet. Try again in a moment."
3203
+ } : {
3204
+ correlationId,
3205
+ ok: false,
3206
+ reason: "not_authorized",
3207
+ error: `"${name}" is not available in this voice session.`
3208
+ };
3209
+ this.webView?.injectJavaScript?.(
3210
+ `window.__adasDeviceToolResult && window.__adasDeviceToolResult(${JSON.stringify(
3211
+ JSON.stringify(payload)
3212
+ )}); true;`
3213
+ );
3214
+ }
3215
+ /** PROPOSE the phone's available tools — this is an offer to Core, not the
3216
+ * authorization set. Core sanitizes it and returns the accepted set in a
3217
+ * `deviceToolAllowset` message, which is what actually gates execution. */
3218
+ sendDeviceToolList(correlationId) {
3219
+ const scope = this.toolScope;
3220
+ const tools = (0, import_plugin_runtime13.listDeviceTools)().filter((t) => scope === null || t.pluginId === scope).map((t) => ({
3221
+ name: t.name,
3222
+ description: t.description,
3223
+ input_schema: t.input_schema
3224
+ }));
3225
+ this.webView?.injectJavaScript?.(
3226
+ `window.__adasDeviceToolResult && window.__adasDeviceToolResult(${JSON.stringify(
3227
+ JSON.stringify({ correlationId, ok: true, tools })
3228
+ )}); true;`
3229
+ );
3230
+ }
3231
+ /** Run a device tool and post the outcome back into the WebView. */
3232
+ async runDeviceTool(data) {
3233
+ const { correlationId, name, args, timeoutMs } = data || {};
3234
+ if (!correlationId || typeof name !== "string") return;
3235
+ let payload;
2308
3236
  try {
2309
- if (tokenNeedsRefresh((0, import_core17.getToken)())) {
2310
- await (0, import_core17.refreshToken)();
3237
+ const outcome = await (0, import_plugin_runtime13.callDeviceTool)(name, args || {}, { timeoutMs });
3238
+ payload = outcome.ok ? { correlationId, ok: true, result: outcome.result } : { correlationId, ok: false, error: outcome.error, reason: outcome.reason };
3239
+ } catch (err) {
3240
+ payload = { correlationId, ok: false, reason: "failed", error: err?.message || String(err) };
3241
+ }
3242
+ this.webView?.injectJavaScript?.(
3243
+ `window.__adasDeviceToolResult && window.__adasDeviceToolResult(${JSON.stringify(
3244
+ JSON.stringify(payload)
3245
+ )}); true;`
3246
+ );
3247
+ }
3248
+ // ── internals ────────────────────────────────────────────────
3249
+ setState(partial) {
3250
+ this.state = { ...this.state, ...partial };
3251
+ this.emit({ type: "state" });
3252
+ }
3253
+ appendTranscript(entry) {
3254
+ this.state = { ...this.state, transcript: [...this.state.transcript, entry] };
3255
+ this.emit({ type: "transcript" });
3256
+ }
3257
+ emit(event) {
3258
+ for (const cb of this.listeners) {
3259
+ try {
3260
+ cb(event);
3261
+ } catch (e) {
3262
+ console.warn("[voiceSession] listener threw", e);
2311
3263
  }
2312
- } catch {
2313
3264
  }
2314
- setCallState("connecting");
2315
- }, []);
2316
- const stopCall = (0, import_react12.useCallback)(() => {
2317
- webViewRef.current?.injectJavaScript?.("window.hangUp && window.hangUp(); true;");
2318
- setCallState("idle");
2319
- setAiPartial("");
2320
- setUserPartial("");
2321
- setMuted(false);
2322
- }, []);
2323
- const toggleMute = (0, import_react12.useCallback)(() => {
2324
- setMuted((prev) => {
2325
- const next = !prev;
2326
- webViewRef.current?.injectJavaScript?.(buildMuteScript(next));
2327
- return next;
3265
+ }
3266
+ waitForConnect() {
3267
+ return new Promise((resolve, reject) => {
3268
+ const timer = setTimeout(() => {
3269
+ this.pending = this.pending.filter((p) => p.timer !== timer);
3270
+ reject(new Error("voice: timed out waiting to connect (is VoiceSessionHost mounted?)"));
3271
+ }, CONNECT_TIMEOUT_MS);
3272
+ this.pending.push({ resolve, reject, timer });
2328
3273
  });
3274
+ }
3275
+ resolvePending() {
3276
+ const snap = this.realSnapshot();
3277
+ const waiting = this.pending;
3278
+ this.pending = [];
3279
+ for (const p of waiting) {
3280
+ clearTimeout(p.timer);
3281
+ p.resolve(snap);
3282
+ }
3283
+ }
3284
+ rejectPending(err) {
3285
+ const waiting = this.pending;
3286
+ this.pending = [];
3287
+ for (const p of waiting) {
3288
+ clearTimeout(p.timer);
3289
+ p.reject(err);
3290
+ }
3291
+ }
3292
+ /** TEST ONLY: reset the singleton to a clean idle state (mirrors the
3293
+ * __resetSessionSyncForTest precedent). Not part of any surface. */
3294
+ __resetForTest() {
3295
+ this.rejectPending(new Error("voice: reset"));
3296
+ this.state = { ...IDLE };
3297
+ this.owner = null;
3298
+ this.toolScope = null;
3299
+ this.pendingStarts.clear();
3300
+ this.frozenTools = null;
3301
+ this.frozenSessionId = null;
3302
+ this.listeners.clear();
3303
+ this.webView = null;
3304
+ }
3305
+ };
3306
+ var voiceSession = new VoiceSessionManager();
3307
+
3308
+ // src/voice/useVoiceCall.ts
3309
+ function useVoiceCall({ skillSlug } = {}) {
3310
+ const [, force] = (0, import_react16.useReducer)((x) => x + 1, 0);
3311
+ (0, import_react16.useEffect)(() => {
3312
+ const sub = voiceSession.subscribe(() => force());
3313
+ return () => sub.unsubscribe();
2329
3314
  }, []);
2330
- const getWebViewSource = (0, import_react12.useCallback)(() => {
2331
- const token = (0, import_core17.getToken)();
2332
- const tenant = (0, import_core17.getTenant)();
2333
- const actorId = (0, import_core17.getUser)()?.actorId || "";
2334
- const skillParam = skillSlug ? `&skillSlug=${encodeURIComponent(skillSlug)}` : "";
2335
- return {
2336
- uri: `${(0, import_core17.getVoiceApiUrl)()}/app?mode=mini&embedded=rn&authToken=${encodeURIComponent(token || "")}&tenant=${encodeURIComponent(tenant)}&actorId=${encodeURIComponent(actorId)}${skillParam}`
2337
- };
2338
- }, [skillSlug]);
3315
+ const s = voiceSession.getInternal();
3316
+ const startCall = (0, import_react16.useCallback)(
3317
+ (opts) => voiceSession.start(opts ?? { skillSlug }),
3318
+ [skillSlug]
3319
+ );
3320
+ const stopCall = (0, import_react16.useCallback)(() => voiceSession.stop(), []);
3321
+ const toggleMute = (0, import_react16.useCallback)(
3322
+ () => voiceSession.setMuted(!voiceSession.getSnapshot().muted),
3323
+ []
3324
+ );
2339
3325
  return {
2340
- callState,
2341
- sessionId,
2342
- error,
2343
- transcript,
2344
- aiPartial,
2345
- userPartial,
2346
- muted,
3326
+ callState: s.callState,
3327
+ sessionId: s.sessionId,
3328
+ error: s.error,
3329
+ transcript: s.transcript,
3330
+ aiPartial: s.aiPartial,
3331
+ userPartial: s.userPartial,
3332
+ muted: s.muted,
2347
3333
  startCall,
2348
3334
  stopCall,
2349
- toggleMute,
2350
- handleWebViewMessage,
2351
- getWebViewSource,
2352
- webViewRef,
2353
- injectedTrackHook: INJECT_TRACK_HOOK
3335
+ toggleMute
2354
3336
  };
2355
3337
  }
2356
3338
 
2357
3339
  // src/voice/VoiceCallSheet.tsx
2358
- var import_jsx_runtime16 = require("react/jsx-runtime");
2359
- var fontFam7 = import_react_native15.Platform.OS === "ios" ? "SF Pro Rounded" : void 0;
3340
+ var import_jsx_runtime21 = require("react/jsx-runtime");
3341
+ var fontFam11 = import_react_native20.Platform.OS === "ios" ? "SF Pro Rounded" : void 0;
2360
3342
  var HIGHLIGHT_PATTERNS = [
2361
3343
  /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g,
2362
3344
  /(?:\+?\d[\s\-().]?){7,}\d/g,
@@ -2399,8 +3381,8 @@ function HighlightedText({
2399
3381
  baseStyle,
2400
3382
  importantStyle
2401
3383
  }) {
2402
- const segments = (0, import_react13.useMemo)(() => highlightSegments(text), [text]);
2403
- return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_react_native15.Text, { style: baseStyle, children: segments.map((seg, i) => /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_react_native15.Text, { style: seg.important ? importantStyle : void 0, children: seg.text }, i)) });
3384
+ const segments = (0, import_react17.useMemo)(() => highlightSegments(text), [text]);
3385
+ return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_react_native20.Text, { style: baseStyle, children: segments.map((seg, i) => /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_react_native20.Text, { style: seg.important ? importantStyle : void 0, children: seg.text }, i)) });
2404
3386
  }
2405
3387
  function VoiceCallSheet({ visible, onClose, skillSlug }) {
2406
3388
  const {
@@ -2412,31 +3394,27 @@ function VoiceCallSheet({ visible, onClose, skillSlug }) {
2412
3394
  muted,
2413
3395
  startCall,
2414
3396
  stopCall,
2415
- toggleMute,
2416
- handleWebViewMessage,
2417
- getWebViewSource,
2418
- webViewRef,
2419
- injectedTrackHook
3397
+ toggleMute
2420
3398
  } = useVoiceCall({ skillSlug });
2421
- const scrollRef = (0, import_react13.useRef)(null);
2422
- (0, import_react13.useEffect)(() => {
3399
+ const scrollRef = (0, import_react17.useRef)(null);
3400
+ (0, import_react17.useEffect)(() => {
2423
3401
  if (visible && callState === "idle") startCall();
2424
3402
  }, [visible]);
2425
- (0, import_react13.useEffect)(() => {
3403
+ (0, import_react17.useEffect)(() => {
2426
3404
  scrollRef.current?.scrollToEnd?.({ animated: true });
2427
3405
  }, [transcript.length, aiPartial, userPartial]);
2428
3406
  const handleHangUp = () => {
2429
3407
  stopCall();
2430
3408
  onClose();
2431
3409
  };
2432
- const orbState = (0, import_react13.useMemo)(() => {
3410
+ const orbState = (0, import_react17.useMemo)(() => {
2433
3411
  if (callState === "connecting") return "thinking";
2434
3412
  if (callState === "error") return "idle";
2435
3413
  if (aiPartial) return "speaking";
2436
3414
  if (userPartial) return "listening";
2437
3415
  return "idle";
2438
3416
  }, [callState, aiPartial, userPartial]);
2439
- const statusLabel = (0, import_react13.useMemo)(() => {
3417
+ const statusLabel = (0, import_react17.useMemo)(() => {
2440
3418
  if (callState === "connecting") return "Connecting\u2026";
2441
3419
  if (callState === "error") return error || "Couldn\u2019t connect";
2442
3420
  if (aiPartial) return "Ada is speaking";
@@ -2446,92 +3424,76 @@ function VoiceCallSheet({ visible, onClose, skillSlug }) {
2446
3424
  }, [callState, aiPartial, userPartial, error]);
2447
3425
  if (!visible) return null;
2448
3426
  const visibleEntries = transcript.filter((e) => e.role === "user" || e.role === "ai");
2449
- return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
2450
- import_plugin_runtime9.SurfaceFullscreen,
3427
+ return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(
3428
+ import_plugin_runtime14.SurfaceFullscreen,
2451
3429
  {
2452
3430
  visible,
2453
3431
  onClose: handleHangUp,
2454
3432
  status: statusLabel,
2455
- hero: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_plugin_runtime9.AdaOrb, { size: 180, state: orbState }),
3433
+ hero: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_plugin_runtime14.AdaOrb, { size: 180, state: orbState }),
2456
3434
  closeIcon: "hangup",
2457
3435
  closeLabel: "End",
2458
3436
  children: [
2459
- /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
2460
- import_react_native15.ScrollView,
3437
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(
3438
+ import_react_native20.ScrollView,
2461
3439
  {
2462
3440
  ref: scrollRef,
2463
- style: styles14.transcript,
2464
- contentContainerStyle: styles14.transcriptContent,
3441
+ style: styles19.transcript,
3442
+ contentContainerStyle: styles19.transcriptContent,
2465
3443
  showsVerticalScrollIndicator: false,
2466
3444
  children: [
2467
- callState === "error" && error ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_react_native15.View, { style: styles14.errorBox, children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_react_native15.Text, { style: styles14.errorText, children: error }) }) : null,
2468
- visibleEntries.map((entry, i) => /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_react_native15.View, { style: styles14.entry, children: [
2469
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_react_native15.Text, { style: styles14.role, children: entry.role === "user" ? "YOU" : "ADA" }),
2470
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3445
+ callState === "error" && error ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_react_native20.View, { style: styles19.errorBox, children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_react_native20.Text, { style: styles19.errorText, children: error }) }) : null,
3446
+ visibleEntries.map((entry, i) => /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(import_react_native20.View, { style: styles19.entry, children: [
3447
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_react_native20.Text, { style: styles19.role, children: entry.role === "user" ? "YOU" : "ADA" }),
3448
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
2471
3449
  HighlightedText,
2472
3450
  {
2473
3451
  text: entry.text,
2474
- baseStyle: styles14.entryText,
2475
- importantStyle: styles14.important
3452
+ baseStyle: styles19.entryText,
3453
+ importantStyle: styles19.important
2476
3454
  }
2477
3455
  )
2478
3456
  ] }, `${entry.ts}-${i}`)),
2479
- userPartial || aiPartial ? /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_react_native15.View, { style: styles14.entry, children: [
2480
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_react_native15.Text, { style: styles14.role, children: userPartial ? "YOU" : "ADA" }),
2481
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3457
+ userPartial || aiPartial ? /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(import_react_native20.View, { style: styles19.entry, children: [
3458
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_react_native20.Text, { style: styles19.role, children: userPartial ? "YOU" : "ADA" }),
3459
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
2482
3460
  HighlightedText,
2483
3461
  {
2484
3462
  text: userPartial || aiPartial,
2485
- baseStyle: styles14.entryTextLive,
2486
- importantStyle: styles14.important
3463
+ baseStyle: styles19.entryTextLive,
3464
+ importantStyle: styles19.important
2487
3465
  }
2488
3466
  )
2489
3467
  ] }) : null
2490
3468
  ]
2491
3469
  }
2492
3470
  ),
2493
- callState === "connected" || callState === "connecting" ? /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_react_native15.View, { style: styles14.muteWrap, pointerEvents: "box-none", children: [
2494
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
2495
- import_react_native15.Pressable,
3471
+ callState === "connected" || callState === "connecting" ? /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(import_react_native20.View, { style: styles19.muteWrap, pointerEvents: "box-none", children: [
3472
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
3473
+ import_react_native20.Pressable,
2496
3474
  {
2497
3475
  onPress: toggleMute,
2498
3476
  accessibilityRole: "button",
2499
3477
  accessibilityLabel: muted ? "Unmute microphone" : "Mute microphone",
2500
3478
  accessibilityState: { selected: muted },
2501
- style: [styles14.muteBtn, muted && styles14.muteBtnActive],
3479
+ style: [styles19.muteBtn, muted && styles19.muteBtnActive],
2502
3480
  hitSlop: 12,
2503
- children: /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_react_native_svg2.default, { width: 22, height: 22, viewBox: "0 0 24 24", fill: "none", stroke: muted ? import_core18.ada.paper : import_core18.ada.ink, strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", children: [
2504
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_react_native_svg2.Path, { d: "M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z" }),
2505
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_react_native_svg2.Path, { d: "M19 10v2a7 7 0 0 1-14 0v-2" }),
2506
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_react_native_svg2.Line, { x1: "12", y1: "19", x2: "12", y2: "23" }),
2507
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_react_native_svg2.Line, { x1: "8", y1: "23", x2: "16", y2: "23" }),
2508
- muted ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_react_native_svg2.Line, { x1: "3", y1: "3", x2: "21", y2: "21", stroke: import_core18.ada.paper, strokeWidth: 2.4 }) : null
3481
+ children: /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(import_react_native_svg2.default, { width: 22, height: 22, viewBox: "0 0 24 24", fill: "none", stroke: muted ? import_core23.ada.paper : import_core23.ada.ink, strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", children: [
3482
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_react_native_svg2.Path, { d: "M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z" }),
3483
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_react_native_svg2.Path, { d: "M19 10v2a7 7 0 0 1-14 0v-2" }),
3484
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_react_native_svg2.Line, { x1: "12", y1: "19", x2: "12", y2: "23" }),
3485
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_react_native_svg2.Line, { x1: "8", y1: "23", x2: "16", y2: "23" }),
3486
+ muted ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_react_native_svg2.Line, { x1: "3", y1: "3", x2: "21", y2: "21", stroke: import_core23.ada.paper, strokeWidth: 2.4 }) : null
2509
3487
  ] })
2510
3488
  }
2511
3489
  ),
2512
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_react_native15.Text, { style: styles14.muteLabel, children: muted ? "Muted" : "Mute" })
2513
- ] }) : null,
2514
- callState !== "idle" && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_react_native15.View, { style: styles14.hiddenWebView, pointerEvents: "none", children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
2515
- import_react_native_webview3.WebView,
2516
- {
2517
- ref: webViewRef,
2518
- source: getWebViewSource(),
2519
- style: { flex: 1, backgroundColor: "transparent" },
2520
- onMessage: handleWebViewMessage,
2521
- onError: (e) => console.log("[VoiceWebView] error:", e.nativeEvent?.description),
2522
- injectedJavaScriptBeforeContentLoaded: injectedTrackHook,
2523
- mediaPlaybackRequiresUserAction: false,
2524
- allowsInlineMediaPlayback: true,
2525
- mediaCapturePermissionGrantType: "grant",
2526
- javaScriptEnabled: true,
2527
- originWhitelist: ["*"]
2528
- }
2529
- ) })
3490
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_react_native20.Text, { style: styles19.muteLabel, children: muted ? "Muted" : "Mute" })
3491
+ ] }) : null
2530
3492
  ]
2531
3493
  }
2532
3494
  );
2533
3495
  }
2534
- var styles14 = import_react_native15.StyleSheet.create({
3496
+ var styles19 = import_react_native20.StyleSheet.create({
2535
3497
  transcript: {
2536
3498
  flex: 1,
2537
3499
  paddingHorizontal: 28
@@ -2548,9 +3510,9 @@ var styles14 = import_react_native15.StyleSheet.create({
2548
3510
  },
2549
3511
  role: {
2550
3512
  fontSize: 10,
2551
- fontFamily: fontFam7,
3513
+ fontFamily: fontFam11,
2552
3514
  fontWeight: "700",
2553
- color: import_core18.ada.inkMuted,
3515
+ color: import_core23.ada.inkMuted,
2554
3516
  letterSpacing: 1.4,
2555
3517
  minWidth: 32,
2556
3518
  paddingTop: 3,
@@ -2559,9 +3521,9 @@ var styles14 = import_react_native15.StyleSheet.create({
2559
3521
  entryText: {
2560
3522
  flex: 1,
2561
3523
  fontSize: 14,
2562
- fontFamily: fontFam7,
3524
+ fontFamily: fontFam11,
2563
3525
  fontWeight: "400",
2564
- color: import_core18.ada.inkSoft,
3526
+ color: import_core23.ada.inkSoft,
2565
3527
  lineHeight: 20,
2566
3528
  letterSpacing: -0.1,
2567
3529
  opacity: 0.42
@@ -2569,18 +3531,18 @@ var styles14 = import_react_native15.StyleSheet.create({
2569
3531
  entryTextLive: {
2570
3532
  flex: 1,
2571
3533
  fontSize: 14,
2572
- fontFamily: fontFam7,
3534
+ fontFamily: fontFam11,
2573
3535
  fontWeight: "400",
2574
- color: import_core18.ada.inkSoft,
3536
+ color: import_core23.ada.inkSoft,
2575
3537
  lineHeight: 20,
2576
3538
  letterSpacing: -0.1,
2577
3539
  opacity: 0.7
2578
3540
  },
2579
3541
  important: {
2580
3542
  fontSize: 17,
2581
- fontFamily: fontFam7,
3543
+ fontFamily: fontFam11,
2582
3544
  fontWeight: "700",
2583
- color: import_core18.ada.ink,
3545
+ color: import_core23.ada.ink,
2584
3546
  letterSpacing: -0.1,
2585
3547
  opacity: 1
2586
3548
  },
@@ -2595,14 +3557,14 @@ var styles14 = import_react_native15.StyleSheet.create({
2595
3557
  },
2596
3558
  errorText: {
2597
3559
  fontSize: 13,
2598
- fontFamily: fontFam7,
3560
+ fontFamily: fontFam11,
2599
3561
  fontWeight: "500",
2600
3562
  color: "#fca5a5",
2601
3563
  lineHeight: 18
2602
3564
  },
2603
3565
  muteWrap: {
2604
3566
  position: "absolute",
2605
- bottom: import_react_native15.Platform.OS === "ios" ? 56 : 40,
3567
+ bottom: import_react_native20.Platform.OS === "ios" ? 56 : 40,
2606
3568
  left: 28,
2607
3569
  alignItems: "center",
2608
3570
  gap: 6
@@ -2611,9 +3573,9 @@ var styles14 = import_react_native15.StyleSheet.create({
2611
3573
  width: 56,
2612
3574
  height: 56,
2613
3575
  borderRadius: 28,
2614
- backgroundColor: import_core18.ada.paperSoft,
3576
+ backgroundColor: import_core23.ada.paperSoft,
2615
3577
  borderWidth: 1,
2616
- borderColor: import_core18.ada.border,
3578
+ borderColor: import_core23.ada.border,
2617
3579
  justifyContent: "center",
2618
3580
  alignItems: "center"
2619
3581
  },
@@ -2623,18 +3585,133 @@ var styles14 = import_react_native15.StyleSheet.create({
2623
3585
  },
2624
3586
  muteLabel: {
2625
3587
  fontSize: 12,
2626
- fontFamily: fontFam7,
3588
+ fontFamily: fontFam11,
2627
3589
  fontWeight: "500",
2628
- color: import_core18.ada.inkMuted,
3590
+ color: import_core23.ada.inkMuted,
2629
3591
  letterSpacing: 0.3
2630
- },
2631
- hiddenWebView: {
3592
+ }
3593
+ });
3594
+
3595
+ // src/voice/VoiceSessionHost.tsx
3596
+ var import_react18 = require("react");
3597
+ var import_react_native21 = require("react-native");
3598
+ var import_react_native_webview3 = require("react-native-webview");
3599
+ var import_jsx_runtime22 = require("react/jsx-runtime");
3600
+ function VoiceSessionHost() {
3601
+ const [, force] = (0, import_react18.useReducer)((x) => x + 1, 0);
3602
+ const lastSig = (0, import_react18.useRef)("");
3603
+ (0, import_react18.useEffect)(() => {
3604
+ const sub = voiceSession.subscribe(() => {
3605
+ const s2 = voiceSession.getInternal();
3606
+ const sig = `${s2.mounted}|${s2.webViewSource?.uri || ""}`;
3607
+ if (sig !== lastSig.current) {
3608
+ lastSig.current = sig;
3609
+ force();
3610
+ }
3611
+ });
3612
+ return () => sub.unsubscribe();
3613
+ }, []);
3614
+ const setRef = (0, import_react18.useCallback)((r) => {
3615
+ voiceSession.setWebView(r);
3616
+ }, []);
3617
+ const s = voiceSession.getInternal();
3618
+ if (!s.mounted || !s.webViewSource) return null;
3619
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_react_native21.View, { style: styles20.hidden, pointerEvents: "none", children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
3620
+ import_react_native_webview3.WebView,
3621
+ {
3622
+ ref: setRef,
3623
+ source: s.webViewSource,
3624
+ style: styles20.web,
3625
+ onMessage: (e) => voiceSession.handleMessage(e),
3626
+ onError: (e) => console.log("[VoiceSessionHost] error:", e.nativeEvent?.description),
3627
+ injectedJavaScriptBeforeContentLoaded: voiceSession.injectedTrackHook,
3628
+ mediaPlaybackRequiresUserAction: false,
3629
+ allowsInlineMediaPlayback: true,
3630
+ mediaCapturePermissionGrantType: "grant",
3631
+ javaScriptEnabled: true,
3632
+ originWhitelist: ["*"]
3633
+ }
3634
+ ) });
3635
+ }
3636
+ var styles20 = import_react_native21.StyleSheet.create({
3637
+ hidden: {
2632
3638
  position: "absolute",
2633
3639
  width: 1,
2634
3640
  height: 1,
2635
3641
  top: -100,
2636
3642
  left: -100,
2637
3643
  opacity: 0
3644
+ },
3645
+ web: { flex: 1, backgroundColor: "transparent" }
3646
+ });
3647
+
3648
+ // src/voice/VoiceMicIndicator.tsx
3649
+ var import_react19 = require("react");
3650
+ var import_react_native22 = require("react-native");
3651
+ var import_core24 = require("@ateam-ai-mobile/core");
3652
+ var import_jsx_runtime23 = require("react/jsx-runtime");
3653
+ var fontFam12 = import_react_native22.Platform.OS === "ios" ? "SF Pro Rounded" : void 0;
3654
+ function VoiceMicIndicator({ topInset = 8 }) {
3655
+ const [, force] = (0, import_react19.useReducer)((x) => x + 1, 0);
3656
+ const pulse = (0, import_react19.useRef)(new import_react_native22.Animated.Value(0.4)).current;
3657
+ (0, import_react19.useEffect)(() => {
3658
+ const sub = voiceSession.subscribe(() => force());
3659
+ return () => sub.unsubscribe();
3660
+ }, []);
3661
+ const snap = voiceSession.getSnapshot();
3662
+ const live = snap.state === "connecting" || snap.state === "connected";
3663
+ (0, import_react19.useEffect)(() => {
3664
+ if (!live) return;
3665
+ const loop = import_react_native22.Animated.loop(
3666
+ import_react_native22.Animated.sequence([
3667
+ import_react_native22.Animated.timing(pulse, { toValue: 1, duration: 700, useNativeDriver: true }),
3668
+ import_react_native22.Animated.timing(pulse, { toValue: 0.4, duration: 700, useNativeDriver: true })
3669
+ ])
3670
+ );
3671
+ loop.start();
3672
+ return () => loop.stop();
3673
+ }, [live, pulse]);
3674
+ if (!live) return null;
3675
+ const label = snap.state === "connecting" ? "Connecting\u2026" : snap.muted ? "Mic muted" : "Ada is listening";
3676
+ const dotColor = snap.muted ? import_core24.ada.inkMuted : "#E5484D";
3677
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_react_native22.View, { style: [styles21.wrap, { top: topInset }], pointerEvents: "none", children: /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)(import_react_native22.View, { style: styles21.pill, children: [
3678
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_react_native22.Animated.View, { style: [styles21.dot, { backgroundColor: dotColor, opacity: snap.muted ? 1 : pulse }] }),
3679
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_react_native22.Text, { style: styles21.label, children: label })
3680
+ ] }) });
3681
+ }
3682
+ var styles21 = import_react_native22.StyleSheet.create({
3683
+ wrap: {
3684
+ position: "absolute",
3685
+ left: 0,
3686
+ right: 0,
3687
+ alignItems: "center",
3688
+ zIndex: 9999,
3689
+ elevation: 9999
3690
+ },
3691
+ pill: {
3692
+ flexDirection: "row",
3693
+ alignItems: "center",
3694
+ gap: 7,
3695
+ paddingHorizontal: 12,
3696
+ paddingVertical: 6,
3697
+ borderRadius: 999,
3698
+ backgroundColor: import_core24.ada.ink,
3699
+ shadowColor: "#000",
3700
+ shadowOpacity: 0.2,
3701
+ shadowRadius: 8,
3702
+ shadowOffset: { width: 0, height: 2 }
3703
+ },
3704
+ dot: {
3705
+ width: 8,
3706
+ height: 8,
3707
+ borderRadius: 4
3708
+ },
3709
+ label: {
3710
+ fontSize: 12,
3711
+ fontFamily: fontFam12,
3712
+ fontWeight: "600",
3713
+ color: import_core24.ada.paper,
3714
+ letterSpacing: 0.2
2638
3715
  }
2639
3716
  });
2640
3717
  // Annotate the CommonJS export names for ESM import in node:
@@ -2643,9 +3720,13 @@ var styles14 = import_react_native15.StyleSheet.create({
2643
3720
  ChatInput,
2644
3721
  DaySeparator,
2645
3722
  EmbeddedPluginWebView,
3723
+ HandoffScreen,
2646
3724
  JobProgressBar,
3725
+ JoinScreen,
2647
3726
  Lightbox,
3727
+ LoginScreen,
2648
3728
  MessageBubble,
3729
+ OpenJoinScreen,
2649
3730
  Plugin,
2650
3731
  PluginSurface,
2651
3732
  TableCardBlock,
@@ -2653,16 +3734,30 @@ var styles14 = import_react_native15.StyleSheet.create({
2653
3734
  TypingIndicator,
2654
3735
  UIActionsRow,
2655
3736
  VoiceCallSheet,
3737
+ VoiceMicIndicator,
3738
+ VoiceSessionHost,
2656
3739
  WidgetHost,
2657
3740
  WidgetWebView,
3741
+ WorkspaceBanner,
2658
3742
  buildPluginBridgeJS,
3743
+ completeInviteJoin,
3744
+ completeOpenJoin,
3745
+ completeProviderSignIn,
3746
+ googleClientIds,
3747
+ handoffErrorMessage,
3748
+ inviteHeadline,
3749
+ openJoinErrorMessage,
2659
3750
  openWidget,
3751
+ runHandoffExchange,
2660
3752
  setClipboardProvider,
3753
+ setOAuthProvider,
2661
3754
  setPluginWebViewExtraHosts,
2662
3755
  setUIActionNavigator,
3756
+ shouldRecoverToDefaultTenant,
2663
3757
  useLightboxStore,
2664
3758
  usePluginBridge,
2665
3759
  useVoiceCall,
2666
- useWidgetHostStore
3760
+ useWidgetHostStore,
3761
+ voiceSession
2667
3762
  });
2668
3763
  //# sourceMappingURL=index.js.map