@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.mjs CHANGED
@@ -208,6 +208,13 @@ import { ada as ada4, fontSize as fontSize2 } from "@ateam-ai-mobile/core";
208
208
  import { useSurfaceStore as useSurfaceStore2, getHaptics } from "@ateam-ai-mobile/plugin-runtime";
209
209
 
210
210
  // src/providers.ts
211
+ var _oauth = null;
212
+ function setOAuthProvider(oauth) {
213
+ _oauth = oauth;
214
+ }
215
+ function getOAuthProvider() {
216
+ return _oauth;
217
+ }
211
218
  var _clipboard = null;
212
219
  function setClipboardProvider(clipboard) {
213
220
  _clipboard = clipboard;
@@ -254,8 +261,7 @@ import {
254
261
  usePlugin,
255
262
  useNativeBridge,
256
263
  usePluginTheme,
257
- getPluginNative,
258
- gatePluginNative
264
+ resolvePluginRuntimeProps
259
265
  } from "@ateam-ai-mobile/plugin-runtime";
260
266
 
261
267
  // src/plugin/EmbeddedPluginWebView.tsx
@@ -749,8 +755,10 @@ function Plugin({ id, manifest, commandBufferRef, props: engineProps }) {
749
755
  const { plugin, loading, error, bundleMissing } = usePlugin(id, manifest);
750
756
  const connectorId = id.startsWith("mcp:") ? id.split(":")[1] : manifest._connectorId;
751
757
  const bridge = useNativeBridge(id, connectorId, commandBufferRef);
752
- const declaredCaps = manifest?.native;
753
- const native = useMemo2(() => gatePluginNative(getPluginNative(), declaredCaps), [declaredCaps]);
758
+ const { device, native, deviceState, voice } = useMemo2(
759
+ () => resolvePluginRuntimeProps({ pluginId: id, manifest, plugin }),
760
+ [id, manifest, plugin]
761
+ );
754
762
  const theme = usePluginTheme();
755
763
  const user = getUser();
756
764
  const context = useMemo2(() => ({
@@ -773,7 +781,7 @@ function Plugin({ id, manifest, commandBufferRef, props: engineProps }) {
773
781
  return /* @__PURE__ */ jsx3(View3, { style: styles3.center, children: /* @__PURE__ */ jsx3(Text3, { style: styles3.errorMessage, children: error || "Plugin not found" }) });
774
782
  }
775
783
  const { Component } = plugin;
776
- return /* @__PURE__ */ jsx3(PluginErrorBoundary, { pluginId: id, children: /* @__PURE__ */ jsx3(Component, { bridge, native, theme, context, props: engineProps }) });
784
+ return /* @__PURE__ */ jsx3(PluginErrorBoundary, { pluginId: id, children: /* @__PURE__ */ jsx3(Component, { bridge, device, native, deviceState, voice, theme, context, props: engineProps }) });
777
785
  }
778
786
  var styles3 = StyleSheet3.create({
779
787
  center: {
@@ -1270,7 +1278,10 @@ function MessageBubble({ message }) {
1270
1278
  message.toolCalls && message.toolCalls.length > 0 && /* @__PURE__ */ jsx9(ToolCallsBadge, { calls: message.toolCalls }),
1271
1279
  message.jobProgress && /* @__PURE__ */ jsx9(JobProgressBar, { progress: message.jobProgress, standalone: isProgressOnly }),
1272
1280
  !isUser && message.actions && message.actions.length > 0 && /* @__PURE__ */ jsx9(UIActionsRow, { actions: message.actions }),
1273
- pq && /* @__PURE__ */ jsx9(QuestionChips, { inputType: pq.inputType, options: pq.options }),
1281
+ pq && // Per-chain: this bubble's OWN chain has a pending question. Parallel
1282
+ // pending chains each render their own chips (jobProgress is per
1283
+ // bubble); answered questions have no jobProgress → no chips.
1284
+ /* @__PURE__ */ jsx9(QuestionChips, { inputType: pq.inputType, options: pq.options }),
1274
1285
  !isProgressOnly && /* @__PURE__ */ jsx9(Text7, { style: [styles7.timeText, isUser && styles7.timeTextUser], children: formatTime(message.timestamp) })
1275
1286
  ]
1276
1287
  }
@@ -2122,24 +2133,540 @@ var styles13 = StyleSheet13.create({
2122
2133
  }
2123
2134
  });
2124
2135
 
2136
+ // src/WorkspaceBanner.tsx
2137
+ import { View as View15, Text as Text11, StyleSheet as StyleSheet14 } from "react-native";
2138
+ import { ada as ada11, fontSize as fontSize7, spacing as spacing2, useWorkspaceStore } from "@ateam-ai-mobile/core";
2139
+ import { jsx as jsx16, jsxs as jsxs12 } from "react/jsx-runtime";
2140
+ var WARM_AMBER2 = "#D49A4E";
2141
+ var WARM_AMBER_SOFT2 = "rgba(212, 154, 78, 0.16)";
2142
+ var WARM_AMBER_BORDER = "rgba(212, 154, 78, 0.4)";
2143
+ function WorkspaceBanner({ topInset = 0 }) {
2144
+ const snapshot = useWorkspaceStore((s) => s.snapshot);
2145
+ if (!snapshot) return null;
2146
+ if (snapshot.state === "active" || snapshot.state === "unknown") return null;
2147
+ if (!snapshot.message) return null;
2148
+ return /* @__PURE__ */ jsxs12(
2149
+ View15,
2150
+ {
2151
+ style: [styles14.container, { paddingTop: spacing2.sm + topInset }],
2152
+ accessibilityRole: "alert",
2153
+ accessibilityLabel: snapshot.message,
2154
+ children: [
2155
+ /* @__PURE__ */ jsx16(View15, { style: styles14.dot }),
2156
+ /* @__PURE__ */ jsx16(Text11, { style: styles14.message, children: snapshot.message })
2157
+ ]
2158
+ }
2159
+ );
2160
+ }
2161
+ var styles14 = StyleSheet14.create({
2162
+ container: {
2163
+ flexDirection: "row",
2164
+ alignItems: "flex-start",
2165
+ gap: spacing2.sm,
2166
+ paddingHorizontal: spacing2.md,
2167
+ paddingBottom: spacing2.sm,
2168
+ backgroundColor: WARM_AMBER_SOFT2,
2169
+ borderBottomWidth: StyleSheet14.hairlineWidth,
2170
+ borderBottomColor: WARM_AMBER_BORDER
2171
+ },
2172
+ dot: {
2173
+ width: 8,
2174
+ height: 8,
2175
+ borderRadius: 4,
2176
+ marginTop: 5,
2177
+ backgroundColor: WARM_AMBER2
2178
+ },
2179
+ message: {
2180
+ flex: 1,
2181
+ color: ada11.ink,
2182
+ fontSize: fontSize7.sm,
2183
+ lineHeight: fontSize7.sm * 1.35
2184
+ }
2185
+ });
2186
+
2187
+ // src/auth/HandoffScreen.tsx
2188
+ import { useEffect as useEffect8, useRef as useRef5, useState as useState9 } from "react";
2189
+ import { View as View16, Text as Text12, Pressable as Pressable9, StyleSheet as StyleSheet15, Platform as Platform10, ActivityIndicator as ActivityIndicator6 } from "react-native";
2190
+ import { exchangeHandoff, ada as ada12 } from "@ateam-ai-mobile/core";
2191
+ import { AdaOrb as AdaOrb3 } from "@ateam-ai-mobile/plugin-runtime";
2192
+
2193
+ // src/auth/handoffExchange.ts
2194
+ async function runHandoffExchange(code, deps) {
2195
+ const res = await deps.exchange(code);
2196
+ if (res.tenant) await deps.onAdoptTenant(res.tenant);
2197
+ await deps.onSignedIn(res.jwt);
2198
+ deps.onDone();
2199
+ }
2200
+ function handoffErrorMessage(msg) {
2201
+ const m = (msg || "").toLowerCase();
2202
+ if (m.includes("429") || m.includes("rate") || m.includes("too many")) {
2203
+ return "Too many attempts \u2014 wait a moment, then get a new sign-in link on the web.";
2204
+ }
2205
+ return "This sign-in link is no longer valid. Get a new one on the web and try again.";
2206
+ }
2207
+
2208
+ // src/auth/HandoffScreen.tsx
2209
+ import { Fragment as Fragment3, jsx as jsx17, jsxs as jsxs13 } from "react/jsx-runtime";
2210
+ var fontFam7 = Platform10.OS === "ios" ? "SF Pro Rounded" : void 0;
2211
+ function HandoffScreen({ code, onAdoptTenant, onSignedIn, onDone, onCancel }) {
2212
+ const [err, setErr] = useState9(null);
2213
+ const startedForRef = useRef5(null);
2214
+ useEffect8(() => {
2215
+ if (!code || startedForRef.current === code) return;
2216
+ startedForRef.current = code;
2217
+ runHandoffExchange(code, { exchange: exchangeHandoff, onAdoptTenant, onSignedIn, onDone }).catch((e) => setErr(handoffErrorMessage(e?.message)));
2218
+ }, [code, onAdoptTenant, onSignedIn, onDone]);
2219
+ return /* @__PURE__ */ jsx17(View16, { style: styles15.container, children: /* @__PURE__ */ jsxs13(View16, { style: styles15.center, children: [
2220
+ /* @__PURE__ */ jsx17(AdaOrb3, { size: 72, state: err ? "idle" : "thinking" }),
2221
+ !err ? /* @__PURE__ */ jsxs13(Fragment3, { children: [
2222
+ /* @__PURE__ */ jsx17(Text12, { style: styles15.title, children: "Signing you in\u2026" }),
2223
+ /* @__PURE__ */ jsx17(ActivityIndicator6, { color: ada12.ink, style: { marginTop: 6 } })
2224
+ ] }) : /* @__PURE__ */ jsxs13(Fragment3, { children: [
2225
+ /* @__PURE__ */ jsx17(Text12, { style: styles15.title, children: "Couldn\u2019t finish sign-in" }),
2226
+ /* @__PURE__ */ jsx17(Text12, { style: styles15.sub, children: err }),
2227
+ /* @__PURE__ */ jsx17(Pressable9, { onPress: onCancel, hitSlop: 8, style: styles15.back, children: /* @__PURE__ */ jsx17(Text12, { style: styles15.backText, children: "Back to sign in" }) })
2228
+ ] })
2229
+ ] }) });
2230
+ }
2231
+ var styles15 = StyleSheet15.create({
2232
+ container: { flex: 1, backgroundColor: ada12.paper },
2233
+ center: { flex: 1, alignItems: "center", justifyContent: "center", gap: 14, paddingHorizontal: 32 },
2234
+ title: { fontSize: 20, fontWeight: "600", color: ada12.ink, fontFamily: fontFam7, marginTop: 8 },
2235
+ sub: { fontSize: 14, lineHeight: 20, color: ada12.inkSoft, textAlign: "center", fontFamily: fontFam7 },
2236
+ back: { marginTop: 10, paddingVertical: 10 },
2237
+ backText: { fontSize: 15, fontWeight: "600", color: ada12.orangeDeep, fontFamily: fontFam7 }
2238
+ });
2239
+
2240
+ // src/auth/OpenJoinScreen.tsx
2241
+ import { useEffect as useEffect9, useState as useState10 } from "react";
2242
+ import { View as View17, Text as Text13, Pressable as Pressable10, StyleSheet as StyleSheet16, ActivityIndicator as ActivityIndicator7, Platform as Platform11, TextInput as TextInput2 } from "react-native";
2243
+ import { openJoinWithGoogle, openJoinWithApple, ada as ada13, spacing as spacing3 } from "@ateam-ai-mobile/core";
2244
+ import { AdaOrb as AdaOrb4 } from "@ateam-ai-mobile/plugin-runtime";
2245
+
2246
+ // src/auth/openJoin.ts
2247
+ async function completeOpenJoin(slug, idToken, deps) {
2248
+ if (!idToken) return false;
2249
+ const consumer = await deps.join(slug, idToken);
2250
+ await deps.onAdoptTenant(slug);
2251
+ await deps.onSignedIn(consumer.jwt);
2252
+ return true;
2253
+ }
2254
+ function openJoinErrorMessage(msg) {
2255
+ const m = (msg || "").toLowerCase();
2256
+ if (m.includes("invite-only") || m.includes("invite only")) {
2257
+ return "This team is invite-only \u2014 ask an admin for an invite link.";
2258
+ }
2259
+ if (m.includes("not found") || m.includes("unknown tenant")) {
2260
+ return "No team with that handle.";
2261
+ }
2262
+ return msg || "Could not join";
2263
+ }
2264
+
2265
+ // src/auth/OpenJoinScreen.tsx
2266
+ import { jsx as jsx18, jsxs as jsxs14 } from "react/jsx-runtime";
2267
+ var fontFam8 = Platform11.OS === "ios" ? "SF Pro Rounded" : void 0;
2268
+ function OpenJoinScreen({ onAdoptTenant, onSignedIn, onBack }) {
2269
+ const [team, setTeam] = useState10("");
2270
+ const [busy, setBusy] = useState10(false);
2271
+ const [err, setErr] = useState10(null);
2272
+ const [appleAvailable, setAppleAvailable] = useState10(false);
2273
+ useEffect9(() => {
2274
+ getOAuthProvider()?.isAppleAvailable().then(setAppleAvailable).catch(() => setAppleAvailable(false));
2275
+ }, []);
2276
+ const slug = team.trim().toLowerCase();
2277
+ const canSubmit = slug.length > 0 && !busy;
2278
+ async function run(acquire, join) {
2279
+ if (!slug) return;
2280
+ const oauth = getOAuthProvider();
2281
+ if (!oauth) {
2282
+ setErr("Sign-in is unavailable on this device.");
2283
+ return;
2284
+ }
2285
+ setErr(null);
2286
+ setBusy(true);
2287
+ try {
2288
+ const idToken = await acquire(oauth);
2289
+ await completeOpenJoin(slug, idToken, { join, onAdoptTenant, onSignedIn });
2290
+ } catch (e) {
2291
+ setErr(openJoinErrorMessage(e?.message));
2292
+ } finally {
2293
+ setBusy(false);
2294
+ }
2295
+ }
2296
+ const handleGoogle = () => run((oauth) => oauth.signInWithGoogle(), openJoinWithGoogle);
2297
+ const handleApple = () => run((oauth) => oauth.signInWithApple(), openJoinWithApple);
2298
+ return /* @__PURE__ */ jsxs14(View17, { style: styles16.container, children: [
2299
+ /* @__PURE__ */ jsxs14(View17, { style: styles16.hero, children: [
2300
+ /* @__PURE__ */ jsx18(AdaOrb4, { size: 110 }),
2301
+ /* @__PURE__ */ jsx18(Text13, { style: styles16.wordmark, children: "Join a team" }),
2302
+ /* @__PURE__ */ jsx18(Text13, { style: styles16.tagline, children: "Enter the team's handle. If it's open to join, you're in." })
2303
+ ] }),
2304
+ /* @__PURE__ */ jsxs14(View17, { style: styles16.form, children: [
2305
+ /* @__PURE__ */ jsx18(
2306
+ TextInput2,
2307
+ {
2308
+ style: styles16.input,
2309
+ value: team,
2310
+ onChangeText: setTeam,
2311
+ placeholder: "team handle (e.g. acme)",
2312
+ placeholderTextColor: ada13.inkMuted,
2313
+ autoCapitalize: "none",
2314
+ autoCorrect: false,
2315
+ returnKeyType: "done"
2316
+ }
2317
+ ),
2318
+ /* @__PURE__ */ jsxs14(
2319
+ Pressable10,
2320
+ {
2321
+ style: [styles16.googleBtn, !canSubmit && styles16.btnDisabled],
2322
+ onPress: handleGoogle,
2323
+ disabled: !canSubmit,
2324
+ children: [
2325
+ /* @__PURE__ */ jsx18(Text13, { style: styles16.googleGlyph, children: "G" }),
2326
+ /* @__PURE__ */ jsx18(Text13, { style: styles16.googleText, children: "Continue with Google" })
2327
+ ]
2328
+ }
2329
+ ),
2330
+ appleAvailable && /* @__PURE__ */ jsx18(View17, { style: { opacity: canSubmit ? 1 : 0.5 }, pointerEvents: canSubmit ? "auto" : "none", children: getOAuthProvider()?.renderAppleButton({ onPress: handleApple, disabled: !canSubmit, style: styles16.appleBtn }) }),
2331
+ busy && /* @__PURE__ */ jsx18(ActivityIndicator7, { color: ada13.ink, style: { marginTop: 10 } }),
2332
+ !!err && /* @__PURE__ */ jsx18(Text13, { style: styles16.err, children: err }),
2333
+ /* @__PURE__ */ jsx18(Pressable10, { onPress: onBack, hitSlop: 8, style: styles16.back, children: /* @__PURE__ */ jsx18(Text13, { style: styles16.backText, children: "Back to sign in" }) })
2334
+ ] })
2335
+ ] });
2336
+ }
2337
+ var styles16 = StyleSheet16.create({
2338
+ container: { flex: 1, backgroundColor: ada13.paper, justifyContent: "space-between" },
2339
+ hero: { alignItems: "center", marginTop: spacing3.xxl },
2340
+ wordmark: { fontSize: 26, fontWeight: "800", color: ada13.ink, marginTop: 14, fontFamily: fontFam8 },
2341
+ tagline: { fontSize: 15, color: ada13.inkSoft, marginTop: 8, textAlign: "center", paddingHorizontal: 28, fontFamily: fontFam8 },
2342
+ form: { paddingHorizontal: 24, paddingBottom: 40, gap: 12 },
2343
+ input: {
2344
+ backgroundColor: ada13.paperSoft,
2345
+ borderWidth: 1,
2346
+ borderColor: ada13.border,
2347
+ borderRadius: 14,
2348
+ paddingHorizontal: 16,
2349
+ paddingVertical: 14,
2350
+ fontSize: 16,
2351
+ color: ada13.ink
2352
+ },
2353
+ googleBtn: {
2354
+ flexDirection: "row",
2355
+ alignItems: "center",
2356
+ justifyContent: "center",
2357
+ gap: 10,
2358
+ backgroundColor: ada13.ink,
2359
+ paddingVertical: 15,
2360
+ borderRadius: 999
2361
+ },
2362
+ btnDisabled: { opacity: 0.5 },
2363
+ googleGlyph: { color: "#fff", fontSize: 18, fontWeight: "800" },
2364
+ googleText: { color: "#fff", fontSize: 16, fontWeight: "600", fontFamily: fontFam8 },
2365
+ appleBtn: { height: 52, width: "100%" },
2366
+ err: { color: "#C15545", fontSize: 13, textAlign: "center", paddingHorizontal: 16, marginTop: 4 },
2367
+ back: { alignItems: "center", paddingVertical: 10 },
2368
+ backText: { color: ada13.inkMuted, fontSize: 15, fontWeight: "500" }
2369
+ });
2370
+
2371
+ // src/auth/JoinScreen.tsx
2372
+ import { useEffect as useEffect10, useMemo as useMemo5, useState as useState11 } from "react";
2373
+ import { View as View18, Text as Text14, Pressable as Pressable11, StyleSheet as StyleSheet17, ActivityIndicator as ActivityIndicator8, Platform as Platform12 } from "react-native";
2374
+ import {
2375
+ previewInvite,
2376
+ joinWithGoogle,
2377
+ joinWithApple,
2378
+ ada as ada14,
2379
+ spacing as spacing4
2380
+ } from "@ateam-ai-mobile/core";
2381
+ import { AdaOrb as AdaOrb5 } from "@ateam-ai-mobile/plugin-runtime";
2382
+
2383
+ // src/auth/inviteJoin.ts
2384
+ async function completeInviteJoin(code, idToken, deps) {
2385
+ if (!idToken) return false;
2386
+ const consumer = await deps.join(code, idToken);
2387
+ deps.onDone();
2388
+ await deps.onSignedIn(consumer.jwt);
2389
+ return true;
2390
+ }
2391
+ function inviteHeadline(preview) {
2392
+ if (!preview) return "";
2393
+ return preview.role ? `Join ${preview.org} as ${preview.role}?` : `Join ${preview.org}?`;
2394
+ }
2395
+
2396
+ // src/auth/JoinScreen.tsx
2397
+ import { Fragment as Fragment4, jsx as jsx19, jsxs as jsxs15 } from "react/jsx-runtime";
2398
+ var fontFam9 = Platform12.OS === "ios" ? "SF Pro Rounded" : void 0;
2399
+ function JoinScreen({ code, onSignedIn, onDone, onCancel }) {
2400
+ const [preview, setPreview] = useState11(null);
2401
+ const [loadErr, setLoadErr] = useState11(null);
2402
+ const [busy, setBusy] = useState11(false);
2403
+ const [err, setErr] = useState11(null);
2404
+ const [appleAvailable, setAppleAvailable] = useState11(false);
2405
+ useEffect10(() => {
2406
+ if (!code) return;
2407
+ setPreview(null);
2408
+ setLoadErr(null);
2409
+ previewInvite(code).then(setPreview).catch((e) => setLoadErr(e?.message || "Could not load invite"));
2410
+ }, [code]);
2411
+ useEffect10(() => {
2412
+ getOAuthProvider()?.isAppleAvailable().then(setAppleAvailable).catch(() => setAppleAvailable(false));
2413
+ }, []);
2414
+ const orgLine = useMemo5(() => inviteHeadline(preview), [preview]);
2415
+ async function run(acquire, join) {
2416
+ if (!code) return;
2417
+ const oauth = getOAuthProvider();
2418
+ if (!oauth) {
2419
+ setErr("Sign-in is unavailable on this device.");
2420
+ return;
2421
+ }
2422
+ setErr(null);
2423
+ setBusy(true);
2424
+ try {
2425
+ const idToken = await acquire(oauth);
2426
+ await completeInviteJoin(code, idToken, { join, onDone, onSignedIn });
2427
+ } catch (e) {
2428
+ setErr(e?.message || "Sign-in failed");
2429
+ } finally {
2430
+ setBusy(false);
2431
+ }
2432
+ }
2433
+ const handleGoogle = () => run((oauth) => oauth.signInWithGoogle(), joinWithGoogle);
2434
+ const handleApple = () => run((oauth) => oauth.signInWithApple(), joinWithApple);
2435
+ return /* @__PURE__ */ jsxs15(View18, { style: styles17.container, children: [
2436
+ /* @__PURE__ */ jsxs15(View18, { style: styles17.hero, children: [
2437
+ /* @__PURE__ */ jsx19(AdaOrb5, { size: 120 }),
2438
+ /* @__PURE__ */ jsx19(Text14, { style: styles17.wordmark, children: "ada" }),
2439
+ loadErr ? /* @__PURE__ */ jsx19(Text14, { style: styles17.err, children: loadErr }) : preview ? /* @__PURE__ */ jsx19(Text14, { style: styles17.tagline, children: orgLine }) : /* @__PURE__ */ jsx19(ActivityIndicator8, { color: ada14.ink, style: { marginTop: 12 } })
2440
+ ] }),
2441
+ /* @__PURE__ */ jsxs15(View18, { style: styles17.buttons, children: [
2442
+ preview && !loadErr && /* @__PURE__ */ jsxs15(Fragment4, { children: [
2443
+ /* @__PURE__ */ jsxs15(
2444
+ Pressable11,
2445
+ {
2446
+ style: [styles17.googleBtn, busy && styles17.btnDisabled],
2447
+ onPress: handleGoogle,
2448
+ disabled: busy,
2449
+ children: [
2450
+ /* @__PURE__ */ jsx19(Text14, { style: styles17.googleGlyph, children: "G" }),
2451
+ /* @__PURE__ */ jsx19(Text14, { style: styles17.googleText, children: "Continue with Google" })
2452
+ ]
2453
+ }
2454
+ ),
2455
+ appleAvailable && getOAuthProvider()?.renderAppleButton({ onPress: handleApple, disabled: busy, style: styles17.appleBtn })
2456
+ ] }),
2457
+ loadErr && /* @__PURE__ */ jsx19(Pressable11, { style: styles17.dismiss, onPress: onCancel, hitSlop: 8, children: /* @__PURE__ */ jsx19(Text14, { style: styles17.dismissText, children: "Back to sign in" }) }),
2458
+ !!err && /* @__PURE__ */ jsx19(Text14, { style: styles17.err, children: err }),
2459
+ busy && /* @__PURE__ */ jsx19(ActivityIndicator8, { color: ada14.ink, style: { marginTop: 12 } }),
2460
+ /* @__PURE__ */ jsx19(Text14, { style: styles17.disclaimer, children: "By continuing you agree to Ada's Terms and Privacy Policy." })
2461
+ ] })
2462
+ ] });
2463
+ }
2464
+ var styles17 = StyleSheet17.create({
2465
+ container: { flex: 1, backgroundColor: ada14.paper, justifyContent: "space-between" },
2466
+ hero: { alignItems: "center", marginTop: spacing4.xxl * 1.4 },
2467
+ wordmark: { fontSize: 34, fontWeight: "800", color: ada14.ink, letterSpacing: 4, marginTop: 12, fontFamily: fontFam9 },
2468
+ tagline: { fontSize: 17, color: ada14.inkSoft, marginTop: 10, textAlign: "center", paddingHorizontal: 24, fontFamily: fontFam9 },
2469
+ buttons: { paddingHorizontal: 24, paddingBottom: 40, gap: 12 },
2470
+ googleBtn: {
2471
+ flexDirection: "row",
2472
+ alignItems: "center",
2473
+ justifyContent: "center",
2474
+ gap: 10,
2475
+ backgroundColor: ada14.ink,
2476
+ paddingVertical: 15,
2477
+ borderRadius: 999
2478
+ },
2479
+ btnDisabled: { opacity: 0.5 },
2480
+ googleGlyph: { color: "#fff", fontSize: 18, fontWeight: "800" },
2481
+ googleText: { color: "#fff", fontSize: 16, fontWeight: "600", fontFamily: fontFam9 },
2482
+ appleBtn: { height: 52, width: "100%" },
2483
+ dismiss: { alignItems: "center", paddingVertical: 10 },
2484
+ dismissText: { color: ada14.inkMuted, fontSize: 15, fontWeight: "500" },
2485
+ err: { color: "#C15545", fontSize: 13, textAlign: "center", paddingHorizontal: 16, marginTop: 8 },
2486
+ disclaimer: { fontSize: 12, color: ada14.inkMuted, textAlign: "center", marginTop: 8 }
2487
+ });
2488
+
2489
+ // src/auth/LoginScreen.tsx
2490
+ import { useEffect as useEffect11, useMemo as useMemo6, useState as useState12 } from "react";
2491
+ import { View as View19, Text as Text15, Pressable as Pressable12, StyleSheet as StyleSheet18, Platform as Platform13, ActivityIndicator as ActivityIndicator9 } from "react-native";
2492
+ import { fetchConsumerConfig, signInWithGoogle, signInWithApple, ada as ada15 } from "@ateam-ai-mobile/core";
2493
+ import { AdaOrb as AdaOrb6 } from "@ateam-ai-mobile/plugin-runtime";
2494
+
2495
+ // src/auth/login.ts
2496
+ function googleClientIds(config) {
2497
+ const g = config.providers.google || {};
2498
+ return {
2499
+ webClientId: g.web_client_id || g.client_id || "",
2500
+ iosClientId: g.ios_client_id || ""
2501
+ };
2502
+ }
2503
+ function shouldRecoverToDefaultTenant(errorMsg, currentTenant, defaultTenant) {
2504
+ return /capability not enabled/i.test(errorMsg || "") && currentTenant !== defaultTenant;
2505
+ }
2506
+ async function completeProviderSignIn(idToken, deps) {
2507
+ if (!idToken) return false;
2508
+ const consumer = await deps.exchange(idToken);
2509
+ await deps.onSignedIn(consumer.jwt);
2510
+ return true;
2511
+ }
2512
+
2513
+ // src/auth/LoginScreen.tsx
2514
+ import { Fragment as Fragment5, jsx as jsx20, jsxs as jsxs16 } from "react/jsx-runtime";
2515
+ var fontFam10 = Platform13.OS === "ios" ? "SF Pro Rounded" : void 0;
2516
+ function LoginScreen({ activeTenant, defaultTenant, onConsumerDisabled, onSignedIn, onJoinTeam }) {
2517
+ const [config, setConfig] = useState12(null);
2518
+ const [error, setError] = useState12(null);
2519
+ useEffect11(() => {
2520
+ setConfig(null);
2521
+ setError(null);
2522
+ fetchConsumerConfig().then(setConfig).catch(async (err) => {
2523
+ const msg = err?.message || "Failed to load sign-in config";
2524
+ if (shouldRecoverToDefaultTenant(msg, activeTenant, defaultTenant)) {
2525
+ await onConsumerDisabled();
2526
+ return;
2527
+ }
2528
+ setError(msg);
2529
+ });
2530
+ }, [activeTenant, defaultTenant, onConsumerDisabled]);
2531
+ return /* @__PURE__ */ jsxs16(View19, { style: styles18.container, children: [
2532
+ /* @__PURE__ */ jsxs16(View19, { style: styles18.hero, children: [
2533
+ /* @__PURE__ */ jsx20(AdaOrb6, { size: 140 }),
2534
+ /* @__PURE__ */ jsx20(Text15, { style: styles18.wordmark, children: "ada" }),
2535
+ /* @__PURE__ */ jsx20(Text15, { style: styles18.tagline, children: "Your private personal AI-Team" })
2536
+ ] }),
2537
+ /* @__PURE__ */ jsxs16(View19, { style: styles18.buttonSection, children: [
2538
+ !config && !error && /* @__PURE__ */ jsx20(ActivityIndicator9, { color: ada15.ink }),
2539
+ !!error && /* @__PURE__ */ jsx20(Text15, { style: styles18.errorText, children: error }),
2540
+ !!config && /* @__PURE__ */ jsx20(SignInButtons, { config, onSignedIn }),
2541
+ onJoinTeam && /* @__PURE__ */ jsx20(Pressable12, { onPress: onJoinTeam, hitSlop: 8, style: { paddingVertical: 8 }, children: /* @__PURE__ */ jsx20(Text15, { style: styles18.joinLink, children: "Join a team by name" }) }),
2542
+ /* @__PURE__ */ jsx20(Text15, { style: styles18.disclaimer, children: "By continuing you agree to Ada's Terms and Privacy Policy." })
2543
+ ] })
2544
+ ] });
2545
+ }
2546
+ function SignInButtons({ config, onSignedIn }) {
2547
+ const [busy, setBusy] = useState12(false);
2548
+ const [error, setError] = useState12(null);
2549
+ const [appleAvailable, setAppleAvailable] = useState12(false);
2550
+ const { webClientId, iosClientId } = useMemo6(() => googleClientIds(config), [config]);
2551
+ useEffect11(() => {
2552
+ getOAuthProvider()?.isAppleAvailable().then(setAppleAvailable).catch(() => setAppleAvailable(false));
2553
+ }, []);
2554
+ const handleGoogleLogin = async () => {
2555
+ setError(null);
2556
+ if (!webClientId) {
2557
+ setError("Google sign-in is not configured for this tenant");
2558
+ return;
2559
+ }
2560
+ const oauth = getOAuthProvider();
2561
+ if (!oauth) {
2562
+ setError("Sign-in is unavailable on this device.");
2563
+ return;
2564
+ }
2565
+ setBusy(true);
2566
+ try {
2567
+ const idToken = await oauth.signInWithGoogle({ webClientId, iosClientId });
2568
+ await completeProviderSignIn(idToken, { exchange: signInWithGoogle, onSignedIn });
2569
+ } catch (err) {
2570
+ setError(err?.message || "Sign-in failed");
2571
+ } finally {
2572
+ setBusy(false);
2573
+ }
2574
+ };
2575
+ const handleAppleLogin = async () => {
2576
+ setError(null);
2577
+ const oauth = getOAuthProvider();
2578
+ if (!oauth) {
2579
+ setError("Sign-in is unavailable on this device.");
2580
+ return;
2581
+ }
2582
+ setBusy(true);
2583
+ try {
2584
+ const idToken = await oauth.signInWithApple();
2585
+ await completeProviderSignIn(idToken, { exchange: signInWithApple, onSignedIn });
2586
+ } catch (err) {
2587
+ setError(err?.message || "Apple sign-in failed");
2588
+ } finally {
2589
+ setBusy(false);
2590
+ }
2591
+ };
2592
+ const googleEnabled = !!webClientId && !busy;
2593
+ const appleEnabled = appleAvailable && !!config.providers.apple?.service_id && !busy;
2594
+ return /* @__PURE__ */ jsxs16(Fragment5, { children: [
2595
+ !!config.providers.google && /* @__PURE__ */ jsxs16(
2596
+ Pressable12,
2597
+ {
2598
+ style: [styles18.signInBtn, !googleEnabled && styles18.signInBtnDisabled],
2599
+ onPress: handleGoogleLogin,
2600
+ disabled: !googleEnabled,
2601
+ children: [
2602
+ /* @__PURE__ */ jsx20(Text15, { style: styles18.signInGlyph, children: "G" }),
2603
+ /* @__PURE__ */ jsx20(Text15, { style: styles18.signInText, children: "Continue with Google" })
2604
+ ]
2605
+ }
2606
+ ),
2607
+ appleEnabled && getOAuthProvider()?.renderAppleButton({ onPress: handleAppleLogin, disabled: busy, style: styles18.appleBtn }),
2608
+ !!error && /* @__PURE__ */ jsx20(Text15, { style: styles18.errorText, children: error }),
2609
+ busy && /* @__PURE__ */ jsx20(ActivityIndicator9, { color: ada15.ink })
2610
+ ] });
2611
+ }
2612
+ var styles18 = StyleSheet18.create({
2613
+ container: {
2614
+ flex: 1,
2615
+ backgroundColor: ada15.paper,
2616
+ justifyContent: "space-between",
2617
+ paddingHorizontal: 32,
2618
+ paddingBottom: 24
2619
+ },
2620
+ hero: { flex: 1, alignItems: "center", justifyContent: "center", gap: 18 },
2621
+ wordmark: { fontSize: 32, fontWeight: "600", color: ada15.ink, letterSpacing: 4, fontFamily: fontFam10, marginTop: 8 },
2622
+ tagline: { fontSize: 16, fontWeight: "500", color: ada15.inkSoft, letterSpacing: -0.1, fontFamily: fontFam10 },
2623
+ buttonSection: { alignItems: "center", gap: 12 },
2624
+ signInBtn: {
2625
+ flexDirection: "row",
2626
+ alignItems: "center",
2627
+ justifyContent: "center",
2628
+ backgroundColor: ada15.ink,
2629
+ paddingVertical: 16,
2630
+ paddingHorizontal: 24,
2631
+ borderRadius: 999,
2632
+ width: "100%",
2633
+ gap: 12
2634
+ },
2635
+ signInBtnDisabled: { opacity: 0.5 },
2636
+ signInGlyph: { fontSize: 18, fontWeight: "700", color: ada15.paper, fontFamily: fontFam10 },
2637
+ signInText: { fontSize: 16, fontWeight: "600", color: ada15.paper, letterSpacing: -0.2, fontFamily: fontFam10 },
2638
+ appleBtn: { width: "100%", height: 52 },
2639
+ errorText: { fontSize: 13, color: "#c43d3d", textAlign: "center", paddingHorizontal: 16 },
2640
+ disclaimer: { fontSize: 12, color: ada15.inkMuted, textAlign: "center", marginTop: 8, lineHeight: 18, fontFamily: fontFam10 },
2641
+ joinLink: { fontSize: 14, fontWeight: "600", color: ada15.orangeDeep, textAlign: "center", fontFamily: fontFam10 }
2642
+ });
2643
+
2125
2644
  // src/voice/VoiceCallSheet.tsx
2126
- import { useEffect as useEffect8, useRef as useRef6, useMemo as useMemo5 } from "react";
2645
+ import { useEffect as useEffect13, useRef as useRef6, useMemo as useMemo7 } from "react";
2127
2646
  import {
2128
- View as View15,
2129
- Text as Text11,
2130
- StyleSheet as StyleSheet14,
2131
- Platform as Platform10,
2132
- Pressable as Pressable9,
2647
+ View as View20,
2648
+ Text as Text16,
2649
+ StyleSheet as StyleSheet19,
2650
+ Platform as Platform14,
2651
+ Pressable as Pressable13,
2133
2652
  ScrollView
2134
2653
  } from "react-native";
2135
2654
  import Svg2, { Path as Path2, Line as Line2 } from "react-native-svg";
2136
- import { WebView as WebView3 } from "react-native-webview";
2137
- import { ada as ada11 } from "@ateam-ai-mobile/core";
2138
- import { AdaOrb as AdaOrb3, SurfaceFullscreen as SurfaceFullscreen2 } from "@ateam-ai-mobile/plugin-runtime";
2655
+ import { ada as ada16 } from "@ateam-ai-mobile/core";
2656
+ import { AdaOrb as AdaOrb7, SurfaceFullscreen as SurfaceFullscreen2 } from "@ateam-ai-mobile/plugin-runtime";
2139
2657
 
2140
2658
  // src/voice/useVoiceCall.ts
2141
- import { useState as useState9, useRef as useRef5, useCallback as useCallback6 } from "react";
2659
+ import { useCallback as useCallback6, useEffect as useEffect12, useReducer } from "react";
2660
+
2661
+ // src/voice/voiceSession.ts
2142
2662
  import { getVoiceApiUrl as getVoiceApiUrl2, getToken as getToken2, getTenant as getTenant5, getUser as getUser2, refreshToken } from "@ateam-ai-mobile/core";
2663
+ import {
2664
+ callDeviceTool,
2665
+ ensureDeviceToolsLoaded,
2666
+ listDeviceTools,
2667
+ PLATFORM_VOICE_CALLER
2668
+ } from "@ateam-ai-mobile/plugin-runtime";
2669
+ var CONNECT_TIMEOUT_MS = 3e4;
2143
2670
  var TOKEN_REFRESH_SKEW_MS = 9e4;
2144
2671
  function tokenNeedsRefresh(token) {
2145
2672
  if (!token) return true;
@@ -2167,7 +2694,6 @@ var INJECT_TRACK_HOOK = `
2167
2694
  }
2168
2695
  }
2169
2696
 
2170
- // 1) Hook getUserMedia \u2014 captures mic streams at request time.
2171
2697
  try {
2172
2698
  if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
2173
2699
  const origGUM = navigator.mediaDevices.getUserMedia.bind(navigator.mediaDevices);
@@ -2181,7 +2707,6 @@ var INJECT_TRACK_HOOK = `
2181
2707
  }
2182
2708
  } catch (e) {}
2183
2709
 
2184
- // 2) Hook RTCPeerConnection.addTrack \u2014 catches whatever the app sends.
2185
2710
  try {
2186
2711
  if (window.RTCPeerConnection) {
2187
2712
  const origPC = window.RTCPeerConnection;
@@ -2201,9 +2726,7 @@ var INJECT_TRACK_HOOK = `
2201
2726
  }
2202
2727
  if (origAddTransceiver) {
2203
2728
  origPC.prototype.addTransceiver = function(arg) {
2204
- if (arg && arg.kind === 'audio') {
2205
- // Transceiver created with kind 'audio' \u2014 sender track set later.
2206
- } else if (arg && arg.kind === undefined && arg.track && arg.track.kind === 'audio') {
2729
+ if (arg && arg.kind === undefined && arg.track && arg.track.kind === 'audio') {
2207
2730
  track(arg.track);
2208
2731
  }
2209
2732
  const tx = origAddTransceiver.apply(this, arguments);
@@ -2216,7 +2739,6 @@ var INJECT_TRACK_HOOK = `
2216
2739
  }
2217
2740
  } catch (e) {}
2218
2741
 
2219
- // 3) Helper exposed for direct calls.
2220
2742
  window._adaSetMuted = function(muted) {
2221
2743
  let n = 0;
2222
2744
  if (Array.isArray(window._allAudioTracks)) {
@@ -2235,7 +2757,6 @@ var buildMuteScript = (muted) => `
2235
2757
  if (typeof window._adaSetMuted === 'function') {
2236
2758
  n = window._adaSetMuted(muted);
2237
2759
  }
2238
- // Last-resort scan if hook somehow missed.
2239
2760
  if (n === 0 && Array.isArray(window._allAudioTracks)) {
2240
2761
  window._allAudioTracks.forEach(function(t) {
2241
2762
  try { t.enabled = !muted; n++; } catch (e) {}
@@ -2244,104 +2765,413 @@ var buildMuteScript = (muted) => `
2244
2765
  console.log('[ada-mute]', muted ? 'MUTED' : 'UNMUTED', 'tracks=', n);
2245
2766
  })(${muted}); true;
2246
2767
  `;
2247
- function useVoiceCall({ skillSlug } = {}) {
2248
- const [callState, setCallState] = useState9("idle");
2249
- const [sessionId, setSessionId] = useState9(null);
2250
- const [error, setError] = useState9(null);
2251
- const [transcript, setTranscript] = useState9([]);
2252
- const [aiPartial, setAiPartial] = useState9("");
2253
- const [userPartial, setUserPartial] = useState9("");
2254
- const [muted, setMuted] = useState9(false);
2255
- const webViewRef = useRef5(null);
2256
- const handleWebViewMessage = useCallback6((event) => {
2768
+ var buildAgentTurnScript = (input) => `
2769
+ (function(input) {
2770
+ if (typeof window.__adasAgentTurn === 'function') {
2771
+ try { window.__adasAgentTurn(input); }
2772
+ catch (e) {
2773
+ window.ReactNativeWebView && window.ReactNativeWebView.postMessage(JSON.stringify({
2774
+ type: 'agentTurnFailed', message: String(e && e.message || e),
2775
+ }));
2776
+ }
2777
+ } else if (window.ReactNativeWebView) {
2778
+ window.ReactNativeWebView.postMessage(JSON.stringify({
2779
+ type: 'agentTurnUnsupported',
2780
+ message: 'voice app does not support agent turns (__adasAgentTurn missing) \u2014 update the voice page',
2781
+ }));
2782
+ }
2783
+ })(${JSON.stringify(input)}); true;
2784
+ `;
2785
+ var buildObserveScript = (text) => `
2786
+ (function(text) {
2787
+ if (typeof window.__adasObserve === 'function') {
2788
+ try { window.__adasObserve(text); }
2789
+ catch (e) {
2790
+ window.ReactNativeWebView && window.ReactNativeWebView.postMessage(JSON.stringify({
2791
+ type: 'observeFailed', message: String(e && e.message || e),
2792
+ }));
2793
+ }
2794
+ } else if (window.ReactNativeWebView) {
2795
+ window.ReactNativeWebView.postMessage(JSON.stringify({
2796
+ type: 'observeUnsupported',
2797
+ message: 'voice app does not support observe (__adasObserve missing) \u2014 update the voice page',
2798
+ }));
2799
+ }
2800
+ })(${JSON.stringify(text)}); true;
2801
+ `;
2802
+ function originOf(url) {
2803
+ if (!url) return null;
2804
+ const m = /^([a-z][a-z0-9+.-]*:\/\/[^/?#]+)/i.exec(url);
2805
+ return m ? m[1].toLowerCase() : null;
2806
+ }
2807
+ var IDLE = {
2808
+ callState: "idle",
2809
+ sessionId: null,
2810
+ error: null,
2811
+ transcript: [],
2812
+ aiPartial: "",
2813
+ userPartial: "",
2814
+ muted: false,
2815
+ userSpeaking: false,
2816
+ lastUserSpeechEndedAt: null,
2817
+ mounted: false,
2818
+ webViewSource: null,
2819
+ skillSlug: void 0
2820
+ };
2821
+ var VoiceSessionManager = class {
2822
+ constructor() {
2823
+ this.state = { ...IDLE };
2824
+ this.listeners = /* @__PURE__ */ new Set();
2825
+ this.webView = null;
2826
+ this.pending = [];
2827
+ /**
2828
+ * WHO owns the one physical session. Set to the caller that started it from
2829
+ * idle (a plugin's pluginId, or the PLATFORM symbol for the built-in sheet),
2830
+ * and kept after stop until the next start reassigns it — so a plugin can read
2831
+ * its own just-ended transcript but another plugin never can.
2832
+ *
2833
+ * ISOLATION (GPT blocker on 8881225): every access is scoped through this. A
2834
+ * different plugin must not read, control, or attach to another plugin's live
2835
+ * conversation just because it declared voice.
2836
+ */
2837
+ this.owner = null;
2838
+ this.injectedTrackHook = INJECT_TRACK_HOOK;
2839
+ }
2840
+ /** The platform (built-in sheet, mic indicator, session host) sees and
2841
+ * controls any session — it is the user's own surface. A plugin caller may
2842
+ * only touch a session it owns. */
2843
+ canAccess(caller) {
2844
+ return caller === PLATFORM_VOICE_CALLER || this.owner === caller;
2845
+ }
2846
+ foreignError() {
2847
+ const err = new Error(
2848
+ "voice: another solution is using the microphone \u2014 one live session per device."
2849
+ );
2850
+ err.code = "RESTRICTED";
2851
+ return err;
2852
+ }
2853
+ // ── Host wiring (used only by VoiceSessionHost) ──────────────
2854
+ /** The offscreen WebView sets its ref here so the session can inject into it. */
2855
+ setWebView(ref) {
2856
+ this.webView = ref;
2857
+ }
2858
+ /** Whole state — the host reads `mounted` + `webViewSource` to decide what to
2859
+ * render, and every reader derives its snapshot from this. */
2860
+ getInternal() {
2861
+ return this.state;
2862
+ }
2863
+ // ── VoiceAPI (what props.voice and useVoiceCall call) ────────
2864
+ async start(opts, caller = PLATFORM_VOICE_CALLER) {
2865
+ if (this.state.callState === "connected") {
2866
+ if (!this.canAccess(caller)) throw this.foreignError();
2867
+ return this.realSnapshot();
2868
+ }
2869
+ if (this.state.callState === "connecting") {
2870
+ if (!this.canAccess(caller)) throw this.foreignError();
2871
+ return this.waitForConnect();
2872
+ }
2257
2873
  try {
2258
- const data = JSON.parse(event.nativeEvent.data);
2259
- console.log("[VoiceWebView] message:", data.type, data.text?.slice?.(0, 50) || data.state || "");
2260
- switch (data.type) {
2261
- case "callState":
2262
- case "call-state":
2263
- setCallState(data.state);
2264
- if (data.sessionId) setSessionId(data.sessionId);
2265
- if (data.error) setError(data.error);
2266
- break;
2267
- case "transcript":
2268
- setTranscript((prev) => [...prev, { role: data.role, text: data.text, ts: Date.now() }]);
2269
- break;
2270
- case "aiPartial":
2271
- setAiPartial(data.text);
2272
- break;
2273
- case "userPartial":
2274
- setUserPartial(data.text);
2275
- break;
2276
- case "clearPartials":
2277
- setAiPartial("");
2278
- setUserPartial("");
2279
- break;
2280
- case "error":
2281
- setError(data.message);
2282
- setCallState("error");
2283
- break;
2284
- }
2285
- } catch {
2874
+ await ensureDeviceToolsLoaded();
2875
+ } catch (e) {
2876
+ console.warn("[voiceSession] device tool preload failed:", e?.message || String(e));
2286
2877
  }
2287
- }, []);
2288
- const startCall = useCallback6(async () => {
2289
- setError(null);
2290
- setTranscript([]);
2291
- setAiPartial("");
2292
- setUserPartial("");
2293
2878
  try {
2294
- if (tokenNeedsRefresh(getToken2())) {
2295
- await refreshToken();
2296
- }
2879
+ if (tokenNeedsRefresh(getToken2())) await refreshToken();
2297
2880
  } catch {
2298
2881
  }
2299
- setCallState("connecting");
2300
- }, []);
2301
- const stopCall = useCallback6(() => {
2302
- webViewRef.current?.injectJavaScript?.("window.hangUp && window.hangUp(); true;");
2303
- setCallState("idle");
2304
- setAiPartial("");
2305
- setUserPartial("");
2306
- setMuted(false);
2307
- }, []);
2308
- const toggleMute = useCallback6(() => {
2309
- setMuted((prev) => {
2310
- const next = !prev;
2311
- webViewRef.current?.injectJavaScript?.(buildMuteScript(next));
2312
- return next;
2882
+ this.owner = caller;
2883
+ const skillSlug = opts?.skillSlug;
2884
+ this.setState({
2885
+ callState: "connecting",
2886
+ error: null,
2887
+ transcript: [],
2888
+ aiPartial: "",
2889
+ userPartial: "",
2890
+ muted: false,
2891
+ mounted: true,
2892
+ skillSlug,
2893
+ webViewSource: { uri: this.buildUri(skillSlug) }
2313
2894
  });
2314
- }, []);
2315
- const getWebViewSource = useCallback6(() => {
2895
+ return this.waitForConnect();
2896
+ }
2897
+ stop(caller = PLATFORM_VOICE_CALLER) {
2898
+ if (this.state.callState !== "idle" && !this.canAccess(caller)) throw this.foreignError();
2899
+ this.webView?.injectJavaScript?.("window.hangUp && window.hangUp(); true;");
2900
+ this.rejectPending(new Error("voice: session stopped before it connected"));
2901
+ this.setState({
2902
+ ...IDLE,
2903
+ // keep the transcript of the call that just ended for any reader still up
2904
+ transcript: this.state.transcript
2905
+ });
2906
+ }
2907
+ setMuted(muted, caller = PLATFORM_VOICE_CALLER) {
2908
+ if (this.state.callState !== "idle" && !this.canAccess(caller)) throw this.foreignError();
2909
+ this.webView?.injectJavaScript?.(buildMuteScript(muted));
2910
+ this.setState({ muted });
2911
+ }
2912
+ prompt(input, caller = PLATFORM_VOICE_CALLER) {
2913
+ if (this.state.callState !== "connected") {
2914
+ throw new Error("voice: session is not connected \u2014 call start() and await it before prompt().");
2915
+ }
2916
+ if (!this.canAccess(caller)) throw this.foreignError();
2917
+ this.webView?.injectJavaScript?.(buildAgentTurnScript(input));
2918
+ }
2919
+ /**
2920
+ * Stream a silent observation into the live session — tell the model without
2921
+ * asking it to speak. The cheap, high-frequency channel (safe once or twice a
2922
+ * second) behind a future props.voice.observe(); today the host uses it to run
2923
+ * the real-device stream proof (VOICE_RT_STREAM_DEVICE_PROOF) before the gated
2924
+ * plugin API is added. Owner-scoped like every other control.
2925
+ */
2926
+ observe(text, caller = PLATFORM_VOICE_CALLER) {
2927
+ if (this.state.callState !== "connected") {
2928
+ throw new Error("voice: session is not connected \u2014 observe() needs a live session.");
2929
+ }
2930
+ if (!this.canAccess(caller)) throw this.foreignError();
2931
+ this.webView?.injectJavaScript?.(buildObserveScript(text));
2932
+ }
2933
+ getSnapshot(caller = PLATFORM_VOICE_CALLER) {
2934
+ return this.canAccess(caller) ? this.realSnapshot() : this.foreignSnapshot();
2935
+ }
2936
+ realSnapshot() {
2937
+ const s = this.state;
2938
+ return {
2939
+ state: s.callState,
2940
+ sessionId: s.sessionId,
2941
+ muted: s.muted,
2942
+ speaking: !!s.aiPartial,
2943
+ listening: !!s.userPartial,
2944
+ userSpeaking: s.userSpeaking,
2945
+ lastUserSpeechEndedAt: s.lastUserSpeechEndedAt,
2946
+ lastError: s.error
2947
+ };
2948
+ }
2949
+ foreignSnapshot() {
2950
+ return {
2951
+ state: "idle",
2952
+ sessionId: null,
2953
+ muted: false,
2954
+ speaking: false,
2955
+ listening: false,
2956
+ userSpeaking: false,
2957
+ lastUserSpeechEndedAt: null,
2958
+ lastError: null
2959
+ };
2960
+ }
2961
+ subscribe(cb, caller = PLATFORM_VOICE_CALLER) {
2962
+ const wrapped = (event) => {
2963
+ if (this.canAccess(caller)) cb(event);
2964
+ };
2965
+ this.listeners.add(wrapped);
2966
+ return { unsubscribe: () => {
2967
+ this.listeners.delete(wrapped);
2968
+ } };
2969
+ }
2970
+ transcript(caller = PLATFORM_VOICE_CALLER) {
2971
+ return this.canAccess(caller) ? this.state.transcript.slice() : [];
2972
+ }
2973
+ // ── WebView plumbing ─────────────────────────────────────────
2974
+ buildUri(skillSlug) {
2316
2975
  const token = getToken2();
2317
2976
  const tenant = getTenant5();
2318
2977
  const actorId = getUser2()?.actorId || "";
2319
2978
  const skillParam = skillSlug ? `&skillSlug=${encodeURIComponent(skillSlug)}` : "";
2320
- return {
2321
- uri: `${getVoiceApiUrl2()}/app?mode=mini&embedded=rn&authToken=${encodeURIComponent(token || "")}&tenant=${encodeURIComponent(tenant)}&actorId=${encodeURIComponent(actorId)}${skillParam}`
2322
- };
2323
- }, [skillSlug]);
2979
+ return `${getVoiceApiUrl2()}/app?mode=mini&embedded=rn&authToken=${encodeURIComponent(token || "")}&tenant=${encodeURIComponent(tenant)}&actorId=${encodeURIComponent(actorId)}${skillParam}`;
2980
+ }
2981
+ /** Handle a postMessage from the WebView. `event` is the RN WebView event. */
2982
+ handleMessage(event) {
2983
+ let data;
2984
+ try {
2985
+ data = JSON.parse(event.nativeEvent.data);
2986
+ } catch {
2987
+ return;
2988
+ }
2989
+ switch (data.type) {
2990
+ case "callState":
2991
+ case "call-state":
2992
+ if (data.state) this.setState({ callState: data.state });
2993
+ if (data.sessionId) this.setState({ sessionId: data.sessionId });
2994
+ if (data.error) this.setState({ error: data.error });
2995
+ if (data.state === "connected") this.resolvePending();
2996
+ if (data.state === "error") this.rejectPending(new Error(data.error || "voice: connection error"));
2997
+ break;
2998
+ case "transcript":
2999
+ this.appendTranscript({ role: data.role, text: data.text, ts: Date.now() });
3000
+ break;
3001
+ case "aiPartial":
3002
+ this.setState({ aiPartial: data.text });
3003
+ break;
3004
+ case "userPartial":
3005
+ this.setState({ userPartial: data.text });
3006
+ break;
3007
+ case "clearPartials":
3008
+ this.setState({ aiPartial: "", userPartial: "" });
3009
+ break;
3010
+ // User VAD from the mini-app. `at` is when it happened; on speaking:false
3011
+ // we stamp lastUserSpeechEndedAt so a solution can gate "speak after N s of
3012
+ // silence" on the first real stop, never before the user has spoken.
3013
+ case "speechActivity": {
3014
+ const speaking = !!data.speaking;
3015
+ const at = typeof data.at === "number" ? data.at : Date.now();
3016
+ this.setState(
3017
+ speaking ? { userSpeaking: true } : { userSpeaking: false, lastUserSpeechEndedAt: at }
3018
+ );
3019
+ break;
3020
+ }
3021
+ case "error":
3022
+ this.setState({ error: data.message, callState: "error" });
3023
+ this.rejectPending(new Error(data.message || "voice: error"));
3024
+ break;
3025
+ // A proactive turn couldn't run (older voice page / hook threw). NON-FATAL:
3026
+ // the session stays live; we only surface why the guide didn't speak.
3027
+ case "agentTurnUnsupported":
3028
+ case "agentTurnFailed":
3029
+ case "observeUnsupported":
3030
+ case "observeFailed":
3031
+ this.setState({ error: data.message || "voice: channel notice" });
3032
+ break;
3033
+ // Device tools — executed locally, origin-checked. The model's function
3034
+ // call already arrived on this phone; only the voice app's own origin may
3035
+ // drive the camera/GPS.
3036
+ case "device-tool-list": {
3037
+ const listSender = originOf(event?.nativeEvent?.url);
3038
+ const listExpected = originOf(getVoiceApiUrl2());
3039
+ if (!listSender || !listExpected || listSender !== listExpected) break;
3040
+ this.sendDeviceToolList(data.correlationId);
3041
+ break;
3042
+ }
3043
+ case "device-tool-call": {
3044
+ const sender = originOf(event?.nativeEvent?.url);
3045
+ const expected = originOf(getVoiceApiUrl2());
3046
+ if (!sender || !expected || sender !== expected) {
3047
+ console.warn(`[voiceSession] REFUSED device tool call from "${sender || "unknown origin"}" (expected "${expected}")`);
3048
+ break;
3049
+ }
3050
+ void this.runDeviceTool(data);
3051
+ break;
3052
+ }
3053
+ }
3054
+ }
3055
+ /** Answer "what device tools do you have?" — the client is the authority. */
3056
+ sendDeviceToolList(correlationId) {
3057
+ const tools = listDeviceTools().map((t) => ({
3058
+ name: t.name,
3059
+ description: t.description,
3060
+ input_schema: t.input_schema
3061
+ }));
3062
+ this.webView?.injectJavaScript?.(
3063
+ `window.__adasDeviceToolResult && window.__adasDeviceToolResult(${JSON.stringify(
3064
+ JSON.stringify({ correlationId, ok: true, tools })
3065
+ )}); true;`
3066
+ );
3067
+ }
3068
+ /** Run a device tool and post the outcome back into the WebView. */
3069
+ async runDeviceTool(data) {
3070
+ const { correlationId, name, args, timeoutMs } = data || {};
3071
+ if (!correlationId || typeof name !== "string") return;
3072
+ let payload;
3073
+ try {
3074
+ const outcome = await callDeviceTool(name, args || {}, { timeoutMs });
3075
+ payload = outcome.ok ? { correlationId, ok: true, result: outcome.result } : { correlationId, ok: false, error: outcome.error, reason: outcome.reason };
3076
+ } catch (err) {
3077
+ payload = { correlationId, ok: false, reason: "failed", error: err?.message || String(err) };
3078
+ }
3079
+ this.webView?.injectJavaScript?.(
3080
+ `window.__adasDeviceToolResult && window.__adasDeviceToolResult(${JSON.stringify(
3081
+ JSON.stringify(payload)
3082
+ )}); true;`
3083
+ );
3084
+ }
3085
+ // ── internals ────────────────────────────────────────────────
3086
+ setState(partial) {
3087
+ this.state = { ...this.state, ...partial };
3088
+ this.emit({ type: "state" });
3089
+ }
3090
+ appendTranscript(entry) {
3091
+ this.state = { ...this.state, transcript: [...this.state.transcript, entry] };
3092
+ this.emit({ type: "transcript" });
3093
+ }
3094
+ emit(event) {
3095
+ for (const cb of this.listeners) {
3096
+ try {
3097
+ cb(event);
3098
+ } catch (e) {
3099
+ console.warn("[voiceSession] listener threw", e);
3100
+ }
3101
+ }
3102
+ }
3103
+ waitForConnect() {
3104
+ return new Promise((resolve, reject) => {
3105
+ const timer = setTimeout(() => {
3106
+ this.pending = this.pending.filter((p) => p.timer !== timer);
3107
+ reject(new Error("voice: timed out waiting to connect (is VoiceSessionHost mounted?)"));
3108
+ }, CONNECT_TIMEOUT_MS);
3109
+ this.pending.push({ resolve, reject, timer });
3110
+ });
3111
+ }
3112
+ resolvePending() {
3113
+ const snap = this.realSnapshot();
3114
+ const waiting = this.pending;
3115
+ this.pending = [];
3116
+ for (const p of waiting) {
3117
+ clearTimeout(p.timer);
3118
+ p.resolve(snap);
3119
+ }
3120
+ }
3121
+ rejectPending(err) {
3122
+ const waiting = this.pending;
3123
+ this.pending = [];
3124
+ for (const p of waiting) {
3125
+ clearTimeout(p.timer);
3126
+ p.reject(err);
3127
+ }
3128
+ }
3129
+ /** TEST ONLY: reset the singleton to a clean idle state (mirrors the
3130
+ * __resetSessionSyncForTest precedent). Not part of any surface. */
3131
+ __resetForTest() {
3132
+ this.rejectPending(new Error("voice: reset"));
3133
+ this.state = { ...IDLE };
3134
+ this.owner = null;
3135
+ this.listeners.clear();
3136
+ this.webView = null;
3137
+ }
3138
+ };
3139
+ var voiceSession = new VoiceSessionManager();
3140
+
3141
+ // src/voice/useVoiceCall.ts
3142
+ function useVoiceCall({ skillSlug } = {}) {
3143
+ const [, force] = useReducer((x) => x + 1, 0);
3144
+ useEffect12(() => {
3145
+ const sub = voiceSession.subscribe(() => force());
3146
+ return () => sub.unsubscribe();
3147
+ }, []);
3148
+ const s = voiceSession.getInternal();
3149
+ const startCall = useCallback6(
3150
+ (opts) => voiceSession.start(opts ?? { skillSlug }),
3151
+ [skillSlug]
3152
+ );
3153
+ const stopCall = useCallback6(() => voiceSession.stop(), []);
3154
+ const toggleMute = useCallback6(
3155
+ () => voiceSession.setMuted(!voiceSession.getSnapshot().muted),
3156
+ []
3157
+ );
2324
3158
  return {
2325
- callState,
2326
- sessionId,
2327
- error,
2328
- transcript,
2329
- aiPartial,
2330
- userPartial,
2331
- muted,
3159
+ callState: s.callState,
3160
+ sessionId: s.sessionId,
3161
+ error: s.error,
3162
+ transcript: s.transcript,
3163
+ aiPartial: s.aiPartial,
3164
+ userPartial: s.userPartial,
3165
+ muted: s.muted,
2332
3166
  startCall,
2333
3167
  stopCall,
2334
- toggleMute,
2335
- handleWebViewMessage,
2336
- getWebViewSource,
2337
- webViewRef,
2338
- injectedTrackHook: INJECT_TRACK_HOOK
3168
+ toggleMute
2339
3169
  };
2340
3170
  }
2341
3171
 
2342
3172
  // src/voice/VoiceCallSheet.tsx
2343
- import { jsx as jsx16, jsxs as jsxs12 } from "react/jsx-runtime";
2344
- var fontFam7 = Platform10.OS === "ios" ? "SF Pro Rounded" : void 0;
3173
+ import { jsx as jsx21, jsxs as jsxs17 } from "react/jsx-runtime";
3174
+ var fontFam11 = Platform14.OS === "ios" ? "SF Pro Rounded" : void 0;
2345
3175
  var HIGHLIGHT_PATTERNS = [
2346
3176
  /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g,
2347
3177
  /(?:\+?\d[\s\-().]?){7,}\d/g,
@@ -2384,8 +3214,8 @@ function HighlightedText({
2384
3214
  baseStyle,
2385
3215
  importantStyle
2386
3216
  }) {
2387
- const segments = useMemo5(() => highlightSegments(text), [text]);
2388
- return /* @__PURE__ */ jsx16(Text11, { style: baseStyle, children: segments.map((seg, i) => /* @__PURE__ */ jsx16(Text11, { style: seg.important ? importantStyle : void 0, children: seg.text }, i)) });
3217
+ const segments = useMemo7(() => highlightSegments(text), [text]);
3218
+ return /* @__PURE__ */ jsx21(Text16, { style: baseStyle, children: segments.map((seg, i) => /* @__PURE__ */ jsx21(Text16, { style: seg.important ? importantStyle : void 0, children: seg.text }, i)) });
2389
3219
  }
2390
3220
  function VoiceCallSheet({ visible, onClose, skillSlug }) {
2391
3221
  const {
@@ -2397,31 +3227,27 @@ function VoiceCallSheet({ visible, onClose, skillSlug }) {
2397
3227
  muted,
2398
3228
  startCall,
2399
3229
  stopCall,
2400
- toggleMute,
2401
- handleWebViewMessage,
2402
- getWebViewSource,
2403
- webViewRef,
2404
- injectedTrackHook
3230
+ toggleMute
2405
3231
  } = useVoiceCall({ skillSlug });
2406
3232
  const scrollRef = useRef6(null);
2407
- useEffect8(() => {
3233
+ useEffect13(() => {
2408
3234
  if (visible && callState === "idle") startCall();
2409
3235
  }, [visible]);
2410
- useEffect8(() => {
3236
+ useEffect13(() => {
2411
3237
  scrollRef.current?.scrollToEnd?.({ animated: true });
2412
3238
  }, [transcript.length, aiPartial, userPartial]);
2413
3239
  const handleHangUp = () => {
2414
3240
  stopCall();
2415
3241
  onClose();
2416
3242
  };
2417
- const orbState = useMemo5(() => {
3243
+ const orbState = useMemo7(() => {
2418
3244
  if (callState === "connecting") return "thinking";
2419
3245
  if (callState === "error") return "idle";
2420
3246
  if (aiPartial) return "speaking";
2421
3247
  if (userPartial) return "listening";
2422
3248
  return "idle";
2423
3249
  }, [callState, aiPartial, userPartial]);
2424
- const statusLabel = useMemo5(() => {
3250
+ const statusLabel = useMemo7(() => {
2425
3251
  if (callState === "connecting") return "Connecting\u2026";
2426
3252
  if (callState === "error") return error || "Couldn\u2019t connect";
2427
3253
  if (aiPartial) return "Ada is speaking";
@@ -2431,92 +3257,76 @@ function VoiceCallSheet({ visible, onClose, skillSlug }) {
2431
3257
  }, [callState, aiPartial, userPartial, error]);
2432
3258
  if (!visible) return null;
2433
3259
  const visibleEntries = transcript.filter((e) => e.role === "user" || e.role === "ai");
2434
- return /* @__PURE__ */ jsxs12(
3260
+ return /* @__PURE__ */ jsxs17(
2435
3261
  SurfaceFullscreen2,
2436
3262
  {
2437
3263
  visible,
2438
3264
  onClose: handleHangUp,
2439
3265
  status: statusLabel,
2440
- hero: /* @__PURE__ */ jsx16(AdaOrb3, { size: 180, state: orbState }),
3266
+ hero: /* @__PURE__ */ jsx21(AdaOrb7, { size: 180, state: orbState }),
2441
3267
  closeIcon: "hangup",
2442
3268
  closeLabel: "End",
2443
3269
  children: [
2444
- /* @__PURE__ */ jsxs12(
3270
+ /* @__PURE__ */ jsxs17(
2445
3271
  ScrollView,
2446
3272
  {
2447
3273
  ref: scrollRef,
2448
- style: styles14.transcript,
2449
- contentContainerStyle: styles14.transcriptContent,
3274
+ style: styles19.transcript,
3275
+ contentContainerStyle: styles19.transcriptContent,
2450
3276
  showsVerticalScrollIndicator: false,
2451
3277
  children: [
2452
- callState === "error" && error ? /* @__PURE__ */ jsx16(View15, { style: styles14.errorBox, children: /* @__PURE__ */ jsx16(Text11, { style: styles14.errorText, children: error }) }) : null,
2453
- visibleEntries.map((entry, i) => /* @__PURE__ */ jsxs12(View15, { style: styles14.entry, children: [
2454
- /* @__PURE__ */ jsx16(Text11, { style: styles14.role, children: entry.role === "user" ? "YOU" : "ADA" }),
2455
- /* @__PURE__ */ jsx16(
3278
+ callState === "error" && error ? /* @__PURE__ */ jsx21(View20, { style: styles19.errorBox, children: /* @__PURE__ */ jsx21(Text16, { style: styles19.errorText, children: error }) }) : null,
3279
+ visibleEntries.map((entry, i) => /* @__PURE__ */ jsxs17(View20, { style: styles19.entry, children: [
3280
+ /* @__PURE__ */ jsx21(Text16, { style: styles19.role, children: entry.role === "user" ? "YOU" : "ADA" }),
3281
+ /* @__PURE__ */ jsx21(
2456
3282
  HighlightedText,
2457
3283
  {
2458
3284
  text: entry.text,
2459
- baseStyle: styles14.entryText,
2460
- importantStyle: styles14.important
3285
+ baseStyle: styles19.entryText,
3286
+ importantStyle: styles19.important
2461
3287
  }
2462
3288
  )
2463
3289
  ] }, `${entry.ts}-${i}`)),
2464
- userPartial || aiPartial ? /* @__PURE__ */ jsxs12(View15, { style: styles14.entry, children: [
2465
- /* @__PURE__ */ jsx16(Text11, { style: styles14.role, children: userPartial ? "YOU" : "ADA" }),
2466
- /* @__PURE__ */ jsx16(
3290
+ userPartial || aiPartial ? /* @__PURE__ */ jsxs17(View20, { style: styles19.entry, children: [
3291
+ /* @__PURE__ */ jsx21(Text16, { style: styles19.role, children: userPartial ? "YOU" : "ADA" }),
3292
+ /* @__PURE__ */ jsx21(
2467
3293
  HighlightedText,
2468
3294
  {
2469
3295
  text: userPartial || aiPartial,
2470
- baseStyle: styles14.entryTextLive,
2471
- importantStyle: styles14.important
3296
+ baseStyle: styles19.entryTextLive,
3297
+ importantStyle: styles19.important
2472
3298
  }
2473
3299
  )
2474
3300
  ] }) : null
2475
3301
  ]
2476
3302
  }
2477
3303
  ),
2478
- callState === "connected" || callState === "connecting" ? /* @__PURE__ */ jsxs12(View15, { style: styles14.muteWrap, pointerEvents: "box-none", children: [
2479
- /* @__PURE__ */ jsx16(
2480
- Pressable9,
3304
+ callState === "connected" || callState === "connecting" ? /* @__PURE__ */ jsxs17(View20, { style: styles19.muteWrap, pointerEvents: "box-none", children: [
3305
+ /* @__PURE__ */ jsx21(
3306
+ Pressable13,
2481
3307
  {
2482
3308
  onPress: toggleMute,
2483
3309
  accessibilityRole: "button",
2484
3310
  accessibilityLabel: muted ? "Unmute microphone" : "Mute microphone",
2485
3311
  accessibilityState: { selected: muted },
2486
- style: [styles14.muteBtn, muted && styles14.muteBtnActive],
3312
+ style: [styles19.muteBtn, muted && styles19.muteBtnActive],
2487
3313
  hitSlop: 12,
2488
- children: /* @__PURE__ */ jsxs12(Svg2, { width: 22, height: 22, viewBox: "0 0 24 24", fill: "none", stroke: muted ? ada11.paper : ada11.ink, strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", children: [
2489
- /* @__PURE__ */ jsx16(Path2, { d: "M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z" }),
2490
- /* @__PURE__ */ jsx16(Path2, { d: "M19 10v2a7 7 0 0 1-14 0v-2" }),
2491
- /* @__PURE__ */ jsx16(Line2, { x1: "12", y1: "19", x2: "12", y2: "23" }),
2492
- /* @__PURE__ */ jsx16(Line2, { x1: "8", y1: "23", x2: "16", y2: "23" }),
2493
- muted ? /* @__PURE__ */ jsx16(Line2, { x1: "3", y1: "3", x2: "21", y2: "21", stroke: ada11.paper, strokeWidth: 2.4 }) : null
3314
+ children: /* @__PURE__ */ jsxs17(Svg2, { width: 22, height: 22, viewBox: "0 0 24 24", fill: "none", stroke: muted ? ada16.paper : ada16.ink, strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", children: [
3315
+ /* @__PURE__ */ jsx21(Path2, { d: "M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z" }),
3316
+ /* @__PURE__ */ jsx21(Path2, { d: "M19 10v2a7 7 0 0 1-14 0v-2" }),
3317
+ /* @__PURE__ */ jsx21(Line2, { x1: "12", y1: "19", x2: "12", y2: "23" }),
3318
+ /* @__PURE__ */ jsx21(Line2, { x1: "8", y1: "23", x2: "16", y2: "23" }),
3319
+ muted ? /* @__PURE__ */ jsx21(Line2, { x1: "3", y1: "3", x2: "21", y2: "21", stroke: ada16.paper, strokeWidth: 2.4 }) : null
2494
3320
  ] })
2495
3321
  }
2496
3322
  ),
2497
- /* @__PURE__ */ jsx16(Text11, { style: styles14.muteLabel, children: muted ? "Muted" : "Mute" })
2498
- ] }) : null,
2499
- callState !== "idle" && /* @__PURE__ */ jsx16(View15, { style: styles14.hiddenWebView, pointerEvents: "none", children: /* @__PURE__ */ jsx16(
2500
- WebView3,
2501
- {
2502
- ref: webViewRef,
2503
- source: getWebViewSource(),
2504
- style: { flex: 1, backgroundColor: "transparent" },
2505
- onMessage: handleWebViewMessage,
2506
- onError: (e) => console.log("[VoiceWebView] error:", e.nativeEvent?.description),
2507
- injectedJavaScriptBeforeContentLoaded: injectedTrackHook,
2508
- mediaPlaybackRequiresUserAction: false,
2509
- allowsInlineMediaPlayback: true,
2510
- mediaCapturePermissionGrantType: "grant",
2511
- javaScriptEnabled: true,
2512
- originWhitelist: ["*"]
2513
- }
2514
- ) })
3323
+ /* @__PURE__ */ jsx21(Text16, { style: styles19.muteLabel, children: muted ? "Muted" : "Mute" })
3324
+ ] }) : null
2515
3325
  ]
2516
3326
  }
2517
3327
  );
2518
3328
  }
2519
- var styles14 = StyleSheet14.create({
3329
+ var styles19 = StyleSheet19.create({
2520
3330
  transcript: {
2521
3331
  flex: 1,
2522
3332
  paddingHorizontal: 28
@@ -2533,9 +3343,9 @@ var styles14 = StyleSheet14.create({
2533
3343
  },
2534
3344
  role: {
2535
3345
  fontSize: 10,
2536
- fontFamily: fontFam7,
3346
+ fontFamily: fontFam11,
2537
3347
  fontWeight: "700",
2538
- color: ada11.inkMuted,
3348
+ color: ada16.inkMuted,
2539
3349
  letterSpacing: 1.4,
2540
3350
  minWidth: 32,
2541
3351
  paddingTop: 3,
@@ -2544,9 +3354,9 @@ var styles14 = StyleSheet14.create({
2544
3354
  entryText: {
2545
3355
  flex: 1,
2546
3356
  fontSize: 14,
2547
- fontFamily: fontFam7,
3357
+ fontFamily: fontFam11,
2548
3358
  fontWeight: "400",
2549
- color: ada11.inkSoft,
3359
+ color: ada16.inkSoft,
2550
3360
  lineHeight: 20,
2551
3361
  letterSpacing: -0.1,
2552
3362
  opacity: 0.42
@@ -2554,18 +3364,18 @@ var styles14 = StyleSheet14.create({
2554
3364
  entryTextLive: {
2555
3365
  flex: 1,
2556
3366
  fontSize: 14,
2557
- fontFamily: fontFam7,
3367
+ fontFamily: fontFam11,
2558
3368
  fontWeight: "400",
2559
- color: ada11.inkSoft,
3369
+ color: ada16.inkSoft,
2560
3370
  lineHeight: 20,
2561
3371
  letterSpacing: -0.1,
2562
3372
  opacity: 0.7
2563
3373
  },
2564
3374
  important: {
2565
3375
  fontSize: 17,
2566
- fontFamily: fontFam7,
3376
+ fontFamily: fontFam11,
2567
3377
  fontWeight: "700",
2568
- color: ada11.ink,
3378
+ color: ada16.ink,
2569
3379
  letterSpacing: -0.1,
2570
3380
  opacity: 1
2571
3381
  },
@@ -2580,14 +3390,14 @@ var styles14 = StyleSheet14.create({
2580
3390
  },
2581
3391
  errorText: {
2582
3392
  fontSize: 13,
2583
- fontFamily: fontFam7,
3393
+ fontFamily: fontFam11,
2584
3394
  fontWeight: "500",
2585
3395
  color: "#fca5a5",
2586
3396
  lineHeight: 18
2587
3397
  },
2588
3398
  muteWrap: {
2589
3399
  position: "absolute",
2590
- bottom: Platform10.OS === "ios" ? 56 : 40,
3400
+ bottom: Platform14.OS === "ios" ? 56 : 40,
2591
3401
  left: 28,
2592
3402
  alignItems: "center",
2593
3403
  gap: 6
@@ -2596,9 +3406,9 @@ var styles14 = StyleSheet14.create({
2596
3406
  width: 56,
2597
3407
  height: 56,
2598
3408
  borderRadius: 28,
2599
- backgroundColor: ada11.paperSoft,
3409
+ backgroundColor: ada16.paperSoft,
2600
3410
  borderWidth: 1,
2601
- borderColor: ada11.border,
3411
+ borderColor: ada16.border,
2602
3412
  justifyContent: "center",
2603
3413
  alignItems: "center"
2604
3414
  },
@@ -2608,18 +3418,133 @@ var styles14 = StyleSheet14.create({
2608
3418
  },
2609
3419
  muteLabel: {
2610
3420
  fontSize: 12,
2611
- fontFamily: fontFam7,
3421
+ fontFamily: fontFam11,
2612
3422
  fontWeight: "500",
2613
- color: ada11.inkMuted,
3423
+ color: ada16.inkMuted,
2614
3424
  letterSpacing: 0.3
2615
- },
2616
- hiddenWebView: {
3425
+ }
3426
+ });
3427
+
3428
+ // src/voice/VoiceSessionHost.tsx
3429
+ import { useCallback as useCallback7, useEffect as useEffect14, useReducer as useReducer2, useRef as useRef7 } from "react";
3430
+ import { View as View21, StyleSheet as StyleSheet20 } from "react-native";
3431
+ import { WebView as WebView3 } from "react-native-webview";
3432
+ import { jsx as jsx22 } from "react/jsx-runtime";
3433
+ function VoiceSessionHost() {
3434
+ const [, force] = useReducer2((x) => x + 1, 0);
3435
+ const lastSig = useRef7("");
3436
+ useEffect14(() => {
3437
+ const sub = voiceSession.subscribe(() => {
3438
+ const s2 = voiceSession.getInternal();
3439
+ const sig = `${s2.mounted}|${s2.webViewSource?.uri || ""}`;
3440
+ if (sig !== lastSig.current) {
3441
+ lastSig.current = sig;
3442
+ force();
3443
+ }
3444
+ });
3445
+ return () => sub.unsubscribe();
3446
+ }, []);
3447
+ const setRef = useCallback7((r) => {
3448
+ voiceSession.setWebView(r);
3449
+ }, []);
3450
+ const s = voiceSession.getInternal();
3451
+ if (!s.mounted || !s.webViewSource) return null;
3452
+ return /* @__PURE__ */ jsx22(View21, { style: styles20.hidden, pointerEvents: "none", children: /* @__PURE__ */ jsx22(
3453
+ WebView3,
3454
+ {
3455
+ ref: setRef,
3456
+ source: s.webViewSource,
3457
+ style: styles20.web,
3458
+ onMessage: (e) => voiceSession.handleMessage(e),
3459
+ onError: (e) => console.log("[VoiceSessionHost] error:", e.nativeEvent?.description),
3460
+ injectedJavaScriptBeforeContentLoaded: voiceSession.injectedTrackHook,
3461
+ mediaPlaybackRequiresUserAction: false,
3462
+ allowsInlineMediaPlayback: true,
3463
+ mediaCapturePermissionGrantType: "grant",
3464
+ javaScriptEnabled: true,
3465
+ originWhitelist: ["*"]
3466
+ }
3467
+ ) });
3468
+ }
3469
+ var styles20 = StyleSheet20.create({
3470
+ hidden: {
2617
3471
  position: "absolute",
2618
3472
  width: 1,
2619
3473
  height: 1,
2620
3474
  top: -100,
2621
3475
  left: -100,
2622
3476
  opacity: 0
3477
+ },
3478
+ web: { flex: 1, backgroundColor: "transparent" }
3479
+ });
3480
+
3481
+ // src/voice/VoiceMicIndicator.tsx
3482
+ import { useEffect as useEffect15, useReducer as useReducer3, useRef as useRef8 } from "react";
3483
+ import { View as View22, Text as Text17, Animated as Animated2, StyleSheet as StyleSheet21, Platform as Platform15 } from "react-native";
3484
+ import { ada as ada17 } from "@ateam-ai-mobile/core";
3485
+ import { jsx as jsx23, jsxs as jsxs18 } from "react/jsx-runtime";
3486
+ var fontFam12 = Platform15.OS === "ios" ? "SF Pro Rounded" : void 0;
3487
+ function VoiceMicIndicator({ topInset = 8 }) {
3488
+ const [, force] = useReducer3((x) => x + 1, 0);
3489
+ const pulse = useRef8(new Animated2.Value(0.4)).current;
3490
+ useEffect15(() => {
3491
+ const sub = voiceSession.subscribe(() => force());
3492
+ return () => sub.unsubscribe();
3493
+ }, []);
3494
+ const snap = voiceSession.getSnapshot();
3495
+ const live = snap.state === "connecting" || snap.state === "connected";
3496
+ useEffect15(() => {
3497
+ if (!live) return;
3498
+ const loop = Animated2.loop(
3499
+ Animated2.sequence([
3500
+ Animated2.timing(pulse, { toValue: 1, duration: 700, useNativeDriver: true }),
3501
+ Animated2.timing(pulse, { toValue: 0.4, duration: 700, useNativeDriver: true })
3502
+ ])
3503
+ );
3504
+ loop.start();
3505
+ return () => loop.stop();
3506
+ }, [live, pulse]);
3507
+ if (!live) return null;
3508
+ const label = snap.state === "connecting" ? "Connecting\u2026" : snap.muted ? "Mic muted" : "Ada is listening";
3509
+ const dotColor = snap.muted ? ada17.inkMuted : "#E5484D";
3510
+ return /* @__PURE__ */ jsx23(View22, { style: [styles21.wrap, { top: topInset }], pointerEvents: "none", children: /* @__PURE__ */ jsxs18(View22, { style: styles21.pill, children: [
3511
+ /* @__PURE__ */ jsx23(Animated2.View, { style: [styles21.dot, { backgroundColor: dotColor, opacity: snap.muted ? 1 : pulse }] }),
3512
+ /* @__PURE__ */ jsx23(Text17, { style: styles21.label, children: label })
3513
+ ] }) });
3514
+ }
3515
+ var styles21 = StyleSheet21.create({
3516
+ wrap: {
3517
+ position: "absolute",
3518
+ left: 0,
3519
+ right: 0,
3520
+ alignItems: "center",
3521
+ zIndex: 9999,
3522
+ elevation: 9999
3523
+ },
3524
+ pill: {
3525
+ flexDirection: "row",
3526
+ alignItems: "center",
3527
+ gap: 7,
3528
+ paddingHorizontal: 12,
3529
+ paddingVertical: 6,
3530
+ borderRadius: 999,
3531
+ backgroundColor: ada17.ink,
3532
+ shadowColor: "#000",
3533
+ shadowOpacity: 0.2,
3534
+ shadowRadius: 8,
3535
+ shadowOffset: { width: 0, height: 2 }
3536
+ },
3537
+ dot: {
3538
+ width: 8,
3539
+ height: 8,
3540
+ borderRadius: 4
3541
+ },
3542
+ label: {
3543
+ fontSize: 12,
3544
+ fontFamily: fontFam12,
3545
+ fontWeight: "600",
3546
+ color: ada17.paper,
3547
+ letterSpacing: 0.2
2623
3548
  }
2624
3549
  });
2625
3550
  export {
@@ -2627,9 +3552,13 @@ export {
2627
3552
  ChatInput,
2628
3553
  DaySeparator,
2629
3554
  EmbeddedPluginWebView,
3555
+ HandoffScreen,
2630
3556
  JobProgressBar,
3557
+ JoinScreen,
2631
3558
  Lightbox,
3559
+ LoginScreen,
2632
3560
  MessageBubble,
3561
+ OpenJoinScreen,
2633
3562
  Plugin,
2634
3563
  PluginSurface,
2635
3564
  TableCardBlock,
@@ -2637,16 +3566,30 @@ export {
2637
3566
  TypingIndicator,
2638
3567
  UIActionsRow,
2639
3568
  VoiceCallSheet,
3569
+ VoiceMicIndicator,
3570
+ VoiceSessionHost,
2640
3571
  WidgetHost,
2641
3572
  WidgetWebView,
3573
+ WorkspaceBanner,
2642
3574
  buildPluginBridgeJS,
3575
+ completeInviteJoin,
3576
+ completeOpenJoin,
3577
+ completeProviderSignIn,
3578
+ googleClientIds,
3579
+ handoffErrorMessage,
3580
+ inviteHeadline,
3581
+ openJoinErrorMessage,
2643
3582
  openWidget,
3583
+ runHandoffExchange,
2644
3584
  setClipboardProvider,
3585
+ setOAuthProvider,
2645
3586
  setPluginWebViewExtraHosts,
2646
3587
  setUIActionNavigator,
3588
+ shouldRecoverToDefaultTenant,
2647
3589
  useLightboxStore,
2648
3590
  usePluginBridge,
2649
3591
  useVoiceCall,
2650
- useWidgetHostStore
3592
+ useWidgetHostStore,
3593
+ voiceSession
2651
3594
  };
2652
3595
  //# sourceMappingURL=index.mjs.map