@ateam-ai-mobile/ui 0.1.5 → 0.1.7

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,413 @@ 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
+ mounted: false,
2841
+ webViewSource: null,
2842
+ skillSlug: void 0
2843
+ };
2844
+ var VoiceSessionManager = class {
2845
+ constructor() {
2846
+ this.state = { ...IDLE };
2847
+ this.listeners = /* @__PURE__ */ new Set();
2848
+ this.webView = null;
2849
+ this.pending = [];
2850
+ /**
2851
+ * WHO owns the one physical session. Set to the caller that started it from
2852
+ * idle (a plugin's pluginId, or the PLATFORM symbol for the built-in sheet),
2853
+ * and kept after stop until the next start reassigns it — so a plugin can read
2854
+ * its own just-ended transcript but another plugin never can.
2855
+ *
2856
+ * ISOLATION (GPT blocker on 8881225): every access is scoped through this. A
2857
+ * different plugin must not read, control, or attach to another plugin's live
2858
+ * conversation just because it declared voice.
2859
+ */
2860
+ this.owner = null;
2861
+ this.injectedTrackHook = INJECT_TRACK_HOOK;
2862
+ }
2863
+ /** The platform (built-in sheet, mic indicator, session host) sees and
2864
+ * controls any session — it is the user's own surface. A plugin caller may
2865
+ * only touch a session it owns. */
2866
+ canAccess(caller) {
2867
+ return caller === import_plugin_runtime13.PLATFORM_VOICE_CALLER || this.owner === caller;
2868
+ }
2869
+ foreignError() {
2870
+ const err = new Error(
2871
+ "voice: another solution is using the microphone \u2014 one live session per device."
2872
+ );
2873
+ err.code = "RESTRICTED";
2874
+ return err;
2875
+ }
2876
+ // ── Host wiring (used only by VoiceSessionHost) ──────────────
2877
+ /** The offscreen WebView sets its ref here so the session can inject into it. */
2878
+ setWebView(ref) {
2879
+ this.webView = ref;
2880
+ }
2881
+ /** Whole state — the host reads `mounted` + `webViewSource` to decide what to
2882
+ * render, and every reader derives its snapshot from this. */
2883
+ getInternal() {
2884
+ return this.state;
2885
+ }
2886
+ // ── VoiceAPI (what props.voice and useVoiceCall call) ────────
2887
+ async start(opts, caller = import_plugin_runtime13.PLATFORM_VOICE_CALLER) {
2888
+ if (this.state.callState === "connected") {
2889
+ if (!this.canAccess(caller)) throw this.foreignError();
2890
+ return this.realSnapshot();
2891
+ }
2892
+ if (this.state.callState === "connecting") {
2893
+ if (!this.canAccess(caller)) throw this.foreignError();
2894
+ return this.waitForConnect();
2895
+ }
2272
2896
  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("");
2294
- break;
2295
- case "error":
2296
- setError(data.message);
2297
- setCallState("error");
2897
+ await (0, import_plugin_runtime13.ensureDeviceToolsLoaded)();
2898
+ } catch (e) {
2899
+ console.warn("[voiceSession] device tool preload failed:", e?.message || String(e));
2900
+ }
2901
+ try {
2902
+ if (tokenNeedsRefresh((0, import_core22.getToken)())) await (0, import_core22.refreshToken)();
2903
+ } catch {
2904
+ }
2905
+ this.owner = caller;
2906
+ const skillSlug = opts?.skillSlug;
2907
+ this.setState({
2908
+ callState: "connecting",
2909
+ error: null,
2910
+ transcript: [],
2911
+ aiPartial: "",
2912
+ userPartial: "",
2913
+ muted: false,
2914
+ mounted: true,
2915
+ skillSlug,
2916
+ webViewSource: { uri: this.buildUri(skillSlug) }
2917
+ });
2918
+ return this.waitForConnect();
2919
+ }
2920
+ stop(caller = import_plugin_runtime13.PLATFORM_VOICE_CALLER) {
2921
+ if (this.state.callState !== "idle" && !this.canAccess(caller)) throw this.foreignError();
2922
+ this.webView?.injectJavaScript?.("window.hangUp && window.hangUp(); true;");
2923
+ this.rejectPending(new Error("voice: session stopped before it connected"));
2924
+ this.setState({
2925
+ ...IDLE,
2926
+ // keep the transcript of the call that just ended for any reader still up
2927
+ transcript: this.state.transcript
2928
+ });
2929
+ }
2930
+ setMuted(muted, caller = import_plugin_runtime13.PLATFORM_VOICE_CALLER) {
2931
+ if (this.state.callState !== "idle" && !this.canAccess(caller)) throw this.foreignError();
2932
+ this.webView?.injectJavaScript?.(buildMuteScript(muted));
2933
+ this.setState({ muted });
2934
+ }
2935
+ prompt(input, caller = import_plugin_runtime13.PLATFORM_VOICE_CALLER) {
2936
+ if (this.state.callState !== "connected") {
2937
+ throw new Error("voice: session is not connected \u2014 call start() and await it before prompt().");
2938
+ }
2939
+ if (!this.canAccess(caller)) throw this.foreignError();
2940
+ this.webView?.injectJavaScript?.(buildAgentTurnScript(input));
2941
+ }
2942
+ /**
2943
+ * Stream a silent observation into the live session — tell the model without
2944
+ * asking it to speak. The cheap, high-frequency channel (safe once or twice a
2945
+ * second) behind a future props.voice.observe(); today the host uses it to run
2946
+ * the real-device stream proof (VOICE_RT_STREAM_DEVICE_PROOF) before the gated
2947
+ * plugin API is added. Owner-scoped like every other control.
2948
+ */
2949
+ observe(text, caller = import_plugin_runtime13.PLATFORM_VOICE_CALLER) {
2950
+ if (this.state.callState !== "connected") {
2951
+ throw new Error("voice: session is not connected \u2014 observe() needs a live session.");
2952
+ }
2953
+ if (!this.canAccess(caller)) throw this.foreignError();
2954
+ this.webView?.injectJavaScript?.(buildObserveScript(text));
2955
+ }
2956
+ getSnapshot(caller = import_plugin_runtime13.PLATFORM_VOICE_CALLER) {
2957
+ return this.canAccess(caller) ? this.realSnapshot() : this.foreignSnapshot();
2958
+ }
2959
+ realSnapshot() {
2960
+ const s = this.state;
2961
+ return {
2962
+ state: s.callState,
2963
+ sessionId: s.sessionId,
2964
+ muted: s.muted,
2965
+ speaking: !!s.aiPartial,
2966
+ listening: !!s.userPartial,
2967
+ userSpeaking: s.userSpeaking,
2968
+ lastUserSpeechEndedAt: s.lastUserSpeechEndedAt,
2969
+ lastError: s.error
2970
+ };
2971
+ }
2972
+ foreignSnapshot() {
2973
+ return {
2974
+ state: "idle",
2975
+ sessionId: null,
2976
+ muted: false,
2977
+ speaking: false,
2978
+ listening: false,
2979
+ userSpeaking: false,
2980
+ lastUserSpeechEndedAt: null,
2981
+ lastError: null
2982
+ };
2983
+ }
2984
+ subscribe(cb, caller = import_plugin_runtime13.PLATFORM_VOICE_CALLER) {
2985
+ const wrapped = (event) => {
2986
+ if (this.canAccess(caller)) cb(event);
2987
+ };
2988
+ this.listeners.add(wrapped);
2989
+ return { unsubscribe: () => {
2990
+ this.listeners.delete(wrapped);
2991
+ } };
2992
+ }
2993
+ transcript(caller = import_plugin_runtime13.PLATFORM_VOICE_CALLER) {
2994
+ return this.canAccess(caller) ? this.state.transcript.slice() : [];
2995
+ }
2996
+ // ── WebView plumbing ─────────────────────────────────────────
2997
+ buildUri(skillSlug) {
2998
+ const token = (0, import_core22.getToken)();
2999
+ const tenant = (0, import_core22.getTenant)();
3000
+ const actorId = (0, import_core22.getUser)()?.actorId || "";
3001
+ const skillParam = skillSlug ? `&skillSlug=${encodeURIComponent(skillSlug)}` : "";
3002
+ return `${(0, import_core22.getVoiceApiUrl)()}/app?mode=mini&embedded=rn&authToken=${encodeURIComponent(token || "")}&tenant=${encodeURIComponent(tenant)}&actorId=${encodeURIComponent(actorId)}${skillParam}`;
3003
+ }
3004
+ /** Handle a postMessage from the WebView. `event` is the RN WebView event. */
3005
+ handleMessage(event) {
3006
+ let data;
3007
+ try {
3008
+ data = JSON.parse(event.nativeEvent.data);
3009
+ } catch {
3010
+ return;
3011
+ }
3012
+ switch (data.type) {
3013
+ case "callState":
3014
+ case "call-state":
3015
+ if (data.state) this.setState({ callState: data.state });
3016
+ if (data.sessionId) this.setState({ sessionId: data.sessionId });
3017
+ if (data.error) this.setState({ error: data.error });
3018
+ if (data.state === "connected") this.resolvePending();
3019
+ if (data.state === "error") this.rejectPending(new Error(data.error || "voice: connection error"));
3020
+ break;
3021
+ case "transcript":
3022
+ this.appendTranscript({ role: data.role, text: data.text, ts: Date.now() });
3023
+ break;
3024
+ case "aiPartial":
3025
+ this.setState({ aiPartial: data.text });
3026
+ break;
3027
+ case "userPartial":
3028
+ this.setState({ userPartial: data.text });
3029
+ break;
3030
+ case "clearPartials":
3031
+ this.setState({ aiPartial: "", userPartial: "" });
3032
+ break;
3033
+ // User VAD from the mini-app. `at` is when it happened; on speaking:false
3034
+ // we stamp lastUserSpeechEndedAt so a solution can gate "speak after N s of
3035
+ // silence" on the first real stop, never before the user has spoken.
3036
+ case "speechActivity": {
3037
+ const speaking = !!data.speaking;
3038
+ const at = typeof data.at === "number" ? data.at : Date.now();
3039
+ this.setState(
3040
+ speaking ? { userSpeaking: true } : { userSpeaking: false, lastUserSpeechEndedAt: at }
3041
+ );
3042
+ break;
3043
+ }
3044
+ case "error":
3045
+ this.setState({ error: data.message, callState: "error" });
3046
+ this.rejectPending(new Error(data.message || "voice: error"));
3047
+ break;
3048
+ // A proactive turn couldn't run (older voice page / hook threw). NON-FATAL:
3049
+ // the session stays live; we only surface why the guide didn't speak.
3050
+ case "agentTurnUnsupported":
3051
+ case "agentTurnFailed":
3052
+ case "observeUnsupported":
3053
+ case "observeFailed":
3054
+ this.setState({ error: data.message || "voice: channel notice" });
3055
+ break;
3056
+ // Device tools — executed locally, origin-checked. The model's function
3057
+ // call already arrived on this phone; only the voice app's own origin may
3058
+ // drive the camera/GPS.
3059
+ case "device-tool-list": {
3060
+ const listSender = originOf(event?.nativeEvent?.url);
3061
+ const listExpected = originOf((0, import_core22.getVoiceApiUrl)());
3062
+ if (!listSender || !listExpected || listSender !== listExpected) break;
3063
+ this.sendDeviceToolList(data.correlationId);
3064
+ break;
3065
+ }
3066
+ case "device-tool-call": {
3067
+ const sender = originOf(event?.nativeEvent?.url);
3068
+ const expected = originOf((0, import_core22.getVoiceApiUrl)());
3069
+ if (!sender || !expected || sender !== expected) {
3070
+ console.warn(`[voiceSession] REFUSED device tool call from "${sender || "unknown origin"}" (expected "${expected}")`);
2298
3071
  break;
3072
+ }
3073
+ void this.runDeviceTool(data);
3074
+ break;
2299
3075
  }
2300
- } catch {
2301
3076
  }
2302
- }, []);
2303
- const startCall = (0, import_react12.useCallback)(async () => {
2304
- setError(null);
2305
- setTranscript([]);
2306
- setAiPartial("");
2307
- setUserPartial("");
3077
+ }
3078
+ /** Answer "what device tools do you have?" — the client is the authority. */
3079
+ sendDeviceToolList(correlationId) {
3080
+ const tools = (0, import_plugin_runtime13.listDeviceTools)().map((t) => ({
3081
+ name: t.name,
3082
+ description: t.description,
3083
+ input_schema: t.input_schema
3084
+ }));
3085
+ this.webView?.injectJavaScript?.(
3086
+ `window.__adasDeviceToolResult && window.__adasDeviceToolResult(${JSON.stringify(
3087
+ JSON.stringify({ correlationId, ok: true, tools })
3088
+ )}); true;`
3089
+ );
3090
+ }
3091
+ /** Run a device tool and post the outcome back into the WebView. */
3092
+ async runDeviceTool(data) {
3093
+ const { correlationId, name, args, timeoutMs } = data || {};
3094
+ if (!correlationId || typeof name !== "string") return;
3095
+ let payload;
2308
3096
  try {
2309
- if (tokenNeedsRefresh((0, import_core17.getToken)())) {
2310
- await (0, import_core17.refreshToken)();
3097
+ const outcome = await (0, import_plugin_runtime13.callDeviceTool)(name, args || {}, { timeoutMs });
3098
+ payload = outcome.ok ? { correlationId, ok: true, result: outcome.result } : { correlationId, ok: false, error: outcome.error, reason: outcome.reason };
3099
+ } catch (err) {
3100
+ payload = { correlationId, ok: false, reason: "failed", error: err?.message || String(err) };
3101
+ }
3102
+ this.webView?.injectJavaScript?.(
3103
+ `window.__adasDeviceToolResult && window.__adasDeviceToolResult(${JSON.stringify(
3104
+ JSON.stringify(payload)
3105
+ )}); true;`
3106
+ );
3107
+ }
3108
+ // ── internals ────────────────────────────────────────────────
3109
+ setState(partial) {
3110
+ this.state = { ...this.state, ...partial };
3111
+ this.emit({ type: "state" });
3112
+ }
3113
+ appendTranscript(entry) {
3114
+ this.state = { ...this.state, transcript: [...this.state.transcript, entry] };
3115
+ this.emit({ type: "transcript" });
3116
+ }
3117
+ emit(event) {
3118
+ for (const cb of this.listeners) {
3119
+ try {
3120
+ cb(event);
3121
+ } catch (e) {
3122
+ console.warn("[voiceSession] listener threw", e);
2311
3123
  }
2312
- } catch {
2313
3124
  }
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;
3125
+ }
3126
+ waitForConnect() {
3127
+ return new Promise((resolve, reject) => {
3128
+ const timer = setTimeout(() => {
3129
+ this.pending = this.pending.filter((p) => p.timer !== timer);
3130
+ reject(new Error("voice: timed out waiting to connect (is VoiceSessionHost mounted?)"));
3131
+ }, CONNECT_TIMEOUT_MS);
3132
+ this.pending.push({ resolve, reject, timer });
2328
3133
  });
3134
+ }
3135
+ resolvePending() {
3136
+ const snap = this.realSnapshot();
3137
+ const waiting = this.pending;
3138
+ this.pending = [];
3139
+ for (const p of waiting) {
3140
+ clearTimeout(p.timer);
3141
+ p.resolve(snap);
3142
+ }
3143
+ }
3144
+ rejectPending(err) {
3145
+ const waiting = this.pending;
3146
+ this.pending = [];
3147
+ for (const p of waiting) {
3148
+ clearTimeout(p.timer);
3149
+ p.reject(err);
3150
+ }
3151
+ }
3152
+ /** TEST ONLY: reset the singleton to a clean idle state (mirrors the
3153
+ * __resetSessionSyncForTest precedent). Not part of any surface. */
3154
+ __resetForTest() {
3155
+ this.rejectPending(new Error("voice: reset"));
3156
+ this.state = { ...IDLE };
3157
+ this.owner = null;
3158
+ this.listeners.clear();
3159
+ this.webView = null;
3160
+ }
3161
+ };
3162
+ var voiceSession = new VoiceSessionManager();
3163
+
3164
+ // src/voice/useVoiceCall.ts
3165
+ function useVoiceCall({ skillSlug } = {}) {
3166
+ const [, force] = (0, import_react16.useReducer)((x) => x + 1, 0);
3167
+ (0, import_react16.useEffect)(() => {
3168
+ const sub = voiceSession.subscribe(() => force());
3169
+ return () => sub.unsubscribe();
2329
3170
  }, []);
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]);
3171
+ const s = voiceSession.getInternal();
3172
+ const startCall = (0, import_react16.useCallback)(
3173
+ (opts) => voiceSession.start(opts ?? { skillSlug }),
3174
+ [skillSlug]
3175
+ );
3176
+ const stopCall = (0, import_react16.useCallback)(() => voiceSession.stop(), []);
3177
+ const toggleMute = (0, import_react16.useCallback)(
3178
+ () => voiceSession.setMuted(!voiceSession.getSnapshot().muted),
3179
+ []
3180
+ );
2339
3181
  return {
2340
- callState,
2341
- sessionId,
2342
- error,
2343
- transcript,
2344
- aiPartial,
2345
- userPartial,
2346
- muted,
3182
+ callState: s.callState,
3183
+ sessionId: s.sessionId,
3184
+ error: s.error,
3185
+ transcript: s.transcript,
3186
+ aiPartial: s.aiPartial,
3187
+ userPartial: s.userPartial,
3188
+ muted: s.muted,
2347
3189
  startCall,
2348
3190
  stopCall,
2349
- toggleMute,
2350
- handleWebViewMessage,
2351
- getWebViewSource,
2352
- webViewRef,
2353
- injectedTrackHook: INJECT_TRACK_HOOK
3191
+ toggleMute
2354
3192
  };
2355
3193
  }
2356
3194
 
2357
3195
  // 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;
3196
+ var import_jsx_runtime21 = require("react/jsx-runtime");
3197
+ var fontFam11 = import_react_native20.Platform.OS === "ios" ? "SF Pro Rounded" : void 0;
2360
3198
  var HIGHLIGHT_PATTERNS = [
2361
3199
  /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g,
2362
3200
  /(?:\+?\d[\s\-().]?){7,}\d/g,
@@ -2399,8 +3237,8 @@ function HighlightedText({
2399
3237
  baseStyle,
2400
3238
  importantStyle
2401
3239
  }) {
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)) });
3240
+ const segments = (0, import_react17.useMemo)(() => highlightSegments(text), [text]);
3241
+ 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
3242
  }
2405
3243
  function VoiceCallSheet({ visible, onClose, skillSlug }) {
2406
3244
  const {
@@ -2412,31 +3250,27 @@ function VoiceCallSheet({ visible, onClose, skillSlug }) {
2412
3250
  muted,
2413
3251
  startCall,
2414
3252
  stopCall,
2415
- toggleMute,
2416
- handleWebViewMessage,
2417
- getWebViewSource,
2418
- webViewRef,
2419
- injectedTrackHook
3253
+ toggleMute
2420
3254
  } = useVoiceCall({ skillSlug });
2421
- const scrollRef = (0, import_react13.useRef)(null);
2422
- (0, import_react13.useEffect)(() => {
3255
+ const scrollRef = (0, import_react17.useRef)(null);
3256
+ (0, import_react17.useEffect)(() => {
2423
3257
  if (visible && callState === "idle") startCall();
2424
3258
  }, [visible]);
2425
- (0, import_react13.useEffect)(() => {
3259
+ (0, import_react17.useEffect)(() => {
2426
3260
  scrollRef.current?.scrollToEnd?.({ animated: true });
2427
3261
  }, [transcript.length, aiPartial, userPartial]);
2428
3262
  const handleHangUp = () => {
2429
3263
  stopCall();
2430
3264
  onClose();
2431
3265
  };
2432
- const orbState = (0, import_react13.useMemo)(() => {
3266
+ const orbState = (0, import_react17.useMemo)(() => {
2433
3267
  if (callState === "connecting") return "thinking";
2434
3268
  if (callState === "error") return "idle";
2435
3269
  if (aiPartial) return "speaking";
2436
3270
  if (userPartial) return "listening";
2437
3271
  return "idle";
2438
3272
  }, [callState, aiPartial, userPartial]);
2439
- const statusLabel = (0, import_react13.useMemo)(() => {
3273
+ const statusLabel = (0, import_react17.useMemo)(() => {
2440
3274
  if (callState === "connecting") return "Connecting\u2026";
2441
3275
  if (callState === "error") return error || "Couldn\u2019t connect";
2442
3276
  if (aiPartial) return "Ada is speaking";
@@ -2446,92 +3280,76 @@ function VoiceCallSheet({ visible, onClose, skillSlug }) {
2446
3280
  }, [callState, aiPartial, userPartial, error]);
2447
3281
  if (!visible) return null;
2448
3282
  const visibleEntries = transcript.filter((e) => e.role === "user" || e.role === "ai");
2449
- return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
2450
- import_plugin_runtime9.SurfaceFullscreen,
3283
+ return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(
3284
+ import_plugin_runtime14.SurfaceFullscreen,
2451
3285
  {
2452
3286
  visible,
2453
3287
  onClose: handleHangUp,
2454
3288
  status: statusLabel,
2455
- hero: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_plugin_runtime9.AdaOrb, { size: 180, state: orbState }),
3289
+ hero: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_plugin_runtime14.AdaOrb, { size: 180, state: orbState }),
2456
3290
  closeIcon: "hangup",
2457
3291
  closeLabel: "End",
2458
3292
  children: [
2459
- /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
2460
- import_react_native15.ScrollView,
3293
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(
3294
+ import_react_native20.ScrollView,
2461
3295
  {
2462
3296
  ref: scrollRef,
2463
- style: styles14.transcript,
2464
- contentContainerStyle: styles14.transcriptContent,
3297
+ style: styles19.transcript,
3298
+ contentContainerStyle: styles19.transcriptContent,
2465
3299
  showsVerticalScrollIndicator: false,
2466
3300
  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)(
3301
+ 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,
3302
+ visibleEntries.map((entry, i) => /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(import_react_native20.View, { style: styles19.entry, children: [
3303
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_react_native20.Text, { style: styles19.role, children: entry.role === "user" ? "YOU" : "ADA" }),
3304
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
2471
3305
  HighlightedText,
2472
3306
  {
2473
3307
  text: entry.text,
2474
- baseStyle: styles14.entryText,
2475
- importantStyle: styles14.important
3308
+ baseStyle: styles19.entryText,
3309
+ importantStyle: styles19.important
2476
3310
  }
2477
3311
  )
2478
3312
  ] }, `${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)(
3313
+ userPartial || aiPartial ? /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(import_react_native20.View, { style: styles19.entry, children: [
3314
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_react_native20.Text, { style: styles19.role, children: userPartial ? "YOU" : "ADA" }),
3315
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
2482
3316
  HighlightedText,
2483
3317
  {
2484
3318
  text: userPartial || aiPartial,
2485
- baseStyle: styles14.entryTextLive,
2486
- importantStyle: styles14.important
3319
+ baseStyle: styles19.entryTextLive,
3320
+ importantStyle: styles19.important
2487
3321
  }
2488
3322
  )
2489
3323
  ] }) : null
2490
3324
  ]
2491
3325
  }
2492
3326
  ),
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,
3327
+ callState === "connected" || callState === "connecting" ? /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(import_react_native20.View, { style: styles19.muteWrap, pointerEvents: "box-none", children: [
3328
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
3329
+ import_react_native20.Pressable,
2496
3330
  {
2497
3331
  onPress: toggleMute,
2498
3332
  accessibilityRole: "button",
2499
3333
  accessibilityLabel: muted ? "Unmute microphone" : "Mute microphone",
2500
3334
  accessibilityState: { selected: muted },
2501
- style: [styles14.muteBtn, muted && styles14.muteBtnActive],
3335
+ style: [styles19.muteBtn, muted && styles19.muteBtnActive],
2502
3336
  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
3337
+ 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: [
3338
+ /* @__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" }),
3339
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_react_native_svg2.Path, { d: "M19 10v2a7 7 0 0 1-14 0v-2" }),
3340
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_react_native_svg2.Line, { x1: "12", y1: "19", x2: "12", y2: "23" }),
3341
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_react_native_svg2.Line, { x1: "8", y1: "23", x2: "16", y2: "23" }),
3342
+ 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
3343
  ] })
2510
3344
  }
2511
3345
  ),
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
- ) })
3346
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_react_native20.Text, { style: styles19.muteLabel, children: muted ? "Muted" : "Mute" })
3347
+ ] }) : null
2530
3348
  ]
2531
3349
  }
2532
3350
  );
2533
3351
  }
2534
- var styles14 = import_react_native15.StyleSheet.create({
3352
+ var styles19 = import_react_native20.StyleSheet.create({
2535
3353
  transcript: {
2536
3354
  flex: 1,
2537
3355
  paddingHorizontal: 28
@@ -2548,9 +3366,9 @@ var styles14 = import_react_native15.StyleSheet.create({
2548
3366
  },
2549
3367
  role: {
2550
3368
  fontSize: 10,
2551
- fontFamily: fontFam7,
3369
+ fontFamily: fontFam11,
2552
3370
  fontWeight: "700",
2553
- color: import_core18.ada.inkMuted,
3371
+ color: import_core23.ada.inkMuted,
2554
3372
  letterSpacing: 1.4,
2555
3373
  minWidth: 32,
2556
3374
  paddingTop: 3,
@@ -2559,9 +3377,9 @@ var styles14 = import_react_native15.StyleSheet.create({
2559
3377
  entryText: {
2560
3378
  flex: 1,
2561
3379
  fontSize: 14,
2562
- fontFamily: fontFam7,
3380
+ fontFamily: fontFam11,
2563
3381
  fontWeight: "400",
2564
- color: import_core18.ada.inkSoft,
3382
+ color: import_core23.ada.inkSoft,
2565
3383
  lineHeight: 20,
2566
3384
  letterSpacing: -0.1,
2567
3385
  opacity: 0.42
@@ -2569,18 +3387,18 @@ var styles14 = import_react_native15.StyleSheet.create({
2569
3387
  entryTextLive: {
2570
3388
  flex: 1,
2571
3389
  fontSize: 14,
2572
- fontFamily: fontFam7,
3390
+ fontFamily: fontFam11,
2573
3391
  fontWeight: "400",
2574
- color: import_core18.ada.inkSoft,
3392
+ color: import_core23.ada.inkSoft,
2575
3393
  lineHeight: 20,
2576
3394
  letterSpacing: -0.1,
2577
3395
  opacity: 0.7
2578
3396
  },
2579
3397
  important: {
2580
3398
  fontSize: 17,
2581
- fontFamily: fontFam7,
3399
+ fontFamily: fontFam11,
2582
3400
  fontWeight: "700",
2583
- color: import_core18.ada.ink,
3401
+ color: import_core23.ada.ink,
2584
3402
  letterSpacing: -0.1,
2585
3403
  opacity: 1
2586
3404
  },
@@ -2595,14 +3413,14 @@ var styles14 = import_react_native15.StyleSheet.create({
2595
3413
  },
2596
3414
  errorText: {
2597
3415
  fontSize: 13,
2598
- fontFamily: fontFam7,
3416
+ fontFamily: fontFam11,
2599
3417
  fontWeight: "500",
2600
3418
  color: "#fca5a5",
2601
3419
  lineHeight: 18
2602
3420
  },
2603
3421
  muteWrap: {
2604
3422
  position: "absolute",
2605
- bottom: import_react_native15.Platform.OS === "ios" ? 56 : 40,
3423
+ bottom: import_react_native20.Platform.OS === "ios" ? 56 : 40,
2606
3424
  left: 28,
2607
3425
  alignItems: "center",
2608
3426
  gap: 6
@@ -2611,9 +3429,9 @@ var styles14 = import_react_native15.StyleSheet.create({
2611
3429
  width: 56,
2612
3430
  height: 56,
2613
3431
  borderRadius: 28,
2614
- backgroundColor: import_core18.ada.paperSoft,
3432
+ backgroundColor: import_core23.ada.paperSoft,
2615
3433
  borderWidth: 1,
2616
- borderColor: import_core18.ada.border,
3434
+ borderColor: import_core23.ada.border,
2617
3435
  justifyContent: "center",
2618
3436
  alignItems: "center"
2619
3437
  },
@@ -2623,18 +3441,133 @@ var styles14 = import_react_native15.StyleSheet.create({
2623
3441
  },
2624
3442
  muteLabel: {
2625
3443
  fontSize: 12,
2626
- fontFamily: fontFam7,
3444
+ fontFamily: fontFam11,
2627
3445
  fontWeight: "500",
2628
- color: import_core18.ada.inkMuted,
3446
+ color: import_core23.ada.inkMuted,
2629
3447
  letterSpacing: 0.3
2630
- },
2631
- hiddenWebView: {
3448
+ }
3449
+ });
3450
+
3451
+ // src/voice/VoiceSessionHost.tsx
3452
+ var import_react18 = require("react");
3453
+ var import_react_native21 = require("react-native");
3454
+ var import_react_native_webview3 = require("react-native-webview");
3455
+ var import_jsx_runtime22 = require("react/jsx-runtime");
3456
+ function VoiceSessionHost() {
3457
+ const [, force] = (0, import_react18.useReducer)((x) => x + 1, 0);
3458
+ const lastSig = (0, import_react18.useRef)("");
3459
+ (0, import_react18.useEffect)(() => {
3460
+ const sub = voiceSession.subscribe(() => {
3461
+ const s2 = voiceSession.getInternal();
3462
+ const sig = `${s2.mounted}|${s2.webViewSource?.uri || ""}`;
3463
+ if (sig !== lastSig.current) {
3464
+ lastSig.current = sig;
3465
+ force();
3466
+ }
3467
+ });
3468
+ return () => sub.unsubscribe();
3469
+ }, []);
3470
+ const setRef = (0, import_react18.useCallback)((r) => {
3471
+ voiceSession.setWebView(r);
3472
+ }, []);
3473
+ const s = voiceSession.getInternal();
3474
+ if (!s.mounted || !s.webViewSource) return null;
3475
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_react_native21.View, { style: styles20.hidden, pointerEvents: "none", children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
3476
+ import_react_native_webview3.WebView,
3477
+ {
3478
+ ref: setRef,
3479
+ source: s.webViewSource,
3480
+ style: styles20.web,
3481
+ onMessage: (e) => voiceSession.handleMessage(e),
3482
+ onError: (e) => console.log("[VoiceSessionHost] error:", e.nativeEvent?.description),
3483
+ injectedJavaScriptBeforeContentLoaded: voiceSession.injectedTrackHook,
3484
+ mediaPlaybackRequiresUserAction: false,
3485
+ allowsInlineMediaPlayback: true,
3486
+ mediaCapturePermissionGrantType: "grant",
3487
+ javaScriptEnabled: true,
3488
+ originWhitelist: ["*"]
3489
+ }
3490
+ ) });
3491
+ }
3492
+ var styles20 = import_react_native21.StyleSheet.create({
3493
+ hidden: {
2632
3494
  position: "absolute",
2633
3495
  width: 1,
2634
3496
  height: 1,
2635
3497
  top: -100,
2636
3498
  left: -100,
2637
3499
  opacity: 0
3500
+ },
3501
+ web: { flex: 1, backgroundColor: "transparent" }
3502
+ });
3503
+
3504
+ // src/voice/VoiceMicIndicator.tsx
3505
+ var import_react19 = require("react");
3506
+ var import_react_native22 = require("react-native");
3507
+ var import_core24 = require("@ateam-ai-mobile/core");
3508
+ var import_jsx_runtime23 = require("react/jsx-runtime");
3509
+ var fontFam12 = import_react_native22.Platform.OS === "ios" ? "SF Pro Rounded" : void 0;
3510
+ function VoiceMicIndicator({ topInset = 8 }) {
3511
+ const [, force] = (0, import_react19.useReducer)((x) => x + 1, 0);
3512
+ const pulse = (0, import_react19.useRef)(new import_react_native22.Animated.Value(0.4)).current;
3513
+ (0, import_react19.useEffect)(() => {
3514
+ const sub = voiceSession.subscribe(() => force());
3515
+ return () => sub.unsubscribe();
3516
+ }, []);
3517
+ const snap = voiceSession.getSnapshot();
3518
+ const live = snap.state === "connecting" || snap.state === "connected";
3519
+ (0, import_react19.useEffect)(() => {
3520
+ if (!live) return;
3521
+ const loop = import_react_native22.Animated.loop(
3522
+ import_react_native22.Animated.sequence([
3523
+ import_react_native22.Animated.timing(pulse, { toValue: 1, duration: 700, useNativeDriver: true }),
3524
+ import_react_native22.Animated.timing(pulse, { toValue: 0.4, duration: 700, useNativeDriver: true })
3525
+ ])
3526
+ );
3527
+ loop.start();
3528
+ return () => loop.stop();
3529
+ }, [live, pulse]);
3530
+ if (!live) return null;
3531
+ const label = snap.state === "connecting" ? "Connecting\u2026" : snap.muted ? "Mic muted" : "Ada is listening";
3532
+ const dotColor = snap.muted ? import_core24.ada.inkMuted : "#E5484D";
3533
+ 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: [
3534
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_react_native22.Animated.View, { style: [styles21.dot, { backgroundColor: dotColor, opacity: snap.muted ? 1 : pulse }] }),
3535
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_react_native22.Text, { style: styles21.label, children: label })
3536
+ ] }) });
3537
+ }
3538
+ var styles21 = import_react_native22.StyleSheet.create({
3539
+ wrap: {
3540
+ position: "absolute",
3541
+ left: 0,
3542
+ right: 0,
3543
+ alignItems: "center",
3544
+ zIndex: 9999,
3545
+ elevation: 9999
3546
+ },
3547
+ pill: {
3548
+ flexDirection: "row",
3549
+ alignItems: "center",
3550
+ gap: 7,
3551
+ paddingHorizontal: 12,
3552
+ paddingVertical: 6,
3553
+ borderRadius: 999,
3554
+ backgroundColor: import_core24.ada.ink,
3555
+ shadowColor: "#000",
3556
+ shadowOpacity: 0.2,
3557
+ shadowRadius: 8,
3558
+ shadowOffset: { width: 0, height: 2 }
3559
+ },
3560
+ dot: {
3561
+ width: 8,
3562
+ height: 8,
3563
+ borderRadius: 4
3564
+ },
3565
+ label: {
3566
+ fontSize: 12,
3567
+ fontFamily: fontFam12,
3568
+ fontWeight: "600",
3569
+ color: import_core24.ada.paper,
3570
+ letterSpacing: 0.2
2638
3571
  }
2639
3572
  });
2640
3573
  // Annotate the CommonJS export names for ESM import in node:
@@ -2643,9 +3576,13 @@ var styles14 = import_react_native15.StyleSheet.create({
2643
3576
  ChatInput,
2644
3577
  DaySeparator,
2645
3578
  EmbeddedPluginWebView,
3579
+ HandoffScreen,
2646
3580
  JobProgressBar,
3581
+ JoinScreen,
2647
3582
  Lightbox,
3583
+ LoginScreen,
2648
3584
  MessageBubble,
3585
+ OpenJoinScreen,
2649
3586
  Plugin,
2650
3587
  PluginSurface,
2651
3588
  TableCardBlock,
@@ -2653,16 +3590,30 @@ var styles14 = import_react_native15.StyleSheet.create({
2653
3590
  TypingIndicator,
2654
3591
  UIActionsRow,
2655
3592
  VoiceCallSheet,
3593
+ VoiceMicIndicator,
3594
+ VoiceSessionHost,
2656
3595
  WidgetHost,
2657
3596
  WidgetWebView,
3597
+ WorkspaceBanner,
2658
3598
  buildPluginBridgeJS,
3599
+ completeInviteJoin,
3600
+ completeOpenJoin,
3601
+ completeProviderSignIn,
3602
+ googleClientIds,
3603
+ handoffErrorMessage,
3604
+ inviteHeadline,
3605
+ openJoinErrorMessage,
2659
3606
  openWidget,
3607
+ runHandoffExchange,
2660
3608
  setClipboardProvider,
3609
+ setOAuthProvider,
2661
3610
  setPluginWebViewExtraHosts,
2662
3611
  setUIActionNavigator,
3612
+ shouldRecoverToDefaultTenant,
2663
3613
  useLightboxStore,
2664
3614
  usePluginBridge,
2665
3615
  useVoiceCall,
2666
- useWidgetHostStore
3616
+ useWidgetHostStore,
3617
+ voiceSession
2667
3618
  });
2668
3619
  //# sourceMappingURL=index.js.map